Jelajahi Sumber

Add an example of multiple animated GLTF 3D objects in a single scene.

Girts 6 tahun lalu
induk
melakukan
f7a90d376c
2 mengubah file dengan 323 tambahan dan 0 penghapusan
  1. 1 0
      examples/files.js
  2. 322 0
      examples/webgl_animation_multiple.html

+ 1 - 0
examples/files.js

@@ -4,6 +4,7 @@ var files = {
 		"webgl_animation_keyframes",
 		"webgl_animation_keyframes",
 		"webgl_animation_skinning_blending",
 		"webgl_animation_skinning_blending",
 		"webgl_animation_skinning_morph",
 		"webgl_animation_skinning_morph",
+		"webgl_animation_multiple",
 		"webgl_camera",
 		"webgl_camera",
 		"webgl_camera_array",
 		"webgl_camera_array",
 		"webgl_camera_cinematic",
 		"webgl_camera_cinematic",

+ 322 - 0
examples/webgl_animation_multiple.html

@@ -0,0 +1,322 @@
+<!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>