Playing Animation's from GLB file to VRM file

I new to Animation and Three js .

I have 2 file :

  1. VRM file(having mesh,texture,lighting ,skeletons etc) character file.
  2. GLB file having (animation but no mesh) exported from unreal.

What i am trying to achieve is play animation of GLB file on VRM file . (this way user can swap character and can able to play same animation on different character’s).

Here is my minimal code to achieve this.
So what’s the issue ?
animation is not playing properly i.e there is issue with look and animations it doesn’t look as intended.

Here is code

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
var camera, controls, scene, renderer;
let mixer, model;
const clock = new THREE.Clock(); // For animation timing
let currentVrm = undefined;
let currentMixer = undefined;
let currentMixer2 = undefined;
let mesh1, mesh2, clipp;
let animation_demo;

init();
animate();
function init() {
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  //renderer.setPixelRatio(window.devicePixelRatio);
  document.body.appendChild(renderer.domElement);
  console.log(" Info:", renderer.info.memry);

  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x3232323);

  const ambientLight = new THREE.AmbientLight(0x000000, 0.2); // Soft white light
  scene.add(ambientLight);

  var directionalLight = new THREE.DirectionalLight(0xffffff);
  directionalLight.position.set(0, 1, -2);
  scene.add(directionalLight);

  const cloudTexture = new THREE.TextureLoader().load("images.jpeg");
  const material = new THREE.MeshBasicMaterial({
    map: cloudTexture,
    side: THREE.DoubleSide,
  });
  const sphereGeometry = new THREE.SphereGeometry(50, 3, 3);
  sphereGeometry.scale(-1, 1, 1); // Invert the geometry

  // Apply the material to the sphere
  const sphere = new THREE.Mesh(sphereGeometry, material);
  // scene.add(sphere);

  const groundGeometry = new THREE.PlaneGeometry(10, 10); // Adjust size as needed
  const grassTexture = new THREE.TextureLoader().load(
    "istockphoto-1200569039-612x612.jpg"
  ); // Load grass texture
  grassTexture.wrapS = THREE.RepeatWrapping;
  grassTexture.wrapT = THREE.RepeatWrapping;
  grassTexture.repeat.set(10, 10); // Repeat texture on the ground
  const grassMaterial = new THREE.MeshStandardMaterial({ map: grassTexture });
  const groundMesh = new THREE.Mesh(groundGeometry, grassMaterial);
  groundMesh.rotation.x = -Math.PI / 2; // Rotate to make it horizontal
  // scene.add(groundMesh);

  camera = new THREE.PerspectiveCamera(
    50,
    window.innerWidth / window.innerHeight,
    0.01
  );
  camera.position.set(0, 1.5, -1.5);
  camera.position.x = camera.position.x;
  camera.position.y =
    (camera.position.y - camera.position.y - camera.position.y) / 2 + 1.5;
  // camera.lookAt(1000, 1000, 1000);

  var gridHelper = new THREE.GridHelper(10, 10);
  var axesHelper = new THREE.AxesHelper(5);
  scene.add(gridHelper);
  scene.add(axesHelper);

  controls = new OrbitControls(camera, renderer.domElement);
  controls.minDistance = 1; // Minimum distance camera can be from the center
  controls.maxDistance = 4; // Maximum distance camera can be from the center
  controls.maxPolarAngle = Math.PI / 2; // Limit camera rotation angle to avoid looking upside down
  controls.enablePan = false; // Disable panning
  controls.target.set(0, 0.75 * 1.5, 0);

  const vrmLoader = new GLTFLoader();

  function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  }

  // var vrmLoader = new VRMLoader();
  vrmLoader.register((parser) => {
    return new VRMLoaderPlugin(parser);
  });
  vrmLoader.crossOrigin = "anonymous";
  vrmLoader.crossOrigin = "anonymous";
  vrmLoader.load("testAnim.glb", function (gltf) {
    //mesh1 = gltf.scene.children[0];
    // gltf.scene.scale.set(101,101,101);
    mesh2 = gltf.scene;
    animation_demo = gltf.animations;
    vrmLoader.load(
      "Vroid_Character_1.vrm",
      (vrm) => {
        model = vrm.scene;
        const humanoid = vrm.userData.vrm;
        currentVrm = humanoid;

        scene.add(model);
        currentMixer = new THREE.AnimationMixer(mesh2);

        animation_demo.forEach((clip) => {
          // console.log("Clip");
          // console.log(clip.tracks);
          clip.tracks.forEach((track) => {
            const targetObject = track.name.includes("position") ?? null;
            // const targetObject2 = track.name.includes('scale') ?? null;
            // const targetObject3 = track.name.includes('quaternion') ?? null;
            // console.log(track.name.includes('quaternion'));
            const bindingProp = mesh2;
            if (targetObject) {
              const property = track.name.split(".").pop(); // Extract the property name (e.g., "position")
              const trackBinding = new THREE.PropertyMixer(
                bindingProp,
                property
              );
              currentMixer._bindings.push({
                track: track,
                binding: trackBinding,
              });
              currentMixer._bindingsByRootAndName[
                bindingProp.uuid + "." + property
              ] = trackBinding;
              bindingProp.position.set(...track.values); // Assuming track.values contains [x, y, z]
            }
            const clip2 = new THREE.AnimationClip("Animation", 1.0);

            currentMixer.clipAction(clip, model).play();
          });
        });
      },
      function (progress) {
        console.log(
          "Loading model...",
          100 * (progress.loaded / progress.total),
          "%"
        );
      },
      function (error) {
        console.error(error);
      }
    );
  });
}

function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();

  if (currentVrm) {
    currentVrm.update(delta);
  }

  if (currentMixer) {
    // console.log("currentMixer",currentMixer);
    currentMixer.update(delta);
  }

  controls.update();
  renderer.render(scene, camera);
}

Currently I want to know if

  1. I am in right direction or not ?
  2. There were no errors in console , but can’t able debug these animation’s issue.What i can’t do resolve issue. Guidance in right direction would be helpful.

Here is small demo what i output i am getting in browser
output

Here is my glb file that i am using

testAnim.glb (110.9 KB)

New models have to be rigged to the skeleton that you want to use… and that’s usually a manual process unless you use a service like mixamo.

Yeah hi @manthrax ,Yeah actually there are couples of things i am missing here. Firstly I tried to run animation on vrm directly, firstly as we need to check the skeleton of both VRM and GLB are same or not, as VRM(exported from Vroid) and GLB have same bone hierarchy ,next step that I actually i don’t have idea about is to play animation on normalised bones instead of Bone Nodes. My thought process may be not clear enough but actually i was able to same resolve issue anyhow.

1 Like