- gltfio: Enable -Wall -Werror for gltfio_core - gltfio: Fix various errors that were missed warnings - matdbg: switch from std::atomic_uint64_t to std::atomic<uint64_t> for older clang
937 lines
36 KiB
C++
937 lines
36 KiB
C++
/*
|
|
* Copyright (C) 2020 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 <gltfio/ResourceLoader.h>
|
|
#include <gltfio/TextureProvider.h>
|
|
|
|
#include "GltfEnums.h"
|
|
#include "FFilamentAsset.h"
|
|
#include "TangentsJob.h"
|
|
#include "downcast.h"
|
|
|
|
#include <filament/BufferObject.h>
|
|
#include <filament/Engine.h>
|
|
#include <filament/IndexBuffer.h>
|
|
#include <filament/MaterialInstance.h>
|
|
#include <filament/Texture.h>
|
|
#include <filament/VertexBuffer.h>
|
|
#include <filament/MorphTargetBuffer.h>
|
|
|
|
#include <geometry/Transcoder.h>
|
|
|
|
#include <utils/compiler.h>
|
|
#include <utils/JobSystem.h>
|
|
#include <utils/Log.h>
|
|
#include <utils/Systrace.h>
|
|
#include <utils/Path.h>
|
|
|
|
#include <cgltf.h>
|
|
#include <meshoptimizer.h>
|
|
|
|
#include <math/quat.h>
|
|
#include <math/vec3.h>
|
|
#include <math/vec4.h>
|
|
|
|
#include <tsl/robin_map.h>
|
|
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
using namespace filament;
|
|
using namespace filament::math;
|
|
using namespace utils;
|
|
|
|
using filament::geometry::ComponentType;
|
|
|
|
static const auto FREE_CALLBACK = [](void* mem, size_t, void*) { free(mem); };
|
|
|
|
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*>;
|
|
|
|
enum class CacheResult {
|
|
ERROR,
|
|
NOT_READY,
|
|
FOUND,
|
|
MISS,
|
|
};
|
|
|
|
struct ResourceLoader::Impl {
|
|
explicit Impl(const ResourceConfiguration& config) :
|
|
mEngine(config.engine),
|
|
mNormalizeSkinningWeights(config.normalizeSkinningWeights),
|
|
mGltfPath(config.gltfPath ? config.gltfPath : ""),
|
|
mUriDataCache(std::make_shared<UriDataCache>()) {}
|
|
|
|
Engine* const mEngine;
|
|
bool mNormalizeSkinningWeights;
|
|
std::string mGltfPath;
|
|
|
|
// User-provided resource data with URI string keys, populated with addResourceData().
|
|
// This is used on platforms without traditional file systems, such as Android, iOS, and WebGL.
|
|
UriDataCacheHandle mUriDataCache;
|
|
|
|
// User-provided mapping from mime types to texture providers.
|
|
TextureProviderList mTextureProviders;
|
|
|
|
// Avoid duplicated Texture objects via caches with two key types: buffer pointers and strings.
|
|
BufferTextureCache mBufferTextureCache;
|
|
FilepathTextureCache mFilepathTextureCache;
|
|
|
|
FFilamentAsset* mAsyncAsset = nullptr;
|
|
size_t mRemainingTextureDownloads = 0;
|
|
|
|
void addResourceData(const char* uri, BufferDescriptor&& buffer);
|
|
void computeTangents(FFilamentAsset* asset);
|
|
void createTextures(FFilamentAsset* asset, bool async);
|
|
void cancelTextureDecoding();
|
|
std::pair<Texture*, CacheResult> getOrCreateTexture(FFilamentAsset* asset, size_t textureIndex,
|
|
TextureProvider::TextureFlags flags);
|
|
~Impl();
|
|
};
|
|
|
|
uint32_t computeBindingSize(const cgltf_accessor* accessor);
|
|
uint32_t computeBindingOffset(const cgltf_accessor* accessor);
|
|
|
|
// 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 {
|
|
FFilamentAsset::SourceHandle handle;
|
|
UriDataCacheHandle dataCacheHandle;
|
|
};
|
|
|
|
UploadEvent* uploadUserdata(FFilamentAsset* asset, UriDataCacheHandle dataCache) {
|
|
return new UploadEvent({ asset->mSourceAsset, dataCache });
|
|
}
|
|
|
|
static 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) {
|
|
if (strncmp(uri, "data:", 5) != 0) {
|
|
return nullptr;
|
|
}
|
|
const char* comma = strchr(uri, ',');
|
|
if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) {
|
|
const char* base64 = comma + 1;
|
|
const size_t base64Size = strlen(base64);
|
|
size_t size = base64Size - base64Size / 4;
|
|
if (base64Size >= 2) {
|
|
size -= base64[base64Size - 2] == '=';
|
|
size -= base64[base64Size - 1] == '=';
|
|
}
|
|
void* data = 0;
|
|
cgltf_options options = {};
|
|
cgltf_result result = cgltf_load_buffer_base64(&options, size, base64, &data);
|
|
if (result != cgltf_result_success) {
|
|
return nullptr;
|
|
}
|
|
*mimeType = std::string(uri + 5, comma - 7);
|
|
*psize = size;
|
|
return (const uint8_t*) data;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
ResourceLoader::ResourceLoader(const ResourceConfiguration& config) : pImpl(new Impl(config)) { }
|
|
|
|
ResourceLoader::~ResourceLoader() {
|
|
delete pImpl;
|
|
}
|
|
|
|
void ResourceLoader::setConfiguration(const ResourceConfiguration& config) {
|
|
pImpl->mNormalizeSkinningWeights = config.normalizeSkinningWeights;
|
|
pImpl->mGltfPath = config.gltfPath;
|
|
}
|
|
|
|
void ResourceLoader::addResourceData(const char* uri, BufferDescriptor&& buffer) {
|
|
pImpl->addResourceData(uri, std::move(buffer));
|
|
}
|
|
|
|
static bool endsWith(std::string_view expr, std::string_view ending) {
|
|
if (expr.length() >= ending.length()) {
|
|
return (expr.compare(expr.length() - ending.length(), ending.length(), ending) == 0);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// TODO: This is not a great way to determine if a resource is a texture, but we can remove it after
|
|
// gltfio gains support for concurrent downloading of vertex data:
|
|
// https://github.com/google/filament/issues/5909
|
|
static bool isTexture(const char* uri) {
|
|
using namespace std::literals;
|
|
std::string_view urisv(uri);
|
|
if (endsWith(urisv, ".png"sv)) {
|
|
return true;
|
|
}
|
|
if (endsWith(urisv, ".ktx2"sv)) {
|
|
return true;
|
|
}
|
|
if (endsWith(urisv, ".jpg"sv) || endsWith(urisv, ".jpeg"sv)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ResourceLoader::Impl::addResourceData(const char* uri, BufferDescriptor&& buffer) {
|
|
// Start an async marker the first time this is called and end it when
|
|
// finalization begins. This marker provides a rough indicator of how long
|
|
// the client is taking to load raw data blobs from storage.
|
|
if (mUriDataCache->empty()) {
|
|
SYSTRACE_CONTEXT();
|
|
SYSTRACE_ASYNC_BEGIN("addResourceData", 1);
|
|
}
|
|
// NOTE: replacing an existing item in a robin map does not seem to behave as expected.
|
|
// To work around this, we explicitly erase the old element if it already exists.
|
|
auto iter = mUriDataCache->find(uri);
|
|
if (iter != mUriDataCache->end()) {
|
|
mUriDataCache->erase(iter);
|
|
}
|
|
mUriDataCache->emplace(uri, std::move(buffer));
|
|
|
|
// If this is a texture and async loading has already started, add a new decoder job.
|
|
if (isTexture(uri) && mAsyncAsset && mRemainingTextureDownloads > 0) {
|
|
createTextures(mAsyncAsset, true);
|
|
}
|
|
}
|
|
|
|
bool ResourceLoader::hasResourceData(const char* uri) const {
|
|
return pImpl->mUriDataCache->find(uri) != pImpl->mUriDataCache->end();
|
|
}
|
|
|
|
void ResourceLoader::evictResourceData() {
|
|
// Note that this triggers BufferDescriptor callbacks.
|
|
pImpl->mUriDataCache->clear();
|
|
}
|
|
|
|
bool ResourceLoader::loadResources(FilamentAsset* asset) {
|
|
FFilamentAsset* fasset = downcast(asset);
|
|
return loadResources(fasset, false);
|
|
}
|
|
|
|
bool ResourceLoader::loadResources(FFilamentAsset* asset, bool async) {
|
|
SYSTRACE_CONTEXT();
|
|
SYSTRACE_ASYNC_END("addResourceData", 1);
|
|
|
|
if (asset->mResourcesLoaded) {
|
|
return false;
|
|
}
|
|
asset->mResourcesLoaded = true;
|
|
|
|
// At this point, any entities that are created in the future (i.e. dynamically added instances)
|
|
// will not need the progressive feature to be enabled. This simplifies the dependency graph and
|
|
// prevents it from growing.
|
|
asset->mDependencyGraph.disableProgressiveReveal();
|
|
|
|
// Clear our texture caches. Previous calls to loadResources may have populated these, but the
|
|
// Texture objects could have since been destroyed.
|
|
pImpl->mBufferTextureCache.clear();
|
|
pImpl->mFilepathTextureCache.clear();
|
|
|
|
const cgltf_data* gltf = asset->mSourceAsset->hierarchy;
|
|
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.
|
|
|
|
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) {
|
|
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());
|
|
}
|
|
}
|
|
|
|
// 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 = {};
|
|
|
|
// If any decoding jobs are still underway from a previous load, wait for them to finish.
|
|
for (const auto& iter : pImpl->mTextureProviders) {
|
|
iter.second->waitForCompletion();
|
|
iter.second->updateQueue();
|
|
}
|
|
|
|
// Finally, create Filament Textures and begin loading image files.
|
|
pImpl->createTextures(asset, async);
|
|
|
|
// Non-textured renderables are now considered ready, and we can guarantee that no new
|
|
// materials or textures will be added. Notify the dependency graph.
|
|
asset->mDependencyGraph.commitEdges();
|
|
|
|
for (FFilamentInstance* instance : asset->mInstances) {
|
|
instance->createAnimator();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ResourceLoader::asyncBeginLoad(FilamentAsset* asset) {
|
|
pImpl->mAsyncAsset = downcast(asset);
|
|
return loadResources(downcast(asset), true);
|
|
}
|
|
|
|
void ResourceLoader::asyncCancelLoad() {
|
|
pImpl->cancelTextureDecoding();
|
|
pImpl->mAsyncAsset = nullptr;
|
|
pImpl->mEngine->flushAndWait();
|
|
}
|
|
|
|
void ResourceLoader::addTextureProvider(const char* mimeType, TextureProvider* provider) {
|
|
pImpl->mTextureProviders[mimeType] = provider;
|
|
}
|
|
|
|
float ResourceLoader::asyncGetLoadProgress() const {
|
|
if (pImpl->mTextureProviders.empty() || !pImpl->mAsyncAsset) {
|
|
return 0;
|
|
}
|
|
size_t pushedCount = 0;
|
|
size_t poppedCount = 0;
|
|
for (const auto& iter : pImpl->mTextureProviders) {
|
|
pushedCount += iter.second->getPushedCount();
|
|
poppedCount += iter.second->getPoppedCount();
|
|
}
|
|
|
|
// Textures that haven't been fully downloaded are not yet pushed into one of the
|
|
// decoding queues, so here we include them in the total "pending" count.
|
|
const size_t pendingCount = pushedCount + pImpl->mRemainingTextureDownloads;
|
|
|
|
return pendingCount == 0 ? 1 : (float(poppedCount) / pendingCount);
|
|
}
|
|
|
|
void ResourceLoader::asyncUpdateLoad() {
|
|
if (!pImpl->mAsyncAsset) {
|
|
return;
|
|
}
|
|
for (const auto& iter : pImpl->mTextureProviders) {
|
|
iter.second->updateQueue();
|
|
while (Texture* texture = iter.second->popTexture()) {
|
|
pImpl->mAsyncAsset->mDependencyGraph.markAsReady(texture);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::pair<Texture*, CacheResult> ResourceLoader::Impl::getOrCreateTexture(FFilamentAsset* asset,
|
|
size_t textureIndex, TextureProvider::TextureFlags flags) {
|
|
const cgltf_texture& srcTexture = asset->mSourceAsset->hierarchy->textures[textureIndex];
|
|
const cgltf_image* image = srcTexture.basisu_image ?
|
|
srcTexture.basisu_image : srcTexture.image;
|
|
const cgltf_buffer_view* bv = image->buffer_view;
|
|
const char* uri = image->uri;
|
|
|
|
std::string mime = image->mime_type ? image->mime_type : "";
|
|
size_t dataUriSize;
|
|
const uint8_t* dataUriContent = uri ? parseDataUri(uri, &mime, &dataUriSize) : nullptr;
|
|
|
|
if (mime.empty()) {
|
|
assert_invariant(uri && "Non-URI images must supply a mime type.");
|
|
const std::string extension = Path(uri).getExtension();
|
|
mime = extension == "jpg" ? "image/jpeg" : "image/" + extension;
|
|
}
|
|
|
|
auto foundProvider = mTextureProviders.find(mime);
|
|
if (foundProvider == mTextureProviders.end()) {
|
|
slog.e << "Missing texture provider for " << mime << io::endl;
|
|
return {};
|
|
}
|
|
TextureProvider* provider = foundProvider->second;
|
|
assert_invariant(provider);
|
|
|
|
// Check if the texture slot uses BufferView data.
|
|
if (void** bufferViewData = bv ? &bv->buffer->data : nullptr; bufferViewData) {
|
|
assert_invariant(!dataUriContent);
|
|
const size_t offset = bv ? bv->offset : 0;
|
|
const uint8_t* sourceData = offset + (const uint8_t*) *bufferViewData;
|
|
if (auto iter = mBufferTextureCache.find(sourceData); iter != mBufferTextureCache.end()) {
|
|
return {iter->second, CacheResult::FOUND};
|
|
}
|
|
const uint32_t totalSize = uint32_t(bv ? bv->size : 0);
|
|
if (Texture* texture = provider->pushTexture(sourceData, totalSize, mime.c_str(), flags); texture) {
|
|
mBufferTextureCache[sourceData] = texture;
|
|
return {texture, CacheResult::MISS};
|
|
}
|
|
}
|
|
|
|
// Check if the texture slot is a data URI.
|
|
// Note that this is a data URI in an image, not a buffer. Data URI's in buffers are decoded
|
|
// by the cgltf_load_buffers() function.
|
|
else if (dataUriContent) {
|
|
if (auto iter = mBufferTextureCache.find(uri); iter != mBufferTextureCache.end()) {
|
|
free((void*)dataUriContent);
|
|
return {iter->second, CacheResult::FOUND};
|
|
}
|
|
if (Texture* texture = provider->pushTexture(dataUriContent, dataUriSize, mime.c_str(), flags); texture) {
|
|
free((void*)dataUriContent);
|
|
mBufferTextureCache[uri] = texture;
|
|
return {texture, CacheResult::MISS};
|
|
}
|
|
free((void*)dataUriContent);
|
|
}
|
|
|
|
// Check the user-supplied resource cache for this URI.
|
|
else if (auto iter = mUriDataCache->find(uri); iter != mUriDataCache->end()) {
|
|
const uint8_t* sourceData = (const uint8_t*) iter->second.buffer;
|
|
if (auto iter = mBufferTextureCache.find(sourceData); iter != mBufferTextureCache.end()) {
|
|
return {iter->second, CacheResult::FOUND};
|
|
}
|
|
if (Texture* texture = provider->pushTexture(sourceData, iter->second.size, mime.c_str(), flags); texture) {
|
|
mBufferTextureCache[sourceData] = texture;
|
|
return {texture, CacheResult::MISS};
|
|
}
|
|
}
|
|
|
|
// Finally, try the file system.
|
|
else if constexpr (GLTFIO_USE_FILESYSTEM) {
|
|
if (auto iter = mFilepathTextureCache.find(uri); iter != mFilepathTextureCache.end()) {
|
|
return {iter->second, CacheResult::FOUND};
|
|
}
|
|
Path fullpath = Path(mGltfPath).getParent() + uri;
|
|
if (!fullpath.exists()) {
|
|
slog.e << "Unable to open " << fullpath << io::endl;
|
|
return {};
|
|
}
|
|
using namespace std;
|
|
ifstream filest(fullpath, std::ifstream::in | std::ifstream::binary);
|
|
vector<uint8_t> buffer;
|
|
filest.seekg(0, ios::end);
|
|
buffer.reserve((size_t) filest.tellg());
|
|
filest.seekg(0, ios::beg);
|
|
buffer.assign((istreambuf_iterator<char>(filest)), istreambuf_iterator<char>());
|
|
if (Texture* texture = provider->pushTexture(buffer.data(), buffer.size(), mime.c_str(), flags); texture) {
|
|
mFilepathTextureCache[uri] = texture;
|
|
return {texture, CacheResult::MISS};
|
|
}
|
|
|
|
} else {
|
|
// If we reach here, the app has not yet called addResourceData() for this texture,
|
|
// perhaps because it is still being downloaded.
|
|
return {nullptr, CacheResult::NOT_READY};
|
|
}
|
|
|
|
const char* name = srcTexture.name ? srcTexture.name : uri;
|
|
slog.e << "Unable to create texture " << name << ": " << provider->getPushMessage() << io::endl;
|
|
return {};
|
|
}
|
|
|
|
void ResourceLoader::Impl::cancelTextureDecoding() {
|
|
for (const auto& iter : mTextureProviders) {
|
|
iter.second->cancelDecoding();
|
|
}
|
|
mAsyncAsset = nullptr;
|
|
}
|
|
|
|
void ResourceLoader::Impl::createTextures(FFilamentAsset* asset, bool async) {
|
|
mRemainingTextureDownloads = 0;
|
|
|
|
// Create new texture objects if they are not cached and kick off decoding jobs.
|
|
for (size_t textureIndex = 0, n = asset->mTextures.size(); textureIndex < n; ++textureIndex) {
|
|
FFilamentAsset::TextureInfo& info = asset->mTextures[textureIndex];
|
|
auto [texture, cacheResult] = getOrCreateTexture(asset, textureIndex, info.flags);
|
|
if (texture == nullptr) {
|
|
if (cacheResult == CacheResult::NOT_READY) {
|
|
mRemainingTextureDownloads++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// If this cgtf_texture slot is being initialized, copy the Texture into the slot
|
|
// and note if the Texture was created or re-used.
|
|
if (info.texture == nullptr) {
|
|
info.texture = texture;
|
|
info.isOwner = cacheResult == CacheResult::MISS;
|
|
}
|
|
|
|
// For each binding to a material instance, call setParameter(...) on the material.
|
|
for (const TextureSlot& slot : info.bindings) {
|
|
asset->applyTextureBinding(textureIndex, slot);
|
|
}
|
|
}
|
|
|
|
// Non-threaded systems are required to use the asynchronous API.
|
|
assert_invariant(UTILS_HAS_THREADING || async);
|
|
|
|
if (async) {
|
|
return;
|
|
}
|
|
|
|
for (const auto& iter : mTextureProviders) {
|
|
iter.second->waitForCompletion();
|
|
iter.second->updateQueue();
|
|
}
|
|
}
|
|
|
|
void ResourceLoader::Impl::computeTangents(FFilamentAsset* asset) {
|
|
SYSTRACE_CALL();
|
|
|
|
const cgltf_accessor* kGenerateTangents = &asset->mGenerateTangents;
|
|
const cgltf_accessor* kGenerateNormals = &asset->mGenerateNormals;
|
|
|
|
// Collect all TANGENT vertex attribute slots that need to be populated.
|
|
tsl::robin_map<VertexBuffer*, uint8_t> baseTangents;
|
|
for (auto slot : asset->mBufferSlots) {
|
|
if (slot.accessor != kGenerateTangents && slot.accessor != kGenerateNormals) {
|
|
continue;
|
|
}
|
|
baseTangents[slot.vertexBuffer] = slot.bufferIndex;
|
|
}
|
|
|
|
// Create a job description for each triangle-based primitive.
|
|
using Params = TangentsJob::Params;
|
|
std::vector<Params> jobParams;
|
|
for (auto [prim, vb] : asset->mPrimitives) {
|
|
if (UTILS_UNLIKELY(prim->type != cgltf_primitive_type_triangles)) {
|
|
continue;
|
|
}
|
|
auto iter = baseTangents.find(vb);
|
|
if (iter != baseTangents.end()) {
|
|
jobParams.emplace_back(Params {{ prim }, {vb, nullptr, iter->second }});
|
|
}
|
|
}
|
|
|
|
// Create a job description for morph targets.
|
|
for (size_t i = 0, n = asset->mSourceAsset->hierarchy->meshes_count; i < n; ++i) {
|
|
const cgltf_mesh& mesh = asset->mSourceAsset->hierarchy->meshes[i];
|
|
const FixedCapacityVector<Primitive>& prims = asset->mMeshCache[i];
|
|
if (0 == mesh.weights_count) {
|
|
continue;
|
|
}
|
|
for (cgltf_size pindex = 0, pcount = mesh.primitives_count; pindex < pcount; ++pindex) {
|
|
const cgltf_primitive& prim = mesh.primitives[pindex];
|
|
MorphTargetBuffer* tb = prims[pindex].targets;
|
|
for (cgltf_size tindex = 0, tcount = prim.targets_count; tindex < tcount; ++tindex) {
|
|
const cgltf_morph_target& target = prim.targets[tindex];
|
|
bool hasNormals = false;
|
|
for (cgltf_size aindex = 0; aindex < target.attributes_count; aindex++) {
|
|
const cgltf_attribute& attribute = target.attributes[aindex];
|
|
const cgltf_attribute_type atype = attribute.type;
|
|
if (atype != cgltf_attribute_type_tangent) {
|
|
continue;
|
|
}
|
|
hasNormals = true;
|
|
jobParams.emplace_back(Params { { &prim, (int) tindex },
|
|
{ nullptr, tb, (uint8_t) pindex } });
|
|
break;
|
|
}
|
|
// Generate flat normals if necessary.
|
|
if (!hasNormals && prim.material && !prim.material->unlit) {
|
|
jobParams.emplace_back(Params { { &prim, (int) tindex },
|
|
{ nullptr, tb, (uint8_t) pindex } });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Kick off jobs for computing tangent frames.
|
|
JobSystem* js = &mEngine->getJobSystem();
|
|
JobSystem::Job* parent = js->createJob();
|
|
for (Params& params : jobParams) {
|
|
Params* pptr = ¶ms;
|
|
js->run(jobs::createJob(*js, parent, [pptr] { TangentsJob::run(pptr); }));
|
|
}
|
|
js->runAndWait(parent);
|
|
|
|
// Finally, upload quaternions to the GPU from the main thread.
|
|
for (Params& params : jobParams) {
|
|
if (params.context.vb) {
|
|
BufferObject* bo = BufferObject::Builder()
|
|
.size(params.out.vertexCount * sizeof(short4)).build(*mEngine);
|
|
asset->mBufferObjects.push_back(bo);
|
|
bo->setBuffer(*mEngine, BufferDescriptor(
|
|
params.out.results, bo->getByteCount(), FREE_CALLBACK));
|
|
params.context.vb->setBufferObjectAt(*mEngine, params.context.slot, bo);
|
|
} else {
|
|
assert_invariant(params.context.tb);
|
|
params.context.tb->setTangentsAt(*mEngine, params.in.morphTargetIndex,
|
|
params.out.results, params.out.vertexCount);
|
|
free(params.out.results);
|
|
}
|
|
}
|
|
}
|
|
|
|
ResourceLoader::Impl::~Impl() {
|
|
for (const auto& iter : mTextureProviders) {
|
|
iter.second->cancelDecoding();
|
|
}
|
|
}
|
|
|
|
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
|