Skip to content

Commit 03f390e

Browse files
committed
feat: Refactor shader uniform binding to support shader arrays
1 parent 9694579 commit 03f390e

File tree

10 files changed

+327
-53
lines changed

10 files changed

+327
-53
lines changed

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

Lines changed: 4 additions & 4 deletions
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

Lines changed: 2 additions & 7 deletions
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,13 +13,13 @@ 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', {'model', 'view', 'projection'}),
2019
],
2120
),
2221
fragmentShader: Shader(
23-
_library['TextureFragment']!,
22+
name: 'TextureFragment',
2423
slots: [
2524
UniformSlot.sampler('albedoTexture'),
2625
UniformSlot.value('Material', {
@@ -85,8 +84,4 @@ class SpatialMaterial extends Material {
8584
void _applyLights(GraphicsDevice device) {
8685
device.lightingInfo.apply(fragmentShader);
8786
}
88-
89-
static final _library = gpu.ShaderLibrary.fromAsset(
90-
'packages/flame_3d/assets/shaders/spatial_material.shaderbundle',
91-
)!;
9287
}

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

Lines changed: 1 addition & 0 deletions
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';
Lines changed: 72 additions & 36 deletions
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
}
Lines changed: 89 additions & 0 deletions
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

Lines changed: 5 additions & 1 deletion
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

Lines changed: 9 additions & 1 deletion
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

Lines changed: 8 additions & 0 deletions
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)