Files
renderCsm/renderCsm_frag.glsl
T

731 lines
27 KiB
GLSL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
}