Lightmaps: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: uv (line 34), bake (line 37), live (line 61).
1/// A baked lightmap: indirect light stored in a texture ahead of time, read
2/// through a second coordinate the vertex colour carries.
3///
4/// Quoted by `lightmaps.md` and shown whole in the Source tab.
5library;
7import 'dart:typed_data';
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:vector_math/vector_math.dart';
13const int _lightmapSize = 4;
15final class LightmapsDemo extends ShowcaseDemo {
16 bool baked = true;
18 late final MeshData _floorMesh;
19 late final ByteData _lightmapPixels;
20 late final TextureHandle _lightmap;
21 late final Material _floor;
24 void configureView(DemoContext context) {
25 context.orbit
26 ..distance = 4.0
27 ..pitch = 0.55
28 ..yaw = 0.0;
29 context.orbit.target.setValues(0.0, 0.0, 0.0);
30 }
33 Scene build(DemoContext context) {
34 _floorMesh = const PlaneShape(width: 3, depth: 3).build();
35 _useTexcoordAsLightmapUv(_floorMesh);
37 _lightmapPixels = _bakeBrightCorner();
38 _lightmap = context.device.createTextureFromPixels(
39 width: _lightmapSize,
40 height: _lightmapSize,
41 format: TextureFormat.r8g8b8a8UNormInt,
42 pixels: _lightmapPixels,
43 )!;
44 _floor = Material(
45 name: 'floor',
46 baseColor: Vector4(0.8, 0.8, 0.8, 1.0),
47 doubleSided: true,
48 )..lightmap = _lightmap;
50 final MeshNode floor = MeshNode(
51 DeviceMesh.upload(context.device, _floorMesh),
52 _floor,
53 name: 'floor',
54 )..lightmapped = true;
56 return Scene()
57 ..ambientIntensity = 0.0
58 ..add(floor);
59 }
62 void update(DemoContext context, double dt) {
63 _floor.lightmap = baked ? _lightmap : null;
64 }
67 List<DemoControl> controls(DemoContext context) => <DemoControl>[
68 ToggleControl(
69 'Lightmap on',
70 value: () => baked,
71 onChanged: (bool v) => baked = v,
72 ),
73 ];
76 void verify(Scene scene, FrameResult frame) {
77 final MeshNode floor = scene.meshes.first;
78 if (!floor.lightmapped) {
79 throw StateError('the floor was never marked lightmapped');
80 }
81 final VertexLayout layout = _floorMesh.layout;
82 final int stride = layout.floatsPerVertex;
83 final int uvAt = layout.floatOffsetOf(VertexLayout.texcoord.name);
84 final int colorAt = layout.floatOffsetOf(VertexLayout.color.name);
85 for (var v = 0; v < _floorMesh.vertexCount; v++) {
86 final int base = v * stride;
87 final bool matches =
88 _floorMesh.vertices[base + colorAt] ==
89 _floorMesh.vertices[base + uvAt] &&
90 _floorMesh.vertices[base + colorAt + 1] ==
91 _floorMesh.vertices[base + uvAt + 1];
92 if (!matches) {
93 throw StateError('vertex $v carries a tint, not its lightmap UV');
94 }
95 }
96 final int bright = _lightmapPixels.getUint8(0);
97 final int stride2 = _lightmapSize * 4;
98 final int lastRow = stride2 * (_lightmapSize - 1);
99 final int dark = _lightmapPixels.getUint8(
100 lastRow + (_lightmapSize - 1) * 4,
101 );
102 if (bright <= dark) {
103 throw StateError('the baked lightmap has no bright corner to read');
104 }
105 }
106}
108/// Copies each vertex's texture coordinate into its colour, which is the
109/// place `MeshNode.lightmapped` reads a lightmap at.
110void _useTexcoordAsLightmapUv(MeshData mesh) {
111 final VertexLayout layout = mesh.layout;
112 final int stride = layout.floatsPerVertex;
113 final int uvAt = layout.floatOffsetOf(VertexLayout.texcoord.name);
114 final int colorAt = layout.floatOffsetOf(VertexLayout.color.name);
115 for (var v = 0; v < mesh.vertexCount; v++) {
116 final int base = v * stride;
117 mesh.vertices[base + colorAt] = mesh.vertices[base + uvAt];
118 mesh.vertices[base + colorAt + 1] = mesh.vertices[base + uvAt + 1];
119 }
120}
122/// A small lightmap, RGBM-encoded: bright over the corner at UV (0, 0) and
123/// dim everywhere else, so the two ends of the bake read differently.
124ByteData _bakeBrightCorner() {
125 final ByteData pixels = ByteData(_lightmapSize * _lightmapSize * 4);
126 for (var y = 0; y < _lightmapSize; y++) {
127 for (var x = 0; x < _lightmapSize; x++) {
128 final bool corner = x == 0 && y == 0;
129 final (int, int, int) rgb = corner ? (64, 48, 16) : (2, 2, 3);
130 final int at = (y * _lightmapSize + x) * 4;
131 pixels
132 ..setUint8(at, rgb.$1)
133 ..setUint8(at + 1, rgb.$2)
134 ..setUint8(at + 2, rgb.$3)
135 ..setUint8(at + 3, 255);
136 }
137 }
138 return pixels;
139}