diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index fd76718f38..6abd774aac 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,6 +8,7 @@ A new header is inserted each time a *tag* is created. - The Android support libraries (gltfio and filament-utils) now use dynamic linking. - Screen-space refraction is now supported. - Removed depth-prepass related APIs. +- gltfio: add asynchronous API to ResourceLoader. ## v1.4.5 diff --git a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/ModelViewer.kt b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/ModelViewer.kt index f44ba072d5..032697f5c7 100644 --- a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/ModelViewer.kt +++ b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/ModelViewer.kt @@ -57,6 +57,10 @@ class ModelViewer { var animator: Animator? = null private set + @Suppress("unused") + val progress + get() = resourceLoader.asyncGetLoadProgress() + val engine: Engine val scene: Scene val view: View @@ -69,6 +73,7 @@ class ModelViewer { private val renderer: Renderer private var swapChain: SwapChain? = null private var assetLoader: AssetLoader + private var resourceLoader: ResourceLoader private val eyePos = DoubleArray(3) private val target = DoubleArray(3) @@ -91,6 +96,7 @@ class ModelViewer { view.camera = camera assetLoader = AssetLoader(engine, MaterialProvider(engine), EntityManager.get()) + resourceLoader = ResourceLoader(engine) // Always add a direct light source since it is required for shadowing. // We highly recommend adding an indirect light as well. @@ -140,9 +146,7 @@ class ModelViewer { destroyModel() asset = assetLoader.createAssetFromJson(buffer) asset?.let { asset -> - val resourceLoader = ResourceLoader(engine) - resourceLoader.loadResources(asset) - resourceLoader.destroy() + resourceLoader.asyncBeginLoad(asset) animator = asset.animator asset.releaseSourceData() scene.addEntities(asset.entities) @@ -156,12 +160,10 @@ class ModelViewer { destroyModel() asset = assetLoader.createAssetFromJson(buffer) asset?.let { asset -> - val resourceLoader = ResourceLoader(engine) for (uri in asset.resourceUris) { resourceLoader.addResourceData(uri, callback(uri)) } - resourceLoader.loadResources(asset) - resourceLoader.destroy() + resourceLoader.asyncBeginLoad(asset) animator = asset.animator asset.releaseSourceData() scene.addEntities(asset.entities) @@ -203,12 +205,17 @@ class ModelViewer { return } + // Allow the resource loader to finalize textures that have become ready. + resourceLoader.asyncUpdateLoad() + + // Extract the camera basis from the helper and push it to the Filament camera. cameraManipulator.getLookAt(eyePos, target, upward) camera.lookAt( eyePos[0], eyePos[1], eyePos[2], target[0], target[1], target[2], upward[0], upward[1], upward[2]) + // Render the scene, unless the renderer wants to skip the frame. if (renderer.beginFrame(swapChain!!)) { renderer.render(view) renderer.endFrame() @@ -223,6 +230,7 @@ class ModelViewer { destroyModel() assetLoader.destroy() + resourceLoader.destroy() engine.destroyEntity(light) engine.destroyRenderer(renderer) diff --git a/android/gltfio-android/src/main/cpp/ResourceLoader.cpp b/android/gltfio-android/src/main/cpp/ResourceLoader.cpp index 1dad00871a..c4e5f9ba5d 100644 --- a/android/gltfio-android/src/main/cpp/ResourceLoader.cpp +++ b/android/gltfio-android/src/main/cpp/ResourceLoader.cpp @@ -76,3 +76,25 @@ Java_com_google_android_filament_gltfio_ResourceLoader_nLoadResources(JNIEnv*, j FilamentAsset* asset = (FilamentAsset*) nativeAsset; loader->loadResources(asset); } + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_google_android_filament_gltfio_ResourceLoader_nAsyncBeginLoad(JNIEnv*, jclass, + jlong nativeLoader, jlong nativeAsset) { + ResourceLoader* loader = (ResourceLoader*) nativeLoader; + FilamentAsset* asset = (FilamentAsset*) nativeAsset; + return loader->asyncBeginLoad(asset); +} + +extern "C" JNIEXPORT jfloat JNICALL +Java_com_google_android_filament_gltfio_ResourceLoader_nAsyncGetLoadProgress(JNIEnv*, jclass, + jlong nativeLoader) { + ResourceLoader* loader = (ResourceLoader*) nativeLoader; + return loader->asyncGetLoadProgress(); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_gltfio_ResourceLoader_nAsyncUpdateLoad(JNIEnv*, jclass, + jlong nativeLoader) { + ResourceLoader* loader = (ResourceLoader*) nativeLoader; + loader->asyncUpdateLoad(); +} diff --git a/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/ResourceLoader.java b/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/ResourceLoader.java index 12b92f91be..10820ba6ef 100644 --- a/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/ResourceLoader.java +++ b/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/ResourceLoader.java @@ -26,9 +26,10 @@ import java.lang.reflect.Method; import java.nio.Buffer; /** - * Uploads vertex buffers and textures to the GPU and computes tangents. + * Prepares and uploads vertex buffers and textures to the GPU. * - *

For a usage example, see the documentation for {@link AssetLoader}.

+ *

For a usage example, see the documentation for {@link AssetLoader}. + * All methods should be called from the main thread.

* * @see AssetLoader * @see FilamentAsset @@ -58,12 +59,15 @@ public class ResourceLoader { /** * Feeds the binary content of an external resource into the loader's URI cache. * - *

ResourceLoader does not know how to download external resources on its own - * (for example, external resources might come from a filesystem, a database, or the internet) - * so this method allows clients to download external resources and push them to the loader.

+ * On some platforms, `ResourceLoader` does not know how to download external resources on its + * own (external resources might come from a filesystem, a database, or the internet) so this + * method allows clients to download external resources and push them to the loader. * - *

When loading GLB files (as opposed to JSON-based glTF files), clients typically do not - * need to call this method.

+ * Every resource should be passed in before calling [loadResources] or [asyncBeginLoad]. See + * also [FilamentAsset#getResourceUris]. + * + * When loading GLB files (as opposed to JSON-based glTF files), clients typically do not + * need to call this method. * * @param uri the string path that matches an image URI or buffer URI in the glTF * @param buffer the binary blob corresponding to the given URI @@ -76,7 +80,7 @@ public class ResourceLoader { } /** - * Checks if the given resource has already been loaded. + * Checks if the given resource has already been added to the URI cache. */ public boolean hasResourceData(@NonNull String uri) { return nHasResourceData(mNativeObject, uri); @@ -86,8 +90,7 @@ public class ResourceLoader { * Iterates through all external buffers and images and creates corresponding Filament objects * (vertex buffers, textures, etc), which become owned by the asset. * - *

This is the main entry point for ResourceLoader, and only needs to be called - * once.

+ * NOTE: this is a synchronous API, please see [asyncBeginLoad] as an alternative. * * @param asset the Filament asset that contains URI-based resources * @return self (for daisy chaining) @@ -98,10 +101,43 @@ public class ResourceLoader { return this; } + /** + * Starts an asynchronous resource load. + * + * Returns false if the loading process was unable to start. + * + * This is an alternative to #loadResources and requires periodic calls to #asyncUpdateLoad. + * On multi-threaded systems this creates threads for texture decoding. + */ + public boolean asyncBeginLoad(@NonNull FilamentAsset asset) { + return nAsyncBeginLoad(mNativeObject, asset.getNativeObject()); + } + + /** + * Gets the status of an asynchronous resource load as a percentage in [0,1]. + */ + public float asyncGetLoadProgress() { + return nAsyncGetLoadProgress(mNativeObject); + } + + /** + * Updates an asynchronous load by performing any pending work that must take place + * on the main thread. + * + * Clients must periodically call this until #asyncGetLoadProgress returns 100%. + * After progress reaches 100%, calling this is harmless; it just does nothing. + */ + public void asyncUpdateLoad() { + nAsyncUpdateLoad(mNativeObject); + } + private static native long nCreateResourceLoader(long nativeEngine); private static native void nDestroyResourceLoader(long nativeLoader); private static native void nAddResourceData(long nativeLoader, String url, Buffer buffer, int remaining); private static native boolean nHasResourceData(long nativeLoader, String url); private static native void nLoadResources(long nativeLoader, long nativeAsset); + private static native boolean nAsyncBeginLoad(long nativeLoader, long nativeAsset); + private static native float nAsyncGetLoadProgress(long nativeLoader); + private static native void nAsyncUpdateLoad(long nativeLoader); } diff --git a/libs/gltfio/include/gltfio/ResourceLoader.h b/libs/gltfio/include/gltfio/ResourceLoader.h index 65131e2f04..1b8beef109 100644 --- a/libs/gltfio/include/gltfio/ResourceLoader.h +++ b/libs/gltfio/include/gltfio/ResourceLoader.h @@ -58,20 +58,15 @@ struct ResourceConfiguration { /** * \class ResourceLoader ResourceLoader.h gltfio/ResourceLoader.h - * \brief Asynchronously uploads vertex buffers and textures to the GPU and computes tangents. + * \brief Prepares and uploads vertex buffers and textures to the GPU. * * For a usage example, see the documentation for AssetLoader. * - * In theory, this class could cache a map of URL's to data blobs and could therefore be useful - * across multiple assets. However, clients should feel free to immediately destroy this after - * calling loadResources. There is no need to wait for resources to finish uploading because this is - * done in the the background. - * * ResourceLoader must be destroyed on the same thread that calls filament::Renderer::render() * because it listens to filament::backend::BufferDescriptor callbacks in order to determine when to * free CPU-side data blobs. * - * \todo If clients persist their ResourceLoader, Texture objects are currently re-created upon + * \todo If clients persist their ResourceLoader, Filament textures are currently re-created upon * subsequent re-loads of the same asset. To fix this, we would need to enable shared ownership * of Texture objects between ResourceLoader and FilamentAsset. */ @@ -83,9 +78,24 @@ public: ~ResourceLoader(); /** - * Adds raw resource data into a cache for platforms that do not have filesystem access. + * Feeds the binary content of an external resource into the loader's URI cache. + * + * On some platforms, `ResourceLoader` does not know how to download external resources on its + * own (external resources might come from a filesystem, a database, or the internet) so this + * method allows clients to download external resources and push them to the loader. + * + * Every resource should be passed in before calling #loadResources or #asyncBeginLoad. See + * also FilamentAsset#getResourceUris. + * + * When loading GLB files (as opposed to JSON-based glTF files), clients typically do not + * need to call this method. */ - void addResourceData(const char* url, BufferDescriptor&& buffer); + void addResourceData(const char* uri, BufferDescriptor&& buffer); + + /** + * Checks if the given resource has already been added to the URI cache. + */ + bool hasResourceData(const char* uri) const; /** * Loads resources for the given asset from the filesystem or data cache and "finalizes" the @@ -96,13 +106,33 @@ public: * be loaded. * * Note: this method is synchronous and blocks until all textures have been decoded. + * For an asynchronous alternative, see #asyncBeginLoad. */ bool loadResources(FilamentAsset* asset); /** - * Checks if the given resource has already been loaded. + * Starts an asynchronous resource load. + * + * Returns false if the loading process was unable to start. + * + * This is an alternative to #loadResources and requires periodic calls to #asyncUpdateLoad. + * On multi-threaded systems this creates threads for texture decoding. */ - bool hasResourceData(const char* url) const; + bool asyncBeginLoad(FilamentAsset* asset); + + /** + * Gets the status of an asynchronous resource load as a percentage in [0,1]. + */ + float asyncGetLoadProgress() const; + + /** + * Updates an asynchronous load by performing any pending work that must take place + * on the main thread. + * + * Clients must periodically call this until #asyncGetLoadProgress returns 100%. + * After progress reaches 100%, calling this is harmless; it just does nothing. + */ + void asyncUpdateLoad(); private: bool loadResources(details::FFilamentAsset* asset, bool async); diff --git a/libs/gltfio/src/ResourceLoader.cpp b/libs/gltfio/src/ResourceLoader.cpp index a370e86b50..4fd34da184 100644 --- a/libs/gltfio/src/ResourceLoader.cpp +++ b/libs/gltfio/src/ResourceLoader.cpp @@ -28,15 +28,15 @@ #include -#include -#include -#include - #include #include #include +#include +#include +#include + #include #include @@ -48,7 +48,6 @@ using namespace utils; static const auto FREE_CALLBACK = [](void* mem, size_t, void*) { free(mem); }; namespace { - struct TextureCacheEntry { Texture* texture; std::atomic texels; @@ -62,6 +61,7 @@ namespace { using BufferTextureCache = tsl::robin_map>; using UrlTextureCache = tsl::robin_map>; + using UriDataCache = tsl::robin_map; } namespace gltfio { @@ -69,24 +69,24 @@ namespace gltfio { struct ResourceLoader::Impl { ResourceConfiguration mConfig; - // User-provided resource data with URL string keys, populated via addResourceData(). + // User-provided resource data with URI string keys, populated with addResourceData(). // This is used on platforms without traditional file systems, such as Android and WebGL. - tsl::robin_map mUserCache; + UriDataCache mUriDataCache; - // The resource loader's transient texture cache holds decoded texture data during - // loadResources(). It is discarded when all Filament Texture objects have been created and - // uploaded. Since multiple glTF textures might be loaded from a single URL or buffer pointer, - // the cache prevents needless re-decoding. The cache is split into two maps: one for URL-based - // textures and one for buffer-based textures. + // The two texture caches are populated while textures are being decoded, and they are no longer + // used after all textures have been finalized. Since multiple glTF textures might be loaded + // from a single URL or buffer pointer, these caches prevent needless re-decoding. There are + // two caches: one for URL-based textures and one for buffer-based textures. BufferTextureCache mBufferTextureCache; UrlTextureCache mUrlTextureCache; - int mNumNewCacheEntries; - std::atomic mNumReadyEntries; - utils::JobSystem::Job* mDecodingJob = nullptr; + int mNumDecoderTasks; + int mNumDecoderTasksFinished; + utils::JobSystem::Job* mDecoderRootJob = nullptr; bool createTextures(details::FFilamentAsset* asset, bool async); void addTextureCacheEntry(const TextureBinding& tb); void bindTextureToMaterial(const TextureBinding& tb); + void decodeSingleTexture(); void uploadPendingTextures(); ~Impl(); }; @@ -157,11 +157,11 @@ static void importSkinningData(Skin& dstSkin, const cgltf_skin& srcSkin) { } void ResourceLoader::addResourceData(const char* url, BufferDescriptor&& buffer) { - pImpl->mUserCache.emplace(url, std::move(buffer)); + pImpl->mUriDataCache.emplace(url, std::move(buffer)); } bool ResourceLoader::hasResourceData(const char* url) const { - return pImpl->mUserCache.find(url) != pImpl->mUserCache.end(); + return pImpl->mUriDataCache.find(url) != pImpl->mUriDataCache.end(); } static void convertBytesToShorts(uint16_t* dst, const uint8_t* src, size_t count) { @@ -227,8 +227,8 @@ bool ResourceLoader::loadResources(FFilamentAsset* asset, bool async) { return false; } } else if (strstr(uri, "://") == nullptr) { - auto iter = pImpl->mUserCache.find(uri); - if (iter == pImpl->mUserCache.end()) { + auto iter = pImpl->mUriDataCache.find(uri); + if (iter == pImpl->mUriDataCache.end()) { slog.e << "Unable to load external resource: " << uri << io::endl; missingResources = true; } @@ -341,6 +341,68 @@ bool ResourceLoader::loadResources(FFilamentAsset* asset, bool async) { return pImpl->createTextures(asset, async); } +bool ResourceLoader::asyncBeginLoad(FilamentAsset* asset) { + return loadResources(upcast(asset), true); +} + +float ResourceLoader::asyncGetLoadProgress() const { + const float finished = pImpl->mNumDecoderTasksFinished; + const float total = pImpl->mNumDecoderTasks; + return total == 0 ? 0 : finished / total; +} + +void ResourceLoader::asyncUpdateLoad() { + if (!UTILS_HAS_THREADING) { + pImpl->decodeSingleTexture(); + } + pImpl->uploadPendingTextures(); +} + +void ResourceLoader::Impl::decodeSingleTexture() { + assert(!UTILS_HAS_THREADING); + int w, h, c; + + // Check if any buffer-based textures haven't been decoded yet. + for (auto& pair : mBufferTextureCache) { + const uint8_t* sourceData = (const uint8_t*) pair.first; + TextureCacheEntry* entry = pair.second.get(); + if (entry->texels) { + continue; + } + entry->texels = stbi_load_from_memory(sourceData, entry->bufferSize, &w, &h, &c, 4); + return; + } + + // Check if any URL-based textures haven't been decoded yet. + for (auto& pair : mUrlTextureCache) { + auto uri = pair.first; + TextureCacheEntry* entry = pair.second.get(); + if (entry->texels) { + continue; + } + + // First, check the user-supplied resource cache for this URL. + auto iter = mUriDataCache.find(uri); + if (iter != mUriDataCache.end()) { + const uint8_t* sourceData = (const uint8_t*) iter->second.buffer; + entry->texels = stbi_load_from_memory(sourceData, iter->second.size, &w, &h, &c, 4); + return; + } + + // Otherwise load it from the file system if this platform supports it. + #if defined(STBI_NO_STDIO) + slog.e << "Unable to load texture: " << uri << io::endl; + entry->completed = true; + mNumDecoderTasksFinished++; + return; + #else + utils::Path fullpath = this->mConfig.gltfPath.getParent() + uri; + entry->texels = stbi_load(fullpath.c_str(), &w, &h, &c, 4); + return; + #endif + } +} + void ResourceLoader::Impl::uploadPendingTextures() { auto upload = [this](TextureCacheEntry* entry, Engine& engine) { Texture* texture = entry->texture; @@ -352,7 +414,7 @@ void ResourceLoader::Impl::uploadPendingTextures() { texture->setImage(engine, 0, std::move(pbd)); texture->generateMipmaps(engine); entry->completed = true; - mNumReadyEntries++; + mNumDecoderTasksFinished++; } }; Engine& engine = *mConfig.engine; @@ -375,7 +437,6 @@ void ResourceLoader::Impl::addTextureCacheEntry(const TextureBinding& tb) { entry->srgb = tb.srgb; stbi_info_from_memory(sourceData, tb.totalSize, &entry->width, &entry->height, &entry->numComponents); - mNumNewCacheEntries++; entry->bufferSize = tb.totalSize; return; } @@ -390,12 +451,11 @@ void ResourceLoader::Impl::addTextureCacheEntry(const TextureBinding& tb) { entry->srgb = tb.srgb; // Check the user-supplied resource cache for this URL, otherwise peek at the file. - auto iter = mUserCache.find(tb.uri); - if (iter != mUserCache.end()) { + auto iter = mUriDataCache.find(tb.uri); + if (iter != mUriDataCache.end()) { const uint8_t* sourceData = (const uint8_t*) iter->second.buffer; stbi_info_from_memory(sourceData, iter->second.size, &entry->width, &entry->height, &entry->numComponents); - mNumNewCacheEntries++; return; } #if defined(STBI_NO_STDIO) @@ -403,7 +463,6 @@ void ResourceLoader::Impl::addTextureCacheEntry(const TextureBinding& tb) { #else utils::Path fullpath = directory + tb.uri; stbi_info(fullpath.c_str(), &entry->width, &entry->height, &entry->numComponents); - mNumNewCacheEntries++; #endif } @@ -428,12 +487,11 @@ void ResourceLoader::Impl::bindTextureToMaterial(const TextureBinding& tb) { bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool async) { // If any decoding jobs are still underway, wait for them to finish. utils::JobSystem* js = utils::JobSystem::getJobSystem(); - if (mDecodingJob) { - js->waitAndRelease(mDecodingJob); - mDecodingJob = nullptr; + if (mDecoderRootJob) { + js->waitAndRelease(mDecoderRootJob); + mDecoderRootJob = nullptr; } - mNumNewCacheEntries = 0; mBufferTextureCache.clear(); mUrlTextureCache.clear(); @@ -442,6 +500,17 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a addTextureCacheEntry(asset->getTextureBindings()[i]); } + // Tally up the total number of textures that need to be decoded. Zero textures is a special + // case that needs to report 100% progress right away, so we set NumDecoderTasks and Finished + // both to 1. If they were both 0, this would indicate that loading has not started. + mNumDecoderTasks = mBufferTextureCache.size() + mUrlTextureCache.size(); + if (mNumDecoderTasks == 0) { + mNumDecoderTasks = 1; + mNumDecoderTasksFinished = 1; + } else { + mNumDecoderTasksFinished = 0; + } + // Next create blank Filament textures. auto createTexture = [=](TextureCacheEntry* entry) { entry->texture = Texture::Builder() @@ -460,8 +529,15 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a bindTextureToMaterial(asset->getTextureBindings()[i]); } + // Before creating jobs for PNG / JPEG decoding, we might need to return early. On single + // threaded systems, it is usually fine to create jobs because the job system will simply + // execute serially. However if the client requests async behavior, then we need to wait + // until subsequent calls to asyncUpdateLoad(). + if (!UTILS_HAS_THREADING && async) { + return true; + } + utils::JobSystem::Job* parent = js->createJob(); - mNumReadyEntries = 0; // Kick off jobs that decode texels from buffer pointers. for (auto& pair : mBufferTextureCache) { @@ -475,14 +551,14 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a js->run(decode); } - // Kick off jobs that decode texels from URL strings. + // Kick off jobs that decode texels from URI strings. for (auto& pair : mUrlTextureCache) { auto uri = pair.first; TextureCacheEntry* entry = pair.second.get(); - // First, check the user-supplied resource cache for this URL. - auto iter = mUserCache.find(uri); - if (iter != mUserCache.end()) { + // First, check the user-supplied resource cache for this URI. + auto iter = mUriDataCache.find(uri); + if (iter != mUriDataCache.end()) { const uint8_t* sourceData = (const uint8_t*) iter->second.buffer; utils::JobSystem::Job* decode = utils::jobs::createJob(*js, parent, [=] { int width, height, comp; @@ -508,7 +584,7 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a } if (async) { - mDecodingJob = js->runAndRetain(parent); + mDecoderRootJob = js->runAndRetain(parent); return true; } @@ -523,8 +599,8 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a ResourceLoader::Impl::~Impl() { utils::JobSystem* js = utils::JobSystem::getJobSystem(); - if (mDecodingJob) { - js->waitAndRelease(mDecodingJob); + if (mDecoderRootJob) { + js->waitAndRelease(mDecoderRootJob); } } diff --git a/samples/gltf_viewer.cpp b/samples/gltf_viewer.cpp index e3ec586565..c18516b7f4 100644 --- a/samples/gltf_viewer.cpp +++ b/samples/gltf_viewer.cpp @@ -53,6 +53,7 @@ struct App { MaterialProvider* materials; MaterialSource materialSource = GENERATE_SHADERS; bool actualSize = false; + gltfio::ResourceLoader* resourceLoader = nullptr; }; static const char* DEFAULT_IBL = "venetian_crossroads_2k"; @@ -199,15 +200,15 @@ int main(int argc, char** argv) { configuration.gltfPath = filename.getAbsolutePath(); configuration.normalizeSkinningWeights = true; configuration.recomputeBoundingBoxes = false; - gltfio::ResourceLoader(configuration).loadResources(app.asset); + if (!app.resourceLoader) { + app.resourceLoader = new gltfio::ResourceLoader(configuration); + } + app.resourceLoader->asyncBeginLoad(app.asset); // Load animation data then free the source hierarchy. app.asset->getAnimator(); app.asset->releaseSourceData(); - // Add the renderables to the scene. - app.viewer->setAsset(app.asset, !app.actualSize); - auto ibl = FilamentApp::get().getIBL(); if (ibl) { app.viewer->setIndirectLight(ibl->getIndirectLight(), ibl->getSphericalHarmonics()); @@ -231,6 +232,10 @@ int main(int argc, char** argv) { loadResources(filename); app.viewer->setUiCallback([&app, scene] () { + float progress = app.resourceLoader->asyncGetLoadProgress(); + if (progress < 1.0) { + ImGui::ProgressBar(progress); + } if (ImGui::CollapsingHeader("Stats")) { ImGui::Text("%zu entities in the asset", app.asset->getEntityCount()); ImGui::Text("%zu renderables (excluding UI)", scene->getRenderableCount()); @@ -253,6 +258,13 @@ int main(int argc, char** argv) { }; auto animate = [&app](Engine* engine, View* view, double now) { + app.resourceLoader->asyncUpdateLoad(); + + // Add the renderables to the scene after the textures have finished loading. + if (app.resourceLoader->asyncGetLoadProgress() == 1.0f) { + app.viewer->setAsset(app.asset, !app.actualSize); + } + app.viewer->applyAnimation(now); }; diff --git a/web/filament-js/extensions.js b/web/filament-js/extensions.js index 84111e830d..6d57e2d0ec 100644 --- a/web/filament-js/extensions.js +++ b/web/filament-js/extensions.js @@ -254,12 +254,17 @@ Filament.loadClassExtensions = function() { // URL, but clients can do the following to resolve a relative URL: // const basePath = '' + new URL(myRelativeUrl, document.location); // If the given base path is null, document.location is used as the base. - Filament.gltfio$FilamentAsset.prototype.loadResources = function(onDone, onFetched, basePath) { + // + // The optional asyncInterval argument allows clients to control how finalization is amortized + // over time. It represents the number of milliseconds between each texture decoding task. + Filament.gltfio$FilamentAsset.prototype.loadResources = function(onDone, onFetched, basePath, + asyncInterval) { const asset = this; const engine = this.getEngine(); const names = this.getResourceUris(); const urlset = new Set(); const urlToName = {}; + const interval = asyncInterval || 30; basePath = basePath || document.location; @@ -275,15 +280,21 @@ Filament.loadClassExtensions = function() { const onComplete = function() { const finalize = function() { - resourceLoader.loadResources(asset); + resourceLoader.asyncBeginLoad(asset); - // The buffer data won't get sent to the GPU until the next call to - // "renderer.render()", so wait two frames before freeing the CPU-side data. - window.requestAnimationFrame(function() { - window.requestAnimationFrame(function() { + // Decode a PNG or JPEG every 100 milliseconds. This is slow but it's useful to + // decode in the native layer instead of using Canvas2D. This allows us to have more + // control (handling of alpha, srgb, etc) and better parity with Filament on native + // platforms. In the future we may wish to offload this to web workers. + const timer = setInterval(() => { + resourceLoader.asyncUpdateLoad(); + const progress = resourceLoader.asyncGetLoadProgress(); + if (progress >= 1) { + clearInterval(timer); resourceLoader.delete(); - }); - }); + } + }, interval); + }; if (onDone) { onDone(finalize); diff --git a/web/filament-js/jsbindings.cpp b/web/filament-js/jsbindings.cpp index fce3b9cd5f..bbcd0dd74e 100644 --- a/web/filament-js/jsbindings.cpp +++ b/web/filament-js/jsbindings.cpp @@ -1455,6 +1455,13 @@ class_("gltfio$ResourceLoader") .function("loadResources", EMBIND_LAMBDA(bool, (ResourceLoader* self, FilamentAsset* asset), { return self->loadResources(asset); - }), allow_raw_pointers()); + }), allow_raw_pointers()) + + .function("asyncBeginLoad", EMBIND_LAMBDA(bool, (ResourceLoader* self, FilamentAsset* asset), { + return self->asyncBeginLoad(asset); + }), allow_raw_pointers()) + + .function("asyncGetLoadProgress", &ResourceLoader::asyncGetLoadProgress) + .function("asyncUpdateLoad", &ResourceLoader::asyncUpdateLoad); } // EMSCRIPTEN_BINDINGS