From ce0ca9f95f96e94aa32eb6cac8cf7163c39b99ec Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Tue, 24 Mar 2026 10:12:51 +0100 Subject: [PATCH] feat(flame_3d): Allow shader uniform structures use array elements --- .../shaders/spatial_material.shaderbundle | Bin 213208 -> 52600 bytes .../lib/src/resources/light/light.dart | 6 +- .../src/resources/light/lighting_info.dart | 14 ++-- .../resources/material/spatial_material.dart | 2 +- .../lib/src/resources/shader/shader.dart | 11 +-- .../src/resources/shader/uniform_value.dart | 48 ++++++++++--- .../flame_3d/shaders/spatial_material.frag | 64 ++++------------- .../flame_3d/shaders/spatial_material.vert | 66 ++---------------- .../shader/uniform_binding_test.dart | 9 ++- .../test/shaders/shader_compilation_test.dart | 48 +++++++++++++ 10 files changed, 131 insertions(+), 137 deletions(-) create mode 100644 packages/flame_3d/test/shaders/shader_compilation_test.dart diff --git a/packages/flame_3d/assets/shaders/spatial_material.shaderbundle b/packages/flame_3d/assets/shaders/spatial_material.shaderbundle index 2521a622aacd1310be60ff5baf49067cf3443f7e..99f87218c797cee712262c818a3a826402fbb06d 100644 GIT binary patch literal 52600 zcmeI53!Gg?ec!Ji7<&;y1_1^fb7U#%6<)3P{jhAYvV|=Zo2WrCq>5Ny?cTL6>_gdI z*^+s52_~T>am?eBj}y$Jlu(+sV2DcyrNp(V>$ZFdgnocFBVUgc?(r&oRvfx0f0WaK}sa*@k#y5;4!^eiq-^$ZS4a;37hasipcJ)?4o zlZCw5T4N6QJCvR(0XvlLRoW(c@K05`NhR=CD535q;XV?|hbm8TWmPLX`J>BV*aJ)^ z=P>wzshLjhN0skTI*`<_RGACC*o0oJNAJ^AM+Q15bD3V~pDx%A!44`}J(c%k0Y4t_ z)Kfg|M;{mZUZT3`qs(RcumKogV6+pRHwmyq@pr;_bD0d0;3ZHw%)FMhhR4-Sv|Ftv0(mqmaM>z9r6ouC_hIf+L9?f zSMnI^)WKh+I^}MqN@e-QR_}N9h#z$I_l+C=i0XgvjGM&tP7e>+lVU^Lz*u5f*O@nzmFRVN$9JVutkx89#2<+Lae@uC%WICw-mJ)bu`kher zZM(?X_Fn{}otCf-zAr189O%EMRH-bFTU!|4;IEL!7|-|*9a~gJA24XCGq!=5?5(PE zLH~?^Gydmp)vx~K*Y{gH-l{qT(kD=!JS1EOZkZBt@gG8T>Z(g3&m zGgW8KzDYdDewOOc05|#DRn~teA3X9I!_QS>Zkl}ja9M!=q@*G5d8z{=agotJ^fP|1 zP-5&-XMA3%#Mpd=bRci1%FqBenY&a5PU0d%=9@zsb_;-B=&+T0!d{iJl{t?t#!HW2 zzz+vMUac}POIt!4dIi(}N{3DCzKVL9txmVvCeDFTi(F4sJ1MHuuj81T7 z%e5+F3plgo#VVr*KbX96m7xV^embEOdyXc?esso=>JK>*H8Q4c9VM=*j37NpZEt;QJjy|sMsSchx zaO|XfKt0dgeT@?OXfHDGCHBt*9CXaVxj@HfpwZ#;`G7N@pHv+fiHl5Poj&Hrf&l2_ zK4@}i&>Hoj72_`n(6મxH&Ysxols%=TBQ3>`S*omUxKQ{JCb9eB#S6zISi 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 XKWq07<2^FoKU377-zWEBUDf|DxGwm} literal 213208 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 diff --git a/packages/flame_3d/lib/src/resources/light/light.dart b/packages/flame_3d/lib/src/resources/light/light.dart index b6b584ed2b8..25e9b492794 100644 --- a/packages/flame_3d/lib/src/resources/light/light.dart +++ b/packages/flame_3d/lib/src/resources/light/light.dart @@ -23,8 +23,8 @@ class Light extends Resource { void createResource() {} void apply(int index, Shader shader) { - shader.setVector3('Light$index.position', transform.position); - shader.setColor('Light$index.color', source.color); - shader.setFloat('Light$index.intensity', source.intensity); + shader.setVector3('Lights.positions[$index]', transform.position); + shader.setColor('Lights.colors[$index]', source.color); + shader.setFloat('Lights.intensities[$index]', source.intensity); } } diff --git a/packages/flame_3d/lib/src/resources/light/lighting_info.dart b/packages/flame_3d/lib/src/resources/light/lighting_info.dart index 872ead38c30..6af1280649a 100644 --- a/packages/flame_3d/lib/src/resources/light/lighting_info.dart +++ b/packages/flame_3d/lib/src/resources/light/lighting_info.dart @@ -16,14 +16,13 @@ class LightingInfo { void _applyPointLights(Shader shader) { final pointLights = lights.where((e) => e.source is PointLight); final numLights = pointLights.length; - if (numLights > 3) { - // temporary, until we support dynamic arrays - throw Exception('At most 3 point lights are allowed'); + if (numLights > _maxPointLights) { + throw Exception('At most $_maxPointLights point lights are allowed'); } // NOTE: using floats because Android GLES does not support integer uniforms // Refer to https://github.com/flutter/engine/pull/55329 - shader.setFloat('LightsInfo.numLights', numLights.toDouble()); + shader.setFloat('Lights.numLights', numLights.toDouble()); for (final (index, light) in pointLights.indexed) { light.apply(index, shader); } @@ -40,11 +39,10 @@ class LightingInfo { return ambient.first.source as AmbientLight; } + static const _maxPointLights = 8; + static List shaderSlots = [ UniformSlot.value('AmbientLight'), - UniformSlot.value('LightsInfo'), - UniformSlot.value('Light0'), - UniformSlot.value('Light1'), - UniformSlot.value('Light2'), + UniformSlot.value('Lights'), ]; } diff --git a/packages/flame_3d/lib/src/resources/material/spatial_material.dart b/packages/flame_3d/lib/src/resources/material/spatial_material.dart index a0facfa49f5..36380c11567 100644 --- a/packages/flame_3d/lib/src/resources/material/spatial_material.dart +++ b/packages/flame_3d/lib/src/resources/material/spatial_material.dart @@ -66,7 +66,7 @@ class SpatialMaterial extends Material { ); } for (final (index, transform) in jointTransforms.indexed) { - vertexShader.setMatrix4('JointMatrices.joint$index', transform); + vertexShader.setMatrix4('JointMatrices.joints[$index]', transform); } } diff --git a/packages/flame_3d/lib/src/resources/shader/shader.dart b/packages/flame_3d/lib/src/resources/shader/shader.dart index 3e3be2e0045..782b67a40a0 100644 --- a/packages/flame_3d/lib/src/resources/shader/shader.dart +++ b/packages/flame_3d/lib/src/resources/shader/shader.dart @@ -106,8 +106,9 @@ class Shader { } List parseKey(String key) { - // examples: albedoTexture, Light[2].position, or Foo.bar - final regex = RegExp(r'^(\w+)(?:\[(\d+)\])?(?:\.(\w+))?$'); + // examples: albedoTexture, Lights.positions[0], JointMatrices.joints[5], + // Foo.bar + final regex = RegExp(r'^(\w+)(?:\.(\w+))?(?:\[(\d+)\])?$'); return regex.firstMatch(key)?.groups([1, 2, 3]) ?? []; } @@ -116,9 +117,9 @@ class Shader { void _setTypedValue(String key, T value) { final groups = parseKey(key); - final object = groups[0]; // e.g. Light, albedoTexture - final index = _maybeParseInt(groups[1]); // e.g. 2 (optional) - final field = groups[2]; // e.g. position (optional) + final object = groups[0]; // e.g. Lights, albedoTexture + final field = groups[1]; // e.g. positions (optional) + final index = _maybeParseInt(groups[2]); // e.g. 0 (optional) if (object == null) { throw StateError('Uniform "$key" is missing an object'); diff --git a/packages/flame_3d/lib/src/resources/shader/uniform_value.dart b/packages/flame_3d/lib/src/resources/shader/uniform_value.dart index a5de640489d..c5836a63b05 100644 --- a/packages/flame_3d/lib/src/resources/shader/uniform_value.dart +++ b/packages/flame_3d/lib/src/resources/shader/uniform_value.dart @@ -9,6 +9,9 @@ import 'package:flame_3d/resources.dart'; /// /// The `[]` operator can be used to set the raw data of a field. If the data is /// different from the last set it will recalculated the [resource]. +/// +/// Both scalar fields (`'fieldName'`) and array elements (`'fieldName[index]'`) +/// are supported. Array element offsets are computed using std140 stride rules. /// {@endtemplate} class UniformValue extends UniformInstance { /// {@macro uniform_value} @@ -25,12 +28,20 @@ class UniformValue extends UniformInstance { } final buffer = ByteData(sizeInBytes); - for (final MapEntry(key: field, value: entry) in _storage.entries) { - final offset = gpuSlot.getMemberOffsetInBytes(field); - if (offset == null) { + for (final MapEntry(:key, value: entry) in _storage.entries) { + final (field, index) = _parseMemberKey(key); + + final memberOffset = gpuSlot.getMemberOffsetInBytes(field); + if (memberOffset == null) { throw StateError('Field "$field" not found in uniform "${slot.name}"'); } + final stride = _std140ArrayStride(entry.data.lengthInBytes); + final offset = switch (index) { + final i? => memberOffset + i * stride, + _ => memberOffset, + }; + final bytes = entry.data.buffer.asUint8List( entry.data.offsetInBytes, entry.data.lengthInBytes, @@ -46,9 +57,10 @@ class UniformValue extends UniformInstance { Float32List? operator [](String key) => _storage[key]?.data; void operator []=(String key, Float32List data) { + final (field, _) = _parseMemberKey(key); assert( - !slot.isCompiled || slot.resource!.getMemberOffsetInBytes(key) != null, - 'Field "$key" not found in uniform "${slot.name}"', + !slot.isCompiled || slot.resource!.getMemberOffsetInBytes(field) != null, + 'Field "$field" not found in uniform "${slot.name}"', ); final hash = Object.hashAll(data); @@ -62,13 +74,12 @@ class UniformValue extends UniformInstance { @override String makeKey(int? index, String? field) { - if (index != null) { - throw StateError('index is not supported for ${slot.name}'); - } if (field == null) { throw StateError('field is required for ${slot.name}'); } - + if (index != null) { + return '$field[$index]'; + } return field; } @@ -81,4 +92,23 @@ class UniformValue extends UniformInstance { void set(String key, ByteBuffer value) { this[key] = value.asFloat32List(); } + + /// Parse a storage key into member name and the array index (if any): + /// - `"positions[0]"` becomes `("positions", 0)` + /// - `"numLights"` becomes `("numLights", null)` + static (String name, int? index) _parseMemberKey(String key) { + final bracket = key.indexOf('['); + if (bracket == -1) { + return (key, null); + } + + final name = key.substring(0, bracket); + final index = int.parse(key.substring(bracket + 1, key.length - 1)); + return (name, index); + } + + /// Std140 array element stride: round up to 16-byte boundary. + static int _std140ArrayStride(int elementBytes) { + return (elementBytes + 15) & ~15; + } } diff --git a/packages/flame_3d/shaders/spatial_material.frag b/packages/flame_3d/shaders/spatial_material.frag index af57faa8a7e..dd87626ef1a 100644 --- a/packages/flame_3d/shaders/spatial_material.frag +++ b/packages/flame_3d/shaders/spatial_material.frag @@ -2,7 +2,7 @@ // implementation based on https://learnopengl.com/PBR/Lighting -// #define NUM_LIGHTS 8 +#define MAX_LIGHTS 8 #define PI 3.14159265359 #define EPSILON 0.0001 @@ -30,33 +30,12 @@ uniform AmbientLight { float intensity; } ambientLight; -uniform LightsInfo { +uniform Lights { float numLights; -} lightsInfo; - -// uniform Light { -// vec3 position; -// vec3 color; -// float intensity; -// } lights[NUM_LIGHTS]; - -uniform Light0 { - vec3 position; - vec4 color; - float intensity; -} light0; - -uniform Light1 { - vec3 position; - vec4 color; - float intensity; -} light1; - -uniform Light2 { - vec3 position; - vec4 color; - float intensity; -} light2; + vec3 positions[MAX_LIGHTS]; + vec4 colors[MAX_LIGHTS]; + float intensities[MAX_LIGHTS]; +} lights; // camera info @@ -150,28 +129,13 @@ void main() { vec3 lo = vec3(0.0); - if (lightsInfo.numLights > 0) { - vec3 light0Pos = light0.position; - vec3 light0Color = light0.color.rgb; - float light0Intensity = light0.intensity; - - lo += processLight(light0Pos, light0Color, light0Intensity, baseColor, normal, viewDir, diffuse); - } - - if (lightsInfo.numLights > 1) { - vec3 light1Pos = light1.position; - vec3 light1Color = light1.color.rgb; - float light1Intensity = light1.intensity; - - lo += processLight(light1Pos, light1Color, light1Intensity, baseColor, normal, viewDir, diffuse); - } - - if (lightsInfo.numLights > 2) { - vec3 light2Pos = light2.position; - vec3 light2Color = light2.color.rgb; - float light2Intensity = light2.intensity; - - lo += processLight(light2Pos, light2Color, light2Intensity, baseColor, normal, viewDir, diffuse); + for (int i = 0; i < int(lights.numLights); i++) { + lo += processLight( + lights.positions[i], + lights.colors[i].rgb, + lights.intensities[i], + baseColor, normal, viewDir, diffuse + ); } vec3 color = ambient + lo; @@ -180,4 +144,4 @@ void main() { color = pow(color, vec3(1.0 / 2.2)); outColor = vec4(color, 1.0); -} \ No newline at end of file +} diff --git a/packages/flame_3d/shaders/spatial_material.vert b/packages/flame_3d/shaders/spatial_material.vert index f7dfc0b4966..4337d802a22 100644 --- a/packages/flame_3d/shaders/spatial_material.vert +++ b/packages/flame_3d/shaders/spatial_material.vert @@ -19,72 +19,18 @@ uniform VertexInfo { } vertex_info; uniform JointMatrices { - mat4 joint0; - mat4 joint1; - mat4 joint2; - mat4 joint3; - mat4 joint4; - mat4 joint5; - mat4 joint6; - mat4 joint7; - mat4 joint8; - mat4 joint9; - mat4 joint10; - mat4 joint11; - mat4 joint12; - mat4 joint13; - mat4 joint14; - mat4 joint15; -} joints; - -mat4 jointMat(float jointIndex) { - if (jointIndex == 0.0) { - return joints.joint0; - } else if (jointIndex == 1.0) { - return joints.joint1; - } else if (jointIndex == 2.0) { - return joints.joint2; - } else if (jointIndex == 3.0) { - return joints.joint3; - } else if (jointIndex == 4.0) { - return joints.joint4; - } else if (jointIndex == 5.0) { - return joints.joint5; - } else if (jointIndex == 6.0) { - return joints.joint6; - } else if (jointIndex == 7.0) { - return joints.joint7; - } else if (jointIndex == 8.0) { - return joints.joint8; - } else if (jointIndex == 9.0) { - return joints.joint9; - } else if (jointIndex == 10.0) { - return joints.joint10; - } else if (jointIndex == 11.0) { - return joints.joint11; - } else if (jointIndex == 12.0) { - return joints.joint12; - } else if (jointIndex == 13.0) { - return joints.joint13; - } else if (jointIndex == 14.0) { - return joints.joint14; - } else if (jointIndex == 15.0) { - return joints.joint15; - } else { - return mat4(0.0); - } -} + mat4 joints[16]; +} jointMatrices; mat4 computeSkinMatrix() { if (vertexWeights.x == 0.0 && vertexWeights.y == 0.0 && vertexWeights.z == 0.0 && vertexWeights.w == 0.0) { - // no weights, skip skinning return mat4(1.0); } - return vertexWeights.x * jointMat(vertexJoints.x) + - vertexWeights.y * jointMat(vertexJoints.y) + - vertexWeights.z * jointMat(vertexJoints.z) + - vertexWeights.w * jointMat(vertexJoints.w); + return vertexWeights.x * jointMatrices.joints[int(vertexJoints.x)] + + vertexWeights.y * jointMatrices.joints[int(vertexJoints.y)] + + vertexWeights.z * jointMatrices.joints[int(vertexJoints.z)] + + vertexWeights.w * jointMatrices.joints[int(vertexJoints.w)]; } void main() { diff --git a/packages/flame_3d/test/resources/shader/uniform_binding_test.dart b/packages/flame_3d/test/resources/shader/uniform_binding_test.dart index 13e698d2243..4c8bb1f84a0 100644 --- a/packages/flame_3d/test/resources/shader/uniform_binding_test.dart +++ b/packages/flame_3d/test/resources/shader/uniform_binding_test.dart @@ -38,10 +38,17 @@ void main() { ); // Simple struct field - expect(shader.parseKey('Foo.bar'), ['Foo', null, 'bar']); + expect(shader.parseKey('Foo.bar'), ['Foo', 'bar', null]); // Direct name (sampler) expect(shader.parseKey('albedoTexture'), ['albedoTexture', null, null]); + + // Array index + expect(shader.parseKey('Lights.position[0]'), [ + 'Lights', + 'position', + '0', + ]); }); }); } diff --git a/packages/flame_3d/test/shaders/shader_compilation_test.dart b/packages/flame_3d/test/shaders/shader_compilation_test.dart new file mode 100644 index 00000000000..eef679ab145 --- /dev/null +++ b/packages/flame_3d/test/shaders/shader_compilation_test.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Shader compilation', () { + test('shader bundles are up-to-date with sources', () async { + final packageRoot = Directory.current; + final assetsDir = Directory.fromUri( + packageRoot.uri.resolve('assets/shaders'), + ); + + final bundles = assetsDir + .listSync() + .whereType() + .where((f) => f.path.endsWith('.shaderbundle')) + .toList(); + + final before = { + for (final bundle in bundles) bundle.path: bundle.readAsBytesSync(), + }; + + // Recompile shaders from source. + final result = await Process.run( + 'dart', + ['run', 'bin/build_shaders.dart'], + workingDirectory: packageRoot.path, + ); + + expect( + result.exitCode, + equals(0), + reason: 'Shader compilation failed:\n${result.stderr}', + ); + + for (final bundle in bundles) { + final name = bundle.uri.pathSegments.last; + expect( + bundle.readAsBytesSync(), + equals(before[bundle.path]), + reason: + 'Shader bundle "$name" is out of sync with its sources. ' + 'Run: dart run bin/build_shaders.dart', + ); + } + }); + }); +}