123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <title>Multiple animated objects</title>
- <meta charset="utf-8">
- <meta content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" name="viewport">
- <style>
- body {
- overflow: hidden;
- }
- </style>
- </head>
- <body>
- <div id="container"></div>
- <div id="info"
- style="position: absolute; left: 0; top: 0; width: 100%; background-color: white; border: 1px solid black; margin: 10px; padding: 10px;">
- This demo shows how to load several instances of the same 3D model (same .GLTF file) into the
- scene, position them at different locations and launch different animations for them.
- To do it, some tricky cloning of SkinnedMesh, Skeleton and Bone objects is necessary (done by SkeletonUtils.clone().
- Soldier model from <a href="https://www.mixamo.com" target="_blank" rel="noopener">https://www.mixamo.com</a>.
- </div>
- <script src="../build/three.js"></script>
- <script src="js/WebGL.js"></script>
- <script src="js/loaders/GLTFLoader.js"></script>
- <script src="js/utils/SkeletonUtils.js"></script>
- <script>
- if (WEBGL.isWebGLAvailable() === false) {
- document.body.appendChild(WEBGL.getWebGLErrorMessage());
- }
- //////////////////////////////
- // Global objects
- //////////////////////////////
- let worldScene = null; // THREE.Scene where it all will be rendered
- let renderer = null;
- let camera = null;
- let mixers = []; // All the AnimationMixer objects for all the animations in the scene
- //////////////////////////////
- //////////////////////////////
- // Information about our 3D models and units
- //////////////////////////////
- // The names of the 3D models to load. One-per file.
- // A model may have multiple SkinnedMesh objects as well as several rigs (armatures). Units will define which
- // meshes, armatures and animations to use. We will load the whole scene for each object and clone it for each unit.
- // Models are from https://www.mixamo.com/
- const MODELS = [
- {name: "Soldier"},
- {name: "Parrot"},
- // {name: "RiflePunch"},
- ];
- // Here we define instances of the models that we want to place in the scene, their position, scale and the animations
- // that must be played.
- const UNITS = [
- {
- modelName: "Soldier", // Will use the 3D model from file models/gltf/Soldier.glb
- meshName: "vanguard_Mesh", // Name of the main mesh to animate
- position: {x: 0, y: 0, z: 0}, // Where to put the unit in the scene
- scale: 1, // Scaling of the unit. 1.0 means: use original size, 0.1 means "10 times smaller", etc.
- animationName: "Idle" // Name of animation to run
- },
- {
- modelName: "Soldier",
- meshName: "vanguard_Mesh",
- position: {x: 3, y: 0, z: 0},
- scale: 2,
- animationName: "Walk"
- },
- {
- modelName: "Soldier",
- meshName: "vanguard_Mesh",
- position: {x: 1, y: 0, z: 0},
- scale: 1,
- animationName: "Run"
- },
- {
- modelName: "Parrot",
- meshName: "mesh_0",
- position: {x: -4, y: 0, z: 0},
- rotation: {x: 0, y: Math.PI, z: 0},
- scale: 0.01,
- animationName: "parrot_A_"
- },
- {
- modelName: "Parrot",
- meshName: "mesh_0",
- position: {x: -2, y: 0, z: 0},
- rotation: {x: 0, y: Math.PI / 2, z: 0},
- scale: 0.02,
- animationName: null
- },
- ];
- //////////////////////////////
- // The main setup happens here
- //////////////////////////////
- let numLoadedModels = 0;
- initScene();
- initRenderer();
- loadModels();
- animate();
- //////////////////////////////
- //////////////////////////////
- // Function implementations
- //////////////////////////////
- /**
- * Function that starts loading process for the next model in the queue. The loading process is
- * asynchronous: it happens "in the background". Therefore we don't load all the models at once. We load one,
- * wait until it is done, then load the next one. When all models are loaded, we call loadUnits().
- */
- function loadModels() {
- for (let i = 0; i < MODELS.length; ++i) {
- const m = MODELS[i];
- loadGltfModel(m, function (model) {
- console.log("Done loading model", MODELS[i].name);
- ++numLoadedModels;
- if (numLoadedModels === MODELS.length) {
- console.log("All models loaded, time to instantiate units...");
- instantiateUnits();
- }
- });
- }
- }
- /**
- * Look at UNITS configuration, clone necessary 3D model scenes, place the armatures and meshes in the scene and
- * launch necessary animations
- */
- function instantiateUnits() {
- let numSuccess = 0;
- for (let i = 0; i < UNITS.length; ++i) {
- const u = UNITS[i];
- const model = getModelByName(u.modelName);
- if (model) {
- const clonedScene = THREE.SkeletonUtils.clone(model.scene);
- if (clonedScene) {
- // Scene is cloned properly, let's find one mesh and launch animation for it
- const clonedMesh = clonedScene.getObjectByName(u.meshName);
- if (clonedMesh) {
- const mixer = startAnimation(clonedMesh, model.animations, u.animationName);
- if (mixer) {
- // Save the animation mixer in the list, will need it in the animation loop
- mixers.push(mixer);
- numSuccess++;
- }
- }
- // Different models can have different configurations of armatures and meshes. Therefore,
- // We can't set position, scale or rotation to individual mesh objects. Instead we set
- // it to the whole cloned scene and then add the whole scene to the game world
- // Note: this may have weird effects if you have lights or other items in the GLTF file's scene!
- worldScene.add(clonedScene);
- if (u.position) {
- clonedScene.position.set(u.position.x, u.position.y, u.position.z);
- }
- if (u.scale) {
- clonedScene.scale.set(u.scale, u.scale, u.scale);
- }
- if (u.rotation) {
- clonedScene.rotation.x = u.rotation.x;
- clonedScene.rotation.y = u.rotation.y;
- clonedScene.rotation.z = u.rotation.z;
- }
- }
- } else {
- console.error("Can not find model", u.modelName);
- }
- }
- console.log(`Successfully instantiated ${numSuccess} units`);
- }
- /**
- * Start animation for a specific mesh object. Find the animation by name in the 3D model's animation array
- * @param skinnedMesh {THREE.SkinnedMesh} The mesh to animate
- * @param animations {Array} Array containing all the animations for this model
- * @param animationName {string} Name of the animation to launch
- * @return {THREE.AnimationMixer} Mixer to be used in the render loop
- */
- function startAnimation(skinnedMesh, animations, animationName) {
- let mixer = new THREE.AnimationMixer(skinnedMesh);
- const clip = THREE.AnimationClip.findByName(animations, animationName);
- if (clip) {
- const action = mixer.clipAction(clip);
- action.play();
- }
- return mixer;
- }
- /**
- * Find a model object by name
- * @param name
- * @returns {object|null}
- */
- function getModelByName(name) {
- for (let i = 0; i < MODELS.length; ++i) {
- if (MODELS[i].name === name) {
- return MODELS[i];
- }
- }
- return null;
- }
- /**
- * Load a 3D model from a GLTF file. Use the GLTFLoader.
- * @param model {object} Model config, one item from the MODELS array. It will be updated inside the function!
- * @param onLoaded {function} A callback function that will be called when the model is loaded
- */
- function loadGltfModel(model, onLoaded) {
- const loader = new THREE.GLTFLoader();
- const modelName = "models/gltf/" + model.name + ".glb";
- loader.load(modelName, function (gltf) {
- const scene = gltf.scene;
- model.animations = gltf.animations;
- model.scene = scene;
- // Enable Shadows
- gltf.scene.traverse(function (object) {
- if (object.isMesh) {
- object.castShadow = true;
- }
- });
- onLoaded(model);
- });
- }
- /**
- * Render loop. Renders the next frame of all animations
- */
- function animate() {
- requestAnimationFrame(animate);
- // Get the time elapsed since the last frame
- const mixerUpdateDelta = clock.getDelta();
- // Update all the animation frames
- for (let i = 0; i < mixers.length; ++i) {
- mixers[i].update(mixerUpdateDelta);
- }
- renderer.render(worldScene, camera);
- }
- //////////////////////////////
- // General Three.JS stuff
- //////////////////////////////
- // This part is not anyhow related to the cloning of models, it's just setting up the scene.
- /**
- * Initialize ThreeJS scene renderer
- */
- function initRenderer() {
- const container = document.getElementById('container');
- renderer = new THREE.WebGLRenderer({antialias: true});
- renderer.setPixelRatio(window.devicePixelRatio);
- renderer.setSize(window.innerWidth, window.innerHeight);
- renderer.gammaOutput = true;
- renderer.gammaFactor = 2.2;
- renderer.shadowMap.enabled = true;
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
- container.appendChild(renderer.domElement);
- }
- /**
- * Initialize ThreeJS Scene
- */
- function initScene() {
- camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
- camera.position.set(3, 6, -10);
- camera.lookAt(0, 1, 0);
- clock = new THREE.Clock();
- worldScene = new THREE.Scene();
- worldScene.background = new THREE.Color(0xa0a0a0);
- worldScene.fog = new THREE.Fog(0xa0a0a0, 10, 22);
- const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
- hemiLight.position.set(0, 20, 0);
- worldScene.add(hemiLight);
- const dirLight = new THREE.DirectionalLight(0xffffff);
- dirLight.position.set(-3, 10, -10);
- dirLight.castShadow = true;
- dirLight.shadow.camera.top = 10;
- dirLight.shadow.camera.bottom = -10;
- dirLight.shadow.camera.left = -10;
- dirLight.shadow.camera.right = 10;
- dirLight.shadow.camera.near = 0.1;
- dirLight.shadow.camera.far = 40;
- worldScene.add(dirLight);
- // ground
- const groundMesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(40, 40), new THREE.MeshPhongMaterial({
- color: 0x999999,
- depthWrite: false
- }));
- groundMesh.rotation.x = -Math.PI / 2;
- groundMesh.receiveShadow = true;
- worldScene.add(groundMesh);
- window.addEventListener('resize', onWindowResize, false);
- }
- /**
- * A callback that will be called whenever the browser window is resized.
- */
- function onWindowResize() {
- camera.aspect = window.innerWidth / window.innerHeight;
- camera.updateProjectionMatrix();
- renderer.setSize(window.innerWidth, window.innerHeight);
- }
- </script>
- </body>
- </html>
|