// Crest Water System // Copyright © 2024 Wave Harmonic. All rights reserved. using System; using System.Reflection; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; using WaveHarmonic.Crest.Internal; namespace WaveHarmonic.Crest { interface ILodInput { const int k_QueueMaximumSubIndex = 1000; /// /// Draw the input (the render target will be bound) /// public void Draw(Lod simulation, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1f, int slice = -1); float Filter(WaterRenderer water, int slice); /// /// Whether to apply this input. /// bool Enabled { get; } bool IsCompute { get; } int Queue { get; } int Pass { get; } Rect Rect { get; } MonoBehaviour Component { get; } // Allow sorting within a queue. Callers can pass in things like sibling index to // get deterministic sorting. int Order => Queue * k_QueueMaximumSubIndex + Mathf.Min(Component.transform.GetSiblingIndex(), k_QueueMaximumSubIndex - 1); internal static void Attach(ILodInput input, Utility.SortedList inputs) { inputs.Remove(input); inputs.Add(input.Order, input); } internal static void Detach(ILodInput input, Utility.SortedList inputs) { inputs.Remove(input); } } /// /// Base class for scripts that register inputs to the various LOD data types. /// [@ExecuteDuringEditMode] [@HelpURL("Manual/WaterInputs.html")] public abstract partial class LodInput : ManagedBehaviour { [Tooltip("The mode for this input.\n\nSee the manual for more details about input modes. Use AddComponent(LodInputMode) to set the mode via scripting. The mode cannot be changed after creation.")] [@Filtered((int)LodInputMode.Unset)] [@GenerateAPI(Setter.None)] [SerializeField] internal LodInputMode _Mode = LodInputMode.Unset; // NOTE: // Weight and Feather do not support Depth and Clip as they do not make much sense. // For others it is a case of only supporting unsupported mode(s). [Tooltip("Scales the input.")] [@Predicated(typeof(AlbedoLodInput), inverted: true, hide: true)] [@Predicated(typeof(ClipLodInput), inverted: true, hide: true)] [@Predicated(typeof(DepthLodInput), inverted: true, hide: true)] [@Range(0f, 1f)] [@GenerateAPI] [SerializeField] float _Weight = 1f; [Tooltip("The order this input will render.\n\nOrder is Queue plus SiblingIndex")] [@GenerateAPI(Setter.Custom)] [@DecoratedField, SerializeField] int _Queue; [Tooltip("How this input blends into existing data.\n\nSimilar to blend operations in shaders. For inputs which have materials, use the blend functionality on the shader/material.")] [@Predicated(typeof(AbsorptionLodInput), inverted: true, hide: true)] [@Predicated(typeof(AlbedoLodInput), inverted: true, hide: true)] [@Predicated(typeof(AnimatedWavesLodInput), inverted: true, hide: true)] [@Predicated(typeof(ClipLodInput), inverted: true, hide: true)] [@Predicated(typeof(DepthLodInput), inverted: true, hide: true)] [@Predicated(typeof(DynamicWavesLodInput), inverted: true, hide: true)] [@Predicated(typeof(ScatteringLodInput), inverted: true, hide: true)] [@Predicated(typeof(ShadowLodInput), inverted: true, hide: true)] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global))] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Primitive))] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Renderer))] [@Filtered] [@GenerateAPI] [SerializeField] internal LodInputBlend _Blend = LodInputBlend.Additive; [@Label("Feather")] [Tooltip("The width of the feathering to soften the edges to blend inputs.\n\nInputs that do not support feathering will have this field disabled or hidden in UI.")] [@Predicated(typeof(AlbedoLodInput), inverted: true, hide: true)] [@Predicated(typeof(AnimatedWavesLodInput), inverted: true, hide: true)] [@Predicated(typeof(ClipLodInput), inverted: true, hide: true)] [@Predicated(typeof(DepthLodInput), inverted: true, hide: true)] [@Predicated(typeof(DynamicWavesLodInput), inverted: true, hide: true)] [@Predicated(typeof(LevelLodInput), inverted: true, hide: true)] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Renderer))] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global))] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Primitive))] [@GenerateAPI] [@DecoratedField, SerializeField] float _FeatherWidth = 0.1f; [Tooltip("How this input responds to horizontal displacement.\n\nIf false, data will not move horizontally with the waves. Has a small performance overhead when disabled. Only suitable for inputs of small size.")] [@Predicated(typeof(ClipLodInput), inverted: true, hide: true)] [@Predicated(typeof(FlowLodInput), inverted: true, hide: true)] [@Predicated(typeof(LevelLodInput), inverted: true, hide: true)] [@Predicated(typeof(ShapeWaves), inverted: true, hide: true)] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global))] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Spline))] [@GenerateAPI] [@DecoratedField, SerializeField] private protected bool _FollowHorizontalWaveMotion = false; [@Heading("Mode")] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Unset), hide: true)] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Primitive), hide: true)] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global), hide: true)] [@Stripped] [SerializeReference] internal LodInputData _Data; // Need always visble for space to appear before foldout instead of inside. [@Space(10, isAlwaysVisible: true)] [@Group("Debug", order = k_DebugGroupOrder)] [@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global))] [@DecoratedField, SerializeField] internal bool _DrawBounds; internal const int k_DebugGroupOrder = 10; internal static class ShaderIDs { public static int s_Weight = Shader.PropertyToID("_Crest_Weight"); public static int s_DisplacementAtInputPosition = Shader.PropertyToID("_Crest_DisplacementAtInputPosition"); public static readonly int s_BlendSource = Shader.PropertyToID("_Crest_BlendSource"); public static readonly int s_BlendTarget = Shader.PropertyToID("_Crest_BlendTarget"); public static readonly int s_BlendOperation = Shader.PropertyToID("_Crest_BlendOperation"); } internal abstract Color GizmoColor { get; } internal abstract LodInputMode DefaultMode { get; } private protected abstract Utility.SortedList Inputs { get; } /// /// Disables rendering of input into data, but continues most scripting activities. /// public bool ForceRenderingOff { get; set; } /// /// Properties specific to . /// /// /// /// You will need to cast to a more specific type to change /// certain properties. Types derive from and end with . /// Consider using which will validate and cast. /// /// /// and will /// have no associated data and will be null. The rest will have an /// type which will be prefixed with the input type and /// then mode (eg mode for /// will be ). /// /// /// An exception is and . They /// are derived from and use it as a prefix. (eg ). /// /// public LodInputData Data { get => _Data; internal set => _Data = value; } /// /// Retrieves the typed data and validates the passed type. /// /// /// Validation is stripped from builds. /// /// The data type to cast to. /// The casted data. public T GetData() where T : LodInputData { if (_Mode is LodInputMode.Global or LodInputMode.Primitive or LodInputMode.Unset) { Debug.AssertFormat(false, "Crest: {0} has no associated data type.", _Mode); return null; } Debug.AssertFormat(Data is T, "Crest: Incorrect data type ({1}). The data type is {0}.", Data.GetType().BaseType.Name, typeof(T).Name); return Data as T; } internal bool IsCompute => Mode is LodInputMode.Texture or LodInputMode.Paint or LodInputMode.Global or LodInputMode.Primitive; internal virtual int Pass => -1; internal virtual Rect Rect { get { var rect = Rect.zero; if (_Data != null) { rect = _Data.Rect; rect.center -= _Displacement.XZ(); } return rect; } } readonly SampleCollisionHelper _SampleHeightHelper = new(); Vector3 _Displacement; private protected bool _RecalculateBounds = true; internal virtual bool Enabled => enabled && !ForceRenderingOff && Mode switch { LodInputMode.Unset => false, _ => Data?.IsEnabled ?? false, }; // By default do not follow horizontal motion of waves. This means that the water input will appear on the surface at its XZ location, instead // of moving horizontally with the waves. private protected virtual bool FollowHorizontalMotion => Mode is LodInputMode.Global or LodInputMode.Spline || _FollowHorizontalWaveMotion; // // Event Methods // private protected override void Initialize() { base.Initialize(); Data?.OnEnable(); Attach(); } private protected override void OnDisable() { base.OnDisable(); Detach(); Data?.OnDisable(); } private protected override Action OnUpdateMethod => OnUpdate; private protected virtual void OnUpdate(WaterRenderer water) { if (transform.hasChanged) { _RecalculateBounds = true; } // Input culling depends on displacement. if (!FollowHorizontalMotion) { _SampleHeightHelper.SampleDisplacement(transform.position, out _Displacement); } else { _Displacement = Vector3.zero; } Data?.OnUpdate(); } private protected override Action OnLateUpdateMethod => OnLateUpdate; private protected virtual void OnLateUpdate(WaterRenderer water) { Data?.OnLateUpdate(); transform.hasChanged = false; } // // ILodInput // private protected virtual void Attach() { _Input ??= new(this); ILodInput.Attach(_Input, Inputs); } private protected virtual void Detach() { ILodInput.Detach(_Input, Inputs); } internal virtual void Draw(Lod simulation, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1f, int slice = -1) { if (weight == 0f) { return; } // Must use global as weight can change per slice for ShapeWaves. var wrapper = new PropertyWrapperBuffer(buffer); wrapper.SetFloat(ShaderIDs.s_Weight, weight * _Weight); wrapper.SetVector(ShaderIDs.s_DisplacementAtInputPosition, _Displacement); Data?.Draw(simulation, this, buffer, target, slice); } internal virtual float Filter(WaterRenderer water, int slice) { return 1f; } /// /// Sets the Blend render state using Blend present. /// internal static void SetBlendFromPreset(Material material, LodInputBlend preset) { // Blend.Additive var source = BlendMode.One; var target = BlendMode.One; var operation = BlendOp.Add; switch (preset) { case LodInputBlend.Off: source = BlendMode.One; target = BlendMode.Zero; break; case LodInputBlend.Alpha or LodInputBlend.AlphaClip: source = BlendMode.One; // We apply alpha before blending. target = BlendMode.OneMinusSrcAlpha; break; case LodInputBlend.Maximum: operation = BlendOp.Max; break; case LodInputBlend.Minimum: operation = BlendOp.Min; break; } // SetInteger did not appear to work last time. Will need to revisit. material.SetInt(ShaderIDs.s_BlendSource, (int)source); material.SetInt(ShaderIDs.s_BlendTarget, (int)target); material.SetInt(ShaderIDs.s_BlendOperation, (int)operation); } void SetQueue(int previous, int current) { if (previous == current) return; if (!isActiveAndEnabled) return; Attach(); } internal virtual void InferBlend() { // Correct for most cases. _Blend = LodInputBlend.Additive; } // // Editor Only Methods // #if UNITY_EDITOR [@OnChange(skipIfInactive: false)] void OnChange(string propertyPath, object previousValue) { switch (propertyPath) { case nameof(_Queue): SetQueue((int)previousValue, _Queue); break; case nameof(_Mode): if (!isActiveAndEnabled) { ChangeMode(Mode); break; } OnDisable(); ChangeMode(Mode); UnityEditor.EditorTools.ToolManager.RefreshAvailableTools(); OnEnable(); break; case nameof(_Blend): // TODO: Make compatible with disabled. if (isActiveAndEnabled) Data.OnChange($"../{propertyPath}", previousValue); break; } } internal void ChangeMode(LodInputMode mode) { _Data = null; // Try to infer the mode. var types = TypeCache.GetTypesWithAttribute(); var self = GetType(); foreach (var type in types) { var attributes = type.GetCustomAttributes(); foreach (var attribute in attributes) { if (attribute._Mode != mode) continue; if (!attribute._Type.IsAssignableFrom(self)) continue; _Mode = mode; InferBlend(); _Data = (LodInputData)Activator.CreateInstance(type); _Data._Input = this; _Data.InferMode(this, ref _Mode); return; } } _Mode = DefaultMode; InferBlend(); } /// /// Called when component attached in edit mode, or when Reset clicked by user. /// Besides recovering from Unset default value, also does a nice bit of auto-config. /// private protected override void Reset() { var types = TypeCache.GetTypesWithAttribute(); var self = GetType(); // Use inferred mode. foreach (var type in types) { var attributes = type.GetCustomAttributes(); foreach (var attribute in attributes) { if (!attribute._Type.IsAssignableFrom(self)) continue; var instance = (LodInputData)Activator.CreateInstance(type); instance._Input = this; if (instance.InferMode(this, ref _Mode)) { _Data = instance; InferBlend(); return; } } } // Use default mode. ChangeMode(DefaultMode); _Data?.Reset(); base.Reset(); } #endif } partial class LodInput { Input _Input; sealed class Input : ILodInput { readonly LodInput _Input; public Input(LodInput input) => _Input = input; public bool Enabled => _Input.Enabled; public bool IsCompute => _Input.IsCompute; public int Queue => _Input.Queue; public int Pass => _Input.Pass; public Rect Rect => _Input.Rect; public MonoBehaviour Component => _Input; public float Filter(WaterRenderer water, int slice) => _Input.Filter(water, slice); public void Draw(Lod lod, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1, int slice = -1) => _Input.Draw(lod, buffer, target, pass, weight, slice); } } }