Add the custom renderCsm fragment shader

This commit is contained in:
RallyBlock committed 2026-10-25 07:29:47 -04:00
1 parent 1aa4517282
commit 3d80ba088b
1 file changed
+730
+730
View File
@@ -0,0 +1,730 @@
#version 120
#extension GL_EXT_texture_array : require
#extension GL_EXT_texture_array : enable
// =============================================================================
// renderCsm_frag.glsl — Blockland high-quality / CSM fragment shader
// Originals: _backup_defaults/
//
// What this file does
// Lighting, fog, point lights, and cascaded shadow maps for bricks.
// Shadows: 8-tap Vogel near, 4-tap mid, 1-tap far. Particles: 1-tap.
// brickTOP: hide the overlay and light a five-face bevel in the shader.
// Ground_Plate is not this program (baked plate.png instead).
// Interior lamps: wrap + longer falloff so light crawls out of doorways.
// Outdoor shade crush (bevelAmb) only runs on stud bevels, not walls.
//
// How to reload
// Options → shader quality (or whatever forces a shader recompile).
//
// Do not
// Use fwidth() on the shadow compare value (draws cascade lines).
// Snap shadow UVs to texel centers (moiré on studded plates).
// Compress albedo*light together (washes out brickTOP studs).
// =============================================================================
// --- Sun terminator (Blockland stock) ---------------------------------------
// Softens the 0.04–0.10 NdotL band so faces don't pop from lit to shadow color.
const float NDOTL_TRANSITION_DENOM = 0.06f; // 0.1 - 0.04
// --- Shadow filter averages ---------------------------------------------------
const float INV_VOGEL_SAMPLES = 0.125f; // 1/8 taps
const float INV_VOGEL4_SAMPLES = 0.25f; // 1/4 taps
// --- Diffuse wrap ------------------------------------------------------------
// wrapLit = NdotL * SCALE + BIAS. Makes studs/cylinders a bit rounder.
// Higher BIAS = more light wraps onto the dark side.
const float WRAP_SCALE = 0.88f;
const float WRAP_BIAS = 0.12f;
// --- Shadows / ambient -------------------------------------------------------
// How much sky ambient still reaches a fully shadowed brick (0 = pitch black
// shade, 1 = no shadow darkening on ambient).
const float SHADOW_AMBIENT_FILL = 0.42f;
// --- Highlight shoulder (stops white tiles going solid white) ---------------
// Below KNEE, color is unchanged. Above it, extra brightness rolls off.
// Lower KNEE / higher COMPRESS = less blowout, dimmer lamps.
const float HIGHLIGHT_KNEE = 0.78f;
const float HIGHLIGHT_COMPRESS = 2.2f;
// --- Specular ----------------------------------------------------------------
// SUN: tight NdotH^16 spark from the directional light.
const float SUN_SPEC_GAIN = 1.15f;
// POINT: lamp glint brightness. This is the main "brighter lamps" knob.
const float POINT_SPEC_GAIN = 0.45f;
// Physical bulb size vs Blockland's attenuation radius (which is a volume,
// not the fixture). Higher = wider, softer lamp highlights.
const float POINT_SPEC_BULB = 0.25f;
// Extra spec on bevels vs flat (from |tangent.xy|).
const float BEVEL_SPEC_BOOST = 0.25f;
// Point-light window. 0 = cubic (tight, stock), 1 = quadratic (longer spill).
const float POINT_FALLOFF_GAIN = 0.40f;
const float POINT_FALLOFF_SOFT = 0.35f;
// Sky-bevel dim in shade. Only applied on studs (see applyLighting).
const float BEVEL_AMB_MIN = 0.58f;
// --- brickTOP bevel (procedural, tops only) --------------------------------
// Matches the five-region plate map. STUD_FLAT is |p| with p = uv*2-1
// (0.5 = inner square UV 0.25–0.75). STUD_SLOPE is bevel tilt.
const float STUD_FLAT = 0.50f;
const float STUD_SLOPE = 0.55f;
// --- Cascade blend / PCF radius ---------------------------------------------
// Extra UV radius per cascade index. Keep low so splits don't jump softness.
#define CASCADE_SOFTNESS_SCALE 0.25f
// Start mixing the NEXT cascade this early (0.12 = last 88% of the split).
// Lower = wider blend, more dual-map cost.
#define CASCADE_BLEND_START 0.12f
#define CASCADE_BLEND_RANGE (1.0f - CASCADE_BLEND_START)
// Atlas-edge fade: mix next map when UV is this close to 0 or 1.
#define CASCADE_UV_MARGIN 0.02f
#define CASCADE_UV_FADE 0.20f
// Vogel disk size in texels. Higher = softer 2D PCF.
#define VOGEL_RADIUS_SCALE 2.75f
// --- Distance fade toward unshadowed "minimum" look --------------------------
// Stud self-shadow aliases into rings at range. Fade CSM off between these
// view distances (world units).
#define SHADOW_FADE_START 16.0f
#define SHADOW_FADE_END 88.0f
// =============================================================================
// Engine varyings / uniforms (set by Blockland, do not rename)
// =============================================================================
varying vec4 vPos; // view-space position (cascade pick uses vPos.y)
varying vec3 worldNormal;
varying vec3 worldPos;
uniform vec4 dirLightDir; // sun direction
uniform vec4 dirLightColor;
uniform vec4 dirLightAmbient;
uniform vec4 dirShadowColor; // color in full shadow
uniform vec3 camPos;
uniform mat4 obj2World;
uniform mat4 world2Cam;
uniform int isParticle;
uniform int doColorMultiply;
uniform int glow; // emissive / light brick
uniform sampler2DArray stex; // shadow atlas (cascades in Z)
uniform sampler2D tex; // brick overlay (studs, prints)
varying vec2 texCoord;
// Per-pixel surface state (filled in calculateSurface)
vec4 viewDelta; // xyz = view dir (surface → camera), w = view distance
float specular; // sun Blinn term (NdotH^16)
float specFactor; // 1 + bevel boost (not overlay brightness)
float NdotL; // clamped sun lambert on surfNormal
float NdotLRaw; // signed, used for wrap
float geoNdotL; // geometric face vs sun (shadows / slope bias)
float fresnel; // Schlick, F0=0.04
float distLod; // 0 near → 1 at SHADOW_FADE_END
vec3 surfNormal; // worldNormal, or five-face bevel on brickTOP
// Cotangent frame from screen-space derivatives (no vertex tangents).
// Degenerate UV (stretched brickTOP on a side/sliver) must not perturb —
// inversesqrt of a tiny TBN is the salt-and-pepper sparkle on those faces.
vec3 perturbNormal(vec3 N, vec3 wp, vec2 uv, vec3 tN)
{
vec3 dp1 = dFdx(wp);
vec3 dp2 = dFdy(wp);
vec2 duv1 = dFdx(uv);
vec2 duv2 = dFdy(uv);
float uvLen = abs(duv1.x) + abs(duv1.y) + abs(duv2.x) + abs(duv2.y);
if(uvLen < 1.0e-8f)
return N;
vec3 dp2perp = cross(dp2, N);
vec3 dp1perp = cross(N, dp1);
vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
float m = max(dot(T, T), dot(B, B));
if(m < 1.0e-10f)
return N;
float invmax = inversesqrt(m);
vec3 P = T * (tN.x * invmax) + B * (tN.y * invmax) + N * tN.z;
float p2 = dot(P, P);
if(p2 < 1.0e-10f)
return N;
return P * inversesqrt(p2);
}
// Five-region plate: flat top + N/E/S/W bevels.
vec3 plateTangent(vec2 uv)
{
vec2 p = fract(uv) * 2.0f - 1.0f;
vec2 a = abs(p);
if(a.x <= STUD_FLAT && a.y <= STUD_FLAT)
return vec3(0.0f, 0.0f, 1.0f);
if(a.x > a.y)
return normalize(vec3(sign(p.x) * STUD_SLOPE, 0.0f, 1.0f));
return normalize(vec3(0.0f, sign(p.y) * STUD_SLOPE, 1.0f));
}
float plateSpecFactor(vec3 tN)
{
float bevel = length(tN.xy);
return mix(0.10f, 1.0f, smoothstep(0.02f, STUD_SLOPE * 0.7f, bevel))
+ bevel * BEVEL_SPEC_BOOST;
}
bool colorNear(vec3 c, vec3 t, float e)
{
vec3 d = abs(c - t);
return d.r < e && d.g < e && d.b < e;
}
vec3 probeTex(vec2 uv)
{
return texture2D(tex, uv, -16.0f).rgb;
}
// Stock brickTOP.png. Sides have a black center; prints will not match.
bool isStockBrickTop()
{
vec3 c00 = probeTex(vec2(0.01f, 0.01f));
vec3 c99 = probeTex(vec2(0.99f, 0.99f));
vec3 c50 = probeTex(vec2(0.5f, 0.01f));
vec3 c59 = probeTex(vec2(0.5f, 0.99f));
vec3 c55 = probeTex(vec2(0.5f, 0.5f));
return c00.r > 0.92f && c00.g > 0.92f && c00.b > 0.92f
&& c99.r < 0.08f && c99.g < 0.08f && c99.b < 0.08f
&& c50.r > 0.92f && c50.g > 0.92f && c50.b > 0.92f
&& c59.r < 0.08f && c59.g < 0.08f && c59.b < 0.08f
&& c55.r > 0.92f && c55.g > 0.92f && c55.b > 0.92f;
}
// Five-color nmap still used as brickTOP (not the baked grayscale plate.png).
bool isFiveRegionTopMap()
{
if(worldNormal.z < 0.72f)
return false;
vec3 s = texture2D(tex, texCoord, -16.0f).rgb;
return colorNear(s, vec3(0.451f, 0.451f, 0.906f), 0.09f)
|| colorNear(s, vec3(0.275f, 0.451f, 0.875f), 0.09f)
|| colorNear(s, vec3(0.620f, 0.451f, 0.875f), 0.09f)
|| colorNear(s, vec3(0.451f, 0.620f, 0.875f), 0.09f)
|| colorNear(s, vec3(0.451f, 0.275f, 0.875f), 0.09f);
}
// -----------------------------------------------------------------------------
// Brick color, view, sun spec, fresnel, distance fade.
// -----------------------------------------------------------------------------
void calculateSurface(vec4 color, inout vec4 albedo)
{
viewDelta.xyz = worldPos - camPos;
viewDelta.w = length(viewDelta.xyz);
viewDelta.xyz = -viewDelta.xyz / viewDelta.w; // toward camera
distLod = smoothstep(SHADOW_FADE_START, SHADOW_FADE_END, viewDelta.w);
vec4 texAlbedo = texture2D(tex, texCoord);
surfNormal = worldNormal;
geoNdotL = max(dot(worldNormal, dirLightDir.xyz), 0.0f);
vec3 tN = vec3(0.0f, 0.0f, 1.0f);
specFactor = 1.0f;
// Only real tops. isStockBrickTop() matches the bound texture, so a brick
// that puts brickTOP on a side would otherwise get plateTangent + TBN
// sparkle on that face.
bool topStud = (worldNormal.z > 0.72f)
&& (isStockBrickTop() || isFiveRegionTopMap());
if(topStud)
{
// Hide brickTOP overlay — its baked dark rim fights the moving spec.
albedo.rgb = color.rgb;
tN = plateTangent(texCoord);
specFactor = plateSpecFactor(tN);
surfNormal = perturbNormal(worldNormal, worldPos, texCoord, tN);
}
else
albedo.rgb = mix(color.rgb, texAlbedo.rgb, texAlbedo.a);
if(doColorMultiply == 1)
albedo *= gl_Color;
albedo.a = color.a;
NdotLRaw = dot(surfNormal, dirLightDir.xyz);
NdotL = max(NdotLRaw, 0.0f);
// Schlick fresnel: more on glancing edges.
float NdotV = max(dot(surfNormal, viewDelta.xyz), 0.0f);
float f = 1.0f - NdotV;
float f2 = f * f;
fresnel = 0.04f + 0.96f * f2 * f2 * f;
// Sun spec only if the sun actually hits this (perturbed) face.
if(NdotL > 0.04f)
{
vec3 halfVec = normalize(dirLightDir.xyz + viewDelta.xyz);
float NdotH = max(dot(surfNormal, halfVec), 0.0f);
float d2 = NdotH * NdotH;
float d4 = d2 * d2;
float d8 = d4 * d4;
specular = d8 * d8 * SUN_SPEC_GAIN * specFactor; // NdotH^16
}
else
specular = 0.0f;
}
// Fog (per-vertex fogCoords from the vertex shader).
uniform vec4 fogBaseColor;
uniform vec4 fogConsts;
uniform sampler2D fogTex;
varying vec2 fogCoords;
void applyFog(inout vec4 albedo)
{
vec4 fogColor = texture2D(fogTex, fogCoords) * fogBaseColor;
// Sine remap of fog alpha (stock Blockland).
float fogFactor = 0.5f + 0.5f * sin(fogColor.a * 3.141592654f - 1.570796327f);
albedo = mix(albedo, fogColor, fogFactor);
}
// Soft-clip values above HIGHLIGHT_KNEE so stacked lamps don't go solid white.
vec3 compressHighlights(vec3 c)
{
vec3 over = max(c - vec3(HIGHLIGHT_KNEE), vec3(0.0f));
return min(c, vec3(HIGHLIGHT_KNEE)) + over / (vec3(1.0f) + over * HIGHLIGHT_COMPRESS);
}
// =============================================================================
// Cascaded shadow maps
// far_d = view-depth of each split (x,y,z)
// texSize = atlas size (x) and 1/size (y)
// zScale = per-cascade depth scale for bias
// =============================================================================
uniform vec4 far_d;
uniform vec2 texSize;
uniform vec4 zScale;
uniform int shadowSplitCount;
// 0 at start of split, 1 at the far plane of this cascade.
float cascadeDepthBlend(float viewY, float split)
{
float t = clamp((viewY - split * CASCADE_BLEND_START) / (split * CASCADE_BLEND_RANGE), 0.0f, 1.0f);
return smoothstep(0.0f, 1.0f, t);
}
// 1 when UV is at the atlas border (cascade frustum edge), 0 in the middle.
float cascadeUvBlend(vec2 uv)
{
float edgeDist = min(min(uv.x, 1.0f - uv.x), min(uv.y, 1.0f - uv.y));
return 1.0f - smoothstep(CASCADE_UV_MARGIN, CASCADE_UV_FADE, edgeDist);
}
// Project this pixel into cascade A (and B if blending).
// offset = view-space jitter for the ultra 3D grid; (0,0,0) for a normal sample.
// .xy = atlas UV, .z = cascade layer, .w = depth compare value.
void calculateShadowCoords(vec3 offset, inout vec4 shadow_coordA, inout vec4 shadow_coordB, out float blend, bool allowBlend)
{
vec4 rPos = vPos + vec4(offset, 0.0f);
int index = 3; // farthest cascade unless a nearer split matches
// Depth bias: more on grazing receivers, less on cascade 0 (contact at feet).
float slope = clamp(1.0f - geoNdotL, 0.0f, 1.0f);
float fudgeKey = 0.008f + 0.012f * slope * slope;
if(rPos.y < far_d.x)
fudgeKey *= 0.75f;
float fudgeFactorA = 4.0f * fudgeKey / zScale.w;
float fudgeFactorB = 4.0f * fudgeKey / zScale.w;
float depthBlend = 0.0f;
if(rPos.y < far_d.x)
{
index = 0;
if(allowBlend && shadowSplitCount > 1)
depthBlend = cascadeDepthBlend(rPos.y, far_d.x);
fudgeFactorA = fudgeKey / zScale.x;
fudgeFactorB = 2.0f * fudgeKey / zScale.y;
}
else if(rPos.y < far_d.y)
{
index = 1;
if(allowBlend && shadowSplitCount > 2)
depthBlend = cascadeDepthBlend(rPos.y, far_d.y);
fudgeFactorA = 2.0f * fudgeKey / zScale.y;
fudgeFactorB = 3.0f * fudgeKey / zScale.z;
}
else if(rPos.y < far_d.z)
{
index = 2;
if(allowBlend && shadowSplitCount > 3)
depthBlend = cascadeDepthBlend(rPos.y, far_d.z);
fudgeFactorA = 3.0f * fudgeKey / zScale.z;
fudgeFactorB = 4.0f * fudgeKey / zScale.w;
}
shadow_coordA = gl_TextureMatrix[index]*rPos;
shadow_coordA.w = shadow_coordA.z - fudgeFactorA;
shadow_coordA.z = float(index);
blend = depthBlend;
if(allowBlend && index < 3 && (index + 1) < shadowSplitCount)
{
shadow_coordB = gl_TextureMatrix[index + 1]*rPos;
shadow_coordB.w = shadow_coordB.z - fudgeFactorB;
shadow_coordB.z = float(index + 1);
blend = max(depthBlend, cascadeUvBlend(shadow_coordA.xy));
}
}
// 1 = lit (receiver closer than stored depth), 0 = shadowed.
float sampleShadowTap(vec2 uv, float layer, float compare)
{
return step(0.0f, texture2DArray(stex, vec3(uv, layer)).x - compare - 1.0e-6f);
}
// 8-tap disk PCF (near cascades).
float vogelSample(vec4 shadow_coord, float radius)
{
float compare = shadow_coord.w;
float layer = shadow_coord.z;
vec2 uv = shadow_coord.xy;
float hits = 0.0f;
hits += sampleShadowTap(uv + vec2( 0.250000f, 0.000000f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2(-0.319292f, 0.292492f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2( 0.048890f, -0.556882f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2( 0.401722f, 0.525731f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2(-0.707107f, 0.000000f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2( 0.157013f, 0.693780f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2( 0.453990f, -0.632281f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2(-0.340561f, -0.748511f) * radius, layer, compare);
return hits * INV_VOGEL_SAMPLES;
}
float vogelSample4(vec4 shadow_coord, float radius)
{
float compare = shadow_coord.w;
float layer = shadow_coord.z;
vec2 uv = shadow_coord.xy;
float hits = 0.0f;
hits += sampleShadowTap(uv + vec2( 0.250000f, 0.000000f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2(-0.707107f, 0.000000f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2( 0.048890f, -0.556882f) * radius, layer, compare);
hits += sampleShadowTap(uv + vec2( 0.401722f, 0.525731f) * radius, layer, compare);
return hits * INV_VOGEL4_SAMPLES;
}
float basicShadow(vec4 shadow_coord)
{
return sampleShadowTap(shadow_coord.xy, shadow_coord.z, shadow_coord.w);
}
// 8-tap near, 4-tap mid, 1-tap far.
float filterCascade(vec4 shadow_coord, float radius, int idx)
{
if(idx <= 1)
return vogelSample(shadow_coord, radius);
if(idx == 2)
return vogelSample4(shadow_coord, radius);
return basicShadow(shadow_coord);
}
// =============================================================================
// Point lights (engine binds up to 8; unused have radius 0)
// =============================================================================
uniform vec4 pointLightPos0;
uniform vec4 pointLightColor0;
uniform float pointLightRadius0;
uniform vec4 pointLightPos1;
uniform vec4 pointLightColor1;
uniform float pointLightRadius1;
uniform vec4 pointLightPos2;
uniform vec4 pointLightColor2;
uniform float pointLightRadius2;
uniform vec4 pointLightPos3;
uniform vec4 pointLightColor3;
uniform float pointLightRadius3;
uniform vec4 pointLightPos4;
uniform vec4 pointLightColor4;
uniform float pointLightRadius4;
uniform vec4 pointLightPos5;
uniform vec4 pointLightColor5;
uniform float pointLightRadius5;
uniform vec4 pointLightPos6;
uniform vec4 pointLightColor6;
uniform float pointLightRadius6;
uniform vec4 pointLightPos7;
uniform vec4 pointLightColor7;
uniform float pointLightRadius7;
// 1 at the bulb, 0 at radius. Mix cubic→quadratic so interiors spill farther
// without a big brightness jump at the fixture.
float pointLightWindow(float lightDist, float radius)
{
float ratio = max(0.0f, 1.0f - lightDist / radius);
float r2 = ratio * ratio;
return r2 * mix(ratio, 1.0f, POINT_FALLOFF_SOFT) * POINT_FALLOFF_GAIN;
}
// Lamp specular: tight Blinn on surfNormal. Low exponent used to
// flood whole facets; keep it high so only a small glint reads as plastic.
float pointBlinnSpec(vec3 lightDir, float lightDist, float radius, float lightDot)
{
float bulb = max(radius * POINT_SPEC_BULB, 0.08f);
float lift = clamp(bulb / max(lightDist, bulb * 0.2f), 0.0f, 0.22f);
vec3 L = normalize(lightDir + surfNormal * lift);
vec3 halfVec = normalize(L + viewDelta.xyz);
float nh = max(dot(surfNormal, halfVec), 0.0f);
float angularSize = clamp(bulb / max(lightDist, bulb * 0.2f), 0.0f, 1.0f);
float width = smoothstep(0.12f, 0.9f, angularSize);
float specExp = mix(36.0f, 14.0f, width);
float energy = mix(0.55f, 0.22f, width);
float facing = smoothstep(0.12f, 0.42f, lightDot);
return pow(nh, specExp) * energy * POINT_SPEC_GAIN * specFactor
* (0.80f + 0.20f * fresnel) * facing;
}
void addPointLight(vec3 pos, vec3 color, float radius, inout vec3 diffuse, inout vec3 spec)
{
vec3 lightDelta = worldPos.xyz - pos;
float distSq = dot(lightDelta, lightDelta);
float radiusSq = radius * radius;
if(distSq >= radiusSq)
return;
float invDist = inversesqrt(distSq);
float lightDist = invDist * distSq;
vec3 L = -lightDelta * invDist;
float nDotL = max(dot(L, surfNormal), 0.0f);
float wrapDot = max(dot(L, surfNormal) * WRAP_SCALE + WRAP_BIAS, 0.0f);
float ratio = pointLightWindow(lightDist, radius);
diffuse += ratio * wrapDot * color;
spec += ratio * pointBlinnSpec(L, lightDist, radius, nDotL) * color;
}
vec4 accumulatePointLights(out vec3 pointSpec)
{
vec3 diffuse = vec3(0.0f);
pointSpec = vec3(0.0f);
addPointLight(pointLightPos0.xyz, pointLightColor0.xyz, pointLightRadius0, diffuse, pointSpec);
addPointLight(pointLightPos1.xyz, pointLightColor1.xyz, pointLightRadius1, diffuse, pointSpec);
addPointLight(pointLightPos2.xyz, pointLightColor2.xyz, pointLightRadius2, diffuse, pointSpec);
addPointLight(pointLightPos3.xyz, pointLightColor3.xyz, pointLightRadius3, diffuse, pointSpec);
addPointLight(pointLightPos4.xyz, pointLightColor4.xyz, pointLightRadius4, diffuse, pointSpec);
addPointLight(pointLightPos5.xyz, pointLightColor5.xyz, pointLightRadius5, diffuse, pointSpec);
addPointLight(pointLightPos6.xyz, pointLightColor6.xyz, pointLightRadius6, diffuse, pointSpec);
addPointLight(pointLightPos7.xyz, pointLightColor7.xyz, pointLightRadius7, diffuse, pointSpec);
return vec4(diffuse, 0.0f);
}
// Particles: lights 0–1 only, no NdotL, no spec (stock).
vec4 accumulateParticlePointLights()
{
vec4 pointLightTotal = vec4(0.0f, 0.0f, 0.0f, 0.0f);
vec3 lightDelta = vec3(0.0f, 0.0f, 0.0f);
float distSq = 0.0f;
float lightDist = 0.0f;
lightDelta = worldPos.xyz - pointLightPos0.xyz;
distSq = dot(lightDelta, lightDelta);
float radiusSq0 = pointLightRadius0 * pointLightRadius0;
if(distSq < radiusSq0)
{
float invDist = inversesqrt(distSq);
lightDist = invDist * distSq;
pointLightTotal.xyz += pointLightWindow(lightDist, pointLightRadius0) * pointLightColor0.xyz;
}
lightDelta = worldPos.xyz - pointLightPos1.xyz;
distSq = dot(lightDelta, lightDelta);
float radiusSq1 = pointLightRadius1 * pointLightRadius1;
if(distSq < radiusSq1)
{
float invDist = inversesqrt(distSq);
lightDist = invDist * distSq;
pointLightTotal.xyz += pointLightWindow(lightDist, pointLightRadius1) * pointLightColor1.xyz;
}
return pointLightTotal;
}
// Combine sun, ambient, shadow color, lamps, spec, rim. Compress light, then
// multiply albedo (preserves stud contrast).
void applyLighting(inout vec4 combinedColor, vec4 albedo, float occlusionFactor)
{
// Dielectric: most of the glint takes the brick color so brown stays brown.
vec3 specularTint = mix(vec3(1.0f), albedo.rgb, 0.72f);
vec3 dirLightSpecular = occlusionFactor * specular * 0.5f * dirLightColor.rgb * specularTint;
// More ambient on faces pointing up (world Z). Fades to even at distance.
float skyFactor = mix(surfNormal.z * 0.5f + 0.5f, 0.5f, distLod);
vec4 skyAmbient = dirLightAmbient * (0.72f + skyFactor * 0.55f);
float wrapLit = max(NdotLRaw * WRAP_SCALE + WRAP_BIAS, 0.0f);
float ambOcc = mix(SHADOW_AMBIENT_FILL, 1.0f, occlusionFactor);
vec4 dirLightDirect = ((wrapLit * dirLightColor) * occlusionFactor) + (skyAmbient * ambOcc) + (dirShadowColor * (1.0f - occlusionFactor));
vec3 coolShadow = dirShadowColor.rgb * vec3(0.92f, 0.96f, 1.08f);
vec3 shadedFill = coolShadow + skyAmbient.rgb * SHADOW_AMBIENT_FILL;
// Sky-facing bevel in shade: form lighting on studs only. Walls used to
// get this too, which crushed indoor corridors to flat black.
float studMask = clamp(length(surfNormal - worldNormal) * 5.0f, 0.0f, 1.0f);
vec3 skyDir = vec3(0.14f, 0.28f, 0.95f);
skyDir = skyDir * inversesqrt(dot(skyDir, skyDir));
float skyN = max(dot(surfNormal, skyDir), 0.0f);
float bevelAmb = mix(1.0f, mix(BEVEL_AMB_MIN, 1.0f, skyN), studMask);
shadedFill *= bevelAmb;
dirLightDirect.rgb *= mix(bevelAmb, 1.0f, occlusionFactor);
// Terminator on the geometric face, not the stud N — otherwise each
// pyramid facet dumps to shade as a solid polygon.
if(geoNdotL <= 0.04f)
{
dirLightDirect = vec4(shadedFill, dirShadowColor.a);
dirLightSpecular = vec3(0.0f);
}
else if(geoNdotL <= 0.1f)
{
float val = (geoNdotL - 0.04f) / NDOTL_TRANSITION_DENOM;
dirLightDirect.rgb = (dirLightDirect.rgb * val) + (shadedFill * (1.0f - val));
dirLightSpecular *= val;
}
vec3 pointSpec = vec3(0.0f);
dirLightDirect.rgb += accumulatePointLights(pointSpec).rgb;
vec3 specTotal = dirLightSpecular + pointSpec * specularTint;
vec3 halfSky = normalize(skyDir + viewDelta.xyz);
float ambSpec = pow(max(dot(surfNormal, halfSky), 0.0f), 22.0f)
* specFactor * 0.22f * studMask;
specTotal += ambSpec * specularTint * (dirLightAmbient.rgb + vec3(0.12f));
vec3 rimLight = fresnel * dirLightAmbient.rgb * 0.15f * (1.0f - distLod);
float specIntensity = length(specTotal);
float kD = max(1.0f - specIntensity * 0.2f, 0.8f); // leave some diffuse where spec is strong
float fakeAO = mix(0.90f + surfNormal.z * 0.10f, 1.0f, distLod);
dirLightDirect.a *= min(occlusionFactor + 0.75f, 1.0f);
vec3 light = compressHighlights(dirLightDirect.rgb * kD * fakeAO);
combinedColor.rgb = light * albedo.rgb + rimLight + specTotal;
combinedColor.a = albedo.a;
}
// Returns 0 (shadowed) … 1 (lit). cheap = particles (1-tap + cascade blend).
float shadowCoef(bool cheap)
{
float blend = 0.0f;
vec4 shadow_coordA = vec4(0.0f, 0.0f, 0.0f, 0.0f);
vec4 shadow_coordB = vec4(0.0f, 0.0f, 0.0f, 0.0f);
calculateShadowCoords(vec3(0.0f), shadow_coordA, shadow_coordB, blend, true);
float baseRadius = max(texSize.y, 0.5f / 1024.0f) * VOGEL_RADIUS_SCALE;
float texel = max(texSize.y, 0.5f / 1024.0f);
float slope = max(0.0f, 1.0f - geoNdotL);
float slopeBias = texel * 0.15f * slope;
shadow_coordA.w -= slopeBias;
int idx = int(shadow_coordA.z);
float radiusNear = baseRadius * (1.0f + float(idx) * CASCADE_SOFTNESS_SCALE);
float radiusFar = baseRadius * (1.0f + float(idx + 1) * CASCADE_SOFTNESS_SCALE);
float radiusA = mix(radiusNear, radiusFar, blend);
if(cheap)
{
float shadowA = basicShadow(shadow_coordA);
if(blend > 0.0f)
{
shadow_coordB.w -= slopeBias;
return mix(shadowA, basicShadow(shadow_coordB), blend);
}
return shadowA;
}
// 8 / 4 / 1 tap by cascade. Blend mixes the next map at splits.
float shadowA = filterCascade(shadow_coordA, radiusA, idx);
if(blend > 0.0f)
{
shadow_coordB.w -= slopeBias;
float shadowB = filterCascade(shadow_coordB, radiusFar, idx + 1);
return mix(shadowA, shadowB, blend);
}
return shadowA;
}
void main()
{
// Glow / emitter: skip lighting and shadows.
if(glow == 1 || (worldNormal.x + worldNormal.y + worldNormal.z) > 2.0f)
{
vec4 texAlbedo = texture2D(tex, texCoord);
vec4 albedo;
albedo.rgb = mix(gl_Color.rgb, texAlbedo.rgb, texAlbedo.a);
albedo.a = gl_Color.a;
if(doColorMultiply == 1)
albedo *= gl_Color;
gl_FragColor = vec4(albedo.rgb * 1.5f, albedo.a);
return;
}
if(isParticle == 1)
{
NdotL = max(dot(worldNormal, dirLightDir.xyz), 0.0f);
geoNdotL = NdotL;
float occlusionFactor = 0.0f;
if(NdotL > -0.01f)
{
if(shadowSplitCount <= 0)
occlusionFactor = 1.0f;
else
occlusionFactor = shadowCoef(true);
}
vec4 texAlbedo = texture2D(tex, texCoord);
vec4 dirLightDirect = (dirLightColor * occlusionFactor) + (dirLightAmbient * occlusionFactor) + (dirShadowColor * (1.0f - occlusionFactor));
vec4 plt = accumulateParticlePointLights();
vec4 lightTotal = dirLightDirect + plt;
lightTotal.rgb = compressHighlights(lightTotal.rgb);
vec4 fragColor = texAlbedo * gl_Color * lightTotal;
applyFog(fragColor);
fragColor.a = texAlbedo.a * gl_Color.a;
gl_FragColor = fragColor;
return;
}
vec4 albedo = vec4(0.0f, 0.0f, 0.0f, 0.0f);
calculateSurface(gl_Color, albedo);
float occlusionFactor = 0.0f;
if(geoNdotL > -0.01f)
{
if(shadowSplitCount <= 0 || distLod >= 1.0f)
occlusionFactor = 1.0f;
else
occlusionFactor = shadowCoef(false);
}
occlusionFactor = mix(occlusionFactor, 1.0f, distLod);
vec4 fragColor = vec4(0.0f, 0.0f, 0.0f, 0.0f);
applyLighting(fragColor, albedo, occlusionFactor);
applyFog(fragColor);
gl_FragColor = fragColor;
}