Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
191 changes: 191 additions & 0 deletions Source/Runtime/CesiumMaterialVariants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using UnityEngine;

namespace CesiumForUnity
{
/// <summary>
/// Represents KHR_materials_variants of a glTF primitive in a <see cref="Cesium3DTileset"/>.
/// Allows switching between different material variants at runtime.
/// </summary>
/// <remarks>
/// This component is automatically added to primitive game objects if they
/// contain the KHR_materials_variants extension.
/// </remarks>
[IconAttribute("Packages/com.cesium.unity/Editor/Resources/Cesium-24x24.png")]
[AddComponentMenu("")]
public partial class CesiumMaterialVariants : MonoBehaviour
{
/// <summary>
/// Names of all available variants for this model (from the root glTF extension).
/// </summary>
public string[] variantNames
{
get; internal set;
}

/// <summary>
/// The default material (the one specified in primitive.material).
/// </summary>
internal Material defaultMaterial
{
get; set;
}

/// <summary>
/// Dictionary mapping variant indices to their corresponding materials.
/// Key = variant index, Value = Unity Material for that variant.
/// </summary>
internal Dictionary<int, Material> variantMaterials
{
get; set;
} = new Dictionary<int, Material>();

private MeshRenderer _meshRenderer;
private MeshRenderer CurrentMeshRenderer => _meshRenderer ??= GetComponent<MeshRenderer>();
private int _currentVariantIndex = -1; // -1 means default material is active

/// <summary>
/// Gets the index of the currently active variant, or -1 if the default material is active.
/// </summary>
public int GetCurrentVariantIndex()
{
return _currentVariantIndex;
}

/// <summary>
/// Gets the name of the currently active variant, or "Default" if the default material is active.
/// </summary>
public string GetCurrentVariantName()
{
if (_currentVariantIndex < 0 || _currentVariantIndex >= variantNames.Length)
{
return "Default";
}
return variantNames[_currentVariantIndex];
}

/// <summary>
/// Sets the active material variant by index.
/// Use -1 to switch to the default material.
/// </summary>
/// <param name="variantIndex">The index of the variant to activate, or -1 for default.</param>
/// <returns>True if the variant was successfully set, false otherwise.</returns>
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;
}

/// <summary>
/// Sets the active material variant by name.
/// Use "Default" or an empty string to switch to the default material.
/// </summary>
/// <param name="variantName">The name of the variant to activate.</param>
/// <returns>True if the variant was successfully set, false otherwise.</returns>
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;
}

/// <summary>
/// Toggles between two specific variants. Useful for simple A/B switching.
/// </summary>
/// <param name="variantIndexA">First variant index.</param>
/// <param name="variantIndexB">Second variant index.</param>
public void ToggleBetween(int variantIndexA, int variantIndexB)
{
if (_currentVariantIndex == variantIndexA)
{
SetVariant(variantIndexB);
}
else
{
SetVariant(variantIndexA);
}
}

/// <summary>
/// Toggles between two specific variants by name.
/// </summary>
/// <param name="variantNameA">First variant name.</param>
/// <param name="variantNameB">Second variant name.</param>
public void ToggleBetween(string variantNameA, string variantNameB)
{
string currentName = GetCurrentVariantName();
if (currentName.Equals(variantNameA, StringComparison.OrdinalIgnoreCase))
{
SetVariant(variantNameB);
}
else
{
SetVariant(variantNameA);
}
}

/// <summary>
/// Gets a list of all available variant names.
/// </summary>
/// <returns>Array of variant names, or an empty array if none are available.</returns>
public string[] GetAvailableVariants()
{
return variantNames ?? Array.Empty<string>();
}
}
}
2 changes: 2 additions & 0 deletions Source/Runtime/CesiumMaterialVariants.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions Source/Runtime/ConfigureReinterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,14 @@ Cesium3DTilesetLoadFailureDetails tilesetDetails
sets[0].propertyTableIndex = 0;
sets[0].Dispose();

// CesiumMaterialVariants - for KHR_materials_variants support
CesiumMaterialVariants materialVariants = go.AddComponent<CesiumMaterialVariants>();
materialVariants = go.GetComponent<CesiumMaterialVariants>();
materialVariants.variantNames = new string[] { "variant1", "variant2" };
materialVariants.defaultMaterial = meshRenderer.sharedMaterial;
materialVariants.variantMaterials = new Dictionary<int, Material>();
materialVariants.variantMaterials.Add(0, meshRenderer.sharedMaterial);

CesiumFeatureIdAttribute featureIdAttribute = new CesiumFeatureIdAttribute();
featureIdAttribute.status = featureIdAttribute.status;
featureIdAttribute.featureCount = 1;
Expand Down Expand Up @@ -1008,5 +1016,4 @@ Cesium3DTilesetLoadFailureDetails tilesetDetails
scene.GetRootGameObjects();
}
}
}

}
131 changes: 131 additions & 0 deletions native~/src/Runtime/CesiumMaterialVariantsUtility.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#include "CesiumMaterialVariantsUtility.h"

#include "TextureLoader.h"
#include "TilesetMaterialProperties.h"
#include "UnityPrepareRendererResources.h"

#include <CesiumGltf/ExtensionMeshPrimitiveKhrMaterialsVariants.h>
#include <CesiumGltf/ExtensionModelKhrMaterialsVariants.h>
#include <CesiumGltf/Material.h>
#include <CesiumGltf/Model.h>
#include <CesiumGltf/MeshPrimitive.h>
#include <CesiumUtility/Tracing.h>

#include <unordered_map>

#include <DotNet/CesiumForUnity/CesiumMaterialVariants.h>
#include <DotNet/System/Array1.h>
#include <DotNet/System/Collections/Generic/Dictionary2.h>
#include <DotNet/System/String.h>
#include <DotNet/System/Object.h>
#include <DotNet/UnityEngine/Debug.h>
#include <DotNet/UnityEngine/GameObject.h>
#include <DotNet/UnityEngine/HideFlags.h>
#include <DotNet/UnityEngine/Material.h>
#include <DotNet/UnityEngine/Object.h>
#include <DotNet/UnityEngine/Texture.h>
#include <DotNet/UnityEngine/Vector4.h>
#include <DotNet/UnityEngine/Rendering/CullMode.h>

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<CesiumGltf::ExtensionModelKhrMaterialsVariants>();
if (!pModelVariants || pModelVariants->variants.empty()) {
return nullptr;
}

// Get the primitive-level extension (contains variant-to-material mappings)
const CesiumGltf::ExtensionMeshPrimitiveKhrMaterialsVariants* pPrimitiveVariants =
primitive.getExtension<CesiumGltf::ExtensionMeshPrimitiveKhrMaterialsVariants>();
if (!pPrimitiveVariants || pPrimitiveVariants->mappings.empty()) {
return nullptr;
}

// Build the variant-to-material map from the primitive extension
std::unordered_map<int32_t, int32_t> 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<int32_t>(variantIndex)] = materialIndex;
}
}
}

if (variantMaterialMap.empty()) {
return nullptr;
}

// Create the component
CesiumForUnity::CesiumMaterialVariants variantsComponent =
primitiveGameObject.AddComponent<CesiumForUnity::CesiumMaterialVariants>();

if (variantsComponent == nullptr) {
return nullptr;
}

// Set variant names from model extension
const auto& variants = pModelVariants->variants;
System::Array1<System::String> variantNames(static_cast<std::int32_t>(variants.size()));
for (size_t i = 0; i < variants.size(); i++) {
variantNames.Item(static_cast<std::int32_t>(i), System::String(variants[i].name));
}
variantsComponent.variantNames(variantNames);

variantsComponent.defaultMaterial(defaultMaterial);

if (opaqueMaterial == nullptr) {
UnityEngine::Debug::LogWarning(static_cast<System::Object>(System::String(
"CesiumMaterialVariants: No opaque material provided. Material variants will not be available.")));
return variantsComponent;
}

// Create materials for each variant
auto variantMaterialsDict = Dictionary2<int32_t, UnityEngine::Material>();

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
Loading