Dev log time. I’ve been back in Houdini again, mesmerized by the OFFF By Night 2016 title sequence.
There are a ton of amazing shots in here, but today I’m just looking at the opening shot. Director William Arnold and artist eparizi have some behind the scenes experiments, but no indication of to how (other than “sop solver”)
After a few experiments, here’s what I’ve gotten to:
I’m decently happy with this recreation, but would love to hear if anyone (esp the artists on it!) can break down the effect better.
The Breakdown
The setup is not that complicated: a bunch of thin grid geo repeated and passed into a SOP solver. A solver gives you the previous frame as input, and you modify how the next frame should look, the basis of simulations.
After the solver, the color and vertical position is applied. There is no shading here, separating the layers vertically is just to fix the z-fighting as they overlap.
Well the meat of what’s happening is in the solver itself.
- Every frame, we’re sampling some random activating points (from the
scatter
node above, passed in as an input). - For each point in the stripes, we see if they are within some
RADIUS
of activating points. These points are now activated. - Activated are given a velocity proportional to their distance to the activating point. There’s a lot of extra logic to make sure the points don’t move back, and only the top line of the stripe moves, the bottom is static. Additionally, only affect points in the hemisphere above the activating point, so points aren’t pushed backwards.
With some drag this gives a neat bursting growth, and as long as the radius of affected points is roughly greater than the max distance travelled by each point, the layers give the allusion of not overlapping.
And roughly the VEX
involved:
// find_active_and_dir
int pchandle = pcopen(1, "P", @P, ch("radius"), chi("max_points"));
@group_active = 0;
if (pcnumfound(pchandle) > 0) {
@group_active = 1;
v@active_center = pcfilter(pchandle, "P"); // avg(centers);
}
// set_vel (only applied to active group)
if (v@active_center[2] > @P.z) {
vector to_active_center = (@active_center - @P) * {1,0,1};
v@dir = normalize(to_active_center) * -1 * {0.5, 0, 1};
if (@dir[2] > 0) {
@dir[2] *= -1;
}
float dist = distance(v@active_center, @P);
float dist_scaling = `chs("../find_active_and_dir/radius")` - dist;
v@vel = @dir * ch("vel_scale") * dist_scaling;
}
Currently this overly uses above (greater z), like to determine the hemisphere. This breaks down if you rotate the points as well. This should be turned into forward, say with a dot product of an initial forward vector
What could be better
Unfortunately when you spend your time recreating something, it becomes more about copying exactly than just making something good in its own way.
I’m not 100% happy with the motion, and I think it’s still simpler than what happens in the real sequence. For example, I’m fundamentally lacking these interactions:
I’m not convinced this is a differential curve growth like where I first saw shots from the title sequence, but the motion is definitely more entrancing than what I have.