From fa6b4ebd0479f312298fc0421eaff3089bca72e2 Mon Sep 17 00:00:00 2001 From: Powei Feng Date: Tue, 12 Mar 2024 13:31:19 -0700 Subject: [PATCH] gltfio: refactor for clarity (#7652) - Pull certain utility functions in a separate header and cpp - Refactor ResourceLoader::loadeResources into smaller methods --- android/gltfio-android/CMakeLists.txt | 2 + libs/gltfio/CMakeLists.txt | 2 + libs/gltfio/include/gltfio/ResourceLoader.h | 1 - libs/gltfio/src/AssetLoader.cpp | 31 +- libs/gltfio/src/FFilamentAsset.h | 1 + libs/gltfio/src/ResourceLoader.cpp | 564 +++++++------------- libs/gltfio/src/Utility.cpp | 265 +++++++++ libs/gltfio/src/Utility.h | 59 ++ 8 files changed, 528 insertions(+), 397 deletions(-) create mode 100644 libs/gltfio/src/Utility.cpp create mode 100644 libs/gltfio/src/Utility.h diff --git a/android/gltfio-android/CMakeLists.txt b/android/gltfio-android/CMakeLists.txt index ffac6b8c61..41b546a92d 100644 --- a/android/gltfio-android/CMakeLists.txt +++ b/android/gltfio-android/CMakeLists.txt @@ -81,6 +81,8 @@ set(GLTFIO_SRCS ${GLTFIO_DIR}/src/TangentsJob.cpp ${GLTFIO_DIR}/src/TangentsJob.h ${GLTFIO_DIR}/src/UbershaderProvider.cpp + ${GLTFIO_DIR}/src/Utility.cpp + ${GLTFIO_DIR}/src/Utility.h ${GLTFIO_DIR}/src/Wireframe.cpp ${GLTFIO_DIR}/src/Wireframe.h ${GLTFIO_DIR}/src/downcast.h diff --git a/libs/gltfio/CMakeLists.txt b/libs/gltfio/CMakeLists.txt index d12f24d259..3791525e29 100644 --- a/libs/gltfio/CMakeLists.txt +++ b/libs/gltfio/CMakeLists.txt @@ -47,6 +47,8 @@ set(SRCS src/TangentsJob.cpp src/TangentsJob.h src/UbershaderProvider.cpp + src/Utility.cpp + src/Utility.h src/Wireframe.cpp src/Wireframe.h src/downcast.h diff --git a/libs/gltfio/include/gltfio/ResourceLoader.h b/libs/gltfio/include/gltfio/ResourceLoader.h index 05c8a56d35..acccc9f86f 100644 --- a/libs/gltfio/include/gltfio/ResourceLoader.h +++ b/libs/gltfio/include/gltfio/ResourceLoader.h @@ -156,7 +156,6 @@ public: private: bool loadResources(FFilamentAsset* asset, bool async); - void normalizeSkinningWeights(FFilamentAsset* asset) const; struct Impl; Impl* pImpl; }; diff --git a/libs/gltfio/src/AssetLoader.cpp b/libs/gltfio/src/AssetLoader.cpp index 6e3df0af44..fa73eb2741 100644 --- a/libs/gltfio/src/AssetLoader.cpp +++ b/libs/gltfio/src/AssetLoader.cpp @@ -23,6 +23,7 @@ #include "FNodeManager.h" #include "FTrsTransformManager.h" #include "GltfEnums.h" +#include "Utility.h" #include #include @@ -52,7 +53,6 @@ #include -#define CGLTF_IMPLEMENTATION #include #include "downcast.h" @@ -85,21 +85,6 @@ static constexpr cgltf_material kDefaultMat = { }, }; -// Sometimes a glTF bufferview includes unused data at the end (e.g. in skinning.gltf) so we need to -// compute the correct size of the vertex buffer. Filament automatically infers the size of -// driver-level vertex buffers from the attribute data (stride, count, offset) and clients are -// expected to avoid uploading data blobs that exceed this size. Since this information doesn't -// exist in the glTF we need to compute it manually. This is a bit of a cheat, cgltf_calc_size is -// private but its implementation file is available in this cpp file. -uint32_t computeBindingSize(const cgltf_accessor* accessor) { - cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type); - return uint32_t(accessor->stride * (accessor->count - 1) + element_size); -} - -uint32_t computeBindingOffset(const cgltf_accessor* accessor) { - return uint32_t(accessor->offset + accessor->buffer_view->offset); -} - static const char* getNodeName(const cgltf_node* node, const char* defaultNodeName) { if (node->name) return node->name; if (node->mesh && node->mesh->name) return node->mesh->name; @@ -617,15 +602,15 @@ void FAssetLoader::recurseEntities(const cgltf_node* node, SceneMask scenes, Ent void FAssetLoader::createPrimitives(const cgltf_node* node, const char* name, FFilamentAsset* fAsset) { - const cgltf_data* srcAsset = fAsset->mSourceAsset->hierarchy; + cgltf_data* gltf = fAsset->mSourceAsset->hierarchy; const cgltf_mesh* mesh = node->mesh; - assert_invariant(srcAsset != nullptr); + assert_invariant(gltf != nullptr); assert_invariant(mesh != nullptr); // If the mesh is already loaded, obtain the list of Filament VertexBuffer / IndexBuffer objects // that were already generated (one for each primitive), otherwise allocate a new list of // pointers for the primitives. - FixedCapacityVector& prims = fAsset->mMeshCache[mesh - srcAsset->meshes]; + FixedCapacityVector& prims = fAsset->mMeshCache[mesh - gltf->meshes]; if (prims.empty()) { prims.reserve(mesh->primitives_count); prims.resize(mesh->primitives_count); @@ -791,6 +776,7 @@ void FAssetLoader::createMaterialVariants(const cgltf_mesh* mesh, Entity entity, bool FAssetLoader::createPrimitive(const cgltf_primitive& inPrim, const char* name, Primitive* outPrim, FFilamentAsset* fAsset) { + Material* material = getMaterial(fAsset->mSourceAsset->hierarchy, inPrim.material, &outPrim->uvmap, primitiveHasVertexColor(inPrim)); AttributeBitset requiredAttributes = material->getRequiredAttributes(); @@ -849,7 +835,7 @@ bool FAssetLoader::createPrimitive(const cgltf_primitive& inPrim, const char* na bool hasUv0 = false, hasUv1 = false, hasVertexColor = false, hasNormals = false; uint32_t vertexCount = 0; - const size_t firstSlot = fAsset->mBufferSlots.size(); + const size_t firstSlot = slots->size(); int slot = 0; for (cgltf_size aindex = 0; aindex < inPrim.attributes_count; aindex++) { @@ -891,6 +877,7 @@ bool FAssetLoader::createPrimitive(const cgltf_primitive& inPrim, const char* na utils::slog.e << "Too many joints in " << name << utils::io::endl; continue; } + if (atype == cgltf_attribute_type_texcoord) { if (index >= UvMapSize) { utils::slog.e << "Too many texture coordinate sets in " << name << utils::io::endl; @@ -1065,8 +1052,8 @@ bool FAssetLoader::createPrimitive(const cgltf_primitive& inPrim, const char* na fAsset->mPrimitives.push_back({&inPrim, vertices}); fAsset->mVertexBuffers.push_back(vertices); - for (size_t i = firstSlot; i < fAsset->mBufferSlots.size(); ++i) { - fAsset->mBufferSlots[i].vertexBuffer = vertices; + for (size_t i = firstSlot; i < slots->size(); ++i) { + (*slots)[i].vertexBuffer = vertices; } if (targetsCount > 0) { diff --git a/libs/gltfio/src/FFilamentAsset.h b/libs/gltfio/src/FFilamentAsset.h index ba101bc6e1..a29c016d24 100644 --- a/libs/gltfio/src/FFilamentAsset.h +++ b/libs/gltfio/src/FFilamentAsset.h @@ -45,6 +45,7 @@ #include "DependencyGraph.h" #include "DracoCache.h" #include "FFilamentInstance.h" +#include "Utility.h" #include diff --git a/libs/gltfio/src/ResourceLoader.cpp b/libs/gltfio/src/ResourceLoader.cpp index 9e292ae9c0..135cca7c0e 100644 --- a/libs/gltfio/src/ResourceLoader.cpp +++ b/libs/gltfio/src/ResourceLoader.cpp @@ -21,6 +21,7 @@ #include "FFilamentAsset.h" #include "TangentsJob.h" #include "downcast.h" +#include "Utility.h" #include #include @@ -50,6 +51,7 @@ #include #include #include +#include using namespace filament; using namespace filament::math; @@ -63,16 +65,16 @@ namespace filament::gltfio { using BufferTextureCache = tsl::robin_map; using FilepathTextureCache = tsl::robin_map; -using UriDataCache = tsl::robin_map; -using UriDataCacheHandle = std::shared_ptr; using TextureProviderList = tsl::robin_map; +namespace { enum class CacheResult { ERROR, NOT_READY, FOUND, MISS, }; +} // anonymous namespace struct ResourceLoader::Impl { explicit Impl(const ResourceConfiguration& config) : @@ -108,9 +110,7 @@ struct ResourceLoader::Impl { ~Impl(); }; -uint32_t computeBindingSize(const cgltf_accessor* accessor); -uint32_t computeBindingOffset(const cgltf_accessor* accessor); - +namespace { // This little struct holds a shared_ptr that wraps cgltf_data (and, potentially, glb data) while // uploading vertex buffer data to the GPU. struct UploadEvent { @@ -122,174 +122,14 @@ UploadEvent* uploadUserdata(FFilamentAsset* asset, UriDataCacheHandle dataCache) return new UploadEvent({ asset->mSourceAsset, dataCache }); } -static void uploadCallback(void* buffer, size_t size, void* user) { +void uploadCallback(void* buffer, size_t size, void* user) { auto event = (UploadEvent*) user; delete event; } -static void convertBytesToShorts(uint16_t* dst, const uint8_t* src, size_t count) { - for (size_t i = 0; i < count; ++i) { - dst[i] = src[i]; - } -} - -static bool requiresConversion(const cgltf_accessor* accessor) { - if (UTILS_UNLIKELY(accessor->is_sparse)) { - return true; - } - const cgltf_type type = accessor->type; - const cgltf_component_type ctype = accessor->component_type; - filament::VertexBuffer::AttributeType permitted; - filament::VertexBuffer::AttributeType actual; - bool supported = getElementType(type, ctype, &permitted, &actual); - return supported && permitted != actual; -} - -static bool requiresPacking(const cgltf_accessor* accessor) { - if (requiresConversion(accessor)) { - return true; - } - const size_t dim = cgltf_num_components(accessor->type); - switch (accessor->component_type) { - case cgltf_component_type_r_8: - case cgltf_component_type_r_8u: - return accessor->stride != dim; - case cgltf_component_type_r_16: - case cgltf_component_type_r_16u: - return accessor->stride != dim * 2; - case cgltf_component_type_r_32u: - case cgltf_component_type_r_32f: - return accessor->stride != dim * 4; - default: - assert_invariant(false); - return true; - } -} - -static void decodeDracoMeshes(FFilamentAsset* asset) { - DracoCache* dracoCache = &asset->mSourceAsset->dracoCache; - - // For a given primitive and attribute, find the corresponding accessor. - auto findAccessor = [](const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int idx) { - for (cgltf_size i = 0; i < prim->attributes_count; i++) { - const cgltf_attribute& attr = prim->attributes[i]; - if (attr.type == type && attr.index == idx) { - return attr.data; - } - } - return (cgltf_accessor*) nullptr; - }; - - // Go through every primitive and check if it has a Draco mesh. - for (auto& [prim, vertexBuffer] : asset->mPrimitives) { - if (!prim->has_draco_mesh_compression) { - continue; - } - - const cgltf_draco_mesh_compression& draco = prim->draco_mesh_compression; - - // If an error occurs, we can simply set the primitive's associated VertexBuffer to null. - // This does not cause a leak because it is a weak reference. - - // Check if we have already decoded this mesh. - DracoMesh* mesh = dracoCache->findOrCreateMesh(draco.buffer_view); - if (!mesh) { - slog.e << "Cannot decompress mesh, Draco decoding error." << io::endl; - vertexBuffer = nullptr; - continue; - } - - // Copy over the decompressed data, converting the data type if necessary. - if (prim->indices && !mesh->getFaceIndices(prim->indices)) { - vertexBuffer = nullptr; - continue; - } - - // Go through each attribute in the decompressed mesh. - for (cgltf_size i = 0; i < draco.attributes_count; i++) { - - // In cgltf, each Draco attribute's data pointer is an attribute id, not an accessor. - const uint32_t id = draco.attributes[i].data - asset->mSourceAsset->hierarchy->accessors; - - // Find the destination accessor; this contains the desired component type, etc. - const cgltf_attribute_type type = draco.attributes[i].type; - const cgltf_int index = draco.attributes[i].index; - cgltf_accessor* accessor = findAccessor(prim, type, index); - if (!accessor) { - slog.w << "Cannot find matching accessor for Draco id " << id << io::endl; - continue; - } - - // Copy over the decompressed data, converting the data type if necessary. - if (!mesh->getVertexAttributes(id, accessor)) { - vertexBuffer = nullptr; - break; - } - } - } -} - -static void decodeMeshoptCompression(cgltf_data* data) { - for (size_t i = 0; i < data->buffer_views_count; ++i) { - if (!data->buffer_views[i].has_meshopt_compression) { - continue; - } - - cgltf_meshopt_compression* compression = &data->buffer_views[i].meshopt_compression; - const uint8_t* source = (const uint8_t*) compression->buffer->data; - assert_invariant(source); - source += compression->offset; - - // This memory is freed by cgltf. - void* destination = malloc(compression->count * compression->stride); - assert_invariant(destination); - - UTILS_UNUSED_IN_RELEASE int error = 0; - switch (compression->mode) { - case cgltf_meshopt_compression_mode_invalid: - break; - case cgltf_meshopt_compression_mode_attributes: - error = meshopt_decodeVertexBuffer(destination, compression->count, compression->stride, - source, compression->size); - break; - case cgltf_meshopt_compression_mode_triangles: - error = meshopt_decodeIndexBuffer(destination, compression->count, compression->stride, - source, compression->size); - break; - case cgltf_meshopt_compression_mode_indices: - error = meshopt_decodeIndexSequence(destination, compression->count, compression->stride, - source, compression->size); - break; - default: - assert_invariant(false); - break; - } - assert_invariant(!error); - - switch (compression->filter) { - case cgltf_meshopt_compression_filter_none: - break; - case cgltf_meshopt_compression_filter_octahedral: - meshopt_decodeFilterOct(destination, compression->count, compression->stride); - break; - case cgltf_meshopt_compression_filter_quaternion: - meshopt_decodeFilterQuat(destination, compression->count, compression->stride); - break; - case cgltf_meshopt_compression_filter_exponential: - meshopt_decodeFilterExp(destination, compression->count, compression->stride); - break; - default: - assert_invariant(false); - break; - } - - data->buffer_views[i].data = destination; - } -} - // Parses a data URI and returns a blob that gets malloc'd in cgltf, which the caller must free. // (implementation snarfed from meshoptimizer) -static const uint8_t* parseDataUri(const char* uri, std::string* mimeType, size_t* psize) { +uint8_t const* parseDataUri(const char* uri, std::string* mimeType, size_t* psize) { if (strncmp(uri, "data:", 5) != 0) { return nullptr; } @@ -315,6 +155,165 @@ static const uint8_t* parseDataUri(const char* uri, std::string* mimeType, size_ return nullptr; } +inline void normalizeSkinningWeights(cgltf_data const* gltf) { + auto normalize = [](cgltf_accessor* data) { + if (data->type != cgltf_type_vec4 || data->component_type != cgltf_component_type_r_32f) { + slog.w << "Cannot normalize weights, unsupported attribute type." << io::endl; + return; + } + uint8_t* bytes = (uint8_t*) data->buffer_view->buffer->data; + bytes += data->offset + data->buffer_view->offset; + for (cgltf_size i = 0, n = data->count; i < n; ++i, bytes += data->stride) { + float4* weights = (float4*) bytes; + const float sum = weights->x + weights->y + weights->z + weights->w; + *weights /= sum; + } + }; + cgltf_size mcount = gltf->meshes_count; + for (cgltf_size mindex = 0; mindex < mcount; ++mindex) { + const cgltf_mesh& mesh = gltf->meshes[mindex]; + cgltf_size pcount = mesh.primitives_count; + for (cgltf_size pindex = 0; pindex < pcount; ++pindex) { + const cgltf_primitive& prim = mesh.primitives[pindex]; + cgltf_size acount = prim.attributes_count; + for (cgltf_size aindex = 0; aindex < acount; ++aindex) { + const auto& attr = prim.attributes[aindex]; + if (attr.type == cgltf_attribute_type_weights) { + normalize(attr.data); + } + } + } + } +} + +inline void createSkins(cgltf_data const* gltf, bool normalize, + utils::FixedCapacityVector& skins) { + // For each skin, optionally normalize skinning weights and store a copy of the bind matrices. + if (gltf->skins_count == 0) { + return; + } + if (normalize) { + normalizeSkinningWeights(gltf); + } + skins.reserve(gltf->skins_count); + for (cgltf_size i = 0, len = gltf->skins_count; i < len; ++i) { + const cgltf_skin& srcSkin = gltf->skins[i]; + CString name; + if (srcSkin.name) { + name = CString(srcSkin.name); + } + const cgltf_accessor* srcMatrices = srcSkin.inverse_bind_matrices; + FixedCapacityVector inverseBindMatrices(srcSkin.joints_count); + if (srcMatrices) { + uint8_t* bytes = nullptr; + uint8_t* srcBuffer = nullptr; + if (srcMatrices->buffer_view->has_meshopt_compression) { + bytes = (uint8_t*) srcMatrices->buffer_view->data; + srcBuffer = bytes + srcMatrices->offset; + } else { + bytes = (uint8_t*) srcMatrices->buffer_view->buffer->data; + srcBuffer = bytes + srcMatrices->offset + srcMatrices->buffer_view->offset; + } + assert_invariant(bytes); + memcpy((uint8_t*) inverseBindMatrices.data(), (const void*) srcBuffer, + srcSkin.joints_count * sizeof(mat4f)); + } + FFilamentAsset::Skin skin{ + .name = std::move(name), + .inverseBindMatrices = std::move(inverseBindMatrices), + }; + skins.emplace_back(std::move(skin)); + } +} + +inline void uploadBuffers(FFilamentAsset* asset, Engine& engine, + UriDataCacheHandle uriDataCache) { + // Upload VertexBuffer and IndexBuffer data to the GPU. + auto& slots = asset->mBufferSlots; + for (auto slot: slots) { + const cgltf_accessor* accessor = slot.accessor; + if (!accessor->buffer_view) { + continue; + } + const uint8_t* bufferData = nullptr; + const uint8_t* data = nullptr; + if (accessor->buffer_view->has_meshopt_compression) { + bufferData = (const uint8_t*) accessor->buffer_view->data; + data = bufferData + accessor->offset; + } else { + bufferData = (const uint8_t*) accessor->buffer_view->buffer->data; + data = utility::computeBindingOffset(accessor) + bufferData; + } + assert_invariant(bufferData); + const uint32_t size = utility::computeBindingSize(accessor); + if (slot.vertexBuffer) { + if (utility::requiresConversion(accessor)) { + const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type); + const size_t floatsByteCount = sizeof(float) * floatsCount; + float* floatsData = (float*) malloc(floatsByteCount); + cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount); + BufferObject* bo = BufferObject::Builder().size(floatsByteCount).build(engine); + asset->mBufferObjects.push_back(bo); + bo->setBuffer(engine, BufferDescriptor(floatsData, floatsByteCount, FREE_CALLBACK)); + slot.vertexBuffer->setBufferObjectAt(engine, slot.bufferIndex, bo); + continue; + } + + BufferObject* bo = BufferObject::Builder().size(size).build(engine); + asset->mBufferObjects.push_back(bo); + bo->setBuffer(engine, BufferDescriptor(data, size, uploadCallback, + uploadUserdata(asset, uriDataCache))); + slot.vertexBuffer->setBufferObjectAt(engine, slot.bufferIndex, bo); + continue; + } else if (slot.indexBuffer) { + if (accessor->component_type == cgltf_component_type_r_8u) { + const size_t size16 = size * 2; + uint16_t* data16 = (uint16_t*) malloc(size16); + utility::convertBytesToShorts(data16, data, size); + IndexBuffer::BufferDescriptor bd(data16, size16, FREE_CALLBACK); + + slot.indexBuffer->setBuffer(engine, std::move(bd)); + continue; + } + IndexBuffer::BufferDescriptor bd(data, size, uploadCallback, + uploadUserdata(asset, uriDataCache)); + slot.indexBuffer->setBuffer(engine, std::move(bd)); + continue; + } + + // If the buffer slot does not have an associated VertexBuffer or IndexBuffer, then this + // must be a morph target. + assert(slot.morphTargetBuffer); + + if (utility::requiresPacking(accessor)) { + const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type); + const size_t floatsByteCount = sizeof(float) * floatsCount; + float* floatsData = (float*) malloc(floatsByteCount); + cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount); + if (accessor->type == cgltf_type_vec3) { + slot.morphTargetBuffer->setPositionsAt(engine, slot.bufferIndex, + (const float3*) floatsData, slot.morphTargetBuffer->getVertexCount()); + } else { + slot.morphTargetBuffer->setPositionsAt(engine, slot.bufferIndex, + (const float4*) data, slot.morphTargetBuffer->getVertexCount()); + } + free(floatsData); + continue; + } + + if (accessor->type == cgltf_type_vec3) { + slot.morphTargetBuffer->setPositionsAt(engine, slot.bufferIndex, (const float3*) data, + slot.morphTargetBuffer->getVertexCount()); + } else { + assert_invariant(accessor->type == cgltf_type_vec4); + slot.morphTargetBuffer->setPositionsAt(engine, slot.bufferIndex, (const float4*) data, + slot.morphTargetBuffer->getVertexCount()); + } + } +} + +} // anonymous namespace + ResourceLoader::ResourceLoader(const ResourceConfiguration& config) : pImpl(new Impl(config)) { } ResourceLoader::~ResourceLoader() { @@ -388,6 +387,9 @@ void ResourceLoader::evictResourceData() { bool ResourceLoader::loadResources(FilamentAsset* asset) { FFilamentAsset* fasset = downcast(asset); + + // This is a workaround in case of using extended algo, please see description in + // FFilamentAsset.h return loadResources(fasset, false); } @@ -410,191 +412,37 @@ bool ResourceLoader::loadResources(FFilamentAsset* asset, bool async) { pImpl->mBufferTextureCache.clear(); pImpl->mFilepathTextureCache.clear(); - const cgltf_data* gltf = asset->mSourceAsset->hierarchy; - cgltf_options options {}; + cgltf_data const* gltf = asset->mSourceAsset->hierarchy; - // For emscripten and Android builds we supply a custom file reader callback that looks inside a - // cache of externally-supplied data blobs, rather than loading from the filesystem. + utility::loadCgltfBuffers(gltf, pImpl->mGltfPath.c_str(), pImpl->mUriDataCache); - SYSTRACE_NAME_BEGIN("Load buffers"); - #if !GLTFIO_USE_FILESYSTEM - - struct Closure { - Impl* impl; - const cgltf_data* gltf; - }; - - Closure closure = { pImpl, gltf }; - - options.file.user_data = &closure; - - options.file.read = [](const cgltf_memory_options* memoryOpts, - const cgltf_file_options* fileOpts, const char* path, cgltf_size* size, void** data) { - Closure* closure = (Closure*) fileOpts->user_data; - auto& uriDataCache = closure->impl->mUriDataCache; - - if (auto iter = uriDataCache->find(path); iter != uriDataCache->end()) { - *size = iter->second.size; - *data = iter->second.buffer; - } else { - // Even if we don't find the given resource in the cache, we still return a successful - // error code, because we allow downloads to finish after the decoding work starts. - *size = 0; - *data = 0; - } - - return cgltf_result_success; - }; - - #endif - - // Read data from the file system and base64 URIs. - cgltf_result result = cgltf_load_buffers(&options, (cgltf_data*) gltf, pImpl->mGltfPath.c_str()); - if (result != cgltf_result_success) { - slog.e << "Unable to load resources." << io::endl; - return false; - } - - SYSTRACE_NAME_END(); - - #ifndef NDEBUG - if (cgltf_validate((cgltf_data*) gltf) != cgltf_result_success) { - slog.e << "Failed cgltf validation." << io::endl; - return false; - } - #endif - // Decompress Draco meshes early on, which allows us to exploit subsequent processing such as - // tangent generation. - decodeDracoMeshes(asset); - decodeMeshoptCompression((cgltf_data*) gltf); - - // For each skin, optionally normalize skinning weights and store a copy of the bind matrices. - if (gltf->skins_count > 0) { - if (pImpl->mNormalizeSkinningWeights) { - normalizeSkinningWeights(asset); - } - asset->mSkins.reserve(gltf->skins_count); - for (cgltf_size i = 0, len = gltf->skins_count; i < len; ++i) { - const cgltf_skin& srcSkin = gltf->skins[i]; - CString name; - if (srcSkin.name) { - name = CString(srcSkin.name); - } - const cgltf_accessor* srcMatrices = srcSkin.inverse_bind_matrices; - FixedCapacityVector inverseBindMatrices(srcSkin.joints_count); - if (srcMatrices) { - uint8_t* bytes = nullptr; - uint8_t* srcBuffer = nullptr; - if (srcMatrices->buffer_view->has_meshopt_compression) { - bytes = (uint8_t*) srcMatrices->buffer_view->data; - srcBuffer = bytes + srcMatrices->offset; - } else { - bytes = (uint8_t*) srcMatrices->buffer_view->buffer->data; - srcBuffer = bytes + srcMatrices->offset + srcMatrices->buffer_view->offset; - } - assert_invariant(bytes); - memcpy((uint8_t*) inverseBindMatrices.data(), - (const void*) srcBuffer, srcSkin.joints_count * sizeof(mat4f)); - } - FFilamentAsset::Skin skin { - .name = std::move(name), - .inverseBindMatrices = std::move(inverseBindMatrices), - }; - asset->mSkins.emplace_back(std::move(skin)); - } - } - - Engine& engine = *pImpl->mEngine; - - // Upload VertexBuffer and IndexBuffer data to the GPU. - for (auto slot : asset->mBufferSlots) { - const cgltf_accessor* accessor = slot.accessor; - if (!accessor->buffer_view) { + // Decompress Draco meshes early on, which allows us to exploit subsequent processing such + // as tangent generation. + DracoCache* dracoCache = &asset->mSourceAsset->dracoCache; + auto& primitives = asset->mPrimitives; + // Go through every primitive and check if it has a Draco mesh. + for (auto& [prim, vertexBuffer]: primitives) { + if (!prim->has_draco_mesh_compression) { continue; } - const uint8_t* bufferData = nullptr; - const uint8_t* data = nullptr; - if (accessor->buffer_view->has_meshopt_compression) { - bufferData = (const uint8_t*) accessor->buffer_view->data; - data = bufferData + accessor->offset; - } else { - bufferData = (const uint8_t*) accessor->buffer_view->buffer->data; - data = computeBindingOffset(accessor) + bufferData; - } - assert_invariant(bufferData); - const uint32_t size = computeBindingSize(accessor); - if (slot.vertexBuffer) { - if (requiresConversion(accessor)) { - const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type); - const size_t floatsByteCount = sizeof(float) * floatsCount; - float* floatsData = (float*) malloc(floatsByteCount); - cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount); - BufferObject* bo = BufferObject::Builder().size(floatsByteCount).build(engine); - asset->mBufferObjects.push_back(bo); - bo->setBuffer(engine, BufferDescriptor(floatsData, floatsByteCount, FREE_CALLBACK)); - slot.vertexBuffer->setBufferObjectAt(engine, slot.bufferIndex, bo); - continue; - } - BufferObject* bo = BufferObject::Builder().size(size).build(engine); - asset->mBufferObjects.push_back(bo); - bo->setBuffer(engine, BufferDescriptor(data, size, - uploadCallback, uploadUserdata(asset, pImpl->mUriDataCache))); - slot.vertexBuffer->setBufferObjectAt(engine, slot.bufferIndex, bo); - continue; - } else if (slot.indexBuffer) { - if (accessor->component_type == cgltf_component_type_r_8u) { - const size_t size16 = size * 2; - uint16_t* data16 = (uint16_t*) malloc(size16); - convertBytesToShorts(data16, data, size); - IndexBuffer::BufferDescriptor bd(data16, size16, FREE_CALLBACK); - slot.indexBuffer->setBuffer(engine, std::move(bd)); - continue; - } - IndexBuffer::BufferDescriptor bd(data, size, uploadCallback, - uploadUserdata(asset, pImpl->mUriDataCache)); - slot.indexBuffer->setBuffer(engine, std::move(bd)); - continue; - } - - // If the buffer slot does not have an associated VertexBuffer or IndexBuffer, then this - // must be a morph target. - assert(slot.morphTargetBuffer); - - if (requiresPacking(accessor)) { - const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type); - const size_t floatsByteCount = sizeof(float) * floatsCount; - float* floatsData = (float*) malloc(floatsByteCount); - cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount); - if (accessor->type == cgltf_type_vec3) { - slot.morphTargetBuffer->setPositionsAt(engine, slot.bufferIndex, - (const float3*) floatsData, slot.morphTargetBuffer->getVertexCount()); - } else { - slot.morphTargetBuffer->setPositionsAt(engine, slot.bufferIndex, - (const float4*) data, slot.morphTargetBuffer->getVertexCount()); - } - free(floatsData); - continue; - } - - if (accessor->type == cgltf_type_vec3) { - slot.morphTargetBuffer->setPositionsAt(engine, slot.bufferIndex, - (const float3*) data, slot.morphTargetBuffer->getVertexCount()); - } else { - assert_invariant(accessor->type == cgltf_type_vec4); - slot.morphTargetBuffer->setPositionsAt(engine, slot.bufferIndex, - (const float4*) data, slot.morphTargetBuffer->getVertexCount()); - } + utility::decodeDracoMeshes(gltf, prim, dracoCache); } + utility::decodeMeshoptCompression((cgltf_data*) gltf); - // Compute surface orientation quaternions if necessary. This is similar to sparse data in that - // we need to generate the contents of a GPU buffer by processing one or more CPU buffer(s). + uploadBuffers(asset, *pImpl->mEngine, pImpl->mUriDataCache); + + // Compute surface orientation quaternions if necessary. This is similar to sparse data in + // that we need to generate the contents of a GPU buffer by processing one or more CPU + // buffer(s). pImpl->computeTangents(asset); - asset->mBufferSlots = {}; - asset->mPrimitives = {}; + asset->mBufferSlots.clear(); + asset->mPrimitives.clear(); + + createSkins(gltf, pImpl->mNormalizeSkinningWeights, asset->mSkins); // If any decoding jobs are still underway from a previous load, wait for them to finish. - for (const auto& iter : pImpl->mTextureProviders) { + for (const auto& iter: pImpl->mTextureProviders) { iter.second->waitForCompletion(); iter.second->updateQueue(); } @@ -901,36 +749,4 @@ ResourceLoader::Impl::~Impl() { } } -void ResourceLoader::normalizeSkinningWeights(FFilamentAsset* asset) const { - auto normalize = [](cgltf_accessor* data) { - if (data->type != cgltf_type_vec4 || data->component_type != cgltf_component_type_r_32f) { - slog.w << "Cannot normalize weights, unsupported attribute type." << io::endl; - return; - } - uint8_t* bytes = (uint8_t*) data->buffer_view->buffer->data; - bytes += data->offset + data->buffer_view->offset; - for (cgltf_size i = 0, n = data->count; i < n; ++i, bytes += data->stride) { - float4* weights = (float4*) bytes; - const float sum = weights->x + weights->y + weights->z + weights->w; - *weights /= sum; - } - }; - const cgltf_data* gltf = asset->mSourceAsset->hierarchy; - cgltf_size mcount = gltf->meshes_count; - for (cgltf_size mindex = 0; mindex < mcount; ++mindex) { - const cgltf_mesh& mesh = gltf->meshes[mindex]; - cgltf_size pcount = mesh.primitives_count; - for (cgltf_size pindex = 0; pindex < pcount; ++pindex) { - const cgltf_primitive& prim = mesh.primitives[pindex]; - cgltf_size acount = prim.attributes_count; - for (cgltf_size aindex = 0; aindex < acount; ++aindex) { - const auto& attr = prim.attributes[aindex]; - if (attr.type == cgltf_attribute_type_weights) { - normalize(attr.data); - } - } - } - } -} - } // namespace filament::gltfio diff --git a/libs/gltfio/src/Utility.cpp b/libs/gltfio/src/Utility.cpp new file mode 100644 index 0000000000..f875f71a4e --- /dev/null +++ b/libs/gltfio/src/Utility.cpp @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Utility.h" + +#include "DracoCache.h" +#include "FFilamentAsset.h" +#include "GltfEnums.h" + +#include +#include + +#define CGLTF_IMPLEMENTATION +#include +#include + +namespace filament::gltfio::utility { + +using namespace utils; + +void decodeDracoMeshes(cgltf_data const* gltf, cgltf_primitive const* prim, + DracoCache* dracoCache) { + if (!prim->has_draco_mesh_compression) { + return; + } + + // For a given primitive and attribute, find the corresponding accessor. + auto findAccessor = [](const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int idx) { + for (cgltf_size i = 0; i < prim->attributes_count; i++) { + const cgltf_attribute& attr = prim->attributes[i]; + if (attr.type == type && attr.index == idx) { + return attr.data; + } + } + return (cgltf_accessor*) nullptr; + }; + const cgltf_draco_mesh_compression& draco = prim->draco_mesh_compression; + + // Check if we have already decoded this mesh. + DracoMesh* mesh = dracoCache->findOrCreateMesh(draco.buffer_view); + if (!mesh) { + slog.e << "Cannot decompress mesh, Draco decoding error." << io::endl; + return; + } + + // Copy over the decompressed data, converting the data type if necessary. + if (prim->indices && !mesh->getFaceIndices(prim->indices)) { + return; + } + + // Go through each attribute in the decompressed mesh. + for (cgltf_size i = 0; i < draco.attributes_count; i++) { + + // In cgltf, each Draco attribute's data pointer is an attribute id, not an accessor. + const uint32_t id = draco.attributes[i].data - gltf->accessors; + + // Find the destination accessor; this contains the desired component type, etc. + const cgltf_attribute_type type = draco.attributes[i].type; + const cgltf_int index = draco.attributes[i].index; + cgltf_accessor* accessor = findAccessor(prim, type, index); + if (!accessor) { + slog.w << "Cannot find matching accessor for Draco id " << id << io::endl; + continue; + } + + // Copy over the decompressed data, converting the data type if necessary. + if (!mesh->getVertexAttributes(id, accessor)) { + break; + } + } +} + +void decodeMeshoptCompression(cgltf_data* data) { + for (size_t i = 0; i < data->buffer_views_count; ++i) { + if (!data->buffer_views[i].has_meshopt_compression) { + continue; + } + cgltf_meshopt_compression* compression = &data->buffer_views[i].meshopt_compression; + const uint8_t* source = (const uint8_t*) compression->buffer->data; + assert_invariant(source); + source += compression->offset; + + // This memory is freed by cgltf. + void* destination = malloc(compression->count * compression->stride); + assert_invariant(destination); + + UTILS_UNUSED_IN_RELEASE int error = 0; + switch (compression->mode) { + case cgltf_meshopt_compression_mode_invalid: + break; + case cgltf_meshopt_compression_mode_attributes: + error = meshopt_decodeVertexBuffer(destination, compression->count, + compression->stride, source, compression->size); + break; + case cgltf_meshopt_compression_mode_triangles: + error = meshopt_decodeIndexBuffer(destination, compression->count, + compression->stride, source, compression->size); + break; + case cgltf_meshopt_compression_mode_indices: + error = meshopt_decodeIndexSequence(destination, compression->count, + compression->stride, source, compression->size); + break; + default: + assert_invariant(false); + break; + } + assert_invariant(!error); + + switch (compression->filter) { + case cgltf_meshopt_compression_filter_none: + break; + case cgltf_meshopt_compression_filter_octahedral: + meshopt_decodeFilterOct(destination, compression->count, compression->stride); + break; + case cgltf_meshopt_compression_filter_quaternion: + meshopt_decodeFilterQuat(destination, compression->count, compression->stride); + break; + case cgltf_meshopt_compression_filter_exponential: + meshopt_decodeFilterExp(destination, compression->count, compression->stride); + break; + default: + assert_invariant(false); + break; + } + + data->buffer_views[i].data = destination; + } +} + +bool primitiveHasVertexColor(cgltf_primitive* inPrim) { + for (int slot = 0; slot < inPrim->attributes_count; slot++) { + const cgltf_attribute& inputAttribute = inPrim->attributes[slot]; + if (inputAttribute.type == cgltf_attribute_type_color) { + return true; + } + } + return false; +} + +// Sometimes a glTF bufferview includes unused data at the end (e.g. in skinning.gltf) so we need to +// compute the correct size of the vertex buffer. Filament automatically infers the size of +// driver-level vertex buffers from the attribute data (stride, count, offset) and clients are +// expected to avoid uploading data blobs that exceed this size. Since this information doesn't +// exist in the glTF we need to compute it manually. This is a bit of a cheat, cgltf_calc_size is +// private but its implementation file is available in this cpp file. +uint32_t computeBindingSize(cgltf_accessor const* accessor) { + cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type); + return uint32_t(accessor->stride * (accessor->count - 1) + element_size); +} + +void convertBytesToShorts(uint16_t* dst, uint8_t const* src, size_t count) { + for (size_t i = 0; i < count; ++i) { + dst[i] = src[i]; + } +} + +uint32_t computeBindingOffset(cgltf_accessor const* accessor) { + return uint32_t(accessor->offset + accessor->buffer_view->offset); +} + +bool requiresConversion(cgltf_accessor const* accessor) { + if (UTILS_UNLIKELY(accessor->is_sparse)) { + return true; + } + const cgltf_type type = accessor->type; + const cgltf_component_type ctype = accessor->component_type; + filament::VertexBuffer::AttributeType permitted; + filament::VertexBuffer::AttributeType actual; + UTILS_UNUSED_IN_RELEASE bool supported = getElementType(type, ctype, &permitted, &actual); + assert_invariant(supported && "Unsupported types"); + return permitted != actual; +} + +bool requiresPacking(cgltf_accessor const* accessor) { + if (requiresConversion(accessor)) { + return true; + } + const size_t dim = cgltf_num_components(accessor->type); + switch (accessor->component_type) { + case cgltf_component_type_r_8: + case cgltf_component_type_r_8u: + return accessor->stride != dim; + case cgltf_component_type_r_16: + case cgltf_component_type_r_16u: + return accessor->stride != dim * 2; + case cgltf_component_type_r_32u: + case cgltf_component_type_r_32f: + return accessor->stride != dim * 4; + default: + assert_invariant(false); + return true; + } +} + +bool loadCgltfBuffers(cgltf_data const* gltf, char const* gltfPath, + UriDataCacheHandle uriDataCacheHandle) { + SYSTRACE_CONTEXT(); + SYSTRACE_NAME_BEGIN("Load buffers"); + cgltf_options options{}; + + // For emscripten and Android builds we supply a custom file reader callback that looks inside a + // cache of externally-supplied data blobs, rather than loading from the filesystem. + +#if !GLTFIO_USE_FILESYSTEM + struct Closure { + UriDataCacheHandle uriDataCache; + const cgltf_data* gltf; + }; + + Closure closure = { uriDataCacheHandle, gltf }; + + options.file.user_data = &closure; + + options.file.read = [](const cgltf_memory_options* memoryOpts, + const cgltf_file_options* fileOpts, const char* path, + cgltf_size* size, void** data) { + Closure* closure = (Closure*) fileOpts->user_data; + auto& uriDataCache = closure->uriDataCache; + + if (auto iter = uriDataCache->find(path); iter != uriDataCache->end()) { + *size = iter->second.size; + *data = iter->second.buffer; + } else { + // Even if we don't find the given resource in the cache, we still return a successful + // error code, because we allow downloads to finish after the decoding work starts. + *size = 0; + *data = 0; + } + + return cgltf_result_success; + }; +#endif + + // Read data from the file system and base64 URIs. + cgltf_result result = cgltf_load_buffers(&options, (cgltf_data*) gltf, gltfPath); + if (result != cgltf_result_success) { + slog.e << "Unable to load resources." << io::endl; + return false; + } + + SYSTRACE_NAME_END(); + +#ifndef NDEBUG + if (cgltf_validate((cgltf_data*) gltf) != cgltf_result_success) { + slog.e << "Failed cgltf validation." << io::endl; + return false; + } +#endif + return true; +} + +} // namespace filament::gltfio::utility diff --git a/libs/gltfio/src/Utility.h b/libs/gltfio/src/Utility.h new file mode 100644 index 0000000000..3d2271ec4a --- /dev/null +++ b/libs/gltfio/src/Utility.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GLTFIO_UTILITY_H +#define GLTFIO_UTILITY_H + +#include + +#include + +#include +#include + +struct FFilamentAsset; +struct cgltf_primitive; +struct cgltf_data; +class DracoCache; + +struct cgltf_accessor; + +namespace filament::gltfio { + +// Referenced in ResourceLoader and AssetLoaderExtended +using BufferDescriptor = filament::backend::BufferDescriptor; +using UriDataCache = tsl::robin_map; +using UriDataCacheHandle = std::shared_ptr; + +} // namespace filament::gltfio::utility + +namespace filament::gltfio::utility { + +// Functions that are shared between the original implementation and the extended implementation. +void decodeDracoMeshes(cgltf_data const* gltf, cgltf_primitive const* prim, DracoCache* dracoCache); +void decodeMeshoptCompression(cgltf_data* data); +bool primitiveHasVertexColor(cgltf_primitive* inPrim); +uint32_t computeBindingSize(cgltf_accessor const* accessor); +void convertBytesToShorts(uint16_t* dst, uint8_t const* src, size_t count); +uint32_t computeBindingOffset(cgltf_accessor const* accessor); +bool requiresConversion(cgltf_accessor const* accessor); +bool requiresPacking(cgltf_accessor const* accessor); +bool loadCgltfBuffers(cgltf_data const* gltf, char const* gltfPath, + UriDataCacheHandle uriDataCacheHandle); + +} // namespace filament::gltfio::utility + +#endif