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:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user