diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c509db62..45500bb61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ include(CMakeDependentOption) option(WITH_ASSIMPIMPORTER "Build AssimpImporter plugin" OFF) option(WITH_BASISIMAGECONVERTER "Build BasisImageConverter plugin" OFF) option(WITH_BASISIMPORTER "Build BasisImporter plugin" OFF) +option(WITH_CGLTFIMPORTER "Build CgltfImporter plugin" OFF) option(WITH_DDSIMPORTER "Build DdsImporter plugin" OFF) option(WITH_DEVILIMAGEIMPORTER "Build DevIlImageImporter plugin" OFF) option(WITH_DRFLACAUDIOIMPORTER "Build DrFlacAudioImporter plugin" OFF) diff --git a/doc/building-plugins.dox b/doc/building-plugins.dox index 73e5fbad0..4083dab84 100644 --- a/doc/building-plugins.dox +++ b/doc/building-plugins.dox @@ -342,6 +342,8 @@ By default no plugins are built and you need to select them manually: --- if you are on Vcpkg, it will "just work", otherwise you need to download the sources and point `BASIS_UNIVERSAL_DIR` to their directory so CMake can find them. +- `WITH_CGLTFIMPORTER` --- Build the + @ref Trade::CgltfImporter "CgltfImporter" plugin. - `WITH_DDSIMPORTER` --- Build the @ref Trade::DdsImporter "DdsImporter" plugin. - `WITH_DEVILIMAGEIMPORTER` --- Build the diff --git a/doc/cmake-plugins.dox b/doc/cmake-plugins.dox index b2c50b1da..c7a363f4c 100644 --- a/doc/cmake-plugins.dox +++ b/doc/cmake-plugins.dox @@ -121,6 +121,7 @@ This command will not try to find any actual plugin. The plugins are: - `AssimpImporter` --- @ref Trade::AssimpImporter "AssimpImporter" plugin - `BasisImageConverter` --- @ref Trade::BasisImageConverter "BasisImageConverter" plugin - `BasisImporter` --- @ref Trade::BasisImporter "BasisImporter" plugin +- `CgltfImporter` --- @ref Trade::CgltfImporter "CgltfImporter" plugin - `DdsImporter` --- @ref Trade::DdsImporter "DdsImporter" plugin - `DevIlImageImporter` --- @ref Trade::DevIlImageImporter "DevIlImageImporter" plugin diff --git a/doc/namespaces.dox b/doc/namespaces.dox index 04095ef5b..854a91c7a 100644 --- a/doc/namespaces.dox +++ b/doc/namespaces.dox @@ -37,6 +37,10 @@ * @brief Plugin @ref Magnum::Trade::BasisImporter * @m_since_{plugins,2019,10} */ +/** @dir MagnumPlugins/CgltfImporter + * @brief Plugin @ref Magnum::Trade::CgltfImporter + * @m_since_latest_{plugins} + */ /** @dir MagnumPlugins/DdsImporter * @brief Plugin @ref Magnum::Trade::DdsImporter */ diff --git a/modules/FindMagnumPlugins.cmake b/modules/FindMagnumPlugins.cmake index a5ed7ab9c..b82a2268b 100644 --- a/modules/FindMagnumPlugins.cmake +++ b/modules/FindMagnumPlugins.cmake @@ -15,6 +15,7 @@ # AssimpImporter - Assimp importer # BasisImageConverter - Basis image converter # BasisImporter - Basis importer +# CgltfImporter - GLTF importer using cgltf # DdsImporter - DDS importer # DevIlImageImporter - Image importer using DevIL # DrFlacAudioImporter - FLAC audio importer using dr_flac @@ -316,6 +317,7 @@ foreach(_component ${MagnumPlugins_FIND_COMPONENTS}) INTERFACE_LINK_LIBRARIES basisu_transcoder) endif() + # CgltfImporter has no dependencies # DdsImporter has no dependencies # DevIlImageImporter plugin dependencies diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 18e031526..a38ae5990 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -26,6 +26,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ -DWITH_BASISIMPORTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-android-arm64 b/package/archlinux/PKGBUILD-android-arm64 index a3585de91..420a9f1b3 100644 --- a/package/archlinux/PKGBUILD-android-arm64 +++ b/package/archlinux/PKGBUILD-android-arm64 @@ -34,6 +34,7 @@ build() { -DWITH_ASSIMPIMPORTER=OFF \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=OFF \ -DWITH_DRFLACAUDIOIMPORTER=OFF \ diff --git a/package/archlinux/PKGBUILD-clang b/package/archlinux/PKGBUILD-clang index 582c4c217..ac8ca35bb 100644 --- a/package/archlinux/PKGBUILD-clang +++ b/package/archlinux/PKGBUILD-clang @@ -38,6 +38,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-clang-addresssanitizer b/package/archlinux/PKGBUILD-clang-addresssanitizer index 57d290074..acb05a1ab 100644 --- a/package/archlinux/PKGBUILD-clang-addresssanitizer +++ b/package/archlinux/PKGBUILD-clang-addresssanitizer @@ -28,6 +28,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-clang-threadsanitizer b/package/archlinux/PKGBUILD-clang-threadsanitizer index fa8ce7631..ee32f8f6f 100644 --- a/package/archlinux/PKGBUILD-clang-threadsanitizer +++ b/package/archlinux/PKGBUILD-clang-threadsanitizer @@ -28,6 +28,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-coverage b/package/archlinux/PKGBUILD-coverage index dcc3bd857..f984a0759 100644 --- a/package/archlinux/PKGBUILD-coverage +++ b/package/archlinux/PKGBUILD-coverage @@ -31,6 +31,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-emscripten b/package/archlinux/PKGBUILD-emscripten index f8751afc3..4d688ccfe 100644 --- a/package/archlinux/PKGBUILD-emscripten +++ b/package/archlinux/PKGBUILD-emscripten @@ -35,6 +35,7 @@ build() { -DWITH_ASSIMPIMPORTER=OFF \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=OFF \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=OFF \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-emscripten-wasm b/package/archlinux/PKGBUILD-emscripten-wasm index 3743bfb3f..573f2eaf9 100644 --- a/package/archlinux/PKGBUILD-emscripten-wasm +++ b/package/archlinux/PKGBUILD-emscripten-wasm @@ -34,6 +34,7 @@ build() { -DBASIS_UNIVERSAL_DIR=/opt/basis-universal \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=OFF \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=OFF \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 b/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 index cd224c540..df6bf9ede 100644 --- a/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 +++ b/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 @@ -34,6 +34,7 @@ build() { -DBASIS_UNIVERSAL_DIR=/home/mosra/Code/basis_universal \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=OFF \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=OFF \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-gcc48 b/package/archlinux/PKGBUILD-gcc48 index 8484eea6b..495881d1a 100644 --- a/package/archlinux/PKGBUILD-gcc48 +++ b/package/archlinux/PKGBUILD-gcc48 @@ -38,6 +38,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-mingw-w64 b/package/archlinux/PKGBUILD-mingw-w64 index 73f4926b2..ac82f86de 100644 --- a/package/archlinux/PKGBUILD-mingw-w64 +++ b/package/archlinux/PKGBUILD-mingw-w64 @@ -24,6 +24,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=OFF \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=OFF \ -DWITH_DRFLACAUDIOIMPORTER=ON \ @@ -73,6 +74,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=OFF \ -DWITH_BASISIMPORTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=OFF \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-release b/package/archlinux/PKGBUILD-release index 7b1e4e5cf..0d8c51faf 100644 --- a/package/archlinux/PKGBUILD-release +++ b/package/archlinux/PKGBUILD-release @@ -26,6 +26,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ @@ -72,6 +73,7 @@ build() { -DBUILD_GL_TESTS=ON \ -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMPORTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/magnum-plugins-git/PKGBUILD b/package/archlinux/magnum-plugins-git/PKGBUILD index 7876abe25..f0fc76f02 100644 --- a/package/archlinux/magnum-plugins-git/PKGBUILD +++ b/package/archlinux/magnum-plugins-git/PKGBUILD @@ -36,6 +36,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ -DWITH_BASISIMPORTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/archlinux/magnum-plugins/PKGBUILD b/package/archlinux/magnum-plugins/PKGBUILD index ade70ec46..17a62d1a2 100644 --- a/package/archlinux/magnum-plugins/PKGBUILD +++ b/package/archlinux/magnum-plugins/PKGBUILD @@ -39,6 +39,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ -DWITH_BASISIMPORTER=ON \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/ci/appveyor-desktop-mingw.bat b/package/ci/appveyor-desktop-mingw.bat index 37bd98f48..3c32240e1 100644 --- a/package/ci/appveyor-desktop-mingw.bat +++ b/package/ci/appveyor-desktop-mingw.bat @@ -110,6 +110,7 @@ cmake .. ^ -DWITH_ASSIMPIMPORTER=ON ^ -DWITH_BASISIMAGECONVERTER=ON ^ -DWITH_BASISIMPORTER=ON -DBASIS_UNIVERSAL_DIR=%APPVEYOR_BUILD_FOLDER%/basis_universal ^ + -DWITH_CGLTFIMPORTER=ON ^ -DWITH_DDSIMPORTER=ON ^ -DWITH_DEVILIMAGEIMPORTER=ON ^ -DWITH_DRFLACAUDIOIMPORTER=ON ^ diff --git a/package/ci/appveyor-desktop.bat b/package/ci/appveyor-desktop.bat index ff0fd4573..33633155b 100644 --- a/package/ci/appveyor-desktop.bat +++ b/package/ci/appveyor-desktop.bat @@ -88,6 +88,7 @@ cmake .. ^ -DWITH_ASSIMPIMPORTER=%EXCEPT_MSVC2015% ^ -DWITH_BASISIMAGECONVERTER=ON ^ -DWITH_BASISIMPORTER=ON -DBASIS_UNIVERSAL_DIR=%APPVEYOR_BUILD_FOLDER%/basis_universal ^ + -DWITH_CGLTFIMPORTER=ON ^ -DWITH_DDSIMPORTER=ON ^ -DWITH_DEVILIMAGEIMPORTER=ON ^ -DWITH_DRFLACAUDIOIMPORTER=ON ^ diff --git a/package/ci/appveyor-rt.bat b/package/ci/appveyor-rt.bat index a0a856013..0705cfdcc 100644 --- a/package/ci/appveyor-rt.bat +++ b/package/ci/appveyor-rt.bat @@ -67,6 +67,7 @@ cmake .. ^ -DWITH_ASSIMPIMPORTER=OFF ^ -DWITH_BASISIMAGECONVERTER=ON ^ -DWITH_BASISIMPORTER=ON -DBASIS_UNIVERSAL_DIR=%APPVEYOR_BUILD_FOLDER%/basis_universal ^ + -DWITH_CGLTFIMPORTER=ON ^ -DWITH_DDSIMPORTER=ON ^ -DWITH_DEVILIMAGEIMPORTER=OFF ^ -DWITH_DRFLACAUDIOIMPORTER=OFF ^ diff --git a/package/ci/emscripten.sh b/package/ci/emscripten.sh index e58a1439b..e0dadef13 100755 --- a/package/ci/emscripten.sh +++ b/package/ci/emscripten.sh @@ -87,6 +87,7 @@ cmake .. \ -DWITH_ASSIMPIMPORTER=OFF \ -DWITH_BASISIMAGECONVERTER=OFF \ -DWITH_BASISIMPORTER=ON -DBASIS_UNIVERSAL_DIR=$HOME/basis_universal \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=OFF \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/ci/travis-android-arm.sh b/package/ci/travis-android-arm.sh index 6dcb26d7f..6bf3504f0 100755 --- a/package/ci/travis-android-arm.sh +++ b/package/ci/travis-android-arm.sh @@ -81,6 +81,7 @@ cmake .. \ -DWITH_ASSIMPIMPORTER=OFF \ -DWITH_BASISIMAGECONVERTER=ON \ -DWITH_BASISIMPORTER=ON -DBASIS_UNIVERSAL_DIR=$HOME/basis_universal \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=OFF \ -DWITH_DRMP3AUDIOIMPORTER=OFF \ diff --git a/package/ci/travis-ios-simulator.sh b/package/ci/travis-ios-simulator.sh index a243ca900..d907820ef 100755 --- a/package/ci/travis-ios-simulator.sh +++ b/package/ci/travis-ios-simulator.sh @@ -72,6 +72,7 @@ cmake .. \ -DWITH_ASSIMPIMPORTER=OFF \ -DWITH_BASISIMAGECONVERTER=ON \ -DWITH_BASISIMPORTER=ON -DBASIS_UNIVERSAL_DIR=$HOME/basis_universal \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=OFF \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/ci/unix-desktop.sh b/package/ci/unix-desktop.sh index bacf81407..a2deca763 100755 --- a/package/ci/unix-desktop.sh +++ b/package/ci/unix-desktop.sh @@ -54,6 +54,7 @@ cmake .. \ -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=ON \ -DWITH_BASISIMPORTER=ON -DBASIS_UNIVERSAL_DIR=$HOME/basis_universal \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/debian/rules b/package/debian/rules index c2c308ace..155f3aab7 100755 --- a/package/debian/rules +++ b/package/debian/rules @@ -18,6 +18,7 @@ override_dh_auto_configure: -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=OFF \ -DWITH_BASISIMPORTER=OFF \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/gentoo/dev-libs/magnum-plugins/magnum-plugins-9999.ebuild b/package/gentoo/dev-libs/magnum-plugins/magnum-plugins-9999.ebuild index 86a994c8d..f15a8e4b3 100644 --- a/package/gentoo/dev-libs/magnum-plugins/magnum-plugins-9999.ebuild +++ b/package/gentoo/dev-libs/magnum-plugins/magnum-plugins-9999.ebuild @@ -35,6 +35,7 @@ src_configure() { -DWITH_ASSIMPIMPORTER=ON -DWITH_BASISIMAGECONVERTER=OFF -DWITH_BASISIMPORTER=OFF + -DWITH_CGLTFIMPORTER=ON -DWITH_DDSIMPORTER=ON -DWITH_DEVILIMAGEIMPORTER=ON -DWITH_DRFLACAUDIOIMPORTER=ON diff --git a/package/homebrew/magnum-plugins.rb b/package/homebrew/magnum-plugins.rb index 4e9b7f11d..acc31cd2d 100644 --- a/package/homebrew/magnum-plugins.rb +++ b/package/homebrew/magnum-plugins.rb @@ -53,6 +53,7 @@ def install "-DWITH_ASSIMPIMPORTER=#{(build.with? 'assimp') ? 'ON' : 'OFF'}", "-DWITH_BASISIMAGECONVERTER=ON", "-DWITH_BASISIMPORTER=ON", + "-DWITH_CGLTFIMPORTER=ON", "-DWITH_DDSIMPORTER=ON", "-DWITH_DEVILIMAGEIMPORTER=#{(build.with? 'devil') ? 'ON' : 'OFF'}", "-DWITH_DRFLACAUDIOIMPORTER=ON", diff --git a/package/msys/PKGBUILD b/package/msys/PKGBUILD index f85712257..567643398 100644 --- a/package/msys/PKGBUILD +++ b/package/msys/PKGBUILD @@ -39,6 +39,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=OFF \ -DWITH_BASISIMPORTER=OFF \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/package/msys/magnum-plugins/PKGBUILD b/package/msys/magnum-plugins/PKGBUILD index 0d2c688dd..5633e584d 100644 --- a/package/msys/magnum-plugins/PKGBUILD +++ b/package/msys/magnum-plugins/PKGBUILD @@ -46,6 +46,7 @@ build() { -DWITH_ASSIMPIMPORTER=ON \ -DWITH_BASISIMAGECONVERTER=OFF \ -DWITH_BASISIMPORTER=OFF \ + -DWITH_CGLTFIMPORTER=ON \ -DWITH_DDSIMPORTER=ON \ -DWITH_DEVILIMAGEIMPORTER=ON \ -DWITH_DRFLACAUDIOIMPORTER=ON \ diff --git a/src/MagnumPlugins/CMakeLists.txt b/src/MagnumPlugins/CMakeLists.txt index dcf8ba1c0..361dc8a82 100644 --- a/src/MagnumPlugins/CMakeLists.txt +++ b/src/MagnumPlugins/CMakeLists.txt @@ -47,6 +47,10 @@ if(WITH_BASISIMPORTER) add_subdirectory(BasisImporter) endif() +if(WITH_CGLTFIMPORTER) + add_subdirectory(CgltfImporter) +endif() + if(WITH_DDSIMPORTER) add_subdirectory(DdsImporter) endif() diff --git a/src/MagnumPlugins/CgltfImporter/CMakeLists.txt b/src/MagnumPlugins/CgltfImporter/CMakeLists.txt new file mode 100644 index 000000000..0a1c4b2a7 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/CMakeLists.txt @@ -0,0 +1,79 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021 Vladimír Vondruš +# Copyright © 2021 Pablo Escobar +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +find_package(Magnum REQUIRED Trade AnyImageImporter) + +if(BUILD_PLUGINS_STATIC AND NOT DEFINED MAGNUM_CGLTFIMPORTER_BUILD_STATIC) + set(MAGNUM_CGLTFIMPORTER_BUILD_STATIC 1) +endif() + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h) + +# CgltfImporter plugin +add_plugin(CgltfImporter + "${MAGNUM_PLUGINS_IMPORTER_DEBUG_BINARY_INSTALL_DIR};${MAGNUM_PLUGINS_IMPORTER_DEBUG_LIBRARY_INSTALL_DIR}" + "${MAGNUM_PLUGINS_IMPORTER_RELEASE_BINARY_INSTALL_DIR};${MAGNUM_PLUGINS_IMPORTER_RELEASE_LIBRARY_INSTALL_DIR}" + CgltfImporter.conf + CgltfImporter.cpp + CgltfImporter.h) +if(MAGNUM_CGLTFIMPORTER_BUILD_STATIC AND BUILD_STATIC_PIC) + set_target_properties(CgltfImporter PROPERTIES POSITION_INDEPENDENT_CODE ON) +endif() +target_include_directories(CgltfImporter PUBLIC + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_BINARY_DIR}/src) +# Include the files as a system directory to supress warnings +target_include_directories(CgltfImporter SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/src/external/cgltf) +target_link_libraries(CgltfImporter PUBLIC Magnum::Trade) +if(CORRADE_TARGET_WINDOWS) + target_link_libraries(CgltfImporter PUBLIC Magnum::AnyImageImporter) +elseif(MAGNUM_CGLTFIMPORTER_BUILD_STATIC) + target_link_libraries(CgltfImporter INTERFACE Magnum::AnyImageImporter) +endif() +# Modify output location only if all are set, otherwise it makes no sense +if(CMAKE_RUNTIME_OUTPUT_DIRECTORY AND CMAKE_LIBRARY_OUTPUT_DIRECTORY AND CMAKE_ARCHIVE_OUTPUT_DIRECTORY) + set_target_properties(CgltfImporter PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/magnum$<$:-d>/importers + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/magnum$<$:-d>/importers + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/magnum$<$:-d>/importers) +endif() + +install(FILES CgltfImporter.h ${CMAKE_CURRENT_BINARY_DIR}/configure.h + DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/CgltfImporter) + +# Automatic static plugin import +if(MAGNUM_CGLTFIMPORTER_BUILD_STATIC) + install(FILES importStaticPlugin.cpp DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/CgltfImporter) + target_sources(CgltfImporter INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/importStaticPlugin.cpp) +endif() + +if(BUILD_TESTS) + add_subdirectory(Test) +endif() + +# MagnumPlugins CgltfImporter target alias for superprojects +add_library(MagnumPlugins::CgltfImporter ALIAS CgltfImporter) diff --git a/src/MagnumPlugins/CgltfImporter/CgltfImporter.conf b/src/MagnumPlugins/CgltfImporter/CgltfImporter.conf new file mode 100644 index 000000000..2bc65524e --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/CgltfImporter.conf @@ -0,0 +1,47 @@ +depends=AnyImageImporter +provides=GltfImporter + +# [config] +[configuration] + +# Don't fail the import and only print a warning if an unknown or unsupported +# extension is listed in extensionsRequired. Some things might be missing or +# not get imported correctly if this is enabled. +ignoreRequiredExtensions=false + +# Optimize imported linearly-interpolated quaternion animation tracks to +# ensure shortest path is always chosen. This can be controlled separately for +# each animation import. +optimizeQuaternionShortestPath=true + +# Normalize transformation quaternions and linearly-interpolated quaternion +# animation tracks, if they are not already. Note that spline-interpolated +# quaternion animation tracks are not patched. This can be controlled +# separately for each object/animation import. +normalizeQuaternions=true + +# Merge all animations into a single clip. Useful for preserving cinematic +# animations when using the Blender glTF exporter, as it exports animation of +# every object as a separate clip. See https://blender.stackexchange.com/q/5689 +# and https://github.com/KhronosGroup/glTF-Blender-Exporter/pull/166 for more +# information. +mergeAnimationClips=false + +# Perform Y-flip for texture coordinates in a material texture transform. By +# default texture coordinates are Y-flipped directly in the mesh data to avoid +# the need to supply texture transformation matrix to a shader, enabling this +# will cause all texture coordinate data to be unchanged and instead all +# materials will have a Y-flipping texture transformation present. Note that +# this flag has to be enabled before opening a file, changing it during import +# will have undefined behavior. +textureCoordinateYFlipInMaterial=false + +# The non-standard MeshAttribute::ObjectId is by default recognized under this +# name. Change if your file uses a different identifier. +objectIdAttribute=_OBJECT_ID + +# Provide basic Phong material attributes even for PBR materials in order to be +# compatible with PhongMaterialData workflows from version 2020.06 and before. +# This option will eventually become disabled by default. +phongMaterialFallback=true +# [config] diff --git a/src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp b/src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp new file mode 100644 index 000000000..a14ac74f7 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp @@ -0,0 +1,2647 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + Copyright © 2021 Pablo Escobar + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "CgltfImporter.h" + +#include /* std::stable_sort() */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MagnumPlugins/AnyImageImporter/AnyImageImporter.h" + +/* Cgltf doesn't load .glb on big-endian correctly: + https://github.com/jkuhlmann/cgltf/issues/150 + Even if we patched .glb files in memory, we'd still need to convert all + buffers from little-endian. Adds a lot of complexity, and not testable. */ +#ifdef CORRADE_TARGET_BIG_ENDIAN +#error Big-endian systems are not supported by cgltf +#endif + +/* Since we set custom allocator callbacks for cgltf_parse, we can override + CGLTF_MALLOC / CGLTF_FREE to assert that the default allocation functions + aren't called */ +namespace { + /* LCOV_EXCL_START */ + void* mallocNoop(size_t) { CORRADE_INTERNAL_ASSERT_UNREACHABLE(); } + void freeNoop(void*) { CORRADE_INTERNAL_ASSERT_UNREACHABLE(); } + /* LCOV_EXCL_STOP */ +} +#define CGLTF_MALLOC(size) mallocNoop(size) +#define CGLTF_FREE(ptr) freeNoop(ptr) +/* If we had a good replacement for ato(i|f|ll) we could set the corresponding + CGLTF_ATOI etc. here and prevent stdlib.h from being included in cgltf.h */ + +#define CGLTF_IMPLEMENTATION + +#include + +/* std::hash specialization to be able to use StringView in unordered_map. + Injecting this into namespace std seems to be the designated way but it + feels wrong. */ +namespace std { + template<> struct hash { + std::size_t operator()(const Corrade::Containers::StringView& key) const { + const Corrade::Utility::MurmurHash2 hash; + const Corrade::Utility::HashDigest digest = hash(key.data(), key.size()); + return *reinterpret_cast(digest.byteArray()); + } + }; +} + +namespace Magnum { namespace Trade { + +using namespace Containers::Literals; +using namespace Magnum::Math::Literals; + +namespace { + +/* Convert cgltf type enums back into strings for useful error output */ +Containers::StringView gltfTypeName(cgltf_type type) { + switch(type) { + case cgltf_type_scalar: return "SCALAR"_s; + case cgltf_type_vec2: return "VEC2"_s; + case cgltf_type_vec3: return "VEC3"_s; + case cgltf_type_vec4: return "VEC4"_s; + case cgltf_type_mat2: return "MAT2"_s; + case cgltf_type_mat3: return "MAT3"_s; + case cgltf_type_mat4: return "MAT4"_s; + case cgltf_type_invalid: + break; + } + + return "UNKNOWN"_s; +} + +Containers::StringView gltfComponentTypeName(cgltf_component_type type) { + switch(type) { + case cgltf_component_type_r_8: return "BYTE (5120)"_s; + case cgltf_component_type_r_8u: return "UNSIGNED_BYTE (5121)"_s; + case cgltf_component_type_r_16: return "SHORT (5122)"_s; + case cgltf_component_type_r_16u: return "UNSIGNED_SHORT (5123)"_s; + case cgltf_component_type_r_32u: return "UNSIGNED_INT (5125)"_s; + case cgltf_component_type_r_32f: return "FLOAT (5126)"_s; + case cgltf_component_type_invalid: + break; + } + + return "UNKNOWN"_s; +} + +std::size_t elementSize(const cgltf_accessor* accessor) { + /* Technically cgltf_calc_size isn't part of the public API but we bundle + cgltf so there shouldn't be any surprises. Worst case we'll have + to copy its content from an old version if it gets removed. */ + return cgltf_calc_size(accessor->type, accessor->component_type); +} + +bool checkAccessor(const cgltf_data* data, const char* const function, const cgltf_accessor* accessor) { + CORRADE_INTERNAL_ASSERT(accessor); + const UnsignedInt accessorId = accessor - data->accessors; + + /** @todo Validate alignment rules, calculate correct stride in accessorView(): + https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#data-alignment */ + + if(accessor->is_sparse) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): accessor" << accessorId << "is using sparse storage, which is unsupported"; + return false; + } + /* Buffer views are optional in accessors, we're supposed to fill the view + with zeros. Only makes sense with sparse data and we don't support that. */ + if(!accessor->buffer_view) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): accessor" << accessorId << "has no buffer view"; + return false; + } + + const cgltf_buffer_view* bufferView = accessor->buffer_view; + const UnsignedInt bufferViewId = bufferView - data->buffer_views; + const cgltf_buffer* buffer = bufferView->buffer; + + const std::size_t typeSize = elementSize(accessor); + const std::size_t requiredBufferViewSize = accessor->offset + accessor->stride*(accessor->count - 1) + typeSize; + if(bufferView->size < requiredBufferViewSize) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): accessor" << accessorId << "needs" << requiredBufferViewSize << "bytes but buffer view" << bufferViewId << "has only" << bufferView->size; + return false; + } + + const std::size_t requiredBufferSize = bufferView->offset + bufferView->size; + if(buffer->size < requiredBufferSize) { + const UnsignedInt bufferId = buffer - data->buffers; + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): buffer view" << bufferViewId << "needs" << requiredBufferSize << "bytes but buffer" << bufferId << "has only" << buffer->size; + return false; + } + + /* Cgltf copies the bufferview stride into the accessor. If that's zero, it + copies the element size into the stride. */ + if(accessor->stride < typeSize) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "():" << typeSize << Debug::nospace << "-byte type defined by accessor" << accessorId << "can't fit into buffer view" << bufferViewId << "stride of" << accessor->stride; + return false; + } + + return true; +} + +/* Data URI according to RFC 2397 */ +bool isDataUri(Containers::StringView uri) { + return uri.hasPrefix("data:"_s); +} + +/* Decode percent-encoded characters in URIs: + https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 */ +std::string decodeUri(Containers::StringView uri) { + std::string decoded = uri; + const std::size_t decodedSize = cgltf_decode_uri(&decoded[0]); + decoded.resize(decodedSize); + + return decoded; +} + +struct JsonToken { + jsmntype_t type; + Containers::StringView str; + std::size_t size; +}; + +Containers::Array parseJson(Containers::StringView str) { + jsmn_parser parser{0, 0, 0}; + Int numTokens = jsmn_parse(&parser, str.data(), str.size(), nullptr, 0); + /* All JSON strings we're parsing come from cgltf and should already have + passed jsmn parsing */ + CORRADE_INTERNAL_ASSERT(numTokens >= 0); + + Containers::Array jsmnTokens{std::size_t(numTokens)}; + jsmn_init(&parser); + numTokens = jsmn_parse(&parser, str.data(), str.size(), jsmnTokens.data(), numTokens); + CORRADE_INTERNAL_ASSERT(std::size_t(numTokens) == jsmnTokens.size()); + + Containers::Array tokens{jsmnTokens.size()}; + for(Int i = 0; i != numTokens; ++i) { + tokens[i].type = jsmnTokens[i].type; + tokens[i].str = str.slice(jsmnTokens[i].start, jsmnTokens[i].end); + tokens[i].size = jsmnTokens[i].size; + } + + return tokens; +} + +} + +struct CgltfImporter::Document { + ~Document(); + + Containers::Optional filePath; + Containers::Array fileData; + + cgltf_options options; + cgltf_data* data = nullptr; + + Containers::Optional> loadUri(Containers::StringView uri, Containers::Array& storage, const char* const function); + bool loadBuffer(UnsignedInt id, const char* const function); + Containers::Optional> accessorView(const cgltf_accessor* accessor, const char* const function); + + /* Storage for buffer content if the user set no file callback or a buffer + is embedded as base64. These are filled on demand. We don't check for + duplicate URIs since that's incredibly unlikely and hard to get right, + so the buffer id is used as the index. */ + Containers::Array> bufferData; + + /* Cgltf's JSON parser jsmn doesn't decode escaped characters so we do it + after parsing. If there's nothing to escape, returns a view on the + original string. Decoded strings are cached in a map indexed by the + input view data pointer. This works because we only call this function + with views on strings from cgltf_data. + + Note that parsing inside cgltf happens with unescaped strings, but we + have no influence on that. In practice, this shouldn't be a problem. + Old versions of the spec used to explicitly forbid non-ASCII keys/enums: + https://github.com/KhronosGroup/glTF/tree/fd3ab461a1114fb0250bd76099153d2af50a7a1d/specification/2.0#json-encoding + Newer spec versions changed this to "ASCII characters [...] SHOULD be + written without JSON escaping" */ + Containers::StringView decodeString(Containers::StringView str); + + std::unordered_map decodedStrings; + + /* We can use StringView as the map key here because all underlying strings + won't go out of scope while a file is opened. They either point to the + original name strings in cgltf_data or to decodedStrings. */ + Containers::Optional> + animationsForName, + camerasForName, + lightsForName, + scenesForName, + skinsForName, + nodesForName, + meshesForName, + materialsForName, + imagesForName, + texturesForName; + + /* Unlike the ones above, these are filled already during construction as + we need them in three different places and on-demand construction would + be too annoying to test. Also, assuming the importer knows all builtin + names, in most cases these would be empty anyway. */ + std::unordered_map + meshAttributesForName; + Containers::Array meshAttributeNames; + + /* Mapping for multi-primitive meshes: + + - meshMap.size() is the count of meshes reported to the user + - meshSizeOffsets.size() is the count of original meshes in the file + - meshMap[id] is a pair of (original mesh ID, primitive ID) + - meshSizeOffsets[j] points to the first item in meshMap for + original mesh ID `j` -- which also translates the original ID to + reported ID + - meshSizeOffsets[j + 1] - meshSizeOffsets[j] is count of meshes for + original mesh ID `j` (or number of primitives in given mesh) + */ + Containers::Array> meshMap; + Containers::Array meshSizeOffsets; + + /* Mapping for nodes having multi-primitive nodes. The same as above, but + for nodes. Hierarchy-wise, the subsequent nodes are direct children of + the first, have no transformation or other children and point to the + subsequent meshes. */ + Containers::Array> nodeMap; + Containers::Array nodeSizeOffsets; + + /* If a file contains texture coordinates that are not floats or normalized + in the 0-1, the textureCoordinateYFlipInMaterial option is enabled + implicitly as we can't perform Y-flip directly on the data. */ + bool textureCoordinateYFlipInMaterial = false; + + void materialTexture(const cgltf_texture_view& texture, Containers::Array& attributes, MaterialAttribute attribute, MaterialAttribute matrixAttribute, MaterialAttribute coordinateAttribute) const; + + bool open = false; + + UnsignedInt imageImporterId = ~UnsignedInt{}; + Containers::Optional imageImporter; +}; + +CgltfImporter::Document::~Document() { + if(data) cgltf_free(data); +} + +Containers::Optional> CgltfImporter::Document::loadUri(Containers::StringView uri, Containers::Array& storage, const char* const function) { + const AbstractImporter& importer = *static_cast(options.file.user_data); + + if(isDataUri(uri)) { + /* Data URI with base64 payload according to RFC 2397: + data:[][;base64], */ + Containers::StringView base64; + const Containers::Array3 parts = uri.partition(','); + + /* Non-base64 data URIs are allowed by RFC 2397, but make no sense for + glTF. cgltf_load_buffers doesn't allow them, either. */ + if(parts.front().hasSuffix(";base64"_s)) { + /* This will be empty for both a missing comma and an empty payload */ + base64 = parts.back(); + } + + if(base64.isEmpty()) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): data URI has no base64 payload"; + return Containers::NullOpt; + } + + /* Decoded size. For some reason cgltf_load_buffer_base64 doesn't take + the string length as input, and fails if it finds a padding + character. */ + const std::size_t padding = base64.size() - base64.trimmedSuffix("="_s).size(); + const std::size_t size = base64.size()/4*3 - padding; + + /* cgltf_load_buffer_base64 will allocate using the memory callbacks + set in doOpenData() which use new char[] and delete[]. We can wrap + that memory in an Array with the default deleter. */ + void* decoded = nullptr; + const cgltf_result result = cgltf_load_buffer_base64(&options, size, base64.data(), &decoded); + if(result != cgltf_result_success) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): invalid base64 string in data URI"; + return Containers::NullOpt; + } + CORRADE_INTERNAL_ASSERT(decoded); + storage = Containers::Array{static_cast(decoded), size}; + return Containers::arrayCast(storage); + } else if(importer.fileCallback()) { + const std::string fullPath = Utility::Directory::join(filePath ? *filePath : "", decodeUri(decodeString(uri))); + Containers::Optional> view = importer.fileCallback()(fullPath, InputFileCallbackPolicy::LoadPermanent, importer.fileCallbackUserData()); + if(!view) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): error opening file:" << Containers::StringView{fullPath} << ": file callback failed"; + return Containers::NullOpt; + } + return *view; + } else { + if(!filePath) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): external buffers can be imported only when opening files from the filesystem or if a file callback is present"; + return Containers::NullOpt; + } + const std::string fullPath = Utility::Directory::join(*filePath, decodeUri(decodeString(uri))); + if(!Utility::Directory::exists(fullPath)) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): error opening file:" << Containers::StringView{fullPath} << ": file not found"; + return Containers::NullOpt; + } + storage = Utility::Directory::read(fullPath); + return Containers::arrayCast(storage); + } +} + +bool CgltfImporter::Document::loadBuffer(UnsignedInt id, const char* const function) { + CORRADE_INTERNAL_ASSERT(id < data->buffers_count); + cgltf_buffer& buffer = data->buffers[id]; + if(buffer.data) + return true; + + Containers::ArrayView view; + if(!buffer.uri) { + /* URI may only be empty for buffers referencing the glb binary blob */ + if(id != 0 || !data->bin) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "():" << + "buffer" << id << "has no URI"; + return false; + } + view = Containers::arrayView(static_cast(data->bin), data->bin_size); + } else { + const auto loaded = loadUri(buffer.uri, bufferData[id], function); + if(!loaded) + return false; + view = *loaded; + } + + /* The spec mentions that non-GLB buffer length can be greater than + byteLength. GLB buffer chunks may also be up to 3 bytes larger than + byteLength because of padding. So we can't check for equality. */ + if(view.size() < buffer.size) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "():" << + "buffer" << id << "is too short, expected" << buffer.size << + "bytes but got" << view.size(); + return false; + } + + buffer.data = const_cast(view.data()); /* sigh */ + /* Tell cgltf not to free buffer.data in cgltf_free */ + buffer.data_free_method = cgltf_data_free_method_none; + + return true; +} + +Containers::Optional> CgltfImporter::Document::accessorView(const cgltf_accessor* accessor, const char* const function) { + /* All this assumes the accessor was checked using checkAccessor() */ + const cgltf_buffer_view* bufferView = accessor->buffer_view; + const cgltf_buffer* buffer = bufferView->buffer; + const UnsignedInt bufferId = buffer - data->buffers; + if(!loadBuffer(bufferId, function)) + return Containers::NullOpt; + + return Containers::StridedArrayView2D{Containers::arrayView(buffer->data, buffer->size), + reinterpret_cast(buffer->data) + bufferView->offset + accessor->offset, + {accessor->count, elementSize(accessor)}, + {std::ptrdiff_t(accessor->stride), 1}}; +} + +Containers::StringView CgltfImporter::Document::decodeString(Containers::StringView str) { + if(str.isEmpty()) + return str; + + /* String has been decoded before */ + const auto found = decodedStrings.find(str.data()); + if(found != decodedStrings.end()) + return found->second; + + /* The input string can be UTF-8 encoded but we can use a byte search here + since all multi-byte UTF-8 characters have the high bit set and '\\' + doesn't, so this will only match single-byte ASCII characters. */ + Containers::StringView escape = str.find('\\'); + /* No escaped sequence found. If the view is null-terminated (all strings + in cgltf_data should be), this stores a non-owning String. */ + if(escape.isEmpty()) + return decodedStrings.emplace(str.data(), Containers::String::nullTerminatedView(str)).first->second; + + /* Skip any processing until the first escape character */ + const std::size_t start = escape.data() - str.data(); + + Containers::String decoded{str}; + const std::size_t decodedSize = cgltf_decode_string(decoded.data() + start) + start; + CORRADE_INTERNAL_ASSERT(decodedSize < str.size()); + + return decodedStrings.emplace(str.data(), Containers::String{decoded.prefix(decodedSize)}).first->second; +} + +namespace { + +void fillDefaultConfiguration(Utility::ConfigurationGroup& conf) { + /** @todo horrible workaround, fix this properly */ + conf.setValue("ignoreRequiredExtensions", false); + conf.setValue("optimizeQuaternionShortestPath", true); + conf.setValue("normalizeQuaternions", true); + conf.setValue("mergeAnimationClips", false); + conf.setValue("phongMaterialFallback", true); + conf.setValue("objectIdAttribute", "_OBJECT_ID"); +} + +} + +CgltfImporter::CgltfImporter() { + /** @todo horrible workaround, fix this properly */ + fillDefaultConfiguration(configuration()); +} + +CgltfImporter::CgltfImporter(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractImporter{manager, plugin} {} + +CgltfImporter::CgltfImporter(PluginManager::Manager& manager): AbstractImporter{manager} { + /** @todo horrible workaround, fix this properly */ + fillDefaultConfiguration(configuration()); +} + +CgltfImporter::~CgltfImporter() = default; + +ImporterFeatures CgltfImporter::doFeatures() const { return ImporterFeature::OpenData|ImporterFeature::FileCallback; } + +bool CgltfImporter::doIsOpened() const { return !!_d && _d->open; } + +void CgltfImporter::doClose() { _d = nullptr; } + +void CgltfImporter::doOpenFile(const std::string& filename) { + _d.reset(new Document); + _d->filePath = Utility::Directory::path(filename); + AbstractImporter::doOpenFile(filename); +} + +void CgltfImporter::doOpenData(const Containers::ArrayView data) { + if(!_d) _d.reset(new Document); + + /* Copy file content. We need to keep the data around for .glb binary blobs + and extension data which cgltf stores as pointers into the original + memory passed to cgltf_parse. */ + _d->fileData = Containers::Array{data.size()}; + Utility::copy(data, _d->fileData); + + /* Auto-detect glb/gltf */ + _d->options.type = cgltf_file_type::cgltf_file_type_invalid; + /* Determine json token count to allocate (by parsing twice) */ + _d->options.json_token_count = 0; + + /* Set up memory callbacks. The default memory callbacks (when set to + nullptr) use malloc and free. Prefer using new and delete, allows us to + use the default deleter when wrapping memory in Array, and it'll throw + bad_alloc if allocation fails. */ + _d->options.memory.alloc = [](void*, cgltf_size size) -> void* { return new char[size]; }; + _d->options.memory.free = [](void*, void* ptr) { delete[] static_cast(ptr); }; + _d->options.memory.user_data = nullptr; + + /* The file callbacks are only needed for cgltf_load_buffers which we don't + call, but we still replace the default ones to assert that they're + never called. Unfortunately, this doesn't prevent cgltf from linking to + stdio anyway. */ + _d->options.file.read = [](const cgltf_memory_options*, const cgltf_file_options*, const char*, cgltf_size*, void**) -> cgltf_result { + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + }; + _d->options.file.release = [](const cgltf_memory_options*, const cgltf_file_options*, void* ptr) -> void { + /* cgltf_free calls this function with a nullptr file_data that's only + set when using cgltf_parse_file */ + if(ptr == nullptr) return; + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + }; + _d->options.file.user_data = this; + + /* Parse file, without loading or decoding buffers/images */ + const cgltf_result result = cgltf_parse(&_d->options, _d->fileData.data(), _d->fileData.size(), &_d->data); + + /* A general note on error checking in cgltf: + - cgltf_parse fails if any index is out of bounds, mandatory or not + - cgltf_parse fails if a mandatory property is missing + - optional properties are set to the spec-mandated default value (if + there is one), 0 or nullptr (if they're indices). + + We're not using cgltf_validate() because the error granularity is rather + underwhelming. All of its relevant checks are implemented on our side, + allowing us to delay them to when they're needed, e.g. accessor and + buffer size. */ + if(result != cgltf_result_success) { + const char* error{}; + switch(result) { + /* This can also be returned for arrays with too many items before + any allocation happens, so we can't quite ignore it. Rather + impossible to test, however. */ + case cgltf_result_out_of_memory: + error = "out of memory"; + break; + case cgltf_result_unknown_format: + error = "unknown binary glTF format"; + break; + case cgltf_result_invalid_json: + error = "invalid JSON"; + break; + case cgltf_result_invalid_gltf: + error = "invalid glTF, usually caused by invalid indices or missing required attributes"; + break; + case cgltf_result_legacy_gltf: + error = "legacy glTF version"; + break; + case cgltf_result_data_too_short: + error = "data too short"; + break; + /* LCOV_EXCL_START */ + /* Only returned from cgltf's default file callback */ + case cgltf_result_file_not_found: + /* Only returned by cgltf_load_buffer_base64 and cgltf's default + file callback */ + case cgltf_result_io_error: + /* We passed a nullptr somewhere, this should never happen */ + case cgltf_result_invalid_options: + default: + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + /* LCOV_EXCL_STOP */ + } + + Error{} << "Trade::CgltfImporter::openData(): error opening file:" << error; + doClose(); + return; + } + + CORRADE_INTERNAL_ASSERT(_d->data != nullptr); + + /* Major versions are forward- and backward-compatible, but minVersion can + be used to require support for features added in new minor versions. + So far there's only 2.0 so we can use an exact comparison. + cgltf already checked that asset.version >= 2.0 (if it exists). */ + const cgltf_asset& asset = _d->data->asset; + if(asset.min_version && asset.min_version != "2.0"_s) { + Error{} << "Trade::CgltfImporter::openData(): unsupported minVersion" << asset.min_version << Debug::nospace << ", expected 2.0"; + doClose(); + return; + } + if(asset.version && !Containers::StringView{asset.version}.hasPrefix("2."_s)) { + Error{} << "Trade::CgltfImporter::openData(): unsupported version" << asset.version << Debug::nospace << ", expected 2.x"; + doClose(); + return; + } + + /* Check required extensions. Every extension in extensionsRequired is + required to "load and/or render an asset". */ + const bool ignoreRequiredExtensions = configuration().value("ignoreRequiredExtensions"); + + constexpr Containers::StringView supportedExtensions[]{ + /* Parsed by cgltf and handled by us */ + "KHR_lights_punctual"_s, + "KHR_materials_clearcoat"_s, + "KHR_materials_pbrSpecularGlossiness"_s, + "KHR_materials_unlit"_s, + "KHR_mesh_quantization"_s, + "KHR_texture_basisu"_s, + "KHR_texture_transform"_s, + /* Manually parsed */ + "GOOGLE_texture_basis"_s, + "MSFT_texture_dds"_s + }; + + /* M*N loop should be okay here, extensionsRequired should usually have no or + very few entries. Consider binary search if the list of supported + extensions reaches a few dozen. */ + for(Containers::StringView required: Containers::arrayView(_d->data->extensions_required, _d->data->extensions_required_count)) { + bool found = false; + for(const auto& supported: supportedExtensions) { + if(supported == required) { + found = true; + break; + } + } + + if(!found) { + if(ignoreRequiredExtensions) { + Warning{} << "Trade::CgltfImporter::openData(): required extension" << required << "not supported"; + } else { + Error{} << "Trade::CgltfImporter::openData(): required extension" << required << "not supported"; + doClose(); + return; + } + } + } + + /* Find cycles in node tree */ + for(std::size_t i = 0; i != _d->data->nodes_count; ++i) { + const cgltf_node* p1 = _d->data->nodes[i].parent; + const cgltf_node* p2 = p1 ? p1->parent : nullptr; + + while(p1 && p2) { + if(p1 == p2) { + Error{} << "Trade::CgltfImporter::openData(): node tree contains cycle starting at node" << i; + doClose(); + return; + } + + p1 = p1->parent; + p2 = p2->parent ? p2->parent->parent : nullptr; + } + } + + /* Treat meshes with multiple primitives as separate meshes. Each mesh gets + duplicated as many times as is the size of the primitives array. */ + Containers::arrayReserve(_d->meshMap, _d->data->meshes_count); + _d->meshSizeOffsets = Containers::Array{_d->data->meshes_count + 1}; + + _d->meshSizeOffsets[0] = 0; + for(std::size_t i = 0; i != _d->data->meshes_count; ++i) { + const std::size_t count = _d->data->meshes[i].primitives_count; + CORRADE_INTERNAL_ASSERT(count > 0); + for(std::size_t j = 0; j != count; ++j) + arrayAppend(_d->meshMap, InPlaceInit, i, j); + + _d->meshSizeOffsets[i + 1] = _d->meshMap.size(); + } + + /* In order to support multi-primitive meshes, we need to duplicate the + nodes as well */ + Containers::arrayReserve(_d->nodeMap, _d->data->nodes_count); + _d->nodeSizeOffsets = Containers::Array{_d->data->nodes_count + 1}; + + _d->nodeSizeOffsets[0] = 0; + for(std::size_t i = 0; i != _d->data->nodes_count; ++i) { + const cgltf_mesh* mesh = _d->data->nodes[i].mesh; + /* If a node has a mesh with multiple primitives, add nested nodes + containing the other primitives after it */ + const std::size_t count = mesh ? mesh->primitives_count : 1; + for(std::size_t j = 0; j != count; ++j) + arrayAppend(_d->nodeMap, InPlaceInit, i, j); + + _d->nodeSizeOffsets[i + 1] = _d->nodeMap.size(); + } + + /* Go through all meshes, collect custom attributes and decide about + implicitly enabling textureCoordinateYFlipInMaterial if it isn't already + requested from the configuration and there are any texture coordinates + that need it */ + if(configuration().value("textureCoordinateYFlipInMaterial")) + _d->textureCoordinateYFlipInMaterial = true; + for(const cgltf_mesh& mesh: Containers::arrayView(_d->data->meshes, _d->data->meshes_count)) { + for(const cgltf_primitive& primitive: Containers::arrayView(mesh.primitives, mesh.primitives_count)) { + for(const cgltf_attribute& attribute: Containers::arrayView(primitive.attributes, primitive.attributes_count)) { + if(attribute.type == cgltf_attribute_type_texcoord) { + if(!_d->textureCoordinateYFlipInMaterial) { + const cgltf_component_type type = attribute.data->component_type; + const bool normalized = attribute.data->normalized; + if(type == cgltf_component_type_r_8 || + type == cgltf_component_type_r_16 || + (type == cgltf_component_type_r_8u && !normalized) || + (type == cgltf_component_type_r_16u && !normalized)) { + Debug{} << "Trade::CgltfImporter::openData(): file contains non-normalized texture coordinates, implicitly enabling textureCoordinateYFlipInMaterial"; + _d->textureCoordinateYFlipInMaterial = true; + } + } + + /* If the name isn't recognized or not in MeshAttribute, add + the attribute to custom if not there already */ + } else if(attribute.type != cgltf_attribute_type_position && + attribute.type != cgltf_attribute_type_normal && + attribute.type != cgltf_attribute_type_tangent && + attribute.type != cgltf_attribute_type_color) + { + /* Get the semantic base name ([semantic]_[set_index]) for + known attributes that are not supported in MeshAttribute + (JOINTS_n and WEIGHTS_n). This lets us group multiple + sets to the same attribute. + For unknown/user-defined attributes all name formats are + allowed and we don't attempt to group them. */ + /** @todo Remove all this once Magnum adds these to MeshAttribute + (pending https://github.com/mosra/magnum/pull/441) */ + const Containers::StringView name{attribute.name}; + const Containers::StringView semantic = attribute.type != cgltf_attribute_type_invalid ? + name.partition('_')[0] : name; + + /* The spec says that all user-defined attributes must + start with an underscore. We don't really care and just + print a warning. */ + if(attribute.type == cgltf_attribute_type_invalid && !name.hasPrefix("_"_s)) + Warning{} << "Trade::CgltfImporter::openData(): unknown attribute" << name << Debug::nospace << ", importing as custom attribute"; + + if(_d->meshAttributesForName.emplace(semantic, + meshAttributeCustom(_d->meshAttributeNames.size())).second) + arrayAppend(_d->meshAttributeNames, semantic); + } + } + } + } + + _d->open = true; + + /* Buffers are loaded on demand, but we need to prepare the storage array */ + _d->bufferData = Containers::Array>{_d->data->buffers_count}; + + /* Name maps are lazy-loaded because these might not be needed every time */ +} + +UnsignedInt CgltfImporter::doAnimationCount() const { + /* If the animations are merged, there's at most one */ + if(configuration().value("mergeAnimationClips")) + return _d->data->animations_count == 0 ? 0 : 1; + + return _d->data->animations_count; +} + +Int CgltfImporter::doAnimationForName(const std::string& name) { + /* If the animations are merged, don't report any names */ + if(configuration().value("mergeAnimationClips")) return -1; + + if(!_d->animationsForName) { + _d->animationsForName.emplace(); + _d->animationsForName->reserve(_d->data->animations_count); + for(std::size_t i = 0; i != _d->data->animations_count; ++i) + _d->animationsForName->emplace(_d->decodeString(_d->data->animations[i].name), i); + } + + const auto found = _d->animationsForName->find(name); + return found == _d->animationsForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doAnimationName(UnsignedInt id) { + /* If the animations are merged, don't report any names */ + if(configuration().value("mergeAnimationClips")) return {}; + return _d->decodeString(_d->data->animations[id].name); +} + +namespace { + +template void postprocessSplineTrack(const cgltf_accessor* timeTrackUsed, const Containers::ArrayView keys, const Containers::ArrayView> values) { + /* Already processed, don't do that again */ + if(timeTrackUsed != nullptr) return; + + CORRADE_INTERNAL_ASSERT(keys.size() == values.size()); + if(keys.size() < 2) return; + + /* Convert the `a` values to `n` and the `b` values to `m` as described in + https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#appendix-c-spline-interpolation + Unfortunately I was not able to find any concrete name for this, so it's + not part of the CubicHermite implementation but is kept here locally. */ + for(std::size_t i = 0; i < keys.size() - 1; ++i) { + const Float timeDifference = keys[i + 1] - keys[i]; + values[i].outTangent() *= timeDifference; + values[i + 1].inTangent() *= timeDifference; + } +} + +} + +Containers::Optional CgltfImporter::doAnimation(UnsignedInt id) { + /* Import either a single animation or all of them together. At the moment, + Blender doesn't really support cinematic animations (affecting multiple + objects): https://blender.stackexchange.com/q/5689. And since + https://github.com/KhronosGroup/glTF-Blender-Exporter/pull/166, these + are exported as a set of object-specific clips, which may not be wanted, + so we give the users an option to merge them all together. */ + const std::size_t animationBegin = + configuration().value("mergeAnimationClips") ? 0 : id; + const std::size_t animationEnd = + configuration().value("mergeAnimationClips") ? _d->data->animations_count : id + 1; + + const Containers::ArrayView animations = Containers::arrayView( + _d->data->animations + animationBegin, animationEnd - animationBegin); + + /* First gather the input and output data ranges. Key is unique accessor + pointer so we don't duplicate shared data, value is range in the input + buffer, offset in the output data and pointer of the corresponding key + track in case given track is a spline interpolation. The key pointer is + initialized to nullptr and will be used later to check that a spline + track was not used with more than one time track, as it needs to be + postprocessed for given time track. */ + struct SamplerData { + Containers::StridedArrayView2D src; + std::size_t outputOffset; + const cgltf_accessor* timeTrack; + }; + std::unordered_map samplerData; + std::size_t dataSize = 0; + for(const cgltf_animation& animation: animations) { + for(std::size_t i = 0; i != animation.samplers_count; ++i) { + const cgltf_animation_sampler& sampler = animation.samplers[i]; + + /** @todo handle alignment once we do more than just four-byte types */ + + /* If the input view is not yet present in the output data buffer, + add it */ + if(samplerData.find(sampler.input) == samplerData.end()) { + if(!checkAccessor(_d->data, "animation", sampler.input)) + return Containers::NullOpt; + Containers::Optional> view = _d->accessorView(sampler.input, "animation"); + if(!view) + return Containers::NullOpt; + + samplerData.emplace(sampler.input, SamplerData{*view, dataSize, nullptr}); + dataSize += view->size()[0]*view->size()[1]; + } + + /* If the output view is not yet present in the output data buffer, + add it */ + if(samplerData.find(sampler.output) == samplerData.end()) { + if(!checkAccessor(_d->data, "animation", sampler.output)) + return Containers::NullOpt; + Containers::Optional> view = _d->accessorView(sampler.output, "animation"); + if(!view) + return Containers::NullOpt; + + samplerData.emplace(sampler.output, SamplerData{*view, dataSize, nullptr}); + dataSize += view->size()[0]*view->size()[1]; + } + } + } + + /* Populate the data array */ + /** + * @todo Once memory-mapped files are supported, this can all go away + * except when spline tracks are present -- in that case we need to + * postprocess them and can't just use the memory directly. + */ + Containers::Array data{dataSize}; + for(const std::pair& view: samplerData) { + Containers::StridedArrayView2D src = view.second.src; + Containers::StridedArrayView2D dst{data.suffix(view.second.outputOffset), + src.size()}; + Utility::copy(src, dst); + } + + /* Calculate total track count. If merging all animations together, this is + the sum of all clip track counts. */ + std::size_t trackCount = 0; + for(const cgltf_animation& animation : animations) { + for(std::size_t i = 0; i != animation.channels_count; ++i) { + /* Skip animations without a target node. See comment below. */ + if(animation.channels[i].target_node) + ++trackCount; + } + } + + /* Import all tracks */ + bool hadToRenormalize = false; + std::size_t trackId = 0; + Containers::Array tracks{trackCount}; + for(const cgltf_animation& animation : animations) { + for(std::size_t i = 0; i != animation.channels_count; ++i) { + const cgltf_animation_channel& channel = animation.channels[i]; + const cgltf_animation_sampler& sampler = *channel.sampler; + + /* Skip animations without a target node. Consistent with + tinygltf's behavior, currently there are no extensions for + animating materials or anything else so there's no point in + importing such animations. */ + if(!channel.target_node) + continue; + + /* Key properties -- always float time. Not using checkAccessor() + as this was all checked above once already. */ + const cgltf_accessor* input = sampler.input; + if(input->type != cgltf_type_scalar || input->component_type != cgltf_component_type_r_32f || input->normalized) { + Error{} << "Trade::CgltfImporter::animation(): time track has unexpected type" + << (input->normalized ? "normalized " : "") << Debug::nospace + << gltfTypeName(input->type) << "/" << gltfComponentTypeName(input->component_type); + return Containers::NullOpt; + } + + /* View on the key data */ + const auto inputDataFound = samplerData.find(input); + CORRADE_INTERNAL_ASSERT(inputDataFound != samplerData.end()); + const auto keys = Containers::arrayCast( + data.suffix(inputDataFound->second.outputOffset).prefix( + inputDataFound->second.src.size()[0]* + inputDataFound->second.src.size()[1])); + + /* Interpolation mode */ + Animation::Interpolation interpolation; + if(sampler.interpolation == cgltf_interpolation_type_linear) { + interpolation = Animation::Interpolation::Linear; + } else if(sampler.interpolation == cgltf_interpolation_type_cubic_spline) { + interpolation = Animation::Interpolation::Spline; + } else if(sampler.interpolation == cgltf_interpolation_type_step) { + interpolation = Animation::Interpolation::Constant; + } else { + /* There is no cgltf_interpolation_type_invalid, cgltf falls + back to linear for invalid interpolation modes */ + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } + + /* Decide on value properties. Not using checkAccessor() as this + was all checked above once already. */ + const cgltf_accessor* output = sampler.output; + AnimationTrackTargetType target; + AnimationTrackType type, resultType; + Animation::TrackViewStorage track; + const auto outputDataFound = samplerData.find(output); + CORRADE_INTERNAL_ASSERT(outputDataFound != samplerData.end()); + const auto outputData = data.suffix(outputDataFound->second.outputOffset) + .prefix(outputDataFound->second.src.size()[0]* + outputDataFound->second.src.size()[1]); + const cgltf_accessor*& timeTrackUsed = outputDataFound->second.timeTrack; + + const std::size_t valuesPerKey = interpolation == Animation::Interpolation::Spline ? 3 : 1; + if(input->count*valuesPerKey != output->count) { + Error{} << "Trade::CgltfImporter::animation(): target track size doesn't match time track size, expected" << output->count << "but got" << input->count*valuesPerKey; + return Containers::NullOpt; + } + + /* Translation */ + if(channel.target_path == cgltf_animation_path_type_translation) { + if(output->type != cgltf_type_vec3 || output->component_type != cgltf_component_type_r_32f || output->normalized) { + Error{} << "Trade::CgltfImporter::animation(): translation track has unexpected type" + << (output->normalized ? "normalized " : "") << Debug::nospace + << gltfTypeName(output->type) << "/" << gltfComponentTypeName(output->component_type); + return Containers::NullOpt; + } + + /* View on the value data */ + target = AnimationTrackTargetType::Translation3D; + resultType = AnimationTrackType::Vector3; + if(interpolation == Animation::Interpolation::Spline) { + /* Postprocess the spline track. This can be done only once for + every track -- postprocessSplineTrack() checks that. */ + const auto values = Containers::arrayCast(outputData); + postprocessSplineTrack(timeTrackUsed, keys, values); + + type = AnimationTrackType::CubicHermite3D; + track = Animation::TrackView{ + keys, values, interpolation, + animationInterpolatorFor(interpolation), + Animation::Extrapolation::Constant}; + } else { + type = AnimationTrackType::Vector3; + track = Animation::TrackView{keys, + Containers::arrayCast(outputData), + interpolation, + animationInterpolatorFor(interpolation), + Animation::Extrapolation::Constant}; + } + + /* Rotation */ + } else if(channel.target_path == cgltf_animation_path_type_rotation) { + /** @todo rotation can be also normalized (?!) to a vector of 8/16bit (signed?!) integers + cgltf_accessor_unpack_floats might help with unpacking them */ + + if(output->type != cgltf_type_vec4 || output->component_type != cgltf_component_type_r_32f || output->normalized) { + Error{} << "Trade::CgltfImporter::animation(): rotation track has unexpected type" + << (output->normalized ? "normalized " : "") << Debug::nospace + << gltfTypeName(output->type) << "/" << gltfComponentTypeName(output->component_type); + return Containers::NullOpt; + } + + /* View on the value data */ + target = AnimationTrackTargetType::Rotation3D; + resultType = AnimationTrackType::Quaternion; + if(interpolation == Animation::Interpolation::Spline) { + /* Postprocess the spline track. This can be done only once + for every track -- postprocessSplineTrack() checks + that. */ + const auto values = Containers::arrayCast(outputData); + postprocessSplineTrack(timeTrackUsed, keys, values); + + type = AnimationTrackType::CubicHermiteQuaternion; + track = Animation::TrackView{ + keys, values, interpolation, + animationInterpolatorFor(interpolation), + Animation::Extrapolation::Constant}; + } else { + /* Ensure shortest path is always chosen. Not doing this + for spline interpolation, there it would cause war and + famine. */ + const auto values = Containers::arrayCast(outputData); + if(configuration().value("optimizeQuaternionShortestPath")) { + Float flip = 1.0f; + for(std::size_t j = 0; j + 1 < values.size(); ++j) { + if(Math::dot(values[j], values[j + 1]*flip) < 0) flip = -flip; + values[j + 1] *= flip; + } + } + + /* Normalize the quaternions if not already. Don't attempt + to normalize every time to avoid tiny differences, only + when the quaternion looks to be off. Again, not doing + this for splines as it would cause things to go + haywire. */ + if(configuration().value("normalizeQuaternions")) { + for(auto& quat: values) if(!quat.isNormalized()) { + quat = quat.normalized(); + hadToRenormalize = true; + } + } + + type = AnimationTrackType::Quaternion; + track = Animation::TrackView{ + keys, values, interpolation, + animationInterpolatorFor(interpolation), + Animation::Extrapolation::Constant}; + } + + /* Scale */ + } else if(channel.target_path == cgltf_animation_path_type_scale) { + if(output->type != cgltf_type_vec3 || output->component_type != cgltf_component_type_r_32f || output->normalized) { + Error{} << "Trade::CgltfImporter::animation(): scaling track has unexpected type" + << (output->normalized ? "normalized " : "") << Debug::nospace + << gltfTypeName(output->type) << "/" << gltfComponentTypeName(output->component_type); + return Containers::NullOpt; + } + + /* View on the value data */ + target = AnimationTrackTargetType::Scaling3D; + resultType = AnimationTrackType::Vector3; + if(interpolation == Animation::Interpolation::Spline) { + /* Postprocess the spline track. This can be done only once + for every track -- postprocessSplineTrack() checks + that. */ + const auto values = Containers::arrayCast(outputData); + postprocessSplineTrack(timeTrackUsed, keys, values); + + type = AnimationTrackType::CubicHermite3D; + track = Animation::TrackView{ + keys, values, interpolation, + animationInterpolatorFor(interpolation), + Animation::Extrapolation::Constant}; + } else { + type = AnimationTrackType::Vector3; + track = Animation::TrackView{keys, + Containers::arrayCast(outputData), + interpolation, + animationInterpolatorFor(interpolation), + Animation::Extrapolation::Constant}; + } + + } else { + Error{} << "Trade::CgltfImporter::animation(): unsupported track target" << channel.target_path; + return Containers::NullOpt; + } + + /* Splines were postprocessed using the corresponding time track. + If a spline is not yet marked as postprocessed, mark it. + Otherwise check that the spline track is always used with the + same time track. */ + if(interpolation == Animation::Interpolation::Spline) { + if(timeTrackUsed == nullptr) + timeTrackUsed = sampler.input; + else if(timeTrackUsed != sampler.input) { + Error{} << "Trade::CgltfImporter::animation(): spline track is shared with different time tracks, we don't support that, sorry"; + return Containers::NullOpt; + } + } + + const UnsignedInt targetId = channel.target_node - _d->data->nodes; + tracks[trackId++] = AnimationTrackData{type, resultType, target, + /* In cases where multi-primitive mesh nodes are split into + multiple objects, the animation should affect the first node + -- the other nodes are direct children of it and so they get + affected too */ + UnsignedInt(_d->nodeSizeOffsets[targetId]), + track}; + } + } + + if(hadToRenormalize) + Warning{} << "Trade::CgltfImporter::animation(): quaternions in some rotation tracks were renormalized"; + + return AnimationData{std::move(data), std::move(tracks)}; +} + +UnsignedInt CgltfImporter::doCameraCount() const { + return _d->data->cameras_count; +} + +Int CgltfImporter::doCameraForName(const std::string& name) { + if(!_d->camerasForName) { + _d->camerasForName.emplace(); + _d->camerasForName->reserve(_d->data->cameras_count); + for(std::size_t i = 0; i != _d->data->cameras_count; ++i) + _d->camerasForName->emplace(_d->decodeString(_d->data->cameras[i].name), i); + } + + const auto found = _d->camerasForName->find(name); + return found == _d->camerasForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doCameraName(const UnsignedInt id) { + return _d->decodeString(_d->data->cameras[id].name); +} + +Containers::Optional CgltfImporter::doCamera(UnsignedInt id) { + const cgltf_camera& camera = _d->data->cameras[id]; + + /* https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#projection-matrices */ + + /* Perspective camera. glTF uses vertical FoV and X/Y aspect ratio, so to + avoid accidental bugs we will directly calculate the near plane size and + use that to create the camera data (instead of passing it the horizontal + FoV). */ + /** @todo What if znear == 0? aspect_ratio == 0? cgltf exposes + has_aspect_ratio */ + if(camera.type == cgltf_camera_type_perspective) { + const cgltf_camera_perspective& data = camera.data.perspective; + const Vector2 size = 2.0f*data.znear*Math::tan(data.yfov*0.5_radf)*Vector2::xScale(data.aspect_ratio); + const Float far = data.has_zfar ? data.zfar : Constants::inf(); + return CameraData{CameraType::Perspective3D, size, data.znear, far}; + } + + /* Orthographic camera. glTF uses a "scale" instead of "size", which means + we have to double. */ + if(camera.type == cgltf_camera_type_orthographic) { + const cgltf_camera_orthographic& data = camera.data.orthographic; + return CameraData{CameraType::Orthographic3D, + Vector2{data.xmag, data.ymag}*2.0f, data.znear, data.zfar}; + } + + CORRADE_INTERNAL_ASSERT(camera.type == cgltf_camera_type_invalid); + Error{} << "Trade::CgltfImporter::camera(): invalid camera type"; + return Containers::NullOpt; +} + +UnsignedInt CgltfImporter::doLightCount() const { + return _d->data->lights_count; +} + +Int CgltfImporter::doLightForName(const std::string& name) { + if(!_d->lightsForName) { + _d->lightsForName.emplace(); + _d->lightsForName->reserve(_d->data->lights_count); + for(std::size_t i = 0; i != _d->data->lights_count; ++i) + _d->lightsForName->emplace(_d->decodeString(_d->data->lights[i].name), i); + } + + const auto found = _d->lightsForName->find(name); + return found == _d->lightsForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doLightName(const UnsignedInt id) { + return _d->decodeString(_d->data->lights[id].name); +} + +Containers::Optional CgltfImporter::doLight(UnsignedInt id) { + const cgltf_light& light = _d->data->lights[id]; + + /* https://github.com/KhronosGroup/glTF/tree/5d3dfa44e750f57995ac6821117d9c7061bba1c9/extensions/2.0/Khronos/KHR_lights_punctual */ + + /* Light type */ + LightData::Type type; + if(light.type == cgltf_light_type_point) { + type = LightData::Type::Point; + } else if(light.type == cgltf_light_type_spot) { + type = LightData::Type::Spot; + } else if(light.type == cgltf_light_type_directional) { + type = LightData::Type::Directional; + } else { + CORRADE_INTERNAL_ASSERT(light.type == cgltf_light_type_invalid); + Error{} << "Trade::CgltfImporter::light(): invalid light type"; + return Containers::NullOpt; + } + + /* Cgltf sets range to 0 instead of infinity when it's not present. + That's stupid because it would divide by zero, fix that. Even more + stupid is JSON not having ANY way to represent an infinity, FFS. */ + const Float range = light.range == 0.0f ? Constants::inf() : light.range; + + /* Spotlight cone angles. In glTF they're specified as half-angles (which + is also why the limit on outer angle is 90°, not 180°), to avoid + confusion report a potential error in the original half-angles and + double the angle only at the end. */ + Rad innerConeAngle{NoInit}, outerConeAngle{NoInit}; + if(type == LightData::Type::Spot) { + innerConeAngle = Rad{light.spot_inner_cone_angle}; + outerConeAngle = Rad{light.spot_outer_cone_angle}; + + if(innerConeAngle < Rad(0.0_degf) || innerConeAngle >= outerConeAngle || outerConeAngle >= Rad(90.0_degf)) { + Error{} << "Trade::CgltfImporter::light(): inner and outer cone angle" << Deg(innerConeAngle) << "and" << Deg(outerConeAngle) << "out of allowed bounds"; + return Containers::NullOpt; + } + } else innerConeAngle = outerConeAngle = 180.0_degf; + + /* Range should be infinity for directional lights. Because there's no way + to represent infinity in JSON, directly suggest to remove the range + property, don't even bother printing the value. */ + if(type == LightData::Type::Directional && range != Constants::inf()) { + Error{} << "Trade::CgltfImporter::light(): range can't be defined for a directional light"; + return Containers::NullOpt; + } + + /* As said above, glTF uses half-angles, while we have full angles (for + consistency with existing APIs such as OpenAL cone angles or math intersection routines as well as Blender). */ + return LightData{type, Color3::from(light.color), light.intensity, range, innerConeAngle*2.0f, outerConeAngle*2.0f}; +} + +Int CgltfImporter::doDefaultScene() const { + /* While https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#scenes + says that "When scene is undefined, client implementations MAY delay + rendering until a particular scene is requested.", several official + sample glTF models (e.g. the AnimatedTriangle) have no "scene" property, + so that's a bit stupid behavior to have. As per discussion at + https://github.com/KhronosGroup/glTF/issues/815#issuecomment-274286889, + if a default scene isn't defined and there is at least one scene, just + use the first one. */ + if(!_d->data->scene) + return _d->data->scenes_count > 0 ? 0 : -1; + + const Int sceneId = _d->data->scene - _d->data->scenes; + return sceneId; +} + +UnsignedInt CgltfImporter::doSceneCount() const { + return _d->data->scenes_count; +} + +Int CgltfImporter::doSceneForName(const std::string& name) { + if(!_d->scenesForName) { + _d->scenesForName.emplace(); + _d->scenesForName->reserve(_d->data->scenes_count); + for(std::size_t i = 0; i != _d->data->scenes_count; ++i) + _d->scenesForName->emplace(_d->decodeString(_d->data->scenes[i].name), i); + } + + const auto found = _d->scenesForName->find(name); + return found == _d->scenesForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doSceneName(const UnsignedInt id) { + return _d->decodeString(_d->data->scenes[id].name); +} + +Containers::Optional CgltfImporter::doScene(UnsignedInt id) { + const cgltf_scene& scene = _d->data->scenes[id]; + + /* The scene contains always the top-level nodes, all multi-primitive mesh + nodes are children of them */ + std::vector children; + children.reserve(scene.nodes_count); + for(UnsignedInt i = 0; i != scene.nodes_count; ++i) { + const cgltf_node* node = scene.nodes[i]; + const UnsignedInt nodeId = node - _d->data->nodes; + children.push_back(nodeId); + } + + return SceneData{{}, std::move(children)}; +} + +UnsignedInt CgltfImporter::doObject3DCount() const { + return _d->nodeMap.size(); +} + +Int CgltfImporter::doObject3DForName(const std::string& name) { + if(!_d->nodesForName) { + _d->nodesForName.emplace(); + _d->nodesForName->reserve(_d->data->nodes_count); + for(std::size_t i = 0; i != _d->data->nodes_count; ++i) { + /* A mesh node can be duplicated for as many primitives as the mesh + has, point to the first node in the duplicate sequence */ + _d->nodesForName->emplace(_d->decodeString(_d->data->nodes[i].name), _d->nodeSizeOffsets[i]); + } + } + + const auto found = _d->nodesForName->find(name); + return found == _d->nodesForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doObject3DName(UnsignedInt id) { + /* This returns the same name for all multi-primitive mesh node duplicates */ + return _d->decodeString(_d->data->nodes[_d->nodeMap[id].first()].name); +} + +Containers::Pointer CgltfImporter::doObject3D(UnsignedInt id) { + const std::size_t originalNodeId = _d->nodeMap[id].first(); + const std::size_t nodePrimitiveId = _d->nodeMap[id].second(); + + const cgltf_node& node = _d->data->nodes[originalNodeId]; + + /* This is an extra node added for multi-primitive meshes -- return it with + no children, identity transformation and just a link to the particular + mesh & material combo */ + if(nodePrimitiveId) { + /* This had to be already checked during file import as we remap for + multi-primitive meshes */ + CORRADE_INTERNAL_ASSERT(node.mesh); + + const UnsignedInt originalMeshId = node.mesh - _d->data->meshes; + const UnsignedInt meshId = _d->meshSizeOffsets[originalMeshId] + nodePrimitiveId; + const cgltf_material* material = node.mesh->primitives[nodePrimitiveId].material; + const Int materialId = material ? material - _d->data->materials : -1; + const Int skinId = node.skin ? node.skin - _d->data->skins : -1; + return Containers::pointer(new MeshObjectData3D{{}, {}, {}, Vector3{1.0f}, meshId, materialId, skinId}); + } + + /* Node children: first add extra nodes caused by multi-primitive meshes, + after that the usual children. */ + std::vector children; + const std::size_t extraChildrenCount = _d->nodeSizeOffsets[originalNodeId + 1] - _d->nodeSizeOffsets[originalNodeId] - 1; + children.reserve(extraChildrenCount + node.children_count); + for(std::size_t i = 0; i != extraChildrenCount; ++i) { + /** @todo the test should fail with children.push_back(originalNodeId + i + 1); */ + children.push_back(_d->nodeSizeOffsets[originalNodeId] + i + 1); + } + for(std::size_t i = 0; i != node.children_count; ++i) { + const UnsignedInt childId = node.children[i] - _d->data->nodes; + children.push_back(_d->nodeSizeOffsets[childId]); + } + + /* According to the spec, order is T-R-S: first scale, then rotate, then + translate (or translate*rotate*scale multiplication of matrices). Makes + most sense, since non-uniform scaling of rotated object is unwanted in + 99% cases, similarly with rotating or scaling a translated object. Also + independently verified by exporting a model with translation, rotation + *and* scaling of hierarchic objects. */ + ObjectFlags3D flags; + Matrix4 transformation{NoInit}; + Vector3 translation{NoInit}; + Quaternion rotation{NoInit}; + Vector3 scaling{NoInit}; + if(node.has_matrix) { + transformation = Matrix4::from(node.matrix); + } else { + /* Having TRS is a better property than not having it, so we set this + flag even when there is no transformation at all. */ + flags |= ObjectFlag3D::HasTranslationRotationScaling; + translation = Vector3::from(node.translation); + rotation = Quaternion{Vector3::from(node.rotation), node.rotation[3]}; + if(!rotation.isNormalized() && configuration().value("normalizeQuaternions")) { + rotation = rotation.normalized(); + Warning{} << "Trade::CgltfImporter::object3D(): rotation quaternion was renormalized"; + } + scaling = Vector3::from(node.scale); + } + + /* Node is a mesh */ + if(node.mesh) { + /* Multi-primitive nodes are handled above */ + CORRADE_INTERNAL_ASSERT(_d->nodeMap[id].second() == 0); + CORRADE_INTERNAL_ASSERT(node.mesh->primitives_count > 0); + + const UnsignedInt originalMeshId = node.mesh - _d->data->meshes; + const UnsignedInt meshId = _d->meshSizeOffsets[originalMeshId]; + const cgltf_material* material = node.mesh->primitives[0].material; + const Int materialId = material ? material - _d->data->materials : -1; + const Int skinId = node.skin ? node.skin - _d->data->skins : -1; + return Containers::pointer(flags & ObjectFlag3D::HasTranslationRotationScaling ? + new MeshObjectData3D{std::move(children), translation, rotation, scaling, meshId, materialId, skinId} : + new MeshObjectData3D{std::move(children), transformation, meshId, materialId, skinId}); + } + + /* Unknown nodes are treated as Empty */ + ObjectInstanceType3D instanceType = ObjectInstanceType3D::Empty; + UnsignedInt instanceId = ~UnsignedInt{}; /* -1 */ + + /* Node is a camera */ + if(node.camera) { + instanceType = ObjectInstanceType3D::Camera; + instanceId = node.camera - _d->data->cameras; + + /* Node is a light */ + } else if(node.light) { + instanceType = ObjectInstanceType3D::Light; + instanceId = node.light - _d->data->lights; + } + + return Containers::pointer(flags & ObjectFlag3D::HasTranslationRotationScaling ? + new ObjectData3D{std::move(children), translation, rotation, scaling, instanceType, instanceId} : + new ObjectData3D{std::move(children), transformation, instanceType, instanceId}); +} + +UnsignedInt CgltfImporter::doSkin3DCount() const { + return _d->data->skins_count; +} + +Int CgltfImporter::doSkin3DForName(const std::string& name) { + if(!_d->skinsForName) { + _d->skinsForName.emplace(); + _d->skinsForName->reserve(_d->data->skins_count); + for(std::size_t i = 0; i != _d->data->skins_count; ++i) + _d->skinsForName->emplace(_d->decodeString(_d->data->skins[i].name), i); + } + + const auto found = _d->skinsForName->find(name); + return found == _d->skinsForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doSkin3DName(const UnsignedInt id) { + return _d->decodeString(_d->data->skins[id].name); +} + +Containers::Optional CgltfImporter::doSkin3D(const UnsignedInt id) { + const cgltf_skin& skin = _d->data->skins[id]; + + if(!skin.joints_count) { + Error{} << "Trade::CgltfImporter::skin3D(): skin has no joints"; + return Containers::NullOpt; + } + + /* Joint IDs */ + Containers::Array joints{NoInit, skin.joints_count}; + for(std::size_t i = 0; i != joints.size(); ++i) { + const UnsignedInt nodeId = skin.joints[i] - _d->data->nodes; + joints[i] = nodeId; + } + + /* Inverse bind matrices. If there are none, default is identities */ + Containers::Array inverseBindMatrices{skin.joints_count}; + if(skin.inverse_bind_matrices) { + const cgltf_accessor* accessor = skin.inverse_bind_matrices; + if(!checkAccessor(_d->data, "skin3D", accessor)) + return Containers::NullOpt; + + if(accessor->type != cgltf_type_mat4 || accessor->component_type != cgltf_component_type_r_32f || accessor->normalized) { + Error{} << "Trade::CgltfImporter::skin3D(): inverse bind matrices have unexpected type" + << (accessor->normalized ? "normalized " : "") << Debug::nospace + << gltfTypeName(accessor->type) << "/" << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + + Containers::Optional> view = _d->accessorView(accessor, "skin3D"); + if(!view) + return Containers::NullOpt; + + Containers::StridedArrayView1D matrices = Containers::arrayCast<1, const Matrix4>(*view); + if(matrices.size() != inverseBindMatrices.size()) { + Error{} << "Trade::CgltfImporter::skin3D(): invalid inverse bind matrix count, expected" << inverseBindMatrices.size() << "but got" << matrices.size(); + return Containers::NullOpt; + } + + Utility::copy(matrices, inverseBindMatrices); + } + + return SkinData3D{std::move(joints), std::move(inverseBindMatrices)}; +} + +UnsignedInt CgltfImporter::doMeshCount() const { + return _d->meshMap.size(); +} + +Int CgltfImporter::doMeshForName(const std::string& name) { + if(!_d->meshesForName) { + _d->meshesForName.emplace(); + _d->meshesForName->reserve(_d->data->meshes_count); + for(std::size_t i = 0; i != _d->data->meshes_count; ++i) { + /* The mesh can be duplicated for as many primitives as it has, + point to the first mesh in the duplicate sequence */ + _d->meshesForName->emplace(_d->decodeString(_d->data->meshes[i].name), _d->meshSizeOffsets[i]); + } + } + + const auto found = _d->meshesForName->find(name); + return found == _d->meshesForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doMeshName(const UnsignedInt id) { + /* This returns the same name for all multi-primitive mesh duplicates */ + return _d->decodeString(_d->data->meshes[_d->meshMap[id].first()].name); +} + +Containers::Optional CgltfImporter::doMesh(const UnsignedInt id, UnsignedInt) { + const cgltf_mesh& mesh = _d->data->meshes[_d->meshMap[id].first()]; + const cgltf_primitive& primitive = mesh.primitives[_d->meshMap[id].second()]; + + MeshPrimitive meshPrimitive{}; + if(primitive.type == cgltf_primitive_type_points) { + meshPrimitive = MeshPrimitive::Points; + } else if(primitive.type == cgltf_primitive_type_lines) { + meshPrimitive = MeshPrimitive::Lines; + } else if(primitive.type == cgltf_primitive_type_line_loop) { + meshPrimitive = MeshPrimitive::LineLoop; + } else if(primitive.type == cgltf_primitive_type_line_strip) { + meshPrimitive = MeshPrimitive::LineStrip; + } else if(primitive.type == cgltf_primitive_type_triangles) { + meshPrimitive = MeshPrimitive::Triangles; + } else if(primitive.type == cgltf_primitive_type_triangle_fan) { + meshPrimitive = MeshPrimitive::TriangleFan; + } else if(primitive.type == cgltf_primitive_type_triangle_strip) { + meshPrimitive = MeshPrimitive::TriangleStrip; + } else { + /* Cgltf parses an int and directly casts it to cgltf_primitive_type + without checking for valid values */ + Error{} << "Trade::CgltfImporter::mesh(): unrecognized primitive" << primitive.type; + return Containers::NullOpt; + } + + /* Sort attributes by name so that we add attribute sets in the correct + order and can warn if indices are not contiguous. Stable sort is needed + to preserve declaration order for duplicate attributes, checked below. */ + Containers::Array attributeOrder{primitive.attributes_count}; + for(UnsignedInt i = 0; i < attributeOrder.size(); ++i) + attributeOrder[i] = i; + + std::stable_sort(attributeOrder.begin(), attributeOrder.end(), [&](UnsignedInt a, UnsignedInt b) { + return std::strcmp(primitive.attributes[a].name, primitive.attributes[b].name) < 0; + }); + + /* Find and remove duplicate attributes. This mimics tinygltf behaviour + which replaces the previous attribute of the same name. */ + std::size_t attributeCount = attributeOrder.size(); + for(UnsignedInt i = 0; i + 1 < attributeOrder.size(); ++i) { + const cgltf_attribute& current = primitive.attributes[attributeOrder[i]]; + const cgltf_attribute& next = primitive.attributes[attributeOrder[i + 1]]; + if(std::strcmp(current.name, next.name) == 0) { + --attributeCount; + /* Mark for skipping later */ + attributeOrder[i] = ~0u; + } + } + + /* Gather all (whitelisted) attributes and the total buffer range spanning + them */ + cgltf_buffer* buffer = nullptr; + UnsignedInt vertexCount = 0; + std::size_t attributeId = 0; + cgltf_attribute lastAttribute{}; + Math::Range1D bufferRange; + Containers::Array attributeData{attributeCount}; + for(UnsignedInt a: attributeOrder) { + /* Duplicate attribute, skip */ + if(a == ~0u) + continue; + + const cgltf_attribute& attribute = primitive.attributes[a]; + + const Containers::StringView nameString{attribute.name}; + /* See the comment in doOpenData() for why we do this */ + const Containers::StringView semantic = attribute.type != cgltf_attribute_type_invalid ? + nameString.partition('_')[0] : nameString; + + /* Numbered attributes are expected to be contiguous (COLORS_0, + COLORS_1...). If not, print a warning, because in the MeshData they + will appear as contiguous. */ + if(attribute.type != cgltf_attribute_type_invalid) { + if(attribute.type != lastAttribute.type) + lastAttribute.index = -1; + + if(attribute.index != lastAttribute.index + 1) + Warning{} << "Trade::CgltfImporter::mesh(): found attribute" << nameString << "but expected" << semantic << Debug::nospace << "_" << Debug::nospace << lastAttribute.index + 1; + } + lastAttribute = attribute; + + const cgltf_accessor* accessor = attribute.data; + if(!checkAccessor(_d->data, "mesh", accessor)) + return Containers::NullOpt; + + /* Convert to our vertex format */ + VertexFormat componentFormat; + if(accessor->component_type == cgltf_component_type_r_8) + componentFormat = VertexFormat::Byte; + else if(accessor->component_type == cgltf_component_type_r_8u) + componentFormat = VertexFormat::UnsignedByte; + else if(accessor->component_type == cgltf_component_type_r_16) + componentFormat = VertexFormat::Short; + else if(accessor->component_type == cgltf_component_type_r_16u) + componentFormat = VertexFormat::UnsignedShort; + else if(accessor->component_type == cgltf_component_type_r_32u) + componentFormat = VertexFormat::UnsignedInt; + else if(accessor->component_type == cgltf_component_type_r_32f) + componentFormat = VertexFormat::Float; + else { + CORRADE_INTERNAL_ASSERT(accessor->component_type == cgltf_component_type_invalid); + Error{} << "Trade::CgltfImporter::mesh(): attribute" << nameString << "has an invalid component type"; + return {}; + } + + UnsignedInt componentCount; + UnsignedInt vectorCount = 0; + if(accessor->type == cgltf_type_scalar) + componentCount = 1; + else if(accessor->type == cgltf_type_vec2) + componentCount = 2; + else if(accessor->type == cgltf_type_vec3) + componentCount = 3; + else if(accessor->type == cgltf_type_vec4) + componentCount = 4; + else if(accessor->type == cgltf_type_mat2) { + componentCount = 2; + vectorCount = 2; + } else if(accessor->type == cgltf_type_mat3) { + componentCount = 3; + vectorCount = 3; + } else if(accessor->type == cgltf_type_mat4) { + componentCount = 4; + vectorCount = 4; + } else { + CORRADE_INTERNAL_ASSERT(accessor->type == cgltf_type_invalid); + Error{} << "Trade::CgltfImporter::mesh(): attribute" << nameString << "has an invalid type"; + return {}; + } + + /* Check for illegal normalized types */ + if(accessor->normalized && + (componentFormat == VertexFormat::Float || componentFormat == VertexFormat::UnsignedInt)) { + Error{} << "Trade::CgltfImporter::mesh(): attribute" << nameString << "component type" << gltfComponentTypeName(accessor->component_type) << "can't be normalized"; + return Containers::NullOpt; + } + + /* Check that matrix type is legal */ + if(vectorCount && + componentFormat != VertexFormat::Float && + !(componentFormat == VertexFormat::Byte && accessor->normalized) && + !(componentFormat == VertexFormat::Short && accessor->normalized)) { + Error{} << "Trade::CgltfImporter::mesh(): attribute" << nameString << "has an unsupported matrix component type" + << (accessor->normalized ? "normalized" : "unnormalized") + << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + + const VertexFormat format = vectorCount ? + vertexFormat(componentFormat, vectorCount, componentCount, true) : + vertexFormat(componentFormat, componentCount, accessor->normalized); + + /* Whitelist supported attribute and data type combinations */ + MeshAttribute name; + if(attribute.type == cgltf_attribute_type_position) { + name = MeshAttribute::Position; + + if(accessor->type != cgltf_type_vec3) { + Error{} << "Trade::CgltfImporter::mesh(): unexpected" << semantic << "type" << gltfTypeName(accessor->type); + return Containers::NullOpt; + } + + if(!(componentFormat == VertexFormat::Float && !accessor->normalized) && + /* KHR_mesh_quantization. Both normalized and unnormalized + bytes/shorts are okay. */ + componentFormat != VertexFormat::UnsignedByte && + componentFormat != VertexFormat::Byte && + componentFormat != VertexFormat::UnsignedShort && + componentFormat != VertexFormat::Short) { + Error{} << "Trade::CgltfImporter::mesh(): unsupported" << semantic << "component type" + << (accessor->normalized ? "normalized" : "unnormalized") + << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + + } else if(attribute.type == cgltf_attribute_type_normal) { + name = MeshAttribute::Normal; + + if(accessor->type != cgltf_type_vec3) { + Error{} << "Trade::CgltfImporter::mesh(): unexpected" << semantic << "type" << gltfTypeName(accessor->type); + return Containers::NullOpt; + } + + if(!(componentFormat == VertexFormat::Float && !accessor->normalized) && + /* KHR_mesh_quantization */ + !(componentFormat == VertexFormat::Byte && accessor->normalized) && + !(componentFormat == VertexFormat::Short && accessor->normalized)) { + Error{} << "Trade::CgltfImporter::mesh(): unsupported" << semantic << "component type" + << (accessor->normalized ? "normalized" : "unnormalized") + << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + + } else if(attribute.type == cgltf_attribute_type_tangent) { + name = MeshAttribute::Tangent; + + if(accessor->type != cgltf_type_vec4) { + Error{} << "Trade::CgltfImporter::mesh(): unexpected" << semantic << "type" << gltfTypeName(accessor->type); + return Containers::NullOpt; + } + + if(!(componentFormat == VertexFormat::Float && !accessor->normalized) && + /* KHR_mesh_quantization */ + !(componentFormat == VertexFormat::Byte && accessor->normalized) && + !(componentFormat == VertexFormat::Short && accessor->normalized)) { + Error{} << "Trade::CgltfImporter::mesh(): unsupported" << semantic << "component type" + << (accessor->normalized ? "normalized" : "unnormalized") + << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + + } else if(attribute.type == cgltf_attribute_type_texcoord) { + name = MeshAttribute::TextureCoordinates; + + if(accessor->type != cgltf_type_vec2) { + Error{} << "Trade::CgltfImporter::mesh(): unexpected" << semantic << "type" << gltfTypeName(accessor->type); + return Containers::NullOpt; + } + + /* Core spec only allows float and normalized unsigned bytes/shorts, the + rest is added by KHR_mesh_quantization */ + if(!(componentFormat == VertexFormat::Float && !accessor->normalized) && + componentFormat != VertexFormat::UnsignedByte && + componentFormat != VertexFormat::Byte && + componentFormat != VertexFormat::UnsignedShort && + componentFormat != VertexFormat::Short) { + Error{} << "Trade::CgltfImporter::mesh(): unsupported" << semantic << "component type" + << (accessor->normalized ? "normalized" : "unnormalized") + << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + + } else if(attribute.type == cgltf_attribute_type_color) { + name = MeshAttribute::Color; + + if(accessor->type != cgltf_type_vec4 && accessor->type != cgltf_type_vec3) { + Error{} << "Trade::CgltfImporter::mesh(): unexpected" << semantic << "type" << gltfTypeName(accessor->type); + return Containers::NullOpt; + } + + if(!(componentFormat == VertexFormat::Float && !accessor->normalized) && + !(componentFormat == VertexFormat::UnsignedByte && accessor->normalized) && + !(componentFormat == VertexFormat::UnsignedShort && accessor->normalized)) { + Error{} << "Trade::CgltfImporter::mesh(): unsupported" << semantic << "component type" + << (accessor->normalized ? "normalized" : "unnormalized") + << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + } else if(attribute.type == cgltf_attribute_type_joints) { + name = _d->meshAttributesForName.at(semantic); + + if(accessor->type != cgltf_type_vec4) { + Error{} << "Trade::CgltfImporter::mesh(): unexpected" << semantic << "type" << gltfTypeName(accessor->type); + return Containers::NullOpt; + } + + if(!(componentFormat == VertexFormat::UnsignedByte && !accessor->normalized) && + !(componentFormat == VertexFormat::UnsignedShort && !accessor->normalized)) { + Error{} << "Trade::CgltfImporter::mesh(): unsupported" << semantic << "component type" + << (accessor->normalized ? "normalized" : "unnormalized") + << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + } else if(attribute.type == cgltf_attribute_type_weights) { + name = _d->meshAttributesForName.at(semantic); + + if(accessor->type != cgltf_type_vec4) { + Error{} << "Trade::CgltfImporter::mesh(): unexpected" << semantic << "type" << gltfTypeName(accessor->type); + return Containers::NullOpt; + } + + if(!(componentFormat == VertexFormat::Float && !accessor->normalized) && + !(componentFormat == VertexFormat::UnsignedByte && accessor->normalized) && + !(componentFormat == VertexFormat::UnsignedShort && accessor->normalized)) { + Error{} << "Trade::CgltfImporter::mesh(): unsupported" << semantic << "component type" + << (accessor->normalized ? "normalized" : "unnormalized") + << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + /* Object ID, name user-configurable */ + } else if(nameString == configuration().value("objectIdAttribute")) { + name = MeshAttribute::ObjectId; + + if(accessor->type != cgltf_type_scalar) { + Error{} << "Trade::CgltfImporter::mesh(): unexpected object ID type" << gltfTypeName(accessor->type); + return Containers::NullOpt; + } + + /* The glTF spec says that "Application-specific attribute semantics + MUST NOT use unsigned int component type" but I'm not sure what + the point of enforcing that would be */ + if((componentFormat != VertexFormat::UnsignedInt && + componentFormat != VertexFormat::UnsignedShort && + componentFormat != VertexFormat::UnsignedByte) || + accessor->normalized) { + Error{} << "Trade::CgltfImporter::mesh(): unsupported object ID component type" + << (accessor->normalized ? "normalized" : "unnormalized") + << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + + /* Custom or unrecognized attributes, map to an ID */ + } else { + CORRADE_INTERNAL_ASSERT(attribute.type == cgltf_attribute_type_invalid); + name = _d->meshAttributesForName.at(nameString); + } + + /* Remember which buffer the attribute is in and the range, for + consecutive attribs expand the range */ + const cgltf_buffer_view* bufferView = accessor->buffer_view; + if(attributeId == 0) { + buffer = bufferView->buffer; + bufferRange = Math::Range1D::fromSize(bufferView->offset, bufferView->size); + vertexCount = accessor->count; + } else { + /* ... and probably never will be */ + if(bufferView->buffer != buffer) { + Error{} << "Trade::CgltfImporter::mesh(): meshes spanning multiple buffers are not supported"; + return Containers::NullOpt; + } + + bufferRange = Math::join(bufferRange, Math::Range1D::fromSize(bufferView->offset, bufferView->size)); + + if(accessor->count != vertexCount) { + Error{} << "Trade::CgltfImporter::mesh(): mismatched vertex count for attribute" << semantic << Debug::nospace << ", expected" << vertexCount << "but got" << accessor->count; + return Containers::NullOpt; + } + } + + /** @todo Check that accessor stride >= vertexFormatSize(format)? */ + + /* Fill in an attribute. Offset-only, will be patched to be relative to + the actual output buffer once we know how large it is and where it + is allocated. */ + attributeData[attributeId++] = MeshAttributeData{name, format, + UnsignedInt(accessor->offset + bufferView->offset), vertexCount, + std::ptrdiff_t(accessor->stride)}; + } + + /* Verify we really filled all attributes */ + CORRADE_INTERNAL_ASSERT(attributeId == attributeData.size()); + + /* Allocate & copy vertex data (if any) */ + Containers::Array vertexData{NoInit, bufferRange.size()}; + if(vertexData.size()) { + const UnsignedInt bufferId = buffer - _d->data->buffers; + if(!_d->loadBuffer(bufferId, "mesh")) + return {}; + + Utility::copy(Containers::arrayView(static_cast(buffer->data), buffer->size) + .slice(bufferRange.min(), bufferRange.max()), + vertexData); + } + + /* Convert the attributes from relative to absolute, copy them to a + non-growable array and do additional patching */ + for(std::size_t i = 0; i != attributeData.size(); ++i) { + Containers::StridedArrayView1D data{vertexData, + /* Offset is what with the range min subtracted, as we copied + without the prefix */ + vertexData + attributeData[i].offset(vertexData) - bufferRange.min(), + vertexCount, attributeData[i].stride()}; + + attributeData[i] = MeshAttributeData{attributeData[i].name(), + attributeData[i].format(), data}; + + /* Flip Y axis of texture coordinates, unless it's done in the material + instead */ + if(attributeData[i].name() == MeshAttribute::TextureCoordinates && !_d->textureCoordinateYFlipInMaterial) { + if(attributeData[i].format() == VertexFormat::Vector2) + for(auto& c: Containers::arrayCast(data)) + c.y() = 1.0f - c.y(); + else if(attributeData[i].format() == VertexFormat::Vector2ubNormalized) + for(auto& c: Containers::arrayCast(data)) + c.y() = 255 - c.y(); + else if(attributeData[i].format() == VertexFormat::Vector2usNormalized) + for(auto& c: Containers::arrayCast(data)) + c.y() = 65535 - c.y(); + /* For these it's always done in the material texture transform as + we can't do a 1 - y flip like above. These are allowed only by + the KHR_mesh_quantization formats and in that case the texture + transform should be always present. */ + /* LCOV_EXCL_START */ + else if(attributeData[i].format() != VertexFormat::Vector2bNormalized && + attributeData[i].format() != VertexFormat::Vector2sNormalized && + attributeData[i].format() != VertexFormat::Vector2ub && + attributeData[i].format() != VertexFormat::Vector2b && + attributeData[i].format() != VertexFormat::Vector2us && + attributeData[i].format() != VertexFormat::Vector2s) + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + /* LCOV_EXCL_STOP */ + } + } + + /* Indices */ + MeshIndexData indices; + Containers::Array indexData; + if(primitive.indices) { + const cgltf_accessor* accessor = primitive.indices; + if(!checkAccessor(_d->data, "mesh", accessor)) + return Containers::NullOpt; + + if(accessor->type != cgltf_type_scalar) { + Error() << "Trade::CgltfImporter::mesh(): unexpected index type" << gltfTypeName(accessor->type); + return Containers::NullOpt; + } + + if(accessor->normalized) { + Error() << "Trade::CgltfImporter::mesh(): index type can't be normalized"; + return Containers::NullOpt; + } + + MeshIndexType type; + if(accessor->component_type == cgltf_component_type_r_8u) + type = MeshIndexType::UnsignedByte; + else if(accessor->component_type == cgltf_component_type_r_16u) + type = MeshIndexType::UnsignedShort; + else if(accessor->component_type == cgltf_component_type_r_32u) + type = MeshIndexType::UnsignedInt; + else { + Error{} << "Trade::CgltfImporter::mesh(): unexpected index component type" << gltfComponentTypeName(accessor->component_type); + return Containers::NullOpt; + } + + Containers::Optional> src = _d->accessorView(accessor, "mesh"); + if(!src) + return Containers::NullOpt; + + if(!src->isContiguous()) { + Error{} << "Trade::CgltfImporter::mesh(): index buffer view is not contiguous"; + return Containers::NullOpt; + } + + Containers::ArrayView srcContiguous = src->asContiguous(); + indexData = Containers::Array{srcContiguous.size()}; + Utility::copy(srcContiguous, indexData); + indices = MeshIndexData{type, indexData}; + } + + /* If we have an index-less attribute-less mesh, glTF has no way to supply + a vertex count, so return 0 */ + if(!indices.data().size() && !attributeData.size()) + return MeshData{meshPrimitive, 0}; + + return MeshData{meshPrimitive, + std::move(indexData), indices, + std::move(vertexData), std::move(attributeData), + vertexCount}; +} + +std::string CgltfImporter::doMeshAttributeName(UnsignedShort name) { + return _d && name < _d->meshAttributeNames.size() ? + _d->meshAttributeNames[name] : ""; +} + +MeshAttribute CgltfImporter::doMeshAttributeForName(const std::string& name) { + return _d ? _d->meshAttributesForName[name] : MeshAttribute{}; +} + +UnsignedInt CgltfImporter::doMaterialCount() const { + return _d->data->materials_count; +} + +Int CgltfImporter::doMaterialForName(const std::string& name) { + if(!_d->materialsForName) { + _d->materialsForName.emplace(); + _d->materialsForName->reserve(_d->data->materials_count); + for(std::size_t i = 0; i != _d->data->materials_count; ++i) + _d->materialsForName->emplace(_d->decodeString(_d->data->materials[i].name), i); + } + + const auto found = _d->materialsForName->find(name); + return found == _d->materialsForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doMaterialName(const UnsignedInt id) { + return _d->decodeString(_d->data->materials[id].name); +} + +void CgltfImporter::Document::materialTexture(const cgltf_texture_view& texture, Containers::Array& attributes, const MaterialAttribute attribute, const MaterialAttribute matrixAttribute, const MaterialAttribute coordinateAttribute) const { + CORRADE_INTERNAL_ASSERT(texture.texture); + + UnsignedInt texCoord = texture.texcoord; + + /* Texture transform. Because texture coordinates were Y-flipped, we first + unflip them back, apply the transform (which assumes origin at bottom + left and Y down) and then flip the result again. Sanity of the following + verified with https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/TextureTransformTest */ + if(texture.has_transform) { + Matrix3 matrix; + + /* If material needs an Y-flip, the mesh doesn't have the texture + coordinates flipped and thus we don't need to unflip them first */ + if(!textureCoordinateYFlipInMaterial) + matrix = Matrix3::translation(Vector2::yAxis(1.0f))* + Matrix3::scaling(Vector2::yScale(-1.0f)); + + /* The extension can override texture coordinate index (for example + to have the unextended coordinates already transformed, and + applying transformation to a different set) */ + if(texture.transform.has_texcoord) + texCoord = texture.transform.texcoord; + + matrix = Matrix3::scaling(Vector2::from(texture.transform.scale))*matrix; + + /* Because we import images with Y flipped, counterclockwise + rotation is now clockwise. This has to be done in addition + to the Y flip/unflip. */ + matrix = Matrix3::rotation(-Rad(texture.transform.rotation))*matrix; + + matrix = Matrix3::translation(Vector2::from(texture.transform.offset))*matrix; + + matrix = Matrix3::translation(Vector2::yAxis(1.0f))* + Matrix3::scaling(Vector2::yScale(-1.0f))*matrix; + + arrayAppend(attributes, InPlaceInit, matrixAttribute, matrix); + } + + /* In case the material had no texture transformation but still needs an + Y-flip, put it there */ + if(!texture.has_transform && textureCoordinateYFlipInMaterial) { + arrayAppend(attributes, InPlaceInit, matrixAttribute, + Matrix3::translation(Vector2::yAxis(1.0f))* + Matrix3::scaling(Vector2::yScale(-1.0f))); + } + + /* Add texture coordinate set if non-zero. The KHR_texture_transform + could be modifying it, so do that after */ + if(texCoord != 0) + arrayAppend(attributes, InPlaceInit, coordinateAttribute, texCoord); + + /* In some cases (when dealing with packed textures), we're parsing & + adding texture coordinates and matrix multiple times, but adding the + packed texture ID just once. In other cases the attribute is invalid. */ + if(attribute != MaterialAttribute{}) { + const UnsignedInt textureId = texture.texture - data->textures; + arrayAppend(attributes, InPlaceInit, attribute, textureId); + } +} + +Containers::Optional CgltfImporter::doMaterial(const UnsignedInt id) { + const cgltf_material& material = _d->data->materials[id]; + + Containers::Array layers; + Containers::Array attributes; + MaterialTypes types; + + /* Alpha mode and mask, double sided */ + if(material.alpha_mode == cgltf_alpha_mode_blend) + arrayAppend(attributes, InPlaceInit, MaterialAttribute::AlphaBlend, true); + else if(material.alpha_mode == cgltf_alpha_mode_mask) + arrayAppend(attributes, InPlaceInit, MaterialAttribute::AlphaMask, material.alpha_cutoff); + else if(material.alpha_mode != cgltf_alpha_mode_opaque) { + /* This should never be reached, cgltf treats invalid alpha modes as opaque */ + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } + + if(material.double_sided) + arrayAppend(attributes, InPlaceInit, MaterialAttribute::DoubleSided, true); + + /* Core metallic/roughness material */ + if(material.has_pbr_metallic_roughness) { + types |= MaterialType::PbrMetallicRoughness; + + const Vector4 baseColorFactor = Vector4::from(material.pbr_metallic_roughness.base_color_factor); + if(baseColorFactor != Vector4{1.0f}) + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::BaseColor, + Color4{baseColorFactor}); + if(material.pbr_metallic_roughness.metallic_factor != 1.0f) + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::Metalness, + material.pbr_metallic_roughness.metallic_factor); + if(material.pbr_metallic_roughness.roughness_factor != 1.0f) + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::Roughness, + material.pbr_metallic_roughness.roughness_factor); + + if(material.pbr_metallic_roughness.base_color_texture.texture) { + _d->materialTexture( + material.pbr_metallic_roughness.base_color_texture, + attributes, + MaterialAttribute::BaseColorTexture, + MaterialAttribute::BaseColorTextureMatrix, + MaterialAttribute::BaseColorTextureCoordinates); + } + + if(material.pbr_metallic_roughness.metallic_roughness_texture.texture) { + _d->materialTexture( + material.pbr_metallic_roughness.metallic_roughness_texture, + attributes, + MaterialAttribute::NoneRoughnessMetallicTexture, + MaterialAttribute::MetalnessTextureMatrix, + MaterialAttribute::MetalnessTextureCoordinates); + + /* Add the matrix/coordinates attributes also for the roughness + texture, but skip adding the texture ID again */ + _d->materialTexture( + material.pbr_metallic_roughness.metallic_roughness_texture, + attributes, + MaterialAttribute{}, + MaterialAttribute::RoughnessTextureMatrix, + MaterialAttribute::RoughnessTextureCoordinates); + } + + /** @todo Support for KHR_materials_specular? This adds an explicit + F0 (texture) and a scalar factor (texture) for the entire specular + reflection to a metallic/roughness material. */ + } + + /* Specular/glossiness material */ + if(material.has_pbr_specular_glossiness) { + types |= MaterialType::PbrSpecularGlossiness; + + const Vector4 diffuseFactor = Vector4::from(material.pbr_specular_glossiness.diffuse_factor); + if(diffuseFactor != Vector4{1.0f}) + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::DiffuseColor, + Color4{diffuseFactor}); + + const Vector3 specularFactor = Vector3::from(material.pbr_specular_glossiness.specular_factor); + if(specularFactor != Vector3{1.0f}) + arrayAppend(attributes, InPlaceInit, + /* Specular is 3-component in glTF, alpha should be 0 to not + affect transparent materials */ + MaterialAttribute::SpecularColor, + Color4{specularFactor, 0.0f}); + + if(material.pbr_specular_glossiness.glossiness_factor != 1.0f) + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::Glossiness, + material.pbr_specular_glossiness.glossiness_factor); + + if(material.pbr_specular_glossiness.diffuse_texture.texture) { + _d->materialTexture( + material.pbr_specular_glossiness.diffuse_texture, + attributes, + MaterialAttribute::DiffuseTexture, + MaterialAttribute::DiffuseTextureMatrix, + MaterialAttribute::DiffuseTextureCoordinates); + } + + if(material.pbr_specular_glossiness.specular_glossiness_texture.texture) { + _d->materialTexture( + material.pbr_specular_glossiness.specular_glossiness_texture, + attributes, + MaterialAttribute::SpecularGlossinessTexture, + MaterialAttribute::SpecularTextureMatrix, + MaterialAttribute::SpecularTextureCoordinates); + + /* Add the matrix/coordinates attributes also for the glossiness + texture, but skip adding the texture ID again */ + _d->materialTexture( + material.pbr_specular_glossiness.specular_glossiness_texture, + attributes, + MaterialAttribute{}, + MaterialAttribute::GlossinessTextureMatrix, + MaterialAttribute::GlossinessTextureCoordinates); + } + } + + /* Unlit material -- reset all types and add just Flat */ + if(material.unlit) + types = MaterialType::Flat; + + /* Normal texture */ + if(material.normal_texture.texture) { + _d->materialTexture( + material.normal_texture, + attributes, + MaterialAttribute::NormalTexture, + MaterialAttribute::NormalTextureMatrix, + MaterialAttribute::NormalTextureCoordinates); + + if(material.normal_texture.scale != 1.0f) + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::NormalTextureScale, + material.normal_texture.scale); + } + + /* Occlusion texture */ + if(material.occlusion_texture.texture) { + _d->materialTexture( + material.occlusion_texture, + attributes, + MaterialAttribute::OcclusionTexture, + MaterialAttribute::OcclusionTextureMatrix, + MaterialAttribute::OcclusionTextureCoordinates); + + /* cgltf exposes the strength multiplier as scale */ + if(material.occlusion_texture.scale != 1.0f) + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::OcclusionTextureStrength, + material.occlusion_texture.scale); + } + + /* Emissive factor & texture */ + const Vector3 emissiveFactor = Vector3::from(material.emissive_factor); + if(emissiveFactor != Vector3{0.0f}) + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::EmissiveColor, + Color3{emissiveFactor}); + if(material.emissive_texture.texture) { + _d->materialTexture( + material.emissive_texture, + attributes, + MaterialAttribute::EmissiveTexture, + MaterialAttribute::EmissiveTextureMatrix, + MaterialAttribute::EmissiveTextureCoordinates); + } + + /* Phong material fallback for backwards compatibility */ + if(configuration().value("phongMaterialFallback")) { + /* This adds a Phong type even to Flat materials because that's exactly + how it behaved before */ + types |= MaterialType::Phong; + + /* Create Diffuse attributes from BaseColor */ + Containers::Optional diffuseColor; + Containers::Optional diffuseTexture; + Containers::Optional diffuseTextureMatrix; + Containers::Optional diffuseTextureCoordinates; + for(const MaterialAttributeData& attribute: attributes) { + if(attribute.name() == "BaseColor") + diffuseColor = attribute.value(); + else if(attribute.name() == "BaseColorTexture") + diffuseTexture = attribute.value(); + else if(attribute.name() == "BaseColorTextureMatrix") + diffuseTextureMatrix = attribute.value(); + else if(attribute.name() == "BaseColorTextureCoordinates") + diffuseTextureCoordinates = attribute.value(); + } + + /* But if there already are those from the specular/glossiness + material, don't add them again. Has to be done in a separate pass + to avoid resetting too early. */ + for(const MaterialAttributeData& attribute: attributes) { + if(attribute.name() == "DiffuseColor") + diffuseColor = Containers::NullOpt; + else if(attribute.name() == "DiffuseTexture") + diffuseTexture = Containers::NullOpt; + else if(attribute.name() == "DiffuseTextureMatrix") + diffuseTextureMatrix = Containers::NullOpt; + else if(attribute.name() == "DiffuseTextureCoordinates") + diffuseTextureCoordinates = Containers::NullOpt; + } + + if(diffuseColor) + arrayAppend(attributes, InPlaceInit, MaterialAttribute::DiffuseColor, *diffuseColor); + if(diffuseTexture) + arrayAppend(attributes, InPlaceInit, MaterialAttribute::DiffuseTexture, *diffuseTexture); + if(diffuseTextureMatrix) + arrayAppend(attributes, InPlaceInit, MaterialAttribute::DiffuseTextureMatrix, *diffuseTextureMatrix); + if(diffuseTextureCoordinates) + arrayAppend(attributes, InPlaceInit, MaterialAttribute::DiffuseTextureCoordinates, *diffuseTextureCoordinates); + } + + /* Clear coat layer -- needs to be after all base material attributes */ + if(material.has_clearcoat) { + types |= MaterialType::PbrClearCoat; + + /* Add a new layer -- this works both if layers are empty and if + there's something already */ + arrayAppend(layers, UnsignedInt(attributes.size())); + arrayAppend(attributes, InPlaceInit, MaterialLayer::ClearCoat); + + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::LayerFactor, + material.clearcoat.clearcoat_factor); + + if(material.clearcoat.clearcoat_texture.texture) { + _d->materialTexture( + material.clearcoat.clearcoat_texture, + attributes, + MaterialAttribute::LayerFactorTexture, + MaterialAttribute::LayerFactorTextureMatrix, + MaterialAttribute::LayerFactorTextureCoordinates); + } + + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::Roughness, + material.clearcoat.clearcoat_roughness_factor); + + if(material.clearcoat.clearcoat_roughness_texture.texture) { + _d->materialTexture( + material.clearcoat.clearcoat_roughness_texture, + attributes, + MaterialAttribute::RoughnessTexture, + MaterialAttribute::RoughnessTextureMatrix, + MaterialAttribute::RoughnessTextureCoordinates); + + /* The extension description doesn't mention it, but the schema + says the clearcoat roughness is actually in the G channel: + https://github.com/KhronosGroup/glTF/blob/dc5519b9ce9834f07c30ec4c957234a0cd6280a2/extensions/2.0/Khronos/KHR_materials_clearcoat/schema/glTF.KHR_materials_clearcoat.schema.json#L32 */ + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::RoughnessTextureSwizzle, + MaterialTextureSwizzle::G); + } + + if(material.clearcoat.clearcoat_normal_texture.texture) { + _d->materialTexture( + material.clearcoat.clearcoat_normal_texture, + attributes, + MaterialAttribute::NormalTexture, + MaterialAttribute::NormalTextureMatrix, + MaterialAttribute::NormalTextureCoordinates); + + if(material.clearcoat.clearcoat_normal_texture.scale != 1.0f) + arrayAppend(attributes, InPlaceInit, + MaterialAttribute::NormalTextureScale, + material.clearcoat.clearcoat_normal_texture.scale); + } + } + + /* If there's any layer, add the final attribute count */ + arrayAppend(layers, UnsignedInt(attributes.size())); + + /* Can't use growable deleters in a plugin, convert back to the default + deleter */ + arrayShrink(layers); + arrayShrink(attributes, DefaultInit); + return MaterialData{types, std::move(attributes), std::move(layers)}; +} + +UnsignedInt CgltfImporter::doTextureCount() const { + return _d->data->textures_count; +} + +Int CgltfImporter::doTextureForName(const std::string& name) { + if(!_d->texturesForName) { + _d->texturesForName.emplace(); + _d->texturesForName->reserve(_d->data->textures_count); + for(std::size_t i = 0; i != _d->data->textures_count; ++i) + _d->texturesForName->emplace(_d->decodeString(_d->data->textures[i].name), i); + } + + const auto found = _d->texturesForName->find(name); + return found == _d->texturesForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doTextureName(const UnsignedInt id) { + return _d->decodeString(_d->data->textures[id].name); +} + +Containers::Optional CgltfImporter::doTexture(const UnsignedInt id) { + const cgltf_texture& tex = _d->data->textures[id]; + + UnsignedInt imageId = ~0u; + + /* Various extensions, they override the standard image */ + if(tex.has_basisu && tex.basisu_image) { + /* KHR_texture_basisu. Allows the usage of mimeType image/ktx2 but only + explicitly talks about KTX2 with Basis compression. We don't care + since we delegate to AnyImageImporter and let it figure out the file + type based on magic. Note: The core glTF spec only allows image/jpeg + and image/png but we don't check that either. */ + imageId = tex.basisu_image - _d->data->images; + } else { + constexpr Containers::StringView extensions[]{ + /* GOOGLE_texture_basis is not a registered extension but can be found + in some of the early Basis Universal examples. Basis files don't + have a registered mimetype either, but as explained above we don't + care about mimetype at all. */ + "GOOGLE_texture_basis"_s, + "MSFT_texture_dds"_s + /** @todo EXT_texture_webp once a plugin provides WebpImporter */ + }; + /* Use the first supported extension, assuming that extension order + indicates a preference */ + /** @todo Figure out a better priority + - extensionsRequired? + - image importers available via manager()->aliasList()? + - are there even files out there with more than one extension? */ + for(std::size_t i = 0; i != tex.extensions_count && imageId == ~0u; ++i) { + for(const auto& ext: extensions) { + if(tex.extensions[i].name == ext) { + const auto tokens = parseJson(tex.extensions[i].data); + if(tokens.size() == 3 && tokens[0].type == JSMN_OBJECT && tokens[1].type == JSMN_STRING && tokens[1].str == "source" && tokens[2].type == JSMN_PRIMITIVE) { + std::size_t parsed = 0; + const Int source = std::stoi(tokens[2].str, &parsed); + if(parsed != tokens[2].str.size() || source < 0 || UnsignedInt(source) >= _d->data->images_count) { + Error{} << "Trade::CgltfImporter::texture():" << ext << "image" << source << "out of bounds for" << _d->data->images_count << "images"; + return Containers::NullOpt; + } + imageId = source; + } + break; + } + } + } + } + + if(imageId == ~0u) { + /* If not overwritten by an extension, use the standard 'source' + attribute. It's not mandatory, so this can still fail. */ + if(tex.image) + imageId = tex.image - _d->data->images; + else { + Error{} << "Trade::CgltfImporter::texture(): no image source found"; + return Containers::NullOpt; + } + } + + CORRADE_INTERNAL_ASSERT(imageId < _d->data->images_count); + + /* Sampler */ + if(!tex.sampler) { + /* The specification instructs to use "auto sampling", i.e. it is left + to the implementor to decide on the default values... */ + return TextureData{TextureType::Texture2D, SamplerFilter::Linear, SamplerFilter::Linear, + SamplerMipmap::Linear, {SamplerWrapping::Repeat, SamplerWrapping::Repeat, SamplerWrapping::Repeat}, imageId}; + } + + /* GL filter enums */ + enum GltfTextureFilter: cgltf_int { + Nearest = 9728, + Linear = 9729, + NearestMipmapNearest = 9984, + LinearMipmapNearest = 9985, + NearestMipmapLinear = 9986, + LinearMipmapLinear = 9987 + }; + + SamplerFilter minFilter; + SamplerMipmap mipmap; + switch(tex.sampler->min_filter) { + case GltfTextureFilter::Nearest: + minFilter = SamplerFilter::Nearest; + mipmap = SamplerMipmap::Base; + break; + case GltfTextureFilter::Linear: + minFilter = SamplerFilter::Linear; + mipmap = SamplerMipmap::Base; + break; + case GltfTextureFilter::NearestMipmapNearest: + minFilter = SamplerFilter::Nearest; + mipmap = SamplerMipmap::Nearest; + break; + case GltfTextureFilter::NearestMipmapLinear: + minFilter = SamplerFilter::Nearest; + mipmap = SamplerMipmap::Linear; + break; + case GltfTextureFilter::LinearMipmapNearest: + minFilter = SamplerFilter::Linear; + mipmap = SamplerMipmap::Nearest; + break; + case GltfTextureFilter::LinearMipmapLinear: + /* glTF 2.0 spec does not define a default value for 'minFilter' and + 'magFilter'. In this case cgltf sets it to 0. */ + case 0: + minFilter = SamplerFilter::Linear; + mipmap = SamplerMipmap::Linear; + break; + default: + Error{} << "Trade::CgltfImporter::texture(): invalid minFilter" << tex.sampler->min_filter; + return Containers::NullOpt; + } + + SamplerFilter magFilter; + switch(tex.sampler->mag_filter) { + case GltfTextureFilter::Nearest: + magFilter = SamplerFilter::Nearest; + break; + case GltfTextureFilter::Linear: + /* glTF 2.0 spec does not define a default value for 'minFilter' and + 'magFilter'. In this case cgltf sets it to 0. */ + case 0: + magFilter = SamplerFilter::Linear; + break; + default: + Error{} << "Trade::CgltfImporter::texture(): invalid magFilter" << tex.sampler->mag_filter; + return Containers::NullOpt; + } + + /* GL wrap enums */ + enum GltfTextureWrap: cgltf_int { + Repeat = 10497, + ClampToEdge = 33071, + MirroredRepeat = 33648 + }; + + Math::Vector3 wrapping; + wrapping.z() = SamplerWrapping::Repeat; + for(auto&& wrap: std::initializer_list>{ + {tex.sampler->wrap_s, 0}, {tex.sampler->wrap_t, 1}}) + { + switch(wrap.first()) { + case GltfTextureWrap::Repeat: + wrapping[wrap.second()] = SamplerWrapping::Repeat; + break; + case GltfTextureWrap::ClampToEdge: + wrapping[wrap.second()] = SamplerWrapping::ClampToEdge; + break; + case GltfTextureWrap::MirroredRepeat: + wrapping[wrap.second()] = SamplerWrapping::MirroredRepeat; + break; + default: + Error{} << "Trade::CgltfImporter::texture(): invalid wrap mode" << wrap.first(); + return Containers::NullOpt; + } + } + + /* glTF supports only 2D textures */ + return TextureData{TextureType::Texture2D, minFilter, magFilter, + mipmap, wrapping, imageId}; +} + +UnsignedInt CgltfImporter::doImage2DCount() const { + return _d->data->images_count; +} + +Int CgltfImporter::doImage2DForName(const std::string& name) { + if(!_d->imagesForName) { + _d->imagesForName.emplace(); + _d->imagesForName->reserve(_d->data->images_count); + for(std::size_t i = 0; i != _d->data->images_count; ++i) + _d->imagesForName->emplace(_d->decodeString(_d->data->images[i].name), i); + } + + const auto found = _d->imagesForName->find(name); + return found == _d->imagesForName->end() ? -1 : found->second; +} + +std::string CgltfImporter::doImage2DName(const UnsignedInt id) { + return _d->decodeString(_d->data->images[id].name); +} + +AbstractImporter* CgltfImporter::setupOrReuseImporterForImage(const UnsignedInt id, const char* const function) { + /* Looking for the same ID, so reuse an importer populated before. If the + previous attempt failed, the importer is not set, so return nullptr in + that case. Going through everything below again would not change the + outcome anyway, only spam the output with redundant messages. */ + if(_d->imageImporterId == id) + return _d->imageImporter ? &*_d->imageImporter : nullptr; + + /* Otherwise reset the importer and remember the new ID. If the import + fails, the importer will stay unset, but the ID will be updated so the + next round can again just return nullptr above instead of going through + the doomed-to-fail process again. */ + _d->imageImporter = Containers::NullOpt; + _d->imageImporterId = id; + + AnyImageImporter importer{*manager()}; + if(fileCallback()) importer.setFileCallback(fileCallback(), fileCallbackUserData()); + + const cgltf_image& image = _d->data->images[id]; + + /* Load embedded image. Can either be a buffer view or a base64 payload. + Buffers are kept in memory until the importer closes but decoded base64 + data is freed after opening the image. */ + if(!image.uri || isDataUri(image.uri)) { + Containers::Array imageData; + Containers::ArrayView imageView; + + if(image.uri) { + const auto view = _d->loadUri(image.uri, imageData, function); + if(!view) + return nullptr; + imageView = *view; + } else { + if(!image.buffer_view) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): image has neither a URI nor a buffer view"; + return nullptr; + } + + const cgltf_buffer* buffer = image.buffer_view->buffer; + const UnsignedInt bufferId = buffer - _d->data->buffers; + if(!_d->loadBuffer(bufferId, function)) + return nullptr; + imageView = Containers::arrayView(static_cast(buffer->data) + image.buffer_view->offset, image.buffer_view->size); + } + + if(!importer.openData(imageView)) + return nullptr; + return &_d->imageImporter.emplace(std::move(importer)); + } + + /* Load external image */ + if(!_d->filePath && !fileCallback()) { + Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): external images can be imported only when opening files from the filesystem or if a file callback is present"; + return nullptr; + } + + if(!importer.openFile(Utility::Directory::join(_d->filePath ? *_d->filePath : "", decodeUri(_d->decodeString(image.uri))))) + return nullptr; + return &_d->imageImporter.emplace(std::move(importer)); +} + +UnsignedInt CgltfImporter::doImage2DLevelCount(const UnsignedInt id) { + CORRADE_ASSERT(manager(), "Trade::CgltfImporter::image2DLevelCount(): the plugin must be instantiated with access to plugin manager in order to open image files", {}); + + AbstractImporter* importer = setupOrReuseImporterForImage(id, "image2DLevelCount"); + /* image2DLevelCount() isn't supposed to fail (image2D() is, instead), so + report 1 on failure and expect image2D() to fail later */ + if(!importer) return 1; + + return importer->image2DLevelCount(0); +} + +Containers::Optional CgltfImporter::doImage2D(const UnsignedInt id, const UnsignedInt level) { + CORRADE_ASSERT(manager(), "Trade::CgltfImporter::image2D(): the plugin must be instantiated with access to plugin manager in order to load images", {}); + + AbstractImporter* importer = setupOrReuseImporterForImage(id, "image2D"); + if(!importer) return Containers::NullOpt; + + Containers::Optional imageData = importer->image2D(0, level); + if(!imageData) return Containers::NullOpt; + return ImageData2D{std::move(*imageData)}; +} + +}} + +CORRADE_PLUGIN_REGISTER(CgltfImporter, Magnum::Trade::CgltfImporter, + "cz.mosra.magnum.Trade.AbstractImporter/0.3.3") diff --git a/src/MagnumPlugins/CgltfImporter/CgltfImporter.h b/src/MagnumPlugins/CgltfImporter/CgltfImporter.h new file mode 100644 index 000000000..1a83b74e4 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/CgltfImporter.h @@ -0,0 +1,476 @@ +#ifndef Magnum_Trade_CgltfImporter_h +#define Magnum_Trade_CgltfImporter_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + Copyright © 2021 Pablo Escobar + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Class @ref Magnum::Trade::CgltfImporter + * @m_since_latest_{plugins} + */ + +#include + +#include "MagnumPlugins/CgltfImporter/configure.h" + +namespace Magnum { namespace Trade { + +#ifndef DOXYGEN_GENERATING_OUTPUT +#ifndef MAGNUM_CGLTFIMPORTER_BUILD_STATIC + #ifdef CgltfImporter_EXPORTS + #define MAGNUM_CGLTFIMPORTER_EXPORT CORRADE_VISIBILITY_EXPORT + #else + #define MAGNUM_CGLTFIMPORTER_EXPORT CORRADE_VISIBILITY_IMPORT + #endif +#else + #define MAGNUM_CGLTFIMPORTER_EXPORT CORRADE_VISIBILITY_STATIC +#endif +#define MAGNUM_CGLTFIMPORTER_LOCAL CORRADE_VISIBILITY_LOCAL +#else +#define MAGNUM_CGLTFIMPORTER_EXPORT +#define MAGNUM_CGLTFIMPORTER_LOCAL +#endif + +/** +@brief Cgltf importer plugin +@m_since_latest_{plugins} + +@m_keywords{GltfImporter} + +Imports glTF and binary glTF using the [cgltf](https://github.com/jkuhlmann/cgltf) +library. + +This plugin provides the `GltfImporter` plugin. + +@m_class{m-block m-success} + +@thirdparty This plugin makes use of the [cgltf](https://github.com/jkuhlmann/cgltf) + library, licensed under @m_class{m-label m-success} **MIT** + ([license text](https://github.com/jkuhlmann/cgltf/blob/master/LICENSE), + [choosealicense.com](https://choosealicense.com/licenses/mit/)). + It requires attribution for public use. Cgltf itself uses + [jsmn](https://github.com/nlohmann/json), licensed under + @m_class{m-label m-success} **MIT** as well + ([license text](https://github.com/zserge/jsmn/blob/master/LICENSE)). + +@section Trade-CgltfImporter-usage Usage + +This plugin depends on the @ref Trade library and the @ref AnyImageImporter +plugin and is built if `WITH_CGLTFIMPORTER` is enabled when building Magnum +Plugins. To use as a dynamic plugin, load @cpp "CgltfImporter" @ce via +@ref Corrade::PluginManager::Manager. + +Additionally, if you're using Magnum as a CMake subproject, bundle the +[magnum-plugins repository](https://github.com/mosra/magnum-plugins) and do the +following: + +@code{.cmake} +set(WITH_ANYIMAGEIMPORTER ON CACHE BOOL "" FORCE) +add_subdirectory(magnum EXCLUDE_FROM_ALL) + +set(WITH_CGLTFIMPORTER ON CACHE BOOL "" FORCE) +add_subdirectory(magnum-plugins EXCLUDE_FROM_ALL) + +# So the dynamically loaded plugin gets built implicitly +add_dependencies(your-app MagnumPlugins::CgltfImporter) +@endcode + +To use as a static plugin or as a dependency of another plugin with CMake, put +[FindMagnumPlugins.cmake](https://github.com/mosra/magnum-plugins/blob/master/modules/FindMagnumPlugins.cmake) +into your `modules/` directory, request the `CgltfImporter` component of the +`MagnumPlugins` package and link to the `MagnumPlugins::CgltfImporter` +target: + +@code{.cmake} +find_package(MagnumPlugins REQUIRED CgltfImporter) + +# ... +target_link_libraries(your-app PRIVATE MagnumPlugins::CgltfImporter) +@endcode + +See @ref building-plugins, @ref cmake-plugins, @ref plugins and +@ref file-formats for more information. + +@section Trade-CgltfImporter-behavior Behavior and limitations + +The plugin supports @ref ImporterFeature::OpenData and +@ref ImporterFeature::FileCallback features. All buffers are loaded on-demand +and kept in memory for any later access. As a result, external file loading +callbacks are called with @ref InputFileCallbackPolicy::LoadPermanent. +Resources returned from file callbacks can only be safely freed after closing +the importer instance. In case of images, the files are loaded on-demand inside +@ref image2D() calls with @ref InputFileCallbackPolicy::LoadTemporary and +@ref InputFileCallbackPolicy::Close is emitted right after the file is fully +read. + +Error reporting by cgltf is rather rudimentary, resulting in vague error +messages and no line numbers for several classes of errors, including +out-of-bounds indices and missing required attributes. If you need more +detailed errors, consider using the [glTF Validator](https://github.khronos.org/glTF-Validator/). + +The content of the global [`extensionsRequired`](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#specifying-extensions) +array is checked against all extensions supported by the plugin. If a glTF file +requires an unknown extension, the import will fail. This behaviour can be +disabled with the @cb{.ini} ignoreRequiredExtensions @ce option, see +@ref Trade-CgltfImporter-configuration "below". + +Import of morph data is not supported at the moment. + +@subsection Trade-CgltfImporter-behavior-animation Animation and skin import + +- Linear quaternion rotation tracks are postprocessed in order to make it + possible to use the faster + @ref Math::lerp(const Quaternion&, const Quaternion&, T) "Math::lerp()" / + @ref Math::slerp(const Quaternion&, const Quaternion&, T) "Math::slerp()" + functions instead of + @ref Math::lerpShortestPath(const Quaternion&, const Quaternion&, T) "Math::lerpShortestPath()" / + @ref Math::slerpShortestPath(const Quaternion&, const Quaternion&, T) "Math::slerpShortestPath()". Can be disabled per-animation with the + @cb{.ini} optimizeQuaternionShortestPath @ce option, see + @ref Trade-CgltfImporter-configuration "below". This doesn't affect + spline-interpolated rotation tracks. +- If linear quaternion rotation tracks are not normalized, the importer + prints a warning and normalizes them. Can be disabled per-animation with + the @cb{.ini} normalizeQuaternions @ce option, see + @ref Trade-CgltfImporter-configuration "below". This doesn't affect + spline-interpolated rotation tracks. +- Skin `skeleton` property is not imported +- Morph targets are not supported +- Animation tracks are always imported with + @ref Animation::Extrapolation::Constant, because glTF doesn't support + anything else +- It's possible to request all animation clips to be merged into one using + the @cb{.ini} mergeAnimationClips @ce option in order to for example + preserve cinematic animations when using the Blender glTF exporter (as it + otherwise outputs a separate clip for each object). When this option is + enabled, @ref animationCount() always report either @cpp 0 @ce or + @cpp 1 @ce and the merged animation has no name. With this option enabled, + however, it can happen that multiple conflicting tracks affecting the same + node are merged in the same clip, causing the animation to misbehave. + +@subsection Trade-CgltfImporter-behavior-objects Scene and object import + +- If no @cb{.json} "scene" @ce property is present and the file contains at + least one scene, @ref defaultScene() returns @cpp 0 @ce instead of + @cpp -1 @ce. According to the [glTF 2.0 specification](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#scenes) + the importer is free to not render anything, but the suggested behavior + would break even some official sample models. +- In case object transformation is set via separate + translation/rotation/scaling properties in the source file, + @ref ObjectData3D is created with @ref ObjectFlag3D::HasTranslationRotationScaling and these separate properties accessible +- If object rotation quaternion is not normalized, the importer prints a + warning and normalizes it. Can be disabled per-object with the + @cb{.ini} normalizeQuaternions @ce option, see + @ref Trade-CgltfImporter-configuration "below". + +@subsection Trade-CgltfImporter-behavior-camera Camera import + +- Cameras in glTF are specified with vertical FoV and vertical:horizontal + aspect ratio, these values are recalculated for horizontal FoV and + horizontal:vertical aspect ratio as is common in Magnum + +@subsection Trade-CgltfImporter-behavior-lights Light import + +- The importer supports the [KHR_lights_punctual](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_lights_punctual/README.md) + extension + +@subsection Trade-CgltfImporter-behavior-meshes Mesh import + +- Indices are imported as either @ref MeshIndexType::UnsignedByte, + @ref MeshIndexType::UnsignedShort or @ref MeshIndexType::UnsignedInt +- Positions are imported as @ref VertexFormat::Vector3, + @ref VertexFormat::Vector3ub, @ref VertexFormat::Vector3b, + @ref VertexFormat::Vector3us, @ref VertexFormat::Vector3s, + @ref VertexFormat::Vector3ubNormalized, + @ref VertexFormat::Vector3bNormalized, + @ref VertexFormat::Vector3usNormalized or + @ref VertexFormat::Vector3sNormalized (which includes the additional types + specified by [KHR_mesh_quantization](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md)) +- Normals, if any, are imported as @ref VertexFormat::Vector3, + @ref VertexFormat::Vector3bNormalized or + @ref VertexFormat::Vector3sNormalized +- Texture coordinates are imported as @ref VertexFormat::Vector3, + @ref VertexFormat::Vector3ub, @ref VertexFormat::Vector3b, + @ref VertexFormat::Vector3us, @ref VertexFormat::Vector3s, + @ref VertexFormat::Vector3ubNormalized, + @ref VertexFormat::Vector3bNormalized, + @ref VertexFormat::Vector3usNormalized or + @ref VertexFormat::Vector3sNormalized (which includes the additional types + specified by [KHR_mesh_quantization](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md)). The + data are by default Y-flipped on import unless + @cb{.conf} textureCoordinateYFlipInMaterial @ce is either explicitly + enabled, or if the file contains non-normalized integer or normalized + signed integer texture coordinates (which can't easily be flipped). In that + case texture coordinate data are kept as-is and materials provide a texture + transformation that does the Y-flip instead. +- Colors are imported as @ref VertexFormat::Vector3, + @ref VertexFormat::Vector4, + @ref VertexFormat::Vector3ubNormalized, + @ref VertexFormat::Vector4ubNormalized, + @ref VertexFormat::Vector3usNormalized or + @ref VertexFormat::Vector4usNormalized +- Joint IDs and weights for skinning are imported as custom vertex attributes + named "JOINTS" and "WEIGHTS". Their mapping to/from a string can be queried + using @ref meshAttributeName() and @ref meshAttributeForName(). + Joint IDs are imported as @ref VertexFormat::Vector4ub or + @ref VertexFormat::Vector4us. Joint weights are imported as + @ref VertexFormat::Vector4, @ref VertexFormat::Vector4ubNormalized or + @ref VertexFormat::Vector4usNormalized. +- Per-vertex object ID attribute is imported as either + @ref VertexFormat::UnsignedInt, @ref VertexFormat::UnsignedShort or + @ref VertexFormat::UnsignedByte. By default `_OBJECT_ID` is the recognized + name, use the @cb{.ini} objectIdAttribute @ce + @ref Trade-CgltfImporter-configuration "configuration option" to change + the identifier that's being looked for. +- Multi-primitive meshes are loaded as follows, consistently with the + behavior of @link AssimpImporter @endlink: + - The @ref meshCount() query returns a number of all *primitives*, not + meshes + - Each multi-primitive mesh is split into a sequence of @ref MeshData + instances following each other + - @ref meshForName() points to the first mesh in given sequence and + @ref meshName() returns the same name for all meshes in given + sequence + - The @ref object3DCount() query returns a number of all nodes extended + with number of extra nodes for each additional mesh primitive + - Each node referencing a multi-primitive mesh is split into a sequence + of @ref MeshObjectData3D instances following each other; the extra + nodes being a direct and immediate children of the first one with an + identity transformation + - @ref object3DForName() points to the first object containing the first + primitive, @ref object3DName() returns the same name for all objects in + given sequence + - @ref AnimationData instances returned by @ref animation() have their + @ref AnimationData::trackTarget() values patched to account for the + extra nodes, always pointing to the first object in the sequence and + thus indirectly affecting transformations of the extra nodes + represented as its children +- Attribute-less meshes either with or without an index buffer are supported, + however since glTF has no way of specifying vertex count for those, + returned @ref Trade::MeshData::vertexCount() is set to @cpp 0 @ce + +Custom and unrecognized vertex attributes of allowed types are present in the +imported meshes as well. Their mapping to/from a string can be queried using +@ref meshAttributeName() and @ref meshAttributeForName(). Attributes with +unsupported types (such as non-normalized integer matrices) cause the import to +fail. + +@subsection Trade-CgltfImporter-behavior-materials Material import + +- If present, builtin [metallic/roughness](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#metallic-roughness-material) material is imported, + setting @ref MaterialType::PbrMetallicRoughness on the @ref MaterialData. +- If the [KHR_materials_pbrSpecularGlossiness](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness/README.md) + extension is present, its properties are imported with + @ref MaterialType::PbrSpecularGlossiness present in material types. +- Additional normal, occlusion and emissive maps are imported, together with + related properties +- If the [KHR_materials_unlit](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_unlit/README.md) + extension is present, @ref MaterialType::Flat is set in material types, + replacing @ref MaterialType::PbrMetallicRoughness or + @ref MaterialType::PbrSpecularGlossiness. +- If the [KHR_materials_clearcoat](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md) + extension is present, @ref MaterialType::PbrClearCoat is set in material + types, and a new layer with clearcoat properties is added +- Custom texture coordinate sets as well as [KHR_texture_transform](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_transform/README.md) + properties are imported on all textures. +- If the on-by-default @cb{.ini} phongMaterialFallback @ce + @ref Trade-CgltfImporter-configuration "configuration option" is + enabled, the importer provides a Phong fallback for backwards + compatibility: + - @ref MaterialType::Phong is added to material types + - Base color and base color texture along with custom texture coordinate + set and transformation, if present, is exposed as a diffuse color and + texture, unless already present together with specular color / texture + from the specular/glossiness material + - All other @ref PhongMaterialData values are is kept at their defaults + +@subsection Trade-CgltfImporter-behavior-textures Texture and image import + +
    +
  • Texture type is always @ref Trade::TextureType::Texture2D, as glTF doesn't +support anything else
  • +
  • Z coordinate of @ref Trade::TextureData::wrapping() is always +@ref SamplerWrapping::Repeat, as glTF doesn't support 3D textures
  • +
  • +@m_class{m-nopadb} + +glTF leaves the defaults of sampler properties to the application, the +following defaults have been chosen for this importer: + +- Minification/magnification/mipmap filter: @ref SamplerFilter::Linear, + @ref SamplerMipmap::Linear +- Wrapping (all axes): @ref SamplerWrapping::Repeat +
  • +
  • + The importer supports the following extensions for image types not defined + in the [core glTF 2.0 specification](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#gltf-basics): + [MSFT_texture_dds](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/MSFT_texture_dds/README.md) + for DirectDraw Surface images (`*.dds`), + [KHR_texture_basisu](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_basisu/README.md) + for Khronos Texture 2.0 images (`*.ktx2`) with [Basis Universal](https://github.com/binomialLLC/basis_universal) + supercompression, as well as the original provisional `GOOGLE_texture_basis` + extension for referencing plain Basis Universal files (`*.basis`). There was + no formal specification of the extension but the use is like below, + [equivalently to Basis own glTF example](https://github.com/BinomialLLC/basis_universal/blob/1cae1d57266e2c95bc011b0bf1ccb9940988c184/webgl/gltf/assets/AgiHqSmall.gltf#L230-L240): + + @code{.json} + { + ... + "textures": [ + { + "extensions": { + "GOOGLE_texture_basis": { + "source": 0 + } + } + } + ], + "images": [ + { + "mimeType": "image/x-basis", + "uri": "texture.basis" + } + ], + "extensionsUsed": [ + "GOOGLE_texture_basis" + ], + "extensionsRequired": [ + "GOOGLE_texture_basis" + ] + } + @endcode + + The MIME type (if one exists) is ignored by the importer. Delegation to the + correct importer alias happens via @ref AnyImageImporter which uses the + file extension or buffer content to determine the image type. +
  • +
+ +@section Trade-CgltfImporter-configuration Plugin-specific config + +It's possible to tune various output options through @ref configuration(). See +below for all options and their default values. + +@snippet MagnumPlugins/CgltfImporter/CgltfImporter.conf config + +See @ref plugins-configuration for more information and an example showing how +to edit the configuration values. +*/ +class MAGNUM_CGLTFIMPORTER_EXPORT CgltfImporter: public AbstractImporter { + public: + /** + * @brief Default constructor + * + * In case you want to open images, use + * @ref CgltfImporter(PluginManager::Manager&) + * instead. + */ + explicit CgltfImporter(); + + /** + * @brief Constructor + * + * The plugin needs access to plugin manager for importing images. + */ + explicit CgltfImporter(PluginManager::Manager& manager); + + /** @brief Plugin manager constructor */ + explicit CgltfImporter(PluginManager::AbstractManager& manager, const std::string& plugin); + + ~CgltfImporter(); + + private: + struct Document; + + MAGNUM_CGLTFIMPORTER_LOCAL ImporterFeatures doFeatures() const override; + + MAGNUM_CGLTFIMPORTER_LOCAL bool doIsOpened() const override; + + MAGNUM_CGLTFIMPORTER_LOCAL void doOpenData(Containers::ArrayView data) override; + MAGNUM_CGLTFIMPORTER_LOCAL void doOpenFile(const std::string& filename) override; + MAGNUM_CGLTFIMPORTER_LOCAL void doClose() override; + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doAnimationCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doAnimationForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doAnimationName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Optional doAnimation(UnsignedInt id) override; + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doCameraCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doCameraForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doCameraName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Optional doCamera(UnsignedInt id) override; + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doLightCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doLightForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doLightName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Optional doLight(UnsignedInt id) override; + + MAGNUM_CGLTFIMPORTER_LOCAL Int doDefaultScene() const override; + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doSceneCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doSceneForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doSceneName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Optional doScene(UnsignedInt id) override; + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doObject3DCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doObject3DForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doObject3DName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Pointer doObject3D(UnsignedInt id) override; + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doSkin3DCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doSkin3DForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doSkin3DName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Optional doSkin3D(UnsignedInt id) override; + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doMeshCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doMeshForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doMeshName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Optional doMesh(UnsignedInt id, UnsignedInt level) override; + MAGNUM_CGLTFIMPORTER_LOCAL MeshAttribute doMeshAttributeForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doMeshAttributeName(UnsignedShort name) override; + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doMaterialCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doMaterialForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doMaterialName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Optional doMaterial(UnsignedInt id) override; + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doTextureCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doTextureForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doTextureName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Optional doTexture(UnsignedInt id) override; + + MAGNUM_CGLTFIMPORTER_LOCAL AbstractImporter* setupOrReuseImporterForImage(UnsignedInt id, const char* errorPrefix); + + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doImage2DCount() const override; + MAGNUM_CGLTFIMPORTER_LOCAL UnsignedInt doImage2DLevelCount(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Int doImage2DForName(const std::string& name) override; + MAGNUM_CGLTFIMPORTER_LOCAL std::string doImage2DName(UnsignedInt id) override; + MAGNUM_CGLTFIMPORTER_LOCAL Containers::Optional doImage2D(UnsignedInt id, UnsignedInt level) override; + + Containers::Pointer _d; +}; + +}} + +#endif diff --git a/src/MagnumPlugins/CgltfImporter/Test/CMakeLists.txt b/src/MagnumPlugins/CgltfImporter/Test/CMakeLists.txt new file mode 100644 index 000000000..91fe2f1d8 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/CMakeLists.txt @@ -0,0 +1,252 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021 Vladimír Vondruš +# Copyright © 2021 Pablo Escobar +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) + set(TINYGLTFIMPORTER_TEST_DIR ".") + set(CGLTFIMPORTER_TEST_DIR ".") +else() + set(TINYGLTFIMPORTER_TEST_DIR ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test) + set(CGLTFIMPORTER_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}) +endif() + +# CMake before 3.8 has broken $ expressions for iOS (see +# https://gitlab.kitware.com/cmake/cmake/merge_requests/404) and since Corrade +# doesn't support dynamic plugins on iOS, this sorta works around that. Should +# be revisited when updating Travis to newer Xcode (xcode7.3 has CMake 3.6). +if(NOT MAGNUM_CGLTFIMPORTER_BUILD_STATIC) + set(CGLTFIMPORTER_PLUGIN_FILENAME $) + if(WITH_BASISIMPORTER) + set(BASISIMPORTER_PLUGIN_FILENAME $) + endif() + if(WITH_STBIMAGEIMPORTER) + set(STBIMAGEIMPORTER_PLUGIN_FILENAME $) + endif() +endif() + +# First replace ${} variables, then $<> generator expressions +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/configure.h + INPUT ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) + +corrade_add_resource(CgltfImporterTest_RESOURCES resources.conf) + +corrade_add_test(CgltfImporterTest + CgltfImporterTest.cpp + ${CgltfImporterTest_RESOURCES} + LIBRARIES Magnum::Trade + FILES + animation-buffer-notfound.gltf + animation-invalid-types.gltf + # modified version of animation-patching.* from TinyGltfImporter + # without size mismatch between input and output samplers + animation-patching-fixed.bin + animation-patching-fixed.gltf + buffer-short-size.glb + buffer-short-size.gltf + buffer-short-size-embedded.glb + buffer-short-size-embedded.gltf + # identical to external-data.bin from TinyGltfImporter, but needed + # locally for buffer-short-size* + external-data.bin + external-data-order.glb + external-data-order.gltf + image-buffer-notfound.gltf + image-no-data.gltf + mesh-indices-buffer-notfound.gltf + mesh-invalid-types.gltf + skin-buffer-notfound.gltf + skin-invalid-types.gltf + uri-invalid.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/empty.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/empty.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/buffer-notfound.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/buffer-notfound.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/buffer-no-uri.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/buffer-no-uri.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/buffer-wrong-size.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/buffer-wrong-size.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-embedded.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-embedded.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-invalid.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-invalid-node-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-invalid-input-accessor-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-invalid-sampler-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-invalid-output-accessor-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-missing-target-node.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-patching.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-patching.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/animation-splines-sharing.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/camera.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/camera.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/camera-invalid-type.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/encoded-uris.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/escaped-strings.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-embedded.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-embedded.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-buffer.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-buffer.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-buffer-embedded.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-buffer-embedded.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-notfound.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-basis.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-basis.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-basis-embedded.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/image-basis-embedded.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/light.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/light.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/light-invalid.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/light-invalid-color-size.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/light-missing-type.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/light-missing-spot.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-common.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-clearcoat.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-alpha-mode.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-clearcoat-factor-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-clearcoat-normal-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-clearcoat-roughness-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-emissive-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-normal-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-occlusion-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-pbr-base-color-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-pbr-diffuse-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-pbr-metallic-roughness-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-invalid-pbr-specular-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-metallicroughness.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-phong-fallback.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-specularglossiness.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-texcoord-flip.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-texcoord-flip.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-texcoord-flip-unnormalized.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/material-unlit.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-colors.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-colors.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-custom-attributes.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-custom-attributes.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-duplicate-attributes.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-embedded.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-embedded.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-index-accessor-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-invalid.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-invalid.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-invalid-accessor-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-invalid-buffer-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-invalid-bufferview-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-multiple-primitives.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-primitives-types.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-primitives-types.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-skin-attributes.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-skin-attributes.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/mesh-unordered-attributes.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/přívodní-šňůra.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/přívodní-šňůra.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/přívodní-šňůra.png + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/required-extensions.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/required-extensions-unsupported.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-cycle.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-cycle-deep.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-nodefault.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-nodefault.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-camera-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-child-not-root.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-child-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-default-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-light-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-material-oob-multi-primitive.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-material-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-mesh-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-multiple-parents.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-node-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-skin-oob-multi-primitive.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/scene-invalid-skin-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/skin-embedded.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/skin-embedded.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/skin-invalid.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/skin-invalid-accessor-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/skin-invalid-joint-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/skin-no-joints.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/skin.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/skin.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/skin.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/object-transformation.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/object-transformation.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/object-transformation-patching.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture.basis + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture.png + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-default-sampler.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-default-sampler.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-empty-sampler.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-empty-sampler.glb + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-extensions.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-extensions-invalid.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-extensions-invalid-basisu-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-invalid.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-invalid-image-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-invalid-sampler-oob.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/texture-missing-source.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/version-legacy.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/version-supported.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/version-unsupported.gltf + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/TinyGltfImporter/Test/version-unsupported-min.gltf) +target_include_directories(CgltfImporterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) +if(MAGNUM_CGLTFIMPORTER_BUILD_STATIC) + target_link_libraries(CgltfImporterTest PRIVATE CgltfImporter) + if(WITH_BASISIMPORTER) + target_link_libraries(CgltfImporterTest PRIVATE BasisImporter) + endif() + if(WITH_STBIMAGEIMPORTER) + target_link_libraries(CgltfImporterTest PRIVATE StbImageImporter) + endif() +else() + # So the plugins get properly built when building the test + add_dependencies(CgltfImporterTest CgltfImporter) + if(WITH_BASISIMPORTER) + add_dependencies(CgltfImporterTest BasisImporter) + endif() + if(WITH_STBIMAGEIMPORTER) + add_dependencies(CgltfImporterTest StbImageImporter) + endif() +endif() +set_target_properties(CgltfImporterTest PROPERTIES FOLDER "MagnumPlugins/CgltfImporter/Test") +if(CORRADE_BUILD_STATIC AND NOT MAGNUM_CGLTFIMPORTER_BUILD_STATIC) + # CMake < 3.4 does this implicitly, but 3.4+ not anymore (see CMP0065). + # That's generally okay, *except if* the build is static, the executable + # uses a plugin manager and needs to share globals with the plugins (such + # as output redirection and so on). + set_target_properties(CgltfImporterTest PROPERTIES ENABLE_EXPORTS ON) +endif() diff --git a/src/MagnumPlugins/CgltfImporter/Test/CgltfImporterTest.cpp b/src/MagnumPlugins/CgltfImporter/Test/CgltfImporterTest.cpp new file mode 100644 index 000000000..7b69dd5de --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/CgltfImporterTest.cpp @@ -0,0 +1,4603 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + Copyright © 2021 Pablo Escobar + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "configure.h" + +namespace Magnum { namespace Trade { namespace Test { namespace { + +struct CgltfImporterTest: TestSuite::Tester { + explicit CgltfImporterTest(); + + void open(); + void openError(); + void openExternalDataOrder(); + void openExternalDataNotFound(); + void openExternalDataNoPathNoCallback(); + void openExternalDataTooLong(); + void openExternalDataTooShort(); + void openExternalDataNoUri(); + void openExternalDataInvalidUri(); + + void requiredExtensions(); + void requiredExtensionsUnsupported(); + void requiredExtensionsUnsupportedDisabled(); + + void animation(); + void animationOutOfBounds(); + void animationInvalid(); + void animationInvalidBufferNotFound(); + void animationInvalidInterpolation(); + void animationInvalidTypes(); + void animationMismatchingCount(); + void animationMissingTargetNode(); + + void animationSpline(); + void animationSplineSharedWithSameTimeTrack(); + void animationSplineSharedWithDifferentTimeTrack(); + + void animationShortestPathOptimizationEnabled(); + void animationShortestPathOptimizationDisabled(); + void animationQuaternionNormalizationEnabled(); + void animationQuaternionNormalizationDisabled(); + void animationMergeEmpty(); + void animationMerge(); + + void camera(); + void cameraInvalidType(); + + void light(); + void lightInvalid(); + void lightInvalidColorSize(); + void lightMissingType(); + void lightMissingSpot(); + + void scene(); + void sceneEmpty(); + void sceneNoDefault(); + void sceneOutOfBounds(); + void sceneInvalid(); + void sceneCycle(); + + void objectTransformation(); + void objectTransformationQuaternionNormalizationEnabled(); + void objectTransformationQuaternionNormalizationDisabled(); + + void skin(); + void skinOutOfBounds(); + void skinInvalid(); + void skinInvalidBufferNotFound(); + void skinInvalidTypes(); + void skinNoJointsProperty(); + + void mesh(); + void meshAttributeless(); + void meshIndexed(); + void meshIndexedAttributeless(); + void meshColors(); + void meshSkinAttributes(); + void meshCustomAttributes(); + void meshCustomAttributesNoFileOpened(); + void meshDuplicateAttributes(); + void meshUnorderedAttributes(); + void meshMultiplePrimitives(); + void meshPrimitivesTypes(); + void meshOutOfBounds(); + void meshInvalid(); + void meshInvalidIndicesBufferNotFound(); + void meshInvalidTypes(); + + void materialPbrMetallicRoughness(); + void materialPbrSpecularGlossiness(); + void materialCommon(); + void materialUnlit(); + void materialClearCoat(); + void materialPhongFallback(); + + void materialOutOfBounds(); + void materialInvalidAlphaMode(); + void materialTexCoordFlip(); + + void texture(); + void textureOutOfBounds(); + void textureInvalid(); + void textureDefaultSampler(); + void textureEmptySampler(); + void textureMissingSource(); + void textureExtensions(); + void textureExtensionsOutOfBounds(); + void textureExtensionsInvalid(); + + void imageEmbedded(); + void imageExternal(); + void imageExternalNotFound(); + void imageExternalBufferNotFound(); + void imageExternalNoPathNoCallback(); + void imageNoData(); + + void imageBasis(); + void imageMipLevels(); + + void fileCallbackBuffer(); + void fileCallbackBufferNotFound(); + void fileCallbackImage(); + void fileCallbackImageNotFound(); + + void utf8filenames(); + void escapedStrings(); + void encodedUris(); + + void versionSupported(); + void versionUnsupported(); + + /* Needs to load AnyImageImporter from system-wide location */ + PluginManager::Manager _manager; +}; + +/* The external-data.* files are packed in via a resource, filename mapping + done in resources.conf */ + +using namespace Containers::Literals; +using namespace Magnum::Math::Literals; + + +constexpr struct { + const char* name; + const Containers::StringView data; + const char* message; +} OpenErrorData[]{ + {"short ascii", "?"_s, "data too short"}, + {"short binary", "glTF?"_s, "data too short"}, + {"short binary chunk", "glTF\x02\x00\x00\x00\x66\x00\x00\x00"_s, "data too short"}, + {"unknown binary version", "glTF\x10\x00\x00\x00\x0c\x00\x00\x00"_s, "unknown binary glTF format"}, + {"unknown binary JSON version", "glTF\x02\x00\x00\x00\x16\x00\x00\x00\x02\x00\x00\x00JSUN{}"_s, "unknown binary glTF format"}, + {"unknown binary GLB version", "glTF\x02\x00\x00\x00\x22\x00\x00\x00\x02\x00\x00\x00JSON{}\x04\x00\x00\0BIB\x00\xff\xff\xff\xff"_s, "unknown binary glTF format"}, + {"invalid JSON ascii", "{\"asset\":{\"version\":\"2.0\"}"_s, "invalid JSON"}, + {"invalid JSON binary", "glTF\x02\x00\x00\x00\x16\x00\x00\x00\x02\x00\x00\x00JSON{{"_s, "invalid JSON"} +}; + +constexpr struct { + const char* name; + const char* suffix; +} SingleFileData[]{ + {"ascii", ".gltf"}, + {"binary", ".glb"} +}; + +constexpr struct { + const char* name; + const char* suffix; +} MultiFileData[]{ + {"ascii external", ".gltf"}, + {"ascii embedded", "-embedded.gltf"}, + {"binary external", ".glb"}, + {"binary embedded", "-embedded.glb"} +}; + +constexpr struct { + const char* name; + const char* message; +} InvalidUriData[]{ + {"no payload", "data URI has no base64 payload"}, + {"no base64", "data URI has no base64 payload"}, + {"empty base64", "data URI has no base64 payload"}, + {"invalid base64", "invalid base64 string in data URI"} +}; + +constexpr struct { + const char* name; + const char* file; +} AnimationOutOfBoundsData[]{ + {"sampler index out of bounds", "animation-invalid-sampler-oob.gltf"}, + {"node index out of bounds", "animation-invalid-node-oob.gltf"}, + {"sampler input accessor index out of bounds", "animation-invalid-input-accessor-oob.gltf"}, + {"sampler output accessor index out of bounds", "animation-invalid-output-accessor-oob.gltf"} +}; + +constexpr struct { + const char* name; + const char* message; +} AnimationInvalidData[]{ + {"unexpected time type", "time track has unexpected type VEC4 / FLOAT (5126)"}, + {"unexpected translation type", "translation track has unexpected type VEC4 / FLOAT (5126)"}, + {"unexpected rotation type", "rotation track has unexpected type SCALAR / FLOAT (5126)"}, + {"unexpected scaling type", "scaling track has unexpected type VEC4 / FLOAT (5126)"}, + {"unsupported path", "unsupported track target 0"}, + {"invalid input accessor", "accessor 3 needs 40 bytes but buffer view 0 has only 0"}, + {"invalid output accessor", "accessor 4 needs 120 bytes but buffer view 0 has only 0"} +}; + +constexpr struct { + const char* name; + const char* message; +} AnimationInvalidTypesData[]{ + {"unknown type", "rotation track has unexpected type UNKNOWN / UNSIGNED_BYTE (5121)"}, + {"unknown component type", "time track has unexpected type MAT2 / UNKNOWN"}, + {"normalized float", "scaling track has unexpected type normalized VEC3 / FLOAT (5126)"} +}; + +constexpr struct { + const char* name; + const char* message; +} AnimationInvalidBufferNotFoundData[]{ + {"input buffer not found", "error opening file: /nonexistent1.bin : file not found"}, + {"output buffer not found", "error opening file: /nonexistent2.bin : file not found"} +}; + +constexpr struct { + const char* name; + const char* message; +} LightInvalidData[]{ + {"unknown type", "invalid light type"}, + {"directional with range", "range can't be defined for a directional light"}, + {"spot with too small inner angle", "inner and outer cone angle Deg(-0.572958) and Deg(45) out of allowed bounds"}, + /* These are kinda silly (not sure why we should limit to 90° and why inner + can't be the same as outer), but let's follow the spec */ + {"spot with too large outer angle", "inner and outer cone angle Deg(0) and Deg(90.5273) out of allowed bounds"}, + {"spot with inner angle same as outer", "inner and outer cone angle Deg(14.3239) and Deg(14.3239) out of allowed bounds"} +}; + +constexpr struct { + const char* name; + const char* file; +} SkinOutOfBoundsData[]{ + {"joint out of bounds", "skin-invalid-joint-oob.gltf"}, + {"accessor out of bounds", "skin-invalid-accessor-oob.gltf"} +}; + +constexpr struct { + const char* name; + const char* message; +} SkinInvalidData[]{ + {"no joints", "skin has no joints"}, + {"wrong accessor type", "inverse bind matrices have unexpected type MAT3 / FLOAT (5126)"}, + {"wrong accessor component type", "inverse bind matrices have unexpected type MAT4 / UNSIGNED_SHORT (5123)"}, + {"wrong accessor count", "invalid inverse bind matrix count, expected 2 but got 3"}, + {"invalid accessor", "accessor 3 needs 196 bytes but buffer view 0 has only 192"} +}; + +constexpr struct { + const char* name; + const char* message; +} SkinInvalidTypesData[]{ + {"unknown type", "inverse bind matrices have unexpected type UNKNOWN / FLOAT (5126)"}, + {"unknown component type", "inverse bind matrices have unexpected type MAT4 / UNKNOWN"}, + {"normalized float", "inverse bind matrices have unexpected type normalized MAT4 / FLOAT (5126)"} +}; + +constexpr struct { + const char* name; + MeshPrimitive primitive; + MeshIndexType indexType; + VertexFormat positionFormat; + VertexFormat normalFormat, tangentFormat; + VertexFormat colorFormat; + VertexFormat textureCoordinateFormat, objectIdFormat; + const char* objectIdAttribute; +} MeshPrimitivesTypesData[]{ + {"positions byte, color4 unsigned short, texcoords normalized unsigned byte; triangle strip", + MeshPrimitive::TriangleStrip, MeshIndexType{}, + VertexFormat::Vector3b, + VertexFormat{}, VertexFormat{}, + VertexFormat::Vector4usNormalized, + VertexFormat::Vector2ubNormalized, VertexFormat{}, nullptr}, + {"positions short, colors unsigned byte, texcoords normalized unsigned short; lines", + MeshPrimitive::Lines, MeshIndexType{}, + VertexFormat::Vector3s, + VertexFormat{}, VertexFormat{}, + VertexFormat::Vector3ubNormalized, + VertexFormat::Vector2usNormalized, VertexFormat{}, nullptr}, + {"positions unsigned byte, normals byte, texcoords short; indices unsigned int; line loop", + MeshPrimitive::LineLoop, MeshIndexType::UnsignedInt, + VertexFormat::Vector3ub, + VertexFormat::Vector3bNormalized, VertexFormat{}, + VertexFormat{}, + VertexFormat::Vector2s, VertexFormat{}, nullptr}, + {"positions unsigned short, normals short, texcoords byte; indices unsigned byte; triangle fan", + MeshPrimitive::TriangleFan, MeshIndexType::UnsignedByte, + VertexFormat::Vector3us, + VertexFormat::Vector3sNormalized, VertexFormat{}, + VertexFormat{}, + VertexFormat::Vector2b, VertexFormat{}, nullptr}, + {"positions normalized unsigned byte, tangents short, texcoords normalized short; indices unsigned short; line strip", + MeshPrimitive::LineStrip, MeshIndexType::UnsignedShort, + VertexFormat::Vector3ubNormalized, + VertexFormat{}, VertexFormat::Vector4sNormalized, + VertexFormat{}, + VertexFormat::Vector2sNormalized, VertexFormat{}, nullptr}, + {"positions normalized short, texcoords unsigned byte, tangents byte; triangles", + MeshPrimitive::Triangles, MeshIndexType{}, + VertexFormat::Vector3sNormalized, + VertexFormat{}, VertexFormat::Vector4bNormalized, + VertexFormat{}, + VertexFormat::Vector2ub, VertexFormat{}, nullptr}, + {"positions normalized unsigned short, texcoords normalized byte, objectid unsigned short", + MeshPrimitive::Triangles, MeshIndexType{}, + VertexFormat::Vector3usNormalized, + VertexFormat{}, VertexFormat{}, + VertexFormat{}, + VertexFormat::Vector2bNormalized, VertexFormat::UnsignedShort, nullptr}, + {"positions normalized byte, texcoords unsigned short, objectid unsigned byte", + MeshPrimitive::Triangles, MeshIndexType{}, + VertexFormat::Vector3bNormalized, + VertexFormat{}, VertexFormat{}, + VertexFormat{}, + VertexFormat::Vector2us, VertexFormat::UnsignedByte, "OBJECTID"} +}; + +constexpr struct { + const char* name; + const char* file; +} MeshOutOfBoundsData[]{ + {"buffer index out of bounds", "mesh-invalid-buffer-oob.gltf"}, + {"buffer view index out of bounds", "mesh-invalid-bufferview-oob.gltf"}, + {"accessor index out of bounds", "mesh-invalid-accessor-oob.gltf"}, + {"mesh index accessor out of bounds", "mesh-index-accessor-oob.gltf"} +}; + +constexpr struct { + const char* name; + const char* message; +} MeshInvalidData[]{ + {"invalid primitive", "unrecognized primitive 666"}, + {"different vertex count for each accessor", "mismatched vertex count for attribute TEXCOORD, expected 3 but got 4"}, + {"unexpected position type", "unexpected POSITION type VEC2"}, + {"unsupported position component type", "unsupported POSITION component type unnormalized UNSIGNED_INT (5125)"}, + {"unexpected normal type", "unexpected NORMAL type VEC2"}, + {"unsupported normal component type", "unsupported NORMAL component type unnormalized UNSIGNED_INT (5125)"}, + {"unexpected tangent type", "unexpected TANGENT type VEC3"}, + {"unsupported tangent component type", "unsupported TANGENT component type unnormalized BYTE (5120)"}, + {"unexpected texcoord type", "unexpected TEXCOORD type VEC3"}, + {"unsupported texcoord component type", "unsupported TEXCOORD component type unnormalized UNSIGNED_INT (5125)"}, + {"unexpected color type", "unexpected COLOR type VEC2"}, + {"unsupported color component type", "unsupported COLOR component type unnormalized BYTE (5120)"}, + {"unexpected joints type", "unexpected JOINTS type VEC3"}, + {"unsupported joints component type", "unsupported JOINTS component type unnormalized BYTE (5120)"}, + {"unexpected weights type", "unexpected WEIGHTS type SCALAR"}, + {"unsupported weights component type", "unsupported WEIGHTS component type unnormalized BYTE (5120)"}, + {"unexpected object id type", "unexpected object ID type VEC2"}, + {"unsupported object id component type", "unsupported object ID component type unnormalized SHORT (5122)"}, + {"unexpected index type", "unexpected index type VEC2"}, + {"unsupported index component type", "unexpected index component type SHORT (5122)"}, + {"normalized index type", "index type can't be normalized"}, + {"strided index view", "index buffer view is not contiguous"}, + {"accessor type size larger than buffer stride", "16-byte type defined by accessor 10 can't fit into buffer view 0 stride of 12"}, + {"normalized float", "attribute _THING component type FLOAT (5126) can't be normalized"}, + {"normalized int", "attribute _THING component type UNSIGNED_INT (5125) can't be normalized"}, + {"non-normalized byte matrix", "attribute _THING has an unsupported matrix component type unnormalized BYTE (5120)"}, + {"sparse accessor", "accessor 14 is using sparse storage, which is unsupported"}, + {"no bufferview", "accessor 15 has no buffer view"}, + {"accessor range out of bounds", "accessor 18 needs 48 bytes but buffer view 0 has only 36"}, + {"buffer view range out of bounds", "buffer view 3 needs 164 bytes but buffer 1 has only 160"}, + {"multiple buffers", "meshes spanning multiple buffers are not supported"}, + {"invalid index accessor", "accessor 17 needs 40 bytes but buffer view 0 has only 36"} +}; + +constexpr struct { + const char* name; + const char* message; +} MeshInvalidTypesData[]{ + {"unknown type", "attribute _THING has an invalid type"}, + {"unknown component type", "attribute _THING has an invalid component type"} +}; + +constexpr struct { + const char* name; + const char* file; +} MaterialOutOfBoundsData[]{ + {"invalid texture index pbrMetallicRoughness base color", "material-invalid-pbr-base-color-oob.gltf"}, + {"invalid texture index pbrMetallicRoughness metallic/roughness", "material-invalid-pbr-metallic-roughness-oob.gltf"}, + {"invalid texture index pbrSpecularGlossiness diffuse", "material-invalid-pbr-diffuse-oob.gltf"}, + {"invalid texture index pbrSpecularGlossiness specular", "material-invalid-pbr-specular-oob.gltf"}, + {"invalid texture index normal", "material-invalid-normal-oob.gltf"}, + {"invalid texture index occlusion", "material-invalid-occlusion-oob.gltf"}, + {"invalid texture index emissive", "material-invalid-emissive-oob.gltf"}, + {"invalid texture index clearcoat factor", "material-invalid-clearcoat-factor-oob.gltf"}, + {"invalid texture index clearcoat roughness", "material-invalid-clearcoat-roughness-oob.gltf"}, + {"invalid texture index clearcoat normal", "material-invalid-clearcoat-normal-oob.gltf"} +}; + +constexpr struct { + const char* name; + const char* file; +} SceneOutOfBoundsData[]{ + {"camera out of bounds", "scene-invalid-camera-oob.gltf"}, + {"child out of bounds", "scene-invalid-child-oob.gltf"}, + {"material out of bounds", "scene-invalid-material-oob.gltf"}, + {"material in a multi-primitive mesh out of bounds", "scene-invalid-material-oob-multi-primitive.gltf"}, + {"skin out of bounds", "scene-invalid-skin-oob.gltf"}, + {"skin for a multi-primitive mesh out of bounds", "scene-invalid-skin-oob-multi-primitive.gltf"}, + {"light out of bounds", "scene-invalid-light-oob.gltf"}, + {"default scene out of bounds", "scene-invalid-default-oob.gltf"}, + {"node out of bounds", "scene-invalid-node-oob.gltf"} +}; + +constexpr struct { + const char* name; + const char* file; +} SceneInvalidData[]{ + {"scene node has parent", "scene-invalid-child-not-root.gltf"}, + {"node has multiple parents", "scene-invalid-multiple-parents.gltf"} +}; + +constexpr struct { + const char* name; + const char* file; +} SceneCycleData[]{ + {"child is self", "scene-cycle.gltf"}, + {"great-grandchild is self", "scene-cycle-deep.gltf"} +}; + +constexpr struct { + const char* name; + const char* fileName; + const char* meshName; + bool flipInMaterial; + bool hasTextureTransformation; +} MaterialTexCoordFlipData[]{ + {"no transform", + "material-texcoord-flip.gltf", "float", false, false}, + {"no transform", + "material-texcoord-flip.gltf", "float", true, false}, + {"identity transform", + "material-texcoord-flip.gltf", "float", false, true}, + {"identity transform", + "material-texcoord-flip.gltf", "float", true, true}, + {"transform from normalized unsigned byte", + "material-texcoord-flip.gltf", + "normalized unsigned byte", false, true}, + {"transform from normalized unsigned byte", + "material-texcoord-flip.gltf", + "normalized unsigned byte", true, true}, + {"transform from normalized unsigned short", + "material-texcoord-flip.gltf", + "normalized unsigned short", false, true}, + {"transform from normalized unsigned short", + "material-texcoord-flip.gltf", + "normalized unsigned short", true, true}, + {"transform from normalized signed integer", + "material-texcoord-flip-unnormalized.gltf", + "normalized signed integer", false, true}, + {"transform from normalized signed integer", + "material-texcoord-flip-unnormalized.gltf", + "normalized signed integer", true, true}, + {"transform from signed integer", + "material-texcoord-flip-unnormalized.gltf", + "signed integer", false, true}, + {"transform from signed integer", + "material-texcoord-flip-unnormalized.gltf", + "signed integer", true, true}, +}; + +constexpr struct { + const char* name; + const char* file; +} TextureOutOfBoundsData[]{ + {"image out of bounds", "texture-invalid-image-oob.gltf"}, + {"sampler out of bounds", "texture-invalid-sampler-oob.gltf"} +}; + +constexpr struct { + const char* name; + const char* message; +} TextureInvalidData[]{ + {"invalid sampler minFilter", "invalid minFilter 1"}, + {"invalid sampler magFilter", "invalid magFilter 2"}, + {"invalid sampler wrapS", "invalid wrap mode 3"}, + {"invalid sampler wrapT", "invalid wrap mode 4"} +}; + +constexpr struct { + const char* name; + const UnsignedInt id; +} TextureExtensionsData[]{ + {"GOOGLE_texture_basis", 1}, + {"KHR_texture_basisu", 2}, + {"MSFT_texture_dds", 3}, + /* declaration order decides preference */ + {"MSFT_texture_dds and GOOGLE_texture_basis", 3}, + /* KHR_texture_basisu has preference before all other extensions */ + {"GOOGLE_texture_basis and KHR_texture_basisu", 2}, + {"unknown extension", 0}, + {"GOOGLE_texture_basis and unknown", 1} +}; + +constexpr struct { + const char* name; + const char* message; +} TextureExtensionsInvalidData[]{ + {"out of bounds GOOGLE_texture_basis", "GOOGLE_texture_basis image 3 out of bounds for 3 images"}, + {"unknown extension, no fallback", "no image source found"} +}; + +constexpr struct { + const char* name; + const char* suffix; +} ImageEmbeddedData[]{ + {"ascii", "-embedded.gltf"}, + {"ascii buffer", "-buffer-embedded.gltf"}, + {"binary", "-embedded.glb"}, + {"binary buffer", "-buffer-embedded.glb"} +}; + +constexpr struct { + const char* name; + const char* suffix; +} ImageExternalData[]{ + {"ascii", ".gltf"}, + {"ascii buffer", "-buffer.gltf"}, + {"binary", ".glb"}, + {"binary buffer", "-buffer.glb"}, +}; + +constexpr struct { + const char* name; + const char* suffix; +} ImageBasisData[]{ + {"ascii", ".gltf"}, + {"binary", ".glb"}, + {"embedded ascii", "-embedded.gltf"}, + {"embedded binary", "-embedded.glb"}, +}; + +constexpr struct { + const char* name; + const char* file; + const char* message; +} UnsupportedVersionData[]{ + {"legacy major version", "version-legacy.gltf", "error opening file: legacy glTF version"}, + {"unknown major version", "version-unsupported.gltf", "unsupported version 3.0, expected 2.x"}, + {"unknown minor version", "version-unsupported-min.gltf", "unsupported minVersion 2.1, expected 2.0"} +}; + +CgltfImporterTest::CgltfImporterTest() { + addInstancedTests({&CgltfImporterTest::open}, + Containers::arraySize(SingleFileData)); + + addInstancedTests({&CgltfImporterTest::openError}, + Containers::arraySize(OpenErrorData)); + + addInstancedTests({&CgltfImporterTest::openExternalDataOrder, + &CgltfImporterTest::openExternalDataNotFound, + &CgltfImporterTest::openExternalDataNoPathNoCallback, + &CgltfImporterTest::openExternalDataTooLong}, + Containers::arraySize(SingleFileData)); + + addInstancedTests({&CgltfImporterTest::openExternalDataTooShort}, + Containers::arraySize(MultiFileData)); + + addInstancedTests({&CgltfImporterTest::openExternalDataNoUri}, + Containers::arraySize(SingleFileData)); + + addInstancedTests({&CgltfImporterTest::openExternalDataInvalidUri}, + Containers::arraySize(InvalidUriData)); + + addTests({&CgltfImporterTest::requiredExtensions, + &CgltfImporterTest::requiredExtensionsUnsupported, + &CgltfImporterTest::requiredExtensionsUnsupportedDisabled}); + + addInstancedTests({&CgltfImporterTest::animation}, + Containers::arraySize(MultiFileData)); + + addInstancedTests({&CgltfImporterTest::animationOutOfBounds}, + Containers::arraySize(AnimationOutOfBoundsData)); + + addInstancedTests({&CgltfImporterTest::animationInvalid}, + Containers::arraySize(AnimationInvalidData)); + + addInstancedTests({&CgltfImporterTest::animationInvalidBufferNotFound}, + Containers::arraySize(AnimationInvalidBufferNotFoundData)); + + addTests({&CgltfImporterTest::animationInvalidInterpolation}); + + addInstancedTests({&CgltfImporterTest::animationInvalidTypes}, + Containers::arraySize(AnimationInvalidTypesData)); + + addTests({&CgltfImporterTest::animationMismatchingCount, + &CgltfImporterTest::animationMissingTargetNode}); + + addInstancedTests({&CgltfImporterTest::animationSpline}, + Containers::arraySize(MultiFileData)); + + addTests({&CgltfImporterTest::animationSplineSharedWithSameTimeTrack, + &CgltfImporterTest::animationSplineSharedWithDifferentTimeTrack, + + &CgltfImporterTest::animationShortestPathOptimizationEnabled, + &CgltfImporterTest::animationShortestPathOptimizationDisabled, + &CgltfImporterTest::animationQuaternionNormalizationEnabled, + &CgltfImporterTest::animationQuaternionNormalizationDisabled, + &CgltfImporterTest::animationMergeEmpty, + &CgltfImporterTest::animationMerge}); + + addInstancedTests({&CgltfImporterTest::camera}, + Containers::arraySize(SingleFileData)); + + addTests({&CgltfImporterTest::cameraInvalidType}); + + addInstancedTests({&CgltfImporterTest::light}, + Containers::arraySize(SingleFileData)); + + addInstancedTests({&CgltfImporterTest::lightInvalid}, + Containers::arraySize(LightInvalidData)); + + addTests({&CgltfImporterTest::lightInvalidColorSize, + &CgltfImporterTest::lightMissingType, + &CgltfImporterTest::lightMissingSpot}); + + addInstancedTests({&CgltfImporterTest::scene, + &CgltfImporterTest::sceneEmpty, + &CgltfImporterTest::sceneNoDefault}, + Containers::arraySize(SingleFileData)); + + addInstancedTests({&CgltfImporterTest::sceneOutOfBounds}, + Containers::arraySize(SceneOutOfBoundsData)); + + addInstancedTests({&CgltfImporterTest::sceneInvalid}, + Containers::arraySize(SceneInvalidData)); + + addInstancedTests({&CgltfImporterTest::sceneCycle}, + Containers::arraySize(SceneCycleData)); + + addInstancedTests({&CgltfImporterTest::objectTransformation}, + Containers::arraySize(SingleFileData)); + + addTests({&CgltfImporterTest::objectTransformationQuaternionNormalizationEnabled, + &CgltfImporterTest::objectTransformationQuaternionNormalizationDisabled}); + + addInstancedTests({&CgltfImporterTest::skin}, + Containers::arraySize(MultiFileData)); + + addInstancedTests({&CgltfImporterTest::skinInvalid}, + Containers::arraySize(SkinInvalidData)); + + addTests({&CgltfImporterTest::skinInvalidBufferNotFound}); + + addInstancedTests({&CgltfImporterTest::skinInvalidTypes}, + Containers::arraySize(SkinInvalidTypesData)); + + addInstancedTests({&CgltfImporterTest::skinOutOfBounds}, + Containers::arraySize(SkinOutOfBoundsData)); + + addTests({&CgltfImporterTest::skinNoJointsProperty}); + + addInstancedTests({&CgltfImporterTest::mesh}, + Containers::arraySize(MultiFileData)); + + addTests({&CgltfImporterTest::meshAttributeless, + &CgltfImporterTest::meshIndexed, + &CgltfImporterTest::meshIndexedAttributeless, + &CgltfImporterTest::meshColors, + &CgltfImporterTest::meshSkinAttributes, + &CgltfImporterTest::meshCustomAttributes, + &CgltfImporterTest::meshCustomAttributesNoFileOpened, + &CgltfImporterTest::meshDuplicateAttributes, + &CgltfImporterTest::meshUnorderedAttributes, + &CgltfImporterTest::meshMultiplePrimitives}); + + addInstancedTests({&CgltfImporterTest::meshPrimitivesTypes}, + Containers::arraySize(MeshPrimitivesTypesData)); + + addInstancedTests({&CgltfImporterTest::meshOutOfBounds}, + Containers::arraySize(MeshOutOfBoundsData)); + + addInstancedTests({&CgltfImporterTest::meshInvalid}, + Containers::arraySize(MeshInvalidData)); + + addTests({&CgltfImporterTest::meshInvalidIndicesBufferNotFound}); + + addInstancedTests({&CgltfImporterTest::meshInvalidTypes}, + Containers::arraySize(MeshInvalidTypesData)); + + addTests({&CgltfImporterTest::materialPbrMetallicRoughness, + &CgltfImporterTest::materialPbrSpecularGlossiness, + &CgltfImporterTest::materialCommon, + &CgltfImporterTest::materialUnlit, + &CgltfImporterTest::materialClearCoat, + &CgltfImporterTest::materialPhongFallback}); + + addInstancedTests({&CgltfImporterTest::materialOutOfBounds}, + Containers::arraySize(MaterialOutOfBoundsData)); + + addTests({&CgltfImporterTest::materialInvalidAlphaMode}); + + addInstancedTests({&CgltfImporterTest::materialTexCoordFlip}, + Containers::arraySize(MaterialTexCoordFlipData)); + + addInstancedTests({&CgltfImporterTest::texture}, + Containers::arraySize(SingleFileData)); + + addInstancedTests({&CgltfImporterTest::textureInvalid}, + Containers::arraySize(TextureInvalidData)); + + addInstancedTests({&CgltfImporterTest::textureDefaultSampler, + &CgltfImporterTest::textureEmptySampler}, + Containers::arraySize(SingleFileData)); + + addTests({&CgltfImporterTest::textureMissingSource}); + + addInstancedTests({&CgltfImporterTest::textureExtensions}, + Containers::arraySize(TextureExtensionsData)); + + addTests({&CgltfImporterTest::textureExtensionsOutOfBounds}); + + addInstancedTests({&CgltfImporterTest::textureExtensionsInvalid}, + Containers::arraySize(TextureExtensionsInvalidData)); + + addInstancedTests({&CgltfImporterTest::imageEmbedded}, + Containers::arraySize(ImageEmbeddedData)); + + addInstancedTests({&CgltfImporterTest::imageExternal}, + Containers::arraySize(ImageExternalData)); + + addTests({&CgltfImporterTest::imageExternalNotFound, + &CgltfImporterTest::imageExternalBufferNotFound, + &CgltfImporterTest::imageExternalNoPathNoCallback, + &CgltfImporterTest::imageNoData}); + + addInstancedTests({&CgltfImporterTest::imageBasis}, + Containers::arraySize(ImageBasisData)); + + addTests({&CgltfImporterTest::imageMipLevels}); + + addInstancedTests({&CgltfImporterTest::fileCallbackBuffer, + &CgltfImporterTest::fileCallbackBufferNotFound, + &CgltfImporterTest::fileCallbackImage, + &CgltfImporterTest::fileCallbackImageNotFound}, + Containers::arraySize(SingleFileData)); + + addTests({&CgltfImporterTest::utf8filenames, + &CgltfImporterTest::escapedStrings, + &CgltfImporterTest::encodedUris, + + &CgltfImporterTest::versionSupported}); + + addInstancedTests({&CgltfImporterTest::versionUnsupported}, + Containers::arraySize(UnsupportedVersionData)); + + /* Load the plugin directly from the build tree. Otherwise it's static and + already loaded. It also pulls in the AnyImageImporter dependency. Reset + the plugin dir after so it doesn't load anything else from the filesystem. */ + #ifdef CGLTFIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(CGLTFIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + _manager.setPluginDirectory({}); + #endif + #ifdef BASISIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(BASISIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif + #ifdef STBIMAGEIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(STBIMAGEIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif +} + +void CgltfImporterTest::open() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + auto filename = Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "empty" + std::string{data.suffix}); + CORRADE_VERIFY(importer->openFile(filename)); + CORRADE_VERIFY(importer->isOpened()); + CORRADE_VERIFY(!importer->importerState()); + + CORRADE_VERIFY(importer->openData(Utility::Directory::read(filename))); + CORRADE_VERIFY(importer->isOpened()); + CORRADE_VERIFY(!importer->importerState()); + + importer->close(); + CORRADE_VERIFY(!importer->isOpened()); +} + +void CgltfImporterTest::openError() { + auto&& data = OpenErrorData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + std::ostringstream out; + Error redirectError{&out}; + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(!importer->openData(data.data)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::openData(): error opening file: {}\n", data.message)); +} + +void CgltfImporterTest::openExternalDataOrder() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test"); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->features() & ImporterFeature::FileCallback); + + struct CallbackData { + Containers::StaticArray<3, std::size_t> counts{ValueInit}; + Containers::StaticArray<3, InputFileCallbackPolicy> policies{ValueInit}; + Containers::StaticArray<3, bool> closed{ValueInit}; + Utility::Resource rs{"data"}; + } callbackData{}; + + importer->setFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, CallbackData& callbackData) + -> Containers::Optional> + { + std::size_t index = 0; + if(filename.find("data1.bin") == 0) + index = 0; + else if(filename.find("data2.bin") == 0) + index = 1; + else if(filename.find("data.png") == 0) + index = 2; + + if(policy == InputFileCallbackPolicy::Close) + callbackData.closed[index] = true; + else { + callbackData.closed[index] = false; + callbackData.policies[index] = policy; + } + ++callbackData.counts[index]; + + return callbackData.rs.getRaw(Utility::Directory::join("some/path", filename)); + }, callbackData); + + /* Prevent the file callback being used for the main glTF content */ + const auto content = Utility::Directory::read(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "external-data-order" + std::string{data.suffix})); + CORRADE_VERIFY(importer->openData(content)); + + CORRADE_COMPARE(importer->meshCount(), 4); + CORRADE_COMPARE(importer->image2DCount(), 2); + + /* Buffers and images are only loaded on demand */ + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({0, 0, 0}), TestSuite::Compare::Container); + + CORRADE_VERIFY(importer->mesh(0)); + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({1, 0, 0}), TestSuite::Compare::Container); + CORRADE_COMPARE(callbackData.policies[0], InputFileCallbackPolicy::LoadPermanent); + + CORRADE_VERIFY(importer->mesh(1)); + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({1, 1, 0}), TestSuite::Compare::Container); + CORRADE_COMPARE(callbackData.policies[1], InputFileCallbackPolicy::LoadPermanent); + + /* Buffer content is cached. An already loaded buffer should not invoke the + file callback again. */ + + /* Mesh already loaded */ + CORRADE_VERIFY(importer->mesh(0)); + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({1, 1, 0}), TestSuite::Compare::Container); + /* Different mesh, same buffer as mesh 0 */ + CORRADE_VERIFY(importer->mesh(2)); + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({1, 1, 0}), TestSuite::Compare::Container); + /* Different mesh, different buffer, but same URI. The caching does not + use URI, only buffer id. */ + CORRADE_VERIFY(importer->mesh(3)); + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({2, 1, 0}), TestSuite::Compare::Container); + CORRADE_COMPARE(callbackData.policies[0], InputFileCallbackPolicy::LoadPermanent); + + /* Image content is not cached. Requesting the same image later should + result in two callback invocations. However, the image importer is + cached, so the file callback is only called again if we load a different + image in between. */ + CORRADE_VERIFY(importer->image2D(0)); + /* Count increases by 2 because file callback is invoked with LoadTemporary + followed by Close */ + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({2, 1, 2}), TestSuite::Compare::Container); + CORRADE_COMPARE(callbackData.policies[2], InputFileCallbackPolicy::LoadTemporary); + + /* Same importer */ + CORRADE_VERIFY(importer->image2D(0)); + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({2, 1, 2}), TestSuite::Compare::Container); + /* Same URI, but different image. Importer caching uses the image id, not + the URI. */ + CORRADE_VERIFY(importer->image2D(1)); + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({2, 1, 4}), TestSuite::Compare::Container); + CORRADE_VERIFY(importer->image2D(0)); + CORRADE_COMPARE_AS(callbackData.counts, Containers::arrayView({2, 1, 6}), TestSuite::Compare::Container); + + CORRADE_COMPARE_AS(callbackData.closed, Containers::arrayView({false, false, true}), TestSuite::Compare::Container); +} + +void CgltfImporterTest::openExternalDataNotFound() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + auto filename = Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "buffer-notfound" + std::string{data.suffix}); + + /* Importing should succeed, buffers are loaded on demand */ + CORRADE_VERIFY(importer->openFile(filename)); + CORRADE_COMPARE(importer->meshCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->mesh(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::mesh(): error opening file: /nonexistent.bin : file not found\n"); +} + +void CgltfImporterTest::openExternalDataNoPathNoCallback() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + auto filename = Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "buffer-notfound" + std::string{data.suffix}); + + CORRADE_VERIFY(importer->openData(Utility::Directory::read(filename))); + CORRADE_COMPARE(importer->meshCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->mesh(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::mesh(): external buffers can be imported only when opening files from the filesystem or if a file callback is present\n"); +} + +void CgltfImporterTest::openExternalDataTooLong() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "buffer-wrong-size" + std::string{data.suffix}))); + + CORRADE_COMPARE(importer->meshCount(), 1); + CORRADE_VERIFY(importer->mesh(0)); +} + +void CgltfImporterTest::openExternalDataTooShort() { + auto&& data = MultiFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "buffer-short-size" + std::string{data.suffix}))); + CORRADE_COMPARE(importer->meshCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->mesh(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::mesh(): buffer 0 is too short, expected 24 bytes but got 12\n"); +} + +void CgltfImporterTest::openExternalDataNoUri() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "buffer-no-uri" + std::string{data.suffix}))); + CORRADE_COMPARE(importer->meshCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->mesh(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::mesh(): buffer 1 has no URI\n"); +} + +void CgltfImporterTest::openExternalDataInvalidUri() { + auto&& data = InvalidUriData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "uri-invalid.gltf"))); + + /* Check we didn't forget to test anything */ + CORRADE_COMPARE(importer->image2DCount(), Containers::arraySize(InvalidUriData)); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->image2D(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::image2D(): {}\n", data.message)); +} + +void CgltfImporterTest::requiredExtensions() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "required-extensions.gltf"))); +} + +void CgltfImporterTest::requiredExtensionsUnsupported() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Disabled by default */ + CORRADE_VERIFY(!importer->configuration().value("ignoreRequiredExtensions")); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "required-extensions-unsupported.gltf"))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): required extension EXT_lights_image_based not supported\n"); +} + +void CgltfImporterTest::requiredExtensionsUnsupportedDisabled() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->configuration().setValue("ignoreRequiredExtensions", true)); + + std::ostringstream out; + Warning redirectError{&out}; + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "required-extensions-unsupported.gltf"))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): required extension EXT_lights_image_based not supported\n"); +} + +void CgltfImporterTest::animation() { + auto&& data = MultiFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "animation" + std::string{data.suffix}))); + + CORRADE_COMPARE(importer->animationCount(), 4); + + /* Empty animation */ + { + CORRADE_COMPARE(importer->animationName(0), "empty"); + CORRADE_COMPARE(importer->animationForName("empty"), 0); + + auto animation = importer->animation(0); + CORRADE_VERIFY(animation); + CORRADE_VERIFY(animation->data().empty()); + CORRADE_COMPARE(animation->trackCount(), 0); + + /* Empty translation/rotation/scaling animation */ + } { + CORRADE_COMPARE(importer->animationName(1), "empty TRS animation"); + CORRADE_COMPARE(importer->animationForName("empty TRS animation"), 1); + + auto animation = importer->animation(1); + CORRADE_VERIFY(animation); + CORRADE_VERIFY(!animation->importerState()); + + CORRADE_COMPARE(animation->data().size(), 0); + CORRADE_COMPARE(animation->trackCount(), 3); + + /* Not really checking much here, just making sure that this is handled + gracefully */ + + CORRADE_COMPARE(animation->trackTargetType(0), AnimationTrackTargetType::Rotation3D); + auto rotation = animation->track(0); + CORRADE_VERIFY(rotation.keys().empty()); + CORRADE_VERIFY(rotation.values().empty()); + + CORRADE_COMPARE(animation->trackTargetType(1), AnimationTrackTargetType::Translation3D); + auto translation = animation->track(1); + CORRADE_VERIFY(translation.keys().empty()); + CORRADE_VERIFY(translation.values().empty()); + + CORRADE_COMPARE(animation->trackTargetType(2), AnimationTrackTargetType::Scaling3D); + auto scaling = animation->track(2); + CORRADE_VERIFY(scaling.keys().empty()); + CORRADE_VERIFY(scaling.values().empty()); + + /* Translation/rotation/scaling animation */ + } { + CORRADE_COMPARE(importer->animationName(2), "TRS animation"); + CORRADE_COMPARE(importer->animationForName("TRS animation"), 2); + + auto animation = importer->animation(2); + CORRADE_VERIFY(animation); + CORRADE_VERIFY(!animation->importerState()); + /* Two rotation keys, four translation and scaling keys with common + time track */ + CORRADE_COMPARE(animation->data().size(), + 2*(sizeof(Float) + sizeof(Quaternion)) + + 4*(sizeof(Float) + 2*sizeof(Vector3))); + CORRADE_COMPARE(animation->trackCount(), 3); + + /* Rotation, linearly interpolated */ + CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion); + CORRADE_COMPARE(animation->trackResultType(0), AnimationTrackType::Quaternion); + CORRADE_COMPARE(animation->trackTargetType(0), AnimationTrackTargetType::Rotation3D); + CORRADE_COMPARE(animation->trackTarget(0), 0); + Animation::TrackView rotation = animation->track(0); + CORRADE_COMPARE(rotation.interpolation(), Animation::Interpolation::Linear); + CORRADE_COMPARE(rotation.before(), Animation::Extrapolation::Constant); + CORRADE_COMPARE(rotation.after(), Animation::Extrapolation::Constant); + const Float rotationKeys[]{ + 1.25f, + 2.50f + }; + const Quaternion rotationValues[]{ + Quaternion::rotation(0.0_degf, Vector3::xAxis()), + Quaternion::rotation(180.0_degf, Vector3::xAxis()) + }; + CORRADE_COMPARE_AS(rotation.keys(), Containers::stridedArrayView(rotationKeys), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(rotation.values(), Containers::stridedArrayView(rotationValues), TestSuite::Compare::Container); + CORRADE_COMPARE(rotation.at(1.875f), Quaternion::rotation(90.0_degf, Vector3::xAxis())); + + const Float translationScalingKeys[]{ + 0.0f, + 1.25f, + 2.5f, + 3.75f + }; + + /* Translation, constant interpolated, sharing keys with scaling */ + CORRADE_COMPARE(animation->trackType(1), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackResultType(1), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(1), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTarget(1), 1); + Animation::TrackView translation = animation->track(1); + CORRADE_COMPARE(translation.interpolation(), Animation::Interpolation::Constant); + CORRADE_COMPARE(translation.before(), Animation::Extrapolation::Constant); + CORRADE_COMPARE(translation.after(), Animation::Extrapolation::Constant); + const Vector3 translationData[]{ + Vector3::yAxis(0.0f), + Vector3::yAxis(2.5f), + Vector3::yAxis(2.5f), + Vector3::yAxis(0.0f) + }; + CORRADE_COMPARE_AS(translation.keys(), Containers::stridedArrayView(translationScalingKeys), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(translation.values(), Containers::stridedArrayView(translationData), TestSuite::Compare::Container); + CORRADE_COMPARE(translation.at(1.5f), Vector3::yAxis(2.5f)); + + /* Scaling, linearly interpolated, sharing keys with translation */ + CORRADE_COMPARE(animation->trackType(2), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackResultType(2), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(2), AnimationTrackTargetType::Scaling3D); + CORRADE_COMPARE(animation->trackTarget(2), 2); + Animation::TrackView scaling = animation->track(2); + CORRADE_COMPARE(scaling.interpolation(), Animation::Interpolation::Linear); + CORRADE_COMPARE(scaling.before(), Animation::Extrapolation::Constant); + CORRADE_COMPARE(scaling.after(), Animation::Extrapolation::Constant); + const Vector3 scalingData[]{ + Vector3{1.0f}, + Vector3::zScale(5.0f), + Vector3::zScale(6.0f), + Vector3(1.0f), + }; + CORRADE_COMPARE_AS(scaling.keys(), Containers::stridedArrayView(translationScalingKeys), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scaling.values(), Containers::stridedArrayView(scalingData), TestSuite::Compare::Container); + CORRADE_COMPARE(scaling.at(1.5f), Vector3::zScale(5.2f)); + } +} + +void CgltfImporterTest::animationOutOfBounds() { + auto&& data = AnimationOutOfBoundsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, data.file))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): error opening file: invalid glTF, usually caused by invalid indices or missing required attributes\n"); +} + +void CgltfImporterTest::animationInvalid() { + auto&& data = AnimationInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "animation-invalid.gltf"))); + + /* Check we didn't forget to test anything. We skip the invalid + interpolation mode because that imports without errors and defaults to + linear interpolation, tested in animationInvalidInterpolation(). */ + CORRADE_COMPARE(importer->animationCount(), Containers::arraySize(AnimationInvalidData) + 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->animation(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::animation(): {}\n", data.message)); +} + +void CgltfImporterTest::animationInvalidBufferNotFound() { + auto&& data = AnimationInvalidBufferNotFoundData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* These tests have to be separate from TinyGltfImporter because it errors + out during import trying to load the buffer */ + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "animation-buffer-notfound.gltf"))); + + /* Check we didn't forget to test anything */ + CORRADE_COMPARE(importer->animationCount(), Containers::arraySize(AnimationInvalidBufferNotFoundData)); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->animation(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::animation(): {}\n", data.message)); +} + +void CgltfImporterTest::animationInvalidInterpolation() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "animation-invalid.gltf"))); + + auto animation = importer->animation("unsupported interpolation type"); + { + CORRADE_EXPECT_FAIL("Cgltf parses an invalid interpolation mode as linear, without any error."); + CORRADE_VERIFY(!animation); + } + CORRADE_COMPARE(animation->trackCount(), 1); + auto track = animation->track(0); + CORRADE_COMPARE(track.interpolation(), Animation::Interpolation::Linear); +} + +void CgltfImporterTest::animationInvalidTypes() { + auto&& data = AnimationInvalidTypesData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "animation-invalid-types.gltf"))); + + /* Check we didn't forget to test anything */ + CORRADE_COMPARE(importer->animationCount(), Containers::arraySize(AnimationInvalidTypesData)); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->animation(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::animation(): {}\n", data.message)); +} + +void CgltfImporterTest::animationMismatchingCount() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Different input/output accessor counts are not allowed. This + TinyGltfImporter test file has them, so we repurpose it. */ + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "animation-patching.gltf"))); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->animation("Quaternion normalization patching")); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::animation(): target track size doesn't match time track size, expected 3 but got 9\n"); +} + +void CgltfImporterTest::animationMissingTargetNode() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "animation-missing-target-node.gltf"))); + CORRADE_COMPARE(importer->animationCount(), 1); + + /* The importer skips channels that don't have a target node */ + + auto animation = importer->animation(0); + CORRADE_VERIFY(animation); + CORRADE_COMPARE(animation->trackCount(), 2); + + CORRADE_COMPARE(animation->trackTargetType(0), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTarget(0), 1); + CORRADE_COMPARE(animation->trackTargetType(1), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTarget(1), 0); +} + +constexpr Float AnimationSplineTime1Keys[]{ 0.5f, 3.5f, 4.0f, 5.0f }; + +constexpr CubicHermite3D AnimationSplineTime1TranslationData[]{ + {{0.0f, 0.0f, 0.0f}, + {3.0f, 0.1f, 2.5f}, + {-1.0f, 0.0f, 0.3f}}, + {{5.0f, 0.3f, 1.1f}, + {-2.0f, 1.1f, -4.3f}, + {1.5f, 0.3f, 17.0f}}, + {{1.3f, 0.0f, 0.2f}, + {1.5f, 9.8f, -5.1f}, + {0.1f, 0.2f, -7.1f}}, + {{1.3f, 0.5f, 1.0f}, + {5.1f, 0.1f, -7.3f}, + {0.0f, 0.0f, 0.0f}} +}; + +void CgltfImporterTest::animationSpline() { + auto&& data = MultiFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "animation" + std::string{data.suffix}))); + CORRADE_COMPARE(importer->animationCount(), 4); + CORRADE_COMPARE(importer->animationName(3), "TRS animation, splines"); + + auto animation = importer->animation(3); + CORRADE_VERIFY(animation); + CORRADE_VERIFY(!animation->importerState()); + /* Four spline T/R/S keys with one common time track */ + CORRADE_COMPARE(animation->data().size(), + 4*(sizeof(Float) + 3*sizeof(Quaternion) + 2*3*sizeof(Vector3))); + CORRADE_COMPARE(animation->trackCount(), 3); + + /* Rotation */ + CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::CubicHermiteQuaternion); + CORRADE_COMPARE(animation->trackResultType(0), AnimationTrackType::Quaternion); + CORRADE_COMPARE(animation->trackTargetType(0), AnimationTrackTargetType::Rotation3D); + CORRADE_COMPARE(animation->trackTarget(0), 3); + Animation::TrackView rotation = animation->track(0); + CORRADE_COMPARE(rotation.interpolation(), Animation::Interpolation::Spline); + CORRADE_COMPARE(rotation.before(), Animation::Extrapolation::Constant); + CORRADE_COMPARE(rotation.after(), Animation::Extrapolation::Constant); + CORRADE_COMPARE_AS(rotation.keys(), Containers::stridedArrayView(AnimationSplineTime1Keys), TestSuite::Compare::Container); + constexpr CubicHermiteQuaternion rotationValues[]{ + {{{0.0f, 0.0f, 0.0f}, 0.0f}, + {{0.780076f, 0.0260025f, 0.598059f}, 0.182018f}, + {{-1.0f, 0.0f, 0.3f}, 0.4f}}, + {{{5.0f, 0.3f, 1.1f}, 0.5f}, + {{-0.711568f, 0.391362f, 0.355784f}, 0.462519f}, + {{1.5f, 0.3f, 17.0f}, -7.0f}}, + {{{1.3f, 0.0f, 0.2f}, 1.2f}, + {{0.598059f, 0.182018f, 0.0260025f}, 0.780076f}, + {{0.1f, 0.2f, -7.1f}, 1.7f}}, + {{{1.3f, 0.5f, 1.0f}, 0.0f}, + {{0.711568f, -0.355784f, -0.462519f}, -0.391362f}, + {{0.0f, 0.0f, 0.0f}, 0.0f}} + }; + CORRADE_COMPARE_AS(rotation.values(), Containers::stridedArrayView(rotationValues), TestSuite::Compare::Container); + /* The same as in CubicHermiteTest::splerpQuaternion() */ + CORRADE_COMPARE(rotation.at(0.5f + 0.35f*3), + (Quaternion{{-0.309862f, 0.174831f, 0.809747f}, 0.466615f})); + + /* Translation */ + CORRADE_COMPARE(animation->trackType(1), AnimationTrackType::CubicHermite3D); + CORRADE_COMPARE(animation->trackResultType(1), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(1), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTarget(1), 4); + Animation::TrackView translation = animation->track(1); + CORRADE_COMPARE(translation.interpolation(), Animation::Interpolation::Spline); + CORRADE_COMPARE(translation.before(), Animation::Extrapolation::Constant); + CORRADE_COMPARE(translation.after(), Animation::Extrapolation::Constant); + CORRADE_COMPARE_AS(translation.keys(), Containers::stridedArrayView(AnimationSplineTime1Keys), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(translation.values(), Containers::stridedArrayView(AnimationSplineTime1TranslationData), TestSuite::Compare::Container); + /* The same as in CubicHermiteTest::splerpVector() */ + CORRADE_COMPARE(translation.at(0.5f + 0.35f*3), + (Vector3{1.04525f, 0.357862f, 0.540875f})); + + /* Scaling */ + CORRADE_COMPARE(animation->trackType(2), AnimationTrackType::CubicHermite3D); + CORRADE_COMPARE(animation->trackResultType(2), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(2), AnimationTrackTargetType::Scaling3D); + CORRADE_COMPARE(animation->trackTarget(2), 5); + Animation::TrackView scaling = animation->track(2); + CORRADE_COMPARE(scaling.interpolation(), Animation::Interpolation::Spline); + CORRADE_COMPARE(scaling.before(), Animation::Extrapolation::Constant); + CORRADE_COMPARE(scaling.after(), Animation::Extrapolation::Constant); + CORRADE_COMPARE_AS(scaling.keys(), Containers::stridedArrayView(AnimationSplineTime1Keys), TestSuite::Compare::Container); + constexpr CubicHermite3D scalingData[]{ + {{0.0f, 0.0f, 0.0f}, + {-2.0f, 1.1f, -4.3f}, + {1.5f, 0.3f, 17.0f}}, + {{1.3f, 0.5f, 1.0f}, + {5.1f, 0.1f, -7.3f}, + {-1.0f, 0.0f, 0.3f}}, + {{0.1f, 0.2f, -7.1f}, + {3.0f, 0.1f, 2.5f}, + {5.0f, 0.3f, 1.1f}}, + {{1.3f, 0.0f, 0.2f}, + {1.5f, 9.8f, -5.1f}, + {0.0f, 0.0f, 0.0f}} + }; + CORRADE_COMPARE_AS(scaling.values(), Containers::stridedArrayView(scalingData), TestSuite::Compare::Container); + CORRADE_COMPARE(scaling.at(0.5f + 0.35f*3), + (Vector3{0.118725f, 0.8228f, -2.711f})); +} + +void CgltfImporterTest::animationSplineSharedWithSameTimeTrack() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "animation-splines-sharing.gltf"))); + CORRADE_COMPARE(importer->animationCount(), 2); + CORRADE_COMPARE(importer->animationName(0), "TRS animation, splines, sharing data with the same time track"); + + auto animation = importer->animation(0); + CORRADE_VERIFY(animation); + CORRADE_VERIFY(!animation->importerState()); + /* Four spline T keys with one common time track, used as S as well */ + CORRADE_COMPARE(animation->data().size(), + 4*(sizeof(Float) + 3*sizeof(Vector3))); + CORRADE_COMPARE(animation->trackCount(), 2); + + /* Translation using the translation track and the first time track */ + CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::CubicHermite3D); + CORRADE_COMPARE(animation->trackResultType(0), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(0), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTarget(0), 0); + Animation::TrackView translation = animation->track(1); + CORRADE_COMPARE(translation.interpolation(), Animation::Interpolation::Spline); + CORRADE_COMPARE(translation.before(), Animation::Extrapolation::Constant); + CORRADE_COMPARE(translation.after(), Animation::Extrapolation::Constant); + CORRADE_COMPARE_AS(translation.keys(), Containers::stridedArrayView(AnimationSplineTime1Keys), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(translation.values(), Containers::stridedArrayView(AnimationSplineTime1TranslationData), TestSuite::Compare::Container); + /* The same as in CubicHermiteTest::splerpVector() */ + CORRADE_COMPARE(translation.at(0.5f + 0.35f*3), + (Vector3{1.04525f, 0.357862f, 0.540875f})); + + /* Scaling also using the translation track and the first time track. Yes, + it's weird, but a viable test case verifying the same key/value data + pair used in two different tracks. The imported data should be + absolutely the same, not processed twice or anything. */ + CORRADE_COMPARE(animation->trackType(1), AnimationTrackType::CubicHermite3D); + CORRADE_COMPARE(animation->trackResultType(1), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(1), AnimationTrackTargetType::Scaling3D); + CORRADE_COMPARE(animation->trackTarget(1), 0); + Animation::TrackView scaling = animation->track(1); + CORRADE_COMPARE(scaling.interpolation(), Animation::Interpolation::Spline); + CORRADE_COMPARE(scaling.before(), Animation::Extrapolation::Constant); + CORRADE_COMPARE(scaling.after(), Animation::Extrapolation::Constant); + CORRADE_COMPARE_AS(scaling.keys(), Containers::stridedArrayView(AnimationSplineTime1Keys), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scaling.values(), Containers::stridedArrayView(AnimationSplineTime1TranslationData), TestSuite::Compare::Container); + /* The same as in CubicHermiteTest::splerpVector() */ + CORRADE_COMPARE(scaling.at(0.5f + 0.35f*3), + (Vector3{1.04525f, 0.357862f, 0.540875f})); +} + +void CgltfImporterTest::animationSplineSharedWithDifferentTimeTrack() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "animation-splines-sharing.gltf"))); + CORRADE_COMPARE(importer->animationCount(), 2); + CORRADE_COMPARE(importer->animationName(1), "TRS animation, splines, sharing data with different time track"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->animation(1)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::animation(): spline track is shared with different time tracks, we don't support that, sorry\n"); +} + +void CgltfImporterTest::animationShortestPathOptimizationEnabled() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Enabled by default */ + CORRADE_VERIFY(importer->configuration().value("optimizeQuaternionShortestPath")); + /* tinygltf allows animation samplers with different input and output sizes + and picks the smaller size, but cgltf complains about it, nor is it + allowed by the spec. So we need our own test file. */ + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "animation-patching-fixed.gltf"))); + + CORRADE_COMPARE(importer->animationCount(), 2); + CORRADE_COMPARE(importer->animationName(0), "Quaternion shortest-path patching"); + + auto animation = importer->animation(0); + CORRADE_VERIFY(animation); + CORRADE_COMPARE(animation->trackCount(), 1); + CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion); + Animation::TrackView track = animation->track(0); + const Quaternion rotationValues[]{ + {{0.0f, 0.0f, 0.92388f}, -0.382683f}, // 0 s: 225° + {{0.0f, 0.0f, 0.707107f}, -0.707107f}, // 1 s: 270° + {{0.0f, 0.0f, 0.382683f}, -0.92388f}, // 2 s: 315° + {{0.0f, 0.0f, 0.0f}, -1.0f}, // 3 s: 360° / 0° + {{0.0f, 0.0f, -0.382683f}, -0.92388f}, // 4 s: 45° (flipped) + {{0.0f, 0.0f, -0.707107f}, -0.707107f}, // 5 s: 90° (flipped) + {{0.0f, 0.0f, -0.92388f}, -0.382683f}, // 6 s: 135° (flipped back) + {{0.0f, 0.0f, -1.0f}, 0.0f}, // 7 s: 180° (flipped back) + {{0.0f, 0.0f, -0.92388f}, 0.382683f} // 8 s: 225° (flipped) + }; + CORRADE_COMPARE_AS(track.values(), Containers::stridedArrayView(rotationValues), TestSuite::Compare::Container); + + CORRADE_COMPARE(track.at(Math::slerp, 0.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 1.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 2.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 3.5f).axis(), -Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 4.5f).axis(), -Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 5.5f).axis(), -Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 6.5f).axis(), -Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 7.5f).axis(), -Vector3::zAxis()); + + /* Some are negated because of the flipped axis but other than that it's + nicely monotonic */ + CORRADE_COMPARE(track.at(Math::slerp, 0.5f).angle(), 247.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 1.5f).angle(), 292.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 2.5f).angle(), 337.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 3.5f).angle(), 360.0_degf - 22.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 4.5f).angle(), 360.0_degf - 67.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 5.5f).angle(), 360.0_degf - 112.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 6.5f).angle(), 360.0_degf - 157.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 7.5f).angle(), 360.0_degf - 202.5_degf); +} + +void CgltfImporterTest::animationShortestPathOptimizationDisabled() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Explicitly disable */ + importer->configuration().setValue("optimizeQuaternionShortestPath", false); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "animation-patching-fixed.gltf"))); + + CORRADE_COMPARE(importer->animationCount(), 2); + CORRADE_COMPARE(importer->animationName(0), "Quaternion shortest-path patching"); + + auto animation = importer->animation(0); + CORRADE_VERIFY(animation); + CORRADE_COMPARE(animation->trackCount(), 1); + CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion); + Animation::TrackView track = animation->track(0); + + /* Should be the same as in animation-patching.bin.in */ + const Quaternion rotationValues[]{ + {{0.0f, 0.0f, 0.92388f}, -0.382683f}, // 0 s: 225° + {{0.0f, 0.0f, 0.707107f}, -0.707107f}, // 1 s: 270° + {{0.0f, 0.0f, 0.382683f}, -0.92388f}, // 2 s: 315° + {{0.0f, 0.0f, 0.0f}, -1.0f}, // 3 s: 360° / 0° + {{0.0f, 0.0f, 0.382683f}, 0.92388f}, // 4 s: 45° (longer path) + {{0.0f, 0.0f, 0.707107f}, 0.707107f}, // 5 s: 90° + {{0.0f, 0.0f, -0.92388f}, -0.382683f}, // 6 s: 135° (longer path) + {{0.0f, 0.0f, -1.0f}, 0.0f}, // 7 s: 180° + {{0.0f, 0.0f, 0.92388f}, -0.382683f} // 8 s: 225° (longer path) + }; + CORRADE_COMPARE_AS(track.values(), Containers::stridedArrayView(rotationValues), TestSuite::Compare::Container); + + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 0.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 1.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 2.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 3.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 4.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 5.5f).axis(), -Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 6.5f).axis(), -Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 7.5f).axis(), Vector3::zAxis()); + + /* Some are negated because of the flipped axis but other than that it's + nicely monotonic because slerpShortestPath() ensures that */ + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 0.5f).angle(), 247.5_degf); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 1.5f).angle(), 292.5_degf); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 2.5f).angle(), 337.5_degf); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 3.5f).angle(), 22.5_degf); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 4.5f).angle(), 67.5_degf); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 5.5f).angle(), 360.0_degf - 112.5_degf); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 6.5f).angle(), 360.0_degf - 157.5_degf); + CORRADE_COMPARE(track.at(Math::slerpShortestPath, 7.5f).angle(), 202.5_degf); + + CORRADE_COMPARE(track.at(Math::slerp, 0.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 1.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 2.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 3.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 4.5f).axis(), Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 5.5f).axis(), -Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 6.5f).axis(), -Vector3::zAxis()); + CORRADE_COMPARE(track.at(Math::slerp, 7.5f).axis(), -Vector3::zAxis(1.00004f)); /* ?! */ + + /* Things are a complete chaos when using non-SP slerp */ + CORRADE_COMPARE(track.at(Math::slerp, 0.5f).angle(), 247.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 1.5f).angle(), 292.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 2.5f).angle(), 337.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 3.5f).angle(), 202.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 4.5f).angle(), 67.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 5.5f).angle(), 67.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 6.5f).angle(), 202.5_degf); + CORRADE_COMPARE(track.at(Math::slerp, 7.5f).angle(), 337.5_degf); +} + +void CgltfImporterTest::animationQuaternionNormalizationEnabled() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Enabled by default */ + CORRADE_VERIFY(importer->configuration().value("normalizeQuaternions")); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "animation-patching-fixed.gltf"))); + CORRADE_COMPARE(importer->animationCount(), 2); + CORRADE_COMPARE(importer->animationName(1), "Quaternion normalization patching"); + + Containers::Optional animation; + std::ostringstream out; + { + Warning warningRedirection{&out}; + animation = importer->animation(1); + } + CORRADE_VERIFY(animation); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::animation(): quaternions in some rotation tracks were renormalized\n"); + CORRADE_COMPARE(animation->trackCount(), 1); + CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion); + + Animation::TrackView track = animation->track(0); + const Quaternion rotationValues[]{ + {{0.0f, 0.0f, 0.382683f}, 0.92388f}, // is normalized + {{0.0f, 0.0f, 0.707107f}, 0.707107f}, // is not, renormalized + {{0.0f, 0.0f, 0.382683f}, 0.92388f}, // is not, renormalized + }; + CORRADE_COMPARE_AS(track.values(), Containers::stridedArrayView(rotationValues), TestSuite::Compare::Container); +} + +void CgltfImporterTest::animationQuaternionNormalizationDisabled() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Explicitly disable */ + CORRADE_VERIFY(importer->configuration().setValue("normalizeQuaternions", false)); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "animation-patching-fixed.gltf"))); + CORRADE_COMPARE(importer->animationCount(), 2); + CORRADE_COMPARE(importer->animationName(1), "Quaternion normalization patching"); + + auto animation = importer->animation(1); + CORRADE_VERIFY(animation); + CORRADE_COMPARE(animation->trackCount(), 1); + CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion); + + Animation::TrackView track = animation->track(0); + const Quaternion rotationValues[]{ + Quaternion{{0.0f, 0.0f, 0.382683f}, 0.92388f}, // is normalized + Quaternion{{0.0f, 0.0f, 0.707107f}, 0.707107f}*2, // is not + Quaternion{{0.0f, 0.0f, 0.382683f}, 0.92388f}*2, // is not + }; + CORRADE_COMPARE_AS(track.values(), Containers::stridedArrayView(rotationValues), TestSuite::Compare::Container); +} + +void CgltfImporterTest::animationMergeEmpty() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Enable animation merging */ + importer->configuration().setValue("mergeAnimationClips", true); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "empty.gltf"))); + + CORRADE_COMPARE(importer->animationCount(), 0); + CORRADE_COMPARE(importer->animationForName(""), -1); +} + +void CgltfImporterTest::animationMerge() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Enable animation merging */ + importer->configuration().setValue("mergeAnimationClips", true); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "animation.gltf"))); + + CORRADE_COMPARE(importer->animationCount(), 1); + CORRADE_COMPARE(importer->animationName(0), ""); + CORRADE_COMPARE(importer->animationForName(""), -1); + + auto animation = importer->animation(0); + CORRADE_VERIFY(animation); + CORRADE_VERIFY(!animation->importerState()); /* No particular clip */ + /* + - Nothing from the first animation + - Empty T/R/S tracks from the second animation + - Two rotation keys, four translation and scaling keys with common + time track from the third animation + - Four T/R/S spline-interpolated keys with a common time tracks + from the fourth animation + */ + CORRADE_COMPARE(animation->data().size(), + 2*(sizeof(Float) + sizeof(Quaternion)) + + 4*(sizeof(Float) + 2*sizeof(Vector3)) + + 4*(sizeof(Float) + 3*(sizeof(Quaternion) + 2*sizeof(Vector3)))); + /* Or also the same size as the animation binary file, except the time + sharing part that's tested elsewhere */ + CORRADE_COMPARE(animation->data().size(), 664 - 4*sizeof(Float)); + CORRADE_COMPARE(animation->trackCount(), 9); + + /* Rotation, empty */ + CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion); + CORRADE_COMPARE(animation->trackTargetType(0), AnimationTrackTargetType::Rotation3D); + CORRADE_COMPARE(animation->trackTarget(0), 0); + Animation::TrackViewStorage rotation = animation->track(0); + CORRADE_COMPARE(rotation.interpolation(), Animation::Interpolation::Linear); + CORRADE_VERIFY(rotation.keys().empty()); + CORRADE_VERIFY(rotation.values().empty()); + + /* Translation, empty */ + CORRADE_COMPARE(animation->trackType(1), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(1), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTarget(1), 1); + Animation::TrackViewStorage translation = animation->track(1); + CORRADE_COMPARE(translation.interpolation(), Animation::Interpolation::Constant); + CORRADE_VERIFY(translation.keys().empty()); + CORRADE_VERIFY(translation.values().empty()); + + /* Scaling, empty */ + CORRADE_COMPARE(animation->trackType(2), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(2), AnimationTrackTargetType::Scaling3D); + CORRADE_COMPARE(animation->trackTarget(2), 2); + Animation::TrackViewStorage scaling = animation->track(2); + CORRADE_COMPARE(scaling.interpolation(), Animation::Interpolation::Linear); + CORRADE_VERIFY(scaling.keys().empty()); + CORRADE_VERIFY(scaling.values().empty()); + + /* Rotation, linearly interpolated */ + CORRADE_COMPARE(animation->trackType(3), AnimationTrackType::Quaternion); + CORRADE_COMPARE(animation->trackTargetType(3), AnimationTrackTargetType::Rotation3D); + CORRADE_COMPARE(animation->trackTarget(3), 0); + Animation::TrackView rotation2 = animation->track(3); + CORRADE_COMPARE(rotation2.interpolation(), Animation::Interpolation::Linear); + CORRADE_COMPARE(rotation2.at(1.875f), Quaternion::rotation(90.0_degf, Vector3::xAxis())); + + /* Translation, constant interpolated, sharing keys with scaling */ + CORRADE_COMPARE(animation->trackType(4), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(4), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTarget(4), 1); + Animation::TrackView translation2 = animation->track(4); + CORRADE_COMPARE(translation2.interpolation(), Animation::Interpolation::Constant); + CORRADE_COMPARE(translation2.at(1.5f), Vector3::yAxis(2.5f)); + + /* Scaling, linearly interpolated, sharing keys with translation */ + CORRADE_COMPARE(animation->trackType(5), AnimationTrackType::Vector3); + CORRADE_COMPARE(animation->trackTargetType(5), AnimationTrackTargetType::Scaling3D); + CORRADE_COMPARE(animation->trackTarget(5), 2); + Animation::TrackView scaling2 = animation->track(5); + CORRADE_COMPARE(scaling2.interpolation(), Animation::Interpolation::Linear); + CORRADE_COMPARE(scaling2.at(1.5f), Vector3::zScale(5.2f)); + + /* Rotation, spline interpolated */ + CORRADE_COMPARE(animation->trackType(6), AnimationTrackType::CubicHermiteQuaternion); + CORRADE_COMPARE(animation->trackTargetType(6), AnimationTrackTargetType::Rotation3D); + CORRADE_COMPARE(animation->trackTarget(6), 3); + Animation::TrackView rotation3 = animation->track(6); + CORRADE_COMPARE(rotation3.interpolation(), Animation::Interpolation::Spline); + /* The same as in CubicHermiteTest::splerpQuaternion() */ + CORRADE_COMPARE(rotation3.at(0.5f + 0.35f*3), + (Quaternion{{-0.309862f, 0.174831f, 0.809747f}, 0.466615f})); + + /* Translation, spline interpolated */ + CORRADE_COMPARE(animation->trackType(7), AnimationTrackType::CubicHermite3D); + CORRADE_COMPARE(animation->trackTargetType(7), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTarget(7), 4); + Animation::TrackView translation3 = animation->track(7); + CORRADE_COMPARE(translation3.interpolation(), Animation::Interpolation::Spline); + /* The same as in CubicHermiteTest::splerpVector() */ + CORRADE_COMPARE(translation3.at(0.5f + 0.35f*3), + (Vector3{1.04525f, 0.357862f, 0.540875f})); + + /* Scaling, spline interpolated */ + CORRADE_COMPARE(animation->trackType(8), AnimationTrackType::CubicHermite3D); + CORRADE_COMPARE(animation->trackTargetType(8), AnimationTrackTargetType::Scaling3D); + CORRADE_COMPARE(animation->trackTarget(8), 5); + Animation::TrackView scaling3 = animation->track(8); + CORRADE_COMPARE(scaling3.interpolation(), Animation::Interpolation::Spline); + CORRADE_COMPARE(scaling3.at(0.5f + 0.35f*3), + (Vector3{0.118725f, 0.8228f, -2.711f})); +} + +void CgltfImporterTest::camera() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "camera" + std::string{data.suffix}))); + + CORRADE_COMPARE(importer->cameraCount(), 4); + + { + CORRADE_COMPARE(importer->cameraName(0), "Orthographic 4:3"); + CORRADE_COMPARE(importer->cameraForName("Orthographic 4:3"), 0); + + auto cam = importer->camera(0); + CORRADE_VERIFY(cam); + CORRADE_COMPARE(cam->type(), CameraType::Orthographic3D); + CORRADE_COMPARE(cam->size(), (Vector2{4.0f, 3.0f})); + CORRADE_COMPARE(cam->aspectRatio(), 1.333333f); + CORRADE_COMPARE(cam->near(), 0.01f); + CORRADE_COMPARE(cam->far(), 100.0f); + } { + CORRADE_COMPARE(importer->cameraName(1), "Perspective 1:1 75° hFoV"); + + auto cam = importer->camera(1); + CORRADE_VERIFY(cam); + CORRADE_COMPARE(cam->type(), CameraType::Perspective3D); + CORRADE_COMPARE(cam->fov(), 75.0_degf); + CORRADE_COMPARE(cam->aspectRatio(), 1.0f); + CORRADE_COMPARE(cam->near(), 0.1f); + CORRADE_COMPARE(cam->far(), 150.0f); + } { + CORRADE_COMPARE(importer->cameraName(2), "Perspective 4:3 75° hFoV"); + CORRADE_COMPARE(importer->cameraForName("Perspective 4:3 75° hFoV"), 2); + + auto cam = importer->camera(2); + CORRADE_VERIFY(cam); + CORRADE_COMPARE(cam->type(), CameraType::Perspective3D); + CORRADE_COMPARE(cam->fov(), 75.0_degf); + CORRADE_COMPARE(cam->aspectRatio(), 4.0f/3.0f); + CORRADE_COMPARE(cam->near(), 0.1f); + CORRADE_COMPARE(cam->far(), 150.0f); + } { + CORRADE_COMPARE(importer->cameraName(3), "Perspective 16:9 75° hFoV infinite"); + CORRADE_COMPARE(importer->cameraForName("Perspective 16:9 75° hFoV infinite"), 3); + + auto cam = importer->camera(3); + CORRADE_VERIFY(cam); + CORRADE_COMPARE(cam->type(), CameraType::Perspective3D); + CORRADE_COMPARE(cam->fov(), 75.0_degf); + CORRADE_COMPARE(cam->aspectRatio(), 16.0f/9.0f); + CORRADE_COMPARE(cam->near(), 0.1f); + CORRADE_COMPARE(cam->far(), Constants::inf()); + } +} + +void CgltfImporterTest::cameraInvalidType() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "camera-invalid-type.gltf"))); + CORRADE_COMPARE(importer->cameraCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->camera(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::camera(): invalid camera type\n"); +} + +void CgltfImporterTest::light() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "light" + std::string{data.suffix}))); + + CORRADE_COMPARE(importer->lightCount(), 4); + + CORRADE_COMPARE(importer->lightForName("Spot"), 1); + CORRADE_COMPARE(importer->lightName(1), "Spot"); + + { + auto light = importer->light("Point with everything implicit"); + CORRADE_VERIFY(light); + CORRADE_COMPARE(light->type(), LightData::Type::Point); + CORRADE_COMPARE(light->color(), (Color3{1.0f, 1.0f, 1.0f})); + CORRADE_COMPARE(light->intensity(), 1.0f); + CORRADE_COMPARE(light->attenuation(), (Vector3{1.0f, 0.0f, 1.0f})); + CORRADE_COMPARE(light->range(), Constants::inf()); + } { + auto light = importer->light("Spot"); + CORRADE_VERIFY(light); + CORRADE_COMPARE(light->type(), LightData::Type::Spot); + CORRADE_COMPARE(light->color(), (Color3{0.28f, 0.19f, 1.0f})); + CORRADE_COMPARE(light->intensity(), 2.1f); + CORRADE_COMPARE(light->attenuation(), (Vector3{1.0f, 0.0f, 1.0f})); + CORRADE_COMPARE(light->range(), 10.0f); + /* glTF has half-angles, we have full angles */ + CORRADE_COMPARE(light->innerConeAngle(), 0.25_radf*2.0f); + CORRADE_COMPARE(light->outerConeAngle(), 0.35_radf*2.0f); + } { + auto light = importer->light("Spot with implicit angles"); + CORRADE_VERIFY(light); + CORRADE_COMPARE(light->type(), LightData::Type::Spot); + CORRADE_COMPARE(light->innerConeAngle(), 0.0_degf); + /* glTF has half-angles, we have full angles */ + CORRADE_COMPARE(light->outerConeAngle(), 45.0_degf*2.0f); + } { + auto light = importer->light("Sun"); + CORRADE_VERIFY(light); + CORRADE_COMPARE(light->type(), LightData::Type::Directional); + CORRADE_COMPARE(light->color(), (Color3{1.0f, 0.08f, 0.14f})); + CORRADE_COMPARE(light->intensity(), 0.1f); + } +} + +void CgltfImporterTest::lightInvalid() { + auto&& data = LightInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "light-invalid.gltf"))); + + /* Check we didn't forget to test anything */ + CORRADE_COMPARE(importer->lightCount(), Containers::arraySize(LightInvalidData)); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->light(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::light(): {}\n", data.message)); +} + +void CgltfImporterTest::lightInvalidColorSize() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "light-invalid-color-size.gltf"))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): error opening file: invalid glTF, usually caused by invalid indices or missing required attributes\n"); +} + +void CgltfImporterTest::lightMissingType() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "light-missing-type.gltf"))); + CORRADE_COMPARE(importer->lightCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->light(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::light(): invalid light type\n"); +} + +void CgltfImporterTest::lightMissingSpot() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "light-missing-spot.gltf"))); + CORRADE_COMPARE(importer->lightCount(), 1); + + auto light = importer->light(0); + { + CORRADE_EXPECT_FAIL("The spot object is required for lights of type spot but cgltf doesn't care if it's missing. It just sets everything to default values."); + CORRADE_VERIFY(!light); + } + + CORRADE_COMPARE(light->type(), LightData::Type::Spot); + CORRADE_COMPARE(light->color(), (Color3{1.0f, 1.0f, 1.0f})); + CORRADE_COMPARE(light->intensity(), 1.0f); + CORRADE_COMPARE(light->attenuation(), (Vector3{1.0f, 0.0f, 1.0f})); + CORRADE_COMPARE(light->range(), Constants::inf()); + CORRADE_COMPARE(light->innerConeAngle(), 0.0_radf); + /* Magnum uses full angles, glTF uses half angles */ + CORRADE_COMPARE(light->outerConeAngle(), Rad{45.0_degf*2.0f}); +} + +void CgltfImporterTest::scene() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "scene" + std::string{data.suffix}))); + + /* Explicit default scene */ + CORRADE_COMPARE(importer->defaultScene(), 1); + CORRADE_COMPARE(importer->sceneCount(), 2); + CORRADE_COMPARE(importer->sceneName(1), "Scene"); + CORRADE_COMPARE(importer->sceneForName("Scene"), 1); + + auto emptyScene = importer->scene(0); + CORRADE_VERIFY(emptyScene); + CORRADE_VERIFY(!emptyScene->importerState()); + CORRADE_COMPARE(emptyScene->children3D(), std::vector{}); + + auto scene = importer->scene(1); + CORRADE_VERIFY(scene); + CORRADE_VERIFY(!scene->importerState()); + CORRADE_COMPARE(scene->children3D(), (std::vector{2, 4})); + + CORRADE_COMPARE(importer->object3DCount(), 7); + + CORRADE_COMPARE(importer->object3DName(4), "Light"); + CORRADE_COMPARE(importer->object3DForName("Light"), 4); + + { + auto object = importer->object3D("Camera"); + CORRADE_VERIFY(object); + CORRADE_VERIFY(!object->importerState()); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Camera); + CORRADE_COMPARE(object->instance(), 2); + CORRADE_VERIFY(object->children().empty()); + } { + auto object = importer->object3D("Empty with one child"); + CORRADE_VERIFY(object); + CORRADE_VERIFY(!object->importerState()); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Empty); + CORRADE_COMPARE(object->instance(), -1); + CORRADE_COMPARE(object->children(), (std::vector{0})); + } { + auto object = importer->object3D("Mesh w/o material"); + CORRADE_VERIFY(object); + CORRADE_VERIFY(!object->importerState()); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(object->instance(), 1); + CORRADE_COMPARE(static_cast(*object).material(), -1); + CORRADE_COMPARE(static_cast(*object).skin(), -1); + CORRADE_VERIFY(object->children().empty()); + } { + auto object = importer->object3D("Mesh and a material"); + CORRADE_VERIFY(object); + CORRADE_VERIFY(!object->importerState()); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(object->instance(), 0); + CORRADE_COMPARE(static_cast(*object).material(), 1); + CORRADE_COMPARE(static_cast(*object).skin(), -1); + CORRADE_VERIFY(object->children().empty()); + } { + auto object = importer->object3D("Mesh and a skin"); + CORRADE_VERIFY(object); + CORRADE_VERIFY(!object->importerState()); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(object->instance(), 1); + CORRADE_COMPARE(static_cast(*object).material(), -1); + CORRADE_COMPARE(static_cast(*object).skin(), 1); + CORRADE_VERIFY(object->children().empty()); + } { + auto object = importer->object3D("Light"); + CORRADE_VERIFY(object); + CORRADE_VERIFY(!object->importerState()); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Light); + CORRADE_COMPARE(object->instance(), 1); + CORRADE_VERIFY(object->children().empty()); + } { + auto object = importer->object3D("Empty with two children"); + CORRADE_VERIFY(object); + CORRADE_VERIFY(!object->importerState()); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Empty); + CORRADE_COMPARE(object->children(), (std::vector{3, 1})); + } +} + +void CgltfImporterTest::sceneEmpty() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "empty" + std::string{data.suffix}))); + + /* There is no scene, can't have any default */ + CORRADE_COMPARE(importer->defaultScene(), -1); + CORRADE_COMPARE(importer->sceneCount(), 0); +} + +void CgltfImporterTest::sceneNoDefault() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "scene-nodefault" + std::string{data.suffix}))); + + /* There is at least one scene, it's made default */ + CORRADE_COMPARE(importer->defaultScene(), 0); + CORRADE_COMPARE(importer->sceneCount(), 1); + + auto scene = importer->scene(0); + CORRADE_VERIFY(scene); + CORRADE_VERIFY(scene->children3D().empty()); +} + +void CgltfImporterTest::sceneOutOfBounds() { + auto&& data = SceneOutOfBoundsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, data.file))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): error opening file: invalid glTF, usually caused by invalid indices or missing required attributes\n"); +} + +void CgltfImporterTest::sceneInvalid() { + auto&& data = SceneInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* For some reason node relationships are checked in cgltf_parse and not in + cgltf_validate. Cycles are checked in cgltf_validate again. */ + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, data.file))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): error opening file: invalid glTF, usually caused by invalid indices or missing required attributes\n"); +} + +void CgltfImporterTest::sceneCycle() { + auto&& data = SceneCycleData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, data.file))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): node tree contains cycle starting at node 0\n"); +} + +void CgltfImporterTest::objectTransformation() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "object-transformation" + std::string{data.suffix}))); + + CORRADE_COMPARE(importer->object3DCount(), 8); + + { + CORRADE_COMPARE(importer->object3DName(0), "Matrix"); + auto object = importer->object3D(0); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Empty); + CORRADE_COMPARE(object->instance(), -1); + CORRADE_COMPARE(object->flags(), ObjectFlags3D{}); + CORRADE_COMPARE(object->transformation(), + Matrix4::translation({1.5f, -2.5f, 0.3f})* + Matrix4::rotationY(45.0_degf)* + Matrix4::scaling({0.9f, 0.5f, 2.3f})); + CORRADE_COMPARE(object->transformation(), (Matrix4{ + {0.636397f, 0.0f, -0.636395f, 0.0f}, + {0.0f, 0.5f, -0.0f, 0.0f}, + {1.62634f, 0.0f, 1.62635f, 0.0f}, + {1.5f, -2.5f, 0.3f, 1.0f} + })); + } { + CORRADE_COMPARE(importer->object3DName(1), "TRS"); + auto object = importer->object3D(1); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Empty); + CORRADE_COMPARE(object->instance(), -1); + CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(object->transformation(), + Matrix4::translation({1.5f, -2.5f, 0.3f})* + Matrix4::rotationY(45.0_degf)* + Matrix4::scaling({0.9f, 0.5f, 2.3f})); + CORRADE_COMPARE(object->transformation(), (Matrix4{ + {0.636397f, 0.0f, -0.636395f, 0.0f}, + {0.0f, 0.5f, -0.0f, 0.0f}, + {1.62634f, 0.0f, 1.62635f, 0}, + {1.5f, -2.5f, 0.3f, 1.0f} + })); + } { + CORRADE_COMPARE(importer->object3DName(2), "Mesh matrix"); + auto object = importer->object3D(2); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(object->instance(), 0); + CORRADE_COMPARE(object->flags(), ObjectFlags3D{}); + CORRADE_COMPARE(object->transformation(), + Matrix4::translation({1.5f, -2.5f, 0.3f})* + Matrix4::rotationY(45.0_degf)* + Matrix4::scaling({0.9f, 0.5f, 2.3f})); + CORRADE_COMPARE(object->transformation(), (Matrix4{ + {0.636397f, 0.0f, -0.636395f, 0.0f}, + {0.0f, 0.5f, -0.0f, 0.0f}, + {1.62634f, 0.0f, 1.62635f, 0.0f}, + {1.5f, -2.5f, 0.3f, 1.0f} + })); + } { + CORRADE_COMPARE(importer->object3DName(3), "Mesh TRS"); + auto object = importer->object3D(3); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(object->instance(), 0); + CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(object->transformation(), + Matrix4::translation({1.5f, -2.5f, 0.3f})* + Matrix4::rotationY(45.0_degf)* + Matrix4::scaling({0.9f, 0.5f, 2.3f})); + CORRADE_COMPARE(object->transformation(), (Matrix4{ + {0.636397f, 0.0f, -0.636395f, 0.0f}, + {0.0f, 0.5f, -0.0f, 0.0f}, + {1.62634f, 0.0f, 1.62635f, 0}, + {1.5f, -2.5f, 0.3f, 1.0f} + })); + } { + CORRADE_COMPARE(importer->object3DName(4), "Translation"); + auto object = importer->object3D(4); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Empty); + CORRADE_COMPARE(object->instance(), -1); + CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(object->translation(), (Vector3{1.5f, -2.5f, 0.3f})); + CORRADE_COMPARE(object->rotation(), Quaternion{}); + CORRADE_COMPARE(object->scaling(), Vector3{1.0f}); + CORRADE_COMPARE(object->transformation(), Matrix4::translation({1.5f, -2.5f, 0.3f})); + } { + CORRADE_COMPARE(importer->object3DName(5), "Rotation"); + auto object = importer->object3D(5); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Empty); + CORRADE_COMPARE(object->instance(), -1); + CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(object->rotation(), Quaternion::rotation(45.0_degf, Vector3::yAxis())); + CORRADE_COMPARE(object->scaling(), Vector3{1.0f}); + CORRADE_COMPARE(object->transformation(), Matrix4::rotationY(45.0_degf)); + } { + CORRADE_COMPARE(importer->object3DName(6), "Scaling"); + auto object = importer->object3D(6); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Empty); + CORRADE_COMPARE(object->instance(), -1); + CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(object->translation(), Vector3{}); + CORRADE_COMPARE(object->rotation(), Quaternion{}); + CORRADE_COMPARE(object->scaling(), (Vector3{0.9f, 0.5f, 2.3f})); + CORRADE_COMPARE(object->transformation(), Matrix4::scaling({0.9f, 0.5f, 2.3f})); + } { + CORRADE_COMPARE(importer->object3DName(7), "Implicit transformation"); + auto object = importer->object3D(7); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Empty); + CORRADE_COMPARE(object->instance(), -1); + CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(object->translation(), Vector3{}); + CORRADE_COMPARE(object->rotation(), Quaternion{}); + CORRADE_COMPARE(object->scaling(), Vector3{1.0f}); + CORRADE_COMPARE(object->transformation(), Matrix4{Math::IdentityInit}); + } +} + +void CgltfImporterTest::objectTransformationQuaternionNormalizationEnabled() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Enabled by default */ + CORRADE_VERIFY(importer->configuration().value("normalizeQuaternions")); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "object-transformation-patching.gltf"))); + + CORRADE_COMPARE(importer->object3DCount(), 1); + CORRADE_COMPARE(importer->object3DName(0), "Non-normalized rotation"); + + Containers::Pointer object; + std::ostringstream out; + { + Warning warningRedirection{&out}; + object = importer->object3D(0); + } + CORRADE_VERIFY(object); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::object3D(): rotation quaternion was renormalized\n"); + CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(object->rotation(), Quaternion::rotation(45.0_degf, Vector3::yAxis())); +} + +void CgltfImporterTest::objectTransformationQuaternionNormalizationDisabled() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Explicity disable */ + importer->configuration().setValue("normalizeQuaternions", false); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "object-transformation-patching.gltf"))); + + CORRADE_COMPARE(importer->object3DCount(), 1); + CORRADE_COMPARE(importer->object3DName(0), "Non-normalized rotation"); + + auto object = importer->object3D(0); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(object->rotation(), Quaternion::rotation(45.0_degf, Vector3::yAxis())*2.0f); +} + +void CgltfImporterTest::skin() { + auto&& data = MultiFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "skin" + std::string{data.suffix}))); + + CORRADE_COMPARE(importer->skin3DCount(), 2); + CORRADE_COMPARE(importer->skin3DName(1), "explicit inverse bind matrices"); + CORRADE_COMPARE(importer->skin3DForName("explicit inverse bind matrices"), 1); + CORRADE_COMPARE(importer->skin3DForName("nonexistent"), -1); + + { + CORRADE_COMPARE(importer->skin3DName(0), "implicit inverse bind matrices"); + + auto skin = importer->skin3D(0); + CORRADE_VERIFY(skin); + CORRADE_VERIFY(!skin->importerState()); + CORRADE_COMPARE_AS(skin->joints(), + Containers::arrayView({1, 2}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(skin->inverseBindMatrices(), + Containers::arrayView({Matrix4{}, Matrix4{}}), + TestSuite::Compare::Container); + + } { + CORRADE_COMPARE(importer->skin3DName(1), "explicit inverse bind matrices"); + + auto skin = importer->skin3D(1); + CORRADE_VERIFY(skin); + CORRADE_VERIFY(!skin->importerState()); + CORRADE_COMPARE_AS(skin->joints(), + Containers::arrayView({0, 2, 1}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(skin->inverseBindMatrices(), + Containers::arrayView({ + Matrix4::rotationX(35.0_degf), + Matrix4::translation({2.0f, 3.0f, 4.0f}), + Matrix4::scaling({2.0f, 3.0f, 4.0f}) + }), TestSuite::Compare::Container); + } +} + +void CgltfImporterTest::skinOutOfBounds() { + auto&& data = SkinOutOfBoundsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, data.file))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): error opening file: invalid glTF, usually caused by invalid indices or missing required attributes\n"); +} + +void CgltfImporterTest::skinInvalid() { + auto&& data = SkinInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "skin-invalid.gltf"))); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->skin3D(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::skin3D(): {}\n", data.message)); +} + +void CgltfImporterTest::skinInvalidBufferNotFound() { + /* This test has to be separate from TinyGltfImporter because it errors + out during import trying to load the buffer */ + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "skin-buffer-notfound.gltf"))); + + CORRADE_COMPARE(importer->skin3DCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->skin3D("buffer not found")); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::skin3D(): error opening file: /nonexistent.bin : file not found\n"); +} + +void CgltfImporterTest::skinInvalidTypes() { + auto&& data = SkinInvalidTypesData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "skin-invalid-types.gltf"))); + + /* Check we didn't forget to test anything */ + CORRADE_COMPARE(importer->skin3DCount(), Containers::arraySize(AnimationInvalidTypesData)); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->skin3D(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::skin3D(): {}\n", data.message)); +} + +void CgltfImporterTest::skinNoJointsProperty() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "skin-no-joints.gltf"))); + CORRADE_COMPARE(importer->skin3DCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->skin3D(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::skin3D(): skin has no joints\n"); +} + +void CgltfImporterTest::mesh() { + auto&& data = MultiFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh" + std::string{data.suffix}))); + + CORRADE_COMPARE(importer->meshCount(), 4); + CORRADE_COMPARE(importer->meshName(0), "Non-indexed mesh"); + CORRADE_COMPARE(importer->meshForName("Non-indexed mesh"), 0); + + auto mesh = importer->mesh(0); + CORRADE_VERIFY(mesh); + CORRADE_VERIFY(!mesh->importerState()); + CORRADE_COMPARE(mesh->primitive(), MeshPrimitive::Triangles); + + CORRADE_VERIFY(!mesh->isIndexed()); + + CORRADE_COMPARE(mesh->attributeCount(), 2); + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Position)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Position), VertexFormat::Vector3); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::Position), + Containers::arrayView({ + /* Interleaved with normals (which are in a different mesh) */ + {1.5f, -1.0f, -0.5f}, + {-0.5f, 2.5f, 0.75f}, + {-2.0f, 1.0f, 0.3f} + }), TestSuite::Compare::Container); + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::TextureCoordinates)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::TextureCoordinates), VertexFormat::Vector2); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::TextureCoordinates), + Containers::arrayView({ + /* Y-flipped compared to the input */ + {0.3f, 1.0f}, + {0.0f, 0.5f}, + {0.3f, 0.7f} + }), TestSuite::Compare::Container); +} + +void CgltfImporterTest::meshAttributeless() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh.gltf"))); + + auto mesh = importer->mesh("Attribute-less mesh"); + CORRADE_VERIFY(mesh); + CORRADE_VERIFY(!mesh->importerState()); + CORRADE_COMPARE(mesh->primitive(), MeshPrimitive::Triangles); + CORRADE_VERIFY(!mesh->isIndexed()); + CORRADE_COMPARE(mesh->vertexCount(), 0); + CORRADE_COMPARE(mesh->attributeCount(), 0); +} + +void CgltfImporterTest::meshIndexed() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh.gltf"))); + + CORRADE_COMPARE(importer->meshCount(), 4); + CORRADE_COMPARE(importer->meshName(1), "Indexed mesh"); + CORRADE_COMPARE(importer->meshForName("Indexed mesh"), 1); + + auto mesh = importer->mesh(1); + CORRADE_VERIFY(mesh); + CORRADE_VERIFY(!mesh->importerState()); + CORRADE_COMPARE(mesh->primitive(), MeshPrimitive::Triangles); + + CORRADE_VERIFY(mesh->isIndexed()); + CORRADE_COMPARE(mesh->indexType(), MeshIndexType::UnsignedByte); + CORRADE_COMPARE_AS(mesh->indices(), + Containers::arrayView({0, 1, 2}), + TestSuite::Compare::Container); + + CORRADE_COMPARE(mesh->attributeCount(), 4); + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Position)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Position), VertexFormat::Vector3); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::Position), + Containers::arrayView({ + {1.5f, -1.0f, -0.5f}, + {-0.5f, 2.5f, 0.75f}, + {-2.0f, 1.0f, 0.3f} + }), TestSuite::Compare::Container); + + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Normal)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Normal), VertexFormat::Vector3); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::Normal), + Containers::arrayView({ + {0.1f, 0.2f, 0.3f}, + {0.4f, 0.5f, 0.6f}, + {0.7f, 0.8f, 0.9f} + }), TestSuite::Compare::Container); + + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Tangent)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Tangent), VertexFormat::Vector4); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::Tangent), + Containers::arrayView({ + {-0.1f, -0.2f, -0.3f, 1.0f}, + {-0.4f, -0.5f, -0.6f, -1.0f}, + {-0.7f, -0.8f, -0.9f, 1.0f} + }), TestSuite::Compare::Container); + + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::ObjectId)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::ObjectId), VertexFormat::UnsignedInt); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::ObjectId), + Containers::arrayView({ + 215, 71, 133 + }), TestSuite::Compare::Container); +} + +void CgltfImporterTest::meshIndexedAttributeless() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh.gltf"))); + + auto mesh = importer->mesh("Attribute-less indexed mesh"); + CORRADE_VERIFY(mesh); + CORRADE_VERIFY(!mesh->importerState()); + CORRADE_COMPARE(mesh->primitive(), MeshPrimitive::Triangles); + CORRADE_VERIFY(mesh->isIndexed()); + CORRADE_COMPARE_AS(mesh->indicesAsArray(), + Containers::arrayView({0, 1, 2}), + TestSuite::Compare::Container); + CORRADE_COMPARE(mesh->vertexCount(), 0); + CORRADE_COMPARE(mesh->attributeCount(), 0); +} + +void CgltfImporterTest::meshColors() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh-colors.gltf"))); + + CORRADE_COMPARE(importer->meshCount(), 1); + + auto mesh = importer->mesh(0); + CORRADE_VERIFY(mesh); + CORRADE_VERIFY(!mesh->isIndexed()); + + CORRADE_COMPARE(mesh->attributeCount(), 3); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Position), VertexFormat::Vector3); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::Position), + Containers::arrayView({ + {1.5f, -1.0f, -0.5f}, + {-0.5f, 2.5f, 0.75f}, + {-2.0f, 1.0f, 0.3f} + }), TestSuite::Compare::Container); + CORRADE_COMPARE(mesh->attributeCount(MeshAttribute::Color), 2); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Color, 0), VertexFormat::Vector3); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::Color), + Containers::arrayView({ + {0.1f, 0.2f, 0.3f}, + {0.4f, 0.5f, 0.6f}, + {0.7f, 0.8f, 0.9f} + }), TestSuite::Compare::Container); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Color, 1), VertexFormat::Vector4); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::Color, 1), + Containers::arrayView({ + {0.1f, 0.2f, 0.3f, 0.4f}, + {0.5f, 0.6f, 0.7f, 0.8f}, + {0.9f, 1.0f, 1.1f, 1.2f} + }), TestSuite::Compare::Container); +} + +void CgltfImporterTest::meshSkinAttributes() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh-skin-attributes.gltf"))); + + /* The mapping should be available even before the mesh is imported */ + const MeshAttribute jointsAttribute = importer->meshAttributeForName("JOINTS"); + CORRADE_VERIFY(jointsAttribute != MeshAttribute{}); + const MeshAttribute weightsAttribute = importer->meshAttributeForName("WEIGHTS"); + CORRADE_VERIFY(weightsAttribute != MeshAttribute{}); + + CORRADE_COMPARE(importer->meshAttributeForName("JOINTS_0"), MeshAttribute{}); + CORRADE_COMPARE(importer->meshAttributeForName("JOINTS_1"), MeshAttribute{}); + CORRADE_COMPARE(importer->meshAttributeForName("WEIGHTS_0"), MeshAttribute{}); + CORRADE_COMPARE(importer->meshAttributeForName("WEIGHTS_1"), MeshAttribute{}); + + CORRADE_COMPARE(importer->meshCount(), 1); + + auto mesh = importer->mesh(0); + CORRADE_VERIFY(mesh); + CORRADE_VERIFY(!mesh->isIndexed()); + + CORRADE_COMPARE(mesh->attributeCount(), 5); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Position), VertexFormat::Vector3); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::Position), + Containers::arrayView({ + {1.5f, -1.0f, -0.5f}, + {-0.5f, 2.5f, 0.75f}, + {-2.0f, 1.0f, 0.3f} + }), TestSuite::Compare::Container); + + /* Custom attributes with multiple sets */ + CORRADE_COMPARE(mesh->attributeCount(jointsAttribute), 2); + CORRADE_COMPARE(mesh->attributeFormat(jointsAttribute, 0), VertexFormat::Vector4ub); + CORRADE_COMPARE_AS(mesh->attribute(jointsAttribute), + Containers::arrayView({ + {1, 2, 3, 4}, + {5, 6, 7, 8}, + {9, 10, 11, 12} + }), TestSuite::Compare::Container); + CORRADE_COMPARE(mesh->attributeFormat(jointsAttribute, 1), VertexFormat::Vector4us); + CORRADE_COMPARE_AS(mesh->attribute(jointsAttribute, 1), + Containers::arrayView({ + {13, 14, 15, 16}, + {17, 18, 19, 20}, + {21, 22, 23, 24} + }), TestSuite::Compare::Container); + CORRADE_COMPARE(mesh->attributeCount(weightsAttribute), 2); + CORRADE_COMPARE(mesh->attributeFormat(weightsAttribute, 0), VertexFormat::Vector4); + CORRADE_COMPARE_AS(mesh->attribute(weightsAttribute), + Containers::arrayView({ + {0.125f, 0.25f, 0.375f, 0.0f}, + {0.1f, 0.05f, 0.05f, 0.05f}, + {0.2f, 0.0f, 0.3f, 0.0f} + }), TestSuite::Compare::Container); + CORRADE_COMPARE(mesh->attributeFormat(weightsAttribute, 1), VertexFormat::Vector4usNormalized); + CORRADE_COMPARE_AS(mesh->attribute(weightsAttribute, 1), + Containers::arrayView({ + { 0, 0xffff/8, 0, 0xffff/8}, + {0xffff/2, 0xffff/8, 0xffff/16, 0xffff/16}, + { 0, 0xffff/4, 0xffff/4, 0} + }), TestSuite::Compare::Container); +} + +void CgltfImporterTest::meshCustomAttributes() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + { + std::ostringstream out; + Warning redirectWarning{&out}; + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh-custom-attributes.gltf"))); + CORRADE_COMPARE(importer->meshCount(), 2); + + CORRADE_COMPARE(out.str(), + "Trade::CgltfImporter::openData(): unknown attribute OBJECT_ID3, importing as custom attribute\n" + "Trade::CgltfImporter::openData(): unknown attribute NOT_AN_IDENTITY, importing as custom attribute\n"); + } + + /* The mapping should be available even before the mesh is imported. + Attributes are sorted in declaration order. */ + const MeshAttribute tbnAttribute = importer->meshAttributeForName("_TBN"); + CORRADE_COMPARE(tbnAttribute, meshAttributeCustom(0)); + CORRADE_COMPARE(importer->meshAttributeName(tbnAttribute), "_TBN"); + + const MeshAttribute uvRotation = importer->meshAttributeForName("_UV_ROTATION"); + CORRADE_COMPARE(uvRotation, meshAttributeCustom(1)); + CORRADE_COMPARE(importer->meshAttributeName(uvRotation), "_UV_ROTATION"); + + const MeshAttribute tbnPreciserAttribute = importer->meshAttributeForName("_TBN_PRECISER"); + const MeshAttribute objectIdAttribute = importer->meshAttributeForName("OBJECT_ID3"); + + const MeshAttribute doubleShotAttribute = importer->meshAttributeForName("_DOUBLE_SHOT"); + CORRADE_COMPARE(doubleShotAttribute, meshAttributeCustom(6)); + const MeshAttribute negativePaddingAttribute = importer->meshAttributeForName("_NEGATIVE_PADDING"); + CORRADE_COMPARE(negativePaddingAttribute, meshAttributeCustom(4)); + const MeshAttribute notAnIdentityAttribute = importer->meshAttributeForName("NOT_AN_IDENTITY"); + CORRADE_VERIFY(notAnIdentityAttribute != MeshAttribute{}); + + auto mesh = importer->mesh("standard types"); + CORRADE_VERIFY(mesh); + CORRADE_COMPARE(mesh->attributeCount(), 4); + + CORRADE_VERIFY(mesh->hasAttribute(tbnAttribute)); + CORRADE_COMPARE(mesh->attributeFormat(tbnAttribute), VertexFormat::Matrix3x3bNormalizedAligned); + CORRADE_COMPARE_AS(mesh->attribute(tbnAttribute), + Containers::arrayView({{ + Vector4b{1, 2, 3, 0}, + Vector4b{4, 5, 6, 0}, + Vector4b{7, 8, 9, 0} + }}), TestSuite::Compare::Container); + + CORRADE_VERIFY(mesh->hasAttribute(uvRotation)); + CORRADE_COMPARE(mesh->attributeFormat(uvRotation), VertexFormat::Matrix2x2bNormalizedAligned); + CORRADE_COMPARE_AS(mesh->attribute(uvRotation), + Containers::arrayView({{ + Vector4b{10, 11, 0, 0}, + Vector4b{12, 13, 0, 0}, + }}), TestSuite::Compare::Container); + + CORRADE_VERIFY(mesh->hasAttribute(tbnPreciserAttribute)); + CORRADE_COMPARE(mesh->attributeFormat(tbnPreciserAttribute), VertexFormat::Matrix3x3sNormalizedAligned); + CORRADE_COMPARE_AS(mesh->attribute(tbnPreciserAttribute), + Containers::arrayView({{ + Vector4s{-1, -2, -3, 0}, + Vector4s{-4, -5, -6, 0}, + Vector4s{-7, -8, -9, 0} + }}), TestSuite::Compare::Container); + + CORRADE_VERIFY(mesh->hasAttribute(objectIdAttribute)); + CORRADE_COMPARE(mesh->attributeFormat(objectIdAttribute), VertexFormat::UnsignedInt); + CORRADE_COMPARE_AS(mesh->attribute(objectIdAttribute), + Containers::arrayView({5678125}), + TestSuite::Compare::Container); + + /* Not testing import failure of non-core glTF attribute types, that's + already tested in meshInvalid() */ +} + +void CgltfImporterTest::meshCustomAttributesNoFileOpened() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* These should return nothing (and not crash) */ + CORRADE_COMPARE(importer->meshAttributeName(meshAttributeCustom(564)), ""); + CORRADE_COMPARE(importer->meshAttributeForName("thing"), MeshAttribute{}); +} + +void CgltfImporterTest::meshDuplicateAttributes() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh-duplicate-attributes.gltf"))); + CORRADE_COMPARE(importer->meshCount(), 1); + + const MeshAttribute thingAttribute = importer->meshAttributeForName("_THING"); + CORRADE_VERIFY(thingAttribute != MeshAttribute{}); + + auto mesh = importer->mesh(0); + CORRADE_VERIFY(mesh); + CORRADE_COMPARE(mesh->attributeCount(), 3); + + /* Duplicate attributes replace previously declared attributes with the + same name. Checking the formats should be enough to test the right + accessor is being used. */ + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Color)); + CORRADE_COMPARE(mesh->attributeCount(MeshAttribute::Color), 2); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Color, 0), VertexFormat::Vector4); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Color, 1), VertexFormat::Vector3); + + CORRADE_VERIFY(mesh->hasAttribute(thingAttribute)); + CORRADE_COMPARE(mesh->attributeCount(thingAttribute), 1); + CORRADE_COMPARE(mesh->attributeFormat(thingAttribute), VertexFormat::Vector2); +} + +void CgltfImporterTest::meshUnorderedAttributes() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh-unordered-attributes.gltf"))); + CORRADE_COMPARE(importer->meshCount(), 1); + + const MeshAttribute customAttribute4 = importer->meshAttributeForName("_CUSTOM_4"); + CORRADE_VERIFY(customAttribute4 != MeshAttribute{}); + const MeshAttribute customAttribute1 = importer->meshAttributeForName("_CUSTOM_1"); + CORRADE_VERIFY(customAttribute1 != MeshAttribute{}); + + /* Custom attributes are sorted in declaration order */ + CORRADE_VERIFY(customAttribute4 < customAttribute1); + + std::ostringstream out; + Warning redirectWarning{&out}; + + auto mesh = importer->mesh(0); + CORRADE_VERIFY(mesh); + CORRADE_COMPARE(mesh->attributeCount(), 7); + + /* No warning about _CUSTOM_4 and _CUSTOM_1 */ + CORRADE_COMPARE(out.str(), + "Trade::CgltfImporter::mesh(): found attribute COLOR_3 but expected COLOR_0\n" + "Trade::CgltfImporter::mesh(): found attribute COLOR_9 but expected COLOR_4\n" + ); + + /* Sets of the same attribute are imported in ascending set order. Checking + the formats should be enough to test the import order. */ + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::TextureCoordinates)); + CORRADE_COMPARE(mesh->attributeCount(MeshAttribute::TextureCoordinates), 3); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::TextureCoordinates, 0), VertexFormat::Vector2usNormalized); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::TextureCoordinates, 1), VertexFormat::Vector2ubNormalized); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::TextureCoordinates, 2), VertexFormat::Vector2); + + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Color)); + CORRADE_COMPARE(mesh->attributeCount(MeshAttribute::Color), 2); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Color, 0), VertexFormat::Vector4); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Color, 1), VertexFormat::Vector3); + + /* Custom attributes (besides JOINTS and WEIGHTS) don't have sets */ + CORRADE_VERIFY(mesh->hasAttribute(customAttribute4)); + CORRADE_COMPARE(mesh->attributeCount(customAttribute4), 1); + CORRADE_COMPARE(mesh->attributeFormat(customAttribute4), VertexFormat::Vector2); + + CORRADE_VERIFY(mesh->hasAttribute(customAttribute1)); + CORRADE_COMPARE(mesh->attributeCount(customAttribute1), 1); + CORRADE_COMPARE(mesh->attributeFormat(customAttribute1), VertexFormat::Vector3); +} + +void CgltfImporterTest::meshMultiplePrimitives() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh-multiple-primitives.gltf"))); + + /* Four meshes, but one has three primitives and one two. Distinguishing + using the primitive type, hopefully that's enough. */ + CORRADE_COMPARE(importer->meshCount(), 7); + { + CORRADE_COMPARE(importer->meshName(0), "Single-primitive points"); + CORRADE_COMPARE(importer->meshForName("Single-primitive points"), 0); + auto mesh = importer->mesh(0); + CORRADE_VERIFY(mesh); + CORRADE_COMPARE(mesh->primitive(), MeshPrimitive::Points); + } { + CORRADE_COMPARE(importer->meshName(1), "Multi-primitive lines, triangles, triangle strip"); + CORRADE_COMPARE(importer->meshName(2), "Multi-primitive lines, triangles, triangle strip"); + CORRADE_COMPARE(importer->meshName(3), "Multi-primitive lines, triangles, triangle strip"); + CORRADE_COMPARE(importer->meshForName("Multi-primitive lines, triangles, triangle strip"), 1); + auto mesh1 = importer->mesh(1); + CORRADE_VERIFY(mesh1); + CORRADE_COMPARE(mesh1->primitive(), MeshPrimitive::Lines); + auto mesh2 = importer->mesh(2); + CORRADE_VERIFY(mesh2); + CORRADE_COMPARE(mesh2->primitive(), MeshPrimitive::Triangles); + auto mesh3 = importer->mesh(3); + CORRADE_VERIFY(mesh3); + CORRADE_COMPARE(mesh3->primitive(), MeshPrimitive::TriangleStrip); + } { + CORRADE_COMPARE(importer->meshName(4), "Single-primitive line loop"); + CORRADE_COMPARE(importer->meshForName("Single-primitive line loop"), 4); + auto mesh = importer->mesh(4); + CORRADE_VERIFY(mesh); + CORRADE_COMPARE(mesh->primitive(), MeshPrimitive::LineLoop); + } { + CORRADE_COMPARE(importer->meshName(5), "Multi-primitive triangle fan, line strip"); + CORRADE_COMPARE(importer->meshName(6), "Multi-primitive triangle fan, line strip"); + CORRADE_COMPARE(importer->meshForName("Multi-primitive triangle fan, line strip"), 5); + auto mesh5 = importer->mesh(5); + CORRADE_VERIFY(mesh5); + CORRADE_COMPARE(mesh5->primitive(), MeshPrimitive::TriangleFan); + auto mesh6 = importer->mesh(6); + CORRADE_VERIFY(mesh6); + CORRADE_COMPARE(mesh6->primitive(), MeshPrimitive::LineStrip); + } + + /* Five objects, but two refer a three-primitive mesh and one refers a + two-primitive one */ + CORRADE_COMPARE(importer->object3DCount(), 10); + { + CORRADE_COMPARE(importer->object3DName(0), "Using the second mesh, should have 4 children"); + CORRADE_COMPARE(importer->object3DName(1), "Using the second mesh, should have 4 children"); + CORRADE_COMPARE(importer->object3DName(2), "Using the second mesh, should have 4 children"); + CORRADE_COMPARE(importer->object3DForName("Using the second mesh, should have 4 children"), 0); + auto object = importer->object3D(0); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(object->instance(), 1); + CORRADE_COMPARE(object->children(), (std::vector{1, 2, 8, 3})); + + auto child1 = importer->object3D(1); + CORRADE_VERIFY(child1); + CORRADE_COMPARE(child1->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(child1->instance(), 2); + CORRADE_COMPARE(child1->children(), {}); + CORRADE_COMPARE(child1->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(child1->translation(), Vector3{}); + CORRADE_COMPARE(child1->rotation(), Quaternion{}); + CORRADE_COMPARE(child1->scaling(), Vector3{1.0f}); + + auto child2 = importer->object3D(2); + CORRADE_VERIFY(child2); + CORRADE_COMPARE(child2->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(child2->instance(), 3); + CORRADE_COMPARE(child2->children(), {}); + CORRADE_COMPARE(child2->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(child2->translation(), Vector3{}); + CORRADE_COMPARE(child2->rotation(), Quaternion{}); + CORRADE_COMPARE(child2->scaling(), Vector3{1.0f}); + } { + CORRADE_COMPARE(importer->object3DName(3), "Using the first mesh, no children"); + CORRADE_COMPARE(importer->object3DForName("Using the first mesh, no children"), 3); + auto object = importer->object3D(3); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(object->instance(), 0); + CORRADE_COMPARE(object->children(), {}); + } { + CORRADE_COMPARE(importer->object3DName(4), "Just a non-mesh node"); + CORRADE_COMPARE(importer->object3DForName("Just a non-mesh node"), 4); + auto object = importer->object3D(4); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Empty); + CORRADE_COMPARE(object->instance(), -1); + CORRADE_COMPARE(object->children(), {}); + } { + CORRADE_COMPARE(importer->object3DName(5), "Using the second mesh again, again 2 children"); + CORRADE_COMPARE(importer->object3DName(6), "Using the second mesh again, again 2 children"); + CORRADE_COMPARE(importer->object3DName(7), "Using the second mesh again, again 2 children"); + CORRADE_COMPARE(importer->object3DForName("Using the second mesh again, again 2 children"), 5); + auto object = importer->object3D(5); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(object->instance(), 1); + CORRADE_COMPARE(object->children(), (std::vector{6, 7})); + + auto child6 = importer->object3D(6); + CORRADE_VERIFY(child6); + CORRADE_COMPARE(child6->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(child6->instance(), 2); + CORRADE_COMPARE(child6->children(), {}); + CORRADE_COMPARE(child6->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(child6->translation(), Vector3{}); + CORRADE_COMPARE(child6->rotation(), Quaternion{}); + CORRADE_COMPARE(child6->scaling(), Vector3{1.0f}); + + auto child7 = importer->object3D(7); + CORRADE_VERIFY(child7); + CORRADE_COMPARE(child7->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(child7->instance(), 3); + CORRADE_COMPARE(child7->children(), {}); + CORRADE_COMPARE(child7->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(child7->translation(), Vector3{}); + CORRADE_COMPARE(child7->rotation(), Quaternion{}); + CORRADE_COMPARE(child7->scaling(), Vector3{1.0f}); + } { + CORRADE_COMPARE(importer->object3DName(8), "Using the fourth mesh, 1 child"); + CORRADE_COMPARE(importer->object3DName(9), "Using the fourth mesh, 1 child"); + CORRADE_COMPARE(importer->object3DForName("Using the fourth mesh, 1 child"), 8); + auto object = importer->object3D(8); + CORRADE_VERIFY(object); + CORRADE_COMPARE(object->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(object->instance(), 5); + CORRADE_COMPARE(object->children(), (std::vector{9})); + + auto child9 = importer->object3D(9); + CORRADE_VERIFY(child9); + CORRADE_COMPARE(child9->instanceType(), ObjectInstanceType3D::Mesh); + CORRADE_COMPARE(child9->instance(), 6); + CORRADE_COMPARE(child9->children(), {}); + CORRADE_COMPARE(child9->flags(), ObjectFlag3D::HasTranslationRotationScaling); + CORRADE_COMPARE(child9->translation(), Vector3{}); + CORRADE_COMPARE(child9->rotation(), Quaternion{}); + CORRADE_COMPARE(child9->scaling(), Vector3{1.0f}); + } + + /* Animations -- the instance ID should point to the right expanded nodes */ + CORRADE_COMPARE(importer->animationCount(), 1); + { + CORRADE_COMPARE(importer->animationName(0), "Animation affecting multi-primitive nodes"); + CORRADE_COMPARE(importer->animationForName("Animation affecting multi-primitive nodes"), 0); + + auto animation = importer->animation(0); + CORRADE_VERIFY(animation); + CORRADE_COMPARE(animation->trackCount(), 4); + CORRADE_COMPARE(animation->trackTargetType(0), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTargetType(1), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTargetType(2), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTargetType(3), AnimationTrackTargetType::Translation3D); + CORRADE_COMPARE(animation->trackTarget(0), 5); /* not 3 */ + CORRADE_COMPARE(animation->trackTarget(1), 3); /* not 1 */ + CORRADE_COMPARE(animation->trackTarget(2), 4); /* not 2 */ + CORRADE_COMPARE(animation->trackTarget(3), 8); /* not 4 */ + } +} + +void CgltfImporterTest::meshPrimitivesTypes() { + auto&& data = MeshPrimitivesTypesData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Disable Y-flipping to have consistent results. Tested separately for all + types in materialTexCoordFlip(). */ + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + importer->configuration().setValue("textureCoordinateYFlipInMaterial", true); + + if(data.objectIdAttribute) + importer->configuration().setValue("objectIdAttribute", data.objectIdAttribute); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh-primitives-types.gltf"))); + + /* Ensure we didn't forget to test any case */ + CORRADE_COMPARE(importer->meshCount(), Containers::arraySize(MeshPrimitivesTypesData)); + + auto mesh = importer->mesh(data.name); + CORRADE_VERIFY(mesh); + CORRADE_COMPARE(mesh->primitive(), data.primitive); + + if(data.indexType != MeshIndexType{}) { + CORRADE_VERIFY(mesh->isIndexed()); + CORRADE_COMPARE(mesh->indexType(), data.indexType); + CORRADE_COMPARE_AS(mesh->indicesAsArray(), + Containers::arrayView({0, 2, 1, 4, 3, 0}), + TestSuite::Compare::Container); + } else CORRADE_VERIFY(!mesh->isIndexed()); + + /* Positions */ + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Position), data.positionFormat); + if(isVertexFormatNormalized(data.positionFormat)) { + if(vertexFormatComponentFormat(data.positionFormat) == VertexFormat::UnsignedByte || + vertexFormatComponentFormat(data.positionFormat) == VertexFormat::UnsignedShort) { + CORRADE_COMPARE_AS(mesh->positions3DAsArray(), + Containers::arrayView({ + {0.8f, 0.4f, 0.2f}, + {1.0f, 0.333333f, 0.666667f}, + {0.733333f, 0.866667f, 0.0f}, + {0.066667f, 0.133333f, 0.933333f}, + {0.6f, 0.266667f, 0.466667f} + }), TestSuite::Compare::Container); + } else if(vertexFormatComponentFormat(data.positionFormat) == VertexFormat::Byte || + vertexFormatComponentFormat(data.positionFormat) == VertexFormat::Short) { + + constexpr Vector3 expected[]{ + {-0.133333f, -0.333333f, -0.2f}, + {-0.8f, -0.133333f, -0.4f}, + {-1.0f, -0.933333f, -0.0f}, + {-0.4f, -0.6f, -0.333333f}, + {-0.666667f, -0.733333f, -0.933333f} + }; + + /* Because the signed packed formats are extremely imprecise, we + increase the fuzziness a bit */ + auto positions = mesh->positions3DAsArray(); + const Float precision = Math::pow(10.0f, -1.5f*vertexFormatSize(vertexFormatComponentFormat(data.positionFormat))); + CORRADE_COMPARE_AS(precision, 5.0e-2f, TestSuite::Compare::Less); + CORRADE_COMPARE_AS(precision, 1.0e-6f, TestSuite::Compare::GreaterOrEqual); + CORRADE_COMPARE(positions.size(), Containers::arraySize(expected)); + CORRADE_ITERATION("precision" << precision); + for(std::size_t i = 0; i != positions.size(); ++i) { + CORRADE_ITERATION(i); + CORRADE_COMPARE_WITH(positions[i], expected[i], + TestSuite::Compare::around(Vector3{precision})); + } + } else { + CORRADE_ITERATION(data.positionFormat); + CORRADE_VERIFY(false); + } + } else { + CORRADE_COMPARE_AS(mesh->positions3DAsArray(), + Containers::arrayView({ + {1.0f, 3.0f, 2.0f}, + {1.0f, 1.0f, 2.0f}, + {3.0f, 3.0f, 2.0f}, + {3.0f, 1.0f, 2.0f}, + {5.0f, 3.0f, 9.0f} + }), TestSuite::Compare::Container); + } + + /* Normals */ + if(data.normalFormat != VertexFormat{}) { + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Normal)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Normal), data.normalFormat); + + constexpr Vector3 expected[]{ + {-0.333333f, -0.6666667f, -0.933333f}, + {-0.0f, -0.133333f, -1.0f}, + {-0.6f, -0.8f, -0.2f}, + {-0.4f, -0.733333f, -0.933333f}, + {-0.133333f, -0.733333f, -0.4f} + }; + + /* Because the signed packed formats are extremely imprecise, we + increase the fuzziness a bit */ + auto normals = mesh->normalsAsArray(); + const Float precision = Math::pow(10.0f, -1.5f*vertexFormatSize(vertexFormatComponentFormat(data.normalFormat))); + CORRADE_COMPARE_AS(precision, 5.0e-2f, TestSuite::Compare::Less); + CORRADE_COMPARE_AS(precision, 1.0e-6f, TestSuite::Compare::GreaterOrEqual); + CORRADE_COMPARE(normals.size(), Containers::arraySize(expected)); + CORRADE_ITERATION("precision" << precision); + for(std::size_t i = 0; i != normals.size(); ++i) { + CORRADE_ITERATION(i); + CORRADE_COMPARE_WITH(normals[i], expected[i], + TestSuite::Compare::around(Vector3{precision})); + } + } else CORRADE_VERIFY(!mesh->hasAttribute(MeshAttribute::Normal)); + + /* Tangents */ + if(data.tangentFormat != VertexFormat{}) { + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Tangent)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Tangent), data.tangentFormat); + + constexpr Vector3 expected[]{ + {-0.933333f, -0.333333f, -0.6666667f}, + {-1.0f, -0.0f, -0.133333f}, + {-0.2f, -0.6f, -0.8f}, + {-0.933333f, -0.4f, -0.733333f}, + {-0.4f, -0.133333f, -0.733333f} + }; + + /* Because the signed packed formats are extremely imprecise, we + increase the fuzziness a bit */ + auto tangents = mesh->tangentsAsArray(); + const Float precision = Math::pow(10.0f, -1.5f*vertexFormatSize(vertexFormatComponentFormat(data.tangentFormat))); + CORRADE_COMPARE_AS(precision, 5.0e-2f, TestSuite::Compare::Less); + CORRADE_COMPARE_AS(precision, 1.0e-6f, TestSuite::Compare::GreaterOrEqual); + CORRADE_COMPARE(tangents.size(), Containers::arraySize(expected)); + CORRADE_ITERATION("precision" << precision); + for(std::size_t i = 0; i != tangents.size(); ++i) { + CORRADE_ITERATION(i); + CORRADE_COMPARE_WITH(tangents[i], expected[i], + TestSuite::Compare::around(Vector3{precision})); + } + + /* However the bitangents signs are just 1 or -1, so no need to take + extreme measures */ + CORRADE_COMPARE_AS(mesh->bitangentSignsAsArray(), + Containers::arrayView({1.0f, -1.0f, 1.0f, -1.0f, 1.0f}), + TestSuite::Compare::Container); + } else CORRADE_VERIFY(!mesh->hasAttribute(MeshAttribute::Tangent)); + + /* Colors */ + if(data.colorFormat == VertexFormat{}) { + CORRADE_VERIFY(!mesh->hasAttribute(MeshAttribute::Color)); + } else if(vertexFormatComponentCount(data.colorFormat) == 3) { + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Color)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Color), data.colorFormat); + CORRADE_COMPARE_AS(Containers::arrayCast(Containers::stridedArrayView(mesh->colorsAsArray())), + Containers::stridedArrayView({ + {0.8f, 0.2f, 0.4f}, + {0.6f, 0.666667f, 1.0f}, + {0.0f, 0.0666667f, 0.9333333f}, + {0.733333f, 0.8666666f, 0.133333f}, + {0.266667f, 0.3333333f, 0.466667f} + }), TestSuite::Compare::Container); + } else if(vertexFormatComponentCount(data.colorFormat) == 4) { + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::Color)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::Color), data.colorFormat); + CORRADE_COMPARE_AS(mesh->colorsAsArray(), + Containers::arrayView({ + {0.8f, 0.2f, 0.4f, 0.266667f}, + {0.6f, 0.666667f, 1.0f, 0.8666667f}, + {0.0f, 0.0666667f, 0.9333333f, 0.466667f}, + {0.733333f, 0.8666667f, 0.133333f, 0.666667f}, + {0.266667f, 0.3333333f, 0.466666f, 0.0666667f} + }), TestSuite::Compare::Container); + } else CORRADE_VERIFY(false); + + /* Texture coordinates */ + if(data.textureCoordinateFormat == VertexFormat{}) { + CORRADE_VERIFY(!mesh->hasAttribute(MeshAttribute::TextureCoordinates)); + + } else if(isVertexFormatNormalized(data.textureCoordinateFormat)) { + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::TextureCoordinates)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::TextureCoordinates), data.textureCoordinateFormat); + if(vertexFormatComponentFormat(data.textureCoordinateFormat) == VertexFormat::UnsignedByte || + vertexFormatComponentFormat(data.textureCoordinateFormat) == VertexFormat::UnsignedShort) { + CORRADE_COMPARE_AS(mesh->textureCoordinates2DAsArray(), + Containers::arrayView({ + {0.933333f, 0.3333333f}, + {0.133333f, 0.9333333f}, + {0.666667f, 0.2666667f}, + {0.466666f, 0.3333333f}, + {0.866666f, 0.0666667f} + }), TestSuite::Compare::Container); + } else if(vertexFormatComponentFormat(data.textureCoordinateFormat) == VertexFormat::Byte || + vertexFormatComponentFormat(data.textureCoordinateFormat) == VertexFormat::Short) { + constexpr Vector2 expected[]{ + {-0.666667f, -0.9333333f}, + {-0.4f, -0.7333333f}, + {-0.8f, -0.2f}, + {-0.0f, -0.1333333f}, + {-0.6f, -0.3333333f} + }; + + /* Because the signed packed formats are extremely imprecise, we + increase the fuzziness a bit */ + auto textureCoordinates = mesh->textureCoordinates2DAsArray(); + const Float precision = Math::pow(10.0f, -1.5f*vertexFormatSize(vertexFormatComponentFormat(data.textureCoordinateFormat))); + CORRADE_COMPARE_AS(precision, 5.0e-2f, TestSuite::Compare::Less); + CORRADE_COMPARE_AS(precision, 1.0e-6f, TestSuite::Compare::GreaterOrEqual); + CORRADE_COMPARE(textureCoordinates.size(), Containers::arraySize(expected)); + CORRADE_ITERATION("precision" << precision); + for(std::size_t i = 0; i != textureCoordinates.size(); ++i) { + CORRADE_ITERATION(i); + CORRADE_COMPARE_WITH(textureCoordinates[i], expected[i], + TestSuite::Compare::around(Vector2{precision})); + } + } else { + CORRADE_ITERATION(data.positionFormat); + CORRADE_VERIFY(false); + } + } else { + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::TextureCoordinates)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::TextureCoordinates), data.textureCoordinateFormat); + CORRADE_COMPARE_AS(mesh->textureCoordinates2DAsArray(), + Containers::arrayView({ + {75.0f, 13.0f}, + {98.0f, 22.0f}, + {15.0f, 125.0f}, + {12.0f, 33.0f}, + {24.0f, 57.0f} + }), TestSuite::Compare::Container); + } + + /* Object ID */ + if(data.objectIdFormat != VertexFormat{}) { + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::ObjectId)); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::ObjectId), data.objectIdFormat); + CORRADE_COMPARE_AS(mesh->objectIdsAsArray(), + Containers::stridedArrayView({ + 215, 71, 133, 5, 196 + }), TestSuite::Compare::Container); + } else CORRADE_VERIFY(!mesh->hasAttribute(MeshAttribute::ObjectId)); +} + +void CgltfImporterTest::meshOutOfBounds() { + auto&& data = MeshOutOfBoundsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, data.file))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): error opening file: invalid glTF, usually caused by invalid indices or missing required attributes\n"); +} + +void CgltfImporterTest::meshInvalid() { + auto&& data = MeshInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "mesh-invalid.gltf"))); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->mesh(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::mesh(): {}\n", data.message)); +} + +void CgltfImporterTest::meshInvalidIndicesBufferNotFound() { + /* This test has to be separate from TinyGltfImporter because it errors + out during import trying to load the buffer. + + Not testing this for the attribute buffer since that's already done by + openExternalDataNotFound(). */ + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "mesh-indices-buffer-notfound.gltf"))); + + CORRADE_COMPARE(importer->meshCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->mesh("indices buffer not found")); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::mesh(): error opening file: /nonexistent.bin : file not found\n"); +} + +void CgltfImporterTest::meshInvalidTypes() { + auto&& data = MeshInvalidTypesData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "mesh-invalid-types.gltf"))); + + /* Check we didn't forget to test anything */ + CORRADE_COMPARE(importer->meshCount(), Containers::arraySize(MeshInvalidTypesData)); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->mesh(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::mesh(): {}\n", data.message)); +} + +void CgltfImporterTest::materialPbrMetallicRoughness() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "material-metallicroughness.gltf"))); + CORRADE_COMPARE(importer->materialCount(), 7); + CORRADE_COMPARE(importer->materialForName("textures"), 2); + CORRADE_COMPARE(importer->materialName(2), "textures"); + + { + const char* name = "defaults"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_VERIFY(!material->importerState()); + CORRADE_COMPARE(material->types(), MaterialType::PbrMetallicRoughness); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 0); + + /* These are glTF defaults, just verify those are consistent with + MaterialData API defaults (if they wouldn't be, we'd need to add + explicit attributes to override those) */ + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.baseColor(), (Color4{1.0f})); + CORRADE_COMPARE(pbr.metalness(), 1.0f); + CORRADE_COMPARE(pbr.roughness(), 1.0f); + } { + const char* name = "color"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 3); + + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.baseColor(), (Color4{0.3f, 0.4f, 0.5f, 0.8f})); + CORRADE_COMPARE(pbr.metalness(), 0.56f); + CORRADE_COMPARE(pbr.roughness(), 0.89f); + } { + const char* name = "textures"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 5); + + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::BaseColorTexture)); + CORRADE_COMPARE(pbr.baseColor(), (Color4{0.7f, 0.8f, 0.9f, 1.1f})); + CORRADE_COMPARE(pbr.baseColorTexture(), 0); + CORRADE_COMPARE(pbr.metalness(), 0.6f); + CORRADE_COMPARE(pbr.roughness(), 0.9f); + CORRADE_VERIFY(pbr.hasNoneRoughnessMetallicTexture()); + CORRADE_COMPARE(pbr.metalnessTexture(), 1); + } { + const char* name = "identity texture transform"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 5); + + const auto& pbr = material->as(); + /* Identity transform, but is present */ + CORRADE_VERIFY(pbr.hasTextureTransformation()); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::BaseColorTexture)); + CORRADE_COMPARE(pbr.baseColorTextureMatrix(), (Matrix3{})); + CORRADE_VERIFY(pbr.hasNoneRoughnessMetallicTexture()); + CORRADE_COMPARE(pbr.metalnessTextureMatrix(), (Matrix3{})); + } { + const char* name = "texture transform"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 5); + + const auto& pbr = material->as(); + /* All */ + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::BaseColorTexture)); + CORRADE_COMPARE(pbr.baseColorTextureMatrix(), (Matrix3{ + {0.164968f, 0.472002f, 0.0f}, + {-0.472002f, 0.164968f, 0.0f}, + {0.472002f, -0.164968f, 1.0f} + })); + /* Offset + scale */ + CORRADE_VERIFY(pbr.hasNoneRoughnessMetallicTexture()); + CORRADE_COMPARE(pbr.metalnessTextureMatrix(), (Matrix3{ + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.5f, 0.0f}, + {0.0f, -0.5f, 1.0f} + })); + } { + const char* name = "texture coordinate sets"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 5); + + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::BaseColorTexture)); + CORRADE_COMPARE(pbr.baseColorTextureCoordinates(), 7); + CORRADE_VERIFY(pbr.hasNoneRoughnessMetallicTexture()); + CORRADE_COMPARE(pbr.metalnessTextureCoordinates(), 5); + } { + const char* name = "empty texture transform with overriden coordinate set"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 7); + + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::BaseColorTexture)); + CORRADE_COMPARE(pbr.baseColorTextureMatrix(), Matrix3{}); + CORRADE_VERIFY(pbr.hasNoneRoughnessMetallicTexture()); + CORRADE_COMPARE(pbr.metalnessTextureMatrix(), Matrix3{}); + CORRADE_COMPARE(pbr.metalnessTextureCoordinates(), 2); /* not 5 */ + } +} + +void CgltfImporterTest::materialPbrSpecularGlossiness() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "material-specularglossiness.gltf"))); + CORRADE_COMPARE(importer->materialCount(), 7); + + { + const char* name = "defaults"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_VERIFY(!material->importerState()); + CORRADE_COMPARE(material->types(), MaterialType::PbrSpecularGlossiness); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 0); + + /* These are glTF defaults, just verify those are consistent with + MaterialData API defaults (if they wouldn't be, we'd need to add + explicit attributes to override those) */ + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.diffuseColor(), (Color4{1.0f})); + CORRADE_COMPARE(pbr.specularColor(), (Color4{1.0f, 0.0f})); + CORRADE_COMPARE(pbr.glossiness(), 1.0f); + } { + const char* name = "color"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 3); + + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.diffuseColor(), (Color4{0.3f, 0.4f, 0.5f, 0.8f})); + CORRADE_COMPARE(pbr.specularColor(), (Color4{0.1f, 0.2f, 0.6f, 0.0f})); + CORRADE_COMPARE(pbr.glossiness(), 0.89f); + } { + const char* name = "textures"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 5); + + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::DiffuseTexture)); + CORRADE_COMPARE(pbr.diffuseColor(), (Color4{0.7f, 0.8f, 0.9f, 1.1f})); + CORRADE_COMPARE(pbr.diffuseTexture(), 0); + CORRADE_COMPARE(pbr.specularColor(), (Color4{0.4f, 0.5f, 0.6f, 0.0f})); + CORRADE_VERIFY(pbr.hasSpecularGlossinessTexture()); + CORRADE_COMPARE(pbr.specularTexture(), 1); CORRADE_COMPARE(pbr.glossiness(), 0.9f); + } { + const char* name = "identity texture transform"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 5); + /* Identity transform, but is present */ + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasTextureTransformation()); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::DiffuseTexture)); + CORRADE_COMPARE(pbr.diffuseTextureMatrix(), (Matrix3{})); + CORRADE_VERIFY(pbr.hasSpecularGlossinessTexture()); + CORRADE_COMPARE(pbr.specularTextureMatrix(), (Matrix3{})); + } { + const char* name = "texture transform"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 5); + + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::DiffuseTexture)); + CORRADE_COMPARE(pbr.diffuseTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.0f, -1.0f, 1.0f} + })); + CORRADE_VERIFY(pbr.hasSpecularGlossinessTexture()); + CORRADE_COMPARE(pbr.specularTextureMatrix(), (Matrix3{ + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.5f, 0.0f}, + {0.0f, 0.5f, 1.0f} + })); + } { + const char* name = "texture coordinate sets"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 5); + + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::DiffuseTexture)); + CORRADE_COMPARE(pbr.diffuseTextureCoordinates(), 7); + CORRADE_VERIFY(pbr.hasSpecularGlossinessTexture()); + CORRADE_COMPARE(pbr.specularTextureCoordinates(), 5); + } { + const char* name = "both metallic/roughness and specular/glossiness"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + + CORRADE_COMPARE(material->types(), MaterialType::PbrSpecularGlossiness|MaterialType::PbrMetallicRoughness); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 6); + + const auto& a = material->as(); + CORRADE_COMPARE(a.baseColor(), (Color4{0.3f, 0.4f, 0.5f, 0.8f})); + CORRADE_COMPARE(a.metalness(), 0.56f); + CORRADE_COMPARE(a.roughness(), 0.89f); + + const auto& b = material->as(); + CORRADE_COMPARE(b.diffuseColor(), (Color4{0.3f, 0.4f, 0.5f, 0.8f})); + CORRADE_COMPARE(b.specularColor(), (Color4{0.1f, 0.2f, 0.6f, 0.0f})); + CORRADE_COMPARE(b.glossiness(), 0.89f); + } +} + +void CgltfImporterTest::materialCommon() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "material-common.gltf"))); + CORRADE_COMPARE(importer->materialCount(), 7); + + { + auto material = importer->material("defaults"); + CORRADE_COMPARE(material->types(), MaterialTypes{}); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 0); + + /* These are glTF defaults, just verify those are consistent with + MaterialData API defaults (if they wouldn't be, we'd need to add + explicit attributes to override those) */ + CORRADE_COMPARE(material->alphaMode(), MaterialAlphaMode::Opaque); + CORRADE_COMPARE(material->alphaMask(), 0.5f); + } { + auto material = importer->material("alpha mask"); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 1); + CORRADE_COMPARE(material->alphaMode(), MaterialAlphaMode::Mask); + CORRADE_COMPARE(material->alphaMask(), 0.369f); + } { + auto material = importer->material("double-sided alpha blend"); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 2); + CORRADE_VERIFY(material->isDoubleSided()); + CORRADE_COMPARE(material->alphaMode(), MaterialAlphaMode::Blend); + CORRADE_COMPARE(material->alphaMask(), 0.5f); + } { + auto material = importer->material("opaque"); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 0); + CORRADE_COMPARE(material->alphaMode(), MaterialAlphaMode::Opaque); + CORRADE_COMPARE(material->alphaMask(), 0.5f); + } { + const char* name = "normal, occlusion, emissive texture"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 6); + + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::NormalTexture)); + CORRADE_COMPARE(pbr.normalTexture(), 1); + CORRADE_COMPARE(pbr.normalTextureScale(), 0.56f); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::OcclusionTexture)); + CORRADE_COMPARE(pbr.occlusionTexture(), 2); + CORRADE_COMPARE(pbr.occlusionTextureStrength(), 0.21f); + CORRADE_COMPARE(pbr.emissiveColor(), (Color3{0.1f, 0.2f, 0.3f})); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::EmissiveTexture)); + CORRADE_COMPARE(pbr.emissiveTexture(), 0); + } { + const char* name = "normal, occlusion, emissive texture identity transform"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 6); + + const auto& pbr = material->as(); + /* Identity transform, but is present */ + CORRADE_VERIFY(pbr.hasTextureTransformation()); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::NormalTexture)); + CORRADE_COMPARE(pbr.normalTextureMatrix(), Matrix3{}); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::OcclusionTexture)); + CORRADE_COMPARE(pbr.occlusionTextureMatrix(), Matrix3{}); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::EmissiveTexture)); + CORRADE_COMPARE(pbr.emissiveTextureMatrix(), Matrix3{}); + } { + const char* name = "normal, occlusion, emissive texture transform + sets"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 9); + + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::NormalTexture)); + CORRADE_COMPARE(pbr.normalTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.0f, -1.0f, 1.0f} + })); + CORRADE_COMPARE(pbr.normalTextureCoordinates(), 2); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::OcclusionTexture)); + CORRADE_COMPARE(pbr.occlusionTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, -1.0f, 1.0f} + })); + CORRADE_COMPARE(pbr.occlusionTextureCoordinates(), 3); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::EmissiveTexture)); + CORRADE_COMPARE(pbr.emissiveTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, 0.0f, 1.0f} + })); + CORRADE_COMPARE(pbr.emissiveTextureCoordinates(), 1); + } +} + +void CgltfImporterTest::materialUnlit() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "material-unlit.gltf"))); + CORRADE_COMPARE(importer->materialCount(), 1); + + auto material = importer->material(0); + CORRADE_VERIFY(material); + CORRADE_VERIFY(!material->importerState()); + /* Metallic/roughness is removed from types */ + CORRADE_COMPARE(material->types(), MaterialType::Flat); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 2); + + const auto& flat = material->as(); + CORRADE_COMPARE(flat.color(), (Color4{0.7f, 0.8f, 0.9f, 1.1f})); + CORRADE_VERIFY(flat.hasTexture()); + CORRADE_COMPARE(flat.texture(), 1); +} + +void CgltfImporterTest::materialClearCoat() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "material-clearcoat.gltf"))); + CORRADE_COMPARE(importer->materialCount(), 6); + + { + const char* name = "defaults"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->types(), MaterialType::PbrClearCoat); + CORRADE_COMPARE(material->layerCount(), 2); + CORRADE_VERIFY(material->hasLayer(MaterialLayer::ClearCoat)); + + /* These are glTF defaults, which are *not* consistent with ours */ + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.attributeCount(), 3); + CORRADE_COMPARE(pbr.layerFactor(), 0.0f); + CORRADE_COMPARE(pbr.roughness(), 0.0f); + } { + const char* name = "factors"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 2); + CORRADE_VERIFY(material->hasLayer(MaterialLayer::ClearCoat)); + + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.attributeCount(), 3); + CORRADE_COMPARE(pbr.layerFactor(), 0.67f); + CORRADE_COMPARE(pbr.roughness(), 0.34f); + } { + const char* name = "textures"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 2); + CORRADE_VERIFY(material->hasLayer(MaterialLayer::ClearCoat)); + + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.attributeCount(), 8); + CORRADE_COMPARE(pbr.layerFactor(), 0.7f); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::LayerFactorTexture)); + CORRADE_COMPARE(pbr.layerFactorTexture(), 2); + CORRADE_COMPARE(pbr.layerFactorTextureSwizzle(), MaterialTextureSwizzle::R); + CORRADE_COMPARE(pbr.roughness(), 0.4f); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::RoughnessTexture)); + CORRADE_COMPARE(pbr.roughnessTexture(), 1); + CORRADE_COMPARE(pbr.roughnessTextureSwizzle(), MaterialTextureSwizzle::G); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::NormalTexture)); + CORRADE_COMPARE(pbr.normalTexture(), 0); + CORRADE_COMPARE(pbr.normalTextureScale(), 0.35f); + } { + const char* name = "packed textures"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 2); + CORRADE_VERIFY(material->hasLayer(MaterialLayer::ClearCoat)); + + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.attributeCount(), 6); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::LayerFactorTexture)); + CORRADE_COMPARE(pbr.layerFactorTexture(), 1); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::RoughnessTexture)); + CORRADE_COMPARE(pbr.roughnessTexture(), 1); + CORRADE_VERIFY(pbr.hasLayerFactorRoughnessTexture()); + } { + const char* name = "texture identity transform"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 2); + CORRADE_VERIFY(material->hasLayer(MaterialLayer::ClearCoat)); + + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.attributeCount(), 7 + 3); + CORRADE_VERIFY(pbr.hasTextureTransformation()); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::LayerFactorTexture)); + CORRADE_COMPARE(pbr.layerFactorTextureMatrix(), Matrix3{}); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::RoughnessTexture)); + CORRADE_COMPARE(pbr.roughnessTextureMatrix(), Matrix3{}); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::NormalTexture)); + CORRADE_COMPARE(pbr.normalTextureMatrix(), Matrix3{}); + } { + const char* name = "texture transform + coordinate set"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 2); + CORRADE_VERIFY(material->hasLayer(MaterialLayer::ClearCoat)); + + const auto& pbr = material->as(); + CORRADE_COMPARE(pbr.attributeCount(), 13); + /* Identity transform, but is present */ + CORRADE_VERIFY(pbr.hasTextureTransformation()); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::LayerFactorTexture)); + CORRADE_COMPARE(pbr.layerFactorTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.0f, -1.0f, 1.0f} + })); + CORRADE_COMPARE(pbr.layerFactorTextureCoordinates(), 5); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::RoughnessTexture)); + CORRADE_COMPARE(pbr.roughnessTextureMatrix(), (Matrix3{ + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.5f, 0.0f}, + {0.0f, 0.5f, 1.0f} + })); + CORRADE_COMPARE(pbr.roughnessTextureCoordinates(), 1); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::NormalTexture)); + CORRADE_COMPARE(pbr.normalTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, 0.0f, 1.0f} + })); + CORRADE_COMPARE(pbr.normalTextureCoordinates(), 7); + } +} + +void CgltfImporterTest::materialPhongFallback() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* phongMaterialFallback should be on by default */ + //importer->configuration().setValue("phongMaterialFallback", true); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "material-phong-fallback.gltf"))); + CORRADE_COMPARE(importer->materialCount(), 4); + + { + const char* name = "none"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_VERIFY(!material->importerState()); + CORRADE_COMPARE(material->types(), MaterialType::Phong); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 0); + + /* These are glTF defaults, just verify those are consistent with + MaterialData API defaults (if they wouldn't be, we'd need to add + explicit attributes to override those) */ + const auto& phong = material->as(); + CORRADE_COMPARE(phong.diffuseColor(), (Color4{1.0f})); + CORRADE_COMPARE(phong.specularColor(), (Color4{1.0f, 0.0f})); + } { + const char* name = "metallic/roughness"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->types(), MaterialType::Phong|MaterialType::PbrMetallicRoughness); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 8); + + /* Original properties should stay */ + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::BaseColorTexture)); + CORRADE_COMPARE(pbr.baseColor(), (Color4{0.7f, 0.8f, 0.9f, 1.1f})); + CORRADE_COMPARE(pbr.baseColorTexture(), 1); + CORRADE_COMPARE(pbr.baseColorTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, -1.0f, 1.0f} + })); + CORRADE_COMPARE(pbr.baseColorTextureCoordinates(), 3); + + /* ... and should be copied into phong properties as well */ + const auto& phong = material->as(); + CORRADE_VERIFY(phong.hasAttribute(MaterialAttribute::DiffuseTexture)); + CORRADE_COMPARE(phong.diffuseColor(), (Color4{0.7f, 0.8f, 0.9f, 1.1f})); + CORRADE_COMPARE(phong.diffuseTexture(), 1); + CORRADE_COMPARE(phong.diffuseTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, -1.0f, 1.0f} + })); + CORRADE_COMPARE(phong.diffuseTextureCoordinates(), 3); + /* Defaults for specular */ + CORRADE_COMPARE(phong.specularColor(), (Color4{1.0f, 0.0f})); + CORRADE_VERIFY(!phong.hasSpecularTexture()); + } { + const char* name = "specular/glossiness"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->types(), MaterialType::Phong|MaterialType::PbrSpecularGlossiness); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 10); + + /* Original properties should stay */ + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::DiffuseTexture)); + CORRADE_COMPARE(pbr.diffuseColor(), (Color4{0.7f, 0.8f, 0.9f, 1.1f})); + CORRADE_COMPARE(pbr.diffuseTexture(), 1); + CORRADE_COMPARE(pbr.diffuseTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, -1.0f, 1.0f} + })); + CORRADE_COMPARE(pbr.diffuseTextureCoordinates(), 3); + CORRADE_COMPARE(pbr.specularColor(), (Color4{0.1f, 0.2f, 0.6f, 0.0f})); + CORRADE_COMPARE(pbr.specularTexture(), 0); + CORRADE_COMPARE(pbr.specularTextureMatrix(), (Matrix3{ + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.5f, 0.0f}, + {0.0f, 0.5f, 1.0f} + })); + CORRADE_COMPARE(pbr.specularTextureCoordinates(), 2); + + /* Phong recognizes them directly */ + const auto& phong = material->as(); + CORRADE_VERIFY(phong.hasAttribute(MaterialAttribute::DiffuseTexture)); + CORRADE_COMPARE(phong.diffuseColor(), (Color4{0.7f, 0.8f, 0.9f, 1.1f})); + CORRADE_COMPARE(phong.diffuseTexture(), 1); + CORRADE_COMPARE(phong.diffuseTextureMatrix(), (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, -1.0f, 1.0f} + })); + CORRADE_COMPARE(phong.diffuseTextureCoordinates(), 3); + CORRADE_COMPARE(phong.specularColor(), (Color4{0.1f, 0.2f, 0.6f, 0.0f})); + CORRADE_COMPARE(phong.specularTexture(), 0); + CORRADE_COMPARE(phong.specularTextureMatrix(), (Matrix3{ + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.5f, 0.0f}, + {0.0f, 0.5f, 1.0f} + })); + CORRADE_COMPARE(phong.specularTextureCoordinates(), 2); + } { + const char* name = "unlit"; + auto material = importer->material(name); + CORRADE_ITERATION(name); + CORRADE_VERIFY(material); + /* Phong type is added even for unlit materials, since that's how it + behaved before */ + CORRADE_COMPARE(material->types(), MaterialType::Phong|MaterialType::Flat); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 0); + } +} + +void CgltfImporterTest::materialOutOfBounds() { + auto&& data = MaterialOutOfBoundsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, data.file))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): error opening file: invalid glTF, usually caused by invalid indices or missing required attributes\n"); +} + +void CgltfImporterTest::materialInvalidAlphaMode() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Cgltf parses an invalid alpha mode as opaque, without any error */ + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "material-invalid-alpha-mode.gltf"))); + CORRADE_COMPARE(importer->materialCount(), 1); + + auto material = importer->material(0); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->alphaMode(), MaterialAlphaMode::Opaque); +} + +void CgltfImporterTest::materialTexCoordFlip() { + auto&& data = MaterialTexCoordFlipData[testCaseInstanceId()]; + setTestCaseDescription(Utility::formatString("{}{}", data.name, data.flipInMaterial ? ", textureCoordinateYFlipInMaterial" : "")); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* This should be implicitly enabled on files that contain non-normalized + integer texture coordinates */ + if(data.flipInMaterial) + importer->configuration().setValue("textureCoordinateYFlipInMaterial", true); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + data.fileName))); + + auto mesh = importer->mesh(data.meshName); + CORRADE_VERIFY(mesh); + CORRADE_VERIFY(mesh->hasAttribute(MeshAttribute::TextureCoordinates)); + Containers::Array texCoords = mesh->textureCoordinates2DAsArray(); + + /* Texture transform is added to materials that don't have it yet */ + auto material = importer->material(data.name); + CORRADE_VERIFY(material); + + auto& pbr = static_cast(*material); + CORRADE_COMPARE(pbr.hasTextureTransformation(), data.flipInMaterial || data.hasTextureTransformation); + CORRADE_VERIFY(pbr.hasCommonTextureTransformation()); + + /* Transformed texture coordinates should be the same regardless of the + setting */ + MeshTools::transformPointsInPlace(pbr.commonTextureMatrix(), texCoords); + CORRADE_COMPARE_AS(texCoords, Containers::arrayView({ + {1.0f, 0.5f}, + {0.5f, 1.0f}, + {0.0f, 0.0f} + }), TestSuite::Compare::Container); +} + +void CgltfImporterTest::texture() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "texture" + std::string{data.suffix}))); + CORRADE_COMPARE(importer->materialCount(), 1); + + auto material = importer->material(0); + + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->types(), MaterialType::PbrMetallicRoughness); + + const auto& pbr = material->as(); + CORRADE_VERIFY(pbr.hasAttribute(MaterialAttribute::BaseColorTexture)); + CORRADE_COMPARE(pbr.baseColorTexture(), 0); + + CORRADE_COMPARE(importer->textureCount(), 2); + CORRADE_COMPARE(importer->textureForName("Texture"), 1); + CORRADE_COMPARE(importer->textureName(1), "Texture"); + + auto texture = importer->texture(1); + CORRADE_VERIFY(texture); + CORRADE_VERIFY(!texture->importerState()); + CORRADE_COMPARE(texture->image(), 0); + CORRADE_COMPARE(texture->type(), TextureType::Texture2D); + + CORRADE_COMPARE(texture->magnificationFilter(), SamplerFilter::Nearest); + CORRADE_COMPARE(texture->minificationFilter(), SamplerFilter::Nearest); + CORRADE_COMPARE(texture->mipmapFilter(), SamplerMipmap::Nearest); + + CORRADE_COMPARE(texture->wrapping(), Math::Vector3(SamplerWrapping::MirroredRepeat, SamplerWrapping::ClampToEdge, SamplerWrapping::Repeat)); + + /* Texture coordinates */ + auto mesh = importer->mesh(0); + CORRADE_VERIFY(mesh); + + CORRADE_COMPARE(mesh->attributeCount(MeshAttribute::TextureCoordinates), 2); + CORRADE_COMPARE(mesh->attributeFormat(MeshAttribute::TextureCoordinates), VertexFormat::Vector2); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::TextureCoordinates, 0), + Containers::arrayView({ + {0.94991f, 0.05009f}, {0.3f, 0.94991f}, {0.1f, 0.2f} + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(mesh->attribute(MeshAttribute::TextureCoordinates, 1), + Containers::arrayView({ + {0.5f, 0.5f}, {0.3f, 0.7f}, {0.2f, 0.42f} + }), TestSuite::Compare::Container); +} + +void CgltfImporterTest::textureOutOfBounds() { + auto&& data = TextureOutOfBoundsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, data.file))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): error opening file: invalid glTF, usually caused by invalid indices or missing required attributes\n"); +} + +void CgltfImporterTest::textureInvalid() { + auto&& data = TextureInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "texture-invalid.gltf"))); + + /* Check we didn't forget to test anything */ + CORRADE_COMPARE(importer->textureCount(), Containers::arraySize(TextureInvalidData)); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->texture(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::texture(): {}\n", data.message)); +} + +void CgltfImporterTest::textureDefaultSampler() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "texture-default-sampler" + std::string{data.suffix}))); + + auto texture = importer->texture(0); + CORRADE_VERIFY(texture); + CORRADE_COMPARE(texture->image(), 0); + CORRADE_COMPARE(texture->type(), TextureType::Texture2D); + + CORRADE_COMPARE(texture->magnificationFilter(), SamplerFilter::Linear); + CORRADE_COMPARE(texture->minificationFilter(), SamplerFilter::Linear); + CORRADE_COMPARE(texture->mipmapFilter(), SamplerMipmap::Linear); + + CORRADE_COMPARE(texture->wrapping(), Math::Vector3(SamplerWrapping::Repeat, SamplerWrapping::Repeat, SamplerWrapping::Repeat)); +} + +void CgltfImporterTest::textureEmptySampler() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "texture-empty-sampler" + std::string{data.suffix}))); + + auto texture = importer->texture(0); + CORRADE_VERIFY(texture); + CORRADE_COMPARE(texture->image(), 0); + CORRADE_COMPARE(texture->type(), TextureType::Texture2D); + + CORRADE_COMPARE(texture->magnificationFilter(), SamplerFilter::Linear); + CORRADE_COMPARE(texture->minificationFilter(), SamplerFilter::Linear); + CORRADE_COMPARE(texture->mipmapFilter(), SamplerMipmap::Linear); + + CORRADE_COMPARE(texture->wrapping(), Math::Vector3(SamplerWrapping::Repeat, SamplerWrapping::Repeat, SamplerWrapping::Repeat)); +} + +void CgltfImporterTest::textureMissingSource() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "texture-missing-source.gltf"))); + CORRADE_COMPARE(importer->textureCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->texture(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::texture(): no image source found\n"); +} + +void CgltfImporterTest::textureExtensions() { + auto&& data = TextureExtensionsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "texture-extensions.gltf"))); + + /* Check we didn't forget to test anything */ + CORRADE_COMPARE(importer->textureCount(), Containers::arraySize(TextureExtensionsData)); + + auto texture = importer->texture(data.name); + CORRADE_VERIFY(texture); + CORRADE_COMPARE(texture->image(), data.id); +} + +void CgltfImporterTest::textureExtensionsOutOfBounds() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Cgltf only supports (and therefore checks) KHR_texture_basisu, so this + is the only texture extension leading to an error when opening. The rest + are checked in doTexture(), tested below in textureExtensionsInvalid(). */ + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "texture-extensions-invalid-basisu-oob.gltf"))); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::openData(): error opening file: invalid glTF, usually caused by invalid indices or missing required attributes\n"); +} + +void CgltfImporterTest::textureExtensionsInvalid() { + auto&& data = TextureExtensionsInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "texture-extensions-invalid.gltf"))); + + /* Check we didn't forget to test anything */ + CORRADE_COMPARE(importer->textureCount(), Containers::arraySize(TextureExtensionsInvalidData)); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->texture(data.name)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::texture(): {}\n", data.message)); +} + +constexpr char ExpectedImageData[] = + "\xa8\xa7\xac\xff\x9d\x9e\xa0\xff\xad\xad\xac\xff\xbb\xbb\xba\xff\xb3\xb4\xb6\xff" + "\xb0\xb1\xb6\xff\xa0\xa0\xa1\xff\x9f\x9f\xa0\xff\xbc\xbc\xba\xff\xcc\xcc\xcc\xff" + "\xb2\xb4\xb9\xff\xb8\xb9\xbb\xff\xc1\xc3\xc2\xff\xbc\xbd\xbf\xff\xb8\xb8\xbc\xff"; + +void CgltfImporterTest::imageEmbedded() { + auto&& data = ImageEmbeddedData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test"); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + /* Open as data, so we verify opening embedded images from data does not + cause any problems even when no file callbacks are set */ + CORRADE_VERIFY(importer->openData(Utility::Directory::read(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "image" + std::string{data.suffix})))); + + CORRADE_COMPARE(importer->image2DCount(), 2); + CORRADE_COMPARE(importer->image2DForName("Image"), 1); + CORRADE_COMPARE(importer->image2DName(1), "Image"); + + auto image = importer->image2D(1); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->importerState()); + CORRADE_COMPARE(image->size(), Vector2i(5, 3)); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE_AS(image->data(), Containers::arrayView(ExpectedImageData).prefix(60), TestSuite::Compare::Container); +} + +void CgltfImporterTest::imageExternal() { + auto&& data = ImageExternalData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test"); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "image" + std::string{data.suffix}))); + + CORRADE_COMPARE(importer->image2DCount(), 2); + CORRADE_COMPARE(importer->image2DForName("Image"), 1); + CORRADE_COMPARE(importer->image2DName(1), "Image"); + + auto image = importer->image2D(1); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->importerState()); + CORRADE_COMPARE(image->size(), Vector2i(5, 3)); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE_AS(image->data(), Containers::arrayView(ExpectedImageData).prefix(60), TestSuite::Compare::Container); +} + +void CgltfImporterTest::imageExternalNotFound() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, "image-notfound.gltf"))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->image2D(0)); + CORRADE_COMPARE(out.str(), "Trade::AbstractImporter::openFile(): cannot open file /nonexistent.png\n"); +} + +void CgltfImporterTest::imageExternalBufferNotFound() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, "image-buffer-notfound.gltf"))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->image2D(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::image2D(): error opening file: /nonexistent.bin : file not found\n"); +} + +void CgltfImporterTest::imageExternalNoPathNoCallback() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openData(Utility::Directory::read(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, "image.gltf")))); + CORRADE_COMPARE(importer->image2DCount(), 2); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->image2D(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::image2D(): external images can be imported only when opening files from the filesystem or if a file callback is present\n"); +} + +void CgltfImporterTest::imageNoData() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openData(Utility::Directory::read(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "image-no-data.gltf")))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->image2D(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::image2D(): image has neither a URI nor a buffer view\n"); +} + +void CgltfImporterTest::imageBasis() { + auto&& data = ImageBasisData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + /* Import as ASTC */ + _manager.metadata("BasisImporter")->configuration().setValue("format", "Astc4x4RGBA"); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "image-basis" + std::string{data.suffix}))); + + CORRADE_COMPARE(importer->textureCount(), 1); + CORRADE_COMPARE(importer->image2DCount(), 2); + + auto image = importer->image2D(1); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->importerState()); + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->size(), Vector2i(5, 3)); + CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Astc4x4RGBAUnorm); + + /* The texture refers to the image indirectly via an extension, test the + mapping */ + auto texture = importer->texture(0); + CORRADE_VERIFY(texture); + CORRADE_COMPARE(texture->image(), 1); +} + +void CgltfImporterTest::imageMipLevels() { + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + /* Import as RGBA so we can verify the pixels */ + _manager.metadata("BasisImporter")->configuration().setValue("format", "RGBA8"); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, "image-basis.gltf"))); + CORRADE_COMPARE(importer->image2DCount(), 2); + CORRADE_COMPARE(importer->image2DLevelCount(0), 1); + CORRADE_COMPARE(importer->image2DLevelCount(1), 2); + + /* Verify that loading a different image will properly switch to another + importer instance */ + Containers::Optional image0 = importer->image2D(0); + Containers::Optional image10 = importer->image2D(1); + Containers::Optional image11 = importer->image2D(1, 1); + + CORRADE_VERIFY(image0); + CORRADE_VERIFY(!image0->importerState()); + CORRADE_VERIFY(!image0->isCompressed()); + CORRADE_COMPARE(image0->size(), (Vector2i{5, 3})); + CORRADE_COMPARE(image0->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE_AS(Containers::arrayCast(image0->data()), + Containers::arrayView({ + 168, 167, 172, 255, 157, 158, 160, 255, 173, 173, 172, 255, + 187, 187, 186, 255, 179, 180, 182, 255, 176, 177, 182, 255, + 160, 160, 161, 255, 159, 159, 160, 255, 188, 188, 186, 255, + 204, 204, 204, 255, 178, 180, 185, 255, 184, 185, 187, 255, + 193, 195, 194, 255, 188, 189, 191, 255, 184, 184, 188, 255 + }), TestSuite::Compare::Container); + + CORRADE_VERIFY(image10); + CORRADE_VERIFY(!image10->importerState()); + CORRADE_VERIFY(!image10->isCompressed()); + CORRADE_COMPARE(image10->size(), (Vector2i{5, 3})); + CORRADE_COMPARE(image10->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE_AS(Containers::arrayCast(image10->data()), + Containers::arrayView({ + /* Should be different from the above because this is + Basis-encoded, not a PNG */ + 168, 168, 168, 255, 156, 156, 156, 255, 168, 168, 168, 255, + 190, 190, 190, 255, 182, 182, 190, 255, 178, 178, 178, 255, + 156, 156, 156, 255, 156, 156, 156, 255, 190, 190, 190, 255, + 202, 202, 210, 255, 178, 178, 178, 255, 190, 190, 190, 255, + 190, 190, 190, 255, 190, 190, 190, 255, 182, 182, 190, 255 + }), TestSuite::Compare::Container); + + CORRADE_VERIFY(image11); + CORRADE_VERIFY(!image11->importerState()); + CORRADE_VERIFY(!image11->isCompressed()); + CORRADE_COMPARE(image11->size(), (Vector2i{2, 1})); + CORRADE_COMPARE(image11->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE_AS(Containers::arrayCast(image11->data()), + Containers::arrayView({ + 172, 172, 181, 255, 184, 184, 193, 255 + }), TestSuite::Compare::Container); +} + +void CgltfImporterTest::fileCallbackBuffer() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->features() & ImporterFeature::FileCallback); + + Utility::Resource rs{"data"}; + importer->setFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, Utility::Resource& rs) { + Debug{} << "Loading" << filename << "with" << policy; + return Containers::optional(rs.getRaw(filename)); + }, rs); + + /* Using a different name from the filesystem to avoid false positive + when the file gets loaded from a filesystem */ + CORRADE_VERIFY(importer->openFile("some/path/data" + std::string{data.suffix})); + + CORRADE_COMPARE(importer->meshCount(), 1); + auto mesh = importer->mesh(0); + CORRADE_VERIFY(mesh); + CORRADE_COMPARE(mesh->primitive(), MeshPrimitive::Points); + CORRADE_VERIFY(!mesh->isIndexed()); + + CORRADE_COMPARE(mesh->attributeCount(), 1); + CORRADE_COMPARE_AS(mesh->positions3DAsArray(), Containers::arrayView({ + {1.0f, 2.0f, 3.0f} + }), TestSuite::Compare::Container); +} + +void CgltfImporterTest::fileCallbackBufferNotFound() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->features() & ImporterFeature::FileCallback); + + importer->setFileCallback([](const std::string&, InputFileCallbackPolicy, void*) + -> Containers::Optional> { return {}; }); + + Utility::Resource rs{"data"}; + CORRADE_VERIFY(importer->openData(rs.getRaw("some/path/data" + std::string{data.suffix}))); + CORRADE_COMPARE(importer->meshCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->mesh(0)); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::mesh(): error opening file: data.bin : file callback failed\n"); +} + +void CgltfImporterTest::fileCallbackImage() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test"); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->features() & ImporterFeature::FileCallback); + + Utility::Resource rs{"data"}; + importer->setFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, Utility::Resource& rs) { + Debug{} << "Loading" << filename << "with" << policy; + return Containers::optional(rs.getRaw(filename)); + }, rs); + + /* Using a different name from the filesystem to avoid false positive + when the file gets loaded from a filesystem */ + CORRADE_VERIFY(importer->openFile("some/path/data" + std::string{data.suffix})); + + CORRADE_COMPARE(importer->image2DCount(), 1); + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->size(), Vector2i(5, 3)); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE_AS(image->data(), Containers::arrayView(ExpectedImageData).prefix(60), TestSuite::Compare::Container); +} + +void CgltfImporterTest::fileCallbackImageNotFound() { + auto&& data = SingleFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test"); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->features() & ImporterFeature::FileCallback); + + Utility::Resource rs{"data"}; + importer->setFileCallback([](const std::string& filename, InputFileCallbackPolicy, Utility::Resource& rs) + -> Containers::Optional> + { + if(filename == "data.bin") + return rs.getRaw("some/path/data.bin"); + return {}; + }, rs); + + CORRADE_VERIFY(importer->openData(rs.getRaw("some/path/data" + std::string{data.suffix}))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->image2D(0)); + CORRADE_COMPARE(out.str(), "Trade::AbstractImporter::openFile(): cannot open file data.png\n"); +} + +void CgltfImporterTest::utf8filenames() { + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test"); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "přívodní-šňůra.gltf"))); + + CORRADE_COMPARE(importer->meshCount(), 1); + auto mesh = importer->mesh(0); + CORRADE_VERIFY(mesh); + CORRADE_COMPARE(mesh->primitive(), MeshPrimitive::Points); + CORRADE_VERIFY(!mesh->isIndexed()); + CORRADE_COMPARE(mesh->attributeCount(), 1); + CORRADE_COMPARE_AS(mesh->positions3DAsArray(0), Containers::arrayView({ + {1.0f, 2.0f, 3.0f} + }), TestSuite::Compare::Container); + + CORRADE_COMPARE(importer->image2DCount(), 1); + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->size(), Vector2i(5, 3)); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE_AS(image->data(), Containers::arrayView(ExpectedImageData).prefix(60), TestSuite::Compare::Container); +} + +void CgltfImporterTest::escapedStrings() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "escaped-strings.gltf"))); + + CORRADE_COMPARE(importer->object3DCount(), 6); + CORRADE_COMPARE(importer->object3DName(0), ""); + CORRADE_COMPARE(importer->object3DName(1), "UTF-8: Лорем ипсум долор сит амет"); + CORRADE_COMPARE(importer->object3DName(2), "UTF-8 escaped: Лорем ипсум долор сит амет"); + CORRADE_COMPARE(importer->object3DName(3), "Special: \"/\\\b\f\r\n\t"); + CORRADE_COMPARE(importer->object3DName(4), "Everything: říční člun \t\t\n حليب اللوز"); + /* Keys (in this case, "name") are not decoded by cgltf. Old versions of + the spec used to forbid non-ASCII keys or enums: + https://github.com/KhronosGroup/glTF/tree/fd3ab461a1114fb0250bd76099153d2af50a7a1d/specification/2.0#json-encoding + Newer spec versions changed this to "ASCII characters [...] SHOULD be + written without JSON escaping" */ + CORRADE_COMPARE(importer->object3DName(5), ""); + + /* All user-facing strings are unescaped. URIs are tested in encodedUris(). */ + CORRADE_COMPARE(importer->animationCount(), 1); + CORRADE_COMPARE(importer->animationName(0), "Everything: říční člun \t\t\n حليب اللوز"); + CORRADE_COMPARE(importer->cameraCount(), 1); + CORRADE_COMPARE(importer->cameraName(0), "Everything: říční člun \t\t\n حليب اللوز"); + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DName(0), "Everything: říční člun \t\t\n حليب اللوز"); + CORRADE_COMPARE(importer->lightCount(), 1); + CORRADE_COMPARE(importer->lightName(0), "Everything: říční člun \t\t\n حليب اللوز"); + CORRADE_COMPARE(importer->materialCount(), 1); + CORRADE_COMPARE(importer->materialName(0), "Everything: říční člun \t\t\n حليب اللوز"); + CORRADE_COMPARE(importer->meshCount(), 1); + CORRADE_COMPARE(importer->meshName(0), "Everything: říční člun \t\t\n حليب اللوز"); + CORRADE_COMPARE(importer->sceneCount(), 1); + CORRADE_COMPARE(importer->sceneName(0), "Everything: říční člun \t\t\n حليب اللوز"); + CORRADE_COMPARE(importer->skin3DCount(), 1); + CORRADE_COMPARE(importer->skin3DName(0), "Everything: říční člun \t\t\n حليب اللوز"); + CORRADE_COMPARE(importer->textureCount(), 1); + CORRADE_COMPARE(importer->textureName(0), "Everything: říční člun \t\t\n حليب اللوز"); +} + +void CgltfImporterTest::encodedUris() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + CORRADE_VERIFY(importer->features() & ImporterFeature::FileCallback); + + std::string strings[6]; + + importer->setFileCallback([](const std::string& filename, InputFileCallbackPolicy, std::string (&strings)[6]) + -> Containers::Optional> + { + static const char bytes[4]{}; + if(filename.find("buffer-unencoded") == 0) + strings[0] = filename; + else if(filename.find("buffer-encoded") == 0) + strings[1] = filename; + else if(filename.find("buffer-escaped") == 0) + strings[2] = filename; + else if(filename.find("image-unencoded") == 0) + strings[3] = filename; + else if(filename.find("image-encoded") == 0) + strings[4] = filename; + else if(filename.find("image-escaped") == 0) + strings[5] = filename; + return Containers::arrayView(bytes); + }, strings); + + /* Prevent the file callback being used for the main glTF content */ + const auto data = Utility::Directory::read(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "encoded-uris.gltf")); + CORRADE_VERIFY(importer->openData(data)); + + CORRADE_COMPARE(importer->meshCount(), 3); + /* We don't care about the result, only the callback being invoked */ + importer->mesh(0); + importer->mesh(1); + importer->mesh(2); + + CORRADE_COMPARE(importer->image2DCount(), 3); + importer->image2D(0); + importer->image2D(1); + importer->image2D(2); + + CORRADE_COMPARE(strings[0], "buffer-unencoded/@file#.bin"); + CORRADE_COMPARE(strings[1], "buffer-encoded/@file#.bin"); + CORRADE_COMPARE(strings[2], "buffer-escaped/říční člun.bin"); + CORRADE_COMPARE(strings[3], "image-unencoded/image #1.png"); + CORRADE_COMPARE(strings[4], "image-encoded/image #1.png"); + CORRADE_COMPARE(strings[5], "image-escaped/říční člun.png"); +} + +void CgltfImporterTest::versionSupported() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, + "version-supported.gltf"))); +} + +void CgltfImporterTest::versionUnsupported() { + auto&& data = UnsupportedVersionData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR, data.file))); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::CgltfImporter::openData(): {}\n", data.message)); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Trade::Test::CgltfImporterTest) diff --git a/src/MagnumPlugins/CgltfImporter/Test/README.md b/src/MagnumPlugins/CgltfImporter/Test/README.md new file mode 100644 index 000000000..124c0e2ba --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/README.md @@ -0,0 +1,19 @@ +Shared test files +================= + +Most of the test files are shared with TinyGltfImporter and are located in its +Test directory. Files that can't or don't make sense to be shared reside in +this folder. + +Updating test files +=================== + +Similarly to TinyGltfImporter's test files, `*.gltf` files are "golden sources" +which are then possibly converted to `*.glb` files. See +TinyGltfImporter/Test/README.md for more information. + +Batch conversion +---------------- + +The `convert.sh` script is a convenience tool that executes all conversion +routines on all files. diff --git a/src/MagnumPlugins/CgltfImporter/Test/animation-buffer-notfound.gltf b/src/MagnumPlugins/CgltfImporter/Test/animation-buffer-notfound.gltf new file mode 100644 index 000000000..850fe6191 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/animation-buffer-notfound.gltf @@ -0,0 +1,114 @@ +{ + "asset": { + "version": "2.0" + }, + "animations": [ + { + "name": "input buffer not found", + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "rotation" + } + } + ], + "samplers": [ + { + "input": 2, + "interpolation": "LINEAR", + "output": 1 + } + ] + }, + { + "name": "output buffer not found", + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "rotation" + } + } + ], + "samplers": [ + { + "input": 0, + "interpolation": "LINEAR", + "output": 3 + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5126, + "count": 9, + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 9, + "type": "VEC4" + }, + { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 9, + "type": "SCALAR" + }, + { + "bufferView": 3, + "byteOffset": 0, + "componentType": 5126, + "count": 9, + "type": "VEC4" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 36 + }, + { + "buffer": 0, + "byteOffset": 36, + "byteLength": 192 + }, + { + "buffer": 1, + "byteOffset": 0, + "byteLength": 36 + }, + { + "buffer": 2, + "byteOffset": 36, + "byteLength": 192 + } + ], + "buffers": [ + { + "byteLength": 228, + "uri": "animation-patching-fixed.bin" + }, + { + "byteLength": 228, + "uri": "/nonexistent1.bin" + }, + { + "byteLength": 228, + "uri": "/nonexistent2.bin" + } + ], + "nodes": [ + {} + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/animation-invalid-types.gltf b/src/MagnumPlugins/CgltfImporter/Test/animation-invalid-types.gltf new file mode 100644 index 000000000..25d47c8d3 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/animation-invalid-types.gltf @@ -0,0 +1,126 @@ +{ + "asset": { + "version": "2.0" + }, + "animations": [ + { + "name": "unknown type", + "note": "not in TinyGltfImporter tests because TinyGLTF produces an error during parsing", + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "rotation" + } + } + ], + "samplers": [ + { + "input": 0, + "interpolation": "LINEAR", + "output": 2 + } + ] + }, + { + "name": "unknown component type", + "note": "not in TinyGltfImporter tests because TinyGLTF produces an error during parsing", + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "translation" + } + } + ], + "samplers": [ + { + "input": 3, + "interpolation": "LINEAR", + "output": 1 + } + ] + }, + { + "name": "normalized float", + "note": "not in TinyGltfImporter tests because it doesn't check for normalized floats", + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "scale" + } + } + ], + "samplers": [ + { + "input": 0, + "interpolation": "LINEAR", + "output": 4 + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5126, + "count": 3, + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 3, + "type": "VEC4" + }, + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5121, + "count": 3, + "type": "WAT" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 1234, + "count": 3, + "type": "MAT2" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "normalized": true, + "count": 3, + "type": "VEC3" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 36 + }, + { + "buffer": 0, + "byteOffset": 36, + "byteLength": 192 + } + ], + "buffers": [ + { + "byteLength": 228, + "uri": "animation-patching-fixed.bin" + } + ], + "nodes": [ + {} + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/animation-patching-fixed.bin b/src/MagnumPlugins/CgltfImporter/Test/animation-patching-fixed.bin new file mode 100644 index 000000000..a08b0a734 Binary files /dev/null and b/src/MagnumPlugins/CgltfImporter/Test/animation-patching-fixed.bin differ diff --git a/src/MagnumPlugins/CgltfImporter/Test/animation-patching-fixed.bin.in b/src/MagnumPlugins/CgltfImporter/Test/animation-patching-fixed.bin.in new file mode 100644 index 000000000..acf2719cc --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/animation-patching-fixed.bin.in @@ -0,0 +1,26 @@ +type = "<9f 36f 3f 12f" +input = [ + # time + 0, 1, 2, 3, 4, 5, 6, 7, 8, + + # quaternion shortest-path optimization, rotation around Z + 0, 0, 0.92388, -0.382683, # 225° + 0, 0, 0.707107, -0.707107, # 270° + 0, 0, 0.382683, -0.92388, # 315° + 0, 0, 0, -1, # 360° + 0, 0, 0.382683, 0.92388, # 45° + 0, 0, 0.707107, 0.707107, # 90° + 0, 0, -0.92388, -0.382683, # 135° + 0, 0, -1, 0, # 180° + 0, 0, 0.92388, -0.382683, # 225° + + # time + 9, 10, 11, + + # quaternion normalization + 0, 0, 0.382683, 0.92388, # 45° + 0, 0, 0.707107*2, 0.707107*2, # 90°, denormalized + 0, 0, 0.382683*2, 0.92388*2, # 45°, denormalized +] + +# kate: hl python diff --git a/src/MagnumPlugins/CgltfImporter/Test/animation-patching-fixed.gltf b/src/MagnumPlugins/CgltfImporter/Test/animation-patching-fixed.gltf new file mode 100644 index 000000000..c6c3b4b21 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/animation-patching-fixed.gltf @@ -0,0 +1,92 @@ +{ + "asset": { + "version": "2.0", + "note": "Identical to animation-patching.gltf in TinyGltfImporter/Test, but with a .bin file without a shared time track. cgltf doesn't allow differently sized input and output accessors" + }, + "animations": [ + { + "name": "Quaternion shortest-path patching", + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "rotation" + } + } + ], + "samplers": [ + { + "input": 0, + "interpolation": "LINEAR", + "output": 1 + } + ] + }, + { + "name": "Quaternion normalization patching", + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "rotation" + } + } + ], + "samplers": [ + { + "input": 2, + "interpolation": "LINEAR", + "output": 3 + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5126, + "count": 9, + "type": "SCALAR" + }, + { + "bufferView": 0, + "byteOffset": 36, + "componentType": 5126, + "count": 9, + "type": "VEC4" + }, + { + "bufferView": 0, + "byteOffset": 180, + "componentType": 5126, + "count": 3, + "type": "SCALAR" + }, + { + "bufferView": 0, + "byteOffset": 192, + "componentType": 5126, + "count": 3, + "type": "VEC4" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 240 + } + ], + "buffers": [ + { + "byteLength": 240, + "uri": "animation-patching-fixed.bin" + } + ], + "nodes": [ + {} + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size-embedded.glb b/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size-embedded.glb new file mode 100644 index 000000000..2f07f485e Binary files /dev/null and b/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size-embedded.glb differ diff --git a/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size-embedded.gltf b/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size-embedded.gltf new file mode 100644 index 000000000..7c058f598 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size-embedded.gltf @@ -0,0 +1 @@ +{"asset":{"version":"2.0"},"buffers":[{"note":"the real length is 12","byteLength":24,"uri":"data:application/octet-stream;base64,AACAPwAAAEAAAEBA"}],"meshes":[{"primitives":[{"mode":0,"attributes":{"COLOR_0":0}}]}],"accessors":[{"bufferView":0,"componentType":5123,"normalized":true,"count":4,"type":"VEC3"}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":24}]} \ No newline at end of file diff --git a/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size.glb b/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size.glb new file mode 100644 index 000000000..2f07f485e Binary files /dev/null and b/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size.glb differ diff --git a/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size.gltf b/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size.gltf new file mode 100644 index 000000000..9cb54cede --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/buffer-short-size.gltf @@ -0,0 +1,40 @@ +{ + "asset": { + "version": "2.0" + }, + "buffers": [ + { + "note": "the real length is 12", + "byteLength": 24, + "uri": "external-data.bin" + } + ], + "meshes": [ + { + "primitives": [ + { + "mode": 0, + "attributes": { + "COLOR_0": 0 + } + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5123, + "normalized": true, + "count": 4, + "type": "VEC3" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 24 + } + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/configure.h.cmake b/src/MagnumPlugins/CgltfImporter/Test/configure.h.cmake new file mode 100644 index 000000000..96bc2e9b2 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/configure.h.cmake @@ -0,0 +1,31 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + Copyright © 2021 Pablo Escobar + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#cmakedefine CGLTFIMPORTER_PLUGIN_FILENAME "${CGLTFIMPORTER_PLUGIN_FILENAME}" +#cmakedefine BASISIMPORTER_PLUGIN_FILENAME "${BASISIMPORTER_PLUGIN_FILENAME}" +#cmakedefine STBIMAGEIMPORTER_PLUGIN_FILENAME "${STBIMAGEIMPORTER_PLUGIN_FILENAME}" +#define TINYGLTFIMPORTER_TEST_DIR "${TINYGLTFIMPORTER_TEST_DIR}" +#define CGLTFIMPORTER_TEST_DIR "${CGLTFIMPORTER_TEST_DIR}" diff --git a/src/MagnumPlugins/CgltfImporter/Test/convert.sh b/src/MagnumPlugins/CgltfImporter/Test/convert.sh new file mode 100644 index 000000000..eafcc13bd --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/convert.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +# in -> bin +for i in *.bin.in; do + ../../TinyGltfImporter/Test/in2bin.py ${i} +done + +# gltf -> embedded gltf +for i in buffer-short-size; do + ../../TinyGltfImporter/Test/gltf2embedded.py ${i}.gltf +done + +# gltf -> glb +for i in buffer-short-size buffer-short-size-embedded; do + ../../TinyGltfImporter/Test/gltf2glb.py ${i}.gltf +done + +# special cases +../../TinyGltfImporter/Test/gltf2glb.py external-data-order.gltf --no-embed diff --git a/src/MagnumPlugins/CgltfImporter/Test/external-data-order.glb b/src/MagnumPlugins/CgltfImporter/Test/external-data-order.glb new file mode 100644 index 000000000..df655af9a Binary files /dev/null and b/src/MagnumPlugins/CgltfImporter/Test/external-data-order.glb differ diff --git a/src/MagnumPlugins/CgltfImporter/Test/external-data-order.gltf b/src/MagnumPlugins/CgltfImporter/Test/external-data-order.gltf new file mode 100644 index 000000000..7faaa3fb2 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/external-data-order.gltf @@ -0,0 +1,113 @@ +{ + "asset": { + "version": "2.0" + }, + "meshes": [ + { + "primitives": [ + { + "mode": 0, + "attributes": { + "POSITION": 0 + } + } + ] + }, + { + "primitives": [ + { + "mode": 0, + "attributes": { + "POSITION": 1 + } + } + ] + }, + { + "primitives": [ + { + "mode": 0, + "attributes": { + "POSITION": 2 + } + } + ] + }, + { + "primitives": [ + { + "mode": 0, + "attributes": { + "POSITION": 3 + } + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 1, + "type": "VEC3" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 1, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5126, + "count": 1, + "type": "VEC3" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 1, + "type": "VEC3" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 12 + }, + { + "buffer": 1, + "byteLength": 12 + }, + { + "buffer": 0, + "byteLength": 12 + }, + { + "buffer": 2, + "byteLength": 12 + } + ], + "buffers": [ + { + "byteLength": 12, + "uri": "data1.bin" + }, + { + "byteLength": 12, + "uri": "data2.bin" + }, + { + "byteLength": 12, + "uri": "data1.bin" + } + ], + "images": [ + { + "uri": "data.png" + }, + { + "uri": "data.png" + } + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/external-data.bin b/src/MagnumPlugins/CgltfImporter/Test/external-data.bin new file mode 100644 index 000000000..2bb9e1512 Binary files /dev/null and b/src/MagnumPlugins/CgltfImporter/Test/external-data.bin differ diff --git a/src/MagnumPlugins/CgltfImporter/Test/external-data.bin.in b/src/MagnumPlugins/CgltfImporter/Test/external-data.bin.in new file mode 100644 index 000000000..88bf0bca3 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/external-data.bin.in @@ -0,0 +1,4 @@ +type = "<3f" +input = [ + 1.0, 2.0, 3.0 +] diff --git a/src/MagnumPlugins/CgltfImporter/Test/image-buffer-notfound.gltf b/src/MagnumPlugins/CgltfImporter/Test/image-buffer-notfound.gltf new file mode 100644 index 000000000..185a6d7cf --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/image-buffer-notfound.gltf @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "2.0" + }, + "images": [ + { + "bufferView": 0, + "mimeType": "image/png" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 68 + } + ], + "buffers": [ + { + "byteLength": 68, + "uri": "/nonexistent.bin" + } + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/image-no-data.gltf b/src/MagnumPlugins/CgltfImporter/Test/image-no-data.gltf new file mode 100644 index 000000000..e03835616 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/image-no-data.gltf @@ -0,0 +1,9 @@ +{ + "asset": { + "version": "2.0" + }, + "images": [ + { + } + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/mesh-indices-buffer-notfound.gltf b/src/MagnumPlugins/CgltfImporter/Test/mesh-indices-buffer-notfound.gltf new file mode 100644 index 000000000..77b7f238b --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/mesh-indices-buffer-notfound.gltf @@ -0,0 +1,37 @@ +{ + "asset": { + "version": "2.0" + }, + "meshes": [ + { + "name": "indices buffer not found", + "primitives": [ + { + "attributes": {}, + "indices": 0 + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5121, + "count": 3, + "type": "SCALAR" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 3 + } + ], + "buffers": [ + { + "byteLength": 160, + "uri": "/nonexistent.bin" + } + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/mesh-invalid-types.gltf b/src/MagnumPlugins/CgltfImporter/Test/mesh-invalid-types.gltf new file mode 100644 index 000000000..59ba1b74a --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/mesh-invalid-types.gltf @@ -0,0 +1,54 @@ +{ + "asset": { + "version": "2.0", + "note": "these are not in TinyGltfImporter test files because TinyGLTF produces an error during parsing" + }, + "meshes": [ + { + "name": "unknown type", + "primitives": [ + { + "attributes": { + "_THING": 0 + } + } + ] + }, + { + "name": "unknown component type", + "primitives": [ + { + "attributes": { + "_THING": 1 + } + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 0, + "type": "EEE" + }, + { + "bufferView": 0, + "componentType": 9999, + "count": 0, + "type": "SCALAR" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 0 + } + ], + "buffers": [ + { + "byteLength": 0, + "uri": "external-data.bin" + } + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/resources.conf b/src/MagnumPlugins/CgltfImporter/Test/resources.conf new file mode 100644 index 000000000..684aa029e --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/resources.conf @@ -0,0 +1,25 @@ +group=data + +[file] +filename=../../TinyGltfImporter/Test/external-data.gltf +alias=some/path/data.gltf + +[file] +filename=../../TinyGltfImporter/Test/external-data.bin +alias=some/path/data.bin + +[file] +filename=../../TinyGltfImporter/Test/external-data.glb +alias=some/path/data.glb + +[file] +filename=../../TinyGltfImporter/Test/texture.png +alias=some/path/data.png + +[file] +filename=../../TinyGltfImporter/Test/external-data.bin +alias=some/path/data1.bin + +[file] +filename=../../TinyGltfImporter/Test/external-data.bin +alias=some/path/data2.bin diff --git a/src/MagnumPlugins/CgltfImporter/Test/skin-buffer-notfound.gltf b/src/MagnumPlugins/CgltfImporter/Test/skin-buffer-notfound.gltf new file mode 100644 index 000000000..0dfa9f93e --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/skin-buffer-notfound.gltf @@ -0,0 +1,38 @@ +{ + "asset": { + "version": "2.0" + }, + "nodes": [ + {}, + {} + ], + "skins": [ + { + "name": "buffer not found", + "inverseBindMatrices": 0, + "joints": [0, 1] + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5126, + "count": 2, + "type": "MAT4" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 192 + } + ], + "buffers": [ + { + "byteLength": 192, + "uri": "/nonexistent.bin" + } + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/skin-invalid-types.gltf b/src/MagnumPlugins/CgltfImporter/Test/skin-invalid-types.gltf new file mode 100644 index 000000000..6eb88abb0 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/skin-invalid-types.gltf @@ -0,0 +1,66 @@ +{ + "asset": { + "version": "2.0" + }, + "nodes": [ + {}, + {} + ], + "skins": [ + { + "name": "unknown type", + "note": "not in TinyGltfImporter tests because TinyGLTF produces an error during parsing", + "inverseBindMatrices": 0, + "joints": [0, 1] + }, + { + "name": "unknown component type", + "note": "not in TinyGltfImporter tests because TinyGLTF produces an error during parsing", + "inverseBindMatrices": 1, + "joints": [0, 1] + }, + { + "name": "normalized float", + "note": "not in TinyGltfImporter tests because it doesn't check for normalized floats", + "inverseBindMatrices": 2, + "joints": [0, 1] + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5126, + "count": 2, + "type": "NANI?" + }, + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 999, + "count": 2, + "type": "MAT4" + }, + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5126, + "normalized": true, + "count": 2, + "type": "MAT4" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 192 + } + ], + "buffers": [ + { + "byteLength": 192, + "uri": "external-data.bin" + } + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/uri-invalid.gltf b/src/MagnumPlugins/CgltfImporter/Test/uri-invalid.gltf new file mode 100644 index 000000000..abfaa0bea --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/uri-invalid.gltf @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "2.0" + }, + "images": [ + { + "name": "no payload", + "uri": "data:image/png" + }, + { + "name": "no base64", + "uri": "data:image/png,hello" + }, + { + "name": "empty base64", + "uri": "data:image/png;base64," + }, + { + "name": "invalid base64", + "uri": "data:image/png;base64,aGVsbG9oZWxsb?==" + } + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/configure.h.cmake b/src/MagnumPlugins/CgltfImporter/configure.h.cmake new file mode 100644 index 000000000..5a66139cc --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/configure.h.cmake @@ -0,0 +1,27 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + Copyright © 2021 Pablo Escobar + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#cmakedefine MAGNUM_CGLTFIMPORTER_BUILD_STATIC diff --git a/src/MagnumPlugins/CgltfImporter/importStaticPlugin.cpp b/src/MagnumPlugins/CgltfImporter/importStaticPlugin.cpp new file mode 100644 index 000000000..97a8cf966 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/importStaticPlugin.cpp @@ -0,0 +1,36 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + Copyright © 2021 Pablo Escobar + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "MagnumPlugins/CgltfImporter/configure.h" + +#ifdef MAGNUM_CGLTFIMPORTER_BUILD_STATIC +#include + +static int magnumCgltfImporterStaticImporter() { + CORRADE_PLUGIN_IMPORT(CgltfImporter) + return 1; +} CORRADE_AUTOMATIC_INITIALIZER(magnumCgltfImporterStaticImporter) +#endif diff --git a/src/external/cgltf/cgltf.h b/src/external/cgltf/cgltf.h new file mode 100644 index 000000000..eb6a5b084 --- /dev/null +++ b/src/external/cgltf/cgltf.h @@ -0,0 +1,6507 @@ +/** + * cgltf - a single-file glTF 2.0 parser written in C99. + * + * Version: 1.11 + * + * Website: https://github.com/jkuhlmann/cgltf + * + * Distributed under the MIT License, see notice at the end of this file. + * + * Building: + * Include this file where you need the struct and function + * declarations. Have exactly one source file where you define + * `CGLTF_IMPLEMENTATION` before including this file to get the + * function definitions. + * + * Reference: + * `cgltf_result cgltf_parse(const cgltf_options*, const void*, + * cgltf_size, cgltf_data**)` parses both glTF and GLB data. If + * this function returns `cgltf_result_success`, you have to call + * `cgltf_free()` on the created `cgltf_data*` variable. + * Note that contents of external files for buffers and images are not + * automatically loaded. You'll need to read these files yourself using + * URIs in the `cgltf_data` structure. + * + * `cgltf_options` is the struct passed to `cgltf_parse()` to control + * parts of the parsing process. You can use it to force the file type + * and provide memory allocation as well as file operation callbacks. + * Should be zero-initialized to trigger default behavior. + * + * `cgltf_data` is the struct allocated and filled by `cgltf_parse()`. + * It generally mirrors the glTF format as described by the spec (see + * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0). + * + * `void cgltf_free(cgltf_data*)` frees the allocated `cgltf_data` + * variable. + * + * `cgltf_result cgltf_load_buffers(const cgltf_options*, cgltf_data*, + * const char* gltf_path)` can be optionally called to open and read buffer + * files using the `FILE*` APIs. The `gltf_path` argument is the path to + * the original glTF file, which allows the parser to resolve the path to + * buffer files. + * + * `cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, + * cgltf_size size, const char* base64, void** out_data)` decodes + * base64-encoded data content. Used internally by `cgltf_load_buffers()`. + * This is useful when decoding data URIs in images. + * + * `cgltf_result cgltf_parse_file(const cgltf_options* options, const + * char* path, cgltf_data** out_data)` can be used to open the given + * file using `FILE*` APIs and parse the data using `cgltf_parse()`. + * + * `cgltf_result cgltf_validate(cgltf_data*)` can be used to do additional + * checks to make sure the parsed glTF data is valid. + * + * `cgltf_node_transform_local` converts the translation / rotation / scale properties of a node + * into a mat4. + * + * `cgltf_node_transform_world` calls `cgltf_node_transform_local` on every ancestor in order + * to compute the root-to-node transformation. + * + * `cgltf_accessor_unpack_floats` reads in the data from an accessor, applies sparse data (if any), + * and converts them to floating point. Assumes that `cgltf_load_buffers` has already been called. + * By passing null for the output pointer, users can find out how many floats are required in the + * output buffer. + * + * `cgltf_accessor_num_components` is a tiny utility that tells you the dimensionality of + * a certain accessor type. This can be used before `cgltf_accessor_unpack_floats` to help allocate + * the necessary amount of memory. + * + * `cgltf_accessor_read_float` reads a certain element from a non-sparse accessor and converts it to + * floating point, assuming that `cgltf_load_buffers` has already been called. The passed-in element + * size is the number of floats in the output buffer, which should be in the range [1, 16]. Returns + * false if the passed-in element_size is too small, or if the accessor is sparse. + * + * `cgltf_accessor_read_uint` is similar to its floating-point counterpart, but limited to reading + * vector types and does not support matrix types. The passed-in element size is the number of uints + * in the output buffer, which should be in the range [1, 4]. Returns false if the passed-in + * element_size is too small, or if the accessor is sparse. + * + * `cgltf_accessor_read_index` is similar to its floating-point counterpart, but it returns size_t + * and only works with single-component data types. + * + * `cgltf_result cgltf_copy_extras_json(const cgltf_data*, const cgltf_extras*, + * char* dest, cgltf_size* dest_size)` allows users to retrieve the "extras" data that + * can be attached to many glTF objects (which can be arbitrary JSON data). The + * `cgltf_extras` struct stores the offsets of the start and end of the extras JSON data + * as it appears in the complete glTF JSON data. This function copies the extras data + * into the provided buffer. If `dest` is NULL, the length of the data is written into + * `dest_size`. You can then parse this data using your own JSON parser + * or, if you've included the cgltf implementation using the integrated JSMN JSON parser. + */ +#ifndef CGLTF_H_INCLUDED__ +#define CGLTF_H_INCLUDED__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef size_t cgltf_size; +typedef float cgltf_float; +typedef int cgltf_int; +typedef unsigned int cgltf_uint; +typedef int cgltf_bool; + +typedef enum cgltf_file_type +{ + cgltf_file_type_invalid, + cgltf_file_type_gltf, + cgltf_file_type_glb, +} cgltf_file_type; + +typedef enum cgltf_result +{ + cgltf_result_success, + cgltf_result_data_too_short, + cgltf_result_unknown_format, + cgltf_result_invalid_json, + cgltf_result_invalid_gltf, + cgltf_result_invalid_options, + cgltf_result_file_not_found, + cgltf_result_io_error, + cgltf_result_out_of_memory, + cgltf_result_legacy_gltf, +} cgltf_result; + +typedef struct cgltf_memory_options +{ + void* (*alloc)(void* user, cgltf_size size); + void (*free) (void* user, void* ptr); + void* user_data; +} cgltf_memory_options; + +typedef struct cgltf_file_options +{ + cgltf_result(*read)(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, const char* path, cgltf_size* size, void** data); + void (*release)(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, void* data); + void* user_data; +} cgltf_file_options; + +typedef struct cgltf_options +{ + cgltf_file_type type; /* invalid == auto detect */ + cgltf_size json_token_count; /* 0 == auto */ + cgltf_memory_options memory; + cgltf_file_options file; +} cgltf_options; + +typedef enum cgltf_buffer_view_type +{ + cgltf_buffer_view_type_invalid, + cgltf_buffer_view_type_indices, + cgltf_buffer_view_type_vertices, +} cgltf_buffer_view_type; + +typedef enum cgltf_attribute_type +{ + cgltf_attribute_type_invalid, + cgltf_attribute_type_position, + cgltf_attribute_type_normal, + cgltf_attribute_type_tangent, + cgltf_attribute_type_texcoord, + cgltf_attribute_type_color, + cgltf_attribute_type_joints, + cgltf_attribute_type_weights, +} cgltf_attribute_type; + +typedef enum cgltf_component_type +{ + cgltf_component_type_invalid, + cgltf_component_type_r_8, /* BYTE */ + cgltf_component_type_r_8u, /* UNSIGNED_BYTE */ + cgltf_component_type_r_16, /* SHORT */ + cgltf_component_type_r_16u, /* UNSIGNED_SHORT */ + cgltf_component_type_r_32u, /* UNSIGNED_INT */ + cgltf_component_type_r_32f, /* FLOAT */ +} cgltf_component_type; + +typedef enum cgltf_type +{ + cgltf_type_invalid, + cgltf_type_scalar, + cgltf_type_vec2, + cgltf_type_vec3, + cgltf_type_vec4, + cgltf_type_mat2, + cgltf_type_mat3, + cgltf_type_mat4, +} cgltf_type; + +typedef enum cgltf_primitive_type +{ + cgltf_primitive_type_points, + cgltf_primitive_type_lines, + cgltf_primitive_type_line_loop, + cgltf_primitive_type_line_strip, + cgltf_primitive_type_triangles, + cgltf_primitive_type_triangle_strip, + cgltf_primitive_type_triangle_fan, +} cgltf_primitive_type; + +typedef enum cgltf_alpha_mode +{ + cgltf_alpha_mode_opaque, + cgltf_alpha_mode_mask, + cgltf_alpha_mode_blend, +} cgltf_alpha_mode; + +typedef enum cgltf_animation_path_type { + cgltf_animation_path_type_invalid, + cgltf_animation_path_type_translation, + cgltf_animation_path_type_rotation, + cgltf_animation_path_type_scale, + cgltf_animation_path_type_weights, +} cgltf_animation_path_type; + +typedef enum cgltf_interpolation_type { + cgltf_interpolation_type_linear, + cgltf_interpolation_type_step, + cgltf_interpolation_type_cubic_spline, +} cgltf_interpolation_type; + +typedef enum cgltf_camera_type { + cgltf_camera_type_invalid, + cgltf_camera_type_perspective, + cgltf_camera_type_orthographic, +} cgltf_camera_type; + +typedef enum cgltf_light_type { + cgltf_light_type_invalid, + cgltf_light_type_directional, + cgltf_light_type_point, + cgltf_light_type_spot, +} cgltf_light_type; + +typedef enum cgltf_data_free_method { + cgltf_data_free_method_none, + cgltf_data_free_method_file_release, + cgltf_data_free_method_memory_free, +} cgltf_data_free_method; + +typedef struct cgltf_extras { + cgltf_size start_offset; + cgltf_size end_offset; +} cgltf_extras; + +typedef struct cgltf_extension { + char* name; + char* data; +} cgltf_extension; + +typedef struct cgltf_buffer +{ + char* name; + cgltf_size size; + char* uri; + void* data; /* loaded by cgltf_load_buffers */ + cgltf_data_free_method data_free_method; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_buffer; + +typedef enum cgltf_meshopt_compression_mode { + cgltf_meshopt_compression_mode_invalid, + cgltf_meshopt_compression_mode_attributes, + cgltf_meshopt_compression_mode_triangles, + cgltf_meshopt_compression_mode_indices, +} cgltf_meshopt_compression_mode; + +typedef enum cgltf_meshopt_compression_filter { + cgltf_meshopt_compression_filter_none, + cgltf_meshopt_compression_filter_octahedral, + cgltf_meshopt_compression_filter_quaternion, + cgltf_meshopt_compression_filter_exponential, +} cgltf_meshopt_compression_filter; + +typedef struct cgltf_meshopt_compression +{ + cgltf_buffer* buffer; + cgltf_size offset; + cgltf_size size; + cgltf_size stride; + cgltf_size count; + cgltf_meshopt_compression_mode mode; + cgltf_meshopt_compression_filter filter; +} cgltf_meshopt_compression; + +typedef struct cgltf_buffer_view +{ + char *name; + cgltf_buffer* buffer; + cgltf_size offset; + cgltf_size size; + cgltf_size stride; /* 0 == automatically determined by accessor */ + cgltf_buffer_view_type type; + void* data; /* overrides buffer->data if present, filled by extensions */ + cgltf_bool has_meshopt_compression; + cgltf_meshopt_compression meshopt_compression; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_buffer_view; + +typedef struct cgltf_accessor_sparse +{ + cgltf_size count; + cgltf_buffer_view* indices_buffer_view; + cgltf_size indices_byte_offset; + cgltf_component_type indices_component_type; + cgltf_buffer_view* values_buffer_view; + cgltf_size values_byte_offset; + cgltf_extras extras; + cgltf_extras indices_extras; + cgltf_extras values_extras; + cgltf_size extensions_count; + cgltf_extension* extensions; + cgltf_size indices_extensions_count; + cgltf_extension* indices_extensions; + cgltf_size values_extensions_count; + cgltf_extension* values_extensions; +} cgltf_accessor_sparse; + +typedef struct cgltf_accessor +{ + char* name; + cgltf_component_type component_type; + cgltf_bool normalized; + cgltf_type type; + cgltf_size offset; + cgltf_size count; + cgltf_size stride; + cgltf_buffer_view* buffer_view; + cgltf_bool has_min; + cgltf_float min[16]; + cgltf_bool has_max; + cgltf_float max[16]; + cgltf_bool is_sparse; + cgltf_accessor_sparse sparse; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_accessor; + +typedef struct cgltf_attribute +{ + char* name; + cgltf_attribute_type type; + cgltf_int index; + cgltf_accessor* data; +} cgltf_attribute; + +typedef struct cgltf_image +{ + char* name; + char* uri; + cgltf_buffer_view* buffer_view; + char* mime_type; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_image; + +typedef struct cgltf_sampler +{ + char* name; + cgltf_int mag_filter; + cgltf_int min_filter; + cgltf_int wrap_s; + cgltf_int wrap_t; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_sampler; + +typedef struct cgltf_texture +{ + char* name; + cgltf_image* image; + cgltf_sampler* sampler; + cgltf_bool has_basisu; + cgltf_image* basisu_image; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_texture; + +typedef struct cgltf_texture_transform +{ + cgltf_float offset[2]; + cgltf_float rotation; + cgltf_float scale[2]; + cgltf_bool has_texcoord; + cgltf_int texcoord; +} cgltf_texture_transform; + +typedef struct cgltf_texture_view +{ + cgltf_texture* texture; + cgltf_int texcoord; + cgltf_float scale; /* equivalent to strength for occlusion_texture */ + cgltf_bool has_transform; + cgltf_texture_transform transform; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_texture_view; + +typedef struct cgltf_pbr_metallic_roughness +{ + cgltf_texture_view base_color_texture; + cgltf_texture_view metallic_roughness_texture; + + cgltf_float base_color_factor[4]; + cgltf_float metallic_factor; + cgltf_float roughness_factor; + + cgltf_extras extras; +} cgltf_pbr_metallic_roughness; + +typedef struct cgltf_pbr_specular_glossiness +{ + cgltf_texture_view diffuse_texture; + cgltf_texture_view specular_glossiness_texture; + + cgltf_float diffuse_factor[4]; + cgltf_float specular_factor[3]; + cgltf_float glossiness_factor; +} cgltf_pbr_specular_glossiness; + +typedef struct cgltf_clearcoat +{ + cgltf_texture_view clearcoat_texture; + cgltf_texture_view clearcoat_roughness_texture; + cgltf_texture_view clearcoat_normal_texture; + + cgltf_float clearcoat_factor; + cgltf_float clearcoat_roughness_factor; +} cgltf_clearcoat; + +typedef struct cgltf_transmission +{ + cgltf_texture_view transmission_texture; + cgltf_float transmission_factor; +} cgltf_transmission; + +typedef struct cgltf_ior +{ + cgltf_float ior; +} cgltf_ior; + +typedef struct cgltf_specular +{ + cgltf_texture_view specular_texture; + cgltf_texture_view specular_color_texture; + cgltf_float specular_color_factor[3]; + cgltf_float specular_factor; +} cgltf_specular; + +typedef struct cgltf_volume +{ + cgltf_texture_view thickness_texture; + cgltf_float thickness_factor; + cgltf_float attenuation_color[3]; + cgltf_float attenuation_distance; +} cgltf_volume; + +typedef struct cgltf_sheen +{ + cgltf_texture_view sheen_color_texture; + cgltf_float sheen_color_factor[3]; + cgltf_texture_view sheen_roughness_texture; + cgltf_float sheen_roughness_factor; +} cgltf_sheen; + +typedef struct cgltf_material +{ + char* name; + cgltf_bool has_pbr_metallic_roughness; + cgltf_bool has_pbr_specular_glossiness; + cgltf_bool has_clearcoat; + cgltf_bool has_transmission; + cgltf_bool has_volume; + cgltf_bool has_ior; + cgltf_bool has_specular; + cgltf_bool has_sheen; + cgltf_pbr_metallic_roughness pbr_metallic_roughness; + cgltf_pbr_specular_glossiness pbr_specular_glossiness; + cgltf_clearcoat clearcoat; + cgltf_ior ior; + cgltf_specular specular; + cgltf_sheen sheen; + cgltf_transmission transmission; + cgltf_volume volume; + cgltf_texture_view normal_texture; + cgltf_texture_view occlusion_texture; + cgltf_texture_view emissive_texture; + cgltf_float emissive_factor[3]; + cgltf_alpha_mode alpha_mode; + cgltf_float alpha_cutoff; + cgltf_bool double_sided; + cgltf_bool unlit; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_material; + +typedef struct cgltf_material_mapping +{ + cgltf_size variant; + cgltf_material* material; + cgltf_extras extras; +} cgltf_material_mapping; + +typedef struct cgltf_morph_target { + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_morph_target; + +typedef struct cgltf_draco_mesh_compression { + cgltf_buffer_view* buffer_view; + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_draco_mesh_compression; + +typedef struct cgltf_primitive { + cgltf_primitive_type type; + cgltf_accessor* indices; + cgltf_material* material; + cgltf_attribute* attributes; + cgltf_size attributes_count; + cgltf_morph_target* targets; + cgltf_size targets_count; + cgltf_extras extras; + cgltf_bool has_draco_mesh_compression; + cgltf_draco_mesh_compression draco_mesh_compression; + cgltf_material_mapping* mappings; + cgltf_size mappings_count; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_primitive; + +typedef struct cgltf_mesh { + char* name; + cgltf_primitive* primitives; + cgltf_size primitives_count; + cgltf_float* weights; + cgltf_size weights_count; + char** target_names; + cgltf_size target_names_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_mesh; + +typedef struct cgltf_node cgltf_node; + +typedef struct cgltf_skin { + char* name; + cgltf_node** joints; + cgltf_size joints_count; + cgltf_node* skeleton; + cgltf_accessor* inverse_bind_matrices; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_skin; + +typedef struct cgltf_camera_perspective { + cgltf_bool has_aspect_ratio; + cgltf_float aspect_ratio; + cgltf_float yfov; + cgltf_bool has_zfar; + cgltf_float zfar; + cgltf_float znear; + cgltf_extras extras; +} cgltf_camera_perspective; + +typedef struct cgltf_camera_orthographic { + cgltf_float xmag; + cgltf_float ymag; + cgltf_float zfar; + cgltf_float znear; + cgltf_extras extras; +} cgltf_camera_orthographic; + +typedef struct cgltf_camera { + char* name; + cgltf_camera_type type; + union { + cgltf_camera_perspective perspective; + cgltf_camera_orthographic orthographic; + } data; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_camera; + +typedef struct cgltf_light { + char* name; + cgltf_float color[3]; + cgltf_float intensity; + cgltf_light_type type; + cgltf_float range; + cgltf_float spot_inner_cone_angle; + cgltf_float spot_outer_cone_angle; + cgltf_extras extras; +} cgltf_light; + +struct cgltf_node { + char* name; + cgltf_node* parent; + cgltf_node** children; + cgltf_size children_count; + cgltf_skin* skin; + cgltf_mesh* mesh; + cgltf_camera* camera; + cgltf_light* light; + cgltf_float* weights; + cgltf_size weights_count; + cgltf_bool has_translation; + cgltf_bool has_rotation; + cgltf_bool has_scale; + cgltf_bool has_matrix; + cgltf_float translation[3]; + cgltf_float rotation[4]; + cgltf_float scale[3]; + cgltf_float matrix[16]; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +}; + +typedef struct cgltf_scene { + char* name; + cgltf_node** nodes; + cgltf_size nodes_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_scene; + +typedef struct cgltf_animation_sampler { + cgltf_accessor* input; + cgltf_accessor* output; + cgltf_interpolation_type interpolation; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation_sampler; + +typedef struct cgltf_animation_channel { + cgltf_animation_sampler* sampler; + cgltf_node* target_node; + cgltf_animation_path_type target_path; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation_channel; + +typedef struct cgltf_animation { + char* name; + cgltf_animation_sampler* samplers; + cgltf_size samplers_count; + cgltf_animation_channel* channels; + cgltf_size channels_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation; + +typedef struct cgltf_material_variant +{ + char* name; + cgltf_extras extras; +} cgltf_material_variant; + +typedef struct cgltf_asset { + char* copyright; + char* generator; + char* version; + char* min_version; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_asset; + +typedef struct cgltf_data +{ + cgltf_file_type file_type; + void* file_data; + + cgltf_asset asset; + + cgltf_mesh* meshes; + cgltf_size meshes_count; + + cgltf_material* materials; + cgltf_size materials_count; + + cgltf_accessor* accessors; + cgltf_size accessors_count; + + cgltf_buffer_view* buffer_views; + cgltf_size buffer_views_count; + + cgltf_buffer* buffers; + cgltf_size buffers_count; + + cgltf_image* images; + cgltf_size images_count; + + cgltf_texture* textures; + cgltf_size textures_count; + + cgltf_sampler* samplers; + cgltf_size samplers_count; + + cgltf_skin* skins; + cgltf_size skins_count; + + cgltf_camera* cameras; + cgltf_size cameras_count; + + cgltf_light* lights; + cgltf_size lights_count; + + cgltf_node* nodes; + cgltf_size nodes_count; + + cgltf_scene* scenes; + cgltf_size scenes_count; + + cgltf_scene* scene; + + cgltf_animation* animations; + cgltf_size animations_count; + + cgltf_material_variant* variants; + cgltf_size variants_count; + + cgltf_extras extras; + + cgltf_size data_extensions_count; + cgltf_extension* data_extensions; + + char** extensions_used; + cgltf_size extensions_used_count; + + char** extensions_required; + cgltf_size extensions_required_count; + + const char* json; + cgltf_size json_size; + + const void* bin; + cgltf_size bin_size; + + cgltf_memory_options memory; + cgltf_file_options file; +} cgltf_data; + +cgltf_result cgltf_parse( + const cgltf_options* options, + const void* data, + cgltf_size size, + cgltf_data** out_data); + +cgltf_result cgltf_parse_file( + const cgltf_options* options, + const char* path, + cgltf_data** out_data); + +cgltf_result cgltf_load_buffers( + const cgltf_options* options, + cgltf_data* data, + const char* gltf_path); + +cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data); + +cgltf_size cgltf_decode_string(char* string); +cgltf_size cgltf_decode_uri(char* uri); + +cgltf_result cgltf_validate(cgltf_data* data); + +void cgltf_free(cgltf_data* data); + +void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix); +void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix); + +cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size); +cgltf_bool cgltf_accessor_read_uint(const cgltf_accessor* accessor, cgltf_size index, cgltf_uint* out, cgltf_size element_size); +cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index); + +cgltf_size cgltf_num_components(cgltf_type type); + +cgltf_size cgltf_accessor_unpack_floats(const cgltf_accessor* accessor, cgltf_float* out, cgltf_size float_count); + +cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size); + +#ifdef __cplusplus +} +#endif + +#endif /* #ifndef CGLTF_H_INCLUDED__ */ + +/* + * + * Stop now, if you are only interested in the API. + * Below, you find the implementation. + * + */ + +#if defined(__INTELLISENSE__) || defined(__JETBRAINS_IDE__) +/* This makes MSVC/CLion intellisense work. */ +#define CGLTF_IMPLEMENTATION +#endif + +#ifdef CGLTF_IMPLEMENTATION + +#include /* For uint8_t, uint32_t */ +#include /* For strncpy */ +#include /* For fopen */ +#include /* For UINT_MAX etc */ +#include /* For FLT_MAX */ + +#if !defined(CGLTF_MALLOC) || !defined(CGLTF_FREE) || !defined(CGLTF_ATOI) || !defined(CGLTF_ATOF) || !defined(CGLTF_ATOLL) +#include /* For malloc, free, atoi, atof */ +#endif + +/* JSMN_PARENT_LINKS is necessary to make parsing large structures linear in input size */ +#define JSMN_PARENT_LINKS + +/* JSMN_STRICT is necessary to reject invalid JSON documents */ +#define JSMN_STRICT + +/* + * -- jsmn.h start -- + * Source: https://github.com/zserge/jsmn + * License: MIT + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 +} jsmntype_t; +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; +static void jsmn_init(jsmn_parser *parser); +static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t *tokens, size_t num_tokens); +/* + * -- jsmn.h end -- + */ + + +static const cgltf_size GlbHeaderSize = 12; +static const cgltf_size GlbChunkHeaderSize = 8; +static const uint32_t GlbVersion = 2; +static const uint32_t GlbMagic = 0x46546C67; +static const uint32_t GlbMagicJsonChunk = 0x4E4F534A; +static const uint32_t GlbMagicBinChunk = 0x004E4942; + +#ifndef CGLTF_MALLOC +#define CGLTF_MALLOC(size) malloc(size) +#endif +#ifndef CGLTF_FREE +#define CGLTF_FREE(ptr) free(ptr) +#endif +#ifndef CGLTF_ATOI +#define CGLTF_ATOI(str) atoi(str) +#endif +#ifndef CGLTF_ATOF +#define CGLTF_ATOF(str) atof(str) +#endif +#ifndef CGLTF_ATOLL +#define CGLTF_ATOLL(str) atoll(str) +#endif +#ifndef CGLTF_VALIDATE_ENABLE_ASSERTS +#define CGLTF_VALIDATE_ENABLE_ASSERTS 0 +#endif + +static void* cgltf_default_alloc(void* user, cgltf_size size) +{ + (void)user; + return CGLTF_MALLOC(size); +} + +static void cgltf_default_free(void* user, void* ptr) +{ + (void)user; + CGLTF_FREE(ptr); +} + +static void* cgltf_calloc(cgltf_options* options, size_t element_size, cgltf_size count) +{ + if (SIZE_MAX / element_size < count) + { + return NULL; + } + void* result = options->memory.alloc(options->memory.user_data, element_size * count); + if (!result) + { + return NULL; + } + memset(result, 0, element_size * count); + return result; +} + +static cgltf_result cgltf_default_file_read(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, const char* path, cgltf_size* size, void** data) +{ + (void)file_options; + void* (*memory_alloc)(void*, cgltf_size) = memory_options->alloc ? memory_options->alloc : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = memory_options->free ? memory_options->free : &cgltf_default_free; + + FILE* file = fopen(path, "rb"); + if (!file) + { + return cgltf_result_file_not_found; + } + + cgltf_size file_size = size ? *size : 0; + + if (file_size == 0) + { + fseek(file, 0, SEEK_END); + +#ifdef _WIN32 + __int64 length = _ftelli64(file); +#else + long length = ftell(file); +#endif + + if (length < 0) + { + fclose(file); + return cgltf_result_io_error; + } + + fseek(file, 0, SEEK_SET); + file_size = (cgltf_size)length; + } + + char* file_data = (char*)memory_alloc(memory_options->user_data, file_size); + if (!file_data) + { + fclose(file); + return cgltf_result_out_of_memory; + } + + cgltf_size read_size = fread(file_data, 1, file_size, file); + + fclose(file); + + if (read_size != file_size) + { + memory_free(memory_options->user_data, file_data); + return cgltf_result_io_error; + } + + if (size) + { + *size = file_size; + } + if (data) + { + *data = file_data; + } + + return cgltf_result_success; +} + +static void cgltf_default_file_release(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, void* data) +{ + (void)file_options; + void (*memfree)(void*, void*) = memory_options->free ? memory_options->free : &cgltf_default_free; + memfree(memory_options->user_data, data); +} + +static cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data); + +cgltf_result cgltf_parse(const cgltf_options* options, const void* data, cgltf_size size, cgltf_data** out_data) +{ + if (size < GlbHeaderSize) + { + return cgltf_result_data_too_short; + } + + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + cgltf_options fixed_options = *options; + if (fixed_options.memory.alloc == NULL) + { + fixed_options.memory.alloc = &cgltf_default_alloc; + } + if (fixed_options.memory.free == NULL) + { + fixed_options.memory.free = &cgltf_default_free; + } + + uint32_t tmp; + // Magic + memcpy(&tmp, data, 4); + if (tmp != GlbMagic) + { + if (fixed_options.type == cgltf_file_type_invalid) + { + fixed_options.type = cgltf_file_type_gltf; + } + else if (fixed_options.type == cgltf_file_type_glb) + { + return cgltf_result_unknown_format; + } + } + + if (fixed_options.type == cgltf_file_type_gltf) + { + cgltf_result json_result = cgltf_parse_json(&fixed_options, (const uint8_t*)data, size, out_data); + if (json_result != cgltf_result_success) + { + return json_result; + } + + (*out_data)->file_type = cgltf_file_type_gltf; + + return cgltf_result_success; + } + + const uint8_t* ptr = (const uint8_t*)data; + // Version + memcpy(&tmp, ptr + 4, 4); + uint32_t version = tmp; + if (version != GlbVersion) + { + return version < GlbVersion ? cgltf_result_legacy_gltf : cgltf_result_unknown_format; + } + + // Total length + memcpy(&tmp, ptr + 8, 4); + if (tmp > size) + { + return cgltf_result_data_too_short; + } + + const uint8_t* json_chunk = ptr + GlbHeaderSize; + + if (GlbHeaderSize + GlbChunkHeaderSize > size) + { + return cgltf_result_data_too_short; + } + + // JSON chunk: length + uint32_t json_length; + memcpy(&json_length, json_chunk, 4); + if (GlbHeaderSize + GlbChunkHeaderSize + json_length > size) + { + return cgltf_result_data_too_short; + } + + // JSON chunk: magic + memcpy(&tmp, json_chunk + 4, 4); + if (tmp != GlbMagicJsonChunk) + { + return cgltf_result_unknown_format; + } + + json_chunk += GlbChunkHeaderSize; + + const void* bin = 0; + cgltf_size bin_size = 0; + + if (GlbHeaderSize + GlbChunkHeaderSize + json_length + GlbChunkHeaderSize <= size) + { + // We can read another chunk + const uint8_t* bin_chunk = json_chunk + json_length; + + // Bin chunk: length + uint32_t bin_length; + memcpy(&bin_length, bin_chunk, 4); + if (GlbHeaderSize + GlbChunkHeaderSize + json_length + GlbChunkHeaderSize + bin_length > size) + { + return cgltf_result_data_too_short; + } + + // Bin chunk: magic + memcpy(&tmp, bin_chunk + 4, 4); + if (tmp != GlbMagicBinChunk) + { + return cgltf_result_unknown_format; + } + + bin_chunk += GlbChunkHeaderSize; + + bin = bin_chunk; + bin_size = bin_length; + } + + cgltf_result json_result = cgltf_parse_json(&fixed_options, json_chunk, json_length, out_data); + if (json_result != cgltf_result_success) + { + return json_result; + } + + (*out_data)->file_type = cgltf_file_type_glb; + (*out_data)->bin = bin; + (*out_data)->bin_size = bin_size; + + return cgltf_result_success; +} + +cgltf_result cgltf_parse_file(const cgltf_options* options, const char* path, cgltf_data** out_data) +{ + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + cgltf_result (*file_read)(const struct cgltf_memory_options*, const struct cgltf_file_options*, const char*, cgltf_size*, void**) = options->file.read ? options->file.read : &cgltf_default_file_read; + void (*file_release)(const struct cgltf_memory_options*, const struct cgltf_file_options*, void* data) = options->file.release ? options->file.release : cgltf_default_file_release; + + void* file_data = NULL; + cgltf_size file_size = 0; + cgltf_result result = file_read(&options->memory, &options->file, path, &file_size, &file_data); + if (result != cgltf_result_success) + { + return result; + } + + result = cgltf_parse(options, file_data, file_size, out_data); + + if (result != cgltf_result_success) + { + file_release(&options->memory, &options->file, file_data); + return result; + } + + (*out_data)->file_data = file_data; + + return cgltf_result_success; +} + +static void cgltf_combine_paths(char* path, const char* base, const char* uri) +{ + const char* s0 = strrchr(base, '/'); + const char* s1 = strrchr(base, '\\'); + const char* slash = s0 ? (s1 && s1 > s0 ? s1 : s0) : s1; + + if (slash) + { + size_t prefix = slash - base + 1; + + strncpy(path, base, prefix); + strcpy(path + prefix, uri); + } + else + { + strcpy(path, uri); + } +} + +static cgltf_result cgltf_load_buffer_file(const cgltf_options* options, cgltf_size size, const char* uri, const char* gltf_path, void** out_data) +{ + void* (*memory_alloc)(void*, cgltf_size) = options->memory.alloc ? options->memory.alloc : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = options->memory.free ? options->memory.free : &cgltf_default_free; + cgltf_result (*file_read)(const struct cgltf_memory_options*, const struct cgltf_file_options*, const char*, cgltf_size*, void**) = options->file.read ? options->file.read : &cgltf_default_file_read; + + char* path = (char*)memory_alloc(options->memory.user_data, strlen(uri) + strlen(gltf_path) + 1); + if (!path) + { + return cgltf_result_out_of_memory; + } + + cgltf_combine_paths(path, gltf_path, uri); + + // after combining, the tail of the resulting path is a uri; decode_uri converts it into path + cgltf_decode_uri(path + strlen(path) - strlen(uri)); + + void* file_data = NULL; + cgltf_result result = file_read(&options->memory, &options->file, path, &size, &file_data); + + memory_free(options->memory.user_data, path); + + *out_data = (result == cgltf_result_success) ? file_data : NULL; + + return result; +} + +cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data) +{ + void* (*memory_alloc)(void*, cgltf_size) = options->memory.alloc ? options->memory.alloc : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = options->memory.free ? options->memory.free : &cgltf_default_free; + + unsigned char* data = (unsigned char*)memory_alloc(options->memory.user_data, size); + if (!data) + { + return cgltf_result_out_of_memory; + } + + unsigned int buffer = 0; + unsigned int buffer_bits = 0; + + for (cgltf_size i = 0; i < size; ++i) + { + while (buffer_bits < 8) + { + char ch = *base64++; + + int index = + (unsigned)(ch - 'A') < 26 ? (ch - 'A') : + (unsigned)(ch - 'a') < 26 ? (ch - 'a') + 26 : + (unsigned)(ch - '0') < 10 ? (ch - '0') + 52 : + ch == '+' ? 62 : + ch == '/' ? 63 : + -1; + + if (index < 0) + { + memory_free(options->memory.user_data, data); + return cgltf_result_io_error; + } + + buffer = (buffer << 6) | index; + buffer_bits += 6; + } + + data[i] = (unsigned char)(buffer >> (buffer_bits - 8)); + buffer_bits -= 8; + } + + *out_data = data; + + return cgltf_result_success; +} + +static int cgltf_unhex(char ch) +{ + return + (unsigned)(ch - '0') < 10 ? (ch - '0') : + (unsigned)(ch - 'A') < 6 ? (ch - 'A') + 10 : + (unsigned)(ch - 'a') < 6 ? (ch - 'a') + 10 : + -1; +} + +cgltf_size cgltf_decode_string(char* string) +{ + char* read = string + strcspn(string, "\\"); + if (*read == 0) + { + return read - string; + } + char* write = string; + char* last = string; + + for (;;) + { + // Copy characters since last escaped sequence + cgltf_size written = read - last; + memmove(write, last, written); + write += written; + + if (*read++ == 0) + { + break; + } + + // jsmn already checked that all escape sequences are valid + switch (*read++) + { + case '\"': *write++ = '\"'; break; + case '/': *write++ = '/'; break; + case '\\': *write++ = '\\'; break; + case 'b': *write++ = '\b'; break; + case 'f': *write++ = '\f'; break; + case 'r': *write++ = '\r'; break; + case 'n': *write++ = '\n'; break; + case 't': *write++ = '\t'; break; + case 'u': + { + // UCS-2 codepoint \uXXXX to UTF-8 + int character = 0; + for (cgltf_size i = 0; i < 4; ++i) + { + character = (character << 4) + cgltf_unhex(*read++); + } + + if (character <= 0x7F) + { + *write++ = character & 0xFF; + } + else if (character <= 0x7FF) + { + *write++ = 0xC0 | ((character >> 6) & 0xFF); + *write++ = 0x80 | (character & 0x3F); + } + else + { + *write++ = 0xE0 | ((character >> 12) & 0xFF); + *write++ = 0x80 | ((character >> 6) & 0x3F); + *write++ = 0x80 | (character & 0x3F); + } + break; + } + default: + break; + } + + last = read; + read += strcspn(read, "\\"); + } + + *write = 0; + return write - string; +} + +cgltf_size cgltf_decode_uri(char* uri) +{ + char* write = uri; + char* i = uri; + + while (*i) + { + if (*i == '%') + { + int ch1 = cgltf_unhex(i[1]); + + if (ch1 >= 0) + { + int ch2 = cgltf_unhex(i[2]); + + if (ch2 >= 0) + { + *write++ = (char)(ch1 * 16 + ch2); + i += 3; + continue; + } + } + } + + *write++ = *i++; + } + + *write = 0; + return write - uri; +} + +cgltf_result cgltf_load_buffers(const cgltf_options* options, cgltf_data* data, const char* gltf_path) +{ + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + if (data->buffers_count && data->buffers[0].data == NULL && data->buffers[0].uri == NULL && data->bin) + { + if (data->bin_size < data->buffers[0].size) + { + return cgltf_result_data_too_short; + } + + data->buffers[0].data = (void*)data->bin; + data->buffers[0].data_free_method = cgltf_data_free_method_none; + } + + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + if (data->buffers[i].data) + { + continue; + } + + const char* uri = data->buffers[i].uri; + + if (uri == NULL) + { + continue; + } + + if (strncmp(uri, "data:", 5) == 0) + { + const char* comma = strchr(uri, ','); + + if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) + { + cgltf_result res = cgltf_load_buffer_base64(options, data->buffers[i].size, comma + 1, &data->buffers[i].data); + data->buffers[i].data_free_method = cgltf_data_free_method_memory_free; + + if (res != cgltf_result_success) + { + return res; + } + } + else + { + return cgltf_result_unknown_format; + } + } + else if (strstr(uri, "://") == NULL && gltf_path) + { + cgltf_result res = cgltf_load_buffer_file(options, data->buffers[i].size, uri, gltf_path, &data->buffers[i].data); + data->buffers[i].data_free_method = cgltf_data_free_method_file_release; + + if (res != cgltf_result_success) + { + return res; + } + } + else + { + return cgltf_result_unknown_format; + } + } + + return cgltf_result_success; +} + +static cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type); + +static cgltf_size cgltf_calc_index_bound(cgltf_buffer_view* buffer_view, cgltf_size offset, cgltf_component_type component_type, cgltf_size count) +{ + char* data = (char*)buffer_view->buffer->data + offset + buffer_view->offset; + cgltf_size bound = 0; + + switch (component_type) + { + case cgltf_component_type_r_8u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned char*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + case cgltf_component_type_r_16u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned short*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + case cgltf_component_type_r_32u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned int*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + default: + ; + } + + return bound; +} + +#if CGLTF_VALIDATE_ENABLE_ASSERTS +#define CGLTF_ASSERT_IF(cond, result) assert(!(cond)); if (cond) return result; +#else +#define CGLTF_ASSERT_IF(cond, result) if (cond) return result; +#endif + +cgltf_result cgltf_validate(cgltf_data* data) +{ + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + cgltf_accessor* accessor = &data->accessors[i]; + + cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type); + + if (accessor->buffer_view) + { + cgltf_size req_size = accessor->offset + accessor->stride * (accessor->count - 1) + element_size; + + CGLTF_ASSERT_IF(accessor->buffer_view->size < req_size, cgltf_result_data_too_short); + } + + if (accessor->is_sparse) + { + cgltf_accessor_sparse* sparse = &accessor->sparse; + + cgltf_size indices_component_size = cgltf_calc_size(cgltf_type_scalar, sparse->indices_component_type); + cgltf_size indices_req_size = sparse->indices_byte_offset + indices_component_size * sparse->count; + cgltf_size values_req_size = sparse->values_byte_offset + element_size * sparse->count; + + CGLTF_ASSERT_IF(sparse->indices_buffer_view->size < indices_req_size || + sparse->values_buffer_view->size < values_req_size, cgltf_result_data_too_short); + + CGLTF_ASSERT_IF(sparse->indices_component_type != cgltf_component_type_r_8u && + sparse->indices_component_type != cgltf_component_type_r_16u && + sparse->indices_component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); + + if (sparse->indices_buffer_view->buffer->data) + { + cgltf_size index_bound = cgltf_calc_index_bound(sparse->indices_buffer_view, sparse->indices_byte_offset, sparse->indices_component_type, sparse->count); + + CGLTF_ASSERT_IF(index_bound >= accessor->count, cgltf_result_data_too_short); + } + } + } + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + cgltf_size req_size = data->buffer_views[i].offset + data->buffer_views[i].size; + + CGLTF_ASSERT_IF(data->buffer_views[i].buffer && data->buffer_views[i].buffer->size < req_size, cgltf_result_data_too_short); + + if (data->buffer_views[i].has_meshopt_compression) + { + cgltf_meshopt_compression* mc = &data->buffer_views[i].meshopt_compression; + + CGLTF_ASSERT_IF(mc->buffer == NULL || mc->buffer->size < mc->offset + mc->size, cgltf_result_data_too_short); + + CGLTF_ASSERT_IF(data->buffer_views[i].stride && mc->stride != data->buffer_views[i].stride, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(data->buffer_views[i].size != mc->stride * mc->count, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_invalid, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_attributes && !(mc->stride % 4 == 0 && mc->stride <= 256), cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_triangles && mc->count % 3 != 0, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF((mc->mode == cgltf_meshopt_compression_mode_triangles || mc->mode == cgltf_meshopt_compression_mode_indices) && mc->stride != 2 && mc->stride != 4, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF((mc->mode == cgltf_meshopt_compression_mode_triangles || mc->mode == cgltf_meshopt_compression_mode_indices) && mc->filter != cgltf_meshopt_compression_filter_none, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_octahedral && mc->stride != 4 && mc->stride != 8, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_quaternion && mc->stride != 8, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + if (data->meshes[i].weights) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].weights_count, cgltf_result_invalid_gltf); + } + + if (data->meshes[i].target_names) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].target_names_count, cgltf_result_invalid_gltf); + } + + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets_count != data->meshes[i].primitives[0].targets_count, cgltf_result_invalid_gltf); + + if (data->meshes[i].primitives[j].attributes_count) + { + cgltf_accessor* first = data->meshes[i].primitives[j].attributes[0].data; + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets[k].attributes[m].data->count != first->count, cgltf_result_invalid_gltf); + } + } + + cgltf_accessor* indices = data->meshes[i].primitives[j].indices; + + CGLTF_ASSERT_IF(indices && + indices->component_type != cgltf_component_type_r_8u && + indices->component_type != cgltf_component_type_r_16u && + indices->component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); + + if (indices && indices->buffer_view && indices->buffer_view->buffer->data) + { + cgltf_size index_bound = cgltf_calc_index_bound(indices->buffer_view, indices->offset, indices->component_type, indices->count); + + CGLTF_ASSERT_IF(index_bound >= first->count, cgltf_result_data_too_short); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].mappings[k].variant >= data->variants_count, cgltf_result_invalid_gltf); + } + } + } + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + if (data->nodes[i].weights && data->nodes[i].mesh) + { + CGLTF_ASSERT_IF (data->nodes[i].mesh->primitives_count && data->nodes[i].mesh->primitives[0].targets_count != data->nodes[i].weights_count, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + cgltf_node* p1 = data->nodes[i].parent; + cgltf_node* p2 = p1 ? p1->parent : NULL; + + while (p1 && p2) + { + CGLTF_ASSERT_IF(p1 == p2, cgltf_result_invalid_gltf); + + p1 = p1->parent; + p2 = p2->parent ? p2->parent->parent : NULL; + } + } + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + for (cgltf_size j = 0; j < data->scenes[i].nodes_count; ++j) + { + CGLTF_ASSERT_IF(data->scenes[i].nodes[j]->parent, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + cgltf_animation_channel* channel = &data->animations[i].channels[j]; + + if (!channel->target_node) + { + continue; + } + + cgltf_size components = 1; + + if (channel->target_path == cgltf_animation_path_type_weights) + { + CGLTF_ASSERT_IF(!channel->target_node->mesh || !channel->target_node->mesh->primitives_count, cgltf_result_invalid_gltf); + + components = channel->target_node->mesh->primitives[0].targets_count; + } + + cgltf_size values = channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline ? 3 : 1; + + CGLTF_ASSERT_IF(channel->sampler->input->count * components * values != channel->sampler->output->count, cgltf_result_data_too_short); + } + } + + return cgltf_result_success; +} + +cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size) +{ + cgltf_size json_size = extras->end_offset - extras->start_offset; + + if (!dest) + { + if (dest_size) + { + *dest_size = json_size + 1; + return cgltf_result_success; + } + return cgltf_result_invalid_options; + } + + if (*dest_size + 1 < json_size) + { + strncpy(dest, data->json + extras->start_offset, *dest_size - 1); + dest[*dest_size - 1] = 0; + } + else + { + strncpy(dest, data->json + extras->start_offset, json_size); + dest[json_size] = 0; + } + + return cgltf_result_success; +} + +void cgltf_free_extensions(cgltf_data* data, cgltf_extension* extensions, cgltf_size extensions_count) +{ + for (cgltf_size i = 0; i < extensions_count; ++i) + { + data->memory.free(data->memory.user_data, extensions[i].name); + data->memory.free(data->memory.user_data, extensions[i].data); + } + data->memory.free(data->memory.user_data, extensions); +} + +void cgltf_free(cgltf_data* data) +{ + if (!data) + { + return; + } + + void (*file_release)(const struct cgltf_memory_options*, const struct cgltf_file_options*, void* data) = data->file.release ? data->file.release : cgltf_default_file_release; + + data->memory.free(data->memory.user_data, data->asset.copyright); + data->memory.free(data->memory.user_data, data->asset.generator); + data->memory.free(data->memory.user_data, data->asset.version); + data->memory.free(data->memory.user_data, data->asset.min_version); + + cgltf_free_extensions(data, data->asset.extensions, data->asset.extensions_count); + + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + data->memory.free(data->memory.user_data, data->accessors[i].name); + + if(data->accessors[i].is_sparse) + { + cgltf_free_extensions(data, data->accessors[i].sparse.extensions, data->accessors[i].sparse.extensions_count); + cgltf_free_extensions(data, data->accessors[i].sparse.indices_extensions, data->accessors[i].sparse.indices_extensions_count); + cgltf_free_extensions(data, data->accessors[i].sparse.values_extensions, data->accessors[i].sparse.values_extensions_count); + } + cgltf_free_extensions(data, data->accessors[i].extensions, data->accessors[i].extensions_count); + } + data->memory.free(data->memory.user_data, data->accessors); + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + data->memory.free(data->memory.user_data, data->buffer_views[i].name); + data->memory.free(data->memory.user_data, data->buffer_views[i].data); + + cgltf_free_extensions(data, data->buffer_views[i].extensions, data->buffer_views[i].extensions_count); + } + data->memory.free(data->memory.user_data, data->buffer_views); + + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + data->memory.free(data->memory.user_data, data->buffers[i].name); + + if (data->buffers[i].data_free_method == cgltf_data_free_method_file_release) + { + file_release(&data->memory, &data->file, data->buffers[i].data); + } + else if (data->buffers[i].data_free_method == cgltf_data_free_method_memory_free) + { + data->memory.free(data->memory.user_data, data->buffers[i].data); + } + + data->memory.free(data->memory.user_data, data->buffers[i].uri); + + cgltf_free_extensions(data, data->buffers[i].extensions, data->buffers[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->buffers); + + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + data->memory.free(data->memory.user_data, data->meshes[i].name); + + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + data->memory.free(data->memory.user_data, data->meshes[i].primitives[j].attributes[k].name); + } + + data->memory.free(data->memory.user_data, data->meshes[i].primitives[j].attributes); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + data->memory.free(data->memory.user_data, data->meshes[i].primitives[j].targets[k].attributes[m].name); + } + + data->memory.free(data->memory.user_data, data->meshes[i].primitives[j].targets[k].attributes); + } + + data->memory.free(data->memory.user_data, data->meshes[i].primitives[j].targets); + + if (data->meshes[i].primitives[j].has_draco_mesh_compression) + { + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].draco_mesh_compression.attributes_count; ++k) + { + data->memory.free(data->memory.user_data, data->meshes[i].primitives[j].draco_mesh_compression.attributes[k].name); + } + + data->memory.free(data->memory.user_data, data->meshes[i].primitives[j].draco_mesh_compression.attributes); + } + + data->memory.free(data->memory.user_data, data->meshes[i].primitives[j].mappings); + + cgltf_free_extensions(data, data->meshes[i].primitives[j].extensions, data->meshes[i].primitives[j].extensions_count); + } + + data->memory.free(data->memory.user_data, data->meshes[i].primitives); + data->memory.free(data->memory.user_data, data->meshes[i].weights); + + for (cgltf_size j = 0; j < data->meshes[i].target_names_count; ++j) + { + data->memory.free(data->memory.user_data, data->meshes[i].target_names[j]); + } + + cgltf_free_extensions(data, data->meshes[i].extensions, data->meshes[i].extensions_count); + + data->memory.free(data->memory.user_data, data->meshes[i].target_names); + } + + data->memory.free(data->memory.user_data, data->meshes); + + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + data->memory.free(data->memory.user_data, data->materials[i].name); + + if(data->materials[i].has_pbr_metallic_roughness) + { + cgltf_free_extensions(data, data->materials[i].pbr_metallic_roughness.metallic_roughness_texture.extensions, data->materials[i].pbr_metallic_roughness.metallic_roughness_texture.extensions_count); + cgltf_free_extensions(data, data->materials[i].pbr_metallic_roughness.base_color_texture.extensions, data->materials[i].pbr_metallic_roughness.base_color_texture.extensions_count); + } + if(data->materials[i].has_pbr_specular_glossiness) + { + cgltf_free_extensions(data, data->materials[i].pbr_specular_glossiness.diffuse_texture.extensions, data->materials[i].pbr_specular_glossiness.diffuse_texture.extensions_count); + cgltf_free_extensions(data, data->materials[i].pbr_specular_glossiness.specular_glossiness_texture.extensions, data->materials[i].pbr_specular_glossiness.specular_glossiness_texture.extensions_count); + } + if(data->materials[i].has_clearcoat) + { + cgltf_free_extensions(data, data->materials[i].clearcoat.clearcoat_texture.extensions, data->materials[i].clearcoat.clearcoat_texture.extensions_count); + cgltf_free_extensions(data, data->materials[i].clearcoat.clearcoat_roughness_texture.extensions, data->materials[i].clearcoat.clearcoat_roughness_texture.extensions_count); + cgltf_free_extensions(data, data->materials[i].clearcoat.clearcoat_normal_texture.extensions, data->materials[i].clearcoat.clearcoat_normal_texture.extensions_count); + } + if(data->materials[i].has_specular) + { + cgltf_free_extensions(data, data->materials[i].specular.specular_texture.extensions, data->materials[i].specular.specular_texture.extensions_count); + cgltf_free_extensions(data, data->materials[i].specular.specular_color_texture.extensions, data->materials[i].specular.specular_color_texture.extensions_count); + } + if(data->materials[i].has_transmission) + { + cgltf_free_extensions(data, data->materials[i].transmission.transmission_texture.extensions, data->materials[i].transmission.transmission_texture.extensions_count); + } + if (data->materials[i].has_volume) + { + cgltf_free_extensions(data, data->materials[i].volume.thickness_texture.extensions, data->materials[i].volume.thickness_texture.extensions_count); + } + if(data->materials[i].has_sheen) + { + cgltf_free_extensions(data, data->materials[i].sheen.sheen_color_texture.extensions, data->materials[i].sheen.sheen_color_texture.extensions_count); + cgltf_free_extensions(data, data->materials[i].sheen.sheen_roughness_texture.extensions, data->materials[i].sheen.sheen_roughness_texture.extensions_count); + } + + cgltf_free_extensions(data, data->materials[i].normal_texture.extensions, data->materials[i].normal_texture.extensions_count); + cgltf_free_extensions(data, data->materials[i].occlusion_texture.extensions, data->materials[i].occlusion_texture.extensions_count); + cgltf_free_extensions(data, data->materials[i].emissive_texture.extensions, data->materials[i].emissive_texture.extensions_count); + + cgltf_free_extensions(data, data->materials[i].extensions, data->materials[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->materials); + + for (cgltf_size i = 0; i < data->images_count; ++i) + { + data->memory.free(data->memory.user_data, data->images[i].name); + data->memory.free(data->memory.user_data, data->images[i].uri); + data->memory.free(data->memory.user_data, data->images[i].mime_type); + + cgltf_free_extensions(data, data->images[i].extensions, data->images[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->images); + + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + data->memory.free(data->memory.user_data, data->textures[i].name); + cgltf_free_extensions(data, data->textures[i].extensions, data->textures[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->textures); + + for (cgltf_size i = 0; i < data->samplers_count; ++i) + { + data->memory.free(data->memory.user_data, data->samplers[i].name); + cgltf_free_extensions(data, data->samplers[i].extensions, data->samplers[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->samplers); + + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + data->memory.free(data->memory.user_data, data->skins[i].name); + data->memory.free(data->memory.user_data, data->skins[i].joints); + + cgltf_free_extensions(data, data->skins[i].extensions, data->skins[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->skins); + + for (cgltf_size i = 0; i < data->cameras_count; ++i) + { + data->memory.free(data->memory.user_data, data->cameras[i].name); + cgltf_free_extensions(data, data->cameras[i].extensions, data->cameras[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->cameras); + + for (cgltf_size i = 0; i < data->lights_count; ++i) + { + data->memory.free(data->memory.user_data, data->lights[i].name); + } + + data->memory.free(data->memory.user_data, data->lights); + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + data->memory.free(data->memory.user_data, data->nodes[i].name); + data->memory.free(data->memory.user_data, data->nodes[i].children); + data->memory.free(data->memory.user_data, data->nodes[i].weights); + cgltf_free_extensions(data, data->nodes[i].extensions, data->nodes[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->nodes); + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + data->memory.free(data->memory.user_data, data->scenes[i].name); + data->memory.free(data->memory.user_data, data->scenes[i].nodes); + + cgltf_free_extensions(data, data->scenes[i].extensions, data->scenes[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->scenes); + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + data->memory.free(data->memory.user_data, data->animations[i].name); + for (cgltf_size j = 0; j < data->animations[i].samplers_count; ++j) + { + cgltf_free_extensions(data, data->animations[i].samplers[j].extensions, data->animations[i].samplers[j].extensions_count); + } + data->memory.free(data->memory.user_data, data->animations[i].samplers); + + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + cgltf_free_extensions(data, data->animations[i].channels[j].extensions, data->animations[i].channels[j].extensions_count); + } + data->memory.free(data->memory.user_data, data->animations[i].channels); + + cgltf_free_extensions(data, data->animations[i].extensions, data->animations[i].extensions_count); + } + + data->memory.free(data->memory.user_data, data->animations); + + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + data->memory.free(data->memory.user_data, data->variants[i].name); + } + + data->memory.free(data->memory.user_data, data->variants); + + cgltf_free_extensions(data, data->data_extensions, data->data_extensions_count); + + for (cgltf_size i = 0; i < data->extensions_used_count; ++i) + { + data->memory.free(data->memory.user_data, data->extensions_used[i]); + } + + data->memory.free(data->memory.user_data, data->extensions_used); + + for (cgltf_size i = 0; i < data->extensions_required_count; ++i) + { + data->memory.free(data->memory.user_data, data->extensions_required[i]); + } + + data->memory.free(data->memory.user_data, data->extensions_required); + + file_release(&data->memory, &data->file, data->file_data); + + data->memory.free(data->memory.user_data, data); +} + +void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix) +{ + cgltf_float* lm = out_matrix; + + if (node->has_matrix) + { + memcpy(lm, node->matrix, sizeof(float) * 16); + } + else + { + float tx = node->translation[0]; + float ty = node->translation[1]; + float tz = node->translation[2]; + + float qx = node->rotation[0]; + float qy = node->rotation[1]; + float qz = node->rotation[2]; + float qw = node->rotation[3]; + + float sx = node->scale[0]; + float sy = node->scale[1]; + float sz = node->scale[2]; + + lm[0] = (1 - 2 * qy*qy - 2 * qz*qz) * sx; + lm[1] = (2 * qx*qy + 2 * qz*qw) * sx; + lm[2] = (2 * qx*qz - 2 * qy*qw) * sx; + lm[3] = 0.f; + + lm[4] = (2 * qx*qy - 2 * qz*qw) * sy; + lm[5] = (1 - 2 * qx*qx - 2 * qz*qz) * sy; + lm[6] = (2 * qy*qz + 2 * qx*qw) * sy; + lm[7] = 0.f; + + lm[8] = (2 * qx*qz + 2 * qy*qw) * sz; + lm[9] = (2 * qy*qz - 2 * qx*qw) * sz; + lm[10] = (1 - 2 * qx*qx - 2 * qy*qy) * sz; + lm[11] = 0.f; + + lm[12] = tx; + lm[13] = ty; + lm[14] = tz; + lm[15] = 1.f; + } +} + +void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix) +{ + cgltf_float* lm = out_matrix; + cgltf_node_transform_local(node, lm); + + const cgltf_node* parent = node->parent; + + while (parent) + { + float pm[16]; + cgltf_node_transform_local(parent, pm); + + for (int i = 0; i < 4; ++i) + { + float l0 = lm[i * 4 + 0]; + float l1 = lm[i * 4 + 1]; + float l2 = lm[i * 4 + 2]; + + float r0 = l0 * pm[0] + l1 * pm[4] + l2 * pm[8]; + float r1 = l0 * pm[1] + l1 * pm[5] + l2 * pm[9]; + float r2 = l0 * pm[2] + l1 * pm[6] + l2 * pm[10]; + + lm[i * 4 + 0] = r0; + lm[i * 4 + 1] = r1; + lm[i * 4 + 2] = r2; + } + + lm[12] += pm[12]; + lm[13] += pm[13]; + lm[14] += pm[14]; + + parent = parent->parent; + } +} + +static cgltf_size cgltf_component_read_index(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_16: + return *((const int16_t*) in); + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + case cgltf_component_type_r_32f: + return (cgltf_size)*((const float*) in); + case cgltf_component_type_r_8: + return *((const int8_t*) in); + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + default: + return 0; + } +} + +static cgltf_float cgltf_component_read_float(const void* in, cgltf_component_type component_type, cgltf_bool normalized) +{ + if (component_type == cgltf_component_type_r_32f) + { + return *((const float*) in); + } + + if (normalized) + { + switch (component_type) + { + // note: glTF spec doesn't currently define normalized conversions for 32-bit integers + case cgltf_component_type_r_16: + return *((const int16_t*) in) / (cgltf_float)32767; + case cgltf_component_type_r_16u: + return *((const uint16_t*) in) / (cgltf_float)65535; + case cgltf_component_type_r_8: + return *((const int8_t*) in) / (cgltf_float)127; + case cgltf_component_type_r_8u: + return *((const uint8_t*) in) / (cgltf_float)255; + default: + return 0; + } + } + + return (cgltf_float)cgltf_component_read_index(in, component_type); +} + +static cgltf_size cgltf_component_size(cgltf_component_type component_type); + +static cgltf_bool cgltf_element_read_float(const uint8_t* element, cgltf_type type, cgltf_component_type component_type, cgltf_bool normalized, cgltf_float* out, cgltf_size element_size) +{ + cgltf_size num_components = cgltf_num_components(type); + + if (element_size < num_components) { + return 0; + } + + // There are three special cases for component extraction, see #data-alignment in the 2.0 spec. + + cgltf_size component_size = cgltf_component_size(component_type); + + if (type == cgltf_type_mat2 && component_size == 1) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 1, component_type, normalized); + out[2] = cgltf_component_read_float(element + 4, component_type, normalized); + out[3] = cgltf_component_read_float(element + 5, component_type, normalized); + return 1; + } + + if (type == cgltf_type_mat3 && component_size == 1) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 1, component_type, normalized); + out[2] = cgltf_component_read_float(element + 2, component_type, normalized); + out[3] = cgltf_component_read_float(element + 4, component_type, normalized); + out[4] = cgltf_component_read_float(element + 5, component_type, normalized); + out[5] = cgltf_component_read_float(element + 6, component_type, normalized); + out[6] = cgltf_component_read_float(element + 8, component_type, normalized); + out[7] = cgltf_component_read_float(element + 9, component_type, normalized); + out[8] = cgltf_component_read_float(element + 10, component_type, normalized); + return 1; + } + + if (type == cgltf_type_mat3 && component_size == 2) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 2, component_type, normalized); + out[2] = cgltf_component_read_float(element + 4, component_type, normalized); + out[3] = cgltf_component_read_float(element + 8, component_type, normalized); + out[4] = cgltf_component_read_float(element + 10, component_type, normalized); + out[5] = cgltf_component_read_float(element + 12, component_type, normalized); + out[6] = cgltf_component_read_float(element + 16, component_type, normalized); + out[7] = cgltf_component_read_float(element + 18, component_type, normalized); + out[8] = cgltf_component_read_float(element + 20, component_type, normalized); + return 1; + } + + for (cgltf_size i = 0; i < num_components; ++i) + { + out[i] = cgltf_component_read_float(element + component_size * i, component_type, normalized); + } + return 1; +} + +const uint8_t* cgltf_buffer_view_data(const cgltf_buffer_view* view) +{ + if (view->data) + return (const uint8_t*)view->data; + + if (!view->buffer->data) + return NULL; + + const uint8_t* result = (const uint8_t*)view->buffer->data; + result += view->offset; + return result; +} + +cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size) +{ + if (accessor->is_sparse) + { + return 0; + } + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_size * sizeof(cgltf_float)); + return 1; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset + accessor->stride * index; + return cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, out, element_size); +} + +cgltf_size cgltf_accessor_unpack_floats(const cgltf_accessor* accessor, cgltf_float* out, cgltf_size float_count) +{ + cgltf_size floats_per_element = cgltf_num_components(accessor->type); + cgltf_size available_floats = accessor->count * floats_per_element; + if (out == NULL) + { + return available_floats; + } + + float_count = available_floats < float_count ? available_floats : float_count; + cgltf_size element_count = float_count / floats_per_element; + + // First pass: convert each element in the base accessor. + cgltf_float* dest = out; + cgltf_accessor dense = *accessor; + dense.is_sparse = 0; + for (cgltf_size index = 0; index < element_count; index++, dest += floats_per_element) + { + if (!cgltf_accessor_read_float(&dense, index, dest, floats_per_element)) + { + return 0; + } + } + + // Second pass: write out each element in the sparse accessor. + if (accessor->is_sparse) + { + const cgltf_accessor_sparse* sparse = &dense.sparse; + + const uint8_t* index_data = cgltf_buffer_view_data(sparse->indices_buffer_view); + const uint8_t* reader_head = cgltf_buffer_view_data(sparse->values_buffer_view); + + if (index_data == NULL || reader_head == NULL) + { + return 0; + } + + index_data += sparse->indices_byte_offset; + reader_head += sparse->values_byte_offset; + + cgltf_size index_stride = cgltf_component_size(sparse->indices_component_type); + for (cgltf_size reader_index = 0; reader_index < sparse->count; reader_index++, index_data += index_stride) + { + size_t writer_index = cgltf_component_read_index(index_data, sparse->indices_component_type); + float* writer_head = out + writer_index * floats_per_element; + + if (!cgltf_element_read_float(reader_head, dense.type, dense.component_type, dense.normalized, writer_head, floats_per_element)) + { + return 0; + } + + reader_head += dense.stride; + } + } + + return element_count * floats_per_element; +} + +static cgltf_uint cgltf_component_read_uint(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_8: + return *((const int8_t*) in); + + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + + case cgltf_component_type_r_16: + return *((const int16_t*) in); + + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + + default: + return 0; + } +} + +static cgltf_bool cgltf_element_read_uint(const uint8_t* element, cgltf_type type, cgltf_component_type component_type, cgltf_uint* out, cgltf_size element_size) +{ + cgltf_size num_components = cgltf_num_components(type); + + if (element_size < num_components) + { + return 0; + } + + // Reading integer matrices is not a valid use case + if (type == cgltf_type_mat2 || type == cgltf_type_mat3 || type == cgltf_type_mat4) + { + return 0; + } + + cgltf_size component_size = cgltf_component_size(component_type); + + for (cgltf_size i = 0; i < num_components; ++i) + { + out[i] = cgltf_component_read_uint(element + component_size * i, component_type); + } + return 1; +} + +cgltf_bool cgltf_accessor_read_uint(const cgltf_accessor* accessor, cgltf_size index, cgltf_uint* out, cgltf_size element_size) +{ + if (accessor->is_sparse) + { + return 0; + } + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_size * sizeof( cgltf_uint )); + return 1; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset + accessor->stride * index; + return cgltf_element_read_uint(element, accessor->type, accessor->component_type, out, element_size); +} + +cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index) +{ + if (accessor->is_sparse) + { + return 0; // This is an error case, but we can't communicate the error with existing interface. + } + if (accessor->buffer_view == NULL) + { + return 0; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; // This is an error case, but we can't communicate the error with existing interface. + } + element += accessor->offset + accessor->stride * index; + return cgltf_component_read_index(element, accessor->component_type); +} + +#define CGLTF_ERROR_JSON -1 +#define CGLTF_ERROR_NOMEM -2 +#define CGLTF_ERROR_LEGACY -3 + +#define CGLTF_CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return CGLTF_ERROR_JSON; } +#define CGLTF_CHECK_TOKTYPE_RETTYPE(tok_, type_, ret_) if ((tok_).type != (type_)) { return (ret_)CGLTF_ERROR_JSON; } +#define CGLTF_CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return CGLTF_ERROR_JSON; } /* checking size for 0 verifies that a value follows the key */ + +#define CGLTF_PTRINDEX(type, idx) (type*)((cgltf_size)idx + 1) +#define CGLTF_PTRFIXUP(var, data, size) if (var) { if ((cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1]; } +#define CGLTF_PTRFIXUP_REQ(var, data, size) if (!var || (cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1]; + +static int cgltf_json_strcmp(jsmntok_t const* tok, const uint8_t* json_chunk, const char* str) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_STRING); + size_t const str_len = strlen(str); + size_t const name_length = tok->end - tok->start; + return (str_len == name_length) ? strncmp((const char*)json_chunk + tok->start, str, str_len) : 128; +} + +static int cgltf_json_to_int(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE); + char tmp[128]; + int size = (cgltf_size)(tok->end - tok->start) < sizeof(tmp) ? tok->end - tok->start : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + return CGLTF_ATOI(tmp); +} + +static cgltf_size cgltf_json_to_size(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE_RETTYPE(*tok, JSMN_PRIMITIVE, cgltf_size); + char tmp[128]; + int size = (cgltf_size)(tok->end - tok->start) < sizeof(tmp) ? tok->end - tok->start : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + return (cgltf_size)CGLTF_ATOLL(tmp); +} + +static cgltf_float cgltf_json_to_float(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE); + char tmp[128]; + int size = (cgltf_size)(tok->end - tok->start) < sizeof(tmp) ? tok->end - tok->start : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + return (cgltf_float)CGLTF_ATOF(tmp); +} + +static cgltf_bool cgltf_json_to_bool(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int size = tok->end - tok->start; + return size == 4 && memcmp(json_chunk + tok->start, "true", 4) == 0; +} + +static int cgltf_skip_json(jsmntok_t const* tokens, int i) +{ + int end = i + 1; + + while (i < end) + { + switch (tokens[i].type) + { + case JSMN_OBJECT: + end += tokens[i].size * 2; + break; + + case JSMN_ARRAY: + end += tokens[i].size; + break; + + case JSMN_PRIMITIVE: + case JSMN_STRING: + break; + + default: + return -1; + } + + i++; + } + + return i; +} + +static void cgltf_fill_float_array(float* out_array, int size, float value) +{ + for (int j = 0; j < size; ++j) + { + out_array[j] = value; + } +} + +static int cgltf_parse_json_float_array(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, float* out_array, int size) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + if (tokens[i].size != size) + { + return CGLTF_ERROR_JSON; + } + ++i; + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_array[j] = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + return i; +} + +static int cgltf_parse_json_string(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char** out_string) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_STRING); + if (*out_string) + { + return CGLTF_ERROR_JSON; + } + int size = tokens[i].end - tokens[i].start; + char* result = (char*)options->memory.alloc(options->memory.user_data, size + 1); + if (!result) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(result, (const char*)json_chunk + tokens[i].start, size); + result[size] = 0; + *out_string = result; + return i + 1; +} + +static int cgltf_parse_json_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, size_t element_size, void** out_array, cgltf_size* out_size) +{ + (void)json_chunk; + if (tokens[i].type != JSMN_ARRAY) + { + return tokens[i].type == JSMN_OBJECT ? CGLTF_ERROR_LEGACY : CGLTF_ERROR_JSON; + } + if (*out_array) + { + return CGLTF_ERROR_JSON; + } + int size = tokens[i].size; + void* result = cgltf_calloc(options, element_size, size); + if (!result) + { + return CGLTF_ERROR_NOMEM; + } + *out_array = result; + *out_size = size; + return i + 1; +} + +static int cgltf_parse_json_string_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char*** out_array, cgltf_size* out_size) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(char*), (void**)out_array, out_size); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < *out_size; ++j) + { + i = cgltf_parse_json_string(options, tokens, i, json_chunk, j + (*out_array)); + if (i < 0) + { + return i; + } + } + return i; +} + +static void cgltf_parse_attribute_type(const char* name, cgltf_attribute_type* out_type, int* out_index) +{ + const char* us = strchr(name, '_'); + size_t len = us ? (size_t)(us - name) : strlen(name); + + if (len == 8 && strncmp(name, "POSITION", 8) == 0) + { + *out_type = cgltf_attribute_type_position; + } + else if (len == 6 && strncmp(name, "NORMAL", 6) == 0) + { + *out_type = cgltf_attribute_type_normal; + } + else if (len == 7 && strncmp(name, "TANGENT", 7) == 0) + { + *out_type = cgltf_attribute_type_tangent; + } + else if (len == 8 && strncmp(name, "TEXCOORD", 8) == 0) + { + *out_type = cgltf_attribute_type_texcoord; + } + else if (len == 5 && strncmp(name, "COLOR", 5) == 0) + { + *out_type = cgltf_attribute_type_color; + } + else if (len == 6 && strncmp(name, "JOINTS", 6) == 0) + { + *out_type = cgltf_attribute_type_joints; + } + else if (len == 7 && strncmp(name, "WEIGHTS", 7) == 0) + { + *out_type = cgltf_attribute_type_weights; + } + else + { + *out_type = cgltf_attribute_type_invalid; + } + + if (us && *out_type != cgltf_attribute_type_invalid) + { + *out_index = CGLTF_ATOI(us + 1); + } +} + +static int cgltf_parse_json_attribute_list(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_attribute** out_attributes, cgltf_size* out_attributes_count) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + if (*out_attributes) + { + return CGLTF_ERROR_JSON; + } + + *out_attributes_count = tokens[i].size; + *out_attributes = (cgltf_attribute*)cgltf_calloc(options, sizeof(cgltf_attribute), *out_attributes_count); + ++i; + + if (!*out_attributes) + { + return CGLTF_ERROR_NOMEM; + } + + for (cgltf_size j = 0; j < *out_attributes_count; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + i = cgltf_parse_json_string(options, tokens, i, json_chunk, &(*out_attributes)[j].name); + if (i < 0) + { + return CGLTF_ERROR_JSON; + } + + cgltf_parse_attribute_type((*out_attributes)[j].name, &(*out_attributes)[j].type, &(*out_attributes)[j].index); + + (*out_attributes)[j].data = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + + return i; +} + +static int cgltf_parse_json_extras(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_extras* out_extras) +{ + (void)json_chunk; + out_extras->start_offset = tokens[i].start; + out_extras->end_offset = tokens[i].end; + i = cgltf_skip_json(tokens, i); + return i; +} + +static int cgltf_parse_json_unprocessed_extension(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_extension* out_extension) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_STRING); + CGLTF_CHECK_TOKTYPE(tokens[i+1], JSMN_OBJECT); + if (out_extension->name) + { + return CGLTF_ERROR_JSON; + } + + cgltf_size name_length = tokens[i].end - tokens[i].start; + out_extension->name = (char*)options->memory.alloc(options->memory.user_data, name_length + 1); + if (!out_extension->name) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extension->name, (const char*)json_chunk + tokens[i].start, name_length); + out_extension->name[name_length] = 0; + i++; + + size_t start = tokens[i].start; + size_t size = tokens[i].end - start; + out_extension->data = (char*)options->memory.alloc(options->memory.user_data, size + 1); + if (!out_extension->data) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extension->data, (const char*)json_chunk + start, size); + out_extension->data[size] = '\0'; + + i = cgltf_skip_json(tokens, i); + + return i; +} + +static int cgltf_parse_json_unprocessed_extensions(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_size* out_extensions_count, cgltf_extension** out_extensions) +{ + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(*out_extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + *out_extensions_count = 0; + *out_extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!*out_extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int j = 0; j < extensions_size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + cgltf_size extension_index = (*out_extensions_count)++; + cgltf_extension* extension = &((*out_extensions)[extension_index]); + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, extension); + + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_draco_mesh_compression(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_draco_mesh_compression* out_draco_mesh_compression) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_draco_mesh_compression->attributes, &out_draco_mesh_compression->attributes_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "bufferView") == 0) + { + ++i; + out_draco_mesh_compression->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_material_mapping_data(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material_mapping* out_mappings, cgltf_size* offset) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int obj_size = tokens[i].size; + ++i; + + int material = -1; + int variants_tok = -1; + cgltf_extras extras = {0, 0}; + + for (int k = 0; k < obj_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "material") == 0) + { + ++i; + material = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "variants") == 0) + { + variants_tok = i+1; + CGLTF_CHECK_TOKTYPE(tokens[variants_tok], JSMN_ARRAY); + + i = cgltf_skip_json(tokens, i+1); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + if (material < 0 || variants_tok < 0) + { + return CGLTF_ERROR_JSON; + } + + if (out_mappings) + { + for (int k = 0; k < tokens[variants_tok].size; ++k) + { + int variant = cgltf_json_to_int(&tokens[variants_tok + 1 + k], json_chunk); + if (variant < 0) + return variant; + + out_mappings[*offset].material = CGLTF_PTRINDEX(cgltf_material, material); + out_mappings[*offset].variant = variant; + out_mappings[*offset].extras = extras; + + (*offset)++; + } + } + else + { + (*offset) += tokens[variants_tok].size; + } + } + + return i; +} + +static int cgltf_parse_json_material_mappings(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "mappings") == 0) + { + if (out_prim->mappings) + { + return CGLTF_ERROR_JSON; + } + + cgltf_size mappings_offset = 0; + int k = cgltf_parse_json_material_mapping_data(options, tokens, i + 1, json_chunk, NULL, &mappings_offset); + if (k < 0) + { + return k; + } + + out_prim->mappings_count = mappings_offset; + out_prim->mappings = (cgltf_material_mapping*)cgltf_calloc(options, sizeof(cgltf_material_mapping), out_prim->mappings_count); + + mappings_offset = 0; + i = cgltf_parse_json_material_mapping_data(options, tokens, i + 1, json_chunk, out_prim->mappings, &mappings_offset); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_primitive(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_prim->type = cgltf_primitive_type_triangles; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) + { + ++i; + out_prim->type + = (cgltf_primitive_type) + cgltf_json_to_int(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) + { + ++i; + out_prim->indices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "material") == 0) + { + ++i; + out_prim->material = CGLTF_PTRINDEX(cgltf_material, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_prim->attributes, &out_prim->attributes_count); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "targets") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_morph_target), (void**)&out_prim->targets, &out_prim->targets_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_prim->targets_count; ++k) + { + i = cgltf_parse_json_attribute_list(options, tokens, i, json_chunk, &out_prim->targets[k].attributes, &out_prim->targets[k].attributes_count); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_prim->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_prim->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_prim->extensions_count = 0; + out_prim->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_prim->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_draco_mesh_compression") == 0) + { + out_prim->has_draco_mesh_compression = 1; + i = cgltf_parse_json_draco_mesh_compression(options, tokens, i + 1, json_chunk, &out_prim->draco_mesh_compression); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_variants") == 0) + { + i = cgltf_parse_json_material_mappings(options, tokens, i + 1, json_chunk, out_prim); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_prim->extensions[out_prim->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_mesh(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_mesh* out_mesh) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_mesh->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "primitives") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_primitive), (void**)&out_mesh->primitives, &out_mesh->primitives_count); + if (i < 0) + { + return i; + } + + for (cgltf_size prim_index = 0; prim_index < out_mesh->primitives_count; ++prim_index) + { + i = cgltf_parse_json_primitive(options, tokens, i, json_chunk, &out_mesh->primitives[prim_index]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_mesh->weights, &out_mesh->weights_count); + if (i < 0) + { + return i; + } + + i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_mesh->weights, (int)out_mesh->weights_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + ++i; + + out_mesh->extras.start_offset = tokens[i].start; + out_mesh->extras.end_offset = tokens[i].end; + + if (tokens[i].type == JSMN_OBJECT) + { + int extras_size = tokens[i].size; + ++i; + + for (int k = 0; k < extras_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "targetNames") == 0 && tokens[i+1].type == JSMN_ARRAY) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_mesh->target_names, &out_mesh->target_names_count); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i); + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_mesh->extensions_count, &out_mesh->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_meshes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_mesh), (void**)&out_data->meshes, &out_data->meshes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->meshes_count; ++j) + { + i = cgltf_parse_json_mesh(options, tokens, i, json_chunk, &out_data->meshes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static cgltf_component_type cgltf_json_to_component_type(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int type = cgltf_json_to_int(tok, json_chunk); + + switch (type) + { + case 5120: + return cgltf_component_type_r_8; + case 5121: + return cgltf_component_type_r_8u; + case 5122: + return cgltf_component_type_r_16; + case 5123: + return cgltf_component_type_r_16u; + case 5125: + return cgltf_component_type_r_32u; + case 5126: + return cgltf_component_type_r_32f; + default: + return cgltf_component_type_invalid; + } +} + +static int cgltf_parse_json_accessor_sparse(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor_sparse* out_sparse) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_sparse->count = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int indices_size = tokens[i].size; + ++i; + + for (int k = 0; k < indices_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_sparse->indices_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_sparse->indices_byte_offset = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0) + { + ++i; + out_sparse->indices_component_type = cgltf_json_to_component_type(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sparse->indices_extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sparse->indices_extensions_count, &out_sparse->indices_extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "values") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int values_size = tokens[i].size; + ++i; + + for (int k = 0; k < values_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_sparse->values_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_sparse->values_byte_offset = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sparse->values_extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sparse->values_extensions_count, &out_sparse->values_extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sparse->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sparse->extensions_count, &out_sparse->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_accessor(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor* out_accessor) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_accessor->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_accessor->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_accessor->offset = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0) + { + ++i; + out_accessor->component_type = cgltf_json_to_component_type(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "normalized") == 0) + { + ++i; + out_accessor->normalized = cgltf_json_to_bool(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_accessor->count = + cgltf_json_to_int(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "SCALAR") == 0) + { + out_accessor->type = cgltf_type_scalar; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC2") == 0) + { + out_accessor->type = cgltf_type_vec2; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC3") == 0) + { + out_accessor->type = cgltf_type_vec3; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC4") == 0) + { + out_accessor->type = cgltf_type_vec4; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT2") == 0) + { + out_accessor->type = cgltf_type_mat2; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT3") == 0) + { + out_accessor->type = cgltf_type_mat3; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT4") == 0) + { + out_accessor->type = cgltf_type_mat4; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "min") == 0) + { + ++i; + out_accessor->has_min = 1; + // note: we can't parse the precise number of elements since type may not have been computed yet + int min_size = tokens[i].size > 16 ? 16 : tokens[i].size; + i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->min, min_size); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "max") == 0) + { + ++i; + out_accessor->has_max = 1; + // note: we can't parse the precise number of elements since type may not have been computed yet + int max_size = tokens[i].size > 16 ? 16 : tokens[i].size; + i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->max, max_size); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "sparse") == 0) + { + out_accessor->is_sparse = 1; + i = cgltf_parse_json_accessor_sparse(options, tokens, i + 1, json_chunk, &out_accessor->sparse); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_accessor->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_accessor->extensions_count, &out_accessor->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture_transform(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_transform* out_texture_transform) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "offset") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->offset, 2); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "rotation") == 0) + { + ++i; + out_texture_transform->rotation = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->scale, 2); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0) + { + ++i; + out_texture_transform->has_texcoord = 1; + out_texture_transform->texcoord = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture_view(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_view* out_texture_view) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_texture_view->scale = 1.0f; + cgltf_fill_float_array(out_texture_view->transform.scale, 2, 1.0f); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "index") == 0) + { + ++i; + out_texture_view->texture = CGLTF_PTRINDEX(cgltf_texture, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0) + { + ++i; + out_texture_view->texcoord = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) + { + ++i; + out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "strength") == 0) + { + ++i; + out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_texture_view->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_texture_view->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_texture_view->extensions_count = 0; + out_texture_view->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_texture_view->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_texture_transform") == 0) + { + out_texture_view->has_transform = 1; + i = cgltf_parse_json_texture_transform(tokens, i + 1, json_chunk, &out_texture_view->transform); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_texture_view->extensions[out_texture_view->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_pbr_metallic_roughness(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_metallic_roughness* out_pbr) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "metallicFactor") == 0) + { + ++i; + out_pbr->metallic_factor = + cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "roughnessFactor") == 0) + { + ++i; + out_pbr->roughness_factor = + cgltf_json_to_float(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->base_color_factor, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_pbr->base_color_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "metallicRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_pbr->metallic_roughness_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_pbr->extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_pbr_specular_glossiness(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_specular_glossiness* out_pbr) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->diffuse_factor, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->specular_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "glossinessFactor") == 0) + { + ++i; + out_pbr->glossiness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->diffuse_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularGlossinessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->specular_glossiness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_clearcoat(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_clearcoat* out_clearcoat) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatFactor") == 0) + { + ++i; + out_clearcoat->clearcoat_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatRoughnessFactor") == 0) + { + ++i; + out_clearcoat->clearcoat_roughness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_roughness_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatNormalTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_normal_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_ior(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_ior* out_ior) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default values + out_ior->ior = 1.5f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "ior") == 0) + { + ++i; + out_ior->ior = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_specular(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_specular* out_specular) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default values + out_specular->specular_factor = 1.0f; + cgltf_fill_float_array(out_specular->specular_color_factor, 3, 1.0f); + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "specularFactor") == 0) + { + ++i; + out_specular->specular_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_specular->specular_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_specular->specular_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "specularColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_specular->specular_color_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_transmission(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_transmission* out_transmission) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "transmissionFactor") == 0) + { + ++i; + out_transmission->transmission_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "transmissionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_transmission->transmission_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_volume(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_volume* out_volume) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "thicknessFactor") == 0) + { + ++i; + out_volume->thickness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "thicknessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_volume->thickness_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "attenuationColor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_volume->attenuation_color, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "attenuationDistance") == 0) + { + ++i; + out_volume->attenuation_distance = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_sheen(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_sheen* out_sheen) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_sheen->sheen_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_sheen->sheen_color_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenRoughnessFactor") == 0) + { + ++i; + out_sheen->sheen_roughness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_sheen->sheen_roughness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_image(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_image* out_image) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "uri") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->uri); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_image->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "mimeType") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->mime_type); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_image->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_image->extensions_count, &out_image->extensions); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_sampler* out_sampler) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_sampler->wrap_s = 10497; + out_sampler->wrap_t = 10497; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_sampler->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "magFilter") == 0) + { + ++i; + out_sampler->mag_filter + = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "minFilter") == 0) + { + ++i; + out_sampler->min_filter + = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapS") == 0) + { + ++i; + out_sampler->wrap_s + = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapT") == 0) + { + ++i; + out_sampler->wrap_t + = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sampler->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sampler->extensions_count, &out_sampler->extensions); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture* out_texture) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_texture->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "sampler") == 0) + { + ++i; + out_texture->sampler = CGLTF_PTRINDEX(cgltf_sampler, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_texture->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if (out_texture->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + ++i; + out_texture->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + out_texture->extensions_count = 0; + + if (!out_texture->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_texture_basisu") == 0) + { + out_texture->has_basisu = 1; + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + + for (int t = 0; t < num_properties; ++t) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->basisu_image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + } + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_texture->extensions[out_texture->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_material(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material* out_material) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + cgltf_fill_float_array(out_material->pbr_metallic_roughness.base_color_factor, 4, 1.0f); + out_material->pbr_metallic_roughness.metallic_factor = 1.0f; + out_material->pbr_metallic_roughness.roughness_factor = 1.0f; + + cgltf_fill_float_array(out_material->pbr_specular_glossiness.diffuse_factor, 4, 1.0f); + cgltf_fill_float_array(out_material->pbr_specular_glossiness.specular_factor, 3, 1.0f); + out_material->pbr_specular_glossiness.glossiness_factor = 1.0f; + + cgltf_fill_float_array(out_material->volume.attenuation_color, 3, 1.0f); + out_material->volume.attenuation_distance = FLT_MAX; + + out_material->alpha_cutoff = 0.5f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_material->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "pbrMetallicRoughness") == 0) + { + out_material->has_pbr_metallic_roughness = 1; + i = cgltf_parse_json_pbr_metallic_roughness(options, tokens, i + 1, json_chunk, &out_material->pbr_metallic_roughness); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "emissiveFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_material->emissive_factor, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "normalTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->normal_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "occlusionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->occlusion_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "emissiveTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->emissive_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaMode") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "OPAQUE") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_opaque; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "MASK") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_mask; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "BLEND") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_blend; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaCutoff") == 0) + { + ++i; + out_material->alpha_cutoff = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "doubleSided") == 0) + { + ++i; + out_material->double_sided = + cgltf_json_to_bool(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_material->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_material->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + ++i; + out_material->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + out_material->extensions_count= 0; + + if (!out_material->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_pbrSpecularGlossiness") == 0) + { + out_material->has_pbr_specular_glossiness = 1; + i = cgltf_parse_json_pbr_specular_glossiness(options, tokens, i + 1, json_chunk, &out_material->pbr_specular_glossiness); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_unlit") == 0) + { + out_material->unlit = 1; + i = cgltf_skip_json(tokens, i+1); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_clearcoat") == 0) + { + out_material->has_clearcoat = 1; + i = cgltf_parse_json_clearcoat(options, tokens, i + 1, json_chunk, &out_material->clearcoat); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_ior") == 0) + { + out_material->has_ior = 1; + i = cgltf_parse_json_ior(tokens, i + 1, json_chunk, &out_material->ior); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_specular") == 0) + { + out_material->has_specular = 1; + i = cgltf_parse_json_specular(options, tokens, i + 1, json_chunk, &out_material->specular); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_transmission") == 0) + { + out_material->has_transmission = 1; + i = cgltf_parse_json_transmission(options, tokens, i + 1, json_chunk, &out_material->transmission); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_volume") == 0) + { + out_material->has_volume = 1; + i = cgltf_parse_json_volume(options, tokens, i + 1, json_chunk, &out_material->volume); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_sheen") == 0) + { + out_material->has_sheen = 1; + i = cgltf_parse_json_sheen(options, tokens, i + 1, json_chunk, &out_material->sheen); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_material->extensions[out_material->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_accessors(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_accessor), (void**)&out_data->accessors, &out_data->accessors_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->accessors_count; ++j) + { + i = cgltf_parse_json_accessor(options, tokens, i, json_chunk, &out_data->accessors[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_materials(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_material), (void**)&out_data->materials, &out_data->materials_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->materials_count; ++j) + { + i = cgltf_parse_json_material(options, tokens, i, json_chunk, &out_data->materials[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_images(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_image), (void**)&out_data->images, &out_data->images_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->images_count; ++j) + { + i = cgltf_parse_json_image(options, tokens, i, json_chunk, &out_data->images[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_textures(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_texture), (void**)&out_data->textures, &out_data->textures_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->textures_count; ++j) + { + i = cgltf_parse_json_texture(options, tokens, i, json_chunk, &out_data->textures[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_samplers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_sampler), (void**)&out_data->samplers, &out_data->samplers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->samplers_count; ++j) + { + i = cgltf_parse_json_sampler(options, tokens, i, json_chunk, &out_data->samplers[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_meshopt_compression(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_meshopt_compression* out_meshopt_compression) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "buffer") == 0) + { + ++i; + out_meshopt_compression->buffer = CGLTF_PTRINDEX(cgltf_buffer, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_meshopt_compression->offset = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_meshopt_compression->size = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteStride") == 0) + { + ++i; + out_meshopt_compression->stride = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_meshopt_compression->count = cgltf_json_to_int(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "ATTRIBUTES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_attributes; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "TRIANGLES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_triangles; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "INDICES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_indices; + } + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "filter") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "NONE") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_none; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "OCTAHEDRAL") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_octahedral; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "QUATERNION") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_quaternion; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "EXPONENTIAL") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_exponential; + } + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffer_view(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer_view* out_buffer_view) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer_view->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "buffer") == 0) + { + ++i; + out_buffer_view->buffer = CGLTF_PTRINDEX(cgltf_buffer, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_buffer_view->offset = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_buffer_view->size = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteStride") == 0) + { + ++i; + out_buffer_view->stride = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0) + { + ++i; + int type = cgltf_json_to_int(tokens+i, json_chunk); + switch (type) + { + case 34962: + type = cgltf_buffer_view_type_vertices; + break; + case 34963: + type = cgltf_buffer_view_type_indices; + break; + default: + type = cgltf_buffer_view_type_invalid; + break; + } + out_buffer_view->type = (cgltf_buffer_view_type)type; + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_buffer_view->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_buffer_view->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_buffer_view->extensions_count = 0; + out_buffer_view->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_buffer_view->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "EXT_meshopt_compression") == 0) + { + out_buffer_view->has_meshopt_compression = 1; + i = cgltf_parse_json_meshopt_compression(options, tokens, i + 1, json_chunk, &out_buffer_view->meshopt_compression); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_buffer_view->extensions[out_buffer_view->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffer_views(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer_view), (void**)&out_data->buffer_views, &out_data->buffer_views_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->buffer_views_count; ++j) + { + i = cgltf_parse_json_buffer_view(options, tokens, i, json_chunk, &out_data->buffer_views[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_buffer(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer* out_buffer) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_buffer->size = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "uri") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer->uri); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_buffer->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_buffer->extensions_count, &out_buffer->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer), (void**)&out_data->buffers, &out_data->buffers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->buffers_count; ++j) + { + i = cgltf_parse_json_buffer(options, tokens, i, json_chunk, &out_data->buffers[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_skin(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_skin* out_skin) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_skin->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "joints") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_skin->joints, &out_skin->joints_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_skin->joints_count; ++k) + { + out_skin->joints[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "skeleton") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_skin->skeleton = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "inverseBindMatrices") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_skin->inverse_bind_matrices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_skin->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_skin->extensions_count, &out_skin->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_skins(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_skin), (void**)&out_data->skins, &out_data->skins_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->skins_count; ++j) + { + i = cgltf_parse_json_skin(options, tokens, i, json_chunk, &out_data->skins[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_camera(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_camera* out_camera) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_camera->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "perspective") == 0) + { + out_camera->type = cgltf_camera_type_perspective; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "orthographic") == 0) + { + out_camera->type = cgltf_camera_type_orthographic; + } + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "perspective") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + out_camera->type = cgltf_camera_type_perspective; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "aspectRatio") == 0) + { + ++i; + out_camera->data.perspective.has_aspect_ratio = 1; + out_camera->data.perspective.aspect_ratio = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "yfov") == 0) + { + ++i; + out_camera->data.perspective.yfov = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0) + { + ++i; + out_camera->data.perspective.has_zfar = 1; + out_camera->data.perspective.zfar = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0) + { + ++i; + out_camera->data.perspective.znear = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_camera->data.perspective.extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "orthographic") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + out_camera->type = cgltf_camera_type_orthographic; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "xmag") == 0) + { + ++i; + out_camera->data.orthographic.xmag = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "ymag") == 0) + { + ++i; + out_camera->data.orthographic.ymag = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0) + { + ++i; + out_camera->data.orthographic.zfar = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0) + { + ++i; + out_camera->data.orthographic.znear = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_camera->data.orthographic.extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_camera->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_camera->extensions_count, &out_camera->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_cameras(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_camera), (void**)&out_data->cameras, &out_data->cameras_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->cameras_count; ++j) + { + i = cgltf_parse_json_camera(options, tokens, i, json_chunk, &out_data->cameras[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_light(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_light* out_light) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_light->color[0] = 1.f; + out_light->color[1] = 1.f; + out_light->color[2] = 1.f; + out_light->intensity = 1.f; + + out_light->spot_inner_cone_angle = 0.f; + out_light->spot_outer_cone_angle = 3.1415926535f / 4.0f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_light->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "color") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_light->color, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "intensity") == 0) + { + ++i; + out_light->intensity = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "directional") == 0) + { + out_light->type = cgltf_light_type_directional; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "point") == 0) + { + out_light->type = cgltf_light_type_point; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "spot") == 0) + { + out_light->type = cgltf_light_type_spot; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "range") == 0) + { + ++i; + out_light->range = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "spot") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "innerConeAngle") == 0) + { + ++i; + out_light->spot_inner_cone_angle = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "outerConeAngle") == 0) + { + ++i; + out_light->spot_outer_cone_angle = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_light->extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_lights(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_light), (void**)&out_data->lights, &out_data->lights_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->lights_count; ++j) + { + i = cgltf_parse_json_light(options, tokens, i, json_chunk, &out_data->lights[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_node(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_node* out_node) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_node->rotation[3] = 1.0f; + out_node->scale[0] = 1.0f; + out_node->scale[1] = 1.0f; + out_node->scale[2] = 1.0f; + out_node->matrix[0] = 1.0f; + out_node->matrix[5] = 1.0f; + out_node->matrix[10] = 1.0f; + out_node->matrix[15] = 1.0f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_node->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "children") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_node->children, &out_node->children_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_node->children_count; ++k) + { + out_node->children[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "mesh") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->mesh = CGLTF_PTRINDEX(cgltf_mesh, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "skin") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->skin = CGLTF_PTRINDEX(cgltf_skin, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "camera") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->camera = CGLTF_PTRINDEX(cgltf_camera, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0) + { + out_node->has_translation = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->translation, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0) + { + out_node->has_rotation = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->rotation, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0) + { + out_node->has_scale = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->scale, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "matrix") == 0) + { + out_node->has_matrix = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->matrix, 16); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_node->weights, &out_node->weights_count); + if (i < 0) + { + return i; + } + + i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_node->weights, (int)out_node->weights_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_node->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_node->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_node->extensions_count= 0; + out_node->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_node->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "light") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->light = CGLTF_PTRINDEX(cgltf_light, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_node->extensions[out_node->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_nodes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_node), (void**)&out_data->nodes, &out_data->nodes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->nodes_count; ++j) + { + i = cgltf_parse_json_node(options, tokens, i, json_chunk, &out_data->nodes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_scene(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_scene* out_scene) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_scene->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "nodes") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_scene->nodes, &out_scene->nodes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_scene->nodes_count; ++k) + { + out_scene->nodes[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_scene->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_scene->extensions_count, &out_scene->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_scenes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_scene), (void**)&out_data->scenes, &out_data->scenes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->scenes_count; ++j) + { + i = cgltf_parse_json_scene(options, tokens, i, json_chunk, &out_data->scenes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_animation_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_sampler* out_sampler) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "input") == 0) + { + ++i; + out_sampler->input = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "output") == 0) + { + ++i; + out_sampler->output = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "interpolation") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "LINEAR") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_linear; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "STEP") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_step; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "CUBICSPLINE") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_cubic_spline; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sampler->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sampler->extensions_count, &out_sampler->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animation_channel(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_channel* out_channel) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "sampler") == 0) + { + ++i; + out_channel->sampler = CGLTF_PTRINDEX(cgltf_animation_sampler, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int target_size = tokens[i].size; + ++i; + + for (int k = 0; k < target_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "node") == 0) + { + ++i; + out_channel->target_node = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "path") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0) + { + out_channel->target_path = cgltf_animation_path_type_translation; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0) + { + out_channel->target_path = cgltf_animation_path_type_rotation; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0) + { + out_channel->target_path = cgltf_animation_path_type_scale; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "weights") == 0) + { + out_channel->target_path = cgltf_animation_path_type_weights; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_channel->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_channel->extensions_count, &out_channel->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animation(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation* out_animation) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_animation->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "samplers") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_sampler), (void**)&out_animation->samplers, &out_animation->samplers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_animation->samplers_count; ++k) + { + i = cgltf_parse_json_animation_sampler(options, tokens, i, json_chunk, &out_animation->samplers[k]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "channels") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_channel), (void**)&out_animation->channels, &out_animation->channels_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_animation->channels_count; ++k) + { + i = cgltf_parse_json_animation_channel(options, tokens, i, json_chunk, &out_animation->channels[k]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_animation->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_animation->extensions_count, &out_animation->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animations(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_animation), (void**)&out_data->animations, &out_data->animations_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->animations_count; ++j) + { + i = cgltf_parse_json_animation(options, tokens, i, json_chunk, &out_data->animations[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_variant(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material_variant* out_variant) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_variant->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_variant->extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_variants(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_material_variant), (void**)&out_data->variants, &out_data->variants_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->variants_count; ++j) + { + i = cgltf_parse_json_variant(options, tokens, i, json_chunk, &out_data->variants[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_asset(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_asset* out_asset) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "copyright") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->copyright); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "generator") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->generator); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "version") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->version); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "minVersion") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->min_version); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_asset->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_asset->extensions_count, &out_asset->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + if (out_asset->version && CGLTF_ATOF(out_asset->version) < 2) + { + return CGLTF_ERROR_LEGACY; + } + + return i; +} + +cgltf_size cgltf_num_components(cgltf_type type) { + switch (type) + { + case cgltf_type_vec2: + return 2; + case cgltf_type_vec3: + return 3; + case cgltf_type_vec4: + return 4; + case cgltf_type_mat2: + return 4; + case cgltf_type_mat3: + return 9; + case cgltf_type_mat4: + return 16; + case cgltf_type_invalid: + case cgltf_type_scalar: + default: + return 1; + } +} + +static cgltf_size cgltf_component_size(cgltf_component_type component_type) { + switch (component_type) + { + case cgltf_component_type_r_8: + case cgltf_component_type_r_8u: + return 1; + case cgltf_component_type_r_16: + case cgltf_component_type_r_16u: + return 2; + case cgltf_component_type_r_32u: + case cgltf_component_type_r_32f: + return 4; + case cgltf_component_type_invalid: + default: + return 0; + } +} + +static cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type) +{ + cgltf_size component_size = cgltf_component_size(component_type); + if (type == cgltf_type_mat2 && component_size == 1) + { + return 8 * component_size; + } + else if (type == cgltf_type_mat3 && (component_size == 1 || component_size == 2)) + { + return 12 * component_size; + } + return component_size * cgltf_num_components(type); +} + +static int cgltf_fixup_pointers(cgltf_data* out_data); + +static int cgltf_parse_json_root(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "asset") == 0) + { + i = cgltf_parse_json_asset(options, tokens, i + 1, json_chunk, &out_data->asset); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "meshes") == 0) + { + i = cgltf_parse_json_meshes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "accessors") == 0) + { + i = cgltf_parse_json_accessors(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "bufferViews") == 0) + { + i = cgltf_parse_json_buffer_views(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "buffers") == 0) + { + i = cgltf_parse_json_buffers(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "materials") == 0) + { + i = cgltf_parse_json_materials(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "images") == 0) + { + i = cgltf_parse_json_images(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "textures") == 0) + { + i = cgltf_parse_json_textures(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "samplers") == 0) + { + i = cgltf_parse_json_samplers(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "skins") == 0) + { + i = cgltf_parse_json_skins(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "cameras") == 0) + { + i = cgltf_parse_json_cameras(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "nodes") == 0) + { + i = cgltf_parse_json_nodes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scenes") == 0) + { + i = cgltf_parse_json_scenes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scene") == 0) + { + ++i; + out_data->scene = CGLTF_PTRINDEX(cgltf_scene, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "animations") == 0) + { + i = cgltf_parse_json_animations(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_data->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_data->data_extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_data->data_extensions_count = 0; + out_data->data_extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_data->data_extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "lights") == 0) + { + i = cgltf_parse_json_lights(options, tokens, i + 1, json_chunk, out_data); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_variants") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "variants") == 0) + { + i = cgltf_parse_json_variants(options, tokens, i + 1, json_chunk, out_data); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_data->data_extensions[out_data->data_extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsUsed") == 0) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_used, &out_data->extensions_used_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsRequired") == 0) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_required, &out_data->extensions_required_count); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data) +{ + jsmn_parser parser = { 0, 0, 0 }; + + if (options->json_token_count == 0) + { + int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, NULL, 0); + + if (token_count <= 0) + { + return cgltf_result_invalid_json; + } + + options->json_token_count = token_count; + } + + jsmntok_t* tokens = (jsmntok_t*)options->memory.alloc(options->memory.user_data, sizeof(jsmntok_t) * (options->json_token_count + 1)); + + if (!tokens) + { + return cgltf_result_out_of_memory; + } + + jsmn_init(&parser); + + int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, tokens, options->json_token_count); + + if (token_count <= 0) + { + options->memory.free(options->memory.user_data, tokens); + return cgltf_result_invalid_json; + } + + // this makes sure that we always have an UNDEFINED token at the end of the stream + // for invalid JSON inputs this makes sure we don't perform out of bound reads of token data + tokens[token_count].type = JSMN_UNDEFINED; + + cgltf_data* data = (cgltf_data*)options->memory.alloc(options->memory.user_data, sizeof(cgltf_data)); + + if (!data) + { + options->memory.free(options->memory.user_data, tokens); + return cgltf_result_out_of_memory; + } + + memset(data, 0, sizeof(cgltf_data)); + data->memory = options->memory; + data->file = options->file; + + int i = cgltf_parse_json_root(options, tokens, 0, json_chunk, data); + + options->memory.free(options->memory.user_data, tokens); + + if (i < 0) + { + cgltf_free(data); + + switch (i) + { + case CGLTF_ERROR_NOMEM: return cgltf_result_out_of_memory; + case CGLTF_ERROR_LEGACY: return cgltf_result_legacy_gltf; + default: return cgltf_result_invalid_gltf; + } + } + + if (cgltf_fixup_pointers(data) < 0) + { + cgltf_free(data); + return cgltf_result_invalid_gltf; + } + + data->json = (const char*)json_chunk; + data->json_size = size; + + *out_data = data; + + return cgltf_result_success; +} + +static int cgltf_fixup_pointers(cgltf_data* data) +{ + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + CGLTF_PTRFIXUP(data->meshes[i].primitives[j].indices, data->accessors, data->accessors_count); + CGLTF_PTRFIXUP(data->meshes[i].primitives[j].material, data->materials, data->materials_count); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].attributes[k].data, data->accessors, data->accessors_count); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].targets[k].attributes[m].data, data->accessors, data->accessors_count); + } + } + + if (data->meshes[i].primitives[j].has_draco_mesh_compression) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].draco_mesh_compression.buffer_view, data->buffer_views, data->buffer_views_count); + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].draco_mesh_compression.attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].draco_mesh_compression.attributes[m].data, data->accessors, data->accessors_count); + } + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].mappings[k].material, data->materials, data->materials_count); + } + } + } + + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + CGLTF_PTRFIXUP(data->accessors[i].buffer_view, data->buffer_views, data->buffer_views_count); + + if (data->accessors[i].is_sparse) + { + CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.indices_buffer_view, data->buffer_views, data->buffer_views_count); + CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.values_buffer_view, data->buffer_views, data->buffer_views_count); + } + + if (data->accessors[i].buffer_view) + { + data->accessors[i].stride = data->accessors[i].buffer_view->stride; + } + + if (data->accessors[i].stride == 0) + { + data->accessors[i].stride = cgltf_calc_size(data->accessors[i].type, data->accessors[i].component_type); + } + } + + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + CGLTF_PTRFIXUP(data->textures[i].image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].basisu_image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].sampler, data->samplers, data->samplers_count); + } + + for (cgltf_size i = 0; i < data->images_count; ++i) + { + CGLTF_PTRFIXUP(data->images[i].buffer_view, data->buffer_views, data->buffer_views_count); + } + + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + CGLTF_PTRFIXUP(data->materials[i].normal_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].emissive_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].occlusion_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.base_color_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.metallic_roughness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.diffuse_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.specular_glossiness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_roughness_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_normal_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].specular.specular_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].specular.specular_color_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].transmission.transmission_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].volume.thickness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].sheen.sheen_color_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].sheen.sheen_roughness_texture.texture, data->textures, data->textures_count); + } + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + CGLTF_PTRFIXUP_REQ(data->buffer_views[i].buffer, data->buffers, data->buffers_count); + + if (data->buffer_views[i].has_meshopt_compression) + { + CGLTF_PTRFIXUP_REQ(data->buffer_views[i].meshopt_compression.buffer, data->buffers, data->buffers_count); + } + } + + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + for (cgltf_size j = 0; j < data->skins[i].joints_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->skins[i].joints[j], data->nodes, data->nodes_count); + } + + CGLTF_PTRFIXUP(data->skins[i].skeleton, data->nodes, data->nodes_count); + CGLTF_PTRFIXUP(data->skins[i].inverse_bind_matrices, data->accessors, data->accessors_count); + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + for (cgltf_size j = 0; j < data->nodes[i].children_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->nodes[i].children[j], data->nodes, data->nodes_count); + + if (data->nodes[i].children[j]->parent) + { + return CGLTF_ERROR_JSON; + } + + data->nodes[i].children[j]->parent = &data->nodes[i]; + } + + CGLTF_PTRFIXUP(data->nodes[i].mesh, data->meshes, data->meshes_count); + CGLTF_PTRFIXUP(data->nodes[i].skin, data->skins, data->skins_count); + CGLTF_PTRFIXUP(data->nodes[i].camera, data->cameras, data->cameras_count); + CGLTF_PTRFIXUP(data->nodes[i].light, data->lights, data->lights_count); + } + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + for (cgltf_size j = 0; j < data->scenes[i].nodes_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->scenes[i].nodes[j], data->nodes, data->nodes_count); + + if (data->scenes[i].nodes[j]->parent) + { + return CGLTF_ERROR_JSON; + } + } + } + + CGLTF_PTRFIXUP(data->scene, data->scenes, data->scenes_count); + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + for (cgltf_size j = 0; j < data->animations[i].samplers_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].input, data->accessors, data->accessors_count); + CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].output, data->accessors, data->accessors_count); + } + + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->animations[i].channels[j].sampler, data->animations[i].samplers, data->animations[i].samplers_count); + CGLTF_PTRFIXUP(data->animations[i].channels[j].target_node, data->nodes, data->nodes_count); + } + } + + return 0; +} + +/* + * -- jsmn.c start -- + * Source: https://github.com/zserge/jsmn + * License: MIT + * + * Copyright (c) 2010 Serge A. Zaitsev + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Allocates a fresh unused token from the token pull. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + int start, int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, size_t num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + if (tokens == NULL) + break; + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if(token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) return JSMN_ERROR_INVAL; + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +static void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} +/* + * -- jsmn.c end -- + */ + +#endif /* #ifdef CGLTF_IMPLEMENTATION */ + +/* cgltf is distributed under MIT license: + * + * Copyright (c) 2018-2021 Johannes Kuhlmann + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */