curvy fluidicles

After a lot of study and (more or less successful) tests and experimentation, I finally began working on the shots for my first technical showreel, which will be mostly focused on dynamics, lighting, and rendering, with a smidgen of sparse monkey-code.

Unfortunately, even though I’ve a pretty clear and defined idea of what I want to get to, I keep on being influenced by stuff I see around and slightly re-tune the visual result of my work. Such a phenomenon, which I’m sure is well known to the most of you, ends up triggering further researches and delaying the completion of my project, every time; but I guess that’s part of the game, and I’m still inclined to play it.

So this post is a report of one of those tiny details, which got fatter along the path.

The whole thing started when I saw Particles Pushed by Fluids by Shawn Lipowski:

Since most of my showreel’s shots will involve particles, fluids and PRMan shaders applied to curves, that video suggested me a finer way to render an effect I had in my mind; then, I spent some time to figure out how to reproduce it by using a procedural shader (I guessed Shawn’s version was obtained differently, by emitting static particles from the ones driven by the fluid).

Here is the test I produced, after playing a little bit with the code:

When I showed the earliest tests to a (great) friend of mine, he warmly suggested to reconsider the whole thing and to remember the wisdom of the KISS principle. He was probably right. But he knows how stubborn I am, too, and he surely expected I would have gone for the KICS way, instead (where the C obviously stands for Complex, not Complicated, and the S still stands for Stupid).

Anyway, I created the 3D fluid and used it as a field to drive particles, in the same fashion Shawn did; next step was to generate a bunch of curves from those particles’ path (not that hard, apparently, but in the end trickier than I could expect).
A few weeks before, while googling for boids and crowd systems, I bounced into a very handy, simple script by Carsten Kolve, which seemed to fit perfectly with my need. Unfortunately it didn’t work as it was, since the script expects all the particles to be created at the first frame, and coexist in the timeline at every frame. That’s because (and I realized it only then) Maya’s particles aren’t identified by their IDs; instead, an array is generated at every frame, where only the existing particles at that particular frame are listed. In the end, that’s reasonable as it is definitely more efficient to manage the system that way; nonetheless, that required me to tweak the code so that it worked with my scenario, where particles where created and died at different frames, along the timeline.

What I ended up with is the following script (please note that I removed parts of the original code, just to make it slimmer; the two should be merged and debugged for a cleaner, more generalized solution):

// based on particleMotion2Curve.mel v1.0
// (c) 10.2002 by Carsten Kolve

global proc string getShape( string $loc ){
    string $shapes[];
    $shapes[0] = $loc;
    if ("transform"==`nodeType $loc`){ $shapes = `listRelatives -s $loc`; }
    return $shapes[0];

global proc pt2crv(int $st, int $et, int $step, int $cDeg){   
    string $sel[] = `ls -sl -fl`;
    $sel[0] = getShape($sel[0]);    
    // get list of all particles IDs
    float $id[]; // per frame IDs array
    string $pPt; // path to particle
    int $pId; // particle's ID
    int $s; // array size
    int $t; // time
    int $i; // iterator
    string $makeCrvCmd[]; // curve commands array
    int $pBirth[]; // particles' birth array
    int $pDeath[]; // particles' death array
    for ($t=$st; $t<=$et; $t++) {
        currentTime $t;
        $id = `getAttr ($sel[0] + ".particleId")`;
        $s = size($id);
        for ($i=0; $i<$s; $i++){
            // create curve commands array
            $makeCrvCmd[(int)$id[$i]] = "curve -d " + $cDeg;
            // initialize particle's birth and death
            $pBirth[(int)$id[$i]] = -1;
            $pDeath[(int)$id[$i]] = -1;
    // extend curve commands: per frame, per particle position
    float $v[]; // particle's position 
    for ($t=$st; $t<=$et; $t++) {
        currentTime $t;
            $s = `getAttr ($sel[0] + ".count")`;
            for ($i=0; $i<$s; $i++){
                $id = `particle -at particleId -or $i -q $sel[0]`;
                $pId = $id[0];
                $pPt = $sel[0] + ".pt[" + $i + "]";
                $v = `getParticleAttr -at position $pPt`;
                $makeCrvCmd[$pId] = $makeCrvCmd[$pId] + " -p " + $v[0] + " " + $v[1] + " " + $v[2];
                // set particle's birth and death
                    $pBirth[$pId] = $t;
                $pDeath[$pId] = $t;
    // make curves and parent
    $s = size($makeCrvCmd);
    string $grp = `group -em -n particleCurves`;
    string $crv;
    for ($i=0; $i<$s; $i++){
            $crv = eval($makeCrvCmd[$i]);
			// assign birth and death attributes
            addAttr -ln "birth" -sn "b" -at "float";
            setAttr ($crv + ".b") $pBirth[$i];
            addAttr -ln "death" -sn "d" -at "float";
            setAttr ($crv + ".d") $pDeath[$i];
            parent -relative $crv $grp;


pt2crv(1, 300, 1, 3);

Also, if you look into the code, you'll notice that a couple of attributes were stored in two arrays ($pBirth and $pDeath), and then assigned as attributes to each curve. That was done in order to reuse those informations when rendering, as you'll see below.

Then, it was time to shade and render those curves, with 3Delight. Here's the shader I wrote:

surface curve (
				float 	Kd = 1;
				float	opacityRatio = .5,	// 0 < value < 1
					fadeLength = .25,	// 0.001 < value < 1- opacityRatio
					colorRate = 200,
					colorFade = .3;
				output varying	color	aov_color = 0;
				output varying	color	aov_shadow = 0;
	normal Nf = faceforward(normalize(N),I);
	uniform float birth = 0;
	uniform float death = 0;
	attribute ( "user:birth", birth);
	attribute ( "user:death", death);
	float curveO;
	float life = death - birth;
	float nTime = (time - birth) / life;

	if ((time > birth) && (nTime > v)){
		curveO = 1 - smoothstep(opacityRatio, (opacityRatio + fadeLength),(nTime - v));
	} else { 
		curveO = 0;
	Oi = float curveO;
	float baseH = mod(birth / colorRate, 1);
	float tipH = mod((baseH + colorFade), 1);
	color baseC = color "hsv" (baseH,1,1);
	color tipC = color "hsv" (tipH,1,1);
	color ageC = mix(baseC, tipC, v);
	aov_color = ageC * Oi;
	aov_shadow = Kd * diffuse(Nf) * Oi;
	Ci = ageC * (Kd * diffuse(Nf));

	Ci *= Oi;

Nothing fancy, there. It is basically a Lambert with just a few custom features:

  • most of the parameters are driven by the original particles' birth and death; those are read from the RIB file, through the attribute() function
  • Ci is driven by time, so that hue changes according to the particles' birth
  • Oi is driven by time, too, so that curves are invisible before particles' birth, and they fade out after a certain length is covered
  • a couple of custom AOV outputs (aov_color and aov_shadow) were added, in order to have some (minimal) control over compositing; please note that aov_shadow brutally includes both deep shadows and diffuse

The last few lines of MEL I wrote, are to automatically export each curve's birth and death time as custom RiAttributes in the RIB file. Those lines were added in MEL Scripts' section of the delightGeoAttribs node assigned to the curves' group (within the Pre Geo MEL field). Here they are:

string $parents[] = `listRelatives -fullPath -parent $shape_path`;
string $v = `getAttr ( $parents[0] + ".birth")`;
RiAttribute -n "user" -p "birth" "float" $v;
$v = `getAttr ( $parents[0] + ".death")`;
RiAttribute -n "user" -p "death" "float" $v;

When looking at the whole thing, I'd say it definitely fits with the KICS principle.
Surely some of the code could be cleaned up, made less redundant and more efficient; but it worked, and in the end it didn't take too much time.
Anyway, I guess that's how I'm expected to spend that time, while seeking the nerd side of the moon.

Leave a Reply

Your email address will not be published. Required fields are marked *