341 lines
11 KiB
C#
341 lines
11 KiB
C#
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Controls _GradientIndex property per material slot independently.
|
|
/// Essential for Timeline animation when multiple materials share the same gradient system.
|
|
/// Detects materials with _GradientIndex and exposes individual sliders for each.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// USAGE:
|
|
/// - REQUIRED for 2+ materials: Allows independent gradient animation per material slot
|
|
/// - OPTIONAL for 1 material: Provides a convenient slider (can also animate directly via Renderer > Material)
|
|
///
|
|
/// Before using this component, ensure you've called "Prepare for Timeline" on PotionTextureSetup.
|
|
/// </remarks>
|
|
[ExecuteInEditMode]
|
|
[RequireComponent(typeof(Renderer))]
|
|
public class MaterialIndexController : MonoBehaviour
|
|
{
|
|
[Header("Material Indices (Animate These in Timeline)")]
|
|
[Tooltip("Gradient index for material slot 0. Animatable in Timeline.")]
|
|
[Range(0, 109)] public float indexMaterial0 = 0f;
|
|
|
|
[Tooltip("Gradient index for material slot 1. Animatable in Timeline.")]
|
|
[Range(0, 109)] public float indexMaterial1 = 0f;
|
|
|
|
[Tooltip("Gradient index for material slot 2. Animatable in Timeline.")]
|
|
[Range(0, 109)] public float indexMaterial2 = 0f;
|
|
|
|
[Tooltip("Gradient index for material slot 3. Animatable in Timeline.")]
|
|
[Range(0, 109)] public float indexMaterial3 = 0f;
|
|
|
|
[Header("Info")]
|
|
[SerializeField] private int detectedMaterialsWithIndex = 0;
|
|
|
|
private static readonly int GradientIndexID = Shader.PropertyToID("_GradientIndex");
|
|
private Renderer rend;
|
|
private MaterialPropertyBlock mpb;
|
|
private float[] lastIndices = new float[4];
|
|
private bool usesMaterialInstances = false;
|
|
private bool hasReadInitialValues = false;
|
|
|
|
void Awake()
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
void OnEnable()
|
|
{
|
|
Initialize();
|
|
|
|
// Validate before proceeding
|
|
if (!ValidateComponents())
|
|
{
|
|
enabled = false;
|
|
return;
|
|
}
|
|
|
|
DetectMaterialsWithIndex();
|
|
CheckIfUsingInstances();
|
|
ReadCurrentIndicesFromMaterials();
|
|
|
|
// Info message when component is first added
|
|
if (!hasReadInitialValues && Application.isPlaying)
|
|
{
|
|
if (detectedMaterialsWithIndex > 0)
|
|
{
|
|
Debug.Log($"[MaterialIndexController] Detected {detectedMaterialsWithIndex} material(s) with _GradientIndex on '{gameObject.name}'. Current values loaded.", this);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[MaterialIndexController] No materials with _GradientIndex found on '{gameObject.name}'. " +
|
|
"This component will have no effect. Use a compatible shader (e.g., Liquid_Effect, Stylized_Tint).", this);
|
|
}
|
|
}
|
|
|
|
ApplyIndices();
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
DetectMaterialsWithIndex();
|
|
CheckIfUsingInstances();
|
|
ReadCurrentIndicesFromMaterials();
|
|
ApplyIndices();
|
|
}
|
|
|
|
void OnValidate()
|
|
{
|
|
if (rend == null) rend = GetComponent<Renderer>();
|
|
DetectMaterialsWithIndex();
|
|
CheckIfUsingInstances();
|
|
// Don't read values in OnValidate - user might be editing sliders
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
// Only update if values changed (performance optimization)
|
|
if (HasChanged())
|
|
{
|
|
ApplyIndices();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that all required components are present and properly configured.
|
|
/// </summary>
|
|
/// <returns>True if valid, false otherwise.</returns>
|
|
bool ValidateComponents()
|
|
{
|
|
if (rend == null)
|
|
{
|
|
Debug.LogError($"[MaterialIndexController] Renderer component missing on '{gameObject.name}'. " +
|
|
"Cannot control material indices without a Renderer.", this);
|
|
return false;
|
|
}
|
|
|
|
if (rend.sharedMaterials == null || rend.sharedMaterials.Length == 0)
|
|
{
|
|
Debug.LogWarning($"[MaterialIndexController] Renderer on '{gameObject.name}' has no materials assigned. " +
|
|
"Assign materials to use gradient index control.", this);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes component references and MaterialPropertyBlock with GPU instancing enabled.
|
|
/// </summary>
|
|
void Initialize()
|
|
{
|
|
if (rend == null)
|
|
rend = GetComponent<Renderer>();
|
|
|
|
if (mpb == null)
|
|
{
|
|
mpb = new MaterialPropertyBlock();
|
|
}
|
|
}
|
|
|
|
void CheckIfUsingInstances()
|
|
{
|
|
if (rend == null) return;
|
|
|
|
Material[] materials = rend.sharedMaterials;
|
|
|
|
// Check if ANY material is an instance
|
|
usesMaterialInstances = false;
|
|
foreach (var mat in materials)
|
|
{
|
|
if (mat != null && mat.name.Contains("(Instance)"))
|
|
{
|
|
usesMaterialInstances = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void DetectMaterialsWithIndex()
|
|
{
|
|
if (rend == null) return;
|
|
|
|
detectedMaterialsWithIndex = 0;
|
|
Material[] materials = rend.sharedMaterials;
|
|
|
|
for (int i = 0; i < materials.Length && i < 4; i++)
|
|
{
|
|
if (materials[i] != null && materials[i].HasProperty(GradientIndexID))
|
|
{
|
|
detectedMaterialsWithIndex++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads current gradient index values from materials and syncs controller values.
|
|
/// Only reads once during initialization to avoid overwriting user changes.
|
|
/// </summary>
|
|
void ReadCurrentIndicesFromMaterials()
|
|
{
|
|
if (rend == null) return;
|
|
if (hasReadInitialValues) return; // Only read once on first initialization
|
|
|
|
Material[] materials = rend.sharedMaterials;
|
|
|
|
// Read current index values from materials
|
|
for (int i = 0; i < materials.Length && i < 4; i++)
|
|
{
|
|
if (materials[i] == null) continue;
|
|
if (!materials[i].HasProperty(GradientIndexID)) continue;
|
|
|
|
float currentValue = materials[i].GetFloat(GradientIndexID);
|
|
|
|
// Set the controller values to match current material values
|
|
switch (i)
|
|
{
|
|
case 0: indexMaterial0 = currentValue; break;
|
|
case 1: indexMaterial1 = currentValue; break;
|
|
case 2: indexMaterial2 = currentValue; break;
|
|
case 3: indexMaterial3 = currentValue; break;
|
|
}
|
|
}
|
|
|
|
hasReadInitialValues = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if any index value has changed since last update.
|
|
/// </summary>
|
|
/// <returns>True if any value changed, false otherwise.</returns>
|
|
bool HasChanged()
|
|
{
|
|
return !Mathf.Approximately(indexMaterial0, lastIndices[0]) ||
|
|
!Mathf.Approximately(indexMaterial1, lastIndices[1]) ||
|
|
!Mathf.Approximately(indexMaterial2, lastIndices[2]) ||
|
|
!Mathf.Approximately(indexMaterial3, lastIndices[3]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies gradient index values to materials.
|
|
/// Uses Material Instances (direct write) or MaterialPropertyBlock depending on current mode.
|
|
/// </summary>
|
|
void ApplyIndices()
|
|
{
|
|
if (rend == null) return;
|
|
|
|
Material[] materials = rend.sharedMaterials;
|
|
float[] currentIndices = { indexMaterial0, indexMaterial1, indexMaterial2, indexMaterial3 };
|
|
|
|
if (usesMaterialInstances)
|
|
{
|
|
// Write directly to material instances
|
|
for (int i = 0; i < materials.Length && i < 4; i++)
|
|
{
|
|
if (materials[i] == null) continue;
|
|
if (!materials[i].HasProperty(GradientIndexID)) continue;
|
|
|
|
materials[i].SetFloat(GradientIndexID, currentIndices[i]);
|
|
lastIndices[i] = currentIndices[i];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ⚡ GPU INSTANCING: Use MPB with instancing enabled
|
|
if (mpb == null) return;
|
|
|
|
for (int i = 0; i < materials.Length && i < 4; i++)
|
|
{
|
|
if (materials[i] == null) continue;
|
|
if (!materials[i].HasProperty(GradientIndexID)) continue;
|
|
|
|
rend.GetPropertyBlock(mpb, i);
|
|
mpb.SetFloat(GradientIndexID, currentIndices[i]);
|
|
rend.SetPropertyBlock(mpb, i);
|
|
|
|
lastIndices[i] = currentIndices[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the gradient index for a specific material slot.
|
|
/// </summary>
|
|
/// <param name="materialSlot">The material slot index (0-3).</param>
|
|
/// <param name="value">The gradient index value (0-109).</param>
|
|
/// <remarks>
|
|
/// Use this method to programmatically change gradient indices at runtime.
|
|
/// The value is automatically clamped to the valid range (0-109).
|
|
/// </remarks>
|
|
/// <example>
|
|
/// <code>
|
|
/// // Set gradient index 45 for material slot 0
|
|
/// materialIndexController.SetIndex(0, 45f);
|
|
/// </code>
|
|
/// </example>
|
|
public void SetIndex(int materialSlot, float value)
|
|
{
|
|
value = Mathf.Clamp(value, 0, 109);
|
|
|
|
switch (materialSlot)
|
|
{
|
|
case 0: indexMaterial0 = value; break;
|
|
case 1: indexMaterial1 = value; break;
|
|
case 2: indexMaterial2 = value; break;
|
|
case 3: indexMaterial3 = value; break;
|
|
}
|
|
|
|
ApplyIndices();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current gradient index for a specific material slot.
|
|
/// </summary>
|
|
/// <param name="materialSlot">The material slot index (0-3).</param>
|
|
/// <returns>The gradient index value for the specified slot.</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// float currentIndex = materialIndexController.GetIndex(0);
|
|
/// </code>
|
|
/// </example>
|
|
public float GetIndex(int materialSlot)
|
|
{
|
|
switch (materialSlot)
|
|
{
|
|
case 0: return indexMaterial0;
|
|
case 1: return indexMaterial1;
|
|
case 2: return indexMaterial2;
|
|
case 3: return indexMaterial3;
|
|
default: return 0f;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes instance detection and clears property blocks.
|
|
/// Call this after using "Prepare for Timeline" to switch between MPB and Material Instance modes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method is called automatically by PotionTextureSetup when creating or removing material instances.
|
|
/// You typically don't need to call this manually.
|
|
/// </remarks>
|
|
public void RefreshInstanceDetection()
|
|
{
|
|
CheckIfUsingInstances();
|
|
|
|
// Clear any existing property blocks when switching modes
|
|
if (rend != null)
|
|
{
|
|
Material[] materials = rend.sharedMaterials;
|
|
for (int i = 0; i < materials.Length; i++)
|
|
{
|
|
rend.SetPropertyBlock(null, i);
|
|
}
|
|
}
|
|
|
|
// Read values again after switching to instances
|
|
hasReadInitialValues = false;
|
|
ReadCurrentIndicesFromMaterials();
|
|
|
|
ApplyIndices();
|
|
}
|
|
} |