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..b01b1e5c
--- /dev/null
+++ b/Source/Runtime/CesiumMaterialVariants.cs
@@ -0,0 +1,191 @@
+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 MeshRenderer CurrentMeshRenderer => _meshRenderer ??= GetComponent();
+ private int _currentVariantIndex = -1; // -1 means default material is active
+
+ ///
+ /// 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 (CurrentMeshRenderer == null)
+ {
+ Debug.LogWarning("CesiumMaterialVariants: MeshRenderer not found.");
+ return false;
+ }
+
+ // Handle default material case
+ if (variantIndex < 0)
+ {
+ if (defaultMaterial != null)
+ {
+ CurrentMeshRenderer.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)
+ {
+ CurrentMeshRenderer.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..5406497b
--- /dev/null
+++ b/native~/src/Runtime/CesiumMaterialVariantsUtility.cpp
@@ -0,0 +1,131 @@
+#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
+
+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 DotNet::UnityEngine::Material& opaqueMaterial,
+ 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);
+
+ if (opaqueMaterial == nullptr) {
+ UnityEngine::Debug::LogWarning(static_cast(System::String(
+ "CesiumMaterialVariants: No opaque material provided. 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) {
+ UnityEngine::Material variantMaterial =
+ UnityEngine::Object::Instantiate(opaqueMaterial);
+
+ 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..6f797b91
--- /dev/null
+++ b/native~/src/Runtime/CesiumMaterialVariantsUtility.h
@@ -0,0 +1,49 @@
+#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 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.
+ */
+ 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 DotNet::UnityEngine::Material& opaqueMaterial,
+ const TilesetMaterialProperties& materialProperties) noexcept;
+};
+
+} // namespace CesiumForUnityNative
diff --git a/native~/src/Runtime/UnityPrepareRendererResources.cpp b/native~/src/Runtime/UnityPrepareRendererResources.cpp
index 7efe023a..d4e3fb3a 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,16 @@ void* UnityPrepareRendererResources::prepareInMainThread(
*pFeatures);
}
}
+
+ // Add material variants component if the primitive has variants
+ CesiumMaterialVariantsUtility::addMaterialVariants(
+ primitiveGameObject,
+ gltf,
+ primitive,
+ primitiveInfo,
+ material,
+ opaqueMaterial,
+ 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