webgl_animation_multiple.html 12 KB

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