From fa1f9c6808923835677b8c74216c6c05af6de098 Mon Sep 17 00:00:00 2001 From: Peter Li Date: Sun, 27 Apr 2025 19:11:12 -0700 Subject: [PATCH] filtered shadow maps --- engine/rend/shaders/lit_mesh.frag.hlsl | 51 +++++++++++- engine/rend/src/sgpu/renderer.zig | 2 +- .../content/_shaders/dxil/lit_mesh.frag.dxil | Bin 6192 -> 6304 bytes .../content/_shaders/msl/lit_mesh.frag.msl | 74 +++++++++++------- .../content/_shaders/spv/lit_mesh.frag.spv | Bin 4256 -> 4900 bytes 5 files changed, 96 insertions(+), 31 deletions(-) diff --git a/engine/rend/shaders/lit_mesh.frag.hlsl b/engine/rend/shaders/lit_mesh.frag.hlsl index a97b9e1..9c4f539 100644 --- a/engine/rend/shaders/lit_mesh.frag.hlsl +++ b/engine/rend/shaders/lit_mesh.frag.hlsl @@ -81,6 +81,7 @@ float shadowCalculation(float3 fragPos, float4 DirectionalShadowFragPos) float2 uv = shadowProjection.xy; uv.y = -uv.y; float depthSample = DirectionalShadowDepthMap.Sample(DirectionalShadowSampler, uv); + float fragDepth = DirectionalShadowFragPos.z; float bias = 0.005; @@ -89,6 +90,49 @@ float shadowCalculation(float3 fragPos, float4 DirectionalShadowFragPos) return shadow; } +float shadowCalculationPCF(float3 fragPos, float4 DirectionalShadowFragPos) +{ + // size = 2048 x 2028 + float2 texDim = float2(2048, 2048); + + float scale = 0.5; + float dx = scale * 1.0 / float(texDim.x); + float dy = scale * 1.0 / float(texDim.y); + + float combinedDepth = 0.0; + int count = 0; + int range = 4; + + float3 shadowProjection = DirectionalShadowFragPos.xyz / DirectionalShadowFragPos.w; + shadowProjection = shadowProjection * 0.5 + 0.5 ; + shadowProjection.x = shadowProjection.x; + + float2 uv = shadowProjection.xy; + uv.y = -uv.y; + float fragDepth = DirectionalShadowFragPos.z; + float combinedShadow = 0.0; + float bias = 0.001; + + for (int y = -range ; y <= range ; y++) { + for (int x = -range ; x <= range ; x++) { + float2 Offsets = float2(x * dx, y * dy); + //float3 UVC = float3(uv + Offsets, DirectionalShadowFragPos.z); + float depth = DirectionalShadowDepthMap.Sample(DirectionalShadowSampler, uv + Offsets); + combinedDepth += depth; + combinedShadow += fragDepth - bias > depth ? 1.0 : 0.0; + count += 1; + } + } + + combinedDepth = combinedDepth / count; + combinedShadow = combinedShadow / count; + + // float fragDepth = DirectionalShadowFragPos.z; + // float shadow = fragDepth - bias > combinedDepth ? 1.0 : 0.0; + + return combinedShadow; +} + float3 DirectionalLight(float3 normal, float3 fragPos, float4 DirectionalShadowFragPos) { @@ -106,7 +150,7 @@ float3 DirectionalLight(float3 normal, float3 fragPos, float4 DirectionalShadow // shadow mapping - diffuse = diffuse * (1.0 - shadowCalculation(fragPos, DirectionalShadowFragPos)); + diffuse = diffuse * (1.0 - shadowCalculationPCF(fragPos, DirectionalShadowFragPos)); return diffuse; } @@ -152,9 +196,10 @@ float4 main( // float depthSample = DirectionalShadowDepthMap.Sample(DirectionalShadowSampler, uv); // float fragDepth = shadowProjection.z; - // float t = DirectionalShadowFragPos.z; - // t = depthSample; + // float t = shadowCalculationPCF(WorldPos, DirectionalShadowFragPos); + + // float t = depthSample; // rv = float4(t, t, t, 1.0); // rv.x = rv.x + lightPos.x * 0.0001; diff --git a/engine/rend/src/sgpu/renderer.zig b/engine/rend/src/sgpu/renderer.zig index bc93b8f..a2477ae 100644 --- a/engine/rend/src/sgpu/renderer.zig +++ b/engine/rend/src/sgpu/renderer.zig @@ -594,7 +594,7 @@ pub const Renderer = struct { // can cache this const targetInfo: gpu.GPUColorTargetInfo = std.mem.zeroInit(gpu.GPUColorTargetInfo, .{ .texture = self.swapchainTexture.?, - .clear_color = .{ .r = 0.1, .g = 0.1, .b = 0.1, .a = 1.0 }, + .clear_color = .{ .r = 71.0 / 256.0, .g = 200.0 / 256.0, .b = 1.0, .a = 1.0 }, .load_op = .loadopClear, .store_op = .storeopStore, }); diff --git a/projects/content/_shaders/dxil/lit_mesh.frag.dxil b/projects/content/_shaders/dxil/lit_mesh.frag.dxil index d7893de8c4a79dbfea3a7c81b921b1002190f3ae..66c8b39c0d793ce276f11cb0e263301bf3c15950 100644 GIT binary patch delta 1724 zcmY*Z2~ZPf6yAh&cXJ2<)FcMTrrd|PQ50#XnN47#Rp@B77ArVQ0*Ea~Mvziv%mG3a zgrz`1X)#o4vGoYjcBCGYO%U)#Wv1cMqys7)sFXSm9i3@+OXz53GV}iTz4yKMfA8&Y z#Vm4hQmQKP(aOW8-u}Lz;Hyi*gDb{9v$6mHu+)hqi>7!0fOIBQvcn=c06@_0jcfo& zN_{UGpkoB|SwQ<$iMEF_J^(1$^-=swU2R@^HrV0CO{))q;#iYiJD_2<-fvHSQDNNU z`**H7`@;}$y*74w>eHGUBwhTlX{04>1PT}4z=g#|;eJYZ!zHxI0&d^|%~83z#d)!N zGPClFwwf$lacfojvC*{QmXQWSj;i8`M%bJEQCNIv#P6iL`&Qeby2k5kuGBSlrf|qr znI*A%vkD9L?bT)#Ze^#O_Y=4B(u2}=kS!yUbWu9ZF}j5Eeqr%uNIxD*_ZwP5yN4Pq zJ`7ny_r2>A$}8V?-@Bc{$#3I{TW>mdel4M^k2c@|U7LI-S@kZSAYtnT-hH2=w~sfS z3;vKF^Uok74M|JENEPDsrc@=ge?clqS0vvDes?>qqCdDXhEXz@{5MXI2o>T{I zQ&CHHi5rtoH*6YeOp9#TDZ2#>U~0n!dCGn*_$>K&PJNz#oE|Wt=0YuaHsQ(hc=b$g z{?x($4wvKA45AMPRs&iPMNBg=5Daku6JZ@Q{sa{ha0aF!0Th8GL20=JT#P~;3BIhwT>GoePvv(^Q3l|euXWY;W1C@4>N~V z5u?U)$@J0xf-0?Zu$R~z0uXm$Od+RnKDyebSwDT z0f-C+A7pY*VbFsd6;K_9P&szAI!xfuCC&Igib+JlU`{i7Uda#?oxg5#O>! zbfo1ewN!Eb<>mYy8-NS=AqP4R*-RK&%gI2c5<9L4hIsTf@d-x^Uq!(Ca5xx(X$LXk zhy_m)tf>CJ@-5_DGsU8H%Y13!=?y*$*XQWVhyz$p-wVQAJ532rcnWT?3n7`PobVK^ zkEpaA!4=z)*-i~vyV$=px7nq2fD6&7_JG%;n{r-@rpq#$vsvEheQtnz21DriF@2=U ziU*EsS+JDn!WHVGnPZ_5P!yCZLNpeQ z;Shz=hcpr2UYa3%s=aFCJ(p7f<63({e;-(6sO3xoQb#4K=mLB3YZl41O8z#%st>AJ znhBc^&9shAcJ$X6x$O1T`l*Wq#+y+pcJs51ivB(bQiR3Kh~*_r9}%&dZ-O2xkw4r) zgQ^wp{}!b7XA|o*obqR;wiv>t<);~li!uWTYtF)JY;Sb~lr9nVC$O8ONJ+87U(|u6 zQ_-9`C*emkuA>>tl2`7vOUE^FGC=KLPQ+yh^lH9008;2*RjmTfWl;)ZxChEaDWdX) zNjSnG8NwmwDt*hCt+8%!#IT#Jr^Cm!a^IdllTxt^o0S{uY8O|R<}P<>9{|!8XwHfG z&30yljB6bm`ghL{v3v>1|3!I1Yoa!9p4fu@u4Y=YHE6Ao_N#^%2C&g=5 delta 1607 zcmY*ZeM}Q)7=L>`?v8Rmg$YNo^eUi$Os@zO9n`iUI4zDPE~|=Ks3Hr@;SfKtroFR# zCCEp>kHHq1Rkj&sC>Rm^hqem!3)Hy<4H|F-ovX7TTj;XHUDImxl1rYC-}5~0@ArOm zNV=(rEVVN8fxbs=HKQ2^IRF5vIU>@}5x`pJRW(p7=?{R0b=E-uWT`(^ z0T#n&eT5^OhJSne(*eSa3>j19A@13(CLPy`ywzD9o57 zC-?u8l9OY+fWN%?BKN|V+?Ig)obbJvKEURz#&`+{oE`n>yMTlqJc0ny7^N-Cq5Y1>1{U3OQ#Kp z)1C14ShjW`{c_J*QragSpa$3;{qc;@sStXaUhKIN(-Y5zd*T%$PqX-PQ_Ibxl^e=U z?d>7s%J8K0ljgqF(gAjQydn+szV>%>R&C`3ZqmP;e!bv~`g!HkmN}0aaV&d}Iy~hRh48+ACn5`TYy6 zI0)3j3ENLswj2pf*57F>->BEZUpa^Vl4i5c$l++9g-MK=mvX6Wz>t`p?FXU}f52$E z?^=vVIfXv1RX_%kD6Np6vn;x0krp-q5<}XBn~L+$3V`^)kLcq_MIKr}dDD?m`6U{3 zKj}+rj7%(gKz|4M)!U3XQ+AsZRZpjQ(L-^XeNE_|vSUOHz!+M=+bn!WM{2ySHLXlF zdYF$s@RfFUPjN)w(y6}E__E47=-1u8QkwTjG|v&1`AW|+C{N#kJo1+6iSw>1DDFP( z<1mz9P{bLnLY)uYtt4uhxQiU3o=9ic!8yw-0jrc$u8HCmP{)?QgT)NLehF?Zh9r0n z95r)0306{t8jlOW&^)2A4;z6j8bR0+@&zM;!{FJ72L8BwzB6)!3}kqSpdrp7pI7B^-6|=3lv+fO z(XL$SAkAmzmyx<(0{4<|5mH2oR&rN1HTpq1Cq5#VErDGT5(@BPZk=$um}tP}4D&@1 z$YMQ}e3|Q3KS5{BhCqiFinQ-O(LqSm9%Mq4dh*r4=%3j*4q%JCt!6Wph;iMg_{gkT zj5}5qc6(^HW?Z$B@cuk>A5~c937p7qDCNAc4o0UU{M_}#B33s=b#R{x{resIm(WaI zX>hfEek=-~iSP}KIat7F6qs+Ipl&bLg*miUq-Yu99-}P7)&8 z@1X06M9+3jh5RwW`+1Oel!Jry#;cUfy`4tsppNhX8zwBIy&dI6UU6jW2u;wUKK zonTLMg}zJ)qF?fVuri0n8+oJ3yyMuUP0MM ztTE%_#OzQ=lgx4tMGCHJIC4Jz<0W~W0((dce=E2;0oXv-B}1J?lo+)UVh>0#dqBYT zYbu8=BWqOVv~B5HyWVLlRsuW+r(N5qRyvC>3J7wm!S!Kyoh(&8Yc4S~ZsY2lf(flH z-9B7>sHrjVmHsVxxoK&&PY0##t(o=vQY5laDND5*b)~2L+3h4e*Kwy{r^ILO6YP3p v8U;nH>A9~QtDsmSFg1tUyw{BEqQguQ!5;B@X*%3Oyq7;<3C29H4g38M;5i=0 diff --git a/projects/content/_shaders/msl/lit_mesh.frag.msl b/projects/content/_shaders/msl/lit_mesh.frag.msl index 5c4c56f..74ddcd7 100644 --- a/projects/content/_shaders/msl/lit_mesh.frag.msl +++ b/projects/content/_shaders/msl/lit_mesh.frag.msl @@ -13,7 +13,7 @@ struct type_Uniforms float time; }; -constant float3 _56 = {}; +constant float3 _59 = {}; struct main0_out { @@ -31,53 +31,73 @@ struct main0_in fragment main0_out main0(main0_in in [[stage_in]], constant type_Uniforms& Uniforms [[buffer(0)]], texture2d Texture [[texture(0)]], texture2d DirectionalShadowDepthMap [[texture(1)]], sampler Sampler [[sampler(0)]], sampler DirectionalShadowSampler [[sampler(1)]]) { main0_out out = {}; - float4 _65 = Texture.sample(Sampler, in.in_var_TEXCOORD0); - float _66 = _65.w; - if (_66 < 0.00999999977648258209228515625) + float4 _68 = Texture.sample(Sampler, in.in_var_TEXCOORD0); + float _69 = _68.w; + if (_69 < 0.00999999977648258209228515625) { discard_fragment(); } - float3 _119; + float3 _122; do { - float3 _76 = Uniforms.lightPosition.xyz - in.in_var_TEXCOORD1; - float _77 = length(_76); - float _78 = _77 * _77; - float _84; - if (_77 > 2.0) + float3 _79 = Uniforms.lightPosition.xyz - in.in_var_TEXCOORD1; + float _80 = length(_79); + float _81 = _80 * _80; + float _87; + if (_80 > 2.0) { - _84 = 0.5 / _78; + _87 = 0.5 / _81; } else { - _84 = 1.0 / _78; + _87 = 1.0 / _81; } - float _89; - if (_77 > 4.0) + float _92; + if (_80 > 4.0) { - _89 = _84 * 0.5; + _92 = _87 * 0.5; } else { - _89 = _84; + _92 = _87; } - float _91 = (_77 > 15.0) ? 0.0 : _89; - float _93 = (_91 > 1.0) ? 1.0 : _91; + float _94 = (_80 > 15.0) ? 0.0 : _92; + float _96 = (_94 > 1.0) ? 1.0 : _94; if (length(in.in_var_TEXCOORD2) < 0.100000001490116119384765625) { - _119 = (in.in_var_TEXCOORD1 * float3(0.800000011920928955078125, 0.800000011920928955078125, 0.5)) * _93; + _122 = (in.in_var_TEXCOORD1 * float3(0.800000011920928955078125, 0.800000011920928955078125, 0.5)) * _96; break; } - float3 _100 = fast::normalize(_76); - _119 = ((float3(0.800000011920928955078125, 0.800000011920928955078125, 0.5) * precise::max(dot(_100, in.in_var_TEXCOORD2), 0.0)) * _93) + (((float3(0.800000011920928955078125, 0.800000011920928955078125, 0.5) * powr(precise::max(dot(in.in_var_TEXCOORD2, fast::normalize(_100 + fast::normalize(in.in_var_TEXCOORD1 - Uniforms.viewPos.xyz))), 0.0), 30.0)) * 2.0) * _93); + float3 _103 = fast::normalize(_79); + _122 = ((float3(0.800000011920928955078125, 0.800000011920928955078125, 0.5) * precise::max(dot(_103, in.in_var_TEXCOORD2), 0.0)) * _96) + (((float3(0.800000011920928955078125, 0.800000011920928955078125, 0.5) * powr(precise::max(dot(in.in_var_TEXCOORD2, fast::normalize(_103 + fast::normalize(in.in_var_TEXCOORD1 - Uniforms.viewPos.xyz))), 0.0), 30.0)) * 2.0) * _96); break; } while(false); - float3 _136 = ((in.in_var_TEXCOORD3.xyz / float3(in.in_var_TEXCOORD3.w)) * 0.5) + float3(0.5); - float3 _138; - _138.x = _136.x; - float2 _139 = _138.xy; - _139.y = -_136.y; - out.out_var_SV_Target0 = float4(powr(_65.xyz * ((float3(0.100000001490116119384765625) + _119) + ((Uniforms.directionalLightColor.xyz * precise::max(dot(Uniforms.directionalLight.xyz, in.in_var_TEXCOORD2), 0.0)) * (1.0 - float((in.in_var_TEXCOORD3.z - 0.004999999888241291046142578125) > DirectionalShadowDepthMap.sample(DirectionalShadowSampler, _139).x)))), float3(0.4545454680919647216796875)), _66); + float3 _139 = ((in.in_var_TEXCOORD3.xyz / float3(in.in_var_TEXCOORD3.w)) * 0.5) + float3(0.5); + float3 _141; + _141.x = _139.x; + float2 _142 = _141.xy; + _142.y = -_139.y; + float _149; + int _152; + int _154; + _149 = 0.0; + _152 = 0; + _154 = -4; + float _150; + int _153; + for (; _154 <= 4; _149 = _150, _152 = _153, _154++) + { + _153 = _152; + _150 = _149; + for (int _163 = -4; _163 <= 4; ) + { + _153++; + _150 += float((in.in_var_TEXCOORD3.z - 0.001000000047497451305389404296875) > DirectionalShadowDepthMap.sample(DirectionalShadowSampler, (_142 + float2(float(_163) * 0.000244140625, float(_154) * 0.000244140625))).x); + _163++; + continue; + } + } + out.out_var_SV_Target0 = float4(powr(_68.xyz * ((float3(0.100000001490116119384765625) + _122) + ((Uniforms.directionalLightColor.xyz * precise::max(dot(Uniforms.directionalLight.xyz, in.in_var_TEXCOORD2), 0.0)) * (1.0 - (_149 / float(_152))))), float3(0.4545454680919647216796875)), _69); return out; } diff --git a/projects/content/_shaders/spv/lit_mesh.frag.spv b/projects/content/_shaders/spv/lit_mesh.frag.spv index d7c14fab832a09835ebdd5739c30161675174d58..22a91f3e6d485ff2c1e443d3d390622b836391ac 100644 GIT binary patch literal 4900 zcmZveS9?`O6oxm!B!Vb{q5_tLhz*S$DbfT4HJC^Mv2!FxIGW_d#VT_1ekeP+Ra(Bb)*wZ64xW|i4 zRR+szYn5t$skeK5si(SWQF)-YepzWCUo$G(`~Q4mlia6?JX2gN`zu#e2m6N5n>bP4 zv9VI#)KwjVYtA{V*jrh*z6M$GY;?2J-`sn4#nho26!uZM-J0!gUs&y}nq(9I+{2m< ztsN|v`@1XGQKX2)TGlFkmW}8dv$4#DkYSqho>cnVHvE`@GSrP#-CC;~pSpXh80)QzLK-a9n~gui+#aOm${D%#bHcJEfvuC6`e@m>|} z>P5S~(H`-s$No#nChk@nydyaGm$O#EH6*U3z`3t}HaGmNncVyWXWitMf+a)~cXR?Z z7zft^Jabyo$0NJ<0sOxoa|_U>ygwNIW83NBI{`h`YCiz{u)Skm_#cSQn0FxS4?@t# zy^p1P>Jx!$gubzE67mn*x6cW`$>?%{@H-gp_-#MU4ZlOs2#{AG* za&>s8-dfjp=3yTl`dc&W>4^1*=i1Nt{@SMkduHB8Fq^k}tUnUjUPatTA)9}|-W-kI z0vxVWKPK09ojVP^I)3x4iM%bot)62)JS&E}qlG<=t&6$uB)oiQ<&R6ex-qviyI1`i ztiNk=MMPe%KOUQBdbiZQlb)-;x{r*yYt(&M)V%}hCo_95^xu!2PGN3^Ih9#?GP2); zIqr{rIoXfn=e@8F(O2v8JKE{Y zbJ)ka&H=`%?z*|aSVNzOY^>_mYd$bmb?dqSSUYv^!5P5Xtzwn6UkLP(KcAKEu?Wa} zw&Of}&1EsrW;|cO*{?H!yl=>FSe7t5{^UGuOM#quDgR9smE{(&hrK=v*dO1W$m8t9 zcanv9p9AEC!(Ldouy-IEn{b5Pn3mP4>@VbFkk19i;Jld_+`kiehV>mo9n7~2xR15b zz9rS+JY;R+&crQ8ZUx%5!C9MbppWm8ajal=SQ~9Cft;}@$6l+DeKXwa4{YJ%{0leF z*v<+Mc~O2{LCeek}=-MbuF+azF~YndJ9sjUad znWwUO+Uqiy3nqg56L&@8*D#yAcgA(DHxAdW1IFQf_dYzDdcPi7o3V{a{iz`9Ydy4^ zf1GtCvS;fP-@sMKegkbFzJXq3`MBeK$a2Er{tY1Ra6htn=@&6qkt1g9x1@Xrkd0Y; z_+E`1zS<+dL1c5&9{CL+M}FGPFKo4BvoHD`4kGsr$mXt3a2t{34=Zq+kge020(T9v zzH*VrwaEHvEB4L)d1vk0b-@052lTh6vHp7G$v~gz#SO^zVk(GU+=wh6y|@WkPB@|$ zQL~%j^eNtn;pFF?&~DA*%v+E>b3BMMZ$*}mGdCm42}hh6b-WEupJ5Pdwjj?>VA{FH zb7!L84w^um>)Uci;=9tlzcX?2!gtEL-UZxWd&F>e;(bG+#`hr09|hvK-vWh=RW#H?hhlId-U10j{te^ z(xc420nYLMJ%&65c)uP8PXKM>n4bh~K-*KGXnPucMzTGVY#&p{>B!Fl?MH%N$>BL> z*E)R5v_B8zjYrw|UP!ug#NfHcq0fupB@nS2ukmYp8HCN)UjfFa?Ny*H;(HBQd&Kv8 zvNPrJY1d!g_>`-u7jGcDHhyE?M3$Qc!e=Y8`G?P2$a4C0Q6qc!HgK&rzmGkc-vMKi zKAQPmus87Dx#m4!?_C>z@4S!fI(_5soez-Z^>@VG`Vd*0!#CFZ@e%NjOat*-_Hp9% zkKeLSkmXx}_r;i8Z{6*K_F*_{|01w&6F_kGz`Ds5du@%?y&u-vIo5cutYfT+`|G`l zT7Qb{{fNIcK0|&C$VZN!BOAB2_^!V|z71%L@A^yR>w&iTZQX|40krvha1irXU<8;1 wv>yVZ#$O|AcLetha;ypN+r%|6cd>6f^LN1Y`fFbd=JIzyGqbjTICTd27XeSeoB#j- literal 4256 zcmZvd*>+S#6owBSlRyZg%m^k-fy zOXrTxp3auxa&OD(HTP*BpVT;|wh2i?atCq_^yiBM@>=C;K^@?LWFo9S$~F9HI|xyW z$7tm8>A^zF%HEb@fBt9zy_OxuFz+p#Djy#zq<#(IH=OSu>?;hRrha3Ydorq)RgLKF z#i7FCaq&U!WB0to!cl*I@U0r+H(_U(G-`i~W zSN5uB?SE`5!)9-9Tem7~_PKPtoF3S-|Czn{p`(Q|C)(HQyW;s1@lE?*>FdZP=)l*y z%ym3vR`=pErfy9CW!r_*L%3;Fi$u#)r`sdQ+;N z8_}C9`bZx3(V@RJv!0GvzaZ6qN%z;j5ZJSbYZ0>f1+4u&=uN=kKI)58?bg0a&>L?4 zuwf=|hi|9#v)=9GhW?ZEMs;r?~D5KOn2Y8**m6BHNwaJ zTCnNsT~c4cY#jQldneUhqwXW4ZqL*>zcE3}HcJuJNbWe5THZEgTH*RZGt@}NNZr;8_p_`ZQ zkb4;Ky3AJTTTPn(dbqWG|K{nr4)gvU?E&Ui)>*d)f%~Xiw++BpLf?pNEb7+16&Q=U zb=w52jk(!1!8djPT(2Vw~;!S?=Ijz=BNE)R)>@ZJld2k4rr?Pq4>t}&|_IKxU#vRUhV=#B`jOz|D z8;9$9fpK_`y$@Hi-WQOy8Czr4pCicn+Gp+NA7>p!_H2FP8#spSH!vT>H&8^Dk30Sx zvYc?Ze-6YQejeGp^oyALkRxX8=d*nKk&Rh<_zob4ulC5VglulwBfml9$WOcZh3$pR zW?%GM03!DxWOLUixM5^@zmwp~$kxez1a}-+U%ANR1hT%`s{3aDytDT0B(T4Ep#LEd z>rWxi2Kq!VP9xikg&=xy23bCO@glOEa99g_5jA@WS)b}V@iKbUOuIFUGhadW%*i0m zJc}$JXP!fr6OK4D>T@3M9Ppf2^D1&{2BwiUp1ThH0`T`D&h>41E#tehdw((G(Id37$ua{(W(ZA))Zv$<@VcxONJIL-M zcb4zo_}>Nc?(6!?8RtIwS>Msj?}5flZ(x2Oj05r8_yMv$^6}gFA+o%CIwFpbkhM9Y zwpWm?ZQLVc`xwak&8mNraZ8xvZTvLj-80_C&yeMtf%hiXf1YvLqX%Cgp9k8a2VWwe z0@~so{0ey|(B`*0gZXP