#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; }