Rendering in tiles: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: base (line 18), tile (line 92).
1/// Rendering a scene as a grid of tiles, one at a time, for a picture larger
2/// than any single render target. Every tile is drawn into its own cell of
3/// this page's frame, so the whole picture can be seen put back together.
4///
5/// Quoted by `tiled_render.md` and shown whole in the Source tab.
6library;
8import 'dart:math' as math;
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:vector_math/vector_math.dart';
14final class TiledRenderDemo extends ShowcaseDemo {
15 int tilesAcross = 3;
16 double gap = 0.0;
18 final Projection _base = const PerspectiveProjection(fovYRadians: 0.9);
20 static const int _largestGrid = 4;
22 /// One camera per tile, kept apart from the viewport's own: each carries a
23 /// different crop of the same view.
24 final List<CameraNode> _cameras = <CameraNode>[];
27 void configureView(DemoContext context) {
28 context.orbit
29 ..distance = 7.0
30 ..pitch = 0.3
31 ..yaw = 0.0;
32 }
35 Scene build(DemoContext context) {
36 final Scene scene = Scene()
37 ..ambientColor = Vector3(0.5, 0.55, 0.65)
38 ..ambientIntensity = 0.25
39 ..add(
40 LightNode(name: 'sun', intensity: 3.0)
41 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
42 );
44 // Enough that no tile is empty: a corner of the frame with nothing in it
45 // would say nothing about whether the pieces fit.
46 scene.add(
47 MeshNode(
48 DeviceMesh.upload(
49 context.device,
50 const PlaneShape(width: 16.0, depth: 10.0).build(),
51 ),
52 Material(name: 'floor', baseColor: Vector4(0.4, 0.45, 0.42, 1.0)),
53 name: 'floor',
54 )..setPosition(0.0, -0.6, 0.0),
55 );
56 final DeviceMesh ball = DeviceMesh.upload(
57 context.device,
58 SphereShape(segments: 32, rings: 16).build(),
59 );
60 final DeviceMesh box = DeviceMesh.upload(
61 context.device,
62 CuboidShape(size: Vector3.all(1.2)).build(),
63 );
64 final DeviceMesh ring = DeviceMesh.upload(
65 context.device,
66 TorusShape(radius: 0.7, tubeRadius: 0.25).build(),
67 );
68 Material colour(String name, double r, double g, double b) =>
69 Material(name: name, baseColor: Vector4(r, g, b, 1.0), roughness: 0.6);
70 scene
71 ..add(
72 MeshNode(ball, colour('ball', 0.9, 0.4, 0.3), name: 'ball')
73 ..setPosition(-2.6, 0.0, 0.0),
74 )
75 ..add(
76 MeshNode(box, colour('box', 0.3, 0.6, 0.9), name: 'box')
77 ..setPosition(0.0, 0.0, 0.0),
78 )
79 ..add(
80 MeshNode(ring, colour('ring', 0.4, 0.8, 0.5), name: 'ring')
81 ..setPosition(2.6, 0.0, 0.0),
82 );
84 for (var i = 0; i < _largestGrid * _largestGrid; i++) {
85 final CameraNode camera = CameraNode(name: 'tile $i');
86 _cameras.add(camera);
87 scene.add(camera);
88 }
89 return scene;
90 }
93 List<RenderView> views(DemoContext context) {
94 final int n = tilesAcross;
95 final double cell = 1.0 / n;
96 final double inset = cell * gap / 2;
97 // Cells that tile exactly leave a hairline wherever a cell edge falls
98 // between two pixels, which reads as the pieces not fitting. Reaching a
99 // hair over the neighbour's edge (its own tile is drawn after and takes
100 // the pixels back) closes it; pulled apart, there is nothing to close.
101 final double reach = gap == 0.0 ? 0.002 : 0.0;
102 final Vector3 eye = context.camera.readPosition();
103 final Quaternion facing = context.camera.readRotation();
104 return <RenderView>[
105 for (var row = 0; row < n; row++)
106 for (var column = 0; column < n; column++)
107 RenderView(
108 // The same eye every tile looks from: what differs is which
109 // square of the picture it keeps.
110 camera: _cameras[row * n + column]
111 ..setPositionFrom(eye)
112 ..setRotation(facing)
113 ..projection = TiledProjection(
114 _base,
115 tileX: column,
116 tileY: row,
117 tilesX: n,
118 tilesY: n,
119 ),
120 viewportFraction: ViewportRect(
121 column * cell + inset,
122 row * cell + inset,
123 math.min(cell - 2 * inset + reach, 1.0 - column * cell - inset),
124 math.min(cell - 2 * inset + reach, 1.0 - row * cell - inset),
125 ),
126 ),
127 ];
128 }
131 List<DemoControl> controls(DemoContext context) => <DemoControl>[
132 ChoiceControl(
133 'Grid',
134 options: const <String>['1x1', '2x2', '3x3', '4x4'],
135 index: () => tilesAcross - 1,
136 onChanged: (int i) => tilesAcross = i + 1,
137 ),
138 SliderControl(
139 'Pull apart',
140 min: 0,
141 max: 0.4,
142 value: () => gap,
143 onChanged: (double v) => gap = v,
144 format: (double v) => v.toStringAsFixed(2),
145 ),
146 ];
149 void verify(Scene scene, FrameResult frame) {
150 if (frame.drawCalls < tilesAcross * tilesAcross) {
151 throw StateError('every tile of the grid should have drawn something');
152 }
153 // A 1x1 grid has exactly one tile, and that tile is the whole frustum:
154 // the crop this projection applies must be the identity in that case,
155 // whatever grid is currently on screen.
156 const double aspect = 320 / 180;
157 final Matrix4 whole = _base.toMatrix(aspect);
158 final Matrix4 singleTile = TiledProjection(
159 _base,
160 tileX: 0,
161 tileY: 0,
162 tilesX: 1,
163 tilesY: 1,
164 ).toMatrix(aspect);
165 for (var i = 0; i < 16; i++) {
166 if ((whole.storage[i] - singleTile.storage[i]).abs() > 1e-9) {
167 throw StateError('a single tile did not reproduce the whole frustum');
168 }
169 }
170 }
171}