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