using UnityEngine; /// /// 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. /// /// /// 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. /// [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(); 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(); } } /// /// Validates that all required components are present and properly configured. /// /// True if valid, false otherwise. 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; } /// /// Initializes component references and MaterialPropertyBlock with GPU instancing enabled. /// void Initialize() { if (rend == null) rend = GetComponent(); 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++; } } } /// /// Reads current gradient index values from materials and syncs controller values. /// Only reads once during initialization to avoid overwriting user changes. /// 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; } /// /// Checks if any index value has changed since last update. /// /// True if any value changed, false otherwise. bool HasChanged() { return !Mathf.Approximately(indexMaterial0, lastIndices[0]) || !Mathf.Approximately(indexMaterial1, lastIndices[1]) || !Mathf.Approximately(indexMaterial2, lastIndices[2]) || !Mathf.Approximately(indexMaterial3, lastIndices[3]); } /// /// Applies gradient index values to materials. /// Uses Material Instances (direct write) or MaterialPropertyBlock depending on current mode. /// 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]; } } } /// /// Sets the gradient index for a specific material slot. /// /// The material slot index (0-3). /// The gradient index value (0-109). /// /// Use this method to programmatically change gradient indices at runtime. /// The value is automatically clamped to the valid range (0-109). /// /// /// /// // Set gradient index 45 for material slot 0 /// materialIndexController.SetIndex(0, 45f); /// /// 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(); } /// /// Gets the current gradient index for a specific material slot. /// /// The material slot index (0-3). /// The gradient index value for the specified slot. /// /// /// float currentIndex = materialIndexController.GetIndex(0); /// /// 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; } } /// /// Refreshes instance detection and clears property blocks. /// Call this after using "Prepare for Timeline" to switch between MPB and Material Instance modes. /// /// /// This method is called automatically by PotionTextureSetup when creating or removing material instances. /// You typically don't need to call this manually. /// 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(); } }