From c928cc8456cd42701429099b0b47711ece3e4d24 Mon Sep 17 00:00:00 2001 From: Josiah VanderZee Date: Thu, 18 Apr 2024 12:00:03 -0500 Subject: [PATCH] Put files in the right places --- {src => irr/src}/CGLTFMeshFileLoader.cpp | 1220 ++++++++--------- {src => irr/src}/CGLTFMeshFileLoader.h | 292 ++-- {src => irr/src}/tests/CMakeLists.txt | 0 .../src}/tests/assets/blender_cube.gltf | 0 .../assets/blender_cube_matrix_transform.gltf | 0 .../tests/assets/blender_cube_scaled.gltf | 0 {src => irr/src}/tests/assets/empty.gltf | 0 .../src}/tests/assets/json_missing_brace.gltf | 0 .../src}/tests/assets/minimal_triangle.gltf | 0 {src => irr/src}/tests/assets/snow_man.gltf | 0 .../assets/triangle_with_vertex_stride.gltf | 0 .../assets/triangle_without_indices.gltf | 106 +- .../src}/tests/testCGLTFMeshFileLoader.cpp | 668 ++++----- 13 files changed, 1143 insertions(+), 1143 deletions(-) rename {src => irr/src}/CGLTFMeshFileLoader.cpp (96%) rename {src => irr/src}/CGLTFMeshFileLoader.h (96%) rename {src => irr/src}/tests/CMakeLists.txt (100%) rename {src => irr/src}/tests/assets/blender_cube.gltf (100%) rename {src => irr/src}/tests/assets/blender_cube_matrix_transform.gltf (100%) rename {src => irr/src}/tests/assets/blender_cube_scaled.gltf (100%) rename {src => irr/src}/tests/assets/empty.gltf (100%) rename {src => irr/src}/tests/assets/json_missing_brace.gltf (100%) rename {src => irr/src}/tests/assets/minimal_triangle.gltf (100%) rename {src => irr/src}/tests/assets/snow_man.gltf (100%) rename {src => irr/src}/tests/assets/triangle_with_vertex_stride.gltf (100%) rename {src => irr/src}/tests/assets/triangle_without_indices.gltf (93%) rename {src => irr/src}/tests/testCGLTFMeshFileLoader.cpp (97%) diff --git a/src/CGLTFMeshFileLoader.cpp b/irr/src/CGLTFMeshFileLoader.cpp similarity index 96% rename from src/CGLTFMeshFileLoader.cpp rename to irr/src/CGLTFMeshFileLoader.cpp index 81e117756..8747cb8bb 100644 --- a/src/CGLTFMeshFileLoader.cpp +++ b/irr/src/CGLTFMeshFileLoader.cpp @@ -1,610 +1,610 @@ -#include "CGLTFMeshFileLoader.h" - -#include "coreutil.h" -#include "CSkinnedMesh.h" -#include "ISkinnedMesh.h" -#include "irrTypes.h" -#include "IReadFile.h" -#include "matrix4.h" -#include "path.h" -#include "S3DVertex.h" -#include "quaternion.h" -#include "vector3d.h" - -#include "tiniergltf.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* Notes on the coordinate system. - * - * glTF uses a right-handed coordinate system where +Z is the - * front-facing axis, and Irrlicht uses a left-handed coordinate - * system where -Z is the front-facing axis. - * We convert between them by reflecting the mesh across the X axis. - * Doing this correctly requires negating the Z coordinate on - * vertex positions and normals, and reversing the winding order - * of the vertex indices. - */ - -namespace irr { - -namespace scene { - -CGLTFMeshFileLoader::BufferOffset::BufferOffset( - const std::vector& buf, - const std::size_t offset) - : m_buf(buf) - , m_offset(offset) -{ -} - -CGLTFMeshFileLoader::BufferOffset::BufferOffset( - const CGLTFMeshFileLoader::BufferOffset& other, - const std::size_t fromOffset) - : m_buf(other.m_buf) - , m_offset(other.m_offset + fromOffset) -{ -} - -/** - * Get a raw unsigned char (ubyte) from a buffer offset. -*/ -unsigned char CGLTFMeshFileLoader::BufferOffset::at( - const std::size_t fromOffset) const -{ - return m_buf.at(m_offset + fromOffset); -} - -CGLTFMeshFileLoader::CGLTFMeshFileLoader() noexcept -{ -} - -/** - * The most basic portion of the code base. This tells irllicht if this file has a .gltf extension. -*/ -bool CGLTFMeshFileLoader::isALoadableFileExtension( - const io::path& filename) const -{ - return core::hasFileExtension(filename, "gltf"); -} - -/** - * Entry point into loading a GLTF model. -*/ -IAnimatedMesh* CGLTFMeshFileLoader::createMesh(io::IReadFile* file) -{ - if (file->getSize() <= 0) { - return nullptr; - } - std::optional model = tryParseGLTF(file); - if (!model.has_value()) { - return nullptr; - } - - if (!(model->buffers.has_value() - && model->bufferViews.has_value() - && model->accessors.has_value() - && model->meshes.has_value() - && model->nodes.has_value())) { - return nullptr; - } - - ISkinnedMesh *mesh = new CSkinnedMesh(); - MeshExtractor parser(std::move(model.value()), mesh); - try { - parser.loadNodes(); - } catch (std::runtime_error &e) { - mesh->drop(); - return nullptr; - } - return mesh; -} - -static void transformVertices(std::vector &vertices, const core::matrix4 &transform) -{ - for (auto &vertex : vertices) { - // Apply scaling, rotation and rotation (in that order) to the position. - transform.transformVect(vertex.Pos); - // For the normal, we do not want to apply the translation. - // TODO note that this also applies scaling; the Irrlicht method is misnamed. - transform.rotateVect(vertex.Normal); - // Renormalize (length might have been affected by scaling). - vertex.Normal.normalize(); - } -} - -static void checkIndices(const std::vector &indices, const std::size_t nVerts) -{ - for (u16 index : indices) { - if (index >= nVerts) - throw std::runtime_error("index out of bounds"); - } -} - -static std::vector generateIndices(const std::size_t nVerts) -{ - std::vector indices(nVerts); - for (std::size_t i = 0; i < nVerts; i += 3) { - // Reverse winding order per triangle - indices[i] = i + 2; - indices[i + 1] = i + 1; - indices[i + 2] = i; - } - return indices; -} - -/** - * Load up the rawest form of the model. The vertex positions and indices. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes - * If material is undefined, then a default material MUST be used. -*/ -void CGLTFMeshFileLoader::MeshExtractor::loadMesh( - const std::size_t meshIdx, - ISkinnedMesh::SJoint *parent) const -{ - for (std::size_t j = 0; j < getPrimitiveCount(meshIdx); ++j) { - auto vertices = getVertices(meshIdx, j); - if (!vertices.has_value()) - continue; // "When positions are not specified, client implementations SHOULD skip primitive’s rendering" - - // Excludes the max value for consistency. - if (vertices->size() >= std::numeric_limits::max()) - throw std::runtime_error("too many vertices"); - - // Apply the global transform along the parent chain. - transformVertices(*vertices, parent->GlobalMatrix); - - auto maybeIndices = getIndices(meshIdx, j); - std::vector indices; - if (maybeIndices.has_value()) { - indices = std::move(*maybeIndices); - checkIndices(indices, vertices->size()); - } else { - // Non-indexed geometry - indices = generateIndices(vertices->size()); - } - - auto *meshbuf = m_irr_model->addMeshBuffer(); - meshbuf->append(vertices->data(), vertices->size(), - indices.data(), indices.size()); - } -} - -// Base transformation between left & right handed coordinate systems. -// This just inverts the Z axis. -static core::matrix4 leftToRight = core::matrix4( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, -1, 0, - 0, 0, 0, 1 -); -static core::matrix4 rightToLeft = leftToRight; - -static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m) -{ - // Note: Under the hood, this casts these doubles to floats. - return core::matrix4( - m[0], m[1], m[2], m[3], - m[4], m[5], m[6], m[7], - m[8], m[9], m[10], m[11], - m[12], m[13], m[14], m[15]); -} - -static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs) -{ - const auto &trans = trs.translation; - const auto &rot = trs.rotation; - const auto &scale = trs.scale; - core::matrix4 transMat; - transMat.setTranslation(core::vector3df(trans[0], trans[1], trans[2])); - core::matrix4 rotMat = core::quaternion(rot[0], rot[1], rot[2], rot[3]).getMatrix(); - core::matrix4 scaleMat; - scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2])); - return transMat * rotMat * scaleMat; -} - -static core::matrix4 loadTransform(std::optional> transform) { - if (!transform.has_value()) { - return core::matrix4(); - } - core::matrix4 mat = std::visit([](const auto &t) { return loadTransform(t); }, *transform); - return rightToLeft * mat * leftToRight; -} - -void CGLTFMeshFileLoader::MeshExtractor::loadNode( - const std::size_t nodeIdx, - ISkinnedMesh::SJoint *parent) const -{ - const auto &node = m_gltf_model.nodes->at(nodeIdx); - auto *joint = m_irr_model->addJoint(parent); - const core::matrix4 transform = loadTransform(node.transform); - joint->LocalMatrix = transform; - joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix; - if (node.name.has_value()) { - joint->Name = node.name->c_str(); - } - if (node.mesh.has_value()) { - loadMesh(*node.mesh, joint); - } - if (node.children.has_value()) { - for (const auto &child : *node.children) { - loadNode(child, joint); - } - } -} - -void CGLTFMeshFileLoader::MeshExtractor::loadNodes() const -{ - std::vector isChild(m_gltf_model.nodes->size()); - for (const auto &node : *m_gltf_model.nodes) { - if (!node.children.has_value()) - continue; - for (const auto &child : *node.children) { - isChild[child] = true; - } - } - // Load all nodes that aren't children. - // Children will be loaded by their parent nodes. - for (std::size_t i = 0; i < m_gltf_model.nodes->size(); ++i) { - if (!isChild[i]) { - loadNode(i, nullptr); - } - } -} - -/** - * Extracts GLTF mesh indices into the irrlicht model. -*/ -std::optional> CGLTFMeshFileLoader::MeshExtractor::getIndices( - const std::size_t meshIdx, - const std::size_t primitiveIdx) const -{ - const auto accessorIdx = getIndicesAccessorIdx(meshIdx, primitiveIdx); - if (!accessorIdx.has_value()) - return std::nullopt; // non-indexed geometry - const auto &accessor = m_gltf_model.accessors->at(accessorIdx.value()); - - const auto& buf = getBuffer(accessorIdx.value()); - - std::vector indices{}; - const auto count = getElemCount(accessorIdx.value()); - for (std::size_t i = 0; i < count; ++i) { - std::size_t elemIdx = count - i - 1; // reverse index order - u16 index; - // Note: glTF forbids the max value for each component type. - switch (accessor.componentType) { - case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: { - index = readPrimitive(BufferOffset(buf, elemIdx * sizeof(u8))); - if (index == std::numeric_limits::max()) - throw std::runtime_error("invalid index"); - break; - } - case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: { - index = readPrimitive(BufferOffset(buf, elemIdx * sizeof(u16))); - if (index == std::numeric_limits::max()) - throw std::runtime_error("invalid index"); - break; - } - case tiniergltf::Accessor::ComponentType::UNSIGNED_INT: { - u32 indexWide = readPrimitive(BufferOffset(buf, elemIdx * sizeof(u32))); - // Use >= here for consistency. - if (indexWide >= std::numeric_limits::max()) - throw std::runtime_error("index too large (>= 65536)"); - index = static_cast(indexWide); - break; - } - default: - throw std::runtime_error("invalid index component type"); - } - indices.push_back(index); - } - - return indices; -} - -/** - * Create a vector of video::S3DVertex (model data) from a mesh & primitive index. -*/ -std::optional> CGLTFMeshFileLoader::MeshExtractor::getVertices( - const std::size_t meshIdx, - const std::size_t primitiveIdx) const -{ - const auto positionAccessorIdx = getPositionAccessorIdx( - meshIdx, primitiveIdx); - if (!positionAccessorIdx.has_value()) { - // "When positions are not specified, client implementations SHOULD skip primitive's rendering" - return std::nullopt; - } - - std::vector vertices{}; - vertices.resize(getElemCount(*positionAccessorIdx)); - copyPositions(*positionAccessorIdx, vertices); - - const auto normalAccessorIdx = getNormalAccessorIdx( - meshIdx, primitiveIdx); - if (normalAccessorIdx.has_value()) { - copyNormals(normalAccessorIdx.value(), vertices); - } - - const auto tCoordAccessorIdx = getTCoordAccessorIdx( - meshIdx, primitiveIdx); - if (tCoordAccessorIdx.has_value()) { - copyTCoords(tCoordAccessorIdx.value(), vertices); - } - - return vertices; -} - -/** - * Get the amount of meshes that a model contains. -*/ -std::size_t CGLTFMeshFileLoader::MeshExtractor::getMeshCount() const -{ - return m_gltf_model.meshes->size(); -} - -/** - * Get the amount of primitives that a mesh in a model contains. -*/ -std::size_t CGLTFMeshFileLoader::MeshExtractor::getPrimitiveCount( - const std::size_t meshIdx) const -{ - return m_gltf_model.meshes->at(meshIdx).primitives.size(); -} - -/** - * Templated buffer reader. Based on type width. - * This is specifically used to build upon to read more complex data types. - * It is also used raw to read arrays directly. - * Basically we're using the width of the type to infer - * how big of a gap we have from the beginning of the buffer. -*/ -template -T CGLTFMeshFileLoader::MeshExtractor::readPrimitive( - const BufferOffset& readFrom) -{ - unsigned char d[sizeof(T)]{}; - for (std::size_t i = 0; i < sizeof(T); ++i) { - d[i] = readFrom.at(i); - } - T dest; - std::memcpy(&dest, d, sizeof(dest)); - return dest; -} - -/** - * Read a vector2df from a buffer at an offset. - * @return vec2 core::Vector2df -*/ -core::vector2df CGLTFMeshFileLoader::MeshExtractor::readVec2DF( - const CGLTFMeshFileLoader::BufferOffset& readFrom) -{ - return core::vector2df(readPrimitive(readFrom), - readPrimitive(BufferOffset(readFrom, sizeof(float)))); - -} - -/** - * Read a vector3df from a buffer at an offset. - * Also does right-to-left-handed coordinate system conversion (inverts Z axis). - * @return vec3 core::Vector3df -*/ -core::vector3df CGLTFMeshFileLoader::MeshExtractor::readVec3DF( - const BufferOffset& readFrom, - const core::vector3df scale = {1.0f,1.0f,1.0f}) -{ - return core::vector3df( - readPrimitive(readFrom), - readPrimitive(BufferOffset(readFrom, sizeof(float))), - -readPrimitive(BufferOffset(readFrom, 2 * - sizeof(float)))); -} - -/** - * Streams vertex positions raw data into usable buffer via reference. - * Buffer: ref Vector -*/ -void CGLTFMeshFileLoader::MeshExtractor::copyPositions( - const std::size_t accessorIdx, - std::vector& vertices) const -{ - - const auto& buffer = getBuffer(accessorIdx); - const auto count = getElemCount(accessorIdx); - const auto byteStride = getByteStride(accessorIdx); - - for (std::size_t i = 0; i < count; i++) { - const auto v = readVec3DF(BufferOffset(buffer, byteStride * i)); - vertices[i].Pos = v; - } -} - -/** - * Streams normals raw data into usable buffer via reference. - * Buffer: ref Vector -*/ -void CGLTFMeshFileLoader::MeshExtractor::copyNormals( - const std::size_t accessorIdx, - std::vector& vertices) const -{ - const auto& buffer = getBuffer(accessorIdx); - const auto count = getElemCount(accessorIdx); - - for (std::size_t i = 0; i < count; i++) { - const auto n = readVec3DF(BufferOffset(buffer, - 3 * sizeof(float) * i)); - vertices[i].Normal = n; - } -} - -/** - * Streams texture coordinate raw data into usable buffer via reference. - * Buffer: ref Vector -*/ -void CGLTFMeshFileLoader::MeshExtractor::copyTCoords( - const std::size_t accessorIdx, - std::vector& vertices) const -{ - - const auto& buffer = getBuffer(accessorIdx); - const auto count = getElemCount(accessorIdx); - - for (std::size_t i = 0; i < count; ++i) { - const auto t = readVec2DF(BufferOffset(buffer, - 2 * sizeof(float) * i)); - vertices[i].TCoords = t; - } -} - -/** - * The number of elements referenced by this accessor, not to be confused with the number of bytes or number of components. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_accessor_count - * Type: Integer - * Required: YES -*/ -std::size_t CGLTFMeshFileLoader::MeshExtractor::getElemCount( - const std::size_t accessorIdx) const -{ - return m_gltf_model.accessors->at(accessorIdx).count; -} - -/** - * The stride, in bytes, between vertex attributes. - * When this is not defined, data is tightly packed. - * When two or more accessors use the same buffer view, this field MUST be defined. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_bufferview_bytestride - * Required: NO -*/ -std::size_t CGLTFMeshFileLoader::MeshExtractor::getByteStride( - const std::size_t accessorIdx) const -{ - const auto& accessor = m_gltf_model.accessors->at(accessorIdx); - // FIXME this does not work with sparse / zero-initialized accessors - const auto& view = m_gltf_model.bufferViews->at(accessor.bufferView.value()); - return view.byteStride.value_or(accessor.elementSize()); -} - -/** - * Specifies whether integer data values are normalized (true) to [0, 1] (for unsigned types) - * or to [-1, 1] (for signed types) when they are accessed. This property MUST NOT be set to - * true for accessors with FLOAT or UNSIGNED_INT component type. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_accessor_normalized - * Required: NO -*/ -bool CGLTFMeshFileLoader::MeshExtractor::isAccessorNormalized( - const std::size_t accessorIdx) const -{ - const auto& accessor = m_gltf_model.accessors->at(accessorIdx); - return accessor.normalized; -} - -/** - * Walk through the complex chain of the model to extract the required buffer. - * Accessor -> BufferView -> Buffer -*/ -CGLTFMeshFileLoader::BufferOffset CGLTFMeshFileLoader::MeshExtractor::getBuffer( - const std::size_t accessorIdx) const -{ - const auto& accessor = m_gltf_model.accessors->at(accessorIdx); - // FIXME this does not work with sparse / zero-initialized accessors - const auto& view = m_gltf_model.bufferViews->at(accessor.bufferView.value()); - const auto& buffer = m_gltf_model.buffers->at(view.buffer); - - return BufferOffset(buffer.data, view.byteOffset); -} - -/** - * The index of the accessor that contains the vertex indices. - * When this is undefined, the primitive defines non-indexed geometry. - * When defined, the accessor MUST have SCALAR type and an unsigned integer component type. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_mesh_primitive_indices - * Type: Integer - * Required: NO -*/ -std::optional CGLTFMeshFileLoader::MeshExtractor::getIndicesAccessorIdx( - const std::size_t meshIdx, - const std::size_t primitiveIdx) const -{ - return m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].indices; -} - -/** - * The index of the accessor that contains the POSITIONs. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview - * Type: VEC3 (Float) -*/ -std::optional CGLTFMeshFileLoader::MeshExtractor::getPositionAccessorIdx( - const std::size_t meshIdx, - const std::size_t primitiveIdx) const -{ - return m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.position; -} - -/** - * The index of the accessor that contains the NORMALs. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview - * Type: VEC3 (Float) - * ! Required: NO (Appears to not be, needs another pair of eyes to research.) -*/ -std::optional CGLTFMeshFileLoader::MeshExtractor::getNormalAccessorIdx( - const std::size_t meshIdx, - const std::size_t primitiveIdx) const -{ - return m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.normal; -} - -/** - * The index of the accessor that contains the TEXCOORDs. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview - * Type: VEC3 (Float) - * ! Required: YES (Appears so, needs another pair of eyes to research.) -*/ -std::optional CGLTFMeshFileLoader::MeshExtractor::getTCoordAccessorIdx( - const std::size_t meshIdx, - const std::size_t primitiveIdx) const -{ - const auto& texcoords = m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.texcoord; - if (!texcoords.has_value()) - return std::nullopt; - return texcoords->at(0); -} - -/** - * This is where the actual model's GLTF file is loaded and parsed by tiniergltf. -*/ -std::optional CGLTFMeshFileLoader::tryParseGLTF(io::IReadFile* file) -{ - auto size = file->getSize(); - auto buf = std::make_unique(size + 1); - file->read(buf.get(), size); - // We probably don't need this, but add it just to be sure. - buf[size] = '\0'; - Json::CharReaderBuilder builder; - const std::unique_ptr reader(builder.newCharReader()); - Json::Value json; - JSONCPP_STRING err; - if (!reader->parse(buf.get(), buf.get() + size, &json, &err)) { - return std::nullopt; - } - try { - return tiniergltf::GlTF(json); - } catch (const std::runtime_error &e) { - return std::nullopt; - } catch (const std::out_of_range &e) { - return std::nullopt; - } -} - -} // namespace scene - -} // namespace irr - +#include "CGLTFMeshFileLoader.h" + +#include "coreutil.h" +#include "CSkinnedMesh.h" +#include "ISkinnedMesh.h" +#include "irrTypes.h" +#include "IReadFile.h" +#include "matrix4.h" +#include "path.h" +#include "S3DVertex.h" +#include "quaternion.h" +#include "vector3d.h" + +#include "tiniergltf.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Notes on the coordinate system. + * + * glTF uses a right-handed coordinate system where +Z is the + * front-facing axis, and Irrlicht uses a left-handed coordinate + * system where -Z is the front-facing axis. + * We convert between them by reflecting the mesh across the X axis. + * Doing this correctly requires negating the Z coordinate on + * vertex positions and normals, and reversing the winding order + * of the vertex indices. + */ + +namespace irr { + +namespace scene { + +CGLTFMeshFileLoader::BufferOffset::BufferOffset( + const std::vector& buf, + const std::size_t offset) + : m_buf(buf) + , m_offset(offset) +{ +} + +CGLTFMeshFileLoader::BufferOffset::BufferOffset( + const CGLTFMeshFileLoader::BufferOffset& other, + const std::size_t fromOffset) + : m_buf(other.m_buf) + , m_offset(other.m_offset + fromOffset) +{ +} + +/** + * Get a raw unsigned char (ubyte) from a buffer offset. +*/ +unsigned char CGLTFMeshFileLoader::BufferOffset::at( + const std::size_t fromOffset) const +{ + return m_buf.at(m_offset + fromOffset); +} + +CGLTFMeshFileLoader::CGLTFMeshFileLoader() noexcept +{ +} + +/** + * The most basic portion of the code base. This tells irllicht if this file has a .gltf extension. +*/ +bool CGLTFMeshFileLoader::isALoadableFileExtension( + const io::path& filename) const +{ + return core::hasFileExtension(filename, "gltf"); +} + +/** + * Entry point into loading a GLTF model. +*/ +IAnimatedMesh* CGLTFMeshFileLoader::createMesh(io::IReadFile* file) +{ + if (file->getSize() <= 0) { + return nullptr; + } + std::optional model = tryParseGLTF(file); + if (!model.has_value()) { + return nullptr; + } + + if (!(model->buffers.has_value() + && model->bufferViews.has_value() + && model->accessors.has_value() + && model->meshes.has_value() + && model->nodes.has_value())) { + return nullptr; + } + + ISkinnedMesh *mesh = new CSkinnedMesh(); + MeshExtractor parser(std::move(model.value()), mesh); + try { + parser.loadNodes(); + } catch (std::runtime_error &e) { + mesh->drop(); + return nullptr; + } + return mesh; +} + +static void transformVertices(std::vector &vertices, const core::matrix4 &transform) +{ + for (auto &vertex : vertices) { + // Apply scaling, rotation and rotation (in that order) to the position. + transform.transformVect(vertex.Pos); + // For the normal, we do not want to apply the translation. + // TODO note that this also applies scaling; the Irrlicht method is misnamed. + transform.rotateVect(vertex.Normal); + // Renormalize (length might have been affected by scaling). + vertex.Normal.normalize(); + } +} + +static void checkIndices(const std::vector &indices, const std::size_t nVerts) +{ + for (u16 index : indices) { + if (index >= nVerts) + throw std::runtime_error("index out of bounds"); + } +} + +static std::vector generateIndices(const std::size_t nVerts) +{ + std::vector indices(nVerts); + for (std::size_t i = 0; i < nVerts; i += 3) { + // Reverse winding order per triangle + indices[i] = i + 2; + indices[i + 1] = i + 1; + indices[i + 2] = i; + } + return indices; +} + +/** + * Load up the rawest form of the model. The vertex positions and indices. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes + * If material is undefined, then a default material MUST be used. +*/ +void CGLTFMeshFileLoader::MeshExtractor::loadMesh( + const std::size_t meshIdx, + ISkinnedMesh::SJoint *parent) const +{ + for (std::size_t j = 0; j < getPrimitiveCount(meshIdx); ++j) { + auto vertices = getVertices(meshIdx, j); + if (!vertices.has_value()) + continue; // "When positions are not specified, client implementations SHOULD skip primitive’s rendering" + + // Excludes the max value for consistency. + if (vertices->size() >= std::numeric_limits::max()) + throw std::runtime_error("too many vertices"); + + // Apply the global transform along the parent chain. + transformVertices(*vertices, parent->GlobalMatrix); + + auto maybeIndices = getIndices(meshIdx, j); + std::vector indices; + if (maybeIndices.has_value()) { + indices = std::move(*maybeIndices); + checkIndices(indices, vertices->size()); + } else { + // Non-indexed geometry + indices = generateIndices(vertices->size()); + } + + auto *meshbuf = m_irr_model->addMeshBuffer(); + meshbuf->append(vertices->data(), vertices->size(), + indices.data(), indices.size()); + } +} + +// Base transformation between left & right handed coordinate systems. +// This just inverts the Z axis. +static core::matrix4 leftToRight = core::matrix4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, -1, 0, + 0, 0, 0, 1 +); +static core::matrix4 rightToLeft = leftToRight; + +static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m) +{ + // Note: Under the hood, this casts these doubles to floats. + return core::matrix4( + m[0], m[1], m[2], m[3], + m[4], m[5], m[6], m[7], + m[8], m[9], m[10], m[11], + m[12], m[13], m[14], m[15]); +} + +static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs) +{ + const auto &trans = trs.translation; + const auto &rot = trs.rotation; + const auto &scale = trs.scale; + core::matrix4 transMat; + transMat.setTranslation(core::vector3df(trans[0], trans[1], trans[2])); + core::matrix4 rotMat = core::quaternion(rot[0], rot[1], rot[2], rot[3]).getMatrix(); + core::matrix4 scaleMat; + scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2])); + return transMat * rotMat * scaleMat; +} + +static core::matrix4 loadTransform(std::optional> transform) { + if (!transform.has_value()) { + return core::matrix4(); + } + core::matrix4 mat = std::visit([](const auto &t) { return loadTransform(t); }, *transform); + return rightToLeft * mat * leftToRight; +} + +void CGLTFMeshFileLoader::MeshExtractor::loadNode( + const std::size_t nodeIdx, + ISkinnedMesh::SJoint *parent) const +{ + const auto &node = m_gltf_model.nodes->at(nodeIdx); + auto *joint = m_irr_model->addJoint(parent); + const core::matrix4 transform = loadTransform(node.transform); + joint->LocalMatrix = transform; + joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix; + if (node.name.has_value()) { + joint->Name = node.name->c_str(); + } + if (node.mesh.has_value()) { + loadMesh(*node.mesh, joint); + } + if (node.children.has_value()) { + for (const auto &child : *node.children) { + loadNode(child, joint); + } + } +} + +void CGLTFMeshFileLoader::MeshExtractor::loadNodes() const +{ + std::vector isChild(m_gltf_model.nodes->size()); + for (const auto &node : *m_gltf_model.nodes) { + if (!node.children.has_value()) + continue; + for (const auto &child : *node.children) { + isChild[child] = true; + } + } + // Load all nodes that aren't children. + // Children will be loaded by their parent nodes. + for (std::size_t i = 0; i < m_gltf_model.nodes->size(); ++i) { + if (!isChild[i]) { + loadNode(i, nullptr); + } + } +} + +/** + * Extracts GLTF mesh indices into the irrlicht model. +*/ +std::optional> CGLTFMeshFileLoader::MeshExtractor::getIndices( + const std::size_t meshIdx, + const std::size_t primitiveIdx) const +{ + const auto accessorIdx = getIndicesAccessorIdx(meshIdx, primitiveIdx); + if (!accessorIdx.has_value()) + return std::nullopt; // non-indexed geometry + const auto &accessor = m_gltf_model.accessors->at(accessorIdx.value()); + + const auto& buf = getBuffer(accessorIdx.value()); + + std::vector indices{}; + const auto count = getElemCount(accessorIdx.value()); + for (std::size_t i = 0; i < count; ++i) { + std::size_t elemIdx = count - i - 1; // reverse index order + u16 index; + // Note: glTF forbids the max value for each component type. + switch (accessor.componentType) { + case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: { + index = readPrimitive(BufferOffset(buf, elemIdx * sizeof(u8))); + if (index == std::numeric_limits::max()) + throw std::runtime_error("invalid index"); + break; + } + case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: { + index = readPrimitive(BufferOffset(buf, elemIdx * sizeof(u16))); + if (index == std::numeric_limits::max()) + throw std::runtime_error("invalid index"); + break; + } + case tiniergltf::Accessor::ComponentType::UNSIGNED_INT: { + u32 indexWide = readPrimitive(BufferOffset(buf, elemIdx * sizeof(u32))); + // Use >= here for consistency. + if (indexWide >= std::numeric_limits::max()) + throw std::runtime_error("index too large (>= 65536)"); + index = static_cast(indexWide); + break; + } + default: + throw std::runtime_error("invalid index component type"); + } + indices.push_back(index); + } + + return indices; +} + +/** + * Create a vector of video::S3DVertex (model data) from a mesh & primitive index. +*/ +std::optional> CGLTFMeshFileLoader::MeshExtractor::getVertices( + const std::size_t meshIdx, + const std::size_t primitiveIdx) const +{ + const auto positionAccessorIdx = getPositionAccessorIdx( + meshIdx, primitiveIdx); + if (!positionAccessorIdx.has_value()) { + // "When positions are not specified, client implementations SHOULD skip primitive's rendering" + return std::nullopt; + } + + std::vector vertices{}; + vertices.resize(getElemCount(*positionAccessorIdx)); + copyPositions(*positionAccessorIdx, vertices); + + const auto normalAccessorIdx = getNormalAccessorIdx( + meshIdx, primitiveIdx); + if (normalAccessorIdx.has_value()) { + copyNormals(normalAccessorIdx.value(), vertices); + } + + const auto tCoordAccessorIdx = getTCoordAccessorIdx( + meshIdx, primitiveIdx); + if (tCoordAccessorIdx.has_value()) { + copyTCoords(tCoordAccessorIdx.value(), vertices); + } + + return vertices; +} + +/** + * Get the amount of meshes that a model contains. +*/ +std::size_t CGLTFMeshFileLoader::MeshExtractor::getMeshCount() const +{ + return m_gltf_model.meshes->size(); +} + +/** + * Get the amount of primitives that a mesh in a model contains. +*/ +std::size_t CGLTFMeshFileLoader::MeshExtractor::getPrimitiveCount( + const std::size_t meshIdx) const +{ + return m_gltf_model.meshes->at(meshIdx).primitives.size(); +} + +/** + * Templated buffer reader. Based on type width. + * This is specifically used to build upon to read more complex data types. + * It is also used raw to read arrays directly. + * Basically we're using the width of the type to infer + * how big of a gap we have from the beginning of the buffer. +*/ +template +T CGLTFMeshFileLoader::MeshExtractor::readPrimitive( + const BufferOffset& readFrom) +{ + unsigned char d[sizeof(T)]{}; + for (std::size_t i = 0; i < sizeof(T); ++i) { + d[i] = readFrom.at(i); + } + T dest; + std::memcpy(&dest, d, sizeof(dest)); + return dest; +} + +/** + * Read a vector2df from a buffer at an offset. + * @return vec2 core::Vector2df +*/ +core::vector2df CGLTFMeshFileLoader::MeshExtractor::readVec2DF( + const CGLTFMeshFileLoader::BufferOffset& readFrom) +{ + return core::vector2df(readPrimitive(readFrom), + readPrimitive(BufferOffset(readFrom, sizeof(float)))); + +} + +/** + * Read a vector3df from a buffer at an offset. + * Also does right-to-left-handed coordinate system conversion (inverts Z axis). + * @return vec3 core::Vector3df +*/ +core::vector3df CGLTFMeshFileLoader::MeshExtractor::readVec3DF( + const BufferOffset& readFrom, + const core::vector3df scale = {1.0f,1.0f,1.0f}) +{ + return core::vector3df( + readPrimitive(readFrom), + readPrimitive(BufferOffset(readFrom, sizeof(float))), + -readPrimitive(BufferOffset(readFrom, 2 * + sizeof(float)))); +} + +/** + * Streams vertex positions raw data into usable buffer via reference. + * Buffer: ref Vector +*/ +void CGLTFMeshFileLoader::MeshExtractor::copyPositions( + const std::size_t accessorIdx, + std::vector& vertices) const +{ + + const auto& buffer = getBuffer(accessorIdx); + const auto count = getElemCount(accessorIdx); + const auto byteStride = getByteStride(accessorIdx); + + for (std::size_t i = 0; i < count; i++) { + const auto v = readVec3DF(BufferOffset(buffer, byteStride * i)); + vertices[i].Pos = v; + } +} + +/** + * Streams normals raw data into usable buffer via reference. + * Buffer: ref Vector +*/ +void CGLTFMeshFileLoader::MeshExtractor::copyNormals( + const std::size_t accessorIdx, + std::vector& vertices) const +{ + const auto& buffer = getBuffer(accessorIdx); + const auto count = getElemCount(accessorIdx); + + for (std::size_t i = 0; i < count; i++) { + const auto n = readVec3DF(BufferOffset(buffer, + 3 * sizeof(float) * i)); + vertices[i].Normal = n; + } +} + +/** + * Streams texture coordinate raw data into usable buffer via reference. + * Buffer: ref Vector +*/ +void CGLTFMeshFileLoader::MeshExtractor::copyTCoords( + const std::size_t accessorIdx, + std::vector& vertices) const +{ + + const auto& buffer = getBuffer(accessorIdx); + const auto count = getElemCount(accessorIdx); + + for (std::size_t i = 0; i < count; ++i) { + const auto t = readVec2DF(BufferOffset(buffer, + 2 * sizeof(float) * i)); + vertices[i].TCoords = t; + } +} + +/** + * The number of elements referenced by this accessor, not to be confused with the number of bytes or number of components. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_accessor_count + * Type: Integer + * Required: YES +*/ +std::size_t CGLTFMeshFileLoader::MeshExtractor::getElemCount( + const std::size_t accessorIdx) const +{ + return m_gltf_model.accessors->at(accessorIdx).count; +} + +/** + * The stride, in bytes, between vertex attributes. + * When this is not defined, data is tightly packed. + * When two or more accessors use the same buffer view, this field MUST be defined. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_bufferview_bytestride + * Required: NO +*/ +std::size_t CGLTFMeshFileLoader::MeshExtractor::getByteStride( + const std::size_t accessorIdx) const +{ + const auto& accessor = m_gltf_model.accessors->at(accessorIdx); + // FIXME this does not work with sparse / zero-initialized accessors + const auto& view = m_gltf_model.bufferViews->at(accessor.bufferView.value()); + return view.byteStride.value_or(accessor.elementSize()); +} + +/** + * Specifies whether integer data values are normalized (true) to [0, 1] (for unsigned types) + * or to [-1, 1] (for signed types) when they are accessed. This property MUST NOT be set to + * true for accessors with FLOAT or UNSIGNED_INT component type. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_accessor_normalized + * Required: NO +*/ +bool CGLTFMeshFileLoader::MeshExtractor::isAccessorNormalized( + const std::size_t accessorIdx) const +{ + const auto& accessor = m_gltf_model.accessors->at(accessorIdx); + return accessor.normalized; +} + +/** + * Walk through the complex chain of the model to extract the required buffer. + * Accessor -> BufferView -> Buffer +*/ +CGLTFMeshFileLoader::BufferOffset CGLTFMeshFileLoader::MeshExtractor::getBuffer( + const std::size_t accessorIdx) const +{ + const auto& accessor = m_gltf_model.accessors->at(accessorIdx); + // FIXME this does not work with sparse / zero-initialized accessors + const auto& view = m_gltf_model.bufferViews->at(accessor.bufferView.value()); + const auto& buffer = m_gltf_model.buffers->at(view.buffer); + + return BufferOffset(buffer.data, view.byteOffset); +} + +/** + * The index of the accessor that contains the vertex indices. + * When this is undefined, the primitive defines non-indexed geometry. + * When defined, the accessor MUST have SCALAR type and an unsigned integer component type. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_mesh_primitive_indices + * Type: Integer + * Required: NO +*/ +std::optional CGLTFMeshFileLoader::MeshExtractor::getIndicesAccessorIdx( + const std::size_t meshIdx, + const std::size_t primitiveIdx) const +{ + return m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].indices; +} + +/** + * The index of the accessor that contains the POSITIONs. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview + * Type: VEC3 (Float) +*/ +std::optional CGLTFMeshFileLoader::MeshExtractor::getPositionAccessorIdx( + const std::size_t meshIdx, + const std::size_t primitiveIdx) const +{ + return m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.position; +} + +/** + * The index of the accessor that contains the NORMALs. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview + * Type: VEC3 (Float) + * ! Required: NO (Appears to not be, needs another pair of eyes to research.) +*/ +std::optional CGLTFMeshFileLoader::MeshExtractor::getNormalAccessorIdx( + const std::size_t meshIdx, + const std::size_t primitiveIdx) const +{ + return m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.normal; +} + +/** + * The index of the accessor that contains the TEXCOORDs. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview + * Type: VEC3 (Float) + * ! Required: YES (Appears so, needs another pair of eyes to research.) +*/ +std::optional CGLTFMeshFileLoader::MeshExtractor::getTCoordAccessorIdx( + const std::size_t meshIdx, + const std::size_t primitiveIdx) const +{ + const auto& texcoords = m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.texcoord; + if (!texcoords.has_value()) + return std::nullopt; + return texcoords->at(0); +} + +/** + * This is where the actual model's GLTF file is loaded and parsed by tiniergltf. +*/ +std::optional CGLTFMeshFileLoader::tryParseGLTF(io::IReadFile* file) +{ + auto size = file->getSize(); + auto buf = std::make_unique(size + 1); + file->read(buf.get(), size); + // We probably don't need this, but add it just to be sure. + buf[size] = '\0'; + Json::CharReaderBuilder builder; + const std::unique_ptr reader(builder.newCharReader()); + Json::Value json; + JSONCPP_STRING err; + if (!reader->parse(buf.get(), buf.get() + size, &json, &err)) { + return std::nullopt; + } + try { + return tiniergltf::GlTF(json); + } catch (const std::runtime_error &e) { + return std::nullopt; + } catch (const std::out_of_range &e) { + return std::nullopt; + } +} + +} // namespace scene + +} // namespace irr + diff --git a/src/CGLTFMeshFileLoader.h b/irr/src/CGLTFMeshFileLoader.h similarity index 96% rename from src/CGLTFMeshFileLoader.h rename to irr/src/CGLTFMeshFileLoader.h index aabf1d241..e258db8b8 100644 --- a/src/CGLTFMeshFileLoader.h +++ b/irr/src/CGLTFMeshFileLoader.h @@ -1,146 +1,146 @@ -#ifndef __C_GLTF_MESH_FILE_LOADER_INCLUDED__ -#define __C_GLTF_MESH_FILE_LOADER_INCLUDED__ - -#include "ISkinnedMesh.h" -#include "IMeshLoader.h" -#include "IReadFile.h" -#include "irrTypes.h" -#include "path.h" -#include "S3DVertex.h" -#include "vector2d.h" -#include "vector3d.h" - -#include - -#include -#include - -namespace irr -{ - -namespace scene -{ - -class CGLTFMeshFileLoader : public IMeshLoader -{ -public: - CGLTFMeshFileLoader() noexcept; - - bool isALoadableFileExtension(const io::path& filename) const override; - - IAnimatedMesh* createMesh(io::IReadFile* file) override; - -private: - class BufferOffset - { - public: - BufferOffset(const std::vector& buf, - const std::size_t offset); - - BufferOffset(const BufferOffset& other, - const std::size_t fromOffset); - - unsigned char at(const std::size_t fromOffset) const; - private: - const std::vector& m_buf; - std::size_t m_offset; - int m_filesize; - }; - - class MeshExtractor { - public: - using vertex_t = video::S3DVertex; - - MeshExtractor(const tiniergltf::GlTF &model, - ISkinnedMesh *mesh) noexcept - : m_gltf_model(model), m_irr_model(mesh) {}; - - MeshExtractor(const tiniergltf::GlTF &&model, - ISkinnedMesh *mesh) noexcept - : m_gltf_model(model), m_irr_model(mesh) {}; - - /* Gets indices for the given mesh/primitive. - * - * Values are return in Irrlicht winding order. - */ - std::optional> getIndices(const std::size_t meshIdx, - const std::size_t primitiveIdx) const; - - std::optional> getVertices(std::size_t meshIdx, - const std::size_t primitiveIdx) const; - - std::size_t getMeshCount() const; - - std::size_t getPrimitiveCount(const std::size_t meshIdx) const; - - void loadNodes() const; - - private: - const tiniergltf::GlTF m_gltf_model; - ISkinnedMesh *m_irr_model; - - template - static T readPrimitive(const BufferOffset& readFrom); - - static core::vector2df readVec2DF( - const BufferOffset& readFrom); - - /* Read a vec3df from a buffer with transformations applied. - * - * Values are returned in Irrlicht coordinates. - */ - static core::vector3df readVec3DF( - const BufferOffset& readFrom, - const core::vector3df scale); - - void copyPositions(const std::size_t accessorIdx, - std::vector& vertices) const; - - void copyNormals(const std::size_t accessorIdx, - std::vector& vertices) const; - - void copyTCoords(const std::size_t accessorIdx, - std::vector& vertices) const; - - std::size_t getElemCount(const std::size_t accessorIdx) const; - - std::size_t getByteStride(const std::size_t accessorIdx) const; - - bool isAccessorNormalized(const std::size_t accessorIdx) const; - - BufferOffset getBuffer(const std::size_t accessorIdx) const; - - std::optional getIndicesAccessorIdx(const std::size_t meshIdx, - const std::size_t primitiveIdx) const; - - std::optional getPositionAccessorIdx(const std::size_t meshIdx, - const std::size_t primitiveIdx) const; - - /* Get the accessor id of the normals of a primitive. - */ - std::optional getNormalAccessorIdx(const std::size_t meshIdx, - const std::size_t primitiveIdx) const; - - /* Get the accessor id for the tcoords of a primitive. - */ - std::optional getTCoordAccessorIdx(const std::size_t meshIdx, - const std::size_t primitiveIdx) const; - - void loadMesh( - std::size_t meshIdx, - ISkinnedMesh::SJoint *parentJoint) const; - - void loadNode( - const std::size_t nodeIdx, - ISkinnedMesh::SJoint *parentJoint) const; - }; - - std::optional tryParseGLTF(io::IReadFile* file); -}; - -} // namespace scene - -} // namespace irr - -#endif // __C_GLTF_MESH_FILE_LOADER_INCLUDED__ - +#ifndef __C_GLTF_MESH_FILE_LOADER_INCLUDED__ +#define __C_GLTF_MESH_FILE_LOADER_INCLUDED__ + +#include "ISkinnedMesh.h" +#include "IMeshLoader.h" +#include "IReadFile.h" +#include "irrTypes.h" +#include "path.h" +#include "S3DVertex.h" +#include "vector2d.h" +#include "vector3d.h" + +#include + +#include +#include + +namespace irr +{ + +namespace scene +{ + +class CGLTFMeshFileLoader : public IMeshLoader +{ +public: + CGLTFMeshFileLoader() noexcept; + + bool isALoadableFileExtension(const io::path& filename) const override; + + IAnimatedMesh* createMesh(io::IReadFile* file) override; + +private: + class BufferOffset + { + public: + BufferOffset(const std::vector& buf, + const std::size_t offset); + + BufferOffset(const BufferOffset& other, + const std::size_t fromOffset); + + unsigned char at(const std::size_t fromOffset) const; + private: + const std::vector& m_buf; + std::size_t m_offset; + int m_filesize; + }; + + class MeshExtractor { + public: + using vertex_t = video::S3DVertex; + + MeshExtractor(const tiniergltf::GlTF &model, + ISkinnedMesh *mesh) noexcept + : m_gltf_model(model), m_irr_model(mesh) {}; + + MeshExtractor(const tiniergltf::GlTF &&model, + ISkinnedMesh *mesh) noexcept + : m_gltf_model(model), m_irr_model(mesh) {}; + + /* Gets indices for the given mesh/primitive. + * + * Values are return in Irrlicht winding order. + */ + std::optional> getIndices(const std::size_t meshIdx, + const std::size_t primitiveIdx) const; + + std::optional> getVertices(std::size_t meshIdx, + const std::size_t primitiveIdx) const; + + std::size_t getMeshCount() const; + + std::size_t getPrimitiveCount(const std::size_t meshIdx) const; + + void loadNodes() const; + + private: + const tiniergltf::GlTF m_gltf_model; + ISkinnedMesh *m_irr_model; + + template + static T readPrimitive(const BufferOffset& readFrom); + + static core::vector2df readVec2DF( + const BufferOffset& readFrom); + + /* Read a vec3df from a buffer with transformations applied. + * + * Values are returned in Irrlicht coordinates. + */ + static core::vector3df readVec3DF( + const BufferOffset& readFrom, + const core::vector3df scale); + + void copyPositions(const std::size_t accessorIdx, + std::vector& vertices) const; + + void copyNormals(const std::size_t accessorIdx, + std::vector& vertices) const; + + void copyTCoords(const std::size_t accessorIdx, + std::vector& vertices) const; + + std::size_t getElemCount(const std::size_t accessorIdx) const; + + std::size_t getByteStride(const std::size_t accessorIdx) const; + + bool isAccessorNormalized(const std::size_t accessorIdx) const; + + BufferOffset getBuffer(const std::size_t accessorIdx) const; + + std::optional getIndicesAccessorIdx(const std::size_t meshIdx, + const std::size_t primitiveIdx) const; + + std::optional getPositionAccessorIdx(const std::size_t meshIdx, + const std::size_t primitiveIdx) const; + + /* Get the accessor id of the normals of a primitive. + */ + std::optional getNormalAccessorIdx(const std::size_t meshIdx, + const std::size_t primitiveIdx) const; + + /* Get the accessor id for the tcoords of a primitive. + */ + std::optional getTCoordAccessorIdx(const std::size_t meshIdx, + const std::size_t primitiveIdx) const; + + void loadMesh( + std::size_t meshIdx, + ISkinnedMesh::SJoint *parentJoint) const; + + void loadNode( + const std::size_t nodeIdx, + ISkinnedMesh::SJoint *parentJoint) const; + }; + + std::optional tryParseGLTF(io::IReadFile* file); +}; + +} // namespace scene + +} // namespace irr + +#endif // __C_GLTF_MESH_FILE_LOADER_INCLUDED__ + diff --git a/src/tests/CMakeLists.txt b/irr/src/tests/CMakeLists.txt similarity index 100% rename from src/tests/CMakeLists.txt rename to irr/src/tests/CMakeLists.txt diff --git a/src/tests/assets/blender_cube.gltf b/irr/src/tests/assets/blender_cube.gltf similarity index 100% rename from src/tests/assets/blender_cube.gltf rename to irr/src/tests/assets/blender_cube.gltf diff --git a/src/tests/assets/blender_cube_matrix_transform.gltf b/irr/src/tests/assets/blender_cube_matrix_transform.gltf similarity index 100% rename from src/tests/assets/blender_cube_matrix_transform.gltf rename to irr/src/tests/assets/blender_cube_matrix_transform.gltf diff --git a/src/tests/assets/blender_cube_scaled.gltf b/irr/src/tests/assets/blender_cube_scaled.gltf similarity index 100% rename from src/tests/assets/blender_cube_scaled.gltf rename to irr/src/tests/assets/blender_cube_scaled.gltf diff --git a/src/tests/assets/empty.gltf b/irr/src/tests/assets/empty.gltf similarity index 100% rename from src/tests/assets/empty.gltf rename to irr/src/tests/assets/empty.gltf diff --git a/src/tests/assets/json_missing_brace.gltf b/irr/src/tests/assets/json_missing_brace.gltf similarity index 100% rename from src/tests/assets/json_missing_brace.gltf rename to irr/src/tests/assets/json_missing_brace.gltf diff --git a/src/tests/assets/minimal_triangle.gltf b/irr/src/tests/assets/minimal_triangle.gltf similarity index 100% rename from src/tests/assets/minimal_triangle.gltf rename to irr/src/tests/assets/minimal_triangle.gltf diff --git a/src/tests/assets/snow_man.gltf b/irr/src/tests/assets/snow_man.gltf similarity index 100% rename from src/tests/assets/snow_man.gltf rename to irr/src/tests/assets/snow_man.gltf diff --git a/src/tests/assets/triangle_with_vertex_stride.gltf b/irr/src/tests/assets/triangle_with_vertex_stride.gltf similarity index 100% rename from src/tests/assets/triangle_with_vertex_stride.gltf rename to irr/src/tests/assets/triangle_with_vertex_stride.gltf diff --git a/src/tests/assets/triangle_without_indices.gltf b/irr/src/tests/assets/triangle_without_indices.gltf similarity index 93% rename from src/tests/assets/triangle_without_indices.gltf rename to irr/src/tests/assets/triangle_without_indices.gltf index 0f798342c..1c0736ab3 100644 --- a/src/tests/assets/triangle_without_indices.gltf +++ b/irr/src/tests/assets/triangle_without_indices.gltf @@ -1,54 +1,54 @@ -{ - "scene" : 0, - "scenes" : [ - { - "nodes" : [ 0 ] - } - ], - - "nodes" : [ - { - "mesh" : 0 - } - ], - - "meshes" : [ - { - "primitives" : [ { - "attributes" : { - "POSITION" : 0 - } - } ] - } - ], - - "buffers" : [ - { - "uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA", - "byteLength" : 36 - } - ], - "bufferViews" : [ - { - "buffer" : 0, - "byteOffset" : 0, - "byteLength" : 36, - "target" : 34962 - } - ], - "accessors" : [ - { - "bufferView" : 0, - "byteOffset" : 0, - "componentType" : 5126, - "count" : 3, - "type" : "VEC3", - "max" : [ 1.0, 1.0, 0.0 ], - "min" : [ 0.0, 0.0, 0.0 ] - } - ], - - "asset" : { - "version" : "2.0" - } +{ + "scene" : 0, + "scenes" : [ + { + "nodes" : [ 0 ] + } + ], + + "nodes" : [ + { + "mesh" : 0 + } + ], + + "meshes" : [ + { + "primitives" : [ { + "attributes" : { + "POSITION" : 0 + } + } ] + } + ], + + "buffers" : [ + { + "uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA", + "byteLength" : 36 + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteOffset" : 0, + "byteLength" : 36, + "target" : 34962 + } + ], + "accessors" : [ + { + "bufferView" : 0, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 3, + "type" : "VEC3", + "max" : [ 1.0, 1.0, 0.0 ], + "min" : [ 0.0, 0.0, 0.0 ] + } + ], + + "asset" : { + "version" : "2.0" + } } \ No newline at end of file diff --git a/src/tests/testCGLTFMeshFileLoader.cpp b/irr/src/tests/testCGLTFMeshFileLoader.cpp similarity index 97% rename from src/tests/testCGLTFMeshFileLoader.cpp rename to irr/src/tests/testCGLTFMeshFileLoader.cpp index eb0129060..c70c03a24 100644 --- a/src/tests/testCGLTFMeshFileLoader.cpp +++ b/irr/src/tests/testCGLTFMeshFileLoader.cpp @@ -1,334 +1,334 @@ -#include "CReadFile.h" -#include "vector3d.h" - -#include - -// Catch needs to be included after Irrlicht so that it sees operator<< -// declarations. -#define CATCH_CONFIG_MAIN -#include - -#include - -using namespace std; - -class ScopedMesh -{ -public: - ScopedMesh(irr::io::IReadFile* file) - : m_device { irr::createDevice(irr::video::EDT_NULL) } - , m_mesh { nullptr } - { - auto* smgr = m_device->getSceneManager(); - m_mesh = smgr->getMesh(file); - } - - ScopedMesh(const irr::io::path& filepath) - : m_device { irr::createDevice(irr::video::EDT_NULL) } - , m_mesh { nullptr } - { - auto* smgr = m_device->getSceneManager(); - irr::io::CReadFile f = irr::io::CReadFile(filepath); - m_mesh = smgr->getMesh(&f); - } - - ~ScopedMesh() - { - m_device->drop(); - m_mesh = nullptr; - } - - const irr::scene::IAnimatedMesh* getMesh() const - { - return m_mesh; - } - -private: - irr::IrrlichtDevice* m_device; - irr::scene::IAnimatedMesh* m_mesh; -}; - -TEST_CASE("load empty gltf file") { - ScopedMesh sm("source/Irrlicht/tests/assets/empty.gltf"); - CHECK(sm.getMesh() == nullptr); -} - -TEST_CASE("minimal triangle") { - auto path = GENERATE( - "source/Irrlicht/tests/assets/minimal_triangle.gltf", - "source/Irrlicht/tests/assets/triangle_with_vertex_stride.gltf", - // Test non-indexed geometry. - "source/Irrlicht/tests/assets/triangle_without_indices.gltf"); - INFO(path); - ScopedMesh sm(path); - REQUIRE(sm.getMesh() != nullptr); - REQUIRE(sm.getMesh()->getMeshBufferCount() == 1); - - SECTION("vertex coordinates are correct") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 3); - const auto* vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getVertices()); - CHECK(vertices[0].Pos == irr::core::vector3df {0.0f, 0.0f, 0.0f}); - CHECK(vertices[1].Pos == irr::core::vector3df {1.0f, 0.0f, 0.0f}); - CHECK(vertices[2].Pos == irr::core::vector3df {0.0f, 1.0f, 0.0f}); - } - - SECTION("vertex indices are correct") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getIndexCount() == 3); - const auto* indices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getIndices()); - CHECK(indices[0] == 2); - CHECK(indices[1] == 1); - CHECK(indices[2] == 0); - } -} - -TEST_CASE("blender cube") { - ScopedMesh sm("source/Irrlicht/tests/assets/blender_cube.gltf"); - REQUIRE(sm.getMesh() != nullptr); - REQUIRE(sm.getMesh()->getMeshBufferCount() == 1); - SECTION("vertex coordinates are correct") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); - const auto* vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getVertices()); - CHECK(vertices[0].Pos == irr::core::vector3df{-10.0f, -10.0f, -10.0f}); - CHECK(vertices[3].Pos == irr::core::vector3df{-10.0f, 10.0f, -10.0f}); - CHECK(vertices[6].Pos == irr::core::vector3df{-10.0f, -10.0f, 10.0f}); - CHECK(vertices[9].Pos == irr::core::vector3df{-10.0f, 10.0f, 10.0f}); - CHECK(vertices[12].Pos == irr::core::vector3df{10.0f, -10.0f, -10.0f}); - CHECK(vertices[15].Pos == irr::core::vector3df{10.0f, 10.0f, -10.0f}); - CHECK(vertices[18].Pos == irr::core::vector3df{10.0f, -10.0f, 10.0f}); - CHECK(vertices[21].Pos == irr::core::vector3df{10.0f, 10.0f, 10.0f}); - } - - SECTION("vertex indices are correct") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getIndexCount() == 36); - const auto* indices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getIndices()); - CHECK(indices[0] == 16); - CHECK(indices[1] == 5); - CHECK(indices[2] == 22); - CHECK(indices[35] == 0); - } - - SECTION("vertex normals are correct") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); - const auto* vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getVertices()); - CHECK(vertices[0].Normal == irr::core::vector3df{-1.0f, 0.0f, 0.0f}); - CHECK(vertices[1].Normal == irr::core::vector3df{0.0f, -1.0f, 0.0f}); - CHECK(vertices[2].Normal == irr::core::vector3df{0.0f, 0.0f, -1.0f}); - CHECK(vertices[3].Normal == irr::core::vector3df{-1.0f, 0.0f, 0.0f}); - CHECK(vertices[6].Normal == irr::core::vector3df{-1.0f, 0.0f, 0.0f}); - CHECK(vertices[23].Normal == irr::core::vector3df{1.0f, 0.0f, 0.0f}); - - } - - SECTION("texture coords are correct") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); - const auto* vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getVertices()); - CHECK(vertices[0].TCoords == irr::core::vector2df{0.375f, 1.0f}); - CHECK(vertices[1].TCoords == irr::core::vector2df{0.125f, 0.25f}); - CHECK(vertices[2].TCoords == irr::core::vector2df{0.375f, 0.0f}); - CHECK(vertices[3].TCoords == irr::core::vector2df{0.6250f, 1.0f}); - CHECK(vertices[6].TCoords == irr::core::vector2df{0.375f, 0.75f}); - } -} - -TEST_CASE("mesh loader returns nullptr when given null file pointer") { - ScopedMesh sm(nullptr); - CHECK(sm.getMesh() == nullptr); -} - -TEST_CASE("invalid JSON returns nullptr") { - ScopedMesh sm("source/Irrlicht/tests/assets/json_missing_brace.gltf"); - CHECK(sm.getMesh() == nullptr); -} - -TEST_CASE("blender cube scaled") { - ScopedMesh sm("source/Irrlicht/tests/assets/blender_cube_scaled.gltf"); - REQUIRE(sm.getMesh() != nullptr); - REQUIRE(sm.getMesh()->getMeshBufferCount() == 1); - - SECTION("Scaling is correct") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); - const auto* vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getVertices()); - - CHECK(vertices[0].Pos == irr::core::vector3df{-150.0f, -1.0f, -21.5f}); - CHECK(vertices[3].Pos == irr::core::vector3df{-150.0f, 1.0f, -21.5f}); - CHECK(vertices[6].Pos == irr::core::vector3df{-150.0f, -1.0f, 21.5f}); - CHECK(vertices[9].Pos == irr::core::vector3df{-150.0f, 1.0f, 21.5f}); - CHECK(vertices[12].Pos == irr::core::vector3df{150.0f, -1.0f, -21.5f}); - CHECK(vertices[15].Pos == irr::core::vector3df{150.0f, 1.0f, -21.5f}); - CHECK(vertices[18].Pos == irr::core::vector3df{150.0f, -1.0f, 21.5f}); - CHECK(vertices[21].Pos == irr::core::vector3df{150.0f, 1.0f, 21.5f}); - } -} - -TEST_CASE("blender cube matrix transform") { - ScopedMesh sm("source/Irrlicht/tests/assets/blender_cube_matrix_transform.gltf"); - REQUIRE(sm.getMesh() != nullptr); - REQUIRE(sm.getMesh()->getMeshBufferCount() == 1); - - SECTION("Transformation is correct") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); - const auto* vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getVertices()); - const auto checkVertex = [&](const std::size_t i, irr::core::vector3df vec) { - // The transform scales by (1, 2, 3) and translates by (4, 5, 6). - CHECK(vertices[i].Pos == vec * irr::core::vector3df{1, 2, 3} - // The -6 is due to the coordinate system conversion. - + irr::core::vector3df{4, 5, -6}); - }; - checkVertex(0, irr::core::vector3df{-1, -1, -1}); - checkVertex(3, irr::core::vector3df{-1, 1, -1}); - checkVertex(6, irr::core::vector3df{-1, -1, 1}); - checkVertex(9, irr::core::vector3df{-1, 1, 1}); - checkVertex(12, irr::core::vector3df{1, -1, -1}); - checkVertex(15, irr::core::vector3df{1, 1, -1}); - checkVertex(18, irr::core::vector3df{1, -1, 1}); - checkVertex(21, irr::core::vector3df{1, 1, 1}); - } -} - -TEST_CASE("snow man") { - ScopedMesh sm("source/Irrlicht/tests/assets/snow_man.gltf"); - REQUIRE(sm.getMesh() != nullptr); - REQUIRE(sm.getMesh()->getMeshBufferCount() == 3); - - SECTION("vertex coordinates are correct for all buffers") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); - const auto* vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getVertices()); - - CHECK(vertices[0].Pos == irr::core::vector3df{3.0f, 24.0f, -3.0f}); - CHECK(vertices[3].Pos == irr::core::vector3df{3.0f, 18.0f, 3.0f}); - CHECK(vertices[6].Pos == irr::core::vector3df{-3.0f, 18.0f, -3.0f}); - CHECK(vertices[9].Pos == irr::core::vector3df{3.0f, 24.0f, 3.0f}); - CHECK(vertices[12].Pos == irr::core::vector3df{3.0f, 18.0f, -3.0f}); - CHECK(vertices[15].Pos == irr::core::vector3df{-3.0f, 18.0f, 3.0f}); - CHECK(vertices[18].Pos == irr::core::vector3df{3.0f, 18.0f, -3.0f}); - CHECK(vertices[21].Pos == irr::core::vector3df{3.0f, 18.0f, 3.0f}); - - vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(1)->getVertices()); - - CHECK(vertices[2].Pos == irr::core::vector3df{5.0f, 10.0f, 5.0f}); - CHECK(vertices[3].Pos == irr::core::vector3df{5.0f, 0.0f, 5.0f}); - CHECK(vertices[7].Pos == irr::core::vector3df{-5.0f, 0.0f, 5.0f}); - CHECK(vertices[8].Pos == irr::core::vector3df{5.0f, 10.0f, -5.0f}); - CHECK(vertices[14].Pos == irr::core::vector3df{5.0f, 0.0f, 5.0f}); - CHECK(vertices[16].Pos == irr::core::vector3df{5.0f, 10.0f, -5.0f}); - CHECK(vertices[22].Pos == irr::core::vector3df{-5.0f, 10.0f, 5.0f}); - CHECK(vertices[23].Pos == irr::core::vector3df{-5.0f, 0.0f, 5.0f}); - - vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(2)->getVertices()); - - CHECK(vertices[1].Pos == irr::core::vector3df{4.0f, 10.0f, -4.0f}); - CHECK(vertices[2].Pos == irr::core::vector3df{4.0f, 18.0f, 4.0f}); - CHECK(vertices[3].Pos == irr::core::vector3df{4.0f, 10.0f, 4.0f}); - CHECK(vertices[10].Pos == irr::core::vector3df{-4.0f, 18.0f, -4.0f}); - CHECK(vertices[11].Pos == irr::core::vector3df{-4.0f, 18.0f, 4.0f}); - CHECK(vertices[12].Pos == irr::core::vector3df{4.0f, 10.0f, -4.0f}); - CHECK(vertices[17].Pos == irr::core::vector3df{-4.0f, 18.0f, -4.0f}); - CHECK(vertices[18].Pos == irr::core::vector3df{4.0f, 10.0f, -4.0f}); - } - - SECTION("vertex indices are correct for all buffers") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getIndexCount() == 36); - const auto* indices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getIndices()); - CHECK(indices[0] == 23); - CHECK(indices[1] == 21); - CHECK(indices[2] == 22); - CHECK(indices[35] == 2); - - REQUIRE(sm.getMesh()->getMeshBuffer(1)->getIndexCount() == 36); - indices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(1)->getIndices()); - CHECK(indices[10] == 16); - CHECK(indices[11] == 18); - CHECK(indices[15] == 13); - CHECK(indices[27] == 5); - - REQUIRE(sm.getMesh()->getMeshBuffer(1)->getIndexCount() == 36); - indices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(2)->getIndices()); - CHECK(indices[26] == 6); - CHECK(indices[27] == 5); - CHECK(indices[29] == 6); - CHECK(indices[32] == 2); - } - - - SECTION("vertex normals are correct for all buffers") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); - const auto* vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getVertices()); - CHECK(vertices[0].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); - CHECK(vertices[1].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); - CHECK(vertices[2].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); - CHECK(vertices[3].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); - CHECK(vertices[6].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); - CHECK(vertices[23].Normal == irr::core::vector3df{0.0f, 0.0f, 1.0f}); - - vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(1)->getVertices()); - - CHECK(vertices[0].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); - CHECK(vertices[1].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); - CHECK(vertices[3].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); - CHECK(vertices[6].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); - CHECK(vertices[7].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); - CHECK(vertices[22].Normal == irr::core::vector3df{0.0f, 0.0f, 1.0f}); - - - vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(2)->getVertices()); - - CHECK(vertices[3].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); - CHECK(vertices[4].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); - CHECK(vertices[5].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); - CHECK(vertices[10].Normal == irr::core::vector3df{0.0f, 1.0f, -0.0f}); - CHECK(vertices[11].Normal == irr::core::vector3df{0.0f, 1.0f, -0.0f}); - CHECK(vertices[19].Normal == irr::core::vector3df{0.0f, 0.0f, -1.0f}); - - } - - - SECTION("texture coords are correct for all buffers") { - REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); - const auto* vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(0)->getVertices()); - - CHECK(vertices[0].TCoords == irr::core::vector2df{0.583333, 0.791667}); - CHECK(vertices[1].TCoords == irr::core::vector2df{0.583333, 0.666667}); - CHECK(vertices[2].TCoords == irr::core::vector2df{0.708333, 0.791667}); - CHECK(vertices[5].TCoords == irr::core::vector2df{0.375, 0.416667}); - CHECK(vertices[6].TCoords == irr::core::vector2df{0.5, 0.291667}); - CHECK(vertices[19].TCoords == irr::core::vector2df{0.708333, 0.75}); - - vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(1)->getVertices()); - - CHECK(vertices[1].TCoords == irr::core::vector2df{0, 0.791667}); - CHECK(vertices[4].TCoords == irr::core::vector2df{0.208333, 0.791667}); - CHECK(vertices[5].TCoords == irr::core::vector2df{0, 0.791667}); - CHECK(vertices[6].TCoords == irr::core::vector2df{0.208333, 0.583333}); - CHECK(vertices[12].TCoords == irr::core::vector2df{0.416667, 0.791667}); - CHECK(vertices[15].TCoords == irr::core::vector2df{0.208333, 0.583333}); - - vertices = reinterpret_cast( - sm.getMesh()->getMeshBuffer(2)->getVertices()); - - CHECK(vertices[10].TCoords == irr::core::vector2df{0.375, 0.416667}); - CHECK(vertices[11].TCoords == irr::core::vector2df{0.375, 0.583333}); - CHECK(vertices[12].TCoords == irr::core::vector2df{0.708333, 0.625}); - CHECK(vertices[17].TCoords == irr::core::vector2df{0.541667, 0.458333}); - CHECK(vertices[20].TCoords == irr::core::vector2df{0.208333, 0.416667}); - CHECK(vertices[22].TCoords == irr::core::vector2df{0.375, 0.416667}); - } -} +#include "CReadFile.h" +#include "vector3d.h" + +#include + +// Catch needs to be included after Irrlicht so that it sees operator<< +// declarations. +#define CATCH_CONFIG_MAIN +#include + +#include + +using namespace std; + +class ScopedMesh +{ +public: + ScopedMesh(irr::io::IReadFile* file) + : m_device { irr::createDevice(irr::video::EDT_NULL) } + , m_mesh { nullptr } + { + auto* smgr = m_device->getSceneManager(); + m_mesh = smgr->getMesh(file); + } + + ScopedMesh(const irr::io::path& filepath) + : m_device { irr::createDevice(irr::video::EDT_NULL) } + , m_mesh { nullptr } + { + auto* smgr = m_device->getSceneManager(); + irr::io::CReadFile f = irr::io::CReadFile(filepath); + m_mesh = smgr->getMesh(&f); + } + + ~ScopedMesh() + { + m_device->drop(); + m_mesh = nullptr; + } + + const irr::scene::IAnimatedMesh* getMesh() const + { + return m_mesh; + } + +private: + irr::IrrlichtDevice* m_device; + irr::scene::IAnimatedMesh* m_mesh; +}; + +TEST_CASE("load empty gltf file") { + ScopedMesh sm("source/Irrlicht/tests/assets/empty.gltf"); + CHECK(sm.getMesh() == nullptr); +} + +TEST_CASE("minimal triangle") { + auto path = GENERATE( + "source/Irrlicht/tests/assets/minimal_triangle.gltf", + "source/Irrlicht/tests/assets/triangle_with_vertex_stride.gltf", + // Test non-indexed geometry. + "source/Irrlicht/tests/assets/triangle_without_indices.gltf"); + INFO(path); + ScopedMesh sm(path); + REQUIRE(sm.getMesh() != nullptr); + REQUIRE(sm.getMesh()->getMeshBufferCount() == 1); + + SECTION("vertex coordinates are correct") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 3); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Pos == irr::core::vector3df {0.0f, 0.0f, 0.0f}); + CHECK(vertices[1].Pos == irr::core::vector3df {1.0f, 0.0f, 0.0f}); + CHECK(vertices[2].Pos == irr::core::vector3df {0.0f, 1.0f, 0.0f}); + } + + SECTION("vertex indices are correct") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getIndexCount() == 3); + const auto* indices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getIndices()); + CHECK(indices[0] == 2); + CHECK(indices[1] == 1); + CHECK(indices[2] == 0); + } +} + +TEST_CASE("blender cube") { + ScopedMesh sm("source/Irrlicht/tests/assets/blender_cube.gltf"); + REQUIRE(sm.getMesh() != nullptr); + REQUIRE(sm.getMesh()->getMeshBufferCount() == 1); + SECTION("vertex coordinates are correct") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Pos == irr::core::vector3df{-10.0f, -10.0f, -10.0f}); + CHECK(vertices[3].Pos == irr::core::vector3df{-10.0f, 10.0f, -10.0f}); + CHECK(vertices[6].Pos == irr::core::vector3df{-10.0f, -10.0f, 10.0f}); + CHECK(vertices[9].Pos == irr::core::vector3df{-10.0f, 10.0f, 10.0f}); + CHECK(vertices[12].Pos == irr::core::vector3df{10.0f, -10.0f, -10.0f}); + CHECK(vertices[15].Pos == irr::core::vector3df{10.0f, 10.0f, -10.0f}); + CHECK(vertices[18].Pos == irr::core::vector3df{10.0f, -10.0f, 10.0f}); + CHECK(vertices[21].Pos == irr::core::vector3df{10.0f, 10.0f, 10.0f}); + } + + SECTION("vertex indices are correct") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getIndexCount() == 36); + const auto* indices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getIndices()); + CHECK(indices[0] == 16); + CHECK(indices[1] == 5); + CHECK(indices[2] == 22); + CHECK(indices[35] == 0); + } + + SECTION("vertex normals are correct") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Normal == irr::core::vector3df{-1.0f, 0.0f, 0.0f}); + CHECK(vertices[1].Normal == irr::core::vector3df{0.0f, -1.0f, 0.0f}); + CHECK(vertices[2].Normal == irr::core::vector3df{0.0f, 0.0f, -1.0f}); + CHECK(vertices[3].Normal == irr::core::vector3df{-1.0f, 0.0f, 0.0f}); + CHECK(vertices[6].Normal == irr::core::vector3df{-1.0f, 0.0f, 0.0f}); + CHECK(vertices[23].Normal == irr::core::vector3df{1.0f, 0.0f, 0.0f}); + + } + + SECTION("texture coords are correct") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].TCoords == irr::core::vector2df{0.375f, 1.0f}); + CHECK(vertices[1].TCoords == irr::core::vector2df{0.125f, 0.25f}); + CHECK(vertices[2].TCoords == irr::core::vector2df{0.375f, 0.0f}); + CHECK(vertices[3].TCoords == irr::core::vector2df{0.6250f, 1.0f}); + CHECK(vertices[6].TCoords == irr::core::vector2df{0.375f, 0.75f}); + } +} + +TEST_CASE("mesh loader returns nullptr when given null file pointer") { + ScopedMesh sm(nullptr); + CHECK(sm.getMesh() == nullptr); +} + +TEST_CASE("invalid JSON returns nullptr") { + ScopedMesh sm("source/Irrlicht/tests/assets/json_missing_brace.gltf"); + CHECK(sm.getMesh() == nullptr); +} + +TEST_CASE("blender cube scaled") { + ScopedMesh sm("source/Irrlicht/tests/assets/blender_cube_scaled.gltf"); + REQUIRE(sm.getMesh() != nullptr); + REQUIRE(sm.getMesh()->getMeshBufferCount() == 1); + + SECTION("Scaling is correct") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + + CHECK(vertices[0].Pos == irr::core::vector3df{-150.0f, -1.0f, -21.5f}); + CHECK(vertices[3].Pos == irr::core::vector3df{-150.0f, 1.0f, -21.5f}); + CHECK(vertices[6].Pos == irr::core::vector3df{-150.0f, -1.0f, 21.5f}); + CHECK(vertices[9].Pos == irr::core::vector3df{-150.0f, 1.0f, 21.5f}); + CHECK(vertices[12].Pos == irr::core::vector3df{150.0f, -1.0f, -21.5f}); + CHECK(vertices[15].Pos == irr::core::vector3df{150.0f, 1.0f, -21.5f}); + CHECK(vertices[18].Pos == irr::core::vector3df{150.0f, -1.0f, 21.5f}); + CHECK(vertices[21].Pos == irr::core::vector3df{150.0f, 1.0f, 21.5f}); + } +} + +TEST_CASE("blender cube matrix transform") { + ScopedMesh sm("source/Irrlicht/tests/assets/blender_cube_matrix_transform.gltf"); + REQUIRE(sm.getMesh() != nullptr); + REQUIRE(sm.getMesh()->getMeshBufferCount() == 1); + + SECTION("Transformation is correct") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + const auto checkVertex = [&](const std::size_t i, irr::core::vector3df vec) { + // The transform scales by (1, 2, 3) and translates by (4, 5, 6). + CHECK(vertices[i].Pos == vec * irr::core::vector3df{1, 2, 3} + // The -6 is due to the coordinate system conversion. + + irr::core::vector3df{4, 5, -6}); + }; + checkVertex(0, irr::core::vector3df{-1, -1, -1}); + checkVertex(3, irr::core::vector3df{-1, 1, -1}); + checkVertex(6, irr::core::vector3df{-1, -1, 1}); + checkVertex(9, irr::core::vector3df{-1, 1, 1}); + checkVertex(12, irr::core::vector3df{1, -1, -1}); + checkVertex(15, irr::core::vector3df{1, 1, -1}); + checkVertex(18, irr::core::vector3df{1, -1, 1}); + checkVertex(21, irr::core::vector3df{1, 1, 1}); + } +} + +TEST_CASE("snow man") { + ScopedMesh sm("source/Irrlicht/tests/assets/snow_man.gltf"); + REQUIRE(sm.getMesh() != nullptr); + REQUIRE(sm.getMesh()->getMeshBufferCount() == 3); + + SECTION("vertex coordinates are correct for all buffers") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + + CHECK(vertices[0].Pos == irr::core::vector3df{3.0f, 24.0f, -3.0f}); + CHECK(vertices[3].Pos == irr::core::vector3df{3.0f, 18.0f, 3.0f}); + CHECK(vertices[6].Pos == irr::core::vector3df{-3.0f, 18.0f, -3.0f}); + CHECK(vertices[9].Pos == irr::core::vector3df{3.0f, 24.0f, 3.0f}); + CHECK(vertices[12].Pos == irr::core::vector3df{3.0f, 18.0f, -3.0f}); + CHECK(vertices[15].Pos == irr::core::vector3df{-3.0f, 18.0f, 3.0f}); + CHECK(vertices[18].Pos == irr::core::vector3df{3.0f, 18.0f, -3.0f}); + CHECK(vertices[21].Pos == irr::core::vector3df{3.0f, 18.0f, 3.0f}); + + vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(1)->getVertices()); + + CHECK(vertices[2].Pos == irr::core::vector3df{5.0f, 10.0f, 5.0f}); + CHECK(vertices[3].Pos == irr::core::vector3df{5.0f, 0.0f, 5.0f}); + CHECK(vertices[7].Pos == irr::core::vector3df{-5.0f, 0.0f, 5.0f}); + CHECK(vertices[8].Pos == irr::core::vector3df{5.0f, 10.0f, -5.0f}); + CHECK(vertices[14].Pos == irr::core::vector3df{5.0f, 0.0f, 5.0f}); + CHECK(vertices[16].Pos == irr::core::vector3df{5.0f, 10.0f, -5.0f}); + CHECK(vertices[22].Pos == irr::core::vector3df{-5.0f, 10.0f, 5.0f}); + CHECK(vertices[23].Pos == irr::core::vector3df{-5.0f, 0.0f, 5.0f}); + + vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(2)->getVertices()); + + CHECK(vertices[1].Pos == irr::core::vector3df{4.0f, 10.0f, -4.0f}); + CHECK(vertices[2].Pos == irr::core::vector3df{4.0f, 18.0f, 4.0f}); + CHECK(vertices[3].Pos == irr::core::vector3df{4.0f, 10.0f, 4.0f}); + CHECK(vertices[10].Pos == irr::core::vector3df{-4.0f, 18.0f, -4.0f}); + CHECK(vertices[11].Pos == irr::core::vector3df{-4.0f, 18.0f, 4.0f}); + CHECK(vertices[12].Pos == irr::core::vector3df{4.0f, 10.0f, -4.0f}); + CHECK(vertices[17].Pos == irr::core::vector3df{-4.0f, 18.0f, -4.0f}); + CHECK(vertices[18].Pos == irr::core::vector3df{4.0f, 10.0f, -4.0f}); + } + + SECTION("vertex indices are correct for all buffers") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getIndexCount() == 36); + const auto* indices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getIndices()); + CHECK(indices[0] == 23); + CHECK(indices[1] == 21); + CHECK(indices[2] == 22); + CHECK(indices[35] == 2); + + REQUIRE(sm.getMesh()->getMeshBuffer(1)->getIndexCount() == 36); + indices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(1)->getIndices()); + CHECK(indices[10] == 16); + CHECK(indices[11] == 18); + CHECK(indices[15] == 13); + CHECK(indices[27] == 5); + + REQUIRE(sm.getMesh()->getMeshBuffer(1)->getIndexCount() == 36); + indices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(2)->getIndices()); + CHECK(indices[26] == 6); + CHECK(indices[27] == 5); + CHECK(indices[29] == 6); + CHECK(indices[32] == 2); + } + + + SECTION("vertex normals are correct for all buffers") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); + CHECK(vertices[1].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); + CHECK(vertices[2].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); + CHECK(vertices[3].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); + CHECK(vertices[6].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[23].Normal == irr::core::vector3df{0.0f, 0.0f, 1.0f}); + + vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(1)->getVertices()); + + CHECK(vertices[0].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); + CHECK(vertices[1].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); + CHECK(vertices[3].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); + CHECK(vertices[6].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[7].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[22].Normal == irr::core::vector3df{0.0f, 0.0f, 1.0f}); + + + vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(2)->getVertices()); + + CHECK(vertices[3].Normal == irr::core::vector3df{1.0f, 0.0f, -0.0f}); + CHECK(vertices[4].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[5].Normal == irr::core::vector3df{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[10].Normal == irr::core::vector3df{0.0f, 1.0f, -0.0f}); + CHECK(vertices[11].Normal == irr::core::vector3df{0.0f, 1.0f, -0.0f}); + CHECK(vertices[19].Normal == irr::core::vector3df{0.0f, 0.0f, -1.0f}); + + } + + + SECTION("texture coords are correct for all buffers") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + + CHECK(vertices[0].TCoords == irr::core::vector2df{0.583333, 0.791667}); + CHECK(vertices[1].TCoords == irr::core::vector2df{0.583333, 0.666667}); + CHECK(vertices[2].TCoords == irr::core::vector2df{0.708333, 0.791667}); + CHECK(vertices[5].TCoords == irr::core::vector2df{0.375, 0.416667}); + CHECK(vertices[6].TCoords == irr::core::vector2df{0.5, 0.291667}); + CHECK(vertices[19].TCoords == irr::core::vector2df{0.708333, 0.75}); + + vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(1)->getVertices()); + + CHECK(vertices[1].TCoords == irr::core::vector2df{0, 0.791667}); + CHECK(vertices[4].TCoords == irr::core::vector2df{0.208333, 0.791667}); + CHECK(vertices[5].TCoords == irr::core::vector2df{0, 0.791667}); + CHECK(vertices[6].TCoords == irr::core::vector2df{0.208333, 0.583333}); + CHECK(vertices[12].TCoords == irr::core::vector2df{0.416667, 0.791667}); + CHECK(vertices[15].TCoords == irr::core::vector2df{0.208333, 0.583333}); + + vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(2)->getVertices()); + + CHECK(vertices[10].TCoords == irr::core::vector2df{0.375, 0.416667}); + CHECK(vertices[11].TCoords == irr::core::vector2df{0.375, 0.583333}); + CHECK(vertices[12].TCoords == irr::core::vector2df{0.708333, 0.625}); + CHECK(vertices[17].TCoords == irr::core::vector2df{0.541667, 0.458333}); + CHECK(vertices[20].TCoords == irr::core::vector2df{0.208333, 0.416667}); + CHECK(vertices[22].TCoords == irr::core::vector2df{0.375, 0.416667}); + } +}