From 5ca14eeaec1de46f44e5fc78a9c546840c589f9b Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sat, 19 Sep 2026 19:49:05 +0300 Subject: [PATCH] feat(render3d): contact darkening, so things sit ON the ground The existing occlusion is tuned as AMBIENT occlusion: a wide radius answering "how open is the sky here". That is the right question, and it leaves every object in the frame hovering - because what says a log is ON the ground rather than in front of it is a hard, narrow darkening in the last few centimetres where the two meet, and at a metre and a half of radius that darkening is spread so thin it is not there. A second, tight pass: eight taps inside 40 cm, which at any normal distance is a handful of pixels and stays in cache, with a much tighter range check than the ambient one so a wall across the room does not darken the floor in front of it. Small by frame area, because contact occlusion is - 1.8% of the frame, 7% of the region around the things it grounds. GL 7.0 -> 7.1 s, VK 7.3 s over 400 frames. Co-Authored-By: Claude Opus 5 --- packages/ludic.render3d/post.ludic | 4 +++ .../shaders/spv/676965ab.frag.spv | Bin 13508 -> 16984 bytes .../ludic.render3d/shaders/spv/manifest.txt | 5 +-- packages/ludic.render3d/shaders/ssgi.frag | 31 ++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/ludic.render3d/post.ludic b/packages/ludic.render3d/post.ludic index d44437a9..6ebee0b3 100644 --- a/packages/ludic.render3d/post.ludic +++ b/packages/ludic.render3d/post.ludic @@ -50,6 +50,7 @@ var post_ao_blur: Target = null var post_p_ao: int = 0 var post_p_ao_blur: int = 0 var post_ao_radius: int = 0 +var post_contact: int = 0x3FA00000 # 1.25: how hard a thing is darkened where it meets the ground var post_ao_intensity: int = 0 var post_ao_strength: int = 0 var post_gi_strength: int = 0x3ECCCCCD # 0.4 @@ -297,6 +298,9 @@ function post_ssao_pass() -> void { u_f2(gpu_uniform(post_p_ao, "u_texel"), fr(1, post_w), fr(1, post_h)) u_f(gpu_uniform(post_p_ao, "u_radius"), post_ao_radius) u_f(gpu_uniform(post_p_ao, "u_intensity"), post_ao_intensity) + var contact = post_contact + if r3d_env_has("R3D_NOCONTACT") { contact = F_ZERO } + u_f(gpu_uniform(post_p_ao, "u_contact"), contact) mesh_draw(post_fs) target_bind(post_ao_blur) gpu_use_program(post_p_ao_blur) diff --git a/packages/ludic.render3d/shaders/spv/676965ab.frag.spv b/packages/ludic.render3d/shaders/spv/676965ab.frag.spv index 3a4241528810a213f0592ed97ffcb8c98ff65195..7bbe8af90b536a4f2b76196a75fa06f55d2de92d 100644 GIT binary patch delta 4821 zcmZ9PX^>q-8HW4b%pDS$WI|RFgg`<}Fbj|ff~ibGK$6IkVG$Jr;gXqwQId&w5~5)_ zS=dwrVGs&_@rNq?gGJ2`Evn#`no^;f{H5_hE6zEoh2W>y4g3hvz@PzW_+s@3CX&S_tr;e zkMHa_Vjb8iTu7ToduHV5uAPM?90%*5t>v$LF4#+MDcTzqUbS16c5bD=ylS^h?L5xX zF0a^^Crj30e0Gpm94NnSpOEbg=j@4#>$OEA!`%;!JlNeadgSEY^m%<^ncH(4(yg>(artUCs~IA9&b=aqbLwS^ z+v_8jPn}zQ1L<~}DelN-HACc1vsaY*P55)&>sJTA3;w|U_m54@-SF$CJv_!o;3sVC z?pe%4`snU~8>1h#qsD6Uad}0puC{59)(iKcf$B@SyBCAAvCF*}T)X?R%H5N}T^jDo zDtBK7UyHq)I)YD1>7wKY4Z#lFq``40_i1qL?$auFuLie(R(X*$e&=onOe^ z1-Fs`y4ZdUR)3VOG86t2u$o)#5Qoptz+P|TukEKawKv&1;vRgQH2u#Yhc!JpQd>H; zcnbANnx~6B{SvHpGhXti!7gp}Jz0AOd}+3Y->=Zz658y|N1`V7XspxTqMF}uw<_-k zQ%8}03wW01rNeXC!MnqE?>!GT$s>9AFM!o1&>XV-0L@$`7f0-iU}J~!0AB*DwWqd7 z>1D_PI*HN!9PwlYc8qZLBZyz!HrXoJazE9NRek8{Ivq7<%oGYQU{`jNYnrusP1Aic_-Kov_!%)ZlfcfLHW#EvsG6g_ zm?9>FJ=NBsn|D3Zuz?rk=ROs#<`vqstl>7MP0yOHz0A>B+-E?H*I&Eq z=PvSUn+d0LeAs_h>jAH5Av(+gt8E~e@7~#9HJg{6J2KDFqtNsn0LP4d7_Q!nkMT!? z)ogBUwPV1u3;t+yY8B^zHF!`&@UfYzIo_C0%777|%BrdE~U@LP``QL{8bQX*U&uZ{= z?&`4}&j73QD`Wf;Qj0}*CfKthMt&B23%>baD$29`lZYB3=xi}9&i}PwebnQOUk4^H zW%KP{>h1T!r?T#UBKoYS#oP28um$PU#Xkj|oImMYh>1?7xdxnW9i4$S+_gtx=ZR@C zn)AUPSk+^81Dblo^n#s%c)?u&_lrt>Pr=ms0=W=kpvK4Z{=5imhuXe_mwp$6)h*Iu zqmxggS!{H230PnC@c9hbkH9^-&(FdOMV%e3M#Weht_>H73H%KE95%mlc9TGUDO~L_ zG~>0Y86V$yev&y`d-4A~whw&S7HK1*hN$u~u!Y1(E{E%*9?x+TSlzFpSR+@!ofT~{ z7`4dT1iy$q_?7VeHJ*QaA@I*P0{Qr_CMAur7UF}WFWcv1H<|u+WKwO8I=;ER#~QgB zTV=(Uk8u!9y@=25FF+Kdm@k4Y#zi>*zKW)g9rknA9wWI1Y!MN=1s(&@76S?Yt!U=Z zzi?3v6tc!jn*YqgaXaZeWYjjx!!5EB@n-Q*T= OJwAKuqnDhtwd4QZB=Ye9 delta 1341 zcmYk5OKeP09LCSRGq){j+Q?9i4Xt=K60vMLZIFmvZKcr+(}@vErWFh1N?2I2m{sj< z3A5{3yq~R7inbJ0O1;{$;rrd5Gntcn=KR0!`@hcr-WfjU-N+?keq*IEo>^k5%)ECk zQ5#eSrrj8`zSt6+_QSZj=^2wvXS&3hz*Lxz!pA#v`FuK)OLrfzd|*CD{IPsjHg~wU zD=*);`4Vya^O-EMftiVL&%t!&=!u1VftigsMOYfYxdpDLvp7`sFIJeW?hPrsMlo^i zUHSz#kV6YhdGw$Rm+Q2G?pqyPuG1R2rV-@>m+9N0K5?kB4)k5-5cQ0{$m(-mF_CEU zy-kJY+RMd>+B1H(LeedT88fqevw8;Kj_}nT4o?WrZs=%r_`Pt)MZdL4H(M(I55oAs zH!DY*IA+1BSMf=&usGR}`Xp*nd$nAqtPDp8m~pXO3dX@A#k5yAlxzsUikj9Y#WyR% z5dvmHEZ6CG;gOo%Z4UnszSP&3a`>n4$>fz1{w2JzqbAj)IWYC#!o=VwZB?l^Ce3oa zYO?E=_xsEEG6{s|tqgj_>nm5(gKg0M!zpxkJ+lt zYvR@5lQOaBUx4m%J&-*rKNtH@SdWIG^oWfDd$nD_BXKnB0v^lOV`!FOeDR?eiP2r` z6WQb9`t=Re=^PM-qsE;4hh($<>S6<4Xv|X)FSO_Nxnq-vNj;N2rnB9S{JC&_**>oV z_zjE07oCO%b;A8oK8lf-{7ze|n35J+Cs*{P97*7AGa{QwoK-Q#{z^8threJOcrBYa zG>VK#hjTgK$cA${M}?W%9>wn?>6i$dSdehzmV 0.0) { + float cao = 0.0; + const int CS = 8; + float crad = 0.40 * (1.0 + 0.004 * -P.z); + for (int i = 0; i < CS; i++) { + float a2 = (float(i) + noise) * 2.3999632; + float r2 = sqrt((float(i) + 0.5 + noise) / float(CS)); + vec3 dir2 = vec3(cos(a2) * r2, sin(a2) * r2, sqrt(max(0.0, 1.0 - r2 * r2))); + vec3 s2 = P + (t * dir2.x + b * dir2.y + N * dir2.z) * crad; + vec4 c2 = u_proj * vec4(s2, 1.0); + vec2 uv2 = c2.xy / c2.w * 0.5 + 0.5; + if (uv2.x < 0.0 || uv2.x > 1.0 || uv2.y < 0.0 || uv2.y > 1.0) continue; + float sz2 = viewPos(uv2).z; + // a tight range check: only something genuinely touching counts, or a wall across the + // room darkens the floor in front of it + float rc2 = smoothstep(0.0, 1.0, crad / max(abs(P.z - sz2), 1e-4)); + cao += (sz2 >= s2.z + 0.004 * crad ? 1.0 : 0.0) * rc2; + } + cao = 1.0 - u_contact * cao / float(CS); + ao = min(ao, clamp(cao, 0.0, 1.0)); + } float fade = smoothstep(120.0, 350.0, -P.z); ao = mix(clamp(ao, 0.0, 1.0), 1.0, fade); gi = (giW > 0.0 ? gi / float(S) : vec3(0.0)) * (1.0 - fade);