Terrain in tiles
Open the live demo · Read the source · View on GitHub
A Heightfield drawn as one surface is fine for a strategy map a few hundred
samples across, and wrong for ground that runs to the horizon: every sample
is drawn whether it covers a hundred pixels or a tenth of one. Cutting the
field into tiles, each buildable at more than one resolution, is how ground
stays cheap far away and detailed up close.
Step 1: A field to cut up #
A small slope, eight cells on a side.
// A gentle nine-by-nine slope, eight cells on a side.
final heights = Float32List(9 * 9);
for (var row = 0; row < 9; row++) {
for (var col = 0; col < 9; col++) {
heights[row * 9 + col] = (col + row) * 0.1;
}
}
final field = Heightfield(
columns: 9,
rows: 9,
cellSize: 1.0,
heights: heights,
);
Step 2: Cut it into tiles and build one at two levels #
HeightfieldTiles divides the field into square tiles, each buildable at any
of its levels: level 0 keeps every sample, level 1 keeps every second one.
final tiles = HeightfieldTiles(field, tileCells: 4, levels: 2);
final fine = tiles.build(0, 0, level: 0, material: 'ground');
final coarse = tiles.build(0, 0, level: 1, material: 'ground');
The coarse build has a quarter of the fine one's grid vertices, because it keeps every second sample along each axis.
Step 3: Choose a level by distance #
TileLevelChooser says which level a tile at a given distance should draw
at, with a band around each threshold so a tile does not flicker between two
levels as the camera drifts across it.
final chooser = TileLevelChooser(nearest: 20.0, levels: 2);
final near = chooser.choose(5.0, null);
final far = chooser.choose(60.0, null);
A tile close to the camera gets the fine level; one far away gets the coarse one, which is what keeps a large terrain affordable.
Step 4: Move the eye over it #
The field on this page is thirty-two metres square, cut into sixteen tiles of eight cells, each of which can be drawn at three levels. The blue ball is the eye and it circles the field; every frame each tile asks the chooser which level it should be at, from its own distance to the eye, and is rebuilt only when the answer changes. The colour says which: green is every sample, yellow every second, orange every fourth. Narrow Fine detail within and the green patch shrinks round the eye; the skirts hide the seams between levels.
// A hilly field of 32 by 32 cells, cut into sixteen tiles, each of which
// can be drawn at three levels of detail. Which level is a question put
// to the chooser, tile by tile, every time the eye moves.
final Float32List heights = Float32List(_side * _side);
for (var row = 0; row < _side; row++) {
for (var col = 0; col < _side; col++) {
heights[row * _side + col] =
1.6 * math.sin(col * 0.28) * math.cos(row * 0.23) +
0.35 * math.sin(col * 1.1 + row * 0.9);
}
}
_tiles = HeightfieldTiles(
Heightfield(columns: _side, rows: _side, cellSize: 1.0, heights: heights),
tileCells: _tileCells,
levels: 3,
);
// Rebuilt from the slider each frame; a tile only changes when the answer
// does, and the band round each threshold keeps it from flickering.
final TileLevelChooser chooser = TileLevelChooser(
nearest: nearest,
levels: 3,
);
final int level = chooser.choose(
math.sqrt(dx * dx + dz * dz),
_levels[i],
);