diff --git a/CMakeLists.txt b/CMakeLists.txt index 937ad175..22f61000 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,6 +451,8 @@ else () target_compile_definitions(RaZ PUBLIC RAZ_NO_LUA) endif () +target_link_libraries(RaZ PRIVATE fastgltf simdjson) + # Compiling RaZ's sources target_sources(RaZ PRIVATE ${RAZ_FILES}) diff --git a/include/RaZ/Data/GltfFormat.hpp b/include/RaZ/Data/GltfFormat.hpp new file mode 100644 index 00000000..41cc6273 --- /dev/null +++ b/include/RaZ/Data/GltfFormat.hpp @@ -0,0 +1,25 @@ +#pragma once + +#ifndef RAZ_GLTFFORMAT_HPP +#define RAZ_GLTFFORMAT_HPP + +#include + +namespace Raz { + +class FilePath; +class Mesh; +class MeshRenderer; + +namespace GltfFormat { + +/// Loads a mesh from a glTF or GLB file. +/// \param filePath File from which to load the mesh. +/// \return Pair containing respectively the mesh's data (vertices & indices) and rendering information (materials, textures, ...). +std::pair load(const FilePath& filePath); + +} // namespace GltfFormat + +} // namespace Raz + +#endif // RAZ_GLTFFORMAT_HPP diff --git a/include/RaZ/RaZ.hpp b/include/RaZ/RaZ.hpp index 742f259b..c1f92640 100644 --- a/include/RaZ/RaZ.hpp +++ b/include/RaZ/RaZ.hpp @@ -20,6 +20,7 @@ #include "Data/BvhSystem.hpp" #include "Data/Color.hpp" #include "Data/FbxFormat.hpp" +#include "Data/GltfFormat.hpp" #include "Data/Graph.hpp" #include "Data/Image.hpp" #include "Data/ImageFormat.hpp" diff --git a/src/RaZ/Data/FbxLoad.cpp b/src/RaZ/Data/FbxLoad.cpp index cb71a097..bcf33573 100644 --- a/src/RaZ/Data/FbxLoad.cpp +++ b/src/RaZ/Data/FbxLoad.cpp @@ -239,7 +239,7 @@ std::pair load(const FilePath& filePath) { // TODO: small hack to avoid segfaulting when mesh count > material count, but clearly wrong; find another way submeshRenderer.setMaterialIndex(std::min(meshIndex, scene->GetMaterialCount() - 1)); else - Logger::error("[FBX] Materials can't be mapped to anything other than the whole submesh."); + Logger::error("[FbxLoad] Materials can't be mapped to anything other than the whole submesh."); } mesh.addSubmesh(std::move(submesh)); diff --git a/src/RaZ/Data/GltfLoad.cpp b/src/RaZ/Data/GltfLoad.cpp new file mode 100644 index 00000000..1ada0909 --- /dev/null +++ b/src/RaZ/Data/GltfLoad.cpp @@ -0,0 +1,312 @@ +#include "RaZ/Data/GltfFormat.hpp" +#include "RaZ/Data/Image.hpp" +#include "RaZ/Data/ImageFormat.hpp" +#include "RaZ/Data/Mesh.hpp" +#include "RaZ/Render/MeshRenderer.hpp" +#include "RaZ/Utils/FilePath.hpp" +#include "RaZ/Utils/FileUtils.hpp" +#include "RaZ/Utils/Logger.hpp" + +#include + +namespace Raz::GltfFormat { + +namespace { + +template +void loadVertexData(const fastgltf::Accessor& accessor, + const std::vector& buffers, + const std::vector& bufferViews, + std::vector& vertices, + void (*callback)(Vertex&, const T*)) { + assert("Error: Loading vertex data requires the accessor to reference a buffer view." && accessor.bufferViewIndex.has_value()); + + const fastgltf::BufferView& bufferView = bufferViews[*accessor.bufferViewIndex]; + const fastgltf::DataSource& bufferData = buffers[bufferView.bufferIndex].data; + + if (!std::holds_alternative(bufferData)) + throw std::runtime_error("Error: Cannot load glTF data from sources other than vectors."); + + const std::size_t dataOffset = bufferView.byteOffset + accessor.byteOffset; + const uint8_t* const vertexData = std::get(bufferData).bytes.data() + dataOffset; + const std::size_t dataStride = bufferView.byteStride.value_or(fastgltf::getElementByteSize(accessor.type, accessor.componentType)); + + for (std::size_t vertIndex = 0; vertIndex < vertices.size(); ++vertIndex) { + const auto* data = reinterpret_cast(vertexData + vertIndex * dataStride); + callback(vertices[vertIndex], data); + } +} + +void loadVertices(const fastgltf::Primitive& primitive, + const std::vector& buffers, + const std::vector& bufferViews, + const std::vector& accessors, + std::vector& vertices) { + Logger::debug("[GltfLoad] Loading vertices..."); + + const auto positionIt = primitive.findAttribute("POSITION"); + + if (positionIt == primitive.attributes.end()) + throw std::invalid_argument("Error: Required 'POSITION' attribute not found in the glTF file."); + + const fastgltf::Accessor& positionAccessor = accessors[positionIt->second]; + + if (!positionAccessor.bufferViewIndex.has_value()) + return; + + vertices.resize(positionAccessor.count); + + loadVertexData(positionAccessor, buffers, bufferViews, vertices, [] (Vertex& vert, const float* data) { + vert.position = Vec3f(data[0], data[1], data[2]); + }); + + // The tangent's input W component (data[3]) is either 1 or -1 and represents the handedness + // See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview + constexpr std::array, 3> attributes = {{ + { "TEXCOORD_0", [] (Vertex& vert, const float* data) { vert.texcoords = Vec2f(data[0], data[1]); } }, + { "NORMAL", [] (Vertex& vert, const float* data) { vert.normal = Vec3f(data[0], data[1], data[2]); } }, + { "TANGENT", [] (Vertex& vert, const float* data) { vert.tangent = Vec3f(data[0], data[1], data[2]) * data[3]; } } + }}; + + for (auto&& [attribName, callback] : attributes) { + const auto attribIter = primitive.findAttribute(attribName); + + if (attribIter == primitive.attributes.end()) + continue; + + const fastgltf::Accessor& attribAccessor = accessors[attribIter->second]; + + if (attribAccessor.bufferViewIndex.has_value()) + loadVertexData(attribAccessor, buffers, bufferViews, vertices, callback); + } + + Logger::debug("[GltfLoad] Loaded vertices"); +} + +void loadIndices(const fastgltf::Accessor& indicesAccessor, + const std::vector& buffers, + const std::vector& bufferViews, + std::vector& indices) { + Logger::debug("[GltfLoad] Loading indices..."); + + if (!indicesAccessor.bufferViewIndex.has_value()) + throw std::invalid_argument("Error: Missing glTF buffer to load indices from."); + + indices.resize(indicesAccessor.count); + + const fastgltf::BufferView& indicesView = bufferViews[*indicesAccessor.bufferViewIndex]; + const fastgltf::Buffer& indicesBuffer = buffers[indicesView.bufferIndex]; + + if (!std::holds_alternative(indicesBuffer.data)) + throw std::runtime_error("Error: Cannot load glTF data from sources other than vectors."); + + const std::size_t dataOffset = indicesView.byteOffset + indicesAccessor.byteOffset; + const uint8_t* const indicesData = std::get(indicesBuffer.data).bytes.data() + dataOffset; + const std::size_t dataSize = fastgltf::getElementByteSize(indicesAccessor.type, indicesAccessor.componentType); + const std::size_t dataStride = indicesView.byteStride.value_or(dataSize); + + for (std::size_t i = 0; i < indices.size(); ++i) { + // The indices must be of an unsigned integer type, but its size is unspecified + // See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_mesh_primitive_indices + switch (dataSize) { + case 1: + indices[i] = *reinterpret_cast(indicesData + i * dataStride); + break; + + case 2: + indices[i] = *reinterpret_cast(indicesData + i * dataStride); + break; + + case 4: + indices[i] = *reinterpret_cast(indicesData + i * dataStride); + break; + + default: + throw std::invalid_argument("Error: Unexpected indices data size (" + std::to_string(dataSize) + ")."); + } + } + + Logger::debug("[GltfLoad] Loaded indices"); +} + +std::pair loadMeshes(const std::vector& meshes, + const std::vector& buffers, + const std::vector& bufferViews, + const std::vector& accessors) { + Logger::debug("[GltfLoad] Loading " + std::to_string(meshes.size()) + " meshes..."); + + Mesh loadedMesh; + MeshRenderer loadedMeshRenderer; + + for (const fastgltf::Mesh& mesh : meshes) { + for (const fastgltf::Primitive& primitive : mesh.primitives) { + if (!primitive.indicesAccessor.has_value()) + throw std::invalid_argument("Error: The glTF file requires having indexed geometry."); + + Submesh& submesh = loadedMesh.addSubmesh(); + SubmeshRenderer& submeshRenderer = loadedMeshRenderer.addSubmeshRenderer(); + + loadVertices(primitive, buffers, bufferViews, accessors, submesh.getVertices()); + loadIndices(accessors[*primitive.indicesAccessor], buffers, bufferViews, submesh.getTriangleIndices()); + + submeshRenderer.load(submesh, (primitive.type == fastgltf::PrimitiveType::Triangles ? RenderMode::TRIANGLE : RenderMode::POINT)); + submeshRenderer.setMaterialIndex(primitive.materialIndex.value_or(0)); + } + } + + Logger::debug("[GltfLoad] Loaded meshes"); + + return { std::move(loadedMesh), std::move(loadedMeshRenderer) }; +} + +std::vector loadImages(const std::vector& images, const FilePath& rootFilePath) { + Logger::debug("[GltfLoad] Loading " + std::to_string(images.size()) + " images..."); + + std::vector loadedImages; + loadedImages.reserve(images.size()); + + for (const fastgltf::Image& img : images) { + if (!std::holds_alternative(img.data)) { + Logger::error("[GltfLoad] Images can only be loaded from a file path for now."); + continue; + } + + const auto& imgPath = std::get(img.data).uri.path(); + loadedImages.emplace_back(ImageFormat::load(rootFilePath + imgPath)); + } + + Logger::debug("[GltfLoad] Loaded images"); + + return loadedImages; +} + +Image extractAmbientOcclusionImage(const Image& occlusionImg) { + Image ambientImg(occlusionImg.getWidth(), occlusionImg.getHeight(), ImageColorspace::GRAY, occlusionImg.getDataType()); + + for (std::size_t i = 0; i < occlusionImg.getWidth() * occlusionImg.getHeight(); ++i) { + const std::size_t finalIndex = i * occlusionImg.getChannelCount(); + + // The occlusion is located in the red (1st) channel + // See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_material_occlusiontexture + if (occlusionImg.getDataType() == ImageDataType::BYTE) + static_cast(ambientImg.getDataPtr())[i] = static_cast(occlusionImg.getDataPtr())[finalIndex]; + else + static_cast(ambientImg.getDataPtr())[i] = static_cast(occlusionImg.getDataPtr())[finalIndex]; + } + + return ambientImg; +} + +std::pair extractMetalnessRoughnessImages(const Image& metalRoughImg) { + Image metalnessImg(metalRoughImg.getWidth(), metalRoughImg.getHeight(), ImageColorspace::GRAY, metalRoughImg.getDataType()); + Image roughnessImg(metalRoughImg.getWidth(), metalRoughImg.getHeight(), ImageColorspace::GRAY, metalRoughImg.getDataType()); + + for (std::size_t i = 0; i < metalRoughImg.getWidth() * metalRoughImg.getHeight(); ++i) { + const std::size_t finalIndex = i * metalRoughImg.getChannelCount(); + + // The metalness & roughness are located respectively in the blue (3rd) & green (2nd) channels + // See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_material_pbrmetallicroughness_metallicroughnesstexture + if (metalRoughImg.getDataType() == ImageDataType::BYTE) { + static_cast(metalnessImg.getDataPtr())[i] = static_cast(metalRoughImg.getDataPtr())[finalIndex + 2]; + static_cast(roughnessImg.getDataPtr())[i] = static_cast(metalRoughImg.getDataPtr())[finalIndex + 1]; + } else { + static_cast(metalnessImg.getDataPtr())[i] = static_cast(metalRoughImg.getDataPtr())[finalIndex + 2]; + static_cast(roughnessImg.getDataPtr())[i] = static_cast(metalRoughImg.getDataPtr())[finalIndex + 1]; + } + } + + return { std::move(metalnessImg), std::move(roughnessImg) }; +} + +void loadMaterials(const std::vector& materials, const std::vector& images, MeshRenderer& meshRenderer) { + Logger::debug("[GltfLoad] Loading " + std::to_string(materials.size()) + " materials..."); + + meshRenderer.getMaterials().clear(); + + for (const fastgltf::Material& mat : materials) { + Material& loadedMat = meshRenderer.addMaterial(); + RenderShaderProgram& matProgram = loadedMat.getProgram(); + + matProgram.setAttribute(Vec3f(mat.pbrData.baseColorFactor[0], + mat.pbrData.baseColorFactor[1], + mat.pbrData.baseColorFactor[2]), MaterialAttribute::BaseColor); + matProgram.setAttribute(Vec3f(mat.emissiveFactor[0], + mat.emissiveFactor[1], + mat.emissiveFactor[2]) * mat.emissiveStrength.value_or(1.f), MaterialAttribute::Emissive); + matProgram.setAttribute(mat.pbrData.metallicFactor, MaterialAttribute::Metallic); + matProgram.setAttribute(mat.pbrData.roughnessFactor, MaterialAttribute::Roughness); + + if (mat.pbrData.baseColorTexture) + matProgram.setTexture(Texture2D::create(images[mat.pbrData.baseColorTexture->textureIndex]), MaterialTexture::BaseColor); + + if (mat.emissiveTexture) + matProgram.setTexture(Texture2D::create(images[mat.emissiveTexture->textureIndex]), MaterialTexture::Emissive); + + if (mat.occlusionTexture) { // Ambient occlusion + const Image ambientOcclusionImg = extractAmbientOcclusionImage(images[mat.occlusionTexture->textureIndex]); + matProgram.setTexture(Texture2D::create(ambientOcclusionImg), MaterialTexture::Ambient); + } + + if (mat.normalTexture) + matProgram.setTexture(Texture2D::create(images[mat.normalTexture->textureIndex]), MaterialTexture::Normal); + + if (mat.pbrData.metallicRoughnessTexture) { + const auto [metalnessImg, roughnessImg] = extractMetalnessRoughnessImages(images[mat.pbrData.metallicRoughnessTexture->textureIndex]); + matProgram.setTexture(Texture2D::create(metalnessImg), MaterialTexture::Metallic); + matProgram.setTexture(Texture2D::create(roughnessImg), MaterialTexture::Roughness); + } + + loadedMat.loadType(MaterialType::COOK_TORRANCE); + } + + Logger::debug("[GltfLoad] Loaded materials"); +} + +} // namespace + +std::pair load(const FilePath& filePath) { + Logger::debug("[GltfLoad] Loading glTF file ('" + filePath + "')..."); + + if (!FileUtils::isReadable(filePath)) + throw std::invalid_argument("Error: The glTF file '" + filePath + "' either does not exist or cannot be opened."); + + fastgltf::GltfDataBuffer data; + + if (!data.loadFromFile(filePath.getPath())) + throw std::invalid_argument("Error: Could not load the glTF file."); + + const FilePath parentPath = filePath.recoverPathToFile(); + fastgltf::Expected asset(fastgltf::Error::None); + + fastgltf::Parser parser; + + switch (fastgltf::determineGltfFileType(&data)) { + case fastgltf::GltfType::glTF: + asset = parser.loadGLTF(&data, parentPath.getPath(), fastgltf::Options::LoadExternalBuffers); + break; + + case fastgltf::GltfType::GLB: + asset = parser.loadBinaryGLTF(&data, parentPath.getPath(), fastgltf::Options::LoadGLBBuffers); + break; + + default: + throw std::invalid_argument("Error: Failed to determine glTF container."); + } + + if (asset.error() != fastgltf::Error::None) + throw std::invalid_argument("Error: Failed to load glTF: " + fastgltf::getErrorMessage(asset.error())); + + auto [mesh, meshRenderer] = loadMeshes(asset->meshes, asset->buffers, asset->bufferViews, asset->accessors); + + const std::vector images = loadImages(asset->images, parentPath); + loadMaterials(asset->materials, images, meshRenderer); + + Logger::debug("[GltfLoad] Loaded glTF file (" + std::to_string(mesh.getSubmeshes().size()) + " submesh(es), " + + std::to_string(mesh.recoverVertexCount()) + " vertices, " + + std::to_string(mesh.recoverTriangleCount()) + " triangles, " + + std::to_string(meshRenderer.getMaterials().size()) + " material(s))"); + + return { std::move(mesh), std::move(meshRenderer) }; +} + +} // namespace Raz::GltfFormat diff --git a/src/RaZ/Data/MeshFormat.cpp b/src/RaZ/Data/MeshFormat.cpp index a925db23..01dc2d86 100644 --- a/src/RaZ/Data/MeshFormat.cpp +++ b/src/RaZ/Data/MeshFormat.cpp @@ -1,4 +1,5 @@ #include "RaZ/Data/FbxFormat.hpp" +#include "RaZ/Data/GltfFormat.hpp" #include "RaZ/Data/Mesh.hpp" #include "RaZ/Data/MeshFormat.hpp" #include "RaZ/Data/ObjFormat.hpp" @@ -12,7 +13,9 @@ namespace Raz::MeshFormat { std::pair load(const FilePath& filePath) { const std::string fileExt = StrUtils::toLowercaseCopy(filePath.recoverExtension().toUtf8()); - if (fileExt == "obj") { + if (fileExt == "gltf" || fileExt == "glb") { + return GltfFormat::load(filePath); + } else if (fileExt == "obj") { return ObjFormat::load(filePath); } else if (fileExt == "off") { Mesh mesh = OffFormat::load(filePath); diff --git a/src/RaZ/Data/ObjLoad.cpp b/src/RaZ/Data/ObjLoad.cpp index 545f21ca..9eb225cf 100644 --- a/src/RaZ/Data/ObjLoad.cpp +++ b/src/RaZ/Data/ObjLoad.cpp @@ -380,7 +380,8 @@ std::pair load(const FilePath& filePath) { Logger::debug("[ObjLoad] Loaded OBJ file (" + std::to_string(mesh.getSubmeshes().size()) + " submesh(es), " + std::to_string(mesh.recoverVertexCount()) + " vertices, " - + std::to_string(mesh.recoverTriangleCount()) + " triangles)"); + + std::to_string(mesh.recoverTriangleCount()) + " triangles, " + + std::to_string(meshRenderer.getMaterials().size()) + " material(s))"); return { std::move(mesh), std::move(meshRenderer) }; } diff --git a/src/RaZ/Script/LuaFileFormat.cpp b/src/RaZ/Script/LuaFileFormat.cpp index 1c26f8ad..71c28cbe 100644 --- a/src/RaZ/Script/LuaFileFormat.cpp +++ b/src/RaZ/Script/LuaFileFormat.cpp @@ -4,6 +4,7 @@ #if defined(RAZ_USE_FBX) #include "RaZ/Data/FbxFormat.hpp" #endif +#include "RaZ/Data/GltfFormat.hpp" #include "RaZ/Data/Image.hpp" #include "RaZ/Data/ImageFormat.hpp" #include "RaZ/Data/Mesh.hpp" @@ -42,6 +43,11 @@ void LuaWrapper::registerFileFormatTypes() { } #endif + { + sol::table gltfFormat = state["GltfFormat"].get_or_create(); + gltfFormat["load"] = &GltfFormat::load; + } + { sol::table imageFormat = state["ImageFormat"].get_or_create(); imageFormat["load"] = sol::overload([] (const FilePath& p) { return ImageFormat::load(p); }, diff --git "a/tests/assets/meshes/\303\237\303\270\323\276.glb" "b/tests/assets/meshes/\303\237\303\270\323\276.glb" new file mode 100644 index 00000000..95ec886b Binary files /dev/null and "b/tests/assets/meshes/\303\237\303\270\323\276.glb" differ diff --git "a/tests/assets/meshes/\303\247\303\273b\303\250.bin" "b/tests/assets/meshes/\303\247\303\273b\303\250.bin" new file mode 100644 index 00000000..7dae4b0b Binary files /dev/null and "b/tests/assets/meshes/\303\247\303\273b\303\250.bin" differ diff --git "a/tests/assets/meshes/\303\247\303\273b\303\250.gltf" "b/tests/assets/meshes/\303\247\303\273b\303\250.gltf" new file mode 100644 index 00000000..bd1f5815 --- /dev/null +++ "b/tests/assets/meshes/\303\247\303\273b\303\250.gltf" @@ -0,0 +1,271 @@ +{ + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5123, + "count": 36, + "max": [ + 35 + ], + "min": [ + 0 + ], + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 36, + "max": [ + 1.0, + 1.0, + 1.000001 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 36, + "max": [ + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 0, + "componentType": 5126, + "count": 36, + "max": [ + 1.0, + -0.0, + -0.0, + 1.0 + ], + "min": [ + 0.0, + -0.0, + -1.0, + -1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 4, + "byteOffset": 0, + "componentType": 5126, + "count": 36, + "max": [ + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0 + ], + "type": "VEC2" + } + ], + "asset": { + "generator": "VKTS glTF 2.0 exporter", + "version": "2.0" + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 72, + "byteOffset": 0, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 432, + "byteOffset": 72, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 432, + "byteOffset": 504, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 576, + "byteOffset": 936, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 288, + "byteOffset": 1512, + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 1800, + "uri": "çûbè.bin" + } + ], + "images": [ + { + "uri": "../textures/ŔĜBŖĀ.png" + }, + { + "uri": "../textures/ŔŖȒȐ.png" + }, + { + "uri": "../textures/ĜƓGǦ.png" + } + ], + "materials": [ + { + "name": "Cube1", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.99, + 0.99, + 0.99, + 0.99 + ], + "baseColorTexture": { + "index": 0 + }, + "metallicFactor": 0.5, + "roughnessFactor": 0.25, + "metallicRoughnessTexture": { + "index": 0 + } + }, + "normalTexture": { + "index": 2 + }, + "occlusionTexture": { + "index": 0 + }, + "emissiveFactor": [ + 0.75, + 0.75, + 0.75 + ], + "emissiveTexture": { + "index": 1 + } + }, + { + "name": "Cube2", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.99, + 0.99, + 0.99, + 0.99 + ], + "baseColorTexture": { + "index": 0 + }, + "metallicFactor": 0.5, + "roughnessFactor": 0.25, + "metallicRoughnessTexture": { + "index": 0 + } + }, + "normalTexture": { + "index": 2 + }, + "occlusionTexture": { + "index": 0 + }, + "emissiveFactor": [ + 0.75, + 0.75, + 0.75 + ], + "emissiveTexture": { + "index": 1 + } + } + ], + "meshes": [ + { + "name": "Cube1", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 1, + "TANGENT": 3, + "TEXCOORD_0": 4 + }, + "indices": 0, + "material": 0, + "mode": 4 + } + ] + }, + { + "name": "Cube2", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 1, + "TANGENT": 3, + "TEXCOORD_0": 4 + }, + "indices": 0, + "material": 1, + "mode": 0 + } + ] + } + ], + "nodes": [ + { + "mesh": 0, + "name": "Cube1" + }, + { + "mesh": 1, + "name": "Cube2" + } + ], + "samplers": [ + {} + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 1 + } + ] +} diff --git a/tests/src/RaZ/Data/GltfFormat.cpp b/tests/src/RaZ/Data/GltfFormat.cpp new file mode 100644 index 00000000..ee72e12b --- /dev/null +++ b/tests/src/RaZ/Data/GltfFormat.cpp @@ -0,0 +1,238 @@ +#include "Catch.hpp" + +#include "RaZ/Data/Color.hpp" +#include "RaZ/Data/GltfFormat.hpp" +#include "RaZ/Data/Image.hpp" +#include "RaZ/Data/Mesh.hpp" +#include "RaZ/Render/MeshRenderer.hpp" + +TEST_CASE("GltfFormat load glTF") { + const auto [mesh, meshRenderer] = Raz::GltfFormat::load(RAZ_TESTS_ROOT "assets/meshes/çûbè.gltf"); + + REQUIRE(mesh.getSubmeshes().size() == 2); + CHECK(mesh.getSubmeshes()[0].getVertexCount() == 36); + CHECK(mesh.getSubmeshes()[1].getVertexCount() == 36); + CHECK(mesh.recoverVertexCount() == 72); + CHECK(mesh.getSubmeshes()[0].getTriangleIndexCount() == 36); + CHECK(mesh.getSubmeshes()[1].getTriangleIndexCount() == 36); + CHECK(mesh.recoverTriangleCount() == 24); // 72 / 3 + + CHECK(mesh.getSubmeshes()[0].getVertices()[0] == Raz::Vertex{ Raz::Vec3f(1.f, -1.f, 1.f), Raz::Vec2f(0.f, 0.f), -Raz::Axis::Y, -Raz::Axis::X }); + CHECK(mesh.getSubmeshes()[0].getVertices()[1] == Raz::Vertex{ Raz::Vec3f(-1.f, -1.f, -1.f), Raz::Vec2f(-1.f, 1.f), -Raz::Axis::Y, -Raz::Axis::X }); + CHECK(mesh.getSubmeshes()[0].getVertices()[17] == Raz::Vertex{ Raz::Vec3f(1.f, 1.f, -0.9999989f), Raz::Vec2f(0.f, 1.f), -Raz::Axis::Z, -Raz::Axis::X }); + CHECK(mesh.getSubmeshes()[0].getVertices()[35] == Raz::Vertex{ Raz::Vec3f(-1.f, 1.f, -1.f), Raz::Vec2f(-1.f, 1.f), -Raz::Axis::Z, -Raz::Axis::X }); + + REQUIRE(meshRenderer.getSubmeshRenderers().size() == 2); + CHECK(meshRenderer.getSubmeshRenderers()[0].getRenderMode() == Raz::RenderMode::TRIANGLE); + CHECK(meshRenderer.getSubmeshRenderers()[0].getMaterialIndex() == 0); + CHECK(meshRenderer.getSubmeshRenderers()[1].getRenderMode() == Raz::RenderMode::POINT); + CHECK(meshRenderer.getSubmeshRenderers()[1].getMaterialIndex() == 1); + + REQUIRE(meshRenderer.getMaterials().size() == 2); + + const Raz::RenderShaderProgram& matProgram = meshRenderer.getMaterials()[0].getProgram(); + + CHECK(matProgram.getAttribute(Raz::MaterialAttribute::BaseColor) == Raz::Vec3f(0.99f)); + CHECK(matProgram.getAttribute(Raz::MaterialAttribute::Emissive) == Raz::Vec3f(0.75f)); + CHECK(matProgram.getAttribute(Raz::MaterialAttribute::Metallic) == 0.5f); + CHECK(matProgram.getAttribute(Raz::MaterialAttribute::Roughness) == 0.25f); + + { + const auto& baseColorMap = static_cast(matProgram.getTexture(Raz::MaterialTexture::BaseColor)); + + REQUIRE(baseColorMap.getWidth() == 2); + REQUIRE(baseColorMap.getHeight() == 2); + REQUIRE(baseColorMap.getColorspace() == Raz::TextureColorspace::RGBA); + REQUIRE(baseColorMap.getDataType() == Raz::TextureDataType::BYTE); + +#if !defined(USE_OPENGL_ES) + const Raz::Image baseColorImg = baseColorMap.recoverImage(); + REQUIRE_FALSE(baseColorImg.isEmpty()); + + // --------- + // | R | G | + // |-------| + // | B | R | + // --------- + + CHECK(baseColorImg.recoverPixel(0, 0) == Raz::Vec4b(Raz::Vec3b(Raz::ColorPreset::Red), 127)); + CHECK(baseColorImg.recoverPixel(1, 0) == Raz::Vec4b(Raz::Vec3b(Raz::ColorPreset::Green), 127)); + CHECK(baseColorImg.recoverPixel(0, 1) == Raz::Vec4b(Raz::Vec3b(Raz::ColorPreset::Blue), 127)); + CHECK(baseColorImg.recoverPixel(1, 1) == Raz::Vec4b(Raz::Vec3b(Raz::ColorPreset::Red), 127)); +#endif + } + + { + const auto& emissiveMap = static_cast(matProgram.getTexture(Raz::MaterialTexture::Emissive)); + + REQUIRE(emissiveMap.getWidth() == 2); + REQUIRE(emissiveMap.getHeight() == 2); + REQUIRE(emissiveMap.getColorspace() == Raz::TextureColorspace::RGB); + REQUIRE(emissiveMap.getDataType() == Raz::TextureDataType::BYTE); + +#if !defined(USE_OPENGL_ES) + const Raz::Image emissiveImg = emissiveMap.recoverImage(); + REQUIRE_FALSE(emissiveImg.isEmpty()); + + // --------- + // | R | R | + // |-------| + // | R | R | + // --------- + + CHECK(emissiveImg.recoverPixel(0, 0) == Raz::Vec3b(Raz::ColorPreset::Red)); + CHECK(emissiveImg.recoverPixel(1, 0) == Raz::Vec3b(Raz::ColorPreset::Red)); + CHECK(emissiveImg.recoverPixel(0, 1) == Raz::Vec3b(Raz::ColorPreset::Red)); + CHECK(emissiveImg.recoverPixel(1, 1) == Raz::Vec3b(Raz::ColorPreset::Red)); +#endif + } + + { + const auto& normalMap = static_cast(matProgram.getTexture(Raz::MaterialTexture::Normal)); + + REQUIRE(normalMap.getWidth() == 2); + REQUIRE(normalMap.getHeight() == 2); + REQUIRE(normalMap.getColorspace() == Raz::TextureColorspace::RGB); + REQUIRE(normalMap.getDataType() == Raz::TextureDataType::BYTE); + +#if !defined(USE_OPENGL_ES) + const Raz::Image normalImg = normalMap.recoverImage(); + REQUIRE_FALSE(normalImg.isEmpty()); + + // --------- + // | G | G | + // |-------| + // | G | G | + // --------- + + CHECK(normalImg.recoverPixel(0, 0) == Raz::Vec3b(Raz::ColorPreset::Green)); + CHECK(normalImg.recoverPixel(1, 0) == Raz::Vec3b(Raz::ColorPreset::Green)); + CHECK(normalImg.recoverPixel(0, 1) == Raz::Vec3b(Raz::ColorPreset::Green)); + CHECK(normalImg.recoverPixel(1, 1) == Raz::Vec3b(Raz::ColorPreset::Green)); +#endif + } + + { + const auto& metallicMap = static_cast(matProgram.getTexture(Raz::MaterialTexture::Metallic)); + + REQUIRE(metallicMap.getWidth() == 2); + REQUIRE(metallicMap.getHeight() == 2); + REQUIRE(metallicMap.getColorspace() == Raz::TextureColorspace::GRAY); + REQUIRE(metallicMap.getDataType() == Raz::TextureDataType::BYTE); + +#if !defined(USE_OPENGL_ES) + const Raz::Image metallicImg = metallicMap.recoverImage(); + REQUIRE_FALSE(metallicImg.isEmpty()); + + // The metalness is taken from the image's blue (3rd) channel. The input image being: + // --------- + // | R | G | + // |-------| + // | B | R | + // --------- + // The loaded texture should then be: + // --------- + // | 0 | 0 | + // |-------| + // | 1 | 0 | + // --------- + + CHECK(metallicImg.recoverPixel(0, 0) == 0); + CHECK(metallicImg.recoverPixel(1, 0) == 0); + CHECK(metallicImg.recoverPixel(0, 1) == 255); + CHECK(metallicImg.recoverPixel(1, 1) == 0); +#endif + } + + { + const auto& roughnessMap = static_cast(matProgram.getTexture(Raz::MaterialTexture::Roughness)); + + REQUIRE(roughnessMap.getWidth() == 2); + REQUIRE(roughnessMap.getHeight() == 2); + REQUIRE(roughnessMap.getColorspace() == Raz::TextureColorspace::GRAY); + REQUIRE(roughnessMap.getDataType() == Raz::TextureDataType::BYTE); + +#if !defined(USE_OPENGL_ES) + const Raz::Image roughnessImg = roughnessMap.recoverImage(); + REQUIRE_FALSE(roughnessImg.isEmpty()); + + // The roughness is taken from the image's green (2nd) channel. The input image being: + // --------- + // | R | G | + // |-------| + // | B | R | + // --------- + // The loaded texture should then be: + // --------- + // | 0 | 1 | + // |-------| + // | 0 | 0 | + // --------- + + CHECK(roughnessImg.recoverPixel(0, 0) == 0); + CHECK(roughnessImg.recoverPixel(1, 0) == 255); + CHECK(roughnessImg.recoverPixel(0, 1) == 0); + CHECK(roughnessImg.recoverPixel(1, 1) == 0); +#endif + } + + { + const auto& ambientOcclusionMap = static_cast(matProgram.getTexture(Raz::MaterialTexture::Ambient)); + + REQUIRE(ambientOcclusionMap.getWidth() == 2); + REQUIRE(ambientOcclusionMap.getHeight() == 2); + REQUIRE(ambientOcclusionMap.getColorspace() == Raz::TextureColorspace::GRAY); + REQUIRE(ambientOcclusionMap.getDataType() == Raz::TextureDataType::BYTE); + +#if !defined(USE_OPENGL_ES) + const Raz::Image ambientOcclusionImg = ambientOcclusionMap.recoverImage(); + REQUIRE_FALSE(ambientOcclusionImg.isEmpty()); + + // The ambient occlusion is taken from the image's red (1st) channel. The input image being: + // --------- + // | R | G | + // |-------| + // | B | R | + // --------- + // The loaded texture should then be: + // --------- + // | 1 | 0 | + // |-------| + // | 0 | 1 | + // --------- + + CHECK(ambientOcclusionImg.recoverPixel(0, 0) == 255); + CHECK(ambientOcclusionImg.recoverPixel(1, 0) == 0); + CHECK(ambientOcclusionImg.recoverPixel(0, 1) == 0); + CHECK(ambientOcclusionImg.recoverPixel(1, 1) == 255); +#endif + } +} + +TEST_CASE("GltfFormat load GLB") { + const auto [mesh, meshRenderer] = Raz::GltfFormat::load(RAZ_TESTS_ROOT "assets/meshes/ßøӾ.glb"); + + REQUIRE(mesh.getSubmeshes().size() == 1); + CHECK(mesh.getSubmeshes()[0].getVertexCount() == 24); + CHECK(mesh.recoverVertexCount() == 24); + CHECK(mesh.getSubmeshes()[0].getTriangleIndexCount() == 36); + CHECK(mesh.recoverTriangleCount() == 12); // 36 / 3 + + CHECK(mesh.getSubmeshes()[0].getVertices()[0] == Raz::Vertex{ Raz::Vec3f(-0.5f, -0.5f, 0.5f), Raz::Vec2f(0.f, 0.f), Raz::Axis::Z, Raz::Vec3f(0.f) }); + CHECK(mesh.getSubmeshes()[0].getVertices()[1] == Raz::Vertex{ Raz::Vec3f(0.5f, -0.5f, 0.5f), Raz::Vec2f(0.f, 0.f), Raz::Axis::Z, Raz::Vec3f(0.f) }); + CHECK(mesh.getSubmeshes()[0].getVertices()[12] == Raz::Vertex{ Raz::Vec3f(-0.5f, 0.5f, 0.5f), Raz::Vec2f(0.f, 0.f), Raz::Axis::Y, Raz::Vec3f(0.f) }); + CHECK(mesh.getSubmeshes()[0].getVertices()[23] == Raz::Vertex{ Raz::Vec3f(0.5f, 0.5f, -0.5f), Raz::Vec2f(0.f, 0.f), -Raz::Axis::Z, Raz::Vec3f(0.f) }); + + REQUIRE(meshRenderer.getSubmeshRenderers().size() == 1); + CHECK(meshRenderer.getSubmeshRenderers()[0].getRenderMode() == Raz::RenderMode::TRIANGLE); + CHECK(meshRenderer.getSubmeshRenderers()[0].getMaterialIndex() == 0); + + REQUIRE(meshRenderer.getMaterials().size() == 1); + + const Raz::RenderShaderProgram& matProgram = meshRenderer.getMaterials()[0].getProgram(); + + CHECK(matProgram.getAttribute(Raz::MaterialAttribute::BaseColor) == Raz::Vec3f(0.8f, 0.f, 0.f)); + CHECK(matProgram.getAttribute(Raz::MaterialAttribute::Emissive) == Raz::Vec3f(0.f)); + CHECK(matProgram.getAttribute(Raz::MaterialAttribute::Metallic) == 0.f); + CHECK(matProgram.getAttribute(Raz::MaterialAttribute::Roughness) == 1.f); +} diff --git a/tests/src/RaZ/Data/ObjFormat.cpp b/tests/src/RaZ/Data/ObjFormat.cpp index b1c7fb55..e7096ce5 100644 --- a/tests/src/RaZ/Data/ObjFormat.cpp +++ b/tests/src/RaZ/Data/ObjFormat.cpp @@ -147,7 +147,7 @@ TEST_CASE("ObjFormat load Blinn-Phong") { const Raz::Image diffuseImg = diffuseMap.recoverImage(); REQUIRE_FALSE(diffuseImg.isEmpty()); - // RGBR image with alpha, flipped vertically: verifying that values are BRRG with 50% opacity + // RGBR image with alpha, flipped vertically: checking that values are BRRG with 50% opacity // --------- // | R | G | @@ -396,7 +396,7 @@ TEST_CASE("ObjFormat load Cook-Torrance") { const Raz::Image albedoImg = albedoMap.recoverImage(); REQUIRE_FALSE(albedoImg.isEmpty()); - // RGBR image with alpha, flipped vertically: verifying that values are BRRG with 50% opacity + // RGBR image with alpha, flipped vertically: checking that values are BRRG with 50% opacity // --------- // | R | G | diff --git a/tests/src/RaZ/Script/LuaData.cpp b/tests/src/RaZ/Script/LuaData.cpp index 549778a8..ac53b0a1 100644 --- a/tests/src/RaZ/Script/LuaData.cpp +++ b/tests/src/RaZ/Script/LuaData.cpp @@ -130,6 +130,9 @@ TEST_CASE("LuaData Mesh") { meshData, _ = ObjFormat.load(FilePath.new("téstÊxpørt.obj")) assert(meshData:recoverVertexCount() == 24) ObjFormat.save(FilePath.new("téstÊxpørt.obj"), meshData) + + meshData, _ = GltfFormat.load(FilePath.new(RAZ_TESTS_ROOT .. "assets/meshes/çûbè.gltf")) + assert(meshData:recoverVertexCount() == 72) )")); #if defined(RAZ_USE_FBX)