From 2baffcb9a1441732726aba6cbb08b25dcf5cf4c4 Mon Sep 17 00:00:00 2001 From: Jannik-Hm Date: Mon, 9 Mar 2026 23:12:21 +0100 Subject: [PATCH 1/3] improved gltf parser (now also properly respecting scale/translation/rotation if matrix is unset --- pkg/file_handlers/gltf.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/file_handlers/gltf.go b/pkg/file_handlers/gltf.go index 972fcce..fe40baa 100644 --- a/pkg/file_handlers/gltf.go +++ b/pkg/file_handlers/gltf.go @@ -6,6 +6,7 @@ import ( "io" "math" + "github.com/Patch2PDF/GDTF-Mesh-Reader/v2/pkg/MeshTypes" Types "github.com/Patch2PDF/GDTF-Mesh-Reader/v2/pkg/MeshTypes" "github.com/qmuntal/gltf" ) @@ -23,11 +24,15 @@ func LoadGLTF(file io.Reader, desiredSize *Types.Vector) (*Types.Mesh, error) { continue } matrix := node.MatrixOrDefault() - transformationMatrices[*node.Mesh] = Types.Matrix{ - X00: matrix[0], X01: matrix[4], X02: matrix[8], X03: matrix[12], - X10: matrix[1], X11: matrix[5], X12: matrix[9], X13: matrix[13], - X20: matrix[2], X21: matrix[6], X22: matrix[10], X23: matrix[14], - X30: matrix[3], X31: matrix[7], X32: matrix[11], X33: matrix[15], + if matrix != gltf.DefaultMatrix { + transformationMatrices[*node.Mesh] = Types.Matrix{ + X00: matrix[0], X01: matrix[4], X02: matrix[8], X03: matrix[12], + X10: matrix[1], X11: matrix[5], X12: matrix[9], X13: matrix[13], + X20: matrix[2], X21: matrix[6], X22: matrix[10], X23: matrix[14], + X30: matrix[3], X31: matrix[7], X32: matrix[11], X33: matrix[15], + } + } else { + transformationMatrices[*node.Mesh] = gltfParseScaleRotationTranslation(node.RotationOrDefault(), node.ScaleOrDefault(), node.TranslationOrDefault()) } } @@ -164,3 +169,12 @@ func gltfIndices(doc *gltf.Document, acc *gltf.Accessor) ([]int, error) { return out, nil } + +func gltfParseScaleRotationTranslation(rotation [4]float64, scale [3]float64, translation [3]float64) MeshTypes.Matrix { + return MeshTypes.Matrix{ + X00: (1 - 2*(rotation[1]*rotation[1]+rotation[2]*rotation[2])) * scale[0], X01: 2 * (rotation[0]*rotation[1] - rotation[3]*rotation[2]) * scale[1], X02: 2 * (rotation[0]*rotation[2] + rotation[3]*rotation[1]) * scale[2], X03: translation[0], + X10: 2 * (rotation[0]*rotation[1] + rotation[3]*rotation[2]) * scale[0], X11: (1 - 2*(rotation[0]*rotation[0]+rotation[2]*rotation[2])) * scale[1], X12: 2 * (rotation[1]*rotation[2] - rotation[3]*rotation[0]) * scale[2], X13: translation[1], + X20: 2 * (rotation[0]*rotation[2] - rotation[3]*rotation[1]) * scale[0], X21: 2 * (rotation[1]*rotation[2] + rotation[3]*rotation[0]) * scale[1], X22: (1 - 2*(rotation[0]*rotation[0]+rotation[1]*rotation[1])) * scale[2], X23: translation[2], + X30: 0, X31: 0, X32: 0, X33: 1, + } +} From a6c6d64293f8e7719941addce3fd745ee10baaca Mon Sep 17 00:00:00 2001 From: Jannik-Hm Date: Tue, 10 Mar 2026 17:26:23 +0100 Subject: [PATCH 2/3] gltf parser now follows hierarchy (with temporary scaling fix) --- pkg/file_handlers/gltf.go | 123 ++++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 37 deletions(-) diff --git a/pkg/file_handlers/gltf.go b/pkg/file_handlers/gltf.go index fe40baa..4b216a4 100644 --- a/pkg/file_handlers/gltf.go +++ b/pkg/file_handlers/gltf.go @@ -6,11 +6,22 @@ import ( "io" "math" - "github.com/Patch2PDF/GDTF-Mesh-Reader/v2/pkg/MeshTypes" Types "github.com/Patch2PDF/GDTF-Mesh-Reader/v2/pkg/MeshTypes" "github.com/qmuntal/gltf" ) +type gltfNode struct { + matrix Types.Matrix + mesh *gltf.Mesh +} + +var conversion = Types.Matrix{ // coordinate system conversion matrix + X00: 1, X01: 0, X02: 0, X03: 0, + X10: 0, X11: 0, X12: -1, X13: 0, + X20: 0, X21: 1, X22: 0, X23: 0, + X30: 0, X31: 0, X32: 0, X33: 1, +} + func LoadGLTF(file io.Reader, desiredSize *Types.Vector) (*Types.Mesh, error) { var doc gltf.Document gltf.NewDecoder(file).Decode(&doc) @@ -19,40 +30,33 @@ func LoadGLTF(file io.Reader, desiredSize *Types.Vector) (*Types.Mesh, error) { transformationMatrices := map[int]Types.Matrix{} - for _, node := range doc.Nodes { - if node.Mesh == nil { - continue - } - matrix := node.MatrixOrDefault() - if matrix != gltf.DefaultMatrix { - transformationMatrices[*node.Mesh] = Types.Matrix{ - X00: matrix[0], X01: matrix[4], X02: matrix[8], X03: matrix[12], - X10: matrix[1], X11: matrix[5], X12: matrix[9], X13: matrix[13], - X20: matrix[2], X21: matrix[6], X22: matrix[10], X23: matrix[14], - X30: matrix[3], X31: matrix[7], X32: matrix[11], X33: matrix[15], - } - } else { - transformationMatrices[*node.Mesh] = gltfParseScaleRotationTranslation(node.RotationOrDefault(), node.ScaleOrDefault(), node.TranslationOrDefault()) + gltfNodes := map[int]gltfNode{} + + for _, scene := range doc.Scenes { + for _, node := range scene.Nodes { + handleGLTFNode(gltfNodes, doc, node, transformationMatrices, Types.IdentityMatrix()) } } + // TODO: refine this as pure rotation breaks bounding box -> possibly rotate all 8 outer points of bounding box and redetermine // calculate outer dimensions - min := Types.Vector{} - max := Types.Vector{} - for meshindex, m := range doc.Meshes { + min := Types.Vector{X: math.Inf(1), Y: math.Inf(1), Z: math.Inf(1)} + max := Types.Vector{X: math.Inf(-1), Y: math.Inf(-1), Z: math.Inf(-1)} + for node_index, node := range gltfNodes { + m := node.mesh for _, p := range m.Primitives { // contains Min and Max attr (for dimension calc) posAccessor := doc.Accessors[p.Attributes[gltf.POSITION]] // do transformation before getting the outer dimensions - tempMin := transformationMatrices[meshindex].MulPosition(Types.Vector{ + tempMin := transformationMatrices[node_index].MulPosition(Types.Vector{ X: posAccessor.Min[0], - Y: posAccessor.Min[2], - Z: posAccessor.Min[1], + Y: posAccessor.Min[1], + Z: posAccessor.Min[2], }) - tempMax := transformationMatrices[meshindex].MulPosition(Types.Vector{ + tempMax := transformationMatrices[node_index].MulPosition(Types.Vector{ X: posAccessor.Max[0], - Y: posAccessor.Max[2], - Z: posAccessor.Max[1], + Y: posAccessor.Max[1], + Z: posAccessor.Max[2], }) min = min.Min(tempMin) max = max.Max(tempMax) @@ -62,12 +66,18 @@ func LoadGLTF(file io.Reader, desiredSize *Types.Vector) (*Types.Mesh, error) { scaling := Types.Vector{X: 1, Y: 1, Z: 1} if desiredSize != nil { scaling = desiredSize.Div(max.Sub(min)) + // following is a temporary fix, TODO: remove once bounding box is refined + scaling.X = math.Abs(scaling.X) + scaling.Y = math.Abs(scaling.Y) + scaling.Z = math.Abs(scaling.Z) } - for meshindex, m := range doc.Meshes { + for _, node := range gltfNodes { + m := node.mesh + determinant := node.matrix.Determinant() for _, p := range m.Primitives { posAccessor := doc.Accessors[p.Attributes[gltf.POSITION]] - positions, err := gltfVec3(&doc, posAccessor, transformationMatrices[meshindex], scaling) + positions, err := gltfVec3(&doc, posAccessor, node.matrix, scaling) if err != nil { return nil, err } @@ -75,7 +85,7 @@ func LoadGLTF(file io.Reader, desiredSize *Types.Vector) (*Types.Mesh, error) { var normals []Types.Vector if nIdx, ok := p.Attributes[gltf.NORMAL]; ok { normalAccessor := doc.Accessors[nIdx] - normals, err = gltfVec3(&doc, normalAccessor, transformationMatrices[meshindex], Types.Vector{X: 1, Y: 1, Z: 1}) + normals, err = gltfVec3(&doc, normalAccessor, node.matrix, Types.Vector{X: 1, Y: 1, Z: 1}) if err != nil { return nil, err } @@ -103,7 +113,11 @@ func LoadGLTF(file io.Reader, desiredSize *Types.Vector) (*Types.Mesh, error) { v1 := positions[indices[i+1]].ToVertex(n1) v2 := positions[indices[i+2]].ToVertex(n2) - mesh.AddTriangle(Types.Triangle{V0: v0, V1: v1, V2: v2}) + if determinant < 0 { + mesh.AddTriangle(Types.Triangle{V0: v0, V1: v2, V2: v1}) + } else { + mesh.AddTriangle(Types.Triangle{V0: v0, V1: v1, V2: v2}) + } } meshes.Add(&mesh) @@ -113,6 +127,33 @@ func LoadGLTF(file io.Reader, desiredSize *Types.Vector) (*Types.Mesh, error) { return meshes, nil } +func handleGLTFNode(nodes map[int]gltfNode, doc gltf.Document, node_id int, transformationMatrices map[int]Types.Matrix, parentMatrix Types.Matrix) { + node := doc.Nodes[node_id] + gltf_matrix := node.MatrixOrDefault() + var matrix Types.Matrix = parentMatrix + if gltf_matrix != gltf.DefaultMatrix { + matrix = matrix.Mul(Types.Matrix{ + X00: gltf_matrix[0], X01: gltf_matrix[4], X02: gltf_matrix[8], X03: gltf_matrix[12], + X10: gltf_matrix[1], X11: gltf_matrix[5], X12: gltf_matrix[9], X13: gltf_matrix[13], + X20: gltf_matrix[2], X21: gltf_matrix[6], X22: gltf_matrix[10], X23: gltf_matrix[14], + X30: gltf_matrix[3], X31: gltf_matrix[7], X32: gltf_matrix[11], X33: gltf_matrix[15], + }) + } else { + matrix = matrix.Mul(gltfParseScaleRotationTranslation(node.RotationOrDefault(), node.ScaleOrDefault(), node.TranslationOrDefault())) + } + if node.Mesh != nil { + world_matrix := conversion.Mul(matrix) + transformationMatrices[node_id] = world_matrix + nodes[node_id] = gltfNode{ + matrix: world_matrix, + mesh: doc.Meshes[*node.Mesh], + } + } + for _, child_node := range node.Children { + handleGLTFNode(nodes, doc, child_node, transformationMatrices, matrix) + } +} + func gltfVec3(doc *gltf.Document, acc *gltf.Accessor, transformationMatrix Types.Matrix, scaling Types.Vector) ([]Types.Vector, error) { bufView := doc.BufferViews[*acc.BufferView] buffer := doc.Buffers[bufView.Buffer] @@ -124,18 +165,12 @@ func gltfVec3(doc *gltf.Document, acc *gltf.Accessor, transformationMatrix Types vectors := make([]Types.Vector, acc.Count) for i := 0; i < acc.Count; i++ { base := i * 12 - // axes inverted to convert to correct coordinate system vec := Types.Vector{ X: float64(math.Float32frombits(binary.LittleEndian.Uint32(raw[base+0:]))), Y: float64(math.Float32frombits(binary.LittleEndian.Uint32(raw[base+4:]))), Z: float64(math.Float32frombits(binary.LittleEndian.Uint32(raw[base+8:]))), } - transformed := transformationMatrix.MulPosition(vec) - scaled := Types.Vector{ - X: transformed.X, - Y: -transformed.Z, - Z: transformed.Y, - }.Mult(scaling) + scaled := transformationMatrix.MulPosition(vec).Mult(scaling) vectors[i] = scaled // vec.Mult(scaling) } @@ -170,8 +205,22 @@ func gltfIndices(doc *gltf.Document, acc *gltf.Accessor) ([]int, error) { return out, nil } -func gltfParseScaleRotationTranslation(rotation [4]float64, scale [3]float64, translation [3]float64) MeshTypes.Matrix { - return MeshTypes.Matrix{ +func normalizeQuaternion(q [4]float64) [4]float64 { + // Calculate mag squared + magSq := q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3] + + // Check for near-zero + if magSq < 1e-12 { + return [4]float64{0, 0, 0, 1} + } + + mag := math.Sqrt(magSq) + return [4]float64{q[0] / mag, q[1] / mag, q[2] / mag, q[3] / mag} +} + +func gltfParseScaleRotationTranslation(rotation [4]float64, scale [3]float64, translation [3]float64) Types.Matrix { + rotation = normalizeQuaternion(rotation) + return Types.Matrix{ X00: (1 - 2*(rotation[1]*rotation[1]+rotation[2]*rotation[2])) * scale[0], X01: 2 * (rotation[0]*rotation[1] - rotation[3]*rotation[2]) * scale[1], X02: 2 * (rotation[0]*rotation[2] + rotation[3]*rotation[1]) * scale[2], X03: translation[0], X10: 2 * (rotation[0]*rotation[1] + rotation[3]*rotation[2]) * scale[0], X11: (1 - 2*(rotation[0]*rotation[0]+rotation[2]*rotation[2])) * scale[1], X12: 2 * (rotation[1]*rotation[2] - rotation[3]*rotation[0]) * scale[2], X13: translation[1], X20: 2 * (rotation[0]*rotation[2] - rotation[3]*rotation[1]) * scale[0], X21: 2 * (rotation[1]*rotation[2] + rotation[3]*rotation[0]) * scale[1], X22: (1 - 2*(rotation[0]*rotation[0]+rotation[1]*rotation[1])) * scale[2], X23: translation[2], From b4f4dad86c1a74c1b50627771eedcbc2ad27e047 Mon Sep 17 00:00:00 2001 From: Jannik-Hm Date: Tue, 10 Mar 2026 17:39:38 +0100 Subject: [PATCH 3/3] refined bounding box gen in gltf --- pkg/file_handlers/gltf.go | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pkg/file_handlers/gltf.go b/pkg/file_handlers/gltf.go index 4b216a4..fefaf33 100644 --- a/pkg/file_handlers/gltf.go +++ b/pkg/file_handlers/gltf.go @@ -38,7 +38,6 @@ func LoadGLTF(file io.Reader, desiredSize *Types.Vector) (*Types.Mesh, error) { } } - // TODO: refine this as pure rotation breaks bounding box -> possibly rotate all 8 outer points of bounding box and redetermine // calculate outer dimensions min := Types.Vector{X: math.Inf(1), Y: math.Inf(1), Z: math.Inf(1)} max := Types.Vector{X: math.Inf(-1), Y: math.Inf(-1), Z: math.Inf(-1)} @@ -47,29 +46,38 @@ func LoadGLTF(file io.Reader, desiredSize *Types.Vector) (*Types.Mesh, error) { for _, p := range m.Primitives { // contains Min and Max attr (for dimension calc) posAccessor := doc.Accessors[p.Attributes[gltf.POSITION]] - // do transformation before getting the outer dimensions - tempMin := transformationMatrices[node_index].MulPosition(Types.Vector{ + tempMin := Types.Vector{ X: posAccessor.Min[0], Y: posAccessor.Min[1], Z: posAccessor.Min[2], - }) - tempMax := transformationMatrices[node_index].MulPosition(Types.Vector{ + } + tempMax := Types.Vector{ X: posAccessor.Max[0], Y: posAccessor.Max[1], Z: posAccessor.Max[2], - }) - min = min.Min(tempMin) - max = max.Max(tempMax) + } + // determine all outer points as min/max might switch due to transformation + outer_points := [8]Types.Vector{ + {X: tempMin.X, Y: tempMax.Y, Z: tempMax.Z}, // up front left + {X: tempMin.X, Y: tempMax.Y, Z: tempMin.Z}, // up back left + {X: tempMin.X, Y: tempMin.Y, Z: tempMax.Z}, // down front left + {X: tempMin.X, Y: tempMin.Y, Z: tempMin.Z}, // down back left + {X: tempMax.X, Y: tempMax.Y, Z: tempMax.Z}, // up front right + {X: tempMax.X, Y: tempMax.Y, Z: tempMin.Z}, // up back right + {X: tempMax.X, Y: tempMin.Y, Z: tempMax.Z}, // down front right + {X: tempMax.X, Y: tempMin.Y, Z: tempMin.Z}, // down back right + } + for _, vec := range outer_points { + vec = transformationMatrices[node_index].MulPosition(vec) + min = min.Min(vec) + max = max.Max(vec) + } } } scaling := Types.Vector{X: 1, Y: 1, Z: 1} if desiredSize != nil { scaling = desiredSize.Div(max.Sub(min)) - // following is a temporary fix, TODO: remove once bounding box is refined - scaling.X = math.Abs(scaling.X) - scaling.Y = math.Abs(scaling.Y) - scaling.Z = math.Abs(scaling.Z) } for _, node := range gltfNodes {