Files
Fishing2/Packages/com.waveharmonic.crest/Runtime/Scripts/Surface/WaterBuilder.cs
2026-01-08 22:30:55 +08:00

540 lines
24 KiB
C#

// 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
{
partial class SurfaceRenderer
{
// Keep references to meshes so they can be cleaned up later.
readonly Mesh[] _Meshes = new Mesh[(int)Builder.PatchType.Count];
/// <summary>
/// Instantiates all the water geometry, as a set of tiles.
/// </summary>
static class Builder
{
// 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.
internal 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,
}
// Instance Indices:
// 00 01 02 03
// 04 05
// 06 07
// 08 09 10 11
static readonly Vector2[] s_Offsets =
{
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),
};
// First LOD has inside bit as well:
// 00 01 02 03
// 04 05 06 07
// 08 09 10 11
// 12 13 14 15
internal static readonly Vector2[] s_OffsetsFirstLod =
{
// Interior first for sorted rendering.
new(-0.5f, +0.5f), new(+0.5f, +0.5f), new(-0.5f, -0.5f), new(+0.5f, -0.5f),
// Exterior.
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 vertices that point inwards. The outermost
// ring has both the inward vertices and also an additional outwards set of
// vertices that go to the horizon.
static readonly PatchType[] s_PatchTypes =
{
PatchType.SlimXFatZ, PatchType.SlimX, PatchType.SlimX, PatchType.SlimXZ,
PatchType.FatX, PatchType.SlimX,
PatchType.FatX, PatchType.SlimX,
PatchType.FatXZ, PatchType.FatX, PatchType.FatX, PatchType.FatXSlimZ,
};
// All interior - the "side" types have an extra skirt that points inwards - this
// means that this inner most section does not need any skirting. This is good, as
// this is the highest density part of the mesh.
static readonly PatchType[] s_PatchTypesFirstLod =
{
PatchType.Interior, PatchType.Interior, PatchType.Interior, PatchType.Interior,
PatchType.SlimXFatZ, PatchType.SlimX, PatchType.SlimX, PatchType.SlimXZ,
PatchType.FatX, PatchType.SlimX,
PatchType.FatX, PatchType.SlimX,
PatchType.FatXZ, PatchType.FatX, PatchType.FatX, PatchType.FatXSlimZ,
};
static readonly PatchType[] s_PatchTypesLastLod =
{
PatchType.FatXZOuter, PatchType.FatXOuter, PatchType.FatXOuter, PatchType.FatXZOuter,
PatchType.FatXOuter, PatchType.FatXOuter,
PatchType.FatXOuter, PatchType.FatXOuter,
PatchType.FatXZOuter, PatchType.FatXOuter, PatchType.FatXOuter, PatchType.FatXZOuter,
};
static int s_SiblingIndex;
public static Transform GenerateMesh(WaterRenderer water, SurfaceRenderer surface, 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
s_SiblingIndex = 0;
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.Container.transform;
root.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
root.transform.localScale = Vector3.one;
// create mesh data
// 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++)
{
surface._Meshes[i] = BuildPatch(water, (PatchType)i, tileResolution);
}
for (var i = 0; i < lodCount; i++)
{
CreateLOD(water, surface, tiles, root.transform, i, lodCount, surface._Meshes, lodDataResolution, geoDownSampleFactor, surface.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)
{
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();
var 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();
}
return mesh;
}
static void CreateLOD(WaterRenderer water, SurfaceRenderer surface, List<WaterChunkRenderer> tiles, Transform parent, int lodIndex, int lodCount, Mesh[] meshData, 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 && !surface._Debug._DisableSkirt;
#endif
Vector2[] offsets;
PatchType[] patchTypes;
if (lodIndex != 0)
{
offsets = s_Offsets;
patchTypes = generateSkirt ? s_PatchTypesLastLod : s_PatchTypes;
}
else
{
offsets = s_OffsetsFirstLod;
patchTypes = s_PatchTypesFirstLod;
}
#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 (surface._Debug._UniformTiles)
{
patchTypes = new PatchType[patchTypes.Length];
System.Array.Fill(patchTypes, PatchType.Fat);
}
#endif
// create the water patches
for (var i = 0; i < offsets.Length; i++)
{
// instantiate and place patch
var patch = surface._ChunkTemplate
? Helpers.InstantiatePrefab(surface._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 order = -lodCount + (patchTypes[i] == PatchType.Interior ? -1 : lodIndex);
{
var mesh = meshData[(int)patchTypes[i]];
patch.AddComponent<MeshFilter>().sharedMesh = mesh;
var chunk = patch.AddComponent<WaterChunkRenderer>();
chunk._Water = water;
chunk._SortingOrder = order;
chunk._SiblingIndex = s_SiblingIndex++;
chunk.Initialize(lodIndex, mr, mesh);
// When custom rendering, we loop over chunks to render, which means these need to
// be optimally sorted. We statically sort by LOD. Sub-sort is only done for LOD0,
// where interior tiles are placed first. Further sorting must be done dynamically.
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 = order;
}
else if (!water.Surface.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 = order;
}
mr.shadowCastingMode = water.Surface.CastShadows ? ShadowCastingMode.On : ShadowCastingMode.Off;
// This setting is ignored by Unity for the transparent water shader.
mr.receiveShadows = false;
mr.motionVectorGenerationMode = !water.WriteMotionVectors
? MotionVectorGenerationMode.ForceNoMotion
: MotionVectorGenerationMode.Object;
mr.material = water.Surface.Material;
OnCreateChunkRenderer?.Invoke(mr);
// 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);
}
}
}
}
}
}