diff --git a/packages/flame_3d/assets/shaders/spatial_material.shaderbundle b/packages/flame_3d/assets/shaders/spatial_material.shaderbundle index 2521a622aac..99f87218c79 100644 Binary files a/packages/flame_3d/assets/shaders/spatial_material.shaderbundle and b/packages/flame_3d/assets/shaders/spatial_material.shaderbundle differ 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', + ); + } + }); + }); +}