Skip to content

Morph Animation

Using the system's Time module to calculate the interpolation coefficient interpolation of the model vertex's basic position basePosition and target position morphTargetPosition, continuously change the object model's point front vertex position position to achieve a continuous animation effect.

Currently, the engine only supports the built-in Morph animation state of the model, which needs to be prepared in advance in the modeling tool to create the corresponding model state. The subsequent version will add the code to manually create a custom Morph object.

Basic Usage

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

The engine will automatically add the MeshRenderer component to all nodes of the model for rendering display, and will also add the corresponding rendererMask for all nodes that support Morph animation. We can find all nodes that meet the 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)

Control Interpolation

We can find the morph state corresponding to the node through the morphTargetDictionary property of the node 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 1, completely open the mouth

Instructions

Morph animation, for example, the face expression, assuming that the parts of the face animation are eyes and lips. You need to make the corresponding model in advance, including eye and lip two parts of the morph animation state:

  1. Define the basic state of the model: open eyes and close mouth;
  2. Define the completely closed eye state: anim_close_eye;
  3. Define the completely open mouth state: anim_open_lip;
  4. Corresponding to the interpolation coefficient eye_interpolation of the open / close state of the eye - 0 corresponds to the completely open eye, 1 corresponds to the completely closed eye; Similarly, the difference coefficient lip_interpolation of the open / close state of the mouth - 0 corresponds to the completely closed, 1 corresponds to the completely open;
  5. By adjusting the two interpolation coefficient values in the code, you can mix the corresponding closed eye and open mouth 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, webGPUContext, Color, MorphTargetBlender } from '@orillusion/core';
import * as dat from 'dat.gui';

class Sample_morph {
    scene: Scene3D;
    hoverCameraController: HoverCameraController;

    async run() {
        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, webGPUContext.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
        Engine3D.startRenderView(view);
    }

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

    async initScene(scene: Scene3D) {
        {
            let data = await Engine3D.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 = 15;
        }
        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