using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; namespace StylizedWater2 { /// /// Attached to every mesh using the Stylized Water 2 shader /// Provides a generic way of identifying water objects and accessing their properties /// [ExecuteInEditMode] [AddComponentMenu("Stylized Water 2/Water Object")] [DisallowMultipleComponent] public class WaterObject : MonoBehaviour { /// /// Collection of all available WaterObject instances. Instances (un)register themselves in the OnEnable/OnDisable functions. /// public static readonly List Instances = new List(); public Material material; public MeshFilter meshFilter; public MeshRenderer meshRenderer; private static Vector3 s_PositionOffset; private static readonly int _WaterPositionOffset = Shader.PropertyToID("_WaterPositionOffset"); /// /// For use with floating-point origin systems. In the shader, the world-position (used for UV coordinates) will be offset by this value. /// Buoyancy calculations will also be offset to stay in sync. /// public static Vector3 PositionOffset { set { s_PositionOffset = value; Shader.SetGlobalVector(_WaterPositionOffset, s_PositionOffset); } internal get => s_PositionOffset; } private static float m_customTimeValue = -1f; private static readonly int CustomTimeID = Shader.PropertyToID("_CustomTime"); /// /// Pass in any time value, any kind of animations will use this as a time index, including wave animations (and thus buoyancy calculations as well). /// This is typically used for network synchronized waves or cutscenes. /// To revert to using normal , pass in a value lower than 0. /// /// public static float CustomTime { set { m_customTimeValue = value; Shader.SetGlobalFloat(CustomTimeID, m_customTimeValue); } get => m_customTimeValue; } private MaterialPropertyBlock _props; public MaterialPropertyBlock props { get { //Fetch when required, execution order makes it unreliable otherwise if (_props == null) { CreatePropertyBlock(meshRenderer); } return _props; } private set => _props = value; } private void CreatePropertyBlock(Renderer sourceRenderer) { _props = new MaterialPropertyBlock(); sourceRenderer.GetPropertyBlock(_props); } private void Reset() { meshRenderer = GetComponent(); CreatePropertyBlock(meshRenderer); meshFilter = GetComponent(); } private void OnEnable() { Instances.Add(this); } private void OnDisable() { Instances.Remove(this); } private void OnValidate() { if (!meshRenderer) meshRenderer = GetComponent(); if (!meshFilter) meshFilter = GetComponent(); FetchWaterMaterial(); } /// /// Grabs the material from the attached Mesh Renderer /// public Material FetchWaterMaterial() { if (meshRenderer) { material = meshRenderer.sharedMaterial; return material; } return null; } /// /// Applies to changes made to the Material Property Blocks ('props' property) /// public void ApplyInstancedProperties() { if(props != null) meshRenderer.SetPropertyBlock(props); } /// /// Checks if the position is below the maximum possible wave height. Can be used as a fast broad-phase check, before actually using the more expensive SampleWaves function /// /// public bool CanTouch(Vector3 position) { return Buoyancy.CanTouchWater(position, this); } public void AssignMesh(Mesh mesh) { if (meshFilter) meshFilter.sharedMesh = mesh; } public void AssignMaterial(Material newMaterial) { if (meshRenderer) meshRenderer.sharedMaterial = newMaterial; material = newMaterial; } /// /// Creates a new GameObject with a MeshFilter, MeshRenderer and WaterObject component /// /// If assigned, this material is automatically added to the MeshRenderer /// public static WaterObject New(Material waterMaterial = null, Mesh mesh = null) { GameObject go = new GameObject("Water Object", typeof(MeshFilter), typeof(MeshRenderer), typeof(WaterObject)); go.layer = LayerMask.NameToLayer("Water"); #if UNITY_EDITOR UnityEditor.Undo.RegisterCreatedObjectUndo(go, "Created Water Object"); #endif WaterObject waterObject = go.GetComponent(); waterObject.meshRenderer = waterObject.gameObject.GetComponent(); waterObject.meshFilter = waterObject.gameObject.GetComponent(); waterObject.meshFilter.sharedMesh = mesh; waterObject.meshRenderer.sharedMaterial = waterMaterial; waterObject.meshRenderer.shadowCastingMode = ShadowCastingMode.Off; waterObject.material = waterMaterial; return waterObject; } /// /// Attempt to find the WaterObject above or below the position. Checks against the bounds of ALL Water Object meshes by raycasting on the XZ plane /// /// Position in world-space (height is not relevant) /// Unless this is true, water rotated on the Y-axis will yield incorrect results (but is faster) /// public static WaterObject Find(Vector3 position, bool rotationSupport) { Ray ray = new Ray(position + (Vector3.up * 1000f), Vector3.down); foreach (WaterObject obj in Instances) { if (rotationSupport) { //Local space ray.origin = obj.transform.InverseTransformPoint(ray.origin); if (obj.meshFilter.sharedMesh.bounds.IntersectRay(ray)) return obj; } else { //Axis-aligned bounds if (obj.meshRenderer.bounds.IntersectRay(ray)) return obj; } } return null; } } }