Browse Source

FBXLoader: Fixed morph attributes to match base geometry length (#28397)

* 28378 fixed morph attributes to match base geometry length

* added fbx morph example

* Update webgl_loader_fbx.html

Remove nurbs asset since it already has its own example.

* Update webgl_loader_fbx.html

The Stanford bunny is already used in three other examples. Let's only add assets in `webgl_loader_fbx` which are not used elsewhere.

* resized morph_test and cleaned up adjustments

---------

Co-authored-by: Michael Herzog <[email protected]>
catalin enache 1 year ago
parent
commit
ad0e26e4ed
3 changed files with 114 additions and 38 deletions
  1. 24 13
      examples/jsm/loaders/FBXLoader.js
  2. BIN
      examples/models/fbx/morph_test.fbx
  3. 90 25
      examples/webgl_loader_fbx.html

+ 24 - 13
examples/jsm/loaders/FBXLoader.js

@@ -2051,14 +2051,18 @@ class GeometryParser {
 			// Triangulate n-gon using earcut
 			// Triangulate n-gon using earcut
 
 
 			const vertices = [];
 			const vertices = [];
-
+			// in morphing scenario vertexPositions represent morphPositions
+			// while baseVertexPositions represent the original geometry's positions
+			const positions = geoInfo.baseVertexPositions || geoInfo.vertexPositions;
 			for ( let i = 0; i < facePositionIndexes.length; i += 3 ) {
 			for ( let i = 0; i < facePositionIndexes.length; i += 3 ) {
 
 
-				vertices.push( new Vector3(
-					geoInfo.vertexPositions[ facePositionIndexes[ i ] ],
-					geoInfo.vertexPositions[ facePositionIndexes[ i + 1 ] ],
-					geoInfo.vertexPositions[ facePositionIndexes[ i + 2 ] ]
-				) );
+				vertices.push(
+					new Vector3(
+						positions[ facePositionIndexes[ i ] ],
+						positions[ facePositionIndexes[ i + 1 ] ],
+						positions[ facePositionIndexes[ i + 2 ] ]
+					)
+				);
 
 
 			}
 			}
 
 
@@ -2071,6 +2075,12 @@ class GeometryParser {
 
 
 			}
 			}
 
 
+			// When vertices is an array of [0,0,0] elements (which is the case for vertices not participating in morph)
+			// the triangulationInput will be an array of [0,0] elements
+			// resulting in an array of 0 triangles being returned from ShapeUtils.triangulateShape
+			// leading to not pushing into buffers.vertex the redundant vertices (the vertices that are not morphed).
+			// That's why, in order to support morphing scenario, "positions" is looking first for baseVertexPositions,
+			// so that we don't end up with an array of 0 triangles for the faces not participating in morph.
 			triangles = ShapeUtils.triangulateShape( triangulationInput, [] );
 			triangles = ShapeUtils.triangulateShape( triangulationInput, [] );
 
 
 		} else {
 		} else {
@@ -2225,17 +2235,18 @@ class GeometryParser {
 	// Normal and position attributes only have data for the vertices that are affected by the morph
 	// Normal and position attributes only have data for the vertices that are affected by the morph
 	genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, name ) {
 	genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, name ) {
 
 
-		const vertexIndices = ( parentGeoNode.PolygonVertexIndex !== undefined ) ? parentGeoNode.PolygonVertexIndex.a : [];
+		const basePositions = parentGeoNode.Vertices !== undefined ? parentGeoNode.Vertices.a : [];
+		const baseIndices = parentGeoNode.PolygonVertexIndex !== undefined ? parentGeoNode.PolygonVertexIndex.a : [];
 
 
-		const morphPositionsSparse = ( morphGeoNode.Vertices !== undefined ) ? morphGeoNode.Vertices.a : [];
-		const indices = ( morphGeoNode.Indexes !== undefined ) ? morphGeoNode.Indexes.a : [];
+		const morphPositionsSparse = morphGeoNode.Vertices !== undefined ? morphGeoNode.Vertices.a : [];
+		const morphIndices = morphGeoNode.Indexes !== undefined ? morphGeoNode.Indexes.a : [];
 
 
 		const length = parentGeo.attributes.position.count * 3;
 		const length = parentGeo.attributes.position.count * 3;
 		const morphPositions = new Float32Array( length );
 		const morphPositions = new Float32Array( length );
 
 
-		for ( let i = 0; i < indices.length; i ++ ) {
+		for ( let i = 0; i < morphIndices.length; i ++ ) {
 
 
-			const morphIndex = indices[ i ] * 3;
+			const morphIndex = morphIndices[ i ] * 3;
 
 
 			morphPositions[ morphIndex ] = morphPositionsSparse[ i * 3 ];
 			morphPositions[ morphIndex ] = morphPositionsSparse[ i * 3 ];
 			morphPositions[ morphIndex + 1 ] = morphPositionsSparse[ i * 3 + 1 ];
 			morphPositions[ morphIndex + 1 ] = morphPositionsSparse[ i * 3 + 1 ];
@@ -2245,9 +2256,9 @@ class GeometryParser {
 
 
 		// TODO: add morph normal support
 		// TODO: add morph normal support
 		const morphGeoInfo = {
 		const morphGeoInfo = {
-			vertexIndices: vertexIndices,
+			vertexIndices: baseIndices,
 			vertexPositions: morphPositions,
 			vertexPositions: morphPositions,
-
+			baseVertexPositions: basePositions
 		};
 		};
 
 
 		const morphBuffers = this.genBuffers( morphGeoInfo );
 		const morphBuffers = this.genBuffers( morphGeoInfo );

BIN
examples/models/fbx/morph_test.fbx


+ 90 - 25
examples/webgl_loader_fbx.html

@@ -30,13 +30,23 @@
 
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 			import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
 			import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
-			let camera, scene, renderer, stats;
-
+			let camera, scene, renderer, stats, object, loader, guiMorphsFolder;
 			const clock = new THREE.Clock();
 			const clock = new THREE.Clock();
 
 
 			let mixer;
 			let mixer;
 
 
+			const params = {
+				asset: 'Samba Dancing'
+			};
+
+			const assets = [
+				'Samba Dancing',
+				'morph_test'
+			];
+
+
 			init();
 			init();
 
 
 			function init() {
 			function init() {
@@ -76,15 +86,75 @@
 				grid.material.opacity = 0.2;
 				grid.material.opacity = 0.2;
 				grid.material.transparent = true;
 				grid.material.transparent = true;
 				scene.add( grid );
 				scene.add( grid );
+			
+				loader = new FBXLoader( );
+				loadAsset( params.asset );
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.shadowMap.enabled = true;
+				container.appendChild( renderer.domElement );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 100, 0 );
+				controls.update();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				// stats
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				const gui = new GUI();
+				gui.add( params, 'asset', assets ).onChange( function ( value ) {
+
+					loadAsset( value );
+
+				} );
+
+				guiMorphsFolder = gui.addFolder( 'Morphs' ).hide();
+
+			}
+
+			function loadAsset( asset ) {
+
+				loader.load( 'models/fbx/' + asset + '.fbx', function ( group ) {
+
+					if ( object ) {
 
 
-				// model
-				const loader = new FBXLoader();
-				loader.load( 'models/fbx/Samba Dancing.fbx', function ( object ) {
+						object.traverse( function ( child ) {
 
 
-					mixer = new THREE.AnimationMixer( object );
+							if ( child.material ) child.material.dispose();
+							if ( child.material && child.material.map ) child.material.map.dispose();
+							if ( child.geometry ) child.geometry.dispose();
 
 
-					const action = mixer.clipAction( object.animations[ 0 ] );
-					action.play();
+						} );
+
+						scene.remove( object );
+
+					}
+
+					//
+
+					object = group;
+
+					if ( object.animations && object.animations.length ) {
+
+						mixer = new THREE.AnimationMixer( object );
+
+						const action = mixer.clipAction( object.animations[ 0 ] );
+						action.play();
+
+					} else {
+
+						mixer = null;
+
+					}
+
+					guiMorphsFolder.children.forEach( ( child ) => child.destroy() );
+					guiMorphsFolder.hide();
 
 
 					object.traverse( function ( child ) {
 					object.traverse( function ( child ) {
 
 
@@ -93,6 +163,18 @@
 							child.castShadow = true;
 							child.castShadow = true;
 							child.receiveShadow = true;
 							child.receiveShadow = true;
 
 
+							if ( child.morphTargetDictionary ) {
+
+								guiMorphsFolder.show();
+								const meshFolder = guiMorphsFolder.addFolder( child.name || child.uuid );
+								Object.keys( child.morphTargetDictionary ).forEach( ( key ) => {
+			
+									meshFolder.add( child.morphTargetInfluences, child.morphTargetDictionary[ key ], 0, 1, 0.01 );
+			
+								} );
+			
+							}
+
 						}
 						}
 
 
 					} );
 					} );
@@ -101,23 +183,6 @@
 
 
 				} );
 				} );
 
 
-				renderer = new THREE.WebGLRenderer( { antialias: true } );
-				renderer.setPixelRatio( window.devicePixelRatio );
-				renderer.setSize( window.innerWidth, window.innerHeight );
-				renderer.setAnimationLoop( animate );
-				renderer.shadowMap.enabled = true;
-				container.appendChild( renderer.domElement );
-
-				const controls = new OrbitControls( camera, renderer.domElement );
-				controls.target.set( 0, 100, 0 );
-				controls.update();
-
-				window.addEventListener( 'resize', onWindowResize );
-
-				// stats
-				stats = new Stats();
-				container.appendChild( stats.dom );
-
 			}
 			}
 
 
 			function onWindowResize() {
 			function onWindowResize() {