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.
TIP
Currently, the engine only supports Morph animation states built into the model, which need to be prepared in advance using modeling tools. Future versions will include the ability to manually create custom Morph objects in code.
Basic Usage
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:
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:
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:
- Define the basic state of the model:
open eyes
andclose mouth
; - Define the completely closed eye state:
anim_close_eye
; - Define the completely open mouth state:
anim_open_lip
; - Corresponding to the interpolation coefficient
eye_interpolation
of theopen / close
state of the eye -0
corresponds to the completely open eye,1
corresponds to the completely closed eye; Similarly, the difference coefficientlip_interpolation
of theopen / close
state of the mouth -0
corresponds to the completely closed,1
corresponds to the completely open; - By adjusting the two
interpolation
coefficient values in the code, you can mix the correspondingclosed eye
andopen mouth
dynamic effects.
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();