RenderPipelineManager.beginCameraRendering += RenderSurface;
And fetch a command buffer using CommandBufferPool.Get():
void RenderSurface(ScriptableRenderContext context, Camera camera)
{
CommandBuffer commandBuffer = CommandBufferPool.Get("Water CMD");
UpdateSurface(waterSurface, commandBuffer, context, camera);
context.ExecuteCommandBuffer(commandBuffer);
context.Submit();
commandBuffer.Clear();
CommandBufferPool.Release(commandBuffer);
}
The 'UpdateSurface' updates the simulation state for the ocean surface, caustics and cutouts as well as enqueues the necessary render passes.
One method utalizes the Fast Fourier Transform, explained and implemented here: I Tried Simulating The Entire Ocean.
This technique has been popular in games and movies since it was proposed in the 2004 paper Simulating Ocean Water
by Jerry Tessendorf, and I will attempt to explain it here.
What must be understood before understanding the Fourier Transform is that any periodic waveform can be identically reproduced with a sum of sinusoids.
The purpose of Fourier Transform, is to take a waveform and decompose it into a sum of sinusoids resulting in a close approximation of that waveform.
The output of the Fourier Transform is a wave graph in the "frequency domain" which, as opposed to describing how amplitude at a given time (time domain), describes the amplitude of a wave at a given frequency.
For example, for a wave of frequency 30hz we see a spike in the frequency domain at 30hz.
The initial frequency domain signal will control the 'look' of the ocean; how choppy are the waves, where are the predominant winds, etc.
This signal can be generated in any way you like, which would likely result in some pretty wild looking oceans.
However, luckily there are already methods for generating realistic ocean frequency spectra using models built from real-world wave data, such as the Phillips and JONSWAP spectra.
These wave spectra can represent tens of thousands of frequencies. At a resolution of 256x256, the IFT is performed over 65,000 times (once on each every pixel).
To make this real-time compatable, the Fast Fourier Transform is used.
The FFT uses a 'Butterfly Algorithm' to recursively split the input data in half which avoids unnecessary calculations.
Please see here for a more in-depth exploration of the FFT.
Finally, the time domain is progressed which produces usable displacement and (derrived) slope maps which will be sent to the ocean shader.
However, even with very detailed wave spectra, the tiling issue is not yet completely eradicated. It is inevitable that any periodic waveform, or sum or periodic waveforms, will repeat given enough distance. However, the FFT is so fast we can actually compute several wave spectra simultaneously and scale them over the ocean surface. The millions of waves all interact and distort one another virtually eradicating tiling up to an extreme distance.
As one last trick I learned from HDRP is to define constants in a compute shader's kernals. I used to this give the user control over the resolution of the wave spectra and subsequent FFT passes as a graphics option.
#pragma kernel RowPass_128 FFTPass=RowPass_128 FFT_RESOLUTION=128 BUTTERFLY_COUNT=7
#pragma kernel ColPass_128 FFTPass=ColPass_128 COLUMN_PASS FFT_RESOLUTION=128 BUTTERFLY_COUNT=7
#pragma kernel RowPass_256 FFTPass=RowPass_256 FFT_RESOLUTION=256 BUTTERFLY_COUNT=8
#pragma kernel ColPass_256 FFTPass=ColPass_256 COLUMN_PASS FFT_RESOLUTION=256 BUTTERFLY_COUNT=8
#pragma kernel RowPass_512 FFTPass=RowPass_512 FFT_RESOLUTION=512 BUTTERFLY_COUNT=9
#pragma kernel ColPass_512 FFTPass=ColPass_512 COLUMN_PASS FFT_RESOLUTION=512 BUTTERFLY_COUNT=9
[numthreads(FFT_RESOLUTION, 1, 1)]
void FFTPass(uint3 id : SV_DISPATCHTHREADID)
{
for (int i = 0; i < 8; i++)
{
#ifdef COLUMN_PASS
_FFTTarget[uint3(id.xy, i)] = FFT(id.x, _FFTTarget[uint3(id.xy, i)]);
#else
_FFTTarget[uint3(id.yx, i)] = FFT(id.x, _FFTTarget[uint3(id.yx, i)]);
#endif
}
}
Like using Levels of Detail to render decimated versions of objects as they get further from the camera, the same can be done to oceans only a little more complicated.
Upon creating a HDRP ocean, three meshes are generated; a dense central grid, a rectangular grid mesh and a colossal outer mesh which is essentially a flat plane with a hole in it.
These meshes are dynamically scaled and rotated to fill the surface.
Put together, the meshes look like this:
A neat optimisation here is to use indirect GPU instancing to draw the ring meshes since they are drawn dozens of times.
This eliminates duplicate vertex batches by asking the GPU to cache the ring mesh and simply reuse it when requested.
The instance ID is fetched in the vertex shader (before tessellation) and rotation and scaling data is applied from a Structured Buffer prepared beforehand.
These meshes are frustum culled when generating the buffer, and again in the tessellation stage (per mesh and then per triangle).
Secondly, I implemented GPU Vertex Tessellation to generate more vertices on the GPU.
In a tessellation shader, the distance from the vertex to the camera is used to generate a tessellation constant.
This is used to recursively subdivide the ocean mesh, creating more displaceable geometry without sending it to the GPU with the rest of the scene.
bool ShouldRender(float2 uv, float sceneDepth)
{
float surfaceMask = SAMPLE_TEXTURE2D(_SurfaceFaceMask, sampler_PointClamp, uv).r;
if (surfaceMask > 0.9)
return false;
float3 skyboxPos = ComputeWorldSpacePosition(uv, 0.00001, UNITY_MATRIX_I_VP); // Tiny epsilon to remove line artefact created just under water plane.
return surfaceMask > 0 || (_SurfacePosition.y > skyboxPos.y);
}
While this solution is far more performant than the orthographic camera method and can handle submarines, it does have a few issues that I'd like to address later.
Most prominent is that violent waves can actually loop back around on themselved which creates backfaces above the water surface. These backfaces are improperly rendered as underwater.
The last approach I have considered is using some sort of vertical smearing to propagate the lowest water surface pixels on the screen downwards.
This would be performed in a compute shader after the depth pre-pass has been rasterized and, combined with the position mask, would create a perfect waterline so I may revisit this section later.
float Frag (Varyings i) : SV_Target
{
float intialTriangleArea = length(ddx(i.originalPos)) * length(ddy(i.originalPos));
float refractedTriangleArea = length(ddx(i.refractedPos)) * length(ddy(i.refractedPos));
return intialTriangleArea / refractedTriangleArea;
}
Which produces a texture like this:
float SampleGodrays(float3 positionWS, float3 lightDirection)
{
float3 normal = float3(0.0, -1.0, 0.0);
// Project caustics texture in light direction.
float3 forward = refract(lightDirection, normal, 1.0 / _IndexOfRefraction);
float3 tangent = normalize(cross(forward, float3(0.0, 1.0, 0.0)));
float3 bitangent = cross(tangent, forward);
float3 sampleCoord = positionWS * _TilingFactor;
float2 uv = float2(dot(sampleCoord, tangent), dot(sampleCoord, bitangent)) * 0.5 + 0.5;
// Sample caustics texture at a low LOD to blur away artefacts.
return SAMPLE_TEXTURE2D_LOD(_CausticsTexture, sampler_LinearRepeat, uv, 5).r;
}
This is called from a raymarching function, including the usual upsampling and blur passes, before being composited onto the final image.
float EvaluateTriangle(int triangleIndex, float3 pos)
{
// a, b and c are the vertices of the triangle.
float3 a = _Vertices[_Triangles[0 + triangleIndex * 3]];
float3 b = _Vertices[_Triangles[1 + triangleIndex * 3]];
float3 c = _Vertices[_Triangles[2 + triangleIndex * 3]];
float3 pointOnTriangle = ClosestPointOnTriangle(pos, a, b, c);
float3 normal = cross(b - a, c - a);
float3 v = pos - pointOnTriangle;
float3 dirToFace = normalize(v);
float distToFace = length(v);
if (dot(dirToFace, normal) < 0)
{
distToFace *= -1;
}
return distToFace;
}
float SmallestPointDistanceToMesh(float3 pos)
{
float minAbsoluteDistance = 3.40282347e+38F;
float minDistance = 3.40282347e+38F;
for (int i = 0; i < _NumTriangles; i++)
{
float distance = EvaluateTriangle(i, pos);
float absoluteDistance = abs(distance);
if (absoluteDistance < minAbsoluteDistance)
{
minAbsoluteDistance = absoluteDistance;
minDistance = distance;
}
}
return minDistance;
}
The function I used to find the closest point on a triangle is from the Embree Ray Tracing Repo.
The Fourier Transform
https://www.thefouriertransform.com/Wakes, Explosions and Lighting: Interactive Water Simulation in Atlas
https://www.youtube.com/watch?v=Dqld965-Vv0Reflection, Refraction and Fresnel
https://www.scratchapixel.com/lessons/3d-basic-rendering/introduction-to-shading/reflection-refraction-fresnel.htmlOcean Simulation - antoniospg
https://antoniospg.github.io/UnityOcean/OceanSimulation.htmlSimulating Ocean Water - Jerry Tessendorf
https://people.computing.clemson.edu/~jtessen/reports/papers_files/coursenotes2004.pdfI Tried Simulating The Entire Ocean - Acerola
https://www.youtube.com/watch?v=yPfagLeUa7kCrash Course in BRDF Implementation - Jakub Boksansky
https://boksajak.github.io/files/CrashCourseBRDF.pdfRendering Realtime Caustics in WebGL - Even Wallace
https://medium.com/@evanwallace/rendering-realtime-caustics-in-webgl-2a99a29a0b2cPeriodic Caustic Textures
https://www.dgp.toronto.edu/public_user/stam/reality/Research/PeriodicCaustics/index.html