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> _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> _resolvePackageShaderDirs() async { } return result; } - -Future compute( - Directory assets, - Directory shaders, - List packageShaderDirs, -) async { - // Delete all the bundled shaders so we can replace them with new ones. - if (assets.existsSync()) { - assets.deleteSync(recursive: true); - } - // Create if not exists. - assets.createSync(recursive: true); - - if (!shaders.existsSync()) { - return stderr.writeln('Missing shader directory'); - } - - // Get a list of unique shader names. Each shader should have a .frag and - // .vert with the same basename to be considered a bundle. - final uniqueShaders = shaders - .listSync() - .whereType() - .map((f) => f.path.split(Platform.pathSeparator).last.split('.').first) - .toSet(); - - final impellerC = await findImpellerC(); - final engineShaderLib = impellerC.resolve('./shader_lib/').toFilePath(); - - for (final name in uniqueShaders) { - 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 "$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/lib/src/graphics/backend/web_gpu/gpu_backend.dart b/packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart new file mode 100644 index 00000000000..310e07af942 --- /dev/null +++ b/packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart @@ -0,0 +1,811 @@ +import 'dart:convert'; +import 'dart:ui'; +import 'dart:ui_web'; + +import 'package:flame_3d/src/graphics/backend/gpu_backend.dart' as base; +import 'package:flame_3d/src/graphics/backend/gpu_enums.dart'; +import 'package:flame_3d/src/graphics/backend/gpu_handles.dart'; +import 'package:flame_3d/src/graphics/backend/web_gpu/shader_bundle.dart'; +import 'package:flame_3d/src/graphics/backend/web_gpu/web_gpu_interop.dart'; +import 'package:flutter/services.dart'; + +/// {@template web_gpu_backend} +/// A [base.GpuBackend] implemented on browser WebGPU (`navigator.gpu`). +/// {@endtemplate} +base class GpuBackend extends base.GpuBackend { + GpuBackend._(this._device, this._bundles) + : _queue = _device.queue, + _defaultSampler = _device.createSampler( + GPUSamplerDescriptor( + magFilter: 'linear', + minFilter: 'linear', + addressModeU: 'repeat', + addressModeV: 'repeat', + ), + ), + _zeroBuffer = _device.createBuffer( + GPUBufferDescriptor( + size: _maxUniformSize(_bundles), + usage: GPUBufferUsage.uniform, + ), + ) { + _defaultTexture = _solidTexture(0xFFFFFFFF); + _frame = _WebGpuFrame(this); + } + + /// {@macro web_gpu_backend} + /// + /// Throws [UnsupportedError] if WebGPU is unavailable in this browser. + static Future initialize() async { + final gpu = webGPU; + if (gpu == null) { + throw UnsupportedError('WebGPU is not available in this browser.'); + } + + var adapter = await gpu.requestAdapter(); + adapter ??= await gpu.requestAdapter( + GPURequestAdapterOptions(forceFallbackAdapter: true), + ); + + if (adapter == null) { + throw UnsupportedError('No WebGPU adapter is available.'); + } + + final device = await adapter.requestDevice() + ..addEventListener( + 'uncapturederror', + (GPUUncapturedErrorEvent event) { + final error = event.error; + // ignore: avoid_print + print('[WebGPU] uncaptured error: ${error.message}'); + }, + ); + + GpuBackend._(device, await _loadShaderBundles()); + } + + /// Web has no synchronous backend: WebGPU device acquisition is async, so + /// callers must `await initialize()` first. Returning `null` lets the base + /// `GpuBackend.instance` surface that with a clear error. + static GpuBackend? create() => null; + + /// Loads every `.wgslbundle` shader asset declared by the app. + /// + /// Bundles are discovered through [AssetManifest], so a game's own custom + /// shaders are picked up automatically alongside flame_3d's built-ins. + static Future> _loadShaderBundles() async { + final manifest = await AssetManifest.loadFromAssetBundle(rootBundle); + final bundles = {}; + for (final key in manifest.listAssets()) { + if (key.endsWith('.wgslbundle')) { + final source = await rootBundle.loadString(key); + bundles[key] = WebShaderBundle.fromJson( + jsonDecode(source) as Map, + ); + } + } + return bundles; + } + + final GPUDevice _device; + + final GPUQueue _queue; + + final Map _bundles; + + late final GPUSampler _defaultSampler; + + late final GPUTexture _defaultTexture; + + late final GPUTextureView _defaultTextureView = _defaultTexture.createView(); + + late final GPUBuffer _zeroBuffer; + + late final _WebGpuFrame _frame; + + final Map<_PipelineKey, GPURenderPipeline> _pipelineCache = {}; + + @override + GpuTexture createTexture({ + required GpuStorageMode storageMode, + required int width, + required int height, + required GpuPixelFormat format, + }) { + final texture = _device.createTexture( + GPUTextureDescriptor( + size: GPUExtent3D(width: width, height: height, depthOrArrayLayers: 1), + format: _pixelFormat(format), + usage: GPUTextureUsage.textureBinding | GPUTextureUsage.copyDst, + ), + ); + return _WebGpuSampledTexture(_queue, texture, width, height); + } + + @override + GpuBuffer createBuffer({ + required GpuStorageMode storageMode, + required int sizeInBytes, + }) { + final buffer = _device.createBuffer( + GPUBufferDescriptor( + size: _align(sizeInBytes, 4), + usage: + GPUBufferUsage.vertex | + GPUBufferUsage.index | + GPUBufferUsage.copyDst, + ), + ); + return _WebGpuBuffer(_queue, buffer); + } + + @override + GpuShaderLibrary loadShaderLibrary(String assetName) { + final key = _wgslBundleKey(assetName); + final bundle = _bundles[key]; + if (bundle == null) { + throw StateError( + 'No web shader bundle found for "$assetName" (expected asset "$key"). ' + 'Generate it with `dart run flame_3d:build_shaders --with-web-gpu` ' + 'and declare it in pubspec.yaml.', + ); + } + return _WebGpuShaderLibrary(_device, bundle); + } + + @override + GpuPipeline createPipeline({ + required GpuShader vertexShader, + required GpuShader fragmentShader, + }) { + return _WebGpuPipeline( + vertexShader as _WebGpuShader, + fragmentShader as _WebGpuShader, + ); + } + + @override + GpuRenderTarget createRenderTarget({ + required int width, + required int height, + required Color clearValue, + }) { + final canvas = OffscreenCanvas(width, height); + final context = canvas.contextGPU() + ..configure( + GPUCanvasConfiguration( + device: _device, + format: _canvasFormat, + usage: GPUTextureUsage.renderAttachment, + alphaMode: 'premultiplied', + ), + ); + final depth = _device.createTexture( + GPUTextureDescriptor( + size: GPUExtent3D(width: width, height: height, depthOrArrayLayers: 1), + format: _depthFormat, + usage: GPUTextureUsage.renderAttachment, + ), + ); + return _WebGpuRenderTarget(canvas, context, depth, clearValue); + } + + @override + base.GpuFrame beginFrame() => _frame..begin(); + + /// Resolves (and caches) the concrete [GPURenderPipeline] for [pipeline] + /// under the given [blend], [depthStencil] and [cullMode] state. + GPURenderPipeline _resolvePipeline( + _WebGpuPipeline pipeline, + BlendState blend, + DepthStencilState depthStencil, + CullMode cullMode, + ) { + return _pipelineCache.putIfAbsent( + (pipeline, blend, depthStencil, cullMode), + () => _device.createRenderPipeline( + GPURenderPipelineDescriptor( + layout: 'auto', + vertex: GPUVertexState( + module: pipeline.vertexModule, + entryPoint: 'main', + buffers: [_vertexBufferLayout], + ), + fragment: GPUFragmentState( + module: pipeline.fragmentModule, + entryPoint: 'main', + targets: [_colorTarget(blend)], + ), + primitive: GPUPrimitiveState( + topology: 'triangle-list', + cullMode: _cullMode(cullMode), + frontFace: 'ccw', + ), + depthStencil: GPUDepthStencilState( + format: _depthFormat, + depthWriteEnabled: depthStencil == DepthStencilState.standard, + depthCompare: _depthCompare(depthStencil), + ), + ), + ), + ); + } + + GPUTexture _solidTexture(int argb) { + final texture = _device.createTexture( + GPUTextureDescriptor( + size: GPUExtent3D(width: 1, height: 1, depthOrArrayLayers: 1), + format: 'rgba8unorm', + usage: GPUTextureUsage.textureBinding | GPUTextureUsage.copyDst, + ), + ); + final pixel = Uint8List.fromList([ + (argb >> 16) & 0xFF, + (argb >> 8) & 0xFF, + argb & 0xFF, + (argb >> 24) & 0xFF, + ]); + _queue.writeTexture( + GPUImageCopyTexture(texture: texture), + pixel, + GPUImageDataLayout(offset: 0, bytesPerRow: 4, rowsPerImage: 1), + GPUExtent3D(width: 1, height: 1, depthOrArrayLayers: 1), + ); + return texture; + } + + /// The largest uniform block across [bundles]. + static int _maxUniformSize(Map bundles) { + var max = _uniformAlignment; + for (final bundle in bundles.values) { + for (final slot in bundle.slots.values) { + final size = slot.sizeInBytes; + if (size != null && size > max) { + max = size; + } + } + } + return _align(max, 4); + } +} + +/// The fixed vertex buffer layout, matching `Vertex.storage`. +final _vertexBufferLayout = GPUVertexBufferLayout( + arrayStride: 80, + stepMode: 'vertex', + attributes: [ + GPUVertexAttribute(format: 'float32x3', offset: 0, shaderLocation: 0), + GPUVertexAttribute(format: 'float32x2', offset: 12, shaderLocation: 1), + GPUVertexAttribute(format: 'float32x4', offset: 20, shaderLocation: 2), + GPUVertexAttribute(format: 'float32x3', offset: 36, shaderLocation: 3), + GPUVertexAttribute(format: 'float32x4', offset: 48, shaderLocation: 4), + GPUVertexAttribute(format: 'float32x4', offset: 64, shaderLocation: 5), + ], +); + +String _pixelFormat(GpuPixelFormat format) => switch (format) { + GpuPixelFormat.rgba8888 => 'rgba8unorm', + GpuPixelFormat.bgra8888 => 'bgra8unorm', + GpuPixelFormat.rgbaFloat32 => 'rgba32float', + GpuPixelFormat.depthStencil => _depthFormat, +}; + +String _cullMode(CullMode mode) => switch (mode) { + CullMode.none => 'none', + CullMode.frontFace => 'front', + CullMode.backFace => 'back', +}; + +String _depthCompare(DepthStencilState state) => switch (state) { + DepthStencilState.standard => 'less', + DepthStencilState.depthRead => 'less-equal', + DepthStencilState.none => 'always', +}; + +GPUColorTargetState _colorTarget(BlendState blend) { + GPUBlendComponent component(String src, String dst) => + GPUBlendComponent(operation: 'add', srcFactor: src, dstFactor: dst); + return switch (blend) { + BlendState.opaque => GPUColorTargetState(format: _canvasFormat), + BlendState.alphaBlend => GPUColorTargetState( + format: _canvasFormat, + blend: GPUBlendState( + color: component('one', 'one-minus-src-alpha'), + alpha: component('one', 'one-minus-src-alpha'), + ), + ), + BlendState.additive => GPUColorTargetState( + format: _canvasFormat, + blend: GPUBlendState( + color: component('one', 'one'), + alpha: component('one', 'one'), + ), + ), + }; +} + +/// Cache key for a concrete render pipeline variant. +typedef _PipelineKey = ( + _WebGpuPipeline, + BlendState, + DepthStencilState, + CullMode, +); + +class _WebGpuShaderLibrary implements GpuShaderLibrary { + const _WebGpuShaderLibrary(this._device, this._bundle); + + final GPUDevice _device; + + final WebShaderBundle _bundle; + + @override + GpuShader operator [](String entryPoint) => switch (entryPoint) { + 'TextureVertex' => _WebGpuShader(_device, _bundle.vertex, _bundle.slots), + 'TextureFragment' => _WebGpuShader( + _device, + _bundle.fragment, + _bundle.slots, + ), + _ => throw StateError('Unknown shader entry point "$entryPoint".'), + }; +} + +class _WebGpuShader implements GpuShader { + _WebGpuShader(this._device, this._wgsl, this.slots); + + final GPUDevice _device; + + final String _wgsl; + + final Map slots; + + GPUShaderModule? _module; + + GPUShaderModule get module => _module ??= _device.createShaderModule( + GPUShaderModuleDescriptor(code: _wgsl), + ); + + @override + GpuUniformSlot getUniformSlot(String slot) { + final reflected = slots[slot]; + if (reflected == null) { + throw StateError('Shader has no uniform slot "$slot".'); + } + return _WebGpuUniformSlot(reflected); + } +} + +class _WebGpuUniformSlot implements GpuUniformSlot { + const _WebGpuUniformSlot(this.slot); + + final WebShaderSlot slot; + + @override + int? get sizeInBytes => slot.sizeInBytes; + + @override + int? getMemberOffsetInBytes(String member) => slot.memberOffsets[member]; +} + +class _WebGpuPipeline implements GpuPipeline { + const _WebGpuPipeline(this._vertex, this._fragment); + + final _WebGpuShader _vertex; + + final _WebGpuShader _fragment; + + GPUShaderModule get vertexModule => _vertex.module; + + GPUShaderModule get fragmentModule => _fragment.module; + + Map get slots => _vertex.slots; +} + +class _WebGpuBuffer implements GpuBuffer { + const _WebGpuBuffer(this._queue, this.raw); + + final GPUQueue _queue; + + final GPUBuffer raw; + + @override + void write(ByteData data, {int destinationOffsetInBytes = 0}) { + _queue.writeBuffer(raw, destinationOffsetInBytes, _padded(data)); + } +} + +class _WebGpuSampledTexture implements GpuTexture { + const _WebGpuSampledTexture(this._queue, this.raw, this._width, this._height); + + final GPUQueue _queue; + + final GPUTexture raw; + + final int _width; + + final int _height; + + @override + void write(ByteData data) { + _queue.writeTexture( + GPUImageCopyTexture(texture: raw), + _padded(data), + GPUImageDataLayout( + offset: 0, + bytesPerRow: _width * 4, + rowsPerImage: _height, + ), + GPUExtent3D(width: _width, height: _height, depthOrArrayLayers: 1), + ); + } + + @override + Image asImage() => + throw UnsupportedError('A sampled texture cannot be read as an Image.'); +} + +class _WebGpuRenderTarget implements GpuRenderTarget { + _WebGpuRenderTarget( + this.canvas, + this.context, + this.depthTexture, + Color clearValue, + ) : clearColor = GPUColor( + r: clearValue.r, + g: clearValue.g, + b: clearValue.b, + a: clearValue.a, + ), + _colorTexture = _WebGpuColorTexture( + canvas, + canvas.width, + canvas.height, + ); + + final OffscreenCanvas canvas; + + final GPUCanvasContext context; + + final GPUTexture depthTexture; + + final GPUColor clearColor; + + final _WebGpuColorTexture _colorTexture; + + @override + GpuTexture get colorTexture => _colorTexture; +} + +class _WebGpuColorTexture implements GpuTexture { + _WebGpuColorTexture(this._canvas, int width, int height) + : _blitCanvas = OffscreenCanvas(width, height); + + final OffscreenCanvas _canvas; + + final OffscreenCanvas _blitCanvas; + + late final CanvasContext2D _blit = _blitCanvas.context2D(); + + /// A retained handle to the previous frame's image, kept alive until its + /// composite has finished. + Image? _retained; + + @override + void write(ByteData data) => + throw UnsupportedError('A render-target texture cannot be written to.'); + + @override + Image asImage() { + // Release last frame's retained handle: its `drawImageRect` has + // composited by now, so the underlying `ImageBitmap` is no longer needed. + _retained?.dispose(); + + // Blit the WebGPU canvas onto a plain 2D canvas and snapshot it as an + // `ImageBitmap`: a WebGPU-canvas bitmap crashes the CanvasKit compositor, + // a 2D-canvas one composites cleanly. + _blit.drawImage(_canvas, 0, 0); + + // Create the image from the bitmap, this is guaranteed to be fully + // synchronous (and lazy). + final bitmap = _blitCanvas.transferToImageBitmap(); + final image = createImageFromImageBitmap(bitmap) as Image; + + // We retain a clone of the image as `createImageFromImageBitmap` is lazy. + // Once it has been recorded through `drawImageRect` we can dipose it. + // + // `clone` returns a ref-counted handle, so we are fully in control on when + // it gets disposed. + _retained = image.clone(); + + // We return the old image and not the retained to guarantee the above. + return image; + } +} + +class _WebGpuFrame implements base.GpuFrame { + _WebGpuFrame(this._backend); + + final GpuBackend _backend; + + final List> _uniformPools = [[], [], []]; + int _frameIndex = 0; + + // Allocation cursor into the current frame's pool. + int _bufferIndex = 0; + int _cursor = 0; + + /// Resets the transient uniform allocator for a new frame. + void begin() { + _bufferIndex = 0; + _cursor = 0; + } + + /// Sub-allocates [data] into this frame's uniform pool, returning the buffer + /// it landed in and the (256-aligned) offset it was written at. + /// + /// The pool grows by a buffer whenever a frame's uniforms outgrow the + /// current one, so there is no fixed per-frame uniform budget. + (GPUBuffer, int) allocateUniform(ByteData data) { + final stride = _align(data.lengthInBytes, _uniformAlignment); + if (_cursor + stride > _uniformPoolSize) { + _bufferIndex++; + _cursor = 0; + } + + final pool = _uniformPools[_frameIndex]; + if (_bufferIndex == pool.length) { + pool.add( + _backend._device.createBuffer( + GPUBufferDescriptor( + size: _uniformPoolSize, + usage: GPUBufferUsage.uniform | GPUBufferUsage.copyDst, + ), + ), + ); + } + + final buffer = pool[_bufferIndex]; + final offset = _cursor; + _backend._queue.writeBuffer(buffer, offset, _padded(data)); + _cursor += stride; + return (buffer, offset); + } + + @override + base.GpuRenderPass beginRenderPass( + GpuRenderTarget target, { + required BlendState blend, + required DepthStencilState depthStencil, + }) { + final renderTarget = target as _WebGpuRenderTarget; + final encoder = _backend._device.createCommandEncoder(); + final pass = encoder.beginRenderPass( + GPURenderPassDescriptor( + colorAttachments: [ + GPURenderPassColorAttachment( + view: renderTarget.context.getCurrentTexture().createView(), + clearValue: renderTarget.clearColor, + loadOp: 'clear', + storeOp: 'store', + ), + ], + depthStencilAttachment: GPURenderPassDepthStencilAttachment( + view: renderTarget.depthTexture.createView(), + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + ), + ), + ); + return _WebGpuRenderPass( + _backend, + this, + blend, + depthStencil, + encoder, + pass, + ); + } + + @override + void end() { + _frameIndex = (_frameIndex + 1) % _uniformPools.length; + } +} + +class _WebGpuRenderPass implements base.GpuRenderPass { + _WebGpuRenderPass( + this._backend, + this._frame, + this._blend, + this._depthStencil, + this._encoder, + this._pass, + ); + + final GpuBackend _backend; + + final _WebGpuFrame _frame; + + final BlendState _blend; + + final DepthStencilState _depthStencil; + + final GPUCommandEncoder _encoder; + + final GPURenderPassEncoder _pass; + + late _WebGpuPipeline _pipeline; + + late GPURenderPipeline _renderPipeline; + + int _indexCount = 0; + + // Bindings accumulated for the current draw, keyed by `_key(group, binding)`. + final Map _uniforms = {}; + final Map _textures = {}; + + static int _key(int group, int binding) => (group << 16) | binding; + + @override + void bindPipeline(GpuPipeline pipeline, CullMode cullMode) { + _pipeline = pipeline as _WebGpuPipeline; + _renderPipeline = _backend._resolvePipeline( + _pipeline, + _blend, + _depthStencil, + cullMode, + ); + _pass.setPipeline(_renderPipeline); + } + + @override + void bindVertexBuffer(GpuBufferView view, int vertexCount) { + _pass.setVertexBuffer( + 0, + (view.buffer as _WebGpuBuffer).raw, + view.offsetInBytes, + view.lengthInBytes, + ); + } + + @override + void bindIndexBuffer( + GpuBufferView view, + GpuIndexType indexType, + int indexCount, + ) { + _pass.setIndexBuffer( + (view.buffer as _WebGpuBuffer).raw, + indexType == GpuIndexType.uint16 ? 'uint16' : 'uint32', + view.offsetInBytes, + view.lengthInBytes, + ); + _indexCount = indexCount; + } + + @override + void bindUniform(GpuUniformSlot slot, ByteData data) { + final reflected = (slot as _WebGpuUniformSlot).slot; + final (buffer, offset) = _frame.allocateUniform(data); + _uniforms[_key(reflected.group, reflected.binding)] = ( + buffer, + offset, + data.lengthInBytes, + ); + } + + @override + void bindTexture( + covariant _WebGpuUniformSlot slot, + covariant _WebGpuSampledTexture texture, + ) { + final reflected = slot.slot; + _textures[_key(reflected.group, reflected.binding)] = texture.raw + .createView(); + } + + @override + void clearBindings() { + _uniforms.clear(); + _textures.clear(); + } + + @override + void draw() { + final pipeline = _pipeline; + final renderPipeline = _renderPipeline; + + final groups = >{}; + for (final slot in pipeline.slots.values) { + groups.putIfAbsent(slot.group, () => []).add(slot); + } + + for (final MapEntry(key: group, value: slots) in groups.entries) { + final entries = []; + for (final slot in slots) { + if (slot.sizeInBytes != null) { + final bound = _uniforms[_key(slot.group, slot.binding)]; + entries.add( + GPUBindGroupEntry( + binding: slot.binding, + resource: GPUBufferBinding( + buffer: bound?.$1 ?? _backend._zeroBuffer, + offset: bound?.$2 ?? 0, + size: bound?.$3 ?? slot.sizeInBytes!, + ), + ), + ); + } else { + final view = + _textures[_key(slot.group, slot.binding)] ?? + _backend._defaultTextureView; + entries + ..add(GPUBindGroupEntry(binding: slot.binding, resource: view)) + ..add( + GPUBindGroupEntry( + binding: slot.samplerBinding!, + resource: _backend._defaultSampler, + ), + ); + } + } + _pass.setBindGroup( + group, + _backend._device.createBindGroup( + GPUBindGroupDescriptor( + layout: renderPipeline.getBindGroupLayout(group), + entries: entries, + ), + ), + ); + } + + _pass.drawIndexed(_indexCount); + } + + @override + void submit() { + _pass.end(); + _backend._queue.submit([_encoder.finish()]); + } +} + +/// Returns [data] as a byte list, zero-padded to a multiple of 4 bytes. +Uint8List _padded(ByteData data) { + final source = data.buffer.asUint8List( + data.offsetInBytes, + data.lengthInBytes, + ); + if (source.length % 4 == 0) { + return source; + } + return Uint8List(_align(source.length, 4)) + ..setRange(0, source.length, source); +} + +/// Color format of the render-target canvas and pipeline color targets. +const _canvasFormat = 'rgba8unorm'; + +/// Depth attachment format. +const _depthFormat = 'depth24plus'; + +/// Size, in bytes, of each per-frame uniform buffer. +const _uniformPoolSize = 1 << 20; + +/// WebGPU minimum uniform buffer offset alignment. +const _uniformAlignment = 256; + +int _align(int value, int alignment) => + ((value + alignment - 1) ~/ alignment) * alignment; + +String _wgslBundleKey(String assetName) { + if (assetName.endsWith('.wgslbundle')) { + return assetName; + } + + final dot = assetName.lastIndexOf('.'); + final stem = dot == -1 ? assetName : assetName.substring(0, dot); + return '$stem.wgslbundle'; +} diff --git a/packages/flame_3d/lib/src/graphics/backend/web_gpu/shader_bundle.dart b/packages/flame_3d/lib/src/graphics/backend/web_gpu/shader_bundle.dart new file mode 100644 index 00000000000..41167ffe0ad --- /dev/null +++ b/packages/flame_3d/lib/src/graphics/backend/web_gpu/shader_bundle.dart @@ -0,0 +1,74 @@ +/// A compiled vertex + fragment WGSL pair with its binding reflection. +class WebShaderBundle { + /// Creates a bundle from its [vertex]/[fragment] WGSL and [slots]. + const WebShaderBundle({ + required this.vertex, + required this.fragment, + required this.slots, + }); + + /// Decodes a bundle from its `.wgslbundle` JSON representation. + factory WebShaderBundle.fromJson(Map json) { + return WebShaderBundle( + vertex: json['vertex'] as String, + fragment: json['fragment'] as String, + slots: { + for (final entry in (json['slots'] as Map).entries) + entry.key as String: WebShaderSlot.fromJson( + entry.value as Map, + ), + }, + ); + } + + /// The vertex stage WGSL source. + final String vertex; + + /// The fragment stage WGSL source. + final String fragment; + + /// Binding reflection, keyed by uniform block or texture slot name. + final Map slots; +} + +/// Reflection for one uniform block or texture slot of a [WebShaderBundle]. +class WebShaderSlot { + /// Creates a slot binding at the given [group]/[binding]. + const WebShaderSlot({ + required this.group, + required this.binding, + this.sizeInBytes, + this.samplerBinding, + this.memberOffsets = const {}, + }); + + /// Decodes a slot from its `.wgslbundle` JSON representation. + factory WebShaderSlot.fromJson(Map json) { + return WebShaderSlot( + group: json['group'] as int, + binding: json['binding'] as int, + sizeInBytes: json['sizeInBytes'] as int?, + samplerBinding: json['samplerBinding'] as int?, + memberOffsets: { + for (final entry + in ((json['memberOffsets'] as Map?) ?? const {}).entries) + entry.key as String: entry.value as int, + }, + ); + } + + /// The `@group` the slot is bound to. + final int group; + + /// The `@binding` the slot is bound to. + final int binding; + + /// Size of the uniform block in bytes, or `null` for a texture slot. + final int? sizeInBytes; + + /// The companion sampler's `@binding` for a texture slot, else `null`. + final int? samplerBinding; + + /// Byte offset of each member within a uniform block. + final Map memberOffsets; +} diff --git a/packages/flame_3d/lib/src/graphics/backend/web_gpu/web_gpu_interop.dart b/packages/flame_3d/lib/src/graphics/backend/web_gpu/web_gpu_interop.dart new file mode 100644 index 00000000000..84021c03d2e --- /dev/null +++ b/packages/flame_3d/lib/src/graphics/backend/web_gpu/web_gpu_interop.dart @@ -0,0 +1,442 @@ +import 'dart:js_interop'; +import 'dart:typed_data'; + +@JS('navigator.gpu') +external GPU? get webGPU; + +extension type GPU._(JSObject _) implements JSObject { + Future requestAdapter([GPURequestAdapterOptions? options]) { + return _requestAdapter(options ?? GPURequestAdapterOptions()).toDart; + } + + @JS('requestAdapter') + external JSPromise _requestAdapter([ + GPURequestAdapterOptions options, + ]); +} + +extension type GPURequestAdapterOptions._(JSObject _) implements JSObject { + external factory GPURequestAdapterOptions({ + String powerPreference, + bool forceFallbackAdapter, + }); +} + +extension type GPUAdapter._(JSObject _) implements JSObject { + Future requestDevice() { + return _requestDevice().toDart; + } + + @JS('requestDevice') + external JSPromise _requestDevice(); +} + +extension type GPUDevice._(JSObject _) implements JSObject { + external GPUQueue get queue; + + external GPUBuffer createBuffer(GPUBufferDescriptor descriptor); + + external GPUTexture createTexture(GPUTextureDescriptor descriptor); + + external GPUSampler createSampler(GPUSamplerDescriptor descriptor); + + external GPUShaderModule createShaderModule( + GPUShaderModuleDescriptor descriptor, + ); + + external GPUBindGroup createBindGroup(GPUBindGroupDescriptor descriptor); + + external GPURenderPipeline createRenderPipeline( + GPURenderPipelineDescriptor descriptor, + ); + + external GPUCommandEncoder createCommandEncoder(); + + void addEventListener( + String type, + void Function(GPUUncapturedErrorEvent) listener, + ) => _addEventListener(type, listener.toJS); + + @JS('addEventListener') + external void _addEventListener(String type, JSFunction listener); +} + +extension type GPUError._(JSObject _) implements JSObject { + external String get message; +} + +extension type GPUUncapturedErrorEvent._(JSObject _) implements JSObject { + external GPUError get error; +} + +extension type GPUQueue._(JSObject _) implements JSObject { + void writeBuffer(GPUBuffer buffer, int bufferOffset, Uint8List data) => + _writeBuffer(buffer, bufferOffset, data.toJS); + + @JS('writeBuffer') + external void _writeBuffer( + GPUBuffer buffer, + int bufferOffset, + JSObject data, + ); + + void writeTexture( + GPUImageCopyTexture destination, + Uint8List data, + GPUImageDataLayout dataLayout, + GPUExtent3D size, + ) => _writeTexture(destination, data.toJS, dataLayout, size); + + @JS('writeTexture') + external void _writeTexture( + GPUImageCopyTexture destination, + JSObject data, + GPUImageDataLayout dataLayout, + GPUExtent3D size, + ); + + void submit(List commandBuffers) => + _submit(commandBuffers.toJS); + + @JS('submit') + external void _submit(JSArray commandBuffers); +} + +extension type GPUBuffer._(JSObject _) implements JSObject { + external void destroy(); +} + +extension type GPUTexture._(JSObject _) implements JSObject { + external GPUTextureView createView(); + + external void destroy(); +} + +extension type GPUTextureView._(JSObject _) implements JSObject {} + +extension type GPUSampler._(JSObject _) implements JSObject {} + +extension type GPUShaderModule._(JSObject _) implements JSObject {} + +extension type GPUBindGroupLayout._(JSObject _) implements JSObject {} + +extension type GPUBindGroup._(JSObject _) implements JSObject {} + +extension type GPURenderPipeline._(JSObject _) implements JSObject { + external GPUBindGroupLayout getBindGroupLayout(int index); +} + +extension type GPUCommandBuffer._(JSObject _) implements JSObject {} + +extension type GPUCommandEncoder._(JSObject _) implements JSObject { + external GPURenderPassEncoder beginRenderPass( + GPURenderPassDescriptor descriptor, + ); + + external GPUCommandBuffer finish(); +} + +extension type GPURenderPassEncoder._(JSObject _) implements JSObject { + external void setPipeline(GPURenderPipeline pipeline); + + external void setBindGroup(int index, GPUBindGroup bindGroup); + + external void setVertexBuffer( + int slot, + GPUBuffer buffer, + int offset, + int size, + ); + + external void setIndexBuffer( + GPUBuffer buffer, + String indexFormat, + int offset, + int size, + ); + + external void drawIndexed(int indexCount); + + external void end(); +} + +extension type GPUCanvasContext._(JSObject _) implements JSObject { + external void configure(GPUCanvasConfiguration configuration); + + external GPUTexture getCurrentTexture(); +} + +extension type GPUBufferDescriptor._(JSObject _) implements JSObject { + external factory GPUBufferDescriptor({ + int size, + int usage, + bool mappedAtCreation, + }); +} + +extension type GPUExtent3D._(JSObject _) implements JSObject { + external factory GPUExtent3D({ + int width, + int height, + int depthOrArrayLayers, + }); +} + +extension type GPUTextureDescriptor._(JSObject _) implements JSObject { + external factory GPUTextureDescriptor({ + GPUExtent3D size, + String format, + int usage, + }); +} + +extension type GPUSamplerDescriptor._(JSObject _) implements JSObject { + external factory GPUSamplerDescriptor({ + String magFilter, + String minFilter, + String addressModeU, + String addressModeV, + }); +} + +extension type GPUShaderModuleDescriptor._(JSObject _) implements JSObject { + external factory GPUShaderModuleDescriptor({String code}); +} + +extension type GPUBufferBinding._(JSObject _) implements JSObject { + external factory GPUBufferBinding({ + GPUBuffer buffer, + int offset, + int size, + }); +} + +extension type GPUBindGroupEntry._(JSObject _) implements JSObject { + external factory GPUBindGroupEntry({int binding, JSAny resource}); +} + +extension type GPUBindGroupDescriptor._(JSObject _) implements JSObject { + factory GPUBindGroupDescriptor({ + required GPUBindGroupLayout layout, + required List entries, + }) => GPUBindGroupDescriptor._raw(layout: layout, entries: entries.toJS); + + external factory GPUBindGroupDescriptor._raw({ + GPUBindGroupLayout layout, + JSArray entries, + }); +} + +extension type GPUVertexAttribute._(JSObject _) implements JSObject { + external factory GPUVertexAttribute({ + String format, + int offset, + int shaderLocation, + }); +} + +extension type GPUVertexBufferLayout._(JSObject _) implements JSObject { + factory GPUVertexBufferLayout({ + required int arrayStride, + required String stepMode, + required List attributes, + }) => GPUVertexBufferLayout._raw( + arrayStride: arrayStride, + stepMode: stepMode, + attributes: attributes.toJS, + ); + + external factory GPUVertexBufferLayout._raw({ + int arrayStride, + String stepMode, + JSArray attributes, + }); +} + +extension type GPUVertexState._(JSObject _) implements JSObject { + factory GPUVertexState({ + required GPUShaderModule module, + required String entryPoint, + required List buffers, + }) => GPUVertexState._raw( + module: module, + entryPoint: entryPoint, + buffers: buffers.toJS, + ); + + external factory GPUVertexState._raw({ + GPUShaderModule module, + String entryPoint, + JSArray buffers, + }); +} + +extension type GPUBlendComponent._(JSObject _) implements JSObject { + external factory GPUBlendComponent({ + String operation, + String srcFactor, + String dstFactor, + }); +} + +extension type GPUBlendState._(JSObject _) implements JSObject { + external factory GPUBlendState({ + GPUBlendComponent color, + GPUBlendComponent alpha, + }); +} + +extension type GPUColorTargetState._(JSObject _) implements JSObject { + external factory GPUColorTargetState({String format, GPUBlendState blend}); +} + +extension type GPUFragmentState._(JSObject _) implements JSObject { + factory GPUFragmentState({ + required GPUShaderModule module, + required String entryPoint, + required List targets, + }) => GPUFragmentState._raw( + module: module, + entryPoint: entryPoint, + targets: targets.toJS, + ); + + external factory GPUFragmentState._raw({ + GPUShaderModule module, + String entryPoint, + JSArray targets, + }); +} + +extension type GPUPrimitiveState._(JSObject _) implements JSObject { + external factory GPUPrimitiveState({ + String topology, + String cullMode, + String frontFace, + }); +} + +extension type GPUDepthStencilState._(JSObject _) implements JSObject { + external factory GPUDepthStencilState({ + String format, + bool depthWriteEnabled, + String depthCompare, + }); +} + +extension type GPURenderPipelineDescriptor._(JSObject _) implements JSObject { + factory GPURenderPipelineDescriptor({ + required String layout, + required GPUVertexState vertex, + required GPUFragmentState fragment, + required GPUPrimitiveState primitive, + required GPUDepthStencilState depthStencil, + }) => GPURenderPipelineDescriptor._raw( + layout: layout.toJS, + vertex: vertex, + fragment: fragment, + primitive: primitive, + depthStencil: depthStencil, + ); + + external factory GPURenderPipelineDescriptor._raw({ + JSAny layout, + GPUVertexState vertex, + GPUFragmentState fragment, + GPUPrimitiveState primitive, + GPUDepthStencilState depthStencil, + }); +} + +extension type GPUColor._(JSObject _) implements JSObject { + external factory GPUColor({double r, double g, double b, double a}); +} + +extension type GPURenderPassColorAttachment._(JSObject _) implements JSObject { + external factory GPURenderPassColorAttachment({ + GPUTextureView view, + GPUColor clearValue, + String loadOp, + String storeOp, + }); +} + +extension type GPURenderPassDepthStencilAttachment._(JSObject _) + implements JSObject { + external factory GPURenderPassDepthStencilAttachment({ + GPUTextureView view, + double depthClearValue, + String depthLoadOp, + String depthStoreOp, + }); +} + +extension type GPURenderPassDescriptor._(JSObject _) implements JSObject { + factory GPURenderPassDescriptor({ + required List colorAttachments, + required GPURenderPassDepthStencilAttachment depthStencilAttachment, + }) => GPURenderPassDescriptor._raw( + colorAttachments: colorAttachments.toJS, + depthStencilAttachment: depthStencilAttachment, + ); + + external factory GPURenderPassDescriptor._raw({ + JSArray colorAttachments, + GPURenderPassDepthStencilAttachment depthStencilAttachment, + }); +} + +extension type GPUImageCopyTexture._(JSObject _) implements JSObject { + external factory GPUImageCopyTexture({GPUTexture texture}); +} + +extension type GPUImageDataLayout._(JSObject _) implements JSObject { + external factory GPUImageDataLayout({ + int offset, + int bytesPerRow, + int rowsPerImage, + }); +} + +extension type GPUCanvasConfiguration._(JSObject _) implements JSObject { + external factory GPUCanvasConfiguration({ + GPUDevice device, + String format, + int usage, + String alphaMode, + }); +} + +extension type OffscreenCanvas._(JSObject _) implements JSObject { + external factory OffscreenCanvas(int width, int height); + + external int get width; + + external int get height; + + external JSObject transferToImageBitmap(); + + GPUCanvasContext contextGPU() => _getContext('webgpu')! as GPUCanvasContext; + + CanvasContext2D context2D() => _getContext('2d')! as CanvasContext2D; + + @JS('getContext') + external JSObject? _getContext(String contextId); +} + +extension type CanvasContext2D._(JSObject _) implements JSObject { + external void drawImage(JSObject image, num dx, num dy); +} + +abstract final class GPUBufferUsage { + static const int copyDst = 0x0008; + static const int index = 0x0010; + static const int vertex = 0x0020; + static const int uniform = 0x0040; +} + +abstract final class GPUTextureUsage { + static const int copyDst = 0x02; + static const int textureBinding = 0x04; + static const int renderAttachment = 0x10; +} diff --git a/packages/flame_3d/shaders/spatial_material.frag b/packages/flame_3d/shaders/spatial_material.frag index dd87626ef1a..446ba7fda66 100644 --- a/packages/flame_3d/shaders/spatial_material.frag +++ b/packages/flame_3d/shaders/spatial_material.frag @@ -34,7 +34,7 @@ uniform Lights { float numLights; vec3 positions[MAX_LIGHTS]; vec4 colors[MAX_LIGHTS]; - float intensities[MAX_LIGHTS]; + vec4 intensities[MAX_LIGHTS]; } lights; // camera info @@ -133,7 +133,7 @@ void main() { lo += processLight( lights.positions[i], lights.colors[i].rgb, - lights.intensities[i], + lights.intensities[i].x, baseColor, normal, viewDir, diffuse ); } From 9ad71f9ee622910487fad42d2ad546a096c9ce13 Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Thu, 21 May 2026 21:00:12 +0200 Subject: [PATCH 2/6] feat(flame_3d): Add experimental `web` support using `webgpu` backend --- .github/.cspell/gamedev_dictionary.txt | 5 +++++ packages/flame_3d/README.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/.cspell/gamedev_dictionary.txt b/.github/.cspell/gamedev_dictionary.txt index e9c15ceeb56..c2f2c5971e6 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,11 @@ 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 +unorm # unsigned normalized integer +uncapturederror # dumb webgpu javascript name viewports # plural of viewport WASD # movement keys on a keyboard WBMP # wireless bitmap image format 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 dfc9264aad7..a1932f27484 100644 --- a/packages/flame_3d/README.md +++ b/packages/flame_3d/README.md @@ -42,6 +42,7 @@ Supported platforms: ⚠️¹ 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 @@ -68,6 +69,7 @@ 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 @@ -103,6 +105,7 @@ 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 @@ -116,6 +119,7 @@ following before your `runApp` call in `main.dart`: await GpuBackend.initialize(); ``` + ## Building shaders If you are using the materials provided by `flame_3d`, you do not need to worry @@ -146,6 +150,7 @@ 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 From c1e53fedfdc8c5ccb99e6f957b0235be03a0e2c0 Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Thu, 21 May 2026 21:09:27 +0200 Subject: [PATCH 3/6] feat(flame_3d): Add experimental `web` support using `webgpu` backend --- .github/.cspell/gamedev_dictionary.txt | 1 + .../flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/.cspell/gamedev_dictionary.txt b/.github/.cspell/gamedev_dictionary.txt index c2f2c5971e6..429d7f46d4a 100644 --- a/.github/.cspell/gamedev_dictionary.txt +++ b/.github/.cspell/gamedev_dictionary.txt @@ -64,6 +64,7 @@ uncapturederror # dumb webgpu javascript name 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/lib/src/graphics/backend/web_gpu/gpu_backend.dart b/packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart index 310e07af942..5a57210d0ce 100644 --- a/packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart +++ b/packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart @@ -512,7 +512,7 @@ class _WebGpuColorTexture implements GpuTexture { final image = createImageFromImageBitmap(bitmap) as Image; // We retain a clone of the image as `createImageFromImageBitmap` is lazy. - // Once it has been recorded through `drawImageRect` we can dipose it. + // Once it has been recorded through `drawImageRect` we can dispose it. // // `clone` returns a ref-counted handle, so we are fully in control on when // it gets disposed. From b5e96bdbc8fc3fddc9afbb788a68878e8e30c2fd Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Thu, 21 May 2026 21:10:52 +0200 Subject: [PATCH 4/6] feat(flame_3d): Add experimental `web` support using `webgpu` backend --- .github/.cspell/gamedev_dictionary.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/.cspell/gamedev_dictionary.txt b/.github/.cspell/gamedev_dictionary.txt index 429d7f46d4a..639095fbfc4 100644 --- a/.github/.cspell/gamedev_dictionary.txt +++ b/.github/.cspell/gamedev_dictionary.txt @@ -59,8 +59,8 @@ 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 -unorm # unsigned normalized integer uncapturederror # dumb webgpu javascript name +unorm # unsigned normalized integer viewports # plural of viewport WASD # movement keys on a keyboard WBMP # wireless bitmap image format From 3142add332057e7161912cf3308ef96a8e73a8d0 Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Fri, 22 May 2026 20:59:17 +0200 Subject: [PATCH 5/6] feat(flame_3d): Add experimental `web` support using `webgpu` backend --- .../lib/src/graphics/backend/web_gpu/gpu_backend.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart b/packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart index 5a57210d0ce..b64b1b9b9e3 100644 --- a/packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart +++ b/packages/flame_3d/lib/src/graphics/backend/web_gpu/gpu_backend.dart @@ -17,10 +17,10 @@ base class GpuBackend extends base.GpuBackend { : _queue = _device.queue, _defaultSampler = _device.createSampler( GPUSamplerDescriptor( - magFilter: 'linear', - minFilter: 'linear', - addressModeU: 'repeat', - addressModeV: 'repeat', + magFilter: 'nearest', + minFilter: 'nearest', + addressModeU: 'clamp-to-edge', + addressModeV: 'clamp-to-edge', ), ), _zeroBuffer = _device.createBuffer( From 4c17bb75c82582a09aa15b72142604a532ffc722 Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Sun, 24 May 2026 11:53:50 +0200 Subject: [PATCH 6/6] feat(flame_3d): Add experimental `web` support using `webgpu` backend --- .../lib/src/graphics/backend/flutter_gpu/gpu_backend.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/flame_3d/lib/src/graphics/backend/flutter_gpu/gpu_backend.dart b/packages/flame_3d/lib/src/graphics/backend/flutter_gpu/gpu_backend.dart index b08d21fd7ae..12f79308766 100644 --- a/packages/flame_3d/lib/src/graphics/backend/flutter_gpu/gpu_backend.dart +++ b/packages/flame_3d/lib/src/graphics/backend/flutter_gpu/gpu_backend.dart @@ -11,7 +11,10 @@ import 'package:flutter_gpu/gpu.dart' as gpu; /// A [base.GpuBackend] implemented on top of `flutter_gpu`. base class GpuBackend extends base.GpuBackend { - static Future initialize() => Future.syncValue(create() as Null); + static Future initialize() { + create(); + return Future.syncValue(null); + } static GpuBackend? create() => GpuBackend();