initial commit-moved from vulkan_guide

This commit is contained in:
2025-10-10 22:53:54 +09:00
commit 8853429937
2484 changed files with 973414 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
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")
target_compile_features(fastgltf_tests PRIVATE cxx_std_17)
target_link_libraries(fastgltf_tests PRIVATE fastgltf fastgltf_simdjson)
target_link_libraries(fastgltf_tests PRIVATE glm::glm Catch2::Catch2WithMain)
fastgltf_compiler_flags(fastgltf_tests)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/deps/catch2")
add_subdirectory(deps/catch2)
target_link_libraries(fastgltf_tests PRIVATE Catch2::Catch2)
endif()
# We only use tinygltf to compare against.
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/gltf_loaders/tinygltf/tiny_gltf.h")
message(STATUS "fastgltf: Found tinygltf")
set(TINYGLTF_INSTALL OFF CACHE BOOL "")
set(TINYGLTF_BUILD_LOADER_EXAMPLE OFF CACHE BOOL "")
set(TINYGLTF_HEADER_ONLY ON CACHE BOOL "")
add_subdirectory(gltf_loaders/tinygltf)
target_link_libraries(fastgltf_tests PRIVATE tinygltf)
target_compile_definitions(fastgltf_tests PRIVATE HAS_TINYGLTF=1)
if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/gltf_loaders/RapidJSON")
# RapidJSON's CMake is weird
message(STATUS "fastgltf: Found RapidJSON")
target_include_directories(fastgltf_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/gltf_loaders/RapidJSON/include")
target_compile_definitions(fastgltf_tests PRIVATE HAS_RAPIDJSON=1 TINYGLTF_USE_RAPIDJSON=1 TINYGLTF_NO_INCLUDE_RAPIDJSON)
endif()
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/gltf_loaders/cgltf/cgltf.h")
message(STATUS "fastgltf: Found cgltf")
target_include_directories(fastgltf_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/gltf_loaders/cgltf")
target_compile_definitions(fastgltf_tests PRIVATE HAS_CGLTF=1)
endif()
if (FASTGLTF_ENABLE_GLTF_RS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/gltf-rs/src/lib.rs")
message(STATUS "fastgltf: Found gltf-rs")
corrosion_import_crate(MANIFEST_PATH gltf-rs/Cargo.toml)
corrosion_add_cxxbridge(gltf-rs-bridge CRATE gltf_rs MANIFEST_PATH gltf-rs FILES lib.rs)
target_link_libraries(fastgltf_tests PUBLIC gltf-rs-bridge)
target_compile_definitions(fastgltf_tests PRIVATE HAS_GLTFRS=1)
endif()
if(FASTGLTF_ENABLE_ASSIMP AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/gltf_loaders/assimp")
message(STATUS "fastgltf: Found assimp")
# Only enable glTF importer
set(ASSIMP_NO_EXPORT ON CACHE BOOL "")
set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "")
set(ASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT OFF CACHE BOOL "")
set(ASSIMP_BUILD_GLTF_IMPORTER ON CACHE BOOL "")
add_subdirectory(gltf_loaders/assimp)
target_link_libraries(fastgltf_tests PRIVATE assimp::assimp)
target_compile_definitions(fastgltf_tests PRIVATE HAS_ASSIMP=1)
endif()
fastgltf_add_source_directory(TARGET fastgltf_tests FOLDER ".")

4
third_party/fastgltf/tests/README.md vendored Normal file
View File

@@ -0,0 +1,4 @@
# tests
The tests are written with C++20 and [Catch2](https://github.com/catchorg/Catch2). To run, one also
needs to init the submodules and therefore download the [glTF-Sample-Models](https://github.com/KhronosGroup/glTF-Sample-Models/).

View File

@@ -0,0 +1,218 @@
#include <catch2/catch_test_macros.hpp>
#include <glm/vec3.hpp>
#include <glm/gtc/epsilon.hpp>
#include <glm/ext/scalar_constants.hpp>
#include <fastgltf/parser.hpp>
#include <fastgltf/tools.hpp>
#include "gltf_path.hpp"
template<>
struct fastgltf::ElementTraits<glm::vec3> : fastgltf::ElementTraitsBase<glm::vec3, AccessorType::Vec3, float> {};
static const std::byte* getBufferData(const fastgltf::Buffer& buffer) {
const std::byte* result = nullptr;
std::visit(fastgltf::visitor {
[](auto&) {},
[&](const fastgltf::sources::Vector& vec) {
result = reinterpret_cast<const std::byte*>(vec.bytes.data());
},
[&](const fastgltf::sources::ByteView& bv) {
result = bv.bytes.data();
},
}, buffer.data);
return result;
}
TEST_CASE("Test data type conversion", "[gltf-tools]") {
// normalized int-to-float and normalized float-to-int
for (auto i = std::numeric_limits<std::int8_t>::min(); i < std::numeric_limits<std::int8_t>::max(); ++i) {
auto converted = fastgltf::internal::convertComponent<float>(i, true);
REQUIRE(glm::epsilonEqual<float>(converted, fastgltf::max<float>(i / 127.0f, -1), glm::epsilon<float>()));
REQUIRE(fastgltf::internal::convertComponent<std::int8_t>(converted, true) == std::round(converted * 127.0f));
}
for (auto i = std::numeric_limits<std::uint8_t>::min(); i < std::numeric_limits<std::uint8_t>::max(); ++i) {
auto converted = fastgltf::internal::convertComponent<float>(i, true);
REQUIRE(glm::epsilonEqual<float>(converted, i / 255.0f, glm::epsilon<float>()));
REQUIRE(fastgltf::internal::convertComponent<std::uint8_t>(converted, true) == std::round(converted * 255.0f));
}
for (auto i = std::numeric_limits<std::int16_t>::min(); i < std::numeric_limits<std::int16_t>::max(); ++i) {
auto converted = fastgltf::internal::convertComponent<float>(i, true);
REQUIRE(glm::epsilonEqual<float>(converted, fastgltf::max<float>(i / 32767.0f, -1), glm::epsilon<float>()));
REQUIRE(fastgltf::internal::convertComponent<std::int16_t>(converted, true) == std::round(converted * 32767.0f));
}
for (auto i = std::numeric_limits<std::uint16_t>::min(); i < std::numeric_limits<std::uint16_t>::max(); ++i) {
auto converted = fastgltf::internal::convertComponent<float>(i, true);
REQUIRE(glm::epsilonEqual<float>(converted, i / 65535.0f, glm::epsilon<float>()));
REQUIRE(fastgltf::internal::convertComponent<std::uint16_t>(converted, true) == std::round(converted * 65535.0f));
}
}
TEST_CASE("Test accessor", "[gltf-tools]") {
auto lightsLamp = sampleModels / "2.0" / "LightsPunctualLamp" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(lightsLamp / "LightsPunctualLamp.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::KHR_lights_punctual);
auto asset = parser.loadGLTF(&jsonData, lightsLamp, fastgltf::Options::LoadExternalBuffers,
fastgltf::Category::Buffers | fastgltf::Category::BufferViews | fastgltf::Category::Accessors);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(asset->accessors.size() == 15);
auto& accessors = asset->accessors;
SECTION("getAccessorElement<std::uint16_t>") {
auto& firstAccessor = accessors[0];
REQUIRE(firstAccessor.type == fastgltf::AccessorType::Scalar);
REQUIRE(firstAccessor.componentType == fastgltf::ComponentType::UnsignedShort);
REQUIRE(firstAccessor.bufferViewIndex.has_value());
auto& view = asset->bufferViews[*firstAccessor.bufferViewIndex];
auto* bufferData = getBufferData(asset->buffers[view.bufferIndex]);
REQUIRE(bufferData != nullptr);
auto* checkData = reinterpret_cast<const std::uint16_t*>(bufferData + view.byteOffset
+ firstAccessor.byteOffset);
REQUIRE(*checkData == fastgltf::getAccessorElement<std::uint16_t>(asset.get(), firstAccessor, 0));
}
{
auto& secondAccessor = accessors[1];
REQUIRE(secondAccessor.type == fastgltf::AccessorType::Vec3);
REQUIRE(secondAccessor.componentType == fastgltf::ComponentType::Float);
REQUIRE(secondAccessor.bufferViewIndex.has_value());
auto& view = asset->bufferViews[*secondAccessor.bufferViewIndex];
auto* bufferData = getBufferData(asset->buffers[view.bufferIndex]);
REQUIRE(bufferData != nullptr);
auto* checkData = reinterpret_cast<const glm::vec3*>(bufferData + view.byteOffset
+ secondAccessor.byteOffset);
SECTION("getAccessorElement<glm::vec3>") {
REQUIRE(*checkData == fastgltf::getAccessorElement<glm::vec3>(asset.get(), secondAccessor, 0));
}
SECTION("iterateAccessor") {
auto dstCopy = std::make_unique<glm::vec3[]>(secondAccessor.count);
std::size_t i = 0;
fastgltf::iterateAccessor<glm::vec3>(asset.get(), secondAccessor, [&](auto&& v3) {
dstCopy[i++] = std::forward<glm::vec3>(v3);
});
REQUIRE(std::memcmp(dstCopy.get(), checkData, secondAccessor.count * sizeof(glm::vec3)) == 0);
}
SECTION("copyFromAccessor") {
auto dstCopy = std::make_unique<glm::vec3[]>(secondAccessor.count);
fastgltf::copyFromAccessor<glm::vec3>(asset.get(), secondAccessor, dstCopy.get());
REQUIRE(std::memcmp(dstCopy.get(), checkData, secondAccessor.count * sizeof(glm::vec3)) == 0);
}
SECTION("Iterator test") {
auto dstCopy = std::make_unique<glm::vec3[]>(secondAccessor.count);
auto accessor = fastgltf::iterateAccessor<glm::vec3>(asset.get(), secondAccessor);
for (auto it = accessor.begin(); it != accessor.end(); ++it) {
dstCopy[std::distance(accessor.begin(), it)] = *it;
}
REQUIRE(std::memcmp(dstCopy.get(), checkData, secondAccessor.count * sizeof(glm::vec3)) == 0);
}
}
}
TEST_CASE("Test sparse accessor", "[gltf-tools]") {
auto simpleSparseAccessor = sampleModels / "2.0" / "SimpleSparseAccessor" / "glTF";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(simpleSparseAccessor / "SimpleSparseAccessor.gltf"));
fastgltf::Parser parser;
auto asset = parser.loadGLTF(jsonData.get(), simpleSparseAccessor, fastgltf::Options::LoadExternalBuffers,
fastgltf::Category::Buffers | fastgltf::Category::BufferViews | fastgltf::Category::Accessors);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(asset->accessors.size() == 2);
REQUIRE(!asset->accessors[0].sparse.has_value());
REQUIRE(asset->accessors[1].sparse.has_value());
auto& sparse = asset->accessors[1].sparse.value();
REQUIRE(sparse.count == 3);
REQUIRE(sparse.indicesBufferView == 2);
REQUIRE(sparse.indicesByteOffset == 0);
REQUIRE(sparse.valuesBufferView == 3);
REQUIRE(sparse.valuesByteOffset == 0);
REQUIRE(sparse.indexComponentType == fastgltf::ComponentType::UnsignedShort);
auto& secondAccessor = asset->accessors[1];
auto& viewIndices = asset->bufferViews[secondAccessor.sparse->indicesBufferView];
auto& viewValues = asset->bufferViews[secondAccessor.sparse->valuesBufferView];
auto& viewData = asset->bufferViews[*secondAccessor.bufferViewIndex];
auto* bufferData = getBufferData(asset->buffers[viewData.bufferIndex]) + viewData.byteOffset
+ secondAccessor.byteOffset;
auto dataStride = viewData.byteStride ? *viewData.byteStride
: fastgltf::getElementByteSize(secondAccessor.type, secondAccessor.componentType);
auto* dataIndices = reinterpret_cast<const std::uint16_t*>(getBufferData(asset->buffers[viewIndices.bufferIndex])
+ viewIndices.byteOffset + secondAccessor.sparse->indicesByteOffset);
auto* dataValues = reinterpret_cast<const glm::vec3*>(getBufferData(asset->buffers[viewValues.bufferIndex])
+ viewValues.byteOffset + secondAccessor.sparse->valuesByteOffset);
auto checkValues = std::make_unique<glm::vec3[]>(secondAccessor.count);
for (std::size_t i = 0, sparseIndex = 0; i < secondAccessor.count; ++i) {
if (sparseIndex < secondAccessor.sparse->count && dataIndices[sparseIndex] == i) {
checkValues[i] = dataValues[sparseIndex];
++sparseIndex;
} else {
checkValues[i] = *reinterpret_cast<const glm::vec3*>(bufferData + dataStride * i);
}
}
SECTION("getAccessorElement") {
for (std::size_t i = 0; i < secondAccessor.count; ++i) {
REQUIRE(checkValues[i] == fastgltf::getAccessorElement<glm::vec3>(asset.get(), secondAccessor, i));
}
}
SECTION("iterateAccessor") {
auto dstCopy = std::make_unique<glm::vec3[]>(secondAccessor.count);
std::size_t i = 0;
fastgltf::iterateAccessor<glm::vec3>(asset.get(), secondAccessor, [&](auto&& v3) {
dstCopy[i++] = std::forward<glm::vec3>(v3);
});
REQUIRE(std::memcmp(dstCopy.get(), checkValues.get(), secondAccessor.count * sizeof(glm::vec3)) == 0);
}
SECTION("iterateAccessor with idx") {
auto dstCopy = std::make_unique<glm::vec3[]>(secondAccessor.count);
fastgltf::iterateAccessorWithIndex<glm::vec3>(asset.get(), secondAccessor, [&](auto&& v3, std::size_t i) {
dstCopy[i] = std::forward<glm::vec3>(v3);
});
REQUIRE(std::memcmp(dstCopy.get(), checkValues.get(), secondAccessor.count * sizeof(glm::vec3)) == 0);
}
SECTION("copyFromAccessor") {
auto dstCopy = std::make_unique<glm::vec3[]>(secondAccessor.count);
fastgltf::copyFromAccessor<glm::vec3>(asset.get(), secondAccessor, dstCopy.get());
REQUIRE(std::memcmp(dstCopy.get(), checkValues.get(), secondAccessor.count * sizeof(glm::vec3)) == 0);
}
SECTION("Iterator test") {
auto dstCopy = std::make_unique<glm::vec3[]>(secondAccessor.count);
auto accessor = fastgltf::iterateAccessor<glm::vec3>(asset.get(), secondAccessor);
for (auto it = accessor.begin(); it != accessor.end(); ++it) {
dstCopy[std::distance(accessor.begin(), it)] = *it;
}
REQUIRE(std::memcmp(dstCopy.get(), checkValues.get(), secondAccessor.count * sizeof(glm::vec3)) == 0);
}
}

View File

@@ -0,0 +1,115 @@
#include <fstream>
#include <sstream>
#include <catch2/catch_test_macros.hpp>
#include <catch2/benchmark/catch_benchmark.hpp>
#include <fastgltf/base64.hpp>
#include <fastgltf/types.hpp>
#include <fastgltf/parser.hpp>
#include "gltf_path.hpp"
constexpr std::string_view testBase64 = "SGVsbG8gV29ybGQuIEhlbGxvIFdvcmxkLiBIZWxsbyBXb3JsZC4=";
TEST_CASE("Check base64 utility functions", "[base64]") {
REQUIRE(fastgltf::base64::getPadding("Li==") == 2);
REQUIRE(fastgltf::base64::getPadding("Li4=") == 1);
REQUIRE(fastgltf::base64::getPadding("Li4u") == 0);
REQUIRE(fastgltf::base64::getOutputSize(4, 0) == 3); // Li4u
REQUIRE(fastgltf::base64::getOutputSize(4, 1) == 2); // Li4=
REQUIRE(fastgltf::base64::getOutputSize(4, 2) == 1); // Li==
}
TEST_CASE("Check base64 decoding", "[base64]") {
// This is "Hello World. Hello World.". The decode function
// uses the best possible SIMD version of the algorithm.
auto bytes = fastgltf::base64::decode(testBase64);
std::string strings(bytes.begin(), bytes.end());
REQUIRE(strings == "Hello World. Hello World. Hello World.");
}
TEST_CASE("Check all base64 decoders", "[base64]") {
// Checks that the base64 decoders return the same.
auto bytes = fastgltf::base64::fallback_decode(testBase64);
std::string strings(bytes.begin(), bytes.end());
REQUIRE(strings == "Hello World. Hello World. Hello World.");
#if defined(__x86_64__) || defined(_M_AMD64) || defined(_M_IX86)
REQUIRE(bytes == fastgltf::base64::avx2_decode(testBase64));
REQUIRE(bytes == fastgltf::base64::sse4_decode(testBase64));
#endif
#if defined(__aarch64__)
REQUIRE(bytes == fastgltf::base64::neon_decode(testBase64));
#endif
}
TEST_CASE("Check big base64 data decoding", "[base64]") {
std::ifstream file(path / "base64.txt");
REQUIRE(file.is_open());
std::stringstream buffer;
buffer << file.rdbuf();
auto encodedBytes = buffer.str();
auto bytes = fastgltf::base64::decode(encodedBytes);
REQUIRE(!bytes.empty());
std::ifstream output(path / "base64.txt.out", std::ios::binary | std::ios::ate);
REQUIRE(output.is_open());
std::vector<uint8_t> decodedBytes(output.tellg());
output.seekg(0);
output.read(reinterpret_cast<char*>(decodedBytes.data()), static_cast<std::streamsize>(decodedBytes.size()));
REQUIRE(bytes == decodedBytes);
}
TEST_CASE("Test base64 buffer decoding", "[base64]") {
fastgltf::Parser parser;
fastgltf::Image texture;
std::string bufferData;
auto cylinderEngine = sampleModels / "2.0" / "2CylinderEngine" / "glTF-Embedded";
auto boxTextured = sampleModels / "2.0" / "BoxTextured" / "glTF-Embedded";
auto tceJsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(tceJsonData->loadFromFile(cylinderEngine / "2CylinderEngine.gltf"));
auto btJsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(btJsonData->loadFromFile(boxTextured / "BoxTextured.gltf"));
SECTION("Validate large buffer load from glTF") {
auto asset = parser.loadGLTF(tceJsonData.get(), cylinderEngine, fastgltf::Options::None, fastgltf::Category::Buffers);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(asset->buffers.size() == 1);
// Load the buffer from the parsed glTF file.
auto& buffer = asset->buffers.front();
REQUIRE(buffer.byteLength == 1794612);
auto bufferVector = std::get_if<fastgltf::sources::Vector>(&buffer.data);
REQUIRE(bufferVector != nullptr);
REQUIRE(bufferVector->mimeType == fastgltf::MimeType::OctetStream);
REQUIRE(!bufferVector->bytes.empty());
}
SECTION("Validate base64 buffer and image load from glTF") {
auto asset = parser.loadGLTF(btJsonData.get(), boxTextured, fastgltf::Options::None, fastgltf::Category::Images | fastgltf::Category::Buffers);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(asset->buffers.size() == 1);
REQUIRE(asset->images.size() == 1);
auto& buffer = asset->buffers.front();
REQUIRE(buffer.byteLength == 840);
auto bufferVector = std::get_if<fastgltf::sources::Vector>(&buffer.data);
REQUIRE(bufferVector != nullptr);
REQUIRE(bufferVector->mimeType == fastgltf::MimeType::OctetStream);
REQUIRE(!bufferVector->bytes.empty());
auto& image = asset->images.front();
auto imageVector = std::get_if<fastgltf::sources::Vector>(&image.data);
REQUIRE(imageVector != nullptr);
REQUIRE(imageVector->mimeType == fastgltf::MimeType::PNG);
REQUIRE(!imageVector->bytes.empty());
}
}

View File

@@ -0,0 +1,583 @@
#include <algorithm>
#include <cstdlib>
#include <random>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/benchmark/catch_benchmark.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtx/matrix_decompose.hpp>
#include <fastgltf/base64.hpp>
#include <fastgltf/parser.hpp>
#include <fastgltf/types.hpp>
#include "gltf_path.hpp"
constexpr auto noOptions = fastgltf::Options::None;
TEST_CASE("Component type tests", "[gltf-loader]") {
using namespace fastgltf;
// clang-format off
REQUIRE(fastgltf::getNumComponents(AccessorType::Scalar) == 1);
REQUIRE(fastgltf::getNumComponents(AccessorType::Vec2) == 2);
REQUIRE(fastgltf::getNumComponents(AccessorType::Vec3) == 3);
REQUIRE(fastgltf::getNumComponents(AccessorType::Vec4) == 4);
REQUIRE(fastgltf::getNumComponents(AccessorType::Mat2) == 4);
REQUIRE(fastgltf::getNumComponents(AccessorType::Mat3) == 9);
REQUIRE(fastgltf::getNumComponents(AccessorType::Mat4) == 16);
REQUIRE(fastgltf::getComponentBitSize(ComponentType::Byte) == 8);
REQUIRE(fastgltf::getComponentBitSize(ComponentType::UnsignedByte) == 8);
REQUIRE(fastgltf::getComponentBitSize(ComponentType::Short) == 16);
REQUIRE(fastgltf::getComponentBitSize(ComponentType::UnsignedShort) == 16);
REQUIRE(fastgltf::getComponentBitSize(ComponentType::UnsignedInt) == 32);
REQUIRE(fastgltf::getComponentBitSize(ComponentType::Float) == 32);
REQUIRE(fastgltf::getComponentBitSize(ComponentType::Double) == 64);
REQUIRE(fastgltf::getComponentBitSize(ComponentType::Invalid) == 0);
REQUIRE(fastgltf::getElementByteSize(AccessorType::Scalar, ComponentType::Byte) == 1);
REQUIRE(fastgltf::getElementByteSize(AccessorType::Vec4, ComponentType::Byte) == 4);
REQUIRE(fastgltf::getElementByteSize(AccessorType::Vec4, ComponentType::Short) == 8);
REQUIRE(fastgltf::getComponentType(5120) == ComponentType::Byte);
REQUIRE(fastgltf::getComponentType(5121) == ComponentType::UnsignedByte);
REQUIRE(fastgltf::getComponentType(5122) == ComponentType::Short);
REQUIRE(fastgltf::getComponentType(5123) == ComponentType::UnsignedShort);
REQUIRE(fastgltf::getComponentType(5125) == ComponentType::UnsignedInt);
REQUIRE(fastgltf::getComponentType(5126) == ComponentType::Float);
REQUIRE(fastgltf::getComponentType(5130) == ComponentType::Double);
REQUIRE(fastgltf::getComponentType(5131) == ComponentType::Invalid);
// clang-format on
}
TEST_CASE("Test all variants of CRC32-C hashing", "[gltf-loader]") {
// TODO: Determine SSE4.2 support here.
for (std::size_t i = 0; i < 256; ++i) {
// Generate a random string up to 256 chars long.
static constexpr std::string_view chars =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
static std::mt19937 rng(std::random_device{}());
static std::uniform_int_distribution<std::string::size_type> pick(0, chars.size() - 1);
std::string str(i, '\0');
for (std::size_t j = 0; j < i; ++j)
str[j] = chars[pick(rng)];
#if defined(__x86_64__) || defined(_M_AMD64) || defined(_M_IX86)
// We'll try and test if the hardware accelerated version generates the same, correct results.
REQUIRE(fastgltf::crc32c(str) == fastgltf::hwcrc32c(str));
#endif
}
}
TEST_CASE("Test extension stringification", "[gltf-loader]") {
auto stringified = stringifyExtension(fastgltf::Extensions::EXT_meshopt_compression);
REQUIRE(stringified == fastgltf::extensions::EXT_meshopt_compression);
stringified = stringifyExtension(fastgltf::Extensions::EXT_meshopt_compression | fastgltf::Extensions::EXT_texture_webp);
REQUIRE(stringified == fastgltf::extensions::EXT_meshopt_compression);
}
TEST_CASE("Test if glTF type detection works", "[gltf-loader]") {
fastgltf::Parser parser;
SECTION("glTF") {
auto gltfPath = sampleModels / "2.0" / "ABeautifulGame" / "glTF";
REQUIRE(std::filesystem::exists(gltfPath));
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(gltfPath / "ABeautifulGame.gltf"));
REQUIRE(fastgltf::determineGltfFileType(&jsonData) == fastgltf::GltfType::glTF);
auto model = parser.loadGLTF(&jsonData, gltfPath);
REQUIRE(model.error() == fastgltf::Error::None);
REQUIRE(model.get_if() != nullptr);
REQUIRE(fastgltf::validate(model.get()) == fastgltf::Error::None);
}
SECTION("GLB") {
auto glbPath = sampleModels / "2.0" / "BoomBox" / "glTF-Binary";
REQUIRE(std::filesystem::exists(glbPath));
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(glbPath / "BoomBox.glb"));
REQUIRE(fastgltf::determineGltfFileType(&jsonData) == fastgltf::GltfType::GLB);
auto model = parser.loadBinaryGLTF(&jsonData, glbPath);
REQUIRE(model.error() == fastgltf::Error::None);
REQUIRE(model.get_if() != nullptr);
}
SECTION("Invalid") {
auto gltfPath = path / "base64.txt"; // Random file in the test directory that's not a glTF file.
REQUIRE(std::filesystem::exists(gltfPath));
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(gltfPath));
REQUIRE(fastgltf::determineGltfFileType(&jsonData) == fastgltf::GltfType::Invalid);
}
}
TEST_CASE("Loading some basic glTF", "[gltf-loader]") {
fastgltf::Parser parser;
SECTION("Loading basic invalid glTF files") {
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(path / "empty_json.gltf"));
auto emptyGltf = parser.loadGLTF(jsonData.get(), path);
REQUIRE(emptyGltf.error() == fastgltf::Error::InvalidOrMissingAssetField);
}
SECTION("Load basic glTF file") {
auto basicJsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(basicJsonData->loadFromFile(path / "basic_gltf.gltf"));
auto basicGltf = parser.loadGLTF(basicJsonData.get(), path);
REQUIRE(basicGltf.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(basicGltf.get()) == fastgltf::Error::None);
}
SECTION("Loading basic Cube.gltf") {
auto cubePath = sampleModels / "2.0" / "Cube" / "glTF";
auto cubeJsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(cubeJsonData->loadFromFile(cubePath / "Cube.gltf"));
auto cube = parser.loadGLTF(cubeJsonData.get(), cubePath, noOptions, fastgltf::Category::OnlyRenderable);
REQUIRE(cube.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(cube.get()) == fastgltf::Error::None);
REQUIRE(cube->scenes.size() == 1);
REQUIRE(cube->scenes.front().nodeIndices.size() == 1);
REQUIRE(cube->scenes.front().nodeIndices.front() == 0);
REQUIRE(cube->nodes.size() == 1);
REQUIRE(cube->nodes.front().name == "Cube");
REQUIRE(std::holds_alternative<fastgltf::Node::TRS>(cube->nodes.front().transform));
REQUIRE(cube->accessors.size() == 5);
REQUIRE(cube->accessors[0].type == fastgltf::AccessorType::Scalar);
REQUIRE(cube->accessors[0].componentType == fastgltf::ComponentType::UnsignedShort);
REQUIRE(cube->accessors[1].type == fastgltf::AccessorType::Vec3);
REQUIRE(cube->accessors[1].componentType == fastgltf::ComponentType::Float);
REQUIRE(cube->bufferViews.size() == 5);
REQUIRE(cube->buffers.size() == 1);
REQUIRE(cube->materials.size() == 1);
auto& material = cube->materials.front();
REQUIRE(material.name == "Cube");
REQUIRE(material.pbrData.baseColorTexture.has_value());
REQUIRE(material.pbrData.baseColorTexture->textureIndex == 0);
REQUIRE(material.pbrData.metallicRoughnessTexture.has_value());
REQUIRE(material.pbrData.metallicRoughnessTexture->textureIndex == 1);
REQUIRE(!material.normalTexture.has_value());
REQUIRE(!material.emissiveTexture.has_value());
REQUIRE(!material.occlusionTexture.has_value());
}
SECTION("Loading basic Box.gltf") {
auto boxPath = sampleModels / "2.0" / "Box" / "glTF";
auto boxJsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(boxJsonData->loadFromFile(boxPath / "Box.gltf"));
auto box = parser.loadGLTF(boxJsonData.get(), boxPath, noOptions, fastgltf::Category::OnlyRenderable);
REQUIRE(box.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(box.get()) == fastgltf::Error::None);
REQUIRE(box->defaultScene.has_value());
REQUIRE(box->defaultScene.value() == 0);
REQUIRE(box->nodes.size() == 2);
REQUIRE(box->nodes[0].children.size() == 1);
REQUIRE(box->nodes[0].children[0] == 1);
REQUIRE(box->nodes[1].children.empty());
REQUIRE(box->nodes[1].meshIndex.has_value());
REQUIRE(box->nodes[1].meshIndex.value() == 0);
REQUIRE(box->materials.size() == 1);
REQUIRE(box->materials[0].name == "Red");
REQUIRE(box->materials[0].pbrData.baseColorFactor[3] == 1.0f);
REQUIRE(box->materials[0].pbrData.metallicFactor == 0.0f);
}
}
TEST_CASE("Loading glTF animation", "[gltf-loader]") {
auto animatedCube = sampleModels / "2.0" / "AnimatedCube" / "glTF";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(animatedCube / "AnimatedCube.gltf"));
fastgltf::Parser parser;
auto asset = parser.loadGLTF(jsonData.get(), animatedCube, noOptions, fastgltf::Category::OnlyAnimations);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(!asset->animations.empty());
auto& animation = asset->animations.front();
REQUIRE(animation.name == "animation_AnimatedCube");
REQUIRE(!animation.channels.empty());
REQUIRE(animation.channels.front().nodeIndex == 0);
REQUIRE(animation.channels.front().samplerIndex == 0);
REQUIRE(animation.channels.front().path == fastgltf::AnimationPath::Rotation);
REQUIRE(!animation.samplers.empty());
REQUIRE(animation.samplers.front().interpolation == fastgltf::AnimationInterpolation::Linear);
REQUIRE(animation.samplers.front().inputAccessor == 0);
REQUIRE(animation.samplers.front().outputAccessor == 1);
}
TEST_CASE("Loading glTF skins", "[gltf-loader]") {
auto simpleSkin = sampleModels / "2.0" / "SimpleSkin" / "glTF";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(simpleSkin / "SimpleSkin.gltf"));
fastgltf::Parser parser;
auto asset = parser.loadGLTF(jsonData.get(), simpleSkin, noOptions, fastgltf::Category::Skins | fastgltf::Category::Nodes);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(!asset->skins.empty());
auto& skin = asset->skins.front();
REQUIRE(skin.joints.size() == 2);
REQUIRE(skin.joints[0] == 1);
REQUIRE(skin.joints[1] == 2);
REQUIRE(skin.inverseBindMatrices.has_value());
REQUIRE(skin.inverseBindMatrices.value() == 4);
REQUIRE(!asset->nodes.empty());
auto& node = asset->nodes.front();
REQUIRE(node.skinIndex.has_value());
REQUIRE(node.skinIndex == 0);
}
TEST_CASE("Loading glTF cameras", "[gltf-loader]") {
auto cameras = sampleModels / "2.0" / "Cameras" / "glTF";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(cameras / "Cameras.gltf"));
fastgltf::Parser parser;
auto asset = parser.loadGLTF(jsonData.get(), cameras, noOptions, fastgltf::Category::Cameras);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->cameras.size() == 2);
REQUIRE(std::holds_alternative<fastgltf::Camera::Perspective>(asset->cameras[0].camera));
REQUIRE(std::holds_alternative<fastgltf::Camera::Orthographic>(asset->cameras[1].camera));
const auto* pPerspective = std::get_if<fastgltf::Camera::Perspective>(&asset->cameras[0].camera);
REQUIRE(pPerspective != nullptr);
REQUIRE(pPerspective->aspectRatio == 1.0f);
REQUIRE(pPerspective->yfov == 0.7f);
REQUIRE(pPerspective->zfar == 100);
REQUIRE(pPerspective->znear == 0.01f);
const auto* pOrthographic = std::get_if<fastgltf::Camera::Orthographic>(&asset->cameras[1].camera);
REQUIRE(pOrthographic != nullptr);
REQUIRE(pOrthographic->xmag == 1.0f);
REQUIRE(pOrthographic->ymag == 1.0f);
REQUIRE(pOrthographic->zfar == 100);
REQUIRE(pOrthographic->znear == 0.01f);
}
TEST_CASE("Validate whole glTF", "[gltf-loader]") {
auto sponza = sampleModels / "2.0" / "Sponza" / "glTF";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(sponza / "Sponza.gltf"));
fastgltf::Parser parser;
auto model = parser.loadGLTF(jsonData.get(), sponza);
REQUIRE(model.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(model.get()) == fastgltf::Error::None);
auto brainStem = sampleModels / "2.0" / "BrainStem" / "glTF";
jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(brainStem / "BrainStem.gltf"));
auto model2 = parser.loadGLTF(jsonData.get(), brainStem);
REQUIRE(model2.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(model2.get()) == fastgltf::Error::None);
}
TEST_CASE("Test allocation callbacks for embedded buffers", "[gltf-loader]") {
auto boxPath = sampleModels / "2.0" / "Box" / "glTF-Embedded";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(boxPath / "Box.gltf"));
std::vector<void*> allocations;
auto mapCallback = [](uint64_t bufferSize, void* userPointer) -> fastgltf::BufferInfo {
REQUIRE(userPointer != nullptr);
auto* allocations = static_cast<std::vector<void*>*>(userPointer);
allocations->emplace_back(std::malloc(bufferSize));
return fastgltf::BufferInfo {
allocations->back(),
allocations->size() - 1,
};
};
fastgltf::Parser parser;
parser.setUserPointer(&allocations);
parser.setBufferAllocationCallback(mapCallback, nullptr);
auto asset = parser.loadGLTF(jsonData.get(), boxPath, noOptions, fastgltf::Category::Buffers);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(allocations.size() == 1);
REQUIRE(asset->buffers.size() == 1);
auto& buffer = asset->buffers.front();
const auto* customBuffer = std::get_if<fastgltf::sources::CustomBuffer>(&buffer.data);
REQUIRE(customBuffer != nullptr);
REQUIRE(customBuffer->id == 0);
for (auto& allocation : allocations) {
REQUIRE(allocation != nullptr);
std::free(allocation);
}
}
TEST_CASE("Test base64 decoding callbacks", "[gltf-loader]") {
auto boxPath = sampleModels / "2.0" / "Box" / "glTF-Embedded";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(boxPath / "Box.gltf"));
size_t decodeCounter = 0;
auto decodeCallback = [](std::string_view encodedData, uint8_t* outputData, size_t padding, size_t outputSize, void* userPointer) {
(*static_cast<size_t*>(userPointer))++;
fastgltf::base64::decode_inplace(encodedData, outputData, padding);
};
fastgltf::Parser parser;
parser.setUserPointer(&decodeCounter);
parser.setBase64DecodeCallback(decodeCallback);
auto model = parser.loadGLTF(jsonData.get(), boxPath, noOptions, fastgltf::Category::Buffers);
REQUIRE(model.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(model.get()) == fastgltf::Error::None);
REQUIRE(decodeCounter != 0);
}
TEST_CASE("Test TRS parsing and optional decomposition", "[gltf-loader]") {
SECTION("Test decomposition on glTF asset") {
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(path / "transform_matrices.gltf"));
// Parse once without decomposing, once with decomposing the matrix.
fastgltf::Parser parser;
auto assetWithMatrix = parser.loadGLTF(jsonData.get(), path, noOptions, fastgltf::Category::Nodes | fastgltf::Category::Cameras);
REQUIRE(assetWithMatrix.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(assetWithMatrix.get()) == fastgltf::Error::None);
auto assetDecomposed = parser.loadGLTF(jsonData.get(), path, fastgltf::Options::DecomposeNodeMatrices, fastgltf::Category::Nodes | fastgltf::Category::Cameras);
REQUIRE(assetDecomposed.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(assetDecomposed.get()) == fastgltf::Error::None);
REQUIRE(assetWithMatrix->cameras.size() == 1);
REQUIRE(assetDecomposed->cameras.size() == 1);
REQUIRE(assetWithMatrix->nodes.size() == 2);
REQUIRE(assetDecomposed->nodes.size() == 2);
REQUIRE(std::holds_alternative<fastgltf::Node::TransformMatrix>(assetWithMatrix->nodes.back().transform));
REQUIRE(std::holds_alternative<fastgltf::Node::TRS>(assetDecomposed->nodes.back().transform));
// Get the TRS components from the first node and use them as the test data for decomposing.
const auto* pDefaultTRS = std::get_if<fastgltf::Node::TRS>(&assetWithMatrix->nodes.front().transform);
REQUIRE(pDefaultTRS != nullptr);
auto translation = glm::make_vec3(pDefaultTRS->translation.data());
auto rotation = glm::make_quat(pDefaultTRS->rotation.data());
auto scale = glm::make_vec3(pDefaultTRS->scale.data());
auto rotationMatrix = glm::toMat4(rotation);
auto transform = glm::translate(glm::mat4(1.0f), translation) * rotationMatrix * glm::scale(glm::mat4(1.0f), scale);
// Check if the parsed matrix is correct.
const auto* pMatrix = std::get_if<fastgltf::Node::TransformMatrix>(&assetWithMatrix->nodes.back().transform);
REQUIRE(pMatrix != nullptr);
REQUIRE(glm::make_mat4x4(pMatrix->data()) == transform);
// Check if the decomposed components equal the original components.
const auto* pDecomposedTRS = std::get_if<fastgltf::Node::TRS>(&assetDecomposed->nodes.back().transform);
REQUIRE(glm::make_vec3(pDecomposedTRS->translation.data()) == translation);
REQUIRE(glm::make_quat(pDecomposedTRS->rotation.data()) == rotation);
REQUIRE(glm::make_vec3(pDecomposedTRS->scale.data()) == scale);
}
SECTION("Test decomposition against glm decomposition") {
// Some random complex transform matrix from one of the glTF sample models.
std::array<float, 16> matrix = {
-0.4234085381031037F,
-0.9059388637542724F,
-7.575183536001616e-11F,
0.0F,
-0.9059388637542724F,
0.4234085381031037F,
-4.821281221478735e-11F,
0.0F,
7.575183536001616e-11F,
4.821281221478735e-11F,
-1.0F,
0.0F,
-90.59386444091796F,
-24.379817962646489F,
-40.05522918701172F,
1.0F
};
std::array<float, 3> translation = {}, scale = {};
std::array<float, 4> rotation = {};
fastgltf::decomposeTransformMatrix(matrix, scale, rotation, translation);
auto glmMatrix = glm::make_mat4x4(matrix.data());
glm::vec3 glmScale, glmTranslation, glmSkew;
glm::quat glmRotation;
glm::vec4 glmPerspective;
glm::decompose(glmMatrix, glmScale, glmRotation, glmTranslation, glmSkew, glmPerspective);
// I use glm::epsilon<float>() * 10 here because some matrices I tested this with resulted
// in an error margin greater than the normal epsilon value. I will investigate this in the
// future, but I suspect using double in the decompose functions should help mitigate most
// of it.
REQUIRE(glm::make_vec3(translation.data()) == glmTranslation);
REQUIRE(glm::all(glm::epsilonEqual(glm::make_quat(rotation.data()), glmRotation, glm::epsilon<float>() * 10)));
REQUIRE(glm::all(glm::epsilonEqual(glm::make_vec3(scale.data()), glmScale, glm::epsilon<float>())));
}
}
TEST_CASE("Validate sparse accessor parsing", "[gltf-loader]") {
auto simpleSparseAccessor = sampleModels / "2.0" / "SimpleSparseAccessor" / "glTF";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(simpleSparseAccessor / "SimpleSparseAccessor.gltf"));
fastgltf::Parser parser;
auto asset = parser.loadGLTF(jsonData.get(), simpleSparseAccessor, noOptions, fastgltf::Category::Accessors);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->accessors.size() == 2);
REQUIRE(!asset->accessors[0].sparse.has_value());
REQUIRE(asset->accessors[1].sparse.has_value());
auto& sparse = asset->accessors[1].sparse.value();
REQUIRE(sparse.count == 3);
REQUIRE(sparse.indicesBufferView == 2);
REQUIRE(sparse.indicesByteOffset == 0);
REQUIRE(sparse.valuesBufferView == 3);
REQUIRE(sparse.valuesByteOffset == 0);
REQUIRE(sparse.indexComponentType == fastgltf::ComponentType::UnsignedShort);
}
TEST_CASE("Validate morph target parsing", "[gltf-loader]") {
auto simpleMorph = sampleModels / "2.0" / "SimpleMorph" / "glTF";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(simpleMorph / "SimpleMorph.gltf"));
fastgltf::Parser parser;
auto asset = parser.loadGLTF(jsonData.get(), simpleMorph, noOptions, fastgltf::Category::Meshes);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->meshes.size() == 1);
REQUIRE(asset->meshes.front().weights.size() == 2);
REQUIRE(asset->meshes.front().primitives.size() == 1);
auto& primitive = asset->meshes.front().primitives.front();
auto position = primitive.findAttribute("POSITION");
REQUIRE(position != primitive.attributes.end());
REQUIRE((*position).second == 1);
REQUIRE(primitive.targets.size() == 2);
auto positionTarget0 = primitive.findTargetAttribute(0, "POSITION");
REQUIRE(positionTarget0 != primitive.targets[0].end());
REQUIRE((*positionTarget0).second == 2);
auto positionTarget1 = primitive.findTargetAttribute(1, "POSITION");
REQUIRE(positionTarget0 != primitive.targets[1].end());
REQUIRE((*positionTarget1).second == 3);
}
TEST_CASE("Test accessors min/max", "[gltf-loader]") {
auto lightsLamp = sampleModels / "2.0" / "LightsPunctualLamp" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(lightsLamp / "LightsPunctualLamp.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::KHR_lights_punctual);
auto asset = parser.loadGLTF(&jsonData, lightsLamp, noOptions, fastgltf::Category::Accessors);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(std::find_if(asset->extensionsUsed.begin(), asset->extensionsUsed.end(), [](auto& string) {
return string == fastgltf::extensions::KHR_lights_punctual;
}) != asset->extensionsUsed.end());
REQUIRE(asset->accessors.size() == 15);
auto& accessors = asset->accessors;
{
auto& firstAccessor = accessors[0];
const auto* max = std::get_if<FASTGLTF_STD_PMR_NS::vector<std::int64_t>>(&firstAccessor.max);
const auto* min = std::get_if<FASTGLTF_STD_PMR_NS::vector<std::int64_t>>(&firstAccessor.min);
REQUIRE(max != nullptr);
REQUIRE(min != nullptr);
REQUIRE(max->size() == fastgltf::getNumComponents(firstAccessor.type));
REQUIRE(max->size() == 1);
REQUIRE(min->size() == 1);
REQUIRE(max->front() == 3211);
REQUIRE(min->front() == 0);
}
{
auto& secondAccessor = accessors[1];
const auto* max = std::get_if<FASTGLTF_STD_PMR_NS::vector<double>>(&secondAccessor.max);
const auto* min = std::get_if<FASTGLTF_STD_PMR_NS::vector<double>>(&secondAccessor.min);
REQUIRE(max != nullptr);
REQUIRE(min != nullptr);
REQUIRE(max->size() == fastgltf::getNumComponents(secondAccessor.type));
REQUIRE(max->size() == 3);
REQUIRE(min->size() == 3);
REQUIRE(glm::epsilonEqual(max->at(0), 0.81497824192047119, glm::epsilon<double>()));
REQUIRE(glm::epsilonEqual(max->at(1), 1.8746249675750732, glm::epsilon<double>()));
REQUIRE(glm::epsilonEqual(max->at(2), 0.32295516133308411, glm::epsilon<double>()));
REQUIRE(glm::epsilonEqual(min->at(0), -0.12269512563943863, glm::epsilon<double>()));
REQUIRE(glm::epsilonEqual(min->at(1), 0.013025385327637196, glm::epsilon<double>()));
REQUIRE(glm::epsilonEqual(min->at(2), -0.32393229007720947, glm::epsilon<double>()));
}
{
auto& fifthAccessor = accessors[4];
const auto* max = std::get_if<FASTGLTF_STD_PMR_NS::vector<double>>(&fifthAccessor.max);
const auto* min = std::get_if<FASTGLTF_STD_PMR_NS::vector<double>>(&fifthAccessor.min);
REQUIRE(max != nullptr);
REQUIRE(min != nullptr);
REQUIRE(max->size() == fastgltf::getNumComponents(fifthAccessor.type));
REQUIRE(max->size() == 4);
REQUIRE(min->size() == 4);
REQUIRE(max->back() == 1.0);
}
}
TEST_CASE("Test unicode characters", "[gltf-loader]") {
auto lightsLamp = sampleModels / "2.0" / std::filesystem::u8path(u8"Unicode❤♻Test") / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(lightsLamp / std::filesystem::u8path(u8"Unicode❤♻Test.gltf")));
fastgltf::Parser parser;
auto asset = parser.loadGLTF(&jsonData, lightsLamp);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(!asset->materials.empty());
REQUIRE(asset->materials[0].name == u8"Unicode❤♻Material");
REQUIRE(!asset->buffers.empty());
auto bufferUri = std::get<fastgltf::sources::URI>(asset->buffers[0].data);
REQUIRE(bufferUri.uri.path() == u8"Unicode❤♻Binary.bin");
}

View File

@@ -0,0 +1,423 @@
#include <fstream>
#include <random>
#include <catch2/benchmark/catch_benchmark.hpp>
#include <catch2/catch_test_macros.hpp>
#include "simdjson.h"
#include <fastgltf/parser.hpp>
#include <fastgltf/base64.hpp>
#include "gltf_path.hpp"
constexpr auto benchmarkOptions = fastgltf::Options::DontRequireValidAssetMember;
#ifdef HAS_RAPIDJSON
#include "rapidjson/document.h"
#include "rapidjson/prettywriter.h"
#include "rapidjson/rapidjson.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
#endif
#ifdef HAS_TINYGLTF
// We don't want tinygltf to load/write images.
#define TINYGLTF_NO_STB_IMAGE_WRITE
#define TINYGLTF_NO_STB_IMAGE
#define TINYGLTF_NO_FS
#define TINYGLTF_IMPLEMENTATION
#include <tiny_gltf.h>
bool tinygltf_FileExistsFunction([[maybe_unused]] const std::string& filename, [[maybe_unused]] void* user) {
return true;
}
std::string tinygltf_ExpandFilePathFunction(const std::string& path, [[maybe_unused]] void* user) {
return path;
}
bool tinygltf_ReadWholeFileFunction(std::vector<unsigned char>* data, std::string*, const std::string&, void*) {
// tinygltf checks if size == 1. It also checks if the size is correct for glb files, but
// well ignore that for now.
data->resize(1);
return true;
}
bool tinygltf_LoadImageData(tinygltf::Image *image, const int image_idx, std::string *err,
std::string *warn, int req_width, int req_height,
const unsigned char *bytes, int size, void *user_data) {
return true;
}
void setTinyGLTFCallbacks(tinygltf::TinyGLTF& gltf) {
gltf.SetFsCallbacks({
tinygltf_FileExistsFunction,
tinygltf_ExpandFilePathFunction,
tinygltf_ReadWholeFileFunction,
nullptr, nullptr,
});
gltf.SetImageLoader(tinygltf_LoadImageData, nullptr);
}
#endif
#ifdef HAS_CGLTF
#define CGLTF_IMPLEMENTATION
#include <cgltf.h>
#endif
#ifdef HAS_GLTFRS
#include "rust/cxx.h"
#include "gltf-rs-bridge/lib.h"
#endif
#ifdef HAS_ASSIMP
#include <assimp/cimport.h>
#include <assimp/scene.h>
#include <assimp/Base64.hpp>
#endif
std::vector<uint8_t> readFileAsBytes(std::filesystem::path path) {
std::ifstream file(path, std::ios::ate | std::ios::binary);
if (!file.is_open())
throw std::runtime_error(std::string { "Failed to open file: " } + path.string());
auto fileSize = file.tellg();
std::vector<uint8_t> bytes(static_cast<size_t>(fileSize) + fastgltf::getGltfBufferPadding());
file.seekg(0, std::ifstream::beg);
file.read(reinterpret_cast<char*>(bytes.data()), fileSize);
file.close();
return bytes;
}
TEST_CASE("Benchmark loading of NewSponza", "[gltf-benchmark]") {
if (!std::filesystem::exists(intelSponza / "NewSponza_Main_glTF_002.gltf")) {
// NewSponza is not part of gltf-Sample-Models, and therefore not always available.
SKIP("Intel's NewSponza (GLTF) is required for this benchmark.");
}
fastgltf::Parser parser;
#ifdef HAS_TINYGLTF
tinygltf::TinyGLTF tinygltf;
tinygltf::Model model;
std::string warn, err;
#endif
auto bytes = readFileAsBytes(intelSponza / "NewSponza_Main_glTF_002.gltf");
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->fromByteView(bytes.data(), bytes.size() - fastgltf::getGltfBufferPadding(), bytes.size()));
BENCHMARK("Parse NewSponza") {
return parser.loadGLTF(jsonData.get(), intelSponza, benchmarkOptions);
};
#ifdef HAS_TINYGLTF
setTinyGLTFCallbacks(tinygltf);
BENCHMARK("Parse NewSponza with tinygltf") {
return tinygltf.LoadASCIIFromString(&model, &err, &warn, reinterpret_cast<char*>(bytes.data()), bytes.size(), intelSponza.string());
};
#endif
#ifdef HAS_CGLTF
BENCHMARK("Parse NewSponza with cgltf") {
cgltf_options options = {};
cgltf_data* data = nullptr;
cgltf_result result = cgltf_parse(&options, bytes.data(), bytes.size(), &data);
REQUIRE(result == cgltf_result_success);
cgltf_free(data);
return result;
};
#endif
#ifdef HAS_GLTFRS
auto padding = fastgltf::getGltfBufferPadding();
BENCHMARK("Parse NewSponza with gltf-rs") {
auto slice = rust::Slice<const std::uint8_t>(reinterpret_cast<std::uint8_t*>(bytes.data()), bytes.size() - padding);
return rust::gltf::run(slice);
};
#endif
#ifdef HAS_ASSIMP
BENCHMARK("Parse NewSponza with assimp") {
return aiImportFileFromMemory(reinterpret_cast<const char*>(bytes.data()), jsonData->getBufferSize(), 0, nullptr);
};
#endif
}
TEST_CASE("Benchmark base64 decoding from glTF file", "[gltf-benchmark]") {
fastgltf::Parser parser;
#ifdef HAS_TINYGLTF
tinygltf::TinyGLTF tinygltf;
tinygltf::Model model;
std::string warn, err;
#endif
auto cylinderEngine = sampleModels / "2.0" / "2CylinderEngine" / "glTF-Embedded";
auto bytes = readFileAsBytes(cylinderEngine / "2CylinderEngine.gltf");
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->fromByteView(bytes.data(), bytes.size() - fastgltf::getGltfBufferPadding(), bytes.size()));
BENCHMARK("Parse 2CylinderEngine and decode base64") {
return parser.loadGLTF(jsonData.get(), cylinderEngine, benchmarkOptions);
};
#ifdef HAS_TINYGLTF
setTinyGLTFCallbacks(tinygltf);
BENCHMARK("2CylinderEngine decode with tinygltf") {
return tinygltf.LoadASCIIFromString(&model, &err, &warn, reinterpret_cast<char*>(bytes.data()), bytes.size(), cylinderEngine.string());
};
#endif
#ifdef HAS_CGLTF
BENCHMARK("2CylinderEngine decode with cgltf") {
cgltf_options options = {};
cgltf_data* data = nullptr;
auto filePath = cylinderEngine.string();
cgltf_result result = cgltf_parse(&options, bytes.data(), bytes.size(), &data);
REQUIRE(result == cgltf_result_success);
result = cgltf_load_buffers(&options, data, filePath.c_str());
cgltf_free(data);
return result;
};
#endif
#ifdef HAS_GLTFRS
auto padding = fastgltf::getGltfBufferPadding();
BENCHMARK("2CylinderEngine with gltf-rs") {
auto slice = rust::Slice<const std::uint8_t>(reinterpret_cast<std::uint8_t*>(bytes.data()), bytes.size() - padding);
return rust::gltf::run(slice);
};
#endif
#ifdef HAS_ASSIMP
BENCHMARK("2CylinderEngine with assimp") {
const auto* scene = aiImportFileFromMemory(reinterpret_cast<const char*>(bytes.data()), jsonData->getBufferSize(), 0, nullptr);
REQUIRE(scene != nullptr);
return scene;
};
#endif
}
TEST_CASE("Benchmark raw JSON parsing", "[gltf-benchmark]") {
fastgltf::Parser parser;
#ifdef HAS_TINYGLTF
tinygltf::TinyGLTF tinygltf;
tinygltf::Model model;
std::string warn, err;
#endif
auto buggyPath = sampleModels / "2.0" / "Buggy" / "glTF";
auto bytes = readFileAsBytes(buggyPath / "Buggy.gltf");
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->fromByteView(bytes.data(), bytes.size() - fastgltf::getGltfBufferPadding(), bytes.size()));
BENCHMARK("Parse Buggy.gltf") {
return parser.loadGLTF(jsonData.get(), buggyPath, benchmarkOptions);
};
#ifdef HAS_TINYGLTF
setTinyGLTFCallbacks(tinygltf);
BENCHMARK("Parse Buggy.gltf with tinygltf") {
return tinygltf.LoadASCIIFromString(&model, &err, &warn, reinterpret_cast<char*>(bytes.data()), bytes.size(), buggyPath.string());
};
#endif
#ifdef HAS_CGLTF
BENCHMARK("Parse Buggy.gltf with cgltf") {
cgltf_options options = {};
cgltf_data* data = nullptr;
auto filePath = buggyPath.string();
cgltf_result result = cgltf_parse(&options, bytes.data(), bytes.size(), &data);
REQUIRE(result == cgltf_result_success);
cgltf_free(data);
return result;
};
#endif
#ifdef HAS_GLTFRS
auto padding = fastgltf::getGltfBufferPadding();
BENCHMARK("Parse Buggy.gltf with gltf-rs") {
auto slice = rust::Slice<const std::uint8_t>(reinterpret_cast<std::uint8_t*>(bytes.data()), bytes.size() - padding);
return rust::gltf::run(slice);
};
#endif
#ifdef HAS_ASSIMP
BENCHMARK("Parse Buggy.gltf with assimp") {
return aiImportFileFromMemory(reinterpret_cast<const char*>(bytes.data()), jsonData->getBufferSize(), 0, nullptr);
};
#endif
}
TEST_CASE("Benchmark massive gltf file", "[gltf-benchmark]") {
if (!std::filesystem::exists(bistroPath / "bistro.gltf")) {
// Bistro is not part of gltf-Sample-Models, and therefore not always available.
SKIP("Amazon's Bistro (GLTF) is required for this benchmark.");
}
fastgltf::Parser parser(fastgltf::Extensions::KHR_mesh_quantization);
#ifdef HAS_TINYGLTF
tinygltf::TinyGLTF tinygltf;
tinygltf::Model model;
std::string warn, err;
#endif
auto bytes = readFileAsBytes(bistroPath / "bistro.gltf");
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->fromByteView(bytes.data(), bytes.size() - fastgltf::getGltfBufferPadding(), bytes.size()));
BENCHMARK("Parse Bistro") {
return parser.loadGLTF(jsonData.get(), bistroPath, benchmarkOptions);
};
#ifdef HAS_TINYGLTF
setTinyGLTFCallbacks(tinygltf);
BENCHMARK("Parse Bistro with tinygltf") {
return tinygltf.LoadASCIIFromString(&model, &err, &warn, reinterpret_cast<char*>(bytes.data()), bytes.size(), bistroPath.string());
};
#endif
#ifdef HAS_CGLTF
BENCHMARK("Parse Bistro with cgltf") {
cgltf_options options = {};
cgltf_data* data = nullptr;
auto filePath = bistroPath.string();
cgltf_result result = cgltf_parse(&options, bytes.data(), bytes.size(), &data);
REQUIRE(result == cgltf_result_success);
cgltf_free(data);
return result;
};
#endif
#ifdef HAS_GLTFRS
auto padding = fastgltf::getGltfBufferPadding();
BENCHMARK("Parse Bistro with gltf-rs") {
auto slice = rust::Slice<const std::uint8_t>(reinterpret_cast<std::uint8_t*>(bytes.data()), bytes.size() - padding);
return rust::gltf::run(slice);
};
#endif
#ifdef HAS_ASSIMP
BENCHMARK("Parse Bistro with assimp") {
return aiImportFileFromMemory(reinterpret_cast<const char*>(bytes.data()), jsonData->getBufferSize(), 0, nullptr);
};
#endif
}
TEST_CASE("Compare parsing performance with minified documents", "[gltf-benchmark]") {
auto buggyPath = sampleModels / "2.0" / "Buggy" / "glTF";
auto bytes = readFileAsBytes(buggyPath / "Buggy.gltf");
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->fromByteView(bytes.data(), bytes.size() - fastgltf::getGltfBufferPadding(), bytes.size()));
// Create a minified JSON string
std::vector<uint8_t> minified(bytes.size());
size_t dstLen = 0;
auto result = simdjson::minify(reinterpret_cast<const char*>(bytes.data()), bytes.size(),
reinterpret_cast<char*>(minified.data()), dstLen);
REQUIRE(result == simdjson::SUCCESS);
minified.resize(dstLen);
// For completeness, benchmark minifying the JSON
BENCHMARK("Minify Buggy.gltf") {
auto result = simdjson::minify(reinterpret_cast<const char*>(bytes.data()), bytes.size(),
reinterpret_cast<char*>(minified.data()), dstLen);
REQUIRE(result == simdjson::SUCCESS);
return result;
};
auto minifiedJsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(minifiedJsonData->fromByteView(minified.data(), minified.size() - fastgltf::getGltfBufferPadding(), minified.size()));
fastgltf::Parser parser;
BENCHMARK("Parse Buggy.gltf with normal JSON") {
return parser.loadGLTF(jsonData.get(), buggyPath, benchmarkOptions);
};
BENCHMARK("Parse Buggy.gltf with minified JSON") {
return parser.loadGLTF(minifiedJsonData.get(), buggyPath, benchmarkOptions);
};
}
#if defined(FASTGLTF_IS_X86)
TEST_CASE("Small CRC32-C benchmark", "[gltf-benchmark]") {
static constexpr std::string_view test = "abcdefghijklmnopqrstuvwxyz";
BENCHMARK("Default 1-byte tabular algorithm") {
return fastgltf::crc32c(reinterpret_cast<const std::uint8_t*>(test.data()), test.size());
};
BENCHMARK("SSE4 hardware algorithm") {
return fastgltf::hwcrc32c(reinterpret_cast<const std::uint8_t*>(test.data()), test.size());
};
}
#endif
TEST_CASE("Compare base64 decoding performance", "[gltf-benchmark]") {
std::string base64Characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
constexpr std::size_t bufferSize = 2 * 1024 * 1024;
// We'll generate a random base64 buffer
std::random_device device;
std::mt19937 gen(device());
std::uniform_int_distribution<> distribution(0, base64Characters.size() - 1);
std::string generatedData;
generatedData.reserve(bufferSize);
for (std::size_t i = 0; i < bufferSize; ++i) {
generatedData.push_back(base64Characters[distribution(gen)]);
}
#ifdef HAS_TINYGLTF
BENCHMARK("Run tinygltf's base64 decoder") {
return tinygltf::base64_decode(generatedData);
};
#endif
#ifdef HAS_CGLTF
cgltf_options options {};
BENCHMARK("Run cgltf's base64 decoder") {
auto padding = fastgltf::base64::getPadding(generatedData);
auto outputSize = fastgltf::base64::getOutputSize(generatedData.size(), padding);
std::string output;
output.resize(outputSize);
auto* outputData = output.data();
return cgltf_load_buffer_base64(&options, generatedData.size(), generatedData.data(), reinterpret_cast<void**>(&outputData));
};
#endif
#ifdef HAS_GLTFRS
BENCHMARK("Run base64 Rust library decoder") {
auto slice = rust::Slice<const std::uint8_t>(reinterpret_cast<std::uint8_t*>(generatedData.data()), generatedData.size());
return rust::gltf::run_base64(slice);
};
#endif
#ifdef HAS_ASSIMP
BENCHMARK("Run Assimp's base64 decoder") {
return Assimp::Base64::Decode(generatedData);
};
#endif
BENCHMARK("Run fastgltf's fallback base64 decoder") {
return fastgltf::base64::fallback_decode(generatedData);
};
#if defined(FASTGLTF_IS_X86)
const auto& impls = simdjson::get_available_implementations();
if (const auto* sse4 = impls["westmere"]; sse4 != nullptr && sse4->supported_by_runtime_system()) {
BENCHMARK("Run fastgltf's SSE4 base64 decoder") {
return fastgltf::base64::sse4_decode(generatedData);
};
}
if (const auto* avx2 = impls["haswell"]; avx2 != nullptr && avx2->supported_by_runtime_system()) {
BENCHMARK("Run fastgltf's AVX2 base64 decoder") {
return fastgltf::base64::avx2_decode(generatedData);
};
}
#elif defined(FASTGLTF_IS_A64)
const auto& impls = simdjson::get_available_implementations();
if (const auto* neon = impls["arm64"]; avx2 != nullptr && neon->supported_by_runtime_system()) {
BENCHMARK("Run fastgltf's Neon base64 decoder") {
return fastgltf::base64::neon_decode(generatedData);
};
}
#endif
}

View File

@@ -0,0 +1,262 @@
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/benchmark/catch_benchmark.hpp>
#include <glm/gtc/epsilon.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <fastgltf/parser.hpp>
#include "gltf_path.hpp"
TEST_CASE("Loading KHR_texture_basisu glTF files", "[gltf-loader]") {
auto stainedLamp = sampleModels / "2.0" / "StainedGlassLamp" / "glTF-KTX-BasisU";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(stainedLamp / "StainedGlassLamp.gltf"));
SECTION("Loading KHR_texture_basisu") {
fastgltf::Parser parser(fastgltf::Extensions::KHR_texture_basisu);
auto asset = parser.loadGLTF(jsonData.get(), path, fastgltf::Options::DontRequireValidAssetMember,
fastgltf::Category::Textures | fastgltf::Category::Images);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->textures.size() == 19);
REQUIRE(!asset->images.empty());
auto& texture = asset->textures[1];
REQUIRE(!texture.imageIndex.has_value());
REQUIRE(texture.samplerIndex == 0);
REQUIRE(texture.basisuImageIndex.has_value());
REQUIRE(texture.basisuImageIndex.value() == 1);
auto& image = asset->images.front();
auto* filePath = std::get_if<fastgltf::sources::URI>(&image.data);
REQUIRE(filePath != nullptr);
REQUIRE(filePath->uri.valid());
REQUIRE(filePath->uri.isLocalPath());
REQUIRE(filePath->mimeType == fastgltf::MimeType::KTX2);
}
SECTION("Testing requiredExtensions") {
// We specify no extensions, yet the StainedGlassLamp requires KHR_texture_basisu.
fastgltf::Parser parser(fastgltf::Extensions::None);
auto stainedGlassLamp = parser.loadGLTF(jsonData.get(), path, fastgltf::Options::DontRequireValidAssetMember);
REQUIRE(stainedGlassLamp.error() == fastgltf::Error::MissingExtensions);
}
}
TEST_CASE("Loading KHR_texture_transform glTF files", "[gltf-loader]") {
auto transformTest = sampleModels / "2.0" / "TextureTransformMultiTest" / "glTF";
auto jsonData = std::make_unique<fastgltf::GltfDataBuffer>();
REQUIRE(jsonData->loadFromFile(transformTest / "TextureTransformMultiTest.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::KHR_texture_transform);
auto asset = parser.loadGLTF(jsonData.get(), transformTest, fastgltf::Options::DontRequireValidAssetMember, fastgltf::Category::Materials);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(!asset->materials.empty());
auto& material = asset->materials.front();
REQUIRE(material.pbrData.baseColorTexture.has_value());
REQUIRE(material.pbrData.baseColorTexture->transform != nullptr);
REQUIRE(material.pbrData.baseColorTexture->transform->uvOffset[0] == 0.705f);
REQUIRE(material.pbrData.baseColorTexture->transform->rotation == Catch::Approx(1.5707963705062866f));
}
TEST_CASE("Test KHR_lights_punctual", "[gltf-loader]") {
auto lightsLamp = sampleModels / "2.0" / "LightsPunctualLamp" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(lightsLamp / "LightsPunctualLamp.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::KHR_lights_punctual);
auto asset = parser.loadGLTF(&jsonData, lightsLamp, fastgltf::Options::None, fastgltf::Category::Nodes);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->lights.size() == 5);
REQUIRE(asset->nodes.size() > 4);
auto& nodes = asset->nodes;
REQUIRE(nodes[3].lightIndex.has_value());
REQUIRE(nodes[3].lightIndex.value() == 0);
auto& lights = asset->lights;
REQUIRE(lights[0].name == "Point");
REQUIRE(lights[0].type == fastgltf::LightType::Point);
REQUIRE(lights[0].intensity == 15.0f);
REQUIRE(glm::epsilonEqual(lights[0].color[0], 1.0f, glm::epsilon<float>()));
REQUIRE(glm::epsilonEqual(lights[0].color[1], 0.63187497854232788f, glm::epsilon<float>()));
REQUIRE(glm::epsilonEqual(lights[0].color[2], 0.23909975588321689f, glm::epsilon<float>()));
}
TEST_CASE("Test KHR_materials_specular", "[gltf-loader]") {
auto specularTest = sampleModels / "2.0" / "SpecularTest" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(specularTest / "SpecularTest.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::KHR_materials_specular);
auto asset = parser.loadGLTF(&jsonData, specularTest, fastgltf::Options::None, fastgltf::Category::Materials);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->materials.size() >= 12);
auto& materials = asset->materials;
REQUIRE(materials[1].specular != nullptr);
REQUIRE(materials[1].specular->specularFactor == 0.0f);
REQUIRE(materials[2].specular != nullptr);
REQUIRE(glm::epsilonEqual(materials[2].specular->specularFactor, 0.051269f, glm::epsilon<float>()));
REQUIRE(materials[8].specular != nullptr);
REQUIRE(glm::epsilonEqual(materials[8].specular->specularColorFactor[0], 0.051269f, glm::epsilon<float>()));
REQUIRE(glm::epsilonEqual(materials[8].specular->specularColorFactor[1], 0.051269f, glm::epsilon<float>()));
REQUIRE(glm::epsilonEqual(materials[8].specular->specularColorFactor[2], 0.051269f, glm::epsilon<float>()));
REQUIRE(materials[12].specular != nullptr);
REQUIRE(materials[12].specular->specularColorTexture.has_value());
REQUIRE(materials[12].specular->specularColorTexture.value().textureIndex == 2);
}
TEST_CASE("Test KHR_materials_ior and KHR_materials_iridescence", "[gltf-loader]") {
auto specularTest = sampleModels / "2.0" / "IridescenceDielectricSpheres" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(specularTest / "IridescenceDielectricSpheres.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::KHR_materials_iridescence | fastgltf::Extensions::KHR_materials_ior);
auto asset = parser.loadGLTF(&jsonData, specularTest, fastgltf::Options::None, fastgltf::Category::Materials);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->materials.size() >= 50);
auto& materials = asset->materials;
REQUIRE(materials[0].iridescence != nullptr);
REQUIRE(materials[0].iridescence->iridescenceFactor == 1.0f);
REQUIRE(materials[0].iridescence->iridescenceIor == 1.0f);
REQUIRE(materials[0].iridescence->iridescenceThicknessMaximum == 100.0f);
REQUIRE(materials[0].ior.has_value());
REQUIRE(materials[0].ior.value() == 1.0f);
REQUIRE(materials[7].ior.has_value());
REQUIRE(materials[7].ior.value() == 1.17f);
REQUIRE(materials[50].iridescence != nullptr);
REQUIRE(materials[50].iridescence->iridescenceFactor == 1.0f);
REQUIRE(materials[50].iridescence->iridescenceIor == 1.17f);
REQUIRE(materials[50].iridescence->iridescenceThicknessMaximum == 200.0f);
}
TEST_CASE("Test KHR_materials_volume and KHR_materials_transmission", "[gltf-loader]") {
auto beautifulGame = sampleModels / "2.0" / "ABeautifulGame" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(beautifulGame / "ABeautifulGame.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::KHR_materials_volume | fastgltf::Extensions::KHR_materials_transmission);
auto asset = parser.loadGLTF(&jsonData, beautifulGame, fastgltf::Options::None, fastgltf::Category::Materials);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->materials.size() >= 5);
auto& materials = asset->materials;
REQUIRE(materials[5].volume != nullptr);
REQUIRE(glm::epsilonEqual(materials[5].volume->thicknessFactor, 0.2199999988079071f, glm::epsilon<float>()));
REQUIRE(glm::epsilonEqual(materials[5].volume->attenuationColor[0], 0.800000011920929f, glm::epsilon<float>()));
REQUIRE(glm::epsilonEqual(materials[5].volume->attenuationColor[1], 0.800000011920929f, glm::epsilon<float>()));
REQUIRE(glm::epsilonEqual(materials[5].volume->attenuationColor[2], 0.800000011920929f, glm::epsilon<float>()));
REQUIRE(materials[5].transmission != nullptr);
REQUIRE(materials[5].transmission->transmissionFactor == 1.0f);
}
TEST_CASE("Test KHR_materials_clearcoat", "[gltf-loader]") {
auto clearcoatTest = sampleModels / "2.0" / "ClearCoatTest" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(clearcoatTest / "ClearCoatTest.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::KHR_materials_clearcoat);
auto asset = parser.loadGLTF(&jsonData, clearcoatTest, fastgltf::Options::None, fastgltf::Category::Materials);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->materials.size() >= 7);
auto& materials = asset->materials;
REQUIRE(materials[1].clearcoat != nullptr);
REQUIRE(materials[1].clearcoat->clearcoatFactor == 1.0f);
REQUIRE(materials[1].clearcoat->clearcoatRoughnessFactor == 0.03f);
REQUIRE(materials[7].clearcoat != nullptr);
REQUIRE(materials[7].clearcoat->clearcoatFactor == 1.0f);
REQUIRE(materials[7].clearcoat->clearcoatRoughnessFactor == 1.0f);
REQUIRE(materials[7].clearcoat->clearcoatRoughnessTexture.has_value());
REQUIRE(materials[7].clearcoat->clearcoatRoughnessTexture->textureIndex == 2);
REQUIRE(materials[7].clearcoat->clearcoatRoughnessTexture->texCoordIndex == 0);
}
TEST_CASE("Test EXT_mesh_gpu_instancing", "[gltf-loader]") {
auto simpleInstancingTest = sampleModels / "2.0" / "SimpleInstancing" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(simpleInstancingTest / "SimpleInstancing.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::EXT_mesh_gpu_instancing);
auto asset = parser.loadGLTF(&jsonData, simpleInstancingTest, fastgltf::Options::None, fastgltf::Category::Accessors | fastgltf::Category::Nodes);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->accessors.size() >= 6);
REQUIRE(asset->nodes.size() >= 1);
auto& nodes = asset->nodes;
REQUIRE(nodes[0].instancingAttributes.size() == 3u);
REQUIRE(nodes[0].findInstancingAttribute("TRANSLATION") != nodes[0].instancingAttributes.cend());
REQUIRE(nodes[0].findInstancingAttribute("SCALE") != nodes[0].instancingAttributes.cend());
REQUIRE(nodes[0].findInstancingAttribute("ROTATION") != nodes[0].instancingAttributes.cend());
}
#if FASTGLTF_ENABLE_DEPRECATED_EXT
TEST_CASE("Test KHR_materials_pbrSpecularGlossiness", "[gltf-loader]") {
auto specularGlossinessTest = sampleModels / "2.0" / "SpecGlossVsMetalRough" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(specularGlossinessTest / "SpecGlossVsMetalRough.gltf"));
fastgltf::Parser parser(fastgltf::Extensions::KHR_materials_pbrSpecularGlossiness | fastgltf::Extensions::KHR_materials_specular);
auto asset = parser.loadGLTF(&jsonData, specularGlossinessTest);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->materials.size() == 4);
auto& materials = asset->materials;
REQUIRE(materials[0].specularGlossiness != nullptr);
REQUIRE(materials[0].specularGlossiness->diffuseFactor[0] == 1.0f);
REQUIRE(materials[0].specularGlossiness->diffuseFactor[1] == 1.0f);
REQUIRE(materials[0].specularGlossiness->diffuseFactor[2] == 1.0f);
REQUIRE(materials[0].specularGlossiness->diffuseFactor[3] == 1.0f);
REQUIRE(materials[0].specularGlossiness->specularFactor[0] == 1.0f);
REQUIRE(materials[0].specularGlossiness->specularFactor[1] == 1.0f);
REQUIRE(materials[0].specularGlossiness->specularFactor[2] == 1.0f);
REQUIRE(materials[0].specularGlossiness->glossinessFactor == 1.0f);
REQUIRE(materials[0].specularGlossiness->diffuseTexture.has_value());
REQUIRE(materials[0].specularGlossiness->diffuseTexture.value().textureIndex == 5);
REQUIRE(materials[0].specularGlossiness->specularGlossinessTexture.has_value());
REQUIRE(materials[0].specularGlossiness->specularGlossinessTexture.value().textureIndex == 6);
REQUIRE(materials[3].specularGlossiness != nullptr);
REQUIRE(materials[3].specularGlossiness->diffuseFactor[0] == 1.0f);
REQUIRE(materials[3].specularGlossiness->diffuseFactor[1] == 1.0f);
REQUIRE(materials[3].specularGlossiness->diffuseFactor[2] == 1.0f);
REQUIRE(materials[3].specularGlossiness->diffuseFactor[3] == 1.0f);
REQUIRE(materials[3].specularGlossiness->specularFactor[0] == 0.0f);
REQUIRE(materials[3].specularGlossiness->specularFactor[1] == 0.0f);
REQUIRE(materials[3].specularGlossiness->specularFactor[2] == 0.0f);
REQUIRE(materials[3].specularGlossiness->glossinessFactor == 0.0f);
REQUIRE(materials[3].specularGlossiness->diffuseTexture.has_value());
REQUIRE(materials[3].specularGlossiness->diffuseTexture.value().textureIndex == 7);
}
#endif

View File

@@ -0,0 +1,57 @@
#include <fstream>
#include <catch2/catch_test_macros.hpp>
#include <fastgltf/parser.hpp>
#include <fastgltf/types.hpp>
#include "gltf_path.hpp"
TEST_CASE("Load basic GLB file", "[gltf-loader]") {
fastgltf::Parser parser;
auto folder = sampleModels / "2.0" / "Box" / "glTF-Binary";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(folder / "Box.glb"));
SECTION("Load basic Box.glb") {
auto asset = parser.loadBinaryGLTF(&jsonData, folder, fastgltf::Options::None, fastgltf::Category::Buffers);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->buffers.size() == 1);
auto& buffer = asset->buffers.front();
auto* bufferView = std::get_if<fastgltf::sources::ByteView>(&buffer.data);
REQUIRE(bufferView != nullptr);
auto jsonSpan = fastgltf::span<std::byte>(jsonData);
REQUIRE(bufferView->bytes.data() - jsonSpan.data() == 1016);
REQUIRE(jsonSpan.size() == 1664);
}
SECTION("Load basic Box.glb and load buffers") {
auto asset = parser.loadBinaryGLTF(&jsonData, folder, fastgltf::Options::LoadGLBBuffers, fastgltf::Category::Buffers);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->buffers.size() == 1);
auto& buffer = asset->buffers.front();
auto* bufferVector = std::get_if<fastgltf::sources::Vector>(&buffer.data);
REQUIRE(bufferVector != nullptr);
REQUIRE(!bufferVector->bytes.empty());
REQUIRE(static_cast<uint64_t>(bufferVector->bytes.size() - buffer.byteLength) < 3);
}
SECTION("Load GLB by bytes") {
std::ifstream file(folder / "Box.glb", std::ios::binary | std::ios::ate);
auto length = static_cast<size_t>(file.tellg());
file.seekg(0, std::ifstream::beg);
std::vector<uint8_t> bytes(length + fastgltf::getGltfBufferPadding());
file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(length));
fastgltf::GltfDataBuffer byteBuffer;
REQUIRE(byteBuffer.fromByteView(bytes.data(), length, length + fastgltf::getGltfBufferPadding()));
auto asset = parser.loadBinaryGLTF(&byteBuffer, folder, fastgltf::Options::LoadGLBBuffers, fastgltf::Category::Buffers);
REQUIRE(asset.error() == fastgltf::Error::None);
}
}

View File

@@ -0,0 +1,16 @@
[package]
name = "gltf_rs"
version = "0.1.0"
edition = "2021"
[dependencies]
cxx = "1.0"
gltf = "1.1.0"
base64 = "0.21.2"
[build-dependencies]
cxx-build = "1.0"
[lib]
crate-type = ["staticlib"]
path = "src/lib.rs"

View File

@@ -0,0 +1,54 @@
use base64::Engine;
use base64::{alphabet, engine};
#[cxx::bridge(namespace = "rust::gltf")]
mod ffi {
extern "Rust" {
fn run(data: &[u8]) -> i32;
fn run_base64(data: &[u8]) -> Vec<u8>;
}
}
fn run(data: &[u8]) -> i32 {
// TODO: Decode URIs and data URIs?
let gltf = gltf::Gltf::from_slice(data)
.unwrap();
// Decode URIs
let json = gltf.document.into_json();
let mut uri_count = 0;
for x in json.buffers {
// gltf-rs doesn't automatically decode base64. Using its "import" feature won't work,
// because we're not interested in file-loaded buffer/image data.
if x.uri.is_some() {
let uri = x.uri.unwrap();
if let Some(rest) = uri.strip_prefix("data:") {
let mut it = rest.split(";base64,");
let data = match (it.next(), it.next()) {
(_, Some(data)) => Some(data),
(Some(data), _) => Some(data),
_ => None
};
if data.is_none() {
continue;
}
base64::engine::GeneralPurpose::new(
&base64::alphabet::STANDARD,
base64::engine::general_purpose::PAD)
.decode(data.unwrap()).expect("Decoded bytes");
uri_count += 1;
}
}
}
uri_count
}
fn run_base64(data: &[u8]) -> Vec<u8> {
return engine::GeneralPurpose::new(
&alphabet::STANDARD,
engine::general_purpose::PAD)
.decode(data).expect("Decoded bytes");
}

View File

@@ -0,0 +1 @@
AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAABAAIAAwACAAEABAAFAAYABwAGAAUACAAJAAoACwAKAAkADAANAA4ADwAOAA0AEAARABIAEwASABEAFAAVABYAFwAWABUA

Binary file not shown.

View File

@@ -0,0 +1,5 @@
{
"asset": {
"version": "2.0"
}
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,40 @@
{
"asset": {
"version": "2.0"
},
"cameras": [
{
"perspective": {
"yfov": 1.0,
"zfar": 1.0,
"znear": 0.001
},
"type": "perspective"
}
],
"nodes": [
{
"name": "TRS components",
"camera": 0,
"translation": [
1.0, 1.0, 1.0
],
"rotation": [
0.0, 1.0, 0.0, 0.0
],
"scale": [
2.0, 0.5, 1.0
]
},
{
"name": "Matrix",
"camera": 0,
"matrix": [
-2.0, 0.0, 0.0, 0.0,
0.0, 0.5, 0.0, 0.0,
0.0, 0.0, -1.0, 0.0,
1.0, 1.0, 1.0, 1.0
]
}
]
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include <filesystem>
// We need to use the __FILE__ macro so that we have access to test glTF files in this
// directory. As Clang does not yet fully support std::source_location, we cannot use that.
inline auto path = std::filesystem::path { __FILE__ }.parent_path() / "gltf";
inline auto sampleModels = std::filesystem::path { __FILE__ }.parent_path() / "gltf" / "glTF-Sample-Models";
inline auto intelSponza = std::filesystem::path { __FILE__ }.parent_path() / "gltf" / "intel_sponza";
inline auto bistroPath = std::filesystem::path {};

153
third_party/fastgltf/tests/uri_tests.cpp vendored Normal file
View File

@@ -0,0 +1,153 @@
#include <catch2/catch_test_macros.hpp>
#include <fastgltf/types.hpp>
#include <fastgltf/parser.hpp>
#include "gltf_path.hpp"
TEST_CASE("Test basic URIs", "[uri-tests]") {
const fastgltf::URI uri1(std::string_view(""));
REQUIRE(uri1.scheme().empty());
REQUIRE(uri1.path().empty());
std::string_view path = "path/somewhere.xyz";
SECTION("Basic local path") {
const fastgltf::URI uri2(path);
REQUIRE(uri2.scheme().empty());
REQUIRE(uri2.path() == path);
REQUIRE(uri2.isLocalPath());
REQUIRE(uri2.fspath() == path);
}
std::string_view abspath = "/path/somewhere.xyz";
SECTION("File scheme path") {
const std::string_view filePath = "file:/path/somewhere.xyz";
const fastgltf::URI uri3(filePath);
REQUIRE(uri3.scheme() == "file");
REQUIRE(uri3.isLocalPath());
REQUIRE(uri3.path() == abspath);
}
SECTION("File scheme localhost path") {
const std::string_view localhostPath = "file://localhost/path/somewhere.xyz";
const fastgltf::URI uri4(localhostPath);
REQUIRE(uri4.scheme() == "file");
REQUIRE(uri4.path() == abspath);
REQUIRE(!uri4.isLocalPath());
}
}
TEST_CASE("Test generic URIs", "[uri-tests]") {
// These are a bunch of example URIs from https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Example_URIs
const fastgltf::URI uri(std::string_view("https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top"));
REQUIRE(uri.scheme() == "https");
REQUIRE(uri.userinfo() == "john.doe");
REQUIRE(uri.host() == "www.example.com");
REQUIRE(uri.port() == "123");
REQUIRE(uri.path() == "/forum/questions/");
REQUIRE(uri.query() == "tag=networking&order=newest");
REQUIRE(uri.fragment() == "top");
const fastgltf::URI uri1(std::string_view("ldap://[2001:db8::7]/c=GB?objectClass?one"));
REQUIRE(uri1.scheme() == "ldap");
REQUIRE(uri1.host() == "2001:db8::7");
REQUIRE(uri1.path() == "/c=GB");
REQUIRE(uri1.query() == "objectClass?one");
const fastgltf::URI uri2(std::string_view("mailto:John.Doe@example.com"));
REQUIRE(uri2.scheme() == "mailto");
REQUIRE(uri2.path() == "John.Doe@example.com");
const fastgltf::URI uri3(std::string_view("telnet://192.0.2.16:80/"));
REQUIRE(uri3.scheme() == "telnet");
REQUIRE(uri3.host() == "192.0.2.16");
REQUIRE(uri3.port() == "80");
REQUIRE(uri3.path() == "/");
}
TEST_CASE("Test percent decoding", "[uri-tests]") {
std::string test = "%22 %25";
fastgltf::URI::decodePercents(test);
REQUIRE(test == "\" %");
}
TEST_CASE("Test data URI parsing", "[uri-tests]") {
// This example base64 data is from an example on https://en.wikipedia.org/wiki/Data_URI_scheme.
const std::string_view data = "data:image/png;base64,iVBORw0KGgoAAA"
"ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4"
"//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU"
"5ErkJggg==";
const fastgltf::URI uri(data);
REQUIRE(uri.scheme() == "data");
REQUIRE(uri.path() == data.substr(5));
}
TEST_CASE("Validate URI copying/moving", "[uri-tests]") {
const std::string_view data = "test.bin";
SECTION("Copy semantics") {
fastgltf::URI uri(data);
REQUIRE(uri.path() == data);
fastgltf::URI uri2(uri);
REQUIRE(uri2.string().data() != uri.string().data());
REQUIRE(uri2.path() == data);
}
SECTION("Move semantics") {
fastgltf::URI uri;
{
fastgltf::URI uri2(data);
uri = std::move(uri2);
REQUIRE(uri2.string().empty());
}
// Test that the values were copied over and that the string views are still valid.
REQUIRE(uri.string() == data);
REQUIRE(uri.path() == uri.string());
}
}
TEST_CASE("Validate escaped/percent-encoded URI", "[uri-tests]") {
const std::string_view gltfString = R"({"images": [{"uri": "grande_sph\u00E8re.png"}]})";
fastgltf::GltfDataBuffer dataBuffer;
dataBuffer.copyBytes((uint8_t*) gltfString.data(), gltfString.size());
fastgltf::Parser parser;
auto asset = parser.loadGLTF(&dataBuffer, "", fastgltf::Options::DontRequireValidAssetMember);
REQUIRE(asset.error() == fastgltf::Error::None);
auto escaped = std::get<fastgltf::sources::URI>(asset->images.front().data);
// This only tests wether the default ctor of fastgltf::URI can handle percent-encoding correctly.
const fastgltf::URI original(std::string_view("grande_sphère.png"));
const fastgltf::URI encoded(std::string_view("grande_sph%C3%A8re.png"));
REQUIRE(original.string() == escaped.uri.string());
REQUIRE(original.string() == encoded.string());
}
TEST_CASE("Test percent-encoded URIs in glTF", "[uri-tests]") {
auto boxWithSpaces = sampleModels / "2.0" / "Box With Spaces" / "glTF";
fastgltf::GltfDataBuffer jsonData;
REQUIRE(jsonData.loadFromFile(boxWithSpaces / "Box With Spaces.gltf"));
fastgltf::Parser parser;
auto asset = parser.loadGLTF(&jsonData, boxWithSpaces);
REQUIRE(asset.error() == fastgltf::Error::None);
REQUIRE(fastgltf::validate(asset.get()) == fastgltf::Error::None);
REQUIRE(asset->images.size() == 3);
auto* image0 = std::get_if<fastgltf::sources::URI>(&asset->images[0].data);
REQUIRE(image0 != nullptr);
REQUIRE(image0->uri.path() == "Normal Map.png");
auto* image1 = std::get_if<fastgltf::sources::URI>(&asset->images[1].data);
REQUIRE(image1 != nullptr);
REQUIRE(image1->uri.path() == "glTF Logo With Spaces.png");
auto* image2 = std::get_if<fastgltf::sources::URI>(&asset->images[2].data);
REQUIRE(image2 != nullptr);
REQUIRE(image2->uri.path() == "Roughness Metallic.png");
auto* buffer0 = std::get_if<fastgltf::sources::URI>(&asset->buffers[0].data);
REQUIRE(buffer0 != nullptr);
REQUIRE(buffer0->uri.path() == "Box With Spaces.bin");
}

View File

@@ -0,0 +1,115 @@
#include <catch2/catch_test_macros.hpp>
#include <fastgltf/types.hpp>
TEST_CASE("Verify clz", "[vector-tests]") {
REQUIRE(fastgltf::clz<std::uint8_t>(0b00000001) == 7);
REQUIRE(fastgltf::clz<std::uint8_t>(0b00000010) == 6);
REQUIRE(fastgltf::clz<std::uint8_t>(0b00000100) == 5);
REQUIRE(fastgltf::clz<std::uint8_t>(0b00001000) == 4);
REQUIRE(fastgltf::clz<std::uint8_t>(0b00010000) == 3);
REQUIRE(fastgltf::clz<std::uint8_t>(0b00100000) == 2);
REQUIRE(fastgltf::clz<std::uint8_t>(0b01000000) == 1);
REQUIRE(fastgltf::clz<std::uint8_t>(0b10000000) == 0);
}
TEST_CASE("Test resize/reserve", "[vector-tests]") {
fastgltf::SmallVector<uint32_t, 4> vec = {1, 2, 3};
REQUIRE(vec[0] == 1);
REQUIRE(vec[1] == 2);
REQUIRE(vec[2] == 3);
vec.resize(5);
REQUIRE(vec.size() == 5);
REQUIRE(vec[3] == 0);
REQUIRE(vec[4] == 0);
vec.resize(2);
REQUIRE(vec.size() == 2);
REQUIRE(vec[0] == 1);
REQUIRE(vec[1] == 2);
vec.resize(6, 4);
REQUIRE(vec.size() == 6);
for (std::size_t i = 2; i < vec.size(); ++i) {
REQUIRE(vec[i] == 4);
}
vec.reserve(8);
REQUIRE(vec.size() == 6);
REQUIRE(vec.capacity() == 8);
vec.shrink_to_fit();
REQUIRE(vec.capacity() == 6);
}
TEST_CASE("Test constructors", "[vector-tests]") {
fastgltf::SmallVector<uint32_t, 4> vec = {0, 1, 2, 3};
for (std::size_t i = 0; i < vec.size(); ++i) {
REQUIRE(vec[i] == i);
}
fastgltf::SmallVector<uint32_t, 4> vec2(vec);
for (std::size_t i = 0; i < vec2.size(); ++i) {
REQUIRE(vec2[i] == i);
}
fastgltf::SmallVector<uint32_t, 4> vec3 = std::move(vec2);
REQUIRE(vec2.empty());
vec3.resize(6);
for (std::size_t i = 0; i < 4; ++i) {
REQUIRE(vec3[i] == i);
}
REQUIRE(vec3[4] == 0);
REQUIRE(vec3[5] == 0);
}
TEST_CASE("Nested SmallVector", "[vector-tests]") {
fastgltf::SmallVector<fastgltf::SmallVector<uint32_t, 2>, 4> vectors(6, {4}); // This should heap allocate straight away.
REQUIRE(vectors.size() == 6);
for (auto& vector : vectors) {
REQUIRE(vector.size() == 1);
REQUIRE(vector.front() == 4);
vector.reserve(6);
}
}
struct RefCountedObject {
static inline std::size_t aliveObjects = 0;
RefCountedObject() {
++aliveObjects;
}
RefCountedObject(const RefCountedObject& other) {
++aliveObjects;
}
RefCountedObject(RefCountedObject&& other) = delete;
~RefCountedObject() {
--aliveObjects;
}
};
TEST_CASE("Test shrinking vectors", "[vector-tests]") {
fastgltf::SmallVector<RefCountedObject, 4> objects;
for (std::size_t i = 0; i < 4; ++i) {
objects.emplace_back();
}
REQUIRE(RefCountedObject::aliveObjects == 4);
objects.emplace_back();
REQUIRE(RefCountedObject::aliveObjects == 5);
objects.resize(4);
REQUIRE(RefCountedObject::aliveObjects == 4);
}
TEST_CASE("Test vectors with polymorphic allocators", "[vector-tests]") {
fastgltf::pmr::SmallVector<std::uint32_t, 4> ints;
ints.assign(10, 5);
REQUIRE(ints.size() == 10);
REQUIRE(ints.data() != nullptr);
for (auto& i : ints) {
REQUIRE(i == 5);
}
}