Files
2026-03-27 16:34:08 +09:00

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();
}
}