diff --git a/README.md b/README.md index 55e9054cd8..99d4f91503 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ steps: - [x] KHR_materials_sheen - [x] KHR_materials_transmission - [x] KHR_materials_unlit + - [x] KHR_materials_variants - [x] KHR_materials_volume - [x] KHR_mesh_quantization - [x] KHR_texture_transform diff --git a/android/gltfio-android/src/main/cpp/FilamentAsset.cpp b/android/gltfio-android/src/main/cpp/FilamentAsset.cpp index 0434526599..d4fbaaf14b 100644 --- a/android/gltfio-android/src/main/cpp/FilamentAsset.cpp +++ b/android/gltfio-android/src/main/cpp/FilamentAsset.cpp @@ -235,6 +235,30 @@ Java_com_google_android_filament_gltfio_FilamentAsset_nGetResourceUris(JNIEnv* e } } +extern "C" JNIEXPORT jint JNICALL +Java_com_google_android_filament_gltfio_FilamentAsset_nGetMaterialVariantCount(JNIEnv*, jclass, + jlong nativeAsset) { + FilamentAsset* asset = (FilamentAsset*) nativeAsset; + return (jint) asset->getMaterialVariantCount(); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_gltfio_FilamentAsset_nGetMaterialVariantNames(JNIEnv* env, jclass, + jlong nativeAsset, jobjectArray result) { + FilamentAsset* asset = (FilamentAsset*) nativeAsset; + for (int i = 0; i < asset->getMaterialVariantCount(); ++i) { + const char* name = asset->getMaterialVariantName(i); + env->SetObjectArrayElement(result, (jsize) i, env->NewStringUTF(name)); + } +} + +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_gltfio_FilamentAsset_nApplyMaterialVariant(JNIEnv* env, jclass, + jlong nativeAsset, jint variantIndex) { + FilamentAsset* asset = (FilamentAsset*) nativeAsset; + asset->applyMaterialVariant(variantIndex); +} + extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_gltfio_FilamentAsset_nReleaseSourceData(JNIEnv* env, jclass, jlong nativeAsset) { diff --git a/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/FilamentAsset.java b/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/FilamentAsset.java index dada72845b..64eb3f76ec 100644 --- a/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/FilamentAsset.java +++ b/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/FilamentAsset.java @@ -217,9 +217,11 @@ public class FilamentAsset { } /** - * Get the target name at target index in the given entity. + * Gets the morph target name at the given index in the given entity. + * + * TODO(prideout): This should be "getMorphTargetNames" */ - public String getMorphTargetNameAt(@Entity int entity, int targetIndex) { + public String getMorphTargetNameAt(@Entity int entity, @IntRange(from = 0) int targetIndex) { return nGetMorphTargetNameAt(getNativeObject(), entity, targetIndex); } @@ -232,6 +234,30 @@ public class FilamentAsset { return uris; } + /** + * Returns the names of all material variants. + */ + public @NonNull String[] getMaterialVariantNames() { + String[] names = new String[nGetMaterialVariantCount(mNativeObject)]; + nGetMaterialVariantNames(mNativeObject, names); + return names; + } + + /* + * Applies the given material variant to all primitives that it affects. + * + * This is efficient because it merely swaps around persistent MaterialInstances. If you change + * a material parameter while a certain variant is active, the updated value will be remembered + * after you re-apply that variant. + * + * If the asset is instanced, this affects all instances in the same way. + * + * Ignored if variantIndex is out of bounds. + */ + public void applyMaterialVariant(@IntRange(from = 0) int variantIndex) { + nApplyMaterialVariant(getNativeObject(), variantIndex); + } + /** * Reclaims CPU-side memory for URI strings, binding lists, and raw animation data. * @@ -268,11 +294,15 @@ public class FilamentAsset { private static native int nGetMaterialInstanceCount(long nativeAsset); private static native void nGetMaterialInstances(long nativeAsset, long[] nativeResults); + private static native int nGetMaterialVariantCount(long nativeAsset); + private static native void nGetMaterialVariantNames(long nativeAsset, String[] result); + private static native void nGetBoundingBox(long nativeAsset, float[] box); private static native String nGetName(long nativeAsset, int entity); private static native String nGetExtras(long nativeAsset, int entity); private static native long nGetAnimator(long nativeAsset); private static native String nGetMorphTargetNameAt(long nativeAsset, int entity, int targetIndex); + private static native void nApplyMaterialVariant(long nativeAsset, int variantIndex); private static native int nGetResourceUriCount(long nativeAsset); private static native void nGetResourceUris(long nativeAsset, String[] result); private static native void nReleaseSourceData(long nativeAsset); diff --git a/libs/gltfio/include/gltfio/FilamentAsset.h b/libs/gltfio/include/gltfio/FilamentAsset.h index 8778dc1884..8f73e5acbb 100644 --- a/libs/gltfio/include/gltfio/FilamentAsset.h +++ b/libs/gltfio/include/gltfio/FilamentAsset.h @@ -200,6 +200,8 @@ public: * Must be called after loadResources or asyncBeginLoad, otherwise returns null. * If the asset is instanced, this returns a "primary" animator that controls all instances. * To animate each instance individually, use \see FilamentInstance. + * + * TODO(prideout): this should return a ref not a ptr */ Animator* getAnimator() const noexcept; @@ -209,25 +211,53 @@ public: size_t getSkinCount() const noexcept; /** - * Get the skin name at skin index. + * Gets the skin name at skin index. */ const char* getSkinNameAt(size_t skinIndex) const noexcept; /** - * Get the number of joints at skin index. + * Gets the number of joints at skin index. */ size_t getJointCountAt(size_t skinIndex) const noexcept; /** - * Get joints at skin index. + * Gets joints at skin index. */ const utils::Entity* getJointsAt(size_t skinIndex) const noexcept; /** - * Get the target name at target index in the given entity. + * Gets the morph target name at the given index in the given entity. */ const char* getMorphTargetNameAt(utils::Entity entity, size_t targetIndex) const noexcept; + /** + * Returns the number of morph targets in the given entity. + */ + size_t getMorphTargetCountAt(utils::Entity entity) const noexcept; + + /** + * Returns the number of material variants in the asset. + */ + size_t getMaterialVariantCount() const noexcept; + + /** + * Returns the name of the given material variant, or null if it is out of bounds. + */ + const char* getMaterialVariantName(size_t variantIndex) const noexcept; + + /* + * Applies the given material variant to all primitives that it affects. + * + * This is efficient because it merely swaps around persistent MaterialInstances. If you change + * a material parameter while a certain variant is active, the updated value will be remembered + * after you re-apply that variant. + * + * If the asset is instanced, this affects all instances in the same way. + * + * Ignored if variantIndex is out of bounds. + */ + void applyMaterialVariant(size_t variantIndex) noexcept; + /** * Lazily creates a single LINES renderable that draws the transformed bounding-box hierarchy * for diagnostic purposes. The wireframe is owned by the asset so clients should not delete it. diff --git a/libs/gltfio/src/AssetLoader.cpp b/libs/gltfio/src/AssetLoader.cpp index e3b06d6128..93db7cbe52 100644 --- a/libs/gltfio/src/AssetLoader.cpp +++ b/libs/gltfio/src/AssetLoader.cpp @@ -289,6 +289,14 @@ void FAssetLoader::createAsset(const cgltf_data* srcAsset, size_t numInstances) mResult->mAssetExtras = CString(srcAsset->json + asset.extras.start_offset, extras_size); } + // Check if the asset has variants. + if (srcAsset->variants_count > 0) { + mResult->mVariants.reserve(srcAsset->variants_count); + for (cgltf_size i = 0, len = srcAsset->variants_count; i < len; ++i) { + mResult->mVariants.push_back({CString(srcAsset->variants[i].name)}); + } + } + if (numInstances == 0) { // For each scene root, recursively create all entities. for (cgltf_size i = 0, len = scene->nodes_count; i < len; ++i) { @@ -481,6 +489,23 @@ void FAssetLoader::createRenderable(const cgltf_data* srcAsset, const cgltf_node continue; } + // Add variants if they exist. + for (size_t i = 0, n = inputPrim->mappings_count; i < n; i++) { + const size_t variantIndex = inputPrim->mappings[i].variant; + const cgltf_material* material = inputPrim->mappings[i].material; + if (variantIndex >= mResult->mVariants.size()) { + mError = true; + break; + } + MaterialInstance* mi = createMaterialInstance(srcAsset, material, &uvmap, hasVertexColor); + if (!mi) { + mError = true; + break; + } + mResult->mDependencyGraph.addEdge(entity, mi); + mResult->mVariants[variantIndex].mappings.push_back({entity, index, mi}); + } + // Expand the object-space bounding box. aabb.min = min(outputPrim->aabb.min, aabb.min); aabb.max = max(outputPrim->aabb.max, aabb.max); @@ -499,9 +524,9 @@ void FAssetLoader::createRenderable(const cgltf_data* srcAsset, const cgltf_node auto& morphTargetNames = mResult->mMorphTargetNames[entity]; assert_invariant(morphTargetNames.empty()); - morphTargetNames.resize(numMorphTargets); + morphTargetNames = FixedCapacityVector(numMorphTargets); for (cgltf_size i = 0, c = mesh->target_names_count; i < c; ++i) { - morphTargetNames[i] = utils::StaticString::make(mesh->target_names[i]); + morphTargetNames[i] = StaticString::make(mesh->target_names[i]); } const Aabb transformed = aabb.transform(worldTransform); diff --git a/libs/gltfio/src/FFilamentAsset.h b/libs/gltfio/src/FFilamentAsset.h index 9a0b364086..b8ee8a0db2 100644 --- a/libs/gltfio/src/FFilamentAsset.h +++ b/libs/gltfio/src/FFilamentAsset.h @@ -203,6 +203,14 @@ struct FFilamentAsset : public FilamentAsset { const char* getMorphTargetNameAt(utils::Entity entity, size_t targetIndex) const noexcept; + size_t getMorphTargetCountAt(utils::Entity entity) const noexcept; + + size_t getMaterialVariantCount() const noexcept; + + const char* getMaterialVariantName(size_t variantIndex) const noexcept; + + void applyMaterialVariant(size_t variantIndex) noexcept; + utils::Entity getWireframe() noexcept; filament::Engine* getEngine() const noexcept { @@ -238,6 +246,17 @@ struct FFilamentAsset : public FilamentAsset { void createAnimators(); + struct VariantMapping { + utils::Entity renderable; + size_t primitiveIndex; + filament::MaterialInstance* material; + }; + + struct Variant { + utils::CString name; + std::vector mappings; + }; + filament::Engine* mEngine; utils::NameComponentManager* mNameManager; utils::EntityManager* mEntityManager; @@ -250,6 +269,7 @@ struct FFilamentAsset : public FilamentAsset { std::vector mIndexBuffers; std::vector mMorphTargetBuffers; std::vector mTextures; + utils::FixedCapacityVector mVariants; filament::Aabb mBoundingBox; utils::Entity mRoot; std::vector mInstances; @@ -260,7 +280,7 @@ struct FFilamentAsset : public FilamentAsset { DependencyGraph mDependencyGraph; tsl::htrie_map> mNameToEntity; tsl::robin_map mNodeExtras; - tsl::robin_map> mMorphTargetNames; + tsl::robin_map> mMorphTargetNames; utils::CString mAssetExtras; // Sentinels for situations where ResourceLoader needs to generate data. diff --git a/libs/gltfio/src/FilamentAsset.cpp b/libs/gltfio/src/FilamentAsset.cpp index 9d68d31f4a..d61892ef2a 100644 --- a/libs/gltfio/src/FilamentAsset.cpp +++ b/libs/gltfio/src/FilamentAsset.cpp @@ -18,6 +18,8 @@ #include +#include + #include #include #include @@ -135,6 +137,42 @@ const char* FFilamentAsset::getMorphTargetNameAt(utils::Entity entity, return morphTargetNames[targetIndex].c_str(); } +size_t FFilamentAsset::getMorphTargetCountAt(utils::Entity entity) const noexcept { + if (!mResourcesLoaded) { + return 0; + } + + const auto iter = mMorphTargetNames.find(entity); + if (iter == mMorphTargetNames.end()) { + return 0; + } + + return iter->second.size(); +} + +size_t FFilamentAsset::getMaterialVariantCount() const noexcept { + return mVariants.size(); +} + +const char* FFilamentAsset::getMaterialVariantName(size_t variantIndex) const noexcept { + if (variantIndex >= mVariants.size()) { + return nullptr; + } + return mVariants[variantIndex].name.c_str(); +} + +void FFilamentAsset::applyMaterialVariant(size_t variantIndex) noexcept { + if (variantIndex >= mVariants.size()) { + return; + } + const std::vector& mappings = mVariants[variantIndex].mappings; + RenderableManager& rm = mEngine->getRenderableManager(); + for (const auto& mapping : mappings) { + auto instance = rm.getInstance(mapping.renderable); + rm.setMaterialInstanceAt(instance, mapping.primitiveIndex, mapping.material); + } +} + Entity FFilamentAsset::getWireframe() noexcept { if (!mWireframe) { mWireframe = new Wireframe(this); @@ -335,6 +373,22 @@ const char* FilamentAsset::getMorphTargetNameAt(utils::Entity entity, return upcast(this)->getMorphTargetNameAt(entity, targetIndex); } +size_t FilamentAsset::getMorphTargetCountAt(utils::Entity entity) const noexcept { + return upcast(this)->getMorphTargetCountAt(entity); +} + +const char* FilamentAsset::getMaterialVariantName(size_t variantIndex) const noexcept { + return upcast(this)->getMaterialVariantName(variantIndex); +} + +void FilamentAsset::applyMaterialVariant(size_t variantIndex) noexcept { + return upcast(this)->applyMaterialVariant(variantIndex); +} + +size_t FilamentAsset::getMaterialVariantCount() const noexcept { + return upcast(this)->getMaterialVariantCount(); +} + Entity FilamentAsset::getWireframe() noexcept { return upcast(this)->getWireframe(); } diff --git a/libs/viewer/include/viewer/SimpleViewer.h b/libs/viewer/include/viewer/SimpleViewer.h index 72dc129780..24cae0c2b4 100644 --- a/libs/viewer/include/viewer/SimpleViewer.h +++ b/libs/viewer/include/viewer/SimpleViewer.h @@ -237,6 +237,7 @@ private: // Properties that can be changed from the UI. int mCurrentAnimation = 1; // It is a 1-based index and 0 means not playing animation + int mCurrentVariant = 0; bool mResetAnimation = true; bool mEnableWireframe = false; int mVsmMsaaSamplesLog2 = 1; diff --git a/libs/viewer/src/SimpleViewer.cpp b/libs/viewer/src/SimpleViewer.cpp index 5169d9e5b8..4512a8e693 100644 --- a/libs/viewer/src/SimpleViewer.cpp +++ b/libs/viewer/src/SimpleViewer.cpp @@ -966,8 +966,24 @@ void SimpleViewer::updateUserInterface() { ImGui::Unindent(); } - // We do not yet support animation selection in the remote UI. To support this feature, we - // would need to send a message from DebugServer to the WebSockets client. + // TODO(prideout): add support for animation and variant selection in the remote UI. To + // support these features, we will need to send a message (list of strings) from DebugServer + // to the WebSockets client. + + if (mAsset->getMaterialVariantCount() > 0 && ImGui::CollapsingHeader("Variants")) { + ImGui::Indent(); + int selectedVariant = mCurrentVariant; + for (size_t i = 0, count = mAsset->getMaterialVariantCount(); i < count; ++i) { + const char* label = mAsset->getMaterialVariantName(i); + ImGui::RadioButton(label, &selectedVariant, i); + } + if (selectedVariant != mCurrentVariant) { + mCurrentVariant = selectedVariant; + mAsset->applyMaterialVariant(mCurrentVariant); + } + ImGui::Unindent(); + } + if (mAnimator->getAnimationCount() > 0 && ImGui::CollapsingHeader("Animation")) { ImGui::Indent(); int selectedAnimation = mCurrentAnimation; diff --git a/web/filament-js/extensions.js b/web/filament-js/extensions.js index 952060ba80..bc1d950f0f 100644 --- a/web/filament-js/extensions.js +++ b/web/filament-js/extensions.js @@ -589,7 +589,6 @@ Filament.loadClassExtensions = function() { asyncInterval, config) { const asset = this; const engine = this.getEngine(); - const names = this.getResourceUris(); const interval = asyncInterval || 30; const defaults = { normalizeSkinningWeights: true, @@ -605,13 +604,10 @@ Filament.loadClassExtensions = function() { // Construct the set of URI strings to fetch. const urlset = new Set(); const urlToName = {}; - for (let i = 0; i < names.size(); i++) { - const name = names.get(i); - if (name) { - const url = '' + new URL(name, basePath); - urlToName[url] = name; - urlset.add(url); - } + for (const name of this.getResourceUris()) { + const url = '' + new URL(name, basePath); + urlToName[url] = name; + urlset.add(url); } // Construct a resource loader and start decoding after all textures are fetched. @@ -672,4 +668,12 @@ Filament.loadClassExtensions = function() { Filament.gltfio$FilamentAsset.prototype.getCameraEntities = function() { return Filament.vectorToArray(this._getCameraEntities()); }; + + Filament.gltfio$FilamentAsset.prototype.getResourceUris = function(buffer, instances) { + return Filament.vectorToArray(this._getResourceUris()); + } + + Filament.gltfio$FilamentAsset.prototype.getMaterialVariantNames = function(buffer, instances) { + return Filament.vectorToArray(this._getMaterialVariantNames()); + } }; diff --git a/web/filament-js/filament-viewer.js b/web/filament-js/filament-viewer.js index c79622d89b..24039b192d 100644 --- a/web/filament-js/filament-viewer.js +++ b/web/filament-js/filament-viewer.js @@ -63,6 +63,7 @@ class FilamentViewer extends LitElement { this.sky = null; // Path to skybox ktx. this.enableDrop = null; // Enables drag and drop. this.intensity = 30000; // Intensity of the image based light. + this.materialVariant = 0; // Index of material variant. // Private properties: this.filamentTasks = new FilamentTasks(); @@ -79,6 +80,7 @@ class FilamentViewer extends LitElement { sky: { type: String }, enableDrop: { type: Boolean }, intensity: { type: Number }, + materialVariant: { type: Number }, } } @@ -111,6 +113,7 @@ class FilamentViewer extends LitElement { if (props.has("intensity") && this.indirectLight) { this.indirectLight.setIntensity(this.intensity); } + if (props.has("materialVariant") && this.asset) this._applyMaterialVariant(); } static get styles() { @@ -370,6 +373,7 @@ class FilamentViewer extends LitElement { this.asset.loadResources(() => { this.animator = this.asset.getAnimator(); this.animationStartTime = Date.now(); + this._applyMaterialVariant(); }, null, basePath); this._updateOverlay(); @@ -423,6 +427,20 @@ class FilamentViewer extends LitElement { window.requestAnimationFrame(this._renderFrame.bind(this)); } + + _applyMaterialVariant() { + if (!this.hasAttribute("materialVariant")) { + return; + } + const names = this.asset.getMaterialVariantNames(); + const index = this.materialVariant; + if (index < 0 || index >= names.length) { + console.error(`Material variant ${index} does not exist in this asset.`); + return; + } + console.info(this.src, `Applying material variant: ${names[index]}`); + this.asset.applyMaterialVariant(index); + } } customElements.define("filament-viewer", FilamentViewer); diff --git a/web/filament-js/jsbindings.cpp b/web/filament-js/jsbindings.cpp index cf5e4bd7fb..3d38d1f379 100644 --- a/web/filament-js/jsbindings.cpp +++ b/web/filament-js/jsbindings.cpp @@ -1822,6 +1822,8 @@ class_("gltfio$FilamentAsset") .function("popRenderable", &FilamentAsset::popRenderable) + .function("applyMaterialVariant", &FilamentAsset::applyMaterialVariant) + .function("getMaterialInstances", EMBIND_LAMBDA(std::vector, (FilamentAsset* self), { const filament::MaterialInstance* const* ptr = self->getMaterialInstances(); @@ -1834,7 +1836,7 @@ class_("gltfio$FilamentAsset") return std::vector(ptr, ptr + self->getAssetInstanceCount()); }), allow_raw_pointers()) - .function("getResourceUris", EMBIND_LAMBDA(std::vector, (FilamentAsset* self), { + .function("_getResourceUris", EMBIND_LAMBDA(std::vector, (FilamentAsset* self), { std::vector retval; auto uris = self->getResourceUris(); for (size_t i = 0, len = self->getResourceUriCount(); i < len; ++i) { @@ -1843,6 +1845,14 @@ class_("gltfio$FilamentAsset") return retval; }), allow_raw_pointers()) + .function("_getMaterialVariantNames", EMBIND_LAMBDA(std::vector, (FilamentAsset* self), { + std::vector retval(self->getMaterialVariantCount()); + for (size_t i = 0, len = retval.size(); i < len; ++i) { + retval[i] = self->getMaterialVariantName(i); + } + return retval; + }), allow_raw_pointers()) + .function("getBoundingBox", &FilamentAsset::getBoundingBox) .function("getName", EMBIND_LAMBDA(std::string, (FilamentAsset* self, utils::Entity entity), { return std::string(self->getName(entity)); diff --git a/web/samples/test-filament-viewer.html b/web/samples/test-filament-viewer.html index df10b867a8..ee15fc579d 100644 --- a/web/samples/test-filament-viewer.html +++ b/web/samples/test-filament-viewer.html @@ -83,7 +83,8 @@ This is a demonstration of the <filament-viewer> web componen

No skybox, no IBL, fixed size, round border