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);
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)
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); // 设置完全张嘴状态
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 = 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()
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()