This commit is contained in:
Gabrieal Jimmy
2026-10-02 12:42:00 -05:00
commit 2c99305fb0
145 changed files with 6525 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
#version 450 core
#include "common.glsl"
#include "pixelart.glsl"
in VS {
vec2 uv;
vec3 worldPos;
vec3 pivot;
vec2 local;
float tint;
} v;
layout(binding = 0) uniform sampler2D uTex;
uniform int uPass;
uniform float uEmissive;
layout(location = 0) out vec4 fragColor;
void main() {
vec4 c = sharpSample(uTex, v.uv);
if (c.a < 0.5) discard;
if (uPass == 1) return;
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 toCam = cross(uBillboardRight.xyz, up);
// Sprites are flat cards; a normal leaning upward keeps them readable
// under top lighting while still responding to light direction.
vec3 n = normalize(toCam * 0.7 + up * 0.7);
vec3 L = uSunDir.xyz;
float ndl = clamp((dot(n, L) + 0.55) / 1.55, 0.0, 1.0);
// Receive shadows at the matching point on this sprite's own sun-facing
// shadow card, nudged toward the sun: no self-shadowing, correct
// occlusion by everything else.
vec3 sp = v.pivot + uShadowRight.xyz * v.local.x + vec3(0.0, v.local.y, 0.0) + L * 0.22;
float vis = sunShadow(sp) * canopyCookie(v.worldPos);
vec3 albedo = c.rgb * v.tint * mix(1.0, 0.8, uWeather.y); // rain-soaked sprites read darker
float gao = groundAO(v.pivot, v.local.y);
vec3 light = uSunColor.rgb * uSunDir.w * ndl * vis * mix(1.0, gao, 0.7) + hemiAmbient(n) * gao +
pointLights(v.worldPos, n);
// Thin-foliage translucency when the sun is behind the sprite.
float behind = max(dot(-toCam, L), 0.0);
light += uSunColor.rgb * uSunDir.w * behind * behind * vis * 0.25;
vec3 col = albedo * light + albedo * uEmissive;
if (uBlob.w > 0.0) { // fold the player's contact shadow onto grass sprites too
float d = length(v.pivot.xz - uBlob.xz) / uBlob.w;
col *= 1.0 - 0.35 * (1.0 - smoothstep(0.3, 1.0, d)) * (1.0 - clamp(v.local.y / 0.6, 0.0, 1.0));
}
fragColor = vec4(col, 1.0);
}

View File

@@ -0,0 +1,58 @@
#version 450 core
#include "common.glsl"
layout(location = 0) in vec4 iPosSway; // pivot xyz, sway
layout(location = 1) in vec4 iSizeFlipTint; // size xy, flip, tint
uniform vec4 uOpaque; // opaque pixel bounds in uv (minX, minY, maxX, maxY)
uniform int uPass; // 0 = color, 1 = shadow
out VS {
vec2 uv;
vec3 worldPos;
vec3 pivot;
vec2 local; // (horizontal offset, height above pivot) in world units
float tint;
} o;
void main() {
vec2 corner = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1));
vec2 size = iSizeFlipTint.xy;
vec3 pivot = iPosSway.xyz;
float lx = (corner.x - 0.5) * size.x;
float ly = (corner.y - uOpaque.y) * size.y; // lowest opaque row sits on the pivot
// Shadow cards face the sun so silhouettes are never edge-on to the light.
vec3 right = uPass == 1 ? uShadowRight.xyz : uBillboardRight.xyz;
vec3 wp = pivot + right * lx + vec3(0.0, ly, 0.0);
float sway = iPosSway.w;
if (sway > 0.0) {
float visibleHeight = max(size.y * (uOpaque.w - uOpaque.y), 1e-3);
float k = clamp(ly / visibleHeight, 0.0, 1.0);
k *= k;
float ph = dot(pivot.xz, vec2(0.37, 0.61));
vec2 wdir = normalize(uWind.xy + 1e-5);
float gust = vnoise(pivot.xz * 0.15 - wdir * uTime * uWind.w);
float w = (sin(uTime * 2.1 + ph) * 0.5 + sin(uTime * 3.7 + ph * 1.7) * 0.25 + gust * 1.3) * uWind.z;
vec2 disp = wdir * w;
// Bend away from the interactor (player) and rustle while it is close.
vec3 toP = pivot - uInteractor.xyz;
float d = length(toP.xz);
float push = clamp(1.0 - d / uInteractor.w, 0.0, 1.0) * step(abs(toP.y), 1.0);
disp += (d > 1e-3 ? toP.xz / d : vec2(0.0)) * push * 0.5;
disp += vec2(sin(uTime * 19.0 + ph * 7.0), cos(uTime * 17.0 + ph * 5.0)) * push * 0.05;
wp.xz += disp * k * sway * visibleHeight;
wp.y -= push * k * sway * 0.3 * visibleHeight;
}
vec2 uv = corner;
if (iSizeFlipTint.z > 0.5) uv.x = 1.0 - uv.x;
o.uv = uv;
o.worldPos = wp;
o.pivot = pivot;
o.local = vec2(lx, ly);
o.tint = iSizeFlipTint.w;
gl_Position = uViewProj * vec4(wp, 1.0);
gl_ClipDistance[0] = dot(vec4(wp, 1.0), uClipPlane);
}

View File

@@ -0,0 +1,40 @@
#version 450 core
// 13-tap downsample (Jimenez, "Next Generation Post Processing in CoD:AW").
// The first pass uses a Karis average to suppress fireflies.
in vec2 vUV;
layout(binding = 0) uniform sampler2D uSrc;
uniform vec2 uSrcTexel;
uniform int uKaris;
layout(location = 0) out vec4 fragColor;
float luma(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
vec3 karis(vec3 c) { return c / (1.0 + luma(c)); }
void main() {
vec2 t = uSrcTexel;
vec3 a = texture(uSrc, vUV + t * vec2(-2, 2)).rgb;
vec3 b = texture(uSrc, vUV + t * vec2(0, 2)).rgb;
vec3 c = texture(uSrc, vUV + t * vec2(2, 2)).rgb;
vec3 d = texture(uSrc, vUV + t * vec2(-2, 0)).rgb;
vec3 e = texture(uSrc, vUV).rgb;
vec3 f = texture(uSrc, vUV + t * vec2(2, 0)).rgb;
vec3 g = texture(uSrc, vUV + t * vec2(-2, -2)).rgb;
vec3 h = texture(uSrc, vUV + t * vec2(0, -2)).rgb;
vec3 i = texture(uSrc, vUV + t * vec2(2, -2)).rgb;
vec3 j = texture(uSrc, vUV + t * vec2(-1, 1)).rgb;
vec3 k = texture(uSrc, vUV + t * vec2(1, 1)).rgb;
vec3 l = texture(uSrc, vUV + t * vec2(-1, -1)).rgb;
vec3 m = texture(uSrc, vUV + t * vec2(1, -1)).rgb;
vec3 r;
if (uKaris == 1) {
vec3 g0 = karis((a + b + d + e) * 0.25), g1 = karis((b + c + e + f) * 0.25);
vec3 g2 = karis((d + e + g + h) * 0.25), g3 = karis((e + f + h + i) * 0.25);
vec3 g4 = karis((j + k + l + m) * 0.25);
r = g4 * 0.5 + (g0 + g1 + g2 + g3) * 0.125;
r /= max(1.0 - luma(r), 1e-3); // undo tonemap-weighting on the average
} else {
r = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;
}
fragColor = vec4(max(r, vec3(0.0)), 1.0);
}

View File

@@ -0,0 +1,18 @@
#version 450 core
// 3x3 tent upsample, additively blended into the next larger mip.
in vec2 vUV;
layout(binding = 0) uniform sampler2D uSrc;
uniform vec2 uSrcTexel;
uniform float uRadius;
layout(location = 0) out vec4 fragColor;
void main() {
vec2 t = uSrcTexel * uRadius;
vec3 s = texture(uSrc, vUV).rgb * 4.0;
s += (texture(uSrc, vUV + vec2(-t.x, 0)).rgb + texture(uSrc, vUV + vec2(t.x, 0)).rgb +
texture(uSrc, vUV + vec2(0, -t.y)).rgb + texture(uSrc, vUV + vec2(0, t.y)).rgb) * 2.0;
s += texture(uSrc, vUV + vec2(-t.x, -t.y)).rgb + texture(uSrc, vUV + vec2(t.x, -t.y)).rgb +
texture(uSrc, vUV + vec2(-t.x, t.y)).rgb + texture(uSrc, vUV + vec2(t.x, t.y)).rgb;
fragColor = vec4(s / 16.0, 1.0);
}

200
assets/shaders/common.glsl Normal file
View File

@@ -0,0 +1,200 @@
// Shared frame data and helpers. Mirrors FrameUBO in Renderer.cpp (std140).
layout(std140, binding = 0) uniform Frame {
mat4 uView;
mat4 uProj;
mat4 uViewProj;
mat4 uInvViewProj;
mat4 uLightViewProj;
vec4 uCamPos; // xyz, w = time (seconds)
vec4 uCamParams; // near, far, focus distance, unused
vec4 uScreen; // target width, height, 1/width, 1/height
vec4 uSunDir; // xyz toward sun, w = intensity
vec4 uSunColor; // rgb
vec4 uSkyZenith; // rgb, w = ambient intensity
vec4 uSkyHorizon; // rgb, w = camera-side fill light intensity
vec4 uSkyGround; // rgb
vec4 uFogColor; // rgb, w = density
vec4 uFogParams; // height falloff, base height, noise amount, anisotropy g
vec4 uFogParams2; // god ray intensity, max distance, steps, unused
vec4 uWind; // dir.xy, strength, gust speed
vec4 uInteractor; // xyz, radius
vec4 uBlob; // xyz, radius
vec4 uClipPlane; // world-space plane for gl_ClipDistance[0]
vec4 uBillboardRight;// camera-facing billboard right vector
vec4 uShadowRight; // sun-facing billboard right vector (shadow cards)
vec4 uCookie; // strength, scale, speed, coverage
vec4 uShadowParams; // texel size, softness, unused, unused
vec4 uShaft; // god ray beam strength, coverage, pattern scale, unused
vec4 uWaterParams; // level, caustics intensity, has water, unused
vec4 uGroundAO; // world -> uv: minX, minZ, 1/sizeX, 1/sizeZ (z = 0: disabled)
vec4 uWeather; // rain intensity, surface wetness, rain fall speed, unused
ivec4 uLightCount;
vec4 uLightPos[16]; // xyz, w = radius
vec4 uLightColor[16];
};
#define uTime uCamPos.w
#define PI 3.14159265359
layout(binding = 8) uniform sampler2DShadow uShadowMap;
layout(binding = 9) uniform sampler2D uGroundAOTex;
// ---------------------------------------------------------------- noise ----
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float hash13(vec3 p3) {
p3 = fract(p3 * 0.1031);
p3 += dot(p3, p3.zyx + 31.32);
return fract((p3.x + p3.y) * p3.z);
}
float vnoise(vec2 p) {
vec2 i = floor(p), f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(hash12(i), hash12(i + vec2(1, 0)), u.x),
mix(hash12(i + vec2(0, 1)), hash12(i + vec2(1, 1)), u.x), u.y);
}
float vnoise3(vec3 p) {
vec3 i = floor(p), f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
float a = mix(mix(hash13(i), hash13(i + vec3(1, 0, 0)), u.x),
mix(hash13(i + vec3(0, 1, 0)), hash13(i + vec3(1, 1, 0)), u.x), u.y);
float b = mix(mix(hash13(i + vec3(0, 0, 1)), hash13(i + vec3(1, 0, 1)), u.x),
mix(hash13(i + vec3(0, 1, 1)), hash13(i + vec3(1, 1, 1)), u.x), u.y);
return mix(a, b, u.z);
}
float fbm2(vec2 p) {
float a = 0.5, s = 0.0;
for (int i = 0; i < 4; ++i) {
s += a * vnoise(p);
p = p * 2.03 + 17.1;
a *= 0.5;
}
return s / 0.9375;
}
// Interleaved gradient noise: cheap per-pixel dither for ray marching.
float ign(vec2 p) { return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); }
// ---------------------------------------------------------------- depth ----
float linearDepth(float d) {
float n = uCamParams.x, f = uCamParams.y;
float z = d * 2.0 - 1.0;
return 2.0 * n * f / (f + n - z * (f - n));
}
vec3 worldFromDepth(vec2 uv, float d) {
vec4 p = uInvViewProj * vec4(uv * 2.0 - 1.0, d * 2.0 - 1.0, 1.0);
return p.xyz / p.w;
}
// ------------------------------------------------------------- lighting ----
vec3 shadowCoord(vec3 wp) {
vec4 lc = uLightViewProj * vec4(wp, 1.0);
return lc.xyz / lc.w * 0.5 + 0.5;
}
float sunShadow(vec3 wp) {
vec3 p = shadowCoord(wp);
if (any(lessThan(p.xy, vec2(0.0))) || any(greaterThan(p.xyz, vec3(1.0)))) return 1.0;
vec2 o = vec2(uShadowParams.x * uShadowParams.y);
float s = 0.0;
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x)
s += texture(uShadowMap, vec3(p.xy + vec2(x, y) * o, p.z));
return s / 9.0;
}
float sunShadowFast(vec3 wp) {
vec3 p = shadowCoord(wp);
if (any(lessThan(p.xy, vec2(0.0))) || any(greaterThan(p.xyz, vec3(1.0)))) return 1.0;
return texture(uShadowMap, p);
}
float canopyPattern(vec3 wp, float scale, float coverage, float octaves) {
const float canopyHeight = 14.0;
vec3 L = uSunDir.xyz;
vec2 p = wp.xz + L.xz / max(L.y, 0.15) * (canopyHeight - wp.y);
p = p * scale + uTime * uCookie.z * vec2(1.0, 0.6);
float n = octaves > 1.5 ? fbm2(p) : vnoise(p) * 0.75 + vnoise(p * 2.7 + 9.1) * 0.25;
return smoothstep(coverage - 0.06, coverage + 0.06, n);
}
// Dappled light from an implied tree canopy high above the scene, projected
// along the sun direction onto surfaces.
float canopyCookie(vec3 wp) {
if (uCookie.x <= 0.0) return 1.0;
return mix(1.0, canopyPattern(wp, uCookie.y, uCookie.w, 4.0), uCookie.x);
}
// Sparse beams through the same implied canopy, used inside fog so sunlight
// gathers into distinct god ray shafts instead of a uniform glow.
float shaftCookie(vec3 wp) {
if (uShaft.x <= 0.0) return 1.0;
return mix(1.0, canopyPattern(wp, uShaft.z, uShaft.y, 1.0), uShaft.x);
}
float sunVisibility(vec3 wp) { return sunShadow(wp) * canopyCookie(wp); }
vec3 hemiAmbient(vec3 n) {
vec3 sky = mix(uSkyHorizon.rgb, uSkyZenith.rgb, 0.45);
return mix(uSkyGround.rgb, sky, n.y * 0.5 + 0.5) * uSkyZenith.w;
}
vec3 pointLights(vec3 wp, vec3 n) {
vec3 sum = vec3(0.0);
for (int i = 0; i < uLightCount.x; ++i) {
vec3 d = uLightPos[i].xyz - wp;
float dist = length(d);
float r = uLightPos[i].w;
if (dist >= r) continue;
float att = 1.0 - (dist * dist) / (r * r);
att *= att;
float ndl = clamp((dot(n, d / max(dist, 1e-4)) + 0.4) / 1.4, 0.0, 1.0);
sum += uLightColor[i].rgb * att * ndl;
}
return sum;
}
// Baked contact occlusion under scenery (tree bases, bushes, rocks). `height`
// is the shaded point's height above the ground it stands on: the darkening
// only applies near the ground.
float groundAO(vec3 wp, float height) {
if (uGroundAO.z <= 0.0) return 1.0;
float ao = texture(uGroundAOTex, (wp.xz - uGroundAO.xy) * uGroundAO.zw).r;
return mix(ao, 1.0, smoothstep(0.0, 1.2, height));
}
// Rain-soaked surfaces: darker albedo plus a sky/sun sheen on upward faces.
vec3 wetSurface(vec3 albedo, vec3 n, vec3 V, float vis, out vec3 sheen) {
float wet = uWeather.y * smoothstep(0.2, 0.8, n.y);
vec3 R = reflect(-V, n);
float F = 0.04 + 0.96 * pow(1.0 - max(dot(n, V), 0.0), 5.0);
sheen = wet * (uSkyHorizon.rgb * uSkyZenith.w * F * 0.6 +
uSunColor.rgb * uSunDir.w * pow(max(dot(R, uSunDir.xyz), 0.0), 60.0) * 0.5 * vis);
return albedo * mix(1.0, 0.68, uWeather.y);
}
vec3 skyColor(vec3 dir) {
float h = dir.y;
vec3 c = mix(uSkyHorizon.rgb, uSkyZenith.rgb, pow(clamp(h, 0.0, 1.0), 0.55));
if (h < 0.0) c = mix(uSkyHorizon.rgb, uSkyGround.rgb, clamp(-h * 3.0, 0.0, 1.0));
float sd = max(dot(dir, uSunDir.xyz), 0.0);
if (h > 0.0) {
vec2 cp = dir.xz / (h + 0.12) * 1.3 + uTime * vec2(0.012, 0.004);
float cl = smoothstep(0.5 - 0.45 * uWeather.x, 0.85 - 0.3 * uWeather.x, fbm2(cp)); // overcast when raining
c = mix(c, uSunColor.rgb * (0.75 + 0.6 * sd), cl * 0.55 * smoothstep(0.0, 0.2, h));
}
c += uSunColor.rgb * (pow(sd, 900.0) * 40.0 + pow(sd, 10.0) * 0.3);
return c;
}
// Animated caustic web (after joltz0r's "water turbulence").
float caustics(vec2 uv, float time) {
vec2 p = mod(uv * 6.28318, 6.28318) - 250.0;
vec2 i = p;
float c = 1.0;
const float inten = 0.005;
for (int n = 0; n < 4; ++n) {
float t = time * (1.0 - (3.5 / float(n + 1)));
i = p + vec2(cos(t - i.x) + sin(t + i.y), sin(t - i.y) + cos(t + i.x));
c += 1.0 / length(vec2(p.x / (sin(i.x + t) / inten), p.y / (cos(i.y + t) / inten)));
}
c /= 4.0;
c = 1.17 - pow(c, 1.4);
return pow(abs(c), 8.0);
}

View File

@@ -0,0 +1,3 @@
#version 450 core
// Depth-only pass (shadow map).
void main() {}

View File

@@ -0,0 +1,35 @@
#version 450 core
// Scatter-as-gather bokeh blur over a golden-angle disc at half resolution.
// Samples farther than the center cannot spread over it (keeps in-focus
// subjects from smearing into the background); nearer samples can (near
// field blur bleeds over sharp areas like a real lens).
in vec2 vUV;
layout(binding = 0) uniform sampler2D uHalf;
uniform float uMaxRadius;
uniform vec2 uTexel;
layout(location = 0) out vec4 fragColor;
const int SAMPLES = 64;
void main() {
vec4 c0 = texture(uHalf, vUV);
float cc = c0.a;
vec3 acc = c0.rgb;
float wsum = 1.0;
float nearCoverage = 0.0;
for (int i = 0; i < SAMPLES; ++i) {
float r = sqrt((float(i) + 0.5) / float(SAMPLES)) * uMaxRadius;
float th = float(i) * 2.39996323;
vec2 o = vec2(cos(th), sin(th)) * r;
vec4 s = textureLod(uHalf, vUV + o * uTexel, 0.0);
float cs = s.a;
float e = abs(cs);
if (cs > cc) e = min(e, abs(cc));
float w = clamp(e * uMaxRadius - r + 1.0, 0.0, 1.0);
acc += s.rgb * w;
wsum += w;
if (cs < 0.0) nearCoverage = max(nearCoverage, w * abs(cs));
}
fragColor = vec4(acc / wsum, max(abs(cc), nearCoverage));
}

View File

@@ -0,0 +1,12 @@
uniform vec4 uDof; // focus distance, focus range, transition, max radius (half-res px)
uniform float uTiltShift;
// Signed CoC in [-1, 1]. Distance-based blur plus an optional screen-space
// tilt-shift band that exaggerates the miniature/diorama look.
float cocFromDepth(float linDepth, vec2 uv) {
float s = linDepth - uDof.x;
float a = smoothstep(uDof.y, uDof.y + uDof.z, abs(s));
float tilt = uTiltShift * smoothstep(0.18, 0.5, abs(uv.y - 0.5));
float sgn = abs(s) > 0.25 ? sign(s) : sign(uv.y - 0.5);
return max(a, tilt) * sgn;
}

View File

@@ -0,0 +1,17 @@
#version 450 core
#include "common.glsl"
#include "dof_common.glsl"
in vec2 vUV;
layout(binding = 0) uniform sampler2D uSharp;
layout(binding = 1) uniform sampler2D uBlurred;
layout(binding = 2) uniform sampler2D uDepth;
layout(location = 0) out vec4 fragColor;
void main() {
vec3 sharp = texture(uSharp, vUV).rgb;
vec4 blurred = texture(uBlurred, vUV);
float coc = abs(cocFromDepth(linearDepth(texture(uDepth, vUV).r), vUV));
float m = smoothstep(0.02, 0.35, max(coc, blurred.a));
fragColor = vec4(mix(sharp, blurred.rgb, m), 1.0);
}

View File

@@ -0,0 +1,15 @@
#version 450 core
#include "common.glsl"
#include "dof_common.glsl"
// Half-res: downsampled color + signed circle of confusion (near < 0 < far).
in vec2 vUV;
layout(binding = 0) uniform sampler2D uColor;
layout(binding = 1) uniform sampler2D uDepth;
layout(location = 0) out vec4 fragColor;
void main() {
vec3 c = texture(uColor, vUV).rgb; // bilinear = 2x2 box at half res
float coc = cocFromDepth(linearDepth(texture(uDepth, vUV).r), vUV);
fragColor = vec4(c, coc);
}

View File

@@ -0,0 +1,33 @@
#version 450 core
#include "common.glsl"
// Depth-aware upsample of the half-res volumetric buffer, applied to the scene.
in vec2 vUV;
layout(binding = 0) uniform sampler2D uColor;
layout(binding = 1) uniform sampler2D uDepth;
layout(binding = 2) uniform sampler2D uVolume;
uniform vec2 uVolumeTexel;
uniform int uEnabled;
layout(location = 0) out vec4 fragColor;
void main() {
vec3 c = texture(uColor, vUV).rgb;
if (uEnabled == 0) {
fragColor = vec4(c, 1.0);
return;
}
float d0 = linearDepth(texture(uDepth, vUV).r);
vec4 acc = vec4(0.0);
float wsum = 0.0;
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x) {
vec2 uv = vUV + vec2(x, y) * uVolumeTexel;
float ds = linearDepth(texture(uDepth, uv).r);
float w = exp(-abs(ds - d0) / (0.04 * d0 + 0.05)) * (x == 0 && y == 0 ? 2.0 : 1.0);
acc += texture(uVolume, uv) * w;
wsum += w;
}
vec4 v = acc / max(wsum, 1e-4);
fragColor = vec4(c * v.a + v.rgb, 1.0);
}

View File

@@ -0,0 +1,8 @@
#version 450 core
// Fullscreen triangle from gl_VertexID; draw with 3 vertices, no buffers.
out vec2 vUV;
void main() {
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
vUV = p;
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
}

83
assets/shaders/mesh.frag Normal file
View File

@@ -0,0 +1,83 @@
#version 450 core
#include "common.glsl"
// High-resolution 3D scenery (trees, bushes, rocks). Foliage uses wrap
// lighting on its rounded custom normals plus sun translucency.
in VS {
vec3 worldPos;
vec3 normal;
vec2 uv;
vec3 color;
float height;
} v;
layout(binding = 0) uniform sampler2D uTex;
uniform vec4 uBaseColor;
uniform int uAlphaMask;
uniform float uCutoff;
uniform int uFoliage;
uniform int uOccluderFade; // 1 in the main view: dither away scenery hiding the player
uniform vec3 uEmissive; // linear HDR; multiplied by the emissive texture (unit 1)
layout(binding = 1) uniform sampler2D uEmissiveTex;
layout(location = 0) out vec4 fragColor;
void main() {
vec4 tex = texture(uTex, v.uv);
if (uAlphaMask == 1) {
// Sharpen alpha by its screen-space rate of change: keeps leaf coverage
// stable through the mip chain instead of thinning out with distance.
float a = (tex.a - uCutoff) / max(fwidth(tex.a), 1e-4) + 0.5;
if (a < 0.5) discard;
}
if (uOccluderFade == 1 && uBlob.w > 0.0) {
vec4 pc = uViewProj * vec4(uBlob.xyz + vec3(0.0, 0.6, 0.0), 1.0);
vec2 ps = pc.xy / pc.w * 0.5 + 0.5;
vec2 d = (gl_FragCoord.xy * uScreen.zw - ps) * vec2(uScreen.x * uScreen.w, 1.0);
float fragDepth = (uViewProj * vec4(v.worldPos, 1.0)).w;
float fade = (1.0 - smoothstep(0.07, 0.15, length(d))) * step(fragDepth, pc.w - 1.0);
if (ign(gl_FragCoord.xy) < fade * 0.85) discard;
}
vec3 wp = v.worldPos;
vec3 n = normalize(v.normal);
if (uFoliage == 0 && !gl_FrontFacing) n = -n;
vec3 L = uSunDir.xyz;
vec3 V = normalize(uCamPos.xyz - wp);
vec3 albedo = tex.rgb * uBaseColor.rgb * v.color;
float gao = groundAO(wp, v.height);
float ndl = dot(n, L);
float diffuse = uFoliage == 1 ? clamp((ndl + 0.45) / 1.45, 0.0, 1.0) : max(ndl, 0.0);
// Offset along the normal and toward the sun to avoid acne on thin cards.
float vis = sunShadow(wp + n * 0.06 + L * 0.08) * canopyCookie(wp);
if (uFoliage == 1) vis = mix(0.3, 1.0, vis); // light scatters through leaves: soft self-shadowing
vec3 sunRad = uSunColor.rgb * uSunDir.w;
vec3 toCam = cross(uBillboardRight.xyz, vec3(0.0, 1.0, 0.0));
vec3 amb = hemiAmbient(n) + uSkyHorizon.rgb * uSkyHorizon.w * max(dot(n, toCam), 0.0);
vec3 light = sunRad * diffuse * vis * mix(1.0, gao, 0.7) + amb * gao + pointLights(wp, n);
if (uFoliage == 1) {
// Backlit leaves glow; sunlit rims pick up a warm sheen.
float back = pow(max(dot(-V, L), 0.0), 4.0);
light += sunRad * back * vis * 0.35 * vec3(1.0, 1.05, 0.7);
float rim = pow(1.0 - max(dot(n, V), 0.0), 3.0) * max(ndl, 0.0);
light += sunRad * rim * vis * 0.25;
} else {
// Subtle sheen on rocks and bark.
vec3 H = normalize(L + V);
light += sunRad * pow(max(dot(n, H), 0.0), 24.0) * 0.06 * vis;
}
vec3 sheen = vec3(0.0);
if (uWeather.y > 0.0) albedo = wetSurface(albedo, n, V, vis, sheen);
vec3 col = albedo * light + sheen * gao;
col += texture(uEmissiveTex, v.uv).rgb * uEmissive;
if (uBlob.w > 0.0) { // player's contact shadow
float d = length(wp.xz - uBlob.xz) / uBlob.w;
float near = 1.0 - smoothstep(0.0, 0.6, abs(wp.y - uBlob.y));
col *= 1.0 - 0.4 * (1.0 - smoothstep(0.3, 1.0, d)) * near;
}
fragColor = vec4(col, 1.0);
}

54
assets/shaders/mesh.vert Normal file
View File

@@ -0,0 +1,54 @@
#version 450 core
#include "common.glsl"
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aUV;
layout(location = 3) in vec4 aColor; // rgb = tint/AO, a = wind weight
layout(location = 4) in vec4 iPosYaw; // pivot xyz, yaw
layout(location = 5) in vec4 iScaleTint; // scale xyz, tint
uniform float uWindScale;
out VS {
vec3 worldPos;
vec3 normal;
vec2 uv;
vec3 color;
float height; // above the model's base (ground contact AO)
} o;
void main() {
float c = cos(iPosYaw.w), s = sin(iPosYaw.w);
vec3 lp = aPos * iScaleTint.xyz;
vec3 p = vec3(c * lp.x + s * lp.z, lp.y, -s * lp.x + c * lp.z) + iPosYaw.xyz;
vec3 ln = aNormal / iScaleTint.xyz;
vec3 n = normalize(vec3(c * ln.x + s * ln.z, ln.y, -s * ln.x + c * ln.z));
float w = aColor.a * uWindScale;
if (w > 0.0) {
vec3 pivot = iPosYaw.xyz;
float ph = dot(pivot.xz, vec2(0.37, 0.61));
vec2 wdir = normalize(uWind.xy + 1e-5);
float gust = vnoise(pivot.xz * 0.12 - wdir * uTime * uWind.w * 0.6);
// Whole-crown sway grows with height; leaves add a fast, small flutter.
float sway = (sin(uTime * 1.1 + ph) * 0.5 + sin(uTime * 1.9 + ph * 1.3) * 0.25 + gust * 1.2) * uWind.z;
float h = max(lp.y, 0.0);
p.xz += wdir * sway * w * h * 0.35;
// Leaf flutter scales with wind strength, so storms visibly thrash the canopy.
p += n * sin(uTime * (3.0 + uWind.w * 2.0) + dot(aPos, vec3(7.1, 11.3, 5.7)) + ph) * 0.4 * uWind.z * w;
// Low foliage (bushes) parts around the player like the grass does.
vec3 toP = p - uInteractor.xyz;
float d = length(toP.xz);
float push = clamp(1.0 - d / (uInteractor.w * 1.3), 0.0, 1.0) * step(abs(toP.y), 1.2);
p.xz += (d > 1e-3 ? toP.xz / d : vec2(0.0)) * push * 0.3 * w;
}
o.worldPos = p;
o.normal = n;
o.uv = aUV;
o.color = aColor.rgb * iScaleTint.w;
o.height = lp.y;
gl_Position = uViewProj * vec4(p, 1.0);
gl_ClipDistance[0] = dot(vec4(p, 1.0), uClipPlane);
}

View File

@@ -0,0 +1,18 @@
#version 450 core
// Shadow pass for 3D scenery: alpha-tested so leaves cast dappled shadows.
in VS {
vec3 worldPos;
vec3 normal;
vec2 uv;
vec3 color;
float height;
} v;
layout(binding = 0) uniform sampler2D uTex;
uniform int uAlphaMask;
uniform float uCutoff;
void main() {
if (uAlphaMask == 1 && texture(uTex, v.uv).a < uCutoff) discard;
}

View File

@@ -0,0 +1,56 @@
#version 450 core
#include "common.glsl"
// Premultiplied output: glows and fire write alpha 0 (pure additive), sparks
// and smoke write coverage (alpha-blended). Blend func is ONE, ONE_MINUS_SRC_ALPHA.
in vec2 vUV;
in vec4 vColor;
flat in int vKind;
in vec2 vLifeSeed;
layout(location = 0) out vec4 fragColor;
void main() {
vec2 p = vUV * 2.0 - 1.0;
float r = length(p);
float life = vLifeSeed.x, seed = vLifeSeed.y;
if (vKind == 2) { // spark / droplet
float a = (1.0 - smoothstep(0.6, 1.0, r)) * vColor.a;
fragColor = vec4(vColor.rgb * a, a);
return;
}
if (vKind == 3) { // flame: teardrop eroded by scrolling noise, more as it ages
float taper = 1.0 + 0.7 * (p.y * 0.5 + 0.5);
float d = length(vec2(p.x * taper, p.y * 0.9 + 0.1));
vec2 q = vec2(p.x * 2.5, p.y * 2.0 - uTime * 3.5) + seed * 37.0;
float n = vnoise(q) * 0.6 + vnoise(q * 2.3 + 11.0) * 0.4;
float shape = 1.0 - smoothstep(0.45, 0.85, d + (n - 0.5) * (0.6 + 0.9 * life));
float core = 1.0 - smoothstep(0.0, 0.55, d);
float a = shape * vColor.a;
// Mostly additive, but each tongue also dims what is behind it a little,
// so flames stay saturated orange over bright daytime ground.
fragColor = vec4(vColor.rgb * a * (0.7 + 0.8 * core), a * 0.3);
return;
}
if (vKind == 4) { // ember
float core = exp(-r * r * 10.0);
fragColor = vec4(vColor.rgb * vColor.a * core, 0.0);
return;
}
if (vKind == 5) { // smoke puff
vec2 q = p * 1.6 + seed * 23.0 + vec2(0.0, -uTime * 0.3);
float n = vnoise(q) * 0.6 + vnoise(q * 2.1 + 5.0) * 0.4;
float a = (1.0 - smoothstep(0.2, 1.0, r + (n - 0.5) * 0.7)) * vColor.a;
fragColor = vec4(vColor.rgb * a, a);
return;
}
if (vKind == 6) { // fire glow halo
float g = exp(-r * r * 3.5) - 0.03;
fragColor = vec4(vColor.rgb * max(g, 0.0), 0.0);
return;
}
float core = 1.0 - smoothstep(0.0, 0.2, r);
float halo = exp(-r * r * 9.0) * 0.45;
fragColor = vec4(vColor.rgb * vColor.a * (core * 1.5 + halo), 0.0);
}

View File

@@ -0,0 +1,50 @@
#version 450 core
#include "common.glsl"
layout(location = 0) in vec4 iPosSize;
layout(location = 1) in vec4 iColor; // rgb, alpha
// x = kind (0 firefly, 1 mote, 2 spark, 3 flame, 4 ember, 5 smoke, 6 glow),
// y = age / life, z = per-particle random seed
layout(location = 2) in vec4 iParams;
out vec2 vUV;
out vec4 vColor;
flat out int vKind;
out vec2 vLifeSeed;
void main() {
vec2 corner = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1)) - 0.5;
vec3 camRight = vec3(uView[0][0], uView[1][0], uView[2][0]);
vec3 camUp = vec3(uView[0][1], uView[1][1], uView[2][1]);
int kind = int(iParams.x + 0.5);
float seed = iParams.z;
vec2 offset = corner;
float extent = 5.0; // fireflies / motes need room for their halo
if (kind == 2 || kind == 4 || kind == 6) extent = 1.0;
if (kind == 3) { // flame tongues are taller than wide
extent = 1.0;
offset *= vec2(1.0, 1.7);
}
if (kind == 5) { // smoke puffs rotate slowly
extent = 1.0;
float a = seed * 6.2832 + iParams.y * 1.5;
offset = mat2(cos(a), sin(a), -sin(a), cos(a)) * offset;
}
vec3 c = iPosSize.xyz;
vec3 wp = c + (camRight * offset.x + camUp * offset.y) * iPosSize.w * extent;
vec4 col = iColor;
if (kind == 1) { // dust motes only sparkle inside sun shafts
float vis = sunShadowFast(c) * shaftCookie(c);
col.rgb *= uSunColor.rgb * uSunDir.w * 0.8;
col.a *= vis;
}
if (kind == 5) // smoke is lit by the sky and warmed by nearby fire light
col.rgb *= hemiAmbient(vec3(0, 1, 0)) * 2.5 + pointLights(c, vec3(0, -1, 0)) * 0.4;
vUV = corner + 0.5;
vColor = col;
vKind = kind;
vLifeSeed = iParams.yz;
gl_Position = uViewProj * vec4(wp, 1.0);
}

View File

@@ -0,0 +1,19 @@
// Fragment-stage only (uses screen-space derivatives).
// "Sharp bilinear": crisp texel edges when magnified, antialiased boundaries
// and correct mip selection when minified (unlike GL_NEAREST shimmer).
vec4 sharpSample(sampler2D tex, vec2 uv) {
vec2 size = vec2(textureSize(tex, 0));
vec2 px = uv * size;
vec2 seam = floor(px + 0.5);
vec2 fw = max(fwidth(px), vec2(1e-5));
px = seam + clamp((px - seam) / fw, -0.5, 0.5);
return textureGrad(tex, px / size, dFdx(uv), dFdy(uv));
}
vec4 sharpSampleArray(sampler2DArray tex, vec2 uv, float layer) {
vec2 size = vec2(textureSize(tex, 0).xy);
vec2 px = uv * size;
vec2 seam = floor(px + 0.5);
vec2 fw = max(fwidth(px), vec2(1e-5));
px = seam + clamp((px - seam) / fw, -0.5, 0.5);
return textureGrad(tex, vec3(px / size, layer), dFdx(uv), dFdy(uv));
}

15
assets/shaders/rain.frag Normal file
View File

@@ -0,0 +1,15 @@
#version 450 core
#include "common.glsl"
// Thin, slightly bright streak lit by the overcast sky. Premultiplied alpha.
in vec2 vLocal;
in float vAlpha;
layout(location = 0) out vec4 fragColor;
void main() {
float across = 1.0 - abs(vLocal.x);
float along = (1.0 - vLocal.y) * smoothstep(0.0, 0.15, vLocal.y + 0.05); // bright head, fading tail
float a = across * across * along * vAlpha * 0.6;
vec3 c = hemiAmbient(vec3(0.0, 1.0, 0.0)) * 2.0 + uSunColor.rgb * uSunDir.w * 0.2;
fragColor = vec4(c * a, a);
}

43
assets/shaders/rain.vert Normal file
View File

@@ -0,0 +1,43 @@
#version 450 core
#include "common.glsl"
// Fully procedural rain: each instance is one streak in a box that follows
// the camera target. Drops are fixed in world space (the box wraps around
// them), fall at uWeather.z and slant with the wind.
uniform vec3 uRainCenter;
uniform vec2 uRainBox; // half extent (xz), height
out vec2 vLocal; // x across the streak (-1..1), y along it (0 = head)
out float vAlpha;
void main() {
int id = gl_InstanceID;
vec3 h = vec3(hash12(vec2(float(id), 1.37)), hash12(vec2(float(id), 7.13)), hash12(vec2(float(id), 3.71)));
float E = uRainBox.x, H = uRainBox.y;
float speed = uWeather.z * (0.85 + 0.3 * h.x);
vec2 wdir = normalize(uWind.xy + 1e-5);
vec3 vel = vec3(wdir.x, 0.0, wdir.y) * uWind.z * 28.0 + vec3(0.0, -speed, 0.0);
float age = fract(h.y + uTime * speed / H); // 0 = top of the box
float tFall = age * H / speed;
vec3 p;
p.y = uRainCenter.y + H * 0.75 - age * H;
vec2 base = h.xz * 2.0 * E + vel.xz * tFall;
p.xz = uRainCenter.xz + mod(base - uRainCenter.xz + E, 2.0 * E) - E;
vec3 dir = normalize(vel);
float len = speed * 0.032;
vec3 toCam = uCamPos.xyz - p;
float dist = length(toCam);
vec3 side = normalize(cross(dir, toCam / max(dist, 1e-3)));
float width = 0.008 + dist * 0.0006; // never thinner than ~1 px
vec2 corner = vec2(float(gl_VertexID & 1) * 2.0 - 1.0, float(gl_VertexID >> 1));
vec3 wp = p - dir * len * corner.y + side * width * corner.x;
vLocal = corner;
// Fade near the camera (big blurry drops) and at the box edges.
vec2 rel = abs(p.xz - uRainCenter.xz) / E;
vAlpha = smoothstep(1.5, 5.0, dist) * (1.0 - smoothstep(0.75, 1.0, max(rel.x, rel.y))) * (0.6 + 0.4 * h.z);
gl_Position = uViewProj * vec4(wp, 1.0);
}

11
assets/shaders/sky.frag Normal file
View File

@@ -0,0 +1,11 @@
#version 450 core
#include "common.glsl"
in vec2 vUV;
layout(location = 0) out vec4 fragColor;
void main() {
vec3 far = worldFromDepth(vUV, 1.0);
vec3 dir = normalize(far - uCamPos.xyz);
fragColor = vec4(skyColor(dir), 1.0);
}

55
assets/shaders/ssao.frag Normal file
View File

@@ -0,0 +1,55 @@
#version 450 core
#include "common.glsl"
// Half-res world-space hemisphere SSAO. Normals are reconstructed from depth
// (picking the flatter neighbour per axis to avoid smearing across edges).
in vec2 vUV;
layout(binding = 0) uniform sampler2D uDepth;
uniform float uRadius;
uniform float uIntensity;
layout(location = 0) out vec4 fragColor;
const int SAMPLES = 14;
vec3 posAt(vec2 uv) { return worldFromDepth(uv, texture(uDepth, uv).r); }
void main() {
float d = texture(uDepth, vUV).r;
if (d >= 1.0) {
fragColor = vec4(1.0);
return;
}
vec3 P = worldFromDepth(vUV, d);
vec2 t = 1.0 / vec2(textureSize(uDepth, 0));
vec3 px = posAt(vUV + vec2(t.x, 0)), nx = posAt(vUV - vec2(t.x, 0));
vec3 py = posAt(vUV + vec2(0, t.y)), ny = posAt(vUV - vec2(0, t.y));
vec3 dx = length(px - P) < length(P - nx) ? px - P : P - nx;
vec3 dy = length(py - P) < length(P - ny) ? py - P : P - ny;
vec3 N = normalize(cross(dx, dy));
if (dot(N, uCamPos.xyz - P) < 0.0) N = -N;
float rot = ign(gl_FragCoord.xy) * 6.2831853;
vec3 T = normalize(cross(N, abs(N.y) < 0.9 ? vec3(0, 1, 0) : vec3(1, 0, 0)));
vec3 B = cross(N, T);
float centerLin = linearDepth(d);
// Larger radius further away keeps the effect stable on screen.
float radius = uRadius * (0.8 + centerLin * 0.01);
float occ = 0.0;
for (int i = 0; i < SAMPLES; ++i) {
float fi = (float(i) + 0.5) / float(SAMPLES);
float a = float(i) * 2.399963 + rot;
float r = sqrt(fi);
float z = sqrt(max(0.0, 1.0 - r * r));
vec3 k = (T * cos(a) * r + B * sin(a) * r + N * z) * mix(0.15, 1.0, fi * fi);
vec3 S = P + N * 0.03 + k * radius;
vec4 clip = uViewProj * vec4(S, 1.0);
vec2 suv = clip.xy / clip.w * 0.5 + 0.5;
if (any(lessThan(suv, vec2(0.0))) || any(greaterThan(suv, vec2(1.0)))) continue;
float sceneLin = linearDepth(texture(uDepth, suv).r);
float range = smoothstep(0.0, 1.0, radius / max(abs(centerLin - sceneLin), 1e-3));
occ += step(sceneLin, clip.w - 0.02) * range;
}
float ao = 1.0 - occ / float(SAMPLES);
fragColor = vec4(pow(clamp(ao, 0.0, 1.0), uIntensity * 1.6));
}

View File

@@ -0,0 +1,29 @@
#version 450 core
#include "common.glsl"
// Depth-aware upsample of the AO buffer; output is multiplied into the HDR
// target by the blend unit (DST_COLOR, ZERO).
in vec2 vUV;
layout(binding = 0) uniform sampler2D uAo;
layout(binding = 1) uniform sampler2D uDepth;
uniform vec2 uAoTexel;
layout(location = 0) out vec4 fragColor;
void main() {
float raw = texture(uDepth, vUV).r;
if (raw >= 1.0) {
fragColor = vec4(1.0);
return;
}
float d0 = linearDepth(raw);
float sum = 0.0, wsum = 0.0;
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x) {
vec2 uv = vUV + vec2(x, y) * uAoTexel;
float d = linearDepth(texture(uDepth, uv).r);
float w = exp(-abs(d - d0) / (0.03 * d0 + 0.05));
sum += texture(uAo, uv).r * w;
wsum += w;
}
fragColor = vec4(vec3(sum / max(wsum, 1e-4)), 1.0);
}

View File

@@ -0,0 +1,22 @@
#version 450 core
#include "common.glsl"
// Separable depth-aware blur of the half-res AO buffer.
in vec2 vUV;
layout(binding = 0) uniform sampler2D uAo;
layout(binding = 1) uniform sampler2D uDepth;
uniform vec2 uDir; // texel step along the blur axis
layout(location = 0) out vec4 fragColor;
void main() {
float d0 = linearDepth(texture(uDepth, vUV).r);
float sum = 0.0, wsum = 0.0;
for (int i = -4; i <= 4; ++i) {
vec2 uv = vUV + uDir * float(i);
float d = linearDepth(texture(uDepth, uv).r);
float w = exp(-float(i * i) / 12.0) * exp(-abs(d - d0) / (0.03 * d0 + 0.05));
sum += texture(uAo, uv).r * w;
wsum += w;
}
fragColor = vec4(sum / max(wsum, 1e-4));
}

View File

@@ -0,0 +1,96 @@
#version 450 core
#include "common.glsl"
#include "pixelart.glsl"
in VS {
vec3 worldPos;
vec3 normal;
vec2 uv;
flat float layer;
float ao;
flat float kind;
} v;
layout(binding = 0) uniform sampler2DArray uTerrainTex;
layout(binding = 1) uniform usampler2D uMatMap;
uniform ivec2 uMapSize;
uniform ivec4 uMatTop[8]; // topFirst, topCount, accentFirst, accentCount
uniform float uMatAccent[8]; // accent chance
layout(location = 0) out vec4 fragColor;
// Picks the material of the pixel-art texel at `wp`. Tile borders are jittered
// with low-frequency noise plus chunky per-4px offsets so transitions look
// hand-drawn instead of grid aligned.
float topLayer(vec3 wp) {
float texels = float(textureSize(uTerrainTex, 0).x);
vec2 sp = (floor(wp.xz * texels) + 0.5) / texels;
vec2 j = vec2(vnoise(sp * 1.7), vnoise(sp * 1.7 + vec2(31.7, 11.3))) - 0.5;
vec2 chunk = floor(sp * texels / 4.0);
j += (vec2(hash12(chunk), hash12(chunk + 7.7)) - 0.5) * 0.3;
ivec2 cell = clamp(ivec2(floor(sp + j * 0.55)), ivec2(0), uMapSize - 1);
uint m = min(texelFetch(uMatMap, cell, 0).r, 7u);
ivec4 mt = uMatTop[m];
vec2 c = vec2(cell);
int layer = mt.x + int(hash12(c + 0.37) * float(mt.y)) % max(mt.y, 1);
if (mt.w > 0 && hash12(c * 1.37 + 5.1) < uMatAccent[m])
layer = mt.z + int(hash12(c + 9.3) * float(mt.w)) % mt.w;
return float(layer);
}
void main() {
vec3 wp = v.worldPos;
vec3 n = normalize(v.normal);
vec4 albedo;
if (v.kind < 0.5) {
albedo = sharpSampleArray(uTerrainTex, wp.xz, topLayer(wp));
} else {
albedo = sharpSampleArray(uTerrainTex, v.uv, v.layer);
if (v.kind > 1.5) {
// Ragged overhang strip (e.g. grass hanging over a cliff edge).
float texels = float(textureSize(uTerrainTex, 0).x);
float px = floor(v.uv.x * texels);
float lip = 2.0 + floor(hash12(vec2(px, v.layer)) * 2.5) + floor(vnoise(vec2(px * 0.3, 3.0)) * 3.0);
float row = v.uv.y * texels;
if (row < lip) {
albedo = sharpSampleArray(uTerrainTex, vec2(v.uv.x, wp.y), v.kind - 2.0);
albedo.rgb *= mix(1.0, 0.7, row / lip);
} else if (row < lip + 1.0) {
albedo.rgb *= 0.55; // dark contact line under the lip
}
}
}
vec3 L = uSunDir.xyz;
float ndl = max(dot(n, L), 0.0);
float vis = sunVisibility(wp + n * 0.03);
vec3 sun = uSunColor.rgb * uSunDir.w * ndl * vis;
// Camera-side fill keeps cliff faces readable when the sun is behind them.
vec3 toCam = cross(uBillboardRight.xyz, vec3(0.0, 1.0, 0.0));
vec3 amb = (hemiAmbient(n) + uSkyHorizon.rgb * uSkyHorizon.w * max(dot(n, toCam), 0.0)) * v.ao;
float gao = groundAO(wp, v.kind < 0.5 ? 0.0 : 0.6);
vec3 sheen = vec3(0.0);
vec3 base = albedo.rgb;
if (uWeather.y > 0.0 && wp.y > uWaterParams.x)
base = wetSurface(base, n, normalize(uCamPos.xyz - wp), vis, sheen);
// Contact AO dims ambient fully and direct light partially (reads as a soft contact shadow).
vec3 col = base * (sun * mix(1.0, gao, 0.7) + amb * gao + pointLights(wp, n)) + sheen * gao;
// Caustics on underwater surfaces, projected along the sun direction.
if (uWaterParams.z > 0.5 && wp.y < uWaterParams.x) {
float depth = uWaterParams.x - wp.y;
vec2 cp = wp.xz + L.xz / max(L.y, 0.2) * depth;
float c = caustics(cp * 0.33, uTime * 0.55);
float vSurf = sunVisibility(wp + L * (depth / max(L.y, 0.2)));
col += albedo.rgb * uSunColor.rgb * uSunDir.w * c * uWaterParams.y * vSurf *
exp(-depth * 0.7) * max(n.y, 0.35);
}
// Soft contact shadow under the player sprite.
if (uBlob.w > 0.0) {
float d = length(wp.xz - uBlob.xz) / uBlob.w;
float onFloor = 1.0 - smoothstep(0.2, 0.6, abs(wp.y - uBlob.y));
col *= 1.0 - 0.5 * (1.0 - smoothstep(0.3, 1.0, d)) * onFloor;
}
fragColor = vec4(col, 1.0);
}

View File

@@ -0,0 +1,27 @@
#version 450 core
#include "common.glsl"
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aUV;
layout(location = 3) in vec3 aMisc; // layer, ao, kind
out VS {
vec3 worldPos;
vec3 normal;
vec2 uv;
flat float layer;
float ao;
flat float kind;
} o;
void main() {
o.worldPos = aPos;
o.normal = aNormal;
o.uv = aUV;
o.layer = aMisc.x;
o.ao = aMisc.y;
o.kind = aMisc.z;
gl_Position = uViewProj * vec4(aPos, 1.0);
gl_ClipDistance[0] = dot(vec4(aPos, 1.0), uClipPlane);
}

View File

@@ -0,0 +1,63 @@
#version 450 core
// Bloom mix, color grading, ACES tonemap, vignette, chromatic aberration, grain.
in vec2 vUV;
layout(binding = 0) uniform sampler2D uHdr;
layout(binding = 1) uniform sampler2D uBloom;
uniform float uBloomStrength;
uniform float uBloomNorm;
uniform float uExposure;
uniform float uContrast;
uniform float uSaturation;
uniform vec3 uLift;
uniform vec3 uGain;
uniform float uVignette;
uniform float uChromatic;
uniform float uGrain;
uniform float uTimeSeed;
layout(location = 0) out vec4 fragColor;
vec3 acesFitted(vec3 x) {
const mat3 inM = mat3(0.59719, 0.07600, 0.02840, 0.35458, 0.90834, 0.13383, 0.04823, 0.01566, 0.83777);
const mat3 outM = mat3(1.60475, -0.10208, -0.00327, -0.53108, 1.10813, -0.07276, -0.07367, -0.00605, 1.07602);
vec3 v = inM * x;
vec3 a = v * (v + 0.0245786) - 0.000090537;
vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
return clamp(outM * (a / b), 0.0, 1.0);
}
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
void main() {
vec2 dir = vUV - 0.5;
vec3 c;
c.r = texture(uHdr, vUV - dir * uChromatic * 2.0).r;
c.g = texture(uHdr, vUV).g;
c.b = texture(uHdr, vUV + dir * uChromatic * 2.0).b;
vec3 bloom = texture(uBloom, vUV).rgb * uBloomNorm;
c = mix(c, bloom, uBloomStrength);
c *= uExposure;
// Contrast in log space around middle grey, then saturation.
const float mid = 0.18;
c = exp2((log2(max(c, vec3(1e-6))) - log2(mid)) * uContrast + log2(mid));
float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
c = max(mix(vec3(l), c, uSaturation), 0.0);
c *= uGain;
c = acesFitted(c);
c += uLift * (1.0 - c);
float v = 1.0 - smoothstep(0.25, 1.1, length(dir * vec2(1.25, 1.0)) * (0.6 + uVignette));
c *= mix(1.0, v, clamp(uVignette * 1.6, 0.0, 1.0));
c = pow(c, vec3(1.0 / 2.2));
c += (hash12(gl_FragCoord.xy + uTimeSeed) - 0.5) * uGrain;
fragColor = vec4(c, 1.0);
}

View File

@@ -0,0 +1,66 @@
#version 450 core
#include "common.glsl"
// Half-resolution ray march through height fog. Each step samples the sun
// shadow map and canopy cookie, producing physically shadowed light shafts
// (god rays). Output: rgb = in-scattered light, a = transmittance.
in vec2 vUV;
layout(binding = 0) uniform sampler2D uDepth;
layout(location = 0) out vec4 fragColor;
float phaseHG(float c, float g) {
float g2 = g * g;
return (1.0 - g2) / (4.0 * PI * pow(1.0 + g2 - 2.0 * g * c, 1.5));
}
float fogDensity(vec3 p) {
float h = exp(-max(p.y - uFogParams.y, 0.0) * uFogParams.x);
vec3 drift = vec3(uWind.x, 0.05, uWind.y) * uTime * 0.35;
float n = vnoise3(p * 0.16 + drift) * 0.65 + vnoise3(p * 0.45 - drift * 1.7) * 0.35;
n = mix(1.0, smoothstep(0.2, 0.8, n) * 1.8, uFogParams.z);
return uFogColor.w * h * n;
}
void main() {
float d = texture(uDepth, vUV).r;
vec3 wp = worldFromDepth(vUV, d);
vec3 ro = uCamPos.xyz;
vec3 rd = wp - ro;
float dist = length(rd);
rd /= dist;
dist = min(dist, uFogParams2.y);
int steps = int(uFogParams2.z);
float stepLen = dist / float(steps);
float jitter = ign(gl_FragCoord.xy);
float cosT = dot(rd, uSunDir.xyz);
// Dual lobe: strong forward scattering + a little back/side so shafts
// still read when looking away from the sun. Normalized: isotropic = 1.
float phase = (0.75 * phaseHG(cosT, uFogParams.w) + 0.25 * phaseHG(cosT, -0.25)) * 4.0 * PI;
vec3 sunL = uSunColor.rgb * uSunDir.w * uFogParams2.x * phase;
vec3 ambL = uFogColor.rgb * uSkyZenith.w;
vec3 scatter = vec3(0.0);
float T = 1.0;
for (int i = 0; i < steps; ++i) {
vec3 p = ro + rd * ((float(i) + jitter) * stepLen);
float dens = fogDensity(p);
if (dens < 1e-5) continue;
// Sparse beam mask: most sun scattering happens inside distinct shafts.
float vis = sunShadowFast(p) * shaftCookie(p);
vec3 S = (sunL * vis + ambL) * dens;
for (int l = 0; l < uLightCount.y; ++l) { // warm haze around fires
vec3 dl = uLightPos[l].xyz - p;
float r = uLightPos[l].w;
float att = max(1.0 - dot(dl, dl) / (r * r), 0.0);
att *= att;
S += uLightColor[l].rgb * att * att * dens * 0.35;
}
float Ts = exp(-dens * stepLen);
scatter += T * S * (1.0 - Ts) / dens; // energy-conserving step integral
T *= Ts;
if (T < 0.01) break;
}
fragColor = vec4(scatter, T);
}

177
assets/shaders/water.frag Normal file
View File

@@ -0,0 +1,177 @@
#version 450 core
#include "common.glsl"
in vec3 vWorld;
layout(binding = 0) uniform sampler2D uReflection;
layout(binding = 1) uniform sampler2D uSceneColor;
layout(binding = 2) uniform sampler2D uSceneDepth;
layout(binding = 3) uniform sampler2D uShoreDist;
uniform vec4 uShoreRect; // minX, minZ, 1/sizeX, 1/sizeZ
uniform vec3 uShallow;
uniform vec3 uDeep;
uniform vec3 uAbsorb;
uniform float uClarity;
uniform float uWaveScale;
uniform float uWaveStrength;
uniform float uWaveSpeed;
uniform float uRefraction;
uniform float uReflDistort;
uniform float uSpecular;
uniform float uGlitter;
uniform float uFoamWidth;
uniform float uFoamIntensity;
uniform int uHasReflection;
layout(location = 0) out vec4 fragColor;
// Value noise with analytic derivatives (iq).
vec3 noised(vec2 x) {
vec2 i = floor(x), f = fract(x);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
vec2 du = 30.0 * f * f * (f * (f - 2.0) + 1.0);
float a = hash12(i), b = hash12(i + vec2(1, 0)), c = hash12(i + vec2(0, 1)), d = hash12(i + vec2(1, 1));
float k1 = b - a, k2 = c - a, k4 = a - b - c + d;
return vec3(a + k1 * u.x + k2 * u.y + k4 * u.x * u.y, du * vec2(k1 + k4 * u.y, k2 + k4 * u.x));
}
// Twinkling point highlights: one wandering point per cell, each with its own
// blink rhythm. `r` is the point radius in cell units; it never shrinks below
// ~1.5 px so sparkles stay stable instead of aliasing.
float glitter(vec2 p, float t, float r) {
vec2 cell = floor(p), f = fract(p);
float px = fwidth(p.x) * 1.5;
float rr = max(r, px);
float energy = r * r / (rr * rr); // keep brightness constant when widened
float s = 0.0;
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x) {
vec2 id = cell + vec2(x, y);
vec2 h = vec2(hash12(id), hash12(id + 19.19));
vec2 pt = vec2(x, y) + 0.5 + 0.35 * sin(t * (0.4 + h * 0.6) + h.yx * 6.2832);
float blink = pow(max(sin(t * (1.5 + 3.0 * h.x) + h.y * 40.0), 0.0), 6.0);
s += (1.0 - smoothstep(rr * 0.4, rr, length(f - pt))) * blink;
}
return s * energy;
}
// Expanding rings from raindrops: one drop per cell per cycle, random phase.
// Returns the surface gradient contribution.
vec2 rainRipples(vec2 p, float t) {
vec2 cell = floor(p), f = fract(p);
vec2 grad = vec2(0.0);
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x) {
vec2 id = cell + vec2(x, y);
vec2 c = vec2(x, y) + vec2(hash12(id), hash12(id + 5.3)) * 0.8 + 0.1;
float phase = fract(t * 1.3 + hash12(id + 11.1));
vec2 d = f - c;
float r = length(d);
float front = phase * 0.9; // ring radius in cell units
float ring = sin((r - front) * 38.0) * exp(-(r - front) * (r - front) * 120.0) * (1.0 - phase);
grad += (r > 1e-3 ? d / r : vec2(0.0)) * ring;
}
return grad;
}
// Returns (normal.xyz, normalized height) of layered drifting wavelets.
vec4 waterSurface(vec2 p, float t, float shoreDist) {
vec2 rain = uWeather.x > 0.0
? (rainRipples(p * 2.4, t) + rainRipples(p * 3.7 + 9.0, t * 1.17)) * 0.3 * uWeather.x
: vec2(0.0);
vec2 grad = vec2(0.0);
float h = 0.0, amp = 1.0, freq = uWaveScale, norm = 0.0;
const mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);
vec2 dir = vec2(1.0, 0.35);
for (int i = 0; i < 5; ++i) {
vec3 n = noised(p * freq + dir * t * uWaveSpeed * (1.0 + float(i) * 0.4));
h += n.x * amp;
norm += amp;
grad += n.yz * amp * freq;
p = rot * p;
dir = rot * dir;
amp *= 0.5;
freq *= 1.85;
}
grad *= uWaveStrength * 0.09;
// Ripples rolling in toward the shore.
float ring = sin(shoreDist * 9.0 + t * 2.4) * exp(-shoreDist * 1.6) * 0.06 * uWaveStrength;
grad += vec2(ring) + rain;
return vec4(normalize(vec3(-grad.x, 1.0, -grad.y)), h / norm);
}
void main() {
vec3 wp = vWorld;
vec3 V = normalize(uCamPos.xyz - wp);
float t = uTime;
vec2 suv = gl_FragCoord.xy * uScreen.zw;
float shoreDist = texture(uShoreDist, (wp.xz - uShoreRect.xy) * uShoreRect.zw).r;
vec4 surf = waterSurface(wp.xz, t, shoreDist);
vec3 n = surf.xyz;
// Bottom under this pixel.
vec3 sceneP = worldFromDepth(suv, texture(uSceneDepth, suv).r);
float depthV = max(wp.y - sceneP.y, 0.0);
// Refraction: offset by the surface normal, scaled down in the shallows.
vec2 ruv = suv + n.xz * uRefraction * clamp(depthV * 2.5, 0.0, 1.0);
vec3 rP = worldFromDepth(ruv, texture(uSceneDepth, ruv).r);
if (rP.y > wp.y + 0.01) { // refracted sample is above water: fall back
ruv = suv;
rP = sceneP;
}
vec3 refr = texture(uSceneColor, ruv).rgb;
float rDepth = max(wp.y - rP.y, 0.0);
float path = length(rP - wp) + rDepth;
vec3 transmittance = exp(-uAbsorb * path / max(uClarity, 0.05));
float sunVis = sunVisibility(wp);
vec3 L = uSunDir.xyz;
vec3 sunRad = uSunColor.rgb * uSunDir.w;
vec3 lightIn = sunRad * max(L.y, 0.0) * mix(0.35, 1.0, sunVis) + hemiAmbient(vec3(0, 1, 0));
vec3 body = mix(uShallow, uDeep, 1.0 - exp(-rDepth * 0.8)) * lightIn * 0.65;
vec3 under = refr * transmittance + body * (1.0 - transmittance);
// Reflection: planar reflection texture shares screen mapping with the main view.
vec3 R = reflect(-V, n);
vec3 refl = uHasReflection == 1 ? texture(uReflection, suv + n.xz * uReflDistort).rgb : skyColor(R);
refl *= mix(vec3(1.0), normalize(uShallow + 1e-3) * 1.6, 0.35); // water tints what it reflects
float ndv = max(dot(n, V), 0.0);
float F = 0.02 + 0.98 * pow(1.0 - ndv, 5.0);
F = mix(F, 1.0, 0.06); // a hint of reflection even looking straight down
vec3 col = mix(under, refl, F);
// Sky-lit wave crests: stylized bright streaks that keep the surface alive
// even when the sun's reflection is out of view.
float crest = smoothstep(0.58, 0.78, surf.w);
col += skyColor(R) * crest * 0.10 * (0.5 + 0.5 * sunVis);
// Sun glints: tight core + wide sheen (HDR, drives bloom). Shadowed water
// still catches some sky glints so sparkles don't vanish under trees.
vec3 H = normalize(L + V);
float nh = max(dot(n, H), 0.0);
float specVis = mix(0.15, 1.0, sunVis);
float spec = pow(nh, 1200.0) * 120.0 + pow(nh, 160.0) * 4.0 + pow(nh, 24.0) * 0.35;
col += sunRad * spec * uSpecular * 0.05 * specVis;
// Glitter: a field of twinkling point highlights on wave facets, two scales.
// Each is a few pixels wide and very bright, so bloom and depth of field
// turn them into the soft bokeh sparkles typical of HD-2D water.
float lobe = 0.35 + pow(nh, 8.0) * 3.0;
float g = glitter(wp.xz * 3.5, t, 0.13) + glitter(wp.xz * 1.6 + 17.0, t * 0.8, 0.085) * 1.8;
g *= smoothstep(0.3, 0.7, surf.w) * (0.5 + 4.0 * F);
col += sunRad * g * lobe * uGlitter * 8.0 * specVis * max(L.y, 0.25);
// Shoreline foam: broken bands drifting along the distance-to-shore field.
float fn = vnoise(wp.xz * 4.0 + vec2(t * 0.35, -t * 0.25)) * 0.6 + vnoise(wp.xz * 9.0 - t * 0.5) * 0.4;
float d = shoreDist + (fn - 0.5) * 0.12;
float edge = 1.0 - smoothstep(0.0, uFoamWidth * 0.3, d); // solid contact line
float bands = sin(d / max(uFoamWidth, 1e-3) * 9.0 - t * 1.8) * 0.5 + 0.5; // receding wash
float wash = (1.0 - smoothstep(0.0, uFoamWidth, d)) * smoothstep(0.55, 0.85, bands * 0.6 + fn * 0.5);
float foam = clamp(max(edge, wash) * uFoamIntensity, 0.0, 1.0);
vec3 foamCol = vec3(0.92, 0.96, 1.0) * (sunRad * max(L.y, 0.0) * mix(0.4, 1.0, sunVis) + hemiAmbient(vec3(0, 1, 0))) * 0.55;
col = mix(col, foamCol, foam);
fragColor = vec4(col, 1.0);
}

16
assets/shaders/water.vert Normal file
View File

@@ -0,0 +1,16 @@
#version 450 core
#include "common.glsl"
layout(location = 0) in vec3 aPos;
uniform float uWaveStrength;
out vec3 vWorld;
void main() {
vec3 p = aPos;
// Gentle swell; fine detail comes from per-pixel normals.
p.y += (sin(p.x * 1.3 + uTime * 1.1) * cos(p.z * 1.1 + uTime * 0.9) +
sin(p.x * 0.7 - p.z * 0.9 + uTime * 0.6) * 0.5) * 0.018 * uWaveStrength;
vWorld = p;
gl_Position = uViewProj * vec4(p, 1.0);
}