Terrain Files (.ter and .terrain.json)

This document describes BeamNG terrain files and their companion metadata files.

Terrain data is stored mainly in a binary .ter file. When terrain is saved, the engine also writes a companion .terrain.json file that describes the terrain data and referenced materials.

A terrain in a level is represented by a TerrainBlock scene object. The TerrainBlock references the .ter file using its terrainFile field.

This page describes terrain files and terrain material data. For the scene object that places terrain in a level, see TerrainBlock .


File roles

File Purpose
.ter Binary terrain file containing heightmap data, layer map data, and terrain material names.
.terrain.json Metadata file describing the .ter file. Useful for tools and debugging.
.terrainheightmap.png Optional/exported heightmap image reference written by the terrain save path.
items.level.json Contains the TerrainBlock scene object that references the .ter.
*.materials.json Contains TerrainMaterial definitions referenced by the .ter.

Typical file location

Terrain files are usually stored inside the level folder.

levels/<levelName>/

Example:

levels/example/theTerrain.ter
levels/example/theTerrain.terrain.json
levels/example/theTerrain.terrainheightmap.png

This root-level layout is conventional in current shipped levels. TerrainMaterial definitions and their textures are stored separately under levels/<levelName>/art/terrains/.

The terrain object itself is stored in the level scene data, for example:

levels/example/main/items.level.json

TerrainBlock object

A terrain is placed in the level with a TerrainBlock object.

Example:

{
  "class": "TerrainBlock",
  "name": "theTerrain",
  "terrainFile": "/levels/example/theTerrain.ter",
  "materialTextureSet": "exampleTerrainMaterialTextureSet",
  "squareSize": 1,
  "maxHeight": 2048,
  "screenError": 16,
  "castShadows": true,
  "position": [-1024, -1024, 0],
  "rotationMatrix": [1, 0, 0, 0, 1, 0, 0, 0, 1]
}

Important TerrainBlock fields

Field Type Description
terrainFile string Path to the .ter terrain data file.
minimapImage string Optional minimap image for this terrain block.
materialTextureSet string TerrainMaterialTextureSet used by the v1.5 terrain material path. Leave empty only for classic v1 terrain materials.
castShadows bool Whether terrain casts shadows.
squareSize number Distance between heightmap samples in meters.
maxHeight number Maximum terrain height in meters.
baseTexSize integer Legacy generated terrain base texture resolution.
lightMapSize integer Legacy terrain lightmap texture size.
screenError integer Terrain LOD/screen error setting.
Terrain object scaling is disabled. Use squareSize and maxHeight to control terrain dimensions instead of object scale.

Terrain dimensions

The .ter file stores a square grid:

size x size

The world-space terrain width is approximately:

world size = size x squareSize

Example:

size = 2048
squareSize = 1
world size = 2048 m x 2048 m

If squareSize is 2, the same terrain grid covers:

4096 m x 4096 m

Height scale

Terrain heights are stored as unsigned 16-bit values (u16).

The TerrainBlock converts stored height values to meters using:

heightScale = maxHeight / 65536
heightMeters = storedHeight x heightScale

Example with:

"maxHeight": 2048

The height scale is:

2048 / 65536 = 0.03125 m

So one stored height unit equals approximately 0.03125 m.

The terrain object position is then applied on top of this height.


.ter binary format

The .ter file is the main binary terrain data file. It stores terrain height data, material layer indices, and the list of terrain material names.

The file is read sequentially from start to end.


Current saved layout

The current TerrainFile::save() path writes this binary layout:

Step Field Type Size Description
1 version u8 1 byte Terrain file version.
2 size u32 4 bytes Width/height of the square terrain grid.
3 heightMap u16[size x size] size x size x 2 bytes Height samples.
4 layerMap u8[size x size] size x size bytes Terrain material index per sample.
5 materialCount u32 4 bytes Number of terrain material names.
6 materialNames string array variable Terrain material internal names.

Current save pseudocode:

write((u8)FILE_VERSION);
write(mSize);

write(mHeightMap.byteSize(), mHeightMap.address());
write(mLayerMap.byteSize(), mLayerMap.address());

write((u32)mMaterials.size());

for each material:
    write(material.internalName);
The current implementation assumes little-endian data.

Reading strategy

To read a current .ter file:

  1. Read version as u8.
  2. Validate that version <= FILE_VERSION.
  3. Read size as u32.
  4. Compute sampleCount = size x size.
  5. Read sampleCount u16 values as the heightmap.
  6. Read sampleCount u8 values as the layer map.
  7. Read materialCount as u32.
  8. Read materialCount strings.
  9. Resolve material names to TerrainMaterial definitions.
  10. Build the terrain grid/quadtree data for rendering and collision.

Simplified pseudocode:

u8 version;
read(version);

u32 size;
read(size);

u32 sampleCount = size * size;

u16 heightMap[sampleCount];
read(heightMap, sampleCount * sizeof(u16));

u8 layerMap[sampleCount];
read(layerMap, sampleCount * sizeof(u8));

u32 materialCount;
read(materialCount);

for i in materialCount:
    string materialName = readString();

Version handling

The first byte is the terrain file version.

The current terrain file version is 9.

When loading:

  • If version > FILE_VERSION, loading fails.
  • If version >= 7, the modern loader path is used.
  • If version < 7, a legacy loader path is used.

For normal tools and new content, target the current saved layout described above.

The loader contains compatibility paths for older terrain versions. These are mostly for loading old content and should not be used as a reference for new files.

Heightmap encoding

The heightmap is stored as:

u16 heightMap[size x size]

Each sample is an unsigned 16-bit integer.

Conversion to meters:

heightMeters = storedHeight x (maxHeight / 65536)

Example:

storedHeight = 32768
maxHeight = 2048

heightMeters = 32768 x (2048 / 65536)
heightMeters = 1024 m

Layer map encoding

The layer map is stored as:

u8 layerMap[size x size]

Each value is an index into the terrain material name list.

Example:

0 = first terrain material
1 = second terrain material
2 = third terrain material

The value 255 (U8_MAX) is special:

255 = empty terrain / hole

Empty terrain is used by the terrain system to skip rendering and collision in those areas.


Material names

After the heightmap and layer map, the file stores terrain material names:

u32 materialCount
string materialNames[materialCount]

These names are resolved at load time using:

TerrainMaterial::findOrCreate(name)

If no valid materials are found, the terrain falls back to a warning material.

Material count limit

The terrain layer map uses u8 indices, with 255 reserved for empty terrain. The loader supports up to 254 terrain material entries.

Materials after that limit are ignored.

For performance and maintainability, keep terrain material counts much lower than the technical limit.


File size estimate

For a current terrain file, before material name strings:

1 byte                   version
4 bytes                  size
size x size x 2 bytes    heightMap
size x size x 1 byte     layerMap
4 bytes                  materialCount
variable                 material names

For 2048 x 2048:

heightMap = 2048 x 2048 x 2 = 8 MiB
layerMap  = 2048 x 2048 x 1 = 4 MiB

Total map data:

~12 MiB

plus the small header and material name strings.


.terrain.json metadata

When the engine saves a terrain, it writes a companion .terrain.json file.

Example:

{
  "version": 9,
  "datafile": "/levels/example/theTerrain.ter",
  "heightmapImage": "/levels/example/theTerrain.terrainheightmap.png",
  "size": 2048,
  "binaryFormat": "version(char), size(unsigned int), heightMap(heightMapSize * heightMapItemSize), layerMap(layerMapSize * layerMapItemSize), layerTextureMap(layerMapSize * layerMapItemSize), materialNames",
  "heightMapSize": 4194304,
  "heightMapItemSize": 2,
  "layerMapSize": 4194304,
  "layerMapItemSize": 1,
  "materials": [
    "grass",
    "rock",
    "asphalt"
  ]
}

Metadata fields

Field Type Description
version number Terrain file version.
datafile string Path to the binary .ter file.
heightmapImage string Path to associated/exported heightmap image.
size number Terrain grid size.
binaryFormat string Engine-generated human-readable description. It is metadata only and may retain legacy field names.
heightMapSize number Number of heightmap samples.
heightMapItemSize number Size of each heightmap sample in bytes.
layerMapSize number Number of layer map samples.
layerMapItemSize number Size of each layer map sample in bytes.
materials array[string] Terrain material internal names.
.terrain.json is descriptive metadata for tools and debugging. The engine loads the actual terrain data from the .ter file referenced by the TerrainBlock.
The engine currently writes layerTextureMap into the binaryFormat description even though the version 9 payload written by TerrainFile::save() does not contain that array. Parse the .ter file using the current saved layout , not this descriptive string.

Terrain materials

The .ter file stores only terrain material names. The actual material definitions are separate TerrainMaterial objects, stored in a materials JSON file.

Terrain materials come in two versions: the classic v1 path and the modern PBR v1.5 workflow. The sections below describe v1.5 - how it is enabled, how it works, and its fields.


Enabling v1.5 terrain materials

Terrain materials have two versions:

Version When it is used Texture model
v1 (classic) TerrainBlock.materialTextureSet is empty. Single diffuseMap, normalMap, detailMap, macroMap.
v1.5 (PBR) TerrainBlock.materialTextureSet references a TerrainMaterialTextureSet. baseColor, normal, roughness, ao, height, each with base/macro/detail textures.

The whole terrain switches version based on the TerrainBlock. There is no per-material version flag: assigning a materialTextureSet to the TerrainBlock activates v1.5 for every material painted on that terrain.

Upgrading from v1 to v1.5

In the World Editor, the Terrain Material Library has an Upgrade Terrain Materials action. It:

  • Creates a TerrainMaterialTextureSet object (saved in art/terrains/main.materials.json).
  • Assigns it to the TerrainBlock.materialTextureSet field.
  • Clears the obsolete v1 fields (diffuseMap, normalMap, detailMap, macroMap) from existing terrain materials.
Upgrading is one-way. The classic v1 texture assignments are not carried over to the v1.5 slots, so every terrain material must be re-authored with the new base/macro/detail textures after upgrading. Save the level afterwards to persist the TerrainBlock change.

Terrain material file

v1.5 terrain materials and the TerrainMaterialTextureSet are stored in:

levels/<levelName>/art/terrains/main.materials.json

Older levels may still store terrain materials in art/terrains/materials.json. This is a deprecated layout; the editor offers an Upgrade Terrain Material file format action that moves them into main.materials.json.


How v1.5 terrain materials work

A v1.5 terrain material is built from five texture groups, and each group is sampled at three scales that combine into the final surface. Understanding this model is the key to authoring good-looking terrain.

In short (for a first material): make a baseColor, normal, roughness, ao, and height texture, and for each one supply a base (broad look), a macro (mid-range variation), and a detail (close-up) version. Assign them in the World Editor’s Terrain Material Library, set a groundmodelName (e.g. GRASS), and leave the distance/strength values at the typical defaults shown below. The deep-dive subsections explain how to fine-tune from there.

The five texture groups

Group What it controls Color space How to author
Base color The albedo (surface color). sRGB Paint the real color of the surface (grass green, rock gray, …).
Normal Surface relief / bumpiness (tangent-space normal map). Linear A normal map. Only red/green are stored; blue is reconstructed by the shader.
Roughness Glossiness. Dark = glossy/wet, bright = rough/matte. Linear (grayscale) A grayscale roughness map.
Ambient occlusion Self-shadowing in crevices under ambient light. Linear (grayscale) A grayscale AO map; white = no occlusion.
Height Per-pixel height used for layer blending (see below). Linear (grayscale) A grayscale heightmap; white = raised, black = recessed.

Base, macro, and detail scales

Within a single group, the base, macro, and detail textures are three tiling layers at different scales that are sampled and combined per pixel. The simplest way to think about them is as low-, medium-, and high-frequency versions of the same surface:

Scale Typical mapping size (*TexSize) Repetition When it is sampled Role
Base Large - often whole-terrain (≈ 128–2048 m) Low (large tile, sometimes unique) Always, at every distance. The foundation. Sets the broad, large-scale look and the group’s actual value.
Macro Medium (≈ 30–80 m) Medium Within macroDistances, capped at 1000 m. Mid-scale variation that breaks up the base’s repetition at medium/long range.
Detail Small (≈ 2–8 m) High (small tile) Within detailDistances, capped at 250 m. Fine, sharp close-up detail.

Base is the primary layer and the only one that defines an absolute value (the others modify it). In official levels the base is usually a single large texture mapped across the whole terrain - often the same t_terrain_base_* set shared by every material (for example West Coast USA maps it at 2048 m for all materials). Because one tile spans the whole terrain it has no visible repetition, but it is soft / low-resolution up close - which is exactly what the detail layer fixes.

Macro is a second layer at a smaller mapping size (typically 30–80 m). Its job is to hide the fact that the base is repeating: large, soft patches of color or brightness variation that you notice across a hillside but not on a single tile. It is most useful at medium and long range and can be faded with macroDistances.

Detail is a third layer at a very small mapping size (typically 2–8 m), so it repeats often and stays crisp right under the camera or vehicle (individual blades, pebbles, fine normal bumps). Because high-frequency tiling becomes both obvious and expensive at distance, detail is faded out by detailDistances and is never sampled past 250 m.

What the mapping size means

*BaseTexSize, *MacroTexSize, and *DetailTexSize are world distances in meters for one tile of the texture - not the pixel resolution.

  • Larger size → bigger tile → fewer repeats → broad look, little visible tiling, but blurrier up close.
  • Smaller size → smaller tile → more repeats → crisp up close, but tiling becomes obvious if used at distance.

In official content the base is usually the whole-terrain size, macro is around 30–80 m, and detail is around 2–8 m. (If a size field is left unset, the engine defaults are 256 / 60 / 2 m for base / macro / detail.)

Why three scales

A single tiled texture cannot look good at every distance: make the tile large and it is blurry up close; make it small and it tiles visibly across the terrain. Splitting the surface into base + macro + detail lets the terrain stay sharp where the camera is close (detail), varied at mid range (macro), and free of obvious repetition far away (base) - all at the same time.

At a glance, by distance:

far   : base (+ macro)         -> broad, no obvious tiling
mid   : base + macro           -> base broken up by variation
near  : base + macro + detail  -> full crispness

How to use them

  • All scales are required: every group must have a base, macro, and detail texture assigned. The terrain material editor reports an empty slot as a validation error and will not save the material until all five groups have all three textures.
  • Author the base color base as the real surface color; author macro/detail textures around mid-gray so they only add variation (see How the scales combine ).
  • To effectively “disable” a macro or detail layer in a group, still assign a texture but make it neutral - flat mid-gray for color/data, or a flat normal - so it contributes nothing.
  • Use detail to add close-up crispness, and keep its distances short for performance.
  • Use macro to kill visible tiling on large open areas; soft, low-contrast macro textures usually look best.

How the scales combine

For the color and data groups (base color, roughness, AO, height), the base texture sets the value and macro/detail are added as signed overlays around mid-gray:

final = base + (macro  - 0.5) * 2 * macroStrength
             + (detail - 0.5) * 2 * detailStrength

Because of this, macro and detail textures should be authored around mid-gray (0.5): gray = no change, lighter areas brighten the base, darker areas darken it. For the normal group, the base/macro/detail normals are blended together instead of added.

Height-based layer blending

This is the most important behavior to understand. When two terrain materials overlap (where you paint one over another), the engine does not simply cross-fade them. It compares the height texture of each layer per pixel and lets the higher one show through.

This produces natural transitions - for example gravel poking through where it sits “above” sand, instead of a soft, blurry seam.

To use it: author the height map so the parts that should appear first in a blend (pebbles, raised pattern, rocks) are brighter, and the recessed parts (mortar gaps, sand pockets) are darker.

Tuning distance, strength, and attenuation

These fields control how macro and detail fade with camera distance - they matter for both looks and performance.

  • macroDistances / detailDistances - [startFadeIn, near, far, endFadeOut] in meters (ascending). The contribution fades in over startFadeIn → near, is full between near and far, then fades out over far → endFadeOut.
  • *MacroStrength / *DetailStrength - [near, far] intensity (0–1) of the overlay, so you can make a layer strong up close and weaker far away.
  • macroDistAtten / detailDistAtten - [near, far] (0–1) controlling how much the contribution drops at the fade edges (1 = fades fully to zero, 0 = stays).
Independently of these curves, the renderer hard-stops detail sampling beyond 250 m and macro beyond 1000 m. Setting endFadeOut higher than that does not extend them - official content often leaves endFadeOut large (e.g. 3000) and relies on these caps, controlling the real fade with the far value instead.

Typical values seen in official levels:

Field Common value Meaning
macroDistances [0, 10, 100, 3000] Macro fully visible out to ~100 m, present until the 1000 m cap.
detailDistances [0, 0, 30, 60] (or large endFadeOut) Detail fully visible to ~30 m, gone well before the 250 m cap.
*MacroStrength [0.2, 0.4] (color), [0.5, 0.6] (normal) Subtle on color, stronger on normal.
*DetailStrength [0.3, 0] (color), [0.8, 0.15] (normal) Strong up close, fading to nothing far away.
macroDistAtten / detailDistAtten [1, 1] Fade in from zero and out to zero.

Practical guidance:

  • Keep detail far short (often 20–50 m). It is high-frequency, only useful up close, and stopping it early also saves performance.
  • Use macro to break up obvious base-texture tiling at medium and long range.
  • height macro/detail strengths are usually left at 0 - height drives layer blending , so you rarely want macro/detail modulating it.

Slope projection and depth

  • useSideProjection - projects the texture onto vertical faces instead of straight down, so cliffs and steep rock do not look stretched.
  • parallaxScale - adds a parallax / self-occlusion depth effect from the height/normal data.
useSideProjection and parallaxScale belong to the classic v1 terrain path. The v1.5 renderer (and the v1.5 material editor) do not use them, so they have no effect on a terrain that has a materialTextureSet assigned.

Groundmodel and annotation

  • groundmodelName - links the painted area to a groundmodel, which defines the physics surface: friction, tyre particles, skid sounds, rolling resistance, and so on. Match it to the look (GRASS, ROCK, ASPHALT, …). See Groundmodels .
  • annotation - a semantic/debug class (e.g. GRASS, NATURE) used by the annotation render pass for tools such as semantic segmentation and sensors. Defaults to NATURE.

Both fields are used by classic v1 and v1.5 materials.


TerrainMaterial v1.5 fields

A v1.5 TerrainMaterial is defined by per-group texture fields plus a few material-level fields. For what each field does and how to use it, see How v1.5 terrain materials work ; this section is a quick name/type lookup.

Values are stored as JSON numbers and arrays: single values as numbers (512, 0.3) and vectors as arrays ([0.3, 0], [0, 10, 100, 3000]).

Texture group fields

Each of the five groups - baseColor, normal, roughness, ao, height - exposes the same fields. Build the field name from the group and the scale (Base, Macro, Detail):

Field pattern Type Notes
<group><Scale>Tex path Source texture for that group/scale, e.g. baseColorBaseTex, normalDetailTex.
<group><Scale>TexSize number World mapping size in meters (typically whole-terrain for base, 30–80 for macro, 2–8 for detail).
<group>MacroStrength number[2] [near, far] Macro overlay intensity, 0..1.
<group>DetailStrength number[2] [near, far] Detail overlay intensity, 0..1.

For example, the baseColor group expands to baseColorBaseTex, baseColorMacroTex, baseColorDetailTex, baseColorBaseTexSize, baseColorMacroTexSize, baseColorDetailTexSize, baseColorMacroStrength, and baseColorDetailStrength.

Material-level fields

Field Type Notes
internalName string Unique name referenced by the .ter material list.
macroDistances number[4] [startFadeIn, near, far, endFadeOut] Distance fade curve (m) shared by all macro textures. Sampling stops at the 1000 m cap. Typical [0, 10, 100, 3000].
detailDistances number[4] [startFadeIn, near, far, endFadeOut] Distance fade curve (m) shared by all detail textures. Sampling stops at the 250 m cap. Typical [0, 0, 30, 60].
macroDistAtten number[2] [near, far] How much macro fades at the fade edges, 0..1. Typical [1, 1].
detailDistAtten number[2] [near, far] How much detail fades at the fade edges, 0..1. Typical [1, 1].
groundmodelName string Physics groundmodel (friction, particles, sounds).
annotation string Semantic/debug class for the annotation pass. Default NATURE.
useSideProjection bool Classic v1 only - ignored by the v1.5 renderer.
parallaxScale number Classic v1 only - ignored by the v1.5 renderer.

Example v1.5 TerrainMaterial

TerrainMaterial definitions are stored as a name-keyed JSON object, one key per material:

{
  "grass01": {
    "class": "TerrainMaterial",
    "internalName": "grass01",
    "annotation": "GRASS",
    "groundmodelName": "GRASS",

    "baseColorBaseTex": "/levels/example/art/terrains/t_terrain_base_grass_b.png",
    "baseColorBaseTexSize": 512,
    "baseColorMacroTex": "/levels/example/art/terrains/t_macro_grass_b.png",
    "baseColorMacroTexSize": 64,
    "baseColorMacroStrength": [0.1, 0.2],
    "baseColorDetailTex": "/levels/example/art/terrains/t_grass_b.png",
    "baseColorDetailTexSize": 4,
    "baseColorDetailStrength": [0.25, 0],

    "normalBaseTex": "/levels/example/art/terrains/t_terrain_base_grass_nm.png",
    "normalBaseTexSize": 512,
    "normalMacroTex": "/levels/example/art/terrains/t_macro_grass_nm.png",
    "normalMacroTexSize": 64,
    "normalMacroStrength": [0.2, 0.4],
    "normalDetailTex": "/levels/example/art/terrains/t_grass_nm.png",
    "normalDetailTexSize": 4,
    "normalDetailStrength": [0.7, 0.15],

    "roughnessBaseTex": "/levels/example/art/terrains/t_terrain_base_grass_r.png",
    "roughnessBaseTexSize": 512,
    "roughnessMacroTex": "/levels/example/art/terrains/t_macro_grass_r.png",
    "roughnessMacroTexSize": 64,
    "roughnessMacroStrength": [0.15, 0.8],
    "roughnessDetailTex": "/levels/example/art/terrains/t_grass_r.png",
    "roughnessDetailTexSize": 4,
    "roughnessDetailStrength": [0.3, 0.3],

    "aoBaseTex": "/levels/example/art/terrains/t_terrain_base_grass_ao.png",
    "aoBaseTexSize": 512,
    "aoMacroTex": "/levels/example/art/terrains/t_macro_grass_ao.png",
    "aoMacroTexSize": 64,
    "aoDetailTex": "/levels/example/art/terrains/t_grass_ao.png",
    "aoDetailTexSize": 4,

    "heightBaseTex": "/levels/example/art/terrains/t_terrain_base_grass_h.png",
    "heightBaseTexSize": 512,
    "heightMacroTex": "/levels/example/art/terrains/t_macro_grass_h.png",
    "heightMacroTexSize": 64,
    "heightDetailTex": "/levels/example/art/terrains/t_grass_h.png",
    "heightDetailTexSize": 4,

    "macroDistances": [0, 10, 100, 3000],
    "detailDistances": [0, 0, 30, 60],
    "macroDistAtten": [1, 1],
    "detailDistAtten": [1, 1]
  }
}
  • All five groups must have their base, macro, and detail textures assigned - the terrain material editor will not save a material with an empty texture slot.
  • The editor also writes an auto-generated persistentId and uses a <name>-<persistentId> JSON key; both are omitted here for clarity.
  • height strengths are left at their defaults so height only drives layer blending.

TerrainMaterialTextureSet

TerrainMaterialTextureSet defines the expected texture array sizes used by the v1.5 terrain material renderer.

All terrain materials painted on a terrain are packed into a small set of shared GPU texture arrays (one slice per material), and a texture array requires every slice to have the same dimensions. The TerrainMaterialTextureSet declares those dimensions for the base, macro, and detail slots. This is why every texture you assign to a given slot must be exactly the declared size - a mismatch cannot be packed and the material falls back to the warning texture.

There is normally one TerrainMaterialTextureSet per level (named like <levelName>TerrainMaterialTextureSet), shared by all terrain materials in that level.

A TerrainBlock references it using:

"materialTextureSet": "myTerrainTextureSet"

Important fields (pixel size as [width, height]):

Field Description Typical
baseTexSize Pixel size of the base texture array. [512, 512][4096, 4096] (higher when the base is a unique whole-terrain map)
macroTexSize Pixel size of the macro texture array. [1024, 1024]
detailTexSize Pixel size of the detail texture array. [1024, 1024]

Example (from an official level layout):

{
  "templateTerrainMaterialTextureSet": {
    "class": "TerrainMaterialTextureSet",
    "name": "templateTerrainMaterialTextureSet",
    "baseTexSize": [512, 512],
    "macroTexSize": [1024, 1024],
    "detailTexSize": [1024, 1024]
  }
}

The renderer uses these sizes when packing terrain material textures into GPU texture arrays.

All textures assigned to the same slot type must match the expected size defined by the TerrainMaterialTextureSet. For example, all base textures must match baseTexSize.

Terrain texture packing

In the v1.5 terrain material path, source textures are packed into generated cached textures and copied into terrain texture arrays.

Generated cache files can be stored under paths like:

/temp/art/terrainMaterialCache/<hash>.dds

The hash is based on the source texture paths, so the same texture combination can be reused.

The renderer packs terrain data into two main texture groups:

Base color group

This group combines:

  • Base color RGB
  • Ambient occlusion

Typical channel usage:

R/G/B = base color
A     = ambient occlusion

Normal/data group

This group combines:

  • Roughness
  • Height
  • Normal data

Typical channel usage:

R = roughness
G = height
B/A = normal-related data
The exact packed texture is generated automatically by the engine. Authors should provide valid source textures with matching sizes and correct color/data usage.

If a source texture is missing, has the wrong size, or does not contain the expected channel, terrain material packing can fail or produce warning material output.

Source texture requirements

When authoring v1.5 terrain textures:

  • Provide PNG source textures.
  • Texture pixel dimensions must be power-of-two and must exactly match the corresponding TerrainMaterialTextureSet slot size (baseTexSize, macroTexSize, or detailTexSize).
  • Base color textures must be in sRGB color space. Normal, roughness, ambient occlusion, and height textures must be in linear space.

Recommended source formats per slot:

Slot group Color space Recommended source format
Base color sRGB R8G8B8 / R8G8B8A8
Normal Linear R8G8B8 / R8G8B8A8
Roughness / AO / Height Linear R8 (grayscale)

The World Editor terrain material library validates these rules and reports size mismatches, missing textures, and unexpected formats.


Terrain rendering

The terrain renderer divides terrain into cells and builds a quadtree.

Important value:

minimum terrain cell size = 64

The cell system is used for:

  • Culling
  • LOD selection
  • Bounds
  • Material tracking
  • Shadow rendering
  • Reflection rendering
  • Prepass / G-buffer / annotation passes

Terrain cell data is uploaded to GPU buffers for rendering.


Terrain LOD

Terrain LOD is based on screen error and cell distance.

Relevant fields/preferences:

Field Description
screenError Terrain screen error setting.
$pref::Terrain::lodScale Global terrain LOD scale.
$pref::Terrain::detailScale Global terrain detail distance scale.

Terrain skirts

Terrain cells generate extra skirt geometry around cell edges.

Skirts help hide cracks between terrain cells when different LOD levels are used.

This geometry is generated automatically and is not stored in the .ter file.


Empty terrain / holes

The layer index value 255 (U8_MAX) marks empty terrain.

Empty terrain affects:

  • Rendering
  • Collision
  • Terrain cell primitive generation
  • SDF/boolean terrain operations

The renderer can skip empty terrain squares by generating a custom primitive buffer for affected cells.


Collision

Terrain collision is built from the heightmap.

When terrain height changes:

  • Grid data is updated
  • Bounds are recalculated
  • Render cells are updated
  • Physics collision can be queued for rebuild

Terrain collision is used by:

  • Vehicles
  • Props
  • Raycasts
  • Ground detection
  • AI/navigation systems

Importing terrain

Terrain can be imported from a heightmap and opacity/layer maps.

Requirements:

  • Heightmap must be square
  • Heightmap resolution must be power of two
  • Heightmap size must be between 128 and 8192
  • Opacity/layer maps must match the heightmap size
  • Number of material names must match number of opacity layers

Heightmap import

Supported input heightmap behavior:

  • R16 heightmaps are read directly as 16-bit height data.
  • Other image formats are converted from 8-bit values to the full u16 range.

8-bit conversion:

storedHeight = pixelValue / 255 x 65535

Opacity/layer import

Import uses opacity layers to build the terrain layer map.

Each opacity layer can come from a texture channel:

R
G
B
A

For each terrain sample, the material layer with the highest opacity value wins.

Pseudocode:

for each sample:
    bestLayer = 0
    bestValue = 0

    for each opacity layer:
        if opacityValue >= bestValue:
            bestLayer = layer
            bestValue = opacityValue

    layerMap[sample] = bestLayer

Hole map import

An optional hole map can mark terrain samples as empty.

If the hole map value is 0xFF, the layer map value becomes:

255

which means empty terrain.


Y-axis flipping

Import supports a flipYAxis option.

If disabled, the importer flips input data vertically while copying it into terrain memory.

Use this if the imported terrain appears vertically flipped.


Creating terrain

When creating a new terrain, the engine:

  1. Chooses a unique .ter filename.
  2. Creates a TerrainFile.
  3. Adds the initial material.
  4. Rounds terrain resolution up to the next power of two.
  5. Initializes the heightmap and layer map.
  6. Saves the .ter.
  7. Writes the .terrain.json metadata file.
  8. Creates/registers a TerrainBlock.

New terrain height is initialized above zero so the editor has room to excavate.


Minimal TerrainBlock example

{
  "class": "TerrainBlock",
  "name": "theTerrain",
  "terrainFile": "/levels/example/theTerrain.ter",
  "materialTextureSet": "exampleTerrainMaterialTextureSet",
  "squareSize": 1,
  "maxHeight": 2048,
  "screenError": 16,
  "castShadows": true,
  "position": [-1024, -1024, 0],
  "rotationMatrix": [1, 0, 0, 0, 1, 0, 0, 0, 1]
}

Minimal .terrain.json example

{
  "version": 9,
  "datafile": "/levels/example/theTerrain.ter",
  "heightmapImage": "/levels/example/theTerrain.terrainheightmap.png",
  "size": 1024,
  "binaryFormat": "version(char), size(unsigned int), heightMap(heightMapSize * heightMapItemSize), layerMap(layerMapSize * layerMapItemSize), layerTextureMap(layerMapSize * layerMapItemSize), materialNames",
  "heightMapSize": 1048576,
  "heightMapItemSize": 2,
  "layerMapSize": 1048576,
  "layerMapItemSize": 1,
  "materials": [
    "grass",
    "rock"
  ]
}

Best practices

  • Keep terrain resolution power-of-two.
  • Use terrain sizes between 128 and 8192.
  • Use squareSize to control world scale.
  • Use maxHeight to control vertical range.
  • Do not scale the TerrainBlock object.
  • Keep terrain material counts manageable.
  • Use valid TerrainMaterial internal names.
  • Keep opacity/layer maps the same size as the heightmap.
  • Use 255 layer values only for intended holes.
  • Resave old terrain files to upgrade them.
  • Test terrain collision after major edits.
  • Use cooked DDS textures for legacy terrain materials.
  • Keep terrain material texture set sizes consistent.

Common issues

Terrain does not load

Possible causes:

  • Invalid terrainFile path
  • Missing .ter file
  • Terrain file version is newer than the engine supports
  • Corrupted binary data

Terrain appears with warning material

Possible causes:

  • Material name in .ter cannot be resolved
  • TerrainMaterial definition is missing
  • Terrain material texture path is invalid
  • materialTextureSet cannot be found

Terrain height is wrong

Check:

  • maxHeight
  • squareSize
  • Terrain object position
  • Heightmap import scale
  • Y-axis flip setting

Terrain painting looks wrong

Possible causes:

  • Layer map index does not match material order
  • Material order changed
  • Opacity maps are wrong
  • Texture cache is outdated
  • Source textures have incorrect size

Terrain has holes

Layer map values may be 255, which marks terrain as empty.

Terrain collision is outdated

Rebuild terrain collision or save/reload after major height edits.

Import fails

Check that:

  • Heightmap is square
  • Heightmap resolution is power-of-two
  • Heightmap size is between 128 and 8192
  • Opacity maps match the heightmap size
  • Number of materials matches number of opacity layers

Summary

BeamNG terrain uses:

  • A binary .ter file for heightmap, layer map, and material names
  • A .terrain.json metadata file for documentation/tooling
  • A TerrainBlock scene object to place the terrain in the level
  • Separate TerrainMaterial definitions for visual and physical material data

The .ter file is compact and fast to load, while the .terrain.json file helps tools understand the terrain data layout.

Last modified: July 23, 2026

Any further questions?

Join our discord
Our documentation is currently incomplete and undergoing active development. If you have any questions or feedback, please visit this forum thread.