diff --git a/CMakeLists.txt b/CMakeLists.txt index b012a282f..3f944d2c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/compiler_flags.cmake) # Create the library target add_library(fastgltf "src/fastgltf.cpp" "src/base64.cpp" - "include/fastgltf/base64.hpp" "include/fastgltf/glm_element_traits.hpp" "include/fastgltf/parser.hpp" "include/fastgltf/tools.hpp" "include/fastgltf/types.hpp" "include/fastgltf/util.hpp") + "include/fastgltf/base64.hpp" "include/fastgltf/glm_element_traits.hpp" "include/fastgltf/core.hpp" "include/fastgltf/tools.hpp" "include/fastgltf/types.hpp" "include/fastgltf/util.hpp") add_library(fastgltf::fastgltf ALIAS fastgltf) fastgltf_compiler_flags(fastgltf) @@ -100,7 +100,7 @@ if (ANDROID) endif() install( - FILES "include/fastgltf/base64.hpp" "include/fastgltf/glm_element_traits.hpp" "include/fastgltf/parser.hpp" "include/fastgltf/tools.hpp" "include/fastgltf/types.hpp" "include/fastgltf/util.hpp" + FILES "include/fastgltf/base64.hpp" "include/fastgltf/glm_element_traits.hpp" "include/fastgltf/core.hpp" "include/fastgltf/tools.hpp" "include/fastgltf/types.hpp" "include/fastgltf/util.hpp" DESTINATION include/fastgltf ) diff --git a/examples/gl_viewer/gl_viewer.cpp b/examples/gl_viewer/gl_viewer.cpp index fec83e969..706fd4f3d 100644 --- a/examples/gl_viewer/gl_viewer.cpp +++ b/examples/gl_viewer/gl_viewer.cpp @@ -40,7 +40,7 @@ #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" -#include +#include #include constexpr std::string_view vertexShaderSource = R"( diff --git a/include/fastgltf/parser.hpp b/include/fastgltf/core.hpp similarity index 97% rename from include/fastgltf/parser.hpp rename to include/fastgltf/core.hpp index 0be5d20ca..42c2568d8 100644 --- a/include/fastgltf/parser.hpp +++ b/include/fastgltf/core.hpp @@ -762,6 +762,34 @@ namespace fastgltf { void setBase64DecodeCallback(Base64DecodeCallback* decodeCallback) noexcept; void setUserPointer(void* pointer) noexcept; }; + + /** + * A composer for one or more glTF files. + */ + class Composer { + Error errorCode = Error::None; + const Asset* asset = nullptr; + + void writeAccessors(std::string& json); + void writeBuffers(std::string& json); + void writeBufferViews(std::string& json); + void writeCameras(std::string& json); + void writeImages(std::string& json); + void writeLights(std::string& json); + void writeMaterials(std::string& json); + void writeMeshes(std::string& json); + void writeNodes(std::string& json); + void writeSamplers(std::string& json); + void writeScenes(std::string& json); + void writeSkins(std::string& json); + void writeTextures(std::string& json); + + public: + [[nodiscard]] Error getError() const; + + std::string writeGLTF(const Asset* asset); + void writeBinaryGLTF(const Asset* asset); + }; } // namespace fastgltf #ifdef _MSC_VER diff --git a/include/fastgltf/types.hpp b/include/fastgltf/types.hpp index 704237149..c4c9fe08e 100644 --- a/include/fastgltf/types.hpp +++ b/include/fastgltf/types.hpp @@ -375,6 +375,23 @@ namespace fastgltf { return AccessorType::Invalid; } } + + static constexpr std::array accessorTypeNames = { + "SCALAR", + "VEC2", + "VEC3", + "VEC4", + "MAT2", + "MAT3", + "MAT4" + }; + + constexpr std::string_view getAccessorTypeName(AccessorType type) noexcept { + if (type == AccessorType::Invalid) + return ""; + auto idx = to_underlying(type) & 0xFF; + return accessorTypeNames[idx - 1]; + } #pragma endregion #pragma region Containers @@ -1315,9 +1332,9 @@ namespace fastgltf { }; struct Skin { - FASTGLTF_FG_PMR_NS::MaybeSmallVector joints; - Optional skeleton; Optional inverseBindMatrices; + Optional skeleton; + FASTGLTF_FG_PMR_NS::MaybeSmallVector joints; FASTGLTF_STD_PMR_NS::string name; }; diff --git a/src/fastgltf.cpp b/src/fastgltf.cpp index 54f180e75..43cb82cfa 100644 --- a/src/fastgltf.cpp +++ b/src/fastgltf.cpp @@ -52,9 +52,7 @@ static_assert(std::string_view { SIMDJSON_TARGET_VERSION } == SIMDJSON_VERSION, "Outdated version of simdjson. Reconfigure project to update."); #endif -#include -#include -#include +#include #include #if defined(FASTGLTF_IS_X86) @@ -866,6 +864,14 @@ fg::Error fg::validate(const fastgltf::Asset& asset) { } } + for (const auto& light : asset.lights) { + if (light.type != LightType::Spot) { + if (light.innerConeAngle.has_value() || light.outerConeAngle.has_value()) { + return Error::InvalidGltf; + } + } + } + for (const auto& material : asset.materials) { auto isInvalidTexture = [&textures = asset.textures](std::optional textureIndex) { return textureIndex.has_value() && textureIndex.value() >= textures.size(); @@ -1027,7 +1033,11 @@ fg::Error fg::validate(const fastgltf::Asset& asset) { return Error::InvalidGltf; } - if (node.skinIndex.has_value() && node.meshIndex.has_value()) { + if ((node.skinIndex.has_value() || !node.weights.empty()) && !node.meshIndex.has_value()) { + return Error::InvalidGltf; + } + + if (node.skinIndex.has_value()) { // "When the node contains skin, all mesh.primitives MUST contain JOINTS_0 and WEIGHTS_0 attributes." const auto& mesh = asset.meshes[node.meshIndex.value()]; for (const auto& primitive : mesh.primitives) { @@ -3471,6 +3481,595 @@ void fg::Parser::setUserPointer(void* pointer) noexcept { } #pragma endregion +#pragma region Composer +fg::Error fg::Composer::getError() const { + return errorCode; +} + +void fg::Composer::writeAccessors(std::string& json) { + if (asset->accessors.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += R"("accessors":[)"; + for (auto it = asset->accessors.begin(); it != asset->accessors.end(); ++it) { + json += '{'; + + json += "\"byteOffset\":" + std::to_string(it->byteOffset) + ','; + json += "\"count\":" + std::to_string(it->count) + ','; + json += R"("type":")" + std::string(getAccessorTypeName(it->type)) + "\","; + json += "\"componentType\":" + std::to_string(getGLComponentType(it->componentType)); + + if (it->bufferViewIndex.has_value()) { + json += ",\"bufferView\":" + std::to_string(it->bufferViewIndex.value()); + } + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + + json += '}'; + if (std::distance(asset->accessors.begin(), it) + 1 < asset->accessors.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeBuffers(std::string& json) { + if (asset->buffers.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"buffers\":["; + for (auto it = asset->buffers.begin(); it != asset->buffers.end(); ++it) { + json += '{'; + + std::visit(visitor { + [&](auto arg) { + // Covers BufferView and CustomBuffer. + errorCode = Error::InvalidGltf; + }, + [&](const sources::Vector& vector) { + // Either we write to a file or we encode the data as a base64 data uri. + }, + [&](const sources::ByteView& view) { + // Same as above. + }, + [&](const sources::URI& uri) { + json += std::string(R"("uri":")") + std::string(uri.uri.string()) + '"' + ','; + } + }, it->data); + + json += "\"byteLength\":" + std::to_string(it->byteLength); + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + json += '}'; + if (std::distance(asset->buffers.begin(), it) + 1 < asset->buffers.size()) + json += ','; + } + json += "]"; +} + +void fg::Composer::writeBufferViews(std::string& json) { + if (asset->bufferViews.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"bufferViews\":["; + for (auto it = asset->bufferViews.begin(); it != asset->bufferViews.end(); ++it) { + json += '{'; + + json += "\"buffer\":" + std::to_string(it->bufferIndex) + ','; + json += "\"byteOffset\":" + std::to_string(it->byteOffset) + ','; + json += "\"byteLength\":" + std::to_string(it->byteLength); + + if (it->byteStride.has_value()) { + json += ",\"byteStride\":" + std::to_string(it->byteStride.value()); + } + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + + json += '}'; + if (std::distance(asset->bufferViews.begin(), it) + 1 < asset->bufferViews.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeCameras(std::string& json) { + if (asset->cameras.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"cameras\":["; + for (auto it = asset->cameras.begin(); it != asset->cameras.end(); ++it) { + json += '{'; + + std::visit(visitor { + [](auto arg) {}, + [&](const Camera::Perspective& perspective) { + json += "\"perspective\":{"; + + if (perspective.aspectRatio.has_value()) { + json += "\"aspectRatio\":" + std::to_string(perspective.aspectRatio.value()) + ','; + } + + json += "\"yfov\":" + std::to_string(perspective.yfov) + ','; + + if (perspective.zfar.has_value()) { + json += "\"zfar\":" + std::to_string(perspective.zfar.value()) + ','; + } + + json += "\"znear\":" + std::to_string(perspective.znear); + + json += R"(},"type":"perspective")"; + }, + [&](const Camera::Orthographic& orthographic) { + json += "\"orthographic\":{"; + json += "\"xmag\":" + std::to_string(orthographic.xmag) + ','; + json += "\"ymag\":" + std::to_string(orthographic.ymag) + ','; + json += "\"zfar\":" + std::to_string(orthographic.zfar) + ','; + json += "\"znear\":" + std::to_string(orthographic.znear) + ','; + json += R"(},"type":"orthographic")"; + } + }, it->camera); + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + + json += '}'; + if (std::distance(asset->cameras.begin(), it) + 1 < asset->cameras.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeImages(std::string& json) { + if (asset->images.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"images\":["; + for (auto it = asset->images.begin(); it != asset->images.end(); ++it) { + json += '{'; + + std::visit(visitor { + [&](auto arg) { + errorCode = Error::InvalidGltf; + }, + [&](const sources::URI& uri) { + json += std::string(R"("uri":")") + std::string(uri.uri.string()) + '"'; + }, + }, it->data); + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + json += '}'; + if (std::distance(asset->images.begin(), it) + 1 < asset->images.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeLights(std::string& json) { + if (asset->lights.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"lights\":["; + for (auto it = asset->lights.begin(); it != asset->lights.end(); ++it) { + json += '{'; + + // [1.0f, 1.0f, 1.0f] is the default. + if (it->color[0] != 1.0f && it->color[1] != 1.0f && it->color[2] != 1.0f) { + json += R"("color":[)"; + json += std::to_string(it->color[0]) + ',' + std::to_string(it->color[1]) + ',' + std::to_string(it->color[2]); + json += "],"; + } + + if (it->intensity != 1.0f) { + json += R"("intensity":)" + std::to_string(it->intensity) + ','; + } + + switch (it->type) { + case LightType::Directional: { + json += R"("type":"directional")"; + break; + } + case LightType::Spot: { + json += R"("type":"spot")"; + break; + } + case LightType::Point: { + json += R"("type":"point")"; + break; + } + } + + if (it->range.has_value()) { + json += R"(,"range":)" + std::to_string(it->range.value()); + } + + if (it->type == LightType::Spot) { + if (it->innerConeAngle.has_value()) + json += R"("innerConeAngle":)" + std::to_string(it->innerConeAngle.value()) + ','; + + if (it->outerConeAngle.has_value()) + json += R"("outerConeAngle":)" + std::to_string(it->outerConeAngle.value()) + ','; + } + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + json += '}'; + if (std::distance(asset->lights.begin(), it) + 1 < asset->lights.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeMaterials(std::string& json) { + if (asset->materials.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"materials\":["; + for (auto it = asset->materials.begin(); it != asset->materials.end(); ++it) { + json += '{'; + + if (it->alphaMode != AlphaMode::Opaque) { + if (json.back() != '{') json += ','; + json += R"("alphaMode":)"; + if (it->alphaMode == AlphaMode::Blend) { + json += "\"BLEND\""; + } else if (it->alphaMode == AlphaMode::Mask) { + json += "\"MASK\""; + } + } + + if (it->alphaCutoff != 0.5f) { + if (json.back() != '{') json += ','; + json += R"("alphaCutoff":)" + std::to_string(it->alphaCutoff); + } + + if (it->doubleSided) { + if (json.back() != '{') json += ','; + json += R"("doubleSided":true)"; + } + + if (!it->name.empty()) { + if (json.back() != '{') json += ','; + json += R"("name":")" + it->name + '"'; + } + json += '}'; + if (std::distance(asset->materials.begin(), it) + 1 < asset->materials.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeMeshes(std::string& json) { + if (asset->meshes.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"meshes\":["; + for (auto it = asset->meshes.begin(); it != asset->meshes.end(); ++it) { + json += '{'; + + json += R"("primitives":[)"; + auto itp = it->primitives.begin(); + while (itp != it->primitives.end()) { + json += '{'; + + { + json += R"("attributes":{)"; + for (auto ita = itp->attributes.begin(); ita != itp->attributes.end(); ++ita) { + json += '"' + std::string(ita->first) + "\":" + std::to_string(ita->second); + if (std::distance(itp->attributes.begin(), ita) + 1 < itp->attributes.size()) + json += ','; + } + json += '}'; + } + + if (itp->indicesAccessor.has_value()) { + json += R"(,"indices":)" + std::to_string(itp->indicesAccessor.value()); + } + + if (itp->materialIndex.has_value()) { + json += R"(,"material":)" + std::to_string(itp->materialIndex.value()); + } + + if (itp->type != PrimitiveType::Triangles) { + json += R"(,"type":)" + std::to_string(to_underlying(itp->type)); + } + + json += '}'; + ++itp; + if (std::distance(it->primitives.begin(), itp) < it->primitives.size()) + json += ','; + } + json += ']'; + + if (!it->weights.empty()) { + if (json.back() != ',') + json += ','; + json += R"("weights":[)"; + auto itw = it->weights.begin(); + while (itw != it->weights.end()) { + json += std::to_string(*itw); + ++itw; + if (std::distance(it->weights.begin(), itw) < it->weights.size()) + json += ','; + } + json += ']'; + } + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + json += '}'; + if (std::distance(asset->meshes.begin(), it) + 1 < asset->meshes.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeNodes(std::string& json) { + if (asset->nodes.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"nodes\":["; + for (auto it = asset->nodes.begin(); it != asset->nodes.end(); ++it) { + json += '{'; + + if (it->meshIndex.has_value()) { + json += R"("mesh":)" + std::to_string(it->meshIndex.value()) + ","; + } + if (it->cameraIndex.has_value()) { + json += R"("camera":)" + std::to_string(it->cameraIndex.value()) + ","; + } + if (it->skinIndex.has_value()) { + json += R"("skin":)" + std::to_string(it->skinIndex.value()) + ","; + } + + if (!it->children.empty()) { + json += R"("children":[)"; + auto itc = it->children.begin(); + while (itc != it->children.end()) { + json += std::to_string(*itc); + ++itc; + if (std::distance(it->children.begin(), itc) < it->children.size()) + json += ','; + } + json += ']'; + } + + if (!it->weights.empty()) { + if (json.back() != ',') + json += ','; + json += R"("weights":[)"; + auto itw = it->weights.begin(); + while (itw != it->weights.end()) { + json += std::to_string(*itw); + ++itw; + if (std::distance(it->weights.begin(), itw) < it->weights.size()) + json += ','; + } + json += ']'; + } + + std::visit(visitor { + [&](const Node::TRS& trs) { + if (json.back() != ',') + json += ','; + // TODO: Don't write when values are defaulted. + json += R"("rotation":[)"; + json += std::to_string(trs.rotation[0]) + ',' + std::to_string(trs.rotation[1]) + ',' + std::to_string(trs.rotation[2]) + ',' + std::to_string(trs.rotation[3]); + json += "],"; + + json += R"("scale":[)"; + json += std::to_string(trs.scale[0]) + ',' + std::to_string(trs.scale[1]) + ',' + std::to_string(trs.scale[2]); + json += "],"; + + json += R"("translation":[)"; + json += std::to_string(trs.translation[0]) + ',' + std::to_string(trs.translation[1]) + ',' + std::to_string(trs.translation[2]); + json += "]"; + }, + [&](const Node::TransformMatrix& matrix) { + if (json.back() != ',') + json += ','; + json += R"("matrix":[)"; + for (std::size_t i = 0; i < matrix.size(); ++i) { + json += std::to_string(matrix[i]); + if (i + 1 < matrix.size()) { + json += ','; + } + } + json += ']'; + }, + }, it->transform); + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + json += '}'; + if (std::distance(asset->nodes.begin(), it) + 1 < asset->nodes.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeSamplers(std::string& json) { + if (asset->samplers.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"samplers\":["; + for (auto it = asset->samplers.begin(); it != asset->samplers.end(); ++it) { + json += '{'; + + if (it->magFilter.has_value()) { + json += R"("magFilter":)" + std::to_string(to_underlying(it->magFilter.value())) + ','; + } + if (it->minFilter.has_value()) { + json += R"("minFilter":)" + std::to_string(to_underlying(it->minFilter.value())) + ','; + } + json += R"("wrapS":)" + std::to_string(to_underlying(it->wrapS)) + ','; + json += R"("wrapT":)" + std::to_string(to_underlying(it->wrapT)); + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + json += '}'; + if (std::distance(asset->samplers.begin(), it) + 1 < asset->samplers.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeScenes(std::string& json) { + if (asset->scenes.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"scenes\":["; + for (auto it = asset->scenes.begin(); it != asset->scenes.end(); ++it) { + json += '{'; + + json += R"("nodes":[)"; + auto itn = it->nodeIndices.begin(); + while (itn != it->nodeIndices.end()) { + json += std::to_string(*itn); + ++itn; + if (std::distance(it->nodeIndices.begin(), itn) < it->nodeIndices.size()) + json += ','; + } + json += ']'; + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + json += '}'; + if (std::distance(asset->scenes.begin(), it) + 1 < asset->scenes.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeSkins(std::string& json) { + if (asset->skins.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"skins\":["; + for (auto it = asset->skins.begin(); it != asset->skins.end(); ++it) { + json += '{'; + + if (it->inverseBindMatrices.has_value()) + json += R"("inverseBindMatrices":)" + std::to_string(it->inverseBindMatrices.value()) + ','; + + if (it->skeleton.has_value()) + json += R"("skeleton":)" + std::to_string(it->skeleton.value()) + ','; + + json += R"("joints":[)"; + auto itj = it->joints.begin(); + while (itj != it->joints.end()) { + json += std::to_string(*itj); + ++itj; + if (std::distance(it->joints.begin(), itj) < it->joints.size()) + json += ','; + } + json += ']'; + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + json += '}'; + if (std::distance(asset->skins.begin(), it) + 1 < asset->skins.size()) + json += ','; + } + json += ']'; +} + +void fg::Composer::writeTextures(std::string& json) { + if (asset->textures.empty()) + return; + if (json.back() == ']' || json.back() == '}') + json += ','; + + json += "\"textures\":["; + for (auto it = asset->textures.begin(); it != asset->textures.end(); ++it) { + json += '{'; + + if (it->samplerIndex.has_value()) + json += R"("sampler":)" + std::to_string(it->samplerIndex.value()); + + if (!it->name.empty()) + json += R"(,"name":")" + it->name + '"'; + json += '}'; + if (std::distance(asset->textures.begin(), it) + 1 < asset->textures.size()) + json += ','; + } + json += ']'; +} + +std::string fg::Composer::writeGLTF(const fastgltf::Asset* pAsset) { + assert(pAsset != nullptr); + asset = pAsset; + + // Fairly rudimentary approach of just composing the JSON string using a std::string. + std::string outputString; + + outputString += "{"; + + // Write asset info + outputString += "\"asset\":{"; + if (asset->assetInfo.has_value()) { + if (!asset->assetInfo->copyright.empty()) + outputString += R"("copyright":")" + asset->assetInfo->copyright + "\","; + if (!asset->assetInfo->generator.empty()) + outputString += R"("generator":")" + asset->assetInfo->generator + "\","; + outputString += R"("version":")" + asset->assetInfo->gltfVersion + '"'; + } else { + outputString += R"("generator":"fastgltf",)"; + outputString += R"("version":"2.0")"; + } + outputString += '}'; + + writeAccessors(outputString); + writeBuffers(outputString); + writeBufferViews(outputString); + writeCameras(outputString); + writeImages(outputString); + writeLights(outputString); + writeMaterials(outputString); + writeMeshes(outputString); + writeNodes(outputString); + writeSamplers(outputString); + writeScenes(outputString); + writeSkins(outputString); + writeTextures(outputString); + + outputString += "}"; + + return outputString; +} + +void fg::Composer::writeBinaryGLTF(const fastgltf::Asset* asset) { + assert(asset != nullptr); + +} +#pragma endregion + #ifdef _MSC_VER #pragma warning(pop) #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9bf836762..2aea3d3ec 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,7 +3,7 @@ set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL TRUE) # We want these tests to be a optional executable. add_executable(fastgltf_tests EXCLUDE_FROM_ALL "base64_tests.cpp" "basic_test.cpp" "benchmarks.cpp" "glb_tests.cpp" "gltf_path.hpp" - "vector_tests.cpp" "uri_tests.cpp" "extension_tests.cpp" "accessor_tests.cpp") + "vector_tests.cpp" "uri_tests.cpp" "extension_tests.cpp" "accessor_tests.cpp" "composer_tests.cpp") target_compile_features(fastgltf_tests PRIVATE cxx_std_17) target_link_libraries(fastgltf_tests PRIVATE fastgltf) target_link_libraries(fastgltf_tests PRIVATE glm::glm Catch2::Catch2WithMain) diff --git a/tests/accessor_tests.cpp b/tests/accessor_tests.cpp index 2ad5caa06..677951dcc 100644 --- a/tests/accessor_tests.cpp +++ b/tests/accessor_tests.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include "gltf_path.hpp" diff --git a/tests/base64_tests.cpp b/tests/base64_tests.cpp index de3d171a8..0c763110a 100644 --- a/tests/base64_tests.cpp +++ b/tests/base64_tests.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include "gltf_path.hpp" constexpr std::string_view testBase64 = "SGVsbG8gV29ybGQuIEhlbGxvIFdvcmxkLiBIZWxsbyBXb3JsZC4="; diff --git a/tests/basic_test.cpp b/tests/basic_test.cpp index 11369e121..88aafdac1 100644 --- a/tests/basic_test.cpp +++ b/tests/basic_test.cpp @@ -15,7 +15,7 @@ #include #include -#include +#include #include #include "gltf_path.hpp" diff --git a/tests/benchmarks.cpp b/tests/benchmarks.cpp index 28555da79..39a4ce02a 100644 --- a/tests/benchmarks.cpp +++ b/tests/benchmarks.cpp @@ -6,7 +6,7 @@ #include "simdjson.h" -#include +#include #include #include "gltf_path.hpp" diff --git a/tests/composer_tests.cpp b/tests/composer_tests.cpp new file mode 100644 index 000000000..b49640c63 --- /dev/null +++ b/tests/composer_tests.cpp @@ -0,0 +1,41 @@ +#include + +#include +#include "gltf_path.hpp" + +TEST_CASE("Test simple glTF composition", "[composer-tests]") { + fastgltf::BufferView bufferView = {}; + bufferView.bufferIndex = 0; + bufferView.byteStride = 4; + bufferView.byteLength = 16; + + fastgltf::Asset asset; + asset.bufferViews.emplace_back(std::move(bufferView)); + + fastgltf::Composer composer; + auto string = composer.writeGLTF(&asset); + REQUIRE(composer.getError() == fastgltf::Error::None); + REQUIRE(!string.empty()); +} + +#include + +TEST_CASE("Read glTF, write it, and then read it again and validate", "[composer-tests]") { + auto cubePath = sampleModels / "2.0" / "Cube" / "glTF"; + auto cubeJsonData = std::make_unique(); + REQUIRE(cubeJsonData->loadFromFile(cubePath / "Cube.gltf")); + + fastgltf::Parser parser; + auto cube = parser.loadGLTF(cubeJsonData.get(), cubePath); + REQUIRE(cube.error() == fastgltf::Error::None); + REQUIRE(fastgltf::validate(cube.get()) == fastgltf::Error::None); + + fastgltf::Composer composer; + auto json = composer.writeGLTF(&(cube.get())); + + fastgltf::GltfDataBuffer cube2JsonData; + cube2JsonData.copyBytes(reinterpret_cast(json.data()), json.size()); + auto cube2 = parser.loadGLTF(&cube2JsonData, cubePath); + REQUIRE(cube2.error() == fastgltf::Error::None); + REQUIRE(fastgltf::validate(cube2.get()) == fastgltf::Error::None); +} diff --git a/tests/extension_tests.cpp b/tests/extension_tests.cpp index 491cd7d1e..40bfdafa1 100644 --- a/tests/extension_tests.cpp +++ b/tests/extension_tests.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include "gltf_path.hpp" TEST_CASE("Loading KHR_texture_basisu glTF files", "[gltf-loader]") { diff --git a/tests/glb_tests.cpp b/tests/glb_tests.cpp index 6756227ce..791b91da9 100644 --- a/tests/glb_tests.cpp +++ b/tests/glb_tests.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include "gltf_path.hpp" diff --git a/tests/uri_tests.cpp b/tests/uri_tests.cpp index b6268fd0e..01c115af7 100644 --- a/tests/uri_tests.cpp +++ b/tests/uri_tests.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include #include "gltf_path.hpp"