2025-10-21 14:18:31 +09:00

352 lines
13 KiB
C#

// Stylized Water 3 by Staggart Creations (http://staggart.xyz)
// COPYRIGHT PROTECTED UNDER THE UNITY ASSET STORE EULA (https://unity.com/legal/as-terms)
// • Copying or referencing source code for the production of new asset store, or public, content is strictly prohibited!
// • Uploading this file to a public repository will subject it to an automated DMCA takedown request.
using System;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace StylizedWater3
{
[Serializable]
[HelpURL("https://staggart.xyz/unity/stylized-water-3/sw3-docs/?section=waves-2")]
public class WaveProfile : ScriptableObject
{
const float MAX_AMPLITUDE = 5f;
public const int MAX_LAYERS = 64;
[Min(0.01f)]
[InspectorName("Wave length")]
public float waveLengthMultiplier = 1f;
[Min(0.01f)]
public float amplitudeMultiplier = 1f;
[Min(0.01f)]
public float steepnessMultiplier = 1f;
[Range(0f, 1f)]
[Tooltip("A steepness value too high can result in wave crest \"looping\" the geometry.\n\n" +
"To avoid this from happening, the steepness value for each Layer can be clamped to an average.")]
public float steepnessClamping = 1f;
[Space]
[Header(("Curves (value over layer index)"))]
[Tooltip("Scales the wave length over each layer." +
"\n\n" +
"Left=first layer, Right=last layer")]
public AnimationCurve waveLengthCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
public AnimationCurve amplitudeCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
public AnimationCurve steepnessCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
[Serializable]
public class ProceduralSettings
{
public int seed = 0;
[Range(1, MAX_LAYERS)]
[Tooltip("Number of individual wave layers. Aim for the lowest amount as possible")]
public int numLayers = 4;
[Space]
[Tooltip("The wave length represents the distance between two wave peaks. Use a high maximum value to create stormy ocean swells.")]
public Vector2 minMaxWaveLength = new Vector2(8f, 50f);
[Tooltip("Height of the wave, from its base to its peak")]
public Vector2 minMaxAmplitude = new Vector2(0.1f, 1f);
[Range(0f,1f)]
[Tooltip("Scale the amplitude by the wavelength. If at 1, short waves becomes very short as well")]
public float amplitudeByLength;
[Tooltip("Steepness is the amount of horizontal movement a wave creates")]
public Vector2 minMaxSteepness = new Vector2(0.1f, 1f);
[Range(0f,1f)]
[Tooltip("Scale up the steepness value by the wave length, making large waves displace the water more horizontally")]
public float steepnessByLength;
[Space]
[Range(0f,360f)]
public float directionBase = 0f;
[Range(0f, 360)]
[Tooltip("If at 0, all waves move in a single direction, If at 360, all of them go in a random direction")]
public float directionAngleVariation = 180f;
public void Apply(WaveProfile waveProfile)
{
Array.Resize(ref waveProfile.layers, numLayers);
float ScaleByLength(float value, float min, float max, float length, float amount)
{
value *= Mathf.Lerp(1f, value / length, amount);
value = Mathf.Max(value, min);
return value;
}
int layerCount = waveProfile.layers.Length;
for (int i = 0; i < layerCount; i++)
{
if (waveProfile.layers[i] == null) waveProfile.layers[i] = new Wave();
Wave layer = waveProfile.layers[i];
Random.InitState(seed + i);
float t = (float)i / (float)layerCount;
layer.direction = Mathf.Repeat(directionBase + Random.Range(-directionAngleVariation, directionAngleVariation), 360f);
layer.waveLength = Random.Range(minMaxWaveLength.x, minMaxWaveLength.y);
layer.amplitude = Random.Range(minMaxAmplitude.x, minMaxAmplitude.y);
layer.amplitude = ScaleByLength(layer.amplitude, minMaxAmplitude.x, minMaxAmplitude.y, layer.waveLength, amplitudeByLength);
layer.steepness = Random.Range(minMaxSteepness.x, minMaxSteepness.y);
layer.steepness = ScaleByLength(layer.steepness, minMaxSteepness.x, minMaxSteepness.y, layer.waveLength, steepnessByLength);
}
waveProfile.UpdateShaderParameters();
}
}
/// <summary>
/// House various parameter values used for randomize wave profile creation. Call the <see cref="Apply">Apply</see> function to use the settings to generated randomized wave layers.
/// </summary>
public ProceduralSettings proceduralSettings = new ProceduralSettings();
/// <summary>
/// Class to describe a single Gerstner Wave
/// </summary>
[Serializable]
public class Wave
{
[Tooltip("Directional: Waves move in a specific direction (angle)" +
"\n\nRadial: Wave originates from the position defined below")]
public enum Mode
{
Directional,
Radial
}
public bool enabled = true;
[Space]
[Tooltip("Distance between each crest")]
[Range(0.1f, 64f)]
public float waveLength = 10f;
[Tooltip("Height of the wave in units(m)")]
[Range(0.001f, MAX_AMPLITUDE)]
public float amplitude = 1f;
[Tooltip("Amount of horizontal movement. Values too high can cause the crest of a wave to \"loop\"")]
[Range(0.001f, 1f)]
public float steepness = 0.5f;
public Mode mode;
[Tooltip("Direction the wave travels forward in degrees (on the Y-axis)")]
[Range(0f, 360f)]
public float direction;
[Tooltip("Position in world-space")]
public Vector2 origin;
}
[Space]
public Wave[] layers = new Wave[8];
public Texture2D shaderParametersLUT;
public float averageSteepness
{
get;
private set;
}
public float averageAmplitude
{
get;
private set;
}
private void Reset()
{
for (int i = 0; i < layers.Length; i++)
{
layers[i] = new Wave();
float t = (float)i / (float)layers.Length;
if (i == 0)
{
layers[i].enabled = true;
}
else
{
layers[i].enabled = false;
layers[i].direction = t * 360f + Random.Range(-15f, 15f);
layers[i].waveLength = layers.Length - (t * Random.value);
}
}
UpdateShaderParameters();
}
public void UpdateShaderParameters()
{
if (layers.Length == 0) return;
shaderParametersLUT = CreateLookUpTable();
shaderParametersLUT.hideFlags = HideFlags.NotEditable;
shaderParametersLUT.name = this.name + " Shader Parameters";
#if UNITY_EDITOR
string path = AssetDatabase.GetAssetPath(this);
if (path == string.Empty) return;
Texture2D file = (Texture2D)AssetDatabase.LoadAssetAtPath(path, typeof(Texture2D));
if (file == null)
{
Object mainAsset = (WaveProfile)AssetDatabase.LoadAssetAtPath(path, typeof(WaveProfile));
AssetDatabase.AddObjectToAsset(shaderParametersLUT, mainAsset);
AssetDatabase.SaveAssets();
//Import
path = AssetDatabase.GetAssetPath(shaderParametersLUT);
//Reference serialized texture asset
shaderParametersLUT = (Texture2D)AssetDatabase.LoadAssetAtPath(path, typeof(Texture2D));
}
else
{
EditorUtility.CopySerialized(shaderParametersLUT, file);
file.name = shaderParametersLUT.name;
}
//Reference serialized texture asset on disk
shaderParametersLUT = file;
#endif
}
//Having a total of 8 float4 components, 2 rows are required to store them.
private const int LUT_ROWS = 2;
//Settings for a wave layer, converted for GPU use
public struct WaveParameters
{
public float waveLength;
public float amplitude;
public float direction;
public float steepness;
public float2 origin;
public uint mode;
public uint enabled;
};
private Texture2D CreateLookUpTable()
{
int layerCount = layers.Length;
if (layers.Length == 0)
{
throw new Exception("Cannot create a wave profile LUT from 0 wave layers!");
}
//16-bit precision since values can exceed 1
Texture2D texture = new Texture2D(layerCount, LUT_ROWS, TextureFormat.RGBAHalf, false, true)
{
filterMode = FilterMode.Point,
wrapMode = TextureWrapMode.Clamp,
};
//The summed steepness must never exceed a value of 1 in the final calculations
//This would otherwise incur visible loops in the wave crests
//To avoid this, divide the steepness parameter value by the average
RecalculateAverages();
float steepnessRCP = 1f / averageSteepness;
float amplitudeRCP = 1f / averageSteepness;
for (int x = 0; x < layerCount; x++)
{
//Normalized value of the layer (0-1)
float t = (float)x / (float)layerCount;
WaveParameters parameters = CreateWaveParameters(layers[x], t, steepnessRCP);
Color row0 = new Color(parameters.amplitude, parameters.waveLength, parameters.direction, parameters.enabled);
texture.SetPixel(x, 0, row0);
Color row1 = new Color(parameters.origin.x, parameters.origin.y, parameters.mode, parameters.steepness);
texture.SetPixel(x, 1, row1);
}
texture.Apply();
return texture;
}
public WaveParameters CreateWaveParameters(Wave wave, float t, float steepnessRCP)
{
WaveParameters parameters = new WaveParameters
{
enabled = (uint)(wave.enabled ? 1 : 0),
waveLength = Mathf.Max(0.01f, wave.waveLength * waveLengthMultiplier * waveLengthCurve.Evaluate(t)),
amplitude = (wave.amplitude * amplitudeMultiplier * amplitudeCurve.Evaluate(t)),
direction = wave.direction * Mathf.Deg2Rad,
};
parameters.origin.x = wave.origin.x;
parameters.origin.y = wave.origin.y;
parameters.mode = (uint)wave.mode;
parameters.steepness = wave.steepness * steepnessMultiplier * steepnessCurve.Evaluate(t) * Mathf.Lerp(1f, steepnessRCP, steepnessClamping);
return parameters;
}
public void RecalculateAverages()
{
averageSteepness = 0f;
averageAmplitude = 0f;
int activeLayers = 0;
for (int x = 0; x < layers.Length; x++)
{
//Normalized value of the layer (0-1)
float t = (float)x / (float)layers.Length;
if (layers[x].enabled)
{
activeLayers++;
averageSteepness += layers[x].steepness * steepnessMultiplier * steepnessCurve.Evaluate(t);
averageAmplitude += layers[x].amplitude * amplitudeMultiplier * amplitudeCurve.Evaluate(t);
}
}
if (activeLayers > 0)
{
averageSteepness = activeLayers;
averageAmplitude /= activeLayers;
}
}
/// <summary>
/// Assign this wave profile to a material using the Stylized Water 3 shader
/// </summary>
/// <param name="material"></param>
public void ApplyToMaterial(Material material)
{
material.SetTexture(ShaderParams.Properties._WaveProfile, shaderParametersLUT);
}
}
}