diff --git a/irr/CMakeLists.txt b/irr/CMakeLists.txt index ccc00f271..84d8966f5 100644 --- a/irr/CMakeLists.txt +++ b/irr/CMakeLists.txt @@ -11,6 +11,14 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type: Debug or Release" FORCE) endif() +include(FetchContent) +FetchContent_Declare( + tiniergltf + GIT_REPOSITORY https://github.com/appgurueu/tiniergltf.git + GIT_TAG 05572e691ecf8bc3fce2da76251e73779b3577e6 +) +FetchContent_MakeAvailable(tiniergltf) + # FIXME: tests need to be moved to MT if we want to keep them list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") diff --git a/irr/include/IMesh.h b/irr/include/IMesh.h index 6d06eb762..0b458a7f2 100644 --- a/irr/include/IMesh.h +++ b/irr/include/IMesh.h @@ -52,6 +52,9 @@ enum E_ANIMATED_MESH_TYPE //! Halflife MDL model file EAMT_MDL_HALFLIFE, + //! Graphics Language Transmission Format 2.0 (.gltf) mesh + EAMT_GLTF2, + //! generic skinned mesh EAMT_SKINNED, diff --git a/irr/include/SSkinMeshBuffer.h b/irr/include/SSkinMeshBuffer.h index 5ced6057d..47ec4b2b6 100644 --- a/irr/include/SSkinMeshBuffer.h +++ b/irr/include/SSkinMeshBuffer.h @@ -301,7 +301,26 @@ struct SSkinMeshBuffer : public IMeshBuffer } //! append the vertices and indices to the current buffer - void append(const void *const vertices, u32 numVertices, const u16 *const indices, u32 numIndices) override {} + void append(const void* const vertices, u32 numVertices, const u16* const indices, u32 numIndices) override { + if (vertices == getVertices()) + throw std::logic_error("can't append own vertices"); + + if (VertexType != video::EVT_STANDARD) + throw std::logic_error("invalid vertex type"); + + const u32 prevVertexCount = getVertexCount(); + + Vertices_Standard.reallocate(prevVertexCount + numVertices); + for (u32 i=0; i < numVertices; ++i) { + Vertices_Standard.push_back(static_cast(vertices)[i]); + BoundingBox.addInternalPoint(static_cast(vertices)[i].Pos); + } + + Indices.reallocate(getIndexCount() + numIndices); + for (u32 i=0; i < numIndices; ++i) { + Indices.push_back(indices[i] + prevVertexCount); + } + } //! get the current hardware mapping hint for vertex buffers E_HARDWARE_MAPPING getHardwareMappingHint_Vertex() const override diff --git a/irr/src/CGLTFMeshFileLoader.cpp b/irr/src/CGLTFMeshFileLoader.cpp new file mode 100644 index 000000000..8747cb8bb --- /dev/null +++ b/irr/src/CGLTFMeshFileLoader.cpp @@ -0,0 +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 + diff --git a/irr/src/CGLTFMeshFileLoader.h b/irr/src/CGLTFMeshFileLoader.h new file mode 100644 index 000000000..e258db8b8 --- /dev/null +++ b/irr/src/CGLTFMeshFileLoader.h @@ -0,0 +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__ + diff --git a/irr/src/CMakeLists.txt b/irr/src/CMakeLists.txt index 10a841669..763c33b0d 100644 --- a/irr/src/CMakeLists.txt +++ b/irr/src/CMakeLists.txt @@ -15,7 +15,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$") set(CMAKE_CXX_FLAGS_RELEASE "-O3") set(CMAKE_CXX_FLAGS_DEBUG "-g") - add_compile_options(-Wall -pipe -fno-exceptions) + add_compile_options(-Wall -pipe -fno-rtti) # Enable SSE for floating point math on 32-bit x86 by default # reasoning see minetest issue #11810 and https://gcc.gnu.org/wiki/FloatingPointMath @@ -314,6 +314,7 @@ set(link_includes set(IRRMESHLOADER CB3DMeshFileLoader.cpp + CGLTFMeshFileLoader.cpp COBJMeshFileLoader.cpp CXMeshFileLoader.cpp ) @@ -326,6 +327,8 @@ add_library(IRRMESHOBJ OBJECT ${IRRMESHLOADER} ) +target_link_libraries(IRRMESHOBJ PUBLIC tiniergltf::tiniergltf) + add_library(IRROBJ OBJECT CBillboardSceneNode.cpp CCameraSceneNode.cpp @@ -337,6 +340,8 @@ add_library(IRROBJ OBJECT CMeshCache.cpp ) +target_link_libraries(IRROBJ PRIVATE IRRMESHOBJ) + set(IRRDRVROBJ CNullDriver.cpp CGLXManager.cpp @@ -501,6 +506,7 @@ target_include_directories(IrrlichtMt # this needs to be here and not in a variable (like link_includes) due to issues # with the generator expressions on at least CMake 3.22, but not 3.28 or later target_link_libraries(IrrlichtMt PRIVATE + tiniergltf::tiniergltf ${ZLIB_LIBRARY} ${JPEG_LIBRARY} ${PNG_LIBRARY} diff --git a/irr/src/CSceneManager.cpp b/irr/src/CSceneManager.cpp index b20f6010a..568de040a 100644 --- a/irr/src/CSceneManager.cpp +++ b/irr/src/CSceneManager.cpp @@ -18,6 +18,7 @@ #include "CXMeshFileLoader.h" #include "COBJMeshFileLoader.h" #include "CB3DMeshFileLoader.h" +#include "CGLTFMeshFileLoader.h" #include "CBillboardSceneNode.h" #include "CAnimatedMeshSceneNode.h" #include "CCameraSceneNode.h" @@ -76,6 +77,7 @@ CSceneManager::CSceneManager(video::IVideoDriver *driver, MeshLoaderList.push_back(new CXMeshFileLoader(this)); MeshLoaderList.push_back(new COBJMeshFileLoader(this)); MeshLoaderList.push_back(new CB3DMeshFileLoader(this)); + MeshLoaderList.push_back(new CGLTFMeshFileLoader()); } //! destructor diff --git a/irr/src/tests/CMakeLists.txt b/irr/src/tests/CMakeLists.txt new file mode 100644 index 000000000..811b0c122 --- /dev/null +++ b/irr/src/tests/CMakeLists.txt @@ -0,0 +1,33 @@ +add_executable(tests + testCGLTFMeshFileLoader.cpp + "${PROJECT_SOURCE_DIR}/source/Irrlicht/CReadFile.cpp" +) + +set_target_properties(tests PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) + +target_compile_options(tests + PRIVATE + "$<$:-Wall>" +) + +target_include_directories(tests + PRIVATE + # For CReadFile + "${PROJECT_SOURCE_DIR}/source/Irrlicht" +) + +target_link_libraries(tests + PRIVATE + Catch2::Catch + IrrlichtMt::IrrlichtMt +) + +add_test( + NAME tests + COMMAND "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/tests" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" +) diff --git a/irr/src/tests/assets/blender_cube.gltf b/irr/src/tests/assets/blender_cube.gltf new file mode 100644 index 000000000..d3c4cc32d --- /dev/null +++ b/irr/src/tests/assets/blender_cube.gltf @@ -0,0 +1,105 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v1.7.33", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Cube", + "scale" : [ + 10, + 10, + 10 + ] + } + ], + "meshes" : [ + { + "name" : "Cube.004", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3 + } + ] + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 24, + "max" : [ + 1, + 1, + 1 + ], + "min" : [ + -1, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 24, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 24, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 36, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 288 + }, + { + "buffer" : 0, + "byteLength" : 192, + "byteOffset" : 576 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 768 + } + ], + "buffers" : [ + { + "byteLength" : 840, + "uri" : "data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA" + } + ] +} diff --git a/irr/src/tests/assets/blender_cube_matrix_transform.gltf b/irr/src/tests/assets/blender_cube_matrix_transform.gltf new file mode 100644 index 000000000..aefd46fa4 --- /dev/null +++ b/irr/src/tests/assets/blender_cube_matrix_transform.gltf @@ -0,0 +1,106 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v1.7.33", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Cube", + "matrix" : [ + 1, 0, 0, 0, + 0, 2, 0, 0, + 0, 0, 3, 0, + 4, 5, 6, 1 + ] + } + ], + "meshes" : [ + { + "name" : "Cube.004", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3 + } + ] + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 24, + "max" : [ + 1, + 1, + 1 + ], + "min" : [ + -1, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 24, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 24, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 36, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 288 + }, + { + "buffer" : 0, + "byteLength" : 192, + "byteOffset" : 576 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 768 + } + ], + "buffers" : [ + { + "byteLength" : 840, + "uri" : "data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA" + } + ] +} diff --git a/irr/src/tests/assets/blender_cube_scaled.gltf b/irr/src/tests/assets/blender_cube_scaled.gltf new file mode 100644 index 000000000..151cb5b76 --- /dev/null +++ b/irr/src/tests/assets/blender_cube_scaled.gltf @@ -0,0 +1,105 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v1.7.33", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Cube", + "scale" : [ + 150, + 1, + 21.5 + ] + } + ], + "meshes" : [ + { + "name" : "Cube.004", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3 + } + ] + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 24, + "max" : [ + 1, + 1, + 1 + ], + "min" : [ + -1, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 24, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 24, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 36, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 288 + }, + { + "buffer" : 0, + "byteLength" : 192, + "byteOffset" : 576 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 768 + } + ], + "buffers" : [ + { + "byteLength" : 840, + "uri" : "data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA" + } + ] +} diff --git a/irr/src/tests/assets/empty.gltf b/irr/src/tests/assets/empty.gltf new file mode 100644 index 000000000..e69de29bb diff --git a/irr/src/tests/assets/json_missing_brace.gltf b/irr/src/tests/assets/json_missing_brace.gltf new file mode 100644 index 000000000..98232c64f --- /dev/null +++ b/irr/src/tests/assets/json_missing_brace.gltf @@ -0,0 +1 @@ +{ diff --git a/irr/src/tests/assets/minimal_triangle.gltf b/irr/src/tests/assets/minimal_triangle.gltf new file mode 100644 index 000000000..412dbac58 --- /dev/null +++ b/irr/src/tests/assets/minimal_triangle.gltf @@ -0,0 +1,70 @@ +{ + "scene": 0, + "scenes" : [ + { + "nodes" : [ 0 ] + } + ], + + "nodes" : [ + { + "mesh" : 0 + } + ], + + "meshes" : [ + { + "primitives" : [ { + "attributes" : { + "POSITION" : 1 + }, + "indices" : 0 + } ] + } + ], + + "buffers" : [ + { + "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=", + "byteLength" : 44 + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteOffset" : 0, + "byteLength" : 6, + "target" : 34963 + }, + { + "buffer" : 0, + "byteOffset" : 8, + "byteLength" : 36, + "target" : 34962 + } + ], + "accessors" : [ + { + "bufferView" : 0, + "byteOffset" : 0, + "componentType" : 5123, + "count" : 3, + "type" : "SCALAR", + "max" : [ 2 ], + "min" : [ 0 ] + }, + { + "bufferView" : 1, + "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" + } +} diff --git a/irr/src/tests/assets/snow_man.gltf b/irr/src/tests/assets/snow_man.gltf new file mode 100644 index 000000000..d384867c3 --- /dev/null +++ b/irr/src/tests/assets/snow_man.gltf @@ -0,0 +1 @@ +{"asset":{"version":"2.0","generator":"Blockbench 4.6.0 glTF exporter"},"scenes":[{"nodes":[3],"name":"blockbench_export"}],"scene":0,"nodes":[{"name":"cube","mesh":0},{"name":"cube","mesh":1},{"name":"cube","mesh":2},{"children":[0,1,2]}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":288,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":576,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":768,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":840,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1128,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1416,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":1608,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":1680,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1968,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":2256,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":2448,"byteLength":72,"target":34963}],"buffers":[{"byteLength":2520,"uri":"data:application/octet-stream;base64,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"}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[3,24,3],"min":[-3,18,-3],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"max":[0.8333333134651184,1],"min":[0,0.25],"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":4,"componentType":5126,"count":24,"max":[5,10,5],"min":[-5,0,-5],"type":"VEC3"},{"bufferView":5,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":6,"componentType":5126,"count":24,"max":[0.625,1],"min":[0,0.375],"type":"VEC2"},{"bufferView":7,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":8,"componentType":5126,"count":24,"max":[4,18,4],"min":[-4,10,-4],"type":"VEC3"},{"bufferView":9,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":10,"componentType":5126,"count":24,"max":[0.7083333134651184,0.7916666865348816],"min":[0.2083333283662796,0.25],"type":"VEC2"},{"bufferView":11,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"}],"materials":[{"pbrMetallicRoughness":{"metallicFactor":0,"roughnessFactor":1},"alphaMode":"MASK","alphaCutoff":0.05,"doubleSided":true}],"meshes":[{"primitives":[{"mode":4,"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":4,"NORMAL":5,"TEXCOORD_0":6},"indices":7,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":8,"NORMAL":9,"TEXCOORD_0":10},"indices":11,"material":0}]}]} \ No newline at end of file diff --git a/irr/src/tests/assets/triangle_with_vertex_stride.gltf b/irr/src/tests/assets/triangle_with_vertex_stride.gltf new file mode 100644 index 000000000..fce122c8a --- /dev/null +++ b/irr/src/tests/assets/triangle_with_vertex_stride.gltf @@ -0,0 +1,71 @@ +{ + "scene": 0, + "scenes" : [ + { + "nodes" : [ 0 ] + } + ], + + "nodes" : [ + { + "mesh" : 0 + } + ], + + "meshes" : [ + { + "primitives" : [ { + "attributes" : { + "POSITION" : 1 + }, + "indices" : 0 + } ] + } + ], + + "buffers" : [ + { + "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAA=", + "byteLength" : 80 + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteOffset" : 0, + "byteLength" : 6, + "target" : 34963 + }, + { + "buffer" : 0, + "byteOffset" : 8, + "byteLength" : 36, + "byteStride" : 24, + "target" : 34962 + } + ], + "accessors" : [ + { + "bufferView" : 0, + "byteOffset" : 0, + "componentType" : 5123, + "count" : 3, + "type" : "SCALAR", + "max" : [ 2 ], + "min" : [ 0 ] + }, + { + "bufferView" : 1, + "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" + } +} diff --git a/irr/src/tests/assets/triangle_without_indices.gltf b/irr/src/tests/assets/triangle_without_indices.gltf new file mode 100644 index 000000000..1c0736ab3 --- /dev/null +++ b/irr/src/tests/assets/triangle_without_indices.gltf @@ -0,0 +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" + } +} \ No newline at end of file diff --git a/irr/src/tests/testCGLTFMeshFileLoader.cpp b/irr/src/tests/testCGLTFMeshFileLoader.cpp new file mode 100644 index 000000000..c70c03a24 --- /dev/null +++ b/irr/src/tests/testCGLTFMeshFileLoader.cpp @@ -0,0 +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}); + } +}