From 3d80ba088b1c959a9750a3a0331d5774306fb020 Mon Sep 17 00:00:00 2001 From: RallyBlock Date: Sun, 25 Oct 2026 07:29:47 -0400 Subject: [PATCH] Add the custom renderCsm fragment shader --- renderCsm_frag.glsl | 730 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 730 insertions(+) create mode 100644 renderCsm_frag.glsl diff --git a/renderCsm_frag.glsl b/renderCsm_frag.glsl new file mode 100644 index 0000000..1ded9b5 --- /dev/null +++ b/renderCsm_frag.glsl @@ -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; +}