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)
控制插值
我们可以通过节点 geometry
的 morphTargetDictionary 属性查找到节点对应的 morph
状态,然后通过 setMorphInfluence 调节对应的插值系数来改变模型状态:
ts
console.log(renderer.geometry.morphTargetDictionary)
// {mouth:0} - 完全闭嘴状态
renderer.setMorphInfluence('mouth', 1); // 设置完全张嘴状态
使用说明
morph
动画,以人脸表情为例,假定参与脸部动画部分为 眼睛
和 嘴唇
。 需要提前制作好对应模型,包含 eye
和lip
两块内容的 morph
动画状态:
- 定义模型基础状态:
睁眼
和闭嘴
; - 定义完全闭眼状态:
anim_close_eye
; - 定义完全张嘴状态:
anim_open_lip
; - 将眼睛
开/闭
状态对应插值系数eye_interpolation
-0
对应完全睁眼,1
对应完全闭眼;
同理将嘴唇开/闭
状态对应差值系数lip_interpolation
-0
对应完全闭合,1
对应完全张开; - 在代码中通过调节两者
interpolation
系数数值,即可以混合对应闭眼
和张嘴
动态效果。
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();