BeamNG has several water scene object types:
| Class | Shape | Main use |
|---|---|---|
WaterPlane |
Infinite horizontal plane | Oceans, large lakes, horizon water. |
WaterBlock |
Bounded rectangular water volume | Pools, ponds, reservoirs, boxed water areas. |
River |
Spline-shaped water volume | Rivers, canals, streams, curved water paths. |
All three inherit common water behavior from WaterObject, including water materials, underwater checks, fog/underwater effect support, and optional planar reflections.
| Use case | Recommended class |
|---|---|
| Ocean around an island | WaterPlane |
| Very large flat water surface stretching to horizon | WaterPlane |
| Swimming pool / small lake / rectangular pond | WaterBlock |
| Bounded flat water area with rotation/scale | WaterBlock |
| River following a path | River |
| Canal with variable width/depth | River |
| Twisting water body with flow direction | River |
WaterPlane represents an infinite horizontal water surface.
It is designed mainly for ocean-style water viewed from near ground level. It extends infinitely in X/Y and is positioned only by height.
WaterPlane ignores rotation and scale. Only the Z value of its position matters.
{
"class": "WaterPlane",
"name": "ocean_water",
"position": [0, 0, 0],
"material": "water_ocean",
"gridSize": 101,
"gridElementSize": 1
}
The important part is:
"position": [0, 0, 0]
Only position[2], the Z height, is used as the water level.
| Field | Type | Description |
|---|---|---|
class |
string | Must be "WaterPlane". |
name |
string | Scene object name. |
position |
array[3] | Water height is taken from position.z. X/Y are not meaningful for coverage. |
material |
string | Water material used for rendering. |
gridSize |
integer | Render grid size. |
gridElementSize |
number | Spacing between water grid vertices. |
WaterPlane uses global bounds.
This means it is considered to exist everywhere horizontally. It is useful for oceans, but it is not a good fit for small water bodies.
Use WaterBlock instead if the water should have a finite footprint.
WaterPlane only accepts height changes.
Rotation and scale fields are removed from persistent fields and are not used.
Example:
{
"class": "WaterPlane",
"name": "sea_level",
"position": [0, 0, -2]
}
This creates a water plane at Z = -2.
gridSize and gridElementSize control the render mesh used by the water plane.
"gridSize": 101,
"gridElementSize": 1
WaterPlane renders a camera-centered grid rather than a literal infinite mesh. The water shader projects this grid as a large water surface.
Very large or unusual grid settings can increase render cost or cause visual artifacts.
WaterPlane is intended for ground-level ocean-style rendering.
Because it is effectively infinite and projected toward the far clip distance, artifacts can appear when large objects intersect the far clip distance or visually line up with the water plane.
Common problem cases:
For best results:
WaterPlane for small pools or bounded areas.WaterBlock for local water volumes.WaterBlock is a finite rectangular water volume.
It supports position, rotation, and scale. It is useful for bounded water such as lakes, pools, ponds, basins, and other local water areas.
Unlike WaterPlane, WaterBlock has finite bounds and can be tested for containment.
{
"class": "WaterBlock",
"name": "lake_water",
"position": [100, 200, 5],
"rotationMatrix": [1,0,0,0,1,0,0,0,1],
"scale": [200, 150, 20],
"material": "water_lake",
"gridElementSize": 5
}
| Field | Type | Description |
|---|---|---|
class |
string | Must be "WaterBlock". |
name |
string | Scene object name. |
position |
array[3] | Position of the water block. The water surface is based around the object position plane. |
rotationMatrix |
array[9] | Object rotation. |
scale |
array[3] | Size of the water volume. X/Y define footprint; Z defines vertical volume/depth behavior. |
material |
string | Water material used for rendering. |
gridElementSize |
number | Spacing between generated water mesh vertices. |
gridSize |
number | Compatibility alias for gridElementSize. |
WaterBlock uses its object transform and scale to define a rectangular water volume.
Typical setup:
"position": [0, 0, 10],
"scale": [100, 100, 20]
The X/Y scale controls the visible footprint. The Z scale controls the vertical water volume used for underwater/depth tests.
The water surface height is based on the object’s position.
gridElementSize controls the spacing between generated vertices.
"gridElementSize": 5
Smaller values create denser geometry:
"gridElementSize": 1
Larger values create cheaper, lower-density geometry:
"gridElementSize": 10
If gridElementSize is larger than the block’s X or Y scale, it is clamped to the smaller scale dimension.
gridElementSize values on large WaterBlocks can create a large amount of geometry.WaterBlock builds a regular grid over its footprint.
For large blocks, the mesh may be split into multiple vertex/index buffer blocks to stay within 16-bit index limits.
The object regenerates its water mesh when:
gridElementSize changesWaterBlock supports rotation.
This can be useful for water volumes placed on custom geometry or non-axis-aligned areas.
Reflection behavior can use either an upward normal or the object’s surface normal depending on inherited reflection settings.
River is a spline-shaped water volume.
It is defined by a list of nodes. Each node stores position, width, depth, and normal.
Rivers are useful for streams, canals, drainage channels, winding water paths, or any water volume that needs to follow a curve.
{
"class": "River",
"name": "main_river",
"material": "water_river",
"SegmentLength": 10,
"SubdivideLength": 2.5,
"FlowMagnitudePhysics": 3,
"LowLODDistance": 50,
"nodes": [
[0, 0, 5, 20, 4, 0, 0, 1],
[50, 10, 5, 22, 4, 0, 0, 1],
[100, 40, 4, 18, 3, 0, 0, 1],
[150, 60, 4, 16, 3, 0, 0, 1]
]
}
Each river node is stored as:
[x, y, z, width, depth, normalX, normalY, normalZ]
| Field | Type | Description |
|---|---|---|
class |
string | Must be "River". |
name |
string | Scene object name. |
material |
string | Water material used for rendering. |
nodes |
array | River node list. Each node is [x, y, z, width, depth, normalX, normalY, normalZ]. |
SegmentLength |
number | Approximate length in meters of generated river volume segments. |
SubdivideLength |
number | Maximum render subdivision size for high-detail geometry. |
FlowMagnitudePhysics |
number | River flow speed in meters per second. |
LowLODDistance |
number | Distance at which river segments switch to low-detail rendering. |
Example node:
[50, 10, 5, 22, 4, 0, 0, 1]
| Index | Meaning |
|---|---|
0 |
X position |
1 |
Y position |
2 |
Z position |
3 |
Width in meters |
4 |
Depth in meters |
5 |
Surface normal X |
6 |
Surface normal Y |
7 |
Surface normal Z |
Most normal values are usually:
0, 0, 1
for an upright water surface.
A River uses its control nodes to build a Catmull-Rom spline.
The generation process is roughly:
A river needs at least two nodes to generate usable water geometry.
"SegmentLength": 10
SegmentLength controls how long each generated river volume segment is.
Smaller values create more segments and more accurate containment/coverage queries.
Larger values create fewer segments and can be cheaper.
The minimum enforced value is approximately:
1 meter
"SubdivideLength": 2.5
SubdivideLength controls render subdivision for high-detail river geometry.
Smaller values create denser geometry and better visual undulation.
Larger values create cheaper geometry.
"LowLODDistance": 50
River rendering uses two detail levels:
| LOD | Description |
|---|---|
| High LOD | Subdivided geometry with water undulation. |
| Low LOD | Simpler strip geometry farther from the camera. |
Segments closer than LowLODDistance use high-detail rendering.
Segments farther away use low-detail rendering.
"FlowMagnitudePhysics": 3
Controls the river’s flow speed in meters per second.
The flow direction is based on the direction from one river segment slice to the next.
Node order matters:
node 0 -> node 1 -> node 2
Water flow follows this order.
If the river appears to flow the wrong way, reverse the node order.
Width and depth are stored per node and interpolated along the spline.
Example:
"nodes": [
[0, 0, 5, 10, 2, 0, 0, 1],
[50, 0, 5, 30, 5, 0, 0, 1]
]
This creates a river that widens from 10 meters to 30 meters and deepens from 2 meters to 5 meters.
Width and depth are clamped internally to safe ranges.
Approximate limits from the implementation:
| Value | Minimum | Maximum |
|---|---|---|
| Node width | 0.25 |
1000 |
| Node depth | 0.25 |
500 |
Each node has a normal vector.
For normal horizontal rivers, use:
[0, 0, 1]
Tilted normals can be used for special cases, but they affect generated slice orientation, surface plane, containment, and reflection behavior.
For most rivers, keep normals upright.
The water classes share common rendering and water-volume behavior.
Water objects use water materials.
Example:
"material": "water_lake"
A water material usually controls:
If the material is missing or invalid, the object may render incorrectly or use a fallback material.
Water objects support dynamic planar reflections through inherited water/reflection behavior.
This is commonly controlled by fields on the base water object/material setup, such as fullReflect where available.
Planar reflections work best for flat water.
| Object | Reflection quality notes |
|---|---|
WaterPlane |
Good for oceans and flat surfaces. |
WaterBlock |
Good for flat bounded surfaces. |
River |
Can be less accurate on twisting, sloped, or strongly curved rivers. |
For rivers, the reflection plane is chosen from the nearest river area and/or averaged river surface normals. Highly curved or twisted rivers may show reflection artifacts.
Water objects implement underwater/depth behavior differently:
| Class | Underwater behavior |
|---|---|
WaterPlane |
Underwater if point is below the plane height. |
WaterBlock |
Underwater if point is inside the water block volume below the surface. |
River |
Underwater if point is inside one of the generated river volume segments. |
This affects underwater fog, water coverage, buoyancy-style queries, and other gameplay/rendering systems that ask water objects for depth or containment.
Water objects can report surface height:
| Class | Surface height behavior |
|---|---|
WaterPlane |
Returns the plane Z height. |
WaterBlock |
Returns object Z height if the queried XY point is inside the block bounds, otherwise -1. |
River |
Casts downward against the river surface and returns the collision height, otherwise -1. |
{
"class": "WaterPlane",
"name": "ocean",
"position": [0, 0, 0],
"material": "water_ocean",
"gridSize": 101,
"gridElementSize": 1
}
{
"class": "WaterBlock",
"name": "pool_water",
"position": [0, 0, 2],
"rotationMatrix": [1,0,0,0,1,0,0,0,1],
"scale": [20, 10, 4],
"material": "water_pool",
"gridElementSize": 1
}
{
"class": "River",
"name": "small_stream",
"material": "water_stream",
"SegmentLength": 8,
"SubdivideLength": 2,
"FlowMagnitudePhysics": 1.5,
"LowLODDistance": 40,
"nodes": [
[0, 0, 3, 6, 1, 0, 0, 1],
[30, 5, 3, 7, 1.2, 0, 0, 1],
[60, 20, 2.8, 8, 1.5, 0, 0, 1]
]
}
Older TorqueScript-style serialization used repeated Node fields:
Node = "0 0 3 6 1 0 0 1";
Node = "30 5 3 7 1.2 0 0 1";
Node = "60 20 2.8 8 1.5 0 0 1";
Modern JSON should use the nodes array instead.
{
"class": "WaterPlane",
"name": "level_ocean",
"position": [0, 0, 0],
"material": "water_ocean",
"gridSize": 101,
"gridElementSize": 1
}
Use WaterPlane for large open water.
{
"class": "WaterBlock",
"name": "mountain_lake",
"position": [250, -100, 42],
"rotationMatrix": [1,0,0,0,1,0,0,0,1],
"scale": [180, 120, 12],
"material": "water_lake",
"gridElementSize": 4
}
Use WaterBlock when the lake should have finite boundaries.
{
"class": "WaterBlock",
"name": "pool_water",
"position": [10, 20, 1.2],
"rotationMatrix": [1,0,0,0,1,0,0,0,1],
"scale": [12, 6, 2],
"material": "water_pool",
"gridElementSize": 0.5
}
Use a smaller gridElementSize for close-up water if needed.
{
"class": "River",
"name": "valley_river",
"material": "water_river",
"SegmentLength": 10,
"SubdivideLength": 2.5,
"FlowMagnitudePhysics": 4,
"LowLODDistance": 60,
"nodes": [
[-100, 0, 15, 18, 3, 0, 0, 1],
[-50, 20, 14, 20, 3, 0, 0, 1],
[0, 30, 13, 22, 4, 0, 0, 1],
[70, 10, 12, 18, 3, 0, 0, 1],
[130, -20, 11, 16, 3, 0, 0, 1]
]
}
Flow direction follows node order from first node to last node.
WaterPlane only for very large/infinite water.WaterBlock for bounded ponds, lakes, pools, and basins.River for curved water with width, depth, and flow.WaterPlane near expected sea level and avoid extreme far-clip setups.WaterPlane for small local water areas.WaterBlock.gridElementSize reasonable for the block size.[0, 0, 1] for normal rivers.Possible causes:
River has fewer than two nodesWaterBlock scale is too smallOnly position.z matters.
Check:
"position": [0, 0, desiredWaterHeight]
Rotation and scale do not affect WaterPlane.
Check:
"scale": [width, length, depth]
Also check object rotation and position.
Check that it has at least two nodes:
"nodes": [
[0, 0, 0, 10, 2, 0, 0, 1],
[50, 0, 0, 10, 2, 0, 0, 1]
]
Also check material and subdivision settings.
River flow follows node order.
Reverse the node order, or edit the river so the first node is upstream and the last node is downstream.
Decrease:
"SegmentLength": 10
or decrease:
"SubdivideLength": 2.5
Smaller values create more generated segments/subdivisions.
Possible causes:
For rivers, reduce material reflectivity if the shape twists heavily.
This mostly affects WaterPlane.
Possible fixes:
WaterBlock for bounded water insteadBeamNG provides three main water scene objects:
WaterPlane for infinite ocean-style water.WaterBlock for finite rectangular water volumes.River for spline-based flowing water with variable width and depth.Use WaterPlane for oceans, WaterBlock for local water bodies, and River for path-shaped water. All water objects depend heavily on correct material setup, sensible geometry settings, and appropriate reflection configuration.
Was this article helpful?