From 5f7b37c8d79beb90147700461159464d9ee89b9c Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sat, 7 Sep 2024 15:54:48 -0700 Subject: [PATCH] Allow particles to be arbitrary meshes. Currently, Hanabi requires that particles be 2D quads. This is sufficient for a good deal of VFX, but in many cases 3D objects are required: smoke puffs, bullet casings, etc. This commit fixes this deficiency by allowing particles to take on arbitrary meshes. To set the mesh of a particle, use the new `EffectAsset::mesh` builder method. By default, the mesh is a 2D quad. The implementation is straightforward. The previously-hard-wired quad vertices have been replaced with a `Handle`. The patch uses the existing `bevy_render` infrastructure to upload the mesh to the GPU and retrieve the vertices. Perhaps the most significant change is the generalization of rendering to allow for indexed drawing in addition to non-indexed. Because indexed drawing has a different on-GPU format for indirect draw commands from that of non-indirect draw commands, some additional bookkeeping is required. This patch also adds support for a few features useful for 3D rendering: * A `size3` attribute has been added, to allow the size to be controlled in 3D. * The `SetSizeModifier` now takes a 3D size gradient instead of a 2D one. * Vertex normals are available to modifiers via the `normal` shader variable, as long as they call the new `RenderContext::set_needs_normal` method. A new example, `puffs`, has been added to demonstrate the use of 3D meshes. It depicts the Bevy test fox running with cartoony smoke puffs emitted at a constant rate behind it. Each puff consists of multiple spherical mesh particles offset with some random jitter. A custom Lambertian lighting modifier is supplied with the example, in order to make the smoke puffs not appear solid white. (This modifier dramatically improves the look of this example, but it's very limited, so I didn't upstream it to Hanabi proper. A proper PBR lighting modifier would be useful, but would be a significant amount of work, so I chose to defer that to a follow-up.) --- Cargo.toml | 17 ++ assets/Fox.glb | Bin 0 -> 162856 bytes examples/2d.rs | 2 +- examples/billboard.rs | 2 +- examples/circle.rs | 2 +- examples/expr.rs | 4 +- examples/firework.rs | 6 +- examples/force_field.rs | 2 +- examples/multicam.rs | 8 +- examples/ordering.rs | 6 +- examples/portal.rs | 4 +- examples/puffs.rs | 262 +++++++++++++++++++++++ examples/spawn.rs | 8 +- src/asset.rs | 20 +- src/attributes.rs | 30 ++- src/lib.rs | 65 +++++- src/modifier/mod.rs | 18 +- src/modifier/output.rs | 14 +- src/modifier/ribbon.rs | 2 +- src/plugin.rs | 16 +- src/render/batch.rs | 11 +- src/render/mod.rs | 421 ++++++++++++++++++++++++++----------- src/render/vfx_common.wgsl | 19 +- src/render/vfx_render.wgsl | 39 +++- 24 files changed, 799 insertions(+), 179 deletions(-) create mode 100644 assets/Fox.glb create mode 100644 examples/puffs.rs diff --git a/Cargo.toml b/Cargo.toml index 3eb08be0..57a3c6c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,11 @@ version = "0.14" default-features = false features = [ "bevy_core_pipeline", "bevy_render", "bevy_asset", "x11" ] +[dependencies.bevy_gltf] +version = "0.14" +optional = true +features = [ "bevy_animation" ] + [package.metadata.docs.rs] all-features = true @@ -162,6 +167,18 @@ required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ] name = "ordering" required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ] +[[example]] +name = "puffs" +required-features = [ + "bevy/bevy_winit", + "bevy/bevy_pbr", + "bevy/bevy_scene", + "bevy/bevy_gltf", + "bevy/bevy_animation", + "bevy_gltf/bevy_animation", + "3d", +] + [[test]] name = "empty_effect" path = "gpu_tests/empty_effect.rs" diff --git a/assets/Fox.glb b/assets/Fox.glb new file mode 100644 index 0000000000000000000000000000000000000000..2bb946e2d4815aa7fe9960ea291459e1f6247f0e GIT binary patch literal 162856 zcmeEv2YggT*LSGWdr<*FM5Tm~-E1}+$er0;I#Q%60)`L*1VR#05G!2~rC3l%P(;B3 ziin~J$!0f-C=Vbi3MvX%P(&0F6vXn)&E|ji>{*se9-`m-yzhfQnK?7(oH=vmlqq+0 zeA*S2gTeDKfPf$SXu3bC2hqtmt zMYrmgF*YTw)tFmbU7tEFC9PjldU{HFs|h(dnc1B?b;`(0NgtmyIVCA8Cn_Ure5d4$ z^qiFRoKE91rgcn8AKNi|Lh6{LR;t#@nUgVjQdUxWYPLa~Jt-w8dBV7)F;U4GlRL$X z?KoK>$IL5}MMB9?$ z>`CKdGZ*_|?T)6zOwVxzB0&CbqEQ8G_fQ|)Mr zw#P(ATP%*&k*%kuWM!vjq$~YdqoP~i8QD51IXNXeJ0mN*b>|Vcw;q!_Zd^*%u+)@W zT6d0)RQjHrnW40qGvwCHl-8YNEmphUn47K^z!GP5M7GW;>1aKyd)FAneax*nDFeoh zD_Jc?J~>qlG@_%$8WkIFjk7o`R$GiC-fnY5c8rd)SS@ijTb$kQh_S@PJEAR-9UYFS z==fMAs?{1D7ae1gakXc*^L#sWu#7n}vP8k_0?@($lM+swKaplcXo!QD+#+cZ0=V-dCEvp`F<-Rhua$gyBEmqDeqb{_{n`61JjJ@1f#$N6# zqjqoQtjD-=UzxaaUl~WauZ*MIS0=vPS4Lf)T;y7_bg3?=O%8*>!OT2gnjiXuIjXx$ zbxrDjP*9il{wwDyf4H|VD&S~PzXp}}cjYaV}|3LaJFcZ$A*6RF3YHPrW;>~FE-2op+@j3rM zYS}}Gl*H#JQi=*8Qo1=mk$%=^;;F&UPow6{Y1AywPopcB6Io1`R$s>uDpUD%9|@r{ zm+%}Wp}Viiyv|`07E`0JoXaZAo#Py4q2Df;NO%sru$mfE$-bKzhSk)X^qF(b2L#Mf zFPYhC=W=^p&*AAvtWdG1G*deT^DXH4^coNzickX7#xtrt9-E4^t z({2gVZVl5O6Q;f7cQ)RbFG4ut>BKjAa#xgt%#C5da%G<=RgV=)aG=ZG_UBtCz1 z`fvWhc1c$-kAeTqKXmu_KR5qamF*(|S!s|B>Y!1UcEr#y8`MGLY)}Ucv_Ty-(gt5s5A0^k41Fb@!w-n@NLM&9*b=1IEJ(oOG9l?2Mx7B9W>Mi zb?cru_4>xmr zxS89-&D$ zg(WuTycBV<`YSA@h}9ne>AXvpI^2kk=s0!eq4YK-L#Z+i&7DsK^)XG1{%IHGVNvfn z!9tu@`RcR1lCPR53-txEGE!sxOV^&Pu=w55oM0a>X(v}l$Ms7JSZb~{DgG%k+=Q@ z{g|Zel&%?R8CgS8rsb&b%=Na^^sy=GBWvZkttXe#^hiq1$?#t*t8dNyjw<(J*4-5{v}pCr2CCs2Mie0 zEiqb|cx3B71A6x#GFS)H3f$1Wch6paNJRo;*1Djh^x^eeZolG^C#0s0%}UWfHn&7t z^)COdMOH>ePRZslAfEmKvJ$&w@a#$Y_vs}8i*Seu&?o8)`edZ4&)xMqA%bKJAdN{J zq_7gBqZMo8gs;AO*T33|vqU?h{kER_Bx_feHa#PkG9yT9xSn^k`(S5u;whMh-5M_TPsaW;hz6KjjN#M@$2 zRI*v4;$s{Z_3@U&uD-sH^FOW=@$FPKgELdpQ=%=2(fX(KRC1*WC26ek9E<*|2!}OB zDI-n^rF=SCy_4u)6vsp>=|iMaKLv1sk0XSljnxf0_d<0DIcKTx{#V-o$M zSQDf5A1W|WqNAeKj}b~fz}2cjL<4OuNwswXbirhab_ z1NEy9yi{1R#>U#!CrUB4n7G(b@f=i1{Zo=BB}VH%4hRuTsURjU-eQZkI@Cw4>hnKc zs#vSTuB#|ceZ*q3>Gm4DZr0c+yIL>xbgn*rP`07gx>N!~8NE`H#wJF`h7PF~Ma|dh zh*j%iQFa@nlDitxVppGTss}cEyiJ)|Y{-z7m?*VY@%Ct?i6lbD7jnofI= z{zIOjnVBhBuB^$4(RL-d%i*6Z)ZcFCXBO@!NWLj}-TDo4Mlwn(}v1)PR)KCun zJD(C`i?Kw<#;d96KZrQTvc^P3+f;7SQFPY`s5p4&Uh2n8iP8FxeTX@!byYvBQB-P1 z$~5BQm2yh-79Abuuv)Da)vK&rg%c%;`p8=;TWMZ7xNZfi$)P;mqN0uRO;tTdu&~5O z#i*a|D63b?uRd~&^Z&@lrWCKcNOZLNolk7Mnke^F3kkSm68ol%BQ_BqJP9=iwfOPs z`)(E6tSap3UaQ9u)vVT74|qy*Hs!#|=Tx)){I7&n-~Ov9E0wBw#6>GQg|0ZLCAP+e zT5_pPsJN%huY^&lN!ZjjONA=+l|p345}Sy3#HnDaBKbL@pd~0n=(92`!oo6zG6*Hu zMVW*xTIH8mWua=RxU1`9`%S_Ul0MYegt5wNl*Or?l=|J69)MMLh_=MUsUMxFf0cnc zbXQUqp04E06rp-7wFl)d%3@SV^`{|a4@&WImN*so)%RJ- z+CuChD1i(~N=?)AhvggzMEQ(EML3lNlu;{#jaPv-N;TMGbQ4#i+EjW85lpS7+Ns9H z#>eS2da_j!)e)zXb*%aUhRVq22-gaiObZRoc+^%vOs(qy%CL4cj`MF?-#f`lgI8S(K0j;`c>0du1tB zwfk4$S^s&5l0-!S^;w=nMOKwXluap2^=m+71vceGDl%BkkzQ?44rPhTWmOVW%NiRK z8LPhSQyNk^S-G)FH}QT~RPjc|L^Tn$`;XxUWev(Y5MN(Eo^q7Vm00TMnb9h1I~*#} zsqaK%b*oY#L@k{PY<7!933QHKiTX97irZ?LtJIyj`VNwH9i_sYKJGsY7|1nsuzQSLK1{8aXyf zC0Mm#w8ty!x2bhBMy6^BN&zML6BfoZK#3&rYWt%muco4+ubOkIF{$hnt$zBcY{_vh zSxT)*Z4zSC_o4c4>ipKEhl3K)lyLrBuY$Z)eP6B;geAn7g5tU%s#2Qu9AVwCCN+0u znrh?bAl9TWV~oCA30*M1HL0&yZMu<{Y#~D$`UhB2Bo678bfWWRE{p$Vk(2u^Ss@r&f}IieX|<8!-PSpe6N;)igyliblsdY zdS8q|sTH)V#HF^ueuGjX27RlbPSrh9)6_Y+b!W9-P*-#6XQxJkN^SZ9GCMgXT^)ec zeL+dhf0o3lQspHbMI90S?WyVdPp4Dd>ag4|Nu6iaucljfR+kkM_2Z$AVvUT6R3;T^ zkBp0S=%k(u3k%=t;BKQ`RlIHW;N_{03~*oi_&~S)-jAF!*A8`eeSe_) z;1A24i!K@DK9iH;{^i=W&ZraP+?V=N-HTT`ojD0)@ zJ=1roGwB7aQdjhpIqpkd?CO5&_TH}BcR%K?I<1d;W{rE6x6kb3e#D&? zG+#OwibLCft+QhJ_Cv$mKd8AM=-hPq4KKUg&2EWwQ~9ZGe;scUT)-4bfL-?(m$Z`Gj}oHNf>bN9NjrLV%GLCuejt>lgzaz#-6 zh6hJAZ@IXVJF~LQM``K0H!aSTLv~-{E^0g1x8lyboB#6OCGKABg6q^aC@rm1nOjy^ z?7X&kzw7>2s}+AXue-DB58t^?JeKcjb>%0rZ^XB*F@H>VJ-l_i^P@RCT({b0yPjHc ztFzN_b*TI1zT&#AE^E=sGuG8{&-~)K-*j=V8){>Fq8AprcE0dHF~z4i z%ezOlh>>id?s}UtZkix@(*alw6Xfr7?$np5-BNpQb|%evI=#C4 z>1XEpsGhy6yIr>39^oF>>$V`BSK5BJ(=)D<`^4(2im9*B^`h%e{fo*#$EEU6KcX>* zt~>RG;-TGIoM^k!wJLgjF|}K2tJEf`UnC^Px48QIMAvPj*M_xiN=xzSxZ%d4_!LLk zxfl6@>W#(+y4KVN?t1Z?{LAi)a;LSg@1}LSc9fRcxfi)c*_#(RNZFegd57A$%0XV_ zpz&)nyvRG-3$_*^*HGGUJe3{#kb{(c`jB^MyUIa6^= z8>T!dEybsNsQfDCc#(HhuJIw)P(4t5>dSQQ)P_~e_afI&d!~A(x}`bj9{U0>@(x`u z>UVVA>A2LVs0?(islU)TL+zi+M&m!VRcgc3K4~tXenDv|KDBd7OYx};l$PR9TDo>r z=QNMdI<)~xOY<^4Bhb7<>oj*$TAIh{K9-&@XgwS)T`#&uRAySI`GwNbeKnPh%1`Um zJ}E7YjTDEoh)7S{nB{e%;bH^5Z7%g*kKdvy72nXr1~IrKNMB zJ{2yPQG6PQX`PNmdD8QO!ZFSfN>1bKM(b)`F3gM4QW>b6bYDvAbS!#Kp>@iW($bj5 z^0IL@q&PI@P<)C*;}ykMvCFt~pnGYBV~j=PHuYs1pJ|=$!NSqf_(1nXv`%@_ap{?b z-Z?0I<1T{M6|Hghp|rG};!_!DoytsU=~;&6l=M%JH&1`7k~{8nRI%%-BIl*8s=JSD zjVY%16o=XdwY};$v~oRPwXyrqyeoaQK6Cq(u5k_Px*tB+$wg^Do?O^Gwf-!(cgken z!j3OCAGUR-`_L1yKIiNr=fFAH?q6QD2GzZrTDnG8o#d|F*XyIS4{MEGSG_sLUFlGh z;^mPYTyHn+?rtC1v^c)TA?N%ww|mLIt}3Rqt7ki0rN4FEqRJ-MQ{RYHW0Q9p7C3LG?xMo{vPh zw-1bQz1Zgl_n8}B48k8_KjR!wX|DU{9TnXVxUX?lTs*^l`yZ{{wEcx6OPm|D#qPZ| zuMWD)qWJV~isJ9St7d+&^C9>0^=;fcnw`l%Fm<8({ldOM{HdH2pPsKNf3>GF?(wJ$ zbX8jeL zpS#cRwS)9V<)QLZndw}p%oK;(8?_ngQ`Gio3~yAkv8(El3hqO{EcQ`+>UYz+?RGx< z)^n~7o05yE40Ii-9`yCPx#oEH-FH-QrQ9K#4{0%@q%KxhYhE*Vj(cCX8LmyczG?n; zvhq81e@lIY`qciXtF<`XEw|*3k+$nGUY#1zx362ed|8p8dbM?&^;%`-_q%TbMTUbuF)+U6t|mE z(RD?7p6ksgE-l_tyOyiT^haH1=QJtaS*M2Uj%|0iGCMabj%&QkdEm{vTr0XK$Sm%9d7FE6fN>!9=6Z&$eP zT-?0)#>|7xyJpXFjTmrcar+M!IM4QY$W?t*{o?-LKH?nn#u8V(9*u)!pf*6qt^WOK z=h}qTt|hPh?4$am_!NiAPsgS6rSU56_%*I7L%wm1?=-`;YDGO)pYQH*-PvSX&|Ryx zZIM&QdA8RuSJkP7&Wz_5xZc?@!A19(`k82hy}f&k=R?=&MooRgzW(0TA18`<)`vIGObaIGnM+gqknW2=Z$^InK|KlcfyIQi&uZO z-5I?q&b_O!M^KK-I91&>ss1~zF+0+V->>K6 zz2HOZQPy9*=b~+sb;kMnx04H{3V{%PExagxRmiW82Ojz!}G z&ABwb(fmutrL;6w(>zOg(wx}s@(!-e%cr=L>s}I+$0;qfRk~-Oc1!aq-5=6?MDq^i zPxCU3`7{^Myh(E<G!SbsHD${%h7X zKFX8&GPM(G8#Er$yhCFn^&Og9sUOjNN_9^4+2ZL|t~xu?+*8tXT{O2+d!xCM`U16W zn#-si(z(z*@RaXcx}Kew>OOQZIw(ieInvxp{g>uMnk%Uc^xciVPf=Q$?`RyLxrW{e z(0Cr?3naf#oB44|Ls#@~Z@Ff!TH&H`iP`{-X>{%A9*M?J8tW(@%AfM2I5c0-*hukd z45x9J%0}ncVW_r{b~ z#akcFT*@ZBIG54$8m+UuV9++NXY=_E4{?7{FUK`1`AXlPHJZ3HZ_U*G$k;Q`x;h{E za6Y26%8z_F>(Ly2XJiK-&V5v7+OFb*59dDGPW7gI#CX@BI#F@Yhw~lP&lQcY^x>>W za|F#hbS^4}dvU&_wCdd9!&&dUBTKwE-%)&eW~4cYo@;1bk5Sll!oeiTjy z8UtzGp|a6-st0{d@fJq!-ouSc-?pe8D89-~UYrwE+&13D==&XgQ=s{a%0TfcPkI+b zb)|Ic!}*cQLuC%9LyALXrZ!Cdn%X3_TNU$tI47!@Z`^HCS`~l1I49EhK*y!?rR%G& z7tV|{uhH`(%WH-|Qoo~lkmhdcztjg+?(^ZCNasuEsPdW@=R~zHHSTh$&#JuU!#R=i zr|(~Pbvoe1Ig!%Vm{HM(Gov~u`fyHstadHm)urc0DnpcOg%@W=l@pD(o(>(~@!~w# zEc2ijXFzo}^x|x&&ShSl2i3XEi*u+t8+vgzq-(9tcV3(UsSH$a6o=BP9PPzfTBmuR(oz}d+bbQH`W?;tbWcL@>02TJH@(wVfyQK-vuJ##`HSMyacN$od?-Gh3-xij zx1o16^bL{T`Ov*3rB!D-AI@~tm#J+~o1t-x#x9x*sP9mJq5e*D9Mv1uKeZ*A@6`Fu zhw~lHU({}?kI-C0<)?N?_srDJ)tSzRGaa2T^;w$xXwIVJ()>lgX`%Qu_tBh1a{+(EB%^o5p!M7fMU<>A3W6mY$>O`I^@08z`lvHcaj6@l$r+(IwZp({eiaX#MUTHs8v5 zgWR2SYm}V7u-{YuYrNCZI~hgmB7BUx;^P7zN=xGawFi3cqcTw0XxyN6%9GNnxf^dB zRGx4$)4LQ(OYf=Z{naOpn!0e0P3t->&Q_F`&X@YdX>uP9;ZTPD#Zj z;|+rDMQOaEF-on4@vcE-4mTHyuWZ$&-%$)!cNDZv*PZfIcN8w%Z_u+g#iw=3lhV?5 z>QmIVX+0b*y{n`+l$O?Y*$@XPEzOCFk8y`X_e-=+_g9ow@i*>y)EtfbAL=i(PVI-% zQk_s;(Y52_VqR*#M%<%yIxk8~=R)@k6o9rKRU|icj}fv`)|al$Ofi{GoyGnV;`>?ftD@G2JWDxu|yItz6-|fKSiqN}tBJ z^z=+e>vS(iY3bbpm7nsY^>DOwEV`eh&Yg(soI+T{47wCSE?(yh(knZK^ zxq$MdZ$MNwddEfYwCNt6;?Or5icj|#6o=B%I*q-Qme%QhMxBX_@0Vy@oiB{DN_pVHh%@1JR%-d9js`bI#%Yoc$>v`*g+C@pQL z-&4_jFRknLjJ+|Xr83ib(fb5ir}yxbmd=;9)B6@$r+4y{mgP)uI#rycMhAi$@$gnr(GExJ@&AZQarr@3+94aS02U&iskVA{<)rf_u0)s zv~=#2mbN=4)^^?7WtFp4>OC&Xb4_A3*WxD&owtr&5~P!RYkuiW*ga0X{`@;Z?Nldp zE|fpzNoA({Te|0@`%>!T>b}nSc9HrK-QUuEFWqlbAEZ1fAL@5>e@OAEeNtNLYt-iH zxYW)mK8;b-?UY%NDL!qd{AoLl58?RJxlq|CKGg%2 zjm89Z$8LNRPW_I?2ii{Ullm{c520~``YiS3aPfrR?a+8aeUQpe<)pGvnW-O9pQZTJ zzvx)u_*0*yv4Z+Lty3I27Hy~FQaLF;oeSkp>og`)IhCIp=R4&WUgUk{r^dT2Ju4}@ z@*?lkdw9xc%7Df`w#%=Us4?t;^&-$<84|z^GFk6;Hf%@AdBosBFsSeaNjUp7_Wfz>7VA@*^+uKIKWH8hM zZ=rWY^zMcFAoV-SpZXeYr}s$ICaF(Rp48^4ZBrR2E%h}TPbe)Nm-3;sbX*!M!ud6o zf%+!(cj}LHEXtGO(>mozV+G|&WuSAUv=pD}g!(o0HTvdGV+FNIicfu(#t{1UL4B6S z5cL6ajvJjqWYwD`sPb%>3nHB#iw<8zM!&N=xr3=v^G;N$d2kgVNGH58Vq<`DvZ*^C>NTQ=@wr`YuLkDGsHjZ+!HdZ|=2z+_Gp46&iyKIz|HY$C zwU3={gMLBcY3>)#)JRGslH{*HuP& zmGz|9o!mwO5Bj`me1xoYOIx|*c!a!h=(R=(Jp-@)8@t5Kk!j*so#({0p4-IH_}9hz z#j)a*2ZxEKJ!8dhN!`TCaSpNmxvpY!|Et8PCdu-ZhdRoh%8GfsYA zoGdfi#Yps{z1{7D<#nQuO!+cJ?)^AH{?RW-Hl3AVz@P*EH(5u;O}!%J&Lu}gL{79E zc+W=h!Sz=edV$=qlbb$H7kAv%N*=B{TWp-yN-pj+b&IgWuu|`-GgZL#!v(*VE`v@xyx^BLzl-oMYnl!e(;M`zH-NPB*Q3RXJwwFT%0;H?jEf zF9hJ`xBntq7X2tz|C}W4Fa9PLZ5u1A&O9ZyPfnHx&Yl$~um4n=G%HO`9{#vCkl^EB*r+Dtn2BRFhzpZFlXS0C^eVMn147OT>0hlQ+EN5V_yv%G*w3AkXqrqN5-qE( zn<$5E5%Q<@lZ+C4KwqHs65rjvQGA(^FE%~&tf(-tk7)2qp@2SMSC~h=mp&GImOd*U z&DbfXPkK$Xx_Xy*_=#r>{bK#0XRHg>3F`p6fnCAQU{`=ahjj%V@X!x@jQ?xZZ^V#6 z?+VA4Rd_pjhpSAp!e=Jwcz~ z2m1kE=n-}Td&l~xG#xE>EvO>Lo$MrMY)m)S5$lTeYfwK*X75Wf>;dbIeysbcf(~*^ zofL!5wB(Mm@cGGx-GB~wSXaz@*PqA5u(1UKCG-kB#6#>0>VG*`)Scc>phTPm9`Yvg zCH4;}IUe)_z2xK#m&5WZOO%Koz(YR9I2a%0=-h6iN7dd&-iDuz7(GcY+*wg#9F$lW z-~q;Xh+8NP|1I@r_)|fpNa4NmO#!<=33~zF#?MxXpPMf;;th{O=!d_e1RvNN_6Nvs z$ZIHHZgW~xj<*^6i2MaFhz%W^8T%&WLzLiy{RT?V0T1~S`zqv3#8ucm;w9SQALz$E z26-5K;Lor__%BN26^;k{MLs|YI`G531N&a=J5VB@0uM0yv42I0d<(o0M=Wx6?^rqM z=T0*7-A=Ogp)T@N6=&c`NOV0qRTTWa^!Y(zIh|v zIKKdIR?bh_qSb@tn7R4dW4-zs=c}X7OxHfi?q{5{z>nj>URPm;@D=oS~9ohH#FE!uSwPkf{ zv6kFplQ?n9kDBKbpXfQYiijCfC|+4pMKp{2N}SD368ASeAU?k_Q=q@Y-7kw6Yc=uw zWiN>}?W&8}4PO$AQ;%uWlD3H9pH~r%f=y!I&s7BYLa&f}^4co$&WEZB%n$u&$N1m} zKJbq&2RCcqEEp_o%iq^>r(J5CBO9EuieA1LG5L9$cs%i>*5=Ux^1UnS%9yA7%88#h zmQxS)lz)Cu#VG%E@;Z6^^C}W}$aA~jzgjf^p{oEt-A@~w8f5tAn@fJyx@I;P*L`P` zb@%j^b52Ier(Sf*SqnSMm61*(E@|%#l$SNL$_f(_WdG$3`Ppr`^5IV>%Jh}DNXP69 zS)=YmdE23Mqa4?Jf=ur_-oRVdaiEmhLV^#@wXjF`vO%)et!<2RGw{%lb93Lgfik5+ zR|$SNH{;xeezarVN7Onj9vh!#tUux(;vDSzzRxd{iw^ZR>>2S8b`3k{c;JKhh`0v- zLHq+h;30k@-hmEr>z%cO#e&3MM!WK?N`$)t&tPkQC>;UVGID~ixIgnSoCx0!*zR=Hzi;xR(7;&Qg zng%ld&K5?T17F-H;eHA64e<%_3ULJcTlhEp8u0^qk8PW${rpWA0e^sA;Wy9|@&f!1 zegu5bgAVP85AZYe!>{457zg(s=*PMNZ*@VIoYj1+taW699CH5+GHrd1oVw~}`Tg8+ zvXMwM+JU#`_2pvFj7{S1xa&mcTYbj475ixHi-9+6Qx(zponOWJD|(9I&s3C2Z*>z( z@@pAojYbw>f3TJWUgdYzib@;56;p0{R!qw)5KS)KE;yxkXysCoy zY41r9b$>01e(bvtSFj&Jd9v{w@#qy6Bi>*?eKftIeDbzRlJhOvbBUbyVO`l`?MYF* zzkx9hV91N}HqPnbH+z12?R3)=tbwo`c2SdDqL{y%U9w->*@UnD}cK*}GSp5?H6lz0|8SGG*VvHqoic zZL;Gl*`nUdU8ETCjW+xD1UY}iR;|(Q1UdM&!`d}p-5?J%NfY;dHcn2Dsv){;y-9v^ z_EyofUb@`Vy@u$e_6CREJ+A$$PlmK_uOaGRHc@U!sV;U6oFwm`c}jbA;w1UwpVbUK zeq`@1wYJp+vRnJViq4DbP8_|H=hx#aNAz7uobbIKQQt1NLopT9U=esSb8 zk$PW-+`Q^Hf%8A$>SwcLenJKLLRy^MbG(9_JWP|ri@y`#`}4w#l6$t+QwGZlXVXIV zJ&leGl27N4HsTldO^ADlmnacWfd{_0rvpFO6Yc>gcOE3)Jv~UGANPR3n=pK^Jbv#K ziv3}{XH^^Ey+#(PkVUMuGehuwf?aRM2;vVb}c8PwpqeMIeAH+Ar zHTVJIDdHsP5YG@_K#z9B#lpW1h_|mfBe0HGKg3zYCBz%V3B*mrG4O@oBJRL{5r5DR zeuziFM?d6%T_Vo+sFo<3G(Kb8i{qXPc@FvP!zz8n!QLMT*dg}e^4Z&EQuApN{sKQi z9Dtp`J`kr6moOj9AM1elG^pka;^Rlo2*e+(C+q@o1mhzfA@5?nueJ0N)`w0T{tiD! zoVv{6kZ0<*42ny7oWi|1;u7K%;tt{o;sX2`{*8M)#Dz@>J>=q9e;M%segHp#exWb; z8}=#iBhUdK=O3Jlzz-$X0eJ93_&f06&+vE10X+1BAI_QBFJk|Q67QM7!@6QUrmpHE zs=WD?K-n;Pnb_9hTLC=m)3I;we_Y+KztL5qT=dpUBCc6iV?PhOhMnKoQp!4?){)?g zabV9VVducZejWTV9!l)PfmgU-XF{JI>7xGqUA5Fd`ibjuXKFJ(9B7nFpZPiAp*3>_ z@E{-T0q1MnFT*drt&-%oO-=}uxc3Df&NsNv&N((oG|p}%P~toUJjnO-##F6$U#Gyn z9VO%j9?toY5AveCk39{VDLkcW<z8Lo+cz-}#Mmz9O z!cT#R5^){x4=8cZ20Y{;+$Z5(3nl1)2N?X(4t(&zdkfC%$V{nx)2aazQ-!@!kyklS-@Oy84J(=*U$LL49t=

$8LuP`F`s8j(SV5Y9H*|CDyn6 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

1jLurt$; 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 Brg{JX literal 0 HcmV?d00001 diff --git a/examples/2d.rs b/examples/2d.rs index 4f4d67d9..17f3f3a5 100644 --- a/examples/2d.rs +++ b/examples/2d.rs @@ -92,7 +92,7 @@ fn setup( .init(init_age) .init(init_lifetime) .render(SizeOverLifetimeModifier { - gradient: Gradient::constant(Vec2::splat(0.02)), + gradient: Gradient::constant(Vec3::splat(0.02)), screen_space_size: false, }) .render(ColorOverLifetimeModifier { gradient }) diff --git a/examples/billboard.rs b/examples/billboard.rs index 977c5fc5..feace31a 100644 --- a/examples/billboard.rs +++ b/examples/billboard.rs @@ -135,7 +135,7 @@ fn setup( rotation: Some(rotation_attr), }) .render(SizeOverLifetimeModifier { - gradient: Gradient::constant([0.2; 2].into()), + gradient: Gradient::constant([0.2; 3].into()), screen_space_size: false, }), ); diff --git a/examples/circle.rs b/examples/circle.rs index 3fa04494..665a04e5 100644 --- a/examples/circle.rs +++ b/examples/circle.rs @@ -126,7 +126,7 @@ fn setup( .render(FlipbookModifier { sprite_grid_size }) .render(ColorOverLifetimeModifier { gradient }) .render(SizeOverLifetimeModifier { - gradient: Gradient::constant([0.5; 2].into()), + gradient: Gradient::constant([0.5; 3].into()), screen_space_size: false, }), ); diff --git a/examples/expr.rs b/examples/expr.rs index 402f6814..178deb8a 100644 --- a/examples/expr.rs +++ b/examples/expr.rs @@ -45,8 +45,8 @@ fn setup(mut commands: Commands, mut effects: ResMut>) { color_gradient.add_key(1.0, Vec4::new(0.0, 0.0, 0.0, 0.0)); let mut size_gradient = Gradient::new(); - size_gradient.add_key(0.3, Vec2::new(0.2, 0.02)); - size_gradient.add_key(1.0, Vec2::ZERO); + size_gradient.add_key(0.3, Vec3::new(0.2, 0.02, 1.0)); + size_gradient.add_key(1.0, Vec3::ZERO); let writer = ExprWriter::new(); diff --git a/examples/firework.rs b/examples/firework.rs index 14887161..008c0ea7 100644 --- a/examples/firework.rs +++ b/examples/firework.rs @@ -59,9 +59,9 @@ fn setup(mut commands: Commands, mut effects: ResMut>) { color_gradient1.add_key(1.0, Vec4::new(4.0, 0.0, 0.0, 0.0)); let mut size_gradient1 = Gradient::new(); - size_gradient1.add_key(0.0, Vec2::splat(0.05)); - size_gradient1.add_key(0.3, Vec2::splat(0.05)); - size_gradient1.add_key(1.0, Vec2::splat(0.0)); + size_gradient1.add_key(0.0, Vec3::splat(0.05)); + size_gradient1.add_key(0.3, Vec3::splat(0.05)); + size_gradient1.add_key(1.0, Vec3::splat(0.0)); let writer = ExprWriter::new(); diff --git a/examples/force_field.rs b/examples/force_field.rs index 055c05be..a7ed2cd0 100644 --- a/examples/force_field.rs +++ b/examples/force_field.rs @@ -319,7 +319,7 @@ fn setup( .update(allow_zone) .update(deny_zone) .render(SizeOverLifetimeModifier { - gradient: Gradient::constant(Vec2::splat(0.05)), + gradient: Gradient::constant(Vec3::splat(0.05)), screen_space_size: false, }) .render(ColorOverLifetimeModifier { gradient }), diff --git a/examples/multicam.rs b/examples/multicam.rs index 15d401e7..b6959006 100644 --- a/examples/multicam.rs +++ b/examples/multicam.rs @@ -28,10 +28,10 @@ struct SplitCamera { fn make_effect(color: Color) -> EffectAsset { let mut size_gradient = Gradient::new(); - size_gradient.add_key(0.0, Vec2::splat(1.0)); - size_gradient.add_key(0.5, Vec2::splat(5.0)); - size_gradient.add_key(0.8, Vec2::splat(0.8)); - size_gradient.add_key(1.0, Vec2::splat(0.0)); + size_gradient.add_key(0.0, Vec3::splat(1.0)); + size_gradient.add_key(0.5, Vec3::splat(5.0)); + size_gradient.add_key(0.8, Vec3::splat(0.8)); + size_gradient.add_key(1.0, Vec3::splat(0.0)); let mut color_gradient = Gradient::new(); color_gradient.add_key(0.0, Vec4::splat(1.0)); diff --git a/examples/ordering.rs b/examples/ordering.rs index 9d2fa433..4c899d1a 100644 --- a/examples/ordering.rs +++ b/examples/ordering.rs @@ -37,9 +37,9 @@ fn make_firework() -> EffectAsset { // Keep the size large so we can more visibly see the particles for longer, and // see the effect of alpha blending. let mut size_gradient1 = Gradient::new(); - size_gradient1.add_key(0.0, Vec2::ONE); - size_gradient1.add_key(0.1, Vec2::ONE); - size_gradient1.add_key(1.0, Vec2::ZERO); + size_gradient1.add_key(0.0, Vec3::ONE); + size_gradient1.add_key(0.1, Vec3::ONE); + size_gradient1.add_key(1.0, Vec3::ZERO); let writer = ExprWriter::new(); diff --git a/examples/portal.rs b/examples/portal.rs index 7042fa96..55649277 100644 --- a/examples/portal.rs +++ b/examples/portal.rs @@ -49,8 +49,8 @@ fn setup(mut commands: Commands, mut effects: ResMut>) { color_gradient1.add_key(1.0, Vec4::new(4.0, 0.0, 0.0, 0.0)); let mut size_gradient1 = Gradient::new(); - size_gradient1.add_key(0.3, Vec2::new(0.2, 0.02)); - size_gradient1.add_key(1.0, Vec2::splat(0.0)); + size_gradient1.add_key(0.3, Vec3::new(0.2, 0.02, 1.0)); + size_gradient1.add_key(1.0, Vec3::splat(0.0)); let writer = ExprWriter::new(); diff --git a/examples/puffs.rs b/examples/puffs.rs new file mode 100644 index 00000000..33d5a588 --- /dev/null +++ b/examples/puffs.rs @@ -0,0 +1,262 @@ +//! Puffs +//! +//! This example creates cartoony smoke puffs out of spherical meshes. + +use crate::utils::*; +use bevy::{ + color::palettes::css::FOREST_GREEN, + core_pipeline::tonemapping::Tonemapping, + math::vec3, + prelude::*, + render::mesh::{SphereKind, SphereMeshBuilder}, +}; +use bevy_hanabi::prelude::*; +use serde::{Deserialize, Serialize}; +use std::{error::Error, f32::consts::FRAC_PI_2}; + +mod utils; + +// A simple custom modifier that lights the meshes with Lambertian lighting. +// Other lighting models are possible, up to and including PBR. +#[derive(Clone, Copy, Reflect, Serialize, Deserialize)] +struct LambertianLightingModifier { + // The direction that light is coming from, in particle system space. + light_direction: Vec3, + // The brightness of the ambient light (which is assumed to be white in + // this example). + ambient: f32, +} + +// The position of the light in the scene. +static LIGHT_POSITION: Vec3 = vec3(-20.0, 40.0, 5.0); + +fn main() -> Result<(), Box> { + let app_exit = make_test_app("puffs") + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 500.0, + }) + .add_systems(Startup, setup) + .add_systems(Update, setup_scene_once_loaded) + .run(); + app_exit.into_result() +} + +// Performs initialization of the scene. +fn setup( + mut commands: Commands, + asset_server: Res, + mut effects: ResMut>, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Spawn the camera. + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(25.0, 15.0, 20.0).looking_at(Vec3::ZERO, Vec3::Y), + camera: Camera { + hdr: true, + clear_color: Color::BLACK.into(), + ..default() + }, + tonemapping: Tonemapping::None, + ..default() + }); + + // Spawn the fox. + commands.spawn(SceneBundle { + scene: asset_server.load("Fox.glb#Scene0"), + transform: Transform::from_scale(Vec3::splat(0.1)), + ..default() + }); + + // Spawn the circular base. + commands.spawn(PbrBundle { + mesh: meshes.add(Circle::new(15.0)), + material: materials.add(StandardMaterial { + base_color: FOREST_GREEN.into(), + ..default() + }), + transform: Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2)), + ..default() + }); + + // Spawn a light. + commands.spawn(DirectionalLightBundle { + directional_light: DirectionalLight { + color: Color::WHITE, + illuminance: 2000.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_translation(LIGHT_POSITION).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + // Create the mesh. + let mesh = meshes.add(SphereMeshBuilder::new(0.5, SphereKind::Ico { subdivisions: 4 }).build()); + + // Create the effect asset. + let effect = create_effect(mesh, &mut effects); + + // Spawn the effect. + commands.spawn(( + Name::new("cartoon explosion"), + ParticleEffectBundle { + effect: ParticleEffect::new(effect), + ..default() + }, + )); +} + +// Builds the smoke puffs. +fn create_effect(mesh: Handle, effects: &mut Assets) -> Handle { + let writer = ExprWriter::new(); + + // Position the particle laterally within a small radius. + let init_xz_pos = SetPositionCircleModifier { + center: writer.lit(Vec3::ZERO).expr(), + axis: writer.lit(Vec3::Z).expr(), + radius: writer.lit(1.0).expr(), + dimension: ShapeDimension::Volume, + }; + + // Position the particle vertically. Jiggle it a little bit for variety's + // sake. + let init_y_pos = SetAttributeModifier::new( + Attribute::POSITION, + writer + .attr(Attribute::POSITION) + .add(writer.rand(VectorType::VEC3F) * writer.lit(vec3(0.0, 1.0, 0.0))) + .expr(), + ); + + // Set up the age and lifetime. + let init_age = SetAttributeModifier::new(Attribute::AGE, writer.lit(0.0).expr()); + let init_lifetime = SetAttributeModifier::new(Attribute::LIFETIME, writer.lit(3.0).expr()); + + // Vary the size a bit. + let init_size = SetAttributeModifier::new( + Attribute::F32_0, + (writer.rand(ScalarType::Float) * writer.lit(2.0) + writer.lit(0.5)).expr(), + ); + + // Make the particles move backwards at a constant speed. + let init_velocity = SetAttributeModifier::new( + Attribute::VELOCITY, + writer.lit(vec3(0.0, 0.0, -20.0)).expr(), + ); + + // Make the particles shrink over time. + let update_size = SetAttributeModifier::new( + Attribute::SIZE, + writer + .attr(Attribute::F32_0) + .mul( + writer + .lit(1.0) + .sub((writer.attr(Attribute::AGE)).mul(writer.lit(0.75))) + .max(writer.lit(0.0)), + ) + .expr(), + ); + + // Add some nice shading to the particles. + let render_lambertian = + LambertianLightingModifier::new(LIGHT_POSITION.normalize_or_zero(), 0.7); + + let module = writer.finish(); + + // Add the effect. + effects.add( + EffectAsset::new(vec![256], Spawner::burst(16.0.into(), 0.45.into()), module) + .with_name("cartoon explosion") + .init(init_xz_pos) + .init(init_y_pos) + .init(init_age) + .init(init_lifetime) + .init(init_size) + .init(init_velocity) + .update(update_size) + .render(render_lambertian) + .mesh(mesh), + ) +} + +// A system that plays the running animation once the fox loads. +fn setup_scene_once_loaded( + mut commands: Commands, + asset_server: Res, + mut animation_graphs: ResMut>, + mut players: Query<(Entity, &mut AnimationPlayer), Added>, +) { + for (entity, mut animation_player) in players.iter_mut() { + let (animation_graph, animation_graph_node) = + AnimationGraph::from_clip(asset_server.load("Fox.glb#Animation2")); + let animation_graph = animation_graphs.add(animation_graph); + animation_player.play(animation_graph_node).repeat(); + commands.entity(entity).insert(animation_graph.clone()); + } +} + +impl LambertianLightingModifier { + fn new(light_direction: Vec3, ambient: f32) -> LambertianLightingModifier { + LambertianLightingModifier { + light_direction, + ambient, + } + } +} + +// Boilerplate implementation of `Modifier` for our lighting modifier. +#[cfg_attr(feature = "serde", typetag::serde)] +impl Modifier for LambertianLightingModifier { + fn context(&self) -> ModifierContext { + ModifierContext::Render + } + + fn as_render(&self) -> Option<&dyn RenderModifier> { + Some(self) + } + + fn as_render_mut(&mut self) -> Option<&mut dyn RenderModifier> { + Some(self) + } + + fn attributes(&self) -> &[Attribute] { + &[] + } + + fn boxed_clone(&self) -> BoxedModifier { + Box::new(*self) + } + + fn apply(&self, _: &mut Module, _: &mut ShaderWriter) -> Result<(), ExprError> { + Err(ExprError::TypeError("Wrong modifier context".to_string())) + } +} + +// The implementation of Lambertian lighting. +#[cfg_attr(feature = "serde", typetag::serde)] +impl RenderModifier for LambertianLightingModifier { + fn apply_render(&self, _: &mut Module, context: &mut RenderContext) -> Result<(), ExprError> { + // We need the vertex normals to light the mesh. + context.set_needs_normal(); + + // Shade each fragment. + context.fragment_code += &format!( + "color = vec4(color.rgb * mix({}, 1.0, dot(normal, {})), color.a);", + self.ambient.to_wgsl_string(), + self.light_direction.to_wgsl_string() + ); + + Ok(()) + } + + fn boxed_render_clone(&self) -> Box { + Box::new(*self) + } + + fn as_modifier(&self) -> &dyn Modifier { + self + } +} diff --git a/examples/spawn.rs b/examples/spawn.rs index b4b99d29..ec4e9f70 100644 --- a/examples/spawn.rs +++ b/examples/spawn.rs @@ -75,10 +75,10 @@ fn setup( color_gradient1.add_key(1.0, Vec4::splat(0.0)); let mut size_gradient1 = Gradient::new(); - size_gradient1.add_key(0.0, Vec2::splat(0.1)); - size_gradient1.add_key(0.5, Vec2::splat(0.5)); - size_gradient1.add_key(0.8, Vec2::splat(0.08)); - size_gradient1.add_key(1.0, Vec2::splat(0.0)); + size_gradient1.add_key(0.0, Vec3::splat(0.1)); + size_gradient1.add_key(0.5, Vec3::splat(0.5)); + size_gradient1.add_key(0.8, Vec3::splat(0.08)); + size_gradient1.add_key(1.0, Vec3::splat(0.0)); let writer1 = ExprWriter::new(); diff --git a/src/asset.rs b/src/asset.rs index f02f3ca8..30bcc750 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,7 +1,8 @@ use std::ops::Deref; use bevy::{ - asset::Asset, + asset::{Asset, Handle}, + prelude::Mesh, reflect::Reflect, utils::{default, HashSet}, }; @@ -198,6 +199,12 @@ pub enum AlphaMode { /// /// [`AlphaMask3d`]: bevy::core_pipeline::core_3d::AlphaMask3d Mask(ExprHandle), + + /// Render the effect with no alpha, and update the depth buffer. + /// + /// Use this mode when every pixel covered by the particle's mesh is fully + /// opaque. + Opaque, } /// Asset describing a visual effect. @@ -258,6 +265,11 @@ pub struct EffectAsset { module: Module, /// Alpha mode. pub alpha_mode: AlphaMode, + /// The mesh that each particle renders. + /// + /// This defaults to a quad facing the Z axis. + #[cfg_attr(feature = "serde", serde(skip))] + pub mesh: Option>, } impl EffectAsset { @@ -715,6 +727,12 @@ impl EffectAsset { pub fn texture_layout(&self) -> TextureLayout { self.module.texture_layout() } + + /// Sets the mesh that each particle will render. + pub fn mesh(mut self, mesh: Handle) -> Self { + self.mesh = Some(mesh); + self + } } /// Asset loader for [`EffectAsset`]. diff --git a/src/attributes.rs b/src/attributes.rs index 043c5c66..dbec5ed7 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -85,7 +85,8 @@ //! | [`Attribute::HDR_COLOR`] | The particle's HDR color as `vec4`. | //! | [`Attribute::ALPHA`] | The particle's opacity. | //! | [`Attribute::SIZE`] | The particle's uniform size. | -//! | [`Attribute::SIZE2`] | The particle's non-uniform size. | +//! | [`Attribute::SIZE2`] | The particle's non-uniform 2D size. | +//! | [`Attribute::SIZE3`] | The particle's non-uniform 3D size. | //! | [`Attribute::AXIS_X`] | X axis of the particle frame. | //! | [`Attribute::AXIS_Y`] | Y axis of the particle frame. | //! | [`Attribute::AXIS_Z`] | Z axis of the particle frame. | @@ -537,6 +538,11 @@ impl AttributeInner { Value::Vector(VectorValue::new_vec2(Vec2::ONE)), ); + pub const SIZE3: &'static AttributeInner = &AttributeInner::new( + Cow::Borrowed("size3"), + Value::Vector(VectorValue::new_vec3(Vec3::ONE)), + ); + pub const PREV: &'static AttributeInner = &AttributeInner::new( Cow::Borrowed("prev"), Value::Scalar(ScalarValue::Uint(!0u32)), @@ -958,10 +964,10 @@ impl Attribute { /// [`ScalarType::Float`] pub const SIZE: Attribute = Attribute(AttributeInner::SIZE); - /// The particle's 2D size, for quad rendering. + /// The particle's 2D size. /// - /// The particle, when drawn as a quad, is scaled along its local X and Y - /// axes by these values. + /// The particle is scaled along its local X and Y axes by these values. The + /// Z axis is unaffected. /// /// # Name /// @@ -972,6 +978,19 @@ impl Attribute { /// [`VectorType::VEC2F`] representing the XY sizes of the particle. pub const SIZE2: Attribute = Attribute(AttributeInner::SIZE2); + /// The particle's 3D size. + /// + /// The particle is scaled along its local X, Y, and Z axes by these values. + /// + /// # Name + /// + /// `size3` + /// + /// # Type + /// + /// [`VectorType::VEC3F`] representing the XYZ sizes of the particle. + pub const SIZE3: Attribute = Attribute(AttributeInner::SIZE3); + /// The previous particle in the ribbon chain. /// /// # Name @@ -1143,7 +1162,7 @@ impl Attribute { declare_custom_attr_pub!(F32X4_3, "f32x4_3", 4, VEC4F); /// Collection of all the existing particle attributes. - const ALL: [Attribute; 31] = [ + const ALL: [Attribute; 32] = [ Attribute::POSITION, Attribute::VELOCITY, Attribute::AGE, @@ -1153,6 +1172,7 @@ impl Attribute { Attribute::ALPHA, Attribute::SIZE, Attribute::SIZE2, + Attribute::SIZE3, Attribute::PREV, Attribute::NEXT, Attribute::AXIS_X, diff --git a/src/lib.rs b/src/lib.rs index 1796342d..7c7c347f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -779,7 +779,7 @@ impl EffectShaderSource { if attr == Attribute::SIZE { if !has_size { inputs_code += &format!( - "var size = vec2(particle.{0}, particle.{0});\n", + "var size = vec3(particle.{0}, particle.{0}, particle.{0});\n", Attribute::SIZE.name() ); has_size = true; @@ -788,11 +788,21 @@ impl EffectShaderSource { } } else if attr == Attribute::SIZE2 { if !has_size { - inputs_code += &format!("var size = particle.{0};\n", Attribute::SIZE2.name()); + inputs_code += &format!( + "var size = vec3(particle.{0}, 1.0);\n", + Attribute::SIZE2.name() + ); has_size = true; } else { warn!("Attribute SIZE2 conflicts with another size attribute; ignored."); } + } else if attr == Attribute::SIZE3 { + if !has_size { + inputs_code += &format!("var size = particle.{0};\n", Attribute::SIZE3.name()); + has_size = true; + } else { + warn!("Attribute SIZE3 conflicts with another size attribute; ignored."); + } } else if attr == Attribute::HDR_COLOR { if !has_color { inputs_code += @@ -821,7 +831,7 @@ impl EffectShaderSource { if !has_size { inputs_code += &format!( "var size = {0};\n", - Attribute::SIZE2.default_value().to_wgsl_string() // TODO - or SIZE? + Attribute::SIZE3.default_value().to_wgsl_string() // TODO - or SIZE? ); } if !has_color { @@ -977,6 +987,9 @@ impl EffectShaderSource { if render_context.needs_uv { layout_flags |= LayoutFlags::NEEDS_UV; } + if render_context.needs_normal { + layout_flags |= LayoutFlags::NEEDS_NORMAL; + } let alpha_cutoff_code = if let AlphaMode::Mask(cutoff) = &asset.alpha_mode { render_context.eval(&module, *cutoff).unwrap_or_else(|err| { @@ -1135,6 +1148,7 @@ pub struct CompiledParticleEffect { simulation_condition: SimulationCondition, /// Handle to the effect shader for his effect instance, if configured. effect_shader: Option, + mesh: Option>, /// Textures used by the effect, if any. textures: Vec>, /// 2D layer for the effect instance. @@ -1152,6 +1166,7 @@ impl Default for CompiledParticleEffect { asset: default(), simulation_condition: SimulationCondition::default(), effect_shader: None, + mesh: None, textures: vec![], #[cfg(feature = "2d")] z_layer_2d: FloatOrd(0.0), @@ -1269,6 +1284,8 @@ impl CompiledParticleEffect { render: render_shaders, }); + self.mesh = asset.mesh.clone(); + self.textures = material.map(|mat| &mat.images).cloned().unwrap_or_default(); } @@ -1329,6 +1346,47 @@ impl ShaderCode for Gradient { } } +impl ShaderCode for Gradient { + fn to_shader_code(&self, input: &str) -> String { + if self.keys().is_empty() { + return String::new(); + } + let mut s: String = self + .keys() + .iter() + .enumerate() + .map(|(index, key)| { + format!( + "let t{0} = {1};\nlet v{0} = {2};", + index, + key.ratio().to_wgsl_string(), + key.value.to_wgsl_string() + ) + }) + .fold("// Gradient\n".into(), |s, key| s + &key + "\n"); + if self.keys().len() == 1 { + s + "return v0;\n" + } else { + s += &format!("if ({input} <= t0) {{ return v0; }}\n"); + let mut s = self + .keys() + .iter() + .skip(1) + .enumerate() + .map(|(index, _key)| { + format!( + "else if ({input} <= t{1}) {{ return mix(v{0}, v{1}, ({input} - t{0}) / (t{1} - t{0})); }}\n", + index, + index + 1 + ) + }) + .fold(s, |s, key| s + &key); + let _ = writeln!(s, "else {{ return v{}; }}", self.keys().len() - 1); + s + } + } +} + impl ShaderCode for Gradient { fn to_shader_code(&self, input: &str) -> String { if self.keys().is_empty() { @@ -1833,6 +1891,7 @@ else { return c1; } let mut shader_defs = std::collections::HashMap::::new(); shader_defs.insert("LOCAL_SPACE_SIMULATION".into(), ShaderDefValue::Bool(true)); shader_defs.insert("NEEDS_UV".into(), ShaderDefValue::Bool(true)); + shader_defs.insert("NEEDS_NORMAL".into(), ShaderDefValue::Bool(false)); shader_defs.insert("RENDER_NEEDS_SPAWNER".into(), ShaderDefValue::Bool(true)); shader_defs.insert( "PARTICLE_SCREEN_SPACE_SIZE".into(), diff --git a/src/modifier/mod.rs b/src/modifier/mod.rs index b9d5ea52..97c50d5d 100644 --- a/src/modifier/mod.rs +++ b/src/modifier/mod.rs @@ -27,7 +27,7 @@ use std::{ use bevy::{ asset::Handle, - math::{UVec2, Vec2, Vec4}, + math::{UVec2, Vec3, Vec4}, reflect::Reflect, render::texture::Image, utils::HashMap, @@ -383,9 +383,11 @@ pub struct RenderContext<'a> { /// Color gradients. pub gradients: HashMap>, /// Size gradients. - pub size_gradients: HashMap>, + pub size_gradients: HashMap>, /// The particle needs UV coordinates to sample one or more texture(s). pub needs_uv: bool, + /// The particle needs normals for lighting effects. + pub needs_normal: bool, /// Counter for unique variable names. var_counter: u32, /// Cache of evaluated expressions. @@ -413,6 +415,7 @@ impl<'a> RenderContext<'a> { gradients: HashMap::new(), size_gradients: HashMap::new(), needs_uv: false, + needs_normal: false, var_counter: 0, expr_cache: Default::default(), is_attribute_pointer: false, @@ -420,10 +423,15 @@ impl<'a> RenderContext<'a> { } /// Mark the rendering shader as needing UVs. - fn set_needs_uv(&mut self) { + pub fn set_needs_uv(&mut self) { self.needs_uv = true; } + /// Mark the rendering shader as needing normals. + pub fn set_needs_normal(&mut self) { + self.needs_normal = true; + } + /// Add a color gradient. /// /// # Returns @@ -443,7 +451,7 @@ impl<'a> RenderContext<'a> { /// /// Returns the unique name of the gradient, to be used as function name in /// the shader code. - fn add_size_gradient(&mut self, gradient: Gradient) -> String { + fn add_size_gradient(&mut self, gradient: Gradient) -> String { let func_id = calc_func_id(&gradient); self.size_gradients.insert(func_id, gradient); let func_name = format!("size_gradient_{0:016X}", func_id); @@ -988,7 +996,7 @@ fn main() {{ var particle = Particle(); var position = vec3(0.0, 0.0, 0.0); var velocity = vec3(0.0, 0.0, 0.0); - var size = vec2(1.0, 1.0); + var size = vec3(1.0, 1.0, 1.0); var axis_x = vec3(1.0, 0.0, 0.0); var axis_y = vec3(0.0, 1.0, 0.0); var axis_z = vec3(0.0, 0.0, 1.0); diff --git a/src/modifier/output.rs b/src/modifier/output.rs index badebc62..9c429f8c 100644 --- a/src/modifier/output.rs +++ b/src/modifier/output.rs @@ -291,7 +291,7 @@ impl RenderModifier for SetSizeModifier { #[derive(Debug, Default, Clone, PartialEq, Hash, Reflect, Serialize, Deserialize)] pub struct SizeOverLifetimeModifier { /// The size gradient defining the particle size based on its lifetime. - pub gradient: Gradient, + pub gradient: Gradient, /// Is the particle size in screen-space logical pixel? If `true`, the size /// is in screen-space logical pixels, and not affected by the camera /// projection. If `false`, the particle size is in world units. @@ -312,7 +312,7 @@ impl RenderModifier for SizeOverLifetimeModifier { ) -> Result<(), ExprError> { let func_name = context.add_size_gradient(self.gradient.clone()); context.render_extra += &format!( - r#"fn {0}(key: f32) -> vec2 {{ + r#"fn {0}(key: f32) -> vec3 {{ {1} }} @@ -680,9 +680,9 @@ impl RenderModifier for FlipbookModifier { /// This modifier requires the following particle attributes: /// - [`Attribute::POSITION`] /// -/// If the [`Attribute::SIZE`] or [`Attribute::SIZE2`] are present, they're used -/// to initialize the particle's size. Otherwise the default size is used. So -/// this modifier doesn't require any size attribute. +/// If the [`Attribute::SIZE`], [`Attribute::SIZE2`], or [`Attribute::SIZE3`] +/// are present, they're used to initialize the particle's size. Otherwise the +/// default size is used. So this modifier doesn't require any size attribute. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] pub struct ScreenSpaceSizeModifier; @@ -869,8 +869,8 @@ mod tests { #[test] fn mod_size_over_lifetime() { - let x = Vec2::new(1., 0.); - let y = Vec2::new(0., 1.); + let x = Vec3::new(1., 0., 1.); + let y = Vec3::new(0., 1., 1.); let mut gradient = Gradient::new(); gradient.add_key(0.5, x); gradient.add_key(0.8, y); diff --git a/src/modifier/ribbon.rs b/src/modifier/ribbon.rs index 12f52725..0e07cdc4 100644 --- a/src/modifier/ribbon.rs +++ b/src/modifier/ribbon.rs @@ -36,7 +36,7 @@ impl RenderModifier for RibbonModifier { axis_z = cross(axis_x, axis_y); position = mix(next_particle.position, particle.position, 0.5); - size = vec2(length(delta), size.y); + size = vec3(length(delta), size.y, 1.0); "##; Ok(()) diff --git a/src/plugin.rs b/src/plugin.rs index f87ccda3..b9cfc149 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,7 +1,7 @@ #[cfg(feature = "2d")] use bevy::core_pipeline::core_2d::Transparent2d; #[cfg(feature = "3d")] -use bevy::core_pipeline::core_3d::{AlphaMask3d, Transparent3d}; +use bevy::core_pipeline::core_3d::{AlphaMask3d, Opaque3d, Transparent3d}; use bevy::{ prelude::*, render::{ @@ -260,7 +260,11 @@ impl Plugin for HanabiPlugin { assets.insert(&HANABI_COMMON_TEMPLATE_HANDLE, common_shader); } - let effects_meta = EffectsMeta::new(render_device.clone()); + let effects_meta = { + let mut assets = app.world_mut().resource_mut::>(); + EffectsMeta::new(render_device.clone(), &mut assets) + }; + let effect_cache = EffectCache::new(render_device); // Register the custom render pipeline @@ -338,6 +342,14 @@ impl Plugin for HanabiPlugin { .unwrap() .write() .add(draw_particles); + + let draw_particles = DrawEffects::new(render_app.world_mut()); + render_app + .world() + .get_resource::>() + .unwrap() + .write() + .add(draw_particles); } // Add the simulation sub-graph. This render graph runs once per frame no matter diff --git a/src/render/batch.rs b/src/render/batch.rs index 78625803..4e6554ed 100644 --- a/src/render/batch.rs +++ b/src/render/batch.rs @@ -1,4 +1,7 @@ -use std::ops::{Index, Range}; +use std::{ + fmt::Debug, + ops::{Index, Range}, +}; #[cfg(feature = "2d")] use bevy::math::FloatOrd; @@ -39,6 +42,8 @@ pub(crate) struct EffectBatches { pub particle_layout: ParticleLayout, /// Flags describing the render layout. pub layout_flags: LayoutFlags, + /// The mesh to draw. + pub mesh: Handle, /// Texture layout. pub texture_layout: TextureLayout, /// Textures. @@ -126,6 +131,7 @@ impl EffectBatches { .collect(), handle: input.handle, layout_flags: input.layout_flags, + mesh: input.mesh.clone(), texture_layout: input.texture_layout, textures: input.textures, alpha_mode: input.alpha_mode, @@ -138,7 +144,7 @@ impl EffectBatches { } /// Effect batching input, obtained from extracted effects. -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct BatchesInput { /// Handle of the underlying effect asset describing the effect. pub handle: Handle, @@ -153,6 +159,7 @@ pub(crate) struct BatchesInput { pub effect_shader: EffectShader, /// Various flags related to the effect. pub layout_flags: LayoutFlags, + pub mesh: Handle, /// Texture layout. pub texture_layout: TextureLayout, /// Textures. diff --git a/src/render/mod.rs b/src/render/mod.rs index 6c6c2240..1918bff3 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -11,7 +11,7 @@ use bevy::math::FloatOrd; #[cfg(feature = "3d")] use bevy::{ core_pipeline::{ - core_3d::{AlphaMask3d, Transparent3d}, + core_3d::{AlphaMask3d, Opaque3d, Transparent3d}, prepass::OpaqueNoLightmap3dBinKey, }, render::render_phase::{BinnedPhaseItem, ViewBinnedRenderPhases}, @@ -24,6 +24,7 @@ use bevy::{ log::trace, prelude::*, render::{ + mesh::{GpuBufferInfo, GpuMesh, MeshVertexBufferLayoutRef}, render_asset::RenderAssets, render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo}, render_phase::{ @@ -304,12 +305,25 @@ pub struct GpuRenderEffectMetadata { pub ping: u32, } +/// Indirect draw parameters, with some data of our own tacked on to the end. +/// +/// A few fields of this differ depending on whether the mesh is indexed or +/// non-indexed. #[repr(C)] #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)] pub struct GpuRenderGroupIndirect { + /// The number of vertices in the mesh, if non-indexed; if indexed, the + /// number of indices in the mesh. pub vertex_count: u32, + /// The number of instances to render. pub instance_count: u32, - pub vertex_offset: i32, + /// The first index to render, if the mesh is indexed; the offset of the + /// first vertex, if the mesh is non-indexed. + pub first_index_or_vertex_offset: u32, + /// The offset of the first vertex, if the mesh is indexed; the first + /// instance to render, if the mesh is non-indexed. + pub vertex_offset_or_base_instance: i32, + /// The first instance to render, if indexed; unused if non-indexed. pub base_instance: u32, // pub alive_count: u32, @@ -1030,15 +1044,16 @@ pub(crate) struct ParticleRenderPipelineKey { shader: Handle, /// Particle layout. particle_layout: ParticleLayout, + mesh_layout: Option, /// Texture layout. texture_layout: TextureLayout, /// Key: LOCAL_SPACE_SIMULATION /// The effect is simulated in local space, and during rendering all /// particles are transformed by the effect's [`GlobalTransform`]. local_space_simulation: bool, - /// Key: USE_ALPHA_MASK - /// The effect is rendered with alpha masking. - use_alpha_mask: bool, + /// Key: USE_ALPHA_MASK, OPAQUE + /// The particle's alpha masking behavior. + alpha_mask: ParticleRenderAlphaMaskPipelineKey, /// The effect needs Alpha blend. alpha_mode: AlphaMode, /// Key: FLIPBOOK @@ -1048,6 +1063,9 @@ pub(crate) struct ParticleRenderPipelineKey { /// Key: NEEDS_UV /// The effect needs UVs. needs_uv: bool, + /// Key: NEEDS_NORMAL + /// The effect needs normals. + needs_normal: bool, /// For dual-mode configurations only, the actual mode of the current render /// pipeline. Otherwise the mode is implicitly determined by the active /// feature. @@ -1059,17 +1077,31 @@ pub(crate) struct ParticleRenderPipelineKey { hdr: bool, } +#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)] +pub(crate) enum ParticleRenderAlphaMaskPipelineKey { + #[default] + Blend, + /// Key: USE_ALPHA_MASK + /// The effect is rendered with alpha masking. + AlphaMask, + /// Key: OPAQUE + /// The effect is rendered fully-opaquely. + Opaque, +} + impl Default for ParticleRenderPipelineKey { fn default() -> Self { Self { shader: Handle::default(), particle_layout: ParticleLayout::empty(), + mesh_layout: None, texture_layout: default(), local_space_simulation: false, - use_alpha_mask: false, + alpha_mask: default(), alpha_mode: AlphaMode::Blend, flipbook: false, needs_uv: false, + needs_normal: false, #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode::Camera3d, msaa_samples: Msaa::default().samples(), @@ -1084,44 +1116,6 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { trace!("Specializing render pipeline for key: {:?}", key); - // Base mandatory part of vertex buffer layout - let vertex_buffer_layout = VertexBufferLayout { - array_stride: 20, - step_mode: VertexStepMode::Vertex, - attributes: vec![ - // @location(0) vertex_position: vec3 - VertexAttribute { - format: VertexFormat::Float32x3, - offset: 0, - shader_location: 0, - }, - // @location(1) vertex_uv: vec2 - VertexAttribute { - format: VertexFormat::Float32x2, - offset: 12, - shader_location: 1, - }, - // @location(1) vertex_color: u32 - // VertexAttribute { - // format: VertexFormat::Uint32, - // offset: 12, - // shader_location: 1, - // }, - // @location(2) vertex_velocity: vec3 - // VertexAttribute { - // format: VertexFormat::Float32x3, - // offset: 12, - // shader_location: 1, - // }, - // @location(3) vertex_uv: vec2 - // VertexAttribute { - // format: VertexFormat::Float32x2, - // offset: 28, - // shader_location: 3, - // }, - ], - }; - let dispatch_indirect_size = GpuDispatchIndirect::aligned_size( self.render_device .limits() @@ -1187,6 +1181,17 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { let mut layout = vec![self.view_layout.clone(), particles_buffer_layout]; let mut shader_defs = vec!["SPAWNER_READONLY".into()]; + let vertex_buffer_layout = key.mesh_layout.and_then(|mesh_layout| { + mesh_layout + .0 + .get_layout(&[ + Mesh::ATTRIBUTE_POSITION.at_shader_location(0), + Mesh::ATTRIBUTE_UV_0.at_shader_location(1), + Mesh::ATTRIBUTE_NORMAL.at_shader_location(2), + ]) + .ok() + }); + if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) { layout.push(material_bind_group_layout.clone()); // // @location(1) vertex_uv: vec2 @@ -1204,9 +1209,16 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { shader_defs.push("RENDER_NEEDS_SPAWNER".into()); } - // Key: USE_ALPHA_MASK - if key.use_alpha_mask { - shader_defs.push("USE_ALPHA_MASK".into()); + match key.alpha_mask { + ParticleRenderAlphaMaskPipelineKey::Blend => {} + ParticleRenderAlphaMaskPipelineKey::AlphaMask => { + // Key: USE_ALPHA_MASK + shader_defs.push("USE_ALPHA_MASK".into()) + } + ParticleRenderAlphaMaskPipelineKey::Opaque => { + // Key: OPAQUE + shader_defs.push("OPAQUE".into()) + } } // Key: FLIPBOOK @@ -1218,14 +1230,23 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { shader_defs.push("NEEDS_UV".into()); } + if key.needs_normal { + shader_defs.push("NEEDS_NORMAL".into()); + } + #[cfg(all(feature = "2d", feature = "3d"))] let depth_stencil = match key.pipeline_mode { // Bevy's Transparent2d render phase doesn't support a depth-stencil buffer. PipelineMode::Camera2d => None, PipelineMode::Camera3d => Some(DepthStencilState { format: TextureFormat::Depth32Float, - // Use depth buffer with alpha-masked particles, not with transparent ones - depth_write_enabled: key.use_alpha_mask, + // Use depth buffer with alpha-masked or opaque particles, not + // with transparent ones + depth_write_enabled: matches!( + key.alpha_mask, + ParticleRenderAlphaMaskPipelineKey::AlphaMask + | ParticleRenderAlphaMaskPipelineKey::Opaque + ), // Bevy uses reverse-Z, so Greater really means closer depth_compare: CompareFunction::Greater, stencil: StencilState::default(), @@ -1240,7 +1261,11 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { let depth_stencil = Some(DepthStencilState { format: TextureFormat::Depth32Float, // Use depth buffer with alpha-masked particles, not with transparent ones - depth_write_enabled: key.use_alpha_mask, + depth_write_enabled: matches!( + key.alpha_mask, + ParticleRenderAlphaMaskPipelineKey::AlphaMask + | ParticleRenderAlphaMaskPipelineKey::Opaque + ), // Bevy uses reverse-Z, so Greater really means closer depth_compare: CompareFunction::Greater, stencil: StencilState::default(), @@ -1284,7 +1309,7 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { shader: key.shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), - buffers: vec![vertex_buffer_layout], + buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")], }, fragment: Some(FragmentState { shader: key.shader, @@ -1356,6 +1381,7 @@ pub(crate) struct ExtractedEffect { pub inverse_transform: Mat4, /// Layout flags. pub layout_flags: LayoutFlags, + pub mesh: Handle, /// Texture layout. pub texture_layout: TextureLayout, /// Textures. @@ -1390,6 +1416,21 @@ pub struct AddedEffect { pub layout_flags: LayoutFlags, /// Handle of the effect asset. pub handle: Handle, + pub gpu_mesh_info: AddedEffectGpuMeshInfo, +} + +/// Mesh information needed to build newly-added effects. +pub enum AddedEffectGpuMeshInfo { + /// The mesh has vertex indices. + Indexed { + /// The number of indices that make up the mesh. + index_count: u32, + }, + /// The mesh doesn't have vertex indices. + NonIndexed { + /// The number of vertices in the mesh. + vertex_count: u32, + }, } /// Collection of all extracted effects for this frame, inserted into the @@ -1445,6 +1486,7 @@ pub(crate) fn extract_effects( time: Extract>>, effects: Extract>>, _images: Extract>>, + meshes: Extract>>, mut query: Extract< ParamSet<( // All existing ParticleEffect components @@ -1467,6 +1509,7 @@ pub(crate) fn extract_effects( mut removed_effects_event_reader: Extract>, mut sim_params: ResMut, mut extracted_effects: ResMut, + effects_meta: Res, ) { trace!("extract_effects"); @@ -1501,6 +1544,10 @@ pub(crate) fn extract_effects( let handle = effect.asset.clone_weak(); let asset = effects.get(&effect.asset)?; let particle_layout = asset.particle_layout(); + let mesh = meshes.get(match effect.mesh { + Some(ref mesh) => mesh.id(), + None => effects_meta.default_mesh.id() + })?; assert!( particle_layout.size() > 0, "Invalid empty particle layout for effect '{}' on entity {:?}. Did you forget to add some modifier to the asset?", @@ -1517,6 +1564,10 @@ pub(crate) fn extract_effects( property_layout, layout_flags: effect.layout_flags, handle, + gpu_mesh_info: match mesh.indices() { + Some(indices) => AddedEffectGpuMeshInfo::Indexed { index_count: indices.len() as u32 }, + None => AddedEffectGpuMeshInfo::NonIndexed { vertex_count: mesh.count_vertices() as u32 }, + }, }) }) .collect(); @@ -1579,6 +1630,10 @@ pub(crate) fn extract_effects( }; let layout_flags = effect.layout_flags; + let mesh = match effect.mesh { + None => effects_meta.default_mesh.clone(), + Some(ref mesh) => (*mesh).clone(), + }; let alpha_mode = effect.alpha_mode; trace!( @@ -1602,6 +1657,7 @@ pub(crate) fn extract_effects( // TODO - more efficient/correct way than inverse()? inverse_transform: transform.compute_matrix().inverse(), layout_flags, + mesh, texture_layout, textures: effect.textures.clone(), alpha_mode, @@ -1613,17 +1669,6 @@ pub(crate) fn extract_effects( } } -/// GPU representation of a single vertex of a particle mesh stored in a GPU -/// buffer. -#[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable, ShaderType)] -struct GpuParticleVertex { - /// Vertex position. - pub position: [f32; 3], - /// UV coordinates of vertex. - pub uv: [f32; 2], -} - /// Various GPU limits and aligned sizes computed once and cached. struct GpuLimits { /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`]. @@ -1772,27 +1817,16 @@ pub struct EffectsMeta { /// each particle group that's populated by the CPU and read (only read) by /// the GPU. particle_group_buffer: AlignedBufferVec, - /// Unscaled vertices of the mesh of a single particle, generally a quad. - /// The mesh is later scaled during rendering by the "particle size". - // FIXME - This is a per-effect thing, unless we merge all meshes into a single buffer (makes - // sense) but in that case we need a vertex slice too to know which mesh to draw per effect. - vertices: BufferVec, + /// The mesh used when particle effects don't specify one (i.e. a quad). + default_mesh: Handle, /// Various GPU limits and aligned sizes lazily allocated and cached for /// convenience. gpu_limits: GpuLimits, } impl EffectsMeta { - pub fn new(device: RenderDevice) -> Self { - let mut vertices = BufferVec::new(BufferUsages::VERTEX); - for v in QUAD_VERTEX_POSITIONS { - let uv = v.truncate() + 0.5; - let v = *v * Vec3::new(1.0, 1.0, 1.0); - vertices.push(GpuParticleVertex { - position: v.into(), - uv: uv.into(), - }); - } + pub fn new(device: RenderDevice, mesh_assets: &mut Assets) -> Self { + let default_mesh = mesh_assets.add(Plane3d::new(Vec3::Z, Vec2::splat(0.5))); let gpu_limits = GpuLimits::from_device(&device); @@ -1841,7 +1875,7 @@ impl EffectsMeta { NonZeroU64::new(item_align), Some("hanabi:buffer:particle_group".to_string()), ), - vertices, + default_mesh, gpu_limits, } } @@ -1937,12 +1971,22 @@ impl EffectsMeta { &mut self.render_group_dispatch_buffer, added_effect.capacities.iter().map(|&capacity| { let indirect_dispatch = GpuRenderGroupIndirect { - vertex_count: 6, // TODO - Flexible vertex count and mesh particles + vertex_count: match added_effect.gpu_mesh_info { + AddedEffectGpuMeshInfo::Indexed { index_count, .. } => index_count, + AddedEffectGpuMeshInfo::NonIndexed { vertex_count } => vertex_count, + }, + first_index_or_vertex_offset: 0, + vertex_offset_or_base_instance: match added_effect.gpu_mesh_info { + AddedEffectGpuMeshInfo::Indexed { .. } => 0, + AddedEffectGpuMeshInfo::NonIndexed { .. } => current_base_instance, + }, dead_count: capacity, - base_instance: current_base_instance, - ..default() + base_instance: current_base_instance as u32, + instance_count: 0, + alive_count: 0, + max_update: 0, }; - current_base_instance += capacity; + current_base_instance += capacity as i32; indirect_dispatch }), ); @@ -2016,15 +2060,6 @@ impl EffectsMeta { } } -const QUAD_VERTEX_POSITIONS: &[Vec3] = &[ - Vec3::from_array([-0.5, -0.5, 0.0]), - Vec3::from_array([0.5, 0.5, 0.0]), - Vec3::from_array([-0.5, 0.5, 0.0]), - Vec3::from_array([-0.5, -0.5, 0.0]), - Vec3::from_array([0.5, -0.5, 0.0]), - Vec3::from_array([0.5, 0.5, 0.0]), -]; - bitflags! { /// Effect flags. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -2041,6 +2076,10 @@ bitflags! { const FLIPBOOK = (1 << 4); /// The effect needs UVs. const NEEDS_UV = (1 << 5); + /// The effects needs normals. + const NEEDS_NORMAL = (1 << 6); + /// The effect is fully-opaque. + const OPAQUE = (1 << 7); } } @@ -2074,11 +2113,6 @@ pub(crate) fn prepare_effects( // effects_meta.spawner_buffer.push(GpuSpawnerParams::default()); //} - // Write vertices (TODO - lazily once only) - effects_meta - .vertices - .write_buffer(&render_device, &render_queue); - // Clear last frame's buffer resizes which may have occured during last frame, // during `Node::run()` while the `BufferTable` could not be mutated. effects_meta @@ -2131,6 +2165,7 @@ pub(crate) fn prepare_effects( property_layout: extracted_effect.property_layout.clone(), effect_shader: extracted_effect.effect_shader.clone(), layout_flags: extracted_effect.layout_flags, + mesh: extracted_effect.mesh, texture_layout: extracted_effect.texture_layout.clone(), textures: extracted_effect.textures.clone(), alpha_mode: extracted_effect.alpha_mode, @@ -2453,6 +2488,8 @@ pub struct QueueEffectsReadOnlyParams<'w, 's> { draw_functions_3d: Res<'w, DrawFunctions>, #[cfg(feature = "3d")] draw_functions_alpha_mask: Res<'w, DrawFunctions>, + #[cfg(feature = "3d")] + draw_functions_opaque: Res<'w, DrawFunctions>, #[system_param(ignore)] marker: PhantomData<&'s usize>, } @@ -2465,6 +2502,7 @@ fn emit_sorted_draw( effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>, render_pipeline: &mut ParticlesRenderPipeline, mut specialized_render_pipelines: Mut>, + render_meshes: &RenderAssets, pipeline_cache: &PipelineCache, msaa_samples: u32, make_phase_item: F, @@ -2523,7 +2561,10 @@ fn emit_sorted_draw( ); // AlphaMask is a binned draw, so no sorted draw can possibly use it - if batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) { + if batches + .layout_flags + .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE) + { continue; } @@ -2555,17 +2596,20 @@ fn emit_sorted_draw( let local_space_simulation = batches .layout_flags .contains(LayoutFlags::LOCAL_SPACE_SIMULATION); - let use_alpha_mask = batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK); + let alpha_mask = + ParticleRenderAlphaMaskPipelineKey::from_layout_flags(batches.layout_flags); let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK); let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV); + let needs_normal = batches.layout_flags.contains(LayoutFlags::NEEDS_NORMAL); let image_count = batches.texture_layout.layout.len() as u8; + let gpu_mesh = render_meshes.get(&batches.mesh); // Specialize the render pipeline based on the effect batch trace!( - "Specializing render pipeline: render_shaders={:?} image_count={} use_alpha_mask={:?} flipbook={:?} hdr={}", + "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}", batches.render_shaders, image_count, - use_alpha_mask, + alpha_mask, flipbook, view.hdr ); @@ -2577,6 +2621,10 @@ fn emit_sorted_draw( let alpha_mode = batches.alpha_mode; + let Some(mesh_layout) = gpu_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else { + continue; + }; + #[cfg(feature = "trace")] let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered(); let render_pipeline_id = specialized_render_pipelines.specialize( @@ -2584,13 +2632,15 @@ fn emit_sorted_draw( render_pipeline, ParticleRenderPipelineKey { shader: render_shader_source.clone(), + mesh_layout: Some(mesh_layout), particle_layout: batches.particle_layout.clone(), texture_layout: batches.texture_layout.clone(), local_space_simulation, - use_alpha_mask, + alpha_mask, alpha_mode, flipbook, needs_uv, + needs_normal, #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode, msaa_samples, @@ -2635,10 +2685,11 @@ fn emit_binned_draw( render_pipeline: &mut ParticlesRenderPipeline, mut specialized_render_pipelines: Mut>, pipeline_cache: &PipelineCache, + render_meshes: &RenderAssets, msaa_samples: u32, make_bin_key: F, #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode, - use_alpha_mask: bool, + alpha_mask: ParticleRenderAlphaMaskPipelineKey, ) where T: BinnedPhaseItem, F: Fn(CachedRenderPipelineId, &EffectDrawBatch, u32, &ExtractedView) -> T::BinKey, @@ -2648,10 +2699,7 @@ fn emit_binned_draw( trace!("emit_binned_draw() {} views", views.iter().len()); for (view_entity, visible_entities, view) in views.iter() { - trace!( - "Process new binned view (use_alpha_mask={})", - use_alpha_mask - ); + trace!("Process new binned view (alpha_mask={:?})", alpha_mask); let Some(render_phase) = render_phases.get_mut(&view_entity) else { continue; @@ -2697,7 +2745,9 @@ fn emit_binned_draw( batches.layout_flags, ); - if use_alpha_mask != batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) { + if ParticleRenderAlphaMaskPipelineKey::from_layout_flags(batches.layout_flags) + != alpha_mask + { continue; } @@ -2729,17 +2779,20 @@ fn emit_binned_draw( let local_space_simulation = batches .layout_flags .contains(LayoutFlags::LOCAL_SPACE_SIMULATION); - let use_alpha_mask = batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK); + let alpha_mask = + ParticleRenderAlphaMaskPipelineKey::from_layout_flags(batches.layout_flags); let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK); let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV); + let needs_normal = batches.layout_flags.contains(LayoutFlags::NEEDS_NORMAL); let image_count = batches.texture_layout.layout.len() as u8; + let gpu_mesh = render_meshes.get(&batches.mesh); // Specialize the render pipeline based on the effect batch trace!( - "Specializing render pipeline: render_shaders={:?} image_count={} use_alpha_mask={:?} flipbook={:?} hdr={}", + "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}", batches.render_shaders, image_count, - use_alpha_mask, + alpha_mask, flipbook, view.hdr ); @@ -2751,6 +2804,10 @@ fn emit_binned_draw( let alpha_mode = batches.alpha_mode; + let Some(mesh_layout) = gpu_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else { + continue; + }; + #[cfg(feature = "trace")] let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered(); let render_pipeline_id = specialized_render_pipelines.specialize( @@ -2758,13 +2815,15 @@ fn emit_binned_draw( render_pipeline, ParticleRenderPipelineKey { shader: render_shader_source.clone(), + mesh_layout: Some(mesh_layout), particle_layout: batches.particle_layout.clone(), texture_layout: batches.texture_layout.clone(), local_space_simulation, - use_alpha_mask, + alpha_mask, alpha_mode, flipbook, needs_uv, + needs_normal, #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode, msaa_samples, @@ -2808,6 +2867,7 @@ pub(crate) fn queue_effects( effect_batches: Query<(Entity, &mut EffectBatches)>, effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>, events: Res, + render_meshes: Res>, read_params: QueueEffectsReadOnlyParams, msaa: Res, mut view_entities: Local, @@ -2871,6 +2931,7 @@ pub(crate) fn queue_effects( &effect_draw_batches, &mut render_pipeline, specialized_render_pipelines.reborrow(), + &render_meshes, &pipeline_cache, msaa.samples(), |id, entity, draw_batch, _group, _view| Transparent2d { @@ -2911,6 +2972,7 @@ pub(crate) fn queue_effects( &effect_draw_batches, &mut render_pipeline, specialized_render_pipelines.reborrow(), + &render_meshes, &pipeline_cache, msaa.samples(), |id, entity, batch, _group, view| Transparent3d { @@ -2950,6 +3012,7 @@ pub(crate) fn queue_effects( &mut render_pipeline, specialized_render_pipelines.reborrow(), &pipeline_cache, + &render_meshes, msaa.samples(), |id, _batch, _group, _view| OpaqueNoLightmap3dBinKey { pipeline: id, @@ -2965,7 +3028,49 @@ pub(crate) fn queue_effects( }, #[cfg(feature = "2d")] PipelineMode::Camera3d, - true, + ParticleRenderAlphaMaskPipelineKey::AlphaMask, + ); + } + + // Opaque particles + if !views.is_empty() { + #[cfg(feature = "trace")] + let _span_draw = bevy::utils::tracing::info_span!("draw_opaque").entered(); + + trace!("Emit effect draw calls for opaque 3D views..."); + + let draw_effects_function_opaque = read_params + .draw_functions_opaque + .read() + .get_id::() + .unwrap(); + + emit_binned_draw( + &views, + &mut alpha_mask_3d_render_phases, + &mut view_entities, + &effect_batches, + &effect_draw_batches, + &mut render_pipeline, + specialized_render_pipelines.reborrow(), + &pipeline_cache, + &render_meshes, + msaa.samples(), + |id, _batch, _group, _view| OpaqueNoLightmap3dBinKey { + pipeline: id, + draw_function: draw_effects_function_opaque, + asset_id: AssetId::::default().untyped(), + material_bind_group_id: None, + // }, + // distance: view + // .rangefinder3d() + // .distance_translation(&batch.translation_3d), + // batch_range: 0..1, + // extra_index: PhaseItemExtraIndex::NONE, + }, + #[cfg(feature = "2d")] + PipelineMode::Camera3d, + ParticleRenderAlphaMaskPipelineKey::Opaque, ); } } @@ -3355,6 +3460,7 @@ type DrawEffectsSystemState = SystemState<( SRes, SRes, SRes, + SRes>, SQuery>, SQuery>, SQuery>, @@ -3387,11 +3493,19 @@ fn draw<'w>( pipeline_id: CachedRenderPipelineId, params: &mut DrawEffectsSystemState, ) { - let (effects_meta, effect_bind_groups, pipeline_cache, views, effects, effect_draw_batches) = - params.get(world); + let ( + effects_meta, + effect_bind_groups, + pipeline_cache, + meshes, + views, + effects, + effect_draw_batches, + ) = params.get(world); let view_uniform = views.get(view).unwrap(); let effects_meta = effects_meta.into_inner(); let effect_bind_groups = effect_bind_groups.into_inner(); + let meshes = meshes.into_inner(); let effect_draw_batch = effect_draw_batches.get(entity).unwrap(); let effect_batches = effects.get(effect_draw_batch.batches_entity).unwrap(); @@ -3405,8 +3519,23 @@ fn draw<'w>( pass.set_render_pipeline(pipeline); + let Some(gpu_mesh): Option<&GpuMesh> = meshes.get(&effect_batches.mesh) else { + return; + }; + // Vertex buffer containing the particle model to draw. Generally a quad. - pass.set_vertex_buffer(0, effects_meta.vertices.buffer().unwrap().slice(..)); + pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..)); + + match gpu_mesh.buffer_info { + GpuBufferInfo::Indexed { + ref buffer, + count: _, + index_format, + } => { + pass.set_index_buffer(buffer.slice(..), 0, index_format); + } + GpuBufferInfo::NonIndexed => {} + } // View properties (camera matrix, etc.) pass.set_bind_group( @@ -3475,17 +3604,34 @@ fn draw<'w>( "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \ (render_group_dispatch_indirect_index={:?}, group_index={}).", effect_batch.slice.len(), - effects_meta.vertices.len(), + gpu_mesh.vertex_count, effect_batches.buffer_index, render_group_dispatch_indirect_index, group_index, ); - pass.draw_indirect( - render_indirect_buffer, - render_group_dispatch_indirect_index as u64 - * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64, - ); + match gpu_mesh.buffer_info { + GpuBufferInfo::Indexed { + ref buffer, + count: _, + index_format, + } => { + pass.set_index_buffer(buffer.slice(..), 0, index_format); + + pass.draw_indexed_indirect( + render_indirect_buffer, + render_group_dispatch_indirect_index as u64 + * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64, + ); + } + GpuBufferInfo::NonIndexed => { + pass.draw_indirect( + render_indirect_buffer, + render_group_dispatch_indirect_index as u64 + * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64, + ); + } + } } #[cfg(feature = "2d")] @@ -3551,6 +3697,27 @@ impl Draw for DrawEffects { } } +#[cfg(feature = "3d")] +impl Draw for DrawEffects { + fn draw<'w>( + &mut self, + world: &'w World, + pass: &mut TrackedRenderPass<'w>, + view: Entity, + item: &Opaque3d, + ) { + trace!("Draw: view={:?}", view); + draw( + world, + pass, + view, + item.representative_entity, + item.key.pipeline, + &mut self.params, + ); + } +} + /// Render node to run the simulation sub-graph once per frame. /// /// This node doesn't simulate anything by itself, but instead schedules the @@ -3990,6 +4157,18 @@ where first_buffer.expect("No buffers allocated") } +impl ParticleRenderAlphaMaskPipelineKey { + fn from_layout_flags(layout_flags: LayoutFlags) -> Self { + if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) { + ParticleRenderAlphaMaskPipelineKey::AlphaMask + } else if layout_flags.contains(LayoutFlags::OPAQUE) { + ParticleRenderAlphaMaskPipelineKey::Opaque + } else { + ParticleRenderAlphaMaskPipelineKey::Blend + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/render/vfx_common.wgsl b/src/render/vfx_common.wgsl index b1f32657..754aa5c4 100644 --- a/src/render/vfx_common.wgsl +++ b/src/render/vfx_common.wgsl @@ -88,11 +88,12 @@ const REM_OFFSET_PING: u32 = 1u; const RGI_OFFSET_VERTEX_COUNT: u32 = 0u; const RGI_OFFSET_INSTANCE_COUNT: u32 = 1u; -const RGI_OFFSET_VERTEX_OFFSET: u32 = 2u; -const RGI_OFFSET_BASE_INSTANCE: u32 = 3u; -const RGI_OFFSET_ALIVE_COUNT: u32 = 4u; -const RGI_OFFSET_MAX_UPDATE: u32 = 5u; -const RGI_OFFSET_DEAD_COUNT: u32 = 6u; +const RGI_OFFSET_FIRST_INDEX_OR_VERTEX_OFFSET: u32 = 2u; +const RGI_OFFSET_VERTEX_OFFSET_OR_BASE_INSTANCE: u32 = 3u; +const RGI_OFFSET_BASE_INSTANCE: u32 = 4u; +const RGI_OFFSET_ALIVE_COUNT: u32 = 5u; +const RGI_OFFSET_MAX_UPDATE: u32 = 6u; +const RGI_OFFSET_DEAD_COUNT: u32 = 7u; struct RenderEffectMetadata { /// Maxmimum number of init threads to run on next frame. This is cached from @@ -117,9 +118,11 @@ struct RenderGroupIndirect { vertex_count: u32, /// Number of mesh instances, equal to the number of particles. instance_count: atomic, - /// Vertex offset (always zero). - vertex_offset: i32, - /// Base instance. + /// First index (if indexed) or vertex offset (if non-indexed). + first_index_or_vertex_offset: u32, + /// Vertex offset (if indexed) or base instance (if non-indexed). + vertex_offset_or_base_instance: i32, + /// Base instance (if indexed). base_instance: u32, /// Number of particles alive after the init pass, used to calculate the number /// of compute threads to spawn for the update pass and to cap those threads diff --git a/src/render/vfx_render.wgsl b/src/render/vfx_render.wgsl index ef1dd778..b9e9a76b 100644 --- a/src/render/vfx_render.wgsl +++ b/src/render/vfx_render.wgsl @@ -20,6 +20,9 @@ struct VertexOutput { #ifdef NEEDS_UV @location(1) uv: vec2, #endif +#ifdef NEEDS_NORMAL + @location(2) normal: vec3, +#endif } @group(0) @binding(0) var view: View; @@ -76,6 +79,15 @@ fn unpack_compressed_transform(compressed_transform: mat3x4) -> mat4x4 ); } +// Unpacks a compressed transform and transposes is. +fn unpack_compressed_transform_3x3_transpose(compressed_transform: mat3x4) -> mat3x3 { + return mat3x3( + compressed_transform[0].xyz, + compressed_transform[1].xyz, + compressed_transform[2].xyz, + ); +} + /// Transform a simulation space position into a world space position. /// /// The simulation space depends on the effect's SimulationSpace value, and is either @@ -89,6 +101,18 @@ fn transform_position_simulation_to_world(sim_position: vec3) -> vec4 #endif } +fn transform_normal_simulation_to_world(sim_normal: vec3) -> vec3 { +#ifdef LOCAL_SPACE_SIMULATION + // We use the inverse transpose transform to transform normals. + // The inverse transpose is the same as the transposed inverse, so we can + // safely use the inverse transform. + let transform = unpack_compressed_transform_3x3_transpose(spawner.inverse_transform); + return transform * sim_normal; +#else + return sim_normal; +#endif +} + /// Transform a simulation space position into a clip space position. /// /// The simulation space depends on the effect's SimulationSpace value, and is either @@ -107,6 +131,9 @@ fn vertex( @location(0) vertex_position: vec3, #ifdef NEEDS_UV @location(1) vertex_uv: vec2, +#endif +#ifdef NEEDS_NORMAL + @location(2) vertex_normal: vec3, #endif // @location(1) vertex_color: u32, // @location(1) vertex_velocity: vec3, @@ -131,12 +158,17 @@ fn vertex( // Expand particle mesh vertex based on particle position ("origin"), and local // orientation and size of the particle mesh (currently: only quad). - let vpos = vertex_position * vec3(size.x, size.y, 1.0); - let sim_position = position + axis_x * vpos.x + axis_y * vpos.y; + let vpos = vertex_position * size; + let sim_position = position + axis_x * vpos.x + axis_y * vpos.y + axis_z * vpos.z; out.position = transform_position_simulation_to_clip(sim_position); out.color = color; +#ifdef NEEDS_NORMAL + let normal = mat3x3(axis_x, axis_y, axis_z) * vertex_normal; + out.normal = transform_normal_simulation_to_world(normal); +#endif // NEEDS_NORMAL + return out; } @@ -150,6 +182,9 @@ fn fragment(in: VertexOutput) -> @location(0) vec4 { #ifdef NEEDS_UV var uv = in.uv; #endif +#ifdef NEEDS_NORMAL + var normal = in.normal; +#endif {{FRAGMENT_MODIFIERS}}