유니티 셋팅
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d77f9887d365d44db2e454b56c9b6d5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,141 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
/// <summary>
|
||||
/// Editor utility to clear _GrayscaleTex from materials.
|
||||
/// Useful for cleaning up material previews after runtime testing.
|
||||
/// </summary>
|
||||
public class ClearGrayscaleTextures : EditorWindow
|
||||
{
|
||||
private bool showWarning = true;
|
||||
|
||||
[MenuItem("Tools/MesshingAround/Clear GrayscaleTex from Materials")]
|
||||
static void ShowWindow()
|
||||
{
|
||||
GetWindow<ClearGrayscaleTextures>("Clear GrayscaleTex");
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
EditorGUILayout.LabelField("Clear GrayscaleTex from Materials", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"This tool clears the _GrayscaleTex property from all materials.\n\n" +
|
||||
"This is useful for cleaning up material previews in the Project window. " +
|
||||
"The grayscale texture will be re-applied automatically at runtime by PotionTextureSetup.",
|
||||
MessageType.Info
|
||||
);
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
// Warning box
|
||||
if (showWarning)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"⚠️ IMPORTANT WARNINGS:\n\n" +
|
||||
"• Material Instances: If you have material instances in your scene (created via 'Prepare for Timeline'), " +
|
||||
"you may need to reset them after running this tool.\n\n" +
|
||||
"• White Materials: After clearing, materials in the scene may appear white or without the grayscale texture " +
|
||||
"until you enter Play mode or force a repaint (Play/Stop).\n\n" +
|
||||
"• This operation affects ALL materials in the project with _GrayscaleTex property.",
|
||||
MessageType.Warning
|
||||
);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
showWarning = EditorGUILayout.ToggleLeft("Show warnings", showWarning);
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
GUI.backgroundColor = new Color(0.8f, 0.4f, 0.4f);
|
||||
if (GUILayout.Button("Clear _GrayscaleTex from All Materials", GUILayout.Height(40)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog(
|
||||
"Clear GrayscaleTex",
|
||||
"Are you sure you want to clear _GrayscaleTex from all materials?\n\n" +
|
||||
"This will affect all materials in the project.\n" +
|
||||
"Material instances may need to be recreated.",
|
||||
"Yes, Clear",
|
||||
"Cancel"))
|
||||
{
|
||||
ClearAllGrayscaleTextures();
|
||||
}
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"💡 TIP: After running this tool:\n" +
|
||||
"1. Enter Play mode and Stop to refresh materials in the scene\n" +
|
||||
"2. If materials are still white, re-assign the material to the Renderer\n" +
|
||||
"3. If using Timeline, you may need to re-run 'Prepare for Timeline' on affected objects",
|
||||
MessageType.Info
|
||||
);
|
||||
}
|
||||
|
||||
void ClearAllGrayscaleTextures()
|
||||
{
|
||||
string[] guids = AssetDatabase.FindAssets("t:Material");
|
||||
int clearedCount = 0;
|
||||
int totalChecked = 0;
|
||||
|
||||
EditorUtility.DisplayProgressBar("Clearing GrayscaleTex", "Processing materials...", 0f);
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
Material mat = AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
|
||||
if (mat != null)
|
||||
{
|
||||
totalChecked++;
|
||||
|
||||
if (mat.HasProperty("_GrayscaleTex"))
|
||||
{
|
||||
Texture currentTex = mat.GetTexture("_GrayscaleTex");
|
||||
|
||||
if (currentTex != null)
|
||||
{
|
||||
mat.SetTexture("_GrayscaleTex", null);
|
||||
EditorUtility.SetDirty(mat);
|
||||
clearedCount++;
|
||||
Debug.Log($"✓ Cleared _GrayscaleTex from: {mat.name} (was: {currentTex.name})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float progress = (float)i / guids.Length;
|
||||
EditorUtility.DisplayProgressBar("Clearing GrayscaleTex", $"Processing {i + 1}/{guids.Length}", progress);
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
|
||||
// Show results dialog
|
||||
string message = $"Operation completed!\n\n" +
|
||||
$"• Materials checked: {totalChecked}\n" +
|
||||
$"• Materials cleared: {clearedCount}\n\n" +
|
||||
$"Remember to:\n" +
|
||||
$"1. Enter Play mode to refresh scene materials\n" +
|
||||
$"2. Re-run 'Prepare for Timeline' if needed";
|
||||
|
||||
EditorUtility.DisplayDialog("Clear GrayscaleTex - Complete", message, "OK");
|
||||
|
||||
Debug.Log($"===== CLEAR GRAYSCALETEX COMPLETE =====");
|
||||
Debug.Log($"✅ Cleared _GrayscaleTex from {clearedCount}/{totalChecked} material(s)");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: afd91aa4c7bfc4d4cb9593b665e1adc8
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 341020
|
||||
packageName: Stylized Potions Pack - 110 Colors | Liquid Physics | URP + Built-in
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/MesshingAround/StylizedPotionsPack/Scripts/Editor/ClearGrayscaleTextures.cs
|
||||
uploadId: 811526
|
||||
@@ -0,0 +1,146 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
[CustomEditor(typeof(MaterialIndexController))]
|
||||
public class MaterialIndexControllerEditor : Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
MaterialIndexController controller = (MaterialIndexController)target;
|
||||
Renderer rend = controller.GetComponent<Renderer>();
|
||||
|
||||
if (rend == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("No Renderer found!", MessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Material[] materials = rend.sharedMaterials;
|
||||
int materialsWithIndex = 0;
|
||||
|
||||
// Count materials with _GradientIndex
|
||||
for (int i = 0; i < materials.Length; i++)
|
||||
{
|
||||
if (materials[i] != null && materials[i].HasProperty("_GradientIndex"))
|
||||
{
|
||||
materialsWithIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
EditorGUILayout.LabelField("Material Index Controller", EditorStyles.boldLabel);
|
||||
|
||||
string infoMessage = "";
|
||||
if (materialsWithIndex == 1)
|
||||
{
|
||||
infoMessage = "This component is OPTIONAL for single material.\n\n" +
|
||||
"Provides a convenient slider for _GradientIndex.\n" +
|
||||
"You can also animate directly: Renderer > Material > _GradientIndex";
|
||||
}
|
||||
else if (materialsWithIndex >= 2)
|
||||
{
|
||||
infoMessage = "This component is REQUIRED for multiple materials.\n\n" +
|
||||
"Without it, animating _GradientIndex will affect all materials together.\n" +
|
||||
"This controller allows independent animation per material slot.";
|
||||
}
|
||||
else
|
||||
{
|
||||
infoMessage = "No materials with _GradientIndex detected.";
|
||||
}
|
||||
|
||||
EditorGUILayout.HelpBox(infoMessage, materialsWithIndex >= 2 ? MessageType.Warning : MessageType.Info);
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
// Check if using instances
|
||||
bool usingInstances = false;
|
||||
for (int i = 0; i < materials.Length; i++)
|
||||
{
|
||||
if (materials[i] != null && materials[i].name.Contains("(Instance)"))
|
||||
{
|
||||
usingInstances = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!usingInstances && materialsWithIndex > 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"⚠️ Material instances not detected!\n\n" +
|
||||
"For Timeline animation to work:\n" +
|
||||
"1. Find 'PotionTextureSetup' component\n" +
|
||||
"2. Click 'Prepare for Timeline'",
|
||||
MessageType.Warning
|
||||
);
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField($"Detected: {materialsWithIndex} material(s) with _GradientIndex", EditorStyles.miniLabel);
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
EditorGUILayout.LabelField("Individual Material Indices", EditorStyles.boldLabel);
|
||||
|
||||
// Show only the sliders for materials that exist and have _GradientIndex
|
||||
SerializedProperty indexProp0 = serializedObject.FindProperty("indexMaterial0");
|
||||
SerializedProperty indexProp1 = serializedObject.FindProperty("indexMaterial1");
|
||||
SerializedProperty indexProp2 = serializedObject.FindProperty("indexMaterial2");
|
||||
SerializedProperty indexProp3 = serializedObject.FindProperty("indexMaterial3");
|
||||
|
||||
for (int i = 0; i < materials.Length && i < 4; i++)
|
||||
{
|
||||
if (materials[i] == null) continue;
|
||||
if (!materials[i].HasProperty("_GradientIndex")) continue;
|
||||
|
||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||
EditorGUILayout.LabelField($"Material {i}: {materials[i].name}", EditorStyles.miniLabel);
|
||||
|
||||
SerializedProperty prop = null;
|
||||
switch (i)
|
||||
{
|
||||
case 0: prop = indexProp0; break;
|
||||
case 1: prop = indexProp1; break;
|
||||
case 2: prop = indexProp2; break;
|
||||
case 3: prop = indexProp3; break;
|
||||
}
|
||||
|
||||
if (prop != null)
|
||||
{
|
||||
EditorGUILayout.PropertyField(prop, new GUIContent($"Index Material {i}"));
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.Space(3);
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
// Timeline instructions
|
||||
if (materialsWithIndex >= 2)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"Timeline Animation (Multiple Materials):\n" +
|
||||
"1. Add Animation Track\n" +
|
||||
"2. Drag this GameObject to track\n" +
|
||||
"3. Add properties: 'Index Material 0', 'Index Material 1', etc.\n" +
|
||||
"4. Each material animates independently!",
|
||||
MessageType.None
|
||||
);
|
||||
}
|
||||
else if (materialsWithIndex == 1)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"Timeline Animation (Single Material):\n" +
|
||||
"1. Add Animation Track\n" +
|
||||
"2. Drag this GameObject to track\n" +
|
||||
"3. Add property: 'Index Material 0'\n" +
|
||||
" (Or animate directly via Renderer > Material > _GradientIndex)",
|
||||
MessageType.None
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 222b2dfbd04a57449a8d0c9ccd5ef54a
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 341020
|
||||
packageName: Stylized Potions Pack - 110 Colors | Liquid Physics | URP + Built-in
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/MesshingAround/StylizedPotionsPack/Scripts/Editor/MaterialIndexControllerEditor.cs
|
||||
uploadId: 811526
|
||||
@@ -0,0 +1,103 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
[CustomEditor(typeof(PotionTextureSetup))]
|
||||
public class PotionTextureSetupEditor : Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
PotionTextureSetup script = (PotionTextureSetup)target;
|
||||
|
||||
DrawDefaultInspector();
|
||||
|
||||
EditorGUILayout.Space(15);
|
||||
|
||||
Renderer rend = script.GetComponent<Renderer>();
|
||||
int materialCount = rend != null ? rend.sharedMaterials.Length : 0;
|
||||
|
||||
if (materialCount > 0)
|
||||
{
|
||||
bool isPrepared = false;
|
||||
if (rend != null)
|
||||
{
|
||||
Material[] sharedMats = rend.sharedMaterials;
|
||||
if (sharedMats != null && sharedMats.Length > 0)
|
||||
{
|
||||
foreach (var mat in sharedMats)
|
||||
{
|
||||
if (mat != null && mat.name.Contains("(Instance)"))
|
||||
{
|
||||
isPrepared = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField("Timeline Animation Setup", EditorStyles.boldLabel);
|
||||
|
||||
if (!isPrepared)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"To animate material properties in Timeline, you need to prepare this object first.\n\n" +
|
||||
"This creates material instances that allow:\n" +
|
||||
"• Timeline animations to work properly\n" +
|
||||
"• Each object to have independent grayscale textures\n" +
|
||||
"• Properties to animate without affecting other objects",
|
||||
MessageType.Info
|
||||
);
|
||||
|
||||
GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f);
|
||||
if (GUILayout.Button("🎬 Prepare for Timeline", GUILayout.Height(40)))
|
||||
{
|
||||
script.PrepareForTimeline();
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
}
|
||||
else
|
||||
{
|
||||
string helpText = "✓ Timeline-ready! Material instances created.\n\n";
|
||||
helpText += "Animate in Timeline:\n";
|
||||
helpText += "• All properties via: Renderer > Material > [Property]\n";
|
||||
helpText += "• Colors, emission, smoothness, etc.\n\n";
|
||||
|
||||
if (materialCount >= 2)
|
||||
{
|
||||
helpText += "⚠️ For _GradientIndex:\n";
|
||||
helpText += "ADD MaterialIndexController component (REQUIRED)\n";
|
||||
helpText += "Without it, all materials will animate together.\n\n";
|
||||
}
|
||||
else if (materialCount == 1)
|
||||
{
|
||||
helpText += "For _GradientIndex:\n";
|
||||
helpText += "• Animate directly via Renderer > Material, OR\n";
|
||||
helpText += "• Add MaterialIndexController for a cleaner slider (optional)\n\n";
|
||||
}
|
||||
|
||||
helpText += "💡 Tip: Timeline preview in Edit Mode can be unreliable.\n";
|
||||
helpText += "Always test in Play Mode for accurate animation results.";
|
||||
|
||||
EditorGUILayout.HelpBox(helpText, MessageType.None);
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
// Undo button
|
||||
GUI.backgroundColor = new Color(0.9f, 0.5f, 0.3f);
|
||||
if (GUILayout.Button("↩ Remove Instances (Restore Originals)", GUILayout.Height(30)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog(
|
||||
"Remove Material Instances?",
|
||||
"This will restore the original shared materials.\n\nTimeline animations will no longer work until you prepare again.",
|
||||
"Remove",
|
||||
"Cancel"))
|
||||
{
|
||||
script.RemoveInstances();
|
||||
}
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d77c97b0aa656e4eba50acfa5350868
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 341020
|
||||
packageName: Stylized Potions Pack - 110 Colors | Liquid Physics | URP + Built-in
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/MesshingAround/StylizedPotionsPack/Scripts/Editor/PotionTextureSetupEditor.cs
|
||||
uploadId: 811526
|
||||
456
Assets/MesshingAround/StylizedPotionsPack/Scripts/Liquid.cs
Normal file
456
Assets/MesshingAround/StylizedPotionsPack/Scripts/Liquid.cs
Normal file
@@ -0,0 +1,456 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Simulates liquid physics with wobble effects based on object movement and rotation.
|
||||
/// Supports both Material Property Blocks (for shared materials) and Material Instances (for Timeline animation).
|
||||
/// </summary>
|
||||
[ExecuteInEditMode]
|
||||
[RequireComponent(typeof(MeshFilter), typeof(Renderer))]
|
||||
public class Liquid : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines which time source to use for wobble calculations.
|
||||
/// </summary>
|
||||
public enum UpdateMode
|
||||
{
|
||||
/// <summary>Normal game time (affected by Time.timeScale)</summary>
|
||||
Normal,
|
||||
/// <summary>Unscaled time (ignores Time.timeScale, useful for pause menus)</summary>
|
||||
UnscaledTime
|
||||
}
|
||||
|
||||
public UpdateMode updateMode;
|
||||
|
||||
[Header("Wobble Settings")]
|
||||
[Tooltip("Maximum wobble intensity. Higher values = more aggressive liquid movement.")]
|
||||
[Range(0f, 0.4f)] [SerializeField] float MaxWobble = 0.03f;
|
||||
|
||||
[Tooltip("Speed of wobble oscillation. Higher values = faster wobbling.")]
|
||||
[Range(0f, 1.5f)] [SerializeField] float WobbleSpeedMove = 1f;
|
||||
|
||||
[Tooltip("How quickly the wobble returns to rest. Higher values = faster damping.")]
|
||||
[Range(0f, 2f)] [SerializeField] float Recovery = 1f;
|
||||
|
||||
[Tooltip("Minimum velocity magnitude required to trigger wobble. Acts as a threshold.")]
|
||||
[Range(0f, 5f)] [SerializeField] float Thickness = 1f;
|
||||
|
||||
[Header("Fill Settings")]
|
||||
[Tooltip("Current fill level (0 = empty, 1 = full). Can be animated.")]
|
||||
[Range(0f, 1f)] public float fillAmount = 0.5f;
|
||||
|
||||
[Tooltip("Minimum fill value when fillAmount is 0.")]
|
||||
[Range(0f, 1f)] public float minFill = 0f;
|
||||
|
||||
[Tooltip("Maximum fill value when fillAmount is 1.")]
|
||||
[Range(0f, 1f)] public float maxFill = 1f;
|
||||
|
||||
[Tooltip("Compensates for mesh shape. 0 = uses pivot, 1 = uses lowest vertex point.")]
|
||||
[Range(0, 1)] public float CompensateShapeAmount;
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] Mesh mesh;
|
||||
[SerializeField] Renderer rend;
|
||||
|
||||
Vector3 pos;
|
||||
Vector3 lastPos;
|
||||
Vector3 velocity;
|
||||
Quaternion lastRot;
|
||||
Vector3 angularVelocity;
|
||||
float wobbleAmountX;
|
||||
float wobbleAmountZ;
|
||||
float wobbleAmountToAddX;
|
||||
float wobbleAmountToAddZ;
|
||||
float pulse;
|
||||
float sinewave;
|
||||
double timeOffset; // Double for better precision across platforms
|
||||
Vector3 comp;
|
||||
|
||||
MaterialPropertyBlock mpb;
|
||||
|
||||
private static readonly int GrayscaleTexID = Shader.PropertyToID("_GrayscaleTex");
|
||||
private static readonly int WobbleXID = Shader.PropertyToID("_WobbleX");
|
||||
private static readonly int WobbleZID = Shader.PropertyToID("_WobbleZ");
|
||||
private static readonly int FillAmountID = Shader.PropertyToID("_FillAmount");
|
||||
private static readonly int GradientIndexID = Shader.PropertyToID("_GradientIndex");
|
||||
|
||||
private Texture2D cachedGrayscaleTexture;
|
||||
private bool grayscaleTextureNeedsUpdate = false;
|
||||
private bool usesMaterialInstances = false;
|
||||
|
||||
// ⚡ OPTIMIZATION: Cache the lowest point of the mesh (constant value)
|
||||
private float cachedLowestPoint;
|
||||
private bool lowestPointCached = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Validate components before initialization
|
||||
if (!ValidateComponents())
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
GetMeshAndRend();
|
||||
|
||||
// ⚡ GPU INSTANCING: Works automatically with MPB if shader has instancing enabled
|
||||
mpb = new MaterialPropertyBlock();
|
||||
|
||||
CheckIfUsingInstances();
|
||||
CacheLowestPoint();
|
||||
|
||||
// Initialize time offset for consistent behavior across platforms
|
||||
timeOffset = (updateMode == UpdateMode.UnscaledTime) ? Time.unscaledTime : Time.time;
|
||||
|
||||
// ⚡ FIX: Limit framerate for consistent physics across platforms
|
||||
Application.targetFrameRate = 60; // Force 60 FPS everywhere
|
||||
QualitySettings.vSyncCount = 0; // Disable VSync to use targetFrameRate
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
GetMeshAndRend();
|
||||
if (mpb == null)
|
||||
{
|
||||
mpb = new MaterialPropertyBlock();
|
||||
}
|
||||
CheckIfUsingInstances();
|
||||
|
||||
// Recalculate if mesh changes in editor
|
||||
lowestPointCached = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that all required components are present.
|
||||
/// Logs clear errors if components are missing.
|
||||
/// </summary>
|
||||
/// <returns>True if all required components exist, false otherwise.</returns>
|
||||
bool ValidateComponents()
|
||||
{
|
||||
bool isValid = true;
|
||||
|
||||
MeshFilter meshFilter = GetComponent<MeshFilter>();
|
||||
if (meshFilter == null)
|
||||
{
|
||||
Debug.LogError($"[Liquid] MeshFilter component missing on '{gameObject.name}'. " +
|
||||
"Add a MeshFilter component to use Liquid script.", this);
|
||||
isValid = false;
|
||||
}
|
||||
else if (meshFilter.sharedMesh == null)
|
||||
{
|
||||
Debug.LogError($"[Liquid] MeshFilter on '{gameObject.name}' has no mesh assigned. " +
|
||||
"Assign a mesh to the MeshFilter component.", this);
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
Renderer renderer = GetComponent<Renderer>();
|
||||
if (renderer == null)
|
||||
{
|
||||
Debug.LogError($"[Liquid] Renderer component missing on '{gameObject.name}'. " +
|
||||
"Add a Renderer component (MeshRenderer, SkinnedMeshRenderer, etc.).", this);
|
||||
isValid = false;
|
||||
}
|
||||
else if (renderer.sharedMaterials == null || renderer.sharedMaterials.Length == 0)
|
||||
{
|
||||
Debug.LogWarning($"[Liquid] Renderer on '{gameObject.name}' has no materials assigned. " +
|
||||
"Assign at least one material for the liquid effect to work.", this);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
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 GetMeshAndRend()
|
||||
{
|
||||
if (mesh == null)
|
||||
{
|
||||
MeshFilter mf = GetComponent<MeshFilter>();
|
||||
if (mf != null) mesh = mf.sharedMesh;
|
||||
}
|
||||
if (rend == null)
|
||||
rend = GetComponent<Renderer>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ⚡ OPTIMIZATION: Calculates the lowest point of the mesh ONCE and caches it.
|
||||
/// This value is constant for a given mesh and doesn't need recalculation every frame.
|
||||
/// For a mesh with 500 vertices at 60 FPS, this saves 1.8 million iterations per minute.
|
||||
/// </summary>
|
||||
void CacheLowestPoint()
|
||||
{
|
||||
if (mesh == null || lowestPointCached) return;
|
||||
|
||||
float lowestY = float.MaxValue;
|
||||
Vector3 lowestVert = Vector3.zero;
|
||||
Vector3[] vertices = mesh.vertices;
|
||||
|
||||
for (int i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
Vector3 position = transform.TransformPoint(vertices[i]);
|
||||
if (position.y < lowestY)
|
||||
{
|
||||
lowestY = position.y;
|
||||
lowestVert = position;
|
||||
}
|
||||
}
|
||||
|
||||
cachedLowestPoint = lowestVert.y;
|
||||
lowestPointCached = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached lowest point of the mesh in world space.
|
||||
/// If not cached, calculates and caches it first.
|
||||
/// </summary>
|
||||
/// <returns>The Y position of the lowest vertex in world space.</returns>
|
||||
float GetLowestPoint()
|
||||
{
|
||||
if (!lowestPointCached)
|
||||
{
|
||||
CacheLowestPoint();
|
||||
}
|
||||
return cachedLowestPoint;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
float deltaTime = 0;
|
||||
float currentTime = 0;
|
||||
|
||||
switch (updateMode)
|
||||
{
|
||||
case UpdateMode.Normal:
|
||||
deltaTime = Time.deltaTime;
|
||||
currentTime = (float)(Time.time - timeOffset);
|
||||
break;
|
||||
case UpdateMode.UnscaledTime:
|
||||
deltaTime = Time.unscaledDeltaTime;
|
||||
currentTime = (float)(Time.unscaledTime - timeOffset);
|
||||
break;
|
||||
}
|
||||
|
||||
if (deltaTime != 0)
|
||||
{
|
||||
wobbleAmountToAddX = Mathf.Lerp(wobbleAmountToAddX, 0, (deltaTime * Recovery));
|
||||
wobbleAmountToAddZ = Mathf.Lerp(wobbleAmountToAddZ, 0, (deltaTime * Recovery));
|
||||
|
||||
pulse = 2 * Mathf.PI * WobbleSpeedMove;
|
||||
// ⚡ FIX: Use Time.time instead of manual accumulation for platform consistency
|
||||
sinewave = Mathf.Lerp(sinewave, Mathf.Sin(pulse * currentTime),
|
||||
deltaTime * Mathf.Clamp(velocity.magnitude + angularVelocity.magnitude, Thickness, 10));
|
||||
|
||||
wobbleAmountX = wobbleAmountToAddX * sinewave;
|
||||
wobbleAmountZ = wobbleAmountToAddZ * sinewave;
|
||||
|
||||
velocity = (lastPos - transform.position) / deltaTime;
|
||||
angularVelocity = GetAngularVelocity(lastRot, transform.rotation);
|
||||
|
||||
wobbleAmountToAddX += Mathf.Clamp((velocity.x + (velocity.y * 0.2f) + angularVelocity.z + angularVelocity.y) * MaxWobble, -MaxWobble, MaxWobble);
|
||||
wobbleAmountToAddZ += Mathf.Clamp((velocity.z + (velocity.y * 0.2f) + angularVelocity.x + angularVelocity.y) * MaxWobble, -MaxWobble, MaxWobble);
|
||||
}
|
||||
|
||||
UpdatePos(deltaTime);
|
||||
|
||||
if (usesMaterialInstances)
|
||||
ApplyToMaterialInstances();
|
||||
else
|
||||
ApplyToMaterialPropertyBlock();
|
||||
|
||||
lastPos = transform.position;
|
||||
lastRot = transform.rotation;
|
||||
}
|
||||
|
||||
void ApplyToMaterialInstances()
|
||||
{
|
||||
if (rend == null) return;
|
||||
|
||||
Material[] materials = rend.sharedMaterials;
|
||||
|
||||
// Write directly to material instances (not MPB)
|
||||
// This allows Timeline to animate other properties freely
|
||||
for (int i = 0; i < materials.Length; i++)
|
||||
{
|
||||
if (materials[i] == null) continue;
|
||||
|
||||
if (materials[i].HasProperty(WobbleXID))
|
||||
materials[i].SetFloat(WobbleXID, wobbleAmountX);
|
||||
|
||||
if (materials[i].HasProperty(WobbleZID))
|
||||
materials[i].SetFloat(WobbleZID, wobbleAmountZ);
|
||||
|
||||
if (materials[i].HasProperty(FillAmountID))
|
||||
materials[i].SetVector(FillAmountID, pos);
|
||||
|
||||
if (cachedGrayscaleTexture != null && materials[i].HasProperty(GrayscaleTexID))
|
||||
{
|
||||
materials[i].SetTexture(GrayscaleTexID, cachedGrayscaleTexture);
|
||||
}
|
||||
}
|
||||
|
||||
grayscaleTextureNeedsUpdate = false;
|
||||
}
|
||||
|
||||
void ApplyToMaterialPropertyBlock()
|
||||
{
|
||||
if (rend == null || mpb == null) return;
|
||||
|
||||
Material[] materials = rend.sharedMaterials;
|
||||
|
||||
// ⚡ GPU INSTANCING: Use MPB with instancing enabled for batching
|
||||
for (int i = 0; i < materials.Length; i++)
|
||||
{
|
||||
if (materials[i] == null) continue;
|
||||
|
||||
rend.GetPropertyBlock(mpb, i);
|
||||
|
||||
if (materials[i].HasProperty(WobbleXID))
|
||||
mpb.SetFloat(WobbleXID, wobbleAmountX);
|
||||
|
||||
if (materials[i].HasProperty(WobbleZID))
|
||||
mpb.SetFloat(WobbleZID, wobbleAmountZ);
|
||||
|
||||
if (materials[i].HasProperty(FillAmountID))
|
||||
mpb.SetVector(FillAmountID, pos);
|
||||
|
||||
if (!Application.isPlaying || grayscaleTextureNeedsUpdate)
|
||||
{
|
||||
if (cachedGrayscaleTexture != null && materials[i].HasProperty(GrayscaleTexID))
|
||||
{
|
||||
mpb.SetTexture(GrayscaleTexID, cachedGrayscaleTexture);
|
||||
}
|
||||
}
|
||||
|
||||
rend.SetPropertyBlock(mpb, i);
|
||||
}
|
||||
|
||||
grayscaleTextureNeedsUpdate = false;
|
||||
}
|
||||
|
||||
void UpdatePos(float deltaTime)
|
||||
{
|
||||
Vector3 worldPos = transform.TransformPoint(mesh.bounds.center);
|
||||
|
||||
float normalizedFill = Mathf.Lerp(minFill, maxFill, fillAmount);
|
||||
float invertedFill = 1f - normalizedFill;
|
||||
|
||||
if (CompensateShapeAmount > 0)
|
||||
{
|
||||
// ⚡ Now uses cached value - NO recalculation every frame
|
||||
if (deltaTime != 0)
|
||||
comp = Vector3.Lerp(comp, (worldPos - new Vector3(0, GetLowestPoint(), 0)), deltaTime * 10);
|
||||
else
|
||||
comp = (worldPos - new Vector3(0, GetLowestPoint(), 0));
|
||||
|
||||
pos = worldPos - transform.position - new Vector3(0, invertedFill - (comp.y * CompensateShapeAmount), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
pos = worldPos - transform.position - new Vector3(0, invertedFill, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 GetAngularVelocity(Quaternion foreLastFrameRotation, Quaternion lastFrameRotation)
|
||||
{
|
||||
var q = lastFrameRotation * Quaternion.Inverse(foreLastFrameRotation);
|
||||
if (Mathf.Abs(q.w) > 1023.5f / 1024.0f) return Vector3.zero;
|
||||
|
||||
// Use the deltaTime that respects UpdateMode
|
||||
float dt = (updateMode == UpdateMode.UnscaledTime) ? Time.unscaledDeltaTime : Time.deltaTime;
|
||||
if (dt == 0) return Vector3.zero; // Avoid division by zero
|
||||
|
||||
float gain;
|
||||
if (q.w < 0.0f)
|
||||
{
|
||||
var angle = Mathf.Acos(-q.w);
|
||||
gain = -2.0f * angle / (Mathf.Sin(angle) * dt);
|
||||
}
|
||||
else
|
||||
{
|
||||
var angle = Mathf.Acos(q.w);
|
||||
gain = 2.0f * angle / (Mathf.Sin(angle) * dt);
|
||||
}
|
||||
Vector3 angularVelocity = new Vector3(q.x * gain, q.y * gain, q.z * gain);
|
||||
|
||||
if (float.IsNaN(angularVelocity.z)) angularVelocity = Vector3.zero;
|
||||
return angularVelocity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the grayscale texture for LUT-based gradient coloring.
|
||||
/// The texture is used by the shader to map grayscale values to gradient colors.
|
||||
/// </summary>
|
||||
/// <param name="tex">The grayscale texture to apply. Should be a linear gradient from black to white.</param>
|
||||
/// <remarks>
|
||||
/// This method marks the texture as needing update and applies it in the next render cycle.
|
||||
/// The texture will be applied via MaterialPropertyBlock or Material Instance depending on the current mode.
|
||||
/// </remarks>
|
||||
public void SetGrayscale(Texture2D tex)
|
||||
{
|
||||
if (rend == null)
|
||||
{
|
||||
rend = GetComponent<Renderer>();
|
||||
if (rend == null)
|
||||
{
|
||||
Debug.LogWarning($"[Liquid] Cannot set grayscale texture on '{gameObject.name}': Renderer not found.", this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mpb == null)
|
||||
{
|
||||
mpb = new MaterialPropertyBlock();
|
||||
}
|
||||
|
||||
cachedGrayscaleTexture = tex;
|
||||
grayscaleTextureNeedsUpdate = true;
|
||||
|
||||
CheckIfUsingInstances();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Reapply grayscale if needed
|
||||
if (cachedGrayscaleTexture != null)
|
||||
{
|
||||
grayscaleTextureNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef390e80db64db64a944b927a65bab7d
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 341020
|
||||
packageName: Stylized Potions Pack - 110 Colors | Liquid Physics | URP + Built-in
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/MesshingAround/StylizedPotionsPack/Scripts/Liquid.cs
|
||||
uploadId: 811526
|
||||
@@ -0,0 +1,341 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 432f59e3338158d4489ac699c39b67a8
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 341020
|
||||
packageName: Stylized Potions Pack - 110 Colors | Liquid Physics | URP + Built-in
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/MesshingAround/StylizedPotionsPack/Scripts/MaterialIndexController.cs
|
||||
uploadId: 811526
|
||||
@@ -0,0 +1,377 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Manages grayscale texture assignment for LUT-based gradient coloring.
|
||||
/// Supports both MaterialPropertyBlock mode (for shared materials) and Material Instance mode (for Timeline animation).
|
||||
/// </summary>
|
||||
[ExecuteInEditMode]
|
||||
[RequireComponent(typeof(Renderer))]
|
||||
public class PotionTextureSetup : MonoBehaviour
|
||||
{
|
||||
[Header("Grayscale Texture")]
|
||||
[Tooltip("Grayscale texture used for LUT (Look-Up Table) gradient coloring. Should be a linear gradient from black to white.")]
|
||||
public Texture2D grayscaleTexture;
|
||||
|
||||
private static readonly int GrayscaleTexID = Shader.PropertyToID("_GrayscaleTex");
|
||||
|
||||
private Renderer rend;
|
||||
private Liquid liquidComponent;
|
||||
private Texture2D lastTexture;
|
||||
private MaterialPropertyBlock mpb;
|
||||
private bool usesMaterialInstances = false;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
InitializeComponents();
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
InitializeComponents();
|
||||
ApplyTexture();
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Validate components
|
||||
if (!ValidateComponents())
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
CheckIfUsingInstances();
|
||||
ApplyTexture();
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
InitializeComponents();
|
||||
ApplyTexture();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Only update if texture reference changed
|
||||
if (grayscaleTexture != lastTexture)
|
||||
{
|
||||
ApplyTexture();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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($"[PotionTextureSetup] Renderer component missing on '{gameObject.name}'. " +
|
||||
"Cannot apply grayscale texture without a Renderer.", this);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rend.sharedMaterials == null || rend.sharedMaterials.Length == 0)
|
||||
{
|
||||
Debug.LogWarning($"[PotionTextureSetup] Renderer on '{gameObject.name}' has no materials assigned. " +
|
||||
"Assign materials to use grayscale texture feature.", this);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if any material has the _GrayscaleTex property
|
||||
bool hasGrayscaleProperty = false;
|
||||
foreach (var mat in rend.sharedMaterials)
|
||||
{
|
||||
if (mat != null && mat.HasProperty("_GrayscaleTex"))
|
||||
{
|
||||
hasGrayscaleProperty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasGrayscaleProperty)
|
||||
{
|
||||
Debug.LogWarning($"[PotionTextureSetup] None of the materials on '{gameObject.name}' have '_GrayscaleTex' property. " +
|
||||
"This component will have no effect. Use a compatible shader (e.g., Liquid_Effect, Stylized_Tint).", this);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes component references and MaterialPropertyBlock with GPU instancing enabled.
|
||||
/// </summary>
|
||||
void InitializeComponents()
|
||||
{
|
||||
if (rend == null) rend = GetComponent<Renderer>();
|
||||
if (liquidComponent == null) liquidComponent = GetComponent<Liquid>();
|
||||
|
||||
if (mpb == null)
|
||||
{
|
||||
mpb = new MaterialPropertyBlock();
|
||||
}
|
||||
|
||||
CheckIfUsingInstances();
|
||||
}
|
||||
|
||||
void CheckIfUsingInstances()
|
||||
{
|
||||
if (rend == null) return;
|
||||
|
||||
Material[] materials = rend.sharedMaterials;
|
||||
usesMaterialInstances = false;
|
||||
|
||||
foreach (var mat in materials)
|
||||
{
|
||||
if (mat != null && mat.name.Contains("(Instance)"))
|
||||
{
|
||||
usesMaterialInstances = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyTexture()
|
||||
{
|
||||
if (grayscaleTexture == null) return;
|
||||
|
||||
// If Liquid component exists, delegate to it (handles instance detection internally)
|
||||
if (liquidComponent != null)
|
||||
{
|
||||
liquidComponent.SetGrayscale(grayscaleTexture);
|
||||
lastTexture = grayscaleTexture;
|
||||
return;
|
||||
}
|
||||
|
||||
if (rend == null) return;
|
||||
|
||||
Material[] materials = rend.sharedMaterials;
|
||||
|
||||
if (usesMaterialInstances)
|
||||
{
|
||||
// Write directly to material instances (for Timeline mode)
|
||||
for (int i = 0; i < materials.Length; i++)
|
||||
{
|
||||
if (materials[i] != null && materials[i].HasProperty("_GrayscaleTex"))
|
||||
{
|
||||
materials[i].SetTexture(GrayscaleTexID, grayscaleTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ⚡ GPU INSTANCING: Use MPB with instancing enabled
|
||||
if (mpb == null) return;
|
||||
|
||||
for (int i = 0; i < materials.Length; i++)
|
||||
{
|
||||
if (materials[i] != null && materials[i].HasProperty("_GrayscaleTex"))
|
||||
{
|
||||
rend.GetPropertyBlock(mpb, i);
|
||||
mpb.SetTexture(GrayscaleTexID, grayscaleTexture);
|
||||
rend.SetPropertyBlock(mpb, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastTexture = grayscaleTexture;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a new grayscale texture and applies it immediately.
|
||||
/// </summary>
|
||||
/// <param name="texture">The grayscale texture to apply.</param>
|
||||
/// <remarks>
|
||||
/// Use this method to dynamically change the grayscale texture at runtime.
|
||||
/// The texture will be applied according to the current mode (MPB or Material Instance).
|
||||
/// </remarks>
|
||||
public void SetGrayscaleTexture(Texture2D texture)
|
||||
{
|
||||
grayscaleTexture = texture;
|
||||
ApplyTexture();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates material instances for Timeline animation.
|
||||
/// This allows Timeline to animate all material properties independently per object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// After calling this method:
|
||||
/// - Each object will have its own material instances (not shared)
|
||||
/// - Timeline can animate material properties per object
|
||||
/// - Use MaterialIndexController to animate _GradientIndex independently per material slot
|
||||
///
|
||||
/// NOTE: Material instances increase memory usage. Only use when Timeline animation is needed.
|
||||
/// To revert, use RemoveInstances() method.
|
||||
/// </remarks>
|
||||
[ContextMenu("Prepare for Timeline")]
|
||||
public void PrepareForTimeline()
|
||||
{
|
||||
if (rend == null) rend = GetComponent<Renderer>();
|
||||
|
||||
Material[] sharedMats = rend.sharedMaterials;
|
||||
bool alreadyPrepared = true;
|
||||
|
||||
// Check if already using instances
|
||||
for (int i = 0; i < sharedMats.Length; i++)
|
||||
{
|
||||
if (sharedMats[i] != null && !sharedMats[i].name.Contains("(Instance)"))
|
||||
{
|
||||
alreadyPrepared = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadyPrepared && sharedMats.Length > 0)
|
||||
{
|
||||
Debug.Log($"[PotionTextureSetup] '{gameObject.name}' is already using material instances.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create material instances
|
||||
Material[] newInstances = new Material[sharedMats.Length];
|
||||
for (int i = 0; i < sharedMats.Length; i++)
|
||||
{
|
||||
if (sharedMats[i] != null)
|
||||
{
|
||||
newInstances[i] = new Material(sharedMats[i]);
|
||||
newInstances[i].name = sharedMats[i].name + " (Instance)";
|
||||
}
|
||||
}
|
||||
|
||||
rend.sharedMaterials = newInstances;
|
||||
ApplyTexture();
|
||||
|
||||
// Notify other components to switch from MPB to direct material writes
|
||||
if (liquidComponent != null)
|
||||
{
|
||||
liquidComponent.RefreshInstanceDetection();
|
||||
}
|
||||
|
||||
MaterialIndexController indexController = GetComponent<MaterialIndexController>();
|
||||
if (indexController != null)
|
||||
{
|
||||
indexController.RefreshInstanceDetection();
|
||||
}
|
||||
|
||||
CheckIfUsingInstances();
|
||||
ApplyTexture();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Force editor refresh
|
||||
UnityEditor.EditorUtility.SetDirty(gameObject);
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(gameObject.scene);
|
||||
#endif
|
||||
|
||||
Debug.Log($"[PotionTextureSetup] Created {newInstances.Length} material instance(s) for '{gameObject.name}'. Timeline-ready!", this);
|
||||
Debug.Log("Note: Timeline preview in Edit Mode can be unreliable. Always test in Play Mode for accurate results.", this);
|
||||
|
||||
if (newInstances.Length >= 2)
|
||||
{
|
||||
Debug.Log("IMPORTANT: For multiple materials, add MaterialIndexController component to animate _GradientIndex independently per material.", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("Tip: You can optionally add MaterialIndexController for a cleaner _GradientIndex slider (not required for single material).", this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes material instances and restores the original shared materials.
|
||||
/// Use this to revert changes made by PrepareForTimeline().
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// After calling this method:
|
||||
/// - Objects will use shared materials again (better for batching/performance)
|
||||
/// - Timeline animations will no longer work (need to call PrepareForTimeline again)
|
||||
/// - Memory usage will be reduced
|
||||
/// </remarks>
|
||||
[ContextMenu("Remove Instances (Restore Originals)")]
|
||||
public void RemoveInstances()
|
||||
{
|
||||
if (rend == null) rend = GetComponent<Renderer>();
|
||||
|
||||
Material[] currentMats = rend.sharedMaterials;
|
||||
Material[] originalMats = new Material[currentMats.Length];
|
||||
bool foundAnyInstances = false;
|
||||
|
||||
for (int i = 0; i < currentMats.Length; i++)
|
||||
{
|
||||
if (currentMats[i] != null && currentMats[i].name.Contains("(Instance)"))
|
||||
{
|
||||
foundAnyInstances = true;
|
||||
string originalName = currentMats[i].name.Replace(" (Instance)", "").Trim();
|
||||
Material originalMat = FindOriginalMaterial(originalName);
|
||||
|
||||
if (originalMat != null)
|
||||
{
|
||||
originalMats[i] = originalMat;
|
||||
}
|
||||
else
|
||||
{
|
||||
originalMats[i] = currentMats[i];
|
||||
Debug.LogWarning($"[PotionTextureSetup] Could not find original material '{originalName}' on '{gameObject.name}'. Keeping instance.", this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
originalMats[i] = currentMats[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAnyInstances)
|
||||
{
|
||||
rend.sharedMaterials = originalMats;
|
||||
ApplyTexture();
|
||||
|
||||
if (liquidComponent != null)
|
||||
{
|
||||
liquidComponent.RefreshInstanceDetection();
|
||||
}
|
||||
|
||||
MaterialIndexController indexController = GetComponent<MaterialIndexController>();
|
||||
if (indexController != null)
|
||||
{
|
||||
indexController.RefreshInstanceDetection();
|
||||
}
|
||||
|
||||
CheckIfUsingInstances();
|
||||
ApplyTexture();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorUtility.SetDirty(gameObject);
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(gameObject.scene);
|
||||
#endif
|
||||
|
||||
Debug.Log($"[PotionTextureSetup] Material instances removed from '{gameObject.name}'. Restored original materials.", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[PotionTextureSetup] No material instances found on '{gameObject.name}'. Nothing to remove.", this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find the original shared material by name.
|
||||
/// </summary>
|
||||
/// <param name="materialName">Name of the material to find.</param>
|
||||
/// <returns>The original material if found, null otherwise.</returns>
|
||||
Material FindOriginalMaterial(string materialName)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// Search in project for material with matching name
|
||||
string[] guids = UnityEditor.AssetDatabase.FindAssets($"t:Material {materialName}");
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
|
||||
Material mat = UnityEditor.AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
if (mat != null && mat.name == materialName)
|
||||
{
|
||||
return mat;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7de0c1862cc6cdb4baec99f88e37aa84
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 341020
|
||||
packageName: Stylized Potions Pack - 110 Colors | Liquid Physics | URP + Built-in
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/MesshingAround/StylizedPotionsPack/Scripts/PotionTextureSetup.cs
|
||||
uploadId: 811526
|
||||
Reference in New Issue
Block a user