flutter3d
Showcase Changelog 38 packages API reference

The material expression language: the source

since 0.7.0 Shading and materials

Regions the guide quotes: source (line 28), specialise (line 42), evaluate (line 50), report (line 80), check (line 105).

1/// The material expression language: a small GLSL-flavoured source parsed
2/// into a tree that either emits a `.frag` or is evaluated directly.
3///
4/// Quoted by `material_language.md` and shown whole in the Source tab.
5library;
6
7import 'dart:math' as math;
8
9import 'package:flutter/widgets.dart';
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:vector_math/vector_math.dart';
14final class MaterialLanguageDemo extends ShowcaseDemo {
15 late final MaterialProgram _specialised;
16 late final String _glsl;
17 late final List<double> _shaded;
19 static const double _albedo0 = 0.2;
20 static const double _albedo1 = 0.2;
21 static const double _albedo2 = 0.25;
22 static const double _alpha = 1.0;
23 static const double _nDotV = 0.2;
24 static const double _rimPower = 4.0;
26 @override
27 Scene build(DemoContext context) {
28 const String source = '''
29material RimLight {
30 param float rimPower = 2.0;
31 param vec3 rimColor = vec3(0.2, 0.6, 1.0);
33 fragment {
34 let facing = clamp(nDotV, 0.0, 1.0);
35 let rim = pow(1.0 - facing, rimPower);
36 return vec4(albedo + rimColor * rim, alpha);
37 }
39''';
40 final MaterialProgram program = parseMaterial(source);
42 _specialised = specialiseMaterial(
43 program,
44 const MaterialVariant('RimLight_sharp', {
45 'rimPower': <double>[_rimPower],
46 }),
47 );
48 _glsl = emitMaterialFragment(_specialised);
50 _shaded = evaluateMaterial(
51 _specialised,
52 MaterialSurfaceValues(
53 inputs: <String, List<double>>{
54 'albedo': <double>[_albedo0, _albedo1, _albedo2],
55 'alpha': <double>[_alpha],
56 'nDotV': <double>[_nDotV],
57 },
58 sample: (MaterialTextureSlot slot, double u, double v) =>
59 throw StateError('this material declares no texture'),
60 ),
61 );
63 return Scene()
64 ..add(
65 MeshNode(
66 DeviceMesh.upload(
67 context.device,
68 SphereShape(segments: 40, rings: 20).build(),
69 ),
70 Material(baseColor: Vector4(0.8, 0.65, 0.35, 1.0), roughness: 0.5),
71 name: 'ball',
72 ),
73 )
74 ..add(
75 LightNode(name: 'sun', intensity: 3.0)
76 ..setLocalForward(Vector3(-0.4, -0.7, -0.6)),
77 );
78 }
80 String _report() =>
81 'inputs this body reads: ${_specialised.inputsUsed.toList()..sort()}\n\n'
82 'evaluated at nDotV=$_nDotV: $_shaded\n\n'
83 '$_glsl';
85 @override
86 Widget? customBody(BuildContext buildContext, DemoContext context) =>
87 Container(
88 color: const Color(0xFF14161A),
89 padding: const EdgeInsets.all(24),
90 alignment: Alignment.topLeft,
91 child: SingleChildScrollView(
92 child: DefaultTextStyle(
93 style: const TextStyle(
94 color: Color(0xFFE8E8EC),
95 fontSize: 13,
96 fontFamily: 'monospace',
97 ),
98 child: Text(_report()),
99 ),
100 ),
101 );
103 @override
104 void verify(Scene scene, FrameResult frame) {
105 if (!_specialised.inputsUsed.containsAll(<String>[
106 'albedo',
107 'alpha',
108 'nDotV',
109 ])) {
110 throw StateError('the body should read albedo, alpha and nDotV');
111 }
112 final double facing = _nDotV.clamp(0.0, 1.0);
113 final double rim = math.pow(1.0 - facing, _rimPower).toDouble();
114 final List<double> expected = <double>[
115 _albedo0 + 0.2 * rim,
116 _albedo1 + 0.6 * rim,
117 _albedo2 + 1.0 * rim,
118 _alpha,
119 ];
120 for (var i = 0; i < expected.length; i++) {
121 if ((_shaded[i] - expected[i]).abs() > 1e-9) {
122 throw StateError('evaluateMaterial disagrees with the hand-worked sum');
123 }
124 }
125 if (!_glsl.contains('void main()')) {
126 throw StateError('the emitted fragment has no entry point');
127 }
128 if (frame.drawCalls < 1) {
129 throw StateError('the ball was not drawn');
130 }
131 }