Skip to main content
← Roadmap
🌊 Oceanology ProIn Progress6.0.0

GPU water quadtree with occlusion culling

Large water surfaces cost less: tiles the camera cannot see are culled entirely, and detail concentrates near the viewer instead of being spread evenly across the whole surface.


Every water surface inside a zone is drawn by one continuous mesh whose tiles come from a quadtree. Oceanology Pro can build and traverse that quadtree entirely on the GPU and issue indirect draws from it, and it can occlusion-cull water tiles two ways: hardware occlusion queries on the CPU, and hierarchical-Z plus pixel-precise raster queries inside the GPU traverse itself. The practical difference is between an ocean that costs what is on screen and one that costs what exists in the level.

What the quadtree holds

  • Rivers, lakes, oceans and custom bodies all register into the zone's water mesh component and are inserted into one tree. Oceans go in as closed land-mass polygons, lakes as a closed spline polygon, rivers as bounds.
  • Each node carries its water body index, material index, surface base height, Z bounds, maximum wave height, render priority, and whether its whole subtree belongs to one body, which is what lets a parent tile be drawn in place of all its children.
  • LOD is concentric: LOD distance is two to the power of level plus one, times the LOD scale. Smooth vertex morphing between density levels is on by default and can be disabled to skip the vertex-shader work.
  • Per-tile instance data is two float4 streams, three with editor selection support: translation plus wave-data index, then LOD level, morph flag and scale.
  • A far-distance mesh, an eight-quad skirt, extends the water past the zone with its own material, optionally even when there is no ocean in the level.
  • River-to-lake and river-to-ocean transition materials are resolved per exact river/target pair, cached across rebuilds and republished when runtime simulation parameters change without rebuilding the mesh.

Two ways to traverse it

  • CPU path, the default. The scene proxy traverses on the render thread once for all views, culls to the frustum, and emits instance data into buckets sorted by material and density level.
  • GPU path, enabled with r.Oceanology.WaterMesh.GPUQuadTree. Water body meshes are rasterized into a mipped quadtree texture with a companion Z-bounds texture, then a compute traverse writes packed nodes, per-material bucket counts and indirect draw arguments. The CPU never walks a node. It supports instanced stereo through a dedicated indirect-draw vertex factory and runs per view, so it works with the sliding infinite window.
  • Edge quality on the GPU build is tunable: supersampled rasterization, multisampling, multiple jittered draw calls with a Halton or MSAA pattern and an adjustable pixel footprint, or software conservative rasterization instead of jitter.
  • A Z-bounds padding value exists specifically for sloped rivers, whose full Z extent can be underestimated by non-conservative rasterization.

Occlusion culling

On the CPU, the proxy publishes quadtree node bounds in breadth-first order as sub-primitive occlusion queries. Capping the query count therefore does not disable culling, it just makes it coarser from the top of the tree down. Bounds are expanded in XY only so a tile about to come into view is not popped, far-mesh tiles get their own queries, results are tracked per view key, and results from a proxy created on an older tree are rejected.

On the GPU, the traverse does the culling itself. In the default mode it first tests nodes against the view's hierarchical-Z buffer, then emits query boxes and indirect arguments for a depth-read-only raster pass that tests them pixel-precisely against the scene depth target; only surviving nodes reach the draws. Modes for off, HZB only and raster only are also available. Both paths report into the Oceanology Water Mesh stat group, which counts tiles drawn, tiles occlusion culled, draw calls, vertices drawn, drawn materials and occlusion queries issued.

Versus Oceanology NextGen

Oceanology NextGenOceanology Pro
Quadtree residencyCPU-only tree, traversed on the render threadSame CPU path, plus an optional GPU-built and GPU-traversed tree driving indirect draws
Tile occlusionTiles were culled by frustum onlyCPU hardware occlusion queries on node bounds, and HZB plus pixel-precise raster queries inside the GPU traverse
Mesh scopeQuadtree settings authored per water body, with a fixed tile extentA water zone owns one continuous mesh; extent, tile size and quadtree resolution are zone-level, with a sliding infinite window regenerated at runtime
River blendingGeneric river-to-lake and river-to-ocean transition materialsTransition materials resolved per exact river/target pair, cached across rebuilds and refreshable without a rebuild
GPU quadtreer.Oceanology.WaterMesh.GPUQuadTree, default 0
GPU occlusion moder.Oceanology.WaterMesh.GPUQuadTree.OcclusionCulling, default 3 (HZB plus pixel-precise raster)
CPU occlusion cullingr.Oceanology.WaterMesh.OcclusionCulling, default 0
CPU max occlusion queries256, with far-mesh queries always included by default
Occlusion bounds expansion4800 cm in XY
Tile size at LOD02400 cm (per water mesh component)
Tessellation factor1 to 12, default 6, giving 64 quads and 65 vertices per tile side
Max mesh dimension256 tiles per axis before the LOD count is automatically biased down
GPU raster qualitySupersampling 2 (1 to 8), jitter samples 4 (1 to 16), jitter footprint 1.5 px, MSAA jitter pattern
Quads per indirect draw tile8, clamped 2 to 128 and rounded up to a power of two
River Z bounds padding200 cm
WaterInfo render target512 by 512, half precision, velocity blur radius 1
Ray tracingr.RayTracing.Geometry.OceanologyWater, default 0 (water excluded)

This is one entry on the Galidar roadmap. For what already shipped in each release, see the Changelog.