gltfio: refactor for clarity (#7652)

- Pull certain utility functions in a separate header and cpp
- Refactor ResourceLoader::loadeResources into smaller methods
This commit is contained in:
Powei Feng
2024-03-12 13:31:19 -07:00
committed by GitHub
parent 9aad4df441
commit fa6b4ebd04
8 changed files with 528 additions and 397 deletions

View File

@@ -21,6 +21,7 @@
#include "FFilamentAsset.h"
#include "TangentsJob.h"
#include "downcast.h"
#include "Utility.h"
#include <filament/BufferObject.h>
#include <filament/Engine.h>
@@ -50,6 +51,7 @@
#include <fstream>
#include <memory>
#include <string>
#include <tuple>
using namespace filament;
using namespace filament::math;
@@ -63,16 +65,16 @@ namespace filament::gltfio {
using BufferTextureCache = tsl::robin_map<const void*, Texture*>;
using FilepathTextureCache = tsl::robin_map<std::string, Texture*>;
using UriDataCache = tsl::robin_map<std::string, gltfio::ResourceLoader::BufferDescriptor>;
using UriDataCacheHandle = std::shared_ptr<UriDataCache>;
using TextureProviderList = tsl::robin_map<std::string, TextureProvider*>;
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<FFilamentAsset::Skin>& 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<mat4f> 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<mat4f> 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