Face Tracking: Face Studio Mini
A bare-face baseline plus four progressively-glammed looks: Glam Lashes,
Lip Statement, Full Glam, and Party Time. Each look is a
binary pattern over four face-tracking SceneObjects — Eye Color,
Lip Effect, Eyelash Effect, and a hat parented under the
Head Tracker — toggled at runtime via
SceneObject.setEnabledInHierarchy(...). No textures are swapped;
the entire cycle is visibility flipping.

What you'll build
- An Eye Color SceneObject with a
MeshRenderer+EyeColorcomponent — auto-tracked iris-colored overlay. - A Lip Effect SceneObject — a preconfigured
FaceMaskthat targets the lip region. Drop a custom lipstick texture on its Material to change the look. - An Eyelash Effect SceneObject — another
FaceMask, this one targeting the eyelash region. - A Head Tracker hierarchy — a
Face Bindingparent with aFaceBindingV2JScomponent anchored to FaceCenter, plus aHeadchild carrying the head occluder mesh. - A PartyHat sphere parented under the Head Tracker's Head. Because of the parent-child relationship, the hat follows the user's head pose every frame for free.
- A
TitleTextandStatusText2D HUD that mirrors the active look. - A
GameControllerempty SceneObject hostingFaceStudio— the script that subscribes to globalEventType.Touchand toggles the right SceneObjects per look.
Open the demo
Unzip and open in Effect House (5.9.0+). The opening scene contains:
- Face Mask Camera — auto-created by the first face-tracking object; routes the AR overlays to the correct render group.
- Face Binding / Head — the
Head Trackertwo-level hierarchy.Face Bindingcarries theFaceBindingV2JSanchor;Headcarries the occluder mesh. Anything you parent underHead(the hat) tracks head pose automatically. - Eye Color / Lip Effect / Eyelash Effect — the three face decoration SceneObjects, each a built-in object with its own tracking subsystem.
- PartyHat — a Sphere with
localScale = (0.4, 0.6, 0.4)positioned atlocalPosition = (0, 14, 0)relative to the Head occluder, riding above the user's head. - TitleText + StatusText — the 2D HUD.
- GameController — empty SceneObject hosting
FaceStudio.
Read the script
FaceStudio.ts
@component()
export class FaceStudio extends APJS.BasicScriptComponent {
// Each face decoration is a SceneObject — toggling its enabled-in-hierarchy
// flag is the cleanest way to swap face looks at runtime. We avoid touching
// individual EyeColor / FaceMask components because the SceneObject toggle
// ALSO disables the underlying tracking work, saving GPU.
@serializeProperty eyeColor!: APJS.SceneObject;
@serializeProperty lipEffect!: APJS.SceneObject;
@serializeProperty eyelashEffect!: APJS.SceneObject;
@serializeProperty partyHat!: APJS.SceneObject;
// The status label that names the active look.
@serializeProperty statusText!: APJS.SceneObject;
private statusComp!: APJS.Text;
private touchCallback!: (e: APJS.IEvent) => void;
private cycleIndex: number = -1;
private inited: boolean = false;
// Five looks. Bit-packed across the four decoration flags.
// 0 — Natural: nothing on. Useful as a clean before-and-after baseline.
// 1 — Glam Lashes: eyelashes only — the subtle change.
// 2 — Lip Statement: lipstick only.
// 3 — Full Glam: eye color + lashes + lips. The classic "makeup look".
// 4 — Party Time: full glam + a hat parented under the Head tracker.
private static readonly LOOKS: { name: string; eye: boolean; lip: boolean; lash: boolean; hat: boolean }[] = [
{ name: "Natural", eye: false, lip: false, lash: false, hat: false },
{ name: "Glam Lashes", eye: false, lip: false, lash: true, hat: false },
{ name: "Lip Statement", eye: false, lip: true, lash: false, hat: false },
{ name: "Full Glam", eye: true, lip: true, lash: true, hat: false },
{ name: "Party Time", eye: true, lip: true, lash: true, hat: true },
];
onUpdate(_dt: number): void {
if (this.inited) return;
if (!this.eyeColor || !this.lipEffect || !this.eyelashEffect || !this.partyHat) return;
if (!this.statusText) return;
this.statusComp = this.statusText.getComponent("Text") as APJS.Text;
this.touchCallback = (event: APJS.IEvent) => {
const t = event.args[0] as APJS.TouchData;
if (t.phase !== APJS.TouchPhase.Began) return;
this.advance();
};
APJS.EventManager.getGlobalEmitter()
.on(APJS.EventType.Touch, this.touchCallback, this);
this.advance(); // start at index 0 (Natural)
this.inited = true;
console.log("[FaceStudio] ready — " + FaceStudio.LOOKS.length + " looks wired");
}
onDestroy(): void {
if (this.touchCallback) {
APJS.EventManager.getGlobalEmitter()
.off(APJS.EventType.Touch, this.touchCallback, this);
}
}
private advance(): void {
this.cycleIndex = (this.cycleIndex + 1) % FaceStudio.LOOKS.length;
const look = FaceStudio.LOOKS[this.cycleIndex];
// setEnabledInHierarchy(false) disables the SceneObject AND every component
// it carries — the EyeColor / FaceMask tracking subsystems stop processing
// entirely. Flipping back to true re-engages them without setup cost.
this.eyeColor.setEnabledInHierarchy(look.eye);
this.lipEffect.setEnabledInHierarchy(look.lip);
this.eyelashEffect.setEnabledInHierarchy(look.lash);
this.partyHat.setEnabledInHierarchy(look.hat);
this.statusComp.text = (this.cycleIndex + 1) + " of " + FaceStudio.LOOKS.length + "\n" + look.name;
console.log("[FaceStudio] look " + look.name);
}
}
The Face-Tracking namespace calls of interest:
Eye Colorbuilt-in object — auto-creates aMeshRenderer+EyeColorcomponent. Self-contained: includes its own iris tracking, noFace Segmentationparent needed. Drop a custom texture or color on theEyeColorcomponent to change the look.Lip Effect/Eyelash Effectbuilt-in objects — each preconfigures aFaceMaskcomponent targeting the named region. Both auto-track facial landmarks; replace the texture on theMeshRenderermaterial to change the makeup look.Face Maskis the underlying component (also surfaced as a generic built-in object) — it supports aregionenum (Whole,Eyes,Eyelashes,Lips,Teeth) for arbitrary face overlays.Head Trackerbuilt-in object — creates a two-level hierarchy:Face Binding(parent, withFaceBindingV2JSanchored toFaceCenter) →Head(child, with the head occluder mesh). The Head bounds are roughly18 × 29 × 21world units. To make AR accessories follow the head, parent them underHead— they automatically inherit head pose.SceneObject.setEnabledInHierarchy(true|false)— the runtime toggle for whole face-decoration objects. It's the cheapest way to swap looks because it disables the underlying tracking work as well, not just the visible mesh.FaceBinding/FaceBindingV2— the binding component that anchors a SceneObject to a face landmark (FaceBindingAnchorType). Used internally byHead Tracker; expose it directly to attach 3D objects to specific landmarks likeLeftEye,Nose, orLips(see FaceBindingAnchorType).
Look table
| Index | Name | Eye Color | Lip Effect | Eyelash | Party Hat |
|---|---|---|---|---|---|
| 0 | Natural | off | off | off | off |
| 1 | Glam Lashes | off | off | on | off |
| 2 | Lip Statement | off | on | off | off |
| 3 | Full Glam | on | on | on | off |
| 4 | Party Time | on | on | on | on |
Each row is the same physical scene — only four
setEnabledInHierarchy calls differ.
Customize
On GameController → FaceStudio:
eyeColor/lipEffect/eyelashEffect— swap the wired SceneObjects. Any face-bound SceneObject works (a 3D Head Binding Mesh, a custom Face Mask with a specific region, etc.).partyHat— drop in any 3D object parented under theHead Tracker'sHeadchild. The script doesn't care what it is — it just toggles enabled state.LOOKS— astatic readonlyarray at the top of the script. Add rows like a "Subtle Glow" with only the eye color on, or a "Just the Hat" with all face decorations off.
In the editor:
Eye ColorMaterial — change the iris color or plug in a decorative iris texture (see the EyeColor reference for property details).Lip Effect/Eyelash EffectMaterial — replace the texture for a different lipstick / mascara look. Theregionfield onFaceMaskcontrols which part of the face the texture maps to.PartyHatTransform —localPosition.y = 14rides above the head occluder. Drop to9to perch a crown on the brow, push to20for a tall novelty hat.PartyHatMaterial — swap to anyStandard PBRlook. The demo uses a vivid orange emissive so the hat reads against any background.
Suggestions for further play:
- Add a
DyeHairSceneObject and toggle it as part of the cycle for a hair-color preset. Combine withDyeHairModeto choose between full-color replacement and tint blending. - Use
FaceMakeupfor a region-specific cosmetic layer (blush, contour) without using the genericFaceMask. TheFaceMakeupcomponent understands the cosmetic blending modes natively. - Bind a 3D accessory to a specific landmark via
FaceBinding+FaceBindingAnchorTypeinstead of parenting underHead Tracker— useful for sunglasses anchored to the bridge of the nose, or earrings on individual ear landmarks. - Read
Face106Interfaceto drive your own logic from raw face landmarks (the 106-point face mesh used internally byFaceMaskand the makeup components).
What you learned
This tutorial used:
- Built-in face-tracking objects —
Eye Color,Lip Effect,Eyelash Effect,Head Tracker. Each is a one-shotadd_builtin_objectthat brings the right components + hierarchy. SceneObject.setEnabledInHierarchy(true|false)— the runtime toggle. It disables the SceneObject and its tracking work, not just the visible mesh, so cycling looks doesn't waste GPU on hidden trackers.- The
Head Trackerhierarchy —Face Bindingparent +Headchild + your accessory parented underHead. The accessory follows head pose for free. @serializeProperty SceneObject× 5 — wired in the inspector, mapped to runtime references in the lazy-initonUpdate.- The look-table state pattern — a
static readonly LOOKS[]array indexed bycycleIndex, advanced on globalEventType.Touch. The same shape applies to lighting cycles, filter stacks, and material showrooms in the other tutorials.
Read the full EyeColor reference, the FaceMakeup reference, the FaceBinding reference, the FaceBindingAnchorType reference, the DyeHair reference, the Eyelashes3D reference, the Face106Interface reference, and the Face-Tracking namespace overview.
For the touch-event subscription pattern used here, see the Events & Input tutorial. For swapping the look cycle for a per-tap gesture variant, see GestureType.