flutter3d
Showcase Changelog 38 packages API reference

Normal maps: the source

since 0.1.0 or earlier Shading and materials

Regions the guide quotes: texture (line 34), upload (line 59), material (line 68), tangents (line 77), live (line 94).

1/// A normal map: fine surface relief painted into a texture, with no extra
2/// triangles.
3///
4/// Quoted by `normal_mapping.md` and shown whole in the Source tab.
5library;
6
7import 'dart:math' as math;
8import 'dart:typed_data';
9
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:vector_math/vector_math.dart';
14final class NormalMappingDemo extends ShowcaseDemo {
15 double normalScale = 1.0;
16 bool useMap = true;
17 double sunTurn = 0.6;
19 static const int _size = 128;
20 static const int _cobbles = 4;
22 late final Material _ball;
23 late final LightNode _sun;
24 late final TextureHandle _normalMap;
26 @override
27 void configureView(DemoContext context) {
28 context.orbit
29 ..distance = 3.2
30 ..yaw = 0.0
31 ..pitch = 0.1;
32 }
34 /// A grid of round cobbles. Each texel stores the direction the surface
35 /// faces there, x, y and z scaled from -1..1 into 0..255.
36 ByteData _cobbleNormals() {
37 final Uint8List bytes = Uint8List(_size * _size * 4);
38 const int cell = _size ~/ _cobbles;
39 for (var y = 0; y < _size; y++) {
40 for (var x = 0; x < _size; x++) {
41 final double u = ((x % cell) + 0.5) / cell * 2.0 - 1.0;
42 final double v = 1.0 - ((y % cell) + 0.5) / cell * 2.0;
43 final double r2 = u * u + v * v;
44 final Vector3 n = r2 < 0.85
45 ? Vector3(u * 0.9, v * 0.9, math.sqrt(1.0 - r2 * 0.81)).normalized()
46 : Vector3(0.0, 0.0, 1.0);
47 final int at = (y * _size + x) * 4;
48 bytes[at] = ((n.x * 0.5 + 0.5) * 255).round();
49 bytes[at + 1] = ((n.y * 0.5 + 0.5) * 255).round();
50 bytes[at + 2] = ((n.z * 0.5 + 0.5) * 255).round();
51 bytes[at + 3] = 255;
52 }
53 }
54 return bytes.buffer.asByteData();
55 }
57 @override
58 Scene build(DemoContext context) {
59 final ByteData pixels = _cobbleNormals();
60 _normalMap = context.device.createTextureFromPixels(
61 width: _size,
62 height: _size,
63 format: TextureFormat.r8g8b8a8UNormInt,
64 pixels: pixels,
65 mipLevels: MipChain.build(pixels, _size, _size),
66 )!;
68 _ball = Material(
69 name: 'cobbles',
70 baseColor: Vector4(0.62, 0.6, 0.56, 1.0),
71 roughness: 0.55,
72 normal: _normalMap,
73 normalSampler: SamplerOptions.trilinearRepeat,
74 normalScale: normalScale,
75 );
77 final MeshData data = SphereShape(segments: 64, rings: 32)
78 .build(layout: VertexLayout.positionNormalTexcoord)
79 .withGeneratedTangents(target: VertexLayout.standard);
80 final MeshNode ball = MeshNode(
81 DeviceMesh.upload(context.device, data),
82 _ball,
83 name: 'ball',
84 );
86 _sun = LightNode(name: 'sun', intensity: 3.0);
87 return Scene()
88 ..add(ball)
89 ..add(_sun);
90 }
92 @override
93 void update(DemoContext context, double dt) {
94 _ball
95 ..normal = useMap ? _normalMap : null
96 ..normalScale = normalScale;
97 _sun.setLocalForward(Vector3(-math.sin(sunTurn), -0.3, -math.cos(sunTurn)));
98 }
100 @override
101 List<DemoControl> controls(DemoContext context) => <DemoControl>[
102 SliderControl(
103 'Normal scale',
104 min: 0,
105 max: 3,
106 value: () => normalScale,
107 onChanged: (double v) => normalScale = v,
108 ),
109 ToggleControl(
110 'Normal map',
111 value: () => useMap,
112 onChanged: (bool v) => useMap = v,
113 ),
114 SliderControl(
115 'Sun direction',
116 min: -1.4,
117 max: 1.4,
118 value: () => sunTurn,
119 onChanged: (double v) => sunTurn = v,
120 ),
121 ];
123 @override
124 void verify(Scene scene, FrameResult frame) {
125 final Material material = scene.meshes.first.material;
126 if (material.normal == null) {
127 throw StateError('the ball has no normal map bound');
128 }
129 final MeshData? source = (scene.meshes.first.mesh as DeviceMesh).source;
130 if (source == null || !source.layout.has(VertexLayout.tangent)) {
131 throw StateError('the mesh carries no tangents for the map to use');
132 }
133 if (frame.drawCalls < 1) throw StateError('the ball was not drawn');
134 }