Skip to content

Commit a7caa14

Browse files
authored
Merge pull request #16006 from strazdinsg/dev
Add an example of multiple animated GLTF 3D objects in a single scene.
2 parents 94f9b21 + f7a90d3 commit a7caa14

File tree

2 files changed

+323
-0
lines changed

2 files changed

+323
-0
lines changed

examples/files.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var files = {
44
"webgl_animation_keyframes",
55
"webgl_animation_skinning_blending",
66
"webgl_animation_skinning_morph",
7+
"webgl_animation_multiple",
78
"webgl_camera",
89
"webgl_camera_array",
910
"webgl_camera_cinematic",
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>Multiple animated objects</title>
5+
<meta charset="utf-8">
6+
<meta content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" name="viewport">
7+
<style>
8+
body {
9+
overflow: hidden;
10+
}
11+
</style>
12+
</head>
13+
<body>
14+
<div id="container"></div>
15+
16+
<div id="info"
17+
style="position: absolute; left: 0; top: 0; width: 100%; background-color: white; border: 1px solid black; margin: 10px; padding: 10px;">
18+
This demo shows how to load several instances of the same 3D model (same .GLTF file) into the
19+
scene, position them at different locations and launch different animations for them.
20+
To do it, some tricky cloning of SkinnedMesh, Skeleton and Bone objects is necessary (done by SkeletonUtils.clone().
21+
Soldier model from <a href="https://www.mixamo.com" target="_blank" rel="noopener">https://www.mixamo.com</a>.
22+
</div>
23+
24+
<script src="../build/three.js"></script>
25+
<script src="js/WebGL.js"></script>
26+
<script src="js/loaders/GLTFLoader.js"></script>
27+
<script src="js/utils/SkeletonUtils.js"></script>
28+
29+
<script>
30+
31+
if (WEBGL.isWebGLAvailable() === false) {
32+
document.body.appendChild(WEBGL.getWebGLErrorMessage());
33+
}
34+
35+
//////////////////////////////
36+
// Global objects
37+
//////////////////////////////
38+
let worldScene = null; // THREE.Scene where it all will be rendered
39+
let renderer = null;
40+
let camera = null;
41+
let mixers = []; // All the AnimationMixer objects for all the animations in the scene
42+
//////////////////////////////
43+
44+
45+
//////////////////////////////
46+
// Information about our 3D models and units
47+
//////////////////////////////
48+
49+
// The names of the 3D models to load. One-per file.
50+
// A model may have multiple SkinnedMesh objects as well as several rigs (armatures). Units will define which
51+
// meshes, armatures and animations to use. We will load the whole scene for each object and clone it for each unit.
52+
// Models are from https://www.mixamo.com/
53+
const MODELS = [
54+
{name: "Soldier"},
55+
{name: "Parrot"},
56+
// {name: "RiflePunch"},
57+
];
58+
59+
// Here we define instances of the models that we want to place in the scene, their position, scale and the animations
60+
// that must be played.
61+
const UNITS = [
62+
{
63+
modelName: "Soldier", // Will use the 3D model from file models/gltf/Soldier.glb
64+
meshName: "vanguard_Mesh", // Name of the main mesh to animate
65+
position: {x: 0, y: 0, z: 0}, // Where to put the unit in the scene
66+
scale: 1, // Scaling of the unit. 1.0 means: use original size, 0.1 means "10 times smaller", etc.
67+
animationName: "Idle" // Name of animation to run
68+
},
69+
{
70+
modelName: "Soldier",
71+
meshName: "vanguard_Mesh",
72+
position: {x: 3, y: 0, z: 0},
73+
scale: 2,
74+
animationName: "Walk"
75+
},
76+
{
77+
modelName: "Soldier",
78+
meshName: "vanguard_Mesh",
79+
position: {x: 1, y: 0, z: 0},
80+
scale: 1,
81+
animationName: "Run"
82+
},
83+
{
84+
modelName: "Parrot",
85+
meshName: "mesh_0",
86+
position: {x: -4, y: 0, z: 0},
87+
rotation: {x: 0, y: Math.PI, z: 0},
88+
scale: 0.01,
89+
animationName: "parrot_A_"
90+
},
91+
{
92+
modelName: "Parrot",
93+
meshName: "mesh_0",
94+
position: {x: -2, y: 0, z: 0},
95+
rotation: {x: 0, y: Math.PI / 2, z: 0},
96+
scale: 0.02,
97+
animationName: null
98+
},
99+
];
100+
101+
//////////////////////////////
102+
// The main setup happens here
103+
//////////////////////////////
104+
let numLoadedModels = 0;
105+
initScene();
106+
initRenderer();
107+
loadModels();
108+
animate();
109+
//////////////////////////////
110+
111+
112+
//////////////////////////////
113+
// Function implementations
114+
//////////////////////////////
115+
/**
116+
* Function that starts loading process for the next model in the queue. The loading process is
117+
* asynchronous: it happens "in the background". Therefore we don't load all the models at once. We load one,
118+
* wait until it is done, then load the next one. When all models are loaded, we call loadUnits().
119+
*/
120+
function loadModels() {
121+
for (let i = 0; i < MODELS.length; ++i) {
122+
const m = MODELS[i];
123+
loadGltfModel(m, function (model) {
124+
console.log("Done loading model", MODELS[i].name);
125+
++numLoadedModels;
126+
if (numLoadedModels === MODELS.length) {
127+
console.log("All models loaded, time to instantiate units...");
128+
instantiateUnits();
129+
}
130+
});
131+
}
132+
}
133+
134+
/**
135+
* Look at UNITS configuration, clone necessary 3D model scenes, place the armatures and meshes in the scene and
136+
* launch necessary animations
137+
*/
138+
function instantiateUnits() {
139+
let numSuccess = 0;
140+
for (let i = 0; i < UNITS.length; ++i) {
141+
const u = UNITS[i];
142+
const model = getModelByName(u.modelName);
143+
if (model) {
144+
const clonedScene = THREE.SkeletonUtils.clone(model.scene);
145+
if (clonedScene) {
146+
// Scene is cloned properly, let's find one mesh and launch animation for it
147+
const clonedMesh = clonedScene.getObjectByName(u.meshName);
148+
if (clonedMesh) {
149+
const mixer = startAnimation(clonedMesh, model.animations, u.animationName);
150+
if (mixer) {
151+
// Save the animation mixer in the list, will need it in the animation loop
152+
mixers.push(mixer);
153+
numSuccess++;
154+
}
155+
}
156+
157+
// Different models can have different configurations of armatures and meshes. Therefore,
158+
// We can't set position, scale or rotation to individual mesh objects. Instead we set
159+
// it to the whole cloned scene and then add the whole scene to the game world
160+
// Note: this may have weird effects if you have lights or other items in the GLTF file's scene!
161+
worldScene.add(clonedScene);
162+
if (u.position) {
163+
clonedScene.position.set(u.position.x, u.position.y, u.position.z);
164+
}
165+
if (u.scale) {
166+
clonedScene.scale.set(u.scale, u.scale, u.scale);
167+
}
168+
if (u.rotation) {
169+
clonedScene.rotation.x = u.rotation.x;
170+
clonedScene.rotation.y = u.rotation.y;
171+
clonedScene.rotation.z = u.rotation.z;
172+
}
173+
}
174+
} else {
175+
console.error("Can not find model", u.modelName);
176+
}
177+
}
178+
179+
console.log(`Successfully instantiated ${numSuccess} units`);
180+
}
181+
182+
/**
183+
* Start animation for a specific mesh object. Find the animation by name in the 3D model's animation array
184+
* @param skinnedMesh {THREE.SkinnedMesh} The mesh to animate
185+
* @param animations {Array} Array containing all the animations for this model
186+
* @param animationName {string} Name of the animation to launch
187+
* @return {THREE.AnimationMixer} Mixer to be used in the render loop
188+
*/
189+
function startAnimation(skinnedMesh, animations, animationName) {
190+
let mixer = new THREE.AnimationMixer(skinnedMesh);
191+
const clip = THREE.AnimationClip.findByName(animations, animationName);
192+
if (clip) {
193+
const action = mixer.clipAction(clip);
194+
action.play();
195+
}
196+
return mixer;
197+
}
198+
199+
/**
200+
* Find a model object by name
201+
* @param name
202+
* @returns {object|null}
203+
*/
204+
function getModelByName(name) {
205+
for (let i = 0; i < MODELS.length; ++i) {
206+
if (MODELS[i].name === name) {
207+
return MODELS[i];
208+
}
209+
}
210+
return null;
211+
}
212+
213+
/**
214+
* Load a 3D model from a GLTF file. Use the GLTFLoader.
215+
* @param model {object} Model config, one item from the MODELS array. It will be updated inside the function!
216+
* @param onLoaded {function} A callback function that will be called when the model is loaded
217+
*/
218+
function loadGltfModel(model, onLoaded) {
219+
const loader = new THREE.GLTFLoader();
220+
const modelName = "models/gltf/" + model.name + ".glb";
221+
loader.load(modelName, function (gltf) {
222+
const scene = gltf.scene;
223+
model.animations = gltf.animations;
224+
model.scene = scene;
225+
// Enable Shadows
226+
gltf.scene.traverse(function (object) {
227+
if (object.isMesh) {
228+
object.castShadow = true;
229+
}
230+
});
231+
onLoaded(model);
232+
});
233+
}
234+
235+
/**
236+
* Render loop. Renders the next frame of all animations
237+
*/
238+
function animate() {
239+
requestAnimationFrame(animate);
240+
// Get the time elapsed since the last frame
241+
const mixerUpdateDelta = clock.getDelta();
242+
// Update all the animation frames
243+
for (let i = 0; i < mixers.length; ++i) {
244+
mixers[i].update(mixerUpdateDelta);
245+
}
246+
renderer.render(worldScene, camera);
247+
}
248+
249+
//////////////////////////////
250+
// General Three.JS stuff
251+
//////////////////////////////
252+
// This part is not anyhow related to the cloning of models, it's just setting up the scene.
253+
254+
/**
255+
* Initialize ThreeJS scene renderer
256+
*/
257+
function initRenderer() {
258+
const container = document.getElementById('container');
259+
renderer = new THREE.WebGLRenderer({antialias: true});
260+
renderer.setPixelRatio(window.devicePixelRatio);
261+
renderer.setSize(window.innerWidth, window.innerHeight);
262+
renderer.gammaOutput = true;
263+
renderer.gammaFactor = 2.2;
264+
renderer.shadowMap.enabled = true;
265+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
266+
container.appendChild(renderer.domElement);
267+
}
268+
269+
/**
270+
* Initialize ThreeJS Scene
271+
*/
272+
function initScene() {
273+
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
274+
camera.position.set(3, 6, -10);
275+
camera.lookAt(0, 1, 0);
276+
277+
clock = new THREE.Clock();
278+
279+
worldScene = new THREE.Scene();
280+
worldScene.background = new THREE.Color(0xa0a0a0);
281+
worldScene.fog = new THREE.Fog(0xa0a0a0, 10, 22);
282+
283+
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
284+
hemiLight.position.set(0, 20, 0);
285+
worldScene.add(hemiLight);
286+
287+
const dirLight = new THREE.DirectionalLight(0xffffff);
288+
dirLight.position.set(-3, 10, -10);
289+
dirLight.castShadow = true;
290+
dirLight.shadow.camera.top = 10;
291+
dirLight.shadow.camera.bottom = -10;
292+
dirLight.shadow.camera.left = -10;
293+
dirLight.shadow.camera.right = 10;
294+
dirLight.shadow.camera.near = 0.1;
295+
dirLight.shadow.camera.far = 40;
296+
worldScene.add(dirLight);
297+
298+
// ground
299+
const groundMesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(40, 40), new THREE.MeshPhongMaterial({
300+
color: 0x999999,
301+
depthWrite: false
302+
}));
303+
groundMesh.rotation.x = -Math.PI / 2;
304+
groundMesh.receiveShadow = true;
305+
worldScene.add(groundMesh);
306+
window.addEventListener('resize', onWindowResize, false);
307+
}
308+
309+
/**
310+
* A callback that will be called whenever the browser window is resized.
311+
*/
312+
function onWindowResize() {
313+
camera.aspect = window.innerWidth / window.innerHeight;
314+
camera.updateProjectionMatrix();
315+
renderer.setSize(window.innerWidth, window.innerHeight);
316+
}
317+
318+
319+
</script>
320+
321+
</body>
322+
</html>

0 commit comments

Comments
 (0)