Alien Romulus

cover

Zero gravity spinning acid

In the sequence, gravity is disabled so when the alien is shot, the acid floats freely. The acid spins along the corridor and forms a tunnel with an open center, leaving room for characters to move through. For this article I re-created the setup and broke it down into smaller, reusable components for future reference.

Generating Acid caches

Reference

paintball

Play

The acid should read as directional and stringy, like a hand reaching out. It moves slowly, feels dangerous, and drifts through space.

Simulation

splash source

Source

splash source cluster

  1. Deform and transform the source, then use the difference from the original position to create velocity. dopnet
  2. Group points by cluster to create structure. Randomize velocity per-cluster to produce a stringy look; set some cluster velocities to zero to encourage tearing. An alternative is converting the source to geometry, fracturing it, and exploding pieces with an RBD solver to generate initial velocity.

Solver

The most important parameters are scale and surface tension. Scene scale usually needs to be larger than real world to preserve small details. I use a top wedging workflow to find suitable liquid values.

solver settings

  1. Surface oversampling helps with keeping the stingy look by adding particles in the thin area (Example values: particles per voxel:10, surface oversampling:2, oversampling Bandwidth:1.8)
  2. Particle separation keeps points evenly distributed in thin films and reduces holes and popping.
  3. Increasing resolution (reducing grid scale) gives better support for thin features.
  4. Other useful changes: raise max substeps, enable vorticity and id

Wedging setup

topnet

  1. Wedging attributes topnet Wedging the velocity noise size and surface tension to find the right look wedge
  2. Use OpenImageIO for adding details into the picture wedge
  3. Use ImageMagick to combine image for montage
  4. Collect wedges loading wedge
string wedge_list[] = split(chs("wedge_list")," ");
for(int i=0; i<len(wedge_list);i++){
    int id = addpoint(0,set(0,0,0));
    setpointattrib(0,"wedge",id,wedge_list[i]);
}

Results

Other useful resource for surface tension: https://medium.com/@vupham_37726/houdini-flip-surface-tension-demystified-f1239da880ce

VDB Advection with velocity field

While FLIP simulation provides detailed fluid animation, procedural post-sim advection gives greater control for composition and layout.

Advection setup

Vdb advect points

Applying a ramp to the advected field scales the deformation by distance from the core. This creates organic motion but can over-distort if run too long. A common approach is to use two advection passes: one that moves the whole piece for large scale movement and a second per-point advection for finer deformation. Using a static VDB advect point to set initial positions and an animated VDB advect point for motion works well: vdb advect points

Local roll

vector4 original_orient = p@orient;
matrix3 m = qconvert(original_orient);
prerotate(m,radians(ch("amount")*@rotate_speed*(@Frame+ch("frame_offset"))),chv("rotate"));
p@orient = quaternion(m);

velocity field

This builds the velocity field from a curve, useful for both simulation and post-sim deformation: vel field network

Follow, suction and orbit:

curve_vel_field wrangle:

// runs on volume with vector field vel
int prim;
vector primuv;
float radius = ch("radius");
float dist = xyzdist(1,v@P,prim,primuv);
vector N = primuv(1,"N",prim,primuv);
vector P = primuv(1,"P",prim,primuv);
//prep
float r_normalize = fit(dist,0,radius,0,1);
float mask = (dist>radius)? 0:1;
//follow
vector follow_dir = normalize(N);
float follow_mult = ch("follow_mult");
float follow_ramp_r = chramp("follow_ramp_radius",r_normalize);
// suction
vector suction_dir = normalize(P-v@P);
float suction_mult = ch("suction_mult");
float suction_ramp_r = chramp("suction_ramp_radius",r_normalize);
// orbit
vector orbit_dir = normalize(cross(follow_dir,suction_dir));
float orbit_mult = ch("orbit_mult");
float orbit_ramp_r = chramp("orbit_ramp_radius",r_normalize);

v@vel = follow_dir*follow_mult*follow_ramp_r;
v@vel += suction_dir*suction_mult*suction_ramp_r;
v@vel += orbit_dir*orbit_mult*orbit_ramp_r;
v@vel *= mask;