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 mouthInstructions
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 eyesandclose 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_interpolationof theopen / closestate of the eye -0corresponds to the completely open eye,1corresponds to the completely closed eye; Similarly, the difference coefficientlip_interpolationof theopen / closestate of the mouth -0corresponds to the completely closed,1corresponds to the completely open; - By adjusting the two
interpolationcoefficient values in the code, you can mix the correspondingclosed eyeandopen mouthdynamic 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();
