mirror of
https://github.com/jessicanataliagta/PSPRecomp
synced 2026-10-04 10:41:03 -04:00
volumetric clouds update 3
volumetric clouds update 3
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 = ¶meter;
|
||||
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) {
|
||||
|
||||
@@ -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 + "'");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user