From dde7605158d6f375ae8ea7ab726eb15bd789549d Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Tue, 26 Jan 2021 15:56:49 +0000 Subject: [PATCH] Implement draco decoding --- code/AssetLib/glTF2/glTF2Asset.h | 167 +++++++++++----------- code/AssetLib/glTF2/glTF2Asset.inl | 217 ++++++++++++++++++++++++++--- test/unit/utglTF2ImportExport.cpp | 32 +++++ 3 files changed, 319 insertions(+), 97 deletions(-) diff --git a/code/AssetLib/glTF2/glTF2Asset.h b/code/AssetLib/glTF2/glTF2Asset.h index 94cbef2cd..7e252ab6b 100644 --- a/code/AssetLib/glTF2/glTF2Asset.h +++ b/code/AssetLib/glTF2/glTF2Asset.h @@ -376,87 +376,6 @@ struct Object { // Classes for each glTF top-level object type // -//! A typed view into a BufferView. A BufferView contains raw binary data. -//! An accessor provides a typed view into a BufferView or a subset of a BufferView -//! similar to how WebGL's vertexAttribPointer() defines an attribute in a buffer. -struct Accessor : public Object { - struct Sparse; - - Ref bufferView; //!< The ID of the bufferView. (required) - size_t byteOffset; //!< The offset relative to the start of the bufferView in bytes. (required) - ComponentType componentType; //!< The datatype of components in the attribute. (required) - size_t count; //!< The number of attributes referenced by this accessor. (required) - AttribType::Value type; //!< Specifies if the attribute is a scalar, vector, or matrix. (required) - std::vector max; //!< Maximum value of each component in this attribute. - std::vector min; //!< Minimum value of each component in this attribute. - std::unique_ptr sparse; - - unsigned int GetNumComponents(); - unsigned int GetBytesPerComponent(); - unsigned int GetElementSize(); - - inline uint8_t *GetPointer(); - - template - void ExtractData(T *&outData); - - void WriteData(size_t count, const void *src_buffer, size_t src_stride); - void WriteSparseValues(size_t count, const void *src_data, size_t src_dataStride); - void WriteSparseIndices(size_t count, const void *src_idx, size_t src_idxStride); - - //! Helper class to iterate the data - class Indexer { - friend struct Accessor; - - // This field is reported as not used, making it protectd is the easiest way to work around it without going to the bottom of what the problem is: - // ../code/glTF2/glTF2Asset.h:392:19: error: private field 'accessor' is not used [-Werror,-Wunused-private-field] - protected: - Accessor &accessor; - - private: - uint8_t *data; - size_t elemSize, stride; - - Indexer(Accessor &acc); - - public: - //! Accesses the i-th value as defined by the accessor - template - T GetValue(int i); - - //! Accesses the i-th value as defined by the accessor - inline unsigned int GetUInt(int i) { - return GetValue(i); - } - - inline bool IsValid() const { - return data != 0; - } - }; - - inline Indexer GetIndexer() { - return Indexer(*this); - } - - Accessor() {} - void Read(Value &obj, Asset &r); - - //sparse - struct Sparse { - size_t count; - ComponentType indicesType; - Ref indices; - size_t indicesByteOffset; - Ref values; - size_t valuesByteOffset; - - std::vector data; //!< Actual data, which may be defaulted to an array of zeros or the original data, with the sparse buffer view applied on top of it. - - void PopulateData(size_t numBytes, uint8_t *bytes); - void PatchData(unsigned int elementSize); - }; -}; - //! A buffer points to binary geometry, animation, or skins. struct Buffer : public Object { /********************* Types *********************/ @@ -594,6 +513,91 @@ struct BufferView : public Object { uint8_t *GetPointer(size_t accOffset); }; + +//! A typed view into a BufferView. A BufferView contains raw binary data. +//! An accessor provides a typed view into a BufferView or a subset of a BufferView +//! similar to how WebGL's vertexAttribPointer() defines an attribute in a buffer. +struct Accessor : public Object { + struct Sparse; + + Ref bufferView; //!< The ID of the bufferView. (required) + size_t byteOffset; //!< The offset relative to the start of the bufferView in bytes. (required) + ComponentType componentType; //!< The datatype of components in the attribute. (required) + size_t count; //!< The number of attributes referenced by this accessor. (required) + AttribType::Value type; //!< Specifies if the attribute is a scalar, vector, or matrix. (required) + std::vector max; //!< Maximum value of each component in this attribute. + std::vector min; //!< Minimum value of each component in this attribute. + std::unique_ptr sparse; + std::unique_ptr decodedBuffer; // Packed decoded data, returned instead of original bufferView if present + + unsigned int GetNumComponents(); + unsigned int GetBytesPerComponent(); + unsigned int GetElementSize(); + + inline uint8_t *GetPointer(); + inline size_t GetStride(); + inline size_t GetMaxByteSize(); + + template + void ExtractData(T *&outData); + + void WriteData(size_t count, const void *src_buffer, size_t src_stride); + void WriteSparseValues(size_t count, const void *src_data, size_t src_dataStride); + void WriteSparseIndices(size_t count, const void *src_idx, size_t src_idxStride); + + //! Helper class to iterate the data + class Indexer { + friend struct Accessor; + + // This field is reported as not used, making it protectd is the easiest way to work around it without going to the bottom of what the problem is: + // ../code/glTF2/glTF2Asset.h:392:19: error: private field 'accessor' is not used [-Werror,-Wunused-private-field] + protected: + Accessor &accessor; + + private: + uint8_t *data; + size_t elemSize, stride; + + Indexer(Accessor &acc); + + public: + //! Accesses the i-th value as defined by the accessor + template + T GetValue(int i); + + //! Accesses the i-th value as defined by the accessor + inline unsigned int GetUInt(int i) { + return GetValue(i); + } + + inline bool IsValid() const { + return data != 0; + } + }; + + inline Indexer GetIndexer() { + return Indexer(*this); + } + + Accessor() {} + void Read(Value &obj, Asset &r); + + //sparse + struct Sparse { + size_t count; + ComponentType indicesType; + Ref indices; + size_t indicesByteOffset; + Ref values; + size_t valuesByteOffset; + + std::vector data; //!< Actual data, which may be defaulted to an array of zeros or the original data, with the sparse buffer view applied on top of it. + + void PopulateData(size_t numBytes, uint8_t *bytes); + void PatchData(unsigned int elementSize); + }; +}; + struct Camera : public Object { enum Type { Perspective, @@ -1092,6 +1096,7 @@ public: bool KHR_materials_sheen; bool KHR_materials_clearcoat; bool KHR_materials_transmission; + bool KHR_draco_mesh_compression; } extensionsUsed; //! Keeps info about the required extensions diff --git a/code/AssetLib/glTF2/glTF2Asset.inl b/code/AssetLib/glTF2/glTF2Asset.inl index 0e265efef..53b49afe6 100644 --- a/code/AssetLib/glTF2/glTF2Asset.inl +++ b/code/AssetLib/glTF2/glTF2Asset.inl @@ -46,6 +46,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#ifdef ASSIMP_ENABLE_DRACO +#include "draco/compression/decode.h" +#include "draco/core/decoder_buffer.h" +#endif + using namespace Assimp; namespace glTF2 { @@ -175,6 +180,102 @@ inline Value *FindObject(Value &val, const char *id) { } } // namespace +#ifdef ASSIMP_ENABLE_DRACO +// Google draco library headers spew many warnings. Bad Google, no cookie +#if _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4018) // Signed/unsigned mismatch +#pragma warning(disable: 4804) // Unsafe use of type 'bool' +#endif + +template +inline void CopyFaceIndex_Draco(Buffer &decodedIndexBuffer, const draco::Mesh &draco_mesh) { + const size_t faceStride = sizeof(T) * 3; + for (draco::FaceIndex f(0); f < draco_mesh.num_faces(); ++f) { + const draco::Mesh::Face &face = draco_mesh.face(f); + T indices[3] = { static_cast(face[0].value()), static_cast(face[1].value()), static_cast(face[2].value()) }; + memcpy(decodedIndexBuffer.GetPointer() + (f.value() * faceStride), &indices[0], faceStride); + } +} + +inline void SetDecodedIndexBuffer_Draco(const draco::Mesh &dracoMesh, Mesh::Primitive &prim) { + if (!prim.indices || dracoMesh.num_faces() == 0) + return; + + // Create a decoded Index buffer (if there is one) + size_t componentBytes = prim.indices->GetBytesPerComponent(); + + auto decodedIndexBuffer = std::make_unique(); + decodedIndexBuffer->Grow(dracoMesh.num_faces() * 3 * componentBytes); + + // If accessor uses the same size as draco implementation, copy the draco buffer directly + + // Usually uint32_t but shouldn't assume + if (sizeof(dracoMesh.face(draco::FaceIndex(0))[0]) == componentBytes) { + memcpy(decodedIndexBuffer->GetPointer(), &dracoMesh.face(draco::FaceIndex(0))[0], decodedIndexBuffer->byteLength); + return; + } + + // Not same size, convert + switch (componentBytes) { + case sizeof(uint32_t): CopyFaceIndex_Draco(*decodedIndexBuffer, dracoMesh); break; + case sizeof(uint16_t): CopyFaceIndex_Draco(*decodedIndexBuffer, dracoMesh); break; + case sizeof(uint8_t): CopyFaceIndex_Draco(*decodedIndexBuffer, dracoMesh); break; + } + + // Assign this alternate data buffer to the accessor + prim.indices->decodedBuffer.swap(decodedIndexBuffer); +} + +template +static bool GetAttributeForAllPoints(const draco::Mesh &dracoMesh, + const draco::PointAttribute &dracoAttribute, + Buffer &outBuffer) { + size_t byteOffset = 0; + T values[4] = {0, 0, 0, 0}; + for (draco::PointIndex i(0); i < dracoMesh.num_points(); ++i) { + const draco::AttributeValueIndex val_index = dracoAttribute.mapped_index(i); + if (!dracoAttribute.ConvertValue(val_index, dracoAttribute.num_components(), values)) + return false; + + memcpy(outBuffer.GetPointer() + byteOffset, &values[0], sizeof(T) * dracoAttribute.num_components()); + byteOffset += sizeof(T) * dracoAttribute.num_components(); + } + + return true; +} + +inline void SetDecodedAttributeBuffer_Draco(const draco::Mesh &dracoMesh, uint32_t dracoAttribId, Accessor &accessor) { + // Create decoded buffer + const draco::PointAttribute *pDracoAttribute = dracoMesh.GetAttributeByUniqueId(dracoAttribId); + if (pDracoAttribute == nullptr) + throw DeadlyImportError("GLTF: Invalid draco attribute id: ", dracoAttribId); + + size_t componentBytes = accessor.GetBytesPerComponent(); + + auto decodedAttribBuffer = std::make_unique(); + decodedAttribBuffer->Grow(dracoMesh.num_points() * pDracoAttribute->num_components() * componentBytes); + + switch(accessor.componentType) + { + case ComponentType_BYTE: GetAttributeForAllPoints(dracoMesh, *pDracoAttribute, *decodedAttribBuffer); break; + case ComponentType_UNSIGNED_BYTE: GetAttributeForAllPoints(dracoMesh, *pDracoAttribute, *decodedAttribBuffer); break; + case ComponentType_SHORT : GetAttributeForAllPoints(dracoMesh, *pDracoAttribute, *decodedAttribBuffer); break; + case ComponentType_UNSIGNED_SHORT: GetAttributeForAllPoints(dracoMesh, *pDracoAttribute, *decodedAttribBuffer); break; + case ComponentType_UNSIGNED_INT: GetAttributeForAllPoints(dracoMesh, *pDracoAttribute, *decodedAttribBuffer); break; + case ComponentType_FLOAT : GetAttributeForAllPoints(dracoMesh, *pDracoAttribute, *decodedAttribBuffer); break; + } + + // Assign this alternate data buffer to the accessor + accessor.decodedBuffer.swap(decodedAttribBuffer); +} + +#if _MSC_VER +#pragma warning(pop) +#endif + +#endif + // // LazyDict methods // @@ -709,6 +810,9 @@ inline unsigned int Accessor::GetElementSize() { } inline uint8_t *Accessor::GetPointer() { + if (decodedBuffer) + return decodedBuffer->GetPointer(); + if (sparse) return sparse->data.data(); @@ -730,6 +834,24 @@ inline uint8_t *Accessor::GetPointer() { return basePtr + offset; } +inline size_t Accessor::GetStride() +{ + // Decoded buffer is always packed + if (decodedBuffer) + return GetElementSize(); + + // Sparse and normal bufferView + return (bufferView && bufferView->byteStride ? bufferView->byteStride : GetElementSize()); +} + +inline size_t Accessor::GetMaxByteSize() +{ + if (decodedBuffer) + return decodedBuffer->byteLength; + + return (bufferView ? bufferView->byteLength : sparse->data.size()); +} + namespace { inline void CopyData(size_t count, const uint8_t *src, size_t src_stride, @@ -761,7 +883,7 @@ void Accessor::ExtractData(T *&outData) { const size_t elemSize = GetElementSize(); const size_t totalSize = elemSize * count; - const size_t stride = bufferView && bufferView->byteStride ? bufferView->byteStride : elemSize; + const size_t stride = GetStride(); const size_t targetElemSize = sizeof(T); @@ -769,7 +891,7 @@ void Accessor::ExtractData(T *&outData) { throw DeadlyImportError("GLTF: elemSize ", elemSize, " > targetElemSize ", targetElemSize, " in ", getContextForErrorMessages(id, name)); } - const size_t maxSize = (bufferView ? bufferView->byteLength : sparse->data.size()); + const size_t maxSize = GetMaxByteSize(); if (count*stride > maxSize) { throw DeadlyImportError("GLTF: count*stride ", (count * stride), " > maxSize ", maxSize, " in ", getContextForErrorMessages(id, name)); } @@ -828,14 +950,14 @@ inline Accessor::Indexer::Indexer(Accessor &acc) : accessor(acc), data(acc.GetPointer()), elemSize(acc.GetElementSize()), - stride(acc.bufferView && acc.bufferView->byteStride ? acc.bufferView->byteStride : elemSize) { + stride(acc.GetStride()) { } //! Accesses the i-th value as defined by the accessor template T Accessor::Indexer::GetValue(int i) { ai_assert(data); - ai_assert(i * stride < accessor.bufferView->byteLength); + ai_assert(i * stride < accessor.GetMaxByteSize()); // Ensure that the memcpy doesn't overwrite the local. const size_t sizeToCopy = std::min(elemSize, sizeof(T)); T value = T(); @@ -1192,6 +1314,14 @@ inline void Mesh::Read(Value &pJSON_Object, Asset &pAsset_Root) { Primitive &prim = this->primitives[i]; prim.mode = MemberOrDefault(primitive, "mode", PrimitiveMode_TRIANGLES); + if (Value *indices = FindUInt(primitive, "indices")) { + prim.indices = pAsset_Root.accessors.Retrieve(indices->GetUint()); + } + + if (Value *material = FindUInt(primitive, "material")) { + prim.material = pAsset_Root.materials.Retrieve(material->GetUint()); + } + if (Value *attrs = FindObject(primitive, "attributes")) { for (Value::MemberIterator it = attrs->MemberBegin(); it != attrs->MemberEnd(); ++it) { if (!it->value.IsUint()) continue; @@ -1200,11 +1330,11 @@ inline void Mesh::Read(Value &pJSON_Object, Asset &pAsset_Root) { // and WEIGHT.Attribute semantics can be of the form[semantic]_[set_index], e.g., TEXCOORD_0, TEXCOORD_1, etc. int undPos = 0; - Mesh::AccessorList *vec = 0; + Mesh::AccessorList *vec = nullptr; if (GetAttribVector(prim, attr, vec, undPos)) { size_t idx = (attr[undPos] == '_') ? atoi(attr + undPos + 1) : 0; if ((*vec).size() != idx) { - throw DeadlyImportError("GLTF: Invalid attribute: ", attr, ". All indices for indexed attribute semantics must start with 0 and be continuous positive integers: TEXCOORD_0, TEXCOORD_1, etc."); + throw DeadlyImportError("GLTF: Invalid attribute in mesh: ", name, " primitive: ", i,"attrib: ", attr, ". All indices for indexed attribute semantics must start with 0 and be continuous positive integers: TEXCOORD_0, TEXCOORD_1, etc."); } (*vec).resize(idx + 1); (*vec)[idx] = pAsset_Root.accessors.Retrieve(it->value.GetUint()); @@ -1212,6 +1342,66 @@ inline void Mesh::Read(Value &pJSON_Object, Asset &pAsset_Root) { } } +#ifdef ASSIMP_ENABLE_DRACO + // KHR_draco_mesh_compression spec: Draco can only be used for glTF Triangles or Triangle Strips + if (pAsset_Root.extensionsUsed.KHR_draco_mesh_compression && (prim.mode == PrimitiveMode_TRIANGLES || prim.mode == PrimitiveMode_TRIANGLE_STRIP)) { + // Look for draco mesh compression extension and bufferView + // Skip if any missing + if (Value *exts = FindObject(primitive, "extensions")) { + if (Value *dracoExt = FindObject(*exts, "KHR_draco_mesh_compression")) { + if (Value *bufView = FindUInt(*dracoExt, "bufferView")) { + // Attempt to load indices and attributes using draco compression + auto bufferView = pAsset_Root.bufferViews.Retrieve(bufView->GetUint()); + // Attempt to perform the draco decode on the buffer data + const char *bufferViewData = reinterpret_cast(bufferView->buffer->GetPointer() + bufferView->byteOffset); + draco::DecoderBuffer decoderBuffer; + decoderBuffer.Init(bufferViewData, bufferView->byteLength); + draco::Decoder decoder; + auto decodeResult = decoder.DecodeMeshFromBuffer(&decoderBuffer); + if (!decodeResult.ok()) { + // A corrupt Draco isn't actually fatal if the primitive data is also provided in a standard buffer, but does anyone do that? + throw DeadlyImportError("GLTF: Invalid Draco mesh compression in mesh ", name, " primitive ", i, ": ", decodeResult.status().error_msg_string()); + } + + // Now we have a draco mesh + const std::unique_ptr &pDracoMesh = decodeResult.value(); + + // Redirect the accessors to the decoded data + + // Indices + SetDecodedIndexBuffer_Draco(*pDracoMesh, prim); + + // Vertex attributes + if (Value *attrs = FindObject(*dracoExt, "attributes")) { + for (Value::MemberIterator it = attrs->MemberBegin(); it != attrs->MemberEnd(); ++it) { + if (!it->value.IsUint()) continue; + const char *attr = it->name.GetString(); + + int undPos = 0; + Mesh::AccessorList *vec = nullptr; + if (GetAttribVector(prim, attr, vec, undPos)) { + size_t idx = (attr[undPos] == '_') ? atoi(attr + undPos + 1) : 0; + if (idx >= (*vec).size()) { + throw DeadlyImportError("GLTF: Invalid draco attribute in mesh: ", name, " primitive: ", i, " attrib: ", attr, ". " + "All indices for indexed attribute semantics must start with 0 and be continuous positive integers: TEXCOORD_0, TEXCOORD_1, etc."); + } + + Accessor &attribAccessor = *(*vec)[idx]; + if (attribAccessor.count == 0) + throw DeadlyImportError("GLTF: Invalid draco attribute in mesh: ", name, " primitive: ", i, " attrib: ", attr); + + // Redirect this accessor to the appropriate Draco vertex attribute data + const uint32_t dracoAttribId = it->value.GetUint(); + SetDecodedAttributeBuffer_Draco(*pDracoMesh, dracoAttribId, attribAccessor); + } + } + } + } + } + } + } +#endif + Value *targetsArray = FindArray(primitive, "targets"); if (nullptr != targetsArray) { prim.targets.resize(targetsArray->Size()); @@ -1238,14 +1428,6 @@ inline void Mesh::Read(Value &pJSON_Object, Asset &pAsset_Root) { } } } - - if (Value *indices = FindUInt(primitive, "indices")) { - prim.indices = pAsset_Root.accessors.Retrieve(indices->GetUint()); - } - - if (Value *material = FindUInt(primitive, "material")) { - prim.material = pAsset_Root.materials.Retrieve(material->GetUint()); - } } } @@ -1693,10 +1875,12 @@ inline void Asset::Load(const std::string &pFile, bool isBinary) { ReadExtensionsUsed(doc); ReadExtensionsRequired(doc); - // Currently Draco is not supported +#ifndef ASSIMP_ENABLE_DRACO + // Is Draco supported? if (extensionsRequired.KHR_draco_mesh_compression) { - throw DeadlyImportError("GLTF: Draco mesh compression not currently supported."); + throw DeadlyImportError("GLTF: Draco mesh compression not supported."); } +#endif // Prepare the dictionaries for (size_t i = 0; i < mDicts.size(); ++i) { @@ -1784,6 +1968,7 @@ inline void Asset::ReadExtensionsUsed(Document &doc) { CHECK_EXT(KHR_materials_sheen); CHECK_EXT(KHR_materials_clearcoat); CHECK_EXT(KHR_materials_transmission); + CHECK_EXT(KHR_draco_mesh_compression); #undef CHECK_EXT } diff --git a/test/unit/utglTF2ImportExport.cpp b/test/unit/utglTF2ImportExport.cpp index e991b04f3..c49a96f40 100644 --- a/test/unit/utglTF2ImportExport.cpp +++ b/test/unit/utglTF2ImportExport.cpp @@ -574,3 +574,35 @@ TEST_F(utglTF2ImportExport, allIndicesOutOfRange) { std::string error = importer.GetErrorString(); ASSERT_NE(error.find("Mesh \"Mesh\" has no faces"), std::string::npos); } + +///////////////////////////////// +// Draco decoding +#ifdef ASSIMP_ENABLE_DRACO + +TEST_F(utglTF2ImportExport, import_dracoEncoded) { + Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/glTF2/draco/2CylinderEngine.gltf", + aiProcess_ValidateDataStructure); + ASSERT_NE(scene, nullptr); + ASSERT_NE(scene->mMetaData, nullptr); + { + ASSERT_TRUE(scene->mMetaData->HasKey(AI_METADATA_SOURCE_FORMAT)); + aiString format; + ASSERT_TRUE(scene->mMetaData->Get(AI_METADATA_SOURCE_FORMAT, format)); + ASSERT_EQ(strcmp(format.C_Str(), "glTF2 Importer"), 0); + } + { + ASSERT_TRUE(scene->mMetaData->HasKey(AI_METADATA_SOURCE_FORMAT_VERSION)); + aiString version; + ASSERT_TRUE(scene->mMetaData->Get(AI_METADATA_SOURCE_FORMAT_VERSION, version)); + ASSERT_EQ(strcmp(version.C_Str(), "2.0"), 0); + } + { + ASSERT_TRUE(scene->mMetaData->HasKey(AI_METADATA_SOURCE_GENERATOR)); + aiString generator; + ASSERT_TRUE(scene->mMetaData->Get(AI_METADATA_SOURCE_GENERATOR, generator)); + ASSERT_EQ(strcmp(generator.C_Str(), "COLLADA2GLTF"), 0); + } +} + +#endif