Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Refactor shader uniform binding to support shader arrays [flame_3d] #3282

Merged
merged 2 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/lib/stories/system/resize_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ResizingRectangle extends RectangleComponent {
void onGameResize(Vector2 size) {
super.onGameResize(size);

this.size = size * .4;
this.size = size * 0.4;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated DCM

}
}

Expand Down
8 changes: 4 additions & 4 deletions packages/flame_3d/lib/src/resources/material/material.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ abstract class Material extends Resource<gpu.RenderPipeline> {
_fragmentShader = fragmentShader,
super(
gpu.gpuContext.createRenderPipeline(
vertexShader.resource,
fragmentShader.resource,
vertexShader.compile().resource,
fragmentShader.compile().resource,
),
);

Expand All @@ -25,8 +25,8 @@ abstract class Material extends Resource<gpu.RenderPipeline> {
var resource = super.resource;
if (_recreateResource) {
resource = super.resource = gpu.gpuContext.createRenderPipeline(
_vertexShader.resource,
_fragmentShader.resource,
_vertexShader.compile().resource,
_fragmentShader.compile().resource,
Copy link
Member Author

@luanpotter luanpotter Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to any other cleaner ways to make this testable

);
_recreateResource = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:ui';
import 'package:flame_3d/game.dart';
import 'package:flame_3d/graphics.dart';
import 'package:flame_3d/resources.dart';
import 'package:flutter_gpu/gpu.dart' as gpu;

class SpatialMaterial extends Material {
SpatialMaterial({
Expand All @@ -14,7 +13,7 @@ class SpatialMaterial extends Material {
}) : albedoTexture = albedoTexture ?? Texture.standard,
super(
vertexShader: Shader(
_library['TextureVertex']!,
name: 'TextureVertex',
slots: [
UniformSlot.value('VertexInfo', {
'model',
Expand All @@ -28,7 +27,7 @@ class SpatialMaterial extends Material {
],
),
fragmentShader: Shader(
_library['TextureFragment']!,
name: 'TextureFragment',
slots: [
UniformSlot.sampler('albedoTexture'),
UniformSlot.value('Material', {
Expand Down Expand Up @@ -108,9 +107,5 @@ class SpatialMaterial extends Material {
device.lightingInfo.apply(fragmentShader);
}

static final _library = gpu.ShaderLibrary.fromAsset(
'packages/flame_3d/assets/shaders/spatial_material.shaderbundle',
)!;

static const _maxJoints = 16;
}
1 change: 1 addition & 0 deletions packages/flame_3d/lib/src/resources/shader.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'shader/shader.dart';
export 'shader/uniform_array.dart';
export 'shader/uniform_instance.dart';
export 'shader/uniform_sampler.dart';
export 'shader/uniform_slot.dart';
Expand Down
108 changes: 72 additions & 36 deletions packages/flame_3d/lib/src/resources/shader/shader.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:collection';
import 'dart:typed_data';
import 'dart:ui';

Expand All @@ -10,24 +9,48 @@ import 'package:flutter_gpu/gpu.dart' as gpu;
/// {@template shader}
///
/// {@endtemplate}
class Shader extends Resource<gpu.Shader> {
class ShaderResource extends Resource<gpu.Shader> {
final Shader shader;

/// {@macro shader}
Shader(
ShaderResource(
super.resource, {
required String name,
List<UniformSlot> slots = const [],
}) : _slots = slots,
_instances = {} {
}) : shader = Shader(name: name, slots: slots) {
for (final slot in slots) {
slot.resource = resource.getUniformSlot(slot.name);
}
}

final List<UniformSlot> _slots;
factory ShaderResource.create({
required String name,
required List<UniformSlot> slots,
}) {
final shader = _library[name];
if (shader == null) {
throw StateError('Shader "$name" not found in library');
}
return ShaderResource(shader, name: name, slots: slots);
}

static final _library = gpu.ShaderLibrary.fromAsset(
'packages/flame_3d/assets/shaders/spatial_material.shaderbundle',
)!;
}

final Map<String, UniformInstance> _instances;
class Shader {
final String name;
final List<UniformSlot> slots;
final Map<String, UniformInstance> instances = {};

Shader({
required this.name,
required this.slots,
});

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

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

/// Set a [double] at the given [key] on the buffer.
void setFloat(String key, double value) {
_setValue(key, [value]);
_setValue(key, _encodeFloat32(value));
}

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

void bind(GraphicsDevice device) {
for (final slot in _slots) {
_instances[slot.name]?.bind(device);
for (final slot in slots) {
instances[slot.name]?.bind(device);
}
}

/// Set the [data] to the [UniformSlot] identified by [key].
void _setValue(String key, List<double> data) {
final (uniform, field) = _getInstance<UniformValue>(key);
uniform[field!] = data;
void _setValue(String key, Float32List data) {
_setTypedValue(key, data.buffer);
}

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

/// Get the slot for the [key], it only calculates it once for every unique
/// [key].
(T, String?) _getInstance<T extends UniformInstance>(String key) {
final keys = key.split('.');

// Check if we already have a uniform instance created.
if (!_instances.containsKey(keys.first)) {
// If the slot or it's property isn't mapped in the uniform it will be
// enforced.
final slot = _slots.firstWhere(
(e) => e.name == keys.first,
orElse: () => throw StateError('Uniform "$key" is unmapped'),
);
void _setTypedValue<K, T>(String key, T value) {
final groups = parseKey(key);

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

_instances[slot.name] = instance;
if (object == null) {
throw StateError('Uniform "$key" is missing an object');
}

return (_instances[keys.first], keys.elementAtOrNull(1)) as (T, String?);
final instance = instances.putIfAbsent(object, () {
final slot = slots.firstWhere(
(e) => e.name == object,
orElse: () => throw StateError('Uniform "$object" is unmapped'),
);
return slot.create();
}) as UniformInstance<K, T>;

final k = instance.makeKey(idx, field);
instance.set(k, value);
}

static Float32List _encodeUint32(int value, Endian endian) {
return (ByteData(16)..setUint32(0, value, endian)).buffer.asFloat32List();
}

static Float32List _encodeFloat32(double value) {
return Float32List.fromList([value]);
}

static int? _maybeParseInt(String? value) {
if (value == null) {
return null;
}
return int.parse(value);
}

ShaderResource compile() {
return ShaderResource.create(name: name, slots: slots);
}
}
89 changes: 89 additions & 0 deletions packages/flame_3d/lib/src/resources/shader/uniform_array.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import 'dart:collection';
import 'dart:typed_data';

import 'package:flame_3d/graphics.dart';
import 'package:flame_3d/resources.dart';

typedef UniformArrayKey = ({
int idx,
String field,
});

/// {@template uniform_value}
/// Instance of a uniform array. Represented by a [ByteBuffer].
/// {@endtemplate}
class UniformArray extends UniformInstance<UniformArrayKey, ByteBuffer> {
/// {@macro uniform_value}
UniformArray(super.slot);

final List<Map<int, ({int hash, List<double> data})>> _storage = [];

@override
ByteBuffer? get resource {
if (super.resource == null) {
final data = <double>[];
for (final element in _storage) {
var previousIndex = -1;
for (final entry in element.entries) {
if (previousIndex + 1 != entry.key) {
final field = slot.fields.indexed
.firstWhere((e) => e.$1 == previousIndex + 1);
throw StateError(
'Uniform ${slot.name}.${field.$2} was not set',
);
}
previousIndex = entry.key;
data.addAll(entry.value.data);
}
}
super.resource = Float32List.fromList(data).buffer;
}

return super.resource;
}

Map<int, ({int hash, List<double> data})> _get(int idx) {
while (idx >= _storage.length) {
_storage.add(HashMap());
}
return _storage[idx];
}

List<double>? get(int idx, String key) => _get(idx)[slot.indexOf(key)]?.data;

@override
void set(UniformArrayKey key, ByteBuffer buffer) {
final storage = _get(key.idx);
final index = slot.indexOf(key.field);

// Ensure that we are only setting new data if the hash has changed.
final data = buffer.asFloat32List();
final hash = Object.hashAll(data);
if (storage[index]?.hash == hash) {
return;
}

// Store the storage at the given slot index.
storage[index] = (data: data, hash: hash);

// Clear the cache.
super.resource = null;
}

@override
UniformArrayKey makeKey(int? idx, String? field) {
if (idx == null) {
throw StateError('idx is required for ${slot.name}');
}
if (field == null) {
throw StateError('field is required for ${slot.name}');
}

return (idx: idx, field: field);
}

@override
void bind(GraphicsDevice device) {
device.bindUniform(slot.resource!, resource!);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import 'package:flame_3d/resources.dart';
/// An instance of a [UniformSlot] that can cache the [resource] that will be
/// bound to a [Shader].
/// {@endtemplate}
abstract class UniformInstance<T> extends Resource<T?> {
abstract class UniformInstance<K, T> extends Resource<T?> {
/// {@macro uniform_instance}
UniformInstance(this.slot) : super(null);

/// The slot this instance belongs too.
final UniformSlot slot;

void bind(GraphicsDevice device);

void set(K key, T value);

K makeKey(int? idx, String? field);
}
10 changes: 9 additions & 1 deletion packages/flame_3d/lib/src/resources/shader/uniform_sampler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ import 'package:flame_3d/resources.dart';
/// {@template uniform_sampler}
/// Instance of a uniform sampler. Represented by a [Texture].
/// {@endtemplate}
class UniformSampler extends UniformInstance<Texture> {
class UniformSampler extends UniformInstance<void, Texture> {
/// {@macro uniform_sampler}
UniformSampler(super.slot);

@override
void bind(GraphicsDevice device) {
device.bindTexture(slot.resource!, resource!);
}

@override
void set(void key, Texture value) {
resource = value;
}

@override
void makeKey(int? idx, String? field) {}
}
8 changes: 8 additions & 0 deletions packages/flame_3d/lib/src/resources/shader/uniform_slot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ class UniformSlot extends Resource<gpu.UniformSlot?> {
UniformSlot.value(String name, Set<String> fields)
: this._(name, fields, UniformValue.new);

/// {@macro uniform_slot}
///
/// Used for array uniforms in shaders.
///
/// The [fields] should be defined in order as they appear in the struct.
UniformSlot.array(String name, Set<String> fields)
: this._(name, fields, UniformArray.new);

/// {@macro uniform_slot}
///
/// Used for sampler uniforms in shaders.
Expand Down
Loading