Fry3DX/engine/shaders/lights.glsl
Nexoniarz f3e8eca87b Fry3DX 0.4: an easy 3D modeller
Shapes with live settings, shape editing, knife and hole tools, drawing
shapes onto other objects, combining objects (add / subtract / overlap),
copies, thickness, painting with stickers and layers, a Layout tab, a
Print tab with checks, packed .fry files with autosave, export to GLB,
glTF, FBX, OBJ, 3MF, STL and PLY, folders in Layers, its own crash
screen, and a website for GitHub Pages in docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
2026-10-19 21:18:35 +02:00

99 lines
4.8 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.

// Clustered dynamic lights for mesh.frag. Grid constants match render/clusters.h.
const uvec3 kClusters = uvec3(16, 9, 24);
const float kClusterNear = 8.0, kClusterFar = 16384.0;
uint clusterIndex(vec3 world) {
float depth = dot(world - FRAME.camPos.xyz, FRAME.camFwd.xyz);
uint z = depth <= kClusterNear ? 0u : min(uint(log(depth / kClusterNear) / log(kClusterFar / kClusterNear) * float(kClusters.z)), kClusters.z - 1u);
uvec2 tile = min(uvec2(gl_FragCoord.xy / FRAME.screen.xy * vec2(kClusters.xy)), kClusters.xy - 1u);
return (z * kClusters.y + tile.y) * kClusters.x + tile.x;
}
// 1 = lit. 3×3 bilinear PCF taps inside the light's tile of the shadow atlas.
float atlasShadow(int view, vec3 world, vec3 n, float dist) {
ShadowView sv = SHADOW_VIEW(view);
vec4 c = sv.viewProj * vec4(world + n * (0.75 + dist * 0.006), 1.0);
if (c.w <= 0.0) return 1.0;
c.xyz /= c.w;
if (any(greaterThan(abs(c.xy), vec2(1.0))) || c.z > 1.0) return 1.0;
float texel = 1.0 / FRAME.screen.w;
vec2 lo = sv.rect.xy + texel, hi = sv.rect.xy + sv.rect.zw - texel;
vec2 at = sv.rect.xy + (c.xy * 0.5 + 0.5) * sv.rect.zw;
uint atlas = uint(FRAME.screen.z);
float lit = 0.0;
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x) lit += shadowTap(atlas, at + vec2(x, y) * texel, lo, hi, c.z - 0.00002);
return lit / 9.0;
}
// GGX specular: height-correlated Smith visibility, Schlick Fresnel (F0: g_f0).
// Times π, because our light values are "what a white wall shows" (diffuse = albedo × light).
// Multiply by n·l and the light. rough: perceptual roughness.
// Reflectance at normal incidence for this surface (mesh.frag sets it: 0.04 for dielectrics,
// the albedo for metals).
vec3 g_f0 = vec3(0.04);
vec3 specGGX(vec3 n, vec3 l, vec3 v, float rough) {
vec3 h = normalize(l + v);
float a = max(rough * rough, 0.002), a2 = a * a;
float ndl = max(dot(n, l), 1e-4), ndv = max(dot(n, v), 1e-4), ndh = max(dot(n, h), 0.0), vdh = max(dot(v, h), 0.0);
float q = ndh * ndh * (a2 - 1.0) + 1.0;
float d = a2 / (3.14159265 * q * q);
float vis = 0.5 / (ndl * sqrt(ndv * ndv * (1.0 - a2) + a2) + ndv * sqrt(ndl * ndl * (1.0 - a2) + a2));
vec3 f = g_f0 + (1.0 - g_f0) * pow(1.0 - vdh, 5.0);
return min(3.14159265 * d * vis * f, vec3(64.0));
}
// Light reaching the surface from every dynamic light in its cluster: diffuse (returned) and
// the highlights (spec), sharper and stronger the glossier the surface.
vec3 dynamicLights(vec3 world, vec3 n, float rough, vec3 v, out uint count, out vec3 spec) {
count = 0u;
spec = vec3(0.0);
if (FRAME.camPos.w < 0.5) return vec3(0.0);
// A mirror's pass sees from somewhere else than the clusters were made for: every light.
bool every = FRAME.mirror.y > 0.5;
uvec2 cell = every ? uvec2(0u, min(uint(FRAME.camPos.w), 64u)) : CELL(clusterIndex(world));
count = cell.y;
vec3 total = vec3(0.0);
for (uint k = 0u; k < cell.y; ++k) {
Light l = LIGHT(every ? k : LIGHT_INDEX(cell.x + k));
vec3 toLight = l.posRadius.xyz - world;
float dist = length(toLight);
if (dist >= l.posRadius.w) continue;
vec3 L = toLight / max(dist, 0.001);
float ndl = dot(n, L);
if (ndl <= 0.0) continue;
float a = 1.0 - dist / l.posRadius.w;
float atten = a * a;
if (l.spot.w > -1.5) atten *= spotShape(l, dot(-L, l.spot.xyz));
if (atten <= 0.0) continue;
if (l.color.w >= 0.0) {
int view = int(l.color.w);
if (l.spot.w <= -1.5) { // point light: one atlas tile per cube face, +X -X +Y -Y +Z -Z
vec3 d = -toLight, ad = abs(d);
view += ad.x >= ad.y && ad.x >= ad.z ? (d.x > 0.0 ? 0 : 1) : ad.y >= ad.z ? (d.y > 0.0 ? 2 : 3) : (d.z > 0.0 ? 4 : 5);
}
atten *= atlasShadow(view, world, n, dist);
}
total += l.color.rgb * atten * ndl;
// Area light (a sphere of radius extra.y): the highlight comes from the point on it
// nearest the reflected ray, and widens to match (Karis 2013, "representative point").
vec3 Ls = L;
float norm = 1.0;
if (l.extra.y > 0.0) {
vec3 r = reflect(-v, n);
vec3 toRay = dot(toLight, r) * r - toLight;
vec3 closest = toLight + toRay * clamp(l.extra.y / max(length(toRay), 1e-3), 0.0, 1.0);
Ls = normalize(closest);
float a = max(rough * rough, 0.002), a2 = clamp(a + l.extra.y / (2.0 * dist), 0.0, 1.0);
norm = (a / a2) * (a / a2);
}
spec += l.color.rgb * atten * ndl * specGGX(n, Ls, v, rough) * norm;
}
return total;
}
vec3 heatmap(uint count) {
float t = clamp(float(count) / 16.0, 0.0, 1.0);
return count == 0u ? vec3(0.0, 0.0, 0.15) : mix(vec3(0.0, 0.4, 1.0), vec3(1.0, 0.1, 0.0), t);
}