diff --git a/.github/.cspell/gamedev_dictionary.txt b/.github/.cspell/gamedev_dictionary.txt
index e9c15ceeb56..639095fbfc4 100644
--- a/.github/.cspell/gamedev_dictionary.txt
+++ b/.github/.cspell/gamedev_dictionary.txt
@@ -1,6 +1,7 @@
# general development-adjacent terms and expressions
AABB # axis aligned bounding box
abelian # Abelian Group, also known as commutative group
+alignof # alignment of
ARGB # alpha red green blue
arities # plural of arity
backgrounded # moving the app to the background
@@ -58,7 +59,12 @@ tileset # image with a collection of tiles. in games, tiles are small square spr
tilesets # plural of tileset
truecolor # truecolor rendering
tweening # the process of tween
+uncapturederror # dumb webgpu javascript name
+unorm # unsigned normalized integer
viewports # plural of viewport
WASD # movement keys on a keyboard
WBMP # wireless bitmap image format
+webgpu # gpu standard for web browsers
WebP # WebP image format
+WGSL # WGSL shader language
+wgslbundle # WGSL shader bundle format
\ No newline at end of file
diff --git a/packages/flame_3d/README.md b/packages/flame_3d/README.md
index 527a9564dc2..a1932f27484 100644
--- a/packages/flame_3d/README.md
+++ b/packages/flame_3d/README.md
@@ -17,14 +17,16 @@ Adds 3D support for Flame us
---
+
+
# flame_3d
-This package provides an experimental implementation of 3D support for Flame. The main focus is to
-explore the potential capabilities of 3D for Flame while providing a familiar API to existing Flame
-developers.
+This package provides an experimental implementation of 3D support for Flame.
+The main focus is to explore the potential capabilities of 3D for Flame while
+providing a familiar API to existing Flame developers.
Supported platforms:
@@ -35,41 +37,48 @@ Supported platforms:
| macOS | ✅ |
| Windows | ❌ |
| Linux | ❌ |
-| Web | ❌ |
+| Web | ⚠️¹ |
+
+⚠️¹ Web support is experimental, see [Web support](#web-support-experimental)
+below.
## Prologue
-**STOP**, we know you are hyped up and want to start coding some funky 3D stuff but we first have to
-set your expectations and clarify some things. So turn down your music, put away the coffee and make
-some tea instead because you have to do some reading first!
+**STOP**, we know you are hyped up and want to start coding some funky 3D stuff
+but we first have to set your expectations and clarify some things. So turn down
+your music, put away the coffee and make some tea instead because you have to do
+some reading first!
-This package provides 3D support for Flame but it depends on the still experimental
-[Flutter GPU](https://github.com/flutter/flutter/wiki/Flutter-GPU), which in turn depends on
-Impeller.
+This package provides 3D support for Flame but it depends on the still
+experimental [Flutter GPU](https://github.com/flutter/flutter/wiki/Flutter-GPU),
+which in turn depends on Impeller.
Therefore, this package is also experimental; you can check our
[Roadmap](https://github.com/flame-engine/flame/blob/main/packages/flame_3d/ROADMAP.md)
for more details on our plans and what is currently supported.
-This package does not guarantee that it will follow correct [semver](https://semver.org/) versioning
-rules, nor does it assure that its APIs wont break. Be ready to constantly have to refactor your
-code if you are planning on using this package, and potentially to have to contribute with
+This package does not guarantee that it will follow correct
+[semver](https://semver.org/) versioning rules, nor does it assure that its APIs
+wont break. Be ready to constantly have to refactor your code if you are
+planning on using this package, and potentially to have to contribute with
improvements and fixes. Please do not use this for production environments.
-Documentation and tests might be lacking for quite a while because of the potential constant changes
-of the API. Where possible, we will try to provide in-code documentation and code examples to help
-developers but our main goal for now is to enable the usage of 3D rendering within a Flame
-ecosystem.
+Documentation and tests might be lacking for quite a while because of the
+potential constant changes of the API. Where possible, we will try to provide
+in-code documentation and code examples to help developers but our main goal for
+now is to enable the usage of 3D rendering within a Flame ecosystem.
## Prerequisites
-In order to use flame_3d, you will need to ensure a few things. Firstly, the only platforms that we
-have explicitly tested so far for support were Android, iOS, and macOS.
+In order to use flame_3d, you will need to ensure a few things. Firstly, the
+only platforms that we have explicitly tested so far for support were Android,
+iOS, and macOS.
-Then, you need to enable Impeller, if not already enabled by default. For example, for macOS, add
-the following to the generated `macos/runner/Info.plist` directory:
+Then, you need to enable Impeller, if not already enabled by default. For
+example, for macOS, add the following to the generated `macos/runner/Info.plist`
+directory:
```xml
@@ -87,43 +96,71 @@ Alternatively, you can run Flutter with this flag instead:
flutter run --enable-flutter-gpu
```
-Now everything is set up you can start doing some 3D magic! You can check out the
-[example](https://github.com/flame-engine/flame/tree/main/packages/flame_3d/example) to see how you
-can set up a simple 3D environment using Flame.
+Now everything is set up you can start doing some 3D magic! You can check out
+the
+[example](https://github.com/flame-engine/flame/tree/main/packages/flame_3d/example)
+to see how you can set up a simple 3D environment using Flame.
+
+Also check our more advanced examples,
+[Collect the Donut](https://github.com/luanpotter/collect_the_donut) and
+[Defend the Donut](https://github.com/flame-engine/defend_the_donut).
+
-Also check our more advanced examples, [Collect the Donut](https://github.com/luanpotter/collect_the_donut)
-and [Defend the Donut](https://github.com/flame-engine/defend_the_donut).
+## Web support (experimental)
+
+Flame 3D also runs on the web, though this is **even more experimental** than
+the rest of the package. Flutter GPU does not run in the browser (for now), so
+on web `flame_3d` renders through the browser's native
+[WebGPU](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API) API
+instead, via a separate rendering backend. If you want to run on web add the
+following before your `runApp` call in `main.dart`:
+
+```dart
+await GpuBackend.initialize();
+```
## Building shaders
-If you are using the `SpatialMaterial` provided by `flame_3d`, you do not need to worry about shaders.
+If you are using the materials provided by `flame_3d`, you do not need to worry
+about shaders.
-That being said, you can write your own shaders and use them on custom materials.
-Currently, Flutter does not do the bundling of shaders for us so this package provides a simple
-Dart script. Create your fragment and vertex shader in a `shaders` directory,
-make sure the file names are identical. Like so:
+That being said, you can write your own shaders and use them on custom
+materials. Currently, Flutter does not do the bundling of shaders for us so this
+package provides a simple Dart script. Create your fragment and vertex shader in
+a `shaders` directory, make sure the file names are identical. Like so:
- `my_custom_shader`.frag
- `my_custom_shader`.vert
-You can then run `dart pub run flame_3d:build_shaders` to bundle the shaders. They will
-automatically be placed in `assets/shaders`.
+You can then run `dart pub run flame_3d:build_shaders` to bundle the shaders.
+They will automatically be placed in `assets/shaders`.
+
+For [web support](#web-support-experimental), also pass `--with-web-gpu` to
+build the WebGPU shader bundles. That step needs the `naga` CLI on your `PATH`
+(`cargo install naga-cli`).
+
+Shaders can also reuse shared GLSL through `#include`. An
+`#include ` resolves against the `shaders/` directory of
+any package in your dependency graph, so chunks can be shared across packages.
+Including ones that `flame_3d` itself ships, such as
+`#include ` for vertex skinning.
You can check out the
-[default shaders](https://github.com/flame-engine/flame/tree/main/packages/flame_3d/shaders) if you
-want to have some examples.
+[default shaders](https://github.com/flame-engine/flame/tree/main/packages/flame_3d/shaders)
+if you want to have some examples.
## Contributing
-Have you found a bug or have a suggestion of how to enhance the 3D APIs? Open an issue and we will
-take a look at it as soon as possible.
+Have you found a bug or have a suggestion of how to enhance the 3D APIs? Open an
+issue and we will take a look at it as soon as possible.
-Do you want to contribute with a PR? PRs are always welcome, just make sure to create it from the
-correct branch (main) and follow the [checklist](.github/pull_request_template.md) which will
-appear when you open the PR.
+Do you want to contribute with a PR? PRs are always welcome, just make sure to
+create it from the correct branch (main) and follow the
+[checklist](.github/pull_request_template.md) which will appear when you open
+the PR.
-For bigger changes, or if in doubt, make sure to talk about your contribution to the team. Either
-via an issue, GitHub discussion, or reach out to the team using the
-[Discord server](https://discord.gg/pxrBmy4).
+For bigger changes, or if in doubt, make sure to talk about your contribution to
+the team. Either via an issue, GitHub discussion, or reach out to the team using
+the [Discord server](https://discord.gg/pxrBmy4).
diff --git a/packages/flame_3d/assets/shaders/spatial_material.shaderbundle b/packages/flame_3d/assets/shaders/spatial_material.shaderbundle
index 759cf057518..2977cf8d3b8 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/assets/shaders/spatial_material.wgslbundle b/packages/flame_3d/assets/shaders/spatial_material.wgslbundle
new file mode 100644
index 00000000000..23a932c7cca
--- /dev/null
+++ b/packages/flame_3d/assets/shaders/spatial_material.wgslbundle
@@ -0,0 +1,67 @@
+{
+ "vertex": "struct JointMatrices {\n joints: array, 16>,\n}\n\nstruct VertexInfo {\n model: mat4x4,\n view: mat4x4,\n projection: mat4x4,\n}\n\nstruct VertexOutput {\n @location(0) fragTexCoord: vec2,\n @location(1) fragColor: vec4,\n @location(2) fragPosition: vec3,\n @location(3) fragNormal: vec3,\n @builtin(position) gl_Position: vec4,\n}\n\nvar vertexPosition_1: vec3;\nvar vertexTexCoord_1: vec2;\nvar vertexColor_1: vec4;\nvar vertexNormal_1: vec3;\nvar vertexJoints_1: vec4;\nvar vertexWeights_1: vec4;\n@group(0) @binding(0) \nvar jointMatrices: JointMatrices;\nvar fragTexCoord: vec2;\nvar fragColor: vec4;\nvar fragPosition: vec3;\nvar fragNormal: vec3;\n@group(0) @binding(1) \nvar vertex_info: VertexInfo;\nvar gl_Position: vec4;\n\nfn computeSkinMatrix() -> mat4x4 {\n let _e9 = vertexWeights_1;\n let _e13 = vertexWeights_1;\n let _e18 = vertexWeights_1;\n let _e23 = vertexWeights_1;\n if ((((_e9.x == 0f) && (_e13.y == 0f)) && (_e18.z == 0f)) && (_e23.w == 0f)) {\n {\n return mat4x4(vec4(1f, 0f, 0f, 0f), vec4(0f, 1f, 0f, 0f), vec4(0f, 0f, 1f, 0f), vec4(0f, 0f, 0f, 1f));\n }\n }\n let _e35 = vertexWeights_1;\n let _e37 = vertexJoints_1;\n let _e42 = jointMatrices.joints[i32(_e37.x)];\n let _e44 = vertexWeights_1;\n let _e46 = vertexJoints_1;\n let _e51 = jointMatrices.joints[i32(_e46.y)];\n let _e54 = vertexWeights_1;\n let _e56 = vertexJoints_1;\n let _e61 = jointMatrices.joints[i32(_e56.z)];\n let _e64 = vertexWeights_1;\n let _e66 = vertexJoints_1;\n let _e71 = jointMatrices.joints[i32(_e66.w)];\n return ((((_e35.x * _e42) + (_e44.y * _e51)) + (_e54.z * _e61)) + (_e64.w * _e71));\n}\n\nfn main_1() {\n var skinMatrix: mat4x4;\n var position: vec3;\n var normal: vec3;\n var modelViewProjection: mat4x4;\n\n let _e20 = computeSkinMatrix();\n skinMatrix = _e20;\n let _e22 = skinMatrix;\n let _e23 = vertexPosition_1;\n position = (_e22 * vec4(_e23.x, _e23.y, _e23.z, 1f)).xyz;\n let _e32 = skinMatrix;\n let _e33 = vertexNormal_1;\n normal = normalize((_e32 * vec4(_e33.x, _e33.y, _e33.z, 0f)).xyz);\n let _e43 = vertex_info;\n let _e45 = vertex_info;\n let _e48 = vertex_info;\n modelViewProjection = ((_e43.projection * _e45.view) * _e48.model);\n let _e53 = modelViewProjection;\n let _e54 = position;\n gl_Position = (_e53 * vec4(_e54.x, _e54.y, _e54.z, 1f));\n let _e61 = vertexTexCoord_1;\n fragTexCoord = _e61;\n let _e62 = vertexColor_1;\n fragColor = _e62;\n let _e63 = vertex_info;\n let _e65 = position;\n fragPosition = vec3((_e63.model * vec4(_e65.x, _e65.y, _e65.z, 1f)).xyz);\n let _e74 = vertex_info;\n let _e77 = transpose(_naga_inverse_4x4_f32(_e74.model));\n let _e87 = normal;\n fragNormal = (mat3x3(_e77[0].xyz, _e77[1].xyz, _e77[2].xyz) * _e87);\n let _e90 = gl_Position;\n let _e92 = gl_Position;\n gl_Position.z = ((_e90.z + _e92.w) * 0.5f);\n return;\n}\n\n@vertex \nfn main(@location(0) vertexPosition: vec3, @location(1) vertexTexCoord: vec2, @location(2) vertexColor: vec4, @location(3) vertexNormal: vec3, @location(4) vertexJoints: vec4, @location(5) vertexWeights: vec4) -> VertexOutput {\n vertexPosition_1 = vertexPosition;\n vertexTexCoord_1 = vertexTexCoord;\n vertexColor_1 = vertexColor;\n vertexNormal_1 = vertexNormal;\n vertexJoints_1 = vertexJoints;\n vertexWeights_1 = vertexWeights;\n main_1();\n let _e43 = fragTexCoord;\n let _e45 = fragColor;\n let _e47 = fragPosition;\n let _e49 = fragNormal;\n let _e51 = gl_Position;\n return VertexOutput(_e43, _e45, _e47, _e49, _e51);\n}\n\nfn _naga_inverse_4x4_f32(m: mat4x4) -> mat4x4 {\n let sub_factor00: f32 = m[2][2] * m[3][3] - m[3][2] * m[2][3];\n let sub_factor01: f32 = m[2][1] * m[3][3] - m[3][1] * m[2][3];\n let sub_factor02: f32 = m[2][1] * m[3][2] - m[3][1] * m[2][2];\n let sub_factor03: f32 = m[2][0] * m[3][3] - m[3][0] * m[2][3];\n let sub_factor04: f32 = m[2][0] * m[3][2] - m[3][0] * m[2][2];\n let sub_factor05: f32 = m[2][0] * m[3][1] - m[3][0] * m[2][1];\n let sub_factor06: f32 = m[1][2] * m[3][3] - m[3][2] * m[1][3];\n let sub_factor07: f32 = m[1][1] * m[3][3] - m[3][1] * m[1][3];\n let sub_factor08: f32 = m[1][1] * m[3][2] - m[3][1] * m[1][2];\n let sub_factor09: f32 = m[1][0] * m[3][3] - m[3][0] * m[1][3];\n let sub_factor10: f32 = m[1][0] * m[3][2] - m[3][0] * m[1][2];\n let sub_factor11: f32 = m[1][1] * m[3][3] - m[3][1] * m[1][3];\n let sub_factor12: f32 = m[1][0] * m[3][1] - m[3][0] * m[1][1];\n let sub_factor13: f32 = m[1][2] * m[2][3] - m[2][2] * m[1][3];\n let sub_factor14: f32 = m[1][1] * m[2][3] - m[2][1] * m[1][3];\n let sub_factor15: f32 = m[1][1] * m[2][2] - m[2][1] * m[1][2];\n let sub_factor16: f32 = m[1][0] * m[2][3] - m[2][0] * m[1][3];\n let sub_factor17: f32 = m[1][0] * m[2][2] - m[2][0] * m[1][2];\n let sub_factor18: f32 = m[1][0] * m[2][1] - m[2][0] * m[1][1];\n\n var adj: mat4x4;\n adj[0][0] = (m[1][1] * sub_factor00 - m[1][2] * sub_factor01 + m[1][3] * sub_factor02);\n adj[1][0] = - (m[1][0] * sub_factor00 - m[1][2] * sub_factor03 + m[1][3] * sub_factor04);\n adj[2][0] = (m[1][0] * sub_factor01 - m[1][1] * sub_factor03 + m[1][3] * sub_factor05);\n adj[3][0] = - (m[1][0] * sub_factor02 - m[1][1] * sub_factor04 + m[1][2] * sub_factor05);\n adj[0][1] = - (m[0][1] * sub_factor00 - m[0][2] * sub_factor01 + m[0][3] * sub_factor02);\n adj[1][1] = (m[0][0] * sub_factor00 - m[0][2] * sub_factor03 + m[0][3] * sub_factor04);\n adj[2][1] = - (m[0][0] * sub_factor01 - m[0][1] * sub_factor03 + m[0][3] * sub_factor05);\n adj[3][1] = (m[0][0] * sub_factor02 - m[0][1] * sub_factor04 + m[0][2] * sub_factor05);\n adj[0][2] = (m[0][1] * sub_factor06 - m[0][2] * sub_factor07 + m[0][3] * sub_factor08);\n adj[1][2] = - (m[0][0] * sub_factor06 - m[0][2] * sub_factor09 + m[0][3] * sub_factor10);\n adj[2][2] = (m[0][0] * sub_factor11 - m[0][1] * sub_factor09 + m[0][3] * sub_factor12);\n adj[3][2] = - (m[0][0] * sub_factor08 - m[0][1] * sub_factor10 + m[0][2] * sub_factor12);\n adj[0][3] = - (m[0][1] * sub_factor13 - m[0][2] * sub_factor14 + m[0][3] * sub_factor15);\n adj[1][3] = (m[0][0] * sub_factor13 - m[0][2] * sub_factor16 + m[0][3] * sub_factor17);\n adj[2][3] = - (m[0][0] * sub_factor14 - m[0][1] * sub_factor16 + m[0][3] * sub_factor18);\n adj[3][3] = (m[0][0] * sub_factor15 - m[0][1] * sub_factor17 + m[0][2] * sub_factor18);\n\n let det = (m[0][0] * adj[0][0] + m[0][1] * adj[1][0] + m[0][2] * adj[2][0] + m[0][3] * adj[3][0]);\n\n return adj * (1 / det);\n}\n",
+ "fragment": "struct Material {\n albedoColor: vec4,\n metallic: f32,\n roughness: f32,\n}\n\nstruct AmbientLight {\n color: vec4,\n intensity: f32,\n}\n\nstruct Lights {\n numLights: f32,\n positions: array, 8>,\n colors: array, 8>,\n intensities: array, 8>,\n}\n\nstruct Camera {\n position: vec3,\n}\n\nstruct FragmentOutput {\n @location(0) outColor: vec4,\n}\n\nvar fragTexCoord_1: vec2;\nvar fragColor_1: vec4;\nvar fragPosition_1: vec3;\nvar fragNormal_1: vec3;\nvar outColor: vec4;\n@group(1) @binding(0) \nvar albedoTexture: texture_2d;\n@group(1) @binding(1) \nvar albedoTextureSampler: sampler;\n@group(1) @binding(2) \nvar material: Material;\n@group(1) @binding(3) \nvar ambientLight: AmbientLight;\n@group(1) @binding(4) \nvar lights: Lights;\n@group(1) @binding(5) \nvar camera: Camera;\n\nfn fresnelSchlick(cosTheta: f32, f0_: vec3) -> vec3 {\n var cosTheta_1: f32;\n var f0_1: vec3;\n\n cosTheta_1 = cosTheta;\n f0_1 = f0_;\n let _e35 = f0_1;\n let _e37 = f0_1;\n let _e41 = cosTheta_1;\n return (_e35 + ((vec3(1f) - _e37) * pow(clamp((1f - _e41), 0f, 1f), 5f)));\n}\n\nfn distributionGGX(normal: vec3, halfwayDir: vec3, roughness: f32) -> f32 {\n var normal_1: vec3;\n var halfwayDir_1: vec3;\n var roughness_1: f32;\n var a: f32;\n var a2_: f32;\n var num: f32;\n var NdotH: f32;\n var NdotH2_: f32;\n var b: f32;\n var denom: f32;\n\n normal_1 = normal;\n halfwayDir_1 = halfwayDir;\n roughness_1 = roughness;\n let _e37 = roughness_1;\n let _e38 = roughness_1;\n a = (_e37 * _e38);\n let _e41 = a;\n let _e42 = a;\n a2_ = (_e41 * _e42);\n let _e45 = a2_;\n num = _e45;\n let _e47 = normal_1;\n let _e48 = halfwayDir_1;\n NdotH = max(dot(_e47, _e48), 0f);\n let _e53 = NdotH;\n let _e54 = NdotH;\n NdotH2_ = (_e53 * _e54);\n let _e57 = NdotH2_;\n let _e58 = a2_;\n b = ((_e57 * (_e58 - 1f)) + 1f);\n let _e66 = b;\n let _e68 = b;\n denom = ((3.1415927f * _e66) * _e68);\n let _e71 = num;\n let _e72 = denom;\n return (_e71 / _e72);\n}\n\nfn geometrySchlickGGX(NdotV: f32, roughness_2: f32) -> f32 {\n var NdotV_1: f32;\n var roughness_3: f32;\n var r: f32;\n var k: f32;\n var num_1: f32;\n var denom_1: f32;\n\n NdotV_1 = NdotV;\n roughness_3 = roughness_2;\n let _e35 = roughness_3;\n r = (_e35 + 1f);\n let _e39 = r;\n let _e40 = r;\n k = ((_e39 * _e40) / 8f);\n let _e45 = NdotV_1;\n num_1 = _e45;\n let _e47 = NdotV_1;\n let _e49 = k;\n let _e52 = k;\n denom_1 = ((_e47 * (1f - _e49)) + _e52);\n let _e55 = num_1;\n let _e56 = denom_1;\n return (_e55 / _e56);\n}\n\nfn geometrySmith(normal_2: vec3, viewDir: vec3, lightDir: vec3, roughness_4: f32) -> f32 {\n var normal_3: vec3;\n var viewDir_1: vec3;\n var lightDir_1: vec3;\n var roughness_5: f32;\n var NdotV_2: f32;\n var NdotL: f32;\n var ggx2_: f32;\n var ggx1_: f32;\n\n normal_3 = normal_2;\n viewDir_1 = viewDir;\n lightDir_1 = lightDir;\n roughness_5 = roughness_4;\n let _e39 = normal_3;\n let _e40 = viewDir_1;\n NdotV_2 = max(dot(_e39, _e40), 0f);\n let _e45 = normal_3;\n let _e46 = lightDir_1;\n NdotL = max(dot(_e45, _e46), 0f);\n let _e51 = NdotV_2;\n let _e52 = roughness_5;\n let _e53 = geometrySchlickGGX(_e51, _e52);\n ggx2_ = _e53;\n let _e55 = NdotL;\n let _e56 = roughness_5;\n let _e57 = geometrySchlickGGX(_e55, _e56);\n ggx1_ = _e57;\n let _e59 = ggx1_;\n let _e60 = ggx2_;\n return (_e59 * _e60);\n}\n\nfn processLight(lightPos: vec3, lightColor: vec3, lightIntensity: f32, baseColor: vec3, normal_4: vec3, viewDir_2: vec3, diffuse: vec3) -> vec3 {\n var lightPos_1: vec3;\n var lightColor_1: vec3;\n var lightIntensity_1: f32;\n var baseColor_1: vec3;\n var normal_5: vec3;\n var viewDir_3: vec3;\n var diffuse_1: vec3;\n var lightDirVec: vec3;\n var lightDir_2: vec3;\n var distance_: f32;\n var halfwayDir_2: vec3;\n var attenuation: f32;\n var radiance: vec3;\n var ndf: f32;\n var g: f32;\n var f: vec3;\n var kS: vec3;\n var kD: vec3;\n var numerator: vec3;\n var denominator: f32;\n var specular: vec3;\n var NdotL_1: f32;\n\n lightPos_1 = lightPos;\n lightColor_1 = lightColor;\n lightIntensity_1 = lightIntensity;\n baseColor_1 = baseColor;\n normal_5 = normal_4;\n viewDir_3 = viewDir_2;\n diffuse_1 = diffuse;\n let _e45 = lightPos_1;\n let _e46 = fragPosition_1;\n lightDirVec = (_e45 - _e46);\n let _e49 = lightDirVec;\n lightDir_2 = normalize(_e49);\n let _e52 = lightDirVec;\n distance_ = (length(_e52) + 0.0001f);\n let _e57 = viewDir_3;\n let _e58 = lightDir_2;\n halfwayDir_2 = normalize((_e57 + _e58));\n let _e62 = lightIntensity_1;\n let _e63 = distance_;\n let _e64 = distance_;\n attenuation = (_e62 / (_e63 * _e64));\n let _e68 = lightColor_1;\n let _e69 = attenuation;\n radiance = (_e68 * _e69);\n let _e72 = normal_5;\n let _e73 = halfwayDir_2;\n let _e74 = material;\n let _e76 = distributionGGX(_e72, _e73, _e74.roughness);\n ndf = _e76;\n let _e78 = normal_5;\n let _e79 = viewDir_3;\n let _e80 = lightDir_2;\n let _e81 = material;\n let _e83 = geometrySmith(_e78, _e79, _e80, _e81.roughness);\n g = _e83;\n let _e85 = halfwayDir_2;\n let _e86 = viewDir_3;\n let _e90 = diffuse_1;\n let _e91 = fresnelSchlick(max(dot(_e85, _e86), 0f), _e90);\n f = _e91;\n let _e93 = f;\n kS = _e93;\n let _e97 = kS;\n let _e100 = material;\n kD = ((vec3(1f) - _e97) * (1f - _e100.metallic));\n let _e105 = ndf;\n let _e106 = g;\n let _e108 = f;\n numerator = ((_e105 * _e106) * _e108);\n let _e112 = normal_5;\n let _e113 = viewDir_3;\n let _e118 = normal_5;\n let _e119 = lightDir_2;\n denominator = (((4f * max(dot(_e112, _e113), 0f)) * max(dot(_e118, _e119), 0f)) + 0.0001f);\n let _e127 = numerator;\n let _e128 = denominator;\n specular = (_e127 / vec3(_e128));\n let _e132 = normal_5;\n let _e133 = lightDir_2;\n NdotL_1 = max(dot(_e132, _e133), 0f);\n let _e138 = kD;\n let _e139 = baseColor_1;\n let _e144 = specular;\n let _e146 = radiance;\n let _e148 = NdotL_1;\n return (((((_e138 * _e139) / vec3(3.1415927f)) + _e144) * _e146) * _e148);\n}\n\nfn main_1() {\n var normal_6: vec3;\n var viewDir_4: vec3;\n var baseColor_2: vec3;\n var f0_2: vec3 = vec3(0.04f);\n var diffuse_2: vec3;\n var kS_1: vec3;\n var kD_1: vec3;\n var ambient: vec3;\n var lo: vec3 = vec3(0f);\n var i: i32 = 0i;\n var color: vec3;\n\n let _e31 = fragNormal_1;\n normal_6 = normalize(_e31);\n let _e34 = camera;\n let _e36 = fragPosition_1;\n viewDir_4 = normalize((_e34.position - _e36));\n let _e40 = material;\n baseColor_2 = _e40.albedoColor.xyz;\n let _e44 = baseColor_2;\n let _e45 = fragTexCoord_1;\n let _e46 = textureSample(albedoTexture, albedoTextureSampler, _e45);\n baseColor_2 = (_e44 * _e46.xyz);\n let _e52 = f0_2;\n let _e53 = baseColor_2;\n let _e54 = material;\n diffuse_2 = mix(_e52, _e53, vec3(_e54.metallic));\n let _e59 = normal_6;\n let _e60 = viewDir_4;\n let _e64 = diffuse_2;\n let _e65 = fresnelSchlick(max(dot(_e59, _e60), 0f), _e64);\n kS_1 = _e65;\n let _e69 = kS_1;\n let _e72 = material;\n kD_1 = ((vec3(1f) - _e69) * (1f - _e72.metallic));\n let _e77 = kD_1;\n let _e78 = baseColor_2;\n let _e80 = ambientLight;\n let _e84 = ambientLight;\n ambient = (((_e77 * _e78) * _e80.color.xyz) * _e84.intensity);\n loop {\n let _e93 = i;\n let _e94 = lights;\n if !((_e93 < i32(_e94.numLights))) {\n break;\n }\n {\n let _e102 = lo;\n let _e103 = i;\n let _e106 = lights.positions[_e103];\n let _e107 = i;\n let _e110 = lights.colors[_e107];\n let _e112 = i;\n let _e115 = lights.intensities[_e112];\n let _e117 = baseColor_2;\n let _e118 = normal_6;\n let _e119 = viewDir_4;\n let _e120 = diffuse_2;\n let _e121 = processLight(_e106, _e110.xyz, _e115.x, _e117, _e118, _e119, _e120);\n lo = (_e102 + _e121);\n }\n continuing {\n let _e99 = i;\n i = (_e99 + 1i);\n }\n }\n let _e123 = ambient;\n let _e124 = lo;\n color = (_e123 + _e124);\n let _e127 = color;\n let _e128 = color;\n color = (_e127 / (_e128 + vec3(1f)));\n let _e133 = color;\n color = pow(_e133, vec3(0.45454544f));\n let _e139 = color;\n outColor = vec4(_e139.x, _e139.y, _e139.z, 1f);\n return;\n}\n\n@fragment \nfn main(@location(0) fragTexCoord: vec2, @location(1) fragColor: vec4, @location(2) fragPosition: vec3, @location(3) fragNormal: vec3) -> FragmentOutput {\n fragTexCoord_1 = fragTexCoord;\n fragColor_1 = fragColor;\n fragPosition_1 = fragPosition;\n fragNormal_1 = fragNormal;\n main_1();\n let _e47 = outColor;\n return FragmentOutput(_e47);\n}\n",
+ "slots": {
+ "JointMatrices": {
+ "group": 0,
+ "binding": 0,
+ "sizeInBytes": 1024,
+ "memberOffsets": {
+ "joints": 0
+ }
+ },
+ "VertexInfo": {
+ "group": 0,
+ "binding": 1,
+ "sizeInBytes": 192,
+ "memberOffsets": {
+ "model": 0,
+ "view": 64,
+ "projection": 128
+ }
+ },
+ "Material": {
+ "group": 1,
+ "binding": 2,
+ "sizeInBytes": 32,
+ "memberOffsets": {
+ "albedoColor": 0,
+ "metallic": 16,
+ "roughness": 20
+ }
+ },
+ "AmbientLight": {
+ "group": 1,
+ "binding": 3,
+ "sizeInBytes": 32,
+ "memberOffsets": {
+ "color": 0,
+ "intensity": 16
+ }
+ },
+ "Lights": {
+ "group": 1,
+ "binding": 4,
+ "sizeInBytes": 400,
+ "memberOffsets": {
+ "numLights": 0,
+ "positions": 16,
+ "colors": 144,
+ "intensities": 272
+ }
+ },
+ "Camera": {
+ "group": 1,
+ "binding": 5,
+ "sizeInBytes": 16,
+ "memberOffsets": {
+ "position": 0
+ }
+ },
+ "albedoTexture": {
+ "group": 1,
+ "binding": 0,
+ "samplerBinding": 1
+ }
+ }
+}
diff --git a/packages/flame_3d/assets/shaders/unlit_material.wgslbundle b/packages/flame_3d/assets/shaders/unlit_material.wgslbundle
new file mode 100644
index 00000000000..102829ac9f0
--- /dev/null
+++ b/packages/flame_3d/assets/shaders/unlit_material.wgslbundle
@@ -0,0 +1,37 @@
+{
+ "vertex": "struct JointMatrices {\n joints: array, 16>,\n}\n\nstruct VertexInfo {\n model: mat4x4,\n view: mat4x4,\n projection: mat4x4,\n}\n\nstruct VertexOutput {\n @location(0) fragTexCoord: vec2,\n @location(1) fragColor: vec4,\n @location(2) fragPosition: vec3,\n @location(3) fragNormal: vec3,\n @builtin(position) gl_Position: vec4,\n}\n\nvar vertexPosition_1: vec3;\nvar vertexTexCoord_1: vec2;\nvar vertexColor_1: vec4;\nvar vertexNormal_1: vec3;\nvar vertexJoints_1: vec4;\nvar vertexWeights_1: vec4;\n@group(0) @binding(0) \nvar jointMatrices: JointMatrices;\nvar fragTexCoord: vec2;\nvar fragColor: vec4;\nvar fragPosition: vec3;\nvar fragNormal: vec3;\n@group(0) @binding(1) \nvar vertex_info: VertexInfo;\nvar gl_Position: vec4;\n\nfn computeSkinMatrix() -> mat4x4 {\n let _e9 = vertexWeights_1;\n let _e13 = vertexWeights_1;\n let _e18 = vertexWeights_1;\n let _e23 = vertexWeights_1;\n if ((((_e9.x == 0f) && (_e13.y == 0f)) && (_e18.z == 0f)) && (_e23.w == 0f)) {\n {\n return mat4x4(vec4(1f, 0f, 0f, 0f), vec4(0f, 1f, 0f, 0f), vec4(0f, 0f, 1f, 0f), vec4(0f, 0f, 0f, 1f));\n }\n }\n let _e35 = vertexWeights_1;\n let _e37 = vertexJoints_1;\n let _e42 = jointMatrices.joints[i32(_e37.x)];\n let _e44 = vertexWeights_1;\n let _e46 = vertexJoints_1;\n let _e51 = jointMatrices.joints[i32(_e46.y)];\n let _e54 = vertexWeights_1;\n let _e56 = vertexJoints_1;\n let _e61 = jointMatrices.joints[i32(_e56.z)];\n let _e64 = vertexWeights_1;\n let _e66 = vertexJoints_1;\n let _e71 = jointMatrices.joints[i32(_e66.w)];\n return ((((_e35.x * _e42) + (_e44.y * _e51)) + (_e54.z * _e61)) + (_e64.w * _e71));\n}\n\nfn main_1() {\n var skinMatrix: mat4x4;\n var position: vec3;\n var normal: vec3;\n var modelViewProjection: mat4x4;\n\n let _e20 = computeSkinMatrix();\n skinMatrix = _e20;\n let _e22 = skinMatrix;\n let _e23 = vertexPosition_1;\n position = (_e22 * vec4(_e23.x, _e23.y, _e23.z, 1f)).xyz;\n let _e32 = skinMatrix;\n let _e33 = vertexNormal_1;\n normal = normalize((_e32 * vec4(_e33.x, _e33.y, _e33.z, 0f)).xyz);\n let _e43 = vertex_info;\n let _e45 = vertex_info;\n let _e48 = vertex_info;\n modelViewProjection = ((_e43.projection * _e45.view) * _e48.model);\n let _e53 = modelViewProjection;\n let _e54 = position;\n gl_Position = (_e53 * vec4(_e54.x, _e54.y, _e54.z, 1f));\n let _e61 = vertexTexCoord_1;\n fragTexCoord = _e61;\n let _e62 = vertexColor_1;\n fragColor = _e62;\n let _e63 = vertex_info;\n let _e65 = position;\n fragPosition = vec3((_e63.model * vec4(_e65.x, _e65.y, _e65.z, 1f)).xyz);\n let _e74 = vertex_info;\n let _e77 = transpose(_naga_inverse_4x4_f32(_e74.model));\n let _e87 = normal;\n fragNormal = (mat3x3(_e77[0].xyz, _e77[1].xyz, _e77[2].xyz) * _e87);\n let _e90 = gl_Position;\n let _e92 = gl_Position;\n gl_Position.z = ((_e90.z + _e92.w) * 0.5f);\n return;\n}\n\n@vertex \nfn main(@location(0) vertexPosition: vec3, @location(1) vertexTexCoord: vec2, @location(2) vertexColor: vec4, @location(3) vertexNormal: vec3, @location(4) vertexJoints: vec4, @location(5) vertexWeights: vec4) -> VertexOutput {\n vertexPosition_1 = vertexPosition;\n vertexTexCoord_1 = vertexTexCoord;\n vertexColor_1 = vertexColor;\n vertexNormal_1 = vertexNormal;\n vertexJoints_1 = vertexJoints;\n vertexWeights_1 = vertexWeights;\n main_1();\n let _e43 = fragTexCoord;\n let _e45 = fragColor;\n let _e47 = fragPosition;\n let _e49 = fragNormal;\n let _e51 = gl_Position;\n return VertexOutput(_e43, _e45, _e47, _e49, _e51);\n}\n\nfn _naga_inverse_4x4_f32(m: mat4x4) -> mat4x4 {\n let sub_factor00: f32 = m[2][2] * m[3][3] - m[3][2] * m[2][3];\n let sub_factor01: f32 = m[2][1] * m[3][3] - m[3][1] * m[2][3];\n let sub_factor02: f32 = m[2][1] * m[3][2] - m[3][1] * m[2][2];\n let sub_factor03: f32 = m[2][0] * m[3][3] - m[3][0] * m[2][3];\n let sub_factor04: f32 = m[2][0] * m[3][2] - m[3][0] * m[2][2];\n let sub_factor05: f32 = m[2][0] * m[3][1] - m[3][0] * m[2][1];\n let sub_factor06: f32 = m[1][2] * m[3][3] - m[3][2] * m[1][3];\n let sub_factor07: f32 = m[1][1] * m[3][3] - m[3][1] * m[1][3];\n let sub_factor08: f32 = m[1][1] * m[3][2] - m[3][1] * m[1][2];\n let sub_factor09: f32 = m[1][0] * m[3][3] - m[3][0] * m[1][3];\n let sub_factor10: f32 = m[1][0] * m[3][2] - m[3][0] * m[1][2];\n let sub_factor11: f32 = m[1][1] * m[3][3] - m[3][1] * m[1][3];\n let sub_factor12: f32 = m[1][0] * m[3][1] - m[3][0] * m[1][1];\n let sub_factor13: f32 = m[1][2] * m[2][3] - m[2][2] * m[1][3];\n let sub_factor14: f32 = m[1][1] * m[2][3] - m[2][1] * m[1][3];\n let sub_factor15: f32 = m[1][1] * m[2][2] - m[2][1] * m[1][2];\n let sub_factor16: f32 = m[1][0] * m[2][3] - m[2][0] * m[1][3];\n let sub_factor17: f32 = m[1][0] * m[2][2] - m[2][0] * m[1][2];\n let sub_factor18: f32 = m[1][0] * m[2][1] - m[2][0] * m[1][1];\n\n var adj: mat4x4;\n adj[0][0] = (m[1][1] * sub_factor00 - m[1][2] * sub_factor01 + m[1][3] * sub_factor02);\n adj[1][0] = - (m[1][0] * sub_factor00 - m[1][2] * sub_factor03 + m[1][3] * sub_factor04);\n adj[2][0] = (m[1][0] * sub_factor01 - m[1][1] * sub_factor03 + m[1][3] * sub_factor05);\n adj[3][0] = - (m[1][0] * sub_factor02 - m[1][1] * sub_factor04 + m[1][2] * sub_factor05);\n adj[0][1] = - (m[0][1] * sub_factor00 - m[0][2] * sub_factor01 + m[0][3] * sub_factor02);\n adj[1][1] = (m[0][0] * sub_factor00 - m[0][2] * sub_factor03 + m[0][3] * sub_factor04);\n adj[2][1] = - (m[0][0] * sub_factor01 - m[0][1] * sub_factor03 + m[0][3] * sub_factor05);\n adj[3][1] = (m[0][0] * sub_factor02 - m[0][1] * sub_factor04 + m[0][2] * sub_factor05);\n adj[0][2] = (m[0][1] * sub_factor06 - m[0][2] * sub_factor07 + m[0][3] * sub_factor08);\n adj[1][2] = - (m[0][0] * sub_factor06 - m[0][2] * sub_factor09 + m[0][3] * sub_factor10);\n adj[2][2] = (m[0][0] * sub_factor11 - m[0][1] * sub_factor09 + m[0][3] * sub_factor12);\n adj[3][2] = - (m[0][0] * sub_factor08 - m[0][1] * sub_factor10 + m[0][2] * sub_factor12);\n adj[0][3] = - (m[0][1] * sub_factor13 - m[0][2] * sub_factor14 + m[0][3] * sub_factor15);\n adj[1][3] = (m[0][0] * sub_factor13 - m[0][2] * sub_factor16 + m[0][3] * sub_factor17);\n adj[2][3] = - (m[0][0] * sub_factor14 - m[0][1] * sub_factor16 + m[0][3] * sub_factor18);\n adj[3][3] = (m[0][0] * sub_factor15 - m[0][1] * sub_factor17 + m[0][2] * sub_factor18);\n\n let det = (m[0][0] * adj[0][0] + m[0][1] * adj[1][0] + m[0][2] * adj[2][0] + m[0][3] * adj[3][0]);\n\n return adj * (1 / det);\n}\n",
+ "fragment": "struct Material {\n albedoColor: vec4,\n}\n\nstruct FragmentOutput {\n @location(0) outColor: vec4,\n}\n\nvar fragTexCoord_1: vec2;\nvar fragColor_1: vec4;\nvar fragPosition_1: vec3;\nvar fragNormal_1: vec3;\nvar outColor: vec4;\n@group(1) @binding(0) \nvar albedoTexture: texture_2d;\n@group(1) @binding(1) \nvar albedoTextureSampler: sampler;\n@group(1) @binding(2) \nvar material: Material;\n\nfn main_1() {\n var texColor: vec4;\n\n let _e10 = fragTexCoord_1;\n let _e11 = textureSample(albedoTexture, albedoTextureSampler, _e10);\n texColor = _e11;\n let _e13 = texColor;\n let _e14 = material;\n outColor = (_e13 * _e14.albedoColor);\n return;\n}\n\n@fragment \nfn main(@location(0) fragTexCoord: vec2, @location(1) fragColor: vec4, @location(2) fragPosition: vec3, @location(3) fragNormal: vec3) -> FragmentOutput {\n fragTexCoord_1 = fragTexCoord;\n fragColor_1 = fragColor;\n fragPosition_1 = fragPosition;\n fragNormal_1 = fragNormal;\n main_1();\n let _e26 = outColor;\n return FragmentOutput(_e26);\n}\n",
+ "slots": {
+ "JointMatrices": {
+ "group": 0,
+ "binding": 0,
+ "sizeInBytes": 1024,
+ "memberOffsets": {
+ "joints": 0
+ }
+ },
+ "VertexInfo": {
+ "group": 0,
+ "binding": 1,
+ "sizeInBytes": 192,
+ "memberOffsets": {
+ "model": 0,
+ "view": 64,
+ "projection": 128
+ }
+ },
+ "Material": {
+ "group": 1,
+ "binding": 2,
+ "sizeInBytes": 16,
+ "memberOffsets": {
+ "albedoColor": 0
+ }
+ },
+ "albedoTexture": {
+ "group": 1,
+ "binding": 0,
+ "samplerBinding": 1
+ }
+ }
+}
diff --git a/packages/flame_3d/bin/_build_shaderbundle.dart b/packages/flame_3d/bin/_build_shaderbundle.dart
new file mode 100644
index 00000000000..03391deba0b
--- /dev/null
+++ b/packages/flame_3d/bin/_build_shaderbundle.dart
@@ -0,0 +1,142 @@
+import 'dart:convert';
+import 'dart:io';
+
+/// Build each shader in [names] into a `.shaderbundle` asset in [assets].
+///
+/// Each name must have a matching `.vert` and `.frag` in [shaders]. Stale
+/// `.shaderbundle` files are removed first and other assets are left untouched.
+Future build(
+ Iterable names,
+ Directory shaders,
+ Directory assets,
+ List packageShaderDirs,
+) async {
+ for (final file in assets.listSync().whereType()) {
+ if (file.path.endsWith('.shaderbundle')) {
+ file.deleteSync();
+ }
+ }
+
+ final impellerC = await _findImpellerC();
+ final engineShaderLib = impellerC.resolve('./shader_lib/').toFilePath();
+
+ for (final name in names) {
+ final bundle = {
+ 'TextureFragment': {
+ 'type': 'fragment',
+ 'file': '${shaders.path}${Platform.pathSeparator}$name.frag',
+ },
+ 'TextureVertex': {
+ 'type': 'vertex',
+ 'file': '${shaders.path}${Platform.pathSeparator}$name.vert',
+ },
+ };
+
+ stdout.writeln('Computing shader bundle "$name"');
+ final result = await Process.run(impellerC.toFilePath(), [
+ '--sl=${assets.path}${Platform.pathSeparator}$name.shaderbundle',
+ '--shader-bundle=${jsonEncode(bundle)}',
+ '--include=${shaders.path}',
+ for (final dir in packageShaderDirs) '--include=${dir.path}',
+ '--include=$engineShaderLib',
+ ]);
+
+ if (result.exitCode != 0) {
+ stderr.writeln('Failed to compile shader "$name":\n${result.stderr}');
+ exitCode = 1;
+ }
+ }
+}
+
+// Copied from https://github.com/bdero/flutter_gpu_shaders/blob/master/lib/environment.dart#L53
+const _macosHostArtifacts = 'darwin-x64';
+const _linuxHostArtifacts = 'linux-x64';
+const _windowsHostArtifacts = 'windows-x64';
+
+const _impellercLocations = [
+ '$_macosHostArtifacts/impellerc',
+ '$_linuxHostArtifacts/impellerc',
+ '$_windowsHostArtifacts/impellerc.exe',
+];
+
+/// Locate the engine artifacts cache directory in the Flutter SDK.
+Uri _findEngineArtifactsDir({String? dartPath}) {
+ // Could be:
+ // `/path/to/flutter/bin/cache/dart-sdk/bin/dart`
+ // `/path/to/flutter/bin/cache/artifacts/engine/darwin-x64/flutter_tester`
+ // `/path/to/.user/shared/caches/94cf8c8fad31206e440611e309757a5a9b3be712/dart-sdk/bin/dart`
+ final dartExec = Uri.file(dartPath ?? Platform.resolvedExecutable);
+
+ Uri? cacheDir;
+ // Search backwards through the segment list until finding `bin` and `cache`
+ // in sequence.
+ for (var i = dartExec.pathSegments.length - 1; i >= 0; i--) {
+ if (dartExec.pathSegments[i] == 'dart-sdk' ||
+ dartExec.pathSegments[i] == 'artifacts') {
+ // Note: The final empty string denotes that this is a directory path.
+ cacheDir = dartExec.replace(
+ pathSegments: dartExec.pathSegments.sublist(0, i) + [''],
+ );
+ break;
+ }
+ }
+ if (cacheDir == null) {
+ throw Exception(
+ 'Unable to find Flutter SDK cache directory! '
+ 'Dart executable: `${dartExec.toFilePath()}`',
+ );
+ }
+
+ // We should now have a path of `/path/to/flutter/bin/cache/`.
+ final engineArtifactsDir = cacheDir.resolve(
+ './artifacts/engine/',
+ ); // Note: The final slash is important.
+
+ return engineArtifactsDir;
+}
+
+/// Locate the ImpellerC offline shader compiler in the engine artifacts cache
+/// directory.
+Future _findImpellerC() async {
+ /////////////////////////////////////////////////////////////////////////////
+ /// 1. If the `IMPELLERC` environment variable is set, use it.
+ ///
+
+ // ignore: do_not_use_environment
+ const impellercEnvVar = String.fromEnvironment('IMPELLERC');
+ if (impellercEnvVar != '') {
+ if (!File(impellercEnvVar).existsSync()) {
+ throw Exception(
+ 'IMPELLERC environment variable is set, '
+ "but it doesn't point to a valid file!",
+ );
+ }
+ return Uri.file(impellercEnvVar);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////
+ /// 3. Search for the `impellerc` binary within the host-specific artifacts.
+ ///
+
+ final engineArtifactsDir = _findEngineArtifactsDir();
+
+ // No need to get fancy. Just search all the possible directories rather than
+ // picking the correct one for the specific host type.
+ Uri? found;
+ final tried = [];
+ for (final variant in _impellercLocations) {
+ final impellercPath = engineArtifactsDir.resolve(variant);
+ if (File(impellercPath.toFilePath()).existsSync()) {
+ found = impellercPath;
+ break;
+ }
+ tried.add(impellercPath);
+ }
+ if (found == null) {
+ throw Exception(
+ 'Unable to find impellerc! Tried the following locations: $tried',
+ );
+ }
+
+ return found;
+}
diff --git a/packages/flame_3d/bin/_build_wgslbundle.dart b/packages/flame_3d/bin/_build_wgslbundle.dart
new file mode 100644
index 00000000000..70586e18dab
--- /dev/null
+++ b/packages/flame_3d/bin/_build_wgslbundle.dart
@@ -0,0 +1,334 @@
+import 'dart:convert';
+import 'dart:io';
+
+/// Build each shader in [names] to a `.wgslbundle` asset in [assets].
+///
+/// Each name must have a matching `.vert` and `.frag` in [shaders]. Stale
+/// `.wgslbundle` files are removed first and other assets are left untouched.
+///
+/// Requires `naga` (`cargo install naga-cli`) on PATH.
+void build(
+ Iterable names,
+ Directory shaders,
+ Directory assets,
+ List packageShaderDirs,
+) {
+ for (final file in assets.listSync().whereType()) {
+ if (file.path.endsWith('.wgslbundle')) {
+ file.deleteSync();
+ }
+ }
+
+ final includeDirs = [shaders, ...packageShaderDirs];
+ final tmp = Directory.systemTemp.createTempSync('flame3d_web_shaders');
+ const encoder = JsonEncoder.withIndent(' ');
+ try {
+ for (final name in names) {
+ stdout.writeln('Computing web shader bundle "$name"');
+ final vertex = _compileStage(shaders, tmp, name, 'vert', includeDirs);
+ final fragment = _compileStage(shaders, tmp, name, 'frag', includeDirs);
+ final bundle = {
+ 'vertex': vertex,
+ 'fragment': fragment,
+ 'slots': {..._reflect(vertex), ..._reflect(fragment)},
+ };
+ File.fromUri(
+ assets.uri.resolve('$name.wgslbundle'),
+ ).writeAsStringSync('${encoder.convert(bundle)}\n');
+ }
+ } finally {
+ tmp.deleteSync(recursive: true);
+ }
+}
+
+/// Preprocesses and compiles one GLSL [stage] of [name] to WGSL with `naga`.
+String _compileStage(
+ Directory shaders,
+ Directory tmp,
+ String name,
+ String stage,
+ List includeDirs,
+) {
+ final source = File('${shaders.path}/$name.$stage').readAsStringSync();
+ final resolved = _resolveIncludes(source, includeDirs);
+ _checkUniformArrays(resolved, '$name.$stage');
+
+ final glsl = File('${tmp.path}/$name.$stage.glsl')
+ ..writeAsStringSync(_preprocess(resolved, stage));
+ final wgsl = File('${tmp.path}/$name.$stage.wgsl');
+
+ final result = Process.runSync('naga', [glsl.path, wgsl.path]);
+ if (result.exitCode != 0) {
+ stderr
+ ..writeln('naga ${[glsl.path, wgsl.path].join(' ')} failed:')
+ ..writeln(result.stdout)
+ ..writeln(result.stderr);
+ throw ProcessException('naga', [
+ glsl.path,
+ wgsl.path,
+ ], 'exited ${result.exitCode}');
+ }
+
+ return wgsl.readAsStringSync();
+}
+
+String _resolveIncludes(
+ String source,
+ List includeDirs, [
+ Set? seen,
+]) {
+ final included = seen ?? {};
+ final directive = RegExp(
+ r'^[ \t]*#include[ \t]+[<"]([^>"]+)[>"][ \t]*$',
+ multiLine: true,
+ );
+ return source.replaceAllMapped(directive, (match) {
+ final path = match.group(1)!;
+ for (final dir in includeDirs) {
+ final file = File('${dir.path}/$path');
+ if (!file.existsSync()) {
+ continue;
+ }
+
+ if (!included.add(file.absolute.path)) {
+ return ''; // already inlined for this shader
+ }
+
+ return _resolveIncludes(file.readAsStringSync(), includeDirs, included);
+ }
+
+ throw StateError(
+ 'Cannot resolve #include "$path", searched: '
+ '${includeDirs.map((d) => d.path).join(', ')}',
+ );
+ });
+}
+
+/// Fails fast on uniform-block array members WGSL can't represent.
+///
+/// A WGSL uniform block requires every array element on a stride that is a
+/// multiple of 16 bytes, so a `float[]` or `vec2[]` member is rejected
+void _checkUniformArrays(String glsl, String label) {
+ final block = RegExp(r'uniform\s+\w+\s*\{([^}]*)\}');
+ final badMember = RegExp(r'\b(float|vec2)\s+(\w+)\s*\[');
+ for (final match in block.allMatches(glsl)) {
+ final member = badMember.firstMatch(match.group(1)!);
+ if (member != null) {
+ throw StateError(
+ 'Shader "$label": uniform-block array "${member.group(2)}" has '
+ '${member.group(1)} elements, WGSL needs a 16-byte array stride. '
+ 'Declare it as a vec4 array.',
+ );
+ }
+ }
+}
+
+String _preprocess(String glsl, String stage) {
+ final set = stage == 'vert' ? 0 : 1;
+ var inLocation = 0;
+ var outLocation = 0;
+ var binding = 0;
+ final samplers = [];
+ final out = StringBuffer();
+
+ final varying = RegExp(r'^(\s*)((?:smooth\s+)?(?:in|out)\s+\w+\s+\w+;)');
+ final sampler = RegExp(r'^(\s*)uniform\s+sampler2D\s+(\w+);');
+ final block = RegExp(r'^(\s*)uniform\s+\w+\s*\{');
+
+ for (final line in const LineSplitter().convert(glsl)) {
+ final samplerMatch = sampler.firstMatch(line);
+ if (samplerMatch != null) {
+ final indent = samplerMatch.group(1)!;
+ final name = samplerMatch.group(2)!;
+ out.writeln(
+ '${indent}layout(set = $set, binding = ${binding++}) '
+ 'uniform texture2D $name;',
+ );
+ out.writeln(
+ '${indent}layout(set = $set, binding = ${binding++}) '
+ 'uniform sampler ${name}Sampler;',
+ );
+ samplers.add(name);
+ continue;
+ }
+
+ final blockMatch = block.firstMatch(line);
+ if (blockMatch != null) {
+ final indent = blockMatch.group(1)!;
+ out.writeln(
+ line.replaceFirst(
+ '${indent}uniform',
+ '${indent}layout(set = $set, binding = ${binding++}) uniform',
+ ),
+ );
+ continue;
+ }
+
+ final varyingMatch = varying.firstMatch(line);
+ if (varyingMatch != null) {
+ final indent = varyingMatch.group(1)!;
+ final decl = varyingMatch.group(2)!;
+ final location = decl.contains(RegExp(r'\bin\b'))
+ ? inLocation++
+ : outLocation++;
+ out.writeln('${indent}layout(location = $location) $decl');
+ continue;
+ }
+
+ out.writeln(line);
+ }
+
+ var result = out.toString();
+ for (final name in samplers) {
+ result = result.replaceAll(
+ 'texture($name,',
+ 'texture(sampler2D($name, ${name}Sampler),',
+ );
+ }
+
+ if (stage == 'vert') {
+ // `vector_math`'s perspective matrix is GL-style so it outputs clip-space
+ // z in [-1, 1]. WebGPU (like Vulkan/Metal) expects [0, 1] and clips
+ // z < 0. Remap it after `main` has finished writing `gl_Position`.
+ final close = result.lastIndexOf('}');
+ result =
+ '${result.substring(0, close)}'
+ ' gl_Position.z = (gl_Position.z + gl_Position.w) * 0.5;\n'
+ '${result.substring(close)}';
+ }
+ return result;
+}
+
+/// Derives the uniform-block and texture slots of a compiled WGSL [stage].
+Map _reflect(String stage) {
+ final structs = _parseStructs(stage);
+ final slots = {};
+
+ // `@group(N) @binding(M) var[] name: type;`
+ final bindings = RegExp(
+ r'@group\((\d+)\)\s*@binding\((\d+)\)\s*'
+ r'var(?:)?\s+(\w+)\s*:\s*([^;]+);',
+ );
+
+ final samplers = {};
+ final textures = <(String name, int group, int binding)>[];
+
+ for (final match in bindings.allMatches(stage)) {
+ final group = int.parse(match.group(1)!);
+ final binding = int.parse(match.group(2)!);
+ final name = match.group(3)!;
+ final type = match.group(4)!.trim();
+
+ if (type == 'sampler') {
+ samplers[name] = binding;
+ } else if (type.startsWith('texture')) {
+ textures.add((name, group, binding));
+ } else {
+ final layout = _structLayout(structs[type]!, structs);
+ slots[type] = {
+ 'group': group,
+ 'binding': binding,
+ 'sizeInBytes': layout.size,
+ 'memberOffsets': layout.offsets,
+ };
+ }
+ }
+
+ for (final (name, group, binding) in textures) {
+ slots[name] = {
+ 'group': group,
+ 'binding': binding,
+ if (samplers['${name}Sampler'] case final sampler?)
+ 'samplerBinding': sampler,
+ };
+ }
+ return slots;
+}
+
+/// Parses every `struct Name { ... }` declaration, keyed by name. Each value
+/// is the struct's members as ordered `(name, type)` pairs.
+Map> _parseStructs(String wgsl) {
+ final structs = >{};
+ final declaration = RegExp(r'struct\s+(\w+)\s*\{([^}]*)\}');
+ // naga emits one member per line: ` name: type,` (a `@location(N)` prefix
+ // appears on stage-IO structs, which are never laid out).
+ final member = RegExp(r'^\s*(?:@\w+\([^)]*\)\s*)?(\w+)\s*:\s*(.+),\s*$');
+
+ for (final match in declaration.allMatches(wgsl)) {
+ structs[match.group(1)!] = [
+ for (final line in const LineSplitter().convert(match.group(2)!))
+ if (member.firstMatch(line) case final field?)
+ (field.group(1)!, field.group(2)!.trim()),
+ ];
+ }
+
+ return structs;
+}
+
+/// Computes the byte offset of each member and the padded total size of a
+/// struct, per the WGSL uniform address space layout rules.
+({int size, int align, Map offsets}) _structLayout(
+ List<(String, String)> fields,
+ Map> structs,
+) {
+ var offset = 0;
+ var align = 0;
+ final offsets = {};
+ for (final (name, type) in fields) {
+ final (size: size, align: fieldAlign) = _sizeAlign(type, structs);
+ offset = _roundUp(offset, fieldAlign);
+ offsets[name] = offset;
+ offset += size;
+ align = fieldAlign > align ? fieldAlign : align;
+ }
+ return (size: _roundUp(offset, align), align: align, offsets: offsets);
+}
+
+/// The size and alignment, in bytes, of a WGSL [type] under uniform layout.
+({int size, int align}) _sizeAlign(
+ String type,
+ Map> structs,
+) {
+ switch (type) {
+ case 'f32' || 'i32' || 'u32':
+ return (size: 4, align: 4);
+ case 'vec2':
+ return (size: 8, align: 8);
+ case 'vec3':
+ return (size: 12, align: 16);
+ case 'vec4':
+ return (size: 16, align: 16);
+ }
+
+ // matCxR: C columns, each a `vecR` padded to its own alignment.
+ if (RegExp(r'^mat(\d)x(\d)$').firstMatch(type) case final m?) {
+ final columns = int.parse(m.group(1)!);
+ final (size: columnSize, align: align) = _sizeAlign(
+ 'vec${m.group(2)}',
+ structs,
+ );
+ return (size: _roundUp(columnSize, align) * columns, align: align);
+ }
+
+ // array: N elements on a stride of `roundUp(sizeof E, alignof E)`.
+ if (RegExp(r'^array<(.+),\s*(\d+)>$').firstMatch(type) case final m?) {
+ final (size: elementSize, align: align) = _sizeAlign(
+ m.group(1)!.trim(),
+ structs,
+ );
+ final stride = _roundUp(elementSize, align);
+ return (size: stride * int.parse(m.group(2)!), align: align);
+ }
+
+ // A nested struct.
+ if (structs[type] case final fields?) {
+ final layout = _structLayout(fields, structs);
+ return (size: layout.size, align: layout.align);
+ }
+
+ throw StateError('Unsupported WGSL uniform type "$type"');
+}
+
+/// Rounds [value] up to the next multiple of [multiple].
+int _roundUp(int value, int multiple) =>
+ ((value + multiple - 1) ~/ multiple) * multiple;
diff --git a/packages/flame_3d/bin/build_shaders.dart b/packages/flame_3d/bin/build_shaders.dart
index 741ae949b4a..857ee5a21f4 100644
--- a/packages/flame_3d/bin/build_shaders.dart
+++ b/packages/flame_3d/bin/build_shaders.dart
@@ -1,35 +1,61 @@
+// Builds a package's shader assets from the GLSL sources in its `shaders/`
+// directory.
+//
+// Every Dart dependency that ships a top-level `shaders/` directory is made
+// available, so a shader can `#include `.
+//
+// dart run flame_3d:build_shaders [--with-web-gpu] [watch]
+
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
-/// Bundle a shader ('name'.frag & 'name'.vert) into a single shader bundle and
-/// store it in the assets directory.
-///
-/// This script is just a temporary way to bundle shaders. In the long run
-/// Flutter might support auto-bundling themselves but until then we have to
-/// do it manually.
-///
-/// Run from the package root whose shaders are being built. When invoked
-/// from a consumer package via `dart run flame_3d:build_shaders`, the
-/// consumer's own `shaders/` is bundled. Every Dart dependency that ships a
-/// top-level `shaders/` directory is added to impellerc's include path under
-/// its package name, so shaders can `#include ` against
-/// any of them. `` resolves to the engine builtins.
+import '_build_shaderbundle.dart' as shaderbundle;
+import '_build_wgslbundle.dart' as wgslbundle;
+
void main(List arguments) async {
+ final withWebGpu = arguments.contains('--with-web-gpu');
final root = Directory.current;
final assets = Directory.fromUri(root.uri.resolve('assets/shaders'));
final shaders = Directory.fromUri(root.uri.resolve('shaders'));
final packageShaderDirs = await _resolvePackageShaderDirs();
- await compute(assets, shaders, packageShaderDirs);
+ await _build(shaders, assets, packageShaderDirs, withWebGpu: withWebGpu);
+
if (arguments.contains('watch')) {
stdout.writeln('Running in watch mode');
- shaders.watch(recursive: true).listen((event) {
- compute(assets, shaders, packageShaderDirs);
+ shaders.watch(recursive: true).listen((_) {
+ _build(shaders, assets, packageShaderDirs, withWebGpu: withWebGpu);
});
}
}
+Future _build(
+ Directory shaders,
+ Directory assets,
+ List packageShaderDirs, {
+ required bool withWebGpu,
+}) async {
+ if (!shaders.existsSync()) {
+ stderr.writeln('Missing shader directory');
+ return;
+ }
+ assets.createSync(recursive: true);
+
+ // Unique shader base names, each needs a matching `.vert` and `.frag`.
+ // Nested files (e.g. the `shaders/flame_3d/` `#include` chunks) are ignored.
+ final names = shaders
+ .listSync()
+ .whereType()
+ .map((f) => f.path.split(Platform.pathSeparator).last.split('.').first)
+ .toSet();
+
+ await shaderbundle.build(names, shaders, assets, packageShaderDirs);
+ if (withWebGpu) {
+ wgslbundle.build(names, shaders, assets, packageShaderDirs);
+ }
+}
+
/// Returns every Dart dependency's top-level `shaders/` directory, so an
/// `#include ` can resolve to
/// `/shaders/pkg_name/foo.glsl`.
@@ -49,7 +75,7 @@ Future> _resolvePackageShaderDirs() async {
for (final package in packages) {
final name = package['name'] as String;
if (name == 'flutter') {
- // `flutter` ships no shader chunks; its includes come from the engine.
+ // `flutter` ships no shader chunks, they are included from the engine.
continue;
}
@@ -65,151 +91,3 @@ Future