Skip to content

Morph动画

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

目前引擎只支持模型内置的 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 = 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()