Face Points
A face mesh begins as landmark points. Each point marks a stable semantic place on the face: a mouth corner, eyelid edge, brow ridge, nose bridge, cheek contour, or jaw point.
Most browser pipelines store those points in normalized image space so the same data works for any render size.
x: 0.0 left -> 1.0 right
y: 0.0 top -> 1.0 bottom
z: relative depth
The critical rule is point-index identity. Point 61 in expression A and point 61 in expression B must mean the same anatomical point, or the morph will twist.
Interactive Mesh Lab
Scrub between two real expression tiles. The compact teaching mesh uses only 19 landmarks, but it preserves the same three invariants as the full 478-point model: landmark identity, shared triangle indices, and source-specific texture coordinates.
Every triangle keeps the same three landmark indices while its corners move. This is shared topology.
From Points To Triangles
A renderer cannot draw a cloud of points as a face surface. It needs triangles, and the same triangle list should be reused for every expression.
Voronoi And Delaunay Intuition
A Voronoi diagram answers: which landmark owns each patch of nearby image space? Its dual, the Delaunay triangulation, connects landmarks so triangle edges tend to avoid long skinny slivers.
That matters for faces because skinny triangles stretch texture aggressively. Delaunay-style connectivity gives better local neighborhoods for cheeks, lips, and eyes.
Morphing Positions
The simplest morph is linear interpolation. At time t, each vertex is somewhere between expression A and expression B.
P(t) = A + (B - A) * t
When every landmark index is consistent, the whole face appears to change expression. When an index is wrong, a lip vertex may jump into an eyelid or a cheek can fold through the mouth.
Worked vertex
Suppose one mouth corner moves from A = (0.37, 0.61) to B = (0.42, 0.56). At t = 0.25, it has completed one quarter of that motion.
Texture Coordinates And Dual UVs
Geometry motion is only half the trick. The shader also needs to sample each source texture at the correct landmark coordinates. The face viewer keeps two UV attributes: one for expression A and one for expression B.
color = mix(textureA(uvA), textureB(uvB), t)
This is why the texture follows the face points instead of sliding over a static rectangle. The destination mouth texture is sampled from the destination mouth coordinates, even while the geometry is halfway through the morph.
Transforms
Normalized image points must be transformed into the coordinate system used by the canvas or Three.js scene. A common mapping centers the face and flips the Y axis so screen coordinates become world coordinates.
world_x = (x - 0.5) * 2
world_y = -(y - 0.5) * 2
world_z = z * depth_scale
Scale, rotation, and translation are then safe to expose as controls because they operate on the mesh after the landmark topology has been built.
Checkpoint
Try to answer each question before opening the explanation.
Why not triangulate every expression independently?
Independent triangulations can connect different landmark indices. Interpolation then changes both vertex positions and surface connectivity, producing folds and texture jumps. One shared triangle list keeps connectivity invariant.
Why are two UV sets needed if there is only one moving mesh?
The same vertex refers to different pixel locations in the two source images. uvA samples the feature in expression A and uvB samples its matching feature in expression B; the shader blends those two samples.
A mouth shape moves correctly, but its color slides across the cheek. Which data should you inspect?
Inspect the UV attributes and source image dimensions. Correct geometry with sliding texture usually means the vertex positions are right but one texture is sampled with the wrong coordinates or scale.
Debugging Checklist
- Draw the points before drawing triangles.
- Toggle wireframe to inspect topology during the blend.
- Check the same landmark index on both expressions.
- Verify UVs against both source images, not just the current geometry.
- Keep privacy boundaries explicit: this is a morphing demo, not identity verification.
Transfer exercise
- Open face.ragbaz.cc and choose two expressions with a large mouth or brow difference.
- Scrub slowly, then enable wireframe. Identify one region whose triangles move locally and one region that barely moves.
- Swap A and B. The same triangle indices should now travel in reverse without reconnecting.
For the production data flow and shader contract, continue to the Atlas experiment notes.