Fry3DX/engine/shaders/airlight.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

61 lines
3.1 KiB
GLSL

// The light in the air at a point (volumetric.frag, froxel.comp): the sun through its shadow
// cascades, and the dynamic lights of that point's cluster with a one-tap shadow each. Needs
// FRAME, LIGHT, CELL, LIGHT_INDEX and SHADOW_VIEW (model.glsl with KEEL_FRAME_ONLY).
float sunlitAt(vec3 w) {
float depth = dot(w - FRAME.camPos.xyz, FRAME.camFwd.xyz);
if (depth > FRAME.splits.w) return 1.0;
int c = depth < FRAME.splits.x ? 0 : depth < FRAME.splits.y ? 1 : depth < FRAME.splits.z ? 2 : 3;
vec4 p = FRAME.cascades[c] * vec4(w, 1.0);
vec2 uv = p.xy * 0.5 + 0.5;
if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0))) || p.z > 1.0) return 1.0;
vec2 base = vec2(c & 1, c >> 1) * 0.5;
float t = FRAME.shadow.x;
return shadowTap(uint(FRAME.sunDir.w), base + uv * 0.5, base + t, base + 0.5 - t, p.z - 0.0005);
}
// The light cluster a point in the air is in (render/clusters.h), from its pixel's place on screen.
const uvec3 kClusters = uvec3(16, 9, 24);
const float kClusterNear = 8.0, kClusterFar = 16384.0;
uint clusterAt(vec3 w, vec2 uv) {
float depth = dot(w - 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(uv * vec2(kClusters.xy)), kClusters.xy - 1u);
return (z * kClusters.y + tile.y) * kClusters.x + tile.x;
}
float phaseHG(float mu, float g) { return (1.0 - g * g) / (12.566 * pow(1.0 + g * g - 2.0 * g * mu, 1.5)); }
// The dynamic lights lighting the air at w (towards the eye along -rd).
vec3 lightsAt(vec3 w, vec3 rd, vec2 uv) {
if (FRAME.camPos.w < 0.5) return vec3(0.0);
uvec2 cell = CELL(clusterAt(w, uv));
vec3 total = vec3(0.0);
for (uint k = 0u; k < min(cell.y, 8u); ++k) {
Light l = LIGHT(LIGHT_INDEX(cell.x + k));
vec3 toLight = l.posRadius.xyz - w;
float dist = length(toLight);
if (dist >= l.posRadius.w) continue;
vec3 L = toLight / max(dist, 0.001);
float a = 1.0 - dist / l.posRadius.w, 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) { // shadowed: one tap in its atlas tile
int view = int(l.color.w);
if (l.spot.w <= -1.5) {
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);
}
ShadowView sv = SHADOW_VIEW(view);
vec4 c = sv.viewProj * vec4(w, 1.0);
if (c.w > 0.0) {
c.xyz /= c.w;
if (all(lessThanEqual(abs(c.xy), vec2(1.0))) && c.z <= 1.0) {
float texel = 1.0 / FRAME.screen.w;
vec2 at = sv.rect.xy + (c.xy * 0.5 + 0.5) * sv.rect.zw;
atten *= shadowTap(uint(FRAME.screen.z), at, sv.rect.xy + texel, sv.rect.xy + sv.rect.zw - texel, c.z - 0.00005);
}
}
}
total += l.color.rgb * atten * phaseHG(dot(rd, -L), 0.3);
}
return total;
}