Skip to content

Morph动画

使用系统的 Time 模块计算模型顶点的基础位置 basePosition 和目标位置 morphTargetPosition 的插值系数 interpolation,持续改变物体模型的点前顶点位置 position,从而获得连续的动画效果。

TIP

目前引擎只支持模型内置的 Morph 动画状态,需要提前在建模工具中制作好对应的模型状态,后续版本将加入代码内手动创建自定义 Morph 对象。

基本使用

ts
import { Engine3D } from '@orillusion/core';
// 加载支持 Morph 状态模型
let faceObject = await Engine3D.res.loadGltf('gltfs/glb/face.glb');
scene.addChild(faceObject);

引擎会自动为模型的所有节点添加 MeshRenderer 组件用于渲染显示,同时也会为所有支持 Morph 动画的节点添加对应的 rendererMask。我们可以通过遍历所有 MeshRenderer 节点,找到所有符合 MorphTarget 的节点:

ts
function findMorphRenderers(obj: Object3D): MeshRenderer[] {
    let rendererList: MeshRenderer[] = [];
    // 遍历所有节点
    obj.forChild((child) => {
        let mr = child.getComponent(MeshRenderer)
        // 找到同时存在 MeshRenderer 和 MorphTarget 的节点
        if(mr && mr.hasMask(RendererMask.MorphTarget))
            rendererList.push(mr)   
    })
    return rendererList;
}

let MorphRenders = findMorphRenderers(faceObject)

控制插值

我们可以通过节点 geometrymorphTargetDictionary 属性查找到节点对应的 morph 状态,然后通过 setMorphInfluence 调节对应的插值系数来改变模型状态:

ts
console.log(renderer.geometry.morphTargetDictionary)
// {mouth:0} - 完全闭嘴状态
renderer.setMorphInfluence('mouth', 1); // 设置完全张嘴状态

使用说明

morph 动画,以人脸表情为例,假定参与脸部动画部分为 眼睛嘴唇。 需要提前制作好对应模型,包含 eyelip 两块内容的 morph 动画状态:

  1. 定义模型基础状态:睁眼闭嘴
  2. 定义完全闭眼状态:anim_close_eye
  3. 定义完全张嘴状态:anim_open_lip
  4. 将眼睛 开/闭 状态对应插值系数 eye_interpolation - 0 对应完全睁眼,1 对应完全闭眼;
    同理将嘴唇 开/闭 状态对应差值系数 lip_interpolation - 0 对应完全闭合,1 对应完全张开;
  5. 在代码中通过调节两者 interpolation 系数数值,即可以混合对应 闭眼张嘴 动态效果。

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 = 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();