From 9da20c5b9d3f0ce00d3bcffffe55e41615b3145a Mon Sep 17 00:00:00 2001
From: Jochum van der Ploeg
Date: Thu, 21 May 2026 20:32:35 +0200
Subject: [PATCH 1/6] feat(flame_3d): Add experimental `web` support using
`webgpu` backend
---
packages/flame_3d/README.md | 122 ++-
.../shaders/spatial_material.shaderbundle | Bin 52584 -> 52608 bytes
.../shaders/spatial_material.wgslbundle | 67 ++
.../assets/shaders/unlit_material.wgslbundle | 37 +
.../flame_3d/bin/_build_shaderbundle.dart | 142 +++
packages/flame_3d/bin/_build_wgslbundle.dart | 334 ++++++++
packages/flame_3d/bin/build_shaders.dart | 208 +----
.../graphics/backend/web_gpu/gpu_backend.dart | 811 ++++++++++++++++++
.../backend/web_gpu/shader_bundle.dart | 74 ++
.../backend/web_gpu/web_gpu_interop.dart | 442 ++++++++++
.../flame_3d/shaders/spatial_material.frag | 4 +-
11 files changed, 2029 insertions(+), 212 deletions(-)
create mode 100644 packages/flame_3d/assets/shaders/spatial_material.wgslbundle
create mode 100644 packages/flame_3d/assets/shaders/unlit_material.wgslbundle
create mode 100644 packages/flame_3d/bin/_build_shaderbundle.dart
create mode 100644 packages/flame_3d/bin/_build_wgslbundle.dart
create mode 100644 packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart
create mode 100644 packages/flame_3d/lib/src/graphics/backend/web_gpu/shader_bundle.dart
create mode 100644 packages/flame_3d/lib/src/graphics/backend/web_gpu/web_gpu_interop.dart
diff --git a/packages/flame_3d/README.md b/packages/flame_3d/README.md
index 527a9564dc2..dfc9264aad7 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,46 @@ 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 +94,68 @@ 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`.
-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.
+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.
## 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 759cf057518e274d180245df01e7487ab5050500..2977cf8d3b86222645cc5fdb5d02ea520bc9b615 100644
GIT binary patch
delta 4625
zcmeHKZETa*6~6E5g~Sd7CpKwp9Q#euVmnTZz86v`
zEBpGjSw6YvJkPoJo_p^($8hO~j)@;R%G^ST?%umr9GY9R`u$!Zz5qM&?99vw=udYD
z@mfNNx9WsQdo6C_<(V0{Xae@Dn7kjtITIKidPAvjXrf%q6Z;NryGuLx{4dLO?cnrT
z{j#ndm#0haFW##Ou^qxs3^V6od<^#DpI$07!778jA2u;12K)}dc7Qt$S!&w{ZA`7R
zR|hFXLD`SO-t*UESJwTGE?k13DbrJP-qDJOvBo(}N?_b0O3Zkkg5CrygK;h*VvI+i
zvnCo+~Hb@zc0DM_>F-}6qS9}RVo;WY$l#Q>jaUP8LFMyLfT!f)c`w9Rr
zgo~+#UxkjZxiI;?_5!a#=OUDSHg+01gC(bK^$he;2)X6(<6+-`{yfHMNKu{g96%Ck
zgIjP5#3%~$10zm((%v)(-3cECs-8In45o-c$TJPp8uhTrQ*fh2^Ez&W!Nr*?%5f2F
zI+Ih^7KToIwqF!>ALfx$b7Rnn6H~Vnht567so>jn+&`nYuG~t}4mhaj+o3ah6~LXa
zc_Xs}=mSQaq5@b4oj8k470kWRIggz3yRQg?6+}{P@&NSvAaqpG_d};2IW>6{I)j}(
zcROt2v*+%x{m7}g-Oz~_xv;v9-(Um2qZgvy@&a^x&9@}a+fXO^Cg$=)^ieI~Tfpeg
z7C@f<^ik{az~tEi;9LAu510VS9zZAF2FraCI(mR0r%Ld5u-O90sZ75FjCealo$~k4
z`R+We@_QE;=aJhhO09bQKvvH86#oL~0|+lwJ;q0{i66#xy+~GubQv~rC43~}9|Pm^
zCa1k}x2^)l96|4BmVG~zDa+4x>HbQa}*!Trt
zlfZV%59f!9d!endJ=Cr4gH4>bsKWoQjgjMXaplLM<7++_dH(9$i5=L)4nZf+kHVg?
zF~T<1DWnY|I^1EY`SbK7FfPCev9
zN4=@&!*GzS2e-%6u87v
z+9|m|;+Gw_yJf&v=4%B$v}N<)_*i^&WNgd$mXYDH`%SMlU!5nD06jdiee1x`mTzo~
z^rzFUa+}XPpQ{aR9Ns)WXkl7w%E_IL{&XfM&-kkKOhUGFd9|$68mr|4UEzj2ru1i0
zS)tal?gLL
zIVIQDhV^_xu5L0geYj}{qMRSBcjXi25-Cvb=8h7k(5-SZxKcOH$Y5o5!CPl;kkQ(p
zYzyV|d(O(&D+~IXb+V!=%k&K`*8VJOjhtRME00$d^!M+U?^VV1hwhd^PqyTq^>$9{
z73<{zPgd)cTkCTAfnUqNdA{Q7yJ#i#&nkrYb~*k*1z|+Z)3x=#y^_>&xZ0S`-|E+K
zkiJFhZqxGn;f1(18kwXy6Y!t6g`)8ha2)xoP{r=3cw<
zni%2rB^NdE>uWAL$0W>xb7m5BLxI^9=k=TBHDyQ4Dzv3J__I+E%G|i#5F&~INf+Zt
zXIu%YuYxnxe6t3Php(+aW@;kdlFVS5b#GS-k8+l)oC6uB^#USKDR2D@jp&-RrAxDd-_k!=gY9cROj0A
zw3T8wTUuW3G(Mw;`~N}zK9XDHGm~l4z}WbbB{$W@+vK*4x4b`zPK6GIT>KZB_;6>9
zgMIR*+PLL>Q;odh(~VqS%zNF}_HOxRcMLxcR*QaUNdPT+?HwKe!4~c6ssHy{v?0&;
zIGGS{_tfK=ovU`Cw>-M4VRoCou|4WImPL{3y0g_>A^()1{
E0}?KK2mk;8
delta 4619
zcmeHIU2Ig>5kB|&e{EdbYcJh3_TI&*z4orx`@dcpY!e!S!^U+R0;y24&=dlag9EWD
z)FiCkkcRx!sxx^?2q9Hf^(loc52R8^Q`9yUsemR@D^;YTtrS`%4?Of21^4^z-ebFV
zTw1Ap%hk@FnQwm1+;e8;PQ6%m^@XxRvr=kg?5Zs7UVM6
zz6u&}93|Nfo%xZi|KOjBA_6pNKtjKYfu^d164u|BRU>|if}
z4+BeKtwUPe;5c~vR0u?U94l?=RF$oN+0pxe(Kd;72Jv{zCm?el+DtpxE8wxG$5wJ<
z8mzxyO7&3Kw@3Y{?K~11Kyb|;I5qSL6Z6NB2^Dutw;I}$BUyArS
z;M5%hKV5KM>ZbsjK@6q?{|r2Sc3|qCZ~~kM&mg3u4)zXsCQF?@(F@=o0#UaCc6`xa
zfO0H`xK63L5I{
z?*q>+qfR?dfM>FceGfn;zSwutv7=7=ZU#@BaSQr5E@K1UBa2Yq<@@0Av+t66o`!y-
zN8!vjqK$6<$AHnE?N2@JX`|yEcJyq2+M@jn^VM1a!2YKZPlK&B1)ej9I^BZ5hs^e;
zPFMN|z=#il^jrQ3Jnx-vtL;7nMnCFGb{Lu0B=8w`;V*!$fq1C;GX4sgI6HL=y~~vT
z0hzcaUJ~)^z!;u-h9+hr7E|k7gp~s$&RMHpv(i*wQdBUAcCLmDg<==${9TZV>m<3!
zVdyyoshh&auK}9^HZ2tm{=yM(qmG5%?n%hRd5SvyqYg$LuZxkN1&^QNby3gH&KI!*
zhuF8lQ_n|Zvkpes!8WPq91z*z46E$#lK^7?I#ACV8N}C%-z(}q#*KBl5?F
zjmDJpHfCCe&&|!z-UVu)0iJRR_(SH~=Va8MHqOZ9#v$WXX=}=u-#;fe{1J28W{Fs7
z+r)=@Wiv6+ZRYsT#yY
zt{0zb{*58OYN;#C>SVTiz-2klp7DuYlU6>JNXcy0BPaU1`upKb!0NG=<+jNLEFRo-DEQTh$zVR7
zPKT|0GL@ETpU)SADj3YCU_P5(GW+5YeUexCk(UcW}Z!#8_+17Tv?a9_K4)JoU&xpyzoa#7tK$VWdBxLXT+!k-mydbC6SO|Y$U<2;qddMG&-%CFn
zLY!_`y7Ab-GwDT#ZQtR}wl1?jD^ImqaGq^-%NuQ9L6%ngT33J84wHa7*S^j*U%O$M
zZQp1HFG{2_-E#Hf3dL(YBzO8V=FUs$#aMSP|n1EE%_eh`Z|
zT#T*#ol#*|kL>lNin&{Si%Z!pQ=Nl&E0l2!tA!U8l+n)Bt|TvbUNmv}tGfbr6GS6X
zxzg!&rC21|!c&%t(ZpZI61~vnGf_!g&?6VRT5+)yGHH8ZwLr>dWGG10jv#M-c@z1{
z?M^`-t;5arT6+%edSGng;eC_)Cib(IKcTID9@6^~F0
ztwt!hVt4u5xOt9=Rys3QVAGIFvSTfDyM3$=;T_`wS%WGG`Tna@47c28i6a~78-^)t73vd({-
z@nCqbIG$Z%ZFXU*gf_3a*@*5~Qc?>8QIB)v);Hoqw42Xl81;>Kncz*PaQt6OW67rW
zcWYUtTpI}?KIU+hb6;ZQ+~rPjG|MA*g#UL#d30bc_L?8cz43b^hyI(fY>9te4J+fu
wmbJL+K<-ftu;aOo#bfz=?rzDgf7Ypr^1;)eR>h}PA?I@+ITi7TJC7Cq2~C7%kN^Mx
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