gltfio: Introduce the asynchronous API and use it.

We were already using jobs for decoding PNG and JPEG files, but we were
doing a join. This add three methods to ResourceLoader that allow
clients to amortize the decoding process across multiple frames, even on
single-threaded platforms like WebGL.

This PR adds async loading to the following demos:
- samples/gltf_viewer (now shows a progress bar in the UI)
- android/sample-gltf-viewer
- web/samples/helmet.html

Fixes #1876.
This commit is contained in:
Philip Rideout
2020-01-29 11:23:14 -08:00
parent 503e66790b
commit c681cd243a
9 changed files with 280 additions and 77 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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();
}

View File

@@ -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.
*
* <p>For a usage example, see the documentation for {@link AssetLoader}.</p>
* <p>For a usage example, see the documentation for {@link AssetLoader}.
* All methods should be called from the main thread.</p>
*
* @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.
*
* <p><code>ResourceLoader</code> 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.</p>
* 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.
*
* <p>When loading GLB files (as opposed to JSON-based glTF files), clients typically do not
* need to call this method.</p>
* 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.
*
* <p>This is the main entry point for <code>ResourceLoader</code>, and only needs to be called
* once.</p>
* 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);
}

View File

@@ -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);

View File

@@ -28,15 +28,15 @@
#include <geometry/SurfaceOrientation.h>
#include <math/quat.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <utils/JobSystem.h>
#include <utils/Log.h>
#include <cgltf.h>
#include <math/quat.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <tsl/robin_map.h>
#include <string>
@@ -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<stbi_uc*> texels;
@@ -62,6 +61,7 @@ namespace {
using BufferTextureCache = tsl::robin_map<const void*, std::unique_ptr<TextureCacheEntry>>;
using UrlTextureCache = tsl::robin_map<std::string, std::unique_ptr<TextureCacheEntry>>;
using UriDataCache = tsl::robin_map<std::string, gltfio::ResourceLoader::BufferDescriptor>;
}
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<std::string, BufferDescriptor> 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<int> 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);
}
}

View File

@@ -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);
};

View File

@@ -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);

View File

@@ -1455,6 +1455,13 @@ class_<ResourceLoader>("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