Skip to content

Morph Animation

Using the system Time module, the engine computes the interpolation coefficient interpolation between the model vertex's base position basePosition and target position morphTargetPosition, continuously changing the object model's current vertex position position to achieve a continuous animation effect.

TIP

Currently the engine only supports the model's built-in Morph animation states. You need to prepare the corresponding model states in advance in your modeling tool. A future version will add the ability to manually create custom Morph objects in code.

Basic Usage

ts
import { Engine3D } from '@orillusion/core';
// Load a model that supports Morph states
let faceObject = await engine.res.loadGltf('gltfs/glb/face.glb');
scene.addChild(faceObject);

The engine automatically adds the MeshRenderer component to all nodes of the model for rendering display, and also adds the corresponding rendererMask for all nodes that support Morph animation. We can find all nodes that match MorphTarget by traversing all MeshRenderer nodes:

ts
function findMorphRenderers(obj: Object3D): MeshRenderer[] {
    let rendererList: MeshRenderer[] = [];
    // Traverse all nodes
    obj.forChild((child) => {
        let mr = child.getComponent(MeshRenderer)
        // Find nodes with both MeshRenderer and MorphTarget
        if(mr && mr.hasMask(RendererMask.MorphTarget))
            rendererList.push(mr)   
    })
    return rendererList;
}

let MorphRenders = findMorphRenderers(faceObject)

Controlling Interpolation

We can find the morph states corresponding to a node through the morphTargetDictionary property of the node's geometry, and then adjust the corresponding interpolation coefficient through setMorphInfluence to change the model state:

ts
console.log(renderer.geometry.morphTargetDictionary)
// {mouth:0} - completely closed mouth state
renderer.setMorphInfluence('mouth', 1); // Set to the completely open mouth state

Usage Notes

For morph animation, take facial expressions as an example, assuming that the parts involved in the facial animation are the eyes and lips. You need to prepare the corresponding model in advance, containing the morph animation states for the two parts eye and lip:

  1. Define the model's base state: eyes open and mouth closed;
  2. Define the completely closed-eye state: anim_close_eye;
  3. Define the completely open-mouth state: anim_open_lip;
  4. Map the eye open/closed state to the interpolation coefficient eye_interpolation - 0 corresponds to completely open eyes, 1 corresponds to completely closed eyes;
    similarly, map the lip open/closed state to the interpolation coefficient lip_interpolation - 0 corresponds to completely closed, 1 corresponds to completely open;
  5. By adjusting the interpolation coefficient values of the two in code, you can blend the corresponding eyes closed and mouth open dynamic effects.

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Camera3D, Engine3D, DirectLight, AtmosphericComponent, View3D, HoverCameraController, MeshRenderer, Object3D, RendererMask, Scene3D, Color, MorphTargetBlender } from '@orillusion/core';
import * as dat from 'dat.gui';

class Sample_morph {
    scene: Scene3D;
    hoverCameraController: HoverCameraController;
    engine: Engine3D;

    async run() {
        this.engine = await Engine3D.init();

        this.scene = new Scene3D();
        let cameraObj = new Object3D();
        cameraObj.name = `cameraObj`;
        let mainCamera = cameraObj.addComponent(Camera3D);
        this.scene.addChild(cameraObj);

        mainCamera.perspective(60, this.engine.aspect, 1, 5000.0);
        this.hoverCameraController = mainCamera.object3D.addComponent(HoverCameraController);
        this.hoverCameraController.setCamera(0, 0, 110);

        await this.initScene(this.scene);
        // set skybox
        this.scene.addComponent(AtmosphericComponent).sunY = 0.6;

        // create a view with target scene and camera
        let view = new View3D();
        view.scene = this.scene;
        view.camera = mainCamera;
        // start render
        this.engine.startRenderView(view);
    }

    private influenceData: { [key: string]: number } = {};
    private targetRenderers: { [key: string]: MeshRenderer } = {};

    async initScene(scene: Scene3D) {
        {
            let data = await this.engine.res.loadGltf('https://cdn.orillusion.com/gltfs/glb/lion.glb');
            data.addComponent(MorphTargetBlender);
            data.y = -80.0;
            data.x = -30.0;
            scene.addChild(data);

            const GUIHelp = new dat.GUI();
            GUIHelp.addFolder('morph controller');

            let meshRenders: MeshRenderer[] = this.fetchMorphRenderers(data);
            for (const renderer of meshRenders) {
                renderer.setMorphInfluenceIndex(0, 0);
                for (const key in renderer.geometry.morphTargetDictionary) {
                    this.influenceData[key] = 0;
                    this.targetRenderers[key] = renderer;
                    GUIHelp.add(this.influenceData, key, 0, 1, 0.01).onChange((v) => {
                        this.influenceData[key] = v;
                        this.track(this.influenceData, this.targetRenderers);
                    });
                }
            }
            GUIHelp.add(
                {
                    random: () => {
                        for (let i in this.influenceData) {
                            this.influenceData[i] = Math.random();
                        }
                        GUIHelp.updateDisplay();
                        this.track(this.influenceData, this.targetRenderers);
                    }
                },
                'random'
            );
        }
        {
            let ligthObj = new Object3D();
            ligthObj.rotationY = 135;
            ligthObj.rotationX = 45;
            let dl = ligthObj.addComponent(DirectLight);
            dl.lightColor = new Color(1.0, 0.95, 0.84, 1.0);
            scene.addChild(ligthObj);
            dl.intensity = 2;
        }
        return true;
    }

    /**
     * update morph data to mesh
     * @param data {leftEye:0, rightEye:0.5, ...}
     * @param targets {leftEye: MeshRenderer, rightEye: MeshRenderer, ...}
     * @returns
     */
    private track(data: { [key: string]: number }, targets: { [key: string]: MeshRenderer }): void {
        for (let key in targets) {
            let renderer = targets[key];
            let value = data[key];
            renderer.setMorphInfluence(key, value);
        }
    }

    private fetchMorphRenderers(obj: Object3D): MeshRenderer[] {
        let rendererList: MeshRenderer[] = [];
        obj.forChild((child) => {
            let mr = child.getComponent(MeshRenderer);
            if (mr && mr.hasMask(RendererMask.MorphTarget)) rendererList.push(mr);
        });
        return rendererList;
    }
}
new Sample_morph().run();

Released under the MIT License