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 b9eedd015b..a36c89501c 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 @@ -71,6 +71,7 @@ class ModelViewer(val engine: Engine) : android.view.View.OnTouchListener { var normalizeSkinningWeights = true var recomputeBoundingBoxes = false + var ignoreBindTransform = false var cameraFocalLength = 28f set(value) { @@ -111,7 +112,7 @@ class ModelViewer(val engine: Engine) : android.view.View.OnTouchListener { view.camera = camera assetLoader = AssetLoader(engine, UbershaderLoader(engine), EntityManager.get()) - resourceLoader = ResourceLoader(engine, normalizeSkinningWeights, recomputeBoundingBoxes) + resourceLoader = ResourceLoader(engine, normalizeSkinningWeights, recomputeBoundingBoxes, ignoreBindTransform) // Always add a direct light source since it is required for shadowing. // We highly recommend adding an indirect light as well. diff --git a/android/gltfio-android/src/main/cpp/ResourceLoader.cpp b/android/gltfio-android/src/main/cpp/ResourceLoader.cpp index 2795eb6e7f..c16ba604ed 100644 --- a/android/gltfio-android/src/main/cpp/ResourceLoader.cpp +++ b/android/gltfio-android/src/main/cpp/ResourceLoader.cpp @@ -35,10 +35,11 @@ static void destroy(void*, size_t, void *userData) { extern "C" JNIEXPORT jlong JNICALL Java_com_google_android_filament_gltfio_ResourceLoader_nCreateResourceLoader(JNIEnv*, jclass, - jlong nativeEngine, jboolean normalizeSkinningWeights, jboolean recomputeBoundingBoxes) { + jlong nativeEngine, jboolean normalizeSkinningWeights, jboolean recomputeBoundingBoxes, + jboolean ignoreBindTransform) { Engine* engine = (Engine*) nativeEngine; return (jlong) new ResourceLoader({ engine, {}, (bool) normalizeSkinningWeights, - (bool) recomputeBoundingBoxes }); + (bool) recomputeBoundingBoxes, (bool) ignoreBindTransform}); } extern "C" JNIEXPORT void JNICALL 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 bd39de2aaf..976a752caf 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 @@ -45,7 +45,7 @@ public class ResourceLoader { */ public ResourceLoader(@NonNull Engine engine) { long nativeEngine = engine.getNativeObject(); - mNativeObject = nCreateResourceLoader(nativeEngine, false, false); + mNativeObject = nCreateResourceLoader(nativeEngine, false, false, false); } /** @@ -54,14 +54,15 @@ public class ResourceLoader { * @param engine the engine that gets passed to all builder methods * @param normalizeSkinningWeights scale non-conformant skinning weights so they sum to 1 * @param recomputeBoundingBoxes use computed bounding boxes rather than the ones in the asset + * @param ignoreBindTransform ignore skinned primitives bind transform when compute bounding boxes * @throws IllegalAccessException * @throws InvocationTargetException */ public ResourceLoader(@NonNull Engine engine, boolean normalizeSkinningWeights, - boolean recomputeBoundingBoxes) { + boolean recomputeBoundingBoxes, boolean ignoreBindTransform) { long nativeEngine = engine.getNativeObject(); mNativeObject = nCreateResourceLoader(nativeEngine, normalizeSkinningWeights, - recomputeBoundingBoxes); + recomputeBoundingBoxes, ignoreBindTransform); } /** @@ -166,7 +167,7 @@ public class ResourceLoader { } private static native long nCreateResourceLoader(long nativeEngine, - boolean normalizeSkinningWeights, boolean recomputeBoundingBoxes); + boolean normalizeSkinningWeights, boolean recomputeBoundingBoxes, boolean ignoreBindTransform); private static native void nDestroyResourceLoader(long nativeLoader); private static native void nAddResourceData(long nativeLoader, String url, Buffer buffer, int remaining); diff --git a/docs/remote/filament.js b/docs/remote/filament.js index e70870f6f4..e101956fdd 100644 --- a/docs/remote/filament.js +++ b/docs/remote/filament.js @@ -713,7 +713,8 @@ Filament.loadClassExtensions = function() { const interval = asyncInterval || 30; const defaults = { normalizeSkinningWeights: true, - recomputeBoundingBoxes: false + recomputeBoundingBoxes: false, + ignoreBindTransform: false }; config = Object.assign(defaults, config || {}); @@ -736,7 +737,8 @@ Filament.loadClassExtensions = function() { // Construct a resource loader and start decoding after all textures are fetched. const resourceLoader = new Filament.gltfio$ResourceLoader(engine, config.normalizeSkinningWeights, - config.recomputeBoundingBoxes); + config.recomputeBoundingBoxes, + config.ignoreBindTransform); const onComplete = () => { resourceLoader.asyncBeginLoad(asset); diff --git a/docs/viewer/filament-viewer.js b/docs/viewer/filament-viewer.js index 594f6ca535..79f2f6b989 100644 --- a/docs/viewer/filament-viewer.js +++ b/docs/viewer/filament-viewer.js @@ -307,6 +307,7 @@ class FilamentViewer extends LitElement { const config = { normalizeSkinningWeights: true, recomputeBoundingBoxes: false, + ignoreBindTransform: false, asyncInterval: 30 }; @@ -333,7 +334,8 @@ class FilamentViewer extends LitElement { const resourceLoader = new Filament.gltfio$ResourceLoader(this.engine, config.normalizeSkinningWeights, - config.recomputeBoundingBoxes); + config.recomputeBoundingBoxes, + config.ignoreBindTransform); let remaining = Object.keys(this.srcBlobResources).length; for (const name in this.srcBlobResources) { diff --git a/docs/webgl/filament.js b/docs/webgl/filament.js index 1e65c86c3b..b2a6327518 100644 --- a/docs/webgl/filament.js +++ b/docs/webgl/filament.js @@ -713,7 +713,8 @@ Filament.loadClassExtensions = function() { const interval = asyncInterval || 30; const defaults = { normalizeSkinningWeights: true, - recomputeBoundingBoxes: false + recomputeBoundingBoxes: false, + ignoreBindTransform: false }; config = Object.assign(defaults, config || {}); @@ -736,7 +737,8 @@ Filament.loadClassExtensions = function() { // Construct a resource loader and start decoding after all textures are fetched. const resourceLoader = new Filament.gltfio$ResourceLoader(engine, config.normalizeSkinningWeights, - config.recomputeBoundingBoxes); + config.recomputeBoundingBoxes, + config.ignoreBindTransform); const onComplete = () => { resourceLoader.asyncBeginLoad(asset); diff --git a/libs/gltfio/include/gltfio/ResourceLoader.h b/libs/gltfio/include/gltfio/ResourceLoader.h index b69a16e565..a92a4f8517 100644 --- a/libs/gltfio/include/gltfio/ResourceLoader.h +++ b/libs/gltfio/include/gltfio/ResourceLoader.h @@ -52,6 +52,10 @@ struct ResourceConfiguration { //! If true, computes the bounding boxes of all \c POSITION attibutes. Well formed glTF files //! do not need this, but it is useful for robustness. bool recomputeBoundingBoxes; + + //! If true, ignore skinned primitives bind transform when compute bounding box. Implicitly true + //! for instanced asset. Only applicable when recomputeBoundingBoxes is set to true + bool ignoreBindTransform; }; /** diff --git a/libs/gltfio/src/ResourceLoader.cpp b/libs/gltfio/src/ResourceLoader.cpp index 179bea3e06..4d7142b9dd 100644 --- a/libs/gltfio/src/ResourceLoader.cpp +++ b/libs/gltfio/src/ResourceLoader.cpp @@ -86,13 +86,19 @@ struct ResourceLoader::Impl { mEngine = config.engine; mNormalizeSkinningWeights = config.normalizeSkinningWeights; mRecomputeBoundingBoxes = config.recomputeBoundingBoxes; + mIgnoreBindTransform = config.ignoreBindTransform; } Engine* mEngine; bool mNormalizeSkinningWeights; bool mRecomputeBoundingBoxes; + bool mIgnoreBindTransform; std::string mGltfPath; + // This is used to calculate skinIndex when updateBoundingBoxes, so that the correspondency between + // cgltf_node* and FFilamentInstance::Skin can be retrieved. This pointer doesn't need to be freed. + cgltf_skin* cgltfSkinBaseAddress; + // 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. UriDataCache mUriDataCache; @@ -458,6 +464,11 @@ bool ResourceLoader::loadResources(FFilamentAsset* asset, bool async) { } if (pImpl->mRecomputeBoundingBoxes) { + // asset->mSkins is unused for instanced assets + if (!pImpl->mIgnoreBindTransform) { + pImpl->mIgnoreBindTransform = asset->isInstanced(); + } + pImpl->cgltfSkinBaseAddress = &gltf->skins[0]; updateBoundingBoxes(asset); } @@ -1005,27 +1016,99 @@ void ResourceLoader::updateBoundingBoxes(FFilamentAsset* asset) const { *result = aabb; }; - // Collect all mesh primitives that we wish to find bounds for. - std::vector prims; + const size_t posAttrSize = cgltf_num_components(cgltf_type_vec3); + const size_t skinningAttrSize = cgltf_num_components(cgltf_type_vec4); + auto computeBoundingBoxSkinned = [&](const cgltf_primitive* prim, const Skin* skin, Aabb* result) { + Aabb aabb; + std::vector inverseGlobalTransforms(skin->targets.size()); + for (size_t i = 0; i < skin->targets.size(); i++) { + auto xformable = tm.getInstance(skin->targets[i]); + if (xformable) { + inverseGlobalTransforms[i] = inverse(tm.getWorldTransform(xformable)); + } + } + std::vector verts; + std::vector rawJoints; + std::vector weights; + for (cgltf_size slot = 0; slot < prim->attributes_count; slot++) { + const cgltf_attribute& attr = prim->attributes[slot]; + const cgltf_accessor* accessor = attr.data; + if (attr.type == cgltf_attribute_type_position && cgltf_num_components(accessor->type) >= posAttrSize) { + verts.resize(accessor->count * posAttrSize); + cgltf_accessor_unpack_floats(accessor, &verts[0], accessor->count * posAttrSize); + } + if (attr.type == cgltf_attribute_type_joints && cgltf_num_components(accessor->type) >= skinningAttrSize) { + rawJoints.resize(accessor->count * skinningAttrSize); + cgltf_accessor_unpack_floats(accessor, &rawJoints[0], accessor->count * skinningAttrSize); + } + if (attr.type == cgltf_attribute_type_weights && cgltf_num_components(accessor->type) >= skinningAttrSize) { + weights.resize(accessor->count * skinningAttrSize); + cgltf_accessor_unpack_floats(accessor, &weights[0], accessor->count * skinningAttrSize); + } + } + std::vector jointIndices(rawJoints.begin(), rawJoints.end()); + auto primitiveCount = static_cast(verts.size() / posAttrSize); + for (size_t i = 0; i < primitiveCount; i++) { + float3 point(verts[posAttrSize * i], verts[posAttrSize * i + 1], verts[posAttrSize * i + 2]); + mat4f tmp = mat4f(0.0f); + for (size_t j = 0; j < skinningAttrSize; j++) { + size_t jointIndex = jointIndices[skinningAttrSize * i + j]; + float jointWeight = weights[skinningAttrSize * i + j]; + Entity jointEntity = skin->joints[jointIndex]; + mat4f globalJointTransform = tm.getWorldTransform(tm.getInstance(jointEntity)); + mat4f inverseBindMatrix = skin->inverseBindMatrices[jointIndex]; + tmp += jointWeight * globalJointTransform * inverseBindMatrix; + } + for (const auto& inverseGlobalTransform: inverseGlobalTransforms) { + mat4f skinMatrix = inverseGlobalTransform * tmp; + if (!pImpl->mNormalizeSkinningWeights) { + skinMatrix /= skinMatrix[3].w; + } + float3 skinnedPoint = (point.x * skinMatrix[0] + point.y * skinMatrix[1] + point.z * skinMatrix[2] + skinMatrix[3]).xyz; + aabb.min = min(aabb.min, skinnedPoint); + aabb.max = max(aabb.max, skinnedPoint); + } + } + *result = aabb; + }; + + // Collect all mesh primitives that we wish to find bounds for. For each mesh primitive, we also + // collect the skin it bound to (nullptr if not skinned) for bounds computation. + std::vector> primitives; for (auto iter : nodeMap) { - const cgltf_mesh* mesh = iter.first->mesh; - if (mesh) { - for (cgltf_size index = 0, nprims = mesh->primitives_count; index < nprims; ++index) { - prims.push_back(&mesh->primitives[index]); + const Skin* skin = nullptr; + cgltf_skin* const cgltfSkin = iter.first->skin; + if (cgltfSkin) { + // importSkins unpacked cgltfSkin into FFilamentInstance::SkinVector bijectively so that + // the unpacked Skin can be retrieved given cgltfSkin index + int skinIndex = cgltfSkin - pImpl->cgltfSkinBaseAddress; + skin = &asset->mSkins[skinIndex]; + } + const cgltf_mesh* cgltfMesh = iter.first->mesh; + if (cgltfMesh) { + for (cgltf_size index = 0, nprims = cgltfMesh->primitives_count; index < nprims; ++index) { + primitives.push_back({&cgltfMesh->primitives[index], skin}); } } } // Kick off a bounding box job for every primitive. - std::vector bounds(prims.size()); + std::vector bounds(primitives.size()); JobSystem* js = &pImpl->mEngine->getJobSystem(); JobSystem::Job* parent = js->createJob(); - for (size_t i = 0; i < prims.size(); ++i) { - cgltf_primitive const* prim = prims[i]; + for (size_t i = 0; i < primitives.size(); ++i) { Aabb* result = &bounds[i]; - js->run(jobs::createJob(*js, parent, [prim, result, computeBoundingBox] { - computeBoundingBox(prim, result); - })); + if (pImpl->mIgnoreBindTransform || !primitives[i].second) { + cgltf_primitive const* prim = primitives[i].first; + js->run(jobs::createJob(*js, parent, [prim, result, computeBoundingBox] { + computeBoundingBox(prim, result); + })); + } else { + std::pair skinnedPrimitive = primitives[i]; + js->run(jobs::createJob(*js, parent, [skinnedPrimitive, result, computeBoundingBoxSkinned] { + computeBoundingBoxSkinned(skinnedPrimitive.first, skinnedPrimitive.second, result); + })); + } } js->runAndWait(parent); diff --git a/samples/gltf_viewer.cpp b/samples/gltf_viewer.cpp index 60d886ebb5..140271ec56 100644 --- a/samples/gltf_viewer.cpp +++ b/samples/gltf_viewer.cpp @@ -85,6 +85,7 @@ struct App { gltfio::ResourceLoader* resourceLoader = nullptr; bool recomputeAabb = false; + bool ignoreBindTransform = false; bool actualSize = false; @@ -133,6 +134,8 @@ static void printUsage(char* name) { " Do not scale the model to fit into a unit cube\n\n" " --recompute-aabb, -r\n" " Ignore the min/max attributes in the glTF file\n\n" + " --ignore-bind-transform, -g\n" + " Ignore bind transform when recomputing aabb\n\n" " --settings=, -t\n" " Apply the settings in the given JSON file\n\n" " --ubershader, -u\n" @@ -161,19 +164,20 @@ static std::ifstream::pos_type getFileSize(const char* filename) { } static int handleCommandLineArguments(int argc, char* argv[], App* app) { - static constexpr const char* OPTSTR = "ha:i:usc:rt:b:ev"; + static constexpr const char* OPTSTR = "ha:i:usc:rgt:b:ev"; static const struct option OPTIONS[] = { - { "help", no_argument, nullptr, 'h' }, - { "api", required_argument, nullptr, 'a' }, - { "batch", required_argument, nullptr, 'b' }, - { "headless", no_argument, nullptr, 'e' }, - { "ibl", required_argument, nullptr, 'i' }, - { "ubershader", no_argument, nullptr, 'u' }, - { "actual-size", no_argument, nullptr, 's' }, - { "camera", required_argument, nullptr, 'c' }, - { "recompute-aabb", no_argument, nullptr, 'r' }, - { "settings", required_argument, nullptr, 't' }, - { "split-view", no_argument, nullptr, 'v' }, + { "help", no_argument, nullptr, 'h' }, + { "api", required_argument, nullptr, 'a' }, + { "batch", required_argument, nullptr, 'b' }, + { "headless", no_argument, nullptr, 'e' }, + { "ibl", required_argument, nullptr, 'i' }, + { "ubershader", no_argument, nullptr, 'u' }, + { "actual-size", no_argument, nullptr, 's' }, + { "camera", required_argument, nullptr, 'c' }, + { "recompute-aabb", no_argument, nullptr, 'r' }, + { "ignore-bind-transform", no_argument, nullptr, 'g' }, + { "settings", required_argument, nullptr, 't' }, + { "split-view", no_argument, nullptr, 'v' }, { nullptr, 0, nullptr, 0 } }; int opt; @@ -220,6 +224,9 @@ static int handleCommandLineArguments(int argc, char* argv[], App* app) { case 'r': app->recomputeAabb = true; break; + case 'g': + app->ignoreBindTransform = true; + break; case 't': app->settingsFile = arg; break; @@ -412,6 +419,7 @@ int main(int argc, char** argv) { configuration.engine = app.engine; configuration.gltfPath = gltfPath.c_str(); configuration.recomputeBoundingBoxes = app.recomputeAabb; + configuration.ignoreBindTransform = app.ignoreBindTransform; configuration.normalizeSkinningWeights = true; if (!app.resourceLoader) { app.resourceLoader = new gltfio::ResourceLoader(configuration); diff --git a/web/filament-js/extensions.js b/web/filament-js/extensions.js index 1684e11b75..346ff848dd 100644 --- a/web/filament-js/extensions.js +++ b/web/filament-js/extensions.js @@ -578,7 +578,8 @@ Filament.loadClassExtensions = function() { const interval = asyncInterval || 30; const defaults = { normalizeSkinningWeights: true, - recomputeBoundingBoxes: false + recomputeBoundingBoxes: false, + ignoreBindTransform: false }; config = Object.assign(defaults, config || {}); @@ -601,7 +602,8 @@ Filament.loadClassExtensions = function() { // Construct a resource loader and start decoding after all textures are fetched. const resourceLoader = new Filament.gltfio$ResourceLoader(engine, config.normalizeSkinningWeights, - config.recomputeBoundingBoxes); + config.recomputeBoundingBoxes, + config.ignoreBindTransform); const onComplete = () => { resourceLoader.asyncBeginLoad(asset); diff --git a/web/filament-js/filament-viewer.js b/web/filament-js/filament-viewer.js index 594f6ca535..79f2f6b989 100644 --- a/web/filament-js/filament-viewer.js +++ b/web/filament-js/filament-viewer.js @@ -307,6 +307,7 @@ class FilamentViewer extends LitElement { const config = { normalizeSkinningWeights: true, recomputeBoundingBoxes: false, + ignoreBindTransform: false, asyncInterval: 30 }; @@ -333,7 +334,8 @@ class FilamentViewer extends LitElement { const resourceLoader = new Filament.gltfio$ResourceLoader(this.engine, config.normalizeSkinningWeights, - config.recomputeBoundingBoxes); + config.recomputeBoundingBoxes, + config.ignoreBindTransform); let remaining = Object.keys(this.srcBlobResources).length; for (const name in this.srcBlobResources) { diff --git a/web/filament-js/jsbindings.cpp b/web/filament-js/jsbindings.cpp index f2694ecd2b..1c29b48897 100644 --- a/web/filament-js/jsbindings.cpp +++ b/web/filament-js/jsbindings.cpp @@ -1910,12 +1910,13 @@ class_("gltfio$AssetLoader") class_("gltfio$ResourceLoader") .constructor(EMBIND_LAMBDA(ResourceLoader*, (Engine* engine, bool normalizeSkinningWeights, - bool recomputeBoundingBoxes), { + bool recomputeBoundingBoxes, bool ignoreBindTransform), { return new ResourceLoader({ .engine = engine, .gltfPath = nullptr, .normalizeSkinningWeights = normalizeSkinningWeights, - .recomputeBoundingBoxes = recomputeBoundingBoxes + .recomputeBoundingBoxes = recomputeBoundingBoxes, + .ignoreBindTransform = ignoreBindTransform }); }), allow_raw_pointers())