Gen 3: The Fine Art of Surfacing
In the first and second tutorials tutorials in our Gen series, we've taken a look at the gen~ object for processing audio data and the jit.pix object (and its cousin jit.gl.pix) for processing 2d, 4-plane image data. This time out, we're going to have a look at the jit.gen object - while it's just like the jit.pix and jit.gl.pix objects in that all three objects process jitter matrices, the jit.gen object is the generic Gen matrix processing object - it can handle matrices of any type, dimension, and planecount.
Before the arrival of the Jitter Gen objects, one of the best techniques for processing matrix data in interesting and complex procedural ways was to use the jit.expr object. Andrew Benson's tutorial on working with expressions using the jit.expr object did a great job of demystifying normalized numbers, and our last tutorial on image processing using jit.pix made use of those insights to help explain how Jitter gen objects work with matrix data.
Working with jit.gen uses those same basic skills, but we're going to look at how a little familiarity with the Jitter family of gen objects can make working with expressions that can seem bewildering in jit.expr a lot easier. We're going to generate a data surface - a mesh whose contours are described by a mathematical expression. This is a quick way to grasp just how much the arrival of the Jitter family of gen objects has made your life a more pleasant place.
To start, let's take a look at the jit.expr object in action, and review how we use it to represent matrix data (both in jit.expr and in the jit.gen world, as well. The patch contains all the logic we'll need to start - it provides the standard set of objects we use when generating and rendering matrix data in Jitter, along with a jit.expr object that we'll use to generate our data surfaces.
The patch contains a number of jit.gl.mesh attributes that you'll see in all of our example patches (after you've generated your data surface, you might want to take a few minutes to investigate how they alter the look of the rendered matrix if you're not already familiar with them). We'll create our mesh using a 3-plane matrix of 32-bit floating point data whose dimensions are 100 x 100. Each of the three planes represent a dimension of our 3d data, which we'll send to the jit.gl.mesh object:
- x = width
- y = depth
- z = height/displacement
We'll use a jit.expr object to create our 3d plane by using expr messages to fill the matrix with data values that describe our plane. As we go along, we'll also use variations of those messages to create more interesting variations on the plane.
Let's start with something simple - a plane whose coordinates all lie within the range -1.0 to 1.0. As you may know from Andrew Benson's tutorial (or the previous jit.pix gen tutorial), we can do this by specifying signed normalized (snorm) values to specify the coordinate range for our data surface. Sending the message expr snorm snorm 0. to the jit.expr object does just that -the number in the square bracket indicates which plane we're specifying - since the plane is completely flat, all of the values for the z plane can be set to zero by including a value of 0. rather than snorm.
While that works well, it's not very interesting. We can create variations in our data surface by replacing the zero with a mathematical expression that will describe the z-plane values for every single point on our data surface. The expression sin(snorm*PI) does just that. for every x value across the plane, the z offset is calculated by multiplying the x value by 3.14159 and then calculating the sine of that value (which will always remain in the range -1. to 1.) The result is a single cycle of a sine wave that ripples across the x dimension from left to right.
The expression sin(snorm*PI) has a similar result, but the sine wave propogates along the y axis (from top to bottom).
Suppose we wanted to have the sine function propagate along both axes at the same time. How would we go about doing that? To combine the sine waves together, all we have to do is to combine the two expressions we have, but add the results together and take their average. The expression expr snorm snorm (sin(snorm*PI)+sin(snorm*PI))/2 will do that.
While this data surface is certainly lovely, it's still a litle static. Don't worry - as I learned from Andrew Benson, the jit.expr object allows you to work with variables in our expressions. In the jit.gen_2 patch, we'll introduce variables and use them to do two things:
- We'll let you set the number of iterations of the sine function across the x and y axes.
- We'll add a scaling variable to change the amount of vertical offset for our plane.
You can specify input variables in a jit.expr expression by typing in[N] , where N is the number of the inlet associated with your variable. The number of inlets that the jit.expr object has will vary according to the number of variables we specify. They appear to the right of the jit.expr object's left (matrix) inlet, and and numbered starting with 1.
Although the message expr snorm snorm (sin(snorm*(in*PI))+sin(snorm*(in*PI)))/2*in may look a little daunting, you'll like what it does - the values for in and in are used as multipliers to PI, and allow us to set the number of sine functions that are arranged along the x and y axes of our data surface.
Similarly, in multiplies the result of adding and averaging the two sine functions, and lets us change how flat or wavy our data surface is. And, of course, the cool part is that we can do all this on the fly by changing the input values in real time (Note: Since we need to re-render the surface each time we change a variable, we add a trigger object for each of the input variables.
Welcome to the world of jit.gen
While the jit.gen_2 patch is pretty cool to play with, I have a confession to make - for me, the message syntax got really complicated pretty quickly, (especially if you don't have an iron understanding chokehold on your algebraic order of expressions!). After numerous tries at typing that mangle of brackets and rearranging parentheses, I started to grumble about wanting a better and faster way to do all this.
Don't worry - the jit.gen_3 patch uses our new friend the jit.gen object to do everything we've just done in a way that's a lot easier to understand.
Here's the inside of the jit.gen patch that duplicates what we just created using those jit.expr expressions:
Wow. That does look a little simpler, doesn't it? Let's take a look at the patch.
You'll probably find the contents of the patch similar to the patching we did in our previous jit.pix tutorial - using normalized numbers to describe the positions of every element in our matrix (we're using the snorm operator in this patch because we want values in the range of -1.0 to 1.0 rather than 0. to 1.0), and the swiz and vec operators to "unpack" and "pack" up the vector data. What's interesting about this particular jit.gen object is that we're working with only three planes of data - x , y , and z coordinates. The jit.gen adapts to the dimensions and plane count of its input matrix and lets us swizzle out data for each coordinate by using numbered indices (e.g., swiz 0 gives us the x coordinate, swiz 1 gives us the y coordinate, etc.).
Our jit.gen patch includes three parameters, defined using the param operator: the x_iterations and y_iterations parameters specify how many iterations of the sine function we'll use (these are what we used the in and in variables in the message to jit.expr to do in the previous patch), and the scaling parameter is used to modify the amount of "lumpiness" in the surface (which is what we used the in variable for in our previous patch).
NOTE: You may have noticed this convention previously in some of the gen example patches, but I thought I'd mention it here: When you add a named param operator to the inside of your gen patch, then you can use that name when you perform operations inside of the gen patch without having to connect the param object directly.
It's a handy thing to do, but some beginners find it confusing because it's not necessarily "Max-like." You may have encountered something similar when you first learned to work with the buffer~ object and the family of MSP objects that accessed the contents of a buffer by specifying the buffer name as an argument (such as info~ or groove~).
Since our jit.gen patch calculates the z plane vertical offsets of our data surface based on the x and y coordinates, the patch really doesn't take any input apart from the parameter messages - we use the swiz operator to provide us with x and y coordinate values across the range of our matrix (swiz 0 gives us the x coordinate, and swiz 1 provides the y coordinate). We use those values to calculate the sine function across the x and y dimensions, average the resulting values by adding and multiplying them by 0.5, and then multiply by the scaling factor. The vec operator packs up the x and y coordinate values along with the z value we've calculated, wraps them all into a three-element vector, and then outputs the result. It's quick, easy, and (at least for me) simple to see precisely what's going on. And that's why the jit.gen object is destined to become one of your best friends when working with matrices. While it's great to have Jitter gen objects like jit.pix and jit.gl.pix that are specially created to work with image data, the great thing about jit.gen is that it's the Swiss Army Knife gen object - you can use it for any kind of matrix you can imagine. In upcoming tutorials, we'll be using jit.gen to play in the fields of OpenGL.
A Codebox Interlude
While this tutorial has tended to be about moving away from writing the kind of expressions we'd use writing code, I'd be remiss at this point if I didn't take some time out and to show a little love to the gen codebox operator.
Here's an example of a jit.gen object that implements the previous example using the codebox operator.
Apart from a few renamed variables here and there, I think you'll recognize the part of the datasurface code that does the heavy lifting as bearing a strong resemblance to the expression we used in our jit.expr version.
What might be less apparent to you that the contents of our codebox operator defines its internal function (datasurface) before it does anything else such as specifying parameters, etc. That's a requirement for any codebox that contains a user-defined function.
This example of how to define a function, use GenExpr equivalents of operators such as swiz, and to interact with user-defined parameters is provided for the edification of those of you who, having seen the inside of the jit.gen object in the jit.gen_3 patch, felt the tug of nostalgia for the joy of writing code. Please enjoy this codebox example responsibly.
(Data) Surf's Up!
Now that we've seen how much easier it is to work with the jit.gen object for generating data surfaces, what can we do with the 'em? We'll end our tutorial with some sample patches to help you get started imagining some possible futures.
The jit.gen_throb patch provides what's probably the most immediate and rewarding use - using audio data to control the jit.gen object variables as a replacement for that all-too-predictable VJ effect we like to call The Throbbing Techno Donut.
Instead of using the amplitude of an audio file to make a giant torus change its size, this patch controls the number of sine function iterations in our plane and also to control the amount of z plane warping. Of course, you can do all kinds of things to make the patch more interesting (splitting the audio into frequency bands and using different bands to drive individual parameters), but isn't it great to have an alternative to that donut?
The pair of patches labeled jit.gen_skater and jit.gen_surfer involve processes a little closer to my own heart - using the data surfaces as the starting point for generating interesting kinds of control data. In a way, what these patches do is aking to what was at the heart of the Max for Live device I created in this tutorial, which generated a data surface and then created ways of taking a section of that data and using it to generate melodies. These patches are both a little more ambitious and a little more general, but they both make use the same data surface we created using the jit.gen object.
The jit.gen_skater patch adds a subpatcher called vector-skater to the contents of our the jit.gen_3 patch that lets us define a vector. The vector consists of
- An x/y start position (with values in the range -1.0 to 1.0).
- A vector whose direction and velocity is specified by positive or negative floating point values.
- A boundmode that describes how a vector will act when it encounters the data range boundaries. You can choose to have the vector wrap or fold at the boundary for different outputs.
The output of the vector-skater subpatch varies and interacts in interesting ways according to the complexity of the data surface and the parameters for the vector itself. Think of it as a really interesting mutant cousin of your LFO.
The jit.gen_surfer patch is a somewhat eccentric variant of the jit.gen_skater patch. I don't want to spoil the fun by describing it in too much detail, but let's just say that it does things a little differently. Instead of shooting vectors across the data surface in the same way, I've added x and y offsets and folding/wrapping boundmodes to the insides of jit.gen object in order to sample a moving data surface. It's not only fun to watch in action, but the output of the much simpler vector-fetch subpatcher produces some subtle and interesting variations.
That's going to do it for this tutorial. Happy patching!
by Gregory Taylor on
Mar 16, 2012 3:01 PM