volumetric clouds update 3

volumetric clouds update 3
This commit is contained in:
Jessica_Natalia
2026-08-13 16:57:15 -03:00
parent a950e9c218
commit 099ce10186
7 changed files with 634 additions and 363 deletions
+79
View File
@@ -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.
+40 -7
View File
@@ -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
+180 -303
View File
@@ -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<float4> 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<a.volumeBox.y||i>=(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";
+225 -34
View File
@@ -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<float, 4> ray_right_time{};
std::array<float, 4> ray_up_coverage{};
std::array<float, 4> ray_up_seed{};
std::array<float, 4> ray_forward_opacity{};
std::array<float, 4> camera_settings{};
std::array<float, 4> coverage_speed{};
std::array<float, 4> sun_direction_day{};
std::array<float, 4> sun_color_atmosphere{};
std::array<float, 4> cloud_color_mist{};
std::array<float, 4> fog_color_start{};
std::array<float, 4> brightness_padding{};
};
static_assert(sizeof(CloudPresentConstants) == 16u * sizeof(std::uint32_t));
static_assert(sizeof(CloudShaderConstants) == 40u * sizeof(std::uint32_t));
struct Dx12FrameResources {
ComPtr<ID3D12CommandAllocator> allocator;
@@ -198,6 +205,15 @@ struct Dx12RetiredSrv {
UINT64 fence_value{};
};
struct CloudRenderTarget {
ComPtr<ID3D12Resource> 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<ID3D12RootSignature> root_signature;
ComPtr<ID3D12RootSignature> cloud_root_signature;
ComPtr<ID3DBlob> vertex_shader;
ComPtr<ID3DBlob> packed_0115_vertex_shader;
ComPtr<ID3DBlob> pixel_shader;
@@ -275,9 +292,12 @@ struct Dx12GeState {
std::uint32_t swap_height{};
ComPtr<ID3D12PipelineState> present_pipeline;
ComPtr<ID3D12PipelineState> cloud_target_pipeline;
ComPtr<ID3D12PipelineState> cloud_composite_pipeline;
ComPtr<ID3DBlob> present_vertex_shader;
ComPtr<ID3DBlob> present_pixel_shader;
ComPtr<ID3DBlob> cloud_target_pixel_shader;
ComPtr<ID3DBlob> 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<const char *>(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<const char *>(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<std::uint32_t>(
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 = &parameter;
desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
ComPtr<ID3DBlob> blob, errors;
HRESULT hr = D3D12SerializeRootSignature(&desc, D3D_ROOT_SIGNATURE_VERSION_1,
&blob, &errors);
if (FAILED(hr)) {
error = errors ? std::string(static_cast<const char *>(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<float, 16> &matrix,
std::array<double, 16> &inverse) noexcept {
// Gauss-Jordan in double precision. GE projection matrices are small, but
@@ -1753,7 +1867,7 @@ bool invert_cloud_matrix(const std::array<float, 16> &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<float>(ray_right);
out.ray_up_coverage[axis] = static_cast<float>(ray_up);
out.ray_up_seed[axis] = static_cast<float>(ray_up);
out.ray_forward_opacity[axis] = static_cast<float>(center_world[axis]);
out.camera_settings[axis] = camera->camera_position[axis];
}
out.ray_right_time[3] =
static_cast<float>(s.frame_epoch) * (1.0f / 60.0f) * config.speed;
out.ray_up_coverage[3] = config.coverage;
out.ray_right_time[3] = static_cast<float>(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<std::uint32_t>(config.march_steps, 4u, 64u) | 0x100u;
const std::uint32_t settings = std::clamp<std::uint32_t>(config.layers, 1u, 3u) |
(std::clamp<std::uint32_t>(config.shadow_steps, 2u, 8u) << 8u) | 0x10000u;
out.camera_settings[3] = std::bit_cast<float>(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<float>(cloud.width),
static_cast<float>(cloud.height), 0.0f, 1.0f};
D3D12_RECT cloud_scissor{0, 0, static_cast<LONG>(cloud.width),
static_cast<LONG>(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<LONG>(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<std::uint32_t, 21> constants{};
std::memcpy(constants.data() + 5u, &clouds, sizeof(clouds));
s.list->SetGraphicsRoot32BitConstants(
3u, static_cast<UINT>(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<std::uint32_t, 16> zero{}; } clouds{};
std::array<std::uint32_t, 21> 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) {
+58 -6
View File
@@ -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 + "'");
}
+34 -7
View File
@@ -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
+18 -6
View File
@@ -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.