From 099ce101861a5b24be245c50b3eb9b06e99b5877 Mon Sep 17 00:00:00 2001 From: Jessica_Natalia Date: Thu, 13 Aug 2026 16:57:15 -0300 Subject: [PATCH] volumetric clouds update 3 volumetric clouds update 3 --- docs/VCS_CLOUDWORKS_GAME_INTEGRATION.md | 79 +++ profiles/vcs/config/ProperShaders.ini | 47 +- .../vcs/host/ge_cloudworks_present_shader.hpp | 483 +++++++----------- profiles/vcs/host/ge_gpu_backend_dx12.cpp | 259 ++++++++-- profiles/vcs/host/vcs_config.cpp | 64 ++- profiles/vcs/host/vcs_config.hpp | 41 +- profiles/vcs/tests/vcs_config_tests.cpp | 24 +- 7 files changed, 634 insertions(+), 363 deletions(-) create mode 100644 docs/VCS_CLOUDWORKS_GAME_INTEGRATION.md diff --git a/docs/VCS_CLOUDWORKS_GAME_INTEGRATION.md b/docs/VCS_CLOUDWORKS_GAME_INTEGRATION.md new file mode 100644 index 0000000..2cc15b2 --- /dev/null +++ b/docs/VCS_CLOUDWORKS_GAME_INTEGRATION.md @@ -0,0 +1,79 @@ +# CloudWorks: integração 1:1 com o estado do GTA VCS + +## Estado atual + +O shader usa os três perfis físicos, march adaptativo, sombras, extinção, +atmosfera e composição front-to-back do CloudWorks/ProperShaders. A câmera vem +do estado autoritativo do jogo (`TheCamera + 0x9B0`) e o passe é inserido no +world target depois dos opacos e antes do primeiro grupo equivalente a +`FadingEntities`. + +Os valores que o ProperShaders de San Andreas obtém do timecycle e de +`CWeather` ainda são placeholders em `ProperShaders.ini`. Isso é intencional: +ligar um endereço não confirmado ao shader produz mudanças de clima erradas ou +leituras inválidas e não constitui uma integração 1:1. + +## Fontes que precisam ser identificadas no guest + +Para substituir cada placeholder, localizar no ELF do VCS e validar em runtime: + +| Campo do INI | Fonte conceitual do ProperShaders/SA | Evidência necessária no VCS | +|---|---|---| +| `CoverageLow/Mid/High` | grupos de clima atual/anterior e interpolação | observar transições sunny/cloudy/rain/fog e reconstruir os três pesos | +| `SunDirection*` | direção real do sol ou lua | vetor unitário acompanhando hora; confirmar eixos do mundo do VCS | +| `DayProgression` | componente vertical da direção solar | faixa e pontos de troca sol/lua confirmados durante um ciclo completo | +| `SunColor*` | cor da corona/timecycle, modulada pelo clima | identificar RGB linear versus 0–255 e interpolação old/new weather | +| `CloudBaseColor*` | cor da base das nuvens do timecycle | separar da cor do céu/horizonte e validar dia, pôr do sol, noite e chuva | +| `AtmosphereDensity` | intensidade de chuva | localizar intensidade interpolada, não apenas o ID do clima | +| `Mist` | chuva, neblina e cloudiness combinadas | validar as três intensidades continuamente interpoladas | +| `FogColor*` | cor inferior do céu/horizonte | confirmar espaço de cor e ordem RGB | +| `FogStart` | plano inicial de fog da câmera | comparar com comandos GE `FOG1/FOG2` e o far clip do frame | +| `Speed` | tempo em segundos e vento | relógio monotônico do jogo + intensidade de vento interpolada | +| `RandomSeed` | seed de início/save, limitado e convertido em fase | escolher estatística estável e aplicar wrap em `2*pi` | + +O relógio já tem uma pista confirmada: `gp + 0x1DE0` (hora) e +`gp + 0x1DE1` (minuto), usados por Project2DFX. Isso ajuda a localizar as +rotinas adjacentes de `CClock`, mas não substitui a direção solar contínua. + +## Método de investigação + +1. Encontrar referências às variáveis de hora conhecidas no AOT/disassembly e + seguir os consumidores que atualizam iluminação, céu e clima. +2. Instrumentar somente candidatos concretos, no máximo uma vez por frame, em + um log contendo hora, weather IDs/interpolação, vetores e cores. +3. Capturar uma rota reproduzível cobrindo meio-dia, pôr do sol, noite, + amanhecer, chuva e neblina. Confirmar continuidade e faixa de cada campo. +4. Criar um `VcsCloudWeatherState` lido do `GuestMemory` no mesmo ponto em que a + câmera autoritativa é capturada. Nunca ler guest memory no pixel/draw hot path. +5. Manter fallback por campo: valor guest validado quando finito e dentro da + faixa; caso contrário, placeholder do INI e aviso limitado no log. +6. Comparar screenshots pareadas contra o ProperShaders com as mesmas entradas + numéricas antes de ajustar qualquer constante artística. + +## Renderização e ordem + +O PSP não expõe um marcador chamado `RenderFadingEntities`. A fronteira atual é +inferida pelo estado GE: após geometria do world target com depth test+write, +sem blend/alpha test, o primeiro draw com blend, alpha test ou depth sem write é +tratado como o início dos objetos transparentes. O passe de nuvens é gravado +imediatamente antes desse draw, com depth `EQUAL` ao clear reverso (zero), para +preencher somente céu e permitir que árvores/grades/partículas sejam desenhadas +por cima. + +Essa heurística deve ser validada em interiores, água, chuva, reflexos e efeitos +de missão. Se houver falsos limites, o passo seguinte é identificar o PC guest +da função que emite o primeiro draw de fading e transportar um marcador de fase +para o backend GE. + +## Critérios para considerar a integração concluída + +- Nenhum parâmetro visual de clima permanece obrigatório no INI. +- Transições old/new weather são contínuas e não saltam entre frames. +- Sol, lua, fog e base das nuvens acompanham o timecycle em um ciclo de 24 h. +- Nuvens ficam fixas no mundo durante rotação e translação da câmera. +- Opacos ocluem as nuvens; fading/folhagem/partículas são compostos por cima. +- Interior e cutscene não reutilizam uma câmera/target auxiliar. +- Um toggle desativa o recurso sem alterar a imagem original. +- Capturas A/B com entradas iguais reproduzem o ProperShaders sem ajustes + específicos feitos apenas para uma screenshot. + diff --git a/profiles/vcs/config/ProperShaders.ini b/profiles/vcs/config/ProperShaders.ini index b4cc371..1d10fbb 100644 --- a/profiles/vcs/config/ProperShaders.ini +++ b/profiles/vcs/config/ProperShaders.ini @@ -1,10 +1,43 @@ -; Standalone CloudWorks sky for VCSNative. This does not use the game's -; timecycle or weather. Set Enabled=false to restore the unmodified game sky. +; CloudWorks/ProperShaders port for VCSNative. Set Enabled=false to restore the +; unmodified game sky. Parameters marked PLACEHOLDER are the exact inputs that +; ProperShaders obtains from San Andreas timecycle/weather; they stay fixed here +; until their VCS guest-side sources are connected. [VolumetricClouds] Enabled=true -MarchSteps=24 -Coverage=0.72 -Opacity=0.84 -; Keep the density field completely static while validating camera anchoring. -; A small positive value can be restored after the camera test is confirmed. +DownscaleDiv=2 +Layers=3 +ShadowSteps=4 +Opacity=1.0 Speed=0.0 +Brightness=1.0 +RandomSeed=0.0 + +; PLACEHOLDER: weather-derived coverage for each physical cloud deck. +CoverageLow=0.72 +CoverageMid=0.52 +CoverageHigh=0.28 + +; PLACEHOLDER: timecycle sun/moon state. +SunDirectionX=0.38 +SunDirectionY=-0.28 +SunDirectionZ=0.88 +SunColorR=1.0 +SunColorG=0.97 +SunColorB=0.88 +DayProgression=0.88 + +; PLACEHOLDER: timecycle cloud/fog colours and weather density. +CloudBaseColorR=0.70 +CloudBaseColorG=0.70 +CloudBaseColorB=0.70 +AtmosphereDensity=0.0 +Mist=0.50 +FogColorR=0.58 +FogColorG=0.68 +FogColorB=0.78 +FogStart=4500.0 + +; ProperShaders temporal resolve controls. +TemporalBlend=0.50 +TemporalDenoise=1.0 +TemporalClamp=1.0 diff --git a/profiles/vcs/host/ge_cloudworks_present_shader.hpp b/profiles/vcs/host/ge_cloudworks_present_shader.hpp index 8a78196..3eb25c9 100644 --- a/profiles/vcs/host/ge_cloudworks_present_shader.hpp +++ b/profiles/vcs/host/ge_cloudworks_present_shader.hpp @@ -2,322 +2,199 @@ namespace vcs { -// Standalone, single-present-pass adaptation of the low CloudWorks Alpha 4.0 -// cloud profile by Brian Tu (RTU). CloudWorks is licensed CC BY-NC-SA 3.0. -// -// Root-constant contract (b1, 21 DWORDs total): -// 0..3 unused draw-state padding -// 4 unused draw-state padding -// 5..7 CloudRayRight.xyz (NDC-x world-ray coefficient) -// 8 CloudTime (seconds * configured speed) -// 9..11 CloudRayUp.xyz (NDC-y world-ray coefficient) -// 12 CloudCoverage [0, 1] -// 13..15 CloudRayForward.xyz (world ray at NDC 0,0) -// 16 CloudOpacity [0, 1] -// 17..19 CloudCameraPosition.xyz (world space, Z up) -// 20 CloudSettings: bits 0..7 = march steps [4,64], bit 8 = enabled -// -// CloudRayRight/Up are deliberately not normalized camera axes. The host must -// bake inverse projection into them. CloudRayForward must come directly from -// unprojection (never cross(right, up)); this keeps the noise field fixed in -// world space even for a reflected/scaled GE view basis. +// Direct SM5.1 port of the CloudWorks implementation used by ProperShaders. +// Density profiles, adaptive integration, sun shadow march, Beer-Lambert +// extinction, Mie/Rayleigh lighting and front-to-back layer composition retain +// the source equations and constants. The only platform adaptation is the +// fullscreen input/ray contract used by VCSNative's DX12 backend. +// CloudWorks: Brian Tu (RTU), CC BY-NC-SA 3.0. inline constexpr char kCloudWorksPresentShaderHlsl[] = R"CLOUD_HLSL( Texture2D SourceTexture : register(t0); SamplerState SourceSampler : register(s0); -cbuffer CloudPresentState : register(b1) { - uint4 UnusedDrawState0; - uint UnusedDrawState1; - float3 CloudRayRight; - float CloudTime; - float3 CloudRayUp; - float CloudCoverage; - float3 CloudRayForward; - float CloudOpacity; - float3 CloudCameraPosition; - uint CloudSettings; -}; - -struct PresentVertexOutput { - float4 position : SV_POSITION; - float2 uv : TEXCOORD0; +cbuffer CloudState : register(b0) { + float3 CloudRayRight; float g_Time; + float3 CloudRayUp; float randomSeed; + float3 CloudRayForward; float g_Opacity; + float3 CloudCameraPosition; uint g_Settings; + float3 g_CloudCoverage; float g_CloudSpeed; + float3 vSunLightDir; float fDayProgression; + float3 g_vSunColor; float g_AtmDense; + float3 g_vCloudBaseColor; float g_Mist; + float3 g_FogColor; float g_FogDens; + float g_Brightness; float3 CloudPadding; }; +struct PresentVertexOutput { float4 position : SV_POSITION; float2 uv : TEXCOORD0; }; PresentVertexOutput PresentVS(uint id : SV_VertexID) { - PresentVertexOutput output; - if (id == 0u) { - output.position = float4(-1.0, -1.0, 0.0, 1.0); - output.uv = float2(0.0, 1.0); - } else if (id == 1u) { - output.position = float4(-1.0, 3.0, 0.0, 1.0); - output.uv = float2(0.0, -1.0); - } else { - output.position = float4(3.0, -1.0, 0.0, 1.0); - output.uv = float2(2.0, 1.0); - } - return output; + PresentVertexOutput o; + if (id == 0u) { o.position=float4(-1,-1,0,1); o.uv=float2(0,1); } + else if (id == 1u) { o.position=float4(-1,3,0,1); o.uv=float2(0,-1); } + else { o.position=float4(3,-1,0,1); o.uv=float2(2,1); } + return o; +} +float4 PresentPS(PresentVertexOutput i) : SV_TARGET { + return SourceTexture.SampleLevel(SourceSampler, i.uv, 0.0); } -// CloudWorks' original scalar sine hash and linearly interpolated value noise. -float CwHash(float value) { - return frac(sin(value / 1873.1873) * 1618.03398875); +static const float VC_HASH_MUL = 1332.03398875; +float hash(float n) { return frac(sin(n / 1873.1873 + randomSeed) * VC_HASH_MUL); } +float noise2d(float3 p) { + float3 fr=floor(p), ft=frac(p); float n=1153*fr.x+2381*fr.y+p.z; + float nr=n+1153, nd=n+2381, no=nr+2381; + return lerp(lerp(hash(n),hash(nr),ft.x),lerp(hash(nd),hash(no),ft.x),ft.y); +} +float noise3d(float3 p) { + float3 fr=floor(p), ft=frac(p); float n=1153*fr.x+2381*fr.y+fr.z; + float nr=n+1153, nd=n+2381, no=nr+2381; + float v=lerp(hash(n),hash(n+1),ft.z), vr=lerp(hash(nr),hash(nr+1),ft.z); + float vd=lerp(hash(nd),hash(nd+1),ft.z), vo=lerp(hash(no),hash(no+1),ft.z); + return lerp(lerp(v,vr,ft.x),lerp(vd,vo,ft.x),ft.y); +} +float map(float x,float a,float b,float c,float d) { return (x-b)/(a-b)*(c-d)+d; } +float clampMap(float x,float a,float b,float c,float d) { return saturate((x-b)/(a-b))*(c-d)+d; } + +static const float atmosphereStep=15.0, lightStep=3.0, fix=0.00001; +static const float3 sunLightStrength=685.0*float3(1.0,0.96,0.949); +static const float LightingDecay=300.0, rayleighStrength=1.85, rayleighDecay=900.0; +static const float3 waveLengthFactor=float3(6.5*6.5*6.5*6.5,5.4*5.4*5.4*5.4,4.5*4.5*4.5*4.5); +static const float3 scatteringFactor=waveLengthFactor/rayleighDecay; +static const float earthRadius=6.421, groundHeight=6.371, game2atm=1000000.0; +static const float3 AtmOrigin=float3(0,0,groundHeight); +static const float4 earth=float4(0,0,0,earthRadius); +float3 Game2Atm(float3 p) { return p/game2atm+AtmOrigin; } +float3 Game2Atm_Alt(float3 p) { return float3(0,0,p.z/game2atm)+AtmOrigin; } +float4 sphereCast(float3 origin,float3 ray,float4 sphere,float steps,out float3 begin) { + begin=origin; + float3 p=origin-sphere.xyz; float r=length(p), d=length(cross(p,ray)); + if(d>sphere.w+fix){begin=0;return 0;} float sr=sqrt(sphere.w*sphere.w-d*d),dr=-dot(p,ray); + float3 pc=origin+ray*dr,pf=pc+ray*sr,pb=pc-ray*sr; float sl; + if(r>sphere.w){begin=pb;sl=sr*2/steps;}else{begin=origin;sl=length(pf-origin)/steps;} + return float4(ray*sl,sl); +} +float2 Density(float3 pos,float4 sphere,float strength,float condense) { + float atmDensity=3.0+g_AtmDense,fogDensity=0.25+g_Mist; + float r=groundHeight,h=length(pos-sphere.xyz)-r,ep=exp(-(sphere.w-r)*condense); + float fog=fogDensity*(1.0/(1200.0*h+0.5)-0.04)/1.96; + if(h<0)return float2(strength,fogDensity); + return float2((exp(-h*condense)-ep)/(1.0-ep)*strength,fog); +} +float3 rayleighScattering(float c){return (1+c*c)*rayleighStrength/waveLengthFactor;} +float MiePhase(float c){return 1.0+1.6*exp(20.0*(c-1.0));} +float MieScattering(float c){return (0.125+g_Mist*0.1)*MiePhase(c);} +float3 LightDecay(float r,float m){return exp(-r/scatteringFactor-m*100.0);} +float3 SunLight(float3 light,float3 position,float3 lightDirection,float4 sphere){ + float3 smp=position;float4 sms=sphereCast(position,lightDirection,sphere,lightStep,smp);float2 dl=0; + [unroll]for(int j=0;j<3;j++){smp+=sms.xyz/2;dl+=Density(smp,sphere,3.0+g_AtmDense,1.0)*sms.w;smp+=sms.xyz/2;} + return light*LightDecay(dl.x,dl.y)/LightingDecay; +} +float3 LightSource(float3 active,float day,float3 color,out float3 source){ + source=active; + if(day<-0.2)return sunLightStrength*smoothstep(0.1,0.3,-day)*color; + return sunLightStrength*smoothstep(-0.2,-0.1,day)*color; +} +float3 AtmosphereScattering(float3 background,float3 marchPos,float4 marchStep,float3 ray, + float3 lightStrength,float3 lightDirection,float strength,float4 sphere){ + float3 intensity=0;float ang=dot(ray,lightDirection),mie=MieScattering(ang);float3 raylei=rayleighScattering(ang); + if(marchStep.w>0.015)marchStep/=marchStep.w/0.015;float2 dv=0; + [loop]for(int i=0;i<15;i++){float3 smp=marchPos;float4 sms=sphereCast(marchPos,lightDirection,sphere,lightStep,smp); + float2 sampling=Density(marchPos,sphere,3.0+g_AtmDense,1.0)*marchStep.w;dv+=sampling/2;float2 dl=dv; + [unroll]for(int j=0;j<3;j++){smp+=sms.xyz;dl+=Density(smp,sphere,3.0+g_AtmDense,1.0)*sms.w;} + intensity+=LightDecay(dl.x,dl.y)*(raylei*sampling.x+mie*sampling.y);dv+=sampling/2;marchPos+=marchStep.xyz;} + return lightStrength*intensity*strength+background*LightDecay(dv.x,dv.y); +} +float3 atmosphere_scattering(float strength,float3 color,float3 camera,float3 ray,float distance,float3 sunDirection,float4 sphere){ + if(distance<200)return color;float fade=smoothstep(200,300,distance);float4 step=0; + step.w=15.0*distance/atmosphereStep/game2atm;step.xyz=ray*step.w;float3 lightDir=sunDirection; + float3 light=LightSource(sunDirection,fDayProgression,1.0,lightDir); + float3 scattered=AtmosphereScattering(color,Game2Atm_Alt(camera),step,ray,light,lightDir,1.0,sphere); + return lerp(color,scattered,fade); } -float CwNoise2(float3 p) { - float3 cell = floor(p); - float3 f = frac(p); - float n = 1153.0 * cell.x + 2381.0 * cell.y + p.z; - float right = n + 1153.0; - float down = n + 2381.0; - float opposite = right + 2381.0; - return lerp(lerp(CwHash(n), CwHash(right), f.x), - lerp(CwHash(down), CwHash(opposite), f.x), f.y); -} +struct CloudBaseColor {float3 BaseColor;float3 BaseColor_Day;float3 BaseColor_Sunset;}; +struct CloudProfile { + float4 march;float2 cutoff;float2 volumeBox;float4 shape;float brightness;float3 range; + float2 solidness;float2 densityChunk;float4 shadow;float4 distortion;float fade; + float3 densityDetail;float3 scaleChunk;float3 scaleDetail;float3 cloudShift; + float3 offsetA;float3 offsetB;float3 offsetC;float3 offsetD; +}; +float gameTime(){return 1000.0+g_Time*g_CloudSpeed/100.0;} +float3 PosOnPlane(float3 o,float3 d,float h,inout float dist){dist=(h-o.z)/d.z;return o+d*dist;} +float4 CloudShape(float z,float4 shape,float3 range){float soft=map(z,shape.y,shape.x,range.z,range.y); + return float4(smoothstep(shape.z,lerp(shape.y,shape.z,shape.w),z)*smoothstep(shape.x,lerp(shape.y,shape.x,shape.w),z),range.x+soft,range.x-soft,soft);} +float3 DistortionVec(float lump,float4 d){return float3(cos(lump*d.x)*d.y,0,-lump*d.z);} +float Chunk(float3 pos,float2 density,float3 scale,float3 shift,float3 oA,float3 oB,float cs){ + pos.z/=scale.z;pos+=shift*pos.z;float3 pA=(pos+oA)*scale.x,pB=(pos+oB)*scale.y; + return noise3d(pA)*(noise3d(pB)*density.y+density.x)*cs;} +float DetailA(float3 pos,float3 density,float3 scale,float3 oC,float3 dist){return density.x*noise3d((pos+oC+dist)*scale.x);} +float DetailB(float lump,float3 pos,float3 density,float3 scale,float4 dp,float3 oC,float3 oD,float cs){ + float3 d=DistortionVec(lump,dp),pD=pos+oD;float dens=DetailA(pos,density,scale,oC,d);d.z-=dens*dp.w; + dens+=density.y*noise3d((pD+d/3)*scale.y);dens+=dens*density.z*noise3d((pD+d*8)*scale.z);return dens;} +float GetDensity(float df,float height,float low,float high,float2 vb,float2 sol){return clampMap(df,low,high,0,clampMap(height,vb.y,vb.x,sol.y,sol.x));} +float ShadowMarching(float dens,float3 p,CloudProfile a,float3 threshold,float3 sunDir){ + if(dens<=0.025)return dens*a.shadow.x;float stepLen=a.shadow.x*2.0; + float limit=2.0/a.shadow.w/stepLen,d=0;float4 st=float4(sunDir*stepLen,stepLen); + uint shadowSteps=(g_Settings>>8)&15u; + [loop]for(uint i=0;i<8;i++){if(i>=shadowSteps||d>=limit||p.z>=a.volumeBox.x||p.z<=a.volumeBox.y)break; + p+=st.xyz;float4 cs=CloudShape(p.z,a.shape,threshold);float d1=Chunk(p,a.densityChunk,a.scaleChunk,a.cloudShift,a.offsetA,a.offsetB,cs.x); + float d2=DetailA(p,a.densityDetail,a.scaleDetail,a.offsetC,DistortionVec(d1,a.distortion))*a.shadow.y; + d+=GetDensity(d1*d2+d1,p.z,cs.z-a.shadow.z,cs.y,a.volumeBox,a.solidness);} + return d*a.shadow.w*st.w;} -float CwNoise3(float3 p) { - float3 cell = floor(p); - float3 f = frac(p); - float n = 1153.0 * cell.x + 2381.0 * cell.y + cell.z; - float right = n + 1153.0; - float down = n + 2381.0; - float opposite = right + 2381.0; - float a = lerp(CwHash(n), CwHash(n + 1.0), f.z); - float b = lerp(CwHash(right), CwHash(right + 1.0), f.z); - float c = lerp(CwHash(down), CwHash(down + 1.0), f.z); - float d = lerp(CwHash(opposite), CwHash(opposite + 1.0), f.z); - return lerp(lerp(a, b, f.x), lerp(c, d, f.x), f.y); -} +float4 CloudAtRay(CloudProfile a,CloudBaseColor b,float3 dir,float3 cam,float3 light,float3 lightDir,float time,inout float distance){ + float4 d=float4(0,0,0,a.march.y);if(abs(dir.z)<1e-6)return float4(0,0,0,1); + float3 p=PosOnPlane(cam,dir,clamp(cam.z,a.volumeBox.y+0.001,a.volumeBox.x-0.001),d.x);d.y=d.x; + if(d.x>=0&&distance>d.x){a.range.x=1/a.range.x;float3 fx=float3(0,0,1);float last=0,pdf=0; + [loop]for(int i=0;i<64;i++){if(fx.z<=0||p.z>a.volumeBox.x||p.z=(int)a.march.w||d.x-d.w>=distance||d.x>=a.fade)break; + float3 cs=CloudShape(p.z,a.shape,a.range).xyz;float d1=Chunk(p,a.densityChunk,a.scaleChunk,a.cloudShift,a.offsetA,a.offsetB,cs.x); + float d2=DetailB(d1,p,a.densityDetail,a.scaleDetail,a.distortion,a.offsetC,a.offsetD,cs.x);float df=d1*d2+d1; + if(df>cs.z){float dens=GetDensity(df,p.z,cs.z,cs.y,a.volumeBox,a.solidness);float cd=(dens+last)*a.march.x/2;last=dens; + if(d.x>=distance)cd*=d.z/d.w;if(cd>0)d.y=d.y*(1-fx.z)+fx.z*d.x;fx.y+=cd;fx.z=(exp(-fx.y)-a.cutoff.y)/(1-a.cutoff.y);d.z=distance-d.x; + if(fx.y<2.3)fx.x+=cd*exp(-ShadowMarching(cd,p,a,a.range,lightDir)-fx.y);} + d.w=clampMap(2*df-pdf,cs.z*0.85,a.cutoff.x,a.march.x,a.march.y);d.w*=clampMap(d.x,0,a.fade,1,a.march.z); + d.w+=noise2d(p+g_Time)*a.march.x;pdf=df;p+=dir*d.w;d.x+=d.w;} + if(fx.z<1){fx=saturate(fx);float3 z=float3(0,0,cam.z);float3 cbright=SunLight(light,Game2Atm(z+dir*d.y),lightDir,earth)*a.brightness; + float3 C=cbright*fx.x*MiePhase(dot(lightDir,dir))+(b.BaseColor*g_vCloudBaseColor)*(1-fx.z); + C=atmosphere_scattering(1-fx.z,C,Game2Atm(z),dir,d.y/game2atm,lightDir,earth); + distance=distance*fx.z+d.y*(1-fx.z);return float4(C,fx.z);}} + return float4(0,0,0,1);} -float CwSmooth(float edge0, float edge1, float value) { - float x = saturate((value - edge0) / (edge1 - edge0)); - return x * x * (3.0 - 2.0 * x); -} - -// x = shape factor, y = high density threshold, z = low threshold. -float3 CwCloudShape(float height, float coverage) { - const float bottom = 300.0; - const float middle = 450.0; - const float top = 700.0; - float body = CwSmooth(0.0, middle, height) * - (1.0 - CwSmooth(middle, top, height)); - // The handheld sky is a very small target after upscale. Use the same - // CloudWorks profile but broaden its occupied threshold range so the deck - // reads as a cloud mass rather than a few isolated wisps. - float bottomRange = 0.24 + coverage * 0.52; - float soft = (height - top) / (middle - top) * - (bottomRange - 0.1) + 0.1; - float total = rcp(0.92 + coverage * 0.48); - return float3(body, total + soft, total - soft); -} - -float3 CwOffsetA(float flow) { return float3(-1.8, 1.0, 0.0) * flow; } -float3 CwOffsetB(float flow) { return float3(-2.0, -0.2, 0.0) * flow; } -float3 CwOffsetC(float flow) { return float3(-3.0, 0.0, -0.5) * flow; } -float3 CwOffsetD(float flow) { return float3(-3.5, 0.0, 0.1) * flow; } - -float CwChunk(float3 worldPosition, float shapeFactor, float flow) { - float3 p = worldPosition; - p += float3(-0.5, 0.0, 0.0) * p.z; - float largeNoise = CwNoise3((p + CwOffsetA(flow)) * 0.0008); - float smallNoise = CwNoise3((p + CwOffsetB(flow)) * 0.005); - return largeNoise * (smallNoise * 0.5 + 0.3) * shapeFactor; -} - -float3 CwDistortion(float lump) { - return float3(cos(lump * 1.6) * 60.0, 0.0, -lump * 8.0); -} - -float CwDetail(float lump, float3 worldPosition, float flow) { - float3 distortion = CwDistortion(lump); - float detail = 0.3 * CwNoise3( - (worldPosition + CwOffsetC(flow) + distortion) * 0.02); - distortion.z -= detail * 16.0; - float3 detailPosition = worldPosition + CwOffsetD(flow); - detail += 0.2 * CwNoise3((detailPosition + distortion / 3.0) * 0.04); - detail += detail * 0.6 * - CwNoise3((detailPosition + distortion * 8.0) * 0.1); - return detail; -} - -// x = raw density field, y = extinction density, z = low threshold. -float3 CwDensity(float3 worldPosition, float coverage, float flow) { - float3 shape = CwCloudShape(worldPosition.z, coverage); - float lump = CwChunk(worldPosition, shape.x, flow); - float field = lump * (1.0 + CwDetail(lump, worldPosition, flow)); - float solidness = lerp(0.0, 7.0 * coverage, - saturate((worldPosition.z - 300.0) / 400.0)); - float density = saturate((field - shape.z) / - max(shape.y - shape.z, 1.0e-4)) * solidness; - return float3(field, density * 1.45, shape.z); -} - -// One inexpensive approximation of CloudWorks' eight-sample sun shadow march. -// It retains the original Chunk + DetailA profile used by ShadowMarching. -float CwShadowDensity(float3 worldPosition, float coverage, float flow) { - float3 shape = CwCloudShape(worldPosition.z, coverage); - float lump = CwChunk(worldPosition, shape.x, flow); - float3 distortion = CwDistortion(lump); - float detail = 0.3 * CwNoise3( - (worldPosition + CwOffsetC(flow) + distortion) * 0.02) * 1.75; - float field = lump * (1.0 + detail); - float solidness = lerp(0.0, 5.0 * coverage, - saturate((worldPosition.z - 300.0) / 400.0)); - return saturate((field - (shape.z - 0.1)) / - max(shape.y - (shape.z - 0.1), 1.0e-4)) * solidness; -} - -float CwSkyMask(float2 uv, float3 source, float rayHeight) { - uint width = 1u; - uint height = 1u; - SourceTexture.GetDimensions(width, height); - float2 pixel = rcp(float2(max(width, 1u), max(height, 1u))); - float3 left = SourceTexture.SampleLevel(SourceSampler, - saturate(uv - float2(pixel.x, 0.0)), 0.0).rgb; - float3 right = SourceTexture.SampleLevel(SourceSampler, - saturate(uv + float2(pixel.x, 0.0)), 0.0).rgb; - float3 above = SourceTexture.SampleLevel(SourceSampler, - saturate(uv - float2(0.0, pixel.y)), 0.0).rgb; - float3 below = SourceTexture.SampleLevel(SourceSampler, - saturate(uv + float2(0.0, pixel.y)), 0.0).rgb; - float localEdge = max(max(length(source - left), length(source - right)), - max(length(source - above), length(source - below))); - float flatSky = 1.0 - CwSmooth(0.025, 0.12, localEdge); - float blueOverRed = CwSmooth(0.025, 0.16, source.b - source.r); - float blueOverGreen = CwSmooth(-0.04, 0.10, source.b - source.g); - float luminance = dot(source, float3(0.2126, 0.7152, 0.0722)); - float visibleSky = blueOverRed * blueOverGreen * - CwSmooth(0.12, 0.34, luminance); - float aboveHorizon = CwSmooth(0.035, 0.13, rayHeight); - return saturate(aboveHorizon * visibleSky * lerp(0.55, 1.0, flatSky)); -} - -// Returns premultiplied cloud radiance in rgb and remaining transmittance in a. -float4 CwMarchLowLayer(float3 rayOrigin, float3 rayDirection, - float coverage, uint marchSteps, float flow) { - float4 result = float4(0.0, 0.0, 0.0, 1.0); - const float cloudBottom = 300.0; - const float cloudTop = 700.0; - const float cloudFadeDistance = 6000.0; - float validDirection = rayDirection.z > 1.0e-4 ? 1.0 : 0.0; - float safeRayHeight = max(rayDirection.z, 1.0e-4); - float slabBegin = (cloudBottom - rayOrigin.z) / safeRayHeight; - float slabEnd = (cloudTop - rayOrigin.z) / safeRayHeight; - float rayBegin = max(min(slabBegin, slabEnd), 0.0); - float rayEnd = min(max(slabBegin, slabEnd), cloudFadeDistance); - rayEnd = validDirection > 0.5 ? rayEnd : rayBegin; - - // World-anchored jitter breaks coherent contours without producing a - // screen-space stipple pattern that turns with the camera. - float3 entryPosition = rayOrigin + rayDirection * rayBegin; - float nominalStep = (rayEnd - rayBegin) / max(float(marchSteps), 1.0); - float jitter = CwNoise3(entryPosition * float3(0.031, 0.031, 0.013) + 19.19); - float distanceAlongRay = rayBegin + jitter * min(nominalStep, 40.0); - - const float3 sunDirection = normalize(float3(0.38, -0.28, 0.88)); - const float3 baseColor = float3(0.27, 0.32, 0.40); - const float3 sunColor = float3(1.02, 1.00, 0.93); - float3 radiance = 0.0; - float transmittance = 1.0; - float previousField = 0.0; - float previousDensity = 0.0; - - [loop] - for (uint stepIndex = 0u; stepIndex < 64u; ++stepIndex) { - if (stepIndex >= marchSteps || distanceAlongRay >= rayEnd || - transmittance <= 0.02) break; - - float3 worldPosition = rayOrigin + rayDirection * distanceAlongRay; - float3 densitySample = CwDensity(worldPosition, coverage, flow); - - // CloudWorks' dynamic empty-space skipping: dense regions approach a - // five-unit step; empty regions approach 80 units, expanding with - // distance. Unlike the old uniform slab division, the sampled Z - // planes therefore cannot form screen-aligned slices. - float occupancy = saturate((2.0 * densitySample.x - previousField) / - max(densitySample.z * 0.85, 1.0e-4)); - float stepLength = lerp(80.0, 5.0, occupancy); - stepLength *= lerp(1.0, 8.0, - saturate(distanceAlongRay / cloudFadeDistance)); - stepLength += CwNoise2(worldPosition + float3(0.0, 0.0, flow)) * 5.0; - stepLength = min(stepLength, 160.0); - stepLength = min(stepLength, rayEnd - distanceAlongRay); - - if (densitySample.y > 1.0e-4 && stepLength > 0.0) { - // Trapezoidal Beer-Lambert integration makes opacity independent - // of the number of steps and avoids the saturated per-slice alpha - // produced by density * uniformStepLength. - float meanDensity = 0.5 * (previousDensity + densitySample.y); - float opticalDepth = meanDensity * min(stepLength, 80.0) * 0.018; - float segmentAlpha = 1.0 - exp(-opticalDepth); - - float shadowDensity = CwShadowDensity( - worldPosition + sunDirection * 60.0, coverage, flow); - float sunVisibility = exp(-shadowDensity * 1.35); - float forwardScatter = pow(saturate(dot(rayDirection, sunDirection)), 24.0); - float lighting = saturate(0.28 + sunVisibility * 0.72); - float3 cloudColor = lerp(baseColor, sunColor, lighting); - cloudColor += sunColor * forwardScatter * 0.10; - - radiance += transmittance * segmentAlpha * cloudColor; - transmittance *= 1.0 - segmentAlpha; - } - - previousField = densitySample.x; - previousDensity = densitySample.y; - distanceAlongRay += max(stepLength, 1.0); - } - - result = float4(radiance, saturate(transmittance)); - return result; -} - -float4 PresentPS(PresentVertexOutput input) : SV_TARGET { - float4 source = SourceTexture.SampleLevel(SourceSampler, input.uv, 0.0); - const uint enabledBit = 0x100u; - if ((CloudSettings & enabledBit) == 0u) return source; - - uint marchSteps = min(64u, max(4u, CloudSettings & 0xFFu)); - float2 ndc = float2(input.uv.x * 2.0 - 1.0, - 1.0 - input.uv.y * 2.0); - float3 rayDirection = normalize(CloudRayForward + - CloudRayRight * ndc.x + - CloudRayUp * ndc.y); - float skyMask = CwSkyMask(input.uv, source.rgb, rayDirection.z); - if (skyMask <= 1.0e-3) return source; - - // Preserve the existing INI Speed semantics while giving the low profile - // offsets a useful world-space velocity. - float flow = CloudTime * 25.0; - float4 clouds = CwMarchLowLayer(CloudCameraPosition, rayDirection, - saturate(CloudCoverage), marchSteps, flow); - float3 cloudComposite = clouds.rgb + source.rgb * clouds.a; - source.rgb = lerp(source.rgb, cloudComposite, - skyMask * saturate(CloudOpacity)); - return source; -} - -// World-target path. This is deliberately separate from PresentPS: its pixels -// are in the same coordinate system as the selected GE camera, and the D3D12 -// pipeline depth-tests against the world's untouched clear depth. The result is -// blended behind geometry before VCS scales/composites that target to display. -float4 CloudTargetPS(PresentVertexOutput input) : SV_TARGET { - const uint enabledBit = 0x100u; - if ((CloudSettings & enabledBit) == 0u) discard; - uint marchSteps = min(64u, max(4u, CloudSettings & 0xFFu)); - float2 ndc = float2(input.uv.x * 2.0 - 1.0, - 1.0 - input.uv.y * 2.0); - float3 rayDirection = normalize(CloudRayForward + - CloudRayRight * ndc.x + - CloudRayUp * ndc.y); - if (rayDirection.z <= 1.0e-4) discard; - float flow = CloudTime * 25.0; - float4 clouds = CwMarchLowLayer(CloudCameraPosition, rayDirection, - saturate(CloudCoverage), marchSteps, flow); - float alpha = saturate((1.0 - clouds.a) * CloudOpacity); - if (alpha <= 1.0e-4) discard; - // CwMarchLowLayer returns premultiplied radiance, so the matching PSO uses - // ONE / INV_SRC_ALPHA blending. - return float4(clouds.rgb * CloudOpacity, alpha); +CloudProfile BuildProfile0(float time,float coverage){CloudProfile p=(CloudProfile)0; + p.march=float4(5,80,8,64);p.cutoff=float2(0,.2);p.volumeBox=float2(900,500);p.shape=float4(900,650,0,0);p.brightness=.5; + p.range=float3(.9+coverage*.16,.1,.2+coverage*.4);p.solidness=float2(5,0)*coverage;p.densityChunk=float2(.3,.5); + p.shadow=float4(60,1.75,.1,.03);p.distortion=float4(1.6,60,8,16);p.fade=6000;p.densityDetail=float3(.3,.2,.6); + p.scaleChunk=float3(.0008,.005,1);p.scaleDetail=float3(.02,.04,.1);p.cloudShift=float3(-.5,0,0); + p.offsetA=float3(1.8,-1,0)*-time;p.offsetB=float3(2,.2,0)*-time;p.offsetC=float3(3,0,.5)*-time;p.offsetD=float3(3.5,0,-.1)*-time; + float grow=noise3d(float3(p.shape.x,p.volumeBox.y,time/2000))*.45+.65;p.range.x*=grow*(1-coverage)+coverage;return p;} +CloudProfile BuildProfile1(float time,float coverage){CloudProfile p=(CloudProfile)0; + p.march=float4(12,70,8,64);p.cutoff=float2(0,.2);p.volumeBox=float2(1900,1500);p.shape=float4(2100,1650,0,0);p.brightness=.5; + p.range=float3(.85+coverage*.78,0,.3+coverage*.16);p.solidness=float2(.35,.1)*coverage;p.densityChunk=float2(.25,.6); + p.shadow=float4(30,1,.15,.1);p.distortion=float4(6,50,100,50);p.fade=20000;p.densityDetail=float3(.5,.25,.5); + p.scaleChunk=float3(.0008,.004,1.5);p.scaleDetail=float3(.0142857,.0285714,.08);p.cloudShift=0; + p.offsetA=float3(1.5,-1.2,0)*-time;p.offsetB=float3(1.9,.5,0)*-time;p.offsetC=float3(2.5,0,.5)*-time;p.offsetD=float3(3,.1,-.1)*-time; + float grow=noise3d(float3(p.shape.x,p.volumeBox.y,time/2000))*.45+.65;p.range.x*=grow*(1-coverage)+coverage;return p;} +CloudProfile BuildProfile2(float time,float coverage){CloudProfile p=(CloudProfile)0; + p.march=float4(5,75,500,50);p.cutoff=float2(0,.2);p.volumeBox=float2(3600,3500);p.shape=float4(3800,3520,3450,0);p.brightness=.5; + p.range=float3(1+coverage*.9,.2,.35);p.solidness=float2(.25,0);p.densityChunk=float2(.4,.3); + p.shadow=float4(50,1.5,.02,.1);p.distortion=float4(2.5,15000,0,0);p.fade=2000000;p.densityDetail=float3(.2,.1,.6); + p.scaleChunk=float3(.00016,.0008,1.5);p.scaleDetail=float3(.004,.006667,.02);p.cloudShift=0; + p.offsetA=float3(1.3,-1.8,0)*-time;p.offsetB=float3(1.6,.8,0)*-time;p.offsetC=float3(2.5,.2,.5)*-time;p.offsetD=float3(3,.1,-.1)*-time; + float grow=noise3d(float3(p.shape.x,p.volumeBox.y,time/2000))*.45+.65;p.range.x*=grow*(1-coverage)+coverage;return p;} +CloudBaseColor GetCloudsColor(float3 sunDir){CloudBaseColor b=(CloudBaseColor)0;b.BaseColor=.2;b.BaseColor_Day=.2;b.BaseColor_Sunset=.2; + float night=smoothstep(.3,.1,fDayProgression),day=smoothstep(-.03,.05,fDayProgression),sunset=night*day; + b.BaseColor+=lerp(b.BaseColor_Day,b.BaseColor_Sunset,sunset)*day;return b;} +float4 RenderClouds(float3 dir,float3 cam){float time=gameTime();CloudBaseColor base=GetCloudsColor(vSunLightDir); + float3 lightDir=normalize(vSunLightDir);float3 light=LightSource(lightDir,fDayProgression,g_vSunColor,lightDir);float distance=100000; + float4 result=CloudAtRay(BuildProfile0(time,g_CloudCoverage.x),base,dir,cam,light,lightDir,time,distance); + uint layers=g_Settings&3u;if(layers>=3u&&result.w>.01){float4 mid=CloudAtRay(BuildProfile1(time,g_CloudCoverage.y),base,dir,cam,light,lightDir,time,distance);result.rgb+=mid.rgb*result.w;result.w*=mid.w;} + if(layers>=2u&&result.w>.01){float4 high=CloudAtRay(BuildProfile2(time,g_CloudCoverage.z),base,dir,cam,light,lightDir,time,distance);result.rgb+=high.rgb*result.w;result.w*=high.w;} + return result;} +float3 WorldRay(float2 uv){float2 ndc=float2(uv.x*2-1,1-uv.y*2);return normalize(CloudRayForward+CloudRayRight*ndc.x+CloudRayUp*ndc.y);} +float4 CloudTargetPS(PresentVertexOutput i):SV_TARGET {if((g_Settings&0x10000u)==0u)discard;float3 dir=WorldRay(i.uv);if(dir.z<=1e-6)discard; + float4 clouds=RenderClouds(dir,CloudCameraPosition);float alpha=saturate((1-clouds.a)*g_Opacity);if(alpha<=1e-4)discard; + return float4(clouds.rgb*g_Opacity*g_Brightness,alpha);} +float4 CloudCompositePS(PresentVertexOutput i):SV_TARGET { + return SourceTexture.SampleLevel(SourceSampler,i.uv,0.0); } )CLOUD_HLSL"; diff --git a/profiles/vcs/host/ge_gpu_backend_dx12.cpp b/profiles/vcs/host/ge_gpu_backend_dx12.cpp index 0c2670c..4b0609c 100644 --- a/profiles/vcs/host/ge_gpu_backend_dx12.cpp +++ b/profiles/vcs/host/ge_gpu_backend_dx12.cpp @@ -52,6 +52,7 @@ constexpr std::size_t kGeometryUploadCapacity = 64u * 1024u * 1024u; constexpr std::size_t kTextureUploadCapacity = 32u * 1024u * 1024u; constexpr DXGI_FORMAT kColorFormat = DXGI_FORMAT_R8G8B8A8_UNORM; constexpr DXGI_FORMAT kDepthFormat = DXGI_FORMAT_D32_FLOAT; +constexpr DXGI_FORMAT kCloudFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; constexpr UINT kFrameCount = 2u; constexpr UINT kSrvCapacity = 65536u; constexpr UINT kSamplerCapacity = 128u; @@ -133,13 +134,19 @@ struct CloudCameraCandidate { std::uint64_t occluding_weight{}; }; -struct CloudPresentConstants { +struct CloudShaderConstants { std::array ray_right_time{}; - std::array ray_up_coverage{}; + std::array ray_up_seed{}; std::array ray_forward_opacity{}; std::array camera_settings{}; + std::array coverage_speed{}; + std::array sun_direction_day{}; + std::array sun_color_atmosphere{}; + std::array cloud_color_mist{}; + std::array fog_color_start{}; + std::array brightness_padding{}; }; -static_assert(sizeof(CloudPresentConstants) == 16u * sizeof(std::uint32_t)); +static_assert(sizeof(CloudShaderConstants) == 40u * sizeof(std::uint32_t)); struct Dx12FrameResources { ComPtr allocator; @@ -198,6 +205,15 @@ struct Dx12RetiredSrv { UINT64 fence_value{}; }; +struct CloudRenderTarget { + ComPtr image; + std::uint32_t rtv_index{}; + std::uint32_t srv_index{}; + std::uint32_t width{}; + std::uint32_t height{}; + D3D12_RESOURCE_STATES state{D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE}; +}; + struct Dx12GeState { GeGpuBackendReport report{}; bool enabled{}; @@ -247,6 +263,7 @@ struct Dx12GeState { HANDLE fence_event{}; UINT64 next_fence{1u}; ComPtr root_signature; + ComPtr cloud_root_signature; ComPtr vertex_shader; ComPtr packed_0115_vertex_shader; ComPtr pixel_shader; @@ -275,9 +292,12 @@ struct Dx12GeState { std::uint32_t swap_height{}; ComPtr present_pipeline; ComPtr cloud_target_pipeline; + ComPtr cloud_composite_pipeline; ComPtr present_vertex_shader; ComPtr present_pixel_shader; ComPtr cloud_target_pixel_shader; + ComPtr cloud_composite_pixel_shader; + CloudRenderTarget cloud_render_target; bool direct_present_ok{}; std::uint32_t presented_framebuffer{}; std::uint32_t missed_display_intervals{}; @@ -1077,8 +1097,14 @@ float4 PSMain(VSOut input) : SV_TARGET { return false; } errors.Reset(); + // FXC emits X4000 false positives after aggressively inlining the original + // CloudWorks nested march/atmosphere functions (their out parameters are + // explicitly initialized). Keep warnings-as-errors for the host shaders, + // but compile this faithful large port with the same optimisation and let + // genuine HLSL errors remain fatal. + const UINT cloud_flags = D3DCOMPILE_OPTIMIZATION_LEVEL3; hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GECloudTarget", - nullptr, nullptr, "CloudTargetPS", "ps_5_1", flags, 0u, + nullptr, nullptr, "CloudTargetPS", "ps_5_1", cloud_flags, 0u, &s.cloud_target_pixel_shader, &errors); if (FAILED(hr)) { error = errors ? std::string(static_cast(errors->GetBufferPointer()), @@ -1086,6 +1112,16 @@ float4 PSMain(VSOut input) : SV_TARGET { : hr_text(hr, "D3DCompile(DX12 GE cloud target PS)"); return false; } + errors.Reset(); + hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GECloudComposite", + nullptr, nullptr, "CloudCompositePS", "ps_5_1", flags, 0u, + &s.cloud_composite_pixel_shader, &errors); + if (FAILED(hr)) { + error = errors ? std::string(static_cast(errors->GetBufferPointer()), + errors->GetBufferSize()) + : hr_text(hr, "D3DCompile(DX12 GE cloud composite PS)"); + return false; + } return true; } @@ -1234,6 +1270,55 @@ bool create_targets(Dx12GeState &s, std::string &error) noexcept { s.frame_rgba.clear(); s.readback_bytes = 0u; } + + if (vcs_configuration().volumetric_clouds.enabled) { + const std::uint32_t divisor = std::clamp( + vcs_configuration().volumetric_clouds.downscale_div, 1u, 8u); + CloudRenderTarget &cloud = s.cloud_render_target; + cloud.width = std::max(1u, (s.target_width + divisor - 1u) / divisor); + cloud.height = std::max(1u, (s.target_height + divisor - 1u) / divisor); + if (s.next_rtv >= kFramebufferTargetCapacity || s.next_srv >= kSrvCapacity) { + error = "DX12 GE descriptor capacity exhausted by CloudWorks target"; + return false; + } + cloud.rtv_index = s.next_rtv++; + cloud.srv_index = s.next_srv++; + D3D12_RESOURCE_DESC image{}; + image.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + image.Width = cloud.width; + image.Height = cloud.height; + image.DepthOrArraySize = 1u; + image.MipLevels = 1u; + image.Format = kCloudFormat; + image.SampleDesc.Count = 1u; + image.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + image.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + D3D12_HEAP_PROPERTIES heap_properties{}; + heap_properties.Type = D3D12_HEAP_TYPE_DEFAULT; + D3D12_CLEAR_VALUE clear{}; + clear.Format = kCloudFormat; + clear.Color[0] = clear.Color[1] = clear.Color[2] = clear.Color[3] = 0.0f; + hr = s.device->CreateCommittedResource( + &heap_properties, D3D12_HEAP_FLAG_NONE, &image, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, &clear, + IID_PPV_ARGS(&cloud.image)); + if (FAILED(hr)) { + error = hr_text(hr, "CreateCommittedResource(DX12 GE CloudWorks target)"); + return false; + } + s.device->CreateRenderTargetView(cloud.image.Get(), nullptr, + rtv_cpu(s, cloud.rtv_index)); + D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; + srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srv.Format = kCloudFormat; + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srv.Texture2D.MipLevels = 1u; + s.device->CreateShaderResourceView(cloud.image.Get(), &srv, + srv_cpu(s, cloud.srv_index)); + runtime_log_line("CloudWorks render target " + std::to_string(cloud.width) + "x" + + std::to_string(cloud.height) + " (DownscaleDiv=" + + std::to_string(divisor) + ")"); + } return true; } @@ -1702,6 +1787,35 @@ std::uint32_t present_sampler(Dx12GeState &s) noexcept { return ensure_sampler(s, draw); } +bool create_cloud_root_signature(Dx12GeState &s, std::string &error) noexcept { + D3D12_ROOT_PARAMETER parameter{}; + parameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; + parameter.Constants.ShaderRegister = 0u; + parameter.Constants.RegisterSpace = 0u; + parameter.Constants.Num32BitValues = 40u; + parameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + D3D12_ROOT_SIGNATURE_DESC desc{}; + desc.NumParameters = 1u; + desc.pParameters = ¶meter; + desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; + ComPtr blob, errors; + HRESULT hr = D3D12SerializeRootSignature(&desc, D3D_ROOT_SIGNATURE_VERSION_1, + &blob, &errors); + if (FAILED(hr)) { + error = errors ? std::string(static_cast(errors->GetBufferPointer()), + errors->GetBufferSize()) + : hr_text(hr, "D3D12SerializeRootSignature(DX12 GE clouds)"); + return false; + } + hr = s.device->CreateRootSignature(0u, blob->GetBufferPointer(), blob->GetBufferSize(), + IID_PPV_ARGS(&s.cloud_root_signature)); + if (FAILED(hr)) { + error = hr_text(hr, "CreateRootSignature(DX12 GE clouds)"); + return false; + } + return true; +} + bool invert_cloud_matrix(const std::array &matrix, std::array &inverse) noexcept { // Gauss-Jordan in double precision. GE projection matrices are small, but @@ -1753,7 +1867,7 @@ bool invert_cloud_matrix(const std::array &matrix, bool create_cloud_target_pipeline(Dx12GeState &s, std::string &error) noexcept { D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{}; - pso.pRootSignature = s.root_signature.Get(); + pso.pRootSignature = s.cloud_root_signature.Get(); pso.VS = {s.present_vertex_shader->GetBufferPointer(), s.present_vertex_shader->GetBufferSize()}; pso.PS = {s.cloud_target_pixel_shader->GetBufferPointer(), @@ -1763,6 +1877,35 @@ bool create_cloud_target_pipeline(Dx12GeState &s, std::string &error) noexcept { pso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; pso.RasterizerState.DepthClipEnable = TRUE; auto &blend = pso.BlendState.RenderTarget[0]; + blend.BlendEnable = FALSE; + blend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + pso.DepthStencilState.DepthEnable = FALSE; + pso.DepthStencilState.StencilEnable = FALSE; + pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + pso.NumRenderTargets = 1u; + pso.RTVFormats[0] = kCloudFormat; + pso.SampleDesc.Count = 1u; + const HRESULT hr = s.device->CreateGraphicsPipelineState( + &pso, IID_PPV_ARGS(&s.cloud_target_pipeline)); + if (FAILED(hr)) { + error = hr_text(hr, "CreateGraphicsPipelineState(DX12 GE cloud target)"); + return false; + } + return true; +} + +bool create_cloud_composite_pipeline(Dx12GeState &s, std::string &error) noexcept { + D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{}; + pso.pRootSignature = s.root_signature.Get(); + pso.VS = {s.present_vertex_shader->GetBufferPointer(), + s.present_vertex_shader->GetBufferSize()}; + pso.PS = {s.cloud_composite_pixel_shader->GetBufferPointer(), + s.cloud_composite_pixel_shader->GetBufferSize()}; + pso.SampleMask = UINT_MAX; + pso.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + pso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + pso.RasterizerState.DepthClipEnable = TRUE; + auto &blend = pso.BlendState.RenderTarget[0]; blend.BlendEnable = TRUE; blend.SrcBlend = D3D12_BLEND_ONE; blend.DestBlend = D3D12_BLEND_INV_SRC_ALPHA; @@ -1773,10 +1916,7 @@ bool create_cloud_target_pipeline(Dx12GeState &s, std::string &error) noexcept { blend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; pso.DepthStencilState.DepthEnable = TRUE; pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO; - // World targets are cleared to reverse-depth zero. Equality therefore - // restricts the fullscreen pass to pixels untouched by world geometry. pso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_EQUAL; - pso.DepthStencilState.StencilEnable = FALSE; pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; pso.NumRenderTargets = 1u; pso.RTVFormats[0] = kColorFormat; @@ -1784,9 +1924,9 @@ bool create_cloud_target_pipeline(Dx12GeState &s, std::string &error) noexcept { pso.SampleDesc.Count = s.sample_count; pso.SampleDesc.Quality = s.sample_quality; const HRESULT hr = s.device->CreateGraphicsPipelineState( - &pso, IID_PPV_ARGS(&s.cloud_target_pipeline)); + &pso, IID_PPV_ARGS(&s.cloud_composite_pipeline)); if (FAILED(hr)) { - error = hr_text(hr, "CreateGraphicsPipelineState(DX12 GE cloud target)"); + error = hr_text(hr, "CreateGraphicsPipelineState(DX12 GE cloud composite)"); return false; } return true; @@ -1832,8 +1972,8 @@ const CloudCameraCandidate *select_cloud_camera(const Dx12GeState &s) noexcept { return best; } -CloudPresentConstants cloud_present_constants(const Dx12GeState &s) noexcept { - CloudPresentConstants out{}; +CloudShaderConstants cloud_present_constants(const Dx12GeState &s) noexcept { + CloudShaderConstants out{}; const auto &config = vcs_configuration().volumetric_clouds; if (!config.enabled || s.cloud_cameras.empty()) return out; const CloudCameraCandidate *camera = select_cloud_camera(s); @@ -1910,22 +2050,56 @@ CloudPresentConstants cloud_present_constants(const Dx12GeState &s) noexcept { if (!std::isfinite(ray_right) || !std::isfinite(ray_up) || !std::isfinite(center_world[axis])) return {}; out.ray_right_time[axis] = static_cast(ray_right); - out.ray_up_coverage[axis] = static_cast(ray_up); + out.ray_up_seed[axis] = static_cast(ray_up); out.ray_forward_opacity[axis] = static_cast(center_world[axis]); out.camera_settings[axis] = camera->camera_position[axis]; } - out.ray_right_time[3] = - static_cast(s.frame_epoch) * (1.0f / 60.0f) * config.speed; - out.ray_up_coverage[3] = config.coverage; + out.ray_right_time[3] = static_cast(s.frame_epoch) * (1.0f / 60.0f); + out.ray_up_seed[3] = config.random_seed; out.ray_forward_opacity[3] = config.opacity; - const std::uint32_t settings = - std::clamp(config.march_steps, 4u, 64u) | 0x100u; + const std::uint32_t settings = std::clamp(config.layers, 1u, 3u) | + (std::clamp(config.shadow_steps, 2u, 8u) << 8u) | 0x10000u; out.camera_settings[3] = std::bit_cast(settings); + out.coverage_speed = {config.coverage_low, config.coverage_mid, + config.coverage_high, config.speed}; + out.sun_direction_day = {config.sun_direction_x, config.sun_direction_y, + config.sun_direction_z, config.day_progression}; + out.sun_color_atmosphere = {config.sun_color_r, config.sun_color_g, + config.sun_color_b, config.atmosphere_density}; + out.cloud_color_mist = {config.cloud_base_color_r, config.cloud_base_color_g, + config.cloud_base_color_b, config.mist}; + out.fog_color_start = {config.fog_color_r, config.fog_color_g, + config.fog_color_b, config.fog_start}; + out.brightness_padding[0] = config.brightness; return out; } void record_clouds_into_world_target(Dx12GeState &s, Dx12FramebufferTarget &target, - const CloudPresentConstants &clouds) noexcept { + const CloudShaderConstants &clouds) noexcept { + CloudRenderTarget &cloud = s.cloud_render_target; + if (!cloud.image) return; + transition(s.list.Get(), cloud.image.Get(), cloud.state, + D3D12_RESOURCE_STATE_RENDER_TARGET); + cloud.state = D3D12_RESOURCE_STATE_RENDER_TARGET; + const D3D12_CPU_DESCRIPTOR_HANDLE cloud_rtv = rtv_cpu(s, cloud.rtv_index); + s.list->OMSetRenderTargets(1u, &cloud_rtv, FALSE, nullptr); + constexpr float transparent[4]{0.0f, 0.0f, 0.0f, 0.0f}; + s.list->ClearRenderTargetView(cloud_rtv, transparent, 0u, nullptr); + D3D12_VIEWPORT cloud_viewport{0.0f, 0.0f, static_cast(cloud.width), + static_cast(cloud.height), 0.0f, 1.0f}; + D3D12_RECT cloud_scissor{0, 0, static_cast(cloud.width), + static_cast(cloud.height)}; + s.list->RSSetViewports(1u, &cloud_viewport); + s.list->RSSetScissorRects(1u, &cloud_scissor); + s.list->SetPipelineState(s.cloud_target_pipeline.Get()); + s.list->SetGraphicsRootSignature(s.cloud_root_signature.Get()); + s.list->SetGraphicsRoot32BitConstants(0u, 40u, &clouds, 0u); + s.list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + s.list->DrawInstanced(3u, 1u, 0u, 0u); + transition(s.list.Get(), cloud.image.Get(), cloud.state, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + cloud.state = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + prepare_target_for_render(s, target); const D3D12_CPU_DESCRIPTOR_HANDLE rtv = rtv_cpu(s, target.rtv_index); const D3D12_CPU_DESCRIPTOR_HANDLE dsv = dsv_cpu(s, target.dsv_index); @@ -1936,16 +2110,12 @@ void record_clouds_into_world_target(Dx12GeState &s, Dx12FramebufferTarget &targ static_cast(s.target_height)}; s.list->RSSetViewports(1u, &viewport); s.list->RSSetScissorRects(1u, &scissor); - s.list->SetPipelineState(s.cloud_target_pipeline.Get()); + s.list->SetPipelineState(s.cloud_composite_pipeline.Get()); s.list->SetGraphicsRootSignature(s.root_signature.Get()); ID3D12DescriptorHeap *heaps[]{s.srv_heap.Get(), s.sampler_heap.Get()}; s.list->SetDescriptorHeaps(2u, heaps); - s.list->SetGraphicsRootDescriptorTable(0u, srv_gpu(s, 0u)); - s.list->SetGraphicsRootDescriptorTable(1u, sampler_gpu(s, 0u)); - std::array constants{}; - std::memcpy(constants.data() + 5u, &clouds, sizeof(clouds)); - s.list->SetGraphicsRoot32BitConstants( - 3u, static_cast(constants.size()), constants.data(), 0u); + s.list->SetGraphicsRootDescriptorTable(0u, srv_gpu(s, cloud.srv_index)); + s.list->SetGraphicsRootDescriptorTable(1u, sampler_gpu(s, present_sampler(s))); s.list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); s.list->DrawInstanced(3u, 1u, 0u, 0u); } @@ -1981,7 +2151,7 @@ bool record_direct_present(Dx12GeState &s, Dx12FramebufferTarget &source, // Clouds are rendered into the selected 3D world target before VCS samples // it for composition. Applying them here would mix world-camera rays with // final-display pixels and make the layer follow the screen. - const CloudPresentConstants clouds{}; + struct LegacyCloudPresentConstants { std::array zero{}; } clouds{}; std::array present_constants{}; std::memcpy(present_constants.data() + 5u, &clouds, sizeof(clouds)); s.list->SetGraphicsRoot32BitConstants( @@ -2461,9 +2631,11 @@ bool create_backend(Dx12GeState &s, std::string &error) noexcept { if (s.fence_event == nullptr) { error = "CreateEventW failed for DX12 GE fence"; return false; } if (!compile_shaders(s, error)) return false; if (!create_root_signature(s, error)) return false; + if (!create_cloud_root_signature(s, error)) return false; if (!create_targets(s, error)) return false; if (!create_present_pipeline(s, error)) return false; if (!create_cloud_target_pipeline(s, error)) return false; + if (!create_cloud_composite_pipeline(s, error)) return false; s.vertices.reserve(262144u); s.packed_0115_vertices.reserve(2621440u); s.indices.reserve(524288u); @@ -2505,7 +2677,10 @@ void destroy_backend(Dx12GeState &s) noexcept { s.swap_rtv_heap.Reset(); s.present_pipeline.Reset(); s.cloud_target_pipeline.Reset(); + s.cloud_composite_pipeline.Reset(); s.cloud_target_pixel_shader.Reset(); + s.cloud_composite_pixel_shader.Reset(); + s.cloud_render_target = {}; s.present_pixel_shader.Reset(); s.present_vertex_shader.Reset(); s.pipelines.clear(); @@ -2520,6 +2695,7 @@ void destroy_backend(Dx12GeState &s) noexcept { s.packed_0115_vertex_shader.Reset(); s.vertex_shader.Reset(); s.root_signature.Reset(); + s.cloud_root_signature.Reset(); s.readback_buffer.Reset(); s.frame_targets.clear(); s.sampler_heap.Reset(); @@ -3312,21 +3488,36 @@ bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept { D3D12_PRIMITIVE_TOPOLOGY active_topology = D3D_PRIMITIVE_TOPOLOGY_UNDEFINED; bool touched_display = false; const CloudCameraCandidate *cloud_camera = select_cloud_camera(s); - const CloudPresentConstants clouds = cloud_present_constants(s); + const CloudShaderConstants clouds = cloud_present_constants(s); const std::uint32_t cloud_target_address = cloud_camera != nullptr ? cloud_camera->target : 0u; bool clouds_injected = false; + bool cloud_opaque_seen = false; constexpr float black[4]{0.0f, 0.0f, 0.0f, 1.0f}; for (const Dx12Batch &batch : s.batches) { const std::uint32_t address = batch.draw.framebuffer_address & 0x001FFFF0u; - // Insert immediately before the first non-self pass samples the chosen - // world target. At this point its geometry/depth are complete, while - // the later VCS composition has not consumed its color yet. - if (!clouds_injected && cloud_camera != nullptr && - batch.framebuffer_feedback && + const bool cloud_target_batch = address == cloud_target_address; + const bool opaque_world_batch = cloud_target_batch && !batch.draw.clear_mode && + batch.draw.depth_test_enabled && batch.draw.depth_write_enabled && + !batch.draw.blend_enabled && !batch.draw.alpha_test_enabled; + if (opaque_world_batch) cloud_opaque_seen = true; + // PSP has no named RenderFadingEntities marker in its GE stream, but + // the same boundary is visible in render state: after depth-writing, + // non-blended world geometry, fading/foliage starts using blend, alpha + // test, or depth-test without depth writes. Inject before that first + // batch so every transparent object composites over the clouds. + const bool fading_entities_boundary = cloud_target_batch && cloud_opaque_seen && + !batch.draw.clear_mode && + (batch.draw.blend_enabled || batch.draw.alpha_test_enabled || + (batch.draw.depth_test_enabled && !batch.draw.depth_write_enabled)); + // Conservative fallback for unusual frames without a recognizable + // fading pass: inject before the world target is sampled by composition. + const bool world_target_consumer = batch.framebuffer_feedback && (batch.feedback_address & 0x001FFFF0u) == cloud_target_address && - address != cloud_target_address) { + address != cloud_target_address; + if (!clouds_injected && cloud_camera != nullptr && + (fading_entities_boundary || world_target_consumer)) { if (Dx12FramebufferTarget *cloud_target = find_framebuffer_target(s, cloud_target_address); cloud_target != nullptr && cloud_target->color && cloud_target->depth) { diff --git a/profiles/vcs/host/vcs_config.cpp b/profiles/vcs/host/vcs_config.cpp index f7d5fcb..50c9ad5 100644 --- a/profiles/vcs/host/vcs_config.cpp +++ b/profiles/vcs/host/vcs_config.cpp @@ -155,15 +155,67 @@ void load_proper_shaders_configuration(VcsConfiguration &config, if (key == "enabled") { if (!parse_bool(value, config.volumetric_clouds.enabled)) warning(config, line_number, "ProperShaders.ini: Enabled expects true/false"); - } else if (key == "marchsteps") { - if (!parse_u32(value, 4u, 64u, config.volumetric_clouds.march_steps)) - warning(config, line_number, "ProperShaders.ini: MarchSteps must be between 4 and 64"); - } else if (key == "coverage") { - if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage)) bad_float("Coverage"); + } else if (key == "downscalediv") { + if (!parse_u32(value, 1u, 8u, config.volumetric_clouds.downscale_div)) + warning(config, line_number, "ProperShaders.ini: DownscaleDiv must be between 1 and 8"); + } else if (key == "layers") { + if (!parse_u32(value, 1u, 3u, config.volumetric_clouds.layers)) + warning(config, line_number, "ProperShaders.ini: Layers must be between 1 and 3"); + } else if (key == "shadowsteps") { + if (!parse_u32(value, 2u, 8u, config.volumetric_clouds.shadow_steps)) + warning(config, line_number, "ProperShaders.ini: ShadowSteps must be between 2 and 8"); + } else if (key == "coveragelow") { + if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage_low)) bad_float("CoverageLow"); + } else if (key == "coveragemid") { + if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage_mid)) bad_float("CoverageMid"); + } else if (key == "coveragehigh") { + if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage_high)) bad_float("CoverageHigh"); } else if (key == "opacity") { if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.opacity)) bad_float("Opacity"); } else if (key == "speed") { - if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.speed)) bad_float("Speed"); + if (!parse_float(value, 0.0f, 1000.0f, config.volumetric_clouds.speed)) bad_float("Speed"); + } else if (key == "brightness") { + if (!parse_float(value, 0.0f, 8.0f, config.volumetric_clouds.brightness)) bad_float("Brightness"); + } else if (key == "randomseed") { + if (!parse_float(value, 0.0f, 6.2831855f, config.volumetric_clouds.random_seed)) bad_float("RandomSeed"); + } else if (key == "sundirectionx") { + if (!parse_float(value, -1.0f, 1.0f, config.volumetric_clouds.sun_direction_x)) bad_float("SunDirectionX"); + } else if (key == "sundirectiony") { + if (!parse_float(value, -1.0f, 1.0f, config.volumetric_clouds.sun_direction_y)) bad_float("SunDirectionY"); + } else if (key == "sundirectionz") { + if (!parse_float(value, -1.0f, 1.0f, config.volumetric_clouds.sun_direction_z)) bad_float("SunDirectionZ"); + } else if (key == "suncolorr") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.sun_color_r)) bad_float("SunColorR"); + } else if (key == "suncolorg") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.sun_color_g)) bad_float("SunColorG"); + } else if (key == "suncolorb") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.sun_color_b)) bad_float("SunColorB"); + } else if (key == "cloudbasecolorr") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.cloud_base_color_r)) bad_float("CloudBaseColorR"); + } else if (key == "cloudbasecolorg") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.cloud_base_color_g)) bad_float("CloudBaseColorG"); + } else if (key == "cloudbasecolorb") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.cloud_base_color_b)) bad_float("CloudBaseColorB"); + } else if (key == "atmospheredensity") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.atmosphere_density)) bad_float("AtmosphereDensity"); + } else if (key == "mist") { + if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.mist)) bad_float("Mist"); + } else if (key == "fogcolorr") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.fog_color_r)) bad_float("FogColorR"); + } else if (key == "fogcolorg") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.fog_color_g)) bad_float("FogColorG"); + } else if (key == "fogcolorb") { + if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.fog_color_b)) bad_float("FogColorB"); + } else if (key == "fogstart") { + if (!parse_float(value, 1.0f, 100000.0f, config.volumetric_clouds.fog_start)) bad_float("FogStart"); + } else if (key == "dayprogression") { + if (!parse_float(value, -1.0f, 1.0f, config.volumetric_clouds.day_progression)) bad_float("DayProgression"); + } else if (key == "temporalblend") { + if (!parse_float(value, 0.0f, 0.95f, config.volumetric_clouds.temporal_blend)) bad_float("TemporalBlend"); + } else if (key == "temporaldenoise") { + if (!parse_float(value, 0.0f, 16.0f, config.volumetric_clouds.temporal_denoise)) bad_float("TemporalDenoise"); + } else if (key == "temporalclamp") { + if (!parse_float(value, 0.0f, 16.0f, config.volumetric_clouds.temporal_clamp)) bad_float("TemporalClamp"); } else { warning(config, line_number, "ProperShaders.ini: unknown [VolumetricClouds] key '" + key + "'"); } diff --git a/profiles/vcs/host/vcs_config.hpp b/profiles/vcs/host/vcs_config.hpp index 916d5ae..be06c21 100644 --- a/profiles/vcs/host/vcs_config.hpp +++ b/profiles/vcs/host/vcs_config.hpp @@ -173,15 +173,42 @@ struct WidescreenConfiguration { std::uint32_t aspect_y{0u}; }; -// Standalone ProperShaders.ini feature. These values deliberately do not read -// the guest timecycle/weather: the first port is a fixed, independently -// configurable CloudWorks sky layer. +// Standalone ProperShaders.ini feature. Values normally supplied by the San +// Andreas timecycle/weather integration remain explicit placeholders until the +// equivalent VCS guest hooks exist. Keeping the inputs separate is important: +// collapsing the three decks and lighting state into one "coverage" value +// materially changes CloudWorks' appearance. struct VolumetricCloudsConfiguration { bool enabled{false}; - std::uint32_t march_steps{20u}; - float coverage{0.52f}; - float opacity{0.78f}; - float speed{0.018f}; + std::uint32_t downscale_div{2u}; + std::uint32_t layers{3u}; + std::uint32_t shadow_steps{4u}; + float coverage_low{0.35f}; + float coverage_mid{0.25f}; + float coverage_high{0.18f}; + float opacity{1.0f}; + float speed{0.0f}; + float brightness{1.0f}; + float random_seed{0.0f}; + float sun_direction_x{0.38f}; + float sun_direction_y{-0.28f}; + float sun_direction_z{0.88f}; + float sun_color_r{1.0f}; + float sun_color_g{0.97f}; + float sun_color_b{0.88f}; + float cloud_base_color_r{0.70f}; + float cloud_base_color_g{0.70f}; + float cloud_base_color_b{0.70f}; + float atmosphere_density{0.0f}; + float mist{0.50f}; + float fog_color_r{0.58f}; + float fog_color_g{0.68f}; + float fog_color_b{0.78f}; + float fog_start{4500.0f}; + float day_progression{0.88f}; + float temporal_blend{0.50f}; + float temporal_denoise{1.0f}; + float temporal_clamp{1.0f}; }; // The aspect the game itself builds its projection with. VCS loads the diff --git a/profiles/vcs/tests/vcs_config_tests.cpp b/profiles/vcs/tests/vcs_config_tests.cpp index 53c1341..702869d 100644 --- a/profiles/vcs/tests/vcs_config_tests.cpp +++ b/profiles/vcs/tests/vcs_config_tests.cpp @@ -157,18 +157,30 @@ int main() { std::ofstream proper(root / "ProperShaders.ini", std::ios::trunc); proper << "[VolumetricClouds]\n" << "Enabled=true\n" - << "MarchSteps=28\n" - << "Coverage=0.61\n" + << "DownscaleDiv=4\n" + << "Layers=3\n" + << "ShadowSteps=6\n" + << "CoverageLow=0.61\n" + << "CoverageMid=0.42\n" + << "CoverageHigh=0.23\n" << "Opacity=0.72\n" - << "Speed=0.03\n"; + << "Speed=150.0\n" + << "Mist=0.67\n" + << "DayProgression=-0.12\n"; } vcs::initialize_vcs_configuration(root); const auto &clouds = vcs::vcs_configuration().volumetric_clouds; require(clouds.enabled, "ProperShaders.ini VolumetricClouds.Enabled was not parsed"); - require(clouds.march_steps == 28u, "ProperShaders.ini MarchSteps was not parsed"); - require(std::abs(clouds.coverage - 0.61f) < 0.0001f && + require(clouds.downscale_div == 4u && clouds.layers == 3u && + clouds.shadow_steps == 6u, + "ProperShaders.ini cloud quality controls were not parsed"); + require(std::abs(clouds.coverage_low - 0.61f) < 0.0001f && + std::abs(clouds.coverage_mid - 0.42f) < 0.0001f && + std::abs(clouds.coverage_high - 0.23f) < 0.0001f && std::abs(clouds.opacity - 0.72f) < 0.0001f && - std::abs(clouds.speed - 0.03f) < 0.0001f, + std::abs(clouds.speed - 150.0f) < 0.0001f && + std::abs(clouds.mist - 0.67f) < 0.0001f && + std::abs(clouds.day_progression + 0.12f) < 0.0001f, "ProperShaders.ini cloud parameters were not parsed"); // Widescreen: explicit ratio, "auto", and off. The correction must be // exactly neutral when disabled -- PSP parity stays the baseline.