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

275 lines
11 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 System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Profiling;
using Unity.Mathematics;
namespace StylizedWater3
{
public static partial class HeightQuerySystem
{
private const string PROFILER_PREFIX = "[GPU] Water Height Query:";
public static bool DISABLE_IN_EDIT_MODE
{
#if UNITY_EDITOR
get { return UnityEditor.EditorPrefs.GetBool("SW3_HeightQuerySystem_EditMode", false); }
set { UnityEditor.EditorPrefs.SetBool("SW3_HeightQuerySystem_EditMode", value); }
#else
get { return false; }
#endif
}
/// <summary>
/// Reports if the current device/platform supports Compute Shaders
/// </summary>
/// <returns></returns>
public static bool IsSupported()
{
#if UNITY_WEBGL
return false;
#else
return SystemInfo.supportsComputeShaders;
#endif
}
internal const string UNSUPPORTED_MESSAGE = "[Stylized Water 3] Compute shaders are reportedly not supported on this platform. The GPU Height readback technique relies on this, so is not supported either. " +
"If you are using any \"Align To Water\" components, or custom buoyancy physics using the API, switch all of them to the \"CPU\" method.";
/// <summary>
/// Verifies if the returned height value is valid. If not, the sampling position fell outside the camera frustum or was not above any water surface
/// If false, do not incorporate this value in any processing!
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static bool EqualsVoid(float value)
{
return value <= HeightPrePass.VOID_THRESHOLD;
}
/// <summary>
/// Given 4 height values (each representing the points of a +sign) a normal vector can be derived
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <param name="down"></param>
/// <param name="up"></param>
/// <param name="strength"></param>
/// <returns></returns>
public static Vector3 DeriveNormal(float left, float right, float down, float up, float strength = 1f)
{
float xDelta = (left - right) * strength;
float zDelta = (down - up) * strength;
return Vector3.Normalize(new Vector3(xDelta, 1.0f, zDelta));
}
/// <summary>
/// A generic front-end to determine the method used to sampling the water height
/// </summary>
[Serializable]
public class Interface
{
public enum Method
{
[InspectorName("GPU (Async height readback)")]
GPU,
[InspectorName("CPU (Wave pattern replication)")]
CPU
}
[Tooltip("Two completely different methods can be used to reproduce the wave height." +
"\n\n" +
"[GPU] Requires the \"Height Pre-pass\" feature to be enabled on the render feature. This queues up height samples and processes them in a compute shader, the result will be read back from the GPU asynchronously. " +
"\n\n" +
"Height values will represent the water surface height as it literally appears in the world, including any and all displacement effects. Slowest method, but ultimately more flexible." +
"\n\n" +
"[CPU] Given a water level, and water material, the same wave pattern can be 1:1 replicated through script. Does not include displacement effects and supports flat water geometry only. Fastest method")]
public Method method = Method.GPU;
[Tooltip("This reference is required to grab the wave distance and height values")]
public WaterObject waterObject;
[Tooltip("Try to find the Water Object below or above the Transform's position. This is slower than assigning a specific Water Object directly!")]
public bool autoFind = true;
public WaveProfile waveProfile;
public enum WaterLevelSource
{
FixedValue,
[InspectorName("Water Object Y-position")]
WaterObject,
Transform,
Ocean
}
[Tooltip("Configure what should be used to set the base water level. Relative wave height is added to this value")]
public WaterLevelSource waterLevelSource = WaterLevelSource.WaterObject;
[Tooltip("This transform's Y-position is used as the base water level, this value is important and required for correct rendering. As such, underwater rendering does not work with rivers or other non-flat water")]
public Transform waterLevelTransform;
public float waterLevel;
/// <summary>
/// Based on the current configuration, retrieve the water level height value
/// </summary>
/// <returns></returns>
public float GetWaterLevel()
{
if (waterLevelSource == WaterLevelSource.WaterObject && waterObject) return waterObject.transform.position.y;
if (waterLevelSource == WaterLevelSource.Transform && waterLevelTransform) return waterLevelTransform.position.y;
if (waterLevelSource == WaterLevelSource.Ocean && OceanFollowBehaviour.Instance)
{
//Store it, so that it is always valid even when the singleton hasn't loaded yet
waterLevel = OceanFollowBehaviour.Instance.transform.position.y;
return waterLevel;
}
return waterLevel;
}
public bool IsRiverMaterial()
{
return waterObject.material.IsKeywordEnabled(ShaderParams.Keywords.River);
}
public WaterObject GetWaterObject(Vector3 worldPosition)
{
if (autoFind) waterObject = WaterObject.Find(worldPosition, false);
return waterObject;
}
public bool HasMissingReferences()
{
return (waterObject && waterObject.material && waveProfile) == false;
}
}
/// <summary>
/// List of queries being submitted the next frame
/// </summary>
public static readonly List<Query> queries = new List<Query>();
public static int QueryCount { get; private set; }
/// <summary>
/// If there are any height queries present, the displacement pre-pass must execute to ensure data is being returned
/// </summary>
public static bool RequiresHeightPrepass => QueryCount > 0;
private static void AddRequest(AsyncRequest request)
{
//Find the next available query with enough space for the positions
//-If not, create a new query
Profiler.BeginSample($"{PROFILER_PREFIX} Add Request");
foreach (Query q in queries)
{
if (q.requests.ContainsKey(request.hashCode))
{
throw new Exception($"A request with the ID {request.hashCode} has already been issued. Use the \"onReadbackCompleted\" callback to receive the data." +
$"Use the DisposeRequest function only when you are sure you no longer need the data.");
}
}
int queryIndex = queries.Count;
int sampleCount = request.sampler.SampleCount;
if (sampleCount == 0)
{
return;
}
void CreateNewQuery()
{
queries.Add(new Query());
QueryCount++;
queryIndex++;
}
//Initial query needs to be created
if (queryIndex == 0)
{
CreateNewQuery();
}
int occupiedIndices = HeightQuerySystem.Query.MAX_SIZE - queries[queryIndex - 1].availableIndices.Count;
//Not enough space in the latest query for this many samples
if (occupiedIndices + sampleCount >= HeightQuerySystem.Query.MAX_SIZE)
{
CreateNewQuery();
//Debug.Log($"Query #{queryIndex-1} contains {occupiedIndices}, requires {occupiedIndices + sampleCount}. Created a new query (#{queryIndex}).");
}
var query = queries[queryIndex-1];
//Assign the indices available in the query to the request
for (int i = 0; i < sampleCount; i++)
{
request.indices.Add(query.GetNextAvailableIndex());
}
query.sampleCount += sampleCount;
query.requests.Add(request.hashCode, request);
Profiler.EndSample();
}
private static void WithdrawRequest(AsyncRequest request)
{
Profiler.BeginSample($"{PROFILER_PREFIX} Withdraw Request");
for (int i = 0; i < queries.Count; i++)
{
var query = queries[i];
if (query.requests.TryGetValue(request.hashCode, out _))
{
//Remove request from query
query.requests.Remove(request.hashCode);
int indexCount = request.indices.Count;
//Return the occupied indices to the pool
for (int j = 0; j < indexCount; j++)
{
query.ReleaseIndex(request.indices[j]);
}
//Update the current sample count
query.sampleCount -= indexCount;
//Clear the list of occupied indices, these will be repopulate should the request be issued again
request.indices.Clear();
//If the query is now completely empty, yeet it
if (query.requests.Count == 0)
{
//Debug.Log($"Query #{i} is now empty and was disposed");
query.Dispose();
}
}
}
Profiler.EndSample();
}
/// <summary>
/// Clears all queries from the system
/// </summary>
//Need to force a clean start when entering/exiting play mode. Otherwise certain arrays will get de-allocated.
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
public static void Clear()
{
for (int i = 0; i < queries.Count; i++)
{
queries[i].Dispose();
}
}
}
}