Tutorials

Rainy/Snowy Day Max, Part II: Meshes and 3D Spirographics

Welcome back to the second portion of our patch on the mysteries and wonders of the Spirograph. This time out, we are going to augment our Spirograph patch by using the Jitter jit.gl.mesh object rather than jit.lcd. Using the jit.gl.mesh object will let us make use of the drawing modes that the object supports to create even more interesting output from the Jitter matrix of values the Javascript code creates.

SPIROGRAPHICS_PART2.zip
application/zip 19.31 KB
Download the patches used in this tutorial

Meshing Around

Let's start by making a comparison between the Spirograph patch Darwin created last time and the jit.gl.mesh version of the patch. Here is the original from Part I of the tutorial:

Here is the To_Mesh.maxpat patch together with the display window it uses rather than the in-patch jit.window object:

You may be surprised to discover how little this patcher varies from what we created for the jit.lcd-based patch. Apart from some obvious differences related to using the standard jit.world approach to visualizing our mesh, the input routings are precisely the same. The largest difference is that we've replaced the original p Spirographics patcher with a new one (p Spirographics_mesh) that will perform the calculations we need for our jit.world-based rendering.

The part of the patch located below the p Spirographics_mesh patcher handles the jit.world-based rendering, and demonstrates a few of the new things we can add to our patch visualization:

  • Since we are rendering our results to a jit.world object, we can make use of a jit.gl.handle object to let us zoom in and otherwise manipulate the visual results, which means that we won't need to worry about having to scale the image (as we did before using the coord–scale 400 abstraction inside our earlier p Spirographics subpatcher).

  • The jit.gl.mesh object lets us vary the line width for our display using the line_width attribute and select the drawing mode we want to use for our visualization using the draw_mode attribute. We have set defaults for each of these attributes for the jit.gl.mesh object using attributes, and added attrui objects to let us change them on the fly. That's a big win for changing how we do rendering, as you'll see.

  • We can automatically add color to our output and define those colors using the auto_colors and color_mode attributes, respectively. Those have been set by attributes to the jit.gl.mesh object as well.

Let's examine how the jit.lcd-based p Spirographics and the jit.gl.mesh-based p Spirographics_mesh subpatchers vary. Here's the original p Spirographics subpatcher:

And here's the new p Spirographics_mesh subpatcher:

As with the parent patch, there's not a lot of change, and what change there is relates to the fact that this subpatcher is writing its results to a Jitter matrix, which the jit.gl.mesh object will use to render the results.

The jit.matrix object that will contain the data used for rendering is a matrix of 64-bit floating-point values whose dimensions will vary according to the number of points we want to render (we've set a default value using arguments, and we can reset those dimensions any time the number of render points changes in the parent patch using a dim message to the jit.matrix object (by means of the addition of a second patchcord from the fourth outlet of the route object).

Here's one thing that hasn't changed at all — we're using exactly the same js spiro.js object we used in the previous patch. The p meshCommandHandler subpatcher will take the js spiro.js output and reformat it to a matrix whose dimensions are automatically set depending on the number of points we want to plot. If you’re being observant, you may have noticed that our jit.matrix object has three dimensions rather than two. Does that mean we're thinking ahead to a 3D version of our patch? Yes, it does. Read on....

Messages to Matrices

The p meshCommandHandler subpatcher handles taking the js object’s output and formatting it as a matrix:

As before, you will recognize the use of the route object to process incoming messages based on their first argument:

  • The start message uses a setcell message to set the X and Y coordinates of the outside edge of the second/smaller circle as a starting point.

  • A point message is used to generate the cell associated with the X/Y coordinates to be written. The coordinates are generated using a counter object whose upper limit will be set based on the number of points to be plotted — X columns and Y rows. A start message also resets the counter object each time a new set of points will be plotted. Each time a new point message is received, the X and Y matrix memory locations are derived by dividing the count value by 2 for the X coordinate and taking the modulo value to derive the Y coordinate. The pair of values that accompany the point message are then packed to create a list that writes the route object’s output values to the X/Y coordinates in the matrix using a setcell message

  • When the calculations are complete, a bang message is output to the jit.matrix object, and then to the jit.gl.render object to be displayed.

2D or Not 2D

Remember how you noticed a bit earlier that we were working with a 3D jit.matrix object? A quick second look at the code in the js spiro.js object will verify that we’re not doing any calculations for a third (Z) setting that would give us 3D output; when we construct the output to our jit.matrix object inside the p meshCommandHandler subpatcher, we’re simply always writing a value of zero for the Z coordinate values. So now it's time to do something with that Z value and step into the third dimension!

Meet the To_3D_Mesh.maxpat patch:

It's not all that much different from the To_Mesh.maxpat patch, but the output sure looks different!

You'll notice that this patch adds a new DepthScale parameter, and add a 5th inlet to the pak object that formats the list to send to the p Spirograph_mesh subpatcher. Let's look inside that subpatcher to check out the changes in the patching there:


In order to do work in three dimensions. we’re going to need to modify our Javascript code to handle calculating 3D output. You’ll find that new code inside the js spiro3d.js object. Let’s look at the changes we’ve made to enable 3D rendering and walk through them:

We’ve added a single new global variable (depthscale) to set a default value for Z coordinate output.

// global variables
var largeRad = .70;
var smallRad = .15;
var smallRatio = 30;
var renderpts = 1000;
var renderstep = TWOPI / renderpts;
var depthscale = .1;

Our bang() function is where you’ll notice the big changes. The function now includes a third variable (stz) to define the starting point values, which is calculated and stored as we iterate through our calculations.

// the bang function renders the current settings into a set
// of vector locations in the ranges of -1.0 thru 1.0
function bang() {
    var x, y;            // calculated dimensions
    var tmp, theta;            // variables for drawing calcs
    var stx, sty, stz;        // variables containing the starting points
    var started = 0;        // 'first time' flag

The biggest change in the code involves the way we calculate a Z coordinate based on the variables we’re already calculating. We’re taking the tmp value derived from the theta and smallRatio variables, and then modifying that result and scaling the output to a range of -1. to 1. (which is what we’re using for the X/Y coordinate space.

for (theta=0; theta<TWOPI; theta+=renderstep) {
    // calculate the drawing position
    tmp = theta * smallRatio;
    x = largeRad * Math.cos(theta) + smallRad * Math.cos(tmp);
        y = largeRad * Math.sin(theta) + smallRad * Math.sin(tmp);
    
        z = Math.abs((tmp % 2) - 1);    // reduce to 0 - 1, rotate around 0
    z = Math.abs(z - .5);        // rotate around 0
    z = (z - .25) * 4;        // scale to -1 - 1
        
    z *= depthscale;        // do depth scaling

We’ve added a new function - setDepthScale() - to set the depth scaler.

// set the depth scaler
function setDepthScale(v) {
    if ((v < 1.0) && (v > 0.0)) {
        depthscale = v;
    }

And finally, we now output a 5-item message, whose 5th item is the Z coordinate value.

    // send out the end message along with the
    // original start point (to complete the drawing)
    outlet(0, 'end', stx, sty, stz);

Having made those changes in our Javascript, all we need to do is to modify the p meshCommandHandler subpatcher to replace the Z value for the setcell message sent for each point in the calculation – the only change is the setcell message box connected to the route object’s rightmost outlet.

With that change added, we're ready to roll. As before it's a lot of fun to try different settings. And since we're working with OpenGL, there are lots of interesting drawing modes to investigate. Here are just a few examples:

  1. Big Ring: 0.09, Small Ring: 0.5, Theta: 189, Render Points: 330, Depth Scale: 0.44, Draw Mode: polygon

  2. Big Ring: 0.4, Small Ring: 0.0, Theta: 48, Render Points: 144, Depth Scale: 0.1, Draw Mode: line_loop

  3. Big Ring: 0.5, Small Ring: 0.4, Theta: 64, Render Points: 32, Depth Scale: 0.5, Draw Mode: quads

  4. Big Ring: 0.6, Small Ring: 0.0, Theta: 32, Render Points: 80, Depth Scale: 0.941, Draw Mode: line_loop

  5. Big Ring: 0.424, Small Ring: 0.32, Theta: 233, Render Points: 425, Depth Scale: 0.071, Draw Mode: tri_strip_adjacent

  6. Big Ring: 1.0, Small Ring: 0.671, Theta: 250, Render Points: 141, Depth Scale: 0.37, Draw Mode: quad_grid

  7. Big Ring: 0.8, Small Ring: 0.4, Theta: 64, Render Points: 1024, Depth Scale: 1.0, Draw Mode: quads

  8. Big Ring: 0.1, Small Ring: 0.4, Theta: 32, Render Points: 80, Depth Scale: 0.02, Draw Mode: quad_strip

Once you have a setting you like, be sure to use the jit.gl.handle features to click and drag to explore your results in 3D!

Doubling Down

Of course, there are still interesting places we can go with this patch as a starting point — for example, you might consider coming up with your own approach to calculating an offset value for the Z coordinate that makes use of the variables we’re already calculated in the Javascript.

But I'll leave you with my own "next step" — working with two copies of the 3D patch to create a crossfadable mesh pair. It's really not difficult to do, given that our original p Spirograph_mesh patch sends a standard Jitter matrix to the jit.gl.mesh object for rendering. The jit.xfade object is tailor-made for things like this: you give it two matrices and a floating-point value between zero and one, and it will interpolate between both sets of data and output the result. In the case of our patch, that output will be a "morph" between two Spirograph drawings. Here's the Double_3D_mesh.maxpat patch in the midst of just such a crossfade:

However, there is one thing we'll need to take into account: Our original 3D mesh patch outputs a single Jitter matrix each time we change one of its input parameters rather than being connected to some patching that regularly sends bang messages. We're going to have to add some logic to handle generating new crossfade output each time either of the two matrices is updated, and we'll need to add some logic to trigger our output if we manually modify the crossfade value itself. The p 3d_crossfade subpatcher takes care of all of those situations using our old friend the t (trigger) object:

All we need to do is to attach a multislider to the p 3d_crossfade subpatcher's left inlet and connect up the two outlets from each of the p Spirograph_mesh subpatchers to the middle and right-hand inlets of the p 3d_crossfade subpatcher, and feed the output of the p 3d_crossfade subpatcher we borrowed from one of the original Double_3D_mesh.maxpat patches. The results speak for themselves....


Thanks for following along, and we hope you enjoy this bit of patching. If you add any features of your own, feel free to share your results in the comments area.

Happy patching!

by Darwin GrosseGregory Taylor on January 31, 2022

odelano's icon

Great fun, ddg!
Cheers from an old friend,
Oliver.

wbreidi's icon

Hello,
Jit.world is activiated, parameters are tweekd, but nothing happens with my 2D & 3D patches! I get a blank spirographics screen!

Max Gardener's icon

Which patch is giving you trouble?
What version of Max are you running?
What platform?
Are you seeing any error messages in the Max window?

wbreidi's icon

Both patches; To Mesh and To 3D Mesh.
Max version 8.2.0
macOS Catalina 10.15.7 on a MacBook Pro 15 inches 2015, 16GO Ram, Intel Core i7
No error messages displayed in the Max window.

Is there something stupid I need to do, I haven't done?

Gregory Taylor's icon

Hello, Walid.

Please download the zip file again, and let me know if it doesn't sort out your issues.

In the course of working on this, I realized that I was using the same context name for both the single and double 3D mesh patches, which meant that both of them couldn't be opened at the same time. Since that might confuse some readers, I corrected that, too.

Our mutual friend Darwin bears no responsibility at all for these infelicities.

wbreidi's icon

Hello Gregory,
I downloaded the file again but I have the same problem. This is a mystery to me!!

Holland Hopson's icon

Thanks for such a fun series, Gregory! I implemented the spirographics javascript code in gen~ and jit.gen.

Max Patch
Copy patch and select New From Clipboard in Max.

I've been having fun using the gen~ version to drive spatialization, 2dwave playback, etc.

Gregory Taylor's icon

Hey, Holland. Nice work!

Since I was doing the pedagogical thing and using some examples from someone else's website as a starting point for tutorial 1, I did things a little differently than in my normal gen~ patching life. One of the poltocar operator's superpowers is the extent to which it simplifies circular traversals. Here's a version of your patch that uses poltocar (looks a little neater, huh?) for you. While I was at it, I added the ability to displace the flower thingie and wrap the bejeepers out of it on the edges. Happy patching!

Max Patch
Copy patch and select New From Clipboard in Max.



ghostique's icon

Hi! Lovely article. I want to ask also if Gregory's book Step by Step will be available for the Kindle Paperwhite? I can't buy it for my device :-(