diff --git a/src/ObjCommon/XModel/Gltf/GltfConstants.h b/src/ObjCommon/XModel/Gltf/GltfConstants.h index 1ce687d0..a83f4e2e 100644 --- a/src/ObjCommon/XModel/Gltf/GltfConstants.h +++ b/src/ObjCommon/XModel/Gltf/GltfConstants.h @@ -18,4 +18,10 @@ namespace gltf constexpr auto GLTF_JSON_CHUNK_DATA_OFFSET = 20u; constexpr auto GLTF_DATA_URI_PREFIX = "data:application/octet-stream;base64,"; + + constexpr auto GLTF_ATTRIBUTE_POSITION = "POSITION"; + constexpr auto GLTF_ATTRIBUTE_NORMAL = "NORMAL"; + constexpr auto GLTF_ATTRIBUTE_TEXCOORD_0 = "TEXCOORD_0"; + constexpr auto GLTF_ATTRIBUTE_JOINTS_0 = "JOINTS_0"; + constexpr auto GLTF_ATTRIBUTE_WEIGHTS_0 = "WEIGHTS_0"; } // namespace gltf diff --git a/src/ObjCommon/XModel/Gltf/JsonGltf.h b/src/ObjCommon/XModel/Gltf/JsonGltf.h index 16eb8104..dccbc794 100644 --- a/src/ObjCommon/XModel/Gltf/JsonGltf.h +++ b/src/ObjCommon/XModel/Gltf/JsonGltf.h @@ -102,16 +102,29 @@ namespace gltf NLOHMANN_DEFINE_TYPE_EXTENSION(JsonAccessor, bufferView, byteOffset, componentType, count, type, min, max); + enum class JsonBufferViewTarget + { + ARRAY_BUFFER = 34962, + ELEMENT_ARRAY_BUFFER = 34963 + }; + + NLOHMANN_JSON_SERIALIZE_ENUM(JsonBufferViewTarget, + { + {JsonBufferViewTarget::ARRAY_BUFFER, static_cast(JsonBufferViewTarget::ARRAY_BUFFER) }, + {JsonBufferViewTarget::ELEMENT_ARRAY_BUFFER, static_cast(JsonBufferViewTarget::ELEMENT_ARRAY_BUFFER)}, + }); + class JsonBufferView { public: unsigned buffer; unsigned byteLength; - unsigned byteOffset; - unsigned target; + std::optional byteOffset; + std::optional byteStride; + std::optional target; }; - NLOHMANN_DEFINE_TYPE_EXTENSION(JsonBufferView, buffer, byteLength, byteOffset, target); + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonBufferView, buffer, byteLength, byteOffset, byteStride, target); enum class JsonAnimationChannelTargetPath { @@ -211,10 +224,23 @@ namespace gltf {JsonMeshPrimitivesMode::TRIANGLE_FAN, static_cast(JsonMeshPrimitivesMode::TRIANGLE_FAN) }, }); + // This should probably be a map, but the supported models do not have arbitrary primitives anyway + class JsonMeshPrimitivesAttributes + { + public: + std::optional POSITION; + std::optional NORMAL; + std::optional TEXCOORD_0; + std::optional JOINTS_0; + std::optional WEIGHTS_0; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonMeshPrimitivesAttributes, POSITION, NORMAL, TEXCOORD_0, JOINTS_0, WEIGHTS_0); + class JsonMeshPrimitives { public: - std::unordered_map attributes; + JsonMeshPrimitivesAttributes attributes; std::optional indices; std::optional material; std::optional mode; diff --git a/src/ObjWriting/XModel/Gltf/GltfTextOutput.cpp b/src/ObjWriting/XModel/Gltf/GltfTextOutput.cpp index 267e5f68..f04838a3 100644 --- a/src/ObjWriting/XModel/Gltf/GltfTextOutput.cpp +++ b/src/ObjWriting/XModel/Gltf/GltfTextOutput.cpp @@ -5,6 +5,8 @@ #include #include + +#define LTC_NO_PROTOTYPES #include using namespace gltf; @@ -17,16 +19,17 @@ TextOutput::TextOutput(std::ostream& stream) std::optional TextOutput::CreateBufferUri(const void* buffer, const size_t bufferSize) const { static constexpr auto URI_PREFIX_LENGTH = std::char_traits::length(GLTF_DATA_URI_PREFIX); - const auto base64Length = utils::Align(4u * (bufferSize / 3u), 4u); + const auto base64Length = 4u * ((bufferSize + 2u) / 3u); const auto base64BufferSize = URI_PREFIX_LENGTH + base64Length; std::string output(base64BufferSize, '\0'); std::memcpy(output.data(), GLTF_DATA_URI_PREFIX, URI_PREFIX_LENGTH); - unsigned long outLength = base64Length; - base64_encode(static_cast(buffer), bufferSize, &output[URI_PREFIX_LENGTH], &outLength); + unsigned long outLength = base64Length + 1u; + const auto result = base64_encode(static_cast(buffer), bufferSize, &output[URI_PREFIX_LENGTH], &outLength); + assert(result == CRYPT_OK); assert(outLength == base64Length); return output; diff --git a/src/ObjWriting/XModel/Gltf/GltfWriter.cpp b/src/ObjWriting/XModel/Gltf/GltfWriter.cpp index 05b22839..18fe88dc 100644 --- a/src/ObjWriting/XModel/Gltf/GltfWriter.cpp +++ b/src/ObjWriting/XModel/Gltf/GltfWriter.cpp @@ -11,6 +11,13 @@ namespace { constexpr auto GLTF_GENERATOR = "OpenAssetTools " GIT_VERSION; + struct GltfVertex + { + float coordinates[3]; + float normal[3]; + float uv[2]; + }; + class GltfWriterImpl final : public gltf::Writer { static constexpr auto NODE_INDEX_MESH = 0u; @@ -27,15 +34,26 @@ namespace void Write(const XModelCommon& xmodel) override { JsonRoot gltf; + std::vector bufferData; + CreateJsonAsset(gltf.asset); CreateMeshNode(gltf, xmodel); - CreateMesh(gltf, xmodel); + CreateMaterials(gltf, xmodel); CreateSkeletonNodes(gltf, xmodel); CreateSkin(gltf, xmodel); + CreateBufferViews(gltf, xmodel); + CreateAccessors(gltf, xmodel); + CreateMesh(gltf, xmodel); CreateScene(gltf, xmodel); + FillBufferData(xmodel, bufferData); + CreateBuffer(gltf, xmodel, bufferData); const json jRoot = gltf; m_output->EmitJson(jRoot); + + if (!bufferData.empty()) + m_output->EmitBuffer(bufferData.data(), bufferData.size()); + m_output->Finalize(); } @@ -66,16 +84,50 @@ namespace gltf.nodes->emplace_back(std::move(meshNode)); } - static void CreateMesh(JsonRoot& gltf, const XModelCommon& xmodel) + void CreateMesh(JsonRoot& gltf, const XModelCommon& xmodel) { if (!gltf.meshes.has_value()) gltf.meshes.emplace(); JsonMesh mesh; + auto objectIndex = 0u; + for (const auto& object : xmodel.m_objects) + { + JsonMeshPrimitives primitives; + + if (object.materialIndex >= 0) + primitives.material = static_cast(object.materialIndex); + + primitives.attributes.POSITION = m_position_accessor; + primitives.attributes.NORMAL = m_normal_accessor; + primitives.attributes.TEXCOORD_0 = m_uv_accessor; + + primitives.mode = JsonMeshPrimitivesMode::TRIANGLES; + primitives.indices = m_first_index_accessor + objectIndex; + + mesh.primitives.emplace_back(primitives); + objectIndex++; + } + gltf.meshes->emplace_back(std::move(mesh)); } + static void CreateMaterials(JsonRoot& gltf, const XModelCommon& xmodel) + { + if (!gltf.materials.has_value()) + gltf.materials.emplace(); + + for (const auto& modelMaterial : xmodel.m_materials) + { + JsonMaterial material; + + material.name = modelMaterial.name; + + gltf.materials->emplace_back(material); + } + } + static void CreateSkeletonNodes(JsonRoot& gltf, const XModelCommon& xmodel) { if (xmodel.m_bones.empty()) @@ -98,7 +150,7 @@ namespace for (auto maybeChildIndex = 0u; maybeChildIndex < boneCount; maybeChildIndex++) { if (xmodel.m_bones[maybeChildIndex].parentIndex == static_cast(boneIndex)) - children.emplace_back(boneIndex + NODE_FIRST_INDEX_BONES); + children.emplace_back(maybeChildIndex + NODE_FIRST_INDEX_BONES); } if (!children.empty()) boneNode.children = std::move(children); @@ -143,7 +195,161 @@ namespace gltf.scene = 0u; } + void CreateBufferViews(JsonRoot& gltf, const XModelCommon& xmodel) + { + if (!gltf.bufferViews.has_value()) + gltf.bufferViews.emplace(); + + unsigned bufferOffset = 0u; + + JsonBufferView vertexBufferView; + vertexBufferView.buffer = 0u; + vertexBufferView.byteOffset = bufferOffset; + vertexBufferView.byteStride = sizeof(GltfVertex); + vertexBufferView.byteLength = sizeof(GltfVertex) * xmodel.m_vertices.size(); + vertexBufferView.target = JsonBufferViewTarget::ARRAY_BUFFER; + bufferOffset += vertexBufferView.byteLength; + + m_vertex_buffer_view = gltf.bufferViews->size(); + gltf.bufferViews->emplace_back(vertexBufferView); + + m_first_index_buffer_view = gltf.bufferViews->size(); + for (const auto& object : xmodel.m_objects) + { + JsonBufferView indicesBufferView; + indicesBufferView.buffer = 0u; + indicesBufferView.byteOffset = bufferOffset; + indicesBufferView.byteLength = sizeof(unsigned short) * object.m_faces.size() * 3u; + indicesBufferView.target = JsonBufferViewTarget::ELEMENT_ARRAY_BUFFER; + bufferOffset += indicesBufferView.byteLength; + + gltf.bufferViews->emplace_back(indicesBufferView); + } + } + + void CreateAccessors(JsonRoot& gltf, const XModelCommon& xmodel) + { + if (!gltf.accessors.has_value()) + gltf.accessors.emplace(); + + JsonAccessor positionAccessor; + positionAccessor.bufferView = m_vertex_buffer_view; + positionAccessor.byteOffset = offsetof(GltfVertex, coordinates); + positionAccessor.componentType = JsonAccessorComponentType::FLOAT; + positionAccessor.count = xmodel.m_vertices.size(); + positionAccessor.type = JsonAccessorType::VEC3; + m_position_accessor = gltf.accessors->size(); + gltf.accessors->emplace_back(positionAccessor); + + JsonAccessor normalAccessor; + normalAccessor.bufferView = m_vertex_buffer_view; + normalAccessor.byteOffset = offsetof(GltfVertex, normal); + normalAccessor.componentType = JsonAccessorComponentType::FLOAT; + normalAccessor.count = xmodel.m_vertices.size(); + normalAccessor.type = JsonAccessorType::VEC3; + m_normal_accessor = gltf.accessors->size(); + gltf.accessors->emplace_back(normalAccessor); + + JsonAccessor uvAccessor; + uvAccessor.bufferView = m_vertex_buffer_view; + uvAccessor.byteOffset = offsetof(GltfVertex, uv); + uvAccessor.componentType = JsonAccessorComponentType::FLOAT; + uvAccessor.count = xmodel.m_vertices.size(); + uvAccessor.type = JsonAccessorType::VEC2; + m_uv_accessor = gltf.accessors->size(); + gltf.accessors->emplace_back(uvAccessor); + + m_first_index_accessor = gltf.accessors->size(); + for (auto i = 0u; i < xmodel.m_objects.size(); i++) + { + const auto& object = xmodel.m_objects[i]; + + JsonAccessor indicesAccessor; + indicesAccessor.bufferView = m_first_index_buffer_view + i; + indicesAccessor.componentType = JsonAccessorComponentType::UNSIGNED_SHORT; + indicesAccessor.count = object.m_faces.size() * 3u; + indicesAccessor.type = JsonAccessorType::SCALAR; + + gltf.accessors->emplace_back(indicesAccessor); + } + } + + static void FillBufferData(const XModelCommon& xmodel, std::vector& bufferData) + { + const auto expectedBufferSize = GetExpectedBufferSize(xmodel); + bufferData.resize(expectedBufferSize); + + auto currentBufferOffset = 0u; + + for (const auto& commonVertex : xmodel.m_vertices) + { + auto* vertex = reinterpret_cast(&bufferData[currentBufferOffset]); + + vertex->coordinates[0] = commonVertex.coordinates[0]; + vertex->coordinates[1] = commonVertex.coordinates[2]; + vertex->coordinates[2] = -commonVertex.coordinates[1]; + + vertex->normal[0] = commonVertex.normal[0]; + vertex->normal[1] = commonVertex.normal[2]; + vertex->normal[2] = -commonVertex.normal[1]; + + vertex->uv[0] = commonVertex.uv[0]; + vertex->uv[1] = commonVertex.uv[1]; + + currentBufferOffset += sizeof(GltfVertex); + } + + for (const auto& object : xmodel.m_objects) + { + for (const auto& face : object.m_faces) + { + auto* faceIndices = reinterpret_cast(&bufferData[currentBufferOffset]); + faceIndices[0] = static_cast(face.vertexIndex[2]); + faceIndices[1] = static_cast(face.vertexIndex[1]); + faceIndices[2] = static_cast(face.vertexIndex[0]); + + currentBufferOffset += sizeof(unsigned short) * 3u; + } + } + + assert(expectedBufferSize == currentBufferOffset); + } + + static size_t GetExpectedBufferSize(const XModelCommon& xmodel) + { + auto result = 0u; + + result += xmodel.m_vertices.size() * sizeof(GltfVertex); + + for (const auto& object : xmodel.m_objects) + { + result += object.m_faces.size() * sizeof(unsigned short) * 3u; + } + + return result; + } + + void CreateBuffer(JsonRoot& gltf, const XModelCommon& xmodel, const std::vector& bufferData) const + { + if (!gltf.buffers.has_value()) + gltf.buffers.emplace(); + + JsonBuffer jsonBuffer; + jsonBuffer.byteLength = bufferData.size(); + + if (!bufferData.empty()) + jsonBuffer.uri = m_output->CreateBufferUri(bufferData.data(), bufferData.size()); + + gltf.buffers->emplace_back(std::move(jsonBuffer)); + } + unsigned m_skeleton_node = 0u; + unsigned m_position_accessor = 0u; + unsigned m_normal_accessor = 0u; + unsigned m_uv_accessor = 0u; + unsigned m_vertex_buffer_view = 0u; + unsigned m_first_index_buffer_view = 0u; + unsigned m_first_index_accessor = 0u; const Output* m_output; std::string m_game_name;