From b1e991253545fde23eaeeb981fc12b252d6c1384 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 22 Aug 2026 22:56:03 +0200 Subject: [PATCH] fix(render): jitter the volumetric march per pixel (IGN); file #421 for the shadow transform buffer (Campaign VM VM5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed step-count volumetric ray marches (Low/Medium presets, e.g. 40 steps) sample the same t values at every pixel, so a shadow-cascade edge crossing lands at the same march step across a whole neighbourhood of pixels and reads as visible concentric banding around light shafts. Offset each pixel's march phase with interleaved gradient noise (Jimenez 2014, fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))))) instead of the fixed 0.5 sub-step center: t = (step + ign) / steps. This decorrelates the step boundaries across pixels, turning the banding into fine per-pixel noise that the existing quarter/half-res upsample already averages away. Pack-on only — atmospheric_volumetric is never used on the retail default path; extinction, colour, and early-outs are unchanged. Regenerated SPIR-V via tools/compile-shaders.ps1: only atmospheric_volumetric.frag.spv and shaders.manifest.json's source hash for that stage changed. Also files #421 (docs/ISSUES.md) for Campaign AR review finding F7: the directional-shadow pass's DirectionalShadowTransformBufferSet duplicates the main pass's N.5 instance SSBO instead of binding it, to be resolved once the planned GPU-culling step for shadow cascades lands. Updates the VM5 ledger row and outcome note in docs/plans/2026-08-22-visualmaster-campaign.md. Co-Authored-By: Claude Sonnet 5 --- docs/ISSUES.md | 27 ++++++++++++++++++ .../plans/2026-08-22-visualmaster-campaign.md | 5 +++- .../Shaders/atmospheric_volumetric.frag | 11 ++++++- .../spv/atmospheric_volumetric.frag.spv | Bin 6276 -> 6528 bytes .../Shaders/spv/shaders.manifest.json | 2 +- 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 0e951a27..c4376a55 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -24,6 +24,33 @@ What does NOT go here: - Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending. - Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed. +## #421 — Directional-shadow pass uploads its own transform buffer instead of binding the main pass's instance SSBO + +**Status:** OPEN — filed 2026-08-22 at Campaign VM VM5 (post-M7 / with GPU culling). +**Component:** rendering / render packs (Campaign AR Tier 2) + +**Description:** `DirectionalShadowTransformBufferSet` +(`src/AcDream.App/Rendering/DirectionalShadowTransformBufferSet.cs`) +re-composes every caster's world matrix from the same +`MeshRef.PartTransform x LocalToWorld` inputs the main pass uses (same +`WbDrawDispatcher.ComposePartWorldMatrix`, so there is no second pose — +Campaign AR constraint 5 is honoured in spirit) and uploads them into a +second GPU buffer each frame for the animated subset. This is where the +65,536-matrix binding ceiling regression lived (fixed in Campaign AR +Stage 1). Cost is measured small (0.11–0.21 ms CPU incremental on the +reference matrix) but it is a duplicate of the N.5 instance SSBO the main +pass already binds. + +**What to do:** when the planned GPU-culling step for shadow cascades +lands (Campaign AR plan, "If that is too expensive, the next permitted +step is GPU culling"), bind the main pass's instance transform SSBO for +the caster pass instead of maintaining `DirectionalShadowTransformBufferSet`, +and delete the set. Not before — the shared buffer's lifetime/flight +rules differ (main pass ring vs retained caster topology) and unifying +them without the culling redesign would just move the ceiling. +Reference: Campaign AR review F7, +`docs/research/2026-08-22-campaign-ar-review.md`. + ## #420 — Client crashes on the character-select screen (`UiButton.OnDraw` null media-state key) **Status:** ✅ FIXED 2026-08-19, root cause proven by a reverting test. diff --git a/docs/plans/2026-08-22-visualmaster-campaign.md b/docs/plans/2026-08-22-visualmaster-campaign.md index 72e96722..70b5571e 100644 --- a/docs/plans/2026-08-22-visualmaster-campaign.md +++ b/docs/plans/2026-08-22-visualmaster-campaign.md @@ -81,7 +81,7 @@ single-pass path darkens by ~10 %. Neither explains a scene-wide level. | VM2 | F3 — which retail detail path ran | **CLOSED 2026-08-22** — single-pass; see [cdb note](../research/2026-08-22-vm2-retail-detail-path-cdb.md) | | VM3 | F4 — linear-light post stack | automated + **owner visual** | | VM4 | F5 — overclaiming docs, incl. the reviewer's own | **CLOSED 2026-08-22** | -| VM5 | F8 — volumetric banding; F7 filed | automated | +| VM5 | F8 — volumetric banding; F7 filed | **CLOSED 2026-08-22** (#421) | | VM6 | Foliage wind | automated + **owner visual** | | VM7 | Closeout: full gates, register, roadmap, merge | **owner** | @@ -252,6 +252,9 @@ Add the VM0 numbers as the new baseline table. Docs only; no code. - File issue: the shadow pass uploads a second transform buffer; a future GPU-culling step should bind the main pass's instance SSBO. Not built here. +**OUTCOME (2026-08-22):** IGN jitter landed in atmospheric_volumetric.frag +(pack-on only); F7 filed as #421. + ## VM6 — Foliage wind (the feature) ### What "real movement" means here diff --git a/src/AcDream.App/Rendering/Shaders/atmospheric_volumetric.frag b/src/AcDream.App/Rendering/Shaders/atmospheric_volumetric.frag index 94cd28ab..843fd0c2 100644 --- a/src/AcDream.App/Rendering/Shaders/atmospheric_volumetric.frag +++ b/src/AcDream.App/Rendering/Shaders/atmospheric_volumetric.frag @@ -52,10 +52,19 @@ void main() vec3 sceneWorld = reconstructWorld(vUv, sceneDepth); int steps = clamp(int(uPackParams0.z + 0.5), 1, 64); float lit = 0.0; + // Interleaved gradient noise (Jimenez, "Next Generation Post Processing + // in Call of Duty: Advanced Warfare", 2014). Every pixel would otherwise + // sample the same t values along its march, so shadow-cascade edge + // crossings line up between neighbouring pixels and read as visible + // concentric banding. Jittering the march's starting phase per pixel + // decorrelates those step boundaries, turning the banding into fine + // noise that the existing quarter/half-res upsample already averages + // away. + float ign = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); for (int step = 0; step < 64; ++step) { if (step >= steps) break; - float t = (float(step) + 0.5) / float(steps); + float t = (float(step) + ign) / float(steps); lit += directionalVisibility(mix(nearWorld, sceneWorld, t)); } diff --git a/src/AcDream.App/Rendering/Shaders/spv/atmospheric_volumetric.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/atmospheric_volumetric.frag.spv index d782ffe1091c0e44645ef1db7153a1a1190a9d13..5ac257b8bdeaa3791b510814ce252948b0c0ae11 100644 GIT binary patch literal 6528 zcmZvgTa2Am7017JPCKnL^g=JT6ep+j0ZNBb3~h}CdP+N;stp~SDl{R1N&4agsKuyJ zBNc?y7h{Pg7-E$-jbe+In}Am!k

q1-Q4?v(r8Qh&ei?Em;l+&Qajt^ZnkueJ8| zoAyjx)LTuSQ1w+uRi{m?>Nc~QSWQ5ZUq8FcZz%j0cwaTSwhwO{-MIAPuWVg<>hfiF zoL2SJ{p~xQ@mg@{!m%%0q+t*mg7!f7L61N$K(9kb(P;+#_3wp-kxzu1(uQhMEhAQo z{D&6ISYk68n0>_B4b05MOf-FsCDv(RlL|IcW34*wWMuL5Q#Z$EOfkp002-zWYh4K0 zFZyxFJw;yww@;_md#bf?^WH#TWm{rKZ^jI^sOwtiCYJe7Fkdmp)o(@v>nT{ffn`3H zHL%P_2h2K#=(i9u_vp>K_7pp;wySG52iNqTB3q}jEpH6DQP<(w!#*pMF@ms|DTA3(S1 z_Z-~(txwt3tXV8IXWylder+gyQ}Z$~*T^&Gnwfia*F-;c&)dTf_F%@o$5XB4f3x#c*Rd{y;UgKG*91kMwQhxdd*$-b1U3?zt8-$CKb~ z$QZ^{UkY{WZdE5E>+5*QX$7)1C+ANh-?s9K<*|3r&GA_NT?4mo2Xcyh32`VSYI`On77^AW#MWaH}Rd2o#G8nvn|MK^Bz z$B>(KZbi4nzOAd)y76{&_0@x9<6hYb*+;VWE8*s2-1uCD?3zoCYd*Umede>qzk)mV z1jzPbzJLs`1=sIm$iIfGTjOrHP0YDd_b#((-`f>y|I~CRSaWXop&N667t{O6wRDd( z$($wglpOnB_*BR~##X96kFDrRnudk}j0$R(=>(4IqX?wRbD z=fTZiyW^;PcZeD1HMnc4zjk%+4zWR^i@yP7-NG|Jy(HAZo}Bv7FFiDRrepUb^BW&q z-ERW1`1^jX{qpxtk3JLq2Sa~YmHluux<2BA)af0w898~#Pb6FS(HQvRB5y^Wfb2Z2 zpx71cL2GV7HtTx_#B=gC+WGj~-beOMx&U3fd<|av!S07#v-QyFjO1E90Ev$l{XoI& z<9_n3aF2=G$GzyAA+GNFJ_@X0GotM#hrA$xImkWytu>yZ**8msjC7bJ6D&NX%T6&%noh>t1zi*HfE5 zmzR0n2IhSE##~mgi^0SP8rWBh?!2b&*TBS^eZP(_Zr_n|R=$DmnA)sUn|tA8_&31@ z;Y%Us$h+fPVB$ORaqYg1Eao_yswwq;-;OLU*<|kJtnH=6=AQ0n=AHY?!1QZD+WW~* z{JRA|q4@Y#Wj|exO?zkuIeFf$05b>esq;t3snb1VovGo+*sMXH)NnPjxMY(xYxqgA zHEY-frk^!vPYpjUxHah0tl?+q+C!<~8ZdLvo_*VcoPE2Cv9fPl;M%@J{m$7WWU(E{ z7m${SD$0ghI>n2 zbDmFJTi%h~;N~mOzT1O5gwJ}Yd0$=!t}W~M8)SYpX}gg6d~<$_tnHU&-u5DkwXvDQ z4ahd{N^RFeV&} z-0OAFeitO}xXRY#yKpzMHF>5S!_JM25J^wdk^Rxx~d%=W*{i9$EHGW^;17?5y zwKwnUd(n-hPu6EYvbbcEIbLEsP;AZn`T&@I-q+f*77rHOwa}+|Uq6JdJ(T^V{i4 zu&0s5cOqX+#om3-po@D3UxeE{gW}IZVy?Zi-$ah{JhJ0lg`9C-Ko@r(rjI<|NG~Bf zrh7 z*0ni?W5qtR*!@xpQv4ADtJ!gY?OLYG2pY96pZhcZN3O zxn^1OLG&?nZCfF2uK7H0n`^F*@0ghJ<;Kd}CV!K^x{c*GjCJ__8P`12?UQ?BIX1tE z^mUvC=#DG5F28+NBKvKTy5<$1W8nF=ScTmDwm1#lamBMvXCUX>v$;;I!L?md_E!g4 z%=^iAYAy0Lkl!8oPFRP`ul~Oz?YSF=knOKMzUz_sRSO{Px%Y;Voe%Bq|1HR$LiXKhwYTxj8$JTAE%~2~Y=7~*-JeA^rZ(Rf z=j3z9{OY$}`(`NT>GR0iZNbhdm~gOj3uf;58)plc{q@)G{N(RUZSIlt;qDRnVY2uH F^dHqhR&@XX literal 6276 zcmZvgYmA;%5yzkHzU?mTwwBnUrLcL+5~0v8)!2d}c9*uh6`R<)1SRo>R#H*< z2F9pOjF*-#Bqk)@K`52tR4hojwN((1p#FZ(d;Xgv@#LM(%zx&b znK`%TZ2M=84m5N6nxSTHGd8Pf+bPYgrVmMe`+^?7qwqW7L(QDlKDA+b!`LUU-ZJ*~ zl`HHxujz05+xHF4aMOWHTQ^_1O~W#19kdrZ06h#n1N{S$x3Y%T9jV;z` zZRM9b*H>8T+)!buv$yWO7+KTHDnD~Gmk(ChV8K2Fx5h*8eVh^H2P^rZ8M)K$`w?`T zen;WvZ+*(Px@NJ|oPC!*`gNi7P0cI7TqDnyYi91zT@(G(J$rR7#|V`Ck5}@EBEOML z&222q`JKo=x!?;cV;@I1$J6zE}Qm!J+bvqP4|J-bGskim?L~0y^mZ=_ef3VESaa|*!ROv zg6v~#^#cXdez3^)Q~xvEIL@2?>V8MX%*}C+K*#=b?dlPeE^fBK@3kI9P9xIq>Zp6!b@GzvWHs|33^mi`*0vY2l zlrbMi7PsAvpY!zuBz^?CiCn~&TIN!2_|0m{TRBqeK(T7bN_L4$JAz>+T080!?%Jh zgO5SZk>`3FnD|b7?0*%qnB%;!8En^iJF>WBlew3(wpSOMdwQ6eckVw0re6orK1_b% zpDy@0#mDbR_S0vuX%FonC->PkVCJAbb$$^!b-IVFGc|k(n>FZ@8orDyF4<&t4PPy` zx`wZT>1PevQ^U@JTZ2Az4ZF~_hf>4!VCJAb`?eoB`}Ri0%D(M@Yx`{3BeRjkcHpy( zY`lm0Gp7+BW{uPbk@?No_9G7fxEF`u?nSx#+;vz^@$L`z=h@)%D%)GId1q-Gg~w)` zMd+S0ZA(kPACRYK=M;49#{DOG&MkIK?YYrD42heM@}AOn0kVCq!}E43JpHsSgv9Kp zY+V^g{1e2puG5emCv&$LUCgkDEqC}gz|B{lHUB2^I(#Of`mXvGxVD_1ZzJ<-N!wQH^S=KMvbI~wynPp0tc%SY zZbG(sPigxeBxWAU?S3Fnb#e2$4F8*v&1)L!LheQ5eIHz#^{d~4%&)yiv~`ikA@3O9 zQ@4VpKF9D**$pP1vv?b_m}HYVb=K}2?*R~V9DOr)KSUPW4Ec@BdHE5zww%v>$ov}5 zr#AQbSIEz?Z%220ecU5rcObj2+MJ&s!<`?w=f%2DBSB+2X9LKyp}l1d?gZ2B+_>)t ziyilN9kkyCi94>cHF+oBjciSxDaWuT@8zF@Ys(#b53+gMg56s%;b1>27(>n6l|Kiw zzy8|myYd(4#?mM2^GjrL$tH8W#Q0UQ)pzBu!SwU4)Sk8YO~G9Yed@dNx9HkKxhwaB znS=Jk{2j8mE#Ji7qlhKJ^?4Q&Gp=&(wdasM zU*2oEGd$1FgKNvX`53a8_n7bI;{{~TY`z&Up=YmX%X|1` zWczB*e7u4zZVUD*`s?TDH864gZzig7|BWo}yt!t%A1S@9^**rFs(+tz1$l?7&qAg< ze`ED*j^S9b_ZPeGL~-NU$2H0w=3J|L4?5@a>zF&|*89Cht8)swl zO{A~m%tLn^xpn#WS&Ho2B6STFpSke7EtVq>LU~(^Av>;k)@cQD-hK5ty%k*BwPk<3 z4Oz_l$?w!kGBb(sv5&3Im HaUt|yqVPlJ diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json index 9e1b750d..45208862 100644 --- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json +++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json @@ -92,7 +92,7 @@ }, { "stage": "frag", - "sourceSha256": "8d6177707a95cf0230881bbfb7467c0591632eb67dce3c758596c83cafb14ffc", + "sourceSha256": "0195fa5fdfb3850e090bed6f144981d454edfed526b79efe8dd5300b610b2f0f", "compiled": true } ]