From 18dc2f9673b990d093b94ebee31bc5dcf002e246 Mon Sep 17 00:00:00 2001 From: Matt Deardorff Date: Tue, 27 Jan 2026 12:02:50 -0500 Subject: [PATCH 1/3] Add support for KHR_materials_variants extension --- CHANGES.md | 5 + Source/Runtime/CesiumMaterialVariants.cs | 195 ++++++++++++++++++ Source/Runtime/CesiumMaterialVariants.cs.meta | 2 + Source/Runtime/ConfigureReinterop.cs | 11 +- .../Runtime/CesiumMaterialVariantsUtility.cpp | 138 +++++++++++++ .../Runtime/CesiumMaterialVariantsUtility.h | 47 +++++ .../Runtime/UnityPrepareRendererResources.cpp | 18 +- .../Runtime/UnityPrepareRendererResources.h | 13 ++ 8 files changed, 426 insertions(+), 3 deletions(-) create mode 100644 Source/Runtime/CesiumMaterialVariants.cs create mode 100644 Source/Runtime/CesiumMaterialVariants.cs.meta create mode 100644 native~/src/Runtime/CesiumMaterialVariantsUtility.cpp create mode 100644 native~/src/Runtime/CesiumMaterialVariantsUtility.h diff --git a/CHANGES.md b/CHANGES.md index 920f1f21..2658bbbc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,11 @@ ## ? - ? +##### Additions :tada: + +- Added support for the `KHR_materials_variants` glTF extension. Primitives with this extension will have a `CesiumMaterialVariants` component that allows switching between material variants at runtime using `SetVariant(int index)` or `SetVariant(string name)`. + + ##### Fixes :wrench: - Fixed a typo in the the name of the `CesiumGoogleMapTilesRasterOverlay.cs` file that prevented users from adding this component to a `GameObject` in more recent versions of Unity. diff --git a/Source/Runtime/CesiumMaterialVariants.cs b/Source/Runtime/CesiumMaterialVariants.cs new file mode 100644 index 00000000..2f549a40 --- /dev/null +++ b/Source/Runtime/CesiumMaterialVariants.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace CesiumForUnity +{ + /// + /// Represents KHR_materials_variants of a glTF primitive in a . + /// Allows switching between different material variants at runtime. + /// + /// + /// This component is automatically added to primitive game objects if they + /// contain the KHR_materials_variants extension. + /// + [IconAttribute("Packages/com.cesium.unity/Editor/Resources/Cesium-24x24.png")] + [AddComponentMenu("")] + public partial class CesiumMaterialVariants : MonoBehaviour + { + /// + /// Names of all available variants for this model (from the root glTF extension). + /// + public string[] variantNames + { + get; internal set; + } + + /// + /// The default material (the one specified in primitive.material). + /// + internal Material defaultMaterial + { + get; set; + } + + /// + /// Dictionary mapping variant indices to their corresponding materials. + /// Key = variant index, Value = Unity Material for that variant. + /// + internal Dictionary variantMaterials + { + get; set; + } = new Dictionary(); + + private MeshRenderer _meshRenderer; + private int _currentVariantIndex = -1; // -1 means default material is active + + void Start() + { + _meshRenderer = GetComponent(); + } + + /// + /// Gets the index of the currently active variant, or -1 if the default material is active. + /// + public int GetCurrentVariantIndex() + { + return _currentVariantIndex; + } + + /// + /// Gets the name of the currently active variant, or "Default" if the default material is active. + /// + public string GetCurrentVariantName() + { + if (_currentVariantIndex < 0 || _currentVariantIndex >= variantNames.Length) + { + return "Default"; + } + return variantNames[_currentVariantIndex]; + } + + /// + /// Sets the active material variant by index. + /// Use -1 to switch to the default material. + /// + /// The index of the variant to activate, or -1 for default. + /// True if the variant was successfully set, false otherwise. + public bool SetVariant(int variantIndex) + { + if (_meshRenderer == null) + { + Debug.LogWarning("CesiumMaterialVariants: MeshRenderer not found."); + return false; + } + + // Handle default material case + if (variantIndex < 0) + { + if (defaultMaterial != null) + { + _meshRenderer.material = defaultMaterial; + _currentVariantIndex = -1; + return true; + } + Debug.LogWarning("CesiumMaterialVariants: Default material not available."); + return false; + } + + // Validate variant index + if (variantIndex >= variantNames.Length) + { + Debug.LogWarning($"CesiumMaterialVariants: Variant index {variantIndex} is out of range. Available variants: {variantNames.Length}"); + return false; + } + + // Check if we have a material for this variant + if (variantMaterials.TryGetValue(variantIndex, out Material variantMaterial)) + { + if (variantMaterial != null) + { + _meshRenderer.material = variantMaterial; + _currentVariantIndex = variantIndex; + return true; + } + else + { + Debug.LogWarning($"CesiumMaterialVariants: Material for variant '{variantNames[variantIndex]}' is null."); + return false; + } + } + + Debug.LogWarning($"CesiumMaterialVariants: No material found for variant '{variantNames[variantIndex]}' (index {variantIndex})."); + return false; + } + + /// + /// Sets the active material variant by name. + /// Use "Default" or an empty string to switch to the default material. + /// + /// The name of the variant to activate. + /// True if the variant was successfully set, false otherwise. + public bool SetVariant(string variantName) + { + if (string.IsNullOrEmpty(variantName) || variantName.Equals("Default", StringComparison.OrdinalIgnoreCase)) + { + return SetVariant(-1); + } + + for (int i = 0; i < variantNames.Length; i++) + { + if (variantNames[i].Equals(variantName, StringComparison.OrdinalIgnoreCase)) + { + return SetVariant(i); + } + } + + Debug.LogWarning($"CesiumMaterialVariants: Variant '{variantName}' not found. Available variants: {string.Join(", ", variantNames)}"); + return false; + } + + /// + /// Toggles between two specific variants. Useful for simple A/B switching. + /// + /// First variant index. + /// Second variant index. + public void ToggleBetween(int variantIndexA, int variantIndexB) + { + if (_currentVariantIndex == variantIndexA) + { + SetVariant(variantIndexB); + } + else + { + SetVariant(variantIndexA); + } + } + + /// + /// Toggles between two specific variants by name. + /// + /// First variant name. + /// Second variant name. + public void ToggleBetween(string variantNameA, string variantNameB) + { + string currentName = GetCurrentVariantName(); + if (currentName.Equals(variantNameA, StringComparison.OrdinalIgnoreCase)) + { + SetVariant(variantNameB); + } + else + { + SetVariant(variantNameA); + } + } + + /// + /// Gets a list of all available variant names. + /// + /// Array of variant names, or an empty array if none are available. + public string[] GetAvailableVariants() + { + return variantNames ?? Array.Empty(); + } + } +} diff --git a/Source/Runtime/CesiumMaterialVariants.cs.meta b/Source/Runtime/CesiumMaterialVariants.cs.meta new file mode 100644 index 00000000..51647023 --- /dev/null +++ b/Source/Runtime/CesiumMaterialVariants.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c67c838ac98494b438cce99c757ac571 \ No newline at end of file diff --git a/Source/Runtime/ConfigureReinterop.cs b/Source/Runtime/ConfigureReinterop.cs index 6ff1e064..f519e162 100644 --- a/Source/Runtime/ConfigureReinterop.cs +++ b/Source/Runtime/ConfigureReinterop.cs @@ -844,6 +844,14 @@ Cesium3DTilesetLoadFailureDetails tilesetDetails sets[0].propertyTableIndex = 0; sets[0].Dispose(); + // CesiumMaterialVariants - for KHR_materials_variants support + CesiumMaterialVariants materialVariants = go.AddComponent(); + materialVariants = go.GetComponent(); + materialVariants.variantNames = new string[] { "variant1", "variant2" }; + materialVariants.defaultMaterial = meshRenderer.sharedMaterial; + materialVariants.variantMaterials = new Dictionary(); + materialVariants.variantMaterials.Add(0, meshRenderer.sharedMaterial); + CesiumFeatureIdAttribute featureIdAttribute = new CesiumFeatureIdAttribute(); featureIdAttribute.status = featureIdAttribute.status; featureIdAttribute.featureCount = 1; @@ -1008,5 +1016,4 @@ Cesium3DTilesetLoadFailureDetails tilesetDetails scene.GetRootGameObjects(); } } -} - +} \ No newline at end of file diff --git a/native~/src/Runtime/CesiumMaterialVariantsUtility.cpp b/native~/src/Runtime/CesiumMaterialVariantsUtility.cpp new file mode 100644 index 00000000..a48c4087 --- /dev/null +++ b/native~/src/Runtime/CesiumMaterialVariantsUtility.cpp @@ -0,0 +1,138 @@ +#include "CesiumMaterialVariantsUtility.h" + +#include "TextureLoader.h" +#include "TilesetMaterialProperties.h" +#include "UnityPrepareRendererResources.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace DotNet; +using namespace DotNet::System::Collections::Generic; + +namespace CesiumForUnityNative { + +DotNet::CesiumForUnity::CesiumMaterialVariants +CesiumMaterialVariantsUtility::addMaterialVariants( + const DotNet::UnityEngine::GameObject& primitiveGameObject, + const CesiumGltf::Model& model, + const CesiumGltf::MeshPrimitive& primitive, + const CesiumPrimitiveInfo& primitiveInfo, + const DotNet::UnityEngine::Material& defaultMaterial, + const TilesetMaterialProperties& materialProperties) noexcept { + + // Get the model-level extension (contains variant names) + const CesiumGltf::ExtensionModelKhrMaterialsVariants* pModelVariants = + model.getExtension(); + if (!pModelVariants || pModelVariants->variants.empty()) { + return nullptr; + } + + // Get the primitive-level extension (contains variant-to-material mappings) + const CesiumGltf::ExtensionMeshPrimitiveKhrMaterialsVariants* pPrimitiveVariants = + primitive.getExtension(); + if (!pPrimitiveVariants || pPrimitiveVariants->mappings.empty()) { + return nullptr; + } + + // Build the variant-to-material map from the primitive extension + std::unordered_map variantMaterialMap; + for (const auto& mapping : pPrimitiveVariants->mappings) { + int32_t materialIndex = mapping.material; + for (int64_t variantIndex : mapping.variants) { + if (variantIndex >= 0 && variantIndex <= INT32_MAX) { + variantMaterialMap[static_cast(variantIndex)] = materialIndex; + } + } + } + + if (variantMaterialMap.empty()) { + return nullptr; + } + + // Create the component + CesiumForUnity::CesiumMaterialVariants variantsComponent = + primitiveGameObject.AddComponent(); + + if (variantsComponent == nullptr) { + return nullptr; + } + + // Set variant names from model extension + const auto& variants = pModelVariants->variants; + System::Array1 variantNames(static_cast(variants.size())); + for (size_t i = 0; i < variants.size(); i++) { + variantNames.Item(static_cast(i), System::String(variants[i].name)); + } + variantsComponent.variantNames(variantNames); + + variantsComponent.defaultMaterial(defaultMaterial); + + // Load the base material for creating variant materials + UnityEngine::Material baseMaterial = + UnityEngine::Resources::Load( + primitiveInfo.isUnlit + ? System::String("CesiumUnlitTilesetMaterial") + : System::String("CesiumDefaultTilesetMaterial")); + + if (baseMaterial == nullptr) { + UnityEngine::Debug::LogWarning(static_cast(System::String( + "CesiumMaterialVariants: Failed to load base material. Material variants will not be available."))); + return variantsComponent; + } + + // Create materials for each variant + auto variantMaterialsDict = Dictionary2(); + + for (const auto& [variantIndex, materialIndex] : variantMaterialMap) { + const CesiumGltf::Material* pVariantMaterial = + CesiumGltf::Model::getSafe(&model.materials, materialIndex); + + if (pVariantMaterial && baseMaterial != nullptr) { + UnityEngine::Material variantMaterial = + UnityEngine::Object::Instantiate(baseMaterial); + + if (variantMaterial == nullptr) { + continue; + } + + variantMaterial.hideFlags(UnityEngine::HideFlags::HideAndDontSave); + + setGltfMaterialParameterValues( + model, + primitiveInfo, + *pVariantMaterial, + variantMaterial, + materialProperties); + + variantMaterialsDict.Add(variantIndex, variantMaterial); + } + } + + variantsComponent.variantMaterials(variantMaterialsDict); + + return variantsComponent; +} + +} // namespace CesiumForUnityNative diff --git a/native~/src/Runtime/CesiumMaterialVariantsUtility.h b/native~/src/Runtime/CesiumMaterialVariantsUtility.h new file mode 100644 index 00000000..ad398e3e --- /dev/null +++ b/native~/src/Runtime/CesiumMaterialVariantsUtility.h @@ -0,0 +1,47 @@ +#pragma once + +#include "UnityPrepareRendererResources.h" + +namespace DotNet::CesiumForUnity { +class CesiumMaterialVariants; +} // namespace DotNet::CesiumForUnity + +namespace DotNet::UnityEngine { +class GameObject; +class Material; +} // namespace DotNet::UnityEngine + +namespace CesiumGltf { +struct Model; +struct MeshPrimitive; +} // namespace CesiumGltf + +namespace CesiumForUnityNative { + +class CesiumMaterialVariantsUtility { +public: + /** + * @brief Adds a CesiumMaterialVariants component to a primitive GameObject + * if the primitive has the KHR_materials_variants extension. + * + * This function handles all extension parsing internally, following the same + * pattern as CesiumFeaturesMetadataUtility. + * + * @param primitiveGameObject The primitive GameObject to add the component to. + * @param model The glTF model containing the materials and variant definitions. + * @param primitive The glTF primitive (checked for KHR_materials_variants extension). + * @param primitiveInfo Information about the primitive, including UV mappings. + * @param defaultMaterial The default Unity material for this primitive. + * @param materialProperties Material properties for creating variant materials. + * @return The CesiumMaterialVariants component, or null if no variants exist. + */ + static DotNet::CesiumForUnity::CesiumMaterialVariants addMaterialVariants( + const DotNet::UnityEngine::GameObject& primitiveGameObject, + const CesiumGltf::Model& model, + const CesiumGltf::MeshPrimitive& primitive, + const CesiumPrimitiveInfo& primitiveInfo, + const DotNet::UnityEngine::Material& defaultMaterial, + const TilesetMaterialProperties& materialProperties) noexcept; +}; + +} // namespace CesiumForUnityNative diff --git a/native~/src/Runtime/UnityPrepareRendererResources.cpp b/native~/src/Runtime/UnityPrepareRendererResources.cpp index 7efe023a..dcdce7ff 100644 --- a/native~/src/Runtime/UnityPrepareRendererResources.cpp +++ b/native~/src/Runtime/UnityPrepareRendererResources.cpp @@ -1,6 +1,7 @@ #include "UnityPrepareRendererResources.h" #include "CesiumFeaturesMetadataUtility.h" +#include "CesiumMaterialVariantsUtility.h" #include "TextureLoader.h" #include "TilesetMaterialProperties.h" #include "UnityLifetime.h" @@ -30,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -1081,6 +1083,10 @@ gltfVectorToUnityVector(const std::vector& values, float defaultValue) { return result; } +} // namespace + +namespace CesiumForUnityNative { + void setGltfMaterialParameterValues( const CesiumGltf::Model& model, const CesiumPrimitiveInfo& primitiveInfo, @@ -1387,7 +1393,8 @@ void setGltfMaterialParameterValues( } } } -} // namespace + +} // namespace CesiumForUnityNative void* UnityPrepareRendererResources::prepareInMainThread( Cesium3DTilesSelection::Tile& tile, @@ -1644,6 +1651,15 @@ void* UnityPrepareRendererResources::prepareInMainThread( *pFeatures); } } + + // Add material variants component if the primitive has variants + CesiumMaterialVariantsUtility::addMaterialVariants( + primitiveGameObject, + gltf, + primitive, + primitiveInfo, + material, + materialProperties); }); tilesetComponent.BroadcastNewGameObjectCreated(*pModelGameObject); diff --git a/native~/src/Runtime/UnityPrepareRendererResources.h b/native~/src/Runtime/UnityPrepareRendererResources.h index 51f2528d..d7498e21 100644 --- a/native~/src/Runtime/UnityPrepareRendererResources.h +++ b/native~/src/Runtime/UnityPrepareRendererResources.h @@ -136,4 +136,17 @@ class UnityPrepareRendererResources TilesetMaterialProperties _materialProperties; }; +/** + * @brief Sets glTF material parameter values on a Unity material. + * + * This function is used internally to apply glTF material properties + * (base color, metallic, roughness, textures, etc.) to a Unity Material. + */ +void setGltfMaterialParameterValues( + const CesiumGltf::Model& model, + const CesiumPrimitiveInfo& primitiveInfo, + const CesiumGltf::Material& gltfMaterial, + const DotNet::UnityEngine::Material& unityMaterial, + const TilesetMaterialProperties& materialProperties); + } // namespace CesiumForUnityNative From e3e67fc2cb4d06961c33528ee4520b4a7e7e79f5 Mon Sep 17 00:00:00 2001 From: Matt Deardorff Date: Fri, 30 Jan 2026 18:42:31 -0500 Subject: [PATCH 2/3] Use opaqueMaterial from tileset --- .../Runtime/CesiumMaterialVariantsUtility.cpp | 17 +++++------------ .../src/Runtime/CesiumMaterialVariantsUtility.h | 2 ++ .../Runtime/UnityPrepareRendererResources.cpp | 1 + 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/native~/src/Runtime/CesiumMaterialVariantsUtility.cpp b/native~/src/Runtime/CesiumMaterialVariantsUtility.cpp index a48c4087..5406497b 100644 --- a/native~/src/Runtime/CesiumMaterialVariantsUtility.cpp +++ b/native~/src/Runtime/CesiumMaterialVariantsUtility.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -40,6 +39,7 @@ CesiumMaterialVariantsUtility::addMaterialVariants( const CesiumGltf::MeshPrimitive& primitive, const CesiumPrimitiveInfo& primitiveInfo, const DotNet::UnityEngine::Material& defaultMaterial, + const DotNet::UnityEngine::Material& opaqueMaterial, const TilesetMaterialProperties& materialProperties) noexcept { // Get the model-level extension (contains variant names) @@ -89,16 +89,9 @@ CesiumMaterialVariantsUtility::addMaterialVariants( variantsComponent.defaultMaterial(defaultMaterial); - // Load the base material for creating variant materials - UnityEngine::Material baseMaterial = - UnityEngine::Resources::Load( - primitiveInfo.isUnlit - ? System::String("CesiumUnlitTilesetMaterial") - : System::String("CesiumDefaultTilesetMaterial")); - - if (baseMaterial == nullptr) { + if (opaqueMaterial == nullptr) { UnityEngine::Debug::LogWarning(static_cast(System::String( - "CesiumMaterialVariants: Failed to load base material. Material variants will not be available."))); + "CesiumMaterialVariants: No opaque material provided. Material variants will not be available."))); return variantsComponent; } @@ -109,9 +102,9 @@ CesiumMaterialVariantsUtility::addMaterialVariants( const CesiumGltf::Material* pVariantMaterial = CesiumGltf::Model::getSafe(&model.materials, materialIndex); - if (pVariantMaterial && baseMaterial != nullptr) { + if (pVariantMaterial) { UnityEngine::Material variantMaterial = - UnityEngine::Object::Instantiate(baseMaterial); + UnityEngine::Object::Instantiate(opaqueMaterial); if (variantMaterial == nullptr) { continue; diff --git a/native~/src/Runtime/CesiumMaterialVariantsUtility.h b/native~/src/Runtime/CesiumMaterialVariantsUtility.h index ad398e3e..6f797b91 100644 --- a/native~/src/Runtime/CesiumMaterialVariantsUtility.h +++ b/native~/src/Runtime/CesiumMaterialVariantsUtility.h @@ -32,6 +32,7 @@ class CesiumMaterialVariantsUtility { * @param primitive The glTF primitive (checked for KHR_materials_variants extension). * @param primitiveInfo Information about the primitive, including UV mappings. * @param defaultMaterial The default Unity material for this primitive. + * @param opaqueMaterial The tileset's base opaque material to use for variant materials. * @param materialProperties Material properties for creating variant materials. * @return The CesiumMaterialVariants component, or null if no variants exist. */ @@ -41,6 +42,7 @@ class CesiumMaterialVariantsUtility { const CesiumGltf::MeshPrimitive& primitive, const CesiumPrimitiveInfo& primitiveInfo, const DotNet::UnityEngine::Material& defaultMaterial, + const DotNet::UnityEngine::Material& opaqueMaterial, const TilesetMaterialProperties& materialProperties) noexcept; }; diff --git a/native~/src/Runtime/UnityPrepareRendererResources.cpp b/native~/src/Runtime/UnityPrepareRendererResources.cpp index dcdce7ff..d4e3fb3a 100644 --- a/native~/src/Runtime/UnityPrepareRendererResources.cpp +++ b/native~/src/Runtime/UnityPrepareRendererResources.cpp @@ -1659,6 +1659,7 @@ void* UnityPrepareRendererResources::prepareInMainThread( primitive, primitiveInfo, material, + opaqueMaterial, materialProperties); }); From 14c22be65110632867a89701a746b8f828b613bb Mon Sep 17 00:00:00 2001 From: awefel Date: Mon, 9 Feb 2026 11:26:11 -0500 Subject: [PATCH 3/3] change variants to more consistently get meshrenderer --- Source/Runtime/CesiumMaterialVariants.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Source/Runtime/CesiumMaterialVariants.cs b/Source/Runtime/CesiumMaterialVariants.cs index 2f549a40..b01b1e5c 100644 --- a/Source/Runtime/CesiumMaterialVariants.cs +++ b/Source/Runtime/CesiumMaterialVariants.cs @@ -42,13 +42,9 @@ internal Dictionary variantMaterials } = new Dictionary(); private MeshRenderer _meshRenderer; + private MeshRenderer CurrentMeshRenderer => _meshRenderer ??= GetComponent(); private int _currentVariantIndex = -1; // -1 means default material is active - void Start() - { - _meshRenderer = GetComponent(); - } - /// /// Gets the index of the currently active variant, or -1 if the default material is active. /// @@ -77,7 +73,7 @@ public string GetCurrentVariantName() /// True if the variant was successfully set, false otherwise. public bool SetVariant(int variantIndex) { - if (_meshRenderer == null) + if (CurrentMeshRenderer == null) { Debug.LogWarning("CesiumMaterialVariants: MeshRenderer not found."); return false; @@ -88,7 +84,7 @@ public bool SetVariant(int variantIndex) { if (defaultMaterial != null) { - _meshRenderer.material = defaultMaterial; + CurrentMeshRenderer.material = defaultMaterial; _currentVariantIndex = -1; return true; } @@ -108,7 +104,7 @@ public bool SetVariant(int variantIndex) { if (variantMaterial != null) { - _meshRenderer.material = variantMaterial; + CurrentMeshRenderer.material = variantMaterial; _currentVariantIndex = variantIndex; return true; }