From 41f3e5c77653b98cfe99c4edf4253735a31b07c7 Mon Sep 17 00:00:00 2001 From: Christian Helgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Wed, 28 Feb 2024 03:55:45 -0800 Subject: [PATCH] Skinned Mesh Example (#349) * eod * late lunch * Can't figure out what's wrong with vertPositions or indices * committing to fix an issue in another branch * Have some kind of animation working, but not correctly * Safety commit * Elongated trunk' * Scene stuff working, even though it took too long * eod. should have finished today * almuerzo * Whale seems to be transformed by node values, but now it is in a weird position that I don't think I initially intended * Remember to try to add static bgLayout to GLTFSKIN, so layout can be accessed from both main and glbUtils.ts * Some kind of animation is happening, though not the kind I like. I think this needs to be vastly oversimplified to be less generalizable and more focused on the specific mesh and skin we want to target. Which means getting rid of a lot of class shennanigins outside the Mesh -> Primitive -> Accessor pipeline probably * Changed get matrix to apply rotationMatrix to transformationMatrix rather than overriting the transformationMatrix with the value of the rotationMatrix : ( * Fixed it again :( * Simplified primitive generation and rendering * Fixed bug where texcoords were not being loaded in, even though they are not used, which caused interpretation errors with the joints and weights :facepalm: * Remove gltf.ts comments * Fix to the way our buffer is interpreted * Fixed various errors with typedArray/ArrayBuffer interpretation in conversion of mat4x4 joint data from a uint8array to a mat4 reducible float32array * Generally seems to be working but something is off with the scaling in the animSkin function, causing the whale to be very droopy and elongated * This is pretty dumb but I think it's working now * Make initial angle less annoying * Initial description * Proper attribution for parts of glbUtils code * Just removed console.logs * Modified grid and gtlf shaders to take in inverseBindMatrices through uniform buffers rather than calculating them against jointMatrices on the CPU side. Also added additional comments and renamed vertex attributes in grid.wgsl to conform more closely to their counterparts in gltf.wgsl * Removed unneeded jointMatrices code' * Removed camera controls * clamped angle to lower value to prevent animations that look broken/too silly * converted uniform matrix buffers to storage buffers * eod * interpret vertex formats from attributes to make shaders for other glbs easier to write * fix prettier problem * Changed animation * Comments and deletions --- public/assets/gltf/whale.glb | Bin 0 -> 143908 bytes src/pages/samples/[slug].tsx | 1 + src/sample/skinnedMesh/glbUtils.ts | 1017 +++++++++++++++++++++++++++ src/sample/skinnedMesh/gltf.ts | 224 ++++++ src/sample/skinnedMesh/gltf.wgsl | 80 +++ src/sample/skinnedMesh/grid.wgsl | 74 ++ src/sample/skinnedMesh/gridData.ts | 106 +++ src/sample/skinnedMesh/gridUtils.ts | 114 +++ src/sample/skinnedMesh/main.ts | 605 ++++++++++++++++ 9 files changed, 2221 insertions(+) create mode 100644 public/assets/gltf/whale.glb create mode 100644 src/sample/skinnedMesh/glbUtils.ts create mode 100644 src/sample/skinnedMesh/gltf.ts create mode 100644 src/sample/skinnedMesh/gltf.wgsl create mode 100644 src/sample/skinnedMesh/grid.wgsl create mode 100644 src/sample/skinnedMesh/gridData.ts create mode 100644 src/sample/skinnedMesh/gridUtils.ts create mode 100644 src/sample/skinnedMesh/main.ts diff --git a/public/assets/gltf/whale.glb b/public/assets/gltf/whale.glb new file mode 100644 index 0000000000000000000000000000000000000000..4d361020f4d8a15a073daede8edba42a9a6be6bb GIT binary patch literal 143908 zcmeF3XIxat)~^v%5K+K@x6#wA)>~(yFRe|FxdeEk3e=xtW<+C6&3EVQ?L%d`9^@~gPt~12DTU?*Gq?qJ{B=0)jO}Zx~^hxOF*^L+R42h5H6C0Q0*|>Hy z&lIJv)>p0e_VG@MOX}Anp$~sq<*V=>=HuP33;%$39mSu&`gzysFvPo0OmF_oJHq&H zKHhy2V&e?|p!Esx3H)u?Z+|`c&#NH`ed2r-3JtF!DIqx~*;q%1S_)sKzuGUr-(TzJ zr_u%n_$xI&N?*S~l|ritR0pUv0UC9nGOm_F>r+eVtJJCk0s;dRegO)9wN|N(tEE)> zD13P_jh{y2ui_t3`YHT*{mDr&efq_VYLvWhDwV(1Ulpj)D3k%(0A5nVOKSa8{ENK) z0DqM-K;`e_=j*TW_gAU{v}%P`s}5B8^SXJLe1>>;>E0tgHYtt|O$UXchu?z6{IQuzC+)oT7rBZ4F{j~mSwMJ#^Ss-s7uT`TlR2ZO91oGA? z4E-|v(FSVOd~A4Y4E^fYB_^JadiQppF47Yp?Nmq*`?M1ZeG>mSIAXm}e` z8ns~v4FeIV@Z+@v@j{y+SgNw5Dt;fA&HcSH8`=O(5IQ1V79xQzy=4L#yk8DN+uZGcLlR0aAOp7CJ_ zP#HR<h%%&aumwEn`ndbHIM(~CN`YCzO{QQk8#ZSSf zQmat}1aQ02C=Jcw#_sQ@;TERgJ?5iqoZ`QC*6;hyYSnzH`YE)5erl~sOc)2VXa1`RV%q2s*F>?r$iN~^z-AJ0=G9s zp#MMZTjitnG0cX^c>H}V3`QHMM-Wr1;XnpvWWtibWKYvXC@20`bwR}YXTB|Bw ze?HE<1O5s=m4Vs-jn6MV`fflJ-|Uv#av+~Kt(I?5ygoi6epjuylVg&)eLK<( z$E|lAjgNO?OfsL@za4hN4DZl7ZPL`DYJZDQCrzS-nA1MSf#u|Ib+`7rh9lN^_nm>_&E@36+<^@CgT_Y?Xj z8{Y{qSmyuW{XmoV4gLIS1xlq!iC<}vQe{%$*Q!vPl=!78{7ee`S``|T62DZ1ze#~# zt3qp1;+Lv0ox5MF!gTh2r3#f|m~ngj|J&bB7w`Yy{=}(hFvWkK1pkk=zkUOH^!_iK zpNem$Ki!;Erh5(dM}MJ&>DKXUb(n4)zgCCo-tlX7nC>0FR)^{4@oROMZXUl>huY}I z{&~3kQXOj3!{XOU{A~t*WzN;6r^T<;;m3DDe&%wAFfOS_%%5IhY(oFe@o^D7V&h`H z>m(=jkNeM|JfOQl4&VoV=cJ}_$uaTqJ-W0^=-;h-pSXVfd>%5)_$}`K8`YWrXfc0Y z01}gW^zM<|BjwM3j7d&T>e0D>vf-~oyqhl3~$!5X>gc#9p354`fckr zYu2(}Cxv$%?iPkNYaAXKVfYI-={EHnH*ECnZ`^jdg~#^jV)z>0utbLp7Y<_&4Yv>B zLKrlsxDft&(-__aV-fDI^-SQ4%J2%mCHU~m>)&r;!ksr>O^j}I7hbSmLejU7cJAM` zYg|(69&rPV9q5wKJCO@eeUc*wB^q33C6{`Koe$(fohninN0|HbAK_l7NGVj*)>#DizHpJVs2YlRr-h&@~ zfX~$bhaSDQDPZ`0!6k2)6;e%$(>RsXLB|LfHo-~8{kuKHQ^KeP6q^!|Tj zv;Q*qKQr1NSFie+&HkkKhQs-Vee+j;)q&Hbm<|L@FQ^%HadY4!g*b65Yw+<#U**R20HgRlOd z8146KUHucIkQQ@6M%;F7k=cw_o6weTPoH#XA}JaDKm-WC5# z2_Bo^fs?-#;X#o=<8|i06j7@5333I$5<(O_(lDrT9gWIx28SeCAgse zgYn`L{dXmJ2;c`L{Jt-tRQ<5TKl`mV#BP631CJ#AqZ3LV-ugj-e`}Jz-;d_}`zrXQ zqD00rDTc|1VAA%kc+&@cYih)jan9g91wb?@Ro68U0I}0{^E~@R9q`z(vO|5jNV>+50s{=OMbQnhE_>P@>{R_eUg z-Pj)UTjiwI9eBOGo#x4|Qc78OWBbi7SC@{S|Fd5Qf~1g*yf2O2Af&LNR5;p|o_65B zkM@Tr4@1aia;toguaVI>~Xe8=Xs>Ag0(BDaqNL zZv9dLR+Qc>`E7Nl)&&j_vwpR7I>>`Qe^(x2rp}krGCkgPvi5tD}tmd3QyzvA??aZBfokWzcZ%dUESf;9>(vFG25ftSJT5-@0jB&b={Y^ z8{0D`bB%7fmAkQ>#hT^miVk%%wtr0aJ6+3SZr}RV(?hD3;Yx3~xItQJkgR&U(4G0N zkhXM)@CZ_h0{)RMYpdm7&_R_ngbG1=4joiT|Mb$woV7{6~l#a8DW;$f_}-07u3(aG+{ z_LPg=5fr_|jZWw7v~H}>`Q*DA+dt-yRl4*luEu_u&o3&?-RMHM?Bq7!J|s^3O*^huX3jSTbBc`26rU$6)rR**B(-@ zJeLBFxYA5-TL`}PRLc9}Mt2;sfxSZtrKpM?bonD|NYx&Y+Bt^#YF?`2NW;+VFZExe>fAa=%Uls8%);px) z)ZENVZpQX}{4^%lIh~I`Z|9pGcDX}GxER}C_VUc2;p3f+{Tef2r!MS*6Fpm^0zB%H zonzI}gI3v94jxs`${8+u(Q28la6M+0zV%gCdh115==5r=KJ=j%-T27?^aG3PC->!R z(4i!J{IW5(`M#=j>yxsuH~ym(=~julD9S>yeg|}+sor$axU#UnO{nB~ydqsc&JL_= z_tmY-twisfwTGDeJyJ)T%Jk$-2Pm>&xwL7!H+@p799XQUQhG~oYImVLUkl@<^B25m zj>-}C4%#d|DeXmTUFGBdF<;u}T#=@DyTgrh48(cW;Q)E`pjA1-u+E#H&deUWA!@T4z} zSA-R#t;nV!&h+XEz9;wbCp{m#)7dg#%Qaj{VU8yqxW^q<_&bsh+uf)p%LAU1+T>gx z7h1iR2LzuvB7J%6N;SnQ^Sv%#vIA$j>S#p>UO7){JJFf;9a{xb7j2c+&2pmSGkIO3 z_?{NxOs&UOg%w@SOIyY|(s!C_5b`2Yy6NLcZ)hq*G2JKK*!fO$O}~n;?|L1n?N=vy z;8GR1QFo~{)53|?AM6E1%FEKb6c-x&vNG&_hLYnACpyuK*Bi1{T7JZtdK8p_G2`dy z&ULOxZ^V}c>pKxT`nwk$9qtItx?8j9d|z5~z!~0LsgYammK&WI>INTE_v9{*<2KyW z4Zeig>eFkw(RZhu;Ny{fxev~}(+zjr!TM!4-Lpg&y7-_QTuN}t89s{dd+$78i1p~) z_O)CoMArtd!DFS^tg`gQ!5UB`I!~JWxjfZv_Jtc^@1&9C%FuCNE5YN4T{$yvInfbE zE5KgIwbH(PcN*B3?>XyMN%0F@>BSahUq?0=Txj=B9$uEN+f!^-)dQUz+>w4V6KFW?%B8O|RHO+NIV~=@uUJ+Iqe}q)e6Cmh_~r`i=z8 zBfF(Td46Q{@v)Fm+KDAB@g%xuz2RB;NY>=;Oz{4k0G|iM2uJ@OURc$?&~X!Xh5G_JVp{mc3`a=>ZqstS#tgC zOa1Tw9UWDCFk!Zj^(zaS&>@`$5jLt&f4q1Y?X^9fOj{?hfnVy=8_hZqDa})#zapGo zS>BLzyjh^1{97}s@7IVd-E~*rR@IDFo$E$ES8~#SiEl}71+5?-)}P6JRIDLAv3v>H zS}KWc*i?^}yfcp!mR!jOZv(nUl}xU8^kuizP3fm{Hl)C@P(OWaOS+_2NkV$v(d#<2 zq>k%qk!HS~S<(98)MZQ!V#XG+7fy{RPr=@H?0WYc`E4ZiZ7@c1AA4CpxkDtK+qs|A z@#-CYz{d#MKB62+I9G)gEN@9ymT;32W&`t#kEBCe&y|{$OlIzTnj61k<-DGGPHJNO z-Kf$?7TKwV@pr-{9h-VBk|rsqOY*R#tn}h0^hkx)#NvsQ1a%rq^yAC-+oq*#os7 z-Tq}Fd49PX89JvvEu~mWR*pR_MJwx3d4CNu-)}Bo{*0>uK&SRwRjeW$zr!QQ5oJmSISS-amMZ+sMOQ`(1g#Jc!Pq;YpGU@1Nk8=0H zuzbrn(!c6@HhI(_n7-gQ@?n}57u`>Vw;Pj#Hk>Pqt@d|>!+ZZA-ft#hQtc?X7tM$! z4{=-<8De*M;hSs6<;la=fX@go>{&;ahb=n*={=pXpkto=Y|JUBw%ieula9-lXAeQv zRadOG;I!NtPe4>TdvpyrDCb9SgR5wV@|xZ9gtvRawRmw{?#kpHTQ)%Tqfc1M%{-w}z!@8~-zjH&8~~f9+2W&tTjin&b0Jb+6eoY!Dyt6+gTQ4knN{zN@+!}15FB}) znNQs;_a538oVv;^>ce`u(NUSU)H*2;cU zDA=z{XXi$)mfxE@K=9jZEWgtV`PFZ=z{z7jYh=AjKHt6?>=`?g#m29cn+~V|%JzfU z(JxEooY10RA9I*F_FE(me#`ydtZi(|oTc*aDx2ZzM+Z!{I4OU4ISyWh*`i(JW3ubV z1rT=96EmET$afRM;Ze=<7;^fM{PbfPs8q}y^9uLN_i10a*Q658&eh!q-;jD? zC2{2QHF9*9>%^zI1)l4&LY{fKI5^Gu%C;n}l1p~EMFxGm&xV>Wk}GAuBbQFzV_RM? zk+-(EK&Jm*1*^{fYt=qM6Z%o~?K$dXU=OeDKkJE8mab#l_aKcr-v;@C2EuWZKmr5^4d*$Y>c z)8*2{XQu=1Tfz*_GqM)gV#d%M<2#kQIbhV;opMMSb8>Nm3wHjx$N2net`kP}+9rnu z&Xcm*xnTG0+l|koqInxCu9v447D^Y}yW+cIn~cxn+j?Qgz{T?LBF`n=Y*$QKn=V(a zP=q|9u6#W$l80HWBSlY{W9FhM@`W9}NO~1Jym)eo{9(%ja=++7#(tY5m%Mv}?0J#T zI*yttXO}!lisuw3#inL%w$Ip}x zOIEOQV}bsG`3xDKT0v^DO00dY*>cdl5}+(!ot=xADj&D|NY)R@WabxV$yVB8P$X*x zOFleH{?cRxNgQv9S%>Dx^YRan7Te#j2LAKpYjv&?WvN>%S2b6z^7<)xUNWD}eLYt$ z(WwYDUX09P<~(_Ap*75?I+V#h=gQObiouLO(%Eo}x$--=jwI2{0Tq1SI|kcHb3b+EN>+o|?|CrYw|?em)4V&XvT=@=3WLI}8^~7R7AkLHW|qOxWh~fEB6_$c|nM z;K=J-rt{t-zgV&p_9Wh7JHO`0na}1y8y@sgKROK`*p{|8QHR4?$E~zlJ8~q{l30N z=sLMd*W;wVYbkx(X=~&G`yZ3MF2%9^;sf%a$(3PuS95IJ{Gj}?Vo7kCcaELhvR|$` zdjmN$<`7#sWWO9AvYIT=E@P*|Wcf+wVsL*=I$PHPWohzyve#xLyV)&QE;;uhQF^sx z%lUerF?c(f@wh1~OxP`d2slb+)UaglLbl0vmllvwNniC*)wAUBzBh=4%R~Lp37h2D zu?qu&g9Om0$pFHbTd9rQH9Oixs<#Jc*5wD?1 z?9S_4x!2qA>~S+&l_h+v6D8+_8aZVs+Z&SzI^SQ z@io4}<21Xek!5*cuGF~GRd)W?UO6OgzjPxhgVp#X8{csnyPEl2L*wr=M|-oC+w}6| z$M>Wd^G3{k%C~)>LBmKrd%45-&ft>8r0xy2$!lr{5-1{(yoi;?=hxS*CAnkf%kHkH zB-^ppWZ&6!@@k8*WUFQpxm|3YJmBM6;^EMfEbKE^Rv#Thrakp0tuvR)gCceiefKQM z+I6{HzHA05d462(^w9NkeEXZ^%bjD=@mF)?y%+Kbyi|}%fpg`uJs*$*sh)b@_sivk zf+u8utsVMJ{a4FT$1R}5mpi!yTW87VvR;#*{%Yxo*&Mld6-!8qxTQ~gw@`Mv`-Oy5 znx)@uyHqZbZ3{E5*I;cuR>+elTf#p3OZxL{t_(LzLX4#!TN<%I-qFDhidYd=$#SXu zTHa0^_N76r%|pEC-I1hPOa%8@Z!zqsD@-ra0J2?gp;P1TkoXLU(>}z^GLy-s-M6xD z_T40R-|9%lEn7ee%oiBPs8t2m4{D-L(e%hNXFy=}Ad?&}%bUtn`9_ zJWu+p(@`>Td0mJaY(YcUo+GheydnI91HG9ZOopbFgvIL=)QsD(u7Lw2vs(06o5m!o znhWIDsbPG+Zq6rC?!K1xF0LS+Z*P;xGlBG8URM%b#uth&@i4wK)T|y@MVFzMLxvH> zYJi==cE;ysTYbT5uNQ@cKxpwkfOLLUj4oSU3kuIIBL}lB=_D%~NWHvKx;m>W#kppX zdfJYhD^`o9RWgTVBgW~M-}a%)hHD}C<~S+jv<;nE-yc2=zm|LMW+^(VyAL!mTVBV} z)`5Blzb3)$M@gGDDXHD9`p{|eTK(GLui^UaH&XjW8)5V7$1FZRjBFgU4%WPP!NZql zXD?oq3x!qe(PFhFvDmy4h81N*H`D z5S`4{kXmbJ!TK_7adSZosd8p1w8`v^ordou3tCPE*t`T!ByS?`&QFI9?n5!{Vkxp> zrwj+52I1@t^Q0cu2cbCg#<0tebKaeM3YnimFzk$uoO8?Mer*f1x?hP{UDyE)ccx*} zH)F`|d&?l?(G1LL*GF>pxCNh=)y1wk3aPSeP0wVeqEELXB(~96xUr-&w)Jq39%Z}I z*cwYQX`F9v^Q7{$zGf=2$?1|tYeS2-nuX;SzmRO53Sds3u~?<$9m%WG6=>990gi~d zsEgn>8&7D)FeoyU}SPj$V#Rdi?FGx#NTxh|=iKXtV@jjyL%)U|rC0ivHB!Ds6? z>gH&cf_~H~JYaWB(oIjpwC9@mVl!cpkp|-G-#ST?036 z9>9l|W~AeU#o$ryB-(7~K*k-N4G(u7#V<=kNa@g(+}GcWlCnIxYM%i$3T5<}zm8O} zmj)~KC|UH0LA9=k7E$k{>TVaHv~Lcc8|_S#5r-kA z>~=iAsSO!6d?W0CvmDpA(2!13a=~fnO8m{WK(}qGH659E2E#8FNDFU2gI2?GF|6Vm zX8olNq9**5B-6&t2mbssc+p82M0h8{-L)~^f`tGYVZRTw#P^RPfBKD+Xz2orZ z@=81(9>g_*9T4O-2Ysv+l6HqDZ9VT2zPhkYD!I;@UTw$QQ^A@{Y;_nKx1EIpT8`Ac zv#(AoDE`2DDZaWvOVu>y;A5OU+ERL`<$K%6eDrD@71V5JAbr#EA*LsO3EJYNrQ1(E z!Tgs?q=})HbZ?_oXtnigc3BLf^&0$v9~W>9LZ8W$)pp`SkG?>U2e6`LH{&7uM7Zw} z8szsm8{K;=!1Zb^*pzY>&vYh`JEJn}`LGUOtZN97=GJhs+%Yt)!Mt6Ku;sA!c+u69 z-1L|MHTv8^y6q^}E7HK~*a=*J%M4zX>H+URUqj|w4jdxmq3hRVY_d%OwfO(_+8{L7)owK_1~Dhx_3 zxW+D4sSSBUTfzCE4e-d($}sS1SIBQ+hR0eLhu?~%fzy>btZMNf7(F)>4ivM~2X(Ik zyR+gT^@)~k>vWdnMoogc2{ZLgx~?Hlrp<$E$_FgB@G$v2bTm}#ww=_QU<;2bCIXhS zB;j!b$uZ3$7_(v$dl1_Pd}g@eBxw#DcS?ZSNmX!LwefJYZ5V_V^}$Ua#*;aWK*kq; z)D1XG?8>!)@uzB{>xdI1Yh)~ZQu|=*Ne(dmQ+KGeu`~{7kq)=aB4Map63Z5DgD!8! zfK_WN++)84PLHvti|6;nNROqY;Ib|CV=0)&j*+c1O3_A*dg7z^J4wlx_B6R>0?xia zk|=-w1X0_=(I@&T8CCZSxE|f^#h>c-a;sv(-K>|HHA@*=b`9I z3D4U$fR)lBu$f)~r~Nh^W_8Sf*{L4roi`X--y97C4_;&=@@Im@+L`d|*k|Tac`{7c zTY_Hvy$M!*REG51eiqCQRmQoMtzdY029&vCpX!Yu*?SB$f47Uh+&2pDznl&iL$0ub zD$N&V$olXZfAFE!YIF0Y&^Bw#M8R=Dn8Tc-yl0Ep4+> z^0Z$7ZLM!GOS{sfR_p1|yv}(x{AHmO?XVW!SzTjhvqq9jRj0zh$oZ_cuN$ecb}>|Y zp3iEx@g<9rra}hEVPx(-sp+M85HNfMt#^+aHRuG7Lbn|pqhuwm5b=-04m~81_XfD|FF~L$JRD@k8H3dLcA6jHa#MQOdc6klX0 zbpw2@=(Esp9OhJ7IvH+BCqJQB;4(p4``L^-KJ~-gV^yUNwohRGoT{h|u1#7tC`&8R zIPA8l2wB_Tj*gFxLUMYIWZSO{4g1<12Xt^F8^+qw+!L+wr1@Uy`Sqf7_p0`2nGPg^ zpYhYjhoJsRjP!&c9G~NfU!rrQ%MUifrs9=w-h-;7oc09F{-ZXs3cpLp&+3eDI~?BY zfwbb>1Xy>iI1Zn*PFkxS1)u!wv9x7|)Q`-BPY=rBp`vzV!RdpL+%yDT$FCq!2@hd% z=TN*57fe1z=fkf;&8kXHejWz8InEgV@r5*^Nk8~B-V@_rjUvmRo&lS(5!iLGElgCe2Kj@8 zUve!VsOAc2tJPp7)hgnPb4`ZZ*8&H4)DAnh41^w7BMGf0+7l5ZI*ELUzLw zemlGw>V>K>rpI`g*=!9=uCKx-FDF67*OhR5bO7ExI}tXNS_(RkK%6&m0u+6n4g*)# z$Md1dQ21sMOt&C-x5Fq1dbt@~u`yo0+5qMcSjc||v%%fq((KFD!e=BTVJq}JCe+(lZ^}x)!fiSM<0hr#Z1rE1J1kV!tz;A6!tfA=z zK2Eox?cg{JujmiCjtGedT4MJ34sblG1f5)b08akx9a*Qbris&&v7==XxSUd)dgiBK zL989920VgTbsTn8Q~>iotm)A9@i^3aIk6mPPh%?&K);2X$jf?j;L#Wtv{*6+k|x>G zPnKP9K)r5ct6y3AN!tU*tyPia7o#C#RV>@b<2KG^$AZA&=#C2+)r^IU1f_ zy2@;>C)BCZWh@MQx`gFzCi-==heF1V&5WPPtl_Ky@NC;{W`3wp|0HS@I3|8(0fUz4 z=EU)`;iXy4)q6?M+C~ujuqKXjoD9{S0$^ZUEq0BMhb`-x!tvRDSc7`O|CgfCR~3tr3sp!TmSx5PPp2y7=CN z@MRsr%HkF4Fm5+ozZC@CY=vY66Zcx3HLAx1jO9h7f(R zJA?Yqp^ayKNbgbsBM&ZSqic46M^MTEFy$2kea<1<_qbNqeS*F|RJ;AknrKEAw=Z-r##wdmDiZ zOSAyPZ+U$~gs0n~?|2}S&j;hNLI$c+zmd^xn_$G`Ik0=j01|*LF>ORku!*q(UH$s# z{HZN;$vsMZ_E20mErab1aE90$3bg0i)92CeNb_R^i;bTTjx*1Y*%dW7ugU?4(A9)W z6>6jU`9ko^a)s^$*Ksso;dck#G9SG=)+#=B)5nYrd%jP z_4n#P*kF5n^*Rbe?;j;MrX7~(ks#W-y%uZ^mw?~u9)YeKtfAhh5IBO@pk%X`Wb)xq zSXJjfta!MSG~)Y2PS`~VykQ5oTQ-6B?K2^9vnMM@GBK)+K_)v*S=n0mlUXz+7s&8l!BqIonUGs4fO9cm29{_78*?RhmQ7Ya_W8> zpyecEA77qd2)#=kk{)?4$(IV&$PG;7iJ5vSAyu61`pHzmSGwgBQyIWZ7sW}wA-VREaF_*g( zcY{a!G~n3XLhc%hqhzEt8}bS}&54BOiAzbJ z`p>ZNnHsv!sQ?K}-eK!Ho#C02l0BdN7*9Oy4C8Bi)EW5jDZW1z1$OId6RW4s@R3_H z9_QM`ntghW%>%TM;qii<9`XpM4BbaAR@7qJ-aE$jPwqJZUR5YAw>z+iSnkRsDRZXC z4aVJ(257F6jPxn;;IM;aT-TQ zZ{98W2dTGqioB-hC-QV`c`|n3MA@dJC9ulVq;shg$e!>A~=g*dln>QjER~lp3v0X;(C97W;#;0s! zRqFYWPaX~N@~#dzaT1czg8+{l>WH7-429^)bM*1|hvFpbB=8JKA+>8vz^(3s;Cko@ zY2t)2xc`A2Y-&*(Lr=EDrT5xHz@F1=bHCxZqeKcQj>a>m$UZo)*Im-AS8w7wdV>5e zy(M^rZ_zhhm=7txg~O=cmvqPOT!3;<8-dyWip0zRIw%(hgV)OG(!+6IA^7q{@@{-{ z7(UsYMh~e$8dYo$@Z_V>wl}qjh7q^k7=II=KlBY6L{h^%7Bsq|Papp6_f>@-HXQ%b z_|9nO=IG|oMw%9MLQ+ni3W-zS80}>Bof&Xw(NnBjqNgrrN+4ai?h-oJpQnp$6G#t# zxrUX@nXX2%g7#T?563m!sN1>Ak3OyZ0Mnjj=uR)w(h^(l;PRr8x|8+_x?|2m?0a;H z&Za^j9pQV+`2FN#)$z>1K63CUGm^Em5_Tqu^3t7y$fIXY7@w0M9|_(?!djQag|`x9 zb;Adwd(KPtDk@P{&bESQkw;jaQ;BkgnhtQW@;sJ5rLX*cvm4x)T8}N?oFto{bBBH7 z*67)^esU|lGw8CbNwaJAm)}e+3kep^WM*8792Hgq5~ie(khcS5m%@+4^VJGc$$F6d zX4Oq{Z(bg0O9#tqy6q;8)_2L5&%@>OzNut%gA$Mj!(_|b#mTFeW#F{iaHG9BE-wck zW{xoazP(O`3w)Z(rJc)=ytcJa{i20@6K9ayHeMLJqlFwZ=p>n3#2zo7Zy`S%_?1w_ z7v?dtrJUjI0H+V1Vq2<5$dR#CKz=`$eKL=fH%<11zBSbBtye2qom(451uOKu5?af9 zHr9YIuMbFBkJ`vZ9(#eu&S*jgwUf8(a)3hbbmDfZy<9B41SqUGFk2qvgcejVvWK~d*e6U?5$S87@usJ%prjrA--SeI-y(-C<`xS$Gx66S~NsuSz7Luq1 zKF}?g%A*>uB+ok4g_K&sa{mX_$n4`0FfS$8*q7L0Z6LQzUE}X{v4Cy!?d6FLwd7)E z5H?RMD@T6TllaB8vFXP$vRRr1%;vGq$_ZuVt$SR+^_V5LC}l5youCA|{dbnLsJz_U zssZe7vw%gXImqW$w*aMkMb@Ka1$kn{mT>v(@SyGm737(}he7T2bqVQIUVh;SaQI0E zNp0vX*O;z^#9L3uu^le59O4D9V#~m%95;DDLwndnYj6$5LvD8a4as|051#e(l%v(# zNyeUzux_uHT;j5p$hZ4K^|s!|zPzv+0B1f_H2(ImvuFDvTG1J{?{veXuIayYkEGV= zmQqKnJNl?H_JR7Os~3U6LR6czTCB7+9W)mcuZ2TMx}IgM#fAsqJ#q* z`m8aXb!-A5JZ{pbODNqMFon3cd#)cJ-k1{IY63bJmR7R?-4(uzc(e*>?aV6O@q(OJPwNvBB-*yxVo zbn=Dv*six> zr%OaoZ$%Bl%v8w1r$wIPB!H?Au+>spFsK8~cfmduo%b(qA)4hyGs z$CZ*Ct*5hqBjL1SmaTMZ;1D*^rv*)l>n$zY(T>ILZ%KDvKce%f*ok$n6+zcnUYBgs znz0w}zvchnglOj2HG($Nl#+J4q_cy&zvT#xhuN&_fpB`D_#LTQ$HnY$*HG$)52Qrj zaqQKqrnGpMM^dwfJy^#o&1sv0U8GabKFsDb(6RBW2%K!g=KI&D4M$ER%@%1`oyU#o zk@{=My}H?KgH0gqVl|JL^*h1(RM*lC8B>Vda3?!HQld3yA{nnn_Uw|1R;j+7OntNzE1rrZ^BuRd=!e0y(Vkkwtp71qDT>gzbSs`KmB|v@)TiNPYf2fDve~EV4e5tm zcWGpsvutK9L%xvNwtKyiefipe*66oUY73d{%g~0j+s>V2cgN{G_AOD!I!=}kn#fN6 z9zbUfzE5n;%CHx`{pp@Jg#V2lQri%w!NK4Z) z^%J|cre|DINXG?x^|#+PrET}OA{~ci>+8igr*ryEBg4~n>9Zz;(zmq&2-~$+?_IMc z)tm?+QaulSZolSq*vU1day_XLyq zYY${Ef7XolFYQH~_e~Gl*P#XNp7>hY?`I_~+}(n9>3pIuJ3wDl z--y0e&mn`)9?RY2+n7FDFo}?HBlR1ehSI867LcWpE zd-S?_t!R4f`?}9HBlI(SwdVbz+m2soj@6LBDTx# z<4oMgc{`bCC(j{dh8)5~o2nr3Pm98cVZG>wk zOlT%tmq4LQ@H;ybcXm0z8HL`$?FxlmaV-mlmR0!ijb6+t*LIlDcKCgtiTi$u(G&E- zPPpANVYem!h+)EJcI_Czg#N?rhY9;B6=Q`$3*vUpgq?G{LSa{vm^F$!EWal*aZlXV z#u|nFZ^tSq?Em?b$|&x+{Ema-j&tf{Lo}?5U3^{eyC;gfCy&RWh{y4J5Q=+HQX>Tu zcj{K}BbZn_d`&U2rudzUi8~j+i!pH*;~EYcG@Q3w!{O@-#rlfPJ<5dMHNSH|y~w%c zXR%(K#Wv}ddg1@@++!wkkGU_zgfGPXA13@CA}?VgSNWQIHWNNl{PuJv@{qZY#DtH; zeIX`%q0z+$GeZvZa-PG?{Us*+CGI0J;Uk?~zJ!T9WbQ99;V*F?i3uNx=O;6fpB%or zFEjX2W4Ry2^N^XyLrzyEGm*Exv!;v_ouH_Da%$b{HuP->3Web zU2j||z3{=3<9h3b-?iuZkzC<-t+l+a7k*cBO)0(bwYX2E7e3X&;&=4I*Rt&LNN?g_ zF%$oanfO;s_*a!qMKa+daleRJ`(o_nD7T~2d!Wt-=6yeO!x!b2VlYn;C>V{_)+QH zk6OKSJ`;Wd_o*N2>3*T(J>jb@tkD(VnhUMVxdf{Vm|4nc3-(>E;6_kF+grCO!QYQRT z&NVQ>HEhHOx(-(9ZE0mP@FYjf;HeiFcUs7 zXB?P;ad72~1NVWMi4V+#5A3ohn3?#%O!&Z@iC}_>;Qp{)_`}=>W(FU)4EKS#Z_I>m zd|9)K3HE{e#!UFeT)RV|-SONK6uBiKpUjxhuee6aghtA>EGD!pu8}gKk#b#&30;fp zs7&anT;pOwi$YW7`WOm*jO(r_bXTsGq0q{>Hj6@=<+>RP-HhwEOz5|+g&&#F(73kAgtp1` zG$!=4Az=rZ(3aPB+s%Zw#1&8nkN8gl^0=HzqVUu2<`YUd?qjX3*LE zzUgdC=xm(_on%61do=JXGw5ud-*h%K(b>>MXG0U64NY`5G|}16L}x=2oefQNHfEx; zF%zAQ37yTZl?xL(8`smAiJry`dfJq4dKxq6Y0bXrX(;qGp9$V*qNkzI)81SLG||)0 zL{CE#Jq=CtG&IrEQ0Qq_`V~VHJ&l>@Y0N}VV?s~kx*0Rk&6tU9hC(;nvd0BYbTc&3 z&Co{Ff(aq4Ho27lz&Co{G6qMI=j-HZv{jK@t-#7%gd1x?~C zXcA{Z5oh5s8Wb@a9=kyiySed9jUryd<31?jK0F?TA|AwJK+Gft#6%1z`tHW;!LbUO zwd5WaBzxzc8|_J5id@EhqqgS0C|aGCyPA(n&F%Ca&ouP?h|_p~_l4X;@qyHA?InD( zX*rG7%f)ab6bsXPlSE zS(u2k#0;syM4W}^382Um;JLmia(#Im2Spr5wKy0>yoSegP{ee2E-{K+VjkZ?5#QnY z$td!ZIXj7ho#b(LG>NmLh_myYc_wn^EqVUFh+*-V6pEM>=hsm1Ydmg+B5uX=AyMQ* z@;r7FdF(ufg@zc`1RlfUxeX|C8+dL8irkFj_1fu0tj*i}xn9KAs#Pe$1mpA8ts@i6 z3y-@o5qJCCWCas^4QGFtV1Ev6R4@^bzM5j0Sc%jkCj? z=|%(7y__@MJa3$dym6k3jv^PG$DmQfpn0A-iahfHnMxF~a?U}c;Gj9@j`}}2ch172 z2@8*ch37mw3Z9*_^C;MP&L*H>6F3)-f{W)YJPH<`Gx8`HdG~@cD7b9S#iQWjIp>ao zbLU(<3ND`W@+f$D+Y9AUF!H%7M-+@a=igEA@0@K%!M6AJc1IIl9tAJYS$H(C@EM$i z=lnYg{+%=LD46#_d)!el@0_hb!B%ko9R>f+d3hAPJm>CFaQB>xN5RE&RviVa&Y5=< z%)5WF$|!hw&c&nP;#VH6h$iej3U;3J?dFPBgnlSPx82Oz&-B2*{oQp@n#dEeD1>4TK zcobaxqJwTIczGIK8x4$nSI)?D{v8GXUZ=4On(*&v!oQ;l|Bfd7JDTwCXu`jv3IC2J z{5uN%owMyI*!Gbw4rs!{qk)B=`;CQ1!NPO?9R>fsKcNf?{+%=LD46%KE4C=d0rEWye7_)pkPU^*Nk9-v*s)_6D)I`j8}TWV)uH#OfQ&g&P_AHO>+*K z2@cwQ@irzH9G<7dM4l37qM2Z#Im^QY%fmTn6dbg>)I8%~if}&1k|kqY2lHCR{U`aLrsF{14ZR zCR{TLu32?p7#g@{>u+2$nsCi%!Zo7_*Ni4yGYYO*GlilF*NlQ|e)(H16kIcBmQgUv zoN>l&e=*KzV4RP0#yLB*C^|%z0-fc;{)MqnO~DIs42^*k>l#XU;n_!8=bYEYu4InRCZ_!5woBT5sT> zn{y7jY2ZA);FmdH%miP|*WT7tC2;G_b${-&kNYVS&+v1xCRFbB+=PN6EQhG~t5LgbPL!E*J$D z+-q4a6kPCzvYkqCD7awG-ZH`7 zaxRz&E|~MeOz^^-A!ULg<-9O6;e|2xFJ2f;cwuJ33o{d57)^L#CU{}a2s6P5b9R^s zc9=85OfbTn9cF?Z<_s(o3@m3znP5mcJIn+-ywQ0n6YTJ(cpWoghnZl9%U-F+1Ut-m zVJ3KC&Ji=g5p#B!nXto5u)~}e)|>Fcdcg~GE?6(P;I*{_nc#mp^Q$*uewko?2Yy`3 z1oP{6bPO|Lewko?Iorzw+gq;Z118ws_$rntxLxPFUzp%_Ijf5XR`=XDRu@fJT@_?`*Iw#DoV%)rd%ab}itvP^KYXUTac_*u@nGQqkQ`c7vC9<~AJ zVJ)7^%)rmyPnqCPIX}w;Kg+pS6x=IkTA5&4c`mMA;wZ!Aeo0x$QZO{2o&W$p`jYhQ}%8Jfe25+KgqU%u`78N1G z6G_6Zla}ib6&!&1u@!M*+&KNf?3<9BT@T%_ywc}P%!l0dv3SD8o}KmE0T~NNVM>Jr z{e~gdG^}$Px({2gpC^~3cZSbKySpd#)!-Vuoj49}U;kbIf0(-OxSs#+f8csY$Sk6= zQiL+gtX}6d%n-6g$gH%rRVsVWDC;A8lU?uE^Q7#oY?)>Av9kB?`1w4q@9V$ocKvZ( z;$1ynk8|$l)pgD(Z0Wa*#MaSV=ht;@|Lw3`Y0R7W2LOuG#$Tc1}($q;k)KzV^V$<-`6wtD}TIKV8 zJZO4?A_9)6&%!q2<9R2^VC6owsMTuTQM*8K4J^dF_3Kgh>KLWW%~gl_??L^+V{|LK zlGyiX2WAgH^2cuq={Ic~O%b2%GVqLkKXvWDOssQQiDUoSNRuj@&_A>*K(E9TbmQ)8 z{o)7r@pUx9H2D~tB57(IXL^s z2AUM#UYy>!9fw!Vr2di5^`XaguGIog(V|x4RpZKUF>ks`5B6+U&(C;9XTXaG78(Dz8es7>5YF*L$(*d01nnv%pb|YyU*+0@B|G4X~e_O5qvU4Qq9DUApC%G%m7Lufe?3l+p`2X2PHjmI!H0F^s=8-h!k)(Od8hJ%X^T?(^NK?SpKuBxA=8>d%WD6;N z{%0ZCHj=cBCN&3>#xs(}Gm^$LlJtyy*7?#OOEQ8j$=J`+Nn;yxHMTKVV;gg&ZDiMz zD_xKAdy`y^b0p~;*%0MQLzLq2i>&@Lk8B%B+Q#n39*{JTY#T}1Ms|@TT_pQQlKzpM zBT47T{*k1AWamiIIkJDumHv^5teylEy!h^pETZN%}#y zkfgDYq_L2sv5=&(kTm80EF?(_xqJR%3jNPQvVSD$AK68cbdhW!Nn;^NV<3CD~+=G+AsUNn<2QVhM3z;h|hM3z@62kR&Z6o5x&f9@$0aYFuQlbdhW! zbESo3zm_Zg8oS6`=_1)GlC+9!Bx&A%Mv^TgNelT3h%{c3q?cqPNg5+b8~-zsTRWtZ zG?L-4grt#VPe;7{89XOwvfQqa^7l*-Mi2l58YNVY#~WwAxTf|MOp+dx-6jA1{LfvIbe9*-y&~x=*-?^olx!z+rJZCinJc{{ zyU1LPi==1&xk$E_y;4A$=U%H^jT=eh z#;%Q|Yh%AAq+es>MjGQLq;X?YCZs82<0dr5O-SQ5(!^0nlf}M_q%UJrM$(kAH6v-w z*q4#Umyz^kY|2Pu%J_r$*Oaj%BaI^?>B!iQk;aY*jU6M69TU=yu@NI_#O$&yN#4!o zTiGOUWhW)pC3!=e?`D(cZZ>J|W|QV_Hfio=ljd$VY3^o|=597=?q-wbZZ>J|W|QV_ zHfio=ljd$VY3^o|=597=?q;*M{ChWh&7ZIFzq{EHJ?fF>ZZ>J|W|O>|{dz5TzyEhP zo4fJ<-OVP=-E5L~vn$=JMVh}&0Y=` zq`8|-@^1E@Kd0tyHp#o$g|+x={O@iy-^wO=E1Pd}&E|X5q`8|-n!DMg zxtmRzyV<0J|W|O>|{pah_+|8yx zceCN|-E7j_%_hy=Y|`A#Ce7V!l6SNDt~F`yW|QV_Hfio=ljd$VY3^o|=597=?q-wb zZZ>J|W|QV_Hfio=ljd$VY3^o|yqmr9#3;PMxsk*Ri&%#Vo=N{WAVFl z%D}~oAwM=0mzOqzAl)ihaw7nHeIEkr=In(2VhpaxNB~{bakzii2gj_P38m2wK=-vj zKB+PTGH(Bb8tj|41x|*#4=SUFZ8z-Y=LtFc7&=Yyz+KHup~$fznzVDr#Y-}kHGH2h zFU$igruex`>D?H|Rp;9~C2s2LWGh^Gs29%Dol#$Is*Tpy`lC^41yOxPC=6w)nH3%qGB^661yLo2_X*)l>ATr*y)KLjNMy6P=F6k^(}#< z(KF#xkDfR?<^wF5n+s>HoiW|#9ptb20nRg;;{~T%U~t_McRaGfM_#+(a|K6SI@SVT ztnvrX$j)ecvKHRWI-|TC(i^Asw!o0}CaTfpUijm%6;_zjNKE+Rj*ojZ$0?CB#oNNx zxZ}PP`mNh3zB`y>he|zB*FlKqwLimdCm-yVmMuKJa^TY~e-zf4qQiw0SQ;CN(GkHS zz|t0;yqkiBj$hPYS$WFMbMaU{Z@=DTSO+ofbu?by<_Om-wiBJsP2xLSM<8h97qxJf z5AI1f#8(DsdU2}@8jfp#qU?9Y;93j(!8aM_1}}x8Q)XCavmL&S`xm|#mcgNy%rSbk z6}BCG7m|LzgX?LXG2qilc)WNuWTp6`NlSv{B~O)khXQd}{z>@ay;uGCFaWdO+=2cM zbH#4Q;dq11qgBl!v8A0GK8Q8LgYzrUryX^1!;%)*Wu6(u1U>?TeO=J`rYTw7n+sM+ zK6v$T8FJYFKxuV-5}qzRCK}~E)9?8fjXvLoia-NH@vTEJR>^O#)~TH>-i7+(^vv$y zz5R%&e!>TThu?r&mT6-0+OGKT{yNwo+g@C(-5h^TS1?f*>ijgPDvHwHcs8pa`0Or$ zy4`zXd#7tKKWZ0j&uE9<9qXf;aVkXKtBSEEo;Y$$A1K{?5DK>W;~jq&2n_$IMCQig zsv3R3@A!9hKz0lUA4&o5i&>(_h4EPa+fG>RUy=I#+Xe4fmq4>ecJwyF0`=3YVwXcs z6q{B6Hf5V**U%0$Dhpxu-mX|}d_xM>p}xvX9~|%VQOtMmC5E|Ab_LDL*2S!>ZeUni6SJ*7v72>OC>yPZ z6Fz}hX6y&0>W9yYRo5xF@Oc$T39BfMwT{Qx4&7kV0wF#On1~~67r~fZ6H;=zU`_ke zaQbyy@_t(jZ`3iw6VtlTq4vk%O%rSETh^VPziR-$+5r}vw_l* zM9@t?T)S_DcprO6Yz`cX-zK~QyY|VD92S9PzxRU)_CY{3$K$5w6~(;FYY=cH0H1}Q zR_|q=0OQmE+)-Rc-JEp}&NQ8b=aS9UzK4!OZ3ka0Dm<o1vQJeq29x zdN3-(>WQ&&Tj6St0PJl&U+veY0B$!Of#WhesayTaVDX|!XwWlS+}O4Psy~TBlX_D{ zM8n}(zQ1um8gMbiz()FXp-z`P@U zFzYDaByk@FtC|Jjp6u2l!+Qr*whO^Gb0>n|g?ZqfAAld*&4)^HOJURWNw|8$3-!U1 zY}k8q0?sZiQi5w;hOO^|(PD=u41#n>EISr+j;@6}%66C&Fb0=+kB1(g&cWuON$71q z7-qpnIJ0{qz6o0oJNC_mUiRJ5iFZ0r8*hWBU&kZrrn?+ncLyH41Wbxc6!V?yqoKnj ztdhE4?c6gLsINQDD~J~@ye)CBWdyqWol|yOtb}s?CZJPlw9D=Gx1n^q7k)oBN3p3{ z3yWSmqj7$B^`B8KaGZY-&Pds-Zu83qquSjtDSM+ZcQ?kNr^cY6dqpL%+H){D=YqT6 zS%@3+o1#%r6xwC&RaYmZLbZ00cA97+$yxz1yutLGAEx_$(1 z)OB>e+@K7O$g;=9vz+LINmUr%wk{?nTGLS~g)QT)(XmNq3QR3jo^+{7yx1Ckt#=Xs^c#okzHJhd53LeobBEx<(x+nS;{{@#aK)BY8`GIK z<;9upF_`W?5!N+atW13$iDk0pK}YMlO55}hd>6JDZrTlm_QRv`{m^0HZBP^ZBBHSS zm=Ji8TnS>FqR=_h7Y;PDgS2CT_z(Z=Gy2{Y#_#CQpW8L6=RZ~P_4A=PxZeu#sp37T z`f41WPU+|J!>Aa_zio{r4nJW2=<>K@j1D7Wkas9slq07pzCjFJG~V;G}$A{BsD_Eu4zyur!VPKe?x5hyRhu!6L9&` z4ddRt0iW&G)U|Fs6n^JmRvCrvw5Ws+PvoZOZhs4DA4cQxETL2x>`k>lRl|qb_E4~* z7|!L7#>{UopxTqeVnBty_-{#X_)ztdaA`3XOZqJV57XAe0c-g^AP-N$&IfJK;${_gsiH{|2X(0cd&c zX!`lXmGDH$AiNwg5IZ@yg>fIs;eGzS_2J!W<>0qlaLM)v_=XIpi$+J_$dX+6de)mv z=iY*~`Cd@FdH^-f-w!+6Ood0QN6?efyP!VZwQ^vuP&zb!FL*`VU%6yR0D08904D3Y zVr5a5Zt9M}m&t9=Y|9bYp4$M+DLt|I+<{=<(G=Yyt^-=v#KX3Z_%vx}y3(`>rayAS zT~-mew_iF`^UhJWln=#>@E!0hg_Mz3!*ET*P2isrrDRz7^5 z@NC`=i1fIo4zTuBw)-Vg9nPbp_@?^m)(KRx^tkfvX8md$8_@J(hhj6-M+JOjkR6gh}57Oe|68;=ZcrcdsH^T{$59Zg}9d zW-W2S5&oWgXof2XSH`jz+R%YHui3gW)WpSP+Yf|M7_GK5I-7DRLvSqru!xv z)$2cEXm0pOvC=0_Y*_bGR2w&tyf%2L^RA7c>Mp;;N{@H?(-jkGTmDD&p4%Rm!u9hu$aB{O~=>x6I4R+&=NtiVR&<9c zD`P0(SWVdYZYgZ58%izwIzzqSvk+D_fJ%}NDDL(bVU5{%I#SdelA0Nar$227pSh80 zdrxd>!h0!eoyn)!RFUjCiI%NdE}EJei5gi6v@`#azU}2!qG_usR9|POw^~0{b!neO zd)!==h)fUFVPF!SNExAYvOcX88Yj}>uncATiprG4xgzhbH7#$n-C}JsdpbV?=-Rjw z!ff{_8g^!}sMFleV*2<=7bns0 zyzly{VRgl%*Hb9l?V#SW-%NA2XG!!n`HucTN?VsJ-;+3_S~)-W4>vFWGl{Mn)YZ=^ zA;tH8B3;)Fa9(K_4~y(#=%|6M%jh{3@c7&z)Yi}`CpzmUWWJq1j}7c{CPjK-((zg} zf5Ra4rgblDe93|wH;hwO^6$8etakLlU|#meGrrjF`gifz?WR7?B?>1#o+}#a2IyC% zC*t-cdi8cm4`pVWB4xj>Hv z(YRxATTwlDJ}mzliT}7C&=(Y!$7@A{u!YAvWnGg;5MFO64iHNqVQ30mG>yg^ezEGB zUuWU@0zYj3Jr-_c9fmzCf)O`(E8{i3j1``0)D5Snv{hSYDWYPp)@T~#NbWZNs%1l8oPFbmNGVKHf_6sW^bspXf!ijR zF_(NWsGcE(j`^)L*%W|lQu0J6V-wJykH7|=b42I-0dTBEB-YRADY7!`;n=%ioS&E} ztand`#~zb$>}4}CJv9;fMTg?xI^kmBwRPZS7lamZgT=QlIWVQ=B;4%&T`k>F0V`D; zgg1v8h~%iePV{yjqo1)jZRH(5v z0FMN(7SC$L!;-*pSUN}*as5xii^C%^(>G029KRo8Ck{j9-U(s)=mY%IV;C;6A1v-? z6^P=Y<)B-_MDovTDuOn=RhI0Jqhs4$g#E;t_^EYIDm}JZKcS^NR?oGi{#QOKZ+i{J z)8`Focidn&*=QsdW|j!|fSIuD#vrWH`Mq!`%7JOW2IJt2)1u6$1@Nu@Kn(i*K?H>q zSX9)l&uRBNiC$LvCC(Ura+%c7mkgfF5_9U5LZdf>=vKK+7SHy4fR*=0(XU1$ELJaY zM;AX^`jTD?ao&D?h7m zG@myl49~0E#Yg|XVDB^m3&W0yh0!{UTI+?E*Q^wKx7gs5*p3(wG+R{OT@LGS>4_74 z*9fOs_SlQ>yV1cJqRQpkn0dh+3;93Wj`PCiaaGYeu(!Ax>cqFK_`Xli2r=&qqTNm# z^s;Oz6fawxyIaA*-Yvz@SCjG0(fKgd`>^`FtRKG0dI TZ($?gVEpiD111!T;J%# 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 import('../../sample/a-buffer/main')), bitonicSort: dynamic(() => import('../../sample/bitonicSort/main')), normalMap: dynamic(() => import('../../sample/normalMap/main')), + skinnedMesh: dynamic(() => import('../../sample/skinnedMesh/main')), }; function Page({ slug }: Props): JSX.Element { diff --git a/src/sample/skinnedMesh/glbUtils.ts b/src/sample/skinnedMesh/glbUtils.ts new file mode 100644 index 00000000..96d4cdec --- /dev/null +++ b/src/sample/skinnedMesh/glbUtils.ts @@ -0,0 +1,1017 @@ +import { Quat } from 'wgpu-matrix/dist/2.x/quat'; +import { Accessor, BufferView, GlTf, Scene } from './gltf'; +import { Mat4, Vec3, mat4 } from 'wgpu-matrix'; + +//NOTE: GLTF code is not generally extensible to all gltf models +// Modified from Will Usher code found at this link https://www.willusher.io/graphics/2023/05/16/0-to-gltf-first-mesh + +// Associates the mode paramete of a gltf primitive object with the primitive's intended render mode +enum GLTFRenderMode { + POINTS = 0, + LINE = 1, + LINE_LOOP = 2, + LINE_STRIP = 3, + TRIANGLES = 4, + TRIANGLE_STRIP = 5, + TRIANGLE_FAN = 6, +} + +// Determines how to interpret each element of the structure that is accessed from our accessor +enum GLTFDataComponentType { + BYTE = 5120, + UNSIGNED_BYTE = 5121, + SHORT = 5122, + UNSIGNED_SHORT = 5123, + INT = 5124, + UNSIGNED_INT = 5125, + FLOAT = 5126, + DOUBLE = 5130, +} + +// Determines how to interpret the structure of the values accessed by an accessor +enum GLTFDataStructureType { + SCALAR = 0, + VEC2 = 1, + VEC3 = 2, + VEC4 = 3, + MAT2 = 4, + MAT3 = 5, + MAT4 = 6, +} + +export const alignTo = (val: number, align: number): number => { + return Math.floor((val + align - 1) / align) * align; +}; + +const parseGltfDataStructureType = (type: string) => { + switch (type) { + case 'SCALAR': + return GLTFDataStructureType.SCALAR; + case 'VEC2': + return GLTFDataStructureType.VEC2; + case 'VEC3': + return GLTFDataStructureType.VEC3; + case 'VEC4': + return GLTFDataStructureType.VEC4; + case 'MAT2': + return GLTFDataStructureType.MAT2; + case 'MAT3': + return GLTFDataStructureType.MAT3; + case 'MAT4': + return GLTFDataStructureType.MAT4; + default: + throw Error(`Unhandled glTF Type ${type}`); + } +}; + +const gltfDataStructureTypeNumComponents = (type: GLTFDataStructureType) => { + switch (type) { + case GLTFDataStructureType.SCALAR: + return 1; + case GLTFDataStructureType.VEC2: + return 2; + case GLTFDataStructureType.VEC3: + return 3; + case GLTFDataStructureType.VEC4: + case GLTFDataStructureType.MAT2: + return 4; + case GLTFDataStructureType.MAT3: + return 9; + case GLTFDataStructureType.MAT4: + return 16; + default: + throw Error(`Invalid glTF Type ${type}`); + } +}; + +// Note: only returns non-normalized type names, +// so byte/ubyte = sint8/uint8, not snorm8/unorm8, same for ushort +const gltfVertexType = ( + componentType: GLTFDataComponentType, + type: GLTFDataStructureType +) => { + let typeStr = null; + switch (componentType) { + case GLTFDataComponentType.BYTE: + typeStr = 'sint8'; + break; + case GLTFDataComponentType.UNSIGNED_BYTE: + typeStr = 'uint8'; + break; + case GLTFDataComponentType.SHORT: + typeStr = 'sint16'; + break; + case GLTFDataComponentType.UNSIGNED_SHORT: + typeStr = 'uint16'; + break; + case GLTFDataComponentType.INT: + typeStr = 'int32'; + break; + case GLTFDataComponentType.UNSIGNED_INT: + typeStr = 'uint32'; + break; + case GLTFDataComponentType.FLOAT: + typeStr = 'float32'; + break; + default: + throw Error(`Unrecognized or unsupported glTF type ${componentType}`); + } + + switch (gltfDataStructureTypeNumComponents(type)) { + case 1: + return typeStr; + case 2: + return typeStr + 'x2'; + case 3: + return typeStr + 'x3'; + case 4: + return typeStr + 'x4'; + // Vertex attributes should never be a matrix type, so we should not hit this + // unless we're passed an improperly created gltf file + default: + throw Error(`Invalid number of components for gltfType: ${type}`); + } +}; + +const gltfElementSize = ( + componentType: GLTFDataComponentType, + type: GLTFDataStructureType +) => { + let componentSize = 0; + switch (componentType) { + case GLTFDataComponentType.BYTE: + componentSize = 1; + break; + case GLTFDataComponentType.UNSIGNED_BYTE: + componentSize = 1; + break; + case GLTFDataComponentType.SHORT: + componentSize = 2; + break; + case GLTFDataComponentType.UNSIGNED_SHORT: + componentSize = 2; + break; + case GLTFDataComponentType.INT: + componentSize = 4; + break; + case GLTFDataComponentType.UNSIGNED_INT: + componentSize = 4; + break; + case GLTFDataComponentType.FLOAT: + componentSize = 4; + break; + case GLTFDataComponentType.DOUBLE: + componentSize = 8; + break; + default: + throw Error('Unrecognized GLTF Component Type?'); + } + return gltfDataStructureTypeNumComponents(type) * componentSize; +}; + +// Convert differently depending on if the shader is a vertex or compute shader +const convertGPUVertexFormatToWGSLFormat = (vertexFormat: GPUVertexFormat) => { + switch (vertexFormat) { + case 'float32': { + return 'f32'; + } + case 'float32x2': { + return 'vec2'; + } + case 'float32x3': { + return 'vec3'; + } + case 'float32x4': { + return 'vec4'; + } + case 'uint32': { + return 'u32'; + } + case 'uint32x2': { + return 'vec2'; + } + case 'uint32x3': { + return 'vec3'; + } + case 'uint32x4': { + return 'vec4'; + } + case 'uint8x2': { + return 'vec2'; + } + case 'uint8x4': { + return 'vec4'; + } + case 'uint16x4': { + return 'vec4'; + } + case 'uint16x2': { + return 'vec2'; + } + default: { + return 'f32'; + } + } +}; + +export class GLTFBuffer { + buffer: Uint8Array; + constructor(buffer: ArrayBuffer, offset: number, size: number) { + this.buffer = new Uint8Array(buffer, offset, size); + } +} + +export class GLTFBufferView { + byteLength: number; + byteStride: number; + view: Uint8Array; + needsUpload: boolean; + gpuBuffer: GPUBuffer; + usage: number; + constructor(buffer: GLTFBuffer, view: BufferView) { + this.byteLength = view['byteLength']; + this.byteStride = 0; + if (view['byteStride'] !== undefined) { + this.byteStride = view['byteStride']; + } + // Create the buffer view. Note that subarray creates a new typed + // view over the same array buffer, we do not make a copy here. + let viewOffset = 0; + if (view['byteOffset'] !== undefined) { + viewOffset = view['byteOffset']; + } + // NOTE: This creates a uint8array view into the buffer! + // When we call .buffer on this view, it will give us back the original array buffer + // Accordingly, when converting our buffer from a uint8array to a float32array representation + // we need to apply the byte offset of our view when creating our buffer + // ie new Float32Array(this.view.buffer, this.view.byteOffset, this.view.byteLength) + this.view = buffer.buffer.subarray( + viewOffset, + viewOffset + this.byteLength + ); + + this.needsUpload = false; + this.gpuBuffer = null; + this.usage = 0; + } + + addUsage(usage: number) { + this.usage = this.usage | usage; + } + + upload(device: GPUDevice) { + // Note: must align to 4 byte size when mapped at creation is true + const buf: GPUBuffer = device.createBuffer({ + size: alignTo(this.view.byteLength, 4), + usage: this.usage, + mappedAtCreation: true, + }); + new Uint8Array(buf.getMappedRange()).set(this.view); + buf.unmap(); + this.gpuBuffer = buf; + this.needsUpload = false; + } +} + +export class GLTFAccessor { + count: number; + componentType: GLTFDataComponentType; + structureType: GLTFDataStructureType; + view: GLTFBufferView; + byteOffset: number; + constructor(view: GLTFBufferView, accessor: Accessor) { + this.count = accessor['count']; + this.componentType = accessor['componentType']; + this.structureType = parseGltfDataStructureType(accessor['type']); + this.view = view; + this.byteOffset = 0; + if (accessor['byteOffset'] !== undefined) { + this.byteOffset = accessor['byteOffset']; + } + } + + get byteStride() { + const elementSize = gltfElementSize(this.componentType, this.structureType); + return Math.max(elementSize, this.view.byteStride); + } + + get byteLength() { + return this.count * this.byteStride; + } + + // Get the vertex attribute type for accessors that are used as vertex attributes + get vertexType() { + return gltfVertexType(this.componentType, this.structureType); + } +} + +interface AttributeMapInterface { + [key: string]: GLTFAccessor; +} + +export class GLTFPrimitive { + topology: GLTFRenderMode; + renderPipeline: GPURenderPipeline; + private attributeMap: AttributeMapInterface; + private attributes: string[] = []; + constructor( + topology: GLTFRenderMode, + attributeMap: AttributeMapInterface, + attributes: string[] + ) { + this.topology = topology; + this.renderPipeline = null; + // Maps attribute names to accessors + this.attributeMap = attributeMap; + this.attributes = attributes; + + for (const key in this.attributeMap) { + this.attributeMap[key].view.needsUpload = true; + if (key === 'INDICES') { + this.attributeMap['INDICES'].view.addUsage(GPUBufferUsage.INDEX); + continue; + } + this.attributeMap[key].view.addUsage(GPUBufferUsage.VERTEX); + } + } + + buildRenderPipeline( + device: GPUDevice, + vertexShader: string, + fragmentShader: string, + colorFormat: GPUTextureFormat, + depthFormat: GPUTextureFormat, + bgLayouts: GPUBindGroupLayout[], + label: string + ) { + // For now, just check if the attributeMap contains a given attribute using map.has(), and add it if it does + // POSITION, NORMAL, TEXCOORD_0, JOINTS_0, WEIGHTS_0 for order + // Vertex attribute state and shader stage + let VertexInputShaderString = `struct VertexInput {\n`; + const vertexBuffers: GPUVertexBufferLayout[] = this.attributes.map( + (attr, idx) => { + const vertexFormat: GPUVertexFormat = + this.attributeMap[attr].vertexType; + const attrString = attr.toLowerCase().replace(/_0$/, ''); + VertexInputShaderString += `\t@location(${idx}) ${attrString}: ${convertGPUVertexFormatToWGSLFormat( + vertexFormat + )},\n`; + return { + arrayStride: this.attributeMap[attr].byteStride, + attributes: [ + { + format: this.attributeMap[attr].vertexType, + offset: this.attributeMap[attr].byteOffset, + shaderLocation: idx, + }, + ], + } as GPUVertexBufferLayout; + } + ); + VertexInputShaderString += '}'; + + const vertexState: GPUVertexState = { + // Shader stage info + module: device.createShaderModule({ + code: VertexInputShaderString + vertexShader, + }), + entryPoint: 'vertexMain', + buffers: vertexBuffers, + }; + + const fragmentState: GPUFragmentState = { + // Shader info + module: device.createShaderModule({ + code: VertexInputShaderString + fragmentShader, + }), + entryPoint: 'fragmentMain', + // Output render target info + targets: [{ format: colorFormat }], + }; + + // Our loader only supports triangle lists and strips, so by default we set + // the primitive topology to triangle list, and check if it's instead a triangle strip + const primitive: GPUPrimitiveState = { topology: 'triangle-list' }; + if (this.topology == GLTFRenderMode.TRIANGLE_STRIP) { + primitive.topology = 'triangle-strip'; + primitive.stripIndexFormat = this.attributeMap['INDICES'].vertexType; + } + + const layout: GPUPipelineLayout = device.createPipelineLayout({ + bindGroupLayouts: bgLayouts, + label: `${label}.pipelineLayout`, + }); + + const rpDescript: GPURenderPipelineDescriptor = { + layout: layout, + label: `${label}.pipeline`, + vertex: vertexState, + fragment: fragmentState, + primitive: primitive, + depthStencil: { + format: depthFormat, + depthWriteEnabled: true, + depthCompare: 'less', + }, + }; + + this.renderPipeline = device.createRenderPipeline(rpDescript); + } + + render(renderPassEncoder: GPURenderPassEncoder, bindGroups: GPUBindGroup[]) { + renderPassEncoder.setPipeline(this.renderPipeline); + bindGroups.forEach((bg, idx) => { + renderPassEncoder.setBindGroup(idx, bg); + }); + + //if skin do something with bone bind group + this.attributes.map((attr, idx) => { + renderPassEncoder.setVertexBuffer( + idx, + this.attributeMap[attr].view.gpuBuffer, + this.attributeMap[attr].byteOffset, + this.attributeMap[attr].byteLength + ); + }); + + if (this.attributeMap['INDICES']) { + renderPassEncoder.setIndexBuffer( + this.attributeMap['INDICES'].view.gpuBuffer, + this.attributeMap['INDICES'].vertexType, + this.attributeMap['INDICES'].byteOffset, + this.attributeMap['INDICES'].byteLength + ); + renderPassEncoder.drawIndexed(this.attributeMap['INDICES'].count); + } else { + renderPassEncoder.draw(this.attributeMap['POSITION'].count); + } + } +} + +export class GLTFMesh { + name: string; + primitives: GLTFPrimitive[]; + constructor(name: string, primitives: GLTFPrimitive[]) { + this.name = name; + this.primitives = primitives; + } + + buildRenderPipeline( + device: GPUDevice, + vertexShader: string, + fragmentShader: string, + colorFormat: GPUTextureFormat, + depthFormat: GPUTextureFormat, + bgLayouts: GPUBindGroupLayout[] + ) { + // We take a pretty simple approach to start. Just loop through all the primitives and + // build their respective render pipelines + for (let i = 0; i < this.primitives.length; ++i) { + this.primitives[i].buildRenderPipeline( + device, + vertexShader, + fragmentShader, + colorFormat, + depthFormat, + bgLayouts, + `PrimitivePipeline${i}` + ); + } + } + + render(renderPassEncoder: GPURenderPassEncoder, bindGroups: GPUBindGroup[]) { + // We take a pretty simple approach to start. Just loop through all the primitives and + // call their individual draw methods + for (let i = 0; i < this.primitives.length; ++i) { + this.primitives[i].render(renderPassEncoder, bindGroups); + } + } +} + +export const validateGLBHeader = (header: DataView) => { + if (header.getUint32(0, true) != 0x46546c67) { + throw Error('Provided file is not a glB file'); + } + if (header.getUint32(4, true) != 2) { + throw Error('Provided file is glTF 2.0 file'); + } +}; + +export const validateBinaryHeader = (header: Uint32Array) => { + if (header[1] != 0x004e4942) { + throw Error( + 'Invalid glB: The second chunk of the glB file is not a binary chunk!' + ); + } +}; + +type TempReturn = { + meshes: GLTFMesh[]; + nodes: GLTFNode[]; + scenes: GLTFScene[]; + skins: GLTFSkin[]; +}; + +export class BaseTransformation { + position: Vec3; + rotation: Quat; + scale: Vec3; + constructor( + // Identity translation vec3 + position = [0, 0, 0], + // Identity quaternion + rotation = [0, 0, 0, 1], + // Identity scale vec3 + scale = [1, 1, 1] + ) { + this.position = position; + this.rotation = rotation; + this.scale = scale; + } + getMatrix(): Mat4 { + // Analagous to let transformationMatrix: mat4x4f = translation * rotation * scale; + const dst = mat4.identity(); + // Scale the transformation Matrix + mat4.scale(dst, this.scale, dst); + // Calculate the rotationMatrix from the quaternion + const rotationMatrix = mat4.fromQuat(this.rotation); + // Apply the rotation Matrix to the scaleMatrix (rotMat * scaleMat) + mat4.multiply(rotationMatrix, dst, dst); + // Translate the transformationMatrix + mat4.translate(dst, this.position, dst); + return dst; + } +} + +export class GLTFNode { + name: string; + source: BaseTransformation; + parent: GLTFNode | null; + children: GLTFNode[]; + // Transforms all node's children in the node's local space, with node itself acting as the origin + localMatrix: Mat4; + worldMatrix: Mat4; + // List of Meshes associated with this node + drawables: GLTFMesh[]; + test = 0; + skin?: GLTFSkin; + private nodeTransformGPUBuffer: GPUBuffer; + private nodeTransformBindGroup: GPUBindGroup; + + constructor( + device: GPUDevice, + bgLayout: GPUBindGroupLayout, + source: BaseTransformation, + name?: string, + skin?: GLTFSkin + ) { + this.name = name + ? name + : `node_${source.position} ${source.rotation} ${source.scale}`; + this.source = source; + this.parent = null; + this.children = []; + this.localMatrix = mat4.identity(); + this.worldMatrix = mat4.identity(); + this.drawables = []; + this.nodeTransformGPUBuffer = device.createBuffer({ + size: Float32Array.BYTES_PER_ELEMENT * 16, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + this.nodeTransformBindGroup = device.createBindGroup({ + layout: bgLayout, + entries: [ + { + binding: 0, + resource: { + buffer: this.nodeTransformGPUBuffer, + }, + }, + ], + }); + this.skin = skin; + } + + setParent(parent: GLTFNode) { + if (this.parent) { + this.parent.removeChild(this); + this.parent = null; + } + parent.addChild(this); + this.parent = parent; + } + + updateWorldMatrix(device: GPUDevice, parentWorldMatrix?: Mat4) { + // Get local transform of this particular node, and if the node has a parent, + // multiply it against the parent's transform matrix to get transformMatrix relative to world. + this.localMatrix = this.source.getMatrix(); + if (parentWorldMatrix) { + mat4.multiply(parentWorldMatrix, this.localMatrix, this.worldMatrix); + } else { + mat4.copy(this.localMatrix, this.worldMatrix); + } + const worldMatrix = this.worldMatrix as Float32Array; + device.queue.writeBuffer( + this.nodeTransformGPUBuffer, + 0, + worldMatrix.buffer, + worldMatrix.byteOffset, + worldMatrix.byteLength + ); + for (const child of this.children) { + child.updateWorldMatrix(device, worldMatrix); + } + } + + traverse(fn: (n: GLTFNode, ...args) => void) { + fn(this); + for (const child of this.children) { + child.traverse(fn); + } + } + + renderDrawables( + passEncoder: GPURenderPassEncoder, + bindGroups: GPUBindGroup[] + ) { + if (this.drawables !== undefined) { + for (const drawable of this.drawables) { + if (this.skin) { + drawable.render(passEncoder, [ + ...bindGroups, + this.nodeTransformBindGroup, + this.skin.skinBindGroup, + ]); + } else { + drawable.render(passEncoder, [ + ...bindGroups, + this.nodeTransformBindGroup, + ]); + } + } + } + // Render any of its children + for (const child of this.children) { + child.renderDrawables(passEncoder, bindGroups); + } + } + + private addChild(child: GLTFNode) { + this.children.push(child); + } + + private removeChild(child: GLTFNode) { + const ndx = this.children.indexOf(child); + this.children.splice(ndx, 1); + } +} + +export class GLTFScene { + nodes?: number[]; + name?: any; + extensions?: any; + extras?: any; + [k: string]: any; + root: GLTFNode; + + constructor( + device: GPUDevice, + nodeTransformBGL: GPUBindGroupLayout, + baseScene: Scene + ) { + this.nodes = baseScene.nodes; + this.name = baseScene.name; + this.root = new GLTFNode( + device, + nodeTransformBGL, + new BaseTransformation(), + baseScene.name + ); + } +} + +export class GLTFSkin { + // Nodes of the skin's joints + // [5, 2, 3] means our joint info is at nodes 5, 2, and 3 + joints: number[]; + // Bind Group for this skin's uniform buffer + skinBindGroup: GPUBindGroup; + // Static bindGroupLayout shared across all skins + // In a larger shader with more properties, certain bind groups + // would likely have to be combined due to device limitations in the number of bind groups + // allowed within a shader + // Inverse bind matrices parsed from the accessor + private inverseBindMatrices: Float32Array; + private jointMatricesUniformBuffer: GPUBuffer; + private inverseBindMatricesUniformBuffer: GPUBuffer; + static skinBindGroupLayout: GPUBindGroupLayout; + + static createSharedBindGroupLayout(device: GPUDevice) { + this.skinBindGroupLayout = device.createBindGroupLayout({ + label: 'StaticGLTFSkin.bindGroupLayout', + entries: [ + // Holds the initial joint matrices buffer + { + binding: 0, + buffer: { + type: 'read-only-storage', + }, + visibility: GPUShaderStage.VERTEX, + }, + // Holds the inverse bind matrices buffer + { + binding: 1, + buffer: { + type: 'read-only-storage', + }, + visibility: GPUShaderStage.VERTEX, + }, + ], + }); + } + + // For the sake of simplicity and easier debugging, we're going to convert our skin gpu accessor to a + // float32array, which should be performant enough for this example since there is only one skin (again, this) + // is not a comprehensive gltf parser + constructor( + device: GPUDevice, + inverseBindMatricesAccessor: GLTFAccessor, + joints: number[] + ) { + if ( + inverseBindMatricesAccessor.componentType !== + GLTFDataComponentType.FLOAT || + inverseBindMatricesAccessor.byteStride !== 64 + ) { + throw Error( + `This skin's provided accessor does not access a mat4x4 matrix, or does not access the provided mat4x4 data correctly` + ); + } + // NOTE: Come back to this uint8array to float32array conversion in case it is incorrect + this.inverseBindMatrices = new Float32Array( + inverseBindMatricesAccessor.view.view.buffer, + inverseBindMatricesAccessor.view.view.byteOffset, + inverseBindMatricesAccessor.view.view.byteLength / 4 + ); + this.joints = joints; + const skinGPUBufferUsage: GPUBufferDescriptor = { + size: Float32Array.BYTES_PER_ELEMENT * 16 * joints.length, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }; + this.jointMatricesUniformBuffer = device.createBuffer(skinGPUBufferUsage); + this.inverseBindMatricesUniformBuffer = + device.createBuffer(skinGPUBufferUsage); + device.queue.writeBuffer( + this.inverseBindMatricesUniformBuffer, + 0, + this.inverseBindMatrices + ); + this.skinBindGroup = device.createBindGroup({ + layout: GLTFSkin.skinBindGroupLayout, + label: 'StaticGLTFSkin.bindGroup', + entries: [ + { + binding: 0, + resource: { + buffer: this.jointMatricesUniformBuffer, + }, + }, + { + binding: 1, + resource: { + buffer: this.inverseBindMatricesUniformBuffer, + }, + }, + ], + }); + } + + update(device: GPUDevice, currentNodeIndex: number, nodes: GLTFNode[]) { + const globalWorldInverse = mat4.inverse( + nodes[currentNodeIndex].worldMatrix + ); + for (let j = 0; j < this.joints.length; j++) { + const joint = this.joints[j]; + const dstMatrix: Mat4 = mat4.identity(); + mat4.multiply(globalWorldInverse, nodes[joint].worldMatrix, dstMatrix); + const toWrite = dstMatrix as Float32Array; + device.queue.writeBuffer( + this.jointMatricesUniformBuffer, + j * 64, + toWrite.buffer, + toWrite.byteOffset, + toWrite.byteLength + ); + } + } +} + +// Upload a GLB model, parse its JSON and Binary components, and create the requisite GPU resources +// to render them. NOTE: Not extensible to all GLTF contexts at this point in time +export const convertGLBToJSONAndBinary = async ( + buffer: ArrayBuffer, + device: GPUDevice +): Promise => { + // Binary GLTF layout: https://cdn.willusher.io/webgpu-0-to-gltf/glb-layout.svg + const jsonHeader = new DataView(buffer, 0, 20); + validateGLBHeader(jsonHeader); + + // Length of the jsonChunk found at jsonHeader[12 - 15] + const jsonChunkLength = jsonHeader.getUint32(12, true); + + // Parse the JSON chunk of the glB file to a JSON object + const jsonChunk: GlTf = JSON.parse( + new TextDecoder('utf-8').decode(new Uint8Array(buffer, 20, jsonChunkLength)) + ); + + console.log(jsonChunk); + // Binary data located after jsonChunk + const binaryHeader = new Uint32Array(buffer, 20 + jsonChunkLength, 2); + validateBinaryHeader(binaryHeader); + + const binaryChunk = new GLTFBuffer( + buffer, + 28 + jsonChunkLength, + binaryHeader[0] + ); + + //Const populate missing properties of jsonChunk + for (const accessor of jsonChunk.accessors) { + accessor.byteOffset = accessor.byteOffset ?? 0; + accessor.normalized = accessor.normalized ?? false; + } + + for (const bufferView of jsonChunk.bufferViews) { + bufferView.byteOffset = bufferView.byteOffset ?? 0; + } + + if (jsonChunk.samplers) { + for (const sampler of jsonChunk.samplers) { + sampler.wrapS = sampler.wrapS ?? 10497; //GL.REPEAT + sampler.wrapT = sampler.wrapT ?? 10947; //GL.REPEAT + } + } + + //Mark each accessor with its intended usage within the vertexShader. + //Often necessary due to infrequencey with which the BufferView target field is populated. + for (const mesh of jsonChunk.meshes) { + for (const primitive of mesh.primitives) { + if ('indices' in primitive) { + const accessor = jsonChunk.accessors[primitive.indices]; + jsonChunk.accessors[primitive.indices].bufferViewUsage |= + GPUBufferUsage.INDEX; + jsonChunk.bufferViews[accessor.bufferView].usage |= + GPUBufferUsage.INDEX; + } + for (const attribute of Object.values(primitive.attributes)) { + const accessor = jsonChunk.accessors[attribute]; + jsonChunk.accessors[attribute].bufferViewUsage |= GPUBufferUsage.VERTEX; + jsonChunk.bufferViews[accessor.bufferView].usage |= + GPUBufferUsage.VERTEX; + } + } + } + + // Create GLTFBufferView objects for all the buffer views in the glTF file + const bufferViews: GLTFBufferView[] = []; + for (let i = 0; i < jsonChunk.bufferViews.length; ++i) { + bufferViews.push(new GLTFBufferView(binaryChunk, jsonChunk.bufferViews[i])); + } + + const accessors: GLTFAccessor[] = []; + for (let i = 0; i < jsonChunk.accessors.length; ++i) { + const accessorInfo = jsonChunk.accessors[i]; + const viewID = accessorInfo['bufferView']; + accessors.push(new GLTFAccessor(bufferViews[viewID], accessorInfo)); + } + // Load the first mesh + const meshes: GLTFMesh[] = []; + for (let i = 0; i < jsonChunk.meshes.length; i++) { + const mesh = jsonChunk.meshes[i]; + const meshPrimitives: GLTFPrimitive[] = []; + for (let j = 0; j < mesh.primitives.length; ++j) { + const prim = mesh.primitives[j]; + let topology = prim['mode']; + // Default is triangles if mode specified + if (topology === undefined) { + topology = GLTFRenderMode.TRIANGLES; + } + if ( + topology != GLTFRenderMode.TRIANGLES && + topology != GLTFRenderMode.TRIANGLE_STRIP + ) { + throw Error(`Unsupported primitive mode ${prim['mode']}`); + } + + const primitiveAttributeMap = {}; + const attributes = []; + if (jsonChunk['accessors'][prim['indices']] !== undefined) { + const indices = accessors[prim['indices']]; + primitiveAttributeMap['INDICES'] = indices; + } + + // Loop through all the attributes and store within our attributeMap + for (const attr in prim['attributes']) { + const accessor = accessors[prim['attributes'][attr]]; + primitiveAttributeMap[attr] = accessor; + if (accessor.structureType > 3) { + throw Error( + 'Vertex attribute accessor accessed an unsupported data type for vertex attribute' + ); + } + attributes.push(attr); + } + meshPrimitives.push( + new GLTFPrimitive(topology, primitiveAttributeMap, attributes) + ); + } + meshes.push(new GLTFMesh(mesh.name, meshPrimitives)); + } + + const skins: GLTFSkin[] = []; + for (const skin of jsonChunk.skins) { + const inverseBindMatrixAccessor = accessors[skin.inverseBindMatrices]; + inverseBindMatrixAccessor.view.addUsage( + GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST + ); + inverseBindMatrixAccessor.view.needsUpload = true; + } + + // Upload the buffer views used by mesh + for (let i = 0; i < bufferViews.length; ++i) { + if (bufferViews[i].needsUpload) { + bufferViews[i].upload(device); + } + } + + GLTFSkin.createSharedBindGroupLayout(device); + for (const skin of jsonChunk.skins) { + const inverseBindMatrixAccessor = accessors[skin.inverseBindMatrices]; + const joints = skin.joints; + skins.push(new GLTFSkin(device, inverseBindMatrixAccessor, joints)); + } + + const nodes: GLTFNode[] = []; + + // Access each node. If node references a mesh, add mesh to that node + const nodeUniformsBindGroupLayout = device.createBindGroupLayout({ + label: 'NodeUniforms.bindGroupLayout', + entries: [ + { + binding: 0, + buffer: { + type: 'uniform', + }, + visibility: GPUShaderStage.VERTEX, + }, + ], + }); + for (const currNode of jsonChunk.nodes) { + const baseTransformation = new BaseTransformation( + currNode.translation, + currNode.rotation, + currNode.scale + ); + const nodeToCreate = new GLTFNode( + device, + nodeUniformsBindGroupLayout, + baseTransformation, + currNode.name, + skins[currNode.skin] + ); + const meshToAdd = meshes[currNode.mesh]; + if (meshToAdd) { + nodeToCreate.drawables.push(meshToAdd); + } + nodes.push(nodeToCreate); + } + + // Assign each node its children + nodes.forEach((node, idx) => { + const children = jsonChunk.nodes[idx].children; + if (children) { + children.forEach((childIdx) => { + const child = nodes[childIdx]; + child.setParent(node); + }); + } + }); + + const scenes: GLTFScene[] = []; + + for (const jsonScene of jsonChunk.scenes) { + const scene = new GLTFScene(device, nodeUniformsBindGroupLayout, jsonScene); + const sceneChildren = scene.nodes; + sceneChildren.forEach((childIdx) => { + const child = nodes[childIdx]; + child.setParent(scene.root); + }); + scenes.push(scene); + } + return { + meshes, + nodes, + scenes, + skins, + }; +}; diff --git a/src/sample/skinnedMesh/gltf.ts b/src/sample/skinnedMesh/gltf.ts new file mode 100644 index 00000000..e5cd3d52 --- /dev/null +++ b/src/sample/skinnedMesh/gltf.ts @@ -0,0 +1,224 @@ +import { Mat4 } from 'wgpu-matrix'; +import { GLTFNode } from './glbUtils'; + +/* Sourced from https://github.com/bwasty/gltf-loader-ts/blob/master/source/gltf.ts */ +/* License for use can be found here: https://github.com/bwasty/gltf-loader-ts/blob/master/LICENSE */ +/* Comments and types have been excluded from original source for sake of cleanliness and brevity */ +export type GlTfId = number; + +export interface AccessorSparseIndices { + bufferView: GlTfId; + byteOffset?: number; + componentType: 5121 | 5123 | 5125 | number; +} + +export interface AccessorSparseValues { + bufferView: GlTfId; + byteOffset?: number; +} + +export interface AccessorSparse { + count: number; + indices: AccessorSparseIndices; + values: AccessorSparseValues; +} + +export interface Accessor { + bufferView?: GlTfId; + bufferViewUsage?: 34962 | 34963 | number; + byteOffset?: number; + componentType: 5120 | 5121 | 5122 | 5123 | 5125 | 5126 | number; + normalized?: boolean; + count: number; + type: 'SCALAR' | 'VEC2' | 'VEC3' | 'VEC4' | 'MAT2' | 'MAT3' | 'MAT4' | string; + max?: number[]; + min?: number[]; + sparse?: AccessorSparse; + name?: string; +} + +export interface AnimationChannelTarget { + node?: GlTfId; + path: 'translation' | 'rotation' | 'scale' | 'weights' | string; +} + +export interface AnimationChannel { + sampler: GlTfId; + target: AnimationChannelTarget; +} + +export interface AnimationSampler { + input: GlTfId; + interpolation?: 'LINEAR' | 'STEP' | 'CUBICSPLINE' | string; + output: GlTfId; +} + +export interface Animation { + channels: AnimationChannel[]; + samplers: AnimationSampler[]; + name?: string; +} + +export interface Asset { + copyright?: string; + generator?: string; + version: string; + minVersion?: string; +} + +export interface Buffer { + uri?: string; + byteLength: number; + name?: string; +} + +export interface BufferView { + buffer: GlTfId; + byteOffset?: number; + byteLength: number; + byteStride?: number; + target?: 34962 | 34963 | number; + name?: string; + usage?: number; +} + +export interface CameraOrthographic { + xmag: number; + ymag: number; + zfar: number; + znear: number; +} + +export interface CameraPerspective { + aspectRatio?: number; + yfov: number; + zfar?: number; + znear: number; +} + +export interface Camera { + orthographic?: CameraOrthographic; + perspective?: CameraPerspective; + type: 'perspective' | 'orthographic' | string; + name?: any; +} + +export interface Image { + uri?: string; + mimeType?: 'image/jpeg' | 'image/png' | string; + bufferView?: GlTfId; + name?: string; +} + +export interface TextureInfo { + index: GlTfId; + texCoord?: number; +} + +export interface MaterialPbrMetallicRoughness { + baseColorFactor?: number[]; + baseColorTexture?: TextureInfo; + metallicFactor?: number; + roughnessFactor?: number; + metallicRoughnessTexture?: TextureInfo; +} +export interface MaterialNormalTextureInfo { + index?: any; + texCoord?: any; + scale?: number; +} +export interface MaterialOcclusionTextureInfo { + index?: any; + texCoord?: any; + strength?: number; +} + +export interface Material { + name?: string; + pbrMetallicRoughness?: MaterialPbrMetallicRoughness; + normalTexture?: MaterialNormalTextureInfo; + occlusionTexture?: MaterialOcclusionTextureInfo; + emissiveTexture?: TextureInfo; + emissiveFactor?: number[]; + alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND' | string; + alphaCutoff?: number; + doubleSided?: boolean; +} + +export interface MeshPrimitive { + attributes: { + [k: string]: GlTfId; + }; + indices?: GlTfId; + material?: GlTfId; + mode?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | number; + targets?: { + [k: string]: GlTfId; + }[]; +} + +export interface Mesh { + primitives: MeshPrimitive[]; + weights?: number[]; + name?: string; +} + +export interface Node { + camera?: GlTfId; + children?: GlTfId[]; + skin?: GlTfId; + matrix?: number[]; + worldTransformationMatrix?: Mat4; + mesh?: GlTfId; + rotation?: number[]; + scale?: number[]; + translation?: number[]; + weights?: number[]; + name?: string; +} + +export interface Sampler { + magFilter?: 9728 | 9729 | number; + minFilter?: 9728 | 9729 | 9984 | 9985 | 9986 | 9987 | number; + wrapS?: 33071 | 33648 | 10497 | number; + wrapT?: 33071 | 33648 | 10497 | number; + name?: string; +} + +export interface Scene { + nodes?: GlTfId[]; + name?: any; + root?: GLTFNode; +} +export interface Skin { + inverseBindMatrices?: GlTfId; + skeleton?: GlTfId; + joints: GlTfId[]; + name?: string; +} + +export interface Texture { + sampler?: GlTfId; + source?: GlTfId; + name?: string; +} + +export interface GlTf { + extensionsUsed?: string[]; + extensionsRequired?: string[]; + accessors?: Accessor[]; + animations?: Animation[]; + asset: Asset; + buffers?: Buffer[]; + bufferViews?: BufferView[]; + cameras?: Camera[]; + images?: Image[]; + materials?: Material[]; + meshes?: Mesh[]; + nodes?: Node[]; + samplers?: Sampler[]; + scene?: GlTfId; + scenes?: Scene[]; + skins?: Skin[]; + textures?: Texture[]; +} diff --git a/src/sample/skinnedMesh/gltf.wgsl b/src/sample/skinnedMesh/gltf.wgsl new file mode 100644 index 00000000..0310a2a3 --- /dev/null +++ b/src/sample/skinnedMesh/gltf.wgsl @@ -0,0 +1,80 @@ +// Whale.glb Vertex attributes +// Read in VertexInput from attributes +// f32x3 f32x3 f32x2 u8x4 f32x4 +struct VertexOutput { + @builtin(position) Position: vec4, + @location(0) normal: vec3, + @location(1) joints: vec4, + @location(2) weights: vec4, +} + +struct CameraUniforms { + proj_matrix: mat4x4f, + view_matrix: mat4x4f, + model_matrix: mat4x4f, +} + +struct GeneralUniforms { + render_mode: u32, + skin_mode: u32, +} + +struct NodeUniforms { + world_matrix: mat4x4f, +} + +@group(0) @binding(0) var camera_uniforms: CameraUniforms; +@group(1) @binding(0) var general_uniforms: GeneralUniforms; +@group(2) @binding(0) var node_uniforms: NodeUniforms; +@group(3) @binding(0) var joint_matrices: array>; +@group(3) @binding(1) var inverse_bind_matrices: array>; + +@vertex +fn vertexMain(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + // Compute joint_matrices * inverse_bind_matrices + let joint0 = joint_matrices[input.joints[0]] * inverse_bind_matrices[input.joints[0]]; + let joint1 = joint_matrices[input.joints[1]] * inverse_bind_matrices[input.joints[1]]; + let joint2 = joint_matrices[input.joints[2]] * inverse_bind_matrices[input.joints[2]]; + let joint3 = joint_matrices[input.joints[3]] * inverse_bind_matrices[input.joints[3]]; + // Compute influence of joint based on weight + let skin_matrix = + joint0 * input.weights[0] + + joint1 * input.weights[1] + + joint2 * input.weights[2] + + joint3 * input.weights[3]; + // Position of the vertex relative to our world + let world_position = vec4(input.position.x, input.position.y, input.position.z, 1.0); + // Vertex position with model rotation, skinning, and the mesh's node transformation applied. + let skinned_position = camera_uniforms.model_matrix * skin_matrix * node_uniforms.world_matrix * world_position; + // Vertex position with only the model rotation applied. + let rotated_position = camera_uniforms.model_matrix * world_position; + // Determine which position to used based on whether skinMode is turnd on or off. + let transformed_position = select( + rotated_position, + skinned_position, + general_uniforms.skin_mode == 0 + ); + // Apply the camera and projection matrix transformations to our transformed position; + output.Position = camera_uniforms.proj_matrix * camera_uniforms.view_matrix * transformed_position; + output.normal = input.normal; + // Convert u32 joint data to f32s to prevent flat interpolation error. + output.joints = vec4(f32(input.joints[0]), f32(input.joints[1]), f32(input.joints[2]), f32(input.joints[3])); + output.weights = input.weights; + return output; +} + +@fragment +fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { + switch general_uniforms.render_mode { + case 1: { + return input.joints; + } + case 2: { + return input.weights; + } + default: { + return vec4(input.normal, 1.0); + } + } +} \ No newline at end of file diff --git a/src/sample/skinnedMesh/grid.wgsl b/src/sample/skinnedMesh/grid.wgsl new file mode 100644 index 00000000..cdfc9cac --- /dev/null +++ b/src/sample/skinnedMesh/grid.wgsl @@ -0,0 +1,74 @@ +struct VertexInput { + @location(0) vert_pos: vec2, + @location(1) joints: vec4, + @location(2) weights: vec4 +} + +struct VertexOutput { + @builtin(position) Position: vec4, + @location(0) world_pos: vec3, + @location(1) joints: vec4, + @location(2) weights: vec4, +} + +struct CameraUniforms { + projMatrix: mat4x4f, + viewMatrix: mat4x4f, + modelMatrix: mat4x4f, +} + +struct GeneralUniforms { + render_mode: u32, + skin_mode: u32, +} + +@group(0) @binding(0) var camera_uniforms: CameraUniforms; +@group(1) @binding(0) var general_uniforms: GeneralUniforms; +@group(2) @binding(0) var joint_matrices: array>; +@group(2) @binding(1) var inverse_bind_matrices: array>; + +@vertex +fn vertexMain(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var bones = vec4(0.0, 0.0, 0.0, 0.0); + let position = vec4(input.vert_pos.x, input.vert_pos.y, 0.0, 1.0); + // Get relevant 4 bone matrices + let joint0 = joint_matrices[input.joints[0]] * inverse_bind_matrices[input.joints[0]]; + let joint1 = joint_matrices[input.joints[1]] * inverse_bind_matrices[input.joints[1]]; + let joint2 = joint_matrices[input.joints[2]] * inverse_bind_matrices[input.joints[2]]; + let joint3 = joint_matrices[input.joints[3]] * inverse_bind_matrices[input.joints[3]]; + // Compute influence of joint based on weight + let skin_matrix = + joint0 * input.weights[0] + + joint1 * input.weights[1] + + joint2 * input.weights[2] + + joint3 * input.weights[3]; + // Bone transformed mesh + output.Position = select( + camera_uniforms.projMatrix * camera_uniforms.viewMatrix * camera_uniforms.modelMatrix * position, + camera_uniforms.projMatrix * camera_uniforms.viewMatrix * camera_uniforms.modelMatrix * skin_matrix * position, + general_uniforms.skin_mode == 0 + ); + + //Get unadjusted world coordinates + output.world_pos = position.xyz; + output.joints = vec4(f32(input.joints.x), f32(input.joints.y), f32(input.joints.z), f32(input.joints.w)); + output.weights = input.weights; + return output; +} + + +@fragment +fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { + switch general_uniforms.render_mode { + case 1: { + return input.joints; + } + case 2: { + return input.weights; + } + default: { + return vec4(255.0, 0.0, 1.0, 1.0); + } + } +} \ No newline at end of file diff --git a/src/sample/skinnedMesh/gridData.ts b/src/sample/skinnedMesh/gridData.ts new file mode 100644 index 00000000..6cb7c144 --- /dev/null +++ b/src/sample/skinnedMesh/gridData.ts @@ -0,0 +1,106 @@ +/* eslint-disable prettier/prettier */ +export const gridVertices = new Float32Array([ + // B0 + 0, 1, // 0 + 0, -1, // 1 + // CONNECTOR + 2, 1, // 2 + 2, -1, // 3 + // B1 + 4, 1, // 4 + 4, -1, // 5 + // CONNECTOR + 6, 1, // 6 + 6, -1, // 7 + // B2 + 8, 1, // 8 + 8, -1, // 9, + // CONNECTOR + 10, 1, //10 + 10, -1, //11 + // B3 + 12, 1, //12 + 12, -1, //13 +]); + +// Representing the indice of four bones that can influence each vertex +export const gridJoints = new Uint32Array([ + 0, 0, 0, 0, // Vertex 0 is influenced by bone 0 + 0, 0, 0, 0, // 1 + 0, 1, 0, 0, // 2 + 0, 1, 0, 0, // 3 + 1, 0, 0, 0, // 4 + 1, 0, 0, 0, // 5 + 1, 2, 0, 0, // Vertex 6 is influenced by bone 1 and bone 2 + 1, 2, 0, 0, // 7 + 2, 0, 0, 0, // 8 + 2, 0, 0, 0, // 9 + 1, 2, 3, 0, //10 + 1, 2, 3, 0, //11 + 2, 3, 0, 0, //12 + 2, 3, 0, 0, //13 +]) + +// The weights applied when ve +export const gridWeights = new Float32Array([ + // B0 + 1, 0, 0, 0, // 0 + 1, 0, 0, 0, // 1 + // CONNECTOR + .5,.5, 0, 0, // 2 + .5,.5, 0, 0, // 3 + // B1 + 1, 0, 0, 0, // 4 + 1, 0, 0, 0, // 5 + // CONNECTOR + .5,.5, 0, 0, // 6 + .5,.5, 0, 0, // 7 + // B2 + 1, 0, 0, 0, // 8 + 1, 0, 0, 0, // 9 + // CONNECTOR + .5,.5, 0, 0, // 10 + .5,.5, 0, 0, // 11 + // B3 + 1, 0, 0, 0, // 12 + 1, 0, 0, 0, // 13 +]); + +// Using data above... +// Vertex 0 is influenced by bone 0 with a weight of 1 +// Vertex 1 is influenced by bone 1 with a weight of 1 +// Vertex 2 is influenced by bone 0 and 1 with a weight of 0.5 each +// and so on.. +// Although a vertex can hypothetically be influenced by 4 bones, +// in this example, we stick to each vertex being infleunced by only two +// although there can be downstream effects of parent bones influencing child bones +// that influence their own children + +export const gridIndices = new Uint16Array([ + // B0 + 0, 1, + 0, 2, + 1, 3, + // CONNECTOR + 2, 3, // + 2, 4, + 3, 5, + // B1 + 4, 5, + 4, 6, + 5, 7, + // CONNECTOR + 6, 7, + 6, 8, + 7, 9, + // B2 + 8, 9, + 8, 10, + 9, 11, + // CONNECTOR + 10, 11, + 10, 12, + 11, 13, + // B3 + 12, 13, +]); \ No newline at end of file diff --git a/src/sample/skinnedMesh/gridUtils.ts b/src/sample/skinnedMesh/gridUtils.ts new file mode 100644 index 00000000..4d42af41 --- /dev/null +++ b/src/sample/skinnedMesh/gridUtils.ts @@ -0,0 +1,114 @@ +import { gridVertices, gridIndices, gridJoints, gridWeights } from './gridData'; + +// Uses constant grid data to create appropriately sized GPU Buffers for our skinned grid +export const createSkinnedGridBuffers = (device: GPUDevice) => { + // Utility function that creates GPUBuffers from data + const createBuffer = ( + data: Float32Array | Uint32Array, + type: 'f32' | 'u32' + ) => { + const buffer = device.createBuffer({ + size: data.byteLength, + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, + }); + if (type === 'f32') { + new Float32Array(buffer.getMappedRange()).set(data); + } else { + new Uint32Array(buffer.getMappedRange()).set(data); + } + buffer.unmap(); + return buffer; + }; + const positionsBuffer = createBuffer(gridVertices, 'f32'); + const jointsBuffer = createBuffer(gridJoints, 'u32'); + const weightsBuffer = createBuffer(gridWeights, 'f32'); + const indicesBuffer = device.createBuffer({ + size: Uint16Array.BYTES_PER_ELEMENT * gridIndices.length, + usage: GPUBufferUsage.INDEX, + mappedAtCreation: true, + }); + new Uint16Array(indicesBuffer.getMappedRange()).set(gridIndices); + indicesBuffer.unmap(); + + return { + positions: positionsBuffer, + joints: jointsBuffer, + weights: weightsBuffer, + indices: indicesBuffer, + }; +}; + +export const createSkinnedGridRenderPipeline = ( + device: GPUDevice, + presentationFormat: GPUTextureFormat, + vertexShader: string, + fragmentShader: string, + bgLayouts: GPUBindGroupLayout[] +) => { + const pipeline = device.createRenderPipeline({ + label: 'SkinnedGridRenderer', + layout: device.createPipelineLayout({ + label: `SkinnedGridRenderer.pipelineLayout`, + bindGroupLayouts: bgLayouts, + }), + vertex: { + module: device.createShaderModule({ + label: `SkinnedGridRenderer.vertexShader`, + code: vertexShader, + }), + entryPoint: 'vertexMain', + buffers: [ + // Vertex Positions (positions) + { + arrayStride: Float32Array.BYTES_PER_ELEMENT * 2, + attributes: [ + { + format: 'float32x2', + offset: 0, + shaderLocation: 0, + }, + ], + }, + // Bone Indices (joints) + { + arrayStride: Uint32Array.BYTES_PER_ELEMENT * 4, + attributes: [ + { + format: 'uint32x4', + offset: 0, + shaderLocation: 1, + }, + ], + }, + // Bone Weights (weights) + { + arrayStride: Float32Array.BYTES_PER_ELEMENT * 4, + attributes: [ + { + format: 'float32x4', + offset: 0, + shaderLocation: 2, + }, + ], + }, + ], + }, + fragment: { + module: device.createShaderModule({ + label: `SkinnedGridRenderer.fragmentShader`, + code: fragmentShader, + }), + entryPoint: 'fragmentMain', + targets: [ + { + format: presentationFormat, + }, + ], + }, + primitive: { + topology: 'line-list', + }, + }); + return pipeline; +}; diff --git a/src/sample/skinnedMesh/main.ts b/src/sample/skinnedMesh/main.ts new file mode 100644 index 00000000..1bcb98e7 --- /dev/null +++ b/src/sample/skinnedMesh/main.ts @@ -0,0 +1,605 @@ +import { makeSample, SampleInit } from '../../components/SampleLayout'; +import { convertGLBToJSONAndBinary, GLTFSkin } from './glbUtils'; +import gltfWGSL from './gltf.wgsl'; +import gridWGSL from './grid.wgsl'; +import { Mat4, mat4, Quat, vec3 } from 'wgpu-matrix'; +import { createBindGroupCluster } from '../bitonicSort/utils'; +import { + createSkinnedGridBuffers, + createSkinnedGridRenderPipeline, +} from './gridUtils'; +import { gridIndices } from './gridData'; + +const MAT4X4_BYTES = 64; + +interface BoneObject { + transforms: Mat4[]; + bindPoses: Mat4[]; + bindPosesInv: Mat4[]; +} + +enum RenderMode { + NORMAL, + JOINTS, + WEIGHTS, +} + +enum SkinMode { + ON, + OFF, +} + +// Copied from toji/gl-matrix +const getRotation = (mat: Mat4): Quat => { + // Initialize our output quaternion + const out = [0, 0, 0, 0]; + // Extract the scaling factor from the final matrix transformation + // to normalize our rotation; + const scaling = mat4.getScaling(mat); + const is1 = 1 / scaling[0]; + const is2 = 1 / scaling[1]; + const is3 = 1 / scaling[2]; + + // Scale the matrix elements by the scaling factors + const sm11 = mat[0] * is1; + const sm12 = mat[1] * is2; + const sm13 = mat[2] * is3; + const sm21 = mat[4] * is1; + const sm22 = mat[5] * is2; + const sm23 = mat[6] * is3; + const sm31 = mat[8] * is1; + const sm32 = mat[9] * is2; + const sm33 = mat[10] * is3; + + // The trace of a square matrix is the sum of its diagonal entries + // While the matrix trace has many interesting mathematical properties, + // the primary purpose of the trace is to assess the characteristics of the rotation. + const trace = sm11 + sm22 + sm33; + let S = 0; + + // If all matrix elements contribute equally to the rotation. + if (trace > 0) { + S = Math.sqrt(trace + 1.0) * 2; + out[3] = 0.25 * S; + out[0] = (sm23 - sm32) / S; + out[1] = (sm31 - sm13) / S; + out[2] = (sm12 - sm21) / S; + // If the rotation is primarily around the x-axis + } else if (sm11 > sm22 && sm11 > sm33) { + S = Math.sqrt(1.0 + sm11 - sm22 - sm33) * 2; + out[3] = (sm23 - sm32) / S; + out[0] = 0.25 * S; + out[1] = (sm12 + sm21) / S; + out[2] = (sm31 + sm13) / S; + // If rotation is primarily around the y-axis + } else if (sm22 > sm33) { + S = Math.sqrt(1.0 + sm22 - sm11 - sm33) * 2; + out[3] = (sm31 - sm13) / S; + out[0] = (sm12 + sm21) / S; + out[1] = 0.25 * S; + out[2] = (sm23 + sm32) / S; + // If the rotation is primarily around the z-axis + } else { + S = Math.sqrt(1.0 + sm33 - sm11 - sm22) * 2; + out[3] = (sm12 - sm21) / S; + out[0] = (sm31 + sm13) / S; + out[1] = (sm23 + sm32) / S; + out[2] = 0.25 * S; + } + + return out; +}; + +const init: SampleInit = async ({ canvas, pageState, gui }) => { + //Normal setup + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + + if (!pageState.active) return; + const context = canvas.getContext('webgpu') as GPUCanvasContext; + + const devicePixelRatio = window.devicePixelRatio || 1; + canvas.width = canvas.clientWidth * devicePixelRatio; + canvas.height = canvas.clientHeight * devicePixelRatio; + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + + context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', + }); + + const settings = { + cameraX: 0, + cameraY: -5.1, + cameraZ: -14.6, + objectScale: 1, + angle: 0.2, + speed: 50, + object: 'Whale', + renderMode: 'NORMAL', + skinMode: 'ON', + }; + + // Determine whether we want to render our whale or our skinned grid + gui.add(settings, 'object', ['Whale', 'Skinned Grid']).onChange(() => { + if (settings.object === 'Skinned Grid') { + settings.cameraX = -10; + settings.cameraY = 0; + settings.objectScale = 1.27; + } else { + if (settings.skinMode === 'OFF') { + settings.cameraX = 0; + settings.cameraY = 0; + settings.cameraZ = -11; + } else { + settings.cameraX = 0; + settings.cameraY = -5.1; + settings.cameraZ = -14.6; + } + } + }); + + // Output the mesh normals, its joints, or the weights that influence the movement of the joints + gui + .add(settings, 'renderMode', ['NORMAL', 'JOINTS', 'WEIGHTS']) + .onChange(() => { + device.queue.writeBuffer( + generalUniformsBuffer, + 0, + new Uint32Array([RenderMode[settings.renderMode]]) + ); + }); + // Determine whether the mesh is static or whether skinning is activated + gui.add(settings, 'skinMode', ['ON', 'OFF']).onChange(() => { + if (settings.object === 'Whale') { + if (settings.skinMode === 'OFF') { + settings.cameraX = 0; + settings.cameraY = 0; + settings.cameraZ = -11; + } else { + settings.cameraX = 0; + settings.cameraY = -5.1; + settings.cameraZ = -14.6; + } + } + device.queue.writeBuffer( + generalUniformsBuffer, + 4, + new Uint32Array([SkinMode[settings.skinMode]]) + ); + }); + const animFolder = gui.addFolder('Animation Settings'); + animFolder.add(settings, 'angle', 0.05, 0.5).step(0.05); + animFolder.add(settings, 'speed', 10, 100).step(10); + + const depthTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const cameraBuffer = device.createBuffer({ + size: MAT4X4_BYTES * 3, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const cameraBGCluster = createBindGroupCluster( + [0], + [GPUShaderStage.VERTEX], + ['buffer'], + [{ type: 'uniform' }], + [[{ buffer: cameraBuffer }]], + 'Camera', + device + ); + + const generalUniformsBuffer = device.createBuffer({ + size: Uint32Array.BYTES_PER_ELEMENT * 2, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const generalUniformsBGCLuster = createBindGroupCluster( + [0], + [GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT], + ['buffer'], + [{ type: 'uniform' }], + [[{ buffer: generalUniformsBuffer }]], + 'General', + device + ); + + // Same bindGroupLayout as in main file. + const nodeUniformsBindGroupLayout = device.createBindGroupLayout({ + label: 'NodeUniforms.bindGroupLayout', + entries: [ + { + binding: 0, + buffer: { + type: 'uniform', + }, + visibility: GPUShaderStage.VERTEX, + }, + ], + }); + + // Fetch whale resources from the glb file + const whaleScene = await fetch('../assets/gltf/whale.glb') + .then((res) => res.arrayBuffer()) + .then((buffer) => convertGLBToJSONAndBinary(buffer, device)); + + // Builds a render pipeline for our whale mesh + // Since we are building a lightweight gltf parser around a gltf scene with a known + // quantity of meshes, we only build a renderPipeline for the singular mesh present + // within our scene. A more robust gltf parser would loop through all the meshes, + // cache replicated pipelines, and perform other optimizations. + whaleScene.meshes[0].buildRenderPipeline( + device, + gltfWGSL, + gltfWGSL, + presentationFormat, + depthTexture.format, + [ + cameraBGCluster.bindGroupLayout, + generalUniformsBGCLuster.bindGroupLayout, + nodeUniformsBindGroupLayout, + GLTFSkin.skinBindGroupLayout, + ] + ); + + // Create skinned grid resources + const skinnedGridVertexBuffers = createSkinnedGridBuffers(device); + // Buffer for our uniforms, joints, and inverse bind matrices + const skinnedGridUniformBufferUsage: GPUBufferDescriptor = { + // 5 4x4 matrices, one for each bone + size: MAT4X4_BYTES * 5, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }; + const skinnedGridJointUniformBuffer = device.createBuffer( + skinnedGridUniformBufferUsage + ); + const skinnedGridInverseBindUniformBuffer = device.createBuffer( + skinnedGridUniformBufferUsage + ); + const skinnedGridBoneBGCluster = createBindGroupCluster( + [0, 1], + [GPUShaderStage.VERTEX, GPUShaderStage.VERTEX], + ['buffer', 'buffer'], + [{ type: 'read-only-storage' }, { type: 'read-only-storage' }], + [ + [ + { buffer: skinnedGridJointUniformBuffer }, + { buffer: skinnedGridInverseBindUniformBuffer }, + ], + ], + 'SkinnedGridJointUniforms', + device + ); + const skinnedGridPipeline = createSkinnedGridRenderPipeline( + device, + presentationFormat, + gridWGSL, + gridWGSL, + [ + cameraBGCluster.bindGroupLayout, + generalUniformsBGCLuster.bindGroupLayout, + skinnedGridBoneBGCluster.bindGroupLayout, + ] + ); + + // Global Calc + const aspect = canvas.width / canvas.height; + const perspectiveProjection = mat4.perspective( + (2 * Math.PI) / 5, + aspect, + 0.1, + 100.0 + ); + + const orthographicProjection = mat4.ortho(-20, 20, -10, 10, -100, 100); + + function getProjectionMatrix() { + if (settings.object !== 'Skinned Grid') { + return perspectiveProjection as Float32Array; + } + return orthographicProjection as Float32Array; + } + + function getViewMatrix() { + const viewMatrix = mat4.identity(); + if (settings.object === 'Skinned Grid') { + mat4.translate( + viewMatrix, + vec3.fromValues( + settings.cameraX * settings.objectScale, + settings.cameraY * settings.objectScale, + settings.cameraZ + ), + viewMatrix + ); + } else { + mat4.translate( + viewMatrix, + vec3.fromValues(settings.cameraX, settings.cameraY, settings.cameraZ), + viewMatrix + ); + } + return viewMatrix as Float32Array; + } + + function getModelMatrix() { + const modelMatrix = mat4.identity(); + const scaleVector = vec3.fromValues( + settings.objectScale, + settings.objectScale, + settings.objectScale + ); + mat4.scale(modelMatrix, scaleVector, modelMatrix); + if (settings.object === 'Whale') { + mat4.rotateY(modelMatrix, (Date.now() / 1000) * 0.5, modelMatrix); + } + return modelMatrix as Float32Array; + } + + // Pass Descriptor for GLTFs + const gltfRenderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: undefined, // Assigned later + + clearValue: { r: 0.3, g: 0.3, b: 0.3, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + depthLoadOp: 'clear', + depthClearValue: 1.0, + depthStoreOp: 'store', + }, + }; + + // Pass descriptor for grid with no depth testing + const skinnedGridRenderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: undefined, // Assigned later + + clearValue: { r: 0.3, g: 0.3, b: 0.3, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }; + + const animSkinnedGrid = (boneTransforms: Mat4[], angle: number) => { + const m = mat4.identity(); + mat4.rotateZ(m, angle, boneTransforms[0]); + mat4.translate(boneTransforms[0], vec3.create(4, 0, 0), m); + mat4.rotateZ(m, angle, boneTransforms[1]); + mat4.translate(boneTransforms[1], vec3.create(4, 0, 0), m); + mat4.rotateZ(m, angle, boneTransforms[2]); + }; + + // Create a group of bones + // Each index associates an actual bone to its transforms, bindPoses, uniforms, etc + const createBoneCollection = (numBones: number): BoneObject => { + // Initial bone transformation + const transforms: Mat4[] = []; + // Bone bind poses, an extra matrix per joint/bone that represents the starting point + // of the bone before any transformations are applied + const bindPoses: Mat4[] = []; + // Create a transform, bind pose, and inverse bind pose for each bone + for (let i = 0; i < numBones; i++) { + transforms.push(mat4.identity()); + bindPoses.push(mat4.identity()); + } + + // Get initial bind pose positions + animSkinnedGrid(bindPoses, 0); + const bindPosesInv = bindPoses.map((bindPose) => { + return mat4.inverse(bindPose); + }); + + return { + transforms, + bindPoses, + bindPosesInv, + }; + }; + + // Create bones of the skinned grid and write the inverse bind positions to + // the skinned grid's inverse bind matrix array + const gridBoneCollection = createBoneCollection(5); + for (let i = 0; i < gridBoneCollection.bindPosesInv.length; i++) { + device.queue.writeBuffer( + skinnedGridInverseBindUniformBuffer, + i * 64, + gridBoneCollection.bindPosesInv[i] as Float32Array + ); + } + + // A map that maps a joint index to the original matrix transformation of a bone + const origMatrices = new Map(); + const animWhaleSkin = (skin: GLTFSkin, angle: number) => { + for (let i = 0; i < skin.joints.length; i++) { + // Index into the current joint + const joint = skin.joints[i]; + // If our map does + if (!origMatrices.has(joint)) { + origMatrices.set(joint, whaleScene.nodes[joint].source.getMatrix()); + } + // Get the original position, rotation, and scale of the current joint + const origMatrix = origMatrices.get(joint); + let m = mat4.create(); + // Depending on which bone we are accessing, apply a specific rotation to the bone's original + // transformation to animate it + if (joint === 1 || joint === 0) { + m = mat4.rotateY(origMatrix, -angle); + } else if (joint === 3 || joint === 4) { + m = mat4.rotateX(origMatrix, joint === 3 ? angle : -angle); + } else { + m = mat4.rotateZ(origMatrix, angle); + } + // Apply the current transformation to the transform values within the relevant nodes + // (these nodes, of course, each being nodes that represent joints/bones) + whaleScene.nodes[joint].source.position = mat4.getTranslation(m); + whaleScene.nodes[joint].source.scale = mat4.getScaling(m); + whaleScene.nodes[joint].source.rotation = getRotation(m); + } + }; + + function frame() { + // Sample is no longer the active page. + if (!pageState.active) return; + + // Calculate camera matrices + const projectionMatrix = getProjectionMatrix(); + const viewMatrix = getViewMatrix(); + const modelMatrix = getModelMatrix(); + + // Calculate bone transformation + const t = (Date.now() / 20000) * settings.speed; + const angle = Math.sin(t) * settings.angle; + // Compute Transforms when angle is applied + animSkinnedGrid(gridBoneCollection.transforms, angle); + + // Write to mvp to camera buffer + device.queue.writeBuffer( + cameraBuffer, + 0, + projectionMatrix.buffer, + projectionMatrix.byteOffset, + projectionMatrix.byteLength + ); + + device.queue.writeBuffer( + cameraBuffer, + 64, + viewMatrix.buffer, + viewMatrix.byteOffset, + viewMatrix.byteLength + ); + + device.queue.writeBuffer( + cameraBuffer, + 128, + modelMatrix.buffer, + modelMatrix.byteOffset, + modelMatrix.byteLength + ); + + // Write to skinned grid bone uniform buffer + for (let i = 0; i < gridBoneCollection.transforms.length; i++) { + device.queue.writeBuffer( + skinnedGridJointUniformBuffer, + i * 64, + gridBoneCollection.transforms[i] as Float32Array + ); + } + + // Difference between these two render passes is just the presence of depthTexture + gltfRenderPassDescriptor.colorAttachments[0].view = context + .getCurrentTexture() + .createView(); + + skinnedGridRenderPassDescriptor.colorAttachments[0].view = context + .getCurrentTexture() + .createView(); + + // Update node matrixes + for (const scene of whaleScene.scenes) { + scene.root.updateWorldMatrix(device); + } + + // Updates skins (we index into skins in the renderer, which is not the best approach but hey) + animWhaleSkin(whaleScene.skins[0], Math.sin(t) * settings.angle); + // Node 6 should be the only node with a drawable mesh so hopefully this works fine + whaleScene.skins[0].update(device, 6, whaleScene.nodes); + + const commandEncoder = device.createCommandEncoder(); + if (settings.object === 'Whale') { + const passEncoder = commandEncoder.beginRenderPass( + gltfRenderPassDescriptor + ); + for (const scene of whaleScene.scenes) { + scene.root.renderDrawables(passEncoder, [ + cameraBGCluster.bindGroups[0], + generalUniformsBGCLuster.bindGroups[0], + ]); + } + passEncoder.end(); + } else { + // Our skinned grid isn't checking for depth, so we pass it + // a separate render descriptor that does not take in a depth texture + const passEncoder = commandEncoder.beginRenderPass( + skinnedGridRenderPassDescriptor + ); + passEncoder.setPipeline(skinnedGridPipeline); + passEncoder.setBindGroup(0, cameraBGCluster.bindGroups[0]); + passEncoder.setBindGroup(1, generalUniformsBGCLuster.bindGroups[0]); + passEncoder.setBindGroup(2, skinnedGridBoneBGCluster.bindGroups[0]); + // Pass in vertex and index buffers generated from our static skinned grid + // data at ./gridData.ts + passEncoder.setVertexBuffer(0, skinnedGridVertexBuffers.positions); + passEncoder.setVertexBuffer(1, skinnedGridVertexBuffers.joints); + passEncoder.setVertexBuffer(2, skinnedGridVertexBuffers.weights); + passEncoder.setIndexBuffer(skinnedGridVertexBuffers.indices, 'uint16'); + passEncoder.drawIndexed(gridIndices.length, 1); + passEncoder.end(); + } + + device.queue.submit([commandEncoder.finish()]); + + requestAnimationFrame(frame); + } + requestAnimationFrame(frame); +}; + +const skinnedMesh: () => JSX.Element = () => + makeSample({ + name: 'Skinned Mesh', + description: + 'A demonstration of basic gltf loading and mesh skinning, ported from https://webgl2fundamentals.org/webgl/lessons/webgl-skinning.html. Mesh data, per vertex attributes, and skin inverseBindMatrices are taken from the json parsed from the binary output of the .glb file. Animations are generated progrmatically, with animated joint matrices updated and passed to shaders per frame via uniform buffers.', + init, + gui: true, + sources: [ + { + name: __filename.substring(__dirname.length + 1), + contents: __SOURCE__, + }, + { + name: './gridData.ts', + // eslint-disable-next-line @typescript-eslint/no-var-requires + contents: require('!!raw-loader!./gridData.ts').default, + }, + { + name: './gridUtils.ts', + // eslint-disable-next-line @typescript-eslint/no-var-requires + contents: require('!!raw-loader!./gridUtils.ts').default, + }, + { + name: './grid.wgsl', + // eslint-disable-next-line @typescript-eslint/no-var-requires + contents: require('!!raw-loader!./grid.wgsl').default, + }, + { + name: './gltf.ts', + // eslint-disable-next-line @typescript-eslint/no-var-requires + contents: require('!!raw-loader!./gltf.ts').default, + }, + { + name: './glbUtils.ts', + // eslint-disable-next-line @typescript-eslint/no-var-requires + contents: require('!!raw-loader!./glbUtils.ts').default, + }, + { + name: './gltf.wgsl', + contents: gltfWGSL, + }, + ], + filename: __filename, + }); + +export default skinnedMesh;