diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29307b2a..c6175165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,12 @@ name: Ubuntu CI -on: [push, pull_request] +on: + pull_request: + push: + branches: + - 'ign-common[0-9]' + - 'gz-common[0-9]' + - 'main' jobs: jammy-ci: diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 2c94852d..2332244b 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -14,4 +14,3 @@ jobs: with: project-url: https://github.com/orgs/gazebosim/projects/7 github-token: ${{ secrets.TRIAGE_TOKEN }} - diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ec210da..67db83b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,7 @@ gz_find_package( # Find GDAL gz_find_package(GDAL VERSION 3.0 PKGCONFIG gdal + PKGCONFIG_VER_COMPARISON >= PRIVATE_FOR geospatial REQUIRED_BY geospatial) @@ -117,7 +118,7 @@ configure_file("${PROJECT_SOURCE_DIR}/cppcheck.suppress.in" ${PROJECT_BINARY_DIR}/cppcheck.suppress) gz_configure_build(QUIT_IF_BUILD_ERRORS - COMPONENTS av events geospatial graphics io profiler testing) + COMPONENTS av events graphics geospatial io profiler testing) #============================================================================ # Create package information diff --git a/Changelog.md b/Changelog.md index b11aad9b..01d5bbd4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,58 @@ ## Gazebo Common 5.x +## Gazebo Common 5.5.0 (2024-02-26) + +1. Be louder when graphics is missing for geospatial + * [Pull request #573](https://github.com/gazebosim/gz-common/pull/573) + +1. Multiple memory cleanup fixes + * [Pull request #571](https://github.com/gazebosim/gz-common/pull/571) + +1. Clarify GDAL version requirement + * [Pull request #574](https://github.com/gazebosim/gz-common/pull/574) + +1. 🎈 3.17.0 + * [Pull request #567](https://github.com/gazebosim/gz-common/pull/567) + +1. Update CI badges in README + * [Pull request #566](https://github.com/gazebosim/gz-common/pull/566) + +1. Backwards compatible assimp texture name fix + * [Pull request #565](https://github.com/gazebosim/gz-common/pull/565) + +1. Fix error output when creating directories + * [Pull request #561](https://github.com/gazebosim/gz-common/pull/561) + +1. Update github action workflows + * [Pull request #558](https://github.com/gazebosim/gz-common/pull/558) + +1. Fix segfault in case of no write access to log dir + * [Pull request #546](https://github.com/gazebosim/gz-common/pull/546) + +1. ign -> gz + * [Pull request #547](https://github.com/gazebosim/gz-common/pull/547) + +1. Support loading glb with compressed jpeg textures + * [Pull request #545](https://github.com/gazebosim/gz-common/pull/545) + +1. Fix glTF / glb root node transform + * [Pull request #543](https://github.com/gazebosim/gz-common/pull/543) + +1. EnumIface: suppress deprecation warning + * [Pull request #540](https://github.com/gazebosim/gz-common/pull/540) + +1. Prevent loading lightmaps if mesh is a glb file that has an occlusion-metallic-roughness texture + * [Pull request #538](https://github.com/gazebosim/gz-common/pull/538) + +1. 🎈 3.16.0 + * [Pull request #519](https://github.com/gazebosim/gz-common/pull/519) + +1. Fix cstdint with GCC 13 + * [Pull request #528](https://github.com/gazebosim/gz-common/pull/528) + * [Pull request #517](https://github.com/gazebosim/gz-common/pull/517) + * [Pull request #513](https://github.com/gazebosim/gz-common/pull/513) + ## Gazebo Common 5.4.2 (2023-09-26) 1. Documentation fixes @@ -673,6 +725,52 @@ ## Gazebo Common 3.x +## Gazebo Common 3.17.0 (2024-01-05) + +1. Fix error output when creating directories + * [Pull request #561](https://github.com/gazebosim/gz-common/pull/561) + +1. Update github action workflows + * [Pull request #558](https://github.com/gazebosim/gz-common/pull/558) + +1. Fix segfault in case of no write access to log dir + * [Pull request #546](https://github.com/gazebosim/gz-common/pull/546) + +## Gazebo Common 3.16.0 (2023-06-05) + +1. Include cstdint to build with GCC 13 + * [Pull request #517](https://github.com/gazebosim/gz-common/pull/517) + +1. Fix missing cstdint header in latest gcc build + * [Pull request #513](https://github.com/gazebosim/gz-common/pull/513) + +1. Fix for ffmpeg v6 + * [Pull request #497](https://github.com/gazebosim/gz-common/pull/497) + +1. Include cstring for memcpy + * [Pull request #501](https://github.com/gazebosim/gz-common/pull/501) + +1. Fixed MeshManager Singleton + * [Pull request #451](https://github.com/gazebosim/gz-common/pull/451) + +1. Rename COPYING to LICENSE + * [Pull request #494](https://github.com/gazebosim/gz-common/pull/494) + +1. Add marcoag as codeowner + * [Pull request #493](https://github.com/gazebosim/gz-common/pull/493) + +1. CI workflow: use checkout v3 + * [Pull request #490](https://github.com/gazebosim/gz-common/pull/490) + +1. Improved coverage remotery + * [Pull request #467](https://github.com/gazebosim/gz-common/pull/467) + +1. Added BVH and STL loader tests + * [Pull request #466](https://github.com/gazebosim/gz-common/pull/466) + +1. Increased Image coverage + * [Pull request #465](https://github.com/gazebosim/gz-common/pull/465) + ## Gazebo Common 3.15.1 (2022-10-11) 1. Fix build on case-insensitive filesystems diff --git a/README.md b/README.md index 498c9201..628f501d 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ Build | Status -- | -- -Test coverage | [![codecov](https://codecov.io/gh/gazebosim/gz-common/branch/main/graph/badge.svg)](https://codecov.io/gh/gazebosim/gz-common) -Ubuntu Focal | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ignition_common-ci-main-focal-amd64)](https://build.osrfoundation.org/job/ignition_common-ci-main-focal-amd64) -Homebrew | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ignition_common-ci-main-homebrew-amd64)](https://build.osrfoundation.org/job/ignition_common-ci-main-homebrew-amd64) -Windows | [![Build Status](https://build.osrfoundation.org/job/ign_common-ci-win/badge/icon)](https://build.osrfoundation.org/job/ign_common-ci-win/) +Test coverage | [![codecov](https://codecov.io/gh/gazebosim/gz-common/tree/gz-common5/graph/badge.svg)](https://codecov.io/gh/gazebosim/gz-common/tree/gz-common5) +Ubuntu Jammy | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=gz_common-ci-gz-common5-jammy-amd64)](https://build.osrfoundation.org/job/gz_common-ci-gz-common5-jammy-amd64) +Homebrew | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=gz_common-ci-gz-common5-homebrew-amd64)](https://build.osrfoundation.org/job/gz_common-ci-gz-common5-homebrew-amd64) +Windows | [![Build Status](https://build.osrfoundation.org/job/gz_common-5-win/badge/icon)](https://build.osrfoundation.org/job/gz_common-5-win/) Gazebo Common, a component of [Gazebo](https://gazebosim.org), provides a set of libraries that cover many different use cases. An audio-visual library supports diff --git a/geospatial/src/CMakeLists.txt b/geospatial/src/CMakeLists.txt index f9fdcedc..bf528bee 100644 --- a/geospatial/src/CMakeLists.txt +++ b/geospatial/src/CMakeLists.txt @@ -1,24 +1,27 @@ -gz_get_libsources_and_unittests(sources gtest_sources) +if (TARGET ${PROJECT_LIBRARY_TARGET_NAME}-graphics) + gz_get_libsources_and_unittests(sources gtest_sources) + gz_add_component(geospatial + SOURCES ${sources} + DEPENDS_ON_COMPONENTS graphics + GET_TARGET_NAME geospatial_target) -gz_add_component(geospatial - SOURCES ${sources} - DEPENDS_ON_COMPONENTS graphics - GET_TARGET_NAME geospatial_target) + target_link_libraries(${geospatial_target} + PUBLIC + ${PROJECT_LIBRARY_TARGET_NAME}-graphics + gz-math${GZ_MATH_VER}::gz-math${GZ_MATH_VER} + gz-utils${GZ_UTILS_VER}::gz-utils${GZ_UTILS_VER} + PRIVATE + ${GDAL_LIBRARY}) -target_link_libraries(${geospatial_target} - PUBLIC - ${PROJECT_LIBRARY_TARGET_NAME}-graphics - gz-math${GZ_MATH_VER}::gz-math${GZ_MATH_VER} - gz-utils${GZ_UTILS_VER}::gz-utils${GZ_UTILS_VER} - PRIVATE - ${GDAL_LIBRARY}) + target_include_directories(${geospatial_target} + PRIVATE + ${GDAL_INCLUDE_DIR}) -target_include_directories(${geospatial_target} - PRIVATE - ${GDAL_INCLUDE_DIR}) - -gz_build_tests(TYPE UNIT SOURCES ${gtest_sources} - LIB_DEPS - ${geospatial_target} - gz-common${GZ_COMMON_VER}-testing -) + gz_build_tests(TYPE UNIT SOURCES ${gtest_sources} + LIB_DEPS + ${geospatial_target} + gz-common${GZ_COMMON_VER}-testing + ) +else() + message(WARNING "Skipping component [geospatial]: Missing component [graphics].\n ^~~~~ Set SKIP_geospatial=true in cmake to suppress this warning.") +endif() diff --git a/graphics/include/gz/common/Image.hh b/graphics/include/gz/common/Image.hh index 6a73d92e..69d8019a 100644 --- a/graphics/include/gz/common/Image.hh +++ b/graphics/include/gz/common/Image.hh @@ -85,7 +85,11 @@ namespace gz BAYER_GBRG8, BAYER_GRBG8, COMPRESSED_PNG, - PIXEL_FORMAT_COUNT + PIXEL_FORMAT_COUNT, + // \todo(iche033) COMPRESSED_JPEG is added at the end to + // preserve ABI compatibility. Move this enum up when merging + // forward to main + COMPRESSED_JPEG }; diff --git a/graphics/src/AssimpLoader.cc b/graphics/src/AssimpLoader.cc index 183880a5..49a9b5de 100644 --- a/graphics/src/AssimpLoader.cc +++ b/graphics/src/AssimpLoader.cc @@ -155,6 +155,15 @@ class AssimpLoader::Implementation /// calculated from the "old" parent model transform. /// \param[in] _skeleton the skeleton to work on public: void ApplyInvBindTransform(SkeletonPtr _skeleton) const; + + /// Get the updated root node transform. The function updates the original + /// transform by setting the rotation to identity if requested. + /// \param[in] _scene Scene with axes info stored in meta data + /// \param[in] _useIdentityRotation Whether to set rotation to identity. + /// Note: This is currently set to false for glTF / glb meshes. + /// \return Updated transform + public: aiMatrix4x4 UpdatedRootNodeTransform(const aiScene *_scene, + bool _useIdentityRotation = true); }; ////////////////////////////////////////////////// @@ -348,8 +357,26 @@ MaterialPtr AssimpLoader::Implementation::CreateMaterial( } float opacity = 1.0; ret = assimpMat->Get(AI_MATKEY_OPACITY, opacity); - mat->SetTransparency(1.0 - opacity); - mat->SetBlendFactors(opacity, 1.0 - opacity); + if (ret == AI_SUCCESS) + { + mat->SetTransparency(1.0 - opacity); + mat->SetBlendFactors(opacity, 1.0 - opacity); + } + +#ifndef GZ_ASSIMP_PRE_5_1_0 + // basic support for transmission - currently just overrides opacity + // \todo(iche033) The transmission factor can be used with volume + // material extension to simulate effects like refraction + // so consider also extending support for other properties like + // AI_MATKEY_VOLUME_THICKNESS_FACTOR + float transmission = 0.0; + ret = assimpMat->Get(AI_MATKEY_TRANSMISSION_FACTOR, transmission); + if (ret == AI_SUCCESS) + { + mat->SetTransparency(transmission); + } +#endif + // TODO(luca) more than one texture, Gazebo assumes UV index 0 Pbr pbr; aiString texturePath(_path.c_str()); @@ -542,26 +569,49 @@ std::pair ImagePtr AssimpLoader::Implementation::LoadEmbeddedTexture( const aiTexture* _texture) const { - auto img = std::make_shared(); if (_texture->mHeight == 0) { + Image::PixelFormatType format = Image::PixelFormatType::UNKNOWN_PIXEL_FORMAT; if (_texture->CheckFormat("png")) { - img->SetFromCompressedData((unsigned char*)_texture->pcData, - _texture->mWidth, Image::PixelFormatType::COMPRESSED_PNG); + format = Image::PixelFormatType::COMPRESSED_PNG; + } + else if (_texture->CheckFormat("jpg")) + { + format = Image::PixelFormatType::COMPRESSED_JPEG; + } + if (format != Image::PixelFormatType::UNKNOWN_PIXEL_FORMAT) + { + auto img = std::make_shared(); + img->SetFromCompressedData( + reinterpret_cast(_texture->pcData), + _texture->mWidth, format); + return img; + } + else + { + gzerr << "Unable to load embedded texture. " + << "Unsupported compressed image format" + << std::endl; } } - return img; + return ImagePtr(); } ////////////////////////////////////////////////// std::string AssimpLoader::Implementation::GenerateTextureName( const aiScene* _scene, aiMaterial* _mat, const std::string& _type) const { - return ToString(_scene->mRootNode->mName) + "_" + ToString(_mat->GetName()) + +#ifdef GZ_ASSIMP_PRE_5_2_0 + auto rootName = _scene->mRootNode->mName; +#else + auto rootName = _scene->mName; +#endif + return ToString(rootName) + "_" + ToString(_mat->GetName()) + "_" + _type; } +////////////////////////////////////////////////// SubMesh AssimpLoader::Implementation::CreateSubMesh( const aiMesh* _assimpMesh, const math::Matrix4d& _transform) const { @@ -645,16 +695,15 @@ Mesh *AssimpLoader::Load(const std::string &_filename) } auto& rootNode = scene->mRootNode; auto rootName = ToString(rootNode->mName); - auto transform = scene->mRootNode->mTransformation; - aiVector3D rootScaling, rootAxis, rootPos; - float angle; - transform.Decompose(rootScaling, rootAxis, angle, rootPos); - // drop rotation, but keep scaling and position - // TODO(luca) it seems imported assets are rotated by 90 degrees - // as documented here https://github.com/assimp/assimp/issues/849 - // remove workaround when fixed - transform = aiMatrix4x4(rootScaling, aiQuaternion(), rootPos); + // compute assimp root node transform + std::string extension = _filename.substr(_filename.rfind(".") + 1, + _filename.size()); + std::transform(extension.begin(), extension.end(), + extension.begin(), ::tolower); + bool useIdentityRotation = (extension != "glb" && extension != "glTF"); + auto transform = this->dataPtr->UpdatedRootNodeTransform(scene, + useIdentityRotation); auto rootTransform = this->dataPtr->ConvertTransform(transform); // Add the materials first @@ -756,5 +805,29 @@ void AssimpLoader::Implementation::ApplyInvBindTransform( } } +///////////////////////////////////////////////// +aiMatrix4x4 AssimpLoader::Implementation::UpdatedRootNodeTransform( + const aiScene *_scene, bool _useIdentityRotation) +{ + // Some assets apear to be rotated by 90 degrees as documented here + // https://github.com/assimp/assimp/issues/849. + auto transform = _scene->mRootNode->mTransformation; + if (_useIdentityRotation) + { + // drop rotation, but keep scaling and position + aiVector3D rootScaling, rootAxis, rootPos; + float angle; + transform.Decompose(rootScaling, rootAxis, angle, rootPos); + transform = aiMatrix4x4(rootScaling, aiQuaternion(), rootPos); + } + // for glTF / glb meshes, it was found that the transform is needed to + // produce a result that is consistent with other engines / glTF viewers. + else + { + transform = _scene->mRootNode->mTransformation; + } + return transform; +} + } } diff --git a/graphics/src/AssimpLoader_TEST.cc b/graphics/src/AssimpLoader_TEST.cc index 95902723..0c8bed06 100644 --- a/graphics/src/AssimpLoader_TEST.cc +++ b/graphics/src/AssimpLoader_TEST.cc @@ -49,6 +49,7 @@ TEST_F(AssimpLoader, LoadBox) // Make sure we can read a submesh name EXPECT_STREQ("Cube", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + delete mesh; } ///////////////////////////////////////////////// @@ -104,6 +105,8 @@ TEST_F(AssimpLoader, Material) matOpaque->BlendFactors(srcFactor, dstFactor); EXPECT_DOUBLE_EQ(1.0, srcFactor); EXPECT_DOUBLE_EQ(0.0, dstFactor); + delete mesh; + delete meshOpaque; } ///////////////////////////////////////////////// @@ -155,6 +158,7 @@ TEST_F(AssimpLoader, ShareVertices) } } } + delete mesh; } ///////////////////////////////////////////////// @@ -164,6 +168,7 @@ TEST_F(AssimpLoader, LoadZeroCount) common::Mesh *mesh = loader.Load( common::testing::TestFile("data", "zero_count.dae")); ASSERT_TRUE(mesh); + delete mesh; } ///////////////////////////////////////////////// @@ -251,6 +256,7 @@ TEST_F(AssimpLoader, TexCoordSets) subMeshB->SetTexCoordBySet(2u, math::Vector2d(0.1, 0.2), 1u); EXPECT_EQ(math::Vector2d(0.1, 0.2), subMeshB->TexCoordBySet(2u, 1u)); + delete mesh; } // Test fails for assimp below 5.2.0 @@ -289,6 +295,7 @@ TEST_F(AssimpLoader, LoadBoxWithAnimationOutsideSkeleton) 0, 0, 1, 0, 0, 0, 0, 1); EXPECT_EQ(expectedTrans, poseEnd.at("Armature")); + delete mesh; } #endif @@ -308,6 +315,7 @@ TEST_F(AssimpLoader, LoadBoxInstControllerWithoutSkeleton) common::SkeletonPtr skeleton = mesh->MeshSkeleton(); EXPECT_LT(0u, skeleton->NodeCount()); EXPECT_NE(nullptr, skeleton->NodeById("Armature_Bone")); + delete mesh; } ///////////////////////////////////////////////// @@ -335,6 +343,7 @@ TEST_F(AssimpLoader, LoadBoxMultipleInstControllers) common::SkeletonPtr skeleton = mesh->MeshSkeleton(); EXPECT_LT(0u, skeleton->NodeCount()); EXPECT_NE(nullptr, skeleton->NodeById("Armature_Bone")); + delete mesh; } ///////////////////////////////////////////////// @@ -374,6 +383,7 @@ TEST_F(AssimpLoader, LoadBoxNestedAnimation) 0, 0, 1, 0, 0, 0, 0, 1); EXPECT_EQ(expectedTrans, poseEnd.at("Armature_Bone")); + delete mesh; } ///////////////////////////////////////////////// @@ -392,6 +402,7 @@ TEST_F(AssimpLoader, LoadBoxWithDefaultStride) ASSERT_NE(mesh->MeshSkeleton(), nullptr); // TODO(luca) not working, investigate // ASSERT_EQ(1u, mesh->MeshSkeleton()->AnimationCount()); + delete mesh; } ///////////////////////////////////////////////// @@ -409,6 +420,7 @@ TEST_F(AssimpLoader, LoadBoxWithMultipleGeoms) ASSERT_EQ(2u, mesh->SubMeshCount()); EXPECT_EQ(24u, mesh->SubMeshByIndex(0).lock()->NodeAssignmentsCount()); EXPECT_EQ(0u, mesh->SubMeshByIndex(1).lock()->NodeAssignmentsCount()); + delete mesh; } ///////////////////////////////////////////////// @@ -435,6 +447,7 @@ TEST_F(AssimpLoader, LoadBoxWithHierarchicalNodes) // nested node with name EXPECT_EQ("StaticCubeNested", mesh->SubMeshByIndex(4).lock()->Name()); + delete mesh; } ///////////////////////////////////////////////// @@ -448,6 +461,7 @@ TEST_F(AssimpLoader, MergeBoxWithDoubleSkeleton) // The two skeletons have been joined and their root is the // animation root, called Scene EXPECT_EQ(skeleton_ptr->RootNode()->Name(), std::string("Scene")); + delete mesh; } // For assimp below 5.2.0 mesh loading fails because of @@ -487,6 +501,7 @@ TEST_F(AssimpLoader, LoadCylinderAnimatedFrom3dsMax) // EXPECT_EQ("Bone02", anim->Name()); EXPECT_EQ(1u, anim->NodeCount()); EXPECT_TRUE(anim->HasNode("Bone02")); + delete mesh; } #endif @@ -521,6 +536,7 @@ TEST_F(AssimpLoader, LoadObjBox) EXPECT_EQ(mat->Diffuse(), math::Color(0.512f, 0.512f, 0.512f, 1.0f)); EXPECT_EQ(mat->Specular(), math::Color(0.25, 0.25, 0.25, 1.0)); EXPECT_DOUBLE_EQ(mat->Transparency(), 0.0); + delete mesh; } @@ -536,6 +552,7 @@ TEST_F(AssimpLoader, ObjInvalidMaterial) common::Mesh *mesh = loader.Load(meshFilename); EXPECT_TRUE(mesh != nullptr); + delete mesh; } ///////////////////////////////////////////////// @@ -550,6 +567,7 @@ TEST_F(AssimpLoader, NonExistingMesh) common::Mesh *mesh = loader.Load(meshFilename); EXPECT_EQ(mesh->SubMeshCount(), 0); + delete mesh; } ///////////////////////////////////////////////// @@ -584,6 +602,7 @@ TEST_F(AssimpLoader, LoadFbxBox) EXPECT_EQ(mat->Diffuse(), math::Color(0.8f, 0.8f, 0.8f, 1.0f)); EXPECT_EQ(mat->Specular(), math::Color(0.8f, 0.8f, 0.8f, 1.0f)); EXPECT_DOUBLE_EQ(mat->Transparency(), 0.0); + delete mesh; } ///////////////////////////////////////////////// @@ -618,6 +637,7 @@ TEST_F(AssimpLoader, LoadGlTF2Box) EXPECT_EQ(mat->Diffuse(), math::Color(0.8f, 0.8f, 0.8f, 1.0f)); EXPECT_EQ(mat->Specular(), math::Color(0.0f, 0.0f, 0.0f, 1.0f)); EXPECT_DOUBLE_EQ(mat->Transparency(), 0.0); + delete mesh; } ///////////////////////////////////////////////// @@ -642,6 +662,71 @@ TEST_F(AssimpLoader, LoadGlTF2BoxExternalTexture) auto testTextureFile = common::testing::TestFile("data/gltf", "PurpleCube_Diffuse.png"); EXPECT_EQ(testTextureFile, mat->TextureImage()); + delete mesh; +} + +///////////////////////////////////////////////// +// Open a gltf mesh with transmission extension +TEST_F(AssimpLoader, LoadGlTF2BoxTransmission) +{ +#ifndef GZ_ASSIMP_PRE_5_1_0 + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box_transmission.glb")); + + EXPECT_STREQ("unknown", mesh->Name().c_str()); + + // Make sure we can read the submesh name + EXPECT_STREQ("Cube", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + + EXPECT_EQ(mesh->MaterialCount(), 1u); + + const common::MaterialPtr mat = mesh->MaterialByIndex(0u); + ASSERT_TRUE(mat.get()); + // transmission currently modeled as transparency + EXPECT_FLOAT_EQ(0.1, mat->Transparency()); + delete mesh; +#endif +} + +///////////////////////////////////////////////// +// This test loads a box glb mesh with embedded compressed jpeg texture +TEST_F(AssimpLoader, LoadGlTF2BoxWithJPEGTexture) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box_texture_jpg.glb")); + + EXPECT_STREQ("unknown", mesh->Name().c_str()); + EXPECT_EQ(math::Vector3d(1, 1, 1), mesh->Max()); + EXPECT_EQ(math::Vector3d(-1, -1, -1), mesh->Min()); + + EXPECT_EQ(24u, mesh->VertexCount()); + EXPECT_EQ(24u, mesh->NormalCount()); + EXPECT_EQ(36u, mesh->IndexCount()); + EXPECT_EQ(24u, mesh->TexCoordCount()); + EXPECT_EQ(1u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + + // Make sure we can read the submesh name + EXPECT_STREQ("Cube", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + + const common::MaterialPtr mat = mesh->MaterialByIndex(0u); + ASSERT_TRUE(mat.get()); + + // Make sure we read the material color values + EXPECT_EQ(math::Color(0.4f, 0.4f, 0.4f, 1.0f), mat->Ambient()); + EXPECT_EQ(math::Color(1.0f, 1.0f, 1.0f, 1.0f), mat->Diffuse()); + EXPECT_EQ(math::Color(0.0f, 0.0f, 0.0f, 1.0f), mat->Specular()); + // Assimp 5.2.0 and above uses the scene name for its texture names, + // older version use the root node instead. +#ifdef GZ_ASSIMP_PRE_5_2_0 + EXPECT_EQ("Cube_Material_Diffuse", mat->TextureImage()); +#else + EXPECT_EQ("Scene_Material_Diffuse", mat->TextureImage()); +#endif + EXPECT_NE(nullptr, mat->TextureData()); + delete mesh; } ///////////////////////////////////////////////// @@ -694,7 +779,6 @@ TEST_F(AssimpLoader, LoadGlbPbrAsset) EXPECT_EQ(img->Pixel(0, 0), math::Color(0.0f, 0.0f, 0.0f, 1.0f)); EXPECT_EQ(img->Pixel(100, 100), math::Color(1.0f, 1.0f, 1.0f, 1.0f)); - EXPECT_NE(pbr->NormalMapData(), nullptr); // Metallic roughness and alpha from textures only works in assimp > 5.2.0 #ifndef GZ_ASSIMP_PRE_5_2_0 @@ -726,6 +810,7 @@ TEST_F(AssimpLoader, LoadGlbPbrAsset) EXPECT_STREQ("Action1", skel->Animation(0)->Name().c_str()); EXPECT_STREQ("Action2", skel->Animation(1)->Name().c_str()); EXPECT_STREQ("Action3", skel->Animation(2)->Name().c_str()); + delete mesh; } ///////////////////////////////////////////////// @@ -743,6 +828,51 @@ TEST_F(AssimpLoader, CheckNonRootDisplacement) EXPECT_EQ(nullptr, rootNode); auto xDisplacement = skelAnim->XDisplacement(); ASSERT_TRUE(xDisplacement); + delete mesh; +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadGLTF2Triangle) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", + "multiple_texture_coordinates_triangle.glb")); + ASSERT_TRUE(mesh); + + EXPECT_EQ(6u, mesh->VertexCount()); + EXPECT_EQ(6u, mesh->NormalCount()); + EXPECT_EQ(6u, mesh->IndexCount()); + EXPECT_EQ(6u, mesh->TexCoordCount()); + EXPECT_EQ(2u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + + auto sm = mesh->SubMeshByIndex(0u); + auto subMesh = sm.lock(); + EXPECT_NE(nullptr, subMesh); + EXPECT_EQ(math::Vector3d(0, 0, 0), subMesh->Vertex(0u)); + EXPECT_EQ(math::Vector3d(10, 0, 0), subMesh->Vertex(1u)); + EXPECT_EQ(math::Vector3d(10, 10, 0), subMesh->Vertex(2u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMesh->Normal(0u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMesh->Normal(1u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMesh->Normal(2u)); + EXPECT_EQ(math::Vector2d(0, 1), subMesh->TexCoord(0u)); + EXPECT_EQ(math::Vector2d(0, 1), subMesh->TexCoord(1u)); + EXPECT_EQ(math::Vector2d(0, 1), subMesh->TexCoord(2u)); + + auto smb = mesh->SubMeshByIndex(1u); + auto subMeshB = smb.lock(); + EXPECT_NE(nullptr, subMeshB); + EXPECT_EQ(math::Vector3d(10, 0, 0), subMeshB->Vertex(0u)); + EXPECT_EQ(math::Vector3d(20, 0, 0), subMeshB->Vertex(1u)); + EXPECT_EQ(math::Vector3d(20, 10, 0), subMeshB->Vertex(2u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMeshB->Normal(0u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMeshB->Normal(1u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMeshB->Normal(2u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoord(0u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoord(1u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoord(2u)); + delete mesh; } ///////////////////////////////////////////////// diff --git a/graphics/src/CMakeLists.txt b/graphics/src/CMakeLists.txt index cb8e64f8..b6de4d97 100644 --- a/graphics/src/CMakeLists.txt +++ b/graphics/src/CMakeLists.txt @@ -23,11 +23,18 @@ gz_build_tests( ) # Assimp doesn't offer preprocessor version, use cmake to set a compatibility -# mode for versions below 5.2.0 +# mode for versions below 5.2.0 and 5.1.0 if(${GzAssimp_VERSION} STRLESS "5.2.0") message("Warning, assimp below 5.2.0 detected, setting compatibility mode") target_compile_definitions(${graphics_target} PRIVATE GZ_ASSIMP_PRE_5_2_0) if(TARGET UNIT_AssimpLoader_TEST) target_compile_definitions(UNIT_AssimpLoader_TEST PRIVATE GZ_ASSIMP_PRE_5_2_0) endif() + if(${GzAssimp_VERSION} STRLESS "5.1.0") + message("Warning, assimp below 5.1.0 detected, setting compatibility mode") + target_compile_definitions(${graphics_target} PRIVATE GZ_ASSIMP_PRE_5_1_0) + if(TARGET UNIT_AssimpLoader_TEST) + target_compile_definitions(UNIT_AssimpLoader_TEST PRIVATE GZ_ASSIMP_PRE_5_1_0) + endif() + endif() endif() diff --git a/graphics/src/ColladaExporter_TEST.cc b/graphics/src/ColladaExporter_TEST.cc index a6c7eb13..5b4851d2 100644 --- a/graphics/src/ColladaExporter_TEST.cc +++ b/graphics/src/ColladaExporter_TEST.cc @@ -112,6 +112,8 @@ TEST_F(ColladaExporter, ExportBox) meshReloaded->SubMeshByIndex(i).lock()->TexCoord(j)); } } + delete meshOriginal; + delete meshReloaded; } ///////////////////////////////////////////////// @@ -206,6 +208,8 @@ TEST_F(ColladaExporter, ExportCordlessDrill) meshReloaded->SubMeshByIndex(i).lock()->TexCoord(j)); } } + delete meshOriginal; + delete meshReloaded; } ///////////////////////////////////////////////// @@ -319,6 +323,10 @@ TEST_F(ColladaExporter, ExportMeshWithSubmeshes) EXPECT_EQ(outMesh.NormalCount(), meshReloaded->NormalCount()); EXPECT_EQ(outMesh.TexCoordCount(), meshReloaded->TexCoordCount()); + + delete boxMesh; + delete drillMesh; + delete meshReloaded; } TEST_F(ColladaExporter, ExportLights) @@ -448,4 +456,5 @@ TEST_F(ColladaExporter, ExportLights) } } EXPECT_EQ(node_with_light_count, 3); + delete meshOriginal; } diff --git a/graphics/src/ColladaLoader.cc b/graphics/src/ColladaLoader.cc index cbcb0bf7..2040222e 100644 --- a/graphics/src/ColladaLoader.cc +++ b/graphics/src/ColladaLoader.cc @@ -366,9 +366,7 @@ ColladaLoader::ColladaLoader() } ////////////////////////////////////////////////// -ColladaLoader::~ColladaLoader() -{ -} +ColladaLoader::~ColladaLoader() = default; ////////////////////////////////////////////////// Mesh *ColladaLoader::Load(const std::string &_filename) @@ -772,13 +770,16 @@ void ColladaLoader::Implementation::LoadController( { SkeletonNode *rootSkelNode = this->LoadSkeletonNodes(rootNodeXml, nullptr); + if (nullptr == skeleton) { - skeleton = SkeletonPtr(new Skeleton(rootSkelNode)); + skeleton = std::make_shared(rootSkelNode); _mesh->SetSkeleton(skeleton); } else if (nullptr != rootSkelNode) + { this->MergeSkeleton(skeleton, rootSkelNode); + } } if (nullptr == skeleton) { @@ -1191,7 +1192,7 @@ SkeletonNode *ColladaLoader::Implementation::LoadSkeletonNodes( return nullptr; } - auto node = this->LoadSingleSkeletonNode(_xml, _parent); + auto *node = this->LoadSingleSkeletonNode(_xml, _parent); this->SetSkeletonNodeTransform(_xml, node); auto childXml = _xml->FirstChildElement("node"); @@ -2800,8 +2801,14 @@ void ColladaLoader::Implementation::MergeSkeleton(SkeletonPtr _skeleton, if (currentRoot->Id() == _mergeNode->Id()) return; - if (_mergeNode->ChildById(currentRoot->Id())) + auto *rootInMergeNode = _mergeNode->ChildById(currentRoot->Id()); + + if (rootInMergeNode != nullptr) { + if (rootInMergeNode != currentRoot) + { + delete currentRoot; + } _skeleton->RootNode(_mergeNode); return; } @@ -2822,8 +2829,21 @@ void ColladaLoader::Implementation::MergeSkeleton(SkeletonPtr _skeleton, } if (mergeNodeContainsRoot) { + std::function delete_children; + delete_children = + [&delete_children](SkeletonNode *node)-> void { + for (size_t ii = 0; ii < node->ChildCount(); ++ii) + { + auto *child = node->Child(ii); + delete_children(child); + delete child; + } + }; + + // Since we are replacing the whole tree, recursively clean up + // the existing skeleton nodes _skeleton->RootNode(_mergeNode); - // TODO(anyone) since we are replacing the whole tree delete the old one + delete_children(currentRoot); delete currentRoot; return; } @@ -2834,6 +2854,7 @@ void ColladaLoader::Implementation::MergeSkeleton(SkeletonPtr _skeleton, dummyRoot = new SkeletonNode(nullptr, "dummy-root", "dummy-root"); } + if (dummyRoot != currentRoot) { dummyRoot->AddChild(currentRoot); diff --git a/graphics/src/ColladaLoader_TEST.cc b/graphics/src/ColladaLoader_TEST.cc index 1d1e9c97..87540999 100644 --- a/graphics/src/ColladaLoader_TEST.cc +++ b/graphics/src/ColladaLoader_TEST.cc @@ -49,6 +49,7 @@ TEST_F(ColladaLoader, LoadBox) // Make sure we can read a submesh name EXPECT_STREQ("Cube", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + delete mesh; } ///////////////////////////////////////////////// @@ -100,6 +101,7 @@ TEST_F(ColladaLoader, ShareVertices) } } } + delete mesh; } ///////////////////////////////////////////////// @@ -124,6 +126,7 @@ TEST_F(ColladaLoader, LoadZeroCount) EXPECT_NE(log.find("Normal source has a float_array with a count of zero"), std::string::npos); #endif + delete mesh; } ///////////////////////////////////////////////// @@ -179,6 +182,9 @@ TEST_F(ColladaLoader, Material) matOpaque->BlendFactors(srcFactor, dstFactor); EXPECT_DOUBLE_EQ(1.0, srcFactor); EXPECT_DOUBLE_EQ(0.0, dstFactor); + + delete mesh; + delete meshOpaque; } ///////////////////////////////////////////////// @@ -266,6 +272,7 @@ TEST_F(ColladaLoader, TexCoordSets) subMeshB->SetTexCoordBySet(2u, math::Vector2d(0.1, 0.2), 1u); EXPECT_EQ(math::Vector2d(0.1, 0.2), subMeshB->TexCoordBySet(2u, 1u)); + delete mesh; } ///////////////////////////////////////////////// @@ -302,6 +309,7 @@ TEST_F(ColladaLoader, LoadBoxWithAnimationOutsideSkeleton) 0, 0, 1, 0, 0, 0, 0, 1); EXPECT_EQ(expectedTrans, poseEnd.at("Armature")); + delete mesh; } ///////////////////////////////////////////////// @@ -320,6 +328,7 @@ TEST_F(ColladaLoader, LoadBoxInstControllerWithoutSkeleton) common::SkeletonPtr skeleton = mesh->MeshSkeleton(); EXPECT_LT(0u, skeleton->NodeCount()); EXPECT_NE(nullptr, skeleton->NodeById("Armature_Bone")); + delete mesh; } ///////////////////////////////////////////////// @@ -347,6 +356,7 @@ TEST_F(ColladaLoader, LoadBoxMultipleInstControllers) common::SkeletonPtr skeleton = mesh->MeshSkeleton(); EXPECT_NE(nullptr, skeleton->NodeById("Armature_Bone")); EXPECT_NE(nullptr, skeleton->NodeById("Armature_Bone2")); + delete mesh; } ///////////////////////////////////////////////// @@ -383,6 +393,7 @@ TEST_F(ColladaLoader, LoadBoxNestedAnimation) 0, 0, 1, 0, 0, 0, 0, 1); EXPECT_EQ(expectedTrans, poseEnd.at("Bone")); + delete mesh; } ///////////////////////////////////////////////// @@ -398,6 +409,7 @@ TEST_F(ColladaLoader, LoadBoxWithDefaultStride) EXPECT_EQ(1u, mesh->MaterialCount()); EXPECT_EQ(35u, mesh->TexCoordCount()); ASSERT_EQ(1u, mesh->MeshSkeleton()->AnimationCount()); + delete mesh; } ///////////////////////////////////////////////// @@ -415,6 +427,7 @@ TEST_F(ColladaLoader, LoadBoxWithMultipleGeoms) ASSERT_EQ(2u, mesh->SubMeshCount()); EXPECT_EQ(24u, mesh->SubMeshByIndex(0).lock()->NodeAssignmentsCount()); EXPECT_EQ(0u, mesh->SubMeshByIndex(1).lock()->NodeAssignmentsCount()); + delete mesh; } ///////////////////////////////////////////////// @@ -440,6 +453,7 @@ TEST_F(ColladaLoader, LoadBoxWithHierarchicalNodes) // Parent of nested node with name EXPECT_EQ("StaticCubeParent2", mesh->SubMeshByIndex(4).lock()->Name()); + delete mesh; } ///////////////////////////////////////////////// @@ -453,6 +467,7 @@ TEST_F(ColladaLoader, MergeBoxWithDoubleSkeleton) // The two skeletons have been joined and their root is the // animation root, called Armature EXPECT_EQ(skeleton_ptr->RootNode()->Name(), std::string("Armature")); + delete mesh; } ///////////////////////////////////////////////// @@ -488,6 +503,7 @@ TEST_F(ColladaLoader, LoadCylinderAnimatedFrom3dsMax) EXPECT_EQ("Bone02", anim->Name()); EXPECT_EQ(1u, anim->NodeCount()); EXPECT_TRUE(anim->HasNode("Bone02")); + delete mesh; } ///////////////////////////////////////////////// diff --git a/graphics/src/Image.cc b/graphics/src/Image.cc index a8f77d68..03aadd9d 100644 --- a/graphics/src/Image.cc +++ b/graphics/src/Image.cc @@ -268,10 +268,22 @@ void Image::SetFromCompressedData(unsigned char *_data, FreeImage_Unload(this->dataPtr->bitmap); this->dataPtr->bitmap = nullptr; - if (_format == COMPRESSED_PNG) + FREE_IMAGE_FORMAT format = FIF_UNKNOWN; + switch (_format) + { + case COMPRESSED_PNG: + format = FIF_PNG; + break; + case COMPRESSED_JPEG: + format = FIF_JPEG; + break; + default: + break; + } + if (format != FIF_UNKNOWN) { FIMEMORY *fiMem = FreeImage_OpenMemory(_data, _size); - this->dataPtr->bitmap = FreeImage_LoadFromMemory(FIF_PNG, fiMem); + this->dataPtr->bitmap = FreeImage_LoadFromMemory(format, fiMem); FreeImage_CloseMemory(fiMem); } else @@ -415,7 +427,8 @@ math::Color Image::Pixel(unsigned int _x, unsigned int _y) const << _x << " " << _y << "] \n"; return clr; } - clr.Set(firgb.rgbRed, firgb.rgbGreen, firgb.rgbBlue); + clr.Set(firgb.rgbRed / 255.0f, firgb.rgbGreen / 255.0f, + firgb.rgbBlue / 255.0f); } else { @@ -488,7 +501,8 @@ math::Color Image::MaxColor() const << x << " " << y << "] \n"; continue; } - clr.Set(firgb.rgbRed, firgb.rgbGreen, firgb.rgbBlue); + clr.Set(firgb.rgbRed / 255.0f, firgb.rgbGreen / 255.0f, + firgb.rgbBlue / 255.0f); if (clr.R() + clr.G() + clr.B() > maxClr.R() + maxClr.G() + maxClr.B()) { @@ -570,8 +584,17 @@ BOOL Image::Implementation::PixelIndex( ////////////////////////////////////////////////// void Image::Rescale(int _width, int _height) { - this->dataPtr->bitmap = FreeImage_Rescale( + auto *scaled = FreeImage_Rescale( this->dataPtr->bitmap, _width, _height, FILTER_LANCZOS3); + + if (!scaled) + { + gzerr << "Failed to rescale image\n"; + return; + } + + FreeImage_Unload(this->dataPtr->bitmap); + this->dataPtr->bitmap = scaled; } ////////////////////////////////////////////////// diff --git a/graphics/src/OBJLoader_TEST.cc b/graphics/src/OBJLoader_TEST.cc index aa2b7941..8ed7571f 100644 --- a/graphics/src/OBJLoader_TEST.cc +++ b/graphics/src/OBJLoader_TEST.cc @@ -59,6 +59,8 @@ TEST_F(OBJLoaderTest, LoadObjBox) EXPECT_EQ(mat->Diffuse(), math::Color(0.512f, 0.512f, 0.512f, 1.0f)); EXPECT_EQ(mat->Specular(), math::Color(0.25, 0.25, 0.25, 1.0)); EXPECT_DOUBLE_EQ(mat->Transparency(), 0.0); + + delete mesh; } ///////////////////////////////////////////////// @@ -73,6 +75,7 @@ TEST_F(OBJLoaderTest, InvalidMaterial) gz::common::Mesh *mesh = objLoader.Load(meshFilename); EXPECT_TRUE(mesh != nullptr); + delete mesh; } ///////////////////////////////////////////////// @@ -104,6 +107,7 @@ TEST_F(OBJLoaderTest, PBR) EXPECT_EQ("LightDome_Metalness.png", pbr->MetalnessMap()); EXPECT_EQ("LightDome_Roughness.png", pbr->RoughnessMap()); EXPECT_EQ("LightDome_Normal.png", pbr->NormalMap()); + delete mesh; } // load obj file exported by blender - it shoves pbr maps into @@ -131,5 +135,6 @@ TEST_F(OBJLoaderTest, PBR) EXPECT_EQ("mesh_Metal.png", pbr->MetalnessMap()); EXPECT_EQ("mesh_Rough.png", pbr->RoughnessMap()); EXPECT_EQ("mesh_Normal.png", pbr->NormalMap()); + delete mesh; } } diff --git a/graphics/src/STLLoader_TEST.cc b/graphics/src/STLLoader_TEST.cc index 7fd1e1d5..15189896 100644 --- a/graphics/src/STLLoader_TEST.cc +++ b/graphics/src/STLLoader_TEST.cc @@ -61,6 +61,7 @@ TEST_F(STLLoaderTest, LoadSTL) EXPECT_EQ(math::Vector3d(0, 0, -1), subMesh->Normal(2u)); EXPECT_STREQ("", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + delete mesh; mesh = loader.Load( common::testing::TestFile("data", "cube_binary.stl")); @@ -88,4 +89,5 @@ TEST_F(STLLoaderTest, LoadSTL) EXPECT_EQ(math::Vector3d(0, 0, -1), subMesh->Normal(2u)); EXPECT_STREQ("", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + delete mesh; } diff --git a/graphics/src/Skeleton.cc b/graphics/src/Skeleton.cc index cf0bfb95..f63f4f0b 100644 --- a/graphics/src/Skeleton.cc +++ b/graphics/src/Skeleton.cc @@ -72,10 +72,9 @@ Skeleton::Skeleton() ////////////////////////////////////////////////// Skeleton::Skeleton(SkeletonNode *_root) -: dataPtr(gz::utils::MakeUniqueImpl()) +: Skeleton() { - this->dataPtr->root = _root; - this->BuildNodeMap(); + this->RootNode(_root); } ////////////////////////////////////////////////// diff --git a/graphics/src/SkeletonAnimation_TEST.cc b/graphics/src/SkeletonAnimation_TEST.cc index 61e74869..eecb8a36 100644 --- a/graphics/src/SkeletonAnimation_TEST.cc +++ b/graphics/src/SkeletonAnimation_TEST.cc @@ -32,4 +32,6 @@ TEST_F(SkeletonAnimation, CheckNoXDisplacement) new common::SkeletonAnimation("emptyAnimation"); auto xDisplacement = skelAnim->XDisplacement(); ASSERT_FALSE(xDisplacement); + + delete skelAnim; } diff --git a/graphics/src/SkeletonNode.cc b/graphics/src/SkeletonNode.cc index dc562acf..b3e3e0c1 100644 --- a/graphics/src/SkeletonNode.cc +++ b/graphics/src/SkeletonNode.cc @@ -87,9 +87,7 @@ SkeletonNode::SkeletonNode(SkeletonNode *_parent, } ////////////////////////////////////////////////// -SkeletonNode::~SkeletonNode() -{ -} +SkeletonNode::~SkeletonNode() = default; ////////////////////////////////////////////////// void SkeletonNode::Name(const std::string &_name) diff --git a/profiler/src/ProfilerImpl.hh b/profiler/src/ProfilerImpl.hh index 1df02934..c8234ca7 100644 --- a/profiler/src/ProfilerImpl.hh +++ b/profiler/src/ProfilerImpl.hh @@ -18,6 +18,7 @@ #ifndef GZ_COMMON_PROFILERIMPL_HH_ #define GZ_COMMON_PROFILERIMPL_HH_ +#include #include namespace gz diff --git a/src/EnumIface_TEST.cc b/src/EnumIface_TEST.cc index 8f21f02c..1b16c626 100644 --- a/src/EnumIface_TEST.cc +++ b/src/EnumIface_TEST.cc @@ -43,7 +43,7 @@ auto myTypeIface = gz::common::gzEnum( ///////////////////////////////////////////////// TEST_F(EnumIfaceTest, StringCoversion) { - MyType type; + MyType type = MyType::TYPE2; // Set value from string myTypeIface.Set(type, "TYPE1"); diff --git a/test/data/box_texture_jpg.glb b/test/data/box_texture_jpg.glb new file mode 100644 index 00000000..6260273c Binary files /dev/null and b/test/data/box_texture_jpg.glb differ diff --git a/test/data/box_transmission.glb b/test/data/box_transmission.glb new file mode 100644 index 00000000..adc74d38 Binary files /dev/null and b/test/data/box_transmission.glb differ diff --git a/test/data/multiple_texture_coordinates_triangle.glb b/test/data/multiple_texture_coordinates_triangle.glb new file mode 100644 index 00000000..547c93e3 Binary files /dev/null and b/test/data/multiple_texture_coordinates_triangle.glb differ