One of the most feared and respected objects in the Jitter collection, jit.expr arrived on the scene as part of Jitter 1.5. In some circles, there is a belief that harnessing its power will bring you great powers and enable you to achieve untold wonders.

The fact is, jit.expr is a really amazing tool, but unfortunately many users are intimidated by the syntax, which includes a lot of square brackets, parentheses, and special variables. While these sorts of things are common for programmers, they can seem very esoteric and strange to many Max users. Many avid readers of the Jitter Recipes got stuck when jit.expr was used to generate the quads in the TinyVideo and Shatter recipes. Upon getting familiar with the expressions, however, it will become an extremely handy tool that makes seemingly difficult and complicated tasks much simpler. Through this article, I hope to demystify this elusive object and help you to achieve the wonders that you are dreaming of. We'll start by covering the basic elements of jit.expr expressions and then use these to create some basic geometry and image masks.

Download the patches used in this tutorial.

Getting Past the Empty Matrix

The jit.expr object doesn't do much out of the box. In order for it to output anything more than black pixels, you'll have to give it an expression to work with, using the 'expr' attribute. The expressions work in a similar fashion to jit.op where you are performing some arithmetic on each cell of the incoming matrix. If you'd like to just pass through the input from the first inlet of the object, you can type '@expr in[0]'. The in[0] variable stands for the pixel value from the first inlet. The second inlet would be in[1], and if you have more inlets, they are numbered sequentially (in[2],in[3],etc.). If you'd like to add two jit.matrix inputs together (similar to jit.op @op +), you can use '@expr in[0]+in[1]'. Still with me? This is where it starts getting fun.

In addition to accessing specific inputs, the 'in' variable also allows you to access a specific plane of the incoming matrix in your expression by using '.p'. For example, if you use '@expr in[0].p[0]', that gives you the first plane of the first input. To get the second plane, you could write '@expr in[0].p[1]' or '@expr in[1].p[2]' to get the third plane of the right input matrix. So, if we use '@expr in[0].p[1]*in[1].p[2]+in[1].p[0]' we would be multiplying the second plane of the first inlet with the third plane of the second inlet, and then adding the first plane of the second inlet. That was quite a mouthful, but hopefully you get the point. This can be really useful when you have an expression that requires multiple variables, and can take the place of using jit.unpack/pack in some cases.

On a related note, you can also create a unique expression for each plane of the output matrix by simply passing a list of expressions to the 'expr' attribute. Each expression is passed in as a symbol, so it is often beneficial to put quotes around your expressions, especially if it includes spaces. For the most simple case, we could type '@expr in[0].p[0] in[0].p[1] in[0].p[2]', which will just pass the appropriate planes from the first inlet. It's a lot of typing for something so simple, but we'll be making a lot of use of this feature later, so keep it in mind. It's especially useful when generating geometry, since you will often need to use different equations for the x,y, and z values.

Making Numbers

When generating geometry or image masks, you will often need to generate a whole matrix full of values based on an equation. For the simple cases, you could probably use an object like jit.gencoord to generate unique values for each cell of your matrix. One of the great features of jit.expr is the ability to generate cell values by working with the norm[],snorm[],cell[], and dim[] variables. Which one you use will depend on what you are trying to accomplish.

These variables each require you to specify which dimension you would like to generate values across (horizontal=0,vertical=1,etc.) between the square brackets. The dim[] variable simply passes the size of your matrix across the specified dimension. This will be the same for all of the cells of the matrix, and is useful for situations where you would like to adjust values in proportion to the size of the incoming matrix.

Click here for a full-sized version of the screen shot.

The simplest of the other three variables is cell[]. This variable generates integer coordinates across the specified dimension. So, if we have '@expr cell[0] cell[1]', the first plane of the matrix would have values {0.,1.,2.,3.,…} across the horizontal dimension (starting on the left) and the second plane would have values {0.,1.,2.,3.,…} across the vertical dimension (starting from the top). While this might not be so useful as an actual output value, it makes certain things like modulo (%) expressions a lot easier to write when you are working with integers.

Note: You may have noticed the decimal point in all those values. This is to remind you that jit.expr uses floating-point arithmetic internally, and will convert to whatever data type you are using upon output.

Now that we know how to use cell[], norm[] is pretty simple. This handy variable outputs values between 0. and 1. across the specified dimension. Writing '@expr norm[0]' will generate the numbers {0.,…,1.} across the horizontal dimension, starting on the left. This also creates a nice horizontal linear gradient. This expression is the equivalent of '@expr cell[0]/dim[0]' but requires much less typing. Our final variable, snorm[] is very similar to norm[], but with the numbers stretched out to cover values between -1. and 1. This is especially useful when generating vertex values for OpenGL geometry.

Making Masks

In working with imagery, it is often necessary to do things like generating an oval mask for an image, or other shapes that allow you to escape the oppression of rectangles. Now that we've gone through how to use the built-in variables and put together simple expressions, we'll use them to create a few different simple mask shapes. To start, let's try creating a simple oval mask.

Now, in order to create an oval mask shape, we'll need a way to generate values that radiate out from the center of the matrix, rather than just generating linear values. To do this, we can use the hypot() function built into jit.expr, which calculates the length of the hypotenuse of a right triangle, often used as a distance equation for two points. If we use @expr hypot(snorm[0]\,snorm[1]), we'll get values that increase as they get further from the center. Remember that snorm[] creates values between -1. and 1. so the center cell will be 0. So, we're getting close, but we have the opposite of what we really want (although an inverted oval mask can be pretty useful too). To invert the values, all we have to do is alter our expression slightly: @expr 1.-hypot(snorm[0]\,snorm[1]). Now we have a nice radial gradient. Now, you might notice the "\" in that expression. That's there because Max likes to interpret commas in a special way, and we need to include the comma as part of the expression. The backslash prevents the comma from getting eaten up by Max. Suppose we would rather have a hard-edged mask instead of a soft gradient. We could easily add a threshold value by altering our expression again: @expr (1.-hypot(snorm[0]\,snorm[1]))>in[1]. This little extra portion sets the output value to 1. if our expression result is greater than the value at the second inlet. The parentheses are there to enforce the appropriate order of operations.

We could now take this a step further and create other mask shapes based on simple geometry functions. The first one we will look at is using sin() to generate another gradient shape. If we use 'sin(norm[0]*TWOPI)' we will get a full-phase sine wave across the horizontal dimension. In this expression, we are using "TWOPI", which is one of the built-in constants (2.*PI) for jit.expr, provided for our convenience. There is also PI,HALFPI, etc. Multiplying the "norm[0]" by an input value will change the frequency of the sine-wave, which can be useful for creating repeated shapes. We could then take this expression further by multiplying a horizontal sine gradient by a vertical sine gradient. Now, if we are using this to generate imagery or masks, the negative values aren't going to do us much good. To invert the negative values, we can use the abs() operator (absolute value) on the result. Now we have a nice repeating lump gradient pattern.

By now you are probably noticing that these expressions can get pretty long and complicated with just a little tweaking. An important thing to remember is that jit.expr is an object like any other object, and there is no rule that you have to do all of your math within a single expression. In fact you will probably find situations where you need multiple jit.expr or jit.op objects to accomplish what you need.

The Shape of Data

As you are experimenting with jit.expr, you will probably want to have some sort of visual feedback to get a better sense of what is going on. For simple cases, jit.cellblock is a great solution, since it allows you to peek at the number values of the output matrix. You can also visualize your jit.expr output using a jit.window which will represent the values as color intensities. Often times this gives the most intuitive feedback. Another way that jit.expr really shines though is in generating 3D geometry that can be visualized using jit.gl.mesh. Without going into too much detail, jit.gl.mesh allows you to create different sorts of 3D forms by passing in a Jitter Matrix. The matrix is then translated in different ways depending on the chosen "draw_mode". By experimenting with different modes, you can visualize your jit.expr expressions in different ways. And once you get comfortable with the different modes, you can start creating expressions specific to the draw_mode you would like to use. Remember that jit.gl.mesh expects a 3 plane (x,y,z), float32 matrix for the vertex input, so you will need to think about how you would like to generate positions for each of the coordinates. If you would like to apply a texture to your 3D shapes, you will also need to generate texture coordinates for each cell (2 plane float32, 2nd inlet).

Click here for a full-sized version of the screen shot.

A very simple thing, which is very useful in real life, is to create a simple textured plane (similar to jit.gl.videoplane). To do this we can use a 5 plane float32 matrix, and use the following list of expressions in jit.expr: snorm[0] -snorm[1] 0. norm[0] norm[1]. If we break it down, we are just creating a simple grid for the x and y values by using snorm[]. You may have noticed that we left the third expression as "0." Since we are creating a flat plane, we can just zero out the z values. For texture coordinates, jit.gl.mesh expects values between 0 and 1., so we are just generating a grid of coordinates using norm[]. Simple, right. Now, I'll leave it up to you to imagine different sorts of equations that might provide more exciting results. You might need to brush up on your trigonometry, or if you are like me, fake it. I'm including a patch called simple-shapes that demonstrates a couple of simple and potentially useful shapes using the tri_grid draw_mode.

Express Yourself

I hope this is enough of a boost to get you started writing your own expressions with jit.expr. In future articles, we'll look at some of the more advanced features of jit.expr and ways to combine it with other Jitter objects to create unusual and wonderful results. With a little practice and study, you too can learn to tame this mysterious animal.