Tone-map curves: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: lamps (line 32), upload (line 58), table (line 68), exposure (line 116), curve (line 117), display (line 118).
1/// The curve that squeezes light of any brightness into a picture a display can
2/// show, the curves the engine has, and a display transform baked by the page.
3///
4/// Quoted by `tone_mapping.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/pages/post/post_stage.dart';
12import 'package:flutter3d_showcase/src/demo/demo.dart';
13import 'package:vector_math/vector_math.dart';
15final class ToneMappingDemo extends ShowcaseDemo {
16 int curve = 0;
17 int display = 0;
18 double exposure = 1.6;
20 static const List<TonemapCurve> _curves = TonemapCurve.values;
22 late final TextureHandle _table;
25 void configureView(DemoContext context) =>
26 PostStage.frame(context, distance: 7.0);
29 Scene build(DemoContext context) {
30 final PostStage stage = PostStage.build(context, sunIntensity: 6.0);
32 // The lamps light nothing, so they hang on the side the sun comes from,
33 // where the lit faces of the shapes already look.
34 final DeviceMesh ball = DeviceMesh.upload(
35 context.device,
36 const SphereShape(radius: 0.28, segments: 24, rings: 12).build(),
37 );
38 const List<List<double>> colours = <List<double>>[
39 <double>[1.0, 0.1, 0.05],
40 <double>[0.1, 0.9, 0.2],
41 <double>[0.1, 0.25, 1.0],
42 ];
43 for (var i = 0; i < colours.length; i++) {
44 stage.scene.add(
45 MeshNode(
46 ball,
47 Material(
48 name: 'lamp $i',
49 baseColor: Vector4(0.05, 0.05, 0.05, 1.0),
50 emissive: Vector3(colours[i][0], colours[i][1], colours[i][2]),
51 emissiveStrength: 6.0,
52 ),
53 name: 'lamp $i',
54 )..setPosition((i - 1) * 1.1 + 0.5, 2.3, 0.8),
55 );
56 }
58 _table = context.device.createTextureFromPixels(
59 width: _size * _size,
60 height: _size,
61 format: TextureFormat.r16g16b16a16Float,
62 pixels: _bake(),
63 )!;
65 return stage.scene;
66 }
68 /// Entries per axis of the page's own table.
69 static const int _size = 17;
71 /// A display transform in the strip shape the engine reads: `_size` slices
72 /// of `_size` by `_size`, blue picking the slice, red across and green down.
73 /// Entry `i` holds the output for the input `0.18 * 2^(-10 + 20 * i / 16)`.
74 ///
75 /// The curve is Reinhard on the brightest channel, with the other two
76 /// scaled by the same amount so the hue holds. Nothing here walks towards
77 /// white: a bright red stays red however bright it gets.
78 static ByteData _bake() {
79 final ByteData strip = ByteData(_size * _size * _size * 8);
80 double input(int i) =>
81 0.18 * math.pow(2.0, -10.0 + 20.0 * i / (_size - 1)).toDouble();
82 for (var b = 0; b < _size; b++) {
83 for (var g = 0; g < _size; g++) {
84 for (var r = 0; r < _size; r++) {
85 final List<double> colour = <double>[input(r), input(g), input(b)];
86 final double peak = colour.reduce(math.max);
87 final double scale = 1.0 / (1.0 + peak);
88 final int at = (g * _size * _size + b * _size + r) * 8;
89 for (var c = 0; c < 3; c++) {
90 strip.setUint16(
91 at + c * 2,
92 _half(colour[c] * scale),
93 Endian.little,
94 );
95 }
96 strip.setUint16(at + 6, _half(1.0), Endian.little);
97 }
98 }
99 }
100 return strip;
101 }
103 /// [value], from 0 to 1, as the bits of a half float.
104 ///
105 /// It truncates rather than rounds and flushes subnormals to nought, which
106 /// is harmless here: the error is under a thousandth of a display value.
107 static int _half(double value) {
108 final int bits = (ByteData(4)..setFloat32(0, value)).getUint32(0);
109 final int exponent = ((bits >> 23) & 0xff) - 127 + 15;
110 if (exponent <= 0) return 0;
111 return (exponent << 10) | ((bits >> 13) & 0x3ff);
112 }
115 RenderSettings settings(DemoContext context) => RenderSettings(
116 exposure: exposure,
117 tonemapCurve: _curves[curve],
118 look: LookSettings(
119 displayTransform: display == 1
120 ? DisplayTransform(texture: _table, size: _size)
121 : null,
122 ),
123 );
126 List<DemoControl> controls(DemoContext context) => <DemoControl>[
127 ChoiceControl(
128 'Curve',
129 options: <String>[for (final TonemapCurve c in _curves) c.name],
130 index: () => curve,
131 onChanged: (int i) => curve = i,
132 ),
133 ChoiceControl(
134 'Display transform',
135 options: const <String>['none', 'page table'],
136 index: () => display,
137 onChanged: (int i) => display = i,
138 ),
139 SliderControl(
140 'Exposure',
141 min: 0.2,
142 max: 6,
143 value: () => exposure,
144 onChanged: (double v) => exposure = v,
145 ),
146 ];
149 void verify(Scene scene, FrameResult frame) {
150 if (!passRan(frame, 'composite')) {
151 throw StateError(
152 'the composite pass, which applies the curve, is absent',
153 );
154 }
155 if ((frame.exposure - exposure).abs() > 1e-6) {
156 throw StateError('the frame was exposed at ${frame.exposure}');
157 }
158 }
159}