Procedural Underwater Environments
Description
For my master's thesis at SMU Guildhall, I researched, architected, and developed a technical showcase of procedural generation and rendering techniques for creating dynamic, game-optimized ocean environments.
Duration: June 2025 - Present
Technologies:
My custom C++ engine
D3D12
HLSL
ImGui
Key Features
Multi-Threading
3D Procedural Terrain Generation using Perlin Noise and Cube Marching Algorithms
Gerstner Water Simulation
Boid Fish Simulation
Realistic Lighting with Fog, Caustics, Underwater Color Attenuation, and Light Rays
Dynamic Vegetation
Tri-planar Texture Splatting
Compute Shader Simulation
Multi-Pass Rendering


In order to capture the unique characteristics of ocean environments, I created a multi-pass rendering pipeline using DirectX 12. Compute shaders are used for instanced vegetation meshes, volumetric light rays, and particles. Additionally as I draw the scene, I capture data on render targets to be used in later draw calls. This allows me to create interesting effects like water refraction and post-process composited fog and lighting effects.

In order to capture the unique characteristics of ocean environments, I created a multi-pass rendering pipeline using DirectX 12. Compute shaders are used for instanced vegetation meshes, volumetric light rays, and particles. Additionally as I draw the scene, I capture data on render targets to be used in later draw calls. This allows me to create interesting effects like water refraction and post-process composited fog and lighting effects.
Multi-Threaded Chunk Generation
.png)

To simulate an endless ocean, I architected a fully streaming world system built around 3D chunk partitioning. The world is divided into fixed-size volumetric chunks that are generated and destroyed dynamically based on player position.
Chunk generation runs on a multi-threaded job system with explicit state management to ensure thread safety. Each chunk progresses through defined lifecycle states, preventing race conditions between worker threads and the main thread.
A flood fill traversal begins from the player’s current chunk to determine the working set within a configurable radius. Chunks are scored and prioritized based on distance and camera frustum visibility, ensuring that visible terrain is generated first while off-screen work is deferred.
This approach maintains the illusion of infinite space while keeping memory usage and frame time stable under continuous world expansion.
Each chunk is sent to a separate thread for generation and then ultimately re-claimed by the main thread for activation.

To simulate an endless ocean, I architected a fully streaming world system built around 3D chunk partitioning. The world is divided into fixed-size volumetric chunks that are generated and destroyed dynamically based on player position.
Chunk generation runs on a multi-threaded job system with explicit state management to ensure thread safety. Each chunk progresses through defined lifecycle states, preventing race conditions between worker threads and the main thread.
A flood fill traversal begins from the player’s current chunk to determine the working set within a configurable radius. Chunks are scored and prioritized based on distance and camera frustum visibility, ensuring that visible terrain is generated first while off-screen work is deferred.
This approach maintains the illusion of infinite space while keeping memory usage and frame time stable under continuous world expansion.
Each chunk is sent to a separate thread for generation and then ultimately re-claimed by the main thread for activation.
Density Gradient Field


Terrain generation is driven by a signed density field. Positive density represents solid volume, negative density represents air, and the zero-crossing defines the terrain surface. This scalar field becomes the foundation for both mesh extraction and collision queries.
The density field is constructed through layered combinations of 2D and 3D Perlin, Fractal, and domain-warped noise, each responsible for a distinct structural feature.
Base 3D Terrain Layer
A primary 3D Perlin field establishes the volumetric foundation. Density is biased from solid at ocean depth to air near sea level, producing overhangs, outcroppings, and non-heightmap topology. All higher-level terrain modulation builds on this base volume.
Continentalness
A large-scale 2D noise pass applies smooth elevation offsets to the density field, shaping broad valleys and large island masses that intersect the ocean surface.
Erosion
A heavily warped 2D noise layer subtracts from the density field to simulate canyon formation and water-carved structures, increasing geological complexity without disrupting large-scale landmasses.
Peaks and Valleys
Higher-frequency 2D Perlin noise introduces rapid elevation variation, accentuating sharp ridges, deep pits, and localized underwater rock formations.
Once finalized, the density field is used for surface extraction during mesh generation and for volumetric collision checks against dynamic entities such as the player and flocking fish.

Terrain generation is driven by a signed density field. Positive density represents solid volume, negative density represents air, and the zero-crossing defines the terrain surface. This scalar field becomes the foundation for both mesh extraction and collision queries.
The density field is constructed through layered combinations of 2D and 3D Perlin, Fractal, and domain-warped noise, each responsible for a distinct structural feature.
Base 3D Terrain Layer
A primary 3D Perlin field establishes the volumetric foundation. Density is biased from solid at ocean depth to air near sea level, producing overhangs, outcroppings, and non-heightmap topology. All higher-level terrain modulation builds on this base volume.
Continentalness
A large-scale 2D noise pass applies smooth elevation offsets to the density field, shaping broad valleys and large island masses that intersect the ocean surface.
Erosion
A heavily warped 2D noise layer subtracts from the density field to simulate canyon formation and water-carved structures, increasing geological complexity without disrupting large-scale landmasses.
Peaks and Valleys
Higher-frequency 2D Perlin noise introduces rapid elevation variation, accentuating sharp ridges, deep pits, and localized underwater rock formations.
Once finalized, the density field is used for surface extraction during mesh generation and for volumetric collision checks against dynamic entities such as the player and flocking fish.
Cube Marching


Once the signed density field is generated, terrain surfaces are extracted using a Marching Cubes algorithm.
For each voxel cell, density is sampled at its eight corner positions. The sign of each sample determines whether the corner lies inside or outside the surface. These eight sign bits are combined into a case index, which is used to query a precomputed lookup table.
The lookup table defines the triangle topology for that configuration and specifies which edges of the cube contain surface intersections. Vertex positions are computed along those edges by interpolating between density samples at the zero-crossing.

This approach generates geometry only where the density field transitions from negative to positive, efficiently extracting the isosurface while avoiding unnecessary mesh data in fully solid or fully empty regions.
Once the signed density field is generated, terrain surfaces are extracted using a Marching Cubes algorithm.
For each voxel cell, density is sampled at its eight corner positions. The sign of each sample determines whether the corner lies inside or outside the surface. These eight sign bits are combined into a case index, which is used to query a precomputed lookup table.
The lookup table defines the triangle topology for that configuration and specifies which edges of the cube contain surface intersections. Vertex positions are computed along those edges by interpolating between density samples at the zero-crossing.
Vegetation


Using the biome splats generated with the terrain, I place vegetation with a disc-packing algorithm. This allows each biome to spawn distinct vegetation while minimizing overlap between individual instances and maintaining a natural distribution.
To support dense underwater environments with thousands of instances, I built a compute-shader-driven instancing pipeline. This system handles per-instance variation and rendering entirely on the GPU. I also apply vertex displacement in the shader to drive dynamic vegetation movement at scale.
Underwater Currents
I drive vegetation motion using a time-evolving, warped noise field that represents ocean currents. I combine three frequency bands to control motion at different scales: large-scale sway, mid-range drift, and fine detail wobble. On top of this, I add a swirl field to introduce subtle vertical movement, preventing the motion from feeling planar.
In the vertex shader, I apply displacement along each instance’s longitudinal axis, with deformation strength increasing toward the tip. This keeps the base anchored while allowing the upper portions to bend in the sampled current direction. I modulate the final displacement using dot products against the instance normal, combined with an adjustable rigidity parameter. This allows different vegetation types to respond to the same current field while preserving their individual structural behavior and avoiding uniform motion.
Sea Grass
I construct sea grass using dense, segmented triangle strips to allow smooth bending along its length. Since the geometry is flat, I generate curved normals to simulate volumetric lighting, which improves shading and gives the grass a sense of thickness and depth when lit.
Sea Anemone
I build anemones from thin cylindrical segments arranged radially around a center. I apply noise-driven deformation along the length of each cylinder to break uniformity and introduce organic curvature. In addition, I layer a sine-based wobble along the length of each segment, adding lateral motion that reacts to the current and gives the anemone a more lively, reactive appearance.
Kelp
Kelp uses a similar length-based displacement approach but behaves differently from grass and anemones. Instead of uniform curvature, I weight the displacement by each vertex’s height along the blade. This keeps the base firmly anchored while allowing the upper sections to sway more freely, producing a more natural, top-heavy motion that better matches how kelp moves in water.

Using the biome splats generated with the terrain, I place vegetation with a disc-packing algorithm. This allows each biome to spawn distinct vegetation while minimizing overlap between individual instances and maintaining a natural distribution.
To support dense underwater environments with thousands of instances, I built a compute-shader-driven instancing pipeline. This system handles per-instance variation and rendering entirely on the GPU. I also apply vertex displacement in the shader to drive dynamic vegetation movement at scale.
Underwater Currents
I drive vegetation motion using a time-evolving, warped noise field that represents ocean currents. I combine three frequency bands to control motion at different scales: large-scale sway, mid-range drift, and fine detail wobble. On top of this, I add a swirl field to introduce subtle vertical movement, preventing the motion from feeling planar.
In the vertex shader, I apply displacement along each instance’s longitudinal axis, with deformation strength increasing toward the tip. This keeps the base anchored while allowing the upper portions to bend in the sampled current direction. I modulate the final displacement using dot products against the instance normal, combined with an adjustable rigidity parameter. This allows different vegetation types to respond to the same current field while preserving their individual structural behavior and avoiding uniform motion.
Sea Grass
I construct sea grass using dense, segmented triangle strips to allow smooth bending along its length. Since the geometry is flat, I generate curved normals to simulate volumetric lighting, which improves shading and gives the grass a sense of thickness and depth when lit.
Sea Anemone
I build anemones from thin cylindrical segments arranged radially around a center. I apply noise-driven deformation along the length of each cylinder to break uniformity and introduce organic curvature. In addition, I layer a sine-based wobble along the length of each segment, adding lateral motion that reacts to the current and gives the anemone a more lively, reactive appearance.
Kelp
Kelp uses a similar length-based displacement approach but behaves differently from grass and anemones. Instead of uniform curvature, I weight the displacement by each vertex’s height along the blade. This keeps the base firmly anchored while allowing the upper sections to sway more freely, producing a more natural, top-heavy motion that better matches how kelp moves in water.
Terrain Texturing
_edited.jpg)

As additional Perlin noise pass is used to generate a temperature field, which works in conjunction with terrain shaping noise to drive biome distribution across the world. This allows for large-scale environmental variation that feels natural and continuous.
Each generated vertex contributes to a biome splat map, storing four weighted biome indices. In the pixel shader, these indices are used to dynamically sample from a bindless texture array, enabling efficient and flexible material blending.
To ensure seamless texturing across arbitrary geometry, triplanar mapping is used. Textures are projected along the surface normal, producing world-aligned results that eliminate stretching and visible seams. The weighted biome splatting enables smooth transitions between neighboring biomes, avoiding hard edges.
Additionally, each biome defines separate texture groups for floor and wall surfaces, allowing distinct visual treatment for flat terrain versus steep slopes. Every texture group includes a full material set, consisting of albedo, normal, and ambient occlusion maps, contributing to a rich and cohesive visual result.

As additional Perlin noise pass is used to generate a temperature field, which works in conjunction with terrain shaping noise to drive biome distribution across the world. This allows for large-scale environmental variation that feels natural and continuous.
Each generated vertex contributes to a biome splat map, storing four weighted biome indices. In the pixel shader, these indices are used to dynamically sample from a bindless texture array, enabling efficient and flexible material blending.
To ensure seamless texturing across arbitrary geometry, triplanar mapping is used. Textures are projected along the surface normal, producing world-aligned results that eliminate stretching and visible seams. The weighted biome splatting enables smooth transitions between neighboring biomes, avoiding hard edges.
Additionally, each biome defines separate texture groups for floor and wall surfaces, allowing distinct visual treatment for flat terrain versus steep slopes. Every texture group includes a full material set, consisting of albedo, normal, and ambient occlusion maps, contributing to a rich and cohesive visual result.
Fish


To simulate marine life, I implemented a boid-based flocking system for schooling fish. Each entity computes a steering vector as a weighted combination of Cohesion, Alignment, and Separation forces. This produces emergent flock behavior while maintaining local avoidance against terrain and the player.
Behavior variation is data-driven. Each fish type defines its own weighting coefficients, allowing species-level differences in grouping tightness, directional stability, and separation bias without altering core logic.
Swimming motion is handled entirely on the GPU. A vertex shader applies sinusoidal displacement along the length of the mesh, producing a traveling wave that simulates body undulation. This approach avoids skeletal animation overhead while maintaining lightweight, scalable motion across large schools.

To simulate marine life, I implemented a boid-based flocking system for schooling fish. Each entity computes a steering vector as a weighted combination of Cohesion, Alignment, and Separation forces. This produces emergent flock behavior while maintaining local avoidance against terrain and the player.
Behavior variation is data-driven. Each fish type defines its own weighting coefficients, allowing species-level differences in grouping tightness, directional stability, and separation bias without altering core logic.
Swimming motion is handled entirely on the GPU. A vertex shader applies sinusoidal displacement along the length of the mesh, producing a traveling wave that simulates body undulation. This approach avoids skeletal animation overhead while maintaining lightweight, scalable motion across large schools.
Water Surface


I simulate the water surface directly in the vertex shader using a layered Gerstner wave model. Instead of relying on normal animation alone, I displace the mesh over time, accumulating multiple waves to produce both vertical height and lateral motion. This allows for sharper crests and more natural wave shapes.
Wave Modeling
Each wave is parameterized by direction, amplitude, wavelength, speed, and crest steepness. I sum multiple waves per vertex and introduce world-space noise to perturb direction and amplitude, reducing repetition. I also clamp steepness based on wavelength and amplitude to prevent instability and self-intersection.
Surface Shading
I analytically reconstruct the surface normal from accumulated derivatives, ensuring the geometry and lighting remain consistent. In the pixel shader, I layer additional detail using scrolling normal maps, refraction from the scene color buffer, and Fresnel-based reflection with specular highlights. Water depth drives color blending, while foam is generated from wave slope, crest height, and shoreline intersection.
Underwater Rendering
When viewed from below, I switch to a separate shading path that emphasizes light transmission and refraction instead of reflection. This includes a projected sunlight effect and subtle noise-based shimmer to reinforce the surface as a light-filtering boundary.

I simulate the water surface directly in the vertex shader using a layered Gerstner wave model. Instead of relying on normal animation alone, I displace the mesh over time, accumulating multiple waves to produce both vertical height and lateral motion. This allows for sharper crests and more natural wave shapes.
Wave Modeling
Each wave is parameterized by direction, amplitude, wavelength, speed, and crest steepness. I sum multiple waves per vertex and introduce world-space noise to perturb direction and amplitude, reducing repetition. I also clamp steepness based on wavelength and amplitude to prevent instability and self-intersection.
Surface Shading
I analytically reconstruct the surface normal from accumulated derivatives, ensuring the geometry and lighting remain consistent. In the pixel shader, I layer additional detail using scrolling normal maps, refraction from the scene color buffer, and Fresnel-based reflection with specular highlights. Water depth drives color blending, while foam is generated from wave slope, crest height, and shoreline intersection.
Underwater Rendering
When viewed from below, I switch to a separate shading path that emphasizes light transmission and refraction instead of reflection. This includes a projected sunlight effect and subtle noise-based shimmer to reinforce the surface as a light-filtering boundary.
Underwater Lighting


Underwater lighting was a major visual pillar of the environment. I focused on simulating the optical behaviors that make submerged scenes feel distinct from standard atmospheric rendering.
Caustics
In real life, surface waves refract incoming light and project dynamic patterns onto the terrain below. I simulated this effect using animated 2D noise masked by Voronoi edge patterns to produce shifting, high-contrast caustic bands. The result is a time-varying light modulation that blends seemlessly across chunk boundaries.
Color Attenuation
Water selectively absorbs longer wavelengths first, reducing red and green light as distance increases. In the pixel shader, I applied distance-based attenuation to individual color channels, approximating wavelength absorption relative to depth and light source distance. This produces the characteristic blue shift and enhances depth perception in the scene.

Underwater lighting was a major visual pillar of the environment. I focused on simulating the optical behaviors that make submerged scenes feel distinct from standard atmospheric rendering.
Caustics
In real life, surface waves refract incoming light and project dynamic patterns onto the terrain below. I simulated this effect using animated 2D noise masked by Voronoi edge patterns to produce shifting, high-contrast caustic bands. The result is a time-varying light modulation that blends seemlessly across chunk boundaries.
Color Attenuation
Water selectively absorbs longer wavelengths first, reducing red and green light as distance increases. In the pixel shader, I applied distance-based attenuation to individual color channels, approximating wavelength absorption relative to depth and light source distance. This produces the characteristic blue shift and enhances depth perception in the scene.
Volumetric Light Rays and Particles


Light Rays and Particles
I generate light shafts and suspended particles in a view-aligned 3D volume texture centered on the camera. I build this volume in a compute shader, where each thread corresponds to a frustum-aligned voxel and reconstructs world-space positions along the view ray. The red channel stores light shaft density and the green channel stores particle density, producing a view-dependent field that matches the visible scene without requiring a full world-space volume.
I evaluate two density functions during generation. For light shafts, I project sample positions into a sun-aligned basis and evaluate a warped Voronoi edge pattern, consistent with my caustics, then shape it with thresholding and depth fading. For particles, I divide space into cells, place noise-driven particles, animate them, and compute a soft spherical falloff, writing the strongest local contribution into the volume.
Ray Marching
In a full-screen pass, I ray march through the volume from the camera to scene depth. Light shaft density is accumulated along the ray, while particle density takes the maximum contribution. I then weight shaft intensity based on alignment with the sun direction and shape both results into a screen-space volumetric mask used during final composition.
Fog
I apply fog using a simplified radiative transfer model, blending attenuated scene color with added fog color. I derive the medium amount from linearized depth, which controls both transmittance and scattering, with user-defined shaping to control how fog builds over distance.
I then sample the volumetric mask and add light rays and particles as additional scattering to the base fog. Since this runs as a post-process pass, it results in a unified volumetric blend that ties fog, light rays, and particles together within the scene.

Light Rays and Particles
I generate light shafts and suspended particles in a view-aligned 3D volume texture centered on the camera. I build this volume in a compute shader, where each thread corresponds to a frustum-aligned voxel and reconstructs world-space positions along the view ray. The red channel stores light shaft density and the green channel stores particle density, producing a view-dependent field that matches the visible scene without requiring a full world-space volume.
I evaluate two density functions during generation. For light shafts, I project sample positions into a sun-aligned basis and evaluate a warped Voronoi edge pattern, consistent with my caustics, then shape it with thresholding and depth fading. For particles, I divide space into cells, place noise-driven particles, animate them, and compute a soft spherical falloff, writing the strongest local contribution into the volume.
Ray Marching
In a full-screen pass, I ray march through the volume from the camera to scene depth. Light shaft density is accumulated along the ray, while particle density takes the maximum contribution. I then weight shaft intensity based on alignment with the sun direction and shape both results into a screen-space volumetric mask used during final composition.
Fog
I apply fog using a simplified radiative transfer model, blending attenuated scene color with added fog color. I derive the medium amount from linearized depth, which controls both transmittance and scattering, with user-defined shaping to control how fog builds over distance.
I then sample the volumetric mask and add light rays and particles as additional scattering to the base fog. Since this runs as a post-process pass, it results in a unified volumetric blend that ties fog, light rays, and particles together within the scene.
Performance Optimization
.png)

As world scale increased and the scene filled with dense vegetation and flocking entities, memory pressure and frame time instability became primary constraints. I introduced several structural optimizations to keep the engine within real-time performance budgets.
Density Gradient Field Optimization
Most generated chunks were either entirely solid or entirely empty. Storing full volumetric density data for those regions was wasteful. I added a density crossing test during generation and discarded chunks without a surface intersection. This eliminated unnecessary mesh builds and significantly reduced memory usage.
Voxel Scale Tuning
Without a full LOD system, terrain density resolution directly dictated mesh complexity. Increasing voxel scale from 1m to 5m reduced density samples and triangle counts dramatically with minimal visual degradation. The voxel scale remains runtime-configurable through ImGui, allowing controlled tradeoffs between fidelity and performance.
Frustum Culling
As terrain detail increased, draw calls became a bottleneck. I implemented per-chunk frustum culling to exclude non-visible geometry from submission. This reduced triangle throughput and stabilized render time under heavy world loads.
Boid System Optimization
Flocking behavior introduced quadratic neighbor checks that degraded rapidly as fish grouped together. Early distance checks were insufficient due to natural clustering. I introduced spatial partitioning aligned with the chunk system to localize neighbor queries. To prevent worst-case clustering costs, each boid evaluates only a capped subset of nearby entities, selected via a frame-dependent hash combined with a unique fish ID. This maintained emergent flock behavior while producing stable and predictable frame times.

As world scale increased and the scene filled with dense vegetation and flocking entities, memory pressure and frame time instability became primary constraints. I introduced several structural optimizations to keep the engine within real-time performance budgets.
Density Gradient Field Optimization
Most generated chunks were either entirely solid or entirely empty. Storing full volumetric density data for those regions was wasteful. I added a density crossing test during generation and discarded chunks without a surface intersection. This eliminated unnecessary mesh builds and significantly reduced memory usage.
Voxel Scale Tuning
Without a full LOD system, terrain density resolution directly dictated mesh complexity. Increasing voxel scale from 1m to 5m reduced density samples and triangle counts dramatically with minimal visual degradation. The voxel scale remains runtime-configurable through ImGui, allowing controlled tradeoffs between fidelity and performance.
Frustum Culling
As terrain detail increased, draw calls became a bottleneck. I implemented per-chunk frustum culling to exclude non-visible geometry from submission. This reduced triangle throughput and stabilized render time under heavy world loads.
Boid System Optimization
Flocking behavior introduced quadratic neighbor checks that degraded rapidly as fish grouped together. Early distance checks were insufficient due to natural clustering. I introduced spatial partitioning aligned with the chunk system to localize neighbor queries. To prevent worst-case clustering costs, each boid evaluates only a capped subset of nearby entities, selected via a frame-dependent hash combined with a unique fish ID. This maintained emergent flock behavior while producing stable and predictable frame times.