Moana 2

Bioluminescent Magic for Ancestor Transformation

Play Play

The task

In the film, when people pass away they transform into sea animals. In this sequence, Moana’s ancestors gather around Maui’s underwater air bubble and chant to bring Moana back to life. The ancestor’s animal form is a whale shark, which is much larger than the human form. So we needed a way to cover that change smoothly.

Light

Looking up from below

The idea starts from the whale’s glow that slowly becomes shadow. At the same time a soft spherical glow grows from the center. Circular light expands into a human silhouette while a human shadow gradually appears.

Caustics

Caustic

The initial idea came from an Entagma tutorial. I adapted it to use a sphere as the light source and project points onto a curved surface so that the shapes get larger towards the edge and have a natural fading out without a mask.

Overall caustic setup

  1. set orign_N set the base direction from sphere to the project plane
  2. deform the sphere for the normal
  3. refract on the deformed sphere and project onto projection plane
        vector ray_dir;
        vector p2, uvw;
        int prim;
        // orig_N is the direction pointing from the sphere to the 
        // projection plane, N is the normal of the deforming sphere
        ray_dir = refract(v@orig_N, v@N, ch("ior"));
        ray_dir *= 100;
        // check intersection and project onto collider
        v@ray_dir = ray_dir;
        prim = intersect(1, v@P, ray_dir, p2, uvw);
        if (prim == -1) {
            removepoint(0, @ptnum);
            v@Cd = 0;
        } else {
            v@Cd = primuv(1, "Cd", prim, uvw);
            v@target_N = primuv(1, "N", prim, uvw);
        }
        v@P = p2;
  4. Set density to filter out sparse area
        float maxdist = ch("maxdist");
        int pts[] = nearpoints(0,v@P,maxdist);
        float density = fit(len(pts),ch("min"),ch("max"),0,1);
        @Cd = density;
        @density = density;
  5. Use blur sop to shapen and convert to volume

Whale mask (fading)

Whale mask

A simple mask generated from the whale shark silhouette, with a glow at the edge and animation driven by the glow source distance.

Ancestor emerging shadow

Ancestor emerging shadow

The whale is much bigger than the human, so to make the transformation feel smooth we have a shadow “walk out” as the whale turns into light and the human form emerges. After blurring and layering, the mask approximates the light shape you see when looking up from underwater (Snell’s window).

VEX for refraction/projection (similar to caustics):

// get ray direction towards object
vector dir = getbbox_center(1) - v@P;
dir = v@rest_N;
vector ray_dir = refract(dir, v@N, ch("ior"));

// emit ray from deforming surface and check collision
vector p2, uvw;
int prim;
ray_dir *= -100;
prim = intersect(1, v@P, ray_dir, p2, uvw);
v@ray_dir = ray_dir;
if (prim == -1) {
    v@Cd = 0;
} else {
    v@Cd = 1;
    v@target_N = primuv(1, "N", prim, uvw);
}

Water surface contact

Interaction with the water surface Then we tried a couple different ideas The first logical attept was to have a ripple:

Ripple solver

There are many ways to create ripples in Houdini. The ripple solver is a straightforward choice: source the ripple from the distance between surfaces and apply a ramp to the height offset. It works well but lacks a “magical” quality and the shapes are not very clean.

Procedural circle ripples

Raindrop

Rain-drop-like circles are gentler. The circle solver gives outward velocity that trails the circle line, whereas the ripple solver primarily moves geometry up and down. It’s render in RGB from @N for distortion in compositing

Circle Ripple Setup

  1. Animated circle by setting point attributes like trigger frame and life and calculate the pscale foreach point
    //set age and scale
    @age = @Frame- @trigger_frame;
    @pscale = fit(@age,0,@life,v@pscale_range[0],v@pscale_range[1]);
    v@center =v@P;
  2. delete the whole circle before born, add variation to the points circles desolves gradually
    //delete_after_death
    float limit = fit(@age,@life*(.7+@death_offset),@life,0,1);
    if(rand(@id)<limit)
        removepoint(0,@ptnum);

Displace Surface Setup

  1. Offset on in camera z and project onto surface
    matrix Op_Matrix= optransform(chsop("cam")); 
    vector dir  = getbbox_center(0)-Op_Matrix*set(0,0,0);
    v@dir = dir;
    v@P += normalize(dir)*ch("mult");
  2. Copy attributes to surface and create mask base on distance from the circle lines
    int prim;
    vector primuv;
    float dist = xyzdist(1,v@P,prim,primuv);
    vector p = primuv(1,"P",prim,primuv);
    @age = primuv(1,"age",prim,primuv);
    @life = primuv(1,"life",prim,primuv);
    @dist = length(p-v@P);
    @mask = chramp("ramp",fit(@dist,0,ch("maxdist"),0,1));
  3. Calculate height multiplyer with height, then apply displacement onto surface
    v@rest_N = v@N;
    float age_height =chramp("ramp", fit(@age,0,@life,0,1));
    v@P+= v@N*@mask*ch("mult")*age_height;

Bioluminescent particles

Referencing the original movie where the grandmother ray swims by. As an FX artist my instinct was to create a big splash but the sequence needed a more somber, magical feeling.

Whale bubble feeding

Whitewater shape solver

I used a existing studio solver for enhancing foam shape. It uses PCA to add forces so particles form stringy, foam-like patterns. Here is the overall process:

  1. Find all the particles in a patch around each point
  2. Center the patch.
  3. Create the covariance matrix.
  4. Calculate eigenvectors and eigenvalues.

The eigenvector basically represent the overall direction of the patch, using the perpendicular of the principal eigenvector as force aligns the particles along the eiganvector so there are more string like feature.

SOP PCA

Houdini has a SOP PCA implementation. It requires points to be arranged so each point stores neighbor positions. It’s impractical for this use case here but helps illustrate the concept.

pca Set neighbours wrangle:

// for each point, store the neighbors and their position into arrays
// search radius and max neighbours
float proxrad = ch("proxrad");
int maxneigh = chi("maxneigh");
int pts[] = pcfind(0, "P", @P, proxrad, maxneigh);
vector p_data[];
for (int i = 0; i < maxneigh; i++) {
    if (i < len(pts)) {
        p_data[i] = point(0, "P", pts[i]);
    } else {
        p_data[i] = v@P;
    }
}
i[]@__neighbors__ = pts;
v[]@p_data = p_data;

Duplicate neighbors wrangle:

// the number of dimensions (number of neighbors)
// this links to the current iteration of the for loop
int iter = chi("iter");
vector new_p = v[]@p_data[iter-1];
new_p -= v@P;
addpoint(0, new_p);
removepoint(0, @ptnum);

References: