feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -0,0 +1,17 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 stepUv = uPackParams0.xy;
|
||||
vec3 value = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv).rgb * 0.227027;
|
||||
value += ACDREAM_SAMPLE_2D(uTextureIndexA, vUv + stepUv * 1.384615).rgb * 0.316216;
|
||||
value += ACDREAM_SAMPLE_2D(uTextureIndexA, vUv - stepUv * 1.384615).rgb * 0.316216;
|
||||
value += ACDREAM_SAMPLE_2D(uTextureIndexA, vUv + stepUv * 3.230769).rgb * 0.070270;
|
||||
value += ACDREAM_SAMPLE_2D(uTextureIndexA, vUv - stepUv * 3.230769).rgb * 0.070270;
|
||||
oColor = vec4(value, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 scene = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv).rgb
|
||||
+ ACDREAM_SAMPLE_2D(uTextureIndexB, vUv).rgb;
|
||||
if (uPackParams0.w > 0.5)
|
||||
scene += ACDREAM_SAMPLE_2D(uTextureIndexC, vUv).rgb;
|
||||
float brightness = dot(scene, vec3(0.2126, 0.7152, 0.0722));
|
||||
float threshold = uPackParams0.y;
|
||||
float knee = max(uPackParams0.z, 0.0001);
|
||||
float soft = clamp((brightness - threshold + knee) / (2.0 * knee), 0.0, 1.0);
|
||||
soft = soft * soft;
|
||||
float contribution = max(brightness - threshold, 0.0) + soft * knee;
|
||||
contribution /= max(brightness, 0.0001);
|
||||
vec3 bloom = scene * contribution * uPackParams0.x;
|
||||
oColor = vec4(bloom, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
34
src/AcDream.App/Rendering/Shaders/atmospheric_common.glsl
Normal file
34
src/AcDream.App/Rendering/Shaders/atmospheric_common.glsl
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef ACDREAM_ATMOSPHERIC_COMMON_GLSL
|
||||
#define ACDREAM_ATMOSPHERIC_COMMON_GLSL
|
||||
|
||||
// Render-pack shader ABI v1. These declarations are the byte-level SSOT for
|
||||
// AtmosphericFrameUniforms (set 3/binding 5, 160 bytes) and
|
||||
// AtmosphericPackPassUniforms (set 3/binding 7). Binding 6 is intentionally
|
||||
// reserved for directional-shadow data in directional_shadow_common.glsl.
|
||||
layout(std140, ACDREAM_PACK_UBO_SET binding = 5) uniform AtmosphericFrame {
|
||||
vec4 uAtmosphereSunScreen; // 0: uv.xy, ray strength, elevation degrees
|
||||
vec4 uAtmosphereSunColor; // 16: authored linear rgb, policy multiplier
|
||||
vec4 uAtmosphereViewport; // 32: width, height, reciprocal width/height
|
||||
vec4 uAtmosphereWeather; // 48: kind, intensity, delta seconds, outdoor
|
||||
vec4 uAtmosphereSunDirection; // 64: surface-to-sun xyz, authored brightness
|
||||
vec4 uAtmospherePolicy; // 80: day group, group factor, shadow/shaft elevation factors
|
||||
mat4 uAtmosphereInverseViewProjection; // 96: screen/depth to world
|
||||
};
|
||||
|
||||
layout(std140, ACDREAM_PACK_UBO_SET binding = 7) uniform PackPass {
|
||||
vec4 uPackParams0; // 0
|
||||
vec4 uPackParams1; // 16
|
||||
vec4 uPackParams2; // 32
|
||||
vec4 uPackParams3; // 48
|
||||
};
|
||||
|
||||
// FusedAtmosphericPostProcess PackPass ABI (opt-in Low preset only):
|
||||
// sun-rays: Params1 = (enabled, logical mask width, mask height, 0)
|
||||
// filmic: Params1.z = enabled; Params2 = bloom extraction parameters;
|
||||
// Params3.xy = logical bloom texel step
|
||||
|
||||
layout(std140, ACDREAM_PACK_UBO_SET binding = 8) uniform PackSettings {
|
||||
vec4 uPackSettings[16]; // 64 declaration-order scalar setting slots
|
||||
};
|
||||
|
||||
#endif
|
||||
88
src/AcDream.App/Rendering/Shaders/atmospheric_filmic.frag
Normal file
88
src/AcDream.App/Rendering/Shaders/atmospheric_filmic.frag
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
|
||||
vec3 acesFitted(vec3 value)
|
||||
{
|
||||
const float a = 2.51;
|
||||
const float b = 0.03;
|
||||
const float c = 2.43;
|
||||
const float d = 0.59;
|
||||
const float e = 0.14;
|
||||
return clamp((value * (a * value + b)) / (value * (c * value + d) + e), 0.0, 1.0);
|
||||
}
|
||||
|
||||
vec3 sampleBloom(vec2 uv)
|
||||
{
|
||||
return ACDREAM_SAMPLE_2D(uTextureIndexB, uv).rgb;
|
||||
}
|
||||
|
||||
vec3 lowFusedScene(vec2 uv)
|
||||
{
|
||||
vec3 scene = ACDREAM_SAMPLE_2D(uTextureIndexA, uv).rgb
|
||||
+ ACDREAM_SAMPLE_2D(uTextureIndexB, uv).rgb;
|
||||
if (uPackParams2.w > 0.5)
|
||||
scene += ACDREAM_SAMPLE_2D(uTextureIndexC, uv).rgb;
|
||||
return scene;
|
||||
}
|
||||
|
||||
vec3 lowFusedBloomExtract(vec3 scene)
|
||||
{
|
||||
float brightness = dot(scene, vec3(0.2126, 0.7152, 0.0722));
|
||||
float threshold = uPackParams2.y;
|
||||
float knee = max(uPackParams2.z, 0.0001);
|
||||
float soft = clamp((brightness - threshold + knee) / (2.0 * knee), 0.0, 1.0);
|
||||
soft = soft * soft;
|
||||
float contribution = max(brightness - threshold, 0.0) + soft * knee;
|
||||
contribution /= max(brightness, 0.0001);
|
||||
return scene * contribution * uPackParams2.x;
|
||||
}
|
||||
|
||||
vec3 lowFusedBloom(vec3 centerScene)
|
||||
{
|
||||
const float offsets[5] = float[5](
|
||||
-3.230769, -1.384615, 0.0, 1.384615, 3.230769);
|
||||
const float weights[5] = float[5](
|
||||
0.070270, 0.316216, 0.227027, 0.316216, 0.070270);
|
||||
vec3 bloom = vec3(0.0);
|
||||
for (int y = 0; y < 5; ++y) {
|
||||
for (int x = 0; x < 5; ++x) {
|
||||
vec3 scene = x == 2 && y == 2
|
||||
? centerScene
|
||||
: lowFusedScene(vUv + vec2(offsets[x], offsets[y]) * uPackParams3.xy);
|
||||
bloom += lowFusedBloomExtract(scene) * (weights[x] * weights[y]);
|
||||
}
|
||||
}
|
||||
return bloom;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 hdr;
|
||||
if (uPackParams1.z > 0.5) {
|
||||
vec3 scene = lowFusedScene(vUv);
|
||||
hdr = scene + lowFusedBloom(scene);
|
||||
}
|
||||
else {
|
||||
hdr = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv).rgb
|
||||
+ sampleBloom(vUv)
|
||||
+ ACDREAM_SAMPLE_2D(uTextureIndexC, vUv).rgb;
|
||||
if (uPackParams1.y > 0.5)
|
||||
hdr += ACDREAM_SAMPLE_2D(uTextureIndexD, vUv).rgb;
|
||||
}
|
||||
vec3 exposed = max(hdr * uPackParams0.x, vec3(0.0));
|
||||
vec3 linearClamped = clamp(exposed, 0.0, 1.0);
|
||||
vec3 color = mix(linearClamped, acesFitted(exposed), clamp(uPackParams1.x, 0.0, 1.0));
|
||||
|
||||
float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));
|
||||
color = mix(vec3(luminance), color, uPackParams0.y);
|
||||
color = (color - 0.5) * uPackParams0.z + 0.5;
|
||||
|
||||
vec2 centered = vUv * 2.0 - 1.0;
|
||||
float vignette = smoothstep(1.25, 0.25, dot(centered, centered));
|
||||
color *= mix(1.0, vignette, clamp(uPackParams0.w, 0.0, 1.0));
|
||||
oColor = vec4(clamp(color, 0.0, 1.0), 1.0);
|
||||
}
|
||||
10
src/AcDream.App/Rendering/Shaders/atmospheric_filmic.vert
Normal file
10
src/AcDream.App/Rendering/Shaders/atmospheric_filmic.vert
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
|
||||
void main()
|
||||
{
|
||||
float depth = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv).r;
|
||||
float unobstructedSky = smoothstep(0.9975, 0.99995, depth);
|
||||
float enabled = uAtmosphereSunScreen.z * uAtmosphereWeather.w;
|
||||
oColor = vec4(vec3(unobstructedSky * enabled), 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
// Vulkan's negative viewport preserves GL world winding; flip the sampled
|
||||
// image coordinate once here so row zero remains the screen top.
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
58
src/AcDream.App/Rendering/Shaders/atmospheric_sun_rays.frag
Normal file
58
src/AcDream.App/Rendering/Shaders/atmospheric_sun_rays.frag
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
|
||||
float lowFusedMaskTexel(ivec2 coordinate, vec2 maskSize)
|
||||
{
|
||||
ivec2 maximum = ivec2(maskSize) - ivec2(1);
|
||||
ivec2 clampedCoordinate = clamp(coordinate, ivec2(0), maximum);
|
||||
vec2 depthUv = (vec2(clampedCoordinate) + vec2(0.5)) / maskSize;
|
||||
float depth = ACDREAM_SAMPLE_2D(uTextureIndexA, depthUv).r;
|
||||
float unobstructedSky = smoothstep(0.9975, 0.99995, depth);
|
||||
float enabled = uAtmosphereSunScreen.z * uAtmosphereWeather.w;
|
||||
// Match the removed RGBA8_UNORM mask attachment before filtering it.
|
||||
return round(clamp(unobstructedSky * enabled, 0.0, 1.0) * 255.0) / 255.0;
|
||||
}
|
||||
|
||||
float sampleLowFusedMask(vec2 uv)
|
||||
{
|
||||
vec2 maskSize = uPackParams1.yz;
|
||||
vec2 texel = uv * maskSize - vec2(0.5);
|
||||
ivec2 lower = ivec2(floor(texel));
|
||||
vec2 fraction = fract(texel);
|
||||
float topLeft = lowFusedMaskTexel(lower, maskSize);
|
||||
float topRight = lowFusedMaskTexel(lower + ivec2(1, 0), maskSize);
|
||||
float bottomLeft = lowFusedMaskTexel(lower + ivec2(0, 1), maskSize);
|
||||
float bottomRight = lowFusedMaskTexel(lower + ivec2(1, 1), maskSize);
|
||||
return mix(
|
||||
mix(topLeft, topRight, fraction.x),
|
||||
mix(bottomLeft, bottomRight, fraction.x),
|
||||
fraction.y);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
const int SampleCount = 48;
|
||||
float decay = uPackParams0.x;
|
||||
float weight = uPackParams0.y;
|
||||
float density = uPackParams0.z;
|
||||
vec2 delta = (vUv - uAtmosphereSunScreen.xy) * (density / float(SampleCount));
|
||||
vec2 sampleUv = vUv;
|
||||
float illumination = 1.0;
|
||||
float sum = 0.0;
|
||||
for (int i = 0; i < SampleCount; ++i) {
|
||||
sampleUv -= delta;
|
||||
if (any(lessThan(sampleUv, vec2(0.0))) || any(greaterThan(sampleUv, vec2(1.0))))
|
||||
break;
|
||||
float mask = uPackParams1.x > 0.5
|
||||
? sampleLowFusedMask(sampleUv)
|
||||
: ACDREAM_SAMPLE_2D(uTextureIndexA, sampleUv).r;
|
||||
sum += mask * illumination;
|
||||
illumination *= decay;
|
||||
}
|
||||
vec3 rays = uAtmosphereSunColor.rgb * (sum * weight / float(SampleCount));
|
||||
oColor = vec4(rays, 1.0);
|
||||
}
|
||||
10
src/AcDream.App/Rendering/Shaders/atmospheric_sun_rays.vert
Normal file
10
src/AcDream.App/Rendering/Shaders/atmospheric_sun_rays.vert
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
vec3 reconstructWorld(vec2 uv, float depth)
|
||||
{
|
||||
vec4 clip = vec4(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0, depth, 1.0);
|
||||
vec4 world = uAtmosphereInverseViewProjection * clip;
|
||||
return world.xyz / max(abs(world.w), 1e-6);
|
||||
}
|
||||
|
||||
float directionalVisibility(vec3 worldPosition)
|
||||
{
|
||||
// Volumetric shafts are a sun effect. A moon-oriented shadow map remains
|
||||
// valid for world receivers but must not occlude the authored sun ray.
|
||||
if (uint(round(uShadowLightDirectionAndSource.w)) != 1u)
|
||||
return 1.0;
|
||||
uint cascadeCount = clamp(uShadowTextureAndFlags.y, 1u, 4u);
|
||||
vec3 surfaceToSun = normalize(uShadowLightDirectionAndSource.xyz);
|
||||
vec3 biasedPosition = worldPosition
|
||||
+ surfaceToSun * max(uShadowBiasMeters.x, 0.0);
|
||||
for (uint cascade = 0u; cascade < cascadeCount; ++cascade) {
|
||||
vec4 clip = uShadowWorldToClip[cascade] * vec4(biasedPosition, 1.0);
|
||||
vec3 ndc = clip.xyz / max(abs(clip.w), 1e-6);
|
||||
vec2 uv = vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
||||
if (all(greaterThanEqual(uv, vec2(0.0)))
|
||||
&& all(lessThanEqual(uv, vec2(1.0)))
|
||||
&& ndc.z >= 0.0 && ndc.z <= 1.0) {
|
||||
float stored = ACDREAM_SAMPLE_ARRAY(
|
||||
uShadowTextureAndFlags.x,
|
||||
vec3(uv, float(cascade))).r;
|
||||
float visible = ndc.z <= stored ? 1.0 : 0.0;
|
||||
return mix(1.0, visible, clamp(uShadowControl.x, 0.0, 1.0));
|
||||
}
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
float sceneDepth = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv).r;
|
||||
if (sceneDepth >= 0.999999 || uPackParams0.w <= 0.0) {
|
||||
oColor = vec4(0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
vec3 nearWorld = reconstructWorld(vUv, 0.0);
|
||||
vec3 sceneWorld = reconstructWorld(vUv, sceneDepth);
|
||||
int steps = clamp(int(uPackParams0.z + 0.5), 1, 64);
|
||||
float lit = 0.0;
|
||||
for (int step = 0; step < 64; ++step) {
|
||||
if (step >= steps)
|
||||
break;
|
||||
float t = (float(step) + 0.5) / float(steps);
|
||||
lit += directionalVisibility(mix(nearWorld, sceneWorld, t));
|
||||
}
|
||||
|
||||
float integrated = lit / float(steps);
|
||||
float extinction = 1.0 - exp(-uPackParams0.x * length(sceneWorld - nearWorld));
|
||||
vec3 color = uAtmosphereSunColor.rgb
|
||||
* (integrated * extinction * uPackParams0.y);
|
||||
oColor = vec4(max(color, vec3(0.0)), 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#ifndef ACDREAM_DIRECTIONAL_SHADOW_COMMON_GLSL
|
||||
#define ACDREAM_DIRECTIONAL_SHADOW_COMMON_GLSL
|
||||
|
||||
// Render-pack shader ABI v1, set 3/binding 6. This is the byte-level SSOT for
|
||||
// DirectionalShadowUniforms (std140, 336 bytes).
|
||||
layout(std140, ACDREAM_PACK_UBO_SET binding = 6) uniform DirectionalShadow {
|
||||
mat4 uShadowWorldToClip[4]; // 0, 64, 128, 192
|
||||
vec4 uShadowSplitFarMeters; // 256
|
||||
vec4 uShadowControl; // 272: strength, softness, reach m, blend m
|
||||
vec4 uShadowBiasMeters; // 288: constant, slope, normal, caster pad
|
||||
uvec4 uShadowTextureAndFlags; // 304: set2 slot, count, resolution, flags
|
||||
vec4 uShadowLightDirectionAndSource; // 320: surface-to-light xyz, source kind
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
#ifndef ACDREAM_DIRECTIONAL_SHADOW_RECEIVER_GLSL
|
||||
#define ACDREAM_DIRECTIONAL_SHADOW_RECEIVER_GLSL
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
// Receiver policy shared byte-for-byte by terrain and world meshes. Cascade
|
||||
// choice is camera-distance based; the terminal blend band is measured in
|
||||
// world metres, so a projection/FOV change cannot move the seam.
|
||||
int acdreamShadowCascade(float cameraDistanceMeters) {
|
||||
int count = int(uShadowTextureAndFlags.y);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (cameraDistanceMeters <= uShadowSplitFarMeters[i])
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The X/Y clip gradients are the reciprocal orthographic half extent. Their
|
||||
// ratio therefore gives the exact relative texel footprint without extending
|
||||
// the pinned v1 uniform block. The CPU publishes the conservative far-cascade
|
||||
// bias; inner maps scale it down instead of visibly detaching nearby feet,
|
||||
// foliage, and building edges from their receivers.
|
||||
float acdreamShadowBiasScale(int cascade) {
|
||||
int farCascade = max(int(uShadowTextureAndFlags.y) - 1, 0);
|
||||
mat4 cascadeMatrix = uShadowWorldToClip[cascade];
|
||||
mat4 farMatrix = uShadowWorldToClip[farCascade];
|
||||
float cascadeDensity = 0.5 * (
|
||||
length(vec3(cascadeMatrix[0][0], cascadeMatrix[1][0], cascadeMatrix[2][0]))
|
||||
+ length(vec3(cascadeMatrix[0][1], cascadeMatrix[1][1], cascadeMatrix[2][1])));
|
||||
float farDensity = 0.5 * (
|
||||
length(vec3(farMatrix[0][0], farMatrix[1][0], farMatrix[2][0]))
|
||||
+ length(vec3(farMatrix[0][1], farMatrix[1][1], farMatrix[2][1])));
|
||||
return clamp(farDensity / max(cascadeDensity, 1e-7), 0.0, 1.0);
|
||||
}
|
||||
|
||||
// One textureGather returns the four depth texels surrounding the continuous
|
||||
// receiver coordinate. Comparing first and then interpolating is true
|
||||
// bilinear percentage-closer filtering; interpolating depth before comparing
|
||||
// would create false blockers at discontinuities.
|
||||
float acdreamShadowBilinearCompare(
|
||||
int cascade,
|
||||
vec2 uv,
|
||||
float receiverDepth)
|
||||
{
|
||||
float resolution = max(float(uShadowTextureAndFlags.z), 1.0);
|
||||
vec2 texelPosition = uv * resolution - vec2(0.5);
|
||||
vec2 blend = fract(texelPosition);
|
||||
vec4 gatheredDepth = textureGather(
|
||||
ACDREAM_TEXTURE(uShadowTextureAndFlags.x),
|
||||
vec3(uv, float(cascade)),
|
||||
0);
|
||||
vec4 compared = step(vec4(receiverDepth), gatheredDepth);
|
||||
float lower = mix(compared.w, compared.z, blend.x);
|
||||
float upper = mix(compared.x, compared.y, blend.x);
|
||||
return mix(lower, upper, blend.y);
|
||||
}
|
||||
|
||||
float acdreamShadowCascadePcf(
|
||||
int cascade,
|
||||
vec3 receiverWorldPosition,
|
||||
vec3 worldNormal,
|
||||
vec3 surfaceToLight)
|
||||
{
|
||||
float ndl = clamp(dot(worldNormal, surfaceToLight), 0.0, 1.0);
|
||||
vec3 cascadeBiasMeters = max(
|
||||
uShadowBiasMeters.xyz * acdreamShadowBiasScale(cascade),
|
||||
vec3(0.001));
|
||||
float depthBiasMeters = cascadeBiasMeters.x
|
||||
+ cascadeBiasMeters.y * (1.0 - ndl);
|
||||
vec3 biasedWorldPosition = receiverWorldPosition
|
||||
+ worldNormal * cascadeBiasMeters.z
|
||||
+ surfaceToLight * depthBiasMeters;
|
||||
|
||||
vec4 shadowClip = uShadowWorldToClip[cascade]
|
||||
* vec4(biasedWorldPosition, 1.0);
|
||||
vec3 shadowNdc = shadowClip.xyz / max(abs(shadowClip.w), 1e-7);
|
||||
// Vulkan records these maps through a negative-height viewport so the
|
||||
// conventional GL-up clip coordinate samples the top-left-origin image.
|
||||
vec2 uv = vec2(
|
||||
shadowNdc.x * 0.5 + 0.5,
|
||||
0.5 - shadowNdc.y * 0.5);
|
||||
float receiverDepth = shadowNdc.z;
|
||||
if (uv.x <= 0.0 || uv.x >= 1.0
|
||||
|| uv.y <= 0.0 || uv.y >= 1.0
|
||||
|| receiverDepth <= 0.0 || receiverDepth >= 1.0)
|
||||
return 1.0;
|
||||
|
||||
int radius = int((uShadowTextureAndFlags.w >> 8u) & 0xFu);
|
||||
radius = clamp(radius, 0, 2);
|
||||
// Weather changes the physical softness envelope without changing the
|
||||
// preset's bounded kernel. Bilinear PCF removes sub-texel stair stepping;
|
||||
// the higher presets place four/nine bilinear lobes in separable tent
|
||||
// patterns instead of issuing nine/twenty-five blocky nearest comparisons.
|
||||
float texel = 1.0 / max(float(uShadowTextureAndFlags.z), 1.0);
|
||||
float softness = max(uShadowControl.y, 1.0);
|
||||
if (radius == 0)
|
||||
return acdreamShadowBilinearCompare(cascade, uv, receiverDepth);
|
||||
|
||||
float lit = 0.0;
|
||||
float weightSum = 0.0;
|
||||
if (radius == 1) {
|
||||
for (int y = 0; y < 2; ++y) {
|
||||
for (int x = 0; x < 2; ++x) {
|
||||
vec2 offset = (vec2(x, y) - vec2(0.5))
|
||||
* softness * texel;
|
||||
lit += acdreamShadowBilinearCompare(
|
||||
cascade, uv + offset, receiverDepth);
|
||||
weightSum += 1.0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int y = -1; y <= 1; ++y) {
|
||||
for (int x = -1; x <= 1; ++x) {
|
||||
float weight = float(2 - abs(x)) * float(2 - abs(y));
|
||||
vec2 offset = vec2(x, y) * 1.5 * softness * texel;
|
||||
lit += acdreamShadowBilinearCompare(
|
||||
cascade, uv + offset, receiverDepth) * weight;
|
||||
weightSum += weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lit / max(weightSum, 1.0);
|
||||
}
|
||||
|
||||
float acdreamDirectionalShadowVisibility(
|
||||
vec3 receiverWorldPosition,
|
||||
vec3 worldNormal,
|
||||
vec3 cameraWorldPosition,
|
||||
vec3 surfaceToLight)
|
||||
{
|
||||
if ((uShadowTextureAndFlags.w & 1u) == 0u)
|
||||
return 1.0;
|
||||
|
||||
float cameraDistanceMeters = length(
|
||||
receiverWorldPosition - cameraWorldPosition);
|
||||
if (cameraDistanceMeters > uShadowControl.z)
|
||||
return 1.0;
|
||||
|
||||
int cascade = acdreamShadowCascade(cameraDistanceMeters);
|
||||
if (cascade < 0)
|
||||
return 1.0;
|
||||
|
||||
float visibility = acdreamShadowCascadePcf(
|
||||
cascade,
|
||||
receiverWorldPosition,
|
||||
worldNormal,
|
||||
surfaceToLight);
|
||||
int cascadeCount = int(uShadowTextureAndFlags.y);
|
||||
if (cascade + 1 < cascadeCount) {
|
||||
float split = uShadowSplitFarMeters[cascade];
|
||||
float widthMeters = max(uShadowControl.w, 1e-4);
|
||||
float blend = smoothstep(
|
||||
max(0.0, split - widthMeters),
|
||||
split,
|
||||
cameraDistanceMeters);
|
||||
if (blend > 0.0) {
|
||||
float nextVisibility = acdreamShadowCascadePcf(
|
||||
cascade + 1,
|
||||
receiverWorldPosition,
|
||||
worldNormal,
|
||||
surfaceToLight);
|
||||
visibility = mix(visibility, nextVisibility, blend);
|
||||
}
|
||||
}
|
||||
// Fade the terminal cascade over the same world-metre band used for
|
||||
// inter-cascade transitions. This removes the moving ring/pop at maximum
|
||||
// reach while leaving every caster and both/three/four cascades intact.
|
||||
float reachFade = 1.0 - smoothstep(
|
||||
max(0.0, uShadowControl.z - max(uShadowControl.w, 1e-4)),
|
||||
uShadowControl.z,
|
||||
cameraDistanceMeters);
|
||||
float shadowWeight = clamp(uShadowControl.x, 0.0, 1.0) * reachFade;
|
||||
return mix(1.0, visibility, shadowWeight);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#version 460 core
|
||||
|
||||
void main() {
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
#version 460 core
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in uvec4 aPacked0;
|
||||
layout(location = 3) in uvec4 aPacked1;
|
||||
layout(location = 4) in uvec4 aPacked2;
|
||||
layout(location = 5) in uvec4 aPacked3;
|
||||
|
||||
uniform int uRenderPass;
|
||||
|
||||
void main() {
|
||||
gl_Position = uShadowWorldToClip[uRenderPass] * vec4(aPosition, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#version 460 core
|
||||
|
||||
void main() {
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#version 460 core
|
||||
#extension GL_EXT_multiview : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in uvec4 aPacked0;
|
||||
layout(location = 3) in uvec4 aPacked1;
|
||||
layout(location = 4) in uvec4 aPacked2;
|
||||
layout(location = 5) in uvec4 aPacked3;
|
||||
|
||||
void main() {
|
||||
gl_Position = uShadowWorldToClip[int(gl_ViewIndex)] * vec4(aPosition, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#version 460 core
|
||||
#extension GL_ARB_bindless_texture : require
|
||||
|
||||
in vec2 vShadowTexCoord;
|
||||
flat in uint vShadowTextureIndex;
|
||||
flat in uint vShadowTextureLayer;
|
||||
|
||||
void main() {
|
||||
vec4 texel = ACDREAM_SAMPLE_ARRAY(
|
||||
vShadowTextureIndex,
|
||||
vec3(vShadowTexCoord, float(vShadowTextureLayer)));
|
||||
if (texel.a < 0.05)
|
||||
discard;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#version 460 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData { mat4 transform; };
|
||||
struct BatchData {
|
||||
uint textureIndex;
|
||||
uint _pad;
|
||||
uint textureLayer;
|
||||
uint flags;
|
||||
};
|
||||
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||||
BatchData Batches[];
|
||||
};
|
||||
|
||||
uniform int uDrawIDOffset;
|
||||
uniform int uRenderPass;
|
||||
|
||||
out vec2 vShadowTexCoord;
|
||||
flat out uint vShadowTextureIndex;
|
||||
flat out uint vShadowTextureLayer;
|
||||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
gl_Position = uShadowWorldToClip[uRenderPass] * worldPosition;
|
||||
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vShadowTexCoord = aTexCoord;
|
||||
vShadowTextureIndex = batch.textureIndex;
|
||||
vShadowTextureLayer = batch.textureLayer;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#version 460 core
|
||||
#extension GL_ARB_bindless_texture : require
|
||||
|
||||
in vec2 vShadowTexCoord;
|
||||
flat in uint vShadowTextureIndex;
|
||||
flat in uint vShadowTextureLayer;
|
||||
|
||||
void main() {
|
||||
vec4 texel = ACDREAM_SAMPLE_ARRAY(
|
||||
vShadowTextureIndex,
|
||||
vec3(vShadowTexCoord, float(vShadowTextureLayer)));
|
||||
if (texel.a < 0.05)
|
||||
discard;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#version 460 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
#extension GL_EXT_multiview : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData { mat4 transform; };
|
||||
struct BatchData {
|
||||
uint textureIndex;
|
||||
uint _pad;
|
||||
uint textureLayer;
|
||||
uint flags;
|
||||
};
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||||
BatchData Batches[];
|
||||
};
|
||||
|
||||
uniform int uDrawIDOffset;
|
||||
out vec2 vShadowTexCoord;
|
||||
flat out uint vShadowTextureIndex;
|
||||
flat out uint vShadowTextureLayer;
|
||||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
gl_Position = uShadowWorldToClip[int(gl_ViewIndex)] * worldPosition;
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vShadowTexCoord = aTexCoord;
|
||||
vShadowTextureIndex = batch.textureIndex;
|
||||
vShadowTextureLayer = batch.textureLayer;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#version 460 core
|
||||
|
||||
void main() {
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
#version 460 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData { mat4 transform; };
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
|
||||
uniform int uRenderPass;
|
||||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
gl_Position = uShadowWorldToClip[uRenderPass] * worldPosition;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#version 460 core
|
||||
|
||||
void main() {
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
#version 460 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
#extension GL_EXT_multiview : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData { mat4 transform; };
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
gl_Position = uShadowWorldToClip[int(gl_ViewIndex)] * worldPosition;
|
||||
}
|
||||
130
src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag
Normal file
130
src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#version 430 core
|
||||
#extension GL_ARB_bindless_texture : require
|
||||
|
||||
in vec3 vNormal;
|
||||
in vec2 vTexCoord;
|
||||
in vec3 vWorldPos;
|
||||
in vec3 vAmbientLocalLit;
|
||||
in vec3 vDirectionalLit;
|
||||
// Campaign V slice V6e: the table slot, not the bindless handle — see
|
||||
// mesh_modern.vert. The lookup moved here because a Vulkan varying cannot
|
||||
// carry a descriptor.
|
||||
in flat uint vTextureIndex;
|
||||
in flat uint vTextureLayer;
|
||||
in flat float vOpacityMultiplier; // #188
|
||||
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
||||
in flat uint vReceivesDirectionalShadow;
|
||||
|
||||
#include "directional_shadow_receiver.glsl"
|
||||
|
||||
// uRenderPass values (Phase N.5 Decision 2 — two-pass alpha-test):
|
||||
// 0 = opaque pass — discard fragments with alpha < 0.95
|
||||
// (lets the depth write succeed for solid pixels)
|
||||
// 1 = translucent pass — covers AlphaBlend / Additive / InvAlpha;
|
||||
// discard alpha >= 0.95 (already drawn opaque) and
|
||||
// alpha < 0.05 (skip empty fragments — large
|
||||
// transparent overdraw cost otherwise)
|
||||
uniform int uRenderPass;
|
||||
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
||||
|
||||
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
||||
struct Light {
|
||||
vec4 posAndKind;
|
||||
vec4 dirAndRange;
|
||||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
||||
Light uLights[8];
|
||||
vec4 uCellAmbient;
|
||||
vec4 uFogParams;
|
||||
vec4 uFogColor;
|
||||
vec4 uCameraAndTime;
|
||||
};
|
||||
|
||||
// A7 (2026-06-15): per-vertex lighting moved to mesh_modern.vert (Gouraud) to match
|
||||
// retail's fixed-function per-vertex T&L — a per-pixel evaluation made a hard "spotlight"
|
||||
// pool. The SceneLighting UBO above is still declared here for fog (uFogParams/uFogColor/
|
||||
// uCameraAndTime) + the lightning-flash bump; its uLights[]/uCellAmbient are now consumed
|
||||
// in the vertex shader. The std140 layout must stay identical to the vert + the CPU upload.
|
||||
|
||||
vec3 applyFog(vec3 lit, vec3 worldPos) {
|
||||
int mode = int(uFogParams.w);
|
||||
if (mode == 0) return lit;
|
||||
float d = length(worldPos - uCameraAndTime.xyz);
|
||||
float fogStart = uFogParams.x;
|
||||
float fogEnd = uFogParams.y;
|
||||
float span = max(1e-3, fogEnd - fogStart);
|
||||
float fog = clamp((d - fogStart) / span, 0.0, 1.0);
|
||||
return mix(lit, uFogColor.xyz, fog);
|
||||
}
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
||||
|
||||
// Two-pass alpha-test (N.5 Decision 2).
|
||||
// A.5 T20: opaque pass writes alpha as-sampled so GL_SAMPLE_ALPHA_TO_COVERAGE
|
||||
// derives the MSAA sample mask from it — ClipMap foliage edges become smooth.
|
||||
// Discard only fully-transparent (α < 0.05); the GPU handles coverage masking.
|
||||
if (uRenderPass == 0) {
|
||||
if (color.a < 0.05) discard; // opaque pass — kill truly empty only (A2C)
|
||||
} else {
|
||||
// Transparent pass.
|
||||
//
|
||||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): do NOT discard α≥0.95 here.
|
||||
// Native AC transparent-flagged surfaces routinely include
|
||||
// effectively-opaque pixels — e.g. the Holtburg lifestone crystal core
|
||||
// (surface 0x080011DE) which the spawn manifest classifies as
|
||||
// transparent (batch.IsTransparent=True) but whose decoded texture
|
||||
// alpha lands ≥0.95 across the visible surface. Those pixels still
|
||||
// compose correctly under (SrcAlpha, 1-SrcAlpha) alpha-blending, so
|
||||
// discarding them here threw away the whole crystal. The original
|
||||
// N.5 §2 rationale (high-α fragments belong in the opaque pass) does
|
||||
// not apply when the SURFACE is dat-flagged transparent — those
|
||||
// pixels can't reach the opaque pass at all.
|
||||
//
|
||||
// Keep the α<0.05 short-circuit as a fragment-cost optimization
|
||||
// (skip fully-empty pixels — saves blend bandwidth on alpha-keyed
|
||||
// sprites with large transparent margins).
|
||||
if (color.a < 0.05) discard;
|
||||
}
|
||||
|
||||
// Only the authored outdoor directional term is shadowed. Ambient, local
|
||||
// lights and material selection pulses remain exactly on the retail path.
|
||||
vec3 surfaceToLight = normalize(uShadowLightDirectionAndSource.xyz);
|
||||
float directionalVisibility = vReceivesDirectionalShadow != 0u
|
||||
? acdreamDirectionalShadowVisibility(
|
||||
vWorldPos,
|
||||
normalize(vNormal),
|
||||
uCameraAndTime.xyz,
|
||||
surfaceToLight)
|
||||
: 1.0;
|
||||
vec3 sceneLit = vAmbientLocalLit
|
||||
+ vDirectionalLit * directionalVisibility;
|
||||
vec3 lit = vec3(vSelectionLighting.x)
|
||||
+ vSelectionLighting.y * sceneLit;
|
||||
|
||||
// #176 stripe-hunt mode 3: show the raw per-vertex light field (texture
|
||||
// ignored). Stripes visible HERE = a vertex-lighting artifact; absent =
|
||||
// the pattern comes from texture/per-pixel machinery. Throwaway diagnostic.
|
||||
if (uLightDebug == 3) {
|
||||
FragColor = vec4(min(lit, vec3(1.0)), 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lightning flash — additive scene bump (matches mesh_instanced.frag).
|
||||
lit += uFogParams.z * vec3(0.6, 0.6, 0.75);
|
||||
|
||||
// Retail clamp per-channel to 1.0 (r13 §13.1).
|
||||
lit = min(lit, vec3(1.0));
|
||||
|
||||
vec3 rgb = color.rgb * lit;
|
||||
rgb = applyFog(rgb, vWorldPos);
|
||||
// #188: multiply the FINAL alpha only — the discard thresholds above stay
|
||||
// keyed on the raw sampled color.a, so the last few frames of a fade
|
||||
// (multiplier crossing under 0.05) still ramp smoothly toward zero rather
|
||||
// than popping invisible early against the discard cutoff.
|
||||
FragColor = vec4(rgb, color.a * vOpacityMultiplier);
|
||||
}
|
||||
363
src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert
Normal file
363
src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
#version 430 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData {
|
||||
mat4 transform;
|
||||
};
|
||||
|
||||
// Campaign V slice V2 (2026-07-27): textureHandle (uvec2, a 64-bit
|
||||
// GL_ARB_bindless_texture handle) became textureIndex (uint) plus an explicit
|
||||
// pad word. textureIndex is a slot into the global texture table (set 2,
|
||||
// injected by tools/ShaderCompiler/VulkanGlslPreamble.cs — see
|
||||
// ACDREAM_TEXTURE_HANDLE/ACDREAM_SAMPLE_ARRAY) which main() below forwards to
|
||||
// the fragment stage. The pad word keeps textureLayer/flags at their original
|
||||
// std430 offsets (8/12), so the struct is still 16 bytes and every existing
|
||||
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
|
||||
struct BatchData {
|
||||
uint textureIndex; // slot into the global texture table
|
||||
uint _pad; // keeps textureLayer/flags at offsets 8/12
|
||||
uint textureLayer; // layer in the shared WB or pooled composite array
|
||||
uint flags; // reserved — N.5 dispatcher owns all blend state
|
||||
// (glBlendFunc per pass). If a future phase wants
|
||||
// shader-side per-batch additive flag (Decision 2
|
||||
// fallback), encode it here as bit 0.
|
||||
};
|
||||
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
|
||||
// binding=1 here is the SSBO namespace — distinct from the UBO namespace.
|
||||
// SceneLighting UBO also uses binding=1 in the fragment shader; GL keeps
|
||||
// GL_SHADER_STORAGE_BUFFER and GL_UNIFORM_BUFFER binding tables separate.
|
||||
// Task 10 dispatcher binds:
|
||||
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, instanceSsbo)
|
||||
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, batchSsbo)
|
||||
// Existing SceneLightingUboBinding handles the UBO side.
|
||||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||||
BatchData Batches[];
|
||||
};
|
||||
|
||||
// === Phase U.3: per-cell screen-space clip gate (gl_ClipDistance) =============
|
||||
// Two SSBOs add the clip mechanism without disturbing binding=0/1 above.
|
||||
//
|
||||
// binding=2 — SHARED per-frame clip regions, one CellClip per "slot". Uploaded
|
||||
// ONCE per frame by ClipFrame.UploadShared (shared across WbDrawDispatcher +
|
||||
// EnvCellRenderer). Slot 0 is RESERVED = no-clip (count 0 ⇒ every plane passes).
|
||||
//
|
||||
// binding=3 — PER-RENDERER per-instance slot index, parallel to the binding=0
|
||||
// instance buffer and indexed by the IDENTICAL per-instance index
|
||||
// (gl_BaseInstanceARB + gl_InstanceID). instanceClipSlot[i] selects which
|
||||
// CellClip region instance i is clipped against. Default all-zeros in U.3 ⇒
|
||||
// every instance maps to slot 0 ⇒ no clipping ⇒ identical render to pre-U.3.
|
||||
//
|
||||
// CellClip std430 layout (144 bytes/slot): a uint count + 3 pad uints (16 bytes)
|
||||
// then vec4 planes[8] (8 × 16 = 128 bytes). vec4 array stride is 16 under std430.
|
||||
// ClipFrame on the CPU side lays out the bytes to match exactly (verified by
|
||||
// ClipFrameLayoutTests). A clip-space vertex is INSIDE iff dot(plane, gl_Position)
|
||||
// >= 0 for every active plane (see ClipPlaneSet for the plane convention).
|
||||
struct CellClip {
|
||||
uint count;
|
||||
uint _p0;
|
||||
uint _p1;
|
||||
uint _p2;
|
||||
vec4 planes[8];
|
||||
};
|
||||
layout(std430, binding = 2) readonly buffer ClipRegionBuf {
|
||||
CellClip clipRegions[];
|
||||
};
|
||||
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
||||
uint instanceClipSlot[];
|
||||
};
|
||||
|
||||
// === Fix B (A7 #3): per-OBJECT light selection — minimize_object_lighting =====
|
||||
// retail picks up-to-8 point/spot lights PER OBJECT by the object's own position
|
||||
// (minimize_object_lighting 0x0054d480), so a torch always lights the wall it
|
||||
// sits on, camera-INDEPENDENTLY. The previous single global nearest-8-to-CAMERA
|
||||
// UBO set (LightManager.Tick) made a wall brighten as the camera approached
|
||||
// (its torches swapping into the global top-8). Two SSBOs replace that for
|
||||
// point/spot lights (the SUN + ambient still come from the SceneLighting UBO):
|
||||
//
|
||||
// binding=4 — GLOBAL point/spot light array, uploaded once per frame from
|
||||
// LightManager.PointSnapshot. The index of a light here is stable for the frame.
|
||||
// binding=5 — per-instance light SET: MaxLightsPerObject(8) int indices per
|
||||
// instance INTO gLights[] (-1 = unused slot), parallel to the binding=0
|
||||
// instance buffer and indexed by the SAME instanceIndex. WbDrawDispatcher fills
|
||||
// it once per entity (the set is constant across the entity's parts/tuples).
|
||||
struct GlobalLight {
|
||||
vec4 posAndKind;
|
||||
vec4 dirAndRange;
|
||||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std430, binding = 4) readonly buffer GlobalLightBuf {
|
||||
GlobalLight gLights[];
|
||||
};
|
||||
layout(std430, binding = 5) readonly buffer InstanceLightSetBuf {
|
||||
int instanceLightIdx[]; // 8 per instance; -1 = unused
|
||||
};
|
||||
|
||||
// #142: per-instance "indoor" flag, 1 per instance, parallel to the binding=0
|
||||
// instance buffer (same instanceIndex). 1 = object parented to an EnvCell (skip the
|
||||
// sun — retail's useSunlight==0 interior stage); 0 = outdoor object (gets the sun).
|
||||
// Read ONLY inside the uniform `uLightingMode == 0` branch below, so the mode-1
|
||||
// (EnvCell shell) path provably never touches it — EnvCellRenderer need not bind it.
|
||||
layout(std430, binding = 6) readonly buffer InstanceIndoorBuf {
|
||||
uint instanceIndoor[];
|
||||
};
|
||||
|
||||
// #188: per-instance opacity multiplier, 1 per instance, parallel to the
|
||||
// binding=0 instance buffer (same instanceIndex). 1.0 = unmodified; <1.0
|
||||
// while a TransparentPartHook translucency fade is in flight for the
|
||||
// entity/part this instance belongs to (e.g. the "fading wall" secret-
|
||||
// passage doors). Multiplied against the sampled texture alpha in
|
||||
// mesh_modern.frag.
|
||||
layout(std430, binding = 7) readonly buffer InstanceAlphaBuf {
|
||||
float instanceAlpha[];
|
||||
};
|
||||
|
||||
// Retail SmartBox click confirmation. One vec2 per OBJECT instance, parallel
|
||||
// to binding=0: x = CMaterial luminosity, y = CMaterial diffuse. Normal
|
||||
// rendering is (0,1); SmartBox alternates LOW=(0,.35) and HIGH=(.99,1).
|
||||
// EnvCellRenderer uses uLightingMode=1 and deliberately never reads this
|
||||
// object-only binding.
|
||||
layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
|
||||
vec2 instanceSelectionLighting[];
|
||||
};
|
||||
|
||||
// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal
|
||||
// alongside gl_Position. The array is sized 8 to match the CellClip plane budget
|
||||
// and the GL guarantee (GL_MAX_CLIP_DISTANCES >= 8). The host enables
|
||||
// GL_CLIP_DISTANCE0..7 once at startup; unused planes are set to +1.0 below so
|
||||
// they pass everything (no clipping) when the slot's count < 8.
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
float gl_ClipDistance[8];
|
||||
};
|
||||
|
||||
uniform mat4 uViewProjection;
|
||||
// Absolute transform prefix in the shared shadow/world pose arena. Every
|
||||
// parallel per-instance array remains local to this submission, so only the
|
||||
// transform lookup keeps the absolute index.
|
||||
uniform uint uTextureIndexB;
|
||||
|
||||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): per-pass offset into Batches[].
|
||||
// gl_DrawIDARB resets to 0 at the start of each glMultiDrawElementsIndirect
|
||||
// call, so the transparent pass — which begins later in the indirect buffer
|
||||
// — was fetching Batches[0..transparentCount) instead of its actual section
|
||||
// at Batches[opaqueCount..end). The lifestone crystal (a transparent draw)
|
||||
// ended up reading the FIRST OPAQUE batch's TextureHandle every frame. As
|
||||
// the camera moved and the opaque front-to-back sort reordered which group
|
||||
// landed at BatchData[0], the lifestone's apparent texture flickered to
|
||||
// whatever was first — frequently the player character's body parts.
|
||||
//
|
||||
// WbDrawDispatcher.Draw sets this to 0 before the opaque MDI call and to
|
||||
// _opaqueDrawCount before the transparent MDI call, matching WorldBuilder's
|
||||
// uDrawIDOffset pattern in BaseObjectRenderManager.cs line 845.
|
||||
uniform int uDrawIDOffset;
|
||||
uniform int uLightingMode; // A7 Fix D: 0 = OBJECT (plain Lambert + sun), 1 = ENVCELL (half-Lambert wrap, no sun)
|
||||
// #176 stripe-hunt isolation modes (ACDREAM_LIGHT_DEBUG, throwaway diagnostic):
|
||||
// 0 = off; 1 = ambient-only vLit (all point/sun contributions killed);
|
||||
// 2 = DYNAMIC point lights killed (purples + viewer fill off, statics stay);
|
||||
// 3 = handled in the frag (raw vLit visualization, texture ignored).
|
||||
uniform int uLightDebug;
|
||||
|
||||
// SceneLighting UBO — binding=1 in the UBO namespace (GL keeps the SSBO and UBO
|
||||
// binding tables separate, so this coexists with the binding=1 BatchBuffer SSBO
|
||||
// above). IDENTICAL std140 layout to mesh_modern.frag.
|
||||
//
|
||||
// A7 (2026-06-15): lighting moved from the FRAGMENT shader to HERE (per-VERTEX) so
|
||||
// torch/point lights Gouraud-interpolate across each triangle the way retail's
|
||||
// fixed-function T&L does (D3D DrawEnvCell vertex bake + minimize_object_lighting for
|
||||
// objects). A per-PIXEL evaluation made a tight bright "spotlight" pool on flat walls;
|
||||
// per-vertex spreads it into a soft, broad gradient with no hard edge.
|
||||
struct Light {
|
||||
vec4 posAndKind;
|
||||
vec4 dirAndRange;
|
||||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
||||
Light uLights[8];
|
||||
vec4 uCellAmbient;
|
||||
vec4 uFogParams;
|
||||
vec4 uFogColor;
|
||||
vec4 uCameraAndTime;
|
||||
};
|
||||
|
||||
// Faithful calc_point_light (0x0059c8b0) contribution from ONE point/spot light —
|
||||
// the wrap + norm shape, factored out so the per-object SSBO loop shares it. D =
|
||||
// light − vertex, used UN-normalised (length = dist); N is the unit vertex normal.
|
||||
// Returns the RGB to ADD, already per-channel capped to the light's own colour.
|
||||
vec3 pointContribution(vec3 N, vec3 worldPos, GlobalLight L) {
|
||||
int kind = int(L.posAndKind.w);
|
||||
vec3 toL = L.posAndKind.xyz - worldPos; // D (un-normalised)
|
||||
float distsq = dot(toL, toL);
|
||||
float d = sqrt(distsq);
|
||||
float range = L.dirAndRange.w; // falloff_eff = Falloff × 1.3 (static) / × 1.5 (dynamic)
|
||||
if (d >= range || range <= 1e-4) return vec3(0.0);
|
||||
float intensity = L.colorAndIntensity.w;
|
||||
vec3 baseCol = L.colorAndIntensity.xyz;
|
||||
|
||||
// #143: DYNAMIC lights (viewer fill, portal, server-object lights — flagged by
|
||||
// coneAngleEtc.y==1 from GlobalLightPacker) use retail's D3D hardware attenuation
|
||||
// (config_hardware_light 0x0059ad30): a POINT light is given Attenuation1=1 ⇒
|
||||
// att = 1/d (inverse-LINEAR), plain Lambert N·L, hard range cutoff. That spreads
|
||||
// softly across the room (the portal tint, the viewer fill) instead of the static
|
||||
// bake's 1/d³ distance-cube, which makes a tight concentrated pool. No per-light
|
||||
// cap — D3D accumulates then saturates, which accumulateLights does via min(pointAcc,1).
|
||||
if (L.coneAngleEtc.y > 0.5) {
|
||||
if (uLightDebug == 2) return vec3(0.0); // #176 stripe hunt: dynamics killed
|
||||
vec3 Ldir = toL / max(d, 1e-4);
|
||||
float ndl = max(0.0, dot(N, Ldir));
|
||||
if (ndl <= 0.0) return vec3(0.0);
|
||||
if (kind == 2) { // dynamic spot: hard cos-cone gate
|
||||
if (dot(-Ldir, L.dirAndRange.xyz) <= cos(L.coneAngleEtc.x * 0.5)) return vec3(0.0);
|
||||
}
|
||||
return (intensity * ndl / max(d, 1e-3)) * baseCol; // att = 1/d
|
||||
}
|
||||
|
||||
// ── STATIC dat-baked lights: retail's per-vertex bake (calc_point_light 0x0059c8b0) ──
|
||||
// A7 Fix D D-3: angular term by lighting path. ENVCELL bake (mode 1) keeps the
|
||||
// half-Lambert wrap (lights surfaces angled away, retail calc_point_light); OBJECT
|
||||
// mode (0) uses plain Lambert max(0,N·L) so a torch BEHIND a character contributes
|
||||
// nothing (retail's hardware path). toL is un-normalised (length d).
|
||||
float angular = (uLightingMode == 1)
|
||||
? (1.0 / 1.5) * (dot(N, toL) + 0.5 * d) // half-Lambert wrap (EnvCell bake)
|
||||
: max(0.0, dot(N, toL)); // plain Lambert (object/hardware)
|
||||
if (angular <= 0.0) return vec3(0.0);
|
||||
// NORM branch (distance-cube): >1 m → distsq·d ≈ inverse-square soft far halo;
|
||||
// <1 m → just d (dodge the near singularity). "Punchy near, soft far."
|
||||
float norm = (distsq > 1.0) ? (distsq * d) : d;
|
||||
float scale = (1.0 - d / range) * intensity * (angular / norm);
|
||||
if (kind == 2) {
|
||||
// Spotlight: hard-edged cos-cone gate layered on the point ramp.
|
||||
vec3 Ldir = toL / max(d, 1e-4);
|
||||
float cos_edge = cos(L.coneAngleEtc.x * 0.5);
|
||||
float cos_l = dot(-Ldir, L.dirAndRange.xyz);
|
||||
if (cos_l <= cos_edge) scale = 0.0;
|
||||
}
|
||||
// Per-channel no-blowout cap to the light's OWN colour (un-intensity-scaled):
|
||||
// a single light can't push a channel past its colour. Summed lit clamped in frag.
|
||||
return min(scale * baseCol, baseCol);
|
||||
}
|
||||
|
||||
vec3 accumulateAmbientLocalLights(
|
||||
vec3 N,
|
||||
vec3 worldPos,
|
||||
int instanceIndex,
|
||||
out vec3 directionalLit)
|
||||
{
|
||||
vec3 lit = uCellAmbient.xyz;
|
||||
directionalLit = vec3(0.0);
|
||||
if (uLightDebug == 1) return lit; // #176 stripe hunt: ambient only
|
||||
|
||||
// SUN / directional — OBJECT path only (mode 0). retail's EnvCell path
|
||||
// (minimize_envcell_lighting) enables only dynamic lights, NEVER the sun, so
|
||||
// EnvCell walls (mode 1) get no directional sun wash (A7 Fix D D-4).
|
||||
// #142: within mode 0, also skip the sun for indoor objects (ParentCellId is an
|
||||
// EnvCell). This mirrors retail's per-draw-stage useSunlight toggle: the interior
|
||||
// stage runs useSunlightSet(0) (PView::DrawCells 0x005a49f3), so indoor objects
|
||||
// get no sun even in windowed buildings where the player's frame is not sun-killed.
|
||||
if (uLightingMode == 0) {
|
||||
if (instanceIndoor[instanceIndex] == 0u) { // #142: outdoor objects only get the sun
|
||||
int activeLights = int(uCellAmbient.w);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (i >= activeLights) break;
|
||||
if (int(uLights[i].posAndKind.w) != 0) continue; // directional only
|
||||
vec3 Ldir = normalize(uShadowLightDirectionAndSource.xyz);
|
||||
float ndl = max(0.0, dot(N, Ldir));
|
||||
directionalLit += uLights[i].colorAndIntensity.xyz
|
||||
* uLights[i].colorAndIntensity.w * ndl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// POINT / SPOT torches: their OWN accumulator (A7 Fix D, D-1). Retail's
|
||||
// SetStaticLightingVertexColors sums the static point lights from BLACK and
|
||||
// clamps the SUM to [0,1] before anything else (a baked emissive term), so a
|
||||
// few warm intensity-100 torches can't push the whole pixel to white the way
|
||||
// folding them into ambient+sun did. Mirrors LightBake.ComputeVertexColor
|
||||
// (LightBakeConformanceTests). Per-light cap inside pointContribution is unchanged.
|
||||
vec3 pointAcc = vec3(0.0);
|
||||
int base = instanceIndex * 8;
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
int gi = instanceLightIdx[base + k];
|
||||
if (gi < 0) continue;
|
||||
pointAcc += pointContribution(N, worldPos, gLights[gi]);
|
||||
}
|
||||
lit += min(pointAcc, vec3(1.0)); // clamp the torch sum on its own (retail baked emissive)
|
||||
|
||||
return lit; // frag still does the final min(lit, 1.0)
|
||||
}
|
||||
|
||||
out vec3 vNormal;
|
||||
out vec2 vTexCoord;
|
||||
out vec3 vWorldPos;
|
||||
out vec3 vAmbientLocalLit; // authored ambient + capped local/point lights
|
||||
out vec3 vDirectionalLit; // authored outdoor directional sun, shadowable
|
||||
// Campaign V slice V6e: was `flat uvec2 vTextureHandle` — a raw 64-bit
|
||||
// GL_ARB_bindless_texture handle handed across the stage boundary. A varying
|
||||
// cannot carry a Vulkan descriptor, so what travels is the table SLOT and the
|
||||
// fragment stage does the lookup (see mesh_modern.frag). Under GL the value
|
||||
// sampled is bit-for-bit the one the vertex stage used to forward; the SSBO
|
||||
// read simply happens one stage later, and `flat` keeps it one scalar load per
|
||||
// primitive rather than per fragment.
|
||||
out flat uint vTextureIndex;
|
||||
out flat uint vTextureLayer;
|
||||
out flat float vOpacityMultiplier; // #188
|
||||
out flat vec2 vSelectionLighting;
|
||||
out flat uint vReceivesDirectionalShadow;
|
||||
|
||||
void main() {
|
||||
int transformIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
int instanceIndex = transformIndex - int(uTextureIndexB);
|
||||
mat4 model = Instances[transformIndex].transform;
|
||||
vOpacityMultiplier = instanceAlpha[instanceIndex]; // #188
|
||||
vSelectionLighting = (uLightingMode == 0)
|
||||
? instanceSelectionLighting[instanceIndex]
|
||||
: vec2(0.0, 1.0);
|
||||
|
||||
vec4 worldPos = model * vec4(aPosition, 1.0);
|
||||
gl_Position = uViewProjection * worldPos;
|
||||
|
||||
// Phase U.3: per-instance clip gate. instanceClipSlot is indexed by the
|
||||
// SAME instanceIndex used for the binding=0 transform above, so the slot
|
||||
// travels with the instance through the MDI BaseInstance offsets. Slot 0
|
||||
// (the U.3 default) has count 0 ⇒ the second loop sets all 8 distances to
|
||||
// +1.0 ⇒ nothing is clipped.
|
||||
uint _slot = instanceClipSlot[instanceIndex];
|
||||
CellClip _c = clipRegions[_slot];
|
||||
for (uint i = 0u; i < _c.count; ++i)
|
||||
gl_ClipDistance[i] = dot(_c.planes[i], gl_Position);
|
||||
for (uint i = _c.count; i < 8u; ++i)
|
||||
gl_ClipDistance[i] = 1.0;
|
||||
|
||||
vWorldPos = worldPos.xyz;
|
||||
vNormal = normalize(mat3(model) * aNormal);
|
||||
vAmbientLocalLit = accumulateAmbientLocalLights(
|
||||
vNormal,
|
||||
vWorldPos,
|
||||
instanceIndex,
|
||||
vDirectionalLit);
|
||||
// EnvCell-parented objects keep the authored indoor result. The separate
|
||||
// EnvCell shell renderer never selects this receiver variant at all.
|
||||
vReceivesDirectionalShadow = (uLightingMode == 0
|
||||
&& instanceIndoor[instanceIndex] == 0u)
|
||||
? 1u
|
||||
: 0u;
|
||||
vTexCoord = aTexCoord;
|
||||
|
||||
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
// Campaign V slice V6e: forward the table SLOT untouched. V2 looked the
|
||||
// handle up here and passed the handle; the lookup now lives at the sample
|
||||
// site in mesh_modern.frag, which is the only form Vulkan can express.
|
||||
vTextureIndex = b.textureIndex;
|
||||
vTextureLayer = b.textureLayer;
|
||||
}
|
||||
42
src/AcDream.App/Rendering/Shaders/mesh_detail.frag
Normal file
42
src/AcDream.App/Rendering/Shaders/mesh_detail.frag
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#version 430 core
|
||||
#extension GL_ARB_bindless_texture : require
|
||||
|
||||
in vec2 vBaseUv;
|
||||
in vec2 vDetailUv;
|
||||
in float vDetailFade;
|
||||
in flat uint vBaseTextureIndex;
|
||||
in flat uint vBaseTextureLayer;
|
||||
in flat uint vBatchFlags;
|
||||
in flat uint vDetailCategory;
|
||||
|
||||
uniform uint uTextureIndexA; // category detail texture, layer 0
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
// Object command replays may contain ordinary instances; only building
|
||||
// shells survive. Bit 0 means this command came through retail's built-mesh
|
||||
// DrawMesh path. Unlike the land-polygon path, RenderMeshSubset receives
|
||||
// curr_detail_surface for every built-mesh material subset.
|
||||
if (vDetailCategory == 0u || (vBatchFlags & 1u) == 0u)
|
||||
discard;
|
||||
if (vDetailFade <= 0.0)
|
||||
discard;
|
||||
|
||||
vec4 base = ACDREAM_SAMPLE_ARRAY(
|
||||
vBaseTextureIndex,
|
||||
vec3(vBaseUv, float(vBaseTextureLayer)));
|
||||
if (base.a < 0.05)
|
||||
discard;
|
||||
|
||||
vec4 detail = ACDREAM_SAMPLE_ARRAY(
|
||||
uTextureIndexA,
|
||||
vec3(vDetailUv, 0.0));
|
||||
|
||||
// Pipeline blend is retail's DstColor + OneMinusSrcAlpha. Scaling both
|
||||
// source colour and alpha makes fade=0 exactly neutral while fade=1 keeps
|
||||
// retail's measured factor: dest * (detail.rgb + 1 - detail.a).
|
||||
FragColor = vec4(
|
||||
detail.rgb * vDetailFade,
|
||||
detail.a * vDetailFade);
|
||||
}
|
||||
94
src/AcDream.App/Rendering/Shaders/mesh_detail.vert
Normal file
94
src/AcDream.App/Rendering/Shaders/mesh_detail.vert
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#version 430 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData {
|
||||
mat4 transform;
|
||||
};
|
||||
|
||||
struct BatchData {
|
||||
uint textureIndex;
|
||||
uint _pad;
|
||||
uint textureLayer;
|
||||
uint flags;
|
||||
};
|
||||
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||||
BatchData Batches[];
|
||||
};
|
||||
|
||||
struct CellClip {
|
||||
uint count;
|
||||
uint _p0;
|
||||
uint _p1;
|
||||
uint _p2;
|
||||
vec4 planes[8];
|
||||
};
|
||||
layout(std430, binding = 2) readonly buffer ClipRegionBuf {
|
||||
CellClip clipRegions[];
|
||||
};
|
||||
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
||||
uint instanceClipSlot[];
|
||||
};
|
||||
|
||||
// Object renderer only: 1 for a retail building shell, 0 for ordinary
|
||||
// scenery/creatures/players. EnvCellRenderer sets uParamB=0 and ignores the
|
||||
// value, but still binds one valid word because Vulkan sees this static use.
|
||||
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
||||
uint instanceDetailCategory[];
|
||||
};
|
||||
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
float gl_ClipDistance[8];
|
||||
};
|
||||
|
||||
uniform mat4 uViewProjection;
|
||||
uniform int uDrawIDOffset;
|
||||
uniform uint uTextureIndexB; // absolute transform prefix in the shared pose arena
|
||||
uniform float uParamA; // detail UV tiling
|
||||
uniform float uParamB; // 1 = require building instance, 0 = EnvCell category
|
||||
|
||||
out vec2 vBaseUv;
|
||||
out vec2 vDetailUv;
|
||||
out float vDetailFade;
|
||||
out flat uint vBaseTextureIndex;
|
||||
out flat uint vBaseTextureLayer;
|
||||
out flat uint vBatchFlags;
|
||||
out flat uint vDetailCategory;
|
||||
|
||||
void main() {
|
||||
int transformIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
int instanceIndex = transformIndex - int(uTextureIndexB);
|
||||
vec4 worldPos = Instances[transformIndex].transform * vec4(aPosition, 1.0);
|
||||
gl_Position = uViewProjection * worldPos;
|
||||
|
||||
uint slot = instanceClipSlot[instanceIndex];
|
||||
CellClip clip = clipRegions[slot];
|
||||
for (uint i = 0u; i < clip.count; ++i)
|
||||
gl_ClipDistance[i] = dot(clip.planes[i], gl_Position);
|
||||
for (uint i = clip.count; i < 8u; ++i)
|
||||
gl_ClipDistance[i] = 1.0;
|
||||
|
||||
// System.Numerics' perspective projection used by every gameplay camera
|
||||
// makes clip.w the positive view-space depth. Retail get_alpha_for_z uses
|
||||
// that same metric in metres: 255 through 10 m, linearly to 0 at 50 m.
|
||||
float positiveViewDepthMetres = gl_Position.w;
|
||||
vDetailFade = clamp((50.0 - positiveViewDepthMetres) / 40.0, 0.0, 1.0);
|
||||
vBaseUv = aTexCoord;
|
||||
vDetailUv = aTexCoord * uParamA;
|
||||
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vBaseTextureIndex = batch.textureIndex;
|
||||
vBaseTextureLayer = batch.textureLayer;
|
||||
vBatchFlags = batch.flags;
|
||||
vDetailCategory = uParamB > 0.5
|
||||
? instanceDetailCategory[instanceIndex]
|
||||
: 1u;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.frag.spv
Normal file
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.frag.spv
Normal file
Binary file not shown.
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.vert.spv
Normal file
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.vert.spv
Normal file
Binary file not shown.
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_detail.frag.spv
Normal file
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_detail.frag.spv
Normal file
Binary file not shown.
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv
Normal file
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv
Normal file
Binary file not shown.
|
|
@ -1,6 +1,102 @@
|
|||
{
|
||||
"note": "Campaign V slice V6c. Regenerate with tools/compile-shaders.ps1.",
|
||||
"shaders": [
|
||||
{
|
||||
"name": "atmospheric_bloom_blur",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "81222e42a52d0b560f5916b0f312bcc5370e42e596c821477d71c61883c3d025",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "4b22872f61b462cdbc82d4af38c7693212bc7446c7c68faa32dd5585b08c43ae",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "atmospheric_bloom_downsample",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "81222e42a52d0b560f5916b0f312bcc5370e42e596c821477d71c61883c3d025",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "a01f09dfcc62acc5376f6e61be6aa2e8c5b0506550c0fa318f4434cbf3e6a17a",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "atmospheric_filmic",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "81222e42a52d0b560f5916b0f312bcc5370e42e596c821477d71c61883c3d025",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "240d2fe5e13e3850ceb79f178f1c248c71875fdc1973ff8cab1c274660ebbc4b",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "atmospheric_sun_occlusion",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "c21f381f2de05afc4415b2e348884a3cf6440da279b57d9a05a5d6ae5e9ac453",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "bfbc8c508b21dec84b21b2760877bfcb736c4f233e8557f6d1f8b83600683d5e",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "atmospheric_sun_rays",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "81222e42a52d0b560f5916b0f312bcc5370e42e596c821477d71c61883c3d025",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "13875d9f6fd28f1049d1f94741db13086f72cc7170659eb73c369b4c99b00a4f",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "atmospheric_volumetric",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "81222e42a52d0b560f5916b0f312bcc5370e42e596c821477d71c61883c3d025",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "8d6177707a95cf0230881bbfb7467c0591632eb67dce3c758596c83cafb14ffc",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "debug_line",
|
||||
"vulkanReady": true,
|
||||
|
|
@ -17,6 +113,134 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "directional_shadow_terrain",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "e863894aa1d66508daad86806af2b2a3509088109754c491a3607477f78ed644",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "2b9ebbabc96c3ba53ce58cea175c52932c81d1d93820550f29a0734131dba5a3",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "directional_shadow_terrain_multiview",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "2f7d7bba3aa19a4891c30000e29c0bb84a2afdc26a44248d4c48bd0ffed8a04a",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "2b9ebbabc96c3ba53ce58cea175c52932c81d1d93820550f29a0734131dba5a3",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "directional_shadow_world_cutout",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "8331583c1d63ee59b3f7898e25b2e9730df98fd9da28273661caad948fb46ae5",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "cd9c404a9379715061358cb18a99b9310acbdd5062f0ad982ee9a37cd9f99922",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "directional_shadow_world_cutout_multiview",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "a4bb3b86283af310a22d3943dd1afadb8a72b42c9e5501efc9abf2b5124a1446",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "cd9c404a9379715061358cb18a99b9310acbdd5062f0ad982ee9a37cd9f99922",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "directional_shadow_world_opaque",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "c2586df5f09518c40d052bbacdfff2c00a82b1eefec4868749d0580571198067",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "2b9ebbabc96c3ba53ce58cea175c52932c81d1d93820550f29a0734131dba5a3",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "directional_shadow_world_opaque_multiview",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "35b4d524153623691ab523a049212aa35551f96c169c28c08f62d93e31d9e68d",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "2b9ebbabc96c3ba53ce58cea175c52932c81d1d93820550f29a0734131dba5a3",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mesh_atmospheric",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "d4f8bc12ee84379ced84f5b703cf8be95798a23a7063773e214bc1eb6ebbb65e",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "62b001a72080ea74bdaef0fb2a4ed92533feaeab76187ce4215813c6327432e7",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mesh_detail",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "0273312da9fefb5084d3aee120f33c542e1adcfb846e6c7b3fc2b068c1348fb3",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "1fbc3ffb12d260cbe0d111551f28c69f15754ba5e5e11fe3ec43747bebef4883",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mesh_modern",
|
||||
"vulkanReady": true,
|
||||
|
|
@ -97,6 +321,22 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "terrain_atmospheric",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "06258c7ead0e123740e325c802156987ed17dabe355d591d0e90facb420b8e04",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "9927abe9cc4fb83429d3e2ec8ee8d13f0b1936cebf484a4e529dcfdab431c3b8",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "terrain_modern",
|
||||
"vulkanReady": true,
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
208
src/AcDream.App/Rendering/Shaders/terrain_atmospheric.frag
Normal file
208
src/AcDream.App/Rendering/Shaders/terrain_atmospheric.frag
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
#version 460 core
|
||||
#extension GL_ARB_bindless_texture : require
|
||||
|
||||
// Phase N.5b: terrain fragment shader on the modern bindless dispatcher.
|
||||
// Math identical to terrain.frag (Phase 3c per-cell maskBlend3 +
|
||||
// Phase G fog + lightning flash).
|
||||
//
|
||||
// Texture reads go through ACDREAM_SAMPLE_ARRAY, which
|
||||
// tools/ShaderCompiler/VulkanGlslPreamble.cs injects to index the set-2
|
||||
// descriptor array. GL is deleted (Campaign V slice V11); before that this
|
||||
// macro expanded (via the now-deleted common.glsl) to the uvec2-handle +
|
||||
// sampler2DArray-constructor pattern this shader used on that arm — the
|
||||
// documented "always works" form per the ARB_bindless_texture spec, and the
|
||||
// one that avoided the GL_INVALID_OPERATION the alternative (`uniform
|
||||
// sampler2DArray` set via glProgramUniformHandleARB) produced on at least one
|
||||
// driver in practice. The extension requirement above is dropped for Vulkan
|
||||
// by the compiler's preamble, where it would be an error rather than a no-op.
|
||||
|
||||
in vec2 vBaseUV;
|
||||
in vec3 vWorldNormal;
|
||||
in vec3 vWorldPos;
|
||||
in vec3 vAmbientLocalLit;
|
||||
in vec3 vDirectionalLit;
|
||||
in vec4 vOverlay0;
|
||||
in vec4 vOverlay1;
|
||||
in vec4 vOverlay2;
|
||||
in vec4 vRoad0;
|
||||
in vec4 vRoad1;
|
||||
flat in float vBaseTexIdx;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
// Campaign V slice V2b (2026-07-27): uTerrainHandle/uAlphaHandle (uvec2, raw
|
||||
// ARB_bindless_texture handles) became uTextureIndexA/uTextureIndexB (slots
|
||||
// into the global texture table, ACDREAM_TEXTURE_HANDLE injected by
|
||||
// tools/ShaderCompiler/VulkanGlslPreamble.cs). Named to match the pinned
|
||||
// GpuPushConstants.TextureIndexA/B fields so V4d's
|
||||
// move to push constants is a rename, not a redesign — there is no
|
||||
// push-constant plumbing yet, so these stay plain uniforms for now.
|
||||
uniform uint uTextureIndexA;
|
||||
uniform uint uTextureIndexB;
|
||||
|
||||
// Campaign V slice V6f-3: the two atlases are sampled through the
|
||||
// dialect-neutral table read instead of a GL sampler-from-handle constructor.
|
||||
// `sampler2DArray(handle)` is a GL_ARB_bindless_texture form with no Vulkan
|
||||
// equivalent — Vulkan's table is an opaque descriptor array in set 2, and there
|
||||
// is no handle to construct a sampler from. ACDREAM_SAMPLE_ARRAY asks the
|
||||
// question both dialects can answer ("sample table slot N at these
|
||||
// coordinates") and expands to the right thing on each.
|
||||
//
|
||||
// A SAMPLING macro, not a sampler-returning one, for the reason
|
||||
// VulkanGlslPreamble.cs records: under Vulkan the expansion carries `nonuniformEXT` on the indexing
|
||||
// expression, and binding the result to a local sampler2DArray first is exactly
|
||||
// where an implementation may drop that qualifier. The old `#define uTerrain
|
||||
// sampler2DArray(...)` was that shape textually, so keeping it would have
|
||||
// reintroduced the hazard at every use site.
|
||||
#define sampleTerrain(uvw) ACDREAM_SAMPLE_ARRAY(uTextureIndexA, uvw)
|
||||
#define sampleAlpha(uvw) ACDREAM_SAMPLE_ARRAY(uTextureIndexB, uvw)
|
||||
|
||||
// Campaign V slice V6f-2: the 36 per-layer tiling factors moved out of a loose
|
||||
// `uniform float uTexTiling[36]` and into the uniform buffer GpuBindingModel
|
||||
// reserved binding 3 for. Vulkan GLSL has no default uniform block, so the loose
|
||||
// array was unspellable there, and at 144 bytes of payload it cannot ride the
|
||||
// 96-byte push-constant block either. A uniform buffer is the only legal home,
|
||||
// and the same declaration is legal in both dialects.
|
||||
//
|
||||
// The ELEMENT TYPE is deliberately unchanged. std140 pads every array element to
|
||||
// 16 bytes, so the block is 576 bytes rather than 144, and packing four floats
|
||||
// per vec4 would be tighter — but it would also change every use site below.
|
||||
// Keeping `float uTexTiling[36]` means `uTexTiling[int(layer)]` reads exactly as
|
||||
// it did before, so this commit's pixel gate is measuring the move to a uniform
|
||||
// buffer and nothing else. TerrainTextureTilingTable.UniformBufferBytes and the
|
||||
// stride beside it are the CPU half of this layout.
|
||||
layout(std140, ACDREAM_UBO_SET binding = 3) uniform TerrainTiling {
|
||||
float uTexTiling[36];
|
||||
};
|
||||
|
||||
struct Light {
|
||||
vec4 posAndKind;
|
||||
vec4 dirAndRange;
|
||||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
||||
Light uLights[8];
|
||||
vec4 uCellAmbient;
|
||||
vec4 uFogParams;
|
||||
vec4 uFogColor;
|
||||
vec4 uCameraAndTime;
|
||||
};
|
||||
|
||||
#include "directional_shadow_receiver.glsl"
|
||||
|
||||
// Retail TexMerge::CopyAndTile (0x00503580) and TexMerge::Merge
|
||||
// (0x005038C0) pass TerrainTex::tex_tiling to every terrain source before
|
||||
// the cell-scale alpha mask is applied. The atlas stores that value by layer.
|
||||
float terrainTiling(float layer) {
|
||||
return uTexTiling[int(layer)];
|
||||
}
|
||||
|
||||
vec4 maskBlend3(vec4 t0, vec4 t1, vec4 t2, float h0, float h1, float h2) {
|
||||
float a0 = h0 == 0.0 ? 1.0 : t0.a;
|
||||
float a1 = h1 == 0.0 ? 1.0 : t1.a;
|
||||
float a2 = h2 == 0.0 ? 1.0 : t2.a;
|
||||
float aR = 1.0 - (a0 * a1 * a2);
|
||||
float aRsafe = max(aR, 1e-6);
|
||||
a0 = 1.0 - a0;
|
||||
a1 = 1.0 - a1;
|
||||
a2 = 1.0 - a2;
|
||||
vec3 r0 = (a0 * t0.rgb + (1.0 - a0) * a1 * t1.rgb + (1.0 - a1) * a2 * t2.rgb);
|
||||
return vec4(r0 / aRsafe, aR);
|
||||
}
|
||||
|
||||
vec4 combineOverlays(vec2 baseUV, vec4 pOverlay0, vec4 pOverlay1, vec4 pOverlay2) {
|
||||
float h0 = pOverlay0.z < 0.0 ? 0.0 : 1.0;
|
||||
float h1 = pOverlay1.z < 0.0 ? 0.0 : 1.0;
|
||||
float h2 = pOverlay2.z < 0.0 ? 0.0 : 1.0;
|
||||
vec4 t0 = vec4(0.0), t1 = vec4(0.0), t2 = vec4(0.0);
|
||||
|
||||
if (h0 > 0.0) {
|
||||
t0 = sampleTerrain(vec3(baseUV * terrainTiling(pOverlay0.z), pOverlay0.z));
|
||||
if (pOverlay0.w >= 0.0) {
|
||||
vec4 a = sampleAlpha(vec3(pOverlay0.xy, pOverlay0.w));
|
||||
t0.a = a.a;
|
||||
}
|
||||
}
|
||||
if (h1 > 0.0) {
|
||||
t1 = sampleTerrain(vec3(baseUV * terrainTiling(pOverlay1.z), pOverlay1.z));
|
||||
if (pOverlay1.w >= 0.0) {
|
||||
vec4 a = sampleAlpha(vec3(pOverlay1.xy, pOverlay1.w));
|
||||
t1.a = a.a;
|
||||
}
|
||||
}
|
||||
if (h2 > 0.0) {
|
||||
t2 = sampleTerrain(vec3(baseUV * terrainTiling(pOverlay2.z), pOverlay2.z));
|
||||
if (pOverlay2.w >= 0.0) {
|
||||
vec4 a = sampleAlpha(vec3(pOverlay2.xy, pOverlay2.w));
|
||||
t2.a = a.a;
|
||||
}
|
||||
}
|
||||
return maskBlend3(t0, t1, t2, h0, h1, h2);
|
||||
}
|
||||
|
||||
vec4 combineRoad(vec2 baseUV, vec4 pRoad0, vec4 pRoad1) {
|
||||
float h0 = pRoad0.z < 0.0 ? 0.0 : 1.0;
|
||||
float h1 = pRoad1.z < 0.0 ? 0.0 : 1.0;
|
||||
vec4 result = vec4(0.0);
|
||||
if (h0 > 0.0) {
|
||||
result = sampleTerrain(vec3(baseUV * terrainTiling(pRoad0.z), pRoad0.z));
|
||||
if (pRoad0.w >= 0.0) {
|
||||
vec4 a0 = sampleAlpha(vec3(pRoad0.xy, pRoad0.w));
|
||||
result.a = 1.0 - a0.a;
|
||||
if (h1 > 0.0 && pRoad1.w >= 0.0) {
|
||||
vec4 a1 = sampleAlpha(vec3(pRoad1.xy, pRoad1.w));
|
||||
result.a = 1.0 - (a0.a * a1.a);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
vec3 applyFog(vec3 lit, vec3 worldPos) {
|
||||
int mode = int(uFogParams.w);
|
||||
if (mode == 0) return lit;
|
||||
float d = length(worldPos - uCameraAndTime.xyz);
|
||||
float fogStart = uFogParams.x;
|
||||
float fogEnd = uFogParams.y;
|
||||
float span = max(1e-3, fogEnd - fogStart);
|
||||
float fog = clamp((d - fogStart) / span, 0.0, 1.0);
|
||||
return mix(lit, uFogColor.xyz, fog);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 baseColor = vec4(0.0);
|
||||
if (vBaseTexIdx >= 0.0) {
|
||||
baseColor = sampleTerrain(vec3(vBaseUV * terrainTiling(vBaseTexIdx), vBaseTexIdx));
|
||||
}
|
||||
|
||||
vec4 overlays = vec4(0.0);
|
||||
if (vOverlay0.z >= 0.0)
|
||||
overlays = combineOverlays(vBaseUV, vOverlay0, vOverlay1, vOverlay2);
|
||||
|
||||
vec4 roads = vec4(0.0);
|
||||
if (vRoad0.z >= 0.0)
|
||||
roads = combineRoad(vBaseUV, vRoad0, vRoad1);
|
||||
|
||||
vec3 baseMasked = baseColor.rgb * ((1.0 - overlays.a) * (1.0 - roads.a));
|
||||
vec3 ovlMasked = overlays.rgb * (overlays.a * (1.0 - roads.a));
|
||||
vec3 roadMasked = roads.rgb * roads.a;
|
||||
vec3 rgb = clamp(baseMasked + ovlMasked + roadMasked, 0.0, 1.0);
|
||||
|
||||
vec3 surfaceToLight = normalize(uShadowLightDirectionAndSource.xyz);
|
||||
float directionalVisibility = acdreamDirectionalShadowVisibility(
|
||||
vWorldPos,
|
||||
normalize(vWorldNormal),
|
||||
uCameraAndTime.xyz,
|
||||
surfaceToLight);
|
||||
vec3 lighting = vAmbientLocalLit
|
||||
+ vDirectionalLit * directionalVisibility;
|
||||
vec3 lit = rgb * min(lighting, vec3(1.0));
|
||||
|
||||
float flash = uFogParams.z;
|
||||
lit += flash * vec3(0.6, 0.6, 0.75);
|
||||
|
||||
lit = applyFog(lit, vWorldPos);
|
||||
|
||||
fragColor = vec4(lit, 1.0);
|
||||
}
|
||||
197
src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert
Normal file
197
src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
#version 460 core
|
||||
#extension GL_ARB_bindless_texture : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
|
||||
// Phase N.5b: terrain shader on the modern bindless dispatcher.
|
||||
// Math identical to terrain.vert (Phase 3c per-cell mesh + Phase G AdjustPlanes
|
||||
// lighting). The only structural change is the version + bindless extension
|
||||
// — sampler access in the fragment stage is unchanged at the GLSL level.
|
||||
|
||||
layout(location = 0) in vec3 aPos;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in uvec4 aPacked0;
|
||||
layout(location = 3) in uvec4 aPacked1;
|
||||
layout(location = 4) in uvec4 aPacked2;
|
||||
layout(location = 5) in uvec4 aPacked3;
|
||||
|
||||
// Campaign V slice V6f-1: uView/uProjection converged into the single
|
||||
// uViewProjection that GpuPushConstants already carries, so terrain can be
|
||||
// expressed in Vulkan GLSL at all — two loose mat4 uniforms are 128 bytes and
|
||||
// cannot both fit the pinned 96-byte push block, and Vulkan GLSL has no default
|
||||
// uniform block to hold them loose. The product is now formed on the CPU
|
||||
// (camera.View * camera.Projection) instead of per vertex here; the two are the
|
||||
// same transform, and System.Numerics' row-vector layout uploaded untransposed
|
||||
// reads in GLSL as the transpose, so (View*Proj)^T == Proj^T * View^T is exactly
|
||||
// the uProjection * uView this replaced.
|
||||
uniform mat4 uViewProjection;
|
||||
|
||||
struct Light {
|
||||
vec4 posAndKind;
|
||||
vec4 dirAndRange;
|
||||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
||||
Light uLights[8];
|
||||
vec4 uCellAmbient;
|
||||
vec4 uFogParams;
|
||||
vec4 uFogColor;
|
||||
vec4 uCameraAndTime;
|
||||
};
|
||||
|
||||
// === Phase U.3: terrain screen-space clip gate (OutsideView region) ===========
|
||||
// Terrain is a single global region (the OutsideView), so it needs one set of
|
||||
// clip planes, not a per-instance slot table like the mesh shader. A std140 UBO
|
||||
// at binding=2 carries it. The UBO binding namespace is distinct from the SSBO
|
||||
// binding namespace, so this does NOT collide with the mesh shader's SSBO
|
||||
// binding=2 — and within THIS shader binding=1 (SceneLighting) is the only other
|
||||
// UBO, leaving binding=2 free. uTerrainClipCount == 0 (the U.3 default) ungates
|
||||
// terrain entirely (the second loop sets all 8 distances to +1.0). Uploaded by
|
||||
// ClipFrame.UploadShared each frame; TerrainModernRenderer binds it before draw.
|
||||
//
|
||||
// Campaign V slice V6i-2: ACDREAM_UBO_SET is what puts this in set 1 under the
|
||||
// Vulkan dialect and expands to nothing under GL. Omitting it left the block at
|
||||
// set 0 binding 2, which the storage layout declares as a STORAGE buffer — see
|
||||
// plan §5.5.12 finding 2, measured on the committed SPIR-V rather than inferred.
|
||||
// sky.vert declares the SAME block correctly and is the precedent.
|
||||
layout(std140, ACDREAM_UBO_SET binding = 2) uniform TerrainClip {
|
||||
int uTerrainClipCount;
|
||||
vec4 uTerrainClipPlanes[8];
|
||||
};
|
||||
|
||||
// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal.
|
||||
// Sized 8 to match GL_MAX_CLIP_DISTANCES >= 8. Host enables GL_CLIP_DISTANCE0..7
|
||||
// once at startup; unused planes are set to +1.0 below so they pass everything.
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
float gl_ClipDistance[8];
|
||||
};
|
||||
|
||||
out vec2 vBaseUV;
|
||||
out vec3 vWorldNormal;
|
||||
out vec3 vWorldPos;
|
||||
out vec3 vAmbientLocalLit;
|
||||
out vec3 vDirectionalLit;
|
||||
out vec4 vOverlay0;
|
||||
out vec4 vOverlay1;
|
||||
out vec4 vOverlay2;
|
||||
out vec4 vRoad0;
|
||||
out vec4 vRoad1;
|
||||
flat out float vBaseTexIdx;
|
||||
|
||||
// Retail's N·L floor from FUN_00532440 lines 2119/2138/2157/2176 at
|
||||
// chunk_00530000.c (AdjustPlanes). The decompile reads:
|
||||
// if (fVar3 < DAT_00796344) fVar3 = DAT_00796344;
|
||||
// applied to the clamped Lambert result BEFORE it's multiplied into
|
||||
// dirColor. DAT_00796344's exact literal isn't pinned by the decompile
|
||||
// but every other "floor" use in retail clamps negatives to zero (the
|
||||
// physically-correct Lambert half-space). Our previous 0.08 was a
|
||||
// defensive guess from early acdream days that made back-lit terrain
|
||||
// visibly brighter than retail (user-observed 2026-04-24 "acdream
|
||||
// warmer / less blue than retail"). Reverting to 0.0 matches retail
|
||||
// per the decompile and lets ambient fill in the back side.
|
||||
// Cross-ref: docs/research/2026-04-24-lambert-brightness-split.md.
|
||||
const float MIN_FACTOR = 0.0;
|
||||
|
||||
vec4 unpackOverlayLayer(uint texIdxU, uint alphaIdxU, uint rotIdx, vec2 baseUV) {
|
||||
float texIdx = float(texIdxU);
|
||||
float alphaIdx = float(alphaIdxU);
|
||||
if (texIdx >= 254.0) texIdx = -1.0;
|
||||
if (alphaIdx >= 254.0) alphaIdx = -1.0;
|
||||
|
||||
vec2 rotatedUV = baseUV;
|
||||
if (rotIdx == 1u) rotatedUV = vec2(1.0 - baseUV.y, baseUV.x);
|
||||
else if (rotIdx == 2u) rotatedUV = vec2(1.0 - baseUV.x, 1.0 - baseUV.y);
|
||||
else if (rotIdx == 3u) rotatedUV = vec2( baseUV.y, 1.0 - baseUV.x);
|
||||
|
||||
return vec4(rotatedUV.x, rotatedUV.y, texIdx, alphaIdx);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Unpack rotation fields from aPacked3. Bit layout (data3):
|
||||
// .x (byte 0): bits 0-1 rotBase (unused), 2-3 rotOvl0, 4-5 rotOvl1, 6-7 rotOvl2
|
||||
// .y (byte 1): bits 0-1 rotRd0 (= data3 bit 8-9),
|
||||
// bits 2-3 rotRd1 (= data3 bit 10-11),
|
||||
// bit 4 splitDir (= data3 bit 12)
|
||||
uint rotOvl0 = (aPacked3.x >> 2u) & 3u;
|
||||
uint rotOvl1 = (aPacked3.x >> 4u) & 3u;
|
||||
uint rotOvl2 = (aPacked3.x >> 6u) & 3u;
|
||||
uint rotRd0 = aPacked3.y & 3u;
|
||||
uint rotRd1 = (aPacked3.y >> 2u) & 3u;
|
||||
uint splitDir= (aPacked3.y >> 4u) & 1u;
|
||||
|
||||
// Derive which of the 4 cell corners this vertex represents from
|
||||
// gl_VertexID % 6. The CPU-side LandblockMesh emits vertices in a
|
||||
// specific order for each split direction; the tables below must stay
|
||||
// in lockstep with LandblockMesh.Build's SWtoNE/SEtoNW branches.
|
||||
// 2026-04-21 fix: geometry re-derived to match ACE's ConstructPolygons
|
||||
// convention. SWtoNE (cut BL→TR, y=x diagonal) now maps to the {BL,BR,TR}
|
||||
// + {BL,TR,TL} triangle pair; SEtoNW (cut BR→TL, x+y=1 diagonal) maps to
|
||||
// {BL,BR,TL} + {BR,TR,TL}.
|
||||
int vIdx = gl_VertexID % 6;
|
||||
int corner = 0;
|
||||
if (splitDir == 0u) {
|
||||
// SWtoNE order: BL, BR, TR, BL, TR, TL → corners 0, 1, 2, 0, 2, 3
|
||||
if (vIdx == 0) corner = 0;
|
||||
else if (vIdx == 1) corner = 1;
|
||||
else if (vIdx == 2) corner = 2;
|
||||
else if (vIdx == 3) corner = 0;
|
||||
else if (vIdx == 4) corner = 2;
|
||||
else corner = 3;
|
||||
} else {
|
||||
// SEtoNW order: BL, BR, TL, BR, TR, TL → corners 0, 1, 3, 1, 2, 3
|
||||
if (vIdx == 0) corner = 0;
|
||||
else if (vIdx == 1) corner = 1;
|
||||
else if (vIdx == 2) corner = 3;
|
||||
else if (vIdx == 3) corner = 1;
|
||||
else if (vIdx == 4) corner = 2;
|
||||
else corner = 3;
|
||||
}
|
||||
|
||||
vec2 baseUV;
|
||||
if (corner == 0) baseUV = vec2(0.0, 1.0);
|
||||
else if (corner == 1) baseUV = vec2(1.0, 1.0);
|
||||
else if (corner == 2) baseUV = vec2(1.0, 0.0);
|
||||
else baseUV = vec2(0.0, 0.0);
|
||||
|
||||
vBaseUV = baseUV;
|
||||
vWorldPos = aPos;
|
||||
vWorldNormal = normalize(aNormal);
|
||||
|
||||
// Retail AdjustPlanes bake (terrain.vert:124-134 — identical math).
|
||||
vec3 surfaceToLight = normalize(uShadowLightDirectionAndSource.xyz);
|
||||
vec3 sunCol = uLights[0].colorAndIntensity.xyz * uLights[0].colorAndIntensity.w;
|
||||
float L = max(dot(vWorldNormal, surfaceToLight), MIN_FACTOR);
|
||||
// Preserve retail's authored lighting values, but keep the outdoor
|
||||
// directional term separate so the receiver shadows no ambient/local light.
|
||||
vAmbientLocalLit = uCellAmbient.xyz;
|
||||
vDirectionalLit = sunCol * L;
|
||||
|
||||
float baseTex = float(aPacked0.x);
|
||||
if (baseTex >= 254.0) baseTex = -1.0;
|
||||
vBaseTexIdx = baseTex;
|
||||
|
||||
vOverlay0 = unpackOverlayLayer(aPacked0.z, aPacked0.w, rotOvl0, baseUV);
|
||||
vOverlay1 = unpackOverlayLayer(aPacked1.x, aPacked1.y, rotOvl1, baseUV);
|
||||
vOverlay2 = unpackOverlayLayer(aPacked1.z, aPacked1.w, rotOvl2, baseUV);
|
||||
vRoad0 = unpackOverlayLayer(aPacked2.x, aPacked2.y, rotRd0, baseUV);
|
||||
vRoad1 = unpackOverlayLayer(aPacked2.z, aPacked2.w, rotRd1, baseUV);
|
||||
|
||||
// Retail zFightTerrainAdjust (acclient_2013_pseudo_c.txt:1120769 = 0.00999999978,
|
||||
// applied per terrain vertex inside ACRender::landPolysDraw at line 702254,
|
||||
// address 006b6402). Render terrain 1 cm below its physical Z so coplanar
|
||||
// building floors win the depth test. Physics path is unaffected — it reads
|
||||
// the un-nudged heightmap via TerrainSurface.SampleZ.
|
||||
// Closes issue #100; supersedes the hiddenTerrainCells cell-collapse hack.
|
||||
vec3 terrainPos = vec3(aPos.xy, aPos.z - 0.01);
|
||||
gl_Position = uViewProjection * vec4(terrainPos, 1.0);
|
||||
|
||||
// Phase U.3: terrain clip gate against the single OutsideView region. With
|
||||
// uTerrainClipCount == 0 (U.3 default) the first loop is skipped and the
|
||||
// second sets all 8 distances to +1.0 ⇒ no clipping ⇒ identical terrain.
|
||||
for (int i = 0; i < uTerrainClipCount; ++i)
|
||||
gl_ClipDistance[i] = dot(uTerrainClipPlanes[i], gl_Position);
|
||||
for (int i = uTerrainClipCount; i < 8; ++i)
|
||||
gl_ClipDistance[i] = 1.0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue