Browse Source

MMDLoader: Improve Animation system for PMX (#21395)

Takahiro 4 years ago
parent
commit
9728bf414a

+ 6 - 1
docs/examples/en/animations/CCDIKSolver.html

@@ -92,7 +92,12 @@
 
 		<h3>[method:CCDIKSolver update]()</h3>
 		<p>
-		Update bones quaternion by solving CCD algorithm.
+		Update IK bones quaternion by solving CCD algorithm.
+		</p>
+
+		<h3>[method:CCDIKSolver updateOne]( [param:Object ikParam] )</h3>
+		<p>
+		Update an IK bone quaternion by solving CCD algorithm.
 		</p>
 
 		<h2>Source</h2>

+ 2 - 0
docs/examples/en/animations/MMDAnimationHelper.html

@@ -77,6 +77,8 @@
 			<li> [page:Boolean sync] - Whether animation durations of added objects are synched. Default is true.</li>
 			<li> [page:Number afterglow] - Default is 0.0.</li>
 			<li> [page:Boolean resetPhysicsOnLoop] - Default is true.</li>
+			<li> [page:Boolean pmxAnimation] - If it is set to true, the helper follows the complex and costly PMX animation system.
+			Try this option only if your PMX model animation doesn't work well. Default is false.</li>
 		</ul>
 		</p>
 		<p>

+ 5 - 0
docs/examples/zh/animations/CCDIKSolver.html

@@ -95,6 +95,11 @@
 		Update bones quaternion by solving CCD algorithm.
 		</p>
 
+		<h3>[method:CCDIKSolver updateOne]( [param:Object ikParam] )</h3>
+		<p>
+		Update an IK bone quaternion by solving CCD algorithm.
+		</p>
+
 		<h2>Source</h2>
 
 		<p>

+ 2 - 0
docs/examples/zh/animations/MMDAnimationHelper.html

@@ -77,6 +77,8 @@
 			<li> [page:Boolean sync] - Whether animation durations of added objects are synched. Default is true.</li>
 			<li> [page:Number afterglow] - Default is 0.0.</li>
 			<li> [page:Boolean resetPhysicsOnLoop] - Default is true.</li>
+			<li> [page:Boolean pmxAnimation] - If it is set to true, the helper follows the complex and costly PMX animation system.
+			Try this option only if your PMX model animation doesn't work well. Default is false.</li>
 		</ul>
 		</p>
 		<p>

+ 98 - 84
examples/js/animation/CCDIKSolver.js

@@ -38,12 +38,32 @@ THREE.CCDIKSolver = ( function () {
 		constructor: CCDIKSolver,
 
 		/**
-		 * Update IK bones.
+		 * Update all IK bones.
 		 *
-		 * @return {THREE.CCDIKSolver}
+		 * @return {CCDIKSolver}
 		 */
 		update: function () {
 
+			var iks = this.iks;
+
+			for ( var i = 0, il = iks.length; i < il; i ++ ) {
+
+				this.updateOne( iks[ i ] );
+
+			}
+
+			return this;
+
+		},
+
+		/**
+		 * Update one IK bone
+		 *
+		 * @param {Object} ik parameter
+		 * @return {THREE.CCDIKSolver}
+		 */
+		updateOne: function () {
+
 			var q = new THREE.Quaternion();
 			var targetPos = new THREE.Vector3();
 			var targetVec = new THREE.Vector3();
@@ -55,137 +75,131 @@ THREE.CCDIKSolver = ( function () {
 			var axis = new THREE.Vector3();
 			var vector = new THREE.Vector3();
 
-			return function update() {
+			return function update( ik ) {
 
 				var bones = this.mesh.skeleton.bones;
-				var iks = this.iks;
 
 				// for reference overhead reduction in loop
 				var math = Math;
 
-				for ( var i = 0, il = iks.length; i < il; i ++ ) {
-
-					var ik = iks[ i ];
-					var effector = bones[ ik.effector ];
-					var target = bones[ ik.target ];
-
-					// don't use getWorldPosition() here for the performance
-					// because it calls updateMatrixWorld( true ) inside.
-					targetPos.setFromMatrixPosition( target.matrixWorld );
-
-					var links = ik.links;
-					var iteration = ik.iteration !== undefined ? ik.iteration : 1;
-
-					for ( var j = 0; j < iteration; j ++ ) {
+				var effector = bones[ ik.effector ];
+				var target = bones[ ik.target ];
 
-						var rotated = false;
+				// don't use getWorldPosition() here for the performance
+				// because it calls updateMatrixWorld( true ) inside.
+				targetPos.setFromMatrixPosition( target.matrixWorld );
 
-						for ( var k = 0, kl = links.length; k < kl; k ++ ) {
+				var links = ik.links;
+				var iteration = ik.iteration !== undefined ? ik.iteration : 1;
 
-							var link = bones[ links[ k ].index ];
+				for ( var i = 0; i < iteration; i ++ ) {
 
-							// skip this link and following links.
-							// this skip is used for MMD performance optimization.
-							if ( links[ k ].enabled === false ) break;
+					var rotated = false;
 
-							var limitation = links[ k ].limitation;
-							var rotationMin = links[ k ].rotationMin;
-							var rotationMax = links[ k ].rotationMax;
+					for ( var j = 0, jl = links.length; j < jl; j ++ ) {
 
-							// don't use getWorldPosition/Quaternion() here for the performance
-							// because they call updateMatrixWorld( true ) inside.
-							link.matrixWorld.decompose( linkPos, invLinkQ, linkScale );
-							invLinkQ.invert();
-							effectorPos.setFromMatrixPosition( effector.matrixWorld );
+						var link = bones[ links[ j ].index ];
 
-							// work in link world
-							effectorVec.subVectors( effectorPos, linkPos );
-							effectorVec.applyQuaternion( invLinkQ );
-							effectorVec.normalize();
+						// skip this link and following links.
+						// this skip is used for MMD performance optimization.
+						if ( links[ j ].enabled === false ) break;
 
-							targetVec.subVectors( targetPos, linkPos );
-							targetVec.applyQuaternion( invLinkQ );
-							targetVec.normalize();
+						var limitation = links[ j ].limitation;
+						var rotationMin = links[ j ].rotationMin;
+						var rotationMax = links[ j ].rotationMax;
 
-							var angle = targetVec.dot( effectorVec );
+						// don't use getWorldPosition/Quaternion() here for the performance
+						// because they call updateMatrixWorld( true ) inside.
+						link.matrixWorld.decompose( linkPos, invLinkQ, linkScale );
+						invLinkQ.invert();
+						effectorPos.setFromMatrixPosition( effector.matrixWorld );
 
-							if ( angle > 1.0 ) {
+						// work in link world
+						effectorVec.subVectors( effectorPos, linkPos );
+						effectorVec.applyQuaternion( invLinkQ );
+						effectorVec.normalize();
 
-								angle = 1.0;
+						targetVec.subVectors( targetPos, linkPos );
+						targetVec.applyQuaternion( invLinkQ );
+						targetVec.normalize();
 
-							} else if ( angle < - 1.0 ) {
+						var angle = targetVec.dot( effectorVec );
 
-								angle = - 1.0;
+						if ( angle > 1.0 ) {
 
-							}
+							angle = 1.0;
 
-							angle = math.acos( angle );
+						} else if ( angle < - 1.0 ) {
 
-							// skip if changing angle is too small to prevent vibration of bone
-							// Refer to http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mmd.three.js
-							if ( angle < 1e-5 ) continue;
+							angle = - 1.0;
 
-							if ( ik.minAngle !== undefined && angle < ik.minAngle ) {
+						}
 
-								angle = ik.minAngle;
+						angle = math.acos( angle );
 
-							}
+						// skip if changing angle is too small to prevent vibration of bone
+						// Refer to http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mmd.three.js
+						if ( angle < 1e-5 ) continue;
 
-							if ( ik.maxAngle !== undefined && angle > ik.maxAngle ) {
+						if ( ik.minAngle !== undefined && angle < ik.minAngle ) {
 
-								angle = ik.maxAngle;
+							angle = ik.minAngle;
 
-							}
+						}
 
-							axis.crossVectors( effectorVec, targetVec );
-							axis.normalize();
+						if ( ik.maxAngle !== undefined && angle > ik.maxAngle ) {
 
-							q.setFromAxisAngle( axis, angle );
-							link.quaternion.multiply( q );
+							angle = ik.maxAngle;
 
-							// TODO: re-consider the limitation specification
-							if ( limitation !== undefined ) {
+						}
 
-								var c = link.quaternion.w;
+						axis.crossVectors( effectorVec, targetVec );
+						axis.normalize();
 
-								if ( c > 1.0 ) c = 1.0;
+						q.setFromAxisAngle( axis, angle );
+						link.quaternion.multiply( q );
 
-								var c2 = math.sqrt( 1 - c * c );
-								link.quaternion.set( limitation.x * c2,
-								                     limitation.y * c2,
-								                     limitation.z * c2,
-								                     c );
+						// TODO: re-consider the limitation specification
+						if ( limitation !== undefined ) {
 
-							}
+							var c = link.quaternion.w;
 
-							if ( rotationMin !== undefined ) {
+							if ( c > 1.0 ) c = 1.0;
 
-								link.rotation.setFromVector3(
-									link.rotation
-										.toVector3( vector )
-										.max( rotationMin ) );
+							var c2 = math.sqrt( 1 - c * c );
+							link.quaternion.set( limitation.x * c2,
+							                     limitation.y * c2,
+							                     limitation.z * c2,
+							                     c );
 
-							}
+						}
 
-							if ( rotationMax !== undefined ) {
+						if ( rotationMin !== undefined ) {
 
-								link.rotation.setFromVector3(
-									link.rotation
-										.toVector3( vector )
-										.min( rotationMax ) );
+							link.rotation.setFromVector3(
+								link.rotation
+									.toVector3( vector )
+									.max( rotationMin ) );
 
-							}
+						}
 
-							link.updateMatrixWorld( true );
+						if ( rotationMax !== undefined ) {
 
-							rotated = true;
+							link.rotation.setFromVector3(
+								link.rotation
+									.toVector3( vector )
+									.min( rotationMax ) );
 
 						}
 
-						if ( ! rotated ) break;
+						link.updateMatrixWorld( true );
+
+						rotated = true;
 
 					}
 
+					if ( ! rotated ) break;
+
 				}
 
 				return this;

+ 234 - 41
examples/js/animation/MMDAnimationHelper.js

@@ -40,7 +40,9 @@ THREE.MMDAnimationHelper = ( function () {
 			afterglow: params.afterglow !== undefined
 				? params.afterglow : 0.0,
 			resetPhysicsOnLoop: params.resetPhysicsOnLoop !== undefined
-				? params.resetPhysicsOnLoop : true
+				? params.resetPhysicsOnLoop : true,
+			pmxAnimation: params.pmxAnimation !== undefined
+				? params.pmxAnimation : false
 		};
 
 		this.enabled = {
@@ -217,15 +219,28 @@ THREE.MMDAnimationHelper = ( function () {
 
 			mesh.updateMatrixWorld( true );
 
-			if ( params.ik !== false ) {
+			// PMX animation system special path
+			if ( this.configuration.pmxAnimation && 
+				mesh.geometry.userData.MMD && mesh.geometry.userData.MMD.format === 'pmx' ) {
 
-				this._createCCDIKSolver( mesh ).update( params.saveOriginalBonesBeforeIK ); // this param is experimental
+				var sortedBonesData = this._sortBoneDataArray( mesh.geometry.userData.MMD.bones.slice() );
+				var ikSolver = params.ik !== false ? this._createCCDIKSolver( mesh ) : null;
+				var grantSolver = params.grant !== false ? this.createGrantSolver( mesh ) : null;
+				this._animatePMXMesh( mesh, sortedBonesData, ikSolver, grantSolver );
 
-			}
+			} else {
+
+				if ( params.ik !== false ) {
+
+					this._createCCDIKSolver( mesh ).update();
 
-			if ( params.grant !== false ) {
+				}
+
+				if ( params.grant !== false ) {
 
-				this.createGrantSolver( mesh ).update();
+					this.createGrantSolver( mesh ).update();
+
+				}
 
 			}
 
@@ -515,28 +530,45 @@ THREE.MMDAnimationHelper = ( function () {
 			var physics = objects.physics;
 			var looped = objects.looped;
 
-			// alternate solution to save/restore bones but less performant?
-			//mesh.pose();
-			//this._updatePropertyMixersBuffer( mesh );
-
 			if ( mixer && this.enabled.animation ) {
 
+				// alternate solution to save/restore bones but less performant?
+				//mesh.pose();
+				//this._updatePropertyMixersBuffer( mesh );
+
 				this._restoreBones( mesh );
 
 				mixer.update( delta );
 
 				this._saveBones( mesh );
 
-				if ( ikSolver && this.enabled.ik ) {
+				// PMX animation system special path
+				if ( this.configuration.pmxAnimation &&
+					mesh.geometry.userData.MMD && mesh.geometry.userData.MMD.format === 'pmx' ) {
 
-					mesh.updateMatrixWorld( true );
-					ikSolver.update();
+					if ( ! objects.sortedBonesData ) objects.sortedBonesData = this._sortBoneDataArray( mesh.geometry.userData.MMD.bones.slice() );
 
-				}
+					this._animatePMXMesh(
+						mesh,
+						objects.sortedBonesData,
+						ikSolver && this.enabled.ik ? ikSolver : null,
+						grantSolver && this.enabled.grant ? grantSolver : null
+					);
 
-				if ( grantSolver && this.enabled.grant ) {
+				} else {
 
-					grantSolver.update();
+					if ( ikSolver && this.enabled.ik ) {
+
+						mesh.updateMatrixWorld( true );
+						ikSolver.update();
+
+					}
+
+					if ( grantSolver && this.enabled.grant ) {
+
+						grantSolver.update();
+
+					}
 
 				}
 
@@ -559,6 +591,142 @@ THREE.MMDAnimationHelper = ( function () {
 
 		},
 
+		// Sort bones in order by 1. transformationClass and 2. bone index.
+		// In PMX animation system, bone transformations should be processed
+		// in this order.
+		_sortBoneDataArray: function ( boneDataArray ) {
+
+			return boneDataArray.sort( function ( a, b ) {
+
+				if ( a.transformationClass !== b.transformationClass ) {
+
+					return a.transformationClass - b.transformationClass;
+
+				} else {
+
+					return a.index - b.index;
+
+				}
+
+			} );
+
+		},
+
+		// PMX Animation system is a bit too complex and doesn't great match to
+		// Three.js Animation system. This method attempts to simulate it as much as
+		// possible but doesn't perfectly simulate.
+		// This method is more costly than the regular one so
+		// you are recommended to set constructor parameter "pmxAnimation: true"
+		// only if your PMX model animation doesn't work well.
+		// If you need better method you would be required to write your own.
+		_animatePMXMesh: function () {
+
+			// Keep working quaternions for less GC
+			var quaternions = [];
+			var quaternionIndex = 0;
+
+			function getQuaternion() {
+
+				if ( quaternionIndex >= quaternions.length ) {
+
+					quaternions.push( new THREE.Quaternion() );
+
+				}
+
+				return quaternions[ quaternionIndex ++ ];
+
+			}
+
+			// Save rotation whose grant and IK are already applied
+			// used by grant children
+			var grantResultMap = new Map();
+
+			function updateOne( mesh, boneIndex, ikSolver, grantSolver ) {
+
+				var bones = mesh.skeleton.bones;
+				var bonesData = mesh.geometry.userData.MMD.bones;
+				var boneData = bonesData[ boneIndex ];
+				var bone = bones[ boneIndex ];
+
+				// Return if already updated by being referred as a grant parent.
+				if ( grantResultMap.has( boneIndex ) ) return;
+
+				var quaternion = getQuaternion();
+
+				// Initialize grant result here to prevent infinite loop.
+				// If it's referred before updating with actual result later
+				// result without applyting IK or grant is gotten
+				// but better than composing of infinite loop.
+				grantResultMap.set( boneIndex, quaternion.copy( bone.quaternion ) );
+
+				// @TODO: Support global grant and grant position
+				if ( grantSolver && boneData.grant && 
+					! boneData.grant.isLocal && boneData.grant.affectRotation ) {
+
+					var parentIndex = boneData.grant.parentIndex;
+					var ratio = boneData.grant.ratio;
+
+					if ( ! grantResultMap.has( parentIndex ) ) {
+
+						updateOne( mesh, parentIndex, ikSolver, grantSolver );
+
+					}
+
+					grantSolver.addGrantRotation( bone, grantResultMap.get( parentIndex ), ratio );
+
+				}
+
+				if ( ikSolver && boneData.ik ) {
+
+					// @TODO: Updating world matrices every time solving an IK bone is
+					// costly. Optimize if possible. 
+					mesh.updateMatrixWorld( true );
+					ikSolver.updateOne( boneData.ik );
+
+					// No confident, but it seems the grant results with ik links should be updated?
+					var links = boneData.ik.links;
+
+					for ( var i = 0, il = links.length; i < il; i ++ ) {
+
+						var link = links[ i ];
+
+						if ( link.enabled === false ) continue;
+
+						var linkIndex = link.index;
+
+						if ( grantResultMap.has( linkIndex ) ) {
+
+							grantResultMap.set( linkIndex, grantResultMap.get( linkIndex ).copy( bones[ linkIndex ].quaternion ) );
+
+						}
+
+					}
+
+				}
+
+				// Update with the actual result here
+				quaternion.copy( bone.quaternion );
+
+			}
+
+			return function ( mesh, sortedBonesData, ikSolver, grantSolver ) {
+
+				quaternionIndex = 0;
+				grantResultMap.clear();
+
+				for ( var i = 0, il = sortedBonesData.length; i < il; i ++ ) {
+
+					updateOne( mesh, sortedBonesData[ i ].index, ikSolver, grantSolver );
+
+				}
+
+				mesh.updateMatrixWorld( true );
+				return this;
+
+			};
+
+		}(),
+
 		_animateCamera: function ( camera, delta ) {
 
 			var mixer = this.objects.get( camera ).mixer;
@@ -961,6 +1129,10 @@ THREE.MMDAnimationHelper = ( function () {
 	};
 
 	/**
+	 * Solver for Grant (Fuyo in Japanese. I just google translated because
+	 * Fuyo may be MMD specific term and may not be common word in 3D CG terms.)
+	 * Grant propagates a bone's transform to other bones transforms even if
+	 * they are not children.
 	 * @param {THREE.SkinnedMesh} mesh
 	 * @param {Array<Object>} grants
 	 */
@@ -976,54 +1148,75 @@ THREE.MMDAnimationHelper = ( function () {
 		constructor: GrantSolver,
 
 		/**
+		 * Solve all the grant bones
 		 * @return {GrantSolver}
 		 */
 		update: function () {
 
-			var quaternion = new THREE.Quaternion();
+			var grants = this.grants;
 
-			return function () {
+			for ( var i = 0, il = grants.length; i < il; i ++ ) {
 
-				var bones = this.mesh.skeleton.bones;
-				var grants = this.grants;
+				this.updateOne( grants[ i ] );
 
-				for ( var i = 0, il = grants.length; i < il; i ++ ) {
+			}
 
-					var grant = grants[ i ];
-					var bone = bones[ grant.index ];
-					var parentBone = bones[ grant.parentIndex ];
+			return this;
 
-					if ( grant.isLocal ) {
+		},
+
+		/**
+		 * Solve a grant bone
+		 * @param {Object} grant - grant parameter
+		 * @return {GrantSolver}
+		 */
+		updateOne: function ( grant ) {
 
-						// TODO: implement
-						if ( grant.affectPosition ) {
+			var bones = this.mesh.skeleton.bones;
+			var bone = bones[ grant.index ];
+			var parentBone = bones[ grant.parentIndex ];
 
-						}
+			if ( grant.isLocal ) {
 
-						// TODO: implement
-						if ( grant.affectRotation ) {
+				// TODO: implement
+				if ( grant.affectPosition ) {
 
-						}
+				}
 
-					} else {
+				// TODO: implement
+				if ( grant.affectRotation ) {
 
-						// TODO: implement
-						if ( grant.affectPosition ) {
+				}
 
-						}
+			} else {
 
-						if ( grant.affectRotation ) {
+				// TODO: implement
+				if ( grant.affectPosition ) {
 
-							quaternion.set( 0, 0, 0, 1 );
-							quaternion.slerp( parentBone.quaternion, grant.ratio );
-							bone.quaternion.multiply( quaternion );
+				}
 
-						}
+				if ( grant.affectRotation ) {
 
-					}
+					this.addGrantRotation( bone, parentBone.quaternion, grant.ratio );
 
 				}
 
+			}
+
+			return this;
+
+		},
+
+		addGrantRotation: function () {
+
+			var quaternion = new Quaternion();
+
+			return function ( bone, q, ratio ) {
+
+				quaternion.set( 0, 0, 0, 1 );
+				quaternion.slerp( q, ratio );
+				bone.quaternion.multiply( quaternion );
+
 				return this;
 
 			};

+ 52 - 4
examples/js/loaders/MMDLoader.js

@@ -583,6 +583,8 @@ THREE.MMDLoader = ( function () {
 				var boneData = data.bones[ i ];
 
 				var bone = {
+					index: i,
+					transformationClass: boneData.transformationClass,
 					parent: boneData.parentIndex,
 					name: boneData.name,
 					pos: boneData.position.slice( 0, 3 ),
@@ -691,6 +693,10 @@ THREE.MMDLoader = ( function () {
 
 					iks.push( param );
 
+					// Save the reference even from bone data for efficiently
+					// simulating PMX animation system
+					bones[ i ].ik = param;
+
 				}
 
 			}
@@ -699,6 +705,9 @@ THREE.MMDLoader = ( function () {
 
 			if ( data.metadata.format === 'pmx' ) {
 
+				// bone index -> grant entry map
+				var grantEntryMap = {};
+
 				for ( var i = 0; i < data.metadata.boneCount; i ++ ) {
 
 					var boneData = data.bones[ i ];
@@ -716,15 +725,54 @@ THREE.MMDLoader = ( function () {
 						transformationClass: boneData.transformationClass
 					};
 
-					grants.push( param );
+					grantEntryMap[ i ] = { parent: null, children: [], param: param, visited: false };
 
 				}
 
-				grants.sort( function ( a, b ) {
+				var rootEntry = { parent: null, children: [], param: null, visited: false };
 
-					return a.transformationClass - b.transformationClass;
+				// Build a tree representing grant hierarchy
 
-				} );
+				for ( var boneIndex in grantEntryMap ) {
+
+					var grantEntry = grantEntryMap[ boneIndex ];
+					var parentGrantEntry = grantEntryMap[ grantEntry.parentIndex ] || rootEntry;
+
+					grantEntry.parent = parentGrantEntry;
+					parentGrantEntry.children.push( grantEntry );
+
+				}
+
+				// Sort grant parameters from parents to children because
+				// grant uses parent's transform that parent's grant is already applied
+				// so grant should be applied in order from parents to children
+
+				function traverse( entry ) {
+
+					if ( entry.param ) {
+
+						grants.push( entry.param );
+
+						// Save the reference even from bone data for efficiently
+						// simulating PMX animation system
+						bones[ entry.param.index ].grant = entry.param;
+
+					}
+
+					entry.visited = true;
+
+					for ( var i = 0, il = entry.children.length; i < il; i ++ ) {
+
+						var child = entry.children[ i ];
+
+						// Cut off a loop if exists. (Is a grant loop invalid?)
+						if ( ! child.visited ) traverse( child );
+
+					}
+
+				}
+
+				traverse( rootEntry );
 
 			}
 

+ 97 - 83
examples/jsm/animation/CCDIKSolver.js

@@ -53,12 +53,32 @@ var CCDIKSolver = ( function () {
 		constructor: CCDIKSolver,
 
 		/**
-		 * Update IK bones.
+		 * Update all IK bones.
 		 *
 		 * @return {CCDIKSolver}
 		 */
 		update: function () {
 
+			var iks = this.iks;
+
+			for ( var i = 0, il = iks.length; i < il; i ++ ) {
+
+				this.updateOne( iks[ i ] );
+
+			}
+
+			return this;
+
+		},
+
+		/**
+		 * Update one IK bone
+		 *
+		 * @param {Object} ik parameter
+		 * @return {CCDIKSolver}
+		 */
+		updateOne: function () {
+
 			var q = new Quaternion();
 			var targetPos = new Vector3();
 			var targetVec = new Vector3();
@@ -70,137 +90,131 @@ var CCDIKSolver = ( function () {
 			var axis = new Vector3();
 			var vector = new Vector3();
 
-			return function update() {
+			return function update( ik ) {
 
 				var bones = this.mesh.skeleton.bones;
-				var iks = this.iks;
 
 				// for reference overhead reduction in loop
 				var math = Math;
 
-				for ( var i = 0, il = iks.length; i < il; i ++ ) {
-
-					var ik = iks[ i ];
-					var effector = bones[ ik.effector ];
-					var target = bones[ ik.target ];
-
-					// don't use getWorldPosition() here for the performance
-					// because it calls updateMatrixWorld( true ) inside.
-					targetPos.setFromMatrixPosition( target.matrixWorld );
-
-					var links = ik.links;
-					var iteration = ik.iteration !== undefined ? ik.iteration : 1;
-
-					for ( var j = 0; j < iteration; j ++ ) {
+				var effector = bones[ ik.effector ];
+				var target = bones[ ik.target ];
 
-						var rotated = false;
+				// don't use getWorldPosition() here for the performance
+				// because it calls updateMatrixWorld( true ) inside.
+				targetPos.setFromMatrixPosition( target.matrixWorld );
 
-						for ( var k = 0, kl = links.length; k < kl; k ++ ) {
+				var links = ik.links;
+				var iteration = ik.iteration !== undefined ? ik.iteration : 1;
 
-							var link = bones[ links[ k ].index ];
+				for ( var i = 0; i < iteration; i ++ ) {
 
-							// skip this link and following links.
-							// this skip is used for MMD performance optimization.
-							if ( links[ k ].enabled === false ) break;
+					var rotated = false;
 
-							var limitation = links[ k ].limitation;
-							var rotationMin = links[ k ].rotationMin;
-							var rotationMax = links[ k ].rotationMax;
+					for ( var j = 0, jl = links.length; j < jl; j ++ ) {
 
-							// don't use getWorldPosition/Quaternion() here for the performance
-							// because they call updateMatrixWorld( true ) inside.
-							link.matrixWorld.decompose( linkPos, invLinkQ, linkScale );
-							invLinkQ.invert();
-							effectorPos.setFromMatrixPosition( effector.matrixWorld );
+						var link = bones[ links[ j ].index ];
 
-							// work in link world
-							effectorVec.subVectors( effectorPos, linkPos );
-							effectorVec.applyQuaternion( invLinkQ );
-							effectorVec.normalize();
+						// skip this link and following links.
+						// this skip is used for MMD performance optimization.
+						if ( links[ j ].enabled === false ) break;
 
-							targetVec.subVectors( targetPos, linkPos );
-							targetVec.applyQuaternion( invLinkQ );
-							targetVec.normalize();
+						var limitation = links[ j ].limitation;
+						var rotationMin = links[ j ].rotationMin;
+						var rotationMax = links[ j ].rotationMax;
 
-							var angle = targetVec.dot( effectorVec );
+						// don't use getWorldPosition/Quaternion() here for the performance
+						// because they call updateMatrixWorld( true ) inside.
+						link.matrixWorld.decompose( linkPos, invLinkQ, linkScale );
+						invLinkQ.invert();
+						effectorPos.setFromMatrixPosition( effector.matrixWorld );
 
-							if ( angle > 1.0 ) {
+						// work in link world
+						effectorVec.subVectors( effectorPos, linkPos );
+						effectorVec.applyQuaternion( invLinkQ );
+						effectorVec.normalize();
 
-								angle = 1.0;
+						targetVec.subVectors( targetPos, linkPos );
+						targetVec.applyQuaternion( invLinkQ );
+						targetVec.normalize();
 
-							} else if ( angle < - 1.0 ) {
+						var angle = targetVec.dot( effectorVec );
 
-								angle = - 1.0;
+						if ( angle > 1.0 ) {
 
-							}
+							angle = 1.0;
 
-							angle = math.acos( angle );
+						} else if ( angle < - 1.0 ) {
 
-							// skip if changing angle is too small to prevent vibration of bone
-							// Refer to http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mmd.three.js
-							if ( angle < 1e-5 ) continue;
+							angle = - 1.0;
 
-							if ( ik.minAngle !== undefined && angle < ik.minAngle ) {
+						}
 
-								angle = ik.minAngle;
+						angle = math.acos( angle );
 
-							}
+						// skip if changing angle is too small to prevent vibration of bone
+						// Refer to http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mmd.three.js
+						if ( angle < 1e-5 ) continue;
 
-							if ( ik.maxAngle !== undefined && angle > ik.maxAngle ) {
+						if ( ik.minAngle !== undefined && angle < ik.minAngle ) {
 
-								angle = ik.maxAngle;
+							angle = ik.minAngle;
 
-							}
+						}
 
-							axis.crossVectors( effectorVec, targetVec );
-							axis.normalize();
+						if ( ik.maxAngle !== undefined && angle > ik.maxAngle ) {
 
-							q.setFromAxisAngle( axis, angle );
-							link.quaternion.multiply( q );
+							angle = ik.maxAngle;
 
-							// TODO: re-consider the limitation specification
-							if ( limitation !== undefined ) {
+						}
 
-								var c = link.quaternion.w;
+						axis.crossVectors( effectorVec, targetVec );
+						axis.normalize();
 
-								if ( c > 1.0 ) c = 1.0;
+						q.setFromAxisAngle( axis, angle );
+						link.quaternion.multiply( q );
 
-								var c2 = math.sqrt( 1 - c * c );
-								link.quaternion.set( limitation.x * c2,
-								                     limitation.y * c2,
-								                     limitation.z * c2,
-								                     c );
+						// TODO: re-consider the limitation specification
+						if ( limitation !== undefined ) {
 
-							}
+							var c = link.quaternion.w;
 
-							if ( rotationMin !== undefined ) {
+							if ( c > 1.0 ) c = 1.0;
 
-								link.rotation.setFromVector3(
-									link.rotation
-										.toVector3( vector )
-										.max( rotationMin ) );
+							var c2 = math.sqrt( 1 - c * c );
+							link.quaternion.set( limitation.x * c2,
+							                     limitation.y * c2,
+							                     limitation.z * c2,
+							                     c );
 
-							}
+						}
 
-							if ( rotationMax !== undefined ) {
+						if ( rotationMin !== undefined ) {
 
-								link.rotation.setFromVector3(
-									link.rotation
-										.toVector3( vector )
-										.min( rotationMax ) );
+							link.rotation.setFromVector3(
+								link.rotation
+									.toVector3( vector )
+									.max( rotationMin ) );
 
-							}
+						}
 
-							link.updateMatrixWorld( true );
+						if ( rotationMax !== undefined ) {
 
-							rotated = true;
+							link.rotation.setFromVector3(
+								link.rotation
+									.toVector3( vector )
+									.min( rotationMax ) );
 
 						}
 
-						if ( ! rotated ) break;
+						link.updateMatrixWorld( true );
+
+						rotated = true;
 
 					}
 
+					if ( ! rotated ) break;
+
 				}
 
 				return this;

+ 234 - 41
examples/jsm/animation/MMDAnimationHelper.js

@@ -49,7 +49,9 @@ var MMDAnimationHelper = ( function () {
 			afterglow: params.afterglow !== undefined
 				? params.afterglow : 0.0,
 			resetPhysicsOnLoop: params.resetPhysicsOnLoop !== undefined
-				? params.resetPhysicsOnLoop : true
+				? params.resetPhysicsOnLoop : true,
+			pmxAnimation: params.pmxAnimation !== undefined
+				? params.pmxAnimation : false
 		};
 
 		this.enabled = {
@@ -226,15 +228,28 @@ var MMDAnimationHelper = ( function () {
 
 			mesh.updateMatrixWorld( true );
 
-			if ( params.ik !== false ) {
+			// PMX animation system special path
+			if ( this.configuration.pmxAnimation && 
+				mesh.geometry.userData.MMD && mesh.geometry.userData.MMD.format === 'pmx' ) {
 
-				this._createCCDIKSolver( mesh ).update( params.saveOriginalBonesBeforeIK ); // this param is experimental
+				var sortedBonesData = this._sortBoneDataArray( mesh.geometry.userData.MMD.bones.slice() );
+				var ikSolver = params.ik !== false ? this._createCCDIKSolver( mesh ) : null;
+				var grantSolver = params.grant !== false ? this.createGrantSolver( mesh ) : null;
+				this._animatePMXMesh( mesh, sortedBonesData, ikSolver, grantSolver );
 
-			}
+			} else {
+
+				if ( params.ik !== false ) {
+
+					this._createCCDIKSolver( mesh ).update();
 
-			if ( params.grant !== false ) {
+				}
+
+				if ( params.grant !== false ) {
 
-				this.createGrantSolver( mesh ).update();
+					this.createGrantSolver( mesh ).update();
+
+				}
 
 			}
 
@@ -524,28 +539,45 @@ var MMDAnimationHelper = ( function () {
 			var physics = objects.physics;
 			var looped = objects.looped;
 
-			// alternate solution to save/restore bones but less performant?
-			//mesh.pose();
-			//this._updatePropertyMixersBuffer( mesh );
-
 			if ( mixer && this.enabled.animation ) {
 
+				// alternate solution to save/restore bones but less performant?
+				//mesh.pose();
+				//this._updatePropertyMixersBuffer( mesh );
+
 				this._restoreBones( mesh );
 
 				mixer.update( delta );
 
 				this._saveBones( mesh );
 
-				if ( ikSolver && this.enabled.ik ) {
+				// PMX animation system special path
+				if ( this.configuration.pmxAnimation &&
+					mesh.geometry.userData.MMD && mesh.geometry.userData.MMD.format === 'pmx' ) {
 
-					mesh.updateMatrixWorld( true );
-					ikSolver.update();
+					if ( ! objects.sortedBonesData ) objects.sortedBonesData = this._sortBoneDataArray( mesh.geometry.userData.MMD.bones.slice() );
 
-				}
+					this._animatePMXMesh(
+						mesh,
+						objects.sortedBonesData,
+						ikSolver && this.enabled.ik ? ikSolver : null,
+						grantSolver && this.enabled.grant ? grantSolver : null
+					);
 
-				if ( grantSolver && this.enabled.grant ) {
+				} else {
 
-					grantSolver.update();
+					if ( ikSolver && this.enabled.ik ) {
+
+						mesh.updateMatrixWorld( true );
+						ikSolver.update();
+
+					}
+
+					if ( grantSolver && this.enabled.grant ) {
+
+						grantSolver.update();
+
+					}
 
 				}
 
@@ -568,6 +600,142 @@ var MMDAnimationHelper = ( function () {
 
 		},
 
+		// Sort bones in order by 1. transformationClass and 2. bone index.
+		// In PMX animation system, bone transformations should be processed
+		// in this order.
+		_sortBoneDataArray: function ( boneDataArray ) {
+
+			return boneDataArray.sort( function ( a, b ) {
+
+				if ( a.transformationClass !== b.transformationClass ) {
+
+					return a.transformationClass - b.transformationClass;
+
+				} else {
+
+					return a.index - b.index;
+
+				}
+
+			} );
+
+		},
+
+		// PMX Animation system is a bit too complex and doesn't great match to
+		// Three.js Animation system. This method attempts to simulate it as much as
+		// possible but doesn't perfectly simulate.
+		// This method is more costly than the regular one so
+		// you are recommended to set constructor parameter "pmxAnimation: true"
+		// only if your PMX model animation doesn't work well.
+		// If you need better method you would be required to write your own.
+		_animatePMXMesh: function () {
+
+			// Keep working quaternions for less GC
+			var quaternions = [];
+			var quaternionIndex = 0;
+
+			function getQuaternion() {
+
+				if ( quaternionIndex >= quaternions.length ) {
+
+					quaternions.push( new Quaternion() );
+
+				}
+
+				return quaternions[ quaternionIndex ++ ];
+
+			}
+
+			// Save rotation whose grant and IK are already applied
+			// used by grant children
+			var grantResultMap = new Map();
+
+			function updateOne( mesh, boneIndex, ikSolver, grantSolver ) {
+
+				var bones = mesh.skeleton.bones;
+				var bonesData = mesh.geometry.userData.MMD.bones;
+				var boneData = bonesData[ boneIndex ];
+				var bone = bones[ boneIndex ];
+
+				// Return if already updated by being referred as a grant parent.
+				if ( grantResultMap.has( boneIndex ) ) return;
+
+				var quaternion = getQuaternion();
+
+				// Initialize grant result here to prevent infinite loop.
+				// If it's referred before updating with actual result later
+				// result without applyting IK or grant is gotten
+				// but better than composing of infinite loop.
+				grantResultMap.set( boneIndex, quaternion.copy( bone.quaternion ) );
+
+				// @TODO: Support global grant and grant position
+				if ( grantSolver && boneData.grant && 
+					! boneData.grant.isLocal && boneData.grant.affectRotation ) {
+
+					var parentIndex = boneData.grant.parentIndex;
+					var ratio = boneData.grant.ratio;
+
+					if ( ! grantResultMap.has( parentIndex ) ) {
+
+						updateOne( mesh, parentIndex, ikSolver, grantSolver );
+
+					}
+
+					grantSolver.addGrantRotation( bone, grantResultMap.get( parentIndex ), ratio );
+
+				}
+
+				if ( ikSolver && boneData.ik ) {
+
+					// @TODO: Updating world matrices every time solving an IK bone is
+					// costly. Optimize if possible. 
+					mesh.updateMatrixWorld( true );
+					ikSolver.updateOne( boneData.ik );
+
+					// No confident, but it seems the grant results with ik links should be updated?
+					var links = boneData.ik.links;
+
+					for ( var i = 0, il = links.length; i < il; i ++ ) {
+
+						var link = links[ i ];
+
+						if ( link.enabled === false ) continue;
+
+						var linkIndex = link.index;
+
+						if ( grantResultMap.has( linkIndex ) ) {
+
+							grantResultMap.set( linkIndex, grantResultMap.get( linkIndex ).copy( bones[ linkIndex ].quaternion ) );
+
+						}
+
+					}
+
+				}
+
+				// Update with the actual result here
+				quaternion.copy( bone.quaternion );
+
+			}
+
+			return function ( mesh, sortedBonesData, ikSolver, grantSolver ) {
+
+				quaternionIndex = 0;
+				grantResultMap.clear();
+
+				for ( var i = 0, il = sortedBonesData.length; i < il; i ++ ) {
+
+					updateOne( mesh, sortedBonesData[ i ].index, ikSolver, grantSolver );
+
+				}
+
+				mesh.updateMatrixWorld( true );
+				return this;
+
+			};
+
+		}(),
+
 		_animateCamera: function ( camera, delta ) {
 
 			var mixer = this.objects.get( camera ).mixer;
@@ -970,6 +1138,10 @@ var MMDAnimationHelper = ( function () {
 	};
 
 	/**
+	 * Solver for Grant (Fuyo in Japanese. I just google translated because
+	 * Fuyo may be MMD specific term and may not be common word in 3D CG terms.)
+	 * Grant propagates a bone's transform to other bones transforms even if
+	 * they are not children.
 	 * @param {THREE.SkinnedMesh} mesh
 	 * @param {Array<Object>} grants
 	 */
@@ -985,54 +1157,75 @@ var MMDAnimationHelper = ( function () {
 		constructor: GrantSolver,
 
 		/**
+		 * Solve all the grant bones
 		 * @return {GrantSolver}
 		 */
 		update: function () {
 
-			var quaternion = new Quaternion();
+			var grants = this.grants;
 
-			return function () {
+			for ( var i = 0, il = grants.length; i < il; i ++ ) {
 
-				var bones = this.mesh.skeleton.bones;
-				var grants = this.grants;
+				this.updateOne( grants[ i ] );
 
-				for ( var i = 0, il = grants.length; i < il; i ++ ) {
+			}
 
-					var grant = grants[ i ];
-					var bone = bones[ grant.index ];
-					var parentBone = bones[ grant.parentIndex ];
+			return this;
 
-					if ( grant.isLocal ) {
+		},
 
-						// TODO: implement
-						if ( grant.affectPosition ) {
+		/**
+		 * Solve a grant bone
+		 * @param {Object} grant - grant parameter
+		 * @return {GrantSolver}
+		 */
+		updateOne: function ( grant ) {
 
-						}
+			var bones = this.mesh.skeleton.bones;
+			var bone = bones[ grant.index ];
+			var parentBone = bones[ grant.parentIndex ];
 
-						// TODO: implement
-						if ( grant.affectRotation ) {
+			if ( grant.isLocal ) {
 
-						}
+				// TODO: implement
+				if ( grant.affectPosition ) {
 
-					} else {
+				}
 
-						// TODO: implement
-						if ( grant.affectPosition ) {
+				// TODO: implement
+				if ( grant.affectRotation ) {
 
-						}
+				}
 
-						if ( grant.affectRotation ) {
+			} else {
 
-							quaternion.set( 0, 0, 0, 1 );
-							quaternion.slerp( parentBone.quaternion, grant.ratio );
-							bone.quaternion.multiply( quaternion );
+				// TODO: implement
+				if ( grant.affectPosition ) {
 
-						}
+				}
 
-					}
+				if ( grant.affectRotation ) {
+
+					this.addGrantRotation( bone, parentBone.quaternion, grant.ratio );
 
 				}
 
+			}
+
+			return this;
+
+		},
+
+		addGrantRotation: function () {
+
+			var quaternion = new Quaternion();
+
+			return function ( bone, q, ratio ) {
+
+				quaternion.set( 0, 0, 0, 1 );
+				quaternion.slerp( q, ratio );
+				bone.quaternion.multiply( quaternion );
+
 				return this;
 
 			};

+ 52 - 4
examples/jsm/loaders/MMDLoader.js

@@ -618,6 +618,8 @@ var MMDLoader = ( function () {
 				var boneData = data.bones[ i ];
 
 				var bone = {
+					index: i,
+					transformationClass: boneData.transformationClass,
 					parent: boneData.parentIndex,
 					name: boneData.name,
 					pos: boneData.position.slice( 0, 3 ),
@@ -726,6 +728,10 @@ var MMDLoader = ( function () {
 
 					iks.push( param );
 
+					// Save the reference even from bone data for efficiently
+					// simulating PMX animation system
+					bones[ i ].ik = param;
+
 				}
 
 			}
@@ -734,6 +740,9 @@ var MMDLoader = ( function () {
 
 			if ( data.metadata.format === 'pmx' ) {
 
+				// bone index -> grant entry map
+				var grantEntryMap = {};
+
 				for ( var i = 0; i < data.metadata.boneCount; i ++ ) {
 
 					var boneData = data.bones[ i ];
@@ -751,15 +760,54 @@ var MMDLoader = ( function () {
 						transformationClass: boneData.transformationClass
 					};
 
-					grants.push( param );
+					grantEntryMap[ i ] = { parent: null, children: [], param: param, visited: false };
 
 				}
 
-				grants.sort( function ( a, b ) {
+				var rootEntry = { parent: null, children: [], param: null, visited: false };
 
-					return a.transformationClass - b.transformationClass;
+				// Build a tree representing grant hierarchy
 
-				} );
+				for ( var boneIndex in grantEntryMap ) {
+
+					var grantEntry = grantEntryMap[ boneIndex ];
+					var parentGrantEntry = grantEntryMap[ grantEntry.parentIndex ] || rootEntry;
+
+					grantEntry.parent = parentGrantEntry;
+					parentGrantEntry.children.push( grantEntry );
+
+				}
+
+				// Sort grant parameters from parents to children because
+				// grant uses parent's transform that parent's grant is already applied
+				// so grant should be applied in order from parents to children
+
+				function traverse( entry ) {
+
+					if ( entry.param ) {
+
+						grants.push( entry.param );
+
+						// Save the reference even from bone data for efficiently
+						// simulating PMX animation system
+						bones[ entry.param.index ].grant = entry.param;
+
+					}
+
+					entry.visited = true;
+
+					for ( var i = 0, il = entry.children.length; i < il; i ++ ) {
+
+						var child = entry.children[ i ];
+
+						// Cut off a loop if exists. (Is a grant loop invalid?)
+						if ( ! child.visited ) traverse( child );
+
+					}
+
+				}
+
+				traverse( rootEntry );
 
 			}