From 4875d8b171e5417960c047e19547b0fe757d3c30 Mon Sep 17 00:00:00 2001 From: StrandedKitty Date: Tue, 31 Oct 2023 13:03:20 +0100 Subject: [PATCH 1/3] Correctly triangulate polygons with 3+ vertices in FBXLoader --- examples/jsm/loaders/FBXLoader.js | 208 +++++++++++++++++++++--------- 1 file changed, 148 insertions(+), 60 deletions(-) diff --git a/examples/jsm/loaders/FBXLoader.js b/examples/jsm/loaders/FBXLoader.js index dbbcc9821a545a..509a18a0618578 100644 --- a/examples/jsm/loaders/FBXLoader.js +++ b/examples/jsm/loaders/FBXLoader.js @@ -36,6 +36,7 @@ import { Texture, TextureLoader, Uint16BufferAttribute, + Vector2, Vector3, Vector4, VectorKeyframeTrack, @@ -43,6 +44,7 @@ import { } from 'three'; import * as fflate from '../libs/fflate.module.js'; import { NURBSCurve } from '../curves/NURBSCurve.js'; +import { Earcut } from '../../../src/extras/Earcut.js'; /** * Loader loads FBX file and generates Group representing FBX scene. @@ -1944,8 +1946,6 @@ class GeometryParser { if ( endOfFace ) { - if ( faceLength > 4 ) console.warn( 'THREE.FBXLoader: Polygons with more than four sides are not supported. Make sure to triangulate the geometry during export.' ); - scope.genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ); polygonIndex ++; @@ -1967,70 +1967,158 @@ class GeometryParser { } + // See https://www.khronos.org/opengl/wiki/Calculating_a_Surface_Normal + getNormalNewell( vertices ) { + + const normal = new Vector3( 0.0, 0.0, 0.0 ); + + for ( let i = 0; i < vertices.length; i ++ ) { + + const current = vertices[ i ]; + const next = vertices[ ( i + 1 ) % vertices.length ]; + + normal.x += ( current.y - next.y ) * ( current.z + next.z ); + normal.y += ( current.z - next.z ) * ( current.x + next.x ); + normal.z += ( current.x - next.x ) * ( current.y + next.y ); + + } + + normal.normalize(); + + return normal; + + } + + getNormalTangentAndBitangent( vertices ) { + + const normalVector = this.getNormalNewell( vertices ); + // Avoid up being equal or almost equal to normalVector + const up = Math.abs( normalVector.z ) > 0.5 ? new Vector3( 0.0, 1.0, 0.0 ) : new Vector3( 0.0, 0.0, 1.0 ); + const tangent = up.cross( normalVector ).normalize(); + const bitangent = normalVector.clone().cross( tangent ).normalize(); + + return { + normal: normalVector, + tangent: tangent, + bitangent: bitangent + }; + + } + + flattenVertex( vertex, normalTangent, normalBitangent ) { + + return new Vector2( + vertex.dot( normalTangent ), + vertex.dot( normalBitangent ) + ); + + } + // Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) { - for ( let i = 2; i < faceLength; i ++ ) { + let triangles; + + if ( faceLength > 3 ) { + + // Triangulate n-gon using earcut + + const vertices = []; + + 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 ] ] + ) ); + + } + + const { tangent, bitangent } = this.getNormalTangentAndBitangent( vertices ); + const earcutInput = []; + + for ( const vertex of vertices ) { + + const flattened = this.flattenVertex( vertex, tangent, bitangent ); + earcutInput.push( flattened.x, flattened.y ); + + } + + triangles = Earcut.triangulate( earcutInput ); + + } else { + + // Regular triangle, skip earcut triangulation step + triangles = [ 0, 1, 2 ]; + + } + + for ( let i = 0; i < triangles.length; i += 3 ) { + + const i0 = triangles[ i ]; + const i1 = triangles[ i + 1 ]; + const i2 = triangles[ i + 2 ]; - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i0 * 3 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i0 * 3 + 1 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i0 * 3 + 2 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i1 * 3 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i1 * 3 + 1 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i1 * 3 + 2 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i2 * 3 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i2 * 3 + 1 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i2 * 3 + 2 ] ] ); if ( geoInfo.skeleton ) { - buffers.vertexWeights.push( faceWeights[ 0 ] ); - buffers.vertexWeights.push( faceWeights[ 1 ] ); - buffers.vertexWeights.push( faceWeights[ 2 ] ); - buffers.vertexWeights.push( faceWeights[ 3 ] ); + buffers.vertexWeights.push( faceWeights[ i0 * 4 ] ); + buffers.vertexWeights.push( faceWeights[ i0 * 4 + 1 ] ); + buffers.vertexWeights.push( faceWeights[ i0 * 4 + 2 ] ); + buffers.vertexWeights.push( faceWeights[ i0 * 4 + 3 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] ); + buffers.vertexWeights.push( faceWeights[ i1 * 4 ] ); + buffers.vertexWeights.push( faceWeights[ i1 * 4 + 1 ] ); + buffers.vertexWeights.push( faceWeights[ i1 * 4 + 2 ] ); + buffers.vertexWeights.push( faceWeights[ i1 * 4 + 3 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] ); + buffers.vertexWeights.push( faceWeights[ i2 * 4 ] ); + buffers.vertexWeights.push( faceWeights[ i2 * 4 + 1 ] ); + buffers.vertexWeights.push( faceWeights[ i2 * 4 + 2 ] ); + buffers.vertexWeights.push( faceWeights[ i2 * 4 + 3 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 0 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 1 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 2 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 3 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i0 * 4 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i0 * 4 + 1 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i0 * 4 + 2 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i0 * 4 + 3 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i1 * 4 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i1 * 4 + 1 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i1 * 4 + 2 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i1 * 4 + 3 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i2 * 4 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i2 * 4 + 1 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i2 * 4 + 2 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i2 * 4 + 3 ] ); } if ( geoInfo.color ) { - buffers.colors.push( faceColors[ 0 ] ); - buffers.colors.push( faceColors[ 1 ] ); - buffers.colors.push( faceColors[ 2 ] ); + buffers.colors.push( faceColors[ i0 * 3 ] ); + buffers.colors.push( faceColors[ i0 * 3 + 1 ] ); + buffers.colors.push( faceColors[ i0 * 3 + 2 ] ); - buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] ); - buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] ); - buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] ); + buffers.colors.push( faceColors[ i1 * 3 ] ); + buffers.colors.push( faceColors[ i1 * 3 + 1 ] ); + buffers.colors.push( faceColors[ i1 * 3 + 2 ] ); - buffers.colors.push( faceColors[ i * 3 ] ); - buffers.colors.push( faceColors[ i * 3 + 1 ] ); - buffers.colors.push( faceColors[ i * 3 + 2 ] ); + buffers.colors.push( faceColors[ i2 * 3 ] ); + buffers.colors.push( faceColors[ i2 * 3 + 1 ] ); + buffers.colors.push( faceColors[ i2 * 3 + 2 ] ); } @@ -2044,17 +2132,17 @@ class GeometryParser { if ( geoInfo.normal ) { - buffers.normal.push( faceNormals[ 0 ] ); - buffers.normal.push( faceNormals[ 1 ] ); - buffers.normal.push( faceNormals[ 2 ] ); + buffers.normal.push( faceNormals[ i0 * 3 ] ); + buffers.normal.push( faceNormals[ i0 * 3 + 1 ] ); + buffers.normal.push( faceNormals[ i0 * 3 + 2 ] ); - buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] ); - buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] ); - buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] ); + buffers.normal.push( faceNormals[ i1 * 3 ] ); + buffers.normal.push( faceNormals[ i1 * 3 + 1 ] ); + buffers.normal.push( faceNormals[ i1 * 3 + 2 ] ); - buffers.normal.push( faceNormals[ i * 3 ] ); - buffers.normal.push( faceNormals[ i * 3 + 1 ] ); - buffers.normal.push( faceNormals[ i * 3 + 2 ] ); + buffers.normal.push( faceNormals[ i2 * 3 ] ); + buffers.normal.push( faceNormals[ i2 * 3 + 1 ] ); + buffers.normal.push( faceNormals[ i2 * 3 + 2 ] ); } @@ -2064,14 +2152,14 @@ class GeometryParser { if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = []; - buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ i0 * 2 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ i0 * 2 + 1 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ i1 * 2 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ i1 * 2 + 1 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ i2 * 2 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ i2 * 2 + 1 ] ); } ); From c67eaca89bd8fc2d048479d69f2b569b48e998c1 Mon Sep 17 00:00:00 2001 From: StrandedKitty Date: Tue, 31 Oct 2023 15:01:13 +0100 Subject: [PATCH 2/3] Use ShapeUtils instead of directly using Earcut in FBXLoader --- examples/jsm/loaders/FBXLoader.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/examples/jsm/loaders/FBXLoader.js b/examples/jsm/loaders/FBXLoader.js index 509a18a0618578..2a86f2690eb3c0 100644 --- a/examples/jsm/loaders/FBXLoader.js +++ b/examples/jsm/loaders/FBXLoader.js @@ -44,7 +44,7 @@ import { } from 'three'; import * as fflate from '../libs/fflate.module.js'; import { NURBSCurve } from '../curves/NURBSCurve.js'; -import { Earcut } from '../../../src/extras/Earcut.js'; +import { ShapeUtils } from '../../../src/extras/ShapeUtils.js'; /** * Loader loads FBX file and generates Group representing FBX scene. @@ -2036,29 +2036,24 @@ class GeometryParser { } const { tangent, bitangent } = this.getNormalTangentAndBitangent( vertices ); - const earcutInput = []; + const triangulationInput = []; for ( const vertex of vertices ) { - const flattened = this.flattenVertex( vertex, tangent, bitangent ); - earcutInput.push( flattened.x, flattened.y ); + triangulationInput.push( this.flattenVertex( vertex, tangent, bitangent ) ); } - triangles = Earcut.triangulate( earcutInput ); + triangles = ShapeUtils.triangulateShape( triangulationInput, [] ); } else { // Regular triangle, skip earcut triangulation step - triangles = [ 0, 1, 2 ]; + triangles = [[ 0, 1, 2 ]]; } - for ( let i = 0; i < triangles.length; i += 3 ) { - - const i0 = triangles[ i ]; - const i1 = triangles[ i + 1 ]; - const i2 = triangles[ i + 2 ]; + for ( const [ i0, i1, i2 ] of triangles ) { buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i0 * 3 ] ] ); buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i0 * 3 + 1 ] ] ); From a632734da93f7784e5593879e08be3252008ac9b Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Tue, 31 Oct 2023 15:53:31 +0100 Subject: [PATCH 3/3] Update FBXLoader.js Update import. --- examples/jsm/loaders/FBXLoader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/jsm/loaders/FBXLoader.js b/examples/jsm/loaders/FBXLoader.js index 2a86f2690eb3c0..81de08942a4f98 100644 --- a/examples/jsm/loaders/FBXLoader.js +++ b/examples/jsm/loaders/FBXLoader.js @@ -40,11 +40,11 @@ import { Vector3, Vector4, VectorKeyframeTrack, - SRGBColorSpace + SRGBColorSpace, + ShapeUtils } from 'three'; import * as fflate from '../libs/fflate.module.js'; import { NURBSCurve } from '../curves/NURBSCurve.js'; -import { ShapeUtils } from '../../../src/extras/ShapeUtils.js'; /** * Loader loads FBX file and generates Group representing FBX scene.