254 lines
11 KiB
C#
254 lines
11 KiB
C#
// Crest Water System
|
|
// Copyright © 2024 Wave Harmonic. All rights reserved.
|
|
|
|
using UnityEngine;
|
|
using UnityEngine.Experimental.Rendering;
|
|
using UnityEngine.Rendering;
|
|
using WaveHarmonic.Crest.Internal;
|
|
|
|
namespace WaveHarmonic.Crest
|
|
{
|
|
partial class UnderwaterRenderer
|
|
{
|
|
// Adapted from:
|
|
// Packages/com.unity.render-pipelines.universal/Runtime/UniversalRenderer.cs
|
|
#if UNITY_SWITCH || UNITY_ANDROID || UNITY_EMBEDDED_LINUX || UNITY_QNX
|
|
internal const GraphicsFormat k_DepthStencilFormat = GraphicsFormat.D24_UNorm_S8_UInt;
|
|
internal const int k_DepthBufferBits = 24;
|
|
internal const DepthBits k_DepthBits = DepthBits.Depth24;
|
|
#else
|
|
internal const GraphicsFormat k_DepthStencilFormat = GraphicsFormat.D32_SFloat_S8_UInt;
|
|
internal const int k_DepthBufferBits = 32;
|
|
internal const DepthBits k_DepthBits = DepthBits.Depth32;
|
|
#endif
|
|
|
|
internal const int k_ShaderPassWaterSurfaceMask = 0;
|
|
internal const int k_ShaderPassWaterSurfaceDepth = 1;
|
|
internal const int k_ShaderPassWaterHorizonMask = 2;
|
|
|
|
// NOTE: Must match CREST_MASK_BELOW_SURFACE in Constants.hlsl.
|
|
const float k_MaskBelowSurface = -1f;
|
|
// NOTE: Must match CREST_MASK_BELOW_SURFACE_CULLED in Constants.hlsl.
|
|
const float k_MaskBelowSurfaceCull = -2f;
|
|
|
|
internal const string k_ComputeShaderKernelFillMaskArtefacts = "FillMaskArtefacts";
|
|
|
|
static partial class ShaderIDs
|
|
{
|
|
// Local
|
|
public static readonly int s_FarPlaneOffset = Shader.PropertyToID("_Crest_FarPlaneOffset");
|
|
public static readonly int s_MaskBelowSurface = Shader.PropertyToID("_Crest_MaskBelowSurface");
|
|
|
|
// Global
|
|
public static readonly int s_WaterMaskTexture = Shader.PropertyToID("_Crest_WaterMaskTexture");
|
|
public static readonly int s_WaterMaskDepthTexture = Shader.PropertyToID("_Crest_WaterMaskDepthTexture");
|
|
|
|
public static readonly int s_StencilRef = Shader.PropertyToID("_StencilRef");
|
|
}
|
|
|
|
internal Material _MaskMaterial;
|
|
|
|
internal RenderTargetIdentifier _MaskTarget;
|
|
internal RenderTargetIdentifier _DepthTarget;
|
|
|
|
internal readonly Plane[] _CameraFrustumPlanes = new Plane[6];
|
|
CommandBuffer _MaskCommandBuffer;
|
|
|
|
internal RenderTexture _MaskRT;
|
|
RenderTexture _DepthRT;
|
|
|
|
ComputeShader _ArtifactsShader;
|
|
bool _ArtifactsShaderInitialized;
|
|
int _ArtifactsKernel;
|
|
uint _ArtifactsThreadGroupSizeX;
|
|
uint _ArtifactsThreadGroupSizeY;
|
|
|
|
void SetupMask()
|
|
{
|
|
_MaskCommandBuffer ??= new()
|
|
{
|
|
name = "Crest: Underwater Mask",
|
|
};
|
|
}
|
|
|
|
internal void OnEnableMask()
|
|
{
|
|
// Create a reference to handle the RT. The RT properties will be replaced with a descriptor before the
|
|
// native object is created, and since it is lazy it is near zero cost.
|
|
Helpers.CreateRenderTargetTextureReference(ref _MaskRT, ref _MaskTarget);
|
|
_MaskRT.name = "_Crest_WaterMaskTexture";
|
|
Helpers.CreateRenderTargetTextureReference(ref _DepthRT, ref _DepthTarget);
|
|
_DepthRT.name = "_Crest_WaterMaskDepthTexture";
|
|
|
|
SetUpArtifactsShader();
|
|
}
|
|
|
|
internal void OnDisableMask()
|
|
{
|
|
if (_MaskRT != null) _MaskRT.Release();
|
|
if (_DepthRT != null) _DepthRT.Release();
|
|
}
|
|
|
|
internal void SetUpArtifactsShader()
|
|
{
|
|
if (_ArtifactsShaderInitialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_ArtifactsKernel = _ArtifactsShader.FindKernel(k_ComputeShaderKernelFillMaskArtefacts);
|
|
_ArtifactsShader.GetKernelThreadGroupSizes
|
|
(
|
|
_ArtifactsKernel,
|
|
out _ArtifactsThreadGroupSizeX,
|
|
out _ArtifactsThreadGroupSizeY,
|
|
out _
|
|
);
|
|
|
|
_ArtifactsShaderInitialized = true;
|
|
}
|
|
|
|
internal void SetUpMaskTextures(RenderTextureDescriptor descriptor)
|
|
{
|
|
if (!Helpers.RenderTargetTextureNeedsUpdating(_MaskRT, descriptor))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// This will disable MSAA for our textures as MSAA will break sampling later on. This looks safe to do as
|
|
// Unity's CopyDepthPass does the same, but a possible better way or supporting MSAA is worth looking into.
|
|
descriptor.msaaSamples = 1;
|
|
|
|
// @Memory: We could investigate making this an 8-bit texture instead to reduce GPU memory usage.
|
|
// @Memory: We could potentially try a half resolution mask as the mensicus could mask resolution issues.
|
|
// Intel iGPU for Metal and DirectX both had issues with R16. 2021.11.18
|
|
descriptor.colorFormat = Helpers.IsIntelGPU() ? RenderTextureFormat.RFloat : RenderTextureFormat.RHalf;
|
|
descriptor.depthBufferBits = 0;
|
|
descriptor.enableRandomWrite = true;
|
|
|
|
_MaskRT.Release();
|
|
_MaskRT.descriptor = descriptor;
|
|
|
|
descriptor.colorFormat = RenderTextureFormat.Depth;
|
|
descriptor.depthBufferBits = (int)k_DepthBits;
|
|
descriptor.enableRandomWrite = false;
|
|
|
|
_DepthRT.Release();
|
|
_DepthRT.descriptor = descriptor;
|
|
}
|
|
|
|
void OnPreRenderMask(Camera camera)
|
|
{
|
|
_MaskCommandBuffer.Clear();
|
|
|
|
var descriptor = XRHelpers.GetRenderTextureDescriptor(camera);
|
|
|
|
descriptor.useDynamicScale = camera.allowDynamicResolution;
|
|
|
|
// Keywords and other things.
|
|
SetUpMaskTextures(descriptor);
|
|
|
|
#if d_CrestPortals
|
|
// Populate water volume before mask so we can use the stencil.
|
|
if (_Portals.Active)
|
|
{
|
|
_Portals.ReAllocate(descriptor);
|
|
_Portals.RenderMask(camera, _MaskCommandBuffer, _MaskMaterial);
|
|
_Portals.RenderStencil(_MaskCommandBuffer, _DepthRT, _DepthTarget);
|
|
}
|
|
#endif
|
|
|
|
_MaskCommandBuffer.SetRenderTarget(_MaskTarget, _DepthTarget);
|
|
SetUpMask(_MaskCommandBuffer, _MaskTarget, _DepthTarget);
|
|
PopulateMask(_MaskCommandBuffer, camera);
|
|
|
|
FixMaskArtefacts(_MaskCommandBuffer, descriptor, _MaskTarget);
|
|
}
|
|
|
|
internal void SetUpMask(CommandBuffer buffer, RenderTargetIdentifier maskTarget, RenderTargetIdentifier depthTarget)
|
|
{
|
|
// When using the stencil we are already clearing depth and do not want to clear the stencil too. Clear
|
|
// color only when using the stencil as the horizon effectively clears it when not using it.
|
|
buffer.ClearRenderTarget(!UseStencilBuffer, UseStencilBuffer, Color.black);
|
|
buffer.SetGlobalTexture(ShaderIDs.s_WaterMaskTexture, maskTarget);
|
|
buffer.SetGlobalTexture(ShaderIDs.s_WaterMaskDepthTexture, depthTarget);
|
|
}
|
|
|
|
internal void FixMaskArtefacts(CommandBuffer buffer, RenderTextureDescriptor descriptor, RenderTargetIdentifier target)
|
|
{
|
|
if (_Debug._DisableArtifactCorrection)
|
|
{
|
|
return;
|
|
}
|
|
|
|
buffer.SetComputeTextureParam(_ArtifactsShader, _ArtifactsKernel, ShaderIDs.s_WaterMaskTexture, target);
|
|
// XR SPI will have a volume depth of two. If using RTHandles, then set manually as will be two for all cameras.
|
|
_ArtifactsShader.SetKeyword("STEREO_INSTANCING_ON", descriptor.dimension == TextureDimension.Tex2DArray);
|
|
|
|
buffer.DispatchCompute
|
|
(
|
|
_ArtifactsShader,
|
|
_ArtifactsKernel,
|
|
// Viewport sizes are not perfect so round up to cover.
|
|
Mathf.CeilToInt((float)descriptor.width / _ArtifactsThreadGroupSizeX),
|
|
Mathf.CeilToInt((float)descriptor.height / _ArtifactsThreadGroupSizeY),
|
|
descriptor.volumeDepth
|
|
);
|
|
}
|
|
|
|
// Populates a screen space mask which will inform the underwater postprocess. As a future optimisation we may
|
|
// be able to avoid this pass completely if we can reuse the camera depth after transparents are rendered.
|
|
internal void PopulateMask(CommandBuffer commandBuffer, Camera camera)
|
|
{
|
|
// Render horizon into mask using a fullscreen triangle at the far plane. Horizon must be rendered first or
|
|
// it will overwrite the mask with incorrect values.
|
|
{
|
|
var zBufferParameters = Helpers.GetZBufferParameters(camera);
|
|
// Take 0-1 linear depth and convert non-linear depth.
|
|
_MaskMaterial.SetFloat(ShaderIDs.s_FarPlaneOffset, Helpers.LinearDepthToNonLinear(_FarPlaneMultiplier, zBufferParameters));
|
|
|
|
// Render fullscreen triangle with horizon mask pass.
|
|
commandBuffer.DrawProcedural(Matrix4x4.identity, _MaskMaterial, shaderPass: k_ShaderPassWaterHorizonMask, MeshTopology.Triangles, 3, 1);
|
|
}
|
|
|
|
GeometryUtility.CalculateFrustumPlanes(camera, _CameraFrustumPlanes);
|
|
|
|
// Get all water chunks and render them using cmd buffer, but with mask shader.
|
|
if (!_Debug._DisableMask)
|
|
{
|
|
// Spends approx 0.2-0.3ms here on 2018 Dell XPS 15.
|
|
foreach (var chunk in _Water.Chunks)
|
|
{
|
|
var renderer = chunk.Rend;
|
|
// Can happen in edit mode.
|
|
if (renderer == null) continue;
|
|
var bounds = renderer.bounds;
|
|
if (GeometryUtility.TestPlanesAABB(_CameraFrustumPlanes, bounds))
|
|
{
|
|
if ((!chunk._WaterDataHasBeenBound) && chunk.enabled)
|
|
{
|
|
chunk.Bind(camera);
|
|
}
|
|
|
|
// Handle culled tiles for when underwater is rendered before the transparent pass.
|
|
chunk._MaterialPropertyBlock.SetFloat(ShaderIDs.s_MaskBelowSurface, !_EnableShaderAPI || renderer.enabled ? k_MaskBelowSurface : k_MaskBelowSurfaceCull);
|
|
renderer.SetPropertyBlock(chunk._MaterialPropertyBlock);
|
|
|
|
commandBuffer.DrawRenderer(renderer, _MaskMaterial, submeshIndex: 0, shaderPass: k_ShaderPassWaterSurfaceMask);
|
|
|
|
chunk._Visible = true;
|
|
}
|
|
chunk._WaterDataHasBeenBound = false;
|
|
}
|
|
|
|
#if d_CrestPortals
|
|
if (_Portals.Active)
|
|
{
|
|
_Portals.PopulateMask(commandBuffer, _MaskMaterial);
|
|
}
|
|
#endif // d_CrestPortals
|
|
}
|
|
}
|
|
}
|
|
}
|