Skip to content

Commit 0c0e8f2

Browse files
authored
feat: Refactor shader uniform binding to support shader arrays [flame_3d] (#3282)
Refactor shader uniform binding to support shader arrays. This also decouples the whole shader and uniform byte handling code (that we should definitely test) from the flutter_gpu primitives that are impossible to mock (base native classes). This adds tests that ensure the arrays are bound as they should - however the underlying flutter_gpu code does not seem to work. See [this PR](#3284) for a test of using this to support an arbitrary number of lights. Either way, we can merge this as is as this refactors the underlying structure to support arrays when ready, and make it more testable as well.
1 parent 4a77574 commit 0c0e8f2

File tree

11 files changed

+328
-54
lines changed

11 files changed

+328
-54
lines changed

examples/lib/stories/system/resize_example.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class ResizingRectangle extends RectangleComponent {
1313
void onGameResize(Vector2 size) {
1414
super.onGameResize(size);
1515

16-
this.size = size * .4;
16+
this.size = size * 0.4;
1717
}
1818
}
1919

packages/flame_3d/lib/src/resources/material/material.dart

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ abstract class Material extends Resource<gpu.RenderPipeline> {
1515
_fragmentShader = fragmentShader,
1616
super(
1717
gpu.gpuContext.createRenderPipeline(
18-
vertexShader.resource,
19-
fragmentShader.resource,
18+
vertexShader.compile().resource,
19+
fragmentShader.compile().resource,
2020
),
2121
);
2222

@@ -25,8 +25,8 @@ abstract class Material extends Resource<gpu.RenderPipeline> {
2525
var resource = super.resource;
2626
if (_recreateResource) {
2727
resource = super.resource = gpu.gpuContext.createRenderPipeline(
28-
_vertexShader.resource,
29-
_fragmentShader.resource,
28+
_vertexShader.compile().resource,
29+
_fragmentShader.compile().resource,
3030
);
3131
_recreateResource = false;
3232
}

packages/flame_3d/lib/src/resources/material/spatial_material.dart

+2-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import 'dart:ui';
33
import 'package:flame_3d/game.dart';
44
import 'package:flame_3d/graphics.dart';
55
import 'package:flame_3d/resources.dart';
6-
import 'package:flutter_gpu/gpu.dart' as gpu;
76

87
class SpatialMaterial extends Material {
98
SpatialMaterial({
@@ -14,7 +13,7 @@ class SpatialMaterial extends Material {
1413
}) : albedoTexture = albedoTexture ?? Texture.standard,
1514
super(
1615
vertexShader: Shader(
17-
_library['TextureVertex']!,
16+
name: 'TextureVertex',
1817
slots: [
1918
UniformSlot.value('VertexInfo', {
2019
'model',
@@ -28,7 +27,7 @@ class SpatialMaterial extends Material {
2827
],
2928
),
3029
fragmentShader: Shader(
31-
_library['TextureFragment']!,
30+
name: 'TextureFragment',
3231
slots: [
3332
UniformSlot.sampler('albedoTexture'),
3433
UniformSlot.value('Material', {
@@ -108,9 +107,5 @@ class SpatialMaterial extends Material {
108107
device.lightingInfo.apply(fragmentShader);
109108
}
110109

111-
static final _library = gpu.ShaderLibrary.fromAsset(
112-
'packages/flame_3d/assets/shaders/spatial_material.shaderbundle',
113-
)!;
114-
115110
static const _maxJoints = 16;
116111
}

packages/flame_3d/lib/src/resources/shader.dart

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export 'shader/shader.dart';
2+
export 'shader/uniform_array.dart';
23
export 'shader/uniform_instance.dart';
34
export 'shader/uniform_sampler.dart';
45
export 'shader/uniform_slot.dart';
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import 'dart:collection';
21
import 'dart:typed_data';
32
import 'dart:ui';
43

@@ -10,24 +9,48 @@ import 'package:flutter_gpu/gpu.dart' as gpu;
109
/// {@template shader}
1110
///
1211
/// {@endtemplate}
13-
class Shader extends Resource<gpu.Shader> {
12+
class ShaderResource extends Resource<gpu.Shader> {
13+
final Shader shader;
14+
1415
/// {@macro shader}
15-
Shader(
16+
ShaderResource(
1617
super.resource, {
18+
required String name,
1719
List<UniformSlot> slots = const [],
18-
}) : _slots = slots,
19-
_instances = {} {
20+
}) : shader = Shader(name: name, slots: slots) {
2021
for (final slot in slots) {
2122
slot.resource = resource.getUniformSlot(slot.name);
2223
}
2324
}
2425

25-
final List<UniformSlot> _slots;
26+
factory ShaderResource.create({
27+
required String name,
28+
required List<UniformSlot> slots,
29+
}) {
30+
final shader = _library[name];
31+
if (shader == null) {
32+
throw StateError('Shader "$name" not found in library');
33+
}
34+
return ShaderResource(shader, name: name, slots: slots);
35+
}
36+
37+
static final _library = gpu.ShaderLibrary.fromAsset(
38+
'packages/flame_3d/assets/shaders/spatial_material.shaderbundle',
39+
)!;
40+
}
2641

27-
final Map<String, UniformInstance> _instances;
42+
class Shader {
43+
final String name;
44+
final List<UniformSlot> slots;
45+
final Map<String, UniformInstance> instances = {};
46+
47+
Shader({
48+
required this.name,
49+
required this.slots,
50+
});
2851

2952
/// Set a [Texture] at the given [key] on the buffer.
30-
void setTexture(String key, Texture texture) => _setSampler(key, texture);
53+
void setTexture(String key, Texture texture) => _setTypedValue(key, texture);
3154

3255
/// Set a [Vector2] at the given [key] on the buffer.
3356
void setVector2(String key, Vector2 vector) => _setValue(key, vector.storage);
@@ -45,7 +68,7 @@ class Shader extends Resource<gpu.Shader> {
4568

4669
/// Set a [double] at the given [key] on the buffer.
4770
void setFloat(String key, double value) {
48-
_setValue(key, [value]);
71+
_setValue(key, _encodeFloat32(value));
4972
}
5073

5174
/// Set a [Matrix2] at the given [key] on the buffer.
@@ -60,50 +83,63 @@ class Shader extends Resource<gpu.Shader> {
6083
void setColor(String key, Color color) => _setValue(key, color.storage);
6184

6285
void bind(GraphicsDevice device) {
63-
for (final slot in _slots) {
64-
_instances[slot.name]?.bind(device);
86+
for (final slot in slots) {
87+
instances[slot.name]?.bind(device);
6588
}
6689
}
6790

6891
/// Set the [data] to the [UniformSlot] identified by [key].
69-
void _setValue(String key, List<double> data) {
70-
final (uniform, field) = _getInstance<UniformValue>(key);
71-
uniform[field!] = data;
92+
void _setValue(String key, Float32List data) {
93+
_setTypedValue(key, data.buffer);
7294
}
7395

74-
void _setSampler(String key, Texture data) {
75-
final (uniform, _) = _getInstance<UniformSampler>(key);
76-
uniform.resource = data;
96+
List<String?> parseKey(String key) {
97+
// examples: albedoTexture, Light[2].position, or Foo.bar
98+
final regex = RegExp(r'^(\w+)(?:\[(\d+)\])?(?:\.(\w+))?$');
99+
return regex.firstMatch(key)?.groups([1, 2, 3]) ?? [];
77100
}
78101

79102
/// Get the slot for the [key], it only calculates it once for every unique
80103
/// [key].
81-
(T, String?) _getInstance<T extends UniformInstance>(String key) {
82-
final keys = key.split('.');
83-
84-
// Check if we already have a uniform instance created.
85-
if (!_instances.containsKey(keys.first)) {
86-
// If the slot or it's property isn't mapped in the uniform it will be
87-
// enforced.
88-
final slot = _slots.firstWhere(
89-
(e) => e.name == keys.first,
90-
orElse: () => throw StateError('Uniform "$key" is unmapped'),
91-
);
104+
void _setTypedValue<K, T>(String key, T value) {
105+
final groups = parseKey(key);
92106

93-
final instance = slot.create();
94-
if (instance is UniformValue &&
95-
keys.length > 1 &&
96-
!slot.fields.contains(keys[1])) {
97-
throw StateError('Field "${keys[1]}" is unmapped for "${keys.first}"');
98-
}
107+
final object = groups[0]; // e.g. Light, albedoTexture
108+
final idx = _maybeParseInt(groups[1]); // e.g. 2 (optional)
109+
final field = groups[2]; // e.g. position (optional)
99110

100-
_instances[slot.name] = instance;
111+
if (object == null) {
112+
throw StateError('Uniform "$key" is missing an object');
101113
}
102114

103-
return (_instances[keys.first], keys.elementAtOrNull(1)) as (T, String?);
115+
final instance = instances.putIfAbsent(object, () {
116+
final slot = slots.firstWhere(
117+
(e) => e.name == object,
118+
orElse: () => throw StateError('Uniform "$object" is unmapped'),
119+
);
120+
return slot.create();
121+
}) as UniformInstance<K, T>;
122+
123+
final k = instance.makeKey(idx, field);
124+
instance.set(k, value);
104125
}
105126

106127
static Float32List _encodeUint32(int value, Endian endian) {
107128
return (ByteData(16)..setUint32(0, value, endian)).buffer.asFloat32List();
108129
}
130+
131+
static Float32List _encodeFloat32(double value) {
132+
return Float32List.fromList([value]);
133+
}
134+
135+
static int? _maybeParseInt(String? value) {
136+
if (value == null) {
137+
return null;
138+
}
139+
return int.parse(value);
140+
}
141+
142+
ShaderResource compile() {
143+
return ShaderResource.create(name: name, slots: slots);
144+
}
109145
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import 'dart:collection';
2+
import 'dart:typed_data';
3+
4+
import 'package:flame_3d/graphics.dart';
5+
import 'package:flame_3d/resources.dart';
6+
7+
typedef UniformArrayKey = ({
8+
int idx,
9+
String field,
10+
});
11+
12+
/// {@template uniform_value}
13+
/// Instance of a uniform array. Represented by a [ByteBuffer].
14+
/// {@endtemplate}
15+
class UniformArray extends UniformInstance<UniformArrayKey, ByteBuffer> {
16+
/// {@macro uniform_value}
17+
UniformArray(super.slot);
18+
19+
final List<Map<int, ({int hash, List<double> data})>> _storage = [];
20+
21+
@override
22+
ByteBuffer? get resource {
23+
if (super.resource == null) {
24+
final data = <double>[];
25+
for (final element in _storage) {
26+
var previousIndex = -1;
27+
for (final entry in element.entries) {
28+
if (previousIndex + 1 != entry.key) {
29+
final field = slot.fields.indexed
30+
.firstWhere((e) => e.$1 == previousIndex + 1);
31+
throw StateError(
32+
'Uniform ${slot.name}.${field.$2} was not set',
33+
);
34+
}
35+
previousIndex = entry.key;
36+
data.addAll(entry.value.data);
37+
}
38+
}
39+
super.resource = Float32List.fromList(data).buffer;
40+
}
41+
42+
return super.resource;
43+
}
44+
45+
Map<int, ({int hash, List<double> data})> _get(int idx) {
46+
while (idx >= _storage.length) {
47+
_storage.add(HashMap());
48+
}
49+
return _storage[idx];
50+
}
51+
52+
List<double>? get(int idx, String key) => _get(idx)[slot.indexOf(key)]?.data;
53+
54+
@override
55+
void set(UniformArrayKey key, ByteBuffer buffer) {
56+
final storage = _get(key.idx);
57+
final index = slot.indexOf(key.field);
58+
59+
// Ensure that we are only setting new data if the hash has changed.
60+
final data = buffer.asFloat32List();
61+
final hash = Object.hashAll(data);
62+
if (storage[index]?.hash == hash) {
63+
return;
64+
}
65+
66+
// Store the storage at the given slot index.
67+
storage[index] = (data: data, hash: hash);
68+
69+
// Clear the cache.
70+
super.resource = null;
71+
}
72+
73+
@override
74+
UniformArrayKey makeKey(int? idx, String? field) {
75+
if (idx == null) {
76+
throw StateError('idx is required for ${slot.name}');
77+
}
78+
if (field == null) {
79+
throw StateError('field is required for ${slot.name}');
80+
}
81+
82+
return (idx: idx, field: field);
83+
}
84+
85+
@override
86+
void bind(GraphicsDevice device) {
87+
device.bindUniform(slot.resource!, resource!);
88+
}
89+
}

packages/flame_3d/lib/src/resources/shader/uniform_instance.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import 'package:flame_3d/resources.dart';
55
/// An instance of a [UniformSlot] that can cache the [resource] that will be
66
/// bound to a [Shader].
77
/// {@endtemplate}
8-
abstract class UniformInstance<T> extends Resource<T?> {
8+
abstract class UniformInstance<K, T> extends Resource<T?> {
99
/// {@macro uniform_instance}
1010
UniformInstance(this.slot) : super(null);
1111

1212
/// The slot this instance belongs too.
1313
final UniformSlot slot;
1414

1515
void bind(GraphicsDevice device);
16+
17+
void set(K key, T value);
18+
19+
K makeKey(int? idx, String? field);
1620
}

packages/flame_3d/lib/src/resources/shader/uniform_sampler.dart

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@ import 'package:flame_3d/resources.dart';
44
/// {@template uniform_sampler}
55
/// Instance of a uniform sampler. Represented by a [Texture].
66
/// {@endtemplate}
7-
class UniformSampler extends UniformInstance<Texture> {
7+
class UniformSampler extends UniformInstance<void, Texture> {
88
/// {@macro uniform_sampler}
99
UniformSampler(super.slot);
1010

1111
@override
1212
void bind(GraphicsDevice device) {
1313
device.bindTexture(slot.resource!, resource!);
1414
}
15+
16+
@override
17+
void set(void key, Texture value) {
18+
resource = value;
19+
}
20+
21+
@override
22+
void makeKey(int? idx, String? field) {}
1523
}

packages/flame_3d/lib/src/resources/shader/uniform_slot.dart

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ class UniformSlot extends Resource<gpu.UniformSlot?> {
2121
UniformSlot.value(String name, Set<String> fields)
2222
: this._(name, fields, UniformValue.new);
2323

24+
/// {@macro uniform_slot}
25+
///
26+
/// Used for array uniforms in shaders.
27+
///
28+
/// The [fields] should be defined in order as they appear in the struct.
29+
UniformSlot.array(String name, Set<String> fields)
30+
: this._(name, fields, UniformArray.new);
31+
2432
/// {@macro uniform_slot}
2533
///
2634
/// Used for sampler uniforms in shaders.

0 commit comments

Comments
 (0)