From 98d8ea9005deb592af039bffad1a6df6b7542e6c Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Mon, 27 Apr 2026 13:17:59 +0100 Subject: [PATCH 1/2] fix: xmodel glTF dumps dropping `COLOR_0` vertex attributes --- src/ObjCommon/XModel/Gltf/JsonGltf.h | 2 +- src/ObjWriting/XModel/Gltf/GltfWriter.cpp | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/ObjCommon/XModel/Gltf/JsonGltf.h b/src/ObjCommon/XModel/Gltf/JsonGltf.h index fe081504..09a3c7f8 100644 --- a/src/ObjCommon/XModel/Gltf/JsonGltf.h +++ b/src/ObjCommon/XModel/Gltf/JsonGltf.h @@ -265,7 +265,7 @@ namespace gltf std::optional WEIGHTS_0; }; - NLOHMANN_DEFINE_TYPE_EXTENSION(JsonMeshPrimitivesAttributes, POSITION, NORMAL, TEXCOORD_0, JOINTS_0, WEIGHTS_0); + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonMeshPrimitivesAttributes, POSITION, NORMAL, COLOR_0, TEXCOORD_0, JOINTS_0, WEIGHTS_0); class JsonMeshPrimitives { diff --git a/src/ObjWriting/XModel/Gltf/GltfWriter.cpp b/src/ObjWriting/XModel/Gltf/GltfWriter.cpp index 7dc5a564..0aacfc79 100644 --- a/src/ObjWriting/XModel/Gltf/GltfWriter.cpp +++ b/src/ObjWriting/XModel/Gltf/GltfWriter.cpp @@ -23,6 +23,7 @@ namespace { float coordinates[3]; float normal[3]; + float color[4]; float uv[2]; }; @@ -190,6 +191,7 @@ namespace primitives.attributes.POSITION = m_position_accessor; primitives.attributes.NORMAL = m_normal_accessor; + primitives.attributes.COLOR_0 = m_color_accessor; primitives.attributes.TEXCOORD_0 = m_uv_accessor; if (hasBoneWeightData) @@ -455,6 +457,15 @@ namespace m_normal_accessor = static_cast(gltf.accessors->size()); gltf.accessors->emplace_back(normalAccessor); + JsonAccessor colorAccessor; + colorAccessor.bufferView = m_vertex_buffer_view; + colorAccessor.byteOffset = static_cast(offsetof(GltfVertex, color)); + colorAccessor.componentType = JsonAccessorComponentType::FLOAT; + colorAccessor.count = static_cast(xmodel.m_vertices.size()); + colorAccessor.type = JsonAccessorType::VEC4; + m_color_accessor = static_cast(gltf.accessors->size()); + gltf.accessors->emplace_back(colorAccessor); + JsonAccessor uvAccessor; uvAccessor.bufferView = m_vertex_buffer_view; uvAccessor.byteOffset = static_cast(offsetof(GltfVertex, uv)); @@ -545,6 +556,11 @@ namespace vertex->normal[2] = commonVertex.normal[2]; LhcToRhcCoordinates(vertex->normal); + vertex->color[0] = commonVertex.color[0]; + vertex->color[1] = commonVertex.color[1]; + vertex->color[2] = commonVertex.color[2]; + vertex->color[3] = commonVertex.color[3]; + vertex->uv[0] = commonVertex.uv[0]; vertex->uv[1] = commonVertex.uv[1]; @@ -681,6 +697,7 @@ namespace unsigned m_first_bone_node = 0u; unsigned m_position_accessor = 0u; unsigned m_normal_accessor = 0u; + unsigned m_color_accessor = 0u; unsigned m_uv_accessor = 0u; unsigned m_joints_accessor = 0u; unsigned m_weights_accessor = 0u; From 1d6a9d6df190093cfac90b6fd49935752054a4d8 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Mon, 27 Apr 2026 22:47:01 +0200 Subject: [PATCH 2/2] chore: only dump gltf color when there is non-default data --- src/ObjWriting/XModel/Gltf/GltfWriter.cpp | 101 ++++++++++++++++------ 1 file changed, 76 insertions(+), 25 deletions(-) diff --git a/src/ObjWriting/XModel/Gltf/GltfWriter.cpp b/src/ObjWriting/XModel/Gltf/GltfWriter.cpp index 0aacfc79..ab81bbc1 100644 --- a/src/ObjWriting/XModel/Gltf/GltfWriter.cpp +++ b/src/ObjWriting/XModel/Gltf/GltfWriter.cpp @@ -23,10 +23,14 @@ namespace { float coordinates[3]; float normal[3]; - float color[4]; float uv[2]; }; + struct GltfVertexColorData + { + float color[4]; + }; + void LhcToRhcCoordinates(float (&coords)[3]) { const float two[3]{coords[0], coords[1], coords[2]}; @@ -71,6 +75,22 @@ namespace matrix = result; } + [[nodiscard]] bool HasNonDefaultColorData(const XModelCommon& xmodel) + { + for (const auto& vertex : xmodel.m_vertices) + { + if (std::abs(vertex.color[0] - 1.0f) >= std::numeric_limits::epsilon() + || std::abs(vertex.color[1] - 1.0f) >= std::numeric_limits::epsilon() + || std::abs(vertex.color[2] - 1.0f) >= std::numeric_limits::epsilon() + || std::abs(vertex.color[3] - 1.0f) >= std::numeric_limits::epsilon()) + { + return true; + } + } + + return false; + } + class GltfWriterImpl final : public gltf::Writer { public: @@ -86,17 +106,19 @@ namespace JsonRoot gltf; std::vector bufferData; + const auto hasNonDefaultColorData = HasNonDefaultColorData(xmodel); + CreateJsonAsset(gltf.asset); CreateSkeletonNodes(gltf, xmodel); CreateMeshNodes(gltf, xmodel); CreateRootNode(gltf, xmodel); CreateMaterials(gltf, xmodel); - CreateBufferViews(gltf, xmodel); - CreateAccessors(gltf, xmodel); + CreateBufferViews(gltf, xmodel, hasNonDefaultColorData); + CreateAccessors(gltf, xmodel, hasNonDefaultColorData); CreateSkin(gltf, xmodel); - CreateMeshes(gltf, xmodel); + CreateMeshes(gltf, xmodel, hasNonDefaultColorData); CreateScene(gltf, xmodel); - FillBufferData(gltf, xmodel, bufferData); + FillBufferData(gltf, xmodel, bufferData, hasNonDefaultColorData); CreateBuffer(gltf, xmodel, bufferData); const ordered_json jRoot = gltf; @@ -174,7 +196,7 @@ namespace gltf.nodes->emplace_back(std::move(rootNode)); } - void CreateMeshes(JsonRoot& gltf, const XModelCommon& xmodel) + void CreateMeshes(JsonRoot& gltf, const XModelCommon& xmodel, const bool hasNonDefaultColorData) { if (!gltf.meshes.has_value()) gltf.meshes.emplace(); @@ -191,7 +213,8 @@ namespace primitives.attributes.POSITION = m_position_accessor; primitives.attributes.NORMAL = m_normal_accessor; - primitives.attributes.COLOR_0 = m_color_accessor; + if (hasNonDefaultColorData) + primitives.attributes.COLOR_0 = m_color_accessor; primitives.attributes.TEXCOORD_0 = m_uv_accessor; if (hasBoneWeightData) @@ -370,7 +393,7 @@ namespace gltf.scene = 0u; } - void CreateBufferViews(JsonRoot& gltf, const XModelCommon& xmodel) + void CreateBufferViews(JsonRoot& gltf, const XModelCommon& xmodel, const bool hasNonDefaultColorData) { if (!gltf.bufferViews.has_value()) gltf.bufferViews.emplace(); @@ -388,6 +411,19 @@ namespace m_vertex_buffer_view = static_cast(gltf.bufferViews->size()); gltf.bufferViews->emplace_back(vertexBufferView); + if (hasNonDefaultColorData) + { + JsonBufferView colorBufferView; + colorBufferView.buffer = 0u; + colorBufferView.byteOffset = bufferOffset; + colorBufferView.byteLength = static_cast(sizeof(GltfVertexColorData) * xmodel.m_vertices.size()); + colorBufferView.target = JsonBufferViewTarget::ARRAY_BUFFER; + bufferOffset += colorBufferView.byteLength; + + m_color_buffer_view = static_cast(gltf.bufferViews->size()); + gltf.bufferViews->emplace_back(colorBufferView); + } + if (!xmodel.m_bone_weight_data.weights.empty()) { JsonBufferView jointsBufferView; @@ -434,7 +470,7 @@ namespace } } - void CreateAccessors(JsonRoot& gltf, const XModelCommon& xmodel) + void CreateAccessors(JsonRoot& gltf, const XModelCommon& xmodel, const bool hasNonDefaultColorData) { if (!gltf.accessors.has_value()) gltf.accessors.emplace(); @@ -457,14 +493,16 @@ namespace m_normal_accessor = static_cast(gltf.accessors->size()); gltf.accessors->emplace_back(normalAccessor); - JsonAccessor colorAccessor; - colorAccessor.bufferView = m_vertex_buffer_view; - colorAccessor.byteOffset = static_cast(offsetof(GltfVertex, color)); - colorAccessor.componentType = JsonAccessorComponentType::FLOAT; - colorAccessor.count = static_cast(xmodel.m_vertices.size()); - colorAccessor.type = JsonAccessorType::VEC4; - m_color_accessor = static_cast(gltf.accessors->size()); - gltf.accessors->emplace_back(colorAccessor); + if (hasNonDefaultColorData) + { + JsonAccessor colorAccessor; + colorAccessor.bufferView = m_color_buffer_view; + colorAccessor.componentType = JsonAccessorComponentType::FLOAT; + colorAccessor.count = static_cast(xmodel.m_vertices.size()); + colorAccessor.type = JsonAccessorType::VEC4; + m_color_accessor = static_cast(gltf.accessors->size()); + gltf.accessors->emplace_back(colorAccessor); + } JsonAccessor uvAccessor; uvAccessor.bufferView = m_vertex_buffer_view; @@ -517,9 +555,9 @@ namespace } } - void FillBufferData(JsonRoot& gltf, const XModelCommon& xmodel, std::vector& bufferData) const + void FillBufferData(JsonRoot& gltf, const XModelCommon& xmodel, std::vector& bufferData, const bool hasNonDefaultColorData) const { - const auto expectedBufferSize = GetExpectedBufferSize(xmodel); + const auto expectedBufferSize = GetExpectedBufferSize(xmodel, hasNonDefaultColorData); bufferData.resize(expectedBufferSize); auto currentBufferOffset = 0uz; @@ -556,11 +594,6 @@ namespace vertex->normal[2] = commonVertex.normal[2]; LhcToRhcCoordinates(vertex->normal); - vertex->color[0] = commonVertex.color[0]; - vertex->color[1] = commonVertex.color[1]; - vertex->color[2] = commonVertex.color[2]; - vertex->color[3] = commonVertex.color[3]; - vertex->uv[0] = commonVertex.uv[0]; vertex->uv[1] = commonVertex.uv[1]; @@ -573,6 +606,20 @@ namespace gltf.accessors.value()[m_position_accessor].max = std::vector({maxPosition[0], maxPosition[1], maxPosition[2]}); } + if (hasNonDefaultColorData) + { + for (const auto& commonVertex : xmodel.m_vertices) + { + auto* colorData = reinterpret_cast(&bufferData[currentBufferOffset]); + colorData->color[0] = commonVertex.color[0]; + colorData->color[1] = commonVertex.color[1]; + colorData->color[2] = commonVertex.color[2]; + colorData->color[3] = commonVertex.color[3]; + + currentBufferOffset += sizeof(GltfVertexColorData); + } + } + if (!xmodel.m_bone_weight_data.weights.empty()) { assert(xmodel.m_vertex_bone_weights.size() == xmodel.m_vertices.size()); @@ -655,12 +702,15 @@ namespace assert(expectedBufferSize == currentBufferOffset); } - static size_t GetExpectedBufferSize(const XModelCommon& xmodel) + static size_t GetExpectedBufferSize(const XModelCommon& xmodel, const bool hasNonDefaultColorData) { auto result = 0uz; result += xmodel.m_vertices.size() * sizeof(GltfVertex); + if (hasNonDefaultColorData) + result += xmodel.m_vertices.size() * sizeof(GltfVertexColorData); + if (!xmodel.m_bone_weight_data.weights.empty()) { // Joints and weights @@ -703,6 +753,7 @@ namespace unsigned m_weights_accessor = 0u; unsigned m_inverse_bind_matrices_accessor = 0u; unsigned m_vertex_buffer_view = 0u; + unsigned m_color_buffer_view = 0u; unsigned m_joints_buffer_view = 0u; unsigned m_weights_buffer_view = 0u; unsigned m_inverse_bind_matrices_buffer_view = 0u;