Animating multiple objects simultaneously

I have exported 8 animated objects from blender as ‘.glb’ files into my three.js code, but when I’m loading them into a website, only the first is animated. Why is that?
Here’s my whole code, because maybe I’m missing something out of tiredness:

import "./style.css";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

// setup

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector("#bg"),
});

renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
camera.position.setZ(30);

renderer.outputColorSpace = THREE.SRGBColorSpace;

// setting controls

const controls = new OrbitControls(camera, renderer.domElement);

// light

const pointLight = new THREE.PointLight(0xffffff);
pointLight.position.set(5, 5, 5);
const ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(pointLight, ambientLight);

// helpers

const lightHelper = new THREE.PointLightHelper(pointLight);
const gridHelper = new THREE.GridHelper(200, 50);
scene.add(lightHelper, gridHelper);

// cubeMap texture images
const cubeTextureLoader = new THREE.CubeTextureLoader();
const cubeMap = cubeTextureLoader.load([
  "/images/px.png",
  "/images/nx.png",
  "/images/py.png",
  "/images/ny.png",
  "/images/pz.png",
  "/images/nz.png",
]);

//stars
Array(400)
  .fill()
  .forEach(() => {
    const geometry = new THREE.SphereGeometry(0.2, 24, 24);
    const material = new THREE.MeshStandardMaterial({ color: 0xffffff });
    material.envMap = cubeMap;
    material.envMapIntensity = 10;
    const star = new THREE.Mesh(geometry, material);
    const [x, y, z] = Array(3)
      .fill()
      .map(() => THREE.MathUtils.randFloatSpread(100));
    star.position.set(x, y, z);
    scene.add(star);
  });

// bg
const bg = new THREE.TextureLoader().load("/images/bg.png");
scene.background = bg;

// crystals
let materials = [
  new THREE.MeshStandardMaterial({
    color: 0xe7e2da,
    emissive: 0x000fe7,
    roughness: 0,
    metalness: 1,
    flatShading: true,
    vertexColors: true,
    fog: true,
  }),

  new THREE.MeshStandardMaterial({
    color: 0x1e00c4,
    emissive: 0x5700e7,
    roughness: 0,
    metalness: 1,
    flatShading: true,
    vertexColors: true,
    fog: true,
  }),

  new THREE.MeshStandardMaterial({
    color: 0xe7d29a,
    emissive: 0x0b0b0b,
    roughness: 0,
    metalness: 1,
    flatShading: true,
    vertexColors: true,
    fog: true,
  }),

  new THREE.MeshStandardMaterial({
    color: 0x77feff,
    emissive: 0x949494,
    roughness: 0,
    metalness: 1,
    flatShading: true,
    vertexColors: true,
    fog: true,
  }),

  new THREE.MeshStandardMaterial({
    color: 0x1a00e7,
    emissive: 0x000000,
    roughness: 0,
    metalness: 1,
    flatShading: true,
    vertexColors: true,
    fog: true,
  }),

  new THREE.MeshStandardMaterial({
    color: 0xb4895a,
    emissive: 0x684f31,
    roughness: 0,
    metalness: 1,
    flatShading: true,
    vertexColors: true,
    fog: true,
  }),

  new THREE.MeshStandardMaterial({
    color: 0xfffffd,
    emissive: 0x8a00e7,
    roughness: 0,
    metalness: 1,
    flatShading: true,
    vertexColors: true,
    fog: true,
  }),

  new THREE.MeshStandardMaterial({
    color: 0x9dc1e7,
    emissive: 0x001be7,
    roughness: 0,
    metalness: 1,
    flatShading: true,
    vertexColors: true,
    fog: true,
  }),
];

materials.forEach((element) => {
  element.envMap = cubeMap;
  element.envMapIntensity = 1;
});

let crystals = ["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8"];
let mixers = [];
let loader = new GLTFLoader();

crystals.forEach((element, index) => {
  let material = materials[index];
  loader.load(
    "/action/" + element + ".glb",
    function (gltf) {
      gltf.scene.traverse(function (node) {
        if (node.isMesh) {
          node.material = material;
        }
      });
      scene.add(gltf.scene);
      mixers[index] = new THREE.AnimationMixer(gltf.scene);
      let clips = gltf.animations;
      let clip = THREE.AnimationClip.findByName(clips, element + "-action");
      mixers[index].clipAction(clip).play();
    },
    undefined,
    function (error) {
      console.error(error);
    }
  );
});

// loop
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  mixers.forEach((mixer) => {
    mixer && mixer.update(clock.getDelta());
  });

  controls.update();

  renderer.render(scene, camera);
}

animate();

window.addEventListener("resize", () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

Thank you for your time!

  1. Rest.
  2. When faced with issue like this, reduce amount of duplicated / iterated elements to 2 (max. 3.) Then start console logging values of index, mixers, etc - anything that may be connected to the crystals being animated - then just compare logs and determine what doesn’t look right.
  3. When using older versions of three, you may need to set skinning = true on the overriding materials to make animations work (github.)
1 Like

Ok so I discovered that only the first animation is playing because it’s first in my code. When I put animation nr 2 ahead, then only nr 2 is moving etc. I’m still learning how to code, is there a way around this that I don’t know of?

Look at this section

use getDelta() once per loop, and then reuse it.

delta = clock.getDelta()
mixers.forEach((mixer) => {
    mixer && mixer.update(delta);
});

Example : Sharing Animation Clips - Three.js Tutorials

image

1 Like

In my experience, if you are loading Blender objects with animations, you need to also load the animations. Once you loaded and saved them you can activate as many of them as you want. Does each object only have a single animation?

Here is an extract from a file that loads a gltf/glb object and loads the animations by name:

function loadACMine(filename) {
	gltfLoader.load(filename, function (gltf) {	
		ACMine = gltf.scene;  // a gltf/glb file is really a collection
		/* Animations ------------------------------------------------------------- */
		// Propeller
		let clip = THREE.AnimationClip.findByName(gltf.animations, "propellerAction");
		mxProp = new THREE.AnimationMixer(ACMine);
		let actun = mxProp.clipAction(clip);
		actun.play();
		if (mxProp) mxProp.setTime(spnprp/anmfps);
		// Rudder
		mxRudr = new THREE.AnimationMixer(ACMine);
		clip = THREE.AnimationClip.findByName(gltf.animations, "rudderAction");
		actun = mxRudr.clipAction(clip);
		actun.play();
		if (mxRudr) mxRudr.setTime(rudder/anmfps);
		****
		// Add to Scene
		scene.add(ACMine);
	});
}

I can’t believe how easy the solution is! Thank you so much!

Thank you for your time and effort!

This is a bad design decision. A getter accessor should never modify the state of itself.
Too bad fixing it would introduce a huge bc-break. :frowning_face: