Techmo2/VS-Terrain-Diffusion

An implementation of the terrain diffusion generator for vintage story

C#

0

65 commits

updated Oct 6, 2026

See the code

README

VS Terrain Diffusion — a Vintage Story mod

Generates Vintage Story worlds with Terrain Diffusion, a neural model trained on real Earth topography and climate (SIGGRAPH 2026): continents, drainage networks, fjords, plateaus and mountain ranges with the structure of real terrain, and a real climatology alongside the heightmap. Terrain, temperature, rainfall, forests, the surface you walk on and the seasons all come from the same model.

Contents — Installing · Creating a world · What the mod changes · How it works · Commands · Configuration · Building

Installing

Drop the release zip in your Mods folder. The models download themselves on first launch.

  • Vintage Story 1.22 (targets .NET 10, same as the game)
  • ~2.2 GB of disk for the selected model files, fetched once, plus the optimised-graph cache
  • ~3 GB of RAM while the models are resident
  • A GPU is strongly recommended. CPU inference works but is roughly 10-20x slower.
PlatformDevice used automaticallyNotes
Linux + NVIDIACUDANeeds a system CUDA 12 or 13 runtime and cuDNN 9
WindowsDirectMLAny modern GPU; no extra install
macOS (Apple)CoreMLNo extra install
Anything elseCPUWorks, but slow

The matching ONNX Runtime native library is downloaded on first use too (a few MB for CPU/DirectML, ~300 MB for CUDA). Nothing is installed by hand.

Server-side. Clients may install it to keep their weather display in step with the server, but vanilla clients can join normally.

The first world sits on the loading screen until everything is fetched; the screen reports each download, and Logs/server-main.log has the detail.

Creating a world

The mod adds a Terrain Diffusion tab to the Customize screen. Each setting is saved with the world; a dedicated server sets them under WorldConfig.WorldConfiguration in serverconfig.json, and /worldconfig <code> <value> changes one later.

SettingCodeDefaultWhat it does
Terrain DiffusionterraindiffusionEnabledonTurn off to fall back to vanilla terrain, keeping the mod installed.
ResolutionterraindiffusionScale15 mReal-world metres per block, horizontally and vertically.
Vertical exaggerationterraindiffusionVerticalExaggeration1xMultiplies terrain height. 1x is true scale.
Sea levelterraindiffusionSeaLevelvanilla128 fixes the sea at Y 128, so every block a taller world adds goes above it.
ClimateterraindiffusionClimatemodelWhether the model drives climate as well as terrain.
Terrain intensityterraindiffusionCoarsePoolingoffPacks 2x or 4x the landscape into the same distance: ranges, valleys and coasts closer together, relief steeper.
Intensity modeterraindiffusionCoarsePoolModeaverageextreme keeps each block's highest ground and deepest valley floor: taller peaks, deeper cuts, less realistic.
Detail redrawterraindiffusionDetailRedraw35Hundredths. How much of the detail model's first draw is redrawn. 70–100 varies coast shapes and small valleys by 10–17 m on average.
Height noiseterraindiffusionHeightNoise0Random height per large-scale cell (~8 km) before detail is drawn, in hundredths of a sqrt-metre: 200 ≈ ±100 m at 600 m. None at the coast.
Altitude coolingterraindiffusionAltitudeCooling100%Every 100% above 100% takes another 6.5 °C off per km of height, on the ground and in the air, on top of the model's own rate: lower treelines and snowlines. Sea level is unchanged.
Forest densityterraindiffusionForestDensity100%Scales the forest cover the climate implies. Trees on the ground go as its square — see below.
Shrub densityterraindiffusionShrubDensity100%Scales shrub cover the same way.
River valley depthterraindiffusionRiverValleyDepth30%With the Rivers mod, how far the land is lowered along rivers so the model draws valleys for them. 100% drowns a fifth of each corridor into inlets.

With sea level at 128 the terrain, climate, soil bands, trees and weather keep their height above the sea: a 512-block world is laid out like a vanilla world 675 blocks tall, with 128 blocks of rock and ocean below the sea instead of 220. Vanilla data written as fractions of world height (block layers, lake and ocean beds, tree altitude bands) is moved to the same height above the sea in that equivalent world. Rivers resets sea level to vanilla's as it starts; that is overridden.

Terrain intensity is the reference implementation's coarse_pooling: the model's large-scale map is drawn as usual and each 2x2 or 4x4 block of it becomes one cell of the world. Measured over nine 15 km regions, land only:

SettingRelief (std)95th percentileMean grade90th percentile grade
off452 m1196 m17%39%
2x376 m1124 m20%46%
4x743 m2353 m23%46%
2x extreme926 m2934 m57%105%
4x extreme1298 m4172 m54%104%

The world's ocean map is followed as closely at every setting (94-95% of coarse cells at 50% landcover); extreme adds a few points of land along coasts. 4x and extreme need a tall world to keep their peaks at true scale.

The same tab holds the inference settings. worldGen.climateMode, worldGen.scaleOverride and worldGen.verticalExaggerationOverride in the mod config override the first three on every world.

Vanilla settings, and what becomes of them:

SettingDefaultWhat it does here
World height—Set this to 1024. Vanilla's default is far too short for real mountains.
Landcover97.5%Honoured — it decides where the sea goes. See Land and sea.
Landcover scale (oceanscale)500%Honoured — it decides how big the oceans are. Same section.
Global temperaturenormalHonoured — the model draws a world that cold or that hot. See below.
Global precipitationnormalHonoured — same, for rainfall.
Polar–equator distance100 000 blocksHonoured — it puts the tropics, the deserts and the ice where the game says. See Latitude.
Starting climatetemperateHonoured, two ways: it sets which latitude the map centre sits at, and the spawn search finds land there. See below.
Forestation & shrubsnormalHonoured — vanilla applies it on top of the model's forest map, unchanged.
Climate distributionrealistic"Patchy" turns off the latitude bands, since a patchy world has no latitude.
Geologic activityrareHonoured — that byte of the climate map is still vanilla's.
Landform scale100%Almost nothing. See below.
Upheaval rate30%Nothing. See below.

Everything not listed — ores, caves, temporal stability, world size — is untouched and behaves exactly as it does in an unmodded world.

Two settings the mod cannot honour

Both configure maps that only fed vanilla's terrain generator, which this mod replaces.

  • Upheaval rate: no effect. The map is still generated and saved; nothing reads it.
  • Landform scale: no effect on terrain. GenDungeons still reads the landform map to place dungeons needing flat ground, so one can land on what the map calls a plain and the model made a hillside. Two shipped tiled dungeons are affected.

worldGen.slopeDetailStrength and Vertical exaggeration are the nearest relief controls.

Land and sea

The world's ocean map decides where the coastline is, and the model decides what it looks like. Before generating anything the mod reads Vintage Story's ocean map — the one "Land cover" and "Ocean scale" configure — and feeds it to the model as conditioning, so a world set to 50% land gets 50% land, with the shelf, the fjords and the mountains behind them drawn from real terrain.

It reads whichever ocean map is installed — Continental World's, for instance — and leaves it untouched afterwards.

  • Conditioning is soft. The coast wanders around the one it was given rather than tracing it, which is what makes it look natural. Sea fraction comes out within ~4 points of the setting; column by column 88% agree, nearly all disagreement within one cell of a coastline.
  • Resolution floor. One conditioning pixel spans 512 blocks at the default resolution, so anything smaller fills in as land. Low "Ocean scale" worlds lose their smallest islands and lakes.

Vanilla's defaults (97.5% land, 500% ocean scale) give a nearly unbroken continent. 40–60% land is where real coastlines start to appear.

Set worldGen.oceanMap to "output" for the reverse arrangement: the model invents its own continents from real-world terrain, ignoring both world settings, and the ocean map is rewritten to match it.

A hotter, colder, wetter or drier world

Vanilla multiplies these onto the climate map it drew at random. This mod hands them to the model, so a "Semi-Arid" world is drawn semi-arid: the forests, soil, snow line and seasons all follow. Rainfall is scaled outright; temperature moves the world along the real distribution instead, since nowhere on Earth has a mean above 30 °C. What that cannot reach is applied to the output afterwards as vanilla does.

Measured on two seeds across both settings' full range: temperature within 2 °C up to "Hot", which overshoots ~5 °C for want of anywhere hotter to draw from; rainfall within 10% in a region of ordinary wetness, less where it is already very wet or very dry. "Very hot" and "Scorching hot" saturate the game's climate scale, as in an unmodded world — and leave the spawn search no temperate land, so it settles for the coolest thing going.

Set worldGen.globalClimateStrength to 0 to apply both to the model's output instead.

Latitude

Vanilla's latitude is a straight line from +40 °C to −20 °C with no rainfall gradient. This mod conditions the model on a real zonal climate instead — equatorial rain belt, subtropical deserts at 25°, mid-latitude storm track at 50°, dry cold caps — so a tropical belt is drawn tropical, and coasts, rain shadows and mountains all happen within the band.

Where each block sits between equator and pole is the game's answer, read from polarEquatorDistance, so the snow line agrees with day length and the midnight sun. The phase comes with it, which is how "Starting climate" lands the map centre at the right latitude.

Measured over equator-to-pole transects on three seeds: temperature ~1 °C warm on average, 2 °C either way in a belt; from ~80° poleward the model runs out of world (it has never seen a mean below −14 °C) so the last stretch is added afterwards and the poles read −20 °C. Rainfall lands on the band in the median and swings a factor of two either side — geography, not error. A 15 000-block polar distance still tracked the bands to 1.5 °C.

Heading north or south now changes the climate; east or west mostly does not. 200k–400k gives long belts, 15k–25k puts tropics and ice within a day's walk. The hemispheres get opposite years, as the game's calendar already does (worldGen.seasonHemispheres).

Set worldGen.latitudeStrength to 0 for an unrooted world; values between weaken the gradient without moving it. Bands are off on a "Patchy" world.

Starting climate

The bands mean what they do in an unmodded world, as annual mean temperature: hot 28–32 °C, warm 19–23 °C, temperate 6–14 °C, cool −5 to 1 °C, icy −15 to −10 °C.

With latitude bands on, the map centre already sits at the right latitude and the search only has to find land there — usually a few hundred blocks. With latitudeStrength at 0 it hunts for a matching climate wherever the model put one, and cold is found on high ground rather than far north.

It prefers to travel east or west, because distance along Z buys midnight sun past the polar distance. It stops at the first match, so most worlds spawn within a few thousand blocks in under a second; a distant band — usually "hot" — takes longer, and on CPU inference longer still. If the seed has no such land in range the log says so and you spawn at the closest temperature found.

Set worldGen.startingClimateSearch to false to spawn on the nearest land whatever its climate.

What the mod changes

  • Terrain pass — GenTerra's chunk handler fills columns from the diffusion heightmap. If another mod already replaced terrain generation, the model supplies heights to it; see Other terrain mods.
  • Climate map — sea-level temperature and annual rainfall from the model, with the game's altitude correction replaced by the real lapse rate. The geologic activity byte stays vanilla's.
  • Global temperature and precipitation — conditioning rather than post-processing.
  • Latitude — the game's own latitude, from polarEquatorDistance, conditioned in as a real zonal climate in place of vanilla's straight line from +40 °C to −20 °C.
  • Forest and shrub maps — cover derived from the model's moisture and growing season.
  • Ocean map — read, not written: it is what the terrain is conditioned on.
  • Surface pass — slopes too steep for soil are scoured back to bare rock (vanilla upholsters cliffs in eight blocks of dirt), and ground whose warmest month stays below freezing is capped with glacier ice.
  • Seasons — the year swings on the model's seasonality rather than on latitude alone, opposite sides of the equator included.
  • Spawn — moved to solid ground in the world's chosen starting climate.
  • Surface block layer altitudes — only when terrain is vertically exaggerated; at true scale vanilla's bands already line up.
  • Nothing else. The landform and upheaval maps are still generated and still ignored, because the generator that read them is gone — see Two settings the mod cannot honour. Rock strata, ores, caves, rivers, ponds, ruins, traders and temporal stability are vanilla, running unchanged on top.

Other terrain mods

Mods that only supply a map — Continental World's ocean map, for instance — need nothing special: the mod reads whatever map is installed and conditions the model on it.

sneeze's Rivers routes its rivers from the coast the model actually draws, not the ocean map, and starts each river in open sea, so rivers reach the water (95% within their first three nodes, against 75% from the map). Rivers caches each world's zones under RiverCache/, so a world keeps the coast its rivers were first routed from.

Algernon's Terrain Sampler answers other mods' height questions from the model instead of its own copy of vanilla's terrain: exactly what gets generated in full mode, or a blurred coarse-model answer in coarse mode (terrainSamplerHeight). Its climate and vegetation already come from this mod's map layers.

A mod that replaces terrain generation itself cannot layer with this one: two generators filling the same column give the union of both landscapes. So whoever generates terrain gets handed the model's heights and does the filling.

Algernon's Watersheds is supported this way. It disables vanilla GenTerra and fills every column itself, so this mod stops generating terrain and instead answers every height question Watersheds asks: the height its analysis is built on, the height a stream's profile is laid against, the height after a stream has cut in, and which blocks are solid. Answering all of them matters — a stream's water surface and its bed are worked out separately, so one height left coming from Watersheds' own landscape strands water in the air. Its ridge and gully erosion filter is switched off for these worlds: it cuts valley detail into fractal noise, the model's landscape already has erosion, and it is computed inside two of those height answers. Everything downstream — stream water, banks, rapids, groundwater, block layers — is Watersheds' own.

  • World creation takes longer. The analysis samples heights kilometres around spawn, all from the model, so the first load spends a few minutes on tiles it will not visibly use. One-time per area, and cached.
  • Watersheds decides where streams go. It will not path one across terrain rougher than SmallChunkRoughnessThreshold in ModConfig/Watersheds/TerrainAnalysisConfig.json (2 blocks RMSE by default). Ordinary modelled landscape measures 0.0–0.8; genuinely broken ground gets no small streams. Raise the threshold if you want them anyway.
  • Streams need somewhere to drain. At vanilla's default land cover there is almost no ocean to reach, so almost no streams. That is Watersheds' behaviour, not this mod's.

Stream maps live in a database beside the save, so a world explored with an older version of this mod has streams plotted against the wrong landscape: /watersheds clearstreammaps and regenerate, or start a new world.

If Watersheds updates in a way this cannot reach into, the mod says so and takes itself out of the world rather than generating a broken one.

How it works

Scale, and why the world needs to be tall

By default a block is as tall as it is wide — 15 m in every direction at the default resolution — so a 2 000 m massif is 133 blocks of climbing and every slope has its real-world grade.

Real mountains need room. The model's land runs to about 3 000 m at the 95th percentile and 5 000 m at the extreme, which at 15 m per block is 200 and 333 blocks above sea level:

World heightBlocks above seaTerrain held at true scale
256145up to ~1 900 m
512289up to ~3 700 m
1024578up to ~7 400 m

Past that the mapping bends towards the ceiling on u / (1 + u) rather than clipping, so summits round off instead of shearing into mesas — at the cost of the highest ground's faithfulness.

If a tall world is not an option, worldGen.heightMode: "auto" surveys the region around spawn once and stretches the metre-to-block mapping so its peaks reach near the ceiling. The landscape uses the full height at the cost of exaggerated relief — a gentle region might come out at 4x. The measurement depends only on the seed and is stored in the save.

Resolution is also the main performance dial: at 30 m per block you cross a continent in an afternoon, at 5 m the same mountain is four kilometres of walking.

Climate

The model predicts four WorldClim bioclimatic variables everywhere it predicts elevation:

VariableWhat it is
BIO1annual mean temperature, °C
BIO4temperature seasonality — the spread of monthly means
BIO12annual precipitation, mm
BIO15precipitation seasonality — how unevenly it falls

A real climatology, with maritime coasts, continental interiors, rain shadows and altitude already in it — but nothing in the model knows which way is north, so the latitude bands supply that axis. globalTemperature and globalPrecipitation condition the bands themselves, so a "Snowball earth" world is one whose every band sits a quarter of the way up the scale.

From bioclimate to what the game reads

800 mm of rain is generous in Lapland and semi-arid in the Sahel, so the mod derives potential evapotranspiration, an aridity index and a growing season, and keys everything off those. The formulas are ported from the reference implementation's biome classifier.

Rainfall is a quantile map, not a physical conversion. Vanilla draws its 0-255 byte uniformly and every threshold reading it was tuned against that spread, so a physical quantity fed straight in makes the world read as desert. The model's tree moisture goes through its own distribution instead. worldGen.rainfallBasis: "precipitation" maps raw millimetres.

Forest and shrub cover come from the same moisture, scaled by growing season and cut to zero on ground too steep for soil. Vanilla's MapLayerWobbledForest computes 128 - rain * temp / 65025, a product that never exceeds 1, so its forest density is pure noise with no relation to climate; woodland in the foothills and nothing above the treeline are new behaviour. That noise is still used for the one thing it is good at, patchiness: where vanilla's map is open, the model's tree cover is thinned by forestClearings, so wet country gets fields and glens instead of one unbroken wood. Shrub cover is thinned the same way by vanilla's shrub map (shrubClearings). Everything that read that map still does, so animals and undergrowth follow the woods.

Temperature is stored as sea-level temperature, and the mod replaces the lapse rate applied on read. Vanilla's flat 0.157 °C per block is only right at about 24 m per block and over-cools mountains at anything finer; 6.5 °C/km reads back as the model predicted at any vertical scale.

Seasons

Vanilla takes the year's amplitude from latitude alone (|latitude| * 65 degrees), so the equator has no seasons and nothing else about a place matters. Here it comes from BIO4: a maritime coast and a continental interior at the same annual mean get completely different years. Precipitation seasonality does the same for rain, giving monsoon climates a real dry season.

Neither channel fits Vintage Story's packed climate integer, whose interpolator only touches the low three bytes, so they are map region mod data — saved with the region and, unlike its other maps, sent to clients. A vanilla client falls back to vanilla's seasons for display.

/tdiff season <x> <z> walks a year at a position and prints what it does.

Commands

/terraindiffusion, or /tdiff. Requires the controlserver privilege.

SubcommandWhat it shows
statusDevice, world scaling, tiles generated, average tile time, and where that time went: total model inference, its share of tile time, and a per-stage breakdown. A low inference share means something other than the GPU is the bottleneck.
gpulimit [percent]The share of the time inference is allowed to keep the device busy, and how much has been given up to the limit so far. With a percentage, sets it there and now, and saves it to the world.
mapThe debug map's address, and how many tiles it is holding.
hereElevation, slope, full bioclimate and derived cover where you stand, plus the latitude diagnostics below.
season <x> <z>The same diagnostics at a position, and the year's temperature and rainfall cycle there. Usable from a server console, where here is not.
column <x> <z>What actually got generated in a column, next to what the model said.

here and season share four climate diagnostics:

  • Latitude and Hemisphere — distance from the equator, which side, and the season the game's calendar reports there. Check this first if foliage or crops look out of step.
  • Sea-level temperature — the reading with altitude taken back out, and the fitted local lapse rate. The number to compare two places by. Slightly slower than the rest: it is a pipeline query.
  • Band temperature and Band precipitation — what the latitude band asked for and how far this column sits from it, plus Band offset applied when part of the band was added after the model ran.

A single column scatters several degrees either side of its band, which is the model's business; consistent drift over many columns is not.

Configuration

ModConfig/vsterraindiffusion.json, written on first start. CONFIG.md is the whole default file with a comment on every field; the tables below are the short version.

Optional: ConfigLib gives the same settings an in-game screen, editing this file in place rather than keeping a copy.

verboseInference takes effect on save; everything else is read when the world generator starts, so it needs a restart. Useful range below is where a setting does something sensible, not where it is legal — CONFIG.md lists the hard limits.

Inference

World settings, in the Terrain Diffusion tab (see Creating a world). The Customize screen lists every device and precision; one this machine cannot run stops the world from loading, naming the ones it can. Keep the device and the two precisions fixed after exploring a world: changing any of them can make newly generated terrain disagree slightly with existing chunks. Each world loads the runtime for its device in a worker process of its own, which exits when the world closes, so the next world can use a different device without restarting the game.

CodeDefaultUseful rangeMeaning
terraindiffusionInferenceDeviceautoauto cpu openvino cuda tensorrt-rtx directml coremlOpenVINO and TensorRT RTX are opt-in. OpenVINO, on 64-bit Linux, accelerates the decoder while leaving the large stages on ORT CPU. TensorRT RTX needs a GeForce RTX 30xx or newer on 64-bit Windows or Linux, fetches the NVIDIA runtime once (105 MB on Windows, 140 MB on Linux) and builds a cached engine per model; it is about 1.5x faster than CUDA on the same FP32 models and 2.4x with FP16. A compatible device whose runtime cannot be prepared runs that session on what auto would pick, or ORT CPU, and is logged; the setting is never changed. Only what the selected provider needs is fetched: TensorRT RTX skips the CUDA provider library it never loads, and cuda on Windows pulls the cuBLAS/cuFFT/NVRTC/cuDNN libraries it links against (~1 GB, once) only if no CUDA toolkit and cuDNN are installed; Linux and macOS use the system CUDA install.
terraindiffusionDecoderPrecisionfp32fp32 fp16 int8Select and automatically fetch only the matching decoder. FP16 is for GPU providers, INT8 for CPU/OpenVINO. Both change newly generated terrain slightly; a missing or invalid selected decoder stops model loading instead of silently changing precision.
terraindiffusionBasePrecisionfp32fp32 fp16The base model is most of a tile's work, so FP16 here is the biggest GPU win: on an RTX 3060, TensorRT RTX with FP16 base and decoder generated the same ten regions in 7.6 s against 19.0 s on CUDA FP32, for about 4 m mean elevation difference (CPU vs GPU is already ~2.6 m). Needs a GPU provider.
terraindiffusionGpuUtilizationPercent10040 – 100Share of the time world generation may keep the device busy. Lower it if generating chunks makes the game stutter; see Stuttering below. World generation slows by the reciprocal.
terraindiffusionOffloadModelsfalseon / offHold only one model on the GPU at a time, saving about 1 GB of VRAM. Generating a tile runs two or three of the models, so every tile then pays to rebuild a session for a graph of most of a gigabyte: measured on a 6 GB card it triples the average tile time. Turn on only if the models will not fit.
terraindiffusionModelLoadModefilefile memoryOpen model graphs from their files, or read them into RAM first (about 1 GB more).
terraindiffusionValidateModelHashestrueon / offVerify SHA-256 of existing model files on load. Off saves a few seconds of disk read.
terraindiffusionDownloadRuntimetrueon / offFetch the ONNX Runtime and, when selected, OpenVINO native libraries automatically. On 64-bit Windows this includes the Visual C++ runtime (6.8 MB, from Microsoft) when the machine's own is missing or older than 14.39.

Machine settings, in the mod config:

KeyDefaultUseful rangeMeaning
tileCacheMegabytes256128 – 1024Total decoded tensor-window cache across all pipeline stages.
latentBatchSize00 – 4Latent windows per base-model call. Zero chooses 1 on CPU and 4 on GPU.
terrainTileCacheMegabytes256128 – 1024Finished terrain tiles. Raise if you see thrash warnings.
terrainTileSizeBlocks00, 128 – 512Blocks generated per model invocation, a multiple of 32. Zero chooses 128 on CPU and 256 on GPU; larger values amortise the model better but make first-visit stalls longer.
debugMapPort0 (off)8088Serves the debug map on this port. 0 opens no port.
debugMapBindAddress127.0.0.1loopbackWhere the debug map listens. 0.0.0.0 publishes your world's terrain to the network.
debugMapHistoryTiles2048512 – 8192Tiles the debug map remembers, about 28 KB each.
verboseInferencefalseon / offLog every terrain tile at notification level. Noisy; for diagnosing slowness. Off, those lines still go to the debug log and only a tile that stalls — a second or more, and four times the session average — reaches the main one.

Stuttering

In single player the model shares the GPU with the renderer, and a submitted graph runs to completion, so a burst of chunk generation reads as a freeze even though the game thread is not blocked.

terraindiffusionGpuUtilizationPercent below 100 idles the generator after each run, so the renderer gets regular windows. It cannot shorten an individual run, and world generation slows by the reciprocal: at 50% a tile takes about twice as long. Measured on a 6 GB laptop card at 40%, a tile went from 142 ms to 323 ms, and total inference time rose 14.7 s to 16.5 s because a card that keeps going idle drops its clocks.

Start at 50 and go down only as far as the stutter needs; too low and generation cannot keep up with a walking player. /tdiff gpulimit <percent> changes it without a restart and saves it to the world. On a dedicated server set it to 100 unless you want the card for something else.

Debug map

Set debugMapPort and the mod serves a read-only page of what the model is producing, updating as tiles are generated. /tdiff map prints the address; the default binding is loopback.

CategoryLayers
Coarse model inputelevation, temperature, temperature seasonality, precipitation, precipitation seasonality asked for; river basin conditioning
Coarse model outputelevation, mean temperature, temperature seasonality, annual precipitation, precipitation seasonality
Full resolutionmodel elevation, slope, mean temperature, temperature seasonality, annual precipitation, precipitation seasonality, rainfall byte, forest and shrub cover from the model, forest and shrub maps as the game reads them
Vanilla channelssurface height as built (Rivers' valleys and channels included), ocean map, vanilla forest map, vanilla shrub map

Drag to pan, wheel to zoom, hover a column for every layer.

It keeps its own record, because the generator's tile cache drops a tile as soon as it has moved on. Each is a 32×32 thumbnail, a byte per column per layer, so the default 2048-tile history costs about 60 MB. Point it at 0.0.0.0 only to publish your world's terrain to the network; the server logs a warning if you do.

World generation

These decide what the world looks like. Changing one after a world has been explored will make new chunks disagree with old ones.

Height and scale

KeyDefaultUseful rangeMeaning
heightMode"isotropic""isotropic" "manual" "auto"True scale, a fixed metres-per-block, or fit the terrain to the world's height.
metersPerBlockVertical05 – 30"manual" only: metres of elevation per block. 0 leaves the mode's own answer.
linearKneeFraction0.850.7 – 0.95Fraction of the height mapped perfectly linearly before summits start compressing. Lower keeps more of the range for the compressed tail.
oceanDepthFraction0.90.6 – 1How much of the space below sea level the abyss reaches. Lower gives shallower seas and more room for the sea bed's detail. The shore end is not scaled by it — the first column past the beach is one block of water at any world height.
slopeDetailStrength10.5 – 2Perlin roughness added to sloped ground. 0 gives glassy hillsides; above 2 the noise starts competing with the terrain.
scaleOverride01 – 6Overrides the world's resolution: blocks per 30 m model pixel. 0 uses the world setting. Above 6 is settable but generation cost grows with the square.
verticalExaggerationOverride00.5 – 2Overrides the world's height multiplier. 0 uses the world setting.

Height calibration (heightMode: "auto" only)

KeyDefaultUseful rangeMeaning
targetPeakFillFraction0.920.8 – 0.95How much of the available height the region's peaks fill. Leave headroom: 1 puts summits against the ceiling.
peakQuantile0.9950.99 – 0.999Which elevation quantile counts as a peak. Lower ignores the highest ground and exaggerates everything else.
calibrationRadiusBlocks40962048 – 16384Half-width of the surveyed area. Wider is more representative and costs a few more seconds, once.
calibrationProbes84 – 16Full-detail probes on the tallest surveyed cells. 0 falls back to reliefFactor.
reliefFactor1.61.3 – 2Assumed peak-to-survey ratio when probing is off or fails.
minAutoExaggeration / maxAutoExaggeration1 / 201 – 4 / 4 – 30Bounds on the vertical gain calibration may choose. Raising the minimum above 1 forbids a world flatter than true scale.

Climate and vegetation

KeyDefaultUseful rangeMeaning
climateMode"""" "full" "off"Overrides the world's "Climate" setting. Empty uses it.
rainfallBasis"moisture""moisture" "precipitation"What the game's rainfall byte is quantile-mapped from: the model's aridity-derived tree moisture, or raw millimetres.
moistureMedian / moistureSpread0.62 / 1.00.4 – 0.9 / 0.7 – 1.4Log-normal fit to the model's tree moisture over land. Raising the median makes the whole world read wetter to the game's biome thresholds; raising the spread pushes deserts and rainforests further apart.
rainfallMedianMm / rainfallSpread540 / 0.8300 – 900 / 0.6 – 1.2The same for "precipitation" basis.
rainfallBias0.05-0.1 – 0.2Added to the rainfall byte, as a fraction. Raise for a lusher world; see the note below the tables.
temperatureOffsetC0-5 – 5Degrees added to every model temperature, after the latitude band and the world's global setting. A blunt instrument; prefer the world settings.
forestClearings0.80.5 – 1How far vanilla's patchy forest noise opens the model's woods into fields and glens. 0 off, 1 bare clearings.
shrubClearings0.80.5 – 1The same for shrubs, from vanilla's shrub noise.

Seasons and surface

KeyDefaultUseful rangeMeaning
seasonalTemperaturetrueon / offSwing temperature on the model's seasonality (BIO4) instead of on latitude alone.
seasonalTemperatureStrength10.5 – 1.5Multiplies that swing. 0 gives a world with no seasons; above 1.5 a continental winter becomes unsurvivable.
seasonalPrecipitationtrueon / offSwing rainfall on the model's precipitation seasonality (BIO15), giving monsoon climates a real dry season.
seasonalPrecipitationStrength10.5 – 1.5Multiplies the wet/dry contrast. Above about 1.4 the dry season clamps to no rain at all.
seasonHemispherestrueleave onSwing the year the opposite way south of the equator. The game's calendar already does this; off, the southern hemisphere gets leaves that fall in the spring.
bareSlopeRocktrueon / offLeave slopes too steep for soil as bare rock, instead of vanilla's eight blocks of dirt on a cliff face.
glacierIcetrueon / offCap ground whose warmest month stays below freezing with glacier ice.
rescaleBlockLayerAltitudestrueon / offStretch vanilla's altitude bands to the terrain height. No effect at true scale, where they already line up.

Coastlines

KeyDefaultUseful rangeMeaning
oceanMap"input""input" "output""input" conditions the model on the world's ocean map; "output" lets the model invent the continents and rewrites the map to match, ignoring Landcover and Landcover scale.
landmaskStrength10.8 – 1How completely the ocean map overrides the model's own sense of where land belongs. Below about 0.8 the coastline stops resembling the map at all.
landmaskNoiseLevel0.10.05 – 0.5How much noise the model is told the mask carries. Lower binds it more tightly — this runs the opposite way to its name in the model's config. 0.5 is the model's own value and reproduces the map over ~88% of the world; 0.1 gets that to 95%; below 0.05 the gain is under 2% and the land it keeps starts flattening. 0 uses the model's value.

Global climate

KeyDefaultUseful rangeMeaning
globalClimateStrength10.5 – 1How much of the world's global temperature and precipitation settings the model is conditioned on rather than having applied to its output. The world reads the same either way; what changes is whether the model knew. 0 is what vanilla does.
climateNoiseLevel00, or 0.1 – 0.5How much noise the model is told the steered climate carries, on the same inverted scale as landmaskNoiseLevel. 0 uses the model's own value and is the default — unlike the landmask, the climate conditioning already tracks what it is asked for closely.
latitudeStrength10, or 0.5 – 1How much of a north-south climate gradient the world gets. 1 puts the tropics, the subtropical deserts, the storm track and the ice where the game's polarEquatorDistance says. 0 is the unrooted world the mod made before 0.5. Values between the two weaken the gradient without moving it.

Spawn

KeyDefaultUseful rangeMeaning
startingClimateSearchtrueon / offPut the spawn on land in the world's chosen starting climate. Off spawns on the nearest land whatever its climate.
startingClimateSearchRadiusBlocks6553616384 – 262144How far to look before settling for the closest temperature it saw. The search stops at the first match, so this is only the give-up point.
startingClimateNorthSouthCost21 – 4How much more reluctantly the search moves along Z than X, because Z is what buys midnight sun. With latitude bands on it rarely has to move far at all — the map centre already sits at the right latitude.

rainfallBias puts back the average of Vintage Story's "higher ground is wetter" bonus, which the climate map cancels (the model does orography properly) but vanilla's biome thresholds were tuned with.

Forest density is squared on its way to the ground. Vanilla accepts each candidate tree with probability (byte / 255)², so 100% → 140% is roughly double the trees, and the setting bites hardest where cover is already low. The byte saturates at 255, so much above 120% flattens the wet end; 0% still scatters lone trees, because the acceptance probability floors at 0.0025. For a treeless world use "Forestation & shrubs" at −100%.

The two differ: "Forestation & shrubs" is additive, lifting deserts as much as forests; Forest density is proportional, preserving the climate pattern. Both apply.

Building

./build.sh          # Linux and macOS
build.bat           :: Windows

Both take an optional configuration (Release by default) and produce dist/vsterraindiffusion_<version>.zip.

Needs the .NET 10 SDK and a Vintage Story install: /opt/vintagestory or ~/Vintagestory on Linux and macOS, %APPDATA%\Vintagestory on Windows. Override either with VINTAGE_STORY.

Credits

  • Terrain Diffusion model, the reference implementation and the original Minecraft mod: xandergos
  • Mixed-precision decoder derived from that MIT-licensed model; its exact recipe and upstream copyright notice are in scripts/.
  • Vintage Story integration: this mod

Techmo2/VS-Terrain-Diffusion

An implementation of the terrain diffusion generator for vintage story

C#

0

65 commits

updated Oct 6, 2026

See the code

README

VS Terrain Diffusion — a Vintage Story mod

Generates Vintage Story worlds with Terrain Diffusion, a neural model trained on real Earth topography and climate (SIGGRAPH 2026): continents, drainage networks, fjords, plateaus and mountain ranges with the structure of real terrain, and a real climatology alongside the heightmap. Terrain, temperature, rainfall, forests, the surface you walk on and the seasons all come from the same model.

Contents — Installing · Creating a world · What the mod changes · How it works · Commands · Configuration · Building

Installing

Drop the release zip in your Mods folder. The models download themselves on first launch.

  • Vintage Story 1.22 (targets .NET 10, same as the game)
  • ~2.2 GB of disk for the selected model files, fetched once, plus the optimised-graph cache
  • ~3 GB of RAM while the models are resident
  • A GPU is strongly recommended. CPU inference works but is roughly 10-20x slower.
PlatformDevice used automaticallyNotes
Linux + NVIDIACUDANeeds a system CUDA 12 or 13 runtime and cuDNN 9
WindowsDirectMLAny modern GPU; no extra install
macOS (Apple)CoreMLNo extra install
Anything elseCPUWorks, but slow

The matching ONNX Runtime native library is downloaded on first use too (a few MB for CPU/DirectML, ~300 MB for CUDA). Nothing is installed by hand.

Server-side. Clients may install it to keep their weather display in step with the server, but vanilla clients can join normally.

The first world sits on the loading screen until everything is fetched; the screen reports each download, and Logs/server-main.log has the detail.

Creating a world

The mod adds a Terrain Diffusion tab to the Customize screen. Each setting is saved with the world; a dedicated server sets them under WorldConfig.WorldConfiguration in serverconfig.json, and /worldconfig <code> <value> changes one later.

SettingCodeDefaultWhat it does
Terrain DiffusionterraindiffusionEnabledonTurn off to fall back to vanilla terrain, keeping the mod installed.
ResolutionterraindiffusionScale15 mReal-world metres per block, horizontally and vertically.
Vertical exaggerationterraindiffusionVerticalExaggeration1xMultiplies terrain height. 1x is true scale.
Sea levelterraindiffusionSeaLevelvanilla128 fixes the sea at Y 128, so every block a taller world adds goes above it.
ClimateterraindiffusionClimatemodelWhether the model drives climate as well as terrain.
Terrain intensityterraindiffusionCoarsePoolingoffPacks 2x or 4x the landscape into the same distance: ranges, valleys and coasts closer together, relief steeper.
Intensity modeterraindiffusionCoarsePoolModeaverageextreme keeps each block's highest ground and deepest valley floor: taller peaks, deeper cuts, less realistic.
Detail redrawterraindiffusionDetailRedraw35Hundredths. How much of the detail model's first draw is redrawn. 70–100 varies coast shapes and small valleys by 10–17 m on average.
Height noiseterraindiffusionHeightNoise0Random height per large-scale cell (~8 km) before detail is drawn, in hundredths of a sqrt-metre: 200 ≈ ±100 m at 600 m. None at the coast.
Altitude coolingterraindiffusionAltitudeCooling100%Every 100% above 100% takes another 6.5 °C off per km of height, on the ground and in the air, on top of the model's own rate: lower treelines and snowlines. Sea level is unchanged.
Forest densityterraindiffusionForestDensity100%Scales the forest cover the climate implies. Trees on the ground go as its square — see below.
Shrub densityterraindiffusionShrubDensity100%Scales shrub cover the same way.
River valley depthterraindiffusionRiverValleyDepth30%With the Rivers mod, how far the land is lowered along rivers so the model draws valleys for them. 100% drowns a fifth of each corridor into inlets.

With sea level at 128 the terrain, climate, soil bands, trees and weather keep their height above the sea: a 512-block world is laid out like a vanilla world 675 blocks tall, with 128 blocks of rock and ocean below the sea instead of 220. Vanilla data written as fractions of world height (block layers, lake and ocean beds, tree altitude bands) is moved to the same height above the sea in that equivalent world. Rivers resets sea level to vanilla's as it starts; that is overridden.

Terrain intensity is the reference implementation's coarse_pooling: the model's large-scale map is drawn as usual and each 2x2 or 4x4 block of it becomes one cell of the world. Measured over nine 15 km regions, land only:

SettingRelief (std)95th percentileMean grade90th percentile grade
off452 m1196 m17%39%
2x376 m1124 m20%46%
4x743 m2353 m23%46%
2x extreme926 m2934 m57%105%
4x extreme1298 m4172 m54%104%

The world's ocean map is followed as closely at every setting (94-95% of coarse cells at 50% landcover); extreme adds a few points of land along coasts. 4x and extreme need a tall world to keep their peaks at true scale.

The same tab holds the inference settings. worldGen.climateMode, worldGen.scaleOverride and worldGen.verticalExaggerationOverride in the mod config override the first three on every world.

Vanilla settings, and what becomes of them:

SettingDefaultWhat it does here
World height—Set this to 1024. Vanilla's default is far too short for real mountains.
Landcover97.5%Honoured — it decides where the sea goes. See Land and sea.
Landcover scale (oceanscale)500%Honoured — it decides how big the oceans are. Same section.
Global temperaturenormalHonoured — the model draws a world that cold or that hot. See below.
Global precipitationnormalHonoured — same, for rainfall.
Polar–equator distance100 000 blocksHonoured — it puts the tropics, the deserts and the ice where the game says. See Latitude.
Starting climatetemperateHonoured, two ways: it sets which latitude the map centre sits at, and the spawn search finds land there. See below.
Forestation & shrubsnormalHonoured — vanilla applies it on top of the model's forest map, unchanged.
Climate distributionrealistic"Patchy" turns off the latitude bands, since a patchy world has no latitude.
Geologic activityrareHonoured — that byte of the climate map is still vanilla's.
Landform scale100%Almost nothing. See below.
Upheaval rate30%Nothing. See below.

Everything not listed — ores, caves, temporal stability, world size — is untouched and behaves exactly as it does in an unmodded world.

Two settings the mod cannot honour

Both configure maps that only fed vanilla's terrain generator, which this mod replaces.

  • Upheaval rate: no effect. The map is still generated and saved; nothing reads it.
  • Landform scale: no effect on terrain. GenDungeons still reads the landform map to place dungeons needing flat ground, so one can land on what the map calls a plain and the model made a hillside. Two shipped tiled dungeons are affected.

worldGen.slopeDetailStrength and Vertical exaggeration are the nearest relief controls.

Land and sea

The world's ocean map decides where the coastline is, and the model decides what it looks like. Before generating anything the mod reads Vintage Story's ocean map — the one "Land cover" and "Ocean scale" configure — and feeds it to the model as conditioning, so a world set to 50% land gets 50% land, with the shelf, the fjords and the mountains behind them drawn from real terrain.

It reads whichever ocean map is installed — Continental World's, for instance — and leaves it untouched afterwards.

  • Conditioning is soft. The coast wanders around the one it was given rather than tracing it, which is what makes it look natural. Sea fraction comes out within ~4 points of the setting; column by column 88% agree, nearly all disagreement within one cell of a coastline.
  • Resolution floor. One conditioning pixel spans 512 blocks at the default resolution, so anything smaller fills in as land. Low "Ocean scale" worlds lose their smallest islands and lakes.

Vanilla's defaults (97.5% land, 500% ocean scale) give a nearly unbroken continent. 40–60% land is where real coastlines start to appear.

Set worldGen.oceanMap to "output" for the reverse arrangement: the model invents its own continents from real-world terrain, ignoring both world settings, and the ocean map is rewritten to match it.

A hotter, colder, wetter or drier world

Vanilla multiplies these onto the climate map it drew at random. This mod hands them to the model, so a "Semi-Arid" world is drawn semi-arid: the forests, soil, snow line and seasons all follow. Rainfall is scaled outright; temperature moves the world along the real distribution instead, since nowhere on Earth has a mean above 30 °C. What that cannot reach is applied to the output afterwards as vanilla does.

Measured on two seeds across both settings' full range: temperature within 2 °C up to "Hot", which overshoots ~5 °C for want of anywhere hotter to draw from; rainfall within 10% in a region of ordinary wetness, less where it is already very wet or very dry. "Very hot" and "Scorching hot" saturate the game's climate scale, as in an unmodded world — and leave the spawn search no temperate land, so it settles for the coolest thing going.

Set worldGen.globalClimateStrength to 0 to apply both to the model's output instead.

Latitude

Vanilla's latitude is a straight line from +40 °C to −20 °C with no rainfall gradient. This mod conditions the model on a real zonal climate instead — equatorial rain belt, subtropical deserts at 25°, mid-latitude storm track at 50°, dry cold caps — so a tropical belt is drawn tropical, and coasts, rain shadows and mountains all happen within the band.

Where each block sits between equator and pole is the game's answer, read from polarEquatorDistance, so the snow line agrees with day length and the midnight sun. The phase comes with it, which is how "Starting climate" lands the map centre at the right latitude.

Measured over equator-to-pole transects on three seeds: temperature ~1 °C warm on average, 2 °C either way in a belt; from ~80° poleward the model runs out of world (it has never seen a mean below −14 °C) so the last stretch is added afterwards and the poles read −20 °C. Rainfall lands on the band in the median and swings a factor of two either side — geography, not error. A 15 000-block polar distance still tracked the bands to 1.5 °C.

Heading north or south now changes the climate; east or west mostly does not. 200k–400k gives long belts, 15k–25k puts tropics and ice within a day's walk. The hemispheres get opposite years, as the game's calendar already does (worldGen.seasonHemispheres).

Set worldGen.latitudeStrength to 0 for an unrooted world; values between weaken the gradient without moving it. Bands are off on a "Patchy" world.

Starting climate

The bands mean what they do in an unmodded world, as annual mean temperature: hot 28–32 °C, warm 19–23 °C, temperate 6–14 °C, cool −5 to 1 °C, icy −15 to −10 °C.

With latitude bands on, the map centre already sits at the right latitude and the search only has to find land there — usually a few hundred blocks. With latitudeStrength at 0 it hunts for a matching climate wherever the model put one, and cold is found on high ground rather than far north.

It prefers to travel east or west, because distance along Z buys midnight sun past the polar distance. It stops at the first match, so most worlds spawn within a few thousand blocks in under a second; a distant band — usually "hot" — takes longer, and on CPU inference longer still. If the seed has no such land in range the log says so and you spawn at the closest temperature found.

Set worldGen.startingClimateSearch to false to spawn on the nearest land whatever its climate.

What the mod changes

  • Terrain pass — GenTerra's chunk handler fills columns from the diffusion heightmap. If another mod already replaced terrain generation, the model supplies heights to it; see Other terrain mods.
  • Climate map — sea-level temperature and annual rainfall from the model, with the game's altitude correction replaced by the real lapse rate. The geologic activity byte stays vanilla's.
  • Global temperature and precipitation — conditioning rather than post-processing.
  • Latitude — the game's own latitude, from polarEquatorDistance, conditioned in as a real zonal climate in place of vanilla's straight line from +40 °C to −20 °C.
  • Forest and shrub maps — cover derived from the model's moisture and growing season.
  • Ocean map — read, not written: it is what the terrain is conditioned on.
  • Surface pass — slopes too steep for soil are scoured back to bare rock (vanilla upholsters cliffs in eight blocks of dirt), and ground whose warmest month stays below freezing is capped with glacier ice.
  • Seasons — the year swings on the model's seasonality rather than on latitude alone, opposite sides of the equator included.
  • Spawn — moved to solid ground in the world's chosen starting climate.
  • Surface block layer altitudes — only when terrain is vertically exaggerated; at true scale vanilla's bands already line up.
  • Nothing else. The landform and upheaval maps are still generated and still ignored, because the generator that read them is gone — see Two settings the mod cannot honour. Rock strata, ores, caves, rivers, ponds, ruins, traders and temporal stability are vanilla, running unchanged on top.

Other terrain mods

Mods that only supply a map — Continental World's ocean map, for instance — need nothing special: the mod reads whatever map is installed and conditions the model on it.

sneeze's Rivers routes its rivers from the coast the model actually draws, not the ocean map, and starts each river in open sea, so rivers reach the water (95% within their first three nodes, against 75% from the map). Rivers caches each world's zones under RiverCache/, so a world keeps the coast its rivers were first routed from.

Algernon's Terrain Sampler answers other mods' height questions from the model instead of its own copy of vanilla's terrain: exactly what gets generated in full mode, or a blurred coarse-model answer in coarse mode (terrainSamplerHeight). Its climate and vegetation already come from this mod's map layers.

A mod that replaces terrain generation itself cannot layer with this one: two generators filling the same column give the union of both landscapes. So whoever generates terrain gets handed the model's heights and does the filling.

Algernon's Watersheds is supported this way. It disables vanilla GenTerra and fills every column itself, so this mod stops generating terrain and instead answers every height question Watersheds asks: the height its analysis is built on, the height a stream's profile is laid against, the height after a stream has cut in, and which blocks are solid. Answering all of them matters — a stream's water surface and its bed are worked out separately, so one height left coming from Watersheds' own landscape strands water in the air. Its ridge and gully erosion filter is switched off for these worlds: it cuts valley detail into fractal noise, the model's landscape already has erosion, and it is computed inside two of those height answers. Everything downstream — stream water, banks, rapids, groundwater, block layers — is Watersheds' own.

  • World creation takes longer. The analysis samples heights kilometres around spawn, all from the model, so the first load spends a few minutes on tiles it will not visibly use. One-time per area, and cached.
  • Watersheds decides where streams go. It will not path one across terrain rougher than SmallChunkRoughnessThreshold in ModConfig/Watersheds/TerrainAnalysisConfig.json (2 blocks RMSE by default). Ordinary modelled landscape measures 0.0–0.8; genuinely broken ground gets no small streams. Raise the threshold if you want them anyway.
  • Streams need somewhere to drain. At vanilla's default land cover there is almost no ocean to reach, so almost no streams. That is Watersheds' behaviour, not this mod's.

Stream maps live in a database beside the save, so a world explored with an older version of this mod has streams plotted against the wrong landscape: /watersheds clearstreammaps and regenerate, or start a new world.

If Watersheds updates in a way this cannot reach into, the mod says so and takes itself out of the world rather than generating a broken one.

How it works

Scale, and why the world needs to be tall

By default a block is as tall as it is wide — 15 m in every direction at the default resolution — so a 2 000 m massif is 133 blocks of climbing and every slope has its real-world grade.

Real mountains need room. The model's land runs to about 3 000 m at the 95th percentile and 5 000 m at the extreme, which at 15 m per block is 200 and 333 blocks above sea level:

World heightBlocks above seaTerrain held at true scale
256145up to ~1 900 m
512289up to ~3 700 m
1024578up to ~7 400 m

Past that the mapping bends towards the ceiling on u / (1 + u) rather than clipping, so summits round off instead of shearing into mesas — at the cost of the highest ground's faithfulness.

If a tall world is not an option, worldGen.heightMode: "auto" surveys the region around spawn once and stretches the metre-to-block mapping so its peaks reach near the ceiling. The landscape uses the full height at the cost of exaggerated relief — a gentle region might come out at 4x. The measurement depends only on the seed and is stored in the save.

Resolution is also the main performance dial: at 30 m per block you cross a continent in an afternoon, at 5 m the same mountain is four kilometres of walking.

Climate

The model predicts four WorldClim bioclimatic variables everywhere it predicts elevation:

VariableWhat it is
BIO1annual mean temperature, °C
BIO4temperature seasonality — the spread of monthly means
BIO12annual precipitation, mm
BIO15precipitation seasonality — how unevenly it falls

A real climatology, with maritime coasts, continental interiors, rain shadows and altitude already in it — but nothing in the model knows which way is north, so the latitude bands supply that axis. globalTemperature and globalPrecipitation condition the bands themselves, so a "Snowball earth" world is one whose every band sits a quarter of the way up the scale.

From bioclimate to what the game reads

800 mm of rain is generous in Lapland and semi-arid in the Sahel, so the mod derives potential evapotranspiration, an aridity index and a growing season, and keys everything off those. The formulas are ported from the reference implementation's biome classifier.

Rainfall is a quantile map, not a physical conversion. Vanilla draws its 0-255 byte uniformly and every threshold reading it was tuned against that spread, so a physical quantity fed straight in makes the world read as desert. The model's tree moisture goes through its own distribution instead. worldGen.rainfallBasis: "precipitation" maps raw millimetres.

Forest and shrub cover come from the same moisture, scaled by growing season and cut to zero on ground too steep for soil. Vanilla's MapLayerWobbledForest computes 128 - rain * temp / 65025, a product that never exceeds 1, so its forest density is pure noise with no relation to climate; woodland in the foothills and nothing above the treeline are new behaviour. That noise is still used for the one thing it is good at, patchiness: where vanilla's map is open, the model's tree cover is thinned by forestClearings, so wet country gets fields and glens instead of one unbroken wood. Shrub cover is thinned the same way by vanilla's shrub map (shrubClearings). Everything that read that map still does, so animals and undergrowth follow the woods.

Temperature is stored as sea-level temperature, and the mod replaces the lapse rate applied on read. Vanilla's flat 0.157 °C per block is only right at about 24 m per block and over-cools mountains at anything finer; 6.5 °C/km reads back as the model predicted at any vertical scale.

Seasons

Vanilla takes the year's amplitude from latitude alone (|latitude| * 65 degrees), so the equator has no seasons and nothing else about a place matters. Here it comes from BIO4: a maritime coast and a continental interior at the same annual mean get completely different years. Precipitation seasonality does the same for rain, giving monsoon climates a real dry season.

Neither channel fits Vintage Story's packed climate integer, whose interpolator only touches the low three bytes, so they are map region mod data — saved with the region and, unlike its other maps, sent to clients. A vanilla client falls back to vanilla's seasons for display.

/tdiff season <x> <z> walks a year at a position and prints what it does.

Commands

/terraindiffusion, or /tdiff. Requires the controlserver privilege.

SubcommandWhat it shows
statusDevice, world scaling, tiles generated, average tile time, and where that time went: total model inference, its share of tile time, and a per-stage breakdown. A low inference share means something other than the GPU is the bottleneck.
gpulimit [percent]The share of the time inference is allowed to keep the device busy, and how much has been given up to the limit so far. With a percentage, sets it there and now, and saves it to the world.
mapThe debug map's address, and how many tiles it is holding.
hereElevation, slope, full bioclimate and derived cover where you stand, plus the latitude diagnostics below.
season <x> <z>The same diagnostics at a position, and the year's temperature and rainfall cycle there. Usable from a server console, where here is not.
column <x> <z>What actually got generated in a column, next to what the model said.

here and season share four climate diagnostics:

  • Latitude and Hemisphere — distance from the equator, which side, and the season the game's calendar reports there. Check this first if foliage or crops look out of step.
  • Sea-level temperature — the reading with altitude taken back out, and the fitted local lapse rate. The number to compare two places by. Slightly slower than the rest: it is a pipeline query.
  • Band temperature and Band precipitation — what the latitude band asked for and how far this column sits from it, plus Band offset applied when part of the band was added after the model ran.

A single column scatters several degrees either side of its band, which is the model's business; consistent drift over many columns is not.

Configuration

ModConfig/vsterraindiffusion.json, written on first start. CONFIG.md is the whole default file with a comment on every field; the tables below are the short version.

Optional: ConfigLib gives the same settings an in-game screen, editing this file in place rather than keeping a copy.

verboseInference takes effect on save; everything else is read when the world generator starts, so it needs a restart. Useful range below is where a setting does something sensible, not where it is legal — CONFIG.md lists the hard limits.

Inference

World settings, in the Terrain Diffusion tab (see Creating a world). The Customize screen lists every device and precision; one this machine cannot run stops the world from loading, naming the ones it can. Keep the device and the two precisions fixed after exploring a world: changing any of them can make newly generated terrain disagree slightly with existing chunks. Each world loads the runtime for its device in a worker process of its own, which exits when the world closes, so the next world can use a different device without restarting the game.

CodeDefaultUseful rangeMeaning
terraindiffusionInferenceDeviceautoauto cpu openvino cuda tensorrt-rtx directml coremlOpenVINO and TensorRT RTX are opt-in. OpenVINO, on 64-bit Linux, accelerates the decoder while leaving the large stages on ORT CPU. TensorRT RTX needs a GeForce RTX 30xx or newer on 64-bit Windows or Linux, fetches the NVIDIA runtime once (105 MB on Windows, 140 MB on Linux) and builds a cached engine per model; it is about 1.5x faster than CUDA on the same FP32 models and 2.4x with FP16. A compatible device whose runtime cannot be prepared runs that session on what auto would pick, or ORT CPU, and is logged; the setting is never changed. Only what the selected provider needs is fetched: TensorRT RTX skips the CUDA provider library it never loads, and cuda on Windows pulls the cuBLAS/cuFFT/NVRTC/cuDNN libraries it links against (~1 GB, once) only if no CUDA toolkit and cuDNN are installed; Linux and macOS use the system CUDA install.
terraindiffusionDecoderPrecisionfp32fp32 fp16 int8Select and automatically fetch only the matching decoder. FP16 is for GPU providers, INT8 for CPU/OpenVINO. Both change newly generated terrain slightly; a missing or invalid selected decoder stops model loading instead of silently changing precision.
terraindiffusionBasePrecisionfp32fp32 fp16The base model is most of a tile's work, so FP16 here is the biggest GPU win: on an RTX 3060, TensorRT RTX with FP16 base and decoder generated the same ten regions in 7.6 s against 19.0 s on CUDA FP32, for about 4 m mean elevation difference (CPU vs GPU is already ~2.6 m). Needs a GPU provider.
terraindiffusionGpuUtilizationPercent10040 – 100Share of the time world generation may keep the device busy. Lower it if generating chunks makes the game stutter; see Stuttering below. World generation slows by the reciprocal.
terraindiffusionOffloadModelsfalseon / offHold only one model on the GPU at a time, saving about 1 GB of VRAM. Generating a tile runs two or three of the models, so every tile then pays to rebuild a session for a graph of most of a gigabyte: measured on a 6 GB card it triples the average tile time. Turn on only if the models will not fit.
terraindiffusionModelLoadModefilefile memoryOpen model graphs from their files, or read them into RAM first (about 1 GB more).
terraindiffusionValidateModelHashestrueon / offVerify SHA-256 of existing model files on load. Off saves a few seconds of disk read.
terraindiffusionDownloadRuntimetrueon / offFetch the ONNX Runtime and, when selected, OpenVINO native libraries automatically. On 64-bit Windows this includes the Visual C++ runtime (6.8 MB, from Microsoft) when the machine's own is missing or older than 14.39.

Machine settings, in the mod config:

KeyDefaultUseful rangeMeaning
tileCacheMegabytes256128 – 1024Total decoded tensor-window cache across all pipeline stages.
latentBatchSize00 – 4Latent windows per base-model call. Zero chooses 1 on CPU and 4 on GPU.
terrainTileCacheMegabytes256128 – 1024Finished terrain tiles. Raise if you see thrash warnings.
terrainTileSizeBlocks00, 128 – 512Blocks generated per model invocation, a multiple of 32. Zero chooses 128 on CPU and 256 on GPU; larger values amortise the model better but make first-visit stalls longer.
debugMapPort0 (off)8088Serves the debug map on this port. 0 opens no port.
debugMapBindAddress127.0.0.1loopbackWhere the debug map listens. 0.0.0.0 publishes your world's terrain to the network.
debugMapHistoryTiles2048512 – 8192Tiles the debug map remembers, about 28 KB each.
verboseInferencefalseon / offLog every terrain tile at notification level. Noisy; for diagnosing slowness. Off, those lines still go to the debug log and only a tile that stalls — a second or more, and four times the session average — reaches the main one.

Stuttering

In single player the model shares the GPU with the renderer, and a submitted graph runs to completion, so a burst of chunk generation reads as a freeze even though the game thread is not blocked.

terraindiffusionGpuUtilizationPercent below 100 idles the generator after each run, so the renderer gets regular windows. It cannot shorten an individual run, and world generation slows by the reciprocal: at 50% a tile takes about twice as long. Measured on a 6 GB laptop card at 40%, a tile went from 142 ms to 323 ms, and total inference time rose 14.7 s to 16.5 s because a card that keeps going idle drops its clocks.

Start at 50 and go down only as far as the stutter needs; too low and generation cannot keep up with a walking player. /tdiff gpulimit <percent> changes it without a restart and saves it to the world. On a dedicated server set it to 100 unless you want the card for something else.

Debug map

Set debugMapPort and the mod serves a read-only page of what the model is producing, updating as tiles are generated. /tdiff map prints the address; the default binding is loopback.

CategoryLayers
Coarse model inputelevation, temperature, temperature seasonality, precipitation, precipitation seasonality asked for; river basin conditioning
Coarse model outputelevation, mean temperature, temperature seasonality, annual precipitation, precipitation seasonality
Full resolutionmodel elevation, slope, mean temperature, temperature seasonality, annual precipitation, precipitation seasonality, rainfall byte, forest and shrub cover from the model, forest and shrub maps as the game reads them
Vanilla channelssurface height as built (Rivers' valleys and channels included), ocean map, vanilla forest map, vanilla shrub map

Drag to pan, wheel to zoom, hover a column for every layer.

It keeps its own record, because the generator's tile cache drops a tile as soon as it has moved on. Each is a 32×32 thumbnail, a byte per column per layer, so the default 2048-tile history costs about 60 MB. Point it at 0.0.0.0 only to publish your world's terrain to the network; the server logs a warning if you do.

World generation

These decide what the world looks like. Changing one after a world has been explored will make new chunks disagree with old ones.

Height and scale

KeyDefaultUseful rangeMeaning
heightMode"isotropic""isotropic" "manual" "auto"True scale, a fixed metres-per-block, or fit the terrain to the world's height.
metersPerBlockVertical05 – 30"manual" only: metres of elevation per block. 0 leaves the mode's own answer.
linearKneeFraction0.850.7 – 0.95Fraction of the height mapped perfectly linearly before summits start compressing. Lower keeps more of the range for the compressed tail.
oceanDepthFraction0.90.6 – 1How much of the space below sea level the abyss reaches. Lower gives shallower seas and more room for the sea bed's detail. The shore end is not scaled by it — the first column past the beach is one block of water at any world height.
slopeDetailStrength10.5 – 2Perlin roughness added to sloped ground. 0 gives glassy hillsides; above 2 the noise starts competing with the terrain.
scaleOverride01 – 6Overrides the world's resolution: blocks per 30 m model pixel. 0 uses the world setting. Above 6 is settable but generation cost grows with the square.
verticalExaggerationOverride00.5 – 2Overrides the world's height multiplier. 0 uses the world setting.

Height calibration (heightMode: "auto" only)

KeyDefaultUseful rangeMeaning
targetPeakFillFraction0.920.8 – 0.95How much of the available height the region's peaks fill. Leave headroom: 1 puts summits against the ceiling.
peakQuantile0.9950.99 – 0.999Which elevation quantile counts as a peak. Lower ignores the highest ground and exaggerates everything else.
calibrationRadiusBlocks40962048 – 16384Half-width of the surveyed area. Wider is more representative and costs a few more seconds, once.
calibrationProbes84 – 16Full-detail probes on the tallest surveyed cells. 0 falls back to reliefFactor.
reliefFactor1.61.3 – 2Assumed peak-to-survey ratio when probing is off or fails.
minAutoExaggeration / maxAutoExaggeration1 / 201 – 4 / 4 – 30Bounds on the vertical gain calibration may choose. Raising the minimum above 1 forbids a world flatter than true scale.

Climate and vegetation

KeyDefaultUseful rangeMeaning
climateMode"""" "full" "off"Overrides the world's "Climate" setting. Empty uses it.
rainfallBasis"moisture""moisture" "precipitation"What the game's rainfall byte is quantile-mapped from: the model's aridity-derived tree moisture, or raw millimetres.
moistureMedian / moistureSpread0.62 / 1.00.4 – 0.9 / 0.7 – 1.4Log-normal fit to the model's tree moisture over land. Raising the median makes the whole world read wetter to the game's biome thresholds; raising the spread pushes deserts and rainforests further apart.
rainfallMedianMm / rainfallSpread540 / 0.8300 – 900 / 0.6 – 1.2The same for "precipitation" basis.
rainfallBias0.05-0.1 – 0.2Added to the rainfall byte, as a fraction. Raise for a lusher world; see the note below the tables.
temperatureOffsetC0-5 – 5Degrees added to every model temperature, after the latitude band and the world's global setting. A blunt instrument; prefer the world settings.
forestClearings0.80.5 – 1How far vanilla's patchy forest noise opens the model's woods into fields and glens. 0 off, 1 bare clearings.
shrubClearings0.80.5 – 1The same for shrubs, from vanilla's shrub noise.

Seasons and surface

KeyDefaultUseful rangeMeaning
seasonalTemperaturetrueon / offSwing temperature on the model's seasonality (BIO4) instead of on latitude alone.
seasonalTemperatureStrength10.5 – 1.5Multiplies that swing. 0 gives a world with no seasons; above 1.5 a continental winter becomes unsurvivable.
seasonalPrecipitationtrueon / offSwing rainfall on the model's precipitation seasonality (BIO15), giving monsoon climates a real dry season.
seasonalPrecipitationStrength10.5 – 1.5Multiplies the wet/dry contrast. Above about 1.4 the dry season clamps to no rain at all.
seasonHemispherestrueleave onSwing the year the opposite way south of the equator. The game's calendar already does this; off, the southern hemisphere gets leaves that fall in the spring.
bareSlopeRocktrueon / offLeave slopes too steep for soil as bare rock, instead of vanilla's eight blocks of dirt on a cliff face.
glacierIcetrueon / offCap ground whose warmest month stays below freezing with glacier ice.
rescaleBlockLayerAltitudestrueon / offStretch vanilla's altitude bands to the terrain height. No effect at true scale, where they already line up.

Coastlines

KeyDefaultUseful rangeMeaning
oceanMap"input""input" "output""input" conditions the model on the world's ocean map; "output" lets the model invent the continents and rewrites the map to match, ignoring Landcover and Landcover scale.
landmaskStrength10.8 – 1How completely the ocean map overrides the model's own sense of where land belongs. Below about 0.8 the coastline stops resembling the map at all.
landmaskNoiseLevel0.10.05 – 0.5How much noise the model is told the mask carries. Lower binds it more tightly — this runs the opposite way to its name in the model's config. 0.5 is the model's own value and reproduces the map over ~88% of the world; 0.1 gets that to 95%; below 0.05 the gain is under 2% and the land it keeps starts flattening. 0 uses the model's value.

Global climate

KeyDefaultUseful rangeMeaning
globalClimateStrength10.5 – 1How much of the world's global temperature and precipitation settings the model is conditioned on rather than having applied to its output. The world reads the same either way; what changes is whether the model knew. 0 is what vanilla does.
climateNoiseLevel00, or 0.1 – 0.5How much noise the model is told the steered climate carries, on the same inverted scale as landmaskNoiseLevel. 0 uses the model's own value and is the default — unlike the landmask, the climate conditioning already tracks what it is asked for closely.
latitudeStrength10, or 0.5 – 1How much of a north-south climate gradient the world gets. 1 puts the tropics, the subtropical deserts, the storm track and the ice where the game's polarEquatorDistance says. 0 is the unrooted world the mod made before 0.5. Values between the two weaken the gradient without moving it.

Spawn

KeyDefaultUseful rangeMeaning
startingClimateSearchtrueon / offPut the spawn on land in the world's chosen starting climate. Off spawns on the nearest land whatever its climate.
startingClimateSearchRadiusBlocks6553616384 – 262144How far to look before settling for the closest temperature it saw. The search stops at the first match, so this is only the give-up point.
startingClimateNorthSouthCost21 – 4How much more reluctantly the search moves along Z than X, because Z is what buys midnight sun. With latitude bands on it rarely has to move far at all — the map centre already sits at the right latitude.

rainfallBias puts back the average of Vintage Story's "higher ground is wetter" bonus, which the climate map cancels (the model does orography properly) but vanilla's biome thresholds were tuned with.

Forest density is squared on its way to the ground. Vanilla accepts each candidate tree with probability (byte / 255)², so 100% → 140% is roughly double the trees, and the setting bites hardest where cover is already low. The byte saturates at 255, so much above 120% flattens the wet end; 0% still scatters lone trees, because the acceptance probability floors at 0.0025. For a treeless world use "Forestation & shrubs" at −100%.

The two differ: "Forestation & shrubs" is additive, lifting deserts as much as forests; Forest density is proportional, preserving the climate pattern. Both apply.

Building

./build.sh          # Linux and macOS
build.bat           :: Windows

Both take an optional configuration (Release by default) and produce dist/vsterraindiffusion_<version>.zip.

Needs the .NET 10 SDK and a Vintage Story install: /opt/vintagestory or ~/Vintagestory on Linux and macOS, %APPDATA%\Vintagestory on Windows. Override either with VINTAGE_STORY.

Credits

  • Terrain Diffusion model, the reference implementation and the original Minecraft mod: xandergos
  • Mixed-precision decoder derived from that MIT-licensed model; its exact recipe and upstream copyright notice are in scripts/.
  • Vintage Story integration: this mod