Fixing SkeletonUtils retarget() and retargetClip() functions

I’ve been digging into animation code. One thing I’m interested in is animation retargeting.

SkeletonUtils has two methods retarget() and retargetClip() that are for retarting a single pose or all poses in an animation respectively.

These functions appear to be not used in any examples, so what I’d like to do is make examples for them to show on three.js examples, but we need to iron out some issues:

I was not able to get good results retargeting between two different models. As a sanity check I decided to try retargeting between the same exact models, but the results were not as expected. You’d expect that if you retarget an animation from a model to another model loaded from the same GLTF file that the result would be the exact same animation.

Here’s an HTML file you can copy to local to play with, showing that Michelle’s animation retargeted to a second Michelle model does not work as expected (it should work perfectly because there is no difference in the two models, right?):

webgl_animation_retargeting.html
<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - animation - retargeting</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
		<style>
			body {
				background-color: #bfe3dd;
				color: #000;
			}

			a {
				color: #2983ff;
			}
		</style>
	</head>

	<body>

		<div id="container"></div>

		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - animation - keyframes<br/>
			Model: <a href="https://artstation.com/artwork/1AGwX" target="_blank" rel="noopener">Littlest Tokyo</a> by
			<a href="https://artstation.com/glenatron" target="_blank" rel="noopener">Glen Fox</a>, CC Attribution.
		</div>

		<!-- <script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script> -->
		<script type="importmap">
			{
				"imports": {
					"three": "https://unpkg.com/three@0.163.x/build/three.module.js",
					"three/addons/": "https://unpkg.com/three@0.163.x/examples/jsm/"
				}
			}
		</script>

		<script type="module">

			import * as THREE from 'three';

			import Stats from 'three/addons/libs/stats.module.js';

			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
			import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';

			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
			import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';

			import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';

			const clock = new THREE.Clock();
			const container = document.getElementById( 'container' );

			const stats = new Stats();
			container.appendChild( stats.dom );

			const renderer = new THREE.WebGLRenderer( { antialias: true } );
			renderer.setPixelRatio( window.devicePixelRatio );
			renderer.setSize( window.innerWidth, window.innerHeight );
			container.appendChild( renderer.domElement );

			const pmremGenerator = new THREE.PMREMGenerator( renderer );

			const scene = new THREE.Scene();
			scene.background = new THREE.Color( 0xbfe3dd );
			scene.environment = pmremGenerator.fromScene( new RoomEnvironment( renderer ), 0.04 ).texture;

			const camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 100 );
			camera.position.set( 5, 2, 8 );

			const controls = new OrbitControls( camera, renderer.domElement );
			controls.target.set( 0, 0.5, 0 );
			controls.update();
			controls.enablePan = true;
			controls.enableDamping = true;

			const [ sourceModel, targetModel, targetModel2 ] = await Promise.all([

				new Promise( ( resolve, reject ) => {

					// ours
					// new GLTFLoader().load( 'https://assets.codepen.io/191583/Catwalk.gltf', resolve, undefined, reject );

					// three's
					new GLTFLoader().load( 'https://threejs.org/examples/models/gltf/Michelle.glb', resolve, undefined, reject );

				} ),

				new Promise( ( resolve, reject ) => {

					// ours
					new GLTFLoader().load( 'https://assets.codepen.io/191583/pete.gltf', resolve, undefined, reject );

					// three's
					// new GLTFLoader().load( 'https://threejs.org/examples/models/gltf/Soldier.glb', resolve, undefined, reject );
					// new GLTFLoader().load( 'https://threejs.org/examples/models/gltf/Michelle.glb', resolve, undefined, reject );

				} ),

				// new Promise( ( resolve, reject ) => {

				// 	new GLTFLoader().load( 'models/gltf/Michelle.glb', resolve, undefined, reject );

				// } ),

			]);

			const playAnimations = true;

			let targetMixer;
			let sourceMixer;
			let sourceClip
			let sourceSkeletonHelper
			let sourceSkeleton
			handleSource(sourceModel)

			// const target2Mixer = handlePete(targetModel)
			const target2Mixer = handleMichelle(targetModel)
			// const target2Mixer = handleSoldier(targetModel)

			// handleManny()

			function handleSource(sourceModel) {

				console.log( 'source model:', sourceModel );

				sourceClip = sourceModel.animations[ 0 ]

				console.log( 'source clip:',  sourceClip);

				// sourceClip.tracks = sourceClip.tracks.filter(track => ['mixamorigHips', 'mixamorigSpine', 'mixamorigLeftArm'].some(name => track.name.startsWith(name)))

				sourceModel.scene.position.x -= 1.5;
				scene.add( sourceModel.scene );

				const sourceSkin = sourceModel.scene.children[ 0 ].children[ 0 ];

				sourceSkeletonHelper = new THREE.SkeletonHelper( sourceModel.scene );
				scene.add( sourceSkeletonHelper );

				sourceSkeleton = new THREE.Skeleton( sourceSkeletonHelper.bones );
				console.log( sourceSkeleton );

				sourceMixer = new THREE.AnimationMixer( sourceModel.scene );
				sourceMixer.clipAction( sourceModel.animations[ 0 ] ).play();

			}

			function handlePete(targetModel) {

				console.log( 'pete:', targetModel );

				// remove pete's helmet, shield, and sword
				targetModel.scene.remove( targetModel.scene.children[ 1 ] );
				targetModel.scene.remove( targetModel.scene.children[ 1 ] );
				targetModel.scene.remove( targetModel.scene.children[ 1 ] );

				targetModel.scene.position.x += 1.5;
				scene.add( targetModel.scene );

				const targetSkin = targetModel.scene.children[ 0 ].children[ 0 ];

				const targetSkelHelper = new THREE.SkeletonHelper( targetModel.scene );
				scene.add( targetSkelHelper );

				const retargetOptions = {

					// specify the name of the source's hip bone.
					hip: 'mixamorigHips',

					// Map of target's bone names to source's bone names
					names: {

						hips: 'mixamorigHips',

						spine: 'mixamorigSpine',
						chest: 'mixamorigSpine2',
						head: 'mixamorigHead',

						upperarml: 'mixamorigLeftArm',
						upperarmr: 'mixamorigRightArm',
						lowerarml: 'mixamorigLeftForeArm',
						lowerarmr: 'mixamorigRightForeArm',

						upperlegl: 'mixamorigLeftUpLeg',
						upperlegr: 'mixamorigRightUpLeg',
						lowerlegl: 'mixamorigLeftLeg',
						lowerlegr: 'mixamorigRightLeg',
						footl: 'mixamorigLeftFoot',
						footr: 'mixamorigRightFoot',

					}

				};

				// const newNames = {}
				// for (const name in retargetOptions.names) {
				// 	if (['mixamorigHips', 'mixamorigSpine', 'mixamorigLeftArm'].some(name => name.startsWith(name)))
				// 		newNames[name] = retargetOptions.names[name]
				// }
				// retargetOptions.names = newNames

				let mixer;

				if ( playAnimations ) {

					const retargetedClip = SkeletonUtils.retargetClip( targetSkin, sourceSkeleton, sourceClip, retargetOptions );
					console.log( 'retargeted clip', retargetedClip );

					// Apply the mixer directly to the SkinnedMesh, not any
					// ancestor node, because that's what
					// SkeletonUtils.retargetClip outputs the clip to be
					// compatible with.
					mixer = new THREE.AnimationMixer( targetSkin );
					mixer.clipAction( retargetedClip ).play();

				}

				return mixer;

			}

			function handleMichelle(targetModel) {

				console.log( 'michelle:', targetModel );

				targetModel.scene.position.x += 0;
				scene.add( targetModel.scene );

				const targetSkin = targetModel.scene.children[ 0 ].children[ 0 ];

				const retargetOptions = {

					// preservePosition: false,
					// preserveHipPosition: false,

					// specify the name of the target's hip bone.
					hip: 'mixamorigHips',

					// Map of target's bone names to source's bone names
					names: {

						mixamorigHips: 'mixamorigHips',

						mixamorigSpine: 'mixamorigSpine',
						mixamorigSpine1: 'mixamorigSpine1',
						mixamorigSpine2: 'mixamorigSpine2',
						mixamorigNeck: 'mixamorigNeck',
						mixamorigHead: 'mixamorigHead',
						mixamorigHeadTop_End: 'mixamorigHeadTop_End',

						mixamorigLeftShoulder: 'mixamorigLeftShoulder',
						mixamorigRightShoulder: 'mixamorigRightShoulder',
						mixamorigLeftArm: 'mixamorigLeftArm',
						mixamorigRightArm: 'mixamorigRightArm',
						mixamorigLeftForeArm: 'mixamorigLeftForeArm',
						mixamorigRightForeArm: 'mixamorigRightForeArm',
						mixamorigLeftHand: 'mixamorigLeftHand',
						mixamorigRightHand: 'mixamorigRightHand',

						mixamorigLeftUpLeg: 'mixamorigLeftUpLeg',
						mixamorigRightUpLeg: 'mixamorigRightUpLeg',
						mixamorigLeftLeg: 'mixamorigLeftLeg',
						mixamorigRightLeg: 'mixamorigRightLeg',
						mixamorigLeftFoot: 'mixamorigLeftFoot',
						mixamorigRightFoot: 'mixamorigRightFoot',
						mixamorigLeftToeBase: 'mixamorigLeftToeBase',
						mixamorigRightToeBase: 'mixamorigRightToeBase',
						mixamorigLeftToe_End: 'mixamorigLeftToe_End',
						mixamorigRightToe_End: 'mixamorigRightToe_End',

					}

				};

				// const newNames = {}
				// for (const name in retargetOptions.names) {
				// 	if (['mixamorigHips', 'mixamorigSpine', 'mixamorigLeftArm'].some(name => name.startsWith(name)))
				// 		newNames[name] = retargetOptions.names[name]
				// }
				// retargetOptions.names = newNames

				let mixer

				if ( playAnimations ) {

					const retargetedClip = SkeletonUtils.retargetClip( targetSkin, sourceSkeleton, sourceClip, retargetOptions );
					console.log( 'retargeted clip 2', retargetedClip );

					// Apply the mixer directly to the SkinnedMesh, not any
					// ancestor node, because that's what
					// SkeletonUtils.retargetClip outputs the clip to be
					// compatible with.
					mixer = new THREE.AnimationMixer( targetSkin );
					mixer.clipAction( retargetedClip ).play();

				}

				return mixer;

			}

			function handleSoldier(targetModel) {

				console.log( 'soldier:', targetModel );

				targetModel.scene.position.x += 0;
				scene.add( targetModel.scene );

				const targetSkin = targetModel.scene.children[ 0 ].children[ 0 ];
				console.log('skin:', targetSkin)

				const retargetOptions = {

					// specify the name of the target's hip bone.
					hip: 'mixamorigHips',

					// Map of target's bone names to source's bone names
					names: {

						mixamorigHips: 'mixamorigHips',

						mixamorigSpine: 'mixamorigSpine',
						mixamorigSpine2: 'mixamorigSpine2',
						// mixamorigHead: 'mixamorigHead',

						mixamorigLeftArm: 'mixamorigLeftArm',
						// mixamorigRightArm: 'mixamorigRightArm',
						// mixamorigLeftForeArm: 'mixamorigLeftForeArm',
						// mixamorigRightForeArm: 'mixamorigRightForeArm',

						// mixamorigLeftUpLeg: 'mixamorigLeftUpLeg',
						// mixamorigRightUpLeg: 'mixamorigRightUpLeg',
						// mixamorigLeftLeg: 'mixamorigLeftLeg',
						// mixamorigRightLeg: 'mixamorigRightLeg',
						// mixamorigLeftFoot: 'mixamorigLeftFoot',
						// mixamorigRightFoot: 'mixamorigRightFoot',

					}

				};

				let mixer

				if ( playAnimations ) {

					const retargetedClip = SkeletonUtils.retargetClip( targetSkin, sourceSkeleton, sourceClip, retargetOptions );
					console.log( 'retargeted clip 2', retargetedClip );

					// Apply the mixer directly to the SkinnedMesh, not any
					// ancestor node, because that's what
					// SkeletonUtils.retargetClip outputs the clip to be
					// compatible with.
					mixer = new THREE.AnimationMixer( targetSkin );
					mixer.clipAction( retargetedClip ).play();

				}

				return mixer;

			}


			const skelHelper = new THREE.SkeletonHelper( scene );
			scene.add( skelHelper );

			renderer.setAnimationLoop( animate );


			window.onresize = function () {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			};


			function animate() {

				const delta = clock.getDelta();

				if ( playAnimations ) {

					sourceMixer.update( delta );
					targetMixer?.update( delta );
					target2Mixer?.update( delta );

				}

				controls.update();

				stats.update();

				renderer.render( scene, camera );

			}


		</script>

	</body>

</html>

Here’s a codepen with the same example:

Why does the retarget from one model to the same exact model fail? We expect the retargeted animation to be identical right?

If we turn off the preservePosition and preserveHipPosition options that are passed to retargetClip (line 224), the orientation is better but the skin gets distorted and the whole model moves more than expected:

If we try the Pete model, with and without those two options, we get similar results as the last two examples with Michelle:

Other people are having related issues with this too:

This unofficial example by @shootTheLuck uses SkeletonUtils.retargetClip() and somehow has no issues. Why? What is different in that example compared to others? Is it because the retarget(Clip) function was design for something specific that has to do with BVH format? Do you have any idea @shootTheLuck?

This thread points to the old example that used to exist, showing how to retarget BVH (and! It was based on BVH, so I think that’s a clue, because any time I tried to apply BVH to a GLTF model it was similarly messed up):

And what I mean by “messed up” is this happens:

“Is it because the retarget(Clip) function was design for something specific that has to do with BVH format?”

I think you may be right. In r96, for instance, when there was a retargeting example, SkeletonUtils.js was attributed to @sunag and used to retarget BVH animations to SEA3D models.

Perhaps it works with bvh because the bvhLoader creates the animationAction and its tracks from “scratch” (no mixer.clipAction())?

I spent some time with your first example, but couldn’t make it work either.

If I may make one suggestion, it would be to avoid using a skeletonHelper for your retargeting here as it may be adding a layer of complexity that you don’t need. The sourceSkeleton can come directly from the sourceModel leaving the skeletonHelpers for the visuals. When you get this figured out, they will hopefully be unnecessary.

1 Like

Indeed, the helpers are unnecessary, and in fact a SkeletonHelper only works because the retargetClip function currently adds an unofficial .skeleton property to them, and then the underlying AnimationMixer is therefore tricked to believe that the SkeletonHelper is a SkinnedMesh (otherwise the mixer will not find any child nodes of the SkeletonHelper to animate). It’s definitely a hack.

I’m currently thinking of how to reorganize the retarget functions to not do this hack, and update the docs and type defs accordingly. But it won’t be of any use until we figure out what is up with the math.

@AlaricBaraou @mattrossman @Mugen87 @sunag you all have some experience with this already (especially you @sunag :)). Previous discussion:

  1. There seem to be transform issues that we need to account for (see above, plus the transform problems in #25751) besides the parameter types and helper.skeleton hack.

    1. Is there something specific about how the retarget functions work coupled to BVH format (coord system)? @sunag maybe you know?

    2. If I’m not mistaken, don’t we want to use updateWorldMatrix(true, true) instead of updateMatrixWorld() to ensure that all ancestor transforms that may affect skeleton/bones are captured?

    3. Is it better if the retageting is done in world space?

  2. Re parameters: some models have multiple SkinnedMeshes animated by a single skeleton, so it seems intuitive for a user to want to pass a root object (f.e. model.scene) that contains all meshes/bones/skeletons, and I think it’ll be nice DX to allow for that.

    • F.e. retargetClip(model1.scene, model2.scene, clip, options) would be a simple dev experience (the passed-in clip determines which subtree/skeleton/bones needs to be looked at based on track names).

    • retargetClip uses AnimationMixer( object ) so passing in root objects implies no need for the .skeleton hack.

    • Example of possible usage:

      new GLTFLoader().load(path, model => {
        const newClip = SkeletonUtils.retargetClip(
          model.scene,
          sourceModel.scene,
          sourceModel.animations[0],
          options
        )
      })
      

      Of course if the model has multiple sub-models and skeletons then a user can choose sub-nodes, but having to always choose sub-nodes (f.e. SkinnedMesh nodes) is more work in the cases when that would not be necessary.

  3. debuggability: I think it would be nice to add an axis helper option directly to SkeletonHelper so it can show all bone orientations.

I added optional AxesHelpers to SkeletonHelper in this PR: SkeletonHelper: add optional AxesHelpers for each bone by trusktr · Pull Request #28314 · mrdoob/three.js · GitHub

With that I see that the Michelle model has bones that point in different directions while in t-pose:

I see that BVH files have bones that all point in the same direction, for example here is pirouette.bvh without any animation (looks to be in t-pose):

Do we need to apply the rotation offsets for each bone?

Not sure if any help, but I managed to retarget some mixamo clips using the following. It was a while ago, but sharing in case it helps at all. This is for VRM characters encoded in GLB, so it uses some VRM specific data structures (extensions). It was also me hacking various approaches until it did something useful - not pretty code!!! There are things like the older VRM had characters standing backwards, but VRM1.0 had them right way around. But I would love to have a more standard and reusable way of doing this, so sharing in case any assistance to the efforts! (There are a few URLs in the code for sources that bits of the code were derived from as well.)

import * as THREE from 'three';
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
import { FacialAnimationClip } from './FacialAnimationClip';


// https://pixiv.github.io/three-vrm/packages/three-vrm/examples/humanoidAnimation/mixamoVRMRigMap.js
/**
 * A map from Mixamo rig name to VRM Humanoid bone name
 */
const mixamoVRMRigMap = {
  mixamorigHips: "hips",
  mixamorigSpine: "spine",
  mixamorigSpine1: "chest",
  mixamorigSpine2: "upperChest",
  mixamorigNeck: "neck",
  mixamorigHead: "head",
  mixamorigLeftShoulder: "leftShoulder",
  mixamorigLeftArm: "leftUpperArm",
  mixamorigLeftForeArm: "leftLowerArm",
  mixamorigLeftHand: "leftHand",
  mixamorigLeftHandThumb1: "leftThumbMetacarpal",
  mixamorigLeftHandThumb2: "leftThumbProximal",
  mixamorigLeftHandThumb3: "leftThumbDistal",
  mixamorigLeftHandIndex1: "leftIndexProximal",
  mixamorigLeftHandIndex2: "leftIndexIntermediate",
  mixamorigLeftHandIndex3: "leftIndexDistal",
  mixamorigLeftHandMiddle1: "leftMiddleProximal",
  mixamorigLeftHandMiddle2: "leftMiddleIntermediate",
  mixamorigLeftHandMiddle3: "leftMiddleDistal",
  mixamorigLeftHandRing1: "leftRingProximal",
  mixamorigLeftHandRing2: "leftRingIntermediate",
  mixamorigLeftHandRing3: "leftRingDistal",
  mixamorigLeftHandPinky1: "leftLittleProximal",
  mixamorigLeftHandPinky2: "leftLittleIntermediate",
  mixamorigLeftHandPinky3: "leftLittleDistal",
  mixamorigRightShoulder: "rightShoulder",
  mixamorigRightArm: "rightUpperArm",
  mixamorigRightForeArm: "rightLowerArm",
  mixamorigRightHand: "rightHand",
  mixamorigRightHandPinky1: "rightLittleProximal",
  mixamorigRightHandPinky2: "rightLittleIntermediate",
  mixamorigRightHandPinky3: "rightLittleDistal",
  mixamorigRightHandRing1: "rightRingProximal",
  mixamorigRightHandRing2: "rightRingIntermediate",
  mixamorigRightHandRing3: "rightRingDistal",
  mixamorigRightHandMiddle1: "rightMiddleProximal",
  mixamorigRightHandMiddle2: "rightMiddleIntermediate",
  mixamorigRightHandMiddle3: "rightMiddleDistal",
  mixamorigRightHandIndex1: "rightIndexProximal",
  mixamorigRightHandIndex2: "rightIndexIntermediate",
  mixamorigRightHandIndex3: "rightIndexDistal",
  mixamorigRightHandThumb1: "rightThumbMetacarpal",
  mixamorigRightHandThumb2: "rightThumbProximal",
  mixamorigRightHandThumb3: "rightThumbDistal",
  mixamorigLeftUpLeg: "leftUpperLeg",
  mixamorigLeftLeg: "leftLowerLeg",
  mixamorigLeftFoot: "leftFoot",
  mixamorigLeftToeBase: "leftToes",
  mixamorigRightUpLeg: "rightUpperLeg",
  mixamorigRightLeg: "rightLowerLeg",
  mixamorigRightFoot: "rightFoot",
  mixamorigRightToeBase: "rightToes",
};


export function convertFbxToVrmAnimation(animationGlb, characterGlb) {

  console.log("convertFbxToVrmAnimation", animationGlb, characterGlb);

  const clip = animationGlb.animations[0];
  if (clip instanceof FacialAnimationClip) {
    clip.setTargetCharacter(characterGlb);
    return clip.toAnimationClip("facial_animation");
  }
  const vrm = characterGlb.vrm;
  const asset = animationGlb.scene;

  const tracks = []; // KeyframeTracks compatible with VRM will be added here

  // TODO: At the moment, the following only supports mixamo clips.
  if (asset.getObjectByName("mixamorigHips")) {

    const restRotationInverse = new THREE.Quaternion();
    const parentRestWorldRotation = new THREE.Quaternion();
    const _quatA = new THREE.Quaternion();
    const _quatB = new THREE.Quaternion();
    const _vec3 = new THREE.Vector3();

    // Adjust with reference to hips height.
    const motionHipsHeight = asset.getObjectByName("mixamorigHips").position.y;
    const vrmHipsY = vrm.humanoid?.getNormalizedBoneNode("hips").getWorldPosition(_vec3).y;
    const vrmRootY = vrm.scene.getWorldPosition(_vec3).y;
    const vrmHipsHeight = Math.abs(vrmHipsY - vrmRootY);
    const hipsPositionScale = vrmHipsHeight / motionHipsHeight;

const n = asset.getObjectByName('mixamorigHips');
console.log("HIPS AT START", n.rotation, n.position);


    clip.tracks.forEach((track) => {

      // Convert each tracks for VRM use, and push to `tracks`
      const trackSplitted = track.name.split(".");
      const mixamoRigName = trackSplitted[0];
      const vrmBoneName = mixamoVRMRigMap[mixamoRigName];
      const vrmNormalizedNode = vrm.humanoid?.getNormalizedBoneNode(vrmBoneName);
      const vrmNormalizedNodeName = vrmNormalizedNode?.name;
      const vrmRawNode = vrm.humanoid?.getRawBoneNode(vrmBoneName);
      const vrmRawNodeName = vrmRawNode?.name;
      const mixamoRigNode = asset.getObjectByName(mixamoRigName);

      if (vrmNormalizedNodeName != null) {
        const propertyName = trackSplitted[1];

        // Store rotations of rest-pose.
        mixamoRigNode.getWorldQuaternion(restRotationInverse).invert();
        mixamoRigNode.parent.getWorldQuaternion(parentRestWorldRotation);

        if (track instanceof THREE.QuaternionKeyframeTrack) {

          const newTrackValues = new Float32Array(track.values.length);

          // Retarget rotation of mixamoRig to NormalizedBone.
          for (let i = 0; i < track.values.length; i += 4) {

            _quatA.fromArray(track.values, i);

            // World rotation when resting of parent * Track rotation * Inverse of world rotation when resting
            _quatA
              .premultiply(parentRestWorldRotation)
              .multiply(restRotationInverse);

            // Apply normal bone rotation, then copy the raw bone rotation.
            // See update() in https://github.com/pixiv/three-vrm/blob/15bf45f3399b6204db42601786c0612f13e985e3/packages/three-vrm-core/src/humanoid/VRMHumanoidRig.ts
            // Get VRMHumanoidRig
            const hr = vrm.humanoid._normalizedHumanBones;
            const parentWorldRotation = hr._parentWorldRotations[vrmBoneName];
            const invParentWorldRotation = _quatB.copy(parentWorldRotation).invert();
            const boneRotation = hr._boneRotations[vrmBoneName];

            _quatA
              .multiply(parentWorldRotation)
              .premultiply(invParentWorldRotation)
              .multiply(boneRotation);

            //// Move the mass center of the VRM
            //if (vrmBoneName === 'hips') {
            //  const _boneWorldPos = new THREE.Vector3();
            //  const boneWorldPosition = vrmNormalizedNode.getWorldPosition(_boneWorldPos);
            //  vrmRawNode.parent.updateWorldMatrix(true, false);
            //  const parentWorldMatrix = vrmRawNode.parent.matrixWorld;
            //  const localPosition = boneWorldPosition.applyMatrix4(parentWorldMatrix.invert());
            //  vrmRawNode.position.copy(localPosition);
            //}

            // Copy into the new array (to not mess up the original).
            _quatA.toArray(newTrackValues, i);
          }

          tracks.push(
            new THREE.QuaternionKeyframeTrack(
              `${vrmRawNodeName}.${propertyName}`,
              track.times,
              newTrackValues.map((v, i) =>
                vrm.meta?.metaVersion === "0" && i % 2 === 0 ? -v : v
              )
            )
          );

        } else if (track instanceof THREE.VectorKeyframeTrack) {

          const value = track.values.map(
            (v, i) =>
              (vrm.meta?.metaVersion === "0" && i % 3 !== 1 ? -v : v) *
              hipsPositionScale
          );
          tracks.push(
            new THREE.VectorKeyframeTrack(
              `${vrmRawNodeName}.${propertyName}`,
              track.times,
              value
            )
          );
        }
      }
    });
console.log("HIPS AT END", n.rotation, n.position);
  }

  //console.log(tracks)

  const newClip = new THREE.AnimationClip("vrmAnimation", clip.duration, tracks);
  newClip.retargetedFor = characterGlb;
  console.log("Final retargeted clip", newClip);
  return newClip;
}
1 Like

The retarget() is not being able to deal with the bind matrices of glTF, I made some modifications for testing, it seems to work but I can’t apply offset settings, consequently I have this persistent problem on the leg, and on the foot. I’ll have to check this more carefully and get back here.

1 Like

@trusktr Could I use your example to create a PR with these changes?

retarget is performed by converting the source and target bones to global space, after this step the rotation and position is extracted e converted to local space again, the position is preserved from the target if preservePosition is used.

I used bind matrices which would be the initial T-Pose values ​​to make the offset calculations in automatic adjustment with the hierarchy.

To avoid lengthening too much, I applied the offset to the bone’s local matrix, the only difference here is that I would have to apply it to the child bones as well.

image

2 Likes

Yeah, no problem at all, it will be great for the community.

What are “bind matrices”? Are they just the transforms in the higher up tree nodes above the skeleton and/or the model? Or something else?

@didi_softwares that’s unrelated, we’re talking about retargeting animations to models with different skeleton shapes (different number of bones, different rest poses, different rotations, different lengths, etc).