Terrain in tiles: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: live (line 47), choose (line 111), pick (line 122), field (line 190), tiles (line 204), lod (line 208).
1/// Ground cut into tiles, each buildable at several levels of detail, with a
2/// skirt closing the seam between a fine tile and a coarse neighbour.
3///
4/// Quoted by `terrain_tiles.md` and shown whole in the Source tab.
5library;
7import 'dart:math' as math;
8import 'dart:typed_data';
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:flutter3d_sim/flutter3d_sim.dart';
13import 'package:vector_math/vector_math.dart';
15final class TerrainTilesDemo extends ShowcaseDemo {
16 late final String _report;
18 double nearest = 8.0;
19 bool moving = true;
20 double eyeAngle = 0.0;
22 late final HeightfieldTiles _tiles;
23 late final MeshNode _eye;
24 late final DemoContext _context;
25 final List<MeshNode> _nodes = <MeshNode>[];
26 final List<int?> _levels = <int?>[];
27 late final List<Material> _paint;
28 double _clock = 0.0;
30 static const int _side = 33;
31 static const int _tileCells = 8;
34 void configureView(DemoContext context) {
35 context.orbit
36 ..distance = 42.0
37 ..pitch = 0.95
38 ..yaw = 0.0;
39 context.orbit.target.setValues(16.0, 0.0, 16.0);
40 }
43 Scene build(DemoContext context) {
44 _report = _run();
45 _context = context;
47 // A hilly field of 32 by 32 cells, cut into sixteen tiles, each of which
48 // can be drawn at three levels of detail. Which level is a question put
49 // to the chooser, tile by tile, every time the eye moves.
50 final Float32List heights = Float32List(_side * _side);
51 for (var row = 0; row < _side; row++) {
52 for (var col = 0; col < _side; col++) {
53 heights[row * _side + col] =
54 1.6 * math.sin(col * 0.28) * math.cos(row * 0.23) +
55 0.35 * math.sin(col * 1.1 + row * 0.9);
56 }
57 }
58 _tiles = HeightfieldTiles(
59 Heightfield(columns: _side, rows: _side, cellSize: 1.0, heights: heights),
60 tileCells: _tileCells,
61 levels: 3,
62 );
64 // One colour a level, so the choice can be seen.
65 _paint = <Material>[
66 Material(name: 'fine', baseColor: Vector4(0.45, 0.75, 0.4, 1.0)),
67 Material(name: 'middle', baseColor: Vector4(0.85, 0.8, 0.35, 1.0)),
68 Material(name: 'coarse', baseColor: Vector4(0.85, 0.5, 0.35, 1.0)),
69 ];
70 for (var i = 0; i < _tiles.tilesX * _tiles.tilesZ; i++) {
71 _levels.add(null);
72 _nodes.add(
73 MeshNode(
74 DeviceMesh.upload(
75 context.device,
76 MeshBuilder(VertexLayout.standard).build(),
77 ),
78 _paint[0],
79 name: 'tile $i',
80 ),
81 );
82 }
83 _eye = MeshNode(
84 DeviceMesh.upload(
85 context.device,
86 SphereShape(segments: 16, radius: 0.6).build(),
87 ),
88 Material(name: 'eye', baseColor: Vector4(0.45, 0.65, 0.95, 1.0)),
89 name: 'eye',
90 );
91 final Scene scene = Scene()
92 ..ambientColor = Vector3(0.5, 0.55, 0.65)
93 ..ambientIntensity = 0.35
94 ..add(_eye);
95 for (final MeshNode node in _nodes) {
96 scene.add(node);
97 }
98 return scene..add(
99 LightNode(name: 'sun', intensity: 2.6)
100 ..setLocalForward(Vector3(-0.35, -1.0, -0.45)),
101 );
102 }
105 void update(DemoContext context, double dt) {
106 _clock += dt;
107 if (moving) eyeAngle = _clock * 0.35;
108 final double ex = 16.0 + 11.0 * math.cos(eyeAngle);
109 final double ez = 16.0 + 11.0 * math.sin(eyeAngle);
110 _eye.setPosition(ex, 2.5, ez);
111 // Rebuilt from the slider each frame; a tile only changes when the answer
112 // does, and the band round each threshold keeps it from flickering.
113 final TileLevelChooser chooser = TileLevelChooser(
114 nearest: nearest,
115 levels: 3,
116 );
117 for (var tz = 0; tz < _tiles.tilesZ; tz++) {
118 for (var tx = 0; tx < _tiles.tilesX; tx++) {
119 final int i = tz * _tiles.tilesX + tx;
120 final double dx = (tx + 0.5) * _tileCells - ex;
121 final double dz = (tz + 0.5) * _tileCells - ez;
122 final int level = chooser.choose(
123 math.sqrt(dx * dx + dz * dz),
124 _levels[i],
125 );
126 if (level == _levels[i]) continue;
127 _levels[i] = level;
128 final BrushSurface tile = _tiles.build(
129 tx,
130 tz,
131 level: level,
132 material: 'ground',
133 );
134 _nodes[i]
135 ..mesh = DeviceMesh.upload(_context.device, _meshOf(tile))
136 ..material = _paint[level];
137 }
138 }
139 }
141 MeshData _meshOf(BrushSurface tile) {
142 final MeshBuilder builder = MeshBuilder(VertexLayout.standard);
143 final Float32List p = tile.positions;
144 final Float32List n = tile.normals;
145 for (var v = 0; v < p.length ~/ 3; v++) {
146 builder.addVertex(
147 position: Vector3(p[3 * v], p[3 * v + 1], p[3 * v + 2]),
148 normal: Vector3(n[3 * v], n[3 * v + 1], n[3 * v + 2]),
149 );
150 }
151 for (var i = 0; i + 2 < tile.indices.length; i += 3) {
152 builder.addTriangle(
153 tile.indices[i],
154 tile.indices[i + 1],
155 tile.indices[i + 2],
156 );
157 }
158 return builder.build();
159 }
162 List<DemoControl> controls(DemoContext context) => <DemoControl>[
163 SliderControl(
164 'Fine detail within',
165 min: 4,
166 max: 16,
167 value: () => nearest,
168 onChanged: (double v) => nearest = v,
169 format: (double v) => '${v.toStringAsFixed(0)} m',
170 ),
171 ToggleControl(
172 'Move the eye',
173 value: () => moving,
174 onChanged: (bool v) => moving = v,
175 ),
176 SliderControl(
177 'Eye position',
178 min: 0,
179 max: 6.28,
180 value: () => eyeAngle % 6.28,
181 onChanged: (double v) {
182 moving = false;
183 eyeAngle = v;
184 },
185 format: (double v) => '${(v * 57.3).round()}°',
186 ),
187 ];
189 static String _run() {
190 // A gentle nine-by-nine slope, eight cells on a side.
191 final heights = Float32List(9 * 9);
192 for (var row = 0; row < 9; row++) {
193 for (var col = 0; col < 9; col++) {
194 heights[row * 9 + col] = (col + row) * 0.1;
195 }
196 }
197 final field = Heightfield(
198 columns: 9,
199 rows: 9,
200 cellSize: 1.0,
201 heights: heights,
202 );
204 final tiles = HeightfieldTiles(field, tileCells: 4, levels: 2);
205 final fine = tiles.build(0, 0, level: 0, material: 'ground');
206 final coarse = tiles.build(0, 0, level: 1, material: 'ground');
208 final chooser = TileLevelChooser(nearest: 20.0, levels: 2);
209 final near = chooser.choose(5.0, null);
210 final far = chooser.choose(60.0, null);
212 return '${tiles.tilesX}x${tiles.tilesZ} tiles, skirt depth '
213 '${tiles.skirtDepth.toStringAsFixed(3)} m\n'
214 'the fine build has ${fine.positions.length ~/ 3} vertices, the '
215 'coarse one has ${coarse.positions.length ~/ 3}\n'
216 'a tile 5 m away chooses level $near, one 60 m away chooses level $far';
217 }
220 void verify(Scene scene, FrameResult frame) {
221 if (frame.drawCalls < 1) {
222 throw StateError('the ground marker was not drawn');
223 }
224 if (!_report.contains('chooses level 0') ||
225 !_report.contains('chooses level 1')) {
226 throw StateError(
227 'a near tile should choose the fine level and a far '
228 'one the coarse level',
229 );
230 }
231 }
232}