Morph Animation
Using the system Time module, the engine computes the interpolation coefficient interpolation between the model vertex's base position basePosition and target position morphTargetPosition, continuously changing the object model's current vertex position position to achieve a continuous animation effect.
TIP
Currently the engine only supports the model's built-in Morph animation states. You need to prepare the corresponding model states in advance in your modeling tool. A future version will add the ability to manually create custom Morph objects in code.
Basic Usage
import { Engine3D } from '@orillusion/core';
// Load a model that supports Morph states
let faceObject = await engine.res.loadGltf('gltfs/glb/face.glb');
scene.addChild(faceObject);The engine automatically adds the MeshRenderer component to all nodes of the model for rendering display, and also adds the corresponding rendererMask for all nodes that support Morph animation. We can find all nodes that match 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)Controlling Interpolation
We can find the morph states corresponding to a node through the morphTargetDictionary property of the node's 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 the completely open mouth stateUsage Notes
For morph animation, take facial expressions as an example, assuming that the parts involved in the facial animation are the eyes and lips. You need to prepare the corresponding model in advance, containing the morph animation states for the two parts eye and lip:
- Define the model's base state:
eyes openandmouth closed; - Define the completely closed-eye state:
anim_close_eye; - Define the completely open-mouth state:
anim_open_lip; - Map the eye
open/closedstate to the interpolation coefficienteye_interpolation-0corresponds to completely open eyes,1corresponds to completely closed eyes;
similarly, map the lipopen/closedstate to the interpolation coefficientlip_interpolation-0corresponds to completely closed,1corresponds to completely open; - By adjusting the
interpolationcoefficient values of the two in code, you can blend the correspondingeyes closedandmouth opendynamic effects.
import { Camera3D, Engine3D, DirectLight, AtmosphericComponent, View3D, HoverCameraController, MeshRenderer, Object3D, RendererMask, Scene3D, Color, MorphTargetBlender } from '@orillusion/core';
import * as dat from 'dat.gui';
class Sample_morph {
scene: Scene3D;
hoverCameraController: HoverCameraController;
engine: Engine3D;
async run() {
this.engine = 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, this.engine.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
this.engine.startRenderView(view);
}
private influenceData: { [key: string]: number } = {};
private targetRenderers: { [key: string]: MeshRenderer } = {};
async initScene(scene: Scene3D) {
{
let data = await this.engine.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();
