Drive face deformation with expressions
A FACS-driven face rig maps Action Units (AUs) — atomic facial movements like brow-raise, jaw-open, smile-corner — to vertex displacements on a 3D head mesh. In OctoMY, you wire AUs to vertices through formulas, evaluated on every frame as the AU sliders change.
Pro Tip
The expression language is the same as in the spreadsheet and Construct varsets. If you've prototyped a formula in the spreadsheet, it works here verbatim — only the variable names change. See the expression language reference for the complete syntax.
What you wire together
A face rig has three layers:
- Action Units — the input sliders. Each AU is a
floatin[0, 1](sometimes signed in[-1, 1]for symmetric movements). - Vertex offsets — per-vertex, per-axis displacement formulas. Each vertex of the mesh gets up to three formulas (
x,y,z). - Optional secondary signals —
time,frame, or shaped derivatives of AUs if you want pulsing, asymmetry, or follow-through.
Each vertex-axis formula references AUs by their declared name. The engine compiles all formulas once when you save the rig and re-evaluates them on every frame as AUs change.
A first rig: jaw-open lowers the lower-lip vertex
Declare an AU named au_jaw_open. Pick the centre lower-lip vertex and edit its y formula:
au_jaw_open * -0.02
That moves the vertex 2 cm down when the slider is at 1.0. Pick the two corner vertices and use a smaller coefficient (-0.012) so they trail behind the centre.
Drive au_jaw_open from 0 to 1 — you should see the lower lip drop as expected.
Combining AUs
Smile lifts the corners of the mouth diagonally:
// Lip corner X (outward)
au_smile_corner * 0.018
// Lip corner Y (upward)
au_smile_corner * 0.012 + au_jaw_open * -0.005
Mixing AUs is just additive. The two AUs combine — jaw-open partially cancels smile-corner's upward motion when they fire together.
Sneer with bilateral asymmetry
Use signed AUs for asymmetric movements. au_sneer in [-1, 1] lifts the left side of the upper lip when positive, the right side when negative:
// Left upper-lip vertex Y
clamp(au_sneer, 0.0, 1.0) * 0.015
// Right upper-lip vertex Y
clamp(-au_sneer, 0.0, 1.0) * 0.015
clamp gates the AU per side so each vertex only responds to its half of the slider's range.
Add secondary motion
A subtle breathing rhythm with no AU involvement, driven entirely by time:
// Chest vertex Y, breathing
sin(time * 0.8) * 0.005
Layered on top of AU-driven motion, this gives the head a sense of being alive even when no AUs are firing.
Using vector built-ins
Per-vertex normal-direction nudges use vector arithmetic. If you've baked the vertex normal as input variables nx, ny, nz, you can drive surface inflation along the normal:
au_cheek_puff * nx * 0.008 // x axis
au_cheek_puff * ny * 0.008 // y axis
au_cheek_puff * nz * 0.008 // z axis
Or, if your developer surface exposes the normal as a single vec3 normal variable per vertex:
(au_cheek_puff * normal * 0.008).x // .y, .z for the other axes
Helper functions
When the same shaping function appears across many vertices, define a helper at the top of the rig's expression block:
float bell(float x, float peak, float width) {
float d = (x - peak) / width;
return exp(-d * d);
}
Then a vertex driven by a bell-shaped response to au_blink:
bell(au_blink, 0.5, 0.2) * -0.008
The peak hits at slider = 0.5 with width 0.2 — the vertex moves at maximum when the blink is half-fired and tapers off on either side.
Performance
The interpreted backend handles 10 000 vertex axes at 60 Hz comfortably on a single core. For 50 000+ axes or sub-millisecond per-frame budgets, the developer surface can opt into the native (LLVM JIT) backend which is 10–100× faster per evaluation.
If your rig is slow, the most common cause is redundant computation — the same sin(time) appearing in dozens of vertex formulas. Move the shared computation to a helper function or, where available, to a setup-time intermediate variable.
What if a formula breaks?
When a vertex formula fails to parse, type-check, or evaluate, the rig editor shows a diagnostic next to that vertex's slot. The vertex is held at its last valid offset (or zero if it never had one) while you fix the formula.
See also
- Expression language reference — the full grammar, type system, and built-in catalogue.
- Construct expressions — same language driving Construct scene parameters.
- Spreadsheet how-to — prototype formulas with instant feedback.