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.
![]()
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.

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.
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;
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;
A simple mask generated from the whale shark silhouette, with a glow at the edge and animation driven by the glow source distance.
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);
}
Interaction with the water surface Then we tried a couple different ideas The first logical attept was to have a ripple:
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.

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
//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;
//delete_after_death
float limit = fit(@age,@life*(.7+@death_offset),@life,0,1);
if(rand(@id)<limit)
removepoint(0,@ptnum);
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");
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));
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;
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.

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:
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.
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.
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: