修改水
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
// Crest Water System
|
||||
// Copyright © 2024 Wave Harmonic. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using WaveHarmonic.Crest.Internal;
|
||||
|
||||
namespace WaveHarmonic.Crest
|
||||
{
|
||||
/// <summary>
|
||||
/// Demarcates an AABB area where water is present in the world.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If present, water tiles will be culled if they don't overlap any WaterBody.
|
||||
/// </remarks>
|
||||
[@ExecuteDuringEditMode]
|
||||
[AddComponentMenu(Constants.k_MenuPrefixScripts + "Water Body")]
|
||||
[@HelpURL("Manual/WaterBodies.html")]
|
||||
public sealed partial class WaterBody : ManagedBehaviour<WaterRenderer>
|
||||
{
|
||||
[SerializeField, HideInInspector]
|
||||
#pragma warning disable 414
|
||||
int _Version = 0;
|
||||
#pragma warning restore 414
|
||||
|
||||
[Tooltip("Makes sure this water body is not clipped.\n\nIf clipping is enabled and set to clip everywhere by default, this option will register this water body to ensure its area does not get clipped.")]
|
||||
[@GenerateAPI(name: "Clipped")]
|
||||
[SerializeField]
|
||||
bool _Clip = true;
|
||||
|
||||
[Tooltip("Water chunks that overlap this waterbody area will be assigned this material.\n\nThis is useful for varying water appearance across different water bodies. If no override material is specified, the default material assigned to the WaterRenderer component will be used.")]
|
||||
[@AttachMaterialEditor]
|
||||
[@GenerateAPI(name: "AboveSurfaceMaterial")]
|
||||
[@MaterialField("Crest/Water", name: "Water", title: "Create Water Material"), SerializeField]
|
||||
internal Material _Material = null;
|
||||
|
||||
[Tooltip("Overrides the property on the Water Renderer with the same name when the camera is inside the bounds.")]
|
||||
[@AttachMaterialEditor]
|
||||
[@GenerateAPI]
|
||||
[@MaterialField("Crest/Water", name: "Water (Below)", title: "Create Water Material", parent: nameof(_Material)), SerializeField]
|
||||
internal Material _BelowSurfaceMaterial;
|
||||
|
||||
[Tooltip("Overrides the Water Renderer's volume material when the camera is inside the bounds.")]
|
||||
[@MaterialField("Crest/Underwater", name: "Underwater", title: "Create Underwater Material")]
|
||||
[@AttachMaterialEditor]
|
||||
[@GenerateAPI]
|
||||
[SerializeField]
|
||||
internal Material _VolumeMaterial;
|
||||
|
||||
|
||||
bool _RecalculateRect = true;
|
||||
bool _RecalculateBounds = true;
|
||||
|
||||
|
||||
sealed class ClipInput : ILodInput
|
||||
{
|
||||
readonly WaterBody _Owner;
|
||||
readonly Transform _Transform;
|
||||
|
||||
public bool Enabled => WaterRenderer.Instance._ClipLod._DefaultClippingState == DefaultClippingState.EverythingClipped;
|
||||
public bool IsCompute => true;
|
||||
public int Pass => -1;
|
||||
|
||||
// TODO: Expose serialized queue.
|
||||
public int Queue => 0;
|
||||
public MonoBehaviour Component => _Owner;
|
||||
|
||||
public Rect Rect => _Owner.Rect;
|
||||
|
||||
public ClipInput(WaterBody owner)
|
||||
{
|
||||
_Owner = owner;
|
||||
_Transform = owner.transform;
|
||||
}
|
||||
|
||||
public void Draw(Lod simulation, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1f, int slices = -1)
|
||||
{
|
||||
var wrapper = new PropertyWrapperCompute(buffer, WaterResources.Instance.Compute._ClipPrimitive, 0);
|
||||
|
||||
wrapper.SetMatrix(ShaderIDs.s_Matrix, _Transform.worldToLocalMatrix);
|
||||
|
||||
// For culling.
|
||||
wrapper.SetVector(ShaderIDs.s_Position, _Transform.position);
|
||||
wrapper.SetFloat(ShaderIDs.s_Diameter, _Transform.lossyScale.Maximum());
|
||||
|
||||
wrapper.SetKeyword(WaterResources.Instance.Keywords.ClipPrimitiveInverted, true);
|
||||
wrapper.SetKeyword(WaterResources.Instance.Keywords.ClipPrimitiveSphere, false);
|
||||
wrapper.SetKeyword(WaterResources.Instance.Keywords.ClipPrimitiveCube, false);
|
||||
wrapper.SetKeyword(WaterResources.Instance.Keywords.ClipPrimitiveRectangle, true);
|
||||
|
||||
wrapper.SetTexture(ShaderIDs.s_Target, target);
|
||||
|
||||
var threads = simulation.Resolution / Lod.k_ThreadGroupSize;
|
||||
wrapper.Dispatch(threads, threads, slices);
|
||||
}
|
||||
|
||||
public float Filter(WaterRenderer water, int slice)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
}
|
||||
|
||||
internal static List<WaterBody> WaterBodies { get; } = new();
|
||||
|
||||
Bounds _Bounds;
|
||||
internal Bounds AABB
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_RecalculateBounds)
|
||||
{
|
||||
CalculateBounds();
|
||||
_RecalculateBounds = false;
|
||||
}
|
||||
|
||||
return _Bounds;
|
||||
}
|
||||
}
|
||||
|
||||
Rect _Rect;
|
||||
Rect Rect
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_RecalculateRect)
|
||||
{
|
||||
_Rect = AABB.RectXZ();
|
||||
_RecalculateRect = false;
|
||||
}
|
||||
|
||||
return _Rect;
|
||||
}
|
||||
}
|
||||
|
||||
internal Material AboveOrBelowSurfaceMaterial => _BelowSurfaceMaterial == null ? _Material : _BelowSurfaceMaterial;
|
||||
|
||||
ClipInput _ClipInput;
|
||||
|
||||
private protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
CalculateBounds();
|
||||
|
||||
WaterBodies.Add(this);
|
||||
|
||||
HandleClipInputRegistration();
|
||||
}
|
||||
|
||||
private protected override void OnDisable()
|
||||
{
|
||||
base.OnDisable();
|
||||
|
||||
WaterBodies.Remove(this);
|
||||
|
||||
if (_ClipInput != null)
|
||||
{
|
||||
ILodInput.Detach(_ClipInput, ClipLod.s_Inputs);
|
||||
|
||||
_ClipInput = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal void CalculateBounds()
|
||||
{
|
||||
var bounds = new Bounds();
|
||||
bounds.center = transform.position;
|
||||
bounds.Encapsulate(transform.TransformPoint(Vector3.right / 2f + Vector3.forward / 2f));
|
||||
bounds.Encapsulate(transform.TransformPoint(Vector3.right / 2f - Vector3.forward / 2f));
|
||||
bounds.Encapsulate(transform.TransformPoint(-Vector3.right / 2f + Vector3.forward / 2f));
|
||||
bounds.Encapsulate(transform.TransformPoint(-Vector3.right / 2f - Vector3.forward / 2f));
|
||||
|
||||
_Bounds = bounds;
|
||||
}
|
||||
|
||||
void HandleClipInputRegistration()
|
||||
{
|
||||
var registered = _ClipInput != null;
|
||||
var shouldBeRegistered = _Clip;
|
||||
|
||||
if (registered != shouldBeRegistered)
|
||||
{
|
||||
if (shouldBeRegistered)
|
||||
{
|
||||
_ClipInput = new(this);
|
||||
|
||||
ILodInput.Attach(_ClipInput, ClipLod.s_Inputs);
|
||||
}
|
||||
else
|
||||
{
|
||||
ILodInput.Detach(_ClipInput, ClipLod.s_Inputs);
|
||||
|
||||
_ClipInput = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private protected override System.Action<WaterRenderer> OnUpdateMethod => OnUpdate;
|
||||
void OnUpdate(WaterRenderer water)
|
||||
{
|
||||
if (transform.hasChanged)
|
||||
{
|
||||
_RecalculateRect = _RecalculateBounds = true;
|
||||
}
|
||||
}
|
||||
|
||||
private protected override System.Action<WaterRenderer> OnLateUpdateMethod => OnLateUpdate;
|
||||
void OnLateUpdate(WaterRenderer water)
|
||||
{
|
||||
transform.hasChanged = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12fa5fcd0e5ac436b8581c4441a2683e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,539 @@
|
||||
// Crest Water System
|
||||
// Copyright © 2024 Wave Harmonic. All rights reserved.
|
||||
|
||||
//#define PROFILE_CONSTRUCTION
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace WaveHarmonic.Crest
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiates all the water geometry, as a set of tiles.
|
||||
/// </summary>
|
||||
static class WaterBuilder
|
||||
{
|
||||
// The comments below illustrate case when BASE_VERT_DENSITY = 2. The water mesh is built up from these patches. Rotational symmetry
|
||||
// is used where possible to eliminate combinations. The slim variants are used to eliminate overlap between patches.
|
||||
enum PatchType
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds no skirt. Used in interior of highest detail LOD (0)
|
||||
///
|
||||
/// 1 -------
|
||||
/// | | |
|
||||
/// z -------
|
||||
/// | | |
|
||||
/// 0 -------
|
||||
/// 0 1
|
||||
/// x
|
||||
///
|
||||
/// </summary>
|
||||
Interior,
|
||||
|
||||
/// <summary>
|
||||
/// Adds a full skirt all of the way around a patch
|
||||
///
|
||||
/// -------------
|
||||
/// | | | | |
|
||||
/// 1 -------------
|
||||
/// | | | | |
|
||||
/// z -------------
|
||||
/// | | | | |
|
||||
/// 0 -------------
|
||||
/// | | | | |
|
||||
/// -------------
|
||||
/// 0 1
|
||||
/// x
|
||||
///
|
||||
/// </summary>
|
||||
Fat,
|
||||
|
||||
/// <summary>
|
||||
/// Adds a skirt on the right hand side of the patch
|
||||
///
|
||||
/// 1 ----------
|
||||
/// | | | |
|
||||
/// z ----------
|
||||
/// | | | |
|
||||
/// 0 ----------
|
||||
/// 0 1
|
||||
/// x
|
||||
///
|
||||
/// </summary>
|
||||
FatX,
|
||||
|
||||
/// <summary>
|
||||
/// Adds a skirt on the right hand side of the patch, removes skirt from top
|
||||
/// </summary>
|
||||
FatXSlimZ,
|
||||
|
||||
/// <summary>
|
||||
/// Outer most side - this adds an extra skirt on the left hand side of the patch,
|
||||
/// which will point outwards and be extended to Zfar
|
||||
///
|
||||
/// 1 --------------------------------------------------------------------------------------
|
||||
/// | | | |
|
||||
/// z --------------------------------------------------------------------------------------
|
||||
/// | | | |
|
||||
/// 0 --------------------------------------------------------------------------------------
|
||||
/// 0 1
|
||||
/// x
|
||||
///
|
||||
/// </summary>
|
||||
FatXOuter,
|
||||
|
||||
/// <summary>
|
||||
/// Adds skirts at the top and right sides of the patch
|
||||
/// </summary>
|
||||
FatXZ,
|
||||
|
||||
/// <summary>
|
||||
/// Adds skirts at the top and right sides of the patch and pushes them to horizon
|
||||
/// </summary>
|
||||
FatXZOuter,
|
||||
|
||||
/// <summary>
|
||||
/// One less set of verts in x direction
|
||||
/// </summary>
|
||||
SlimX,
|
||||
|
||||
/// <summary>
|
||||
/// One less set of verts in both x and z directions
|
||||
/// </summary>
|
||||
SlimXZ,
|
||||
|
||||
/// <summary>
|
||||
/// One less set of verts in x direction, extra verts at start of z direction
|
||||
///
|
||||
/// ----
|
||||
/// | |
|
||||
/// 1 ----
|
||||
/// | |
|
||||
/// z ----
|
||||
/// | |
|
||||
/// 0 ----
|
||||
/// 0 1
|
||||
/// x
|
||||
///
|
||||
/// </summary>
|
||||
SlimXFatZ,
|
||||
|
||||
/// <summary>
|
||||
/// Number of patch types
|
||||
/// </summary>
|
||||
Count,
|
||||
}
|
||||
|
||||
// Keep references to meshes so they can be cleaned up later.
|
||||
static Mesh[] s_Meshes;
|
||||
|
||||
/// <summary>
|
||||
/// Destroy tiles and any resources.
|
||||
/// </summary>
|
||||
public static void CleanUp(WaterRenderer water)
|
||||
{
|
||||
// Not every mesh is assigned to a chunk thus we should destroy all of them here.
|
||||
for (var i = 0; i < s_Meshes?.Length; i++)
|
||||
{
|
||||
Helpers.Destroy(s_Meshes[i]);
|
||||
}
|
||||
|
||||
water.Chunks.Clear();
|
||||
|
||||
// May not be present when entering play mode.
|
||||
if (water.Root)
|
||||
{
|
||||
Helpers.Destroy(water.Root.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public static Transform GenerateMesh(WaterRenderer water, List<WaterChunkRenderer> tiles, int lodDataResolution, int geoDownSampleFactor, int lodCount)
|
||||
{
|
||||
if (lodCount < 1)
|
||||
{
|
||||
Debug.LogError("Crest: Invalid LOD count: " + lodCount.ToString(), water);
|
||||
return null;
|
||||
}
|
||||
|
||||
#if PROFILE_CONSTRUCTION
|
||||
var sw = new System.Diagnostics.Stopwatch();
|
||||
sw.Start();
|
||||
#endif
|
||||
|
||||
var root = new GameObject("Root");
|
||||
Debug.Assert(root != null, "Crest: The water Root transform could not be immediately constructed. Please report this issue to the Crest developers via our support email or GitHub at https://github.com/wave-harmonic/crest/issues .");
|
||||
|
||||
root.hideFlags = water._Debug._ShowHiddenObjects ? HideFlags.DontSave : HideFlags.HideAndDontSave;
|
||||
root.transform.parent = water.transform;
|
||||
root.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||||
root.transform.localScale = Vector3.one;
|
||||
|
||||
if (!water.IsRunningHeadless && !water.IsRunningWithoutGraphics)
|
||||
{
|
||||
// create mesh data
|
||||
s_Meshes = new Mesh[(int)PatchType.Count];
|
||||
var meshBounds = new Bounds[(int)PatchType.Count];
|
||||
// 4 tiles across a LOD, and support lowering density by a factor
|
||||
var tileResolution = Mathf.Round(0.25f * lodDataResolution / geoDownSampleFactor);
|
||||
for (var i = 0; i < (int)PatchType.Count; i++)
|
||||
{
|
||||
s_Meshes[i] = BuildPatch(water, (PatchType)i, tileResolution, out meshBounds[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < lodCount; i++)
|
||||
{
|
||||
CreateLOD(water, tiles, root.transform, i, lodCount, s_Meshes, meshBounds, lodDataResolution, geoDownSampleFactor, water.Layer);
|
||||
}
|
||||
}
|
||||
|
||||
#if PROFILE_CONSTRUCTION
|
||||
sw.Stop();
|
||||
Debug.Log( "Crest: Finished generating " + lodCount.ToString() + " LODs, time: " + (1000.0*sw.Elapsed.TotalSeconds).ToString(".000") + "ms" );
|
||||
#endif
|
||||
|
||||
return root.transform;
|
||||
}
|
||||
|
||||
static Mesh BuildPatch(WaterRenderer water, PatchType pt, float vertDensity, out Bounds bounds)
|
||||
{
|
||||
var verts = new List<Vector3>();
|
||||
var indices = new List<int>();
|
||||
|
||||
// stick a bunch of verts into a 1m x 1m patch (scaling happens later)
|
||||
var dx = 1f / vertDensity;
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// verts
|
||||
|
||||
// see comments within PatchType for diagrams of each patch mesh
|
||||
|
||||
// skirt widths on left, right, bottom and top (in order)
|
||||
float skirtXminus = 0f, skirtXplus = 0f;
|
||||
float skirtZminus = 0f, skirtZplus = 0f;
|
||||
// set the patch size
|
||||
if (pt == PatchType.Fat) { skirtXminus = skirtXplus = skirtZminus = skirtZplus = 1f; }
|
||||
else if (pt is PatchType.FatX or PatchType.FatXOuter) { skirtXplus = 1f; }
|
||||
else if (pt is PatchType.FatXZ or PatchType.FatXZOuter) { skirtXplus = skirtZplus = 1f; }
|
||||
else if (pt == PatchType.FatXSlimZ) { skirtXplus = 1f; skirtZplus = -1f; }
|
||||
else if (pt == PatchType.SlimX) { skirtXplus = -1f; }
|
||||
else if (pt == PatchType.SlimXZ) { skirtXplus = skirtZplus = -1f; }
|
||||
else if (pt == PatchType.SlimXFatZ) { skirtXplus = -1f; skirtZplus = 1f; }
|
||||
|
||||
var sideLength_verts_x = 1f + vertDensity + skirtXminus + skirtXplus;
|
||||
var sideLength_verts_z = 1f + vertDensity + skirtZminus + skirtZplus;
|
||||
|
||||
var start_x = -0.5f - skirtXminus * dx;
|
||||
var start_z = -0.5f - skirtZminus * dx;
|
||||
var end_x = 0.5f + skirtXplus * dx;
|
||||
var end_z = 0.5f + skirtZplus * dx;
|
||||
|
||||
// With a default value of 100, this will reach the horizon at all levels at
|
||||
// a far plane of 200k.
|
||||
var extentsMultiplier = water._ExtentsSizeMultiplier * (Lod.k_MaximumSlices + 1 - water.LodLevels);
|
||||
|
||||
for (float j = 0; j < sideLength_verts_z; j++)
|
||||
{
|
||||
// interpolate z across patch
|
||||
var z = Mathf.Lerp(start_z, end_z, j / (sideLength_verts_z - 1f));
|
||||
|
||||
// push outermost edge out to horizon
|
||||
if (pt == PatchType.FatXZOuter && j == sideLength_verts_z - 1f)
|
||||
z *= extentsMultiplier;
|
||||
|
||||
for (float i = 0; i < sideLength_verts_x; i++)
|
||||
{
|
||||
// interpolate x across patch
|
||||
var x = Mathf.Lerp(start_x, end_x, i / (sideLength_verts_x - 1f));
|
||||
|
||||
// push outermost edge out to horizon
|
||||
if (i == sideLength_verts_x - 1f && (pt == PatchType.FatXOuter || pt == PatchType.FatXZOuter))
|
||||
x *= extentsMultiplier;
|
||||
|
||||
// could store something in y, although keep in mind this is a shared mesh that is shared across multiple lods
|
||||
verts.Add(new(x, 0f, z));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// indices
|
||||
|
||||
var sideLength_squares_x = (int)sideLength_verts_x - 1;
|
||||
var sideLength_squares_z = (int)sideLength_verts_z - 1;
|
||||
|
||||
for (var j = 0; j < sideLength_squares_z; j++)
|
||||
{
|
||||
for (var i = 0; i < sideLength_squares_x; i++)
|
||||
{
|
||||
var flipEdge = false;
|
||||
|
||||
if (i % 2 == 1) flipEdge = !flipEdge;
|
||||
if (j % 2 == 1) flipEdge = !flipEdge;
|
||||
|
||||
var i0 = i + j * (sideLength_squares_x + 1);
|
||||
var i1 = i0 + 1;
|
||||
var i2 = i0 + (sideLength_squares_x + 1);
|
||||
var i3 = i2 + 1;
|
||||
|
||||
if (!flipEdge)
|
||||
{
|
||||
// tri 1
|
||||
indices.Add(i3);
|
||||
indices.Add(i1);
|
||||
indices.Add(i0);
|
||||
|
||||
// tri 2
|
||||
indices.Add(i0);
|
||||
indices.Add(i2);
|
||||
indices.Add(i3);
|
||||
}
|
||||
else
|
||||
{
|
||||
// tri 1
|
||||
indices.Add(i3);
|
||||
indices.Add(i1);
|
||||
indices.Add(i2);
|
||||
|
||||
// tri 2
|
||||
indices.Add(i0);
|
||||
indices.Add(i2);
|
||||
indices.Add(i1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// create mesh
|
||||
|
||||
var mesh = new Mesh();
|
||||
if (verts != null && verts.Count > 0)
|
||||
{
|
||||
var arrV = new Vector3[verts.Count];
|
||||
verts.CopyTo(arrV);
|
||||
|
||||
var arrI = new int[indices.Count];
|
||||
indices.CopyTo(arrI);
|
||||
|
||||
mesh.SetIndices(null, MeshTopology.Triangles, 0);
|
||||
mesh.vertices = arrV;
|
||||
|
||||
// HDRP needs full data. Do this on a define to keep door open to runtime changing of RP.
|
||||
#if d_UnityHDRP
|
||||
var norms = new Vector3[verts.Count];
|
||||
for (var i = 0; i < norms.Length; i++) norms[i] = Vector3.up;
|
||||
var tans = new Vector4[verts.Count];
|
||||
for (var i = 0; i < tans.Length; i++) tans[i] = new(1, 0, 0, 1);
|
||||
|
||||
mesh.normals = norms;
|
||||
mesh.tangents = tans;
|
||||
#else
|
||||
mesh.normals = null;
|
||||
#endif
|
||||
|
||||
mesh.SetIndices(arrI, MeshTopology.Triangles, 0);
|
||||
|
||||
// recalculate bounds. add a little allowance for snapping. in the chunk renderer script, the bounds will be expanded further
|
||||
// to allow for horizontal displacement
|
||||
mesh.RecalculateBounds();
|
||||
bounds = mesh.bounds;
|
||||
// Increase snapping allowance (see #1148). Value was chosen by observation with a
|
||||
// custom debug mode to show pixels that were out of bounds.
|
||||
dx *= 3f;
|
||||
bounds.extents = new(bounds.extents.x + dx, bounds.extents.y, bounds.extents.z + dx);
|
||||
mesh.bounds = bounds;
|
||||
mesh.name = pt.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
bounds = new();
|
||||
}
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
static void CreateLOD(WaterRenderer water, List<WaterChunkRenderer> tiles, Transform parent, int lodIndex, int lodCount, Mesh[] meshData, Bounds[] meshBounds, int lodDataResolution, int geoDownSampleFactor, int layer)
|
||||
{
|
||||
var horizScale = Mathf.Pow(2f, lodIndex);
|
||||
|
||||
var isBiggestLOD = lodIndex == lodCount - 1;
|
||||
var generateSkirt = isBiggestLOD;
|
||||
|
||||
#if CREST_DEBUG
|
||||
generateSkirt = generateSkirt && !water._Debug._DisableSkirt;
|
||||
#endif
|
||||
|
||||
Vector2[] offsets;
|
||||
PatchType[] patchTypes;
|
||||
|
||||
var leadSideType = generateSkirt ? PatchType.FatXOuter : PatchType.SlimX;
|
||||
var trailSideType = generateSkirt ? PatchType.FatXOuter : PatchType.FatX;
|
||||
var leadCornerType = generateSkirt ? PatchType.FatXZOuter : PatchType.SlimXZ;
|
||||
var trailCornerType = generateSkirt ? PatchType.FatXZOuter : PatchType.FatXZ;
|
||||
var tlCornerType = generateSkirt ? PatchType.FatXZOuter : PatchType.SlimXFatZ;
|
||||
var brCornerType = generateSkirt ? PatchType.FatXZOuter : PatchType.FatXSlimZ;
|
||||
|
||||
if (lodIndex != 0)
|
||||
{
|
||||
// instance indices:
|
||||
// 0 1 2 3
|
||||
// 4 5
|
||||
// 6 7
|
||||
// 8 9 10 11
|
||||
offsets = new Vector2[] {
|
||||
new(-1.5f,1.5f), new(-0.5f,1.5f), new(0.5f,1.5f), new(1.5f,1.5f),
|
||||
new(-1.5f,0.5f), new(1.5f,0.5f),
|
||||
new(-1.5f,-0.5f), new(1.5f,-0.5f),
|
||||
new(-1.5f,-1.5f), new(-0.5f,-1.5f), new(0.5f,-1.5f), new(1.5f,-1.5f),
|
||||
};
|
||||
|
||||
// usually rings have an extra side of verts that point inwards. the outermost ring has both the inward
|
||||
// verts and also and additional outwards set of verts that go to the horizon
|
||||
patchTypes = new PatchType[] {
|
||||
tlCornerType, leadSideType, leadSideType, leadCornerType,
|
||||
trailSideType, leadSideType,
|
||||
trailSideType, leadSideType,
|
||||
trailCornerType, trailSideType, trailSideType, brCornerType,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// first LOD has inside bit as well:
|
||||
// 0 1 2 3
|
||||
// 4 5 6 7
|
||||
// 8 9 10 11
|
||||
// 12 13 14 15
|
||||
offsets = new Vector2[] {
|
||||
new(-1.5f,1.5f), new(-0.5f,1.5f), new(0.5f,1.5f), new(1.5f,1.5f),
|
||||
new(-1.5f,0.5f), new(-0.5f,0.5f), new(0.5f,0.5f), new(1.5f,0.5f),
|
||||
new(-1.5f,-0.5f), new(-0.5f,-0.5f), new(0.5f,-0.5f), new(1.5f,-0.5f),
|
||||
new(-1.5f,-1.5f), new(-0.5f,-1.5f), new(0.5f,-1.5f), new(1.5f,-1.5f),
|
||||
};
|
||||
|
||||
|
||||
// all interior - the "side" types have an extra skirt that points inwards - this means that this inner most
|
||||
// section doesn't need any skirting. this is good - this is the highest density part of the mesh.
|
||||
patchTypes = new PatchType[] {
|
||||
tlCornerType, leadSideType, leadSideType, leadCornerType,
|
||||
trailSideType, PatchType.Interior, PatchType.Interior, leadSideType,
|
||||
trailSideType, PatchType.Interior, PatchType.Interior, leadSideType,
|
||||
trailCornerType, trailSideType, trailSideType, brCornerType,
|
||||
};
|
||||
}
|
||||
|
||||
#if CREST_DEBUG
|
||||
// debug toggle to force all patches to be the same. they'll be made with a surrounding skirt to make sure patches
|
||||
// overlap
|
||||
if (water._Debug._UniformTiles)
|
||||
{
|
||||
for (var i = 0; i < patchTypes.Length; i++)
|
||||
{
|
||||
patchTypes[i] = PatchType.Fat;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// create the water patches
|
||||
for (var i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
// instantiate and place patch
|
||||
var patch = water._ChunkTemplate
|
||||
? Helpers.InstantiatePrefab(water._ChunkTemplate)
|
||||
: new();
|
||||
// Also applying the hide flags to the chunk will prevent it from being pickable in the editor.
|
||||
patch.hideFlags = water._Debug._ShowHiddenObjects ? HideFlags.DontSave : HideFlags.HideAndDontSave;
|
||||
patch.name = $"Tile_L{lodIndex}_{patchTypes[i]}";
|
||||
patch.layer = layer;
|
||||
patch.transform.parent = parent;
|
||||
var pos = offsets[i];
|
||||
patch.transform.localPosition = horizScale * new Vector3(pos.x, 0f, pos.y);
|
||||
// scale only horizontally, otherwise culling bounding box will be scaled up in y
|
||||
patch.transform.localScale = new(horizScale, 1f, horizScale);
|
||||
|
||||
if (!patch.TryGetComponent<MeshRenderer>(out var mr))
|
||||
{
|
||||
mr = patch.AddComponent<MeshRenderer>();
|
||||
// I don't think one would use light probes for a purely specular water surface? (although diffuse
|
||||
// foam shading would benefit).
|
||||
mr.lightProbeUsage = LightProbeUsage.Off;
|
||||
}
|
||||
|
||||
{
|
||||
var mesh = Object.Instantiate(meshData[(int)patchTypes[i]]);
|
||||
mesh.name = meshData[(int)patchTypes[i]].name;
|
||||
patch.AddComponent<MeshFilter>().sharedMesh = mesh;
|
||||
|
||||
var chunk = patch.AddComponent<WaterChunkRenderer>();
|
||||
chunk._Water = water;
|
||||
chunk._BoundsLocal = meshBounds[(int)patchTypes[i]];
|
||||
|
||||
chunk.SetInstanceData(lodIndex);
|
||||
tiles.Add(chunk);
|
||||
}
|
||||
|
||||
// Sorting order to stop unity drawing it back to front. Make the innermost four tiles draw first,
|
||||
// followed by the rest of the tiles by LOD index.
|
||||
if (RenderPipelineHelper.IsHighDefinition)
|
||||
{
|
||||
// HDRP has a different rendering priority system:
|
||||
// https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@10.10/manual/Renderer-And-Material-Priority.html#sorting-by-renderer
|
||||
mr.rendererPriority = -lodCount + (patchTypes[i] == PatchType.Interior ? -1 : lodIndex);
|
||||
}
|
||||
else if (!water.AllowRenderQueueSorting)
|
||||
{
|
||||
// Sorting order to stop unity drawing it back to front. make the innermost 4 tiles draw first, followed by
|
||||
// the rest of the tiles by LOD index. all this happens before layer 0 - the sorting layer takes priority over the
|
||||
// render queue it seems! ( https://cdry.wordpress.com/2017/04/28/unity-render-queues-vs-sorting-layers/ ). This pushes
|
||||
// water rendering way early, so transparent objects will by default render afterwards, which is typical for water rendering.
|
||||
mr.sortingOrder = -lodCount + (patchTypes[i] == PatchType.Interior ? -1 : lodIndex);
|
||||
}
|
||||
|
||||
mr.shadowCastingMode = water.CastShadows ? ShadowCastingMode.On : ShadowCastingMode.Off;
|
||||
|
||||
// This setting is ignored by Unity for the transparent water shader.
|
||||
mr.receiveShadows = false;
|
||||
|
||||
// BIRP needs further investigation.
|
||||
mr.motionVectorGenerationMode = !water.WriteMotionVectors
|
||||
? MotionVectorGenerationMode.ForceNoMotion
|
||||
: MotionVectorGenerationMode.Object;
|
||||
|
||||
mr.material = water.Material;
|
||||
|
||||
// rotate side patches to point the +x side outwards
|
||||
var rotateXOutwards = patchTypes[i] is PatchType.FatX or PatchType.FatXOuter or PatchType.SlimX or PatchType.SlimXFatZ;
|
||||
if (rotateXOutwards)
|
||||
{
|
||||
if (Mathf.Abs(pos.y) >= Mathf.Abs(pos.x))
|
||||
patch.transform.localEulerAngles = 90f * Mathf.Sign(pos.y) * -Vector3.up;
|
||||
else
|
||||
patch.transform.localEulerAngles = pos.x < 0f ? Vector3.up * 180f : Vector3.zero;
|
||||
}
|
||||
|
||||
// rotate the corner patches so the +x and +z sides point outwards
|
||||
var rotateXZOutwards = patchTypes[i] is PatchType.FatXZ or PatchType.SlimXZ or PatchType.FatXSlimZ or PatchType.FatXZOuter;
|
||||
if (rotateXZOutwards)
|
||||
{
|
||||
// xz direction before rotation
|
||||
var from = new Vector3(1f, 0f, 1f).normalized;
|
||||
// target xz direction is outwards vector given by local patch position - assumes this patch is a corner (checked below)
|
||||
var to = patch.transform.localPosition.normalized;
|
||||
if (Mathf.Abs(patch.transform.localPosition.x) < 0.0001f || Mathf.Abs(Mathf.Abs(patch.transform.localPosition.x) - Mathf.Abs(patch.transform.localPosition.z)) > 0.001f)
|
||||
{
|
||||
Debug.LogWarning("Crest: Skipped rotating a patch because it isn't a corner, click here to highlight.", patch);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Detect 180 degree rotations as it doesn't always rotate around Y
|
||||
if (Vector3.Dot(from, to) < -0.99f)
|
||||
patch.transform.localEulerAngles = Vector3.up * 180f;
|
||||
else
|
||||
patch.transform.localRotation = Quaternion.FromToRotation(from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74a58ee5c012f4e38be0d38da61de24a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,403 @@
|
||||
// Crest Water System
|
||||
// Copyright © 2024 Wave Harmonic. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using WaveHarmonic.Crest.Internal;
|
||||
|
||||
namespace WaveHarmonic.Crest
|
||||
{
|
||||
interface IReportsHeight
|
||||
{
|
||||
bool ReportHeight(ref Rect bounds, ref float minimum, ref float maximum);
|
||||
}
|
||||
|
||||
interface IReportsDisplacement
|
||||
{
|
||||
bool ReportDisplacement(ref Rect bounds, ref float horizontal, ref float vertical);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets shader parameters for each geometry tile/chunk.
|
||||
/// </summary>
|
||||
#if !CREST_DEBUG
|
||||
[AddComponentMenu("")]
|
||||
#endif
|
||||
[@ExecuteDuringEditMode]
|
||||
sealed class WaterChunkRenderer : ManagedBehaviour<WaterRenderer>
|
||||
{
|
||||
[SerializeField, HideInInspector]
|
||||
#pragma warning disable 414
|
||||
int _Version = 0;
|
||||
#pragma warning restore 414
|
||||
|
||||
[SerializeField]
|
||||
internal bool _DrawRenderBounds = false;
|
||||
|
||||
static class ShaderIDs
|
||||
{
|
||||
public static readonly int s_ChunkMeshScaleAlpha = Shader.PropertyToID("_Crest_ChunkMeshScaleAlpha");
|
||||
public static readonly int s_ChunkGeometryGridWidth = Shader.PropertyToID("_Crest_ChunkGeometryGridWidth");
|
||||
public static readonly int s_ChunkFarNormalsWeight = Shader.PropertyToID("_Crest_ChunkFarNormalsWeight");
|
||||
public static readonly int s_ChunkNormalScrollSpeed = Shader.PropertyToID("_Crest_ChunkNormalScrollSpeed");
|
||||
public static readonly int s_ChunkMeshScaleAlphaSource = Shader.PropertyToID("_Crest_ChunkMeshScaleAlphaSource");
|
||||
public static readonly int s_ChunkGeometryGridWidthSource = Shader.PropertyToID("_Crest_ChunkGeometryGridWidthSource");
|
||||
}
|
||||
|
||||
internal Bounds _BoundsLocal;
|
||||
Mesh _Mesh;
|
||||
public Renderer Rend { get; private set; }
|
||||
internal MaterialPropertyBlock _MaterialPropertyBlock;
|
||||
Matrix4x4 _PreviousObjectToWorld;
|
||||
|
||||
internal Rect _UnexpandedBoundsXZ = new();
|
||||
public Rect UnexpandedBoundsXZ => _UnexpandedBoundsXZ;
|
||||
|
||||
internal bool _Culled;
|
||||
internal bool _Visible;
|
||||
|
||||
internal WaterRenderer _Water;
|
||||
|
||||
public bool MaterialOverridden { get; set; }
|
||||
|
||||
// We need to ensure that all water data has been bound for the mask to
|
||||
// render properly - this is something that needs to happen irrespective
|
||||
// of occlusion culling because we need the mask to render as a
|
||||
// contiguous surface.
|
||||
internal bool _WaterDataHasBeenBound = true;
|
||||
|
||||
int _LodIndex = -1;
|
||||
|
||||
public static List<IReportsHeight> HeightReporters { get; } = new();
|
||||
public static List<IReportsDisplacement> DisplacementReporters { get; } = new();
|
||||
|
||||
private protected override System.Action<WaterRenderer> OnLateUpdateMethod => OnLateUpdate;
|
||||
|
||||
private protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_MaterialPropertyBlock ??= new();
|
||||
|
||||
if (Rend == null)
|
||||
{
|
||||
Rend = GetComponent<Renderer>();
|
||||
}
|
||||
|
||||
if (_Mesh == null)
|
||||
{
|
||||
// Meshes are cloned so it is safe to use sharedMesh in play mode. We need clones to modify the render bounds.
|
||||
_Mesh = GetComponent<MeshFilter>().sharedMesh;
|
||||
}
|
||||
}
|
||||
|
||||
private protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
|
||||
UpdateMeshBounds();
|
||||
}
|
||||
|
||||
void OnLateUpdate(WaterRenderer water)
|
||||
{
|
||||
// Time slice update to distribute the load.
|
||||
if (!(transform.GetSiblingIndex() % water.TimeSliceBoundsUpdateFrameCount != Time.frameCount % water.Chunks.Count % water.TimeSliceBoundsUpdateFrameCount))
|
||||
{
|
||||
// This needs to be called on Update because the bounds depend on transform scale which can change. Also OnWillRenderObject depends on
|
||||
// the bounds being correct. This could however be called on scale change events, but would add slightly more complexity.
|
||||
UpdateMeshBounds();
|
||||
}
|
||||
|
||||
// Update chunk shader data.
|
||||
_MaterialPropertyBlock ??= new();
|
||||
|
||||
// FIXME: Sometimes thrown.
|
||||
// NullReferenceException: Object reference not set to an instance of an object
|
||||
// WaveHarmonic.Crest.WaterChunkRenderer.OnLateUpdate(WaveHarmonic.Crest.WaterRenderer water)(at Packages/com.waveharmonic.crest/Runtime/Scripts/Surface/WaterChunkRenderer.cs:119)
|
||||
// WaveHarmonic.Crest.WaterRenderer.LateUpdate()(at Packages/com.waveharmonic.crest/Runtime/Scripts/WaterRenderer.cs:733)
|
||||
if (Rend == null)
|
||||
{
|
||||
Rend = GetComponent<Renderer>();
|
||||
}
|
||||
|
||||
Rend.GetPropertyBlock(_MaterialPropertyBlock);
|
||||
_MaterialPropertyBlock.SetInteger(Lod.ShaderIDs.s_LodIndex, _LodIndex);
|
||||
var data = water._PerCascadeInstanceData.Current[_LodIndex];
|
||||
_MaterialPropertyBlock.SetFloat(ShaderIDs.s_ChunkMeshScaleAlpha, data._MeshScaleLerp);
|
||||
_MaterialPropertyBlock.SetFloat(ShaderIDs.s_ChunkGeometryGridWidth, data._GeometryGridWidth);
|
||||
_MaterialPropertyBlock.SetFloat(ShaderIDs.s_ChunkFarNormalsWeight, data._FarNormalsWeight);
|
||||
_MaterialPropertyBlock.SetVector(ShaderIDs.s_ChunkNormalScrollSpeed, data._NormalScrollSpeeds);
|
||||
data = water._PerCascadeInstanceData.Previous(1)[_LodIndex];
|
||||
_MaterialPropertyBlock.SetFloat(ShaderIDs.s_ChunkMeshScaleAlphaSource, data._MeshScaleLerp);
|
||||
_MaterialPropertyBlock.SetFloat(ShaderIDs.s_ChunkGeometryGridWidthSource, data._GeometryGridWidth);
|
||||
Rend.SetPropertyBlock(_MaterialPropertyBlock);
|
||||
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (Application.isPlaying && RenderPipelineHelper.IsUniversal && water.WriteMotionVectors)
|
||||
{
|
||||
var material = water._MotionVectorsMaterial;
|
||||
|
||||
var parameters = new RenderParams(material)
|
||||
{
|
||||
motionVectorMode = MotionVectorGenerationMode.Object,
|
||||
material = material,
|
||||
matProps = _MaterialPropertyBlock,
|
||||
worldBounds = Rend.bounds,
|
||||
layer = water.Layer,
|
||||
receiveShadows = false,
|
||||
shadowCastingMode = ShadowCastingMode.Off,
|
||||
lightProbeUsage = LightProbeUsage.Off,
|
||||
reflectionProbeUsage = ReflectionProbeUsage.Off,
|
||||
};
|
||||
|
||||
if (_Mesh == null)
|
||||
{
|
||||
_Mesh = GetComponent<MeshFilter>().sharedMesh;
|
||||
}
|
||||
|
||||
Graphics.RenderMesh(parameters, _Mesh, 0, transform.localToWorldMatrix, _PreviousObjectToWorld);
|
||||
_PreviousObjectToWorld = transform.localToWorldMatrix;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UpdateMeshBounds()
|
||||
{
|
||||
UnityEngine.Profiling.Profiler.BeginSample("Crest.WaterChunkRenderer.UpdateMeshBounds");
|
||||
|
||||
if (WaterBody.WaterBodies.Count > 0)
|
||||
{
|
||||
_UnexpandedBoundsXZ = ComputeBoundsXZ(transform, ref _BoundsLocal);
|
||||
}
|
||||
|
||||
var newBounds = _BoundsLocal;
|
||||
ExpandBoundsForDisplacements(transform, ref newBounds);
|
||||
|
||||
// FIXME: Fixes a crash which was reported twice. Could not reproduce.
|
||||
// NullReferenceException: Object reference not set to an instance of an object.
|
||||
// at WaveHarmonic.Crest.WaterChunkRenderer.UpdateMeshBounds()[0x00000] in < 00000000000000000000000000000000 >:0
|
||||
// at WaveHarmonic.Crest.WaterChunkRenderer.OnUpdate(WaveHarmonic.Crest.WaterRenderer water)[0x00000] in < 00000000000000000000000000000000 >:0
|
||||
// at WaveHarmonic.Crest.WaterRenderer.Update()[0x00000] in < 00000000000000000000000000000000 >:0
|
||||
if (_Mesh == null)
|
||||
{
|
||||
_Mesh = GetComponent<MeshFilter>().sharedMesh;
|
||||
}
|
||||
|
||||
_Mesh.bounds = newBounds;
|
||||
|
||||
UnityEngine.Profiling.Profiler.EndSample();
|
||||
}
|
||||
|
||||
public static Rect ComputeBoundsXZ(Transform transform, ref Bounds bounds)
|
||||
{
|
||||
// Since chunks are axis-aligned it is safe to rotate the bounds.
|
||||
var center = transform.rotation * bounds.center * transform.lossyScale.x + transform.position;
|
||||
var size = transform.rotation * bounds.size * transform.lossyScale.x;
|
||||
// Rotation can make size negative.
|
||||
return new(0, 0, Mathf.Abs(size.x), Mathf.Abs(size.z))
|
||||
{
|
||||
center = center.XZ(),
|
||||
};
|
||||
}
|
||||
|
||||
static Camera s_CurrentCamera = null;
|
||||
|
||||
static void BeginCameraRendering(ScriptableRenderContext context, Camera camera)
|
||||
{
|
||||
// Camera.current is only supported in the built-in pipeline. This provides the current camera for
|
||||
// OnWillRenderObject for SRPs. BeginCameraRendering is called for each active camera in every frame.
|
||||
// OnWillRenderObject is called after BeginCameraRendering for the current camera so this works.
|
||||
s_CurrentCamera = camera;
|
||||
}
|
||||
|
||||
// Used by the water mask system if we need to render the water mask in situations
|
||||
// where the water itself doesn't need to be rendered or has otherwise been disabled
|
||||
internal void Bind(Camera camera)
|
||||
{
|
||||
_WaterDataHasBeenBound = true;
|
||||
|
||||
if (Rend == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MaterialOverridden && Rend.sharedMaterial != _Water.Material)
|
||||
{
|
||||
Rend.sharedMaterial = _Water.Material;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
Helpers.Destroy(_Mesh);
|
||||
}
|
||||
|
||||
// Called when visible to a camera
|
||||
void OnWillRenderObject()
|
||||
{
|
||||
// Camera.current is only supported in built-in pipeline.
|
||||
if (RenderPipelineHelper.IsLegacy && Camera.current != null)
|
||||
{
|
||||
s_CurrentCamera = Camera.current;
|
||||
}
|
||||
|
||||
// If only the game view is visible, this reference will be dropped for SRP on recompile.
|
||||
if (s_CurrentCamera == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Depth texture is used by water shader for transparency/depth fog, and for fading out foam at shoreline.
|
||||
s_CurrentCamera.depthTextureMode |= DepthTextureMode.Depth;
|
||||
|
||||
Bind(s_CurrentCamera);
|
||||
|
||||
if (_DrawRenderBounds)
|
||||
{
|
||||
Rend.bounds.DebugDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// this is called every frame because the bounds are given in world space and depend on the transform scale, which
|
||||
// can change depending on view altitude
|
||||
public void ExpandBoundsForDisplacements(Transform transform, ref Bounds bounds)
|
||||
{
|
||||
var scale = transform.lossyScale;
|
||||
var rotation = transform.rotation;
|
||||
|
||||
var boundsPadding = _Water.MaximumHorizontalDisplacement;
|
||||
var expandXZ = boundsPadding / scale.x;
|
||||
var boundsY = _Water.MaximumVerticalDisplacement;
|
||||
|
||||
// Extend the kinematic bounds slightly to give room for dynamic waves.
|
||||
if (_Water._DynamicWavesLod.Enabled)
|
||||
{
|
||||
boundsY += 5f;
|
||||
}
|
||||
|
||||
var extents = bounds.extents;
|
||||
|
||||
// Extend bounds by global waves.
|
||||
bounds.extents = new(extents.x + expandXZ, boundsY, extents.z + expandXZ);
|
||||
|
||||
extents = bounds.extents;
|
||||
var center = bounds.center;
|
||||
var size = bounds.size;
|
||||
|
||||
// Get XZ bounds. Doing this manually bypasses updating render bounds call.
|
||||
Rect rect;
|
||||
{
|
||||
var p1 = transform.position;
|
||||
var p2 = rotation * new Vector3(center.x, 0f, center.z);
|
||||
var s1 = scale;
|
||||
var s2 = rotation * new Vector3(size.x, 0f, size.z);
|
||||
|
||||
rect = new(0, 0, Mathf.Abs(s1.x * s2.x), Mathf.Abs(s1.z * s2.z))
|
||||
{
|
||||
center = new(p1.x + p2.x, p1.z + p2.z)
|
||||
};
|
||||
}
|
||||
|
||||
// Extend bounds by local waves.
|
||||
{
|
||||
var totalHorizontal = 0f;
|
||||
var totalVertical = 0f;
|
||||
|
||||
foreach (var reporter in DisplacementReporters)
|
||||
{
|
||||
var horizontal = 0f;
|
||||
var vertical = 0f;
|
||||
if (reporter.ReportDisplacement(ref rect, ref horizontal, ref vertical))
|
||||
{
|
||||
totalHorizontal += horizontal;
|
||||
totalVertical += vertical;
|
||||
}
|
||||
}
|
||||
|
||||
boundsPadding = totalHorizontal;
|
||||
expandXZ = boundsPadding / scale.x;
|
||||
boundsY = totalVertical;
|
||||
|
||||
bounds.extents = new(extents.x + expandXZ, extents.y + boundsY, extents.z + expandXZ);
|
||||
}
|
||||
|
||||
// Expand and offset bounds by height.
|
||||
{
|
||||
var minimumWaterLevelBounds = 0f;
|
||||
var maximumWaterLevelBounds = 0f;
|
||||
|
||||
foreach (var reporter in HeightReporters)
|
||||
{
|
||||
var minimum = 0f;
|
||||
var maximum = 0f;
|
||||
if (reporter.ReportHeight(ref rect, ref minimum, ref maximum))
|
||||
{
|
||||
minimumWaterLevelBounds = Mathf.Max(minimumWaterLevelBounds, Mathf.Abs(Mathf.Min(minimum, _Water.SeaLevel) - _Water.SeaLevel));
|
||||
maximumWaterLevelBounds = Mathf.Max(maximumWaterLevelBounds, Mathf.Abs(Mathf.Max(maximum, _Water.SeaLevel) - _Water.SeaLevel));
|
||||
}
|
||||
}
|
||||
|
||||
minimumWaterLevelBounds *= 0.5f;
|
||||
maximumWaterLevelBounds *= 0.5f;
|
||||
|
||||
boundsY = minimumWaterLevelBounds + maximumWaterLevelBounds;
|
||||
extents = bounds.extents;
|
||||
bounds.extents = new(extents.x, extents.y + boundsY, extents.z);
|
||||
|
||||
var offset = maximumWaterLevelBounds - minimumWaterLevelBounds;
|
||||
center = bounds.center;
|
||||
bounds.center = new(center.x, center.y + offset, center.z);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetInstanceData(int lodIndex)
|
||||
{
|
||||
_LodIndex = lodIndex;
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
static void InitStatics()
|
||||
{
|
||||
// Init here from 2019.3 onwards
|
||||
s_CurrentCamera = null;
|
||||
HeightReporters.Clear();
|
||||
DisplacementReporters.Clear();
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod]
|
||||
static void RunOnStart()
|
||||
{
|
||||
RenderPipelineManager.beginCameraRendering -= BeginCameraRendering;
|
||||
RenderPipelineManager.beginCameraRendering += BeginCameraRendering;
|
||||
}
|
||||
}
|
||||
|
||||
static class BoundsHelper
|
||||
{
|
||||
internal static void DebugDraw(this Bounds b)
|
||||
{
|
||||
var xmin = b.min.x;
|
||||
var ymin = b.min.y;
|
||||
var zmin = b.min.z;
|
||||
var xmax = b.max.x;
|
||||
var ymax = b.max.y;
|
||||
var zmax = b.max.z;
|
||||
|
||||
Debug.DrawLine(new(xmin, ymin, zmin), new(xmin, ymin, zmax));
|
||||
Debug.DrawLine(new(xmin, ymin, zmin), new(xmax, ymin, zmin));
|
||||
Debug.DrawLine(new(xmax, ymin, zmax), new(xmin, ymin, zmax));
|
||||
Debug.DrawLine(new(xmax, ymin, zmax), new(xmax, ymin, zmin));
|
||||
|
||||
Debug.DrawLine(new(xmin, ymax, zmin), new(xmin, ymax, zmax));
|
||||
Debug.DrawLine(new(xmin, ymax, zmin), new(xmax, ymax, zmin));
|
||||
Debug.DrawLine(new(xmax, ymax, zmax), new(xmin, ymax, zmax));
|
||||
Debug.DrawLine(new(xmax, ymax, zmax), new(xmax, ymax, zmin));
|
||||
|
||||
Debug.DrawLine(new(xmax, ymax, zmax), new(xmax, ymin, zmax));
|
||||
Debug.DrawLine(new(xmin, ymin, zmin), new(xmin, ymax, zmin));
|
||||
Debug.DrawLine(new(xmax, ymin, zmin), new(xmax, ymax, zmin));
|
||||
Debug.DrawLine(new(xmin, ymax, zmax), new(xmin, ymin, zmax));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 391e7ec2ec9194dbeb14b0b0af03a29f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,853 @@
|
||||
// Crest Water System
|
||||
// Copyright © 2024 Wave Harmonic. All rights reserved.
|
||||
|
||||
// This script originated from the unity standard assets. It has been modified heavily to be camera-centric (as opposed to
|
||||
// geometry-centric) and assumes a single main camera which simplifies the code.
|
||||
|
||||
using System;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.HighDefinition;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
|
||||
namespace WaveHarmonic.Crest
|
||||
{
|
||||
/// <summary>
|
||||
/// What side of the water surface to render planar reflections for.
|
||||
/// </summary>
|
||||
public enum WaterReflectionSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Both sides. Most expensive.
|
||||
/// </summary>
|
||||
Both,
|
||||
|
||||
/// <summary>
|
||||
/// Above only. Typical for planar reflections.
|
||||
/// </summary>
|
||||
Above,
|
||||
|
||||
/// <summary>
|
||||
/// Below only. For total internal reflections.
|
||||
/// </summary>
|
||||
Below,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders reflections for water. Currently on planar reflections.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed partial class WaterReflections
|
||||
{
|
||||
[SerializeField, HideInInspector]
|
||||
#pragma warning disable 414
|
||||
int _Version = 0;
|
||||
#pragma warning restore 414
|
||||
|
||||
[@Space(10)]
|
||||
|
||||
[@Label("Enable")]
|
||||
[Tooltip("Whether planar reflections are enabled.\n\nAllocates/releases resources if state has changed.")]
|
||||
[@GenerateAPI(Setter.Custom)]
|
||||
[@DecoratedField, SerializeField]
|
||||
internal bool _Enabled;
|
||||
|
||||
|
||||
[@Heading("Capture")]
|
||||
|
||||
[Tooltip("What side of the water surface to render planar reflections for.")]
|
||||
[@GenerateAPI(name: "ReflectionSide")]
|
||||
[@DecoratedField, SerializeField]
|
||||
internal WaterReflectionSide _Mode = WaterReflectionSide.Above;
|
||||
|
||||
[Tooltip("The layers to rendering into reflections.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
LayerMask _Layers = 1; // Default
|
||||
|
||||
[Tooltip("Resolution of the reflection texture.")]
|
||||
[@GenerateAPI]
|
||||
[@Delayed, SerializeField]
|
||||
int _Resolution = 256;
|
||||
|
||||
[Tooltip("Whether to render to the viewer camera only.\n\nWhen disabled, reflections will render for all cameras rendering the water layer, which currently this prevents Refresh Rate from working. Enabling will unlock the Refresh Rate heading.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
internal bool _RenderOnlySingleCamera;
|
||||
|
||||
[@Space(10)]
|
||||
|
||||
[Tooltip("Whether to render the sky or fallback to default reflections.\n\nNot rendering the sky can prevent other custom shaders (like tree leaves) from being in the final output. Enable for best compatibility.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
internal bool _Sky = true;
|
||||
|
||||
[Tooltip("Disables pixel lights (BIRP only).")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
bool _DisablePixelLights = true;
|
||||
|
||||
#pragma warning disable 414
|
||||
[Tooltip("Disables shadows.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
bool _DisableShadows = true;
|
||||
#pragma warning restore 414
|
||||
|
||||
[Tooltip("Whether to allow HDR.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
bool _HDR = true;
|
||||
|
||||
[Tooltip("Whether to allow stencil operations.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
bool _Stencil = false;
|
||||
|
||||
[Tooltip("Whether to allow MSAA.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
bool _AllowMSAA = false;
|
||||
|
||||
[@Space(10)]
|
||||
|
||||
[Tooltip("Overrides global quality settings.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
QualitySettingsOverride _QualitySettingsOverride = new()
|
||||
{
|
||||
_OverrideLodBias = false,
|
||||
_LodBias = 0.5f,
|
||||
_OverrideMaximumLodLevel = false,
|
||||
_MaximumLodLevel = 1,
|
||||
_OverrideTerrainPixelError = false,
|
||||
_TerrainPixelError = 10,
|
||||
};
|
||||
|
||||
[@Heading("Culling")]
|
||||
|
||||
[Tooltip("The near clip plane clips any geometry before it, removing it from reflections.\n\nCan be used to reduce reflection leaks and support varied water level.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
float _ClipPlaneOffset;
|
||||
|
||||
[Tooltip("Anything beyond the far clip plane is not rendered.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
float _FarClipPlane = 1000;
|
||||
|
||||
[Tooltip("Disables occlusion culling.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
bool _DisableOcclusionCulling = true;
|
||||
|
||||
|
||||
[@Heading("Refresh Rate")]
|
||||
|
||||
[Tooltip("Refresh reflection every x frames (one is every frame)")]
|
||||
[@Predicated(nameof(_RenderOnlySingleCamera))]
|
||||
[@DecoratedField, SerializeField]
|
||||
int _RefreshPerFrames = 1;
|
||||
|
||||
[@Predicated(nameof(_RenderOnlySingleCamera))]
|
||||
[@DecoratedField, SerializeField]
|
||||
int _FrameRefreshOffset = 0;
|
||||
|
||||
|
||||
[@Heading("Oblique Matrix")]
|
||||
|
||||
[@Label("Enable")]
|
||||
[Tooltip("An oblique matrix will clip anything below the surface for free.\n\nDisable if you have problems with certain effects. Disabling can cause other artifacts like objects below the surface to appear in reflections.")]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
bool _UseObliqueMatrix = true;
|
||||
|
||||
[Tooltip("Planar relfections using an oblique frustum for better performance.\n\nThis can cause depth issues for TIRs, especially near the surface.")]
|
||||
[@Predicated(nameof(_UseObliqueMatrix))]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
bool _NonObliqueNearSurface;
|
||||
|
||||
[Tooltip("If within this distance from the surface, disable the oblique matrix.")]
|
||||
[@Predicated(nameof(_NonObliqueNearSurface))]
|
||||
[@Predicated(nameof(_UseObliqueMatrix))]
|
||||
[@GenerateAPI]
|
||||
[@DecoratedField, SerializeField]
|
||||
float _NonObliqueNearSurfaceThreshold = 0.05f;
|
||||
|
||||
|
||||
[@Space(10)]
|
||||
|
||||
[@DecoratedField, SerializeField]
|
||||
DebugFields _Debug = new();
|
||||
|
||||
[Serializable]
|
||||
sealed class DebugFields
|
||||
{
|
||||
[@DecoratedField, SerializeField]
|
||||
internal bool _ShowHiddenObjects;
|
||||
|
||||
[Tooltip("Rendering reflections per-camera requires recursive rendering. Check this toggle if experiencing issues. The other downside without it is a one-frame delay.")]
|
||||
[@DecoratedField, SerializeField]
|
||||
internal bool _DisableRecursiveRendering;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// What side of the water surface to render planar reflections for.
|
||||
/// </summary>
|
||||
public WaterReflectionSide Mode { get => _Mode; set => _Mode = value; }
|
||||
|
||||
|
||||
static class ShaderIDs
|
||||
{
|
||||
public static int s_ReflectionTexture = Shader.PropertyToID("_Crest_ReflectionTexture");
|
||||
public static int s_ReflectionPositionNormal = Shader.PropertyToID("_Crest_ReflectionPositionNormal");
|
||||
}
|
||||
|
||||
// Checked in underwater to filter cameras.
|
||||
internal static Camera CurrentCamera { get; private set; }
|
||||
|
||||
internal WaterRenderer _Water;
|
||||
internal UnderwaterRenderer _UnderWater;
|
||||
|
||||
RenderTexture _ReflectionTexture;
|
||||
internal RenderTexture ReflectionTexture => _ReflectionTexture;
|
||||
readonly Vector4[] _ReflectionPositionNormal = new Vector4[2];
|
||||
|
||||
Camera _CameraViewpoint;
|
||||
Skybox _CameraViewpointSkybox;
|
||||
Camera _CameraReflections;
|
||||
Skybox _CameraReflectionsSkybox;
|
||||
|
||||
int RefreshPerFrames => _RenderOnlySingleCamera ? _RefreshPerFrames : 1;
|
||||
long _LastRefreshOnFrame = -1;
|
||||
|
||||
internal bool SupportsRecursiveRendering =>
|
||||
#if !UNITY_6000_0_OR_NEWER
|
||||
// HDRP cannot recursive render for 2022.
|
||||
!RenderPipelineHelper.IsHighDefinition &&
|
||||
#endif
|
||||
!_Debug._DisableRecursiveRendering;
|
||||
|
||||
readonly float[] _CullDistances = new float[32];
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the reflection camera is created.
|
||||
/// </summary>
|
||||
public static Action<Camera> OnCameraAdded { get; set; }
|
||||
|
||||
internal void OnEnable()
|
||||
{
|
||||
_CameraViewpoint = _Water.Viewer;
|
||||
_CameraViewpointSkybox = _CameraViewpoint.GetComponent<Skybox>();
|
||||
|
||||
// This is called also called every frame, but was required here as there was a
|
||||
// black reflection for a frame without this earlier setup call.
|
||||
CreateWaterObjects(_CameraViewpoint);
|
||||
}
|
||||
|
||||
internal void OnDisable()
|
||||
{
|
||||
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionTexture, Texture2D.blackTexture);
|
||||
}
|
||||
|
||||
internal void OnDestroy()
|
||||
{
|
||||
if (_CameraReflections)
|
||||
{
|
||||
Helpers.Destroy(_CameraReflections.gameObject);
|
||||
_CameraReflections = null;
|
||||
}
|
||||
|
||||
if (_ReflectionTexture)
|
||||
{
|
||||
_ReflectionTexture.Release();
|
||||
Helpers.Destroy(_ReflectionTexture);
|
||||
_ReflectionTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnPreRenderCamera(Camera camera)
|
||||
{
|
||||
if (SupportsRecursiveRendering)
|
||||
{
|
||||
if (camera.cameraType is CameraType.Preview or CameraType.Reflection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Helpers.MaskIncludesLayer(camera.cullingMask, _Water.Layer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_RenderOnlySingleCamera && camera != _Water.Viewer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_CameraViewpoint = camera;
|
||||
LateUpdate();
|
||||
}
|
||||
|
||||
if (camera == _CameraViewpoint)
|
||||
{
|
||||
// TODO: Emit an event instead so WBs can listen.
|
||||
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionTexture, _ReflectionTexture);
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnAfterCameraRender(Camera camera)
|
||||
{
|
||||
if (SupportsRecursiveRendering && !_RenderOnlySingleCamera) return;
|
||||
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionTexture, Texture2D.blackTexture);
|
||||
}
|
||||
|
||||
internal void LateUpdate()
|
||||
{
|
||||
// Frame rate limiter.
|
||||
if (_LastRefreshOnFrame > 0 && RefreshPerFrames > 1)
|
||||
{
|
||||
// Check whether we need to refresh the frame.
|
||||
if (Math.Abs(_FrameRefreshOffset) % _RefreshPerFrames != Time.renderedFrameCount % _RefreshPerFrames)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_Water == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SupportsRecursiveRendering)
|
||||
{
|
||||
_CameraViewpoint = _Water.Viewer;
|
||||
}
|
||||
|
||||
if (_CameraViewpoint == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Fix "Screen position out of view frustum" when 2D view activated.
|
||||
{
|
||||
var sceneView = UnityEditor.SceneView.lastActiveSceneView;
|
||||
if (sceneView != null && sceneView.in2DMode && sceneView.camera == _CameraViewpoint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
CreateWaterObjects(_CameraViewpoint);
|
||||
|
||||
if (!_CameraReflections)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateCameraModes();
|
||||
ForceDistanceCulling(_FarClipPlane);
|
||||
|
||||
_CameraReflections.targetTexture = _ReflectionTexture;
|
||||
|
||||
// TODO: Do not do this every frame.
|
||||
if (_Mode != WaterReflectionSide.Both)
|
||||
{
|
||||
Helpers.ClearRenderTexture(_ReflectionTexture, Color.clear, depth: false);
|
||||
}
|
||||
|
||||
// We do not want the water plane when rendering planar reflections.
|
||||
_Water.Root.gameObject.SetActive(false);
|
||||
|
||||
CurrentCamera = _CameraReflections;
|
||||
|
||||
// Optionally disable pixel lights for reflection/refraction
|
||||
var oldPixelLightCount = QualitySettings.pixelLightCount;
|
||||
if (_DisablePixelLights)
|
||||
{
|
||||
QualitySettings.pixelLightCount = 0;
|
||||
}
|
||||
|
||||
// Optionally disable shadows.
|
||||
var oldShadowQuality = QualitySettings.shadows;
|
||||
if (_DisableShadows)
|
||||
{
|
||||
QualitySettings.shadows = UnityEngine.ShadowQuality.Disable;
|
||||
}
|
||||
|
||||
_QualitySettingsOverride.Override();
|
||||
|
||||
// Invert culling because view is mirrored. Does not work for HDRP (handled elsewhere).
|
||||
var oldCulling = GL.invertCulling;
|
||||
GL.invertCulling = !oldCulling;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
try
|
||||
#endif
|
||||
{
|
||||
Render();
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
// Ensure that any global settings are restored.
|
||||
finally
|
||||
#endif
|
||||
{
|
||||
GL.invertCulling = oldCulling;
|
||||
|
||||
// Restore shadows.
|
||||
if (_DisableShadows)
|
||||
{
|
||||
QualitySettings.shadows = oldShadowQuality;
|
||||
}
|
||||
|
||||
// Restore pixel light count
|
||||
if (_DisablePixelLights)
|
||||
{
|
||||
QualitySettings.pixelLightCount = oldPixelLightCount;
|
||||
}
|
||||
|
||||
_QualitySettingsOverride.Restore();
|
||||
|
||||
CurrentCamera = null;
|
||||
_Water.Root.gameObject.SetActive(true);
|
||||
|
||||
// Remember this frame as last refreshed.
|
||||
_LastRefreshOnFrame = Time.renderedFrameCount;
|
||||
}
|
||||
}
|
||||
|
||||
void Render()
|
||||
{
|
||||
var descriptor = _ReflectionTexture.descriptor;
|
||||
descriptor.dimension = TextureDimension.Tex2D;
|
||||
descriptor.volumeDepth = 1;
|
||||
descriptor.useMipMap = false;
|
||||
// No need to clear, as camera clears using the skybox.
|
||||
var target = RenderTexture.GetTemporary(descriptor);
|
||||
_CameraReflections.targetTexture = target;
|
||||
|
||||
if (_Mode != WaterReflectionSide.Below)
|
||||
{
|
||||
_ReflectionPositionNormal[0] = ComputeHorizonPositionAndNormal(_CameraReflections, _Water.SeaLevel, 0.05f, false);
|
||||
|
||||
if (_UnderWater._Enabled)
|
||||
{
|
||||
// Disable underwater layer. It is the only way to exclude probes.
|
||||
_CameraReflections.cullingMask = _Layers & ~(1 << _UnderWater.Layer);
|
||||
}
|
||||
|
||||
RenderCamera(_CameraReflections, Vector3.up, false);
|
||||
Graphics.CopyTexture(target, 0, 0, _ReflectionTexture, 0, 0);
|
||||
|
||||
_CameraReflections.ResetProjectionMatrix();
|
||||
}
|
||||
|
||||
if (_Mode != WaterReflectionSide.Above)
|
||||
{
|
||||
_ReflectionPositionNormal[1] = ComputeHorizonPositionAndNormal(_CameraReflections, _Water.SeaLevel, -0.05f, true);
|
||||
|
||||
if (_UnderWater._Enabled)
|
||||
{
|
||||
// Enable underwater layer.
|
||||
_CameraReflections.cullingMask = _Layers | (1 << _UnderWater.Layer);
|
||||
// We need the depth texture for underwater.
|
||||
_CameraReflections.depthTextureMode = DepthTextureMode.Depth;
|
||||
}
|
||||
|
||||
RenderCamera(_CameraReflections, Vector3.down, _NonObliqueNearSurface);
|
||||
Graphics.CopyTexture(target, 0, 0, _ReflectionTexture, 1, 0);
|
||||
|
||||
_CameraReflections.ResetProjectionMatrix();
|
||||
}
|
||||
|
||||
|
||||
RenderTexture.ReleaseTemporary(target);
|
||||
|
||||
_ReflectionTexture.GenerateMips();
|
||||
|
||||
Shader.SetGlobalVectorArray(ShaderIDs.s_ReflectionPositionNormal, _ReflectionPositionNormal);
|
||||
}
|
||||
|
||||
void RenderCamera(Camera camera, Vector3 planeNormal, bool nonObliqueNearSurface)
|
||||
{
|
||||
// Find out the reflection plane: position and normal in world space
|
||||
var planePosition = _Water.Root.position;
|
||||
|
||||
var offset = _ClipPlaneOffset;
|
||||
{
|
||||
var viewpoint = _CameraViewpoint.transform;
|
||||
if (offset == 0f && viewpoint.position.y == 0f && viewpoint.rotation.eulerAngles.y == 0f)
|
||||
{
|
||||
// Minor offset to prevent "Screen position out of view frustum". Smallest number
|
||||
// to work with both above and below. Smallest number to work with both above and
|
||||
// below. Could be BIRP only.
|
||||
offset = 0.00001f;
|
||||
}
|
||||
}
|
||||
|
||||
// Reflect camera around reflection plane
|
||||
var distance = -Vector3.Dot(planeNormal, planePosition) - offset;
|
||||
var reflectionPlane = new Vector4(planeNormal.x, planeNormal.y, planeNormal.z, distance);
|
||||
|
||||
var reflection = Matrix4x4.zero;
|
||||
CalculateReflectionMatrix(ref reflection, reflectionPlane);
|
||||
|
||||
camera.worldToCameraMatrix = _CameraViewpoint.worldToCameraMatrix * reflection;
|
||||
|
||||
// Setup oblique projection matrix so that near plane is our reflection
|
||||
// plane. This way we clip everything below/above it for free.
|
||||
var clipPlane = CameraSpacePlane(camera, planePosition, planeNormal, 1.0f);
|
||||
|
||||
if (_UseObliqueMatrix && (!nonObliqueNearSurface || Mathf.Abs(_CameraViewpoint.transform.position.y - planePosition.y) > _NonObliqueNearSurfaceThreshold))
|
||||
{
|
||||
camera.projectionMatrix = _CameraViewpoint.CalculateObliqueMatrix(clipPlane);
|
||||
}
|
||||
|
||||
// Set custom culling matrix from the current camera
|
||||
camera.cullingMatrix = _CameraViewpoint.projectionMatrix * _CameraViewpoint.worldToCameraMatrix;
|
||||
|
||||
camera.transform.position = reflection.MultiplyPoint(_CameraViewpoint.transform.position);
|
||||
var euler = _CameraViewpoint.transform.eulerAngles;
|
||||
camera.transform.eulerAngles = new(-euler.x, euler.y, euler.z);
|
||||
camera.cullingMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
|
||||
|
||||
if (SupportsRecursiveRendering)
|
||||
{
|
||||
Helpers.RenderCamera(camera, _Water._Context);
|
||||
}
|
||||
else
|
||||
{
|
||||
camera.Render();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Limit render distance for reflection camera for first 32 layers
|
||||
/// </summary>
|
||||
/// <param name="farClipPlane">reflection far clip distance</param>
|
||||
void ForceDistanceCulling(float farClipPlane)
|
||||
{
|
||||
// Cannot use spherical culling with SRPs. Will error.
|
||||
if (!RenderPipelineHelper.IsLegacy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _CullDistances.Length; i++)
|
||||
{
|
||||
// The culling distance
|
||||
_CullDistances[i] = farClipPlane;
|
||||
}
|
||||
_CameraReflections.layerCullDistances = _CullDistances;
|
||||
_CameraReflections.layerCullSpherical = true;
|
||||
}
|
||||
|
||||
void UpdateCameraModes()
|
||||
{
|
||||
#if d_UnityHDRP
|
||||
if (RenderPipelineHelper.IsHighDefinition)
|
||||
{
|
||||
if (_CameraReflections.TryGetComponent(out HDAdditionalCameraData additionalCameraData))
|
||||
{
|
||||
additionalCameraData.clearColorMode = _Sky ? HDAdditionalCameraData.ClearColorMode.Sky :
|
||||
HDAdditionalCameraData.ClearColorMode.Color;
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
_CameraReflections.clearFlags = _Sky ? CameraClearFlags.Skybox : CameraClearFlags.Color;
|
||||
|
||||
if (_Sky && _CameraViewpoint.TryGetComponent(out _CameraViewpointSkybox))
|
||||
{
|
||||
if (_CameraReflectionsSkybox == null)
|
||||
{
|
||||
_CameraReflectionsSkybox = _CameraReflections.gameObject.AddComponent<Skybox>();
|
||||
}
|
||||
|
||||
_CameraReflectionsSkybox.enabled = _CameraViewpointSkybox.enabled;
|
||||
_CameraReflectionsSkybox.material = _CameraViewpointSkybox.material;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Destroy otherwise skybox will not render if empty.
|
||||
Helpers.Destroy(_CameraViewpointSkybox);
|
||||
}
|
||||
}
|
||||
|
||||
// Update other values to match current camera.
|
||||
// Even if we are supplying custom camera&projection matrices,
|
||||
// some of values are used elsewhere (e.g. skybox uses far plane).
|
||||
|
||||
_CameraReflections.farClipPlane = _CameraViewpoint.farClipPlane;
|
||||
_CameraReflections.nearClipPlane = _CameraViewpoint.nearClipPlane;
|
||||
_CameraReflections.orthographic = _CameraViewpoint.orthographic;
|
||||
_CameraReflections.fieldOfView = _CameraViewpoint.fieldOfView;
|
||||
_CameraReflections.orthographicSize = _CameraViewpoint.orthographicSize;
|
||||
_CameraReflections.allowMSAA = _AllowMSAA;
|
||||
_CameraReflections.aspect = _CameraViewpoint.aspect;
|
||||
_CameraReflections.useOcclusionCulling = !_DisableOcclusionCulling && _CameraViewpoint.useOcclusionCulling;
|
||||
_CameraReflections.depthTextureMode = _CameraViewpoint.depthTextureMode;
|
||||
}
|
||||
|
||||
// On-demand create any objects we need for water
|
||||
void CreateWaterObjects(Camera currentCamera)
|
||||
{
|
||||
var format = _HDR ? RenderTextureFormat.ARGBHalf : RenderTextureFormat.ARGB32;
|
||||
var stencil = _Stencil ? 24 : 16;
|
||||
|
||||
// Reflection render texture
|
||||
if (!_ReflectionTexture || _ReflectionTexture.width != _Resolution || _ReflectionTexture.format != format || _ReflectionTexture.depth != stencil)
|
||||
{
|
||||
if (_ReflectionTexture)
|
||||
{
|
||||
Helpers.Destroy(_ReflectionTexture);
|
||||
}
|
||||
|
||||
Debug.Assert(SystemInfo.SupportsRenderTextureFormat(format), "Crest: The graphics device does not support the render texture format " + format.ToString());
|
||||
_ReflectionTexture = new(_Resolution, _Resolution, stencil, format)
|
||||
{
|
||||
name = "_Crest_WaterReflection",
|
||||
isPowerOfTwo = true,
|
||||
dimension = TextureDimension.Tex2DArray,
|
||||
volumeDepth = 2,
|
||||
useMipMap = true,
|
||||
autoGenerateMips = false,
|
||||
filterMode = FilterMode.Trilinear,
|
||||
};
|
||||
_ReflectionTexture.Create();
|
||||
}
|
||||
|
||||
// Camera for reflection
|
||||
if (!_CameraReflections)
|
||||
{
|
||||
var go = new GameObject("_Crest_WaterReflectionCamera");
|
||||
go.transform.SetParent(_Water.Container.transform, worldPositionStays: true);
|
||||
_CameraReflections = go.AddComponent<Camera>();
|
||||
_CameraReflections.enabled = false;
|
||||
_CameraReflections.cullingMask = _Layers;
|
||||
_CameraReflections.cameraType = CameraType.Reflection;
|
||||
_CameraReflections.backgroundColor = Color.clear;
|
||||
|
||||
if (RenderPipelineHelper.IsLegacy)
|
||||
{
|
||||
_CameraReflections.gameObject.AddComponent<FlareLayer>();
|
||||
}
|
||||
|
||||
#if d_UnityHDRP
|
||||
if (RenderPipelineHelper.IsHighDefinition)
|
||||
{
|
||||
var additionalCameraData = _CameraReflections.gameObject.AddComponent<HDAdditionalCameraData>();
|
||||
additionalCameraData.invertFaceCulling = true;
|
||||
additionalCameraData.defaultFrameSettings = FrameSettingsRenderType.RealtimeReflection;
|
||||
additionalCameraData.backgroundColorHDR = Color.clear;
|
||||
additionalCameraData.customRenderingSettings = true;
|
||||
additionalCameraData.renderingPathCustomFrameSettingsOverrideMask.mask[(uint)FrameSettingsField.CustomPass] = true;
|
||||
additionalCameraData.renderingPathCustomFrameSettings.SetEnabled(FrameSettingsField.CustomPass, true);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if d_UnityURP
|
||||
if (RenderPipelineHelper.IsUniversal)
|
||||
{
|
||||
var additionalCameraData = _CameraReflections.gameObject.AddComponent<UniversalAdditionalCameraData>();
|
||||
additionalCameraData.renderShadows = !_DisableShadows;
|
||||
additionalCameraData.requiresColorTexture = false;
|
||||
additionalCameraData.requiresDepthTexture = false;
|
||||
}
|
||||
#endif
|
||||
OnCameraAdded?.Invoke(_CameraReflections);
|
||||
}
|
||||
|
||||
_CameraReflections.gameObject.hideFlags = _Debug._ShowHiddenObjects ? HideFlags.DontSave : HideFlags.HideAndDontSave;
|
||||
}
|
||||
|
||||
// Given position/normal of the plane, calculates plane in camera space.
|
||||
Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign)
|
||||
{
|
||||
var offset = _ClipPlaneOffset;
|
||||
{
|
||||
var viewpoint = _CameraViewpoint.transform;
|
||||
if (offset == 0f && viewpoint.position.y == 0f && viewpoint.rotation.eulerAngles.y == 0f)
|
||||
{
|
||||
// Minor offset to prevent "Screen position out of view frustum". Smallest number
|
||||
// to work with both above and below. Smallest number to work with both above and
|
||||
// below. Could be BIRP only.
|
||||
offset = 0.00001f;
|
||||
}
|
||||
}
|
||||
|
||||
var offsetPos = pos + normal * offset;
|
||||
var m = cam.worldToCameraMatrix;
|
||||
var cpos = m.MultiplyPoint(offsetPos);
|
||||
var cnormal = m.MultiplyVector(normal).normalized * sideSign;
|
||||
return new(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
|
||||
}
|
||||
|
||||
// Calculates reflection matrix around the given plane
|
||||
static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
|
||||
{
|
||||
reflectionMat.m00 = 1F - 2F * plane[0] * plane[0];
|
||||
reflectionMat.m01 = -2F * plane[0] * plane[1];
|
||||
reflectionMat.m02 = -2F * plane[0] * plane[2];
|
||||
reflectionMat.m03 = -2F * plane[3] * plane[0];
|
||||
|
||||
reflectionMat.m10 = -2F * plane[1] * plane[0];
|
||||
reflectionMat.m11 = 1F - 2F * plane[1] * plane[1];
|
||||
reflectionMat.m12 = -2F * plane[1] * plane[2];
|
||||
reflectionMat.m13 = -2F * plane[3] * plane[1];
|
||||
|
||||
reflectionMat.m20 = -2F * plane[2] * plane[0];
|
||||
reflectionMat.m21 = -2F * plane[2] * plane[1];
|
||||
reflectionMat.m22 = 1F - 2F * plane[2] * plane[2];
|
||||
reflectionMat.m23 = -2F * plane[3] * plane[2];
|
||||
|
||||
reflectionMat.m30 = 0F;
|
||||
reflectionMat.m31 = 0F;
|
||||
reflectionMat.m32 = 0F;
|
||||
reflectionMat.m33 = 1F;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute intersection between the frustum far plane and given plane, and return view space
|
||||
/// position and normal for this horizon line.
|
||||
/// </summary>
|
||||
static Vector4 ComputeHorizonPositionAndNormal(Camera camera, float positionY, float offset, bool flipped)
|
||||
{
|
||||
var position = Vector2.zero;
|
||||
var normal = Vector2.zero;
|
||||
|
||||
// Set up back points of frustum.
|
||||
var positionNDC = new NativeArray<Vector3>(4, Allocator.Temp);
|
||||
var positionWS = new NativeArray<Vector3>(4, Allocator.Temp);
|
||||
try
|
||||
{
|
||||
|
||||
var farPlane = camera.farClipPlane;
|
||||
positionNDC[0] = new(0f, 0f, farPlane);
|
||||
positionNDC[1] = new(0f, 1f, farPlane);
|
||||
positionNDC[2] = new(1f, 1f, farPlane);
|
||||
positionNDC[3] = new(1f, 0f, farPlane);
|
||||
|
||||
// Project out to world.
|
||||
for (var i = 0; i < positionWS.Length; i++)
|
||||
{
|
||||
// Eye parameter works for BIRP. With it we could skip setting matrices.
|
||||
// In HDRP it doesn't work for XR MP. And completely breaks horizon in XR SPI.
|
||||
positionWS[i] = camera.ViewportToWorldPoint(positionNDC[i]);
|
||||
}
|
||||
|
||||
var intersectionsScreen = new NativeArray<Vector2>(2, Allocator.Temp);
|
||||
// This is only used to disambiguate the normal later. Could be removed if we were
|
||||
// more careful with point order/indices below.
|
||||
var intersectionsWorld = new NativeArray<Vector3>(2, Allocator.Temp);
|
||||
try
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
// Iterate over each back point
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
// Get next back point, to obtain line segment between them.
|
||||
var next = (i + 1) % 4;
|
||||
|
||||
// See if one point is above and one point is below sea level - then sign of the two differences
|
||||
// will be different, and multiplying them will give a negative.
|
||||
if ((positionWS[i].y - positionY) * (positionWS[next].y - positionY) < 0f)
|
||||
{
|
||||
// Proportion along line segment where intersection occurs.
|
||||
var proportion = Mathf.Abs((positionY - positionWS[i].y) / (positionWS[next].y - positionWS[i].y));
|
||||
intersectionsScreen[count] = Vector2.Lerp(positionNDC[i], positionNDC[next], proportion);
|
||||
intersectionsWorld[count] = Vector3.Lerp(positionWS[i], positionWS[next], proportion);
|
||||
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Two distinct results - far plane intersects water.
|
||||
if (count == 2)
|
||||
{
|
||||
position = intersectionsScreen[0];
|
||||
var tangent = intersectionsScreen[0] - intersectionsScreen[1];
|
||||
normal.x = -tangent.y;
|
||||
normal.y = tangent.x;
|
||||
|
||||
// Disambiguate the normal. The tangent normal might go from left to right or right
|
||||
// to left since we do not handle ordering of intersection points.
|
||||
if (Vector3.Dot(intersectionsWorld[0] - intersectionsWorld[1], camera.transform.right) > 0f)
|
||||
{
|
||||
normal = -normal;
|
||||
}
|
||||
|
||||
// Invert the normal if camera is upside down.
|
||||
if (camera.transform.up.y <= 0f)
|
||||
{
|
||||
normal = -normal;
|
||||
}
|
||||
|
||||
// The above will sometimes produce a normal that is inverted around 90° along the
|
||||
// Z axis. Here we are using world up to make sure that water is world down.
|
||||
{
|
||||
var cameraFacing = Vector3.Dot(camera.transform.right, Vector3.up);
|
||||
var normalFacing = Vector2.Dot(normal, Vector2.right);
|
||||
|
||||
if (cameraFacing > 0.75f && normalFacing > 0.9f)
|
||||
{
|
||||
normal = -normal;
|
||||
}
|
||||
else if (cameraFacing < -0.75f && normalFacing < -0.9f)
|
||||
{
|
||||
normal = -normal;
|
||||
}
|
||||
}
|
||||
|
||||
// Minor offset helps.
|
||||
position += normal.normalized * offset;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
intersectionsScreen.Dispose();
|
||||
intersectionsWorld.Dispose();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
positionNDC.Dispose();
|
||||
positionWS.Dispose();
|
||||
}
|
||||
|
||||
if (flipped)
|
||||
{
|
||||
normal = -normal;
|
||||
}
|
||||
|
||||
return new(position.x, position.y, normal.x, normal.y);
|
||||
}
|
||||
|
||||
void SetEnabled(bool previous, bool current)
|
||||
{
|
||||
if (previous == current) return;
|
||||
if (_Water == null || !_Water.isActiveAndEnabled) return;
|
||||
if (_Enabled) OnEnable(); else OnDisable();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[@OnChange]
|
||||
void OnChange(string propertyPath, object previousValue)
|
||||
{
|
||||
switch (propertyPath)
|
||||
{
|
||||
case nameof(_Enabled):
|
||||
SetEnabled((bool)previousValue, _Enabled);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8b8696e988b24f1e832400fdd148451
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 205
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user