mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
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
This commit is contained in:
commit
f3e8eca87b
135 changed files with 23423 additions and 0 deletions
32
engine/shaders/adapt.frag
Normal file
32
engine/shaders/adapt.frag
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#version 460
|
||||
// Eye adaptation (1×1): the scene's average brightness (geometric mean of 16 taps of the
|
||||
// smallest bloom level), turned into an exposure that keeps a mid grey where it should be,
|
||||
// eased towards (0.6x to 1.8x, so nothing burns out) from last frame's at `speed` per second (faster into the dark is slower,
|
||||
// as eyes are). The tonemap multiplies its exposure by it.
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
uint src; // the smallest bloom level
|
||||
uint prev; // last frame's (0 = start fresh)
|
||||
float dt;
|
||||
float speed;
|
||||
float key; // the brightness to aim for
|
||||
float minE, maxE;
|
||||
float pad;
|
||||
} pc;
|
||||
layout(location = 0) out vec4 o_exposure;
|
||||
|
||||
void main() {
|
||||
float sum = 0.0;
|
||||
for (int y = 0; y < 4; ++y)
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
vec3 c = sampleTexLod(pc.src, (vec2(x, y) + 0.5) / 4.0, 0.0).rgb;
|
||||
sum += log(max(dot(c, vec3(0.2126, 0.7152, 0.0722)), 1e-4));
|
||||
}
|
||||
float average = exp(sum / 16.0);
|
||||
float target = clamp(pc.key / average, pc.minE, pc.maxE);
|
||||
if (pc.prev == 0u) { o_exposure = vec4(target); return; }
|
||||
float last = sampleTexLod(pc.prev, vec2(0.5), 0.0).r;
|
||||
float rate = target > last ? pc.speed * 0.6 : pc.speed; // brightening (into the dark) is slower
|
||||
o_exposure = vec4(last + (target - last) * (1.0 - exp(-pc.dt * rate)));
|
||||
}
|
||||
61
engine/shaders/airlight.glsl
Normal file
61
engine/shaders/airlight.glsl
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// 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;
|
||||
}
|
||||
|
||||
16
engine/shaders/backend.glsl
Normal file
16
engine/shaders/backend.glsl
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#ifndef KEEL_BACKEND_GLSL
|
||||
#define KEEL_BACKEND_GLSL
|
||||
// What every shader includes: textures and the push block, for the backend being built.
|
||||
// Vulkan: bindless descriptor array + push constants. OpenGL 4.6: ARB_bindless_texture handles
|
||||
// in storage buffers + a uniform block. Shader code only uses the names defined here.
|
||||
#ifdef KEEL_GL
|
||||
#include "backend_gl.glsl"
|
||||
#else
|
||||
#include "backend_vk.glsl"
|
||||
#endif
|
||||
|
||||
float sunLight(vec3 n) {
|
||||
const vec3 sun = normalize(vec3(0.4, 0.3, 0.85));
|
||||
return 0.3 + 0.9 * max(dot(n, sun), 0.0);
|
||||
}
|
||||
#endif
|
||||
26
engine/shaders/backend_gl.glsl
Normal file
26
engine/shaders/backend_gl.glsl
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// OpenGL 4.6 + ARB_bindless_texture (enabled by the loader's preamble): every texture is a
|
||||
// 64-bit handle (sampler state included) in a table, indexed like Vulkan's descriptor array.
|
||||
// Depth textures also get a comparison handle in a second table, for shadows.
|
||||
layout(std430, binding = 9) readonly buffer TextureHandles { uvec2 handles[]; } u_textureHandles;
|
||||
layout(std430, binding = 10) readonly buffer ShadowHandles { uvec2 handles[]; } u_shadowHandles;
|
||||
|
||||
#define PUSH_BLOCK layout(std140, binding = 0) uniform Push
|
||||
#define VERTEX_ID gl_VertexID
|
||||
|
||||
vec4 sampleTex(uint index, vec2 uv) {
|
||||
return texture(sampler2D(u_textureHandles.handles[index]), uv);
|
||||
}
|
||||
|
||||
ivec2 texSize(uint index) { return textureSize(sampler2D(u_textureHandles.handles[index]), 0); }
|
||||
|
||||
vec4 fetchTex(uint index, ivec2 p) {
|
||||
return texelFetch(sampler2D(u_textureHandles.handles[index]), p, 0);
|
||||
}
|
||||
|
||||
vec4 sampleTexLod(uint index, vec2 uv, float lod) {
|
||||
return textureLod(sampler2D(u_textureHandles.handles[index]), uv, lod);
|
||||
}
|
||||
|
||||
float shadowTap(uint index, vec2 uv, vec2 lo, vec2 hi, float ref) {
|
||||
return textureLod(sampler2DShadow(u_shadowHandles.handles[index]), vec3(clamp(uv, lo, hi), ref), 0.0);
|
||||
}
|
||||
35
engine/shaders/backend_vk.glsl
Normal file
35
engine/shaders/backend_vk.glsl
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Vulkan: one global descriptor array of textures, separate samplers, push constants.
|
||||
#extension GL_EXT_nonuniform_qualifier : require
|
||||
#extension GL_EXT_buffer_reference : require
|
||||
layout(set = 0, binding = 0) uniform texture2D u_textures[];
|
||||
layout(set = 0, binding = 1) uniform sampler u_sampler;
|
||||
layout(set = 0, binding = 2) uniform sampler u_shadowSampler; // depth compare, linear: bilinear PCF
|
||||
layout(set = 0, binding = 3) uniform sampler u_clampSampler; // render targets, for the screen-space passes
|
||||
// Post passes (#define KEEL_POST before including): every lookup clamps at the edges.
|
||||
#ifdef KEEL_POST
|
||||
#define KEEL_SAMPLER u_clampSampler
|
||||
#else
|
||||
#define KEEL_SAMPLER u_sampler
|
||||
#endif
|
||||
|
||||
#define PUSH_BLOCK layout(push_constant) uniform Push
|
||||
#define VERTEX_ID gl_VertexIndex
|
||||
|
||||
vec4 sampleTex(uint index, vec2 uv) {
|
||||
return texture(sampler2D(u_textures[nonuniformEXT(index)], KEEL_SAMPLER), uv);
|
||||
}
|
||||
|
||||
ivec2 texSize(uint index) { return textureSize(sampler2D(u_textures[nonuniformEXT(index)], KEEL_SAMPLER), 0); }
|
||||
|
||||
vec4 fetchTex(uint index, ivec2 p) {
|
||||
return texelFetch(sampler2D(u_textures[nonuniformEXT(index)], KEEL_SAMPLER), p, 0);
|
||||
}
|
||||
|
||||
vec4 sampleTexLod(uint index, vec2 uv, float lod) {
|
||||
return textureLod(sampler2D(u_textures[nonuniformEXT(index)], KEEL_SAMPLER), uv, lod);
|
||||
}
|
||||
|
||||
// 1 = lit. Hardware-filtered depth compare at uv (clamped inside [lo, hi]).
|
||||
float shadowTap(uint index, vec2 uv, vec2 lo, vec2 hi, float ref) {
|
||||
return textureLod(sampler2DShadow(u_textures[nonuniformEXT(index)], u_shadowSampler), vec3(clamp(uv, lo, hi), ref), 0.0);
|
||||
}
|
||||
30
engine/shaders/bloom_down.frag
Normal file
30
engine/shaders/bloom_down.frag
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#version 460
|
||||
// Bloom, going down: a 13-tap filter (Jimenez, "Next generation post processing in Call of
|
||||
// Duty: Advanced Warfare") halving the size each step. The first step clamps fireflies.
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
uint src;
|
||||
uint srcW, srcH;
|
||||
uint outW, outH;
|
||||
uint first;
|
||||
} pc;
|
||||
layout(location = 0) out vec4 o_color;
|
||||
|
||||
vec3 tap(vec2 uv) {
|
||||
vec2 h = 0.5 / vec2(pc.srcW, pc.srcH);
|
||||
return sampleTex(pc.src, clamp(uv, h, 1.0 - h)).rgb;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / vec2(pc.outW, pc.outH);
|
||||
vec2 t = 1.0 / vec2(pc.srcW, pc.srcH);
|
||||
vec3 a = tap(uv + t * vec2(-2, -2)), b = tap(uv + t * vec2(0, -2)), c = tap(uv + t * vec2(2, -2));
|
||||
vec3 d = tap(uv + t * vec2(-1, -1)), e = tap(uv + t * vec2(1, -1));
|
||||
vec3 f = tap(uv + t * vec2(-2, 0)), g = tap(uv), hh = tap(uv + t * vec2(2, 0));
|
||||
vec3 i = tap(uv + t * vec2(-1, 1)), j = tap(uv + t * vec2(1, 1));
|
||||
vec3 k = tap(uv + t * vec2(-2, 2)), l = tap(uv + t * vec2(0, 2)), m = tap(uv + t * vec2(2, 2));
|
||||
vec3 sum = (d + e + i + j) * 0.125 + (a + c + k + m) * 0.03125 + (b + f + hh + l) * 0.0625 + g * 0.125;
|
||||
if (pc.first != 0u) sum = min(sum, vec3(32.0)); // no single-pixel sparkle blooming the screen
|
||||
o_color = vec4(sum, 1.0);
|
||||
}
|
||||
25
engine/shaders/bloom_up.frag
Normal file
25
engine/shaders/bloom_up.frag
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#version 460
|
||||
// Bloom, coming up: a 3×3 tent of the smaller level, added onto the larger one (blended).
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
uint src;
|
||||
uint srcW, srcH;
|
||||
uint outW, outH;
|
||||
float radius;
|
||||
} pc;
|
||||
layout(location = 0) out vec4 o_color;
|
||||
|
||||
vec3 tap(vec2 uv) {
|
||||
vec2 h = 0.5 / vec2(pc.srcW, pc.srcH);
|
||||
return sampleTex(pc.src, clamp(uv, h, 1.0 - h)).rgb;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / vec2(pc.outW, pc.outH);
|
||||
vec2 t = pc.radius / vec2(pc.srcW, pc.srcH);
|
||||
vec3 s = tap(uv) * 4.0;
|
||||
s += (tap(uv + vec2(-t.x, 0)) + tap(uv + vec2(t.x, 0)) + tap(uv + vec2(0, -t.y)) + tap(uv + vec2(0, t.y))) * 2.0;
|
||||
s += tap(uv + vec2(-t.x, -t.y)) + tap(uv + vec2(t.x, -t.y)) + tap(uv + vec2(-t.x, t.y)) + tap(uv + vec2(t.x, t.y));
|
||||
o_color = vec4(s / 16.0, 1.0);
|
||||
}
|
||||
20
engine/shaders/box.frag
Normal file
20
engine/shaders/box.frag
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#version 460
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
mat4 viewProj;
|
||||
vec4 center;
|
||||
vec4 extents;
|
||||
vec4 color;
|
||||
} pc;
|
||||
|
||||
layout(location = 0) in vec3 v_normal;
|
||||
layout(location = 1) in vec3 v_world;
|
||||
layout(location = 0) out vec4 o_color;
|
||||
|
||||
void main() {
|
||||
// World-space UVs: textures tile every 128 units regardless of box size.
|
||||
vec3 n = abs(v_normal);
|
||||
vec2 uv = (n.x > 0.5 ? v_world.yz : n.y > 0.5 ? v_world.xz : v_world.xy) / 128.0;
|
||||
vec4 tex = sampleTex(uint(pc.center.w), uv);
|
||||
o_color = vec4(pc.color.rgb * tex.rgb * sunLight(v_normal), 1.0);
|
||||
}
|
||||
26
engine/shaders/box.vert
Normal file
26
engine/shaders/box.vert
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#version 460
|
||||
// Axis-aligned boxes generated from the vertex index: 36 vertices, no vertex buffer.
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
mat4 viewProj;
|
||||
vec4 center; // xyz, w = texture index
|
||||
vec4 extents; // xyz = half size
|
||||
vec4 color;
|
||||
} pc;
|
||||
|
||||
layout(location = 0) out vec3 v_normal;
|
||||
layout(location = 1) out vec3 v_world;
|
||||
|
||||
const vec3 kNormals[6] = vec3[](vec3(1,0,0), vec3(-1,0,0), vec3(0,1,0), vec3(0,-1,0), vec3(0,0,1), vec3(0,0,-1));
|
||||
const vec2 kQuad[6] = vec2[](vec2(-1,-1), vec2(1,-1), vec2(1,1), vec2(-1,-1), vec2(1,1), vec2(-1,1));
|
||||
|
||||
void main() {
|
||||
int face = VERTEX_ID / 6;
|
||||
vec3 n = kNormals[face];
|
||||
vec3 t = abs(n.z) > 0.5 ? vec3(1,0,0) : vec3(0,0,1);
|
||||
vec3 b = cross(n, t);
|
||||
vec2 q = kQuad[VERTEX_ID % 6];
|
||||
v_world = pc.center.xyz + (n + t * q.x + b * q.y) * pc.extents.xyz;
|
||||
v_normal = n;
|
||||
gl_Position = pc.viewProj * vec4(v_world, 1.0);
|
||||
}
|
||||
59
engine/shaders/cull.comp
Normal file
59
engine/shaders/cull.comp
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#version 460
|
||||
// GPU occlusion culling (r_gpuculling): one thread per opaque draw. Its world box, projected the
|
||||
// way the occlusion grid's frame saw the world (last frame's view), is hidden if every grid texel
|
||||
// under it holds something nearer than the box's nearest point. Writes the draw's indirect command
|
||||
// (one instance or none) for the depth pre-pass and the scene pass, and counts the hidden ones.
|
||||
#include "backend.glsl"
|
||||
layout(local_size_x = 64) in;
|
||||
|
||||
struct CullBox { vec4 lo; vec4 hi; uvec4 draw; }; // draw: index count, first index, test (1), -
|
||||
layout(buffer_reference, std430) readonly buffer Boxes { CullBox b[]; };
|
||||
struct Cmd { uint indexCount, instanceCount, firstIndex; int vertexOffset; uint firstInstance; };
|
||||
layout(buffer_reference, std430) writeonly buffer Cmds { Cmd c[]; };
|
||||
layout(buffer_reference, std430) buffer Counter { uint hidden; };
|
||||
|
||||
PUSH_BLOCK {
|
||||
mat4 viewProj; // the frame the grid was made from
|
||||
Boxes boxes;
|
||||
Cmds cmds;
|
||||
Counter counter;
|
||||
uint count;
|
||||
uint hzb; // the pyramid's first level (kHzbLevels slots, each half the last)
|
||||
uint occlusion; // 0: the camera moved too much since (draw everything in view)
|
||||
uint pad;
|
||||
} pc;
|
||||
|
||||
void main() {
|
||||
uint i = gl_GlobalInvocationID.x;
|
||||
if (i >= pc.count) return;
|
||||
CullBox b = pc.boxes.b[i];
|
||||
bool visible = true;
|
||||
if (pc.occlusion != 0u && (b.draw.z & 1u) != 0u) {
|
||||
vec3 mn = vec3(1e9), mx = vec3(-1e9);
|
||||
bool behind = false;
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
vec3 c = vec3((k & 1) != 0 ? b.hi.x : b.lo.x, (k & 2) != 0 ? b.hi.y : b.lo.y, (k & 4) != 0 ? b.hi.z : b.lo.z);
|
||||
vec4 p = pc.viewProj * vec4(c, 1.0);
|
||||
if (p.w <= 0.001) { behind = true; break; } // crosses the eye: can't tell, draw it
|
||||
vec3 n = p.xyz / p.w;
|
||||
mn = min(mn, n), mx = max(mx, n);
|
||||
}
|
||||
if (!behind) {
|
||||
// The level where the box covers a few texels (each holds the farthest depth under
|
||||
// it, so testing there never hides what's visible).
|
||||
vec2 size0 = vec2(texSize(pc.hzb));
|
||||
vec2 extent = (mx.xy - mn.xy) * 0.5 * size0;
|
||||
uint level = uint(clamp(ceil(log2(max(max(extent.x, extent.y), 1.0) / 3.0)), 0.0, 7.0));
|
||||
uint tex = pc.hzb + level;
|
||||
ivec2 size = texSize(tex);
|
||||
ivec2 a = clamp(ivec2(floor((mn.xy * 0.5 + 0.5) * vec2(size))), ivec2(0), size - 1);
|
||||
ivec2 z = clamp(ivec2(floor((mx.xy * 0.5 + 0.5) * vec2(size))), ivec2(0), size - 1);
|
||||
float far = 0.0;
|
||||
for (int y = a.y; y <= z.y; ++y)
|
||||
for (int x = a.x; x <= z.x; ++x) far = max(far, fetchTex(tex, ivec2(x, y)).r);
|
||||
if (mn.z > far + 1e-5) visible = false;
|
||||
}
|
||||
}
|
||||
pc.cmds.c[i] = Cmd(b.draw.x, visible ? 1u : 0u, b.draw.y, 0, 0u);
|
||||
if (!visible) atomicAdd(pc.counter.hidden, 1u);
|
||||
}
|
||||
11
engine/shaders/depth.frag
Normal file
11
engine/shaders/depth.frag
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#version 460
|
||||
// Depth pre-pass (r_prepass): only the depth, with alpha-tested texels cut out like mesh.frag
|
||||
// does, so the scene pass shades each pixel once (what's hidden behind isn't lit for nothing).
|
||||
#include "backend.glsl"
|
||||
#include "model.glsl"
|
||||
|
||||
layout(location = 1) in vec2 v_uv;
|
||||
|
||||
void main() {
|
||||
if (sampleTex(pc.textures & 0xFFFFu, v_uv).a < 0.5) discard;
|
||||
}
|
||||
2
engine/shaders/froxel.comp
Normal file
2
engine/shaders/froxel.comp
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#version 460
|
||||
#include "froxel_main.glsl"
|
||||
30
engine/shaders/froxel.glsl
Normal file
30
engine/shaders/froxel.glsl
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// The fog grid ("froxels", as in Frostbite / id Tech): kFroxels cells over the screen and along
|
||||
// the view, slices spaced exponentially from FRAME.froxel.y to .z. froxel.comp fills it; entry k
|
||||
// of a column holds the light the fog adds (rgb) and how much of what's behind gets through (a)
|
||||
// from the eye to the far side of slice k.
|
||||
const uvec3 kFroxels = uvec3(160, 90, 64);
|
||||
uint froxelIndex(uvec3 c) { return (c.z * kFroxels.y + c.y) * kFroxels.x + c.x; }
|
||||
// Where slice boundary j lies (view depth); slice k spans boundaries k and k + 1.
|
||||
float froxelBoundary(float j) { return FRAME.froxel.y * pow(FRAME.froxel.z / FRAME.froxel.y, j / float(kFroxels.z)); }
|
||||
float froxelSlice(float depth) { return log(max(depth, 1e-3) / FRAME.froxel.y) / log(FRAME.froxel.z / FRAME.froxel.y) * float(kFroxels.z); }
|
||||
|
||||
#ifndef KEEL_FROXEL_WRITER
|
||||
vec4 froxelEntry(ivec2 xy, int z) {
|
||||
uvec2 v = FRAME.froxels.f[froxelIndex(uvec3(clamp(xy, ivec2(0), ivec2(kFroxels.xy) - 1), clamp(z, 0, int(kFroxels.z) - 1)))];
|
||||
return vec4(unpackHalf2x16(v.x), unpackHalf2x16(v.y));
|
||||
}
|
||||
vec4 froxelColumn(ivec2 xy, float c) { // c: entry index, fractional
|
||||
if (c < 0.0) return mix(vec4(0.0, 0.0, 0.0, 1.0), froxelEntry(xy, 0), clamp(c + 1.0, 0.0, 1.0));
|
||||
int z = int(c);
|
||||
return mix(froxelEntry(xy, z), froxelEntry(xy, z + 1), c >= float(kFroxels.z - 1) ? 0.0 : fract(c));
|
||||
}
|
||||
// The fog between the eye and a point at uv on screen, depth along the view: (light, transmittance).
|
||||
vec4 froxelFog(vec2 uv, float depth) {
|
||||
vec2 g = uv * vec2(kFroxels.xy) - 0.5;
|
||||
ivec2 b = ivec2(floor(g));
|
||||
vec2 f = g - vec2(b);
|
||||
// Entry k ends at boundary k + 1; nearer than the first slice's far side fades in from nothing.
|
||||
float c = max(froxelSlice(depth) - 1.0, -1.0);
|
||||
return mix(mix(froxelColumn(b, c), froxelColumn(b + ivec2(1, 0), c), f.x), mix(froxelColumn(b + ivec2(0, 1), c), froxelColumn(b + ivec2(1, 1), c), f.x), f.y);
|
||||
}
|
||||
#endif
|
||||
218
engine/shaders/froxel_main.glsl
Normal file
218
engine/shaders/froxel_main.glsl
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
// The body of froxel.comp and froxel_rt.comp (the same, plus rays for the sky above each cell).
|
||||
// The fog grid (froxel.glsl), in two dispatches:
|
||||
// mode 0, one thread per cell: the fog's density there (height fog, plus the air when
|
||||
// r_volumetric's on) and the light it scatters towards the eye (the sky's, the sun through its
|
||||
// shadow, the lamps of its cluster), at a point jittered along the slice each frame and blended
|
||||
// with last frame's cell at the same place in the world, so it's smooth and doesn't crawl.
|
||||
// mode 1, one thread per column: walked front to back, adding up light and transmittance.
|
||||
#include "backend.glsl"
|
||||
#define KEEL_FRAME_ONLY
|
||||
#include "model.glsl"
|
||||
#include "sky.glsl"
|
||||
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||
layout(buffer_reference, std430) buffer Cells4 { vec4 c[]; }; // per cell: scattered light (per unit), extinction
|
||||
layout(buffer_reference, std430) writeonly buffer Out { uvec2 f[]; };
|
||||
layout(buffer_reference, std430) readonly buffer SmokeCells { uint v[]; }; // two cells a word
|
||||
layout(buffer_reference, std430) writeonly buffer SmokeSky { uvec2 f[]; }; // a column's smoke alone
|
||||
PUSH_BLOCK {
|
||||
FrameData frame;
|
||||
Cells4 cur, prev;
|
||||
Out integrated;
|
||||
vec4 proj; // tan(half fov x), tan(half fov y)
|
||||
vec4 prevPos; // last frame's eye; w 1 = there's a last frame to blend with
|
||||
vec4 prevFwd; // and its view direction; w this frame's jitter along the slice (0..1)
|
||||
uint mode;
|
||||
uint frameIndex;
|
||||
uvec2 tlas; // KEEL_FOG_RAYS: the scene's acceleration structure
|
||||
SmokeCells smoke; // the smoke grid (render/backend.h kSmokeDims): density | shade << 8 a cell
|
||||
SmokeSky smokeSky; // integrate: each column's smoke alone, end to end (smoke_sky.frag lays it over the sky)
|
||||
vec4 smokeOrigin; // its world corner; w a clock (seconds) for the billows, < 0: no smoke
|
||||
} pc;
|
||||
#define FRAME pc.frame
|
||||
#define KEEL_FROXEL_WRITER
|
||||
#include "froxel.glsl"
|
||||
#include "airlight.glsl"
|
||||
|
||||
// ── Smoke (shared/smoke.h): the grid's cells, blended, roughened by drifting billows ──
|
||||
const ivec3 kSmokeDims = ivec3(160, 160, 48);
|
||||
const float kSmokeCell = 16.0;
|
||||
vec2 smokeCell(ivec3 c) {
|
||||
if (any(lessThan(c, ivec3(0))) || any(greaterThanEqual(c, kSmokeDims))) return vec2(0.0);
|
||||
uint i = uint((c.z * kSmokeDims.y + c.y) * kSmokeDims.x + c.x);
|
||||
uint h = (pc.smoke.v[i >> 1] >> ((i & 1u) * 16u)) & 0xFFFFu;
|
||||
return vec2(float(h & 0xFFu), float(h >> 8)) / 255.0;
|
||||
}
|
||||
// x density, y shade (both blended between the 8 cells round w).
|
||||
vec2 smokeAt(vec3 w) {
|
||||
vec3 g = (w - pc.smokeOrigin.xyz) / kSmokeCell - 0.5;
|
||||
ivec3 b = ivec3(floor(g));
|
||||
vec3 f = g - vec3(b);
|
||||
float d = 0.0, s = 0.0;
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
ivec3 o = ivec3(k & 1, (k >> 1) & 1, k >> 2);
|
||||
vec3 tri = mix(1.0 - f, f, vec3(o));
|
||||
vec2 c = smokeCell(b + o);
|
||||
float wgt = tri.x * tri.y * tri.z;
|
||||
d += c.x * wgt;
|
||||
s += c.y * c.x * wgt;
|
||||
}
|
||||
return vec2(d, d > 1e-4 ? s / d : 0.5);
|
||||
}
|
||||
float hash3(vec3 p) {
|
||||
p = fract(p * 0.3183099 + 0.1) * 17.0;
|
||||
return fract(p.x * p.y * p.z * (p.x + p.y + p.z));
|
||||
}
|
||||
float valueNoise(vec3 x) {
|
||||
vec3 i = floor(x), f = fract(x);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(mix(hash3(i), hash3(i + vec3(1, 0, 0)), f.x), mix(hash3(i + vec3(0, 1, 0)), hash3(i + vec3(1, 1, 0)), f.x), f.y),
|
||||
mix(mix(hash3(i + vec3(0, 0, 1)), hash3(i + vec3(1, 0, 1)), f.x), mix(hash3(i + vec3(0, 1, 1)), hash3(i + vec3(1, 1, 1)), f.x), f.y), f.z);
|
||||
}
|
||||
// The density there after the billows: thick inside, torn and drifting at the edges.
|
||||
float smokeDensity(vec3 w, float d) {
|
||||
if (d <= 0.0) return 0.0;
|
||||
float t = pc.smokeOrigin.w;
|
||||
vec3 q = w * 0.03 + vec3(t * 0.04, t * 0.03, -t * 0.12);
|
||||
float n = 0.5 * valueNoise(q) + 0.3 * valueNoise(q * 2.3 + vec3(t * 0.07)) + 0.2 * valueNoise(q * 5.1 - vec3(t * 0.1));
|
||||
// Thin smoke (the rim) is torn most; the thick middle only darkens and lightens a little.
|
||||
return clamp(d * 1.6 - (1.0 - n) * mix(1.1, 0.5, d), 0.0, 1.0) * mix(0.75, 1.15, n);
|
||||
}
|
||||
|
||||
vec3 viewRay(vec3 fwd, vec2 uv) { // not normalized: 1 along fwd
|
||||
vec3 right = normalize(cross(fwd, vec3(0.0, 0.0, 1.0))), up = cross(right, fwd);
|
||||
vec2 s = (uv * 2.0 - 1.0) * pc.proj.xy;
|
||||
return fwd + right * s.x - up * s.y;
|
||||
}
|
||||
|
||||
// Last frame's cell (blended) at a world point; w < 0 when it was off that grid.
|
||||
vec4 history(vec3 w) {
|
||||
vec3 fwd = pc.prevFwd.xyz, right = normalize(cross(fwd, vec3(0.0, 0.0, 1.0))), up = cross(right, fwd);
|
||||
vec3 v = w - pc.prevPos.xyz;
|
||||
float depth = dot(v, fwd);
|
||||
if (depth < FRAME.froxel.y) return vec4(-1.0);
|
||||
vec2 uv = (vec2(dot(v, right), -dot(v, up)) / depth / pc.proj.xy) * 0.5 + 0.5;
|
||||
vec3 g = vec3(uv * vec2(kFroxels.xy), froxelSlice(depth)) - 0.5;
|
||||
if (any(lessThan(g, vec3(0.0))) || any(greaterThan(g, vec3(kFroxels) - 1.0))) return vec4(-1.0);
|
||||
ivec3 b = ivec3(floor(g));
|
||||
vec3 f = g - vec3(b);
|
||||
vec4 r = vec4(0.0);
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
ivec3 o = ivec3(k & 1, (k >> 1) & 1, k >> 2);
|
||||
float wgt = (o.x != 0 ? f.x : 1.0 - f.x) * (o.y != 0 ? f.y : 1.0 - f.y) * (o.z != 0 ? f.z : 1.0 - f.z);
|
||||
r += pc.prev.c[froxelIndex(uvec3(min(b + o, ivec3(kFroxels) - 1)))] * wgt;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
void inject(uvec3 c) {
|
||||
vec2 uv = (vec2(c.xy) + 0.5) / vec2(kFroxels.xy);
|
||||
vec3 ray = viewRay(FRAME.camFwd.xyz, uv), rd = normalize(ray);
|
||||
vec3 w = FRAME.camPos.xyz + ray * froxelBoundary(float(c.z) + pc.prevFwd.w);
|
||||
float fog = 0.0;
|
||||
if (FRAME.sky.x > 0.5 && FRAME.fog.x > 0.0) fog = FRAME.fog.x * exp(-FRAME.fog.y * (w.z - FRAME.fog.w));
|
||||
float air = FRAME.volumetric.x > 0.0 ? FRAME.volumetric.z : 0.0;
|
||||
vec2 smoke = pc.smokeOrigin.w >= 0.0 ? smokeAt(w) : vec2(0.0);
|
||||
float smokeExt = smokeDensity(w, smoke.x) * 0.035; // (thick smoke: ~30 units hide what's behind)
|
||||
vec3 light = vec3(0.0);
|
||||
if (fog + air + smokeExt > 0.0) {
|
||||
const bool sunOn = FRAME.sunColor.w > 0.5;
|
||||
float lit = sunOn ? sunlitAt(w) : 0.0;
|
||||
float mu = dot(rd, FRAME.sunDir.xyz);
|
||||
vec3 lamps = lightsAt(w, rd, uv);
|
||||
// The fog: lit by the sky (blurred, as the old height fog was) and, where the sun reaches
|
||||
// it, glowing towards the sun; the lamps light it too.
|
||||
vec3 sky = FRAME.sky.x > 0.5 ? skyRadiance(uint(FRAME.sky.x), FRAME.sky.y, FRAME.sky.w, normalize(vec3(rd.xy, max(rd.z, 0.05))), FRAME.sky.z) * 0.9 : vec3(0.0);
|
||||
vec3 smokeSky = sky; // (the smoke's own: steady, no per-frame noise to average away)
|
||||
#ifdef KEEL_FOG_RAYS
|
||||
// How much sky this point sees: one ray a frame into the sky above it, in a different
|
||||
// direction each frame (the blend with last frame's cells averages them). Under a roof
|
||||
// the fog gets next to no sky light, so rooms stay clear instead of filling with grey.
|
||||
uint h = (c.x * 73856093u) ^ (c.y * 19349663u) ^ (c.z * 83492791u) ^ (pc.frameIndex * 2654435761u);
|
||||
h = h * 747796405u + 2891336453u;
|
||||
float r1 = float(h & 0xFFFFu) / 65535.0, r2 = float(h >> 16) / 65535.0;
|
||||
vec3 up = normalize(vec3(cos(r1 * 6.2831853), sin(r1 * 6.2831853), 0.0) * sqrt(r2) * 1.2 + vec3(0.0, 0.0, 1.0));
|
||||
rayQueryEXT q;
|
||||
rayQueryInitializeEXT(q, accelerationStructureEXT(pc.tlas), gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT, 0xFF, w, 1.0, up, 6000.0);
|
||||
while (rayQueryProceedEXT(q)) {}
|
||||
float open = rayQueryGetIntersectionTypeEXT(q, true) == gl_RayQueryCommittedIntersectionNoneEXT ? 1.0 : 0.0;
|
||||
sky *= mix(0.06, 1.0, open);
|
||||
if (smokeExt > 0.0) { // under a roof or not: one ray straight up
|
||||
rayQueryEXT q2;
|
||||
rayQueryInitializeEXT(q2, accelerationStructureEXT(pc.tlas), gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT, 0xFF, w, 1.0, vec3(0.0, 0.0, 1.0), 6000.0);
|
||||
while (rayQueryProceedEXT(q2)) {}
|
||||
smokeSky *= rayQueryGetIntersectionTypeEXT(q2, true) == gl_RayQueryCommittedIntersectionNoneEXT ? 1.0 : 0.3;
|
||||
}
|
||||
#else
|
||||
// Where the sun can't reach (under a roof, mostly) the sky's light is dimmer too.
|
||||
if (sunOn) sky *= mix(0.3, 1.0, lit);
|
||||
smokeSky = sky;
|
||||
#endif
|
||||
vec3 fogLight = sky + (sunOn ? FRAME.sunColor.rgb * lit * pow(max(mu, 0.0), 8.0) * 0.25 : vec3(0.0)) + lamps * 12.0;
|
||||
// The air (r_volumetric): shafts of sun and haloes round lamps; it adds light only.
|
||||
vec3 airLight = (sunOn && FRAME.volumetric.w > 0.5 ? FRAME.sunColor.rgb * lit * phaseHG(mu, 0.6) : vec3(0.0)) + lamps * 7.0;
|
||||
light = fog * fogLight + air * airLight * FRAME.volumetric.x;
|
||||
if (smokeExt > 0.0) {
|
||||
// Smoke: the sky's light all round, the sun through its shadows and through the smoke
|
||||
// between it and the sun (a few steps that way), the lamps; tinted by its shade.
|
||||
float toward = sunOn ? smokeAt(w + FRAME.sunDir.xyz * 24.0).x + smokeAt(w + FRAME.sunDir.xyz * 64.0).x + smokeAt(w + FRAME.sunDir.xyz * 128.0).x : 0.0;
|
||||
vec3 sunIn = sunOn ? FRAME.sunColor.rgb * lit * exp(-toward * 1.4) * 0.5 : vec3(0.0);
|
||||
// The smoke above shades what's under it (the sky's light comes mostly from above).
|
||||
float above = smokeAt(w + vec3(0.0, 0.0, 32.0)).x + smokeAt(w + vec3(0.0, 0.0, 80.0)).x;
|
||||
light += smokeExt * smoke.y * (smokeSky * mix(1.0, 0.35, clamp(above * 0.5, 0.0, 1.0)) + sunIn + lamps * 4.0);
|
||||
}
|
||||
}
|
||||
vec4 now = vec4(light, fog + smokeExt);
|
||||
uint i = froxelIndex(c);
|
||||
if (pc.prevPos.w > 0.5) {
|
||||
vec4 h = history(w);
|
||||
if (h.w >= 0.0) now = mix(h, now, 0.12);
|
||||
}
|
||||
pc.cur.c[i] = now;
|
||||
}
|
||||
|
||||
void integrate(uvec2 xy) {
|
||||
vec2 uv = (vec2(xy) + 0.5) / vec2(kFroxels.xy);
|
||||
float rayScale = length(viewRay(FRAME.camFwd.xyz, uv)); // ray length per unit of depth
|
||||
vec3 ray = viewRay(FRAME.camFwd.xyz, uv);
|
||||
vec3 light = vec3(0.0);
|
||||
// The fog never hides more than its max (FRAME.fog.z; 0 or 1: no cap): once the fog alone has
|
||||
// hidden that much, the cells further on keep only their smoke (and its share of the light).
|
||||
float through = 1.0, fogThrough = 1.0, keep = 1.0 - FRAME.fog.z;
|
||||
vec3 smokeLight = vec3(0.0);
|
||||
float smokeThrough = 1.0;
|
||||
bool capped = FRAME.fog.z > 0.0 && FRAME.fog.z < 1.0;
|
||||
float near = 0.0;
|
||||
for (uint z = 0u; z < kFroxels.z; ++z) {
|
||||
float far = froxelBoundary(float(z + 1u)), len = (far - near) * rayScale;
|
||||
vec3 w = FRAME.camPos.xyz + ray * (near + far) * 0.5;
|
||||
near = far;
|
||||
vec4 cell = pc.cur.c[froxelIndex(uvec3(xy, z))];
|
||||
// The smoke here, sampled again (exact: it doesn't move with the slices' jitter as the fog
|
||||
// would); the rest of the cell's extinction is the fog.
|
||||
float smokeHere = pc.smokeOrigin.w >= 0.0 ? min(smokeDensity(w, smokeAt(w).x) * 0.035, cell.w) : 0.0;
|
||||
float fog = max(cell.w - smokeHere, 0.0);
|
||||
if (capped && fogThrough <= keep && cell.w > 1e-9) {
|
||||
cell.rgb *= (cell.w - fog) / cell.w;
|
||||
cell.w -= fog;
|
||||
fog = 0.0;
|
||||
}
|
||||
fogThrough *= exp(-fog * len);
|
||||
if (smokeHere > 1e-9) { // the smoke's share of the cell
|
||||
float se = smokeHere, st = exp(-se * len);
|
||||
smokeLight += smokeThrough * cell.rgb * (se / cell.w) * (1.0 - st) / se;
|
||||
smokeThrough *= st;
|
||||
}
|
||||
float t = exp(-cell.w * len);
|
||||
light += through * (cell.w > 1e-9 ? cell.rgb * (1.0 - t) / cell.w : cell.rgb * len);
|
||||
through *= t;
|
||||
pc.integrated.f[froxelIndex(uvec3(xy, z))] = uvec2(packHalf2x16(light.rg), packHalf2x16(vec2(light.b, through)));
|
||||
}
|
||||
if (pc.smokeOrigin.w >= 0.0) pc.smokeSky.f[xy.y * kFroxels.x + xy.x] = uvec2(packHalf2x16(smokeLight.rg), packHalf2x16(vec2(smokeLight.b, smokeThrough)));
|
||||
}
|
||||
|
||||
void main() {
|
||||
uvec3 id = gl_GlobalInvocationID;
|
||||
if (any(greaterThanEqual(id.xy, kFroxels.xy))) return;
|
||||
if (pc.mode == 0u) inject(id);
|
||||
else integrate(id.xy);
|
||||
}
|
||||
5
engine/shaders/froxel_rt.comp
Normal file
5
engine/shaders/froxel_rt.comp
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#version 460
|
||||
// The fog grid with ray queries: each cell sees whether it has sky above it (froxel_main.glsl).
|
||||
#extension GL_EXT_ray_query : require
|
||||
#define KEEL_FOG_RAYS
|
||||
#include "froxel_main.glsl"
|
||||
11
engine/shaders/fullscreen.vert
Normal file
11
engine/shaders/fullscreen.vert
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#version 460
|
||||
#include "backend.glsl"
|
||||
layout(location = 0) out vec2 v_uv;
|
||||
void main() {
|
||||
v_uv = vec2((VERTEX_ID << 1) & 2, VERTEX_ID & 2);
|
||||
gl_Position = vec4(v_uv * 2.0 - 1.0, 0.0, 1.0);
|
||||
#ifdef KEEL_GL
|
||||
// The window shows row 0 at the bottom in OpenGL; our images keep the view's top in row 0 (as in Vulkan).
|
||||
v_uv.y = 1.0 - v_uv.y;
|
||||
#endif
|
||||
}
|
||||
143
engine/shaders/gi.comp
Normal file
143
engine/shaders/gi.comp
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
#version 460
|
||||
// Real-time GI (gi.glsl): one workgroup per probe, one hardware ray per thread (ray queries
|
||||
// against this frame's scene). Where a ray lands: that surface's colour, lit by the dynamic
|
||||
// lights that reach it (a shadow ray each) and by last frame's probes there (so light bounces
|
||||
// more than once); behind a surface (the probe's stuck in a wall) it counts against the probe.
|
||||
// Then the probe's harmonics and distances, blended with what it had.
|
||||
#extension GL_EXT_ray_query : require
|
||||
#include "backend.glsl"
|
||||
#define KEEL_FRAME_ONLY
|
||||
#include "model.glsl"
|
||||
layout(local_size_x = 32) in;
|
||||
layout(buffer_reference, std430) readonly buffer GiVertices { Vertex v[]; };
|
||||
layout(buffer_reference, std430) readonly buffer GiIndices { uint i[]; };
|
||||
struct RayInst { GiVertices vertices; GiIndices indices; uint firstIndex, textures, tint, pad; };
|
||||
layout(buffer_reference, std430) readonly buffer RayInsts { RayInst r[]; };
|
||||
layout(buffer_reference, std430) writeonly buffer GiProbesOut { GiProbe p[]; };
|
||||
PUSH_BLOCK {
|
||||
FrameData frame;
|
||||
GiProbesOut cur;
|
||||
GiProbes prev;
|
||||
RayInsts insts;
|
||||
uvec2 tlas; // the scene's acceleration structure
|
||||
uint fresh; // 1: start over (nothing to blend with)
|
||||
float hysteresis; // how much of the old light stays each frame
|
||||
vec4 rotation; // this frame's turn of the ray pattern (quaternion)
|
||||
} pc;
|
||||
#define FRAME pc.frame
|
||||
#define GI_PROBES pc.prev
|
||||
#include "gi.glsl"
|
||||
|
||||
const uint kRays = 32u;
|
||||
shared vec3 s_light[kRays], s_dir[kRays];
|
||||
shared float s_dist[kRays];
|
||||
shared uint s_back[kRays];
|
||||
|
||||
vec3 rotate(vec4 q, vec3 v) { return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v); }
|
||||
vec3 fibonacci(uint i, uint n) {
|
||||
float z = 1.0 - (2.0 * float(i) + 1.0) / float(n), r = sqrt(max(1.0 - z * z, 0.0));
|
||||
float phi = float(i) * 2.39996323;
|
||||
return vec3(cos(phi) * r, sin(phi) * r, z);
|
||||
}
|
||||
bool blocked(vec3 from, vec3 dir, float len) {
|
||||
rayQueryEXT q;
|
||||
rayQueryInitializeEXT(q, accelerationStructureEXT(pc.tlas), gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT, 0xFF, from, 0.01, dir, len);
|
||||
while (rayQueryProceedEXT(q)) {}
|
||||
return rayQueryGetIntersectionTypeEXT(q, true) != gl_RayQueryCommittedIntersectionNoneEXT;
|
||||
}
|
||||
|
||||
void main() {
|
||||
uint probe = gl_WorkGroupID.x, t = gl_LocalInvocationID.x;
|
||||
ivec3 slot = ivec3(probe % uint(kGiDims.x), (probe / uint(kGiDims.x)) % uint(kGiDims.y), probe / uint(kGiDims.x * kGiDims.y));
|
||||
ivec3 origin = ivec3(FRAME.giOrigin.xyz);
|
||||
ivec3 cell = origin + giWrap(slot - origin); // the world cell this slot holds now
|
||||
vec3 pos = (vec3(cell) + 0.5) * FRAME.gi.y; // between floors and walls (on the grid lines maps are built on)
|
||||
GiProbe old = pc.prev.p[probe];
|
||||
bool fresh = pc.fresh != 0u || old.tag.w < 0.5 || ivec3(old.tag.xyz) != cell;
|
||||
|
||||
vec3 dir = normalize(rotate(pc.rotation, fibonacci(t, kRays)));
|
||||
const float kFar = FRAME.gi.y * 8.0;
|
||||
rayQueryEXT q;
|
||||
rayQueryInitializeEXT(q, accelerationStructureEXT(pc.tlas), gl_RayFlagsOpaqueEXT, 0xFF, pos, 0.0, dir, kFar);
|
||||
while (rayQueryProceedEXT(q)) {}
|
||||
vec3 light = vec3(0.0);
|
||||
float dist = kFar;
|
||||
uint back = 0u;
|
||||
if (rayQueryGetIntersectionTypeEXT(q, true) == gl_RayQueryCommittedIntersectionTriangleEXT) {
|
||||
dist = rayQueryGetIntersectionTEXT(q, true);
|
||||
RayInst ri = pc.insts.r[rayQueryGetIntersectionInstanceCustomIndexEXT(q, true)];
|
||||
uint first = ri.firstIndex + uint(rayQueryGetIntersectionPrimitiveIndexEXT(q, true)) * 3u;
|
||||
Vertex a = ri.vertices.v[ri.indices.i[first]], b = ri.vertices.v[ri.indices.i[first + 1u]], c = ri.vertices.v[ri.indices.i[first + 2u]];
|
||||
vec2 bc = rayQueryGetIntersectionBarycentricsEXT(q, true);
|
||||
vec3 w3 = vec3(1.0 - bc.x - bc.y, bc.x, bc.y);
|
||||
mat4x3 toWorld = rayQueryGetIntersectionObjectToWorldEXT(q, true);
|
||||
vec3 n = normalize(mat3(toWorld) * (vec3(a.nx, a.ny, a.nz) * w3.x + vec3(b.nx, b.ny, b.nz) * w3.y + vec3(c.nx, c.ny, c.nz) * w3.z));
|
||||
if (dot(n, dir) > 0.0) { // its back: inside something (a wall's far side, a closed box)
|
||||
back = 1u;
|
||||
dist *= 0.2;
|
||||
} else {
|
||||
vec2 uv = vec2(a.u, a.v) * w3.x + vec2(b.u, b.v) * w3.y + vec2(c.u, c.v) * w3.z;
|
||||
vec3 albedo = sampleTexLod(ri.textures & 0xFFFFu, uv, 4.0).rgb * unpackUnorm4x8(ri.tint).rgb;
|
||||
vec3 hit = pos + dir * dist + n * 0.5;
|
||||
vec3 direct = vec3(0.0);
|
||||
uint count = min(uint(FRAME.camPos.w), 32u);
|
||||
for (uint k = 0u; k < count; ++k) {
|
||||
Light l = LIGHT(k);
|
||||
vec3 toLight = l.posRadius.xyz - hit;
|
||||
float d = length(toLight);
|
||||
if (d >= l.posRadius.w) continue;
|
||||
vec3 L = toLight / max(d, 0.001);
|
||||
float ndl = dot(n, L);
|
||||
if (ndl <= 0.0) continue;
|
||||
float fall = 1.0 - d / l.posRadius.w, atten = fall * fall;
|
||||
if (l.spot.w > -1.5) atten *= spotShape(l, dot(-L, l.spot.xyz));
|
||||
// (Stopping short of the light: lamps sit inside their fittings.)
|
||||
if (atten <= 1e-3 || blocked(hit, L, max(d - 12.0, 0.0))) continue;
|
||||
direct += l.color.rgb * atten * ndl;
|
||||
}
|
||||
// More bounces: what last frame's probes say reaches that spot.
|
||||
direct += giIrradiance(hit, n, -dir);
|
||||
light = albedo * direct;
|
||||
}
|
||||
}
|
||||
s_light[t] = light;
|
||||
s_dir[t] = dir;
|
||||
s_dist[t] = dist;
|
||||
s_back[t] = back;
|
||||
barrier();
|
||||
if (t != 0u) return;
|
||||
|
||||
GiProbe p;
|
||||
for (int i = 0; i < 4; ++i) p.sh[i] = vec4(0.0);
|
||||
vec4 mean0 = vec4(0.0), mean1 = vec4(0.0), w0 = vec4(0.0), w1 = vec4(0.0); // +x -x +y -y, +z -z (and their squares)
|
||||
vec4 sq0 = vec4(0.0), sq1 = vec4(0.0);
|
||||
uint backs = 0u;
|
||||
const float weight = 12.566371 / float(kRays);
|
||||
for (uint i = 0u; i < kRays; ++i) {
|
||||
vec3 d = s_dir[i], L = s_light[i] * weight;
|
||||
p.sh[0].rgb += L * 0.282095;
|
||||
p.sh[1].rgb += L * 0.488603 * d.y;
|
||||
p.sh[2].rgb += L * 0.488603 * d.z;
|
||||
p.sh[3].rgb += L * 0.488603 * d.x;
|
||||
vec4 a = vec4(max(d.x, 0.0), max(-d.x, 0.0), max(d.y, 0.0), max(-d.y, 0.0)), b = vec4(max(d.z, 0.0), max(-d.z, 0.0), 0.0, 0.0);
|
||||
a = pow(a, vec4(4.0)), b = pow(b, vec4(4.0));
|
||||
float r = s_dist[i];
|
||||
mean0 += a * r, sq0 += a * r * r, w0 += a;
|
||||
mean1 += b * r, sq1 += b * r * r, w1 += b;
|
||||
backs += s_back[i];
|
||||
}
|
||||
mean0 /= max(w0, vec4(1e-4)), sq0 /= max(w0, vec4(1e-4));
|
||||
mean1 /= max(w1, vec4(1e-4)), sq1 /= max(w1, vec4(1e-4));
|
||||
p.dist[0] = mean0; // +x -x +y -y
|
||||
p.dist[1] = vec4(mean1.xy, sq0.xy); // +z -z, then squares +x -x
|
||||
p.dist[2] = vec4(sq0.zw, sq1.xy); // squares +y -y +z -z
|
||||
// Stuck in a wall (many rays see backs): don't trust it.
|
||||
p.sh[0].w = float(backs) > float(kRays) * 0.25 ? 0.0 : 1.0;
|
||||
p.tag = vec4(vec3(cell), 1.0);
|
||||
if (!fresh) {
|
||||
float h = pc.hysteresis;
|
||||
for (int i = 0; i < 4; ++i) p.sh[i].rgb = mix(p.sh[i].rgb, old.sh[i].rgb, h);
|
||||
for (int i = 0; i < 3; ++i) p.dist[i] = mix(p.dist[i], old.dist[i], 0.97); // shapes change slowly
|
||||
}
|
||||
pc.cur.p[probe] = p;
|
||||
}
|
||||
56
engine/shaders/gi.glsl
Normal file
56
engine/shaders/gi.glsl
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Real-time GI probes (gi.comp fills them, with ray queries): a grid of kGiDims probes,
|
||||
// FRAME.gi.y apart, that follows the eye; a probe's slot is its world cell wrapped into the grid,
|
||||
// so moving keeps what's already there. They hold the light that dynamic lights (flashlight,
|
||||
// fire, explosions, lamps) bounce off what's around; the lightmaps already bounce the rest.
|
||||
// GI_PROBES: the probes to read (#define before including).
|
||||
const ivec3 kGiDims = ivec3(24, 24, 12);
|
||||
// a mod kGiDims, also below zero (GLSL's % isn't defined there)
|
||||
ivec3 giWrap(ivec3 a) { return a - kGiDims * ivec3(floor(vec3(a) / vec3(kGiDims))); }
|
||||
uint giSlot(ivec3 cell) {
|
||||
ivec3 s = giWrap(cell);
|
||||
return uint((s.z * kGiDims.y + s.y) * kGiDims.x + s.x);
|
||||
}
|
||||
// Irradiance / π from L1 spherical harmonics of radiance, facing n.
|
||||
vec3 giShIrradiance(GiProbe p, vec3 n) {
|
||||
return max(0.282095 * p.sh[0].rgb + 0.325735 * (p.sh[1].rgb * n.y + p.sh[2].rgb * n.z + p.sh[3].rgb * n.x), vec3(0.0));
|
||||
}
|
||||
// The light bounced onto a surface at world (normal n, towards the eye v): the 8 probes round it,
|
||||
// weighted by nearness, whether they're in front of the surface, and whether they can see it
|
||||
// (what's between, by the distances each one keeps: Chebyshev, as in DDGI).
|
||||
vec3 giIrradiance(vec3 world, vec3 n, vec3 v) {
|
||||
float s = FRAME.gi.y;
|
||||
ivec3 origin = ivec3(FRAME.giOrigin.xyz);
|
||||
vec3 p = world + (n * 0.2 + v * 0.1) * s;
|
||||
vec3 g = p / s - 0.5; // probe c sits at (c + 0.5) * s
|
||||
ivec3 base = ivec3(floor(g));
|
||||
vec3 f = g - vec3(base);
|
||||
vec3 sum = vec3(0.0);
|
||||
float total = 0.0;
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
ivec3 o = ivec3(k & 1, (k >> 1) & 1, k >> 2), c = base + o;
|
||||
if (any(lessThan(c, origin)) || any(greaterThanEqual(c, origin + kGiDims))) continue;
|
||||
GiProbe pr = GI_PROBES.p[giSlot(c)];
|
||||
if (pr.tag.w < 0.5 || ivec3(pr.tag.xyz) != c) continue;
|
||||
vec3 tri = mix(1.0 - f, f, vec3(o));
|
||||
float w = tri.x * tri.y * tri.z * pr.sh[0].w;
|
||||
vec3 at = (vec3(c) + 0.5) * s;
|
||||
vec3 toProbe = normalize(at - world);
|
||||
float wrap = (dot(toProbe, n) + 1.0) * 0.5;
|
||||
w *= wrap * wrap + 0.2;
|
||||
vec3 d = p - at;
|
||||
float r = length(d);
|
||||
d /= max(r, 1e-4);
|
||||
vec3 d2 = d * d;
|
||||
vec2 m = vec2(d.x > 0.0 ? pr.dist[0].x : pr.dist[0].y, d.x > 0.0 ? pr.dist[1].z : pr.dist[1].w) * d2.x
|
||||
+ vec2(d.y > 0.0 ? pr.dist[0].z : pr.dist[0].w, d.y > 0.0 ? pr.dist[2].x : pr.dist[2].y) * d2.y
|
||||
+ vec2(d.z > 0.0 ? pr.dist[1].x : pr.dist[1].y, d.z > 0.0 ? pr.dist[2].z : pr.dist[2].w) * d2.z;
|
||||
if (r > m.x) {
|
||||
float variance = abs(m.y - m.x * m.x), e = r - m.x;
|
||||
float cheb = variance / (variance + e * e);
|
||||
w *= max(cheb * cheb * cheb, 0.02);
|
||||
}
|
||||
sum += giShIrradiance(pr, n) * w;
|
||||
total += w;
|
||||
}
|
||||
return total > 1e-4 ? sum / total : vec3(0.0);
|
||||
}
|
||||
24
engine/shaders/hzb.frag
Normal file
24
engine/shaders/hzb.frag
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#version 460
|
||||
// The hierarchical Z-buffer's base: the scene's depth shrunk to a small grid, each texel the
|
||||
// farthest depth under it. The CPU reads it back and builds the rest of the pyramid (see
|
||||
// Renderer::occluded). Rows match the depth buffer's on both backends.
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
uint depth; // texture slot of the scene's depth
|
||||
uint srcW, srcH; // its size
|
||||
uint dstW, dstH; // this grid's
|
||||
} pc;
|
||||
|
||||
layout(location = 0) out vec4 o_depth;
|
||||
|
||||
void main() {
|
||||
ivec2 o = ivec2(gl_FragCoord.xy);
|
||||
vec2 scale = vec2(pc.srcW, pc.srcH) / vec2(pc.dstW, pc.dstH);
|
||||
ivec2 lo = ivec2(floor(vec2(o) * scale));
|
||||
ivec2 hi = min(ivec2(ceil(vec2(o + 1) * scale)), ivec2(pc.srcW, pc.srcH));
|
||||
float far = 0.0;
|
||||
for (int y = lo.y; y < hi.y; ++y)
|
||||
for (int x = lo.x; x < hi.x; ++x) far = max(far, fetchTex(pc.depth, ivec2(x, y)).r);
|
||||
o_depth = vec4(far);
|
||||
}
|
||||
99
engine/shaders/lights.glsl
Normal file
99
engine/shaders/lights.glsl
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// 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);
|
||||
}
|
||||
376
engine/shaders/mesh.frag
Normal file
376
engine/shaders/mesh.frag
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
#version 460
|
||||
#include "backend.glsl"
|
||||
#include "model.glsl"
|
||||
#include "lights.glsl"
|
||||
#include "sky.glsl"
|
||||
|
||||
layout(location = 0) in vec3 v_normal;
|
||||
layout(location = 1) in vec2 v_uv;
|
||||
layout(location = 2) in vec2 v_lightmapUv;
|
||||
layout(location = 3) in vec3 v_world;
|
||||
layout(location = 0) out vec4 o_color;
|
||||
layout(location = 1) out vec4 o_gbuf; // view normal (octahedral), roughness, reflection weight: for ssr.frag
|
||||
|
||||
vec2 octEncode(vec3 n) {
|
||||
n /= abs(n.x) + abs(n.y) + abs(n.z);
|
||||
if (n.z < 0.0) n.xy = (1.0 - abs(n.yx)) * vec2(n.x >= 0.0 ? 1.0 : -1.0, n.y >= 0.0 ? 1.0 : -1.0);
|
||||
return n.xy * 0.5 + 0.5;
|
||||
}
|
||||
|
||||
// 1 = sunlit. Cascade by view distance, normal offset by the cascade's texel size,
|
||||
// 3×3 bilinear PCF taps (a smooth 4×4 filter).
|
||||
float sunShadow(vec3 n) {
|
||||
if (FRAME.sunColor.w < 0.5) return 1.0;
|
||||
float depth = dot(v_world - 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;
|
||||
float texel = FRAME.cascadeTexel[c];
|
||||
vec4 p = FRAME.cascades[c] * vec4(v_world + n * texel * 1.5, 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;
|
||||
float atlasTexel = FRAME.shadow.x;
|
||||
vec2 base = vec2(c & 1, c >> 1) * 0.5;
|
||||
vec2 lo = base + atlasTexel, hi = base + 0.5 - atlasTexel;
|
||||
vec2 at = base + uv * 0.5;
|
||||
uint map = uint(FRAME.sunDir.w);
|
||||
float lit = 0.0;
|
||||
float soft = FRAME.shadow.w; // the sun's apparent size (r_shadowsoft); 0 = the plain 3x3 filter
|
||||
if (soft <= 0.0) {
|
||||
for (int y = -1; y <= 1; ++y)
|
||||
for (int x = -1; x <= 1; ++x) lit += shadowTap(map, at + vec2(x, y) * atlasTexel, lo, hi, p.z - 0.0002);
|
||||
lit /= 9.0;
|
||||
} else {
|
||||
// PCSS: how far above this point the things shading it are (blocker search), then a
|
||||
// filter that widens with that gap, so shadows are sharp where they touch and soft away.
|
||||
const vec2 disk[12] = vec2[](vec2(-0.326, -0.406), vec2(-0.840, -0.074), vec2(-0.696, 0.457), vec2(-0.203, 0.621),
|
||||
vec2(0.962, -0.195), vec2(0.473, -0.480), vec2(0.519, 0.767), vec2(0.185, -0.893),
|
||||
vec2(0.507, 0.064), vec2(0.896, 0.412), vec2(-0.322, -0.933), vec2(-0.792, -0.598));
|
||||
float spin = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))) * 6.2831853;
|
||||
mat2 rot = mat2(cos(spin), sin(spin), -sin(spin), cos(spin));
|
||||
float blockers = 0.0, found = 0.0;
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
float d = sampleTexLod(map, clamp(at + rot * disk[i] * atlasTexel * 6.0, lo, hi), 0.0).r;
|
||||
if (d < p.z - 0.0002) blockers += d, found += 1.0;
|
||||
}
|
||||
if (found == 0.0) return 1.0;
|
||||
float gap = (p.z - blockers / found) * 16000.0; // world units (the cascades' depth range)
|
||||
float width = clamp(gap * 0.012 * soft / texel, 1.0, 8.0); // in shadow texels
|
||||
for (int i = 0; i < 12; ++i) lit += shadowTap(map, at + rot * disk[i] * atlasTexel * width, lo, hi, p.z - 0.0002);
|
||||
lit /= 12.0;
|
||||
}
|
||||
// Fade out over the last 10% of the shadow distance instead of a hard edge.
|
||||
float fade = clamp((FRAME.splits.w - depth) / (FRAME.splits.w * 0.1), 0.0, 1.0);
|
||||
return mix(1.0, lit, fade);
|
||||
}
|
||||
|
||||
// Smooth value noise, for ripples and the ragged edges of puddles.
|
||||
float hash3(vec3 p) { return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453); }
|
||||
float vnoise(vec3 p) {
|
||||
vec3 i = floor(p), f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(mix(hash3(i), hash3(i + vec3(1, 0, 0)), f.x), mix(hash3(i + vec3(0, 1, 0)), hash3(i + vec3(1, 1, 0)), f.x), f.y),
|
||||
mix(mix(hash3(i + vec3(0, 0, 1)), hash3(i + vec3(1, 0, 1)), f.x), mix(hash3(i + vec3(0, 1, 1)), hash3(i + vec3(1, 1, 1)), f.x), f.y), f.z);
|
||||
}
|
||||
|
||||
// Tangent frame from screen-space derivatives (no stored tangents needed): T along +u, B along +v.
|
||||
mat3 cotangentFrame(vec3 n, vec3 p, vec2 uv) {
|
||||
vec3 dp1 = dFdx(p), dp2 = dFdy(p);
|
||||
vec2 duv1 = dFdx(uv), duv2 = dFdy(uv);
|
||||
vec3 dp2perp = cross(dp2, n), dp1perp = cross(n, dp1);
|
||||
vec3 t = dp2perp * duv1.x + dp1perp * duv2.x;
|
||||
vec3 b = dp2perp * duv1.y + dp1perp * duv2.y;
|
||||
float invmax = inversesqrt(max(max(dot(t, t), dot(b, b)), 1e-20));
|
||||
return mat3(t * invmax, b * invmax, n);
|
||||
}
|
||||
|
||||
// Image-based light: what the split-sum integral of GGX × Fresnel comes to (F0 scale, bias),
|
||||
// fitted analytically (Karis) instead of a lookup texture.
|
||||
vec2 envBRDF(float rough, float ndv) {
|
||||
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022), c1 = vec4(1.0, 0.0425, 1.04, -0.04);
|
||||
vec4 r = rough * c0 + c1;
|
||||
float a004 = min(r.x * r.x, exp2(-9.28 * ndv)) * r.x + r.y;
|
||||
return vec2(-1.04, 1.04) * a004 + r.zw;
|
||||
}
|
||||
|
||||
bool hasSky() { return FRAME.sky.x > 0.5; }
|
||||
vec3 sky(vec3 dir, float level) { return skyRadiance(uint(FRAME.sky.x), FRAME.sky.y, FRAME.sky.w, dir, level); }
|
||||
// The sky's light on a surface facing n (irradiance / π), from its spherical harmonics.
|
||||
vec3 skyLight(vec3 n) {
|
||||
return FRAME.skySH[0].rgb * 0.282095 + FRAME.skySH[1].rgb * 0.488603 * n.y + FRAME.skySH[2].rgb * 0.488603 * n.z
|
||||
+ FRAME.skySH[3].rgb * 0.488603 * n.x + FRAME.skySH[4].rgb * 1.092548 * n.x * n.y + FRAME.skySH[5].rgb * 1.092548 * n.y * n.z
|
||||
+ FRAME.skySH[6].rgb * 0.315392 * (3.0 * n.z * n.z - 1.0) + FRAME.skySH[7].rgb * 1.092548 * n.x * n.z
|
||||
+ FRAME.skySH[8].rgb * 0.546274 * (n.x * n.x - n.y * n.y);
|
||||
}
|
||||
float luma(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
|
||||
|
||||
// Height fog / aerial perspective: denser low down, coloured by the sky behind (blurred) and
|
||||
// brighter towards the sun. Returns the colour seen through `dist` of it.
|
||||
#ifndef KEEL_GL
|
||||
#include "froxel.glsl"
|
||||
#define GI_PROBES FRAME.giProbes
|
||||
#include "gi.glsl"
|
||||
#endif
|
||||
vec3 applyFog(vec3 color, vec3 world) {
|
||||
vec3 ray = world - FRAME.camPos.xyz;
|
||||
float dist = length(ray);
|
||||
vec3 rd = ray / max(dist, 1e-3);
|
||||
if (!hasSky() || FRAME.fog.x <= 0.0) return color;
|
||||
float k = FRAME.fog.y;
|
||||
float start = FRAME.fog.x * exp(-k * (FRAME.camPos.z - FRAME.fog.w));
|
||||
float kz = k * rd.z;
|
||||
float amount = start * (abs(kz) > 1e-6 ? (1.0 - exp(-kz * dist)) / kz : dist);
|
||||
float f = min(1.0 - exp(-amount), FRAME.fog.z);
|
||||
vec3 fogColor = sky(normalize(vec3(rd.xy, max(rd.z, 0.05))), FRAME.sky.z) * 0.9;
|
||||
if (FRAME.sunColor.w > 0.5) fogColor += FRAME.sunColor.rgb * pow(max(dot(rd, FRAME.sunDir.xyz), 0.0), 8.0) * 0.25;
|
||||
return mix(color, fogColor, f);
|
||||
}
|
||||
|
||||
vec3 ambientCube(vec3 n) {
|
||||
vec3 w = n * n;
|
||||
return w.x * (n.x > 0.0 ? DRAW.cube[0].rgb : DRAW.cube[1].rgb)
|
||||
+ w.y * (n.y > 0.0 ? DRAW.cube[2].rgb : DRAW.cube[3].rgb)
|
||||
+ w.z * (n.z > 0.0 ? DRAW.cube[4].rgb : DRAW.cube[5].rgb);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// A mirror's pass: nothing behind the mirror (the wall it's on, the room beyond).
|
||||
if (dot(FRAME.clip.xyz, FRAME.clip.xyz) > 0.5 && dot(v_world, FRAME.clip.xyz) < FRAME.clip.w) discard;
|
||||
// The mirror itself: the picture its pass made, where this pixel is (slightly darkened, as
|
||||
// real mirrors are), and nothing for the screen-space reflections to add.
|
||||
if (FRAME.mirror.x > 0.5 && (pc.textures & 0xFFFFu) == uint(FRAME.mirror.x)) {
|
||||
o_color = vec4(fetchTex(uint(FRAME.mirror.x), ivec2(gl_FragCoord.xy)).rgb * vec3(0.9, 0.92, 0.92), 1.0);
|
||||
o_gbuf = vec4(0.5, 0.5, 1.0, 0.0);
|
||||
return;
|
||||
}
|
||||
vec4 albedo = sampleTex(pc.textures & 0xFFFFu, v_uv);
|
||||
if (albedo.a < 0.5) discard; // alpha-tested texture (fences, foliage)
|
||||
vec4 tint = unpackUnorm4x8(pc.tint);
|
||||
vec3 base = albedo.rgb * tint.rgb;
|
||||
vec3 n = normalize(v_normal);
|
||||
vec3 geo = n; // the surface itself, before relief
|
||||
vec3 view = normalize(FRAME.camPos.xyz - v_world);
|
||||
// Relief and gloss from the material map. Derivatives first, while control flow is uniform.
|
||||
mat3 tbn = cotangentFrame(n, v_world, v_uv);
|
||||
float gloss = 0.15, metal = 0.0;
|
||||
uint materialMap = pc.material & 0xFFFFu, skyVisMap = pc.material >> 16;
|
||||
if (materialMap != 0u) {
|
||||
vec4 m = sampleTex(materialMap, v_uv);
|
||||
vec2 xy = m.xy * 2.0 - 1.0; // X, Y; Z rebuilt (B holds metalness)
|
||||
n = normalize(tbn * vec3(xy, sqrt(max(1.0 - dot(xy, xy), 0.0))));
|
||||
metal = m.z;
|
||||
gloss = m.w;
|
||||
}
|
||||
// Weather: how open to the sky this spot is (the lightmap's sky map; moving things: their
|
||||
// sun visibility), then rain wetting it (darker, glossy) and snow lying on what faces up.
|
||||
float exposed = skyVisMap != 0u ? sampleTex(skyVisMap, v_lightmapUv).r : DRAW.params.y > 0.5 ? DRAW.params.x : 0.0;
|
||||
exposed = smoothstep(0.25, 0.7, exposed);
|
||||
if (FRAME.weather.x > 0.0) {
|
||||
float wet = FRAME.weather.x * exposed * (0.45 + 0.55 * max(geo.z, 0.0));
|
||||
base *= 1.0 - 0.45 * wet;
|
||||
gloss = mix(gloss, 0.93, wet * 0.85);
|
||||
}
|
||||
float snowCover = 0.0;
|
||||
if (FRAME.weather.y > 0.0) {
|
||||
float snow = FRAME.weather.y * exposed * smoothstep(0.35, 0.8, geo.z) * smoothstep(0.2, 0.6, vnoise(v_world * 0.05) * 0.5 + FRAME.weather.y);
|
||||
snowCover = snow;
|
||||
// Snow isn't flat white: drifts a little brighter, hollows a little bluer.
|
||||
float drift = vnoise(v_world * 0.012) * 0.6 + vnoise(v_world * 0.05) * 0.4;
|
||||
base = mix(base, mix(vec3(0.78, 0.82, 0.9), vec3(0.9, 0.91, 0.93), drift), snow);
|
||||
gloss = mix(gloss, 0.25, snow);
|
||||
metal *= 1.0 - snow;
|
||||
}
|
||||
int liquid = int(DRAW.params.w + 0.5);
|
||||
float liquidAlpha = 1.0;
|
||||
if (liquid == 1) {
|
||||
// A film of liquid: thickest in the middle (UV v = 0), ragged at the edge, rippled.
|
||||
float thickness = 1.0 - v_uv.y;
|
||||
float ragged = vnoise(v_world * 0.35) * 0.6 + vnoise(v_world * 1.1) * 0.4;
|
||||
float edge = 0.12 + 0.45 * ragged;
|
||||
if (thickness < edge) discard;
|
||||
liquidAlpha = smoothstep(edge, edge + 0.2, thickness);
|
||||
float e = 0.05;
|
||||
vec3 q = v_world * 0.6;
|
||||
vec3 grad = vec3(vnoise(q + vec3(e, 0, 0)) - vnoise(q - vec3(e, 0, 0)), vnoise(q + vec3(0, e, 0)) - vnoise(q - vec3(0, e, 0)),
|
||||
vnoise(q + vec3(0, 0, e)) - vnoise(q - vec3(0, 0, e))) / (2.0 * e);
|
||||
n = normalize(n - (grad - n * dot(grad, n)) * 0.12);
|
||||
// Deeper liquid is richer in colour (Beer–Lambert-ish); thin edges just wet the surface.
|
||||
base = mix(base * 0.35, base, 0.4 + 0.6 * thickness);
|
||||
gloss = 0.95;
|
||||
} else if (liquid == 2) {
|
||||
// A body of liquid: light passes through it, more of it where it's thicker. Looking
|
||||
// straight through the middle is the longest path (rich, dense); the edges are thin.
|
||||
float through = abs(dot(n, view));
|
||||
base = mix(base * 1.15, base * 0.5, through);
|
||||
liquidAlpha = mix(0.45, 1.0, through);
|
||||
gloss = 0.92;
|
||||
}
|
||||
if (liquid == 4) {
|
||||
// A decal (a burn mark) on a disc: matte, darkest in the middle, its edge soft and ragged.
|
||||
float thickness = 1.0 - v_uv.y;
|
||||
float ragged = vnoise(v_world * 0.12) * 0.6 + vnoise(v_world * 0.45) * 0.4;
|
||||
liquidAlpha = smoothstep(0.0, 0.9, thickness + (ragged - 0.5) * 0.6);
|
||||
gloss = 0.08;
|
||||
}
|
||||
if (liquid == 3) {
|
||||
// Open water: waves (a few travelling swells plus noise ripples) bend the normal; looking
|
||||
// straight down you see into it, at a grazing angle it mirrors the sky (Fresnel, below).
|
||||
float t = DRAW.probe.z;
|
||||
vec2 p = v_world.xy, g = vec2(0.0);
|
||||
vec2 d1 = vec2(0.8, 0.6), d2 = vec2(-0.5, 0.86), d3 = vec2(0.2, -0.98);
|
||||
g += d1 * cos(dot(p, d1) * 0.035 + t * 1.1) * 0.09;
|
||||
g += d2 * cos(dot(p, d2) * 0.07 + t * 1.7) * 0.06;
|
||||
g += d3 * cos(dot(p, d3) * 0.13 + t * 2.3) * 0.04;
|
||||
// Two layers of ripples drifting different ways (the wind, and the surface's own churn).
|
||||
float e = 0.5;
|
||||
vec3 q = vec3(p * 0.09 + vec2(t * 0.25, t * 0.1), t * 0.35), q2 = vec3(p * 0.21 - vec2(t * 0.12, t * 0.3), t * 0.6);
|
||||
g += vec2(vnoise(q + vec3(e, 0, 0)) - vnoise(q - vec3(e, 0, 0)), vnoise(q + vec3(0, e, 0)) - vnoise(q - vec3(0, e, 0))) * 0.3;
|
||||
g += vec2(vnoise(q2 + vec3(e, 0, 0)) - vnoise(q2 - vec3(e, 0, 0)), vnoise(q2 + vec3(0, e, 0)) - vnoise(q2 - vec3(0, e, 0))) * 0.18;
|
||||
n = normalize(geo + vec3(-g, 0.0));
|
||||
float through = abs(dot(n, view));
|
||||
base = mix(base * 1.25, base * 0.6, through);
|
||||
liquidAlpha = mix(0.95, 0.62, through);
|
||||
gloss = 0.97;
|
||||
}
|
||||
if (DRAW.params.z > 0.5) {
|
||||
// Liquid: cut away above the surface; looking in through the cut, the inside (back)
|
||||
// faces stand in for the flat surface.
|
||||
if (dot(v_world, DRAW.plane.xyz) > DRAW.plane.w) discard;
|
||||
if (!gl_FrontFacing) {
|
||||
n = DRAW.plane.xyz;
|
||||
base *= 1.2;
|
||||
}
|
||||
}
|
||||
// Metals reflect in their own colour and scatter nothing: F0 is the albedo, diffuse goes.
|
||||
g_f0 = mix(vec3(0.04), base, metal);
|
||||
base *= 1.0 - metal;
|
||||
vec3 sunDir = FRAME.sunDir.xyz;
|
||||
// Relief can't light what the surface itself faces away from (no light leaking round corners).
|
||||
float facing = dot(geo, sunDir) > 0.0 ? max(dot(n, sunDir), 0.0) : 0.0;
|
||||
vec3 sun = FRAME.sunColor.rgb * FRAME.sunColor.w * facing;
|
||||
float rough = clamp(1.0 - gloss, 0.05, 1.0);
|
||||
vec3 sunSpec = FRAME.sunColor.rgb * FRAME.sunColor.w * facing * specGGX(n, sunDir, view, rough);
|
||||
// How much open sky this spot sees (reflections of the sky fade with it).
|
||||
float skyVis = 0.0;
|
||||
// Cavities in the relief (grout, pores) get less of the soft, baked light.
|
||||
float cavity = clamp(1.0 - (1.0 - dot(n, geo)) * 2.5, 0.45, 1.0);
|
||||
vec3 specular = vec3(0.0), env = vec3(0.0);
|
||||
bool realtime = FRAME.shadow.z > 0.5;
|
||||
uint lightmap = pc.textures >> 16;
|
||||
vec3 light;
|
||||
if (lightmap != 0u) {
|
||||
vec4 rgbm = sampleTex(lightmap, v_lightmapUv); // RGBM, range 8
|
||||
vec3 baked = rgbm.rgb * rgbm.a * 8.0 * cavity;
|
||||
// Night: what the sky gave (most of the light where it's open) goes; lamps stay.
|
||||
baked *= 1.0 - FRAME.weather.z * 0.9 * smoothstep(0.0, 0.45, skyVisMap != 0u ? sampleTex(skyVisMap, v_lightmapUv).r : 0.0);
|
||||
env = baked;
|
||||
skyVis = skyVisMap != 0u ? sampleTex(skyVisMap, v_lightmapUv).r : 0.0;
|
||||
if (realtime) {
|
||||
float vis = facing > 0.0 ? sunShadow(geo) : 0.0;
|
||||
light = baked + sun * vis;
|
||||
specular = sunSpec * vis;
|
||||
} else { // old maps: the sun is baked in; players and props darken it where they block it
|
||||
float shade = 1.0 - FRAME.shadow.y * (1.0 - sunShadow(n)) * facing * FRAME.sunColor.w;
|
||||
light = baked * shade;
|
||||
}
|
||||
} else if (DRAW.params.y > 0.5) {
|
||||
// Moving things: baked ambient cube plus the sun. With a real-time sun the shadow map
|
||||
// already has the world in it; otherwise the probe's sun visibility stands in for it.
|
||||
float vis = facing > 0.0 ? sunShadow(geo) : 0.0;
|
||||
vis = realtime ? vis : DRAW.params.x * vis;
|
||||
env = ambientCube(n) * cavity * (1.0 - FRAME.weather.z * 0.8 * max(DRAW.params.x, 0.35));
|
||||
light = env + sun * vis;
|
||||
// Probes don't know where the sky is: compare their light with the open sky's.
|
||||
if (hasSky()) skyVis = clamp(luma(env) / max(luma(skyLight(n)), 1e-3), 0.0, 1.0) * mix(0.6, 1.0, DRAW.params.x);
|
||||
specular = sunSpec * vis;
|
||||
} else if (hasSky()) {
|
||||
// No baked light of its own (water, map-less things): the sky's light here and the real
|
||||
// sun or moon, through its shadow — so it darkens at night like everything else.
|
||||
float vis = facing > 0.0 ? sunShadow(geo) : 0.0;
|
||||
env = skyLight(n);
|
||||
light = env + sun * vis;
|
||||
specular = sunSpec * vis;
|
||||
} else {
|
||||
light = vec3(sunLight(n));
|
||||
}
|
||||
uint count;
|
||||
vec3 dynSpec;
|
||||
light += dynamicLights(v_world, n, rough, view, count, dynSpec);
|
||||
#ifndef KEEL_GL
|
||||
if (FRAME.gi.x > 0.5) {
|
||||
vec3 bounced = giIrradiance(v_world, n, view) * cavity; // their light, bounced
|
||||
if (FRAME.gi.z > 0.5) { o_color = vec4(bounced * 4.0, 1.0); o_gbuf = vec4(0.0); return; }
|
||||
light += bounced;
|
||||
}
|
||||
#endif
|
||||
// Reflections (split-sum image-based light): the sky, prefiltered for this roughness, where
|
||||
// the sky is open; the light arriving here stands in for the room where it isn't.
|
||||
float ndv = clamp(dot(n, view), 1e-4, 1.0);
|
||||
vec3 r = reflect(-view, n);
|
||||
r = normalize(r + geo * max(-dot(r, geo), 0.0) * 1.05); // keep it above the surface
|
||||
vec2 ab = envBRDF(rough, ndv);
|
||||
float occlusion = skyVis * skyVis;
|
||||
// What's around: the nearest reflection probe (env_cubemap) where there is one, else the
|
||||
// light arriving here; the sky takes over as far as this spot sees it.
|
||||
// (probes were baked in daylight: at night what they show goes dark too)
|
||||
vec3 near = DRAW.probe.x > 0.5 ? skyRadiance(uint(DRAW.probe.x), 1.0 - 0.92 * FRAME.weather.z, 0.0, r, rough * DRAW.probe.y) : env;
|
||||
vec3 surroundings = hasSky() ? mix(near, sky(r, rough * FRAME.sky.z), occlusion) : near;
|
||||
vec3 reflection = surroundings * (g_f0 * ab.x + ab.y) * cavity;
|
||||
vec3 color = base * light + specular + dynSpec + reflection;
|
||||
// Snow glitters: here and there a crystal catches the sun (or a lamp) towards the eye.
|
||||
if (snowCover > 0.05) {
|
||||
vec3 cell = floor(v_world * 1.7);
|
||||
float h = fract(sin(dot(cell, vec3(12.9898, 78.233, 45.164))) * 43758.5453);
|
||||
float glint = step(0.975, h) * pow(max(dot(n, normalize(sunDir + view)), 0.0), 40.0);
|
||||
color += (FRAME.sunColor.rgb * FRAME.sunColor.w * (facing > 0.0 ? 1.0 : 0.0) + dynSpec * 4.0) * glint * snowCover * 1.2;
|
||||
}
|
||||
float alpha = tint.a * liquidAlpha;
|
||||
if (liquid == 3) {
|
||||
// Open water barely scatters light of its own: it's what's mirrored (more the flatter
|
||||
// you look: Fresnel) over what's seen through it (the bottom, more looking straight
|
||||
// down), tinted by a dim veil of its own colour. Blended over what's behind it, so:
|
||||
// alpha = the share it covers, colour = what covers it.
|
||||
vec3 mirrored = hasSky() ? mix(env, sky(r, 0.0), occlusion) : vec3(0.45, 0.6, 0.8);
|
||||
float fres = 0.02 + 0.98 * pow(1.0 - clamp(abs(dot(n, view)), 0.0, 1.0), 5.0);
|
||||
float clear = 0.72 * clamp(abs(dot(geo, view)), 0.0, 1.0); // (how much of the bottom shows through)
|
||||
vec3 veil = base * (light * 0.3 + vec3(0.015));
|
||||
float cover = fres + (1.0 - fres) * (1.0 - clear);
|
||||
color = (mirrored * fres + veil * (1.0 - fres) * (1.0 - clear)) / max(cover, 1e-3);
|
||||
color += FRAME.sunColor.rgb * FRAME.sunColor.w * facing * specGGX(n, sunDir, view, 0.06) + dynSpec; // (the sun's glint, lamps')
|
||||
alpha = clamp(cover, 0.0, 1.0);
|
||||
} else if (liquid != 0 && liquid != 4) {
|
||||
// Wet: a sharp sun glint and the sky mirrored at grazing angles, on top of what's lit.
|
||||
vec3 mirrored = hasSky() ? mix(env, sky(r, 0.0), occlusion) : vec3(0.45, 0.6, 0.8);
|
||||
float fres = 0.02 + 0.98 * pow(1.0 - clamp(dot(n, view), 0.0, 1.0), 5.0);
|
||||
color += mirrored * fres * 0.9 + FRAME.sunColor.rgb * FRAME.sunColor.w * facing * specGGX(n, sunDir, view, 0.08);
|
||||
if (liquid == 1) alpha = clamp(alpha * (0.75 + fres), 0.0, 0.95);
|
||||
}
|
||||
if (alpha < 0.5 && liquid != 4 && liquid != 3) {
|
||||
// Glass (clear materials; denser translucent things like liquid or cracks shade normally): more reflective at grazing angles (Fresnel), plus a sharp sun glint.
|
||||
vec3 v = normalize(FRAME.camPos.xyz - v_world);
|
||||
vec3 nf = dot(n, v) < 0.0 ? -n : n; // the side facing us, whatever the winding
|
||||
float fresnel = pow(1.0 - clamp(dot(nf, v), 0.0, 1.0), 4.0);
|
||||
float glint = pow(max(dot(nf, normalize(sunDir + v)), 0.0), 150.0) * FRAME.sunColor.w;
|
||||
// Glass barely scatters light itself: mostly what's behind it, plus reflections.
|
||||
vec3 rg = reflect(-v, nf);
|
||||
vec3 mirrored = hasSky() ? mix(env, sky(rg, 0.0), max(occlusion, 0.25)) : vec3(0.45, 0.6, 0.8);
|
||||
color = color * 0.12 + FRAME.sunColor.rgb * glint * 3.0 + mirrored * fresnel * 0.8;
|
||||
// A neutral sheen where it turns away from you (no colour: grey to white), so glass reads
|
||||
// as glass at an angle.
|
||||
color += vec3(0.06 + fresnel * 0.35) * (hasSky() ? max(occlusion, 0.4) : 0.6);
|
||||
alpha = clamp(alpha * 0.6 + fresnel * 0.5 + glint, 0.0, 1.0);
|
||||
}
|
||||
#ifndef KEEL_GL
|
||||
if (FRAME.froxel.x > 0.5) { // the fog grid: lit, shadowed fog (and the air's light)
|
||||
vec4 f = froxelFog(gl_FragCoord.xy / FRAME.screen.xy, dot(v_world - FRAME.camPos.xyz, FRAME.camFwd.xyz));
|
||||
color = color * f.a + f.rgb;
|
||||
} else
|
||||
#endif
|
||||
color = applyFog(color, v_world);
|
||||
o_color = vec4(color, alpha);
|
||||
// View space as the post passes see it: x right, y down the screen, z forward.
|
||||
vec3 fwd = FRAME.camFwd.xyz, right = normalize(cross(fwd, vec3(0.0, 0.0, 1.0))), up = cross(right, fwd);
|
||||
vec3 nv = vec3(dot(n, right), -dot(n, up), dot(n, fwd));
|
||||
float reflectWeight = dot(g_f0 * ab.x + ab.y, vec3(0.333)) * cavity * (liquid != 0 ? 1.5 : 1.0);
|
||||
o_gbuf = vec4(octEncode(nv), rough, reflectWeight);
|
||||
if (FRAME.camFwd.w > 0.5) o_color.rgb = mix(o_color.rgb, heatmap(count), 0.6);
|
||||
}
|
||||
18
engine/shaders/mesh.vert
Normal file
18
engine/shaders/mesh.vert
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#version 460
|
||||
#include "model.glsl"
|
||||
|
||||
layout(location = 0) out vec3 v_normal;
|
||||
layout(location = 1) out vec2 v_uv;
|
||||
layout(location = 2) out vec2 v_lightmapUv;
|
||||
layout(location = 3) out vec3 v_world;
|
||||
|
||||
void main() {
|
||||
Vertex vx = VERTEX(VERTEX_ID);
|
||||
mat4 model = DRAW.model;
|
||||
vec4 world = model * vec4(vx.px, vx.py, vx.pz, 1.0);
|
||||
v_normal = normalize(mat3(model) * vec3(vx.nx, vx.ny, vx.nz));
|
||||
v_uv = vec2(vx.u, vx.v);
|
||||
v_lightmapUv = vec2(vx.lu, vx.lv);
|
||||
v_world = world.xyz;
|
||||
gl_Position = pc.viewProj * world;
|
||||
}
|
||||
169
engine/shaders/model.glsl
Normal file
169
engine/shaders/model.glsl
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#ifndef KEEL_MODEL_GLSL
|
||||
#define KEEL_MODEL_GLSL
|
||||
// Shared by mesh.vert, skinned.vert and mesh.frag: per-draw and per-frame data. The layouts are
|
||||
// identical on both backends (render/backend.h mirrors them); only how they're reached differs.
|
||||
#include "backend.glsl"
|
||||
|
||||
struct Vertex { float px, py, pz, nx, ny, nz, u, v, lu, lv; };
|
||||
// Dynamic lights (clustered, see render/clusters.h).
|
||||
struct Light {
|
||||
vec4 posRadius;
|
||||
vec4 color; // rgb, w = first shadow view or -1
|
||||
vec4 spot; // xyz direction, w = cos(outer cone); -2 = point light
|
||||
vec4 extra; // x = cos(inner cone), y = the light's size (area light radius)
|
||||
};
|
||||
struct ShadowView { mat4 viewProj; vec4 rect; }; // rect: atlas uv offset, size
|
||||
// How much of a spot light reaches a direction at cos c from its axis. A plain spot fades from
|
||||
// its inner cone to its outer one; a reflector (a flashlight, extra.z = 1) has a hot centre, a
|
||||
// faint bright ring where its mirror ends, faint rings in the beam and a dim spill out to the edge.
|
||||
float spotShape(Light l, float c) {
|
||||
if (l.extra.z < 0.5) return smoothstep(l.spot.w, l.extra.x, c);
|
||||
float outer = acos(clamp(l.spot.w, -1.0, 1.0)), inner = acos(clamp(l.extra.x, -1.0, 1.0));
|
||||
float t = acos(clamp(c, -1.0, 1.0)) / max(outer, 1e-3), k = inner / max(outer, 1e-3);
|
||||
float core = mix(0.14, 1.0, 1.0 - smoothstep(k * 0.5, k * 1.1, t));
|
||||
float ring = exp(-pow((t - k * 1.15) * 16.0, 2.0)) * 0.22;
|
||||
float rings = 0.95 + 0.05 * sin(t * 60.0);
|
||||
return (core + ring) * rings * (1.0 - smoothstep(0.7, 1.0, t));
|
||||
}
|
||||
|
||||
// Per draw: transform, baked ambient cube (+X -X +Y -Y +Z -Z), params x = sun visibility,
|
||||
// y = lit by probes, z = liquid (cut at `plane`: xyz normal, w distance, world space),
|
||||
// w = liquid style (1 film, 2 body; see LiquidStyle).
|
||||
#define DRAW_FIELDS mat4 model; vec4 cube[6]; vec4 params; vec4 plane; vec4 probe;
|
||||
// Per frame: sun and its shadow map, camera, dynamic lights.
|
||||
#define FRAME_FIELDS \
|
||||
mat4 cascades[4]; /* sun shadow cascades, each a quarter of the shadow map */ \
|
||||
vec4 splits; /* view distance where each cascade ends */ \
|
||||
vec4 cascadeTexel; /* world size of a shadow texel per cascade */ \
|
||||
vec4 sunDir; /* xyz towards the sun, w = shadow map texture index */ \
|
||||
vec4 sunColor; /* rgb, w = 1 when there is a sun */ \
|
||||
vec4 shadow; /* x = texel size, y = strength on baked surfaces, z = 1 real-time */ \
|
||||
vec4 volumetric; /* volumetric light: x strength, y reach, z air density, w sun too */ \
|
||||
vec4 camPos; /* xyz, w = dynamic light count */ \
|
||||
vec4 camFwd; /* xyz, w = debug view (1 = lights per cluster) */ \
|
||||
vec4 screen; /* width, height, shadow atlas texture index, atlas size */ \
|
||||
vec4 sky; /* x sky texture (0 = none), y intensity, z top level, w yaw */ \
|
||||
vec4 fog; /* x density per unit, y height falloff per unit, z max, w base z */ \
|
||||
vec4 skySH[9]; /* the sky's light: rgb spherical harmonics, irradiance / pi */ \
|
||||
vec4 froxel; /* the fog grid (froxel.glsl): x 1 = on (Vulkan), y near, z far */ \
|
||||
vec4 gi; /* real-time GI probes (gi.glsl): x 1 = on, y spacing */ \
|
||||
vec4 giOrigin; /* the probe grid's first cell (whole numbers) */ \
|
||||
vec4 mirror; /* x: the mirror's picture's texture (a mirror surface shows it); y 1: \
|
||||
the mirror's own pass (every light, not this view's clusters) */ \
|
||||
vec4 clip; /* the mirror's pass: what's behind its plane (xyz·p < w) is cut */ \
|
||||
vec4 weather; /* x wet (rain), y snow cover, z night (0 day .. 1), w lightning flash */
|
||||
|
||||
#ifdef KEEL_FRAME_ONLY
|
||||
// Post passes that need the frame (volumetric.frag): it and the dynamic lights. Vulkan: a
|
||||
// FrameData reference in the pass's own push block (#define FRAME pc.frame there); OpenGL: the
|
||||
// frame's and the lights' bindings.
|
||||
#ifdef KEEL_GL
|
||||
layout(std430, binding = 4) readonly buffer FrameBlock { FRAME_FIELDS uvec2 unusedRefs[6]; } u_frame;
|
||||
layout(std430, binding = 5) readonly buffer Lights { Light l[]; } u_lights;
|
||||
layout(std430, binding = 6) readonly buffer Cells { uvec2 c[]; } u_cells;
|
||||
layout(std430, binding = 7) readonly buffer LightIndices { uint i[]; } u_lightIndices;
|
||||
layout(std430, binding = 8) readonly buffer ShadowViews { ShadowView v[]; } u_shadowViews;
|
||||
#define FRAME u_frame
|
||||
#define LIGHT(n_) u_lights.l[n_]
|
||||
#define CELL(n_) u_cells.c[n_]
|
||||
#define LIGHT_INDEX(n_) u_lightIndices.i[n_]
|
||||
#define SHADOW_VIEW(n_) u_shadowViews.v[n_]
|
||||
#else
|
||||
layout(buffer_reference, std430) readonly buffer Lights { Light l[]; };
|
||||
layout(buffer_reference, std430) readonly buffer Cells { uvec2 c[]; };
|
||||
layout(buffer_reference, std430) readonly buffer LightIndices { uint i[]; };
|
||||
layout(buffer_reference, std430) readonly buffer ShadowViews { ShadowView v[]; };
|
||||
layout(buffer_reference, std430) readonly buffer Froxels { uvec2 f[]; }; // the fog grid, integrated: rgb light, a transmittance (halves)
|
||||
// A GI probe (gi.glsl): the light reaching it as L1 spherical harmonics (sh[0..3].rgb: band 0,
|
||||
// then y, z, x; sh[0].w how much to trust it), the mean distance to what's around it and its
|
||||
// square along ±x ±y ±z, and the world cell it holds (w 1 = filled).
|
||||
struct GiProbe { vec4 sh[4]; vec4 dist[3]; vec4 tag; };
|
||||
layout(buffer_reference, std430) readonly buffer GiProbes { GiProbe p[]; };
|
||||
layout(buffer_reference, std430) readonly buffer FrameData {
|
||||
FRAME_FIELDS
|
||||
Lights lights;
|
||||
Cells cells;
|
||||
LightIndices lightIndices;
|
||||
ShadowViews shadowViews;
|
||||
Froxels froxels;
|
||||
GiProbes giProbes;
|
||||
};
|
||||
#define LIGHT(n_) FRAME.lights.l[n_]
|
||||
#define CELL(n_) FRAME.cells.c[n_]
|
||||
#define LIGHT_INDEX(n_) FRAME.lightIndices.i[n_]
|
||||
#define SHADOW_VIEW(n_) FRAME.shadowViews.v[n_]
|
||||
#endif
|
||||
#elif defined(KEEL_GL)
|
||||
struct DrawData { DRAW_FIELDS };
|
||||
layout(std430, binding = 0) readonly buffer Vertices { Vertex v[]; } u_vertices;
|
||||
layout(std430, binding = 1) readonly buffer Skin { uvec2 s[]; } u_skin; // joints (4×u8), weights (4×u8)
|
||||
layout(std430, binding = 2) readonly buffer Joints { mat4 m[]; } u_joints;
|
||||
layout(std430, binding = 3) readonly buffer Draws { DrawData d[]; } u_draws;
|
||||
layout(std430, binding = 4) readonly buffer FrameBlock { FRAME_FIELDS uvec2 unusedRefs[6]; } u_frame;
|
||||
layout(std430, binding = 5) readonly buffer Lights { Light l[]; } u_lights;
|
||||
layout(std430, binding = 6) readonly buffer Cells { uvec2 c[]; } u_cells; // first index, count
|
||||
layout(std430, binding = 7) readonly buffer LightIndices { uint i[]; } u_lightIndices;
|
||||
layout(std430, binding = 8) readonly buffer ShadowViews { ShadowView v[]; } u_shadowViews;
|
||||
PUSH_BLOCK {
|
||||
mat4 viewProj;
|
||||
uint textures; // albedo | lightmap << 16
|
||||
uint tint; // RGBA8
|
||||
uint material; // texture: rgb tangent-space normal, a gloss (0 = flat, matte-ish) | sky visibility << 16
|
||||
uint drawIndex; // into Draws
|
||||
uint firstJoint; // into Joints (skinned draws)
|
||||
} pc;
|
||||
#define VERTEX(n_) u_vertices.v[n_]
|
||||
#define SKIN(n_) u_skin.s[n_]
|
||||
#define JOINT(n_) u_joints.m[pc.firstJoint + (n_)]
|
||||
#define DRAW u_draws.d[pc.drawIndex]
|
||||
#define FRAME u_frame
|
||||
#define LIGHT(n_) u_lights.l[n_]
|
||||
#define CELL(n_) u_cells.c[n_]
|
||||
#define LIGHT_INDEX(n_) u_lightIndices.i[n_]
|
||||
#define SHADOW_VIEW(n_) u_shadowViews.v[n_]
|
||||
#else
|
||||
layout(buffer_reference, std430) readonly buffer Vertices { Vertex v[]; };
|
||||
layout(buffer_reference, std430) readonly buffer Skin { uvec2 s[]; }; // joints (4×u8), weights (4×u8)
|
||||
layout(buffer_reference, std430) readonly buffer Joints { mat4 m[]; };
|
||||
layout(buffer_reference, std430) readonly buffer DrawData { DRAW_FIELDS };
|
||||
layout(buffer_reference, std430) readonly buffer Lights { Light l[]; };
|
||||
layout(buffer_reference, std430) readonly buffer Cells { uvec2 c[]; }; // first index, count
|
||||
layout(buffer_reference, std430) readonly buffer LightIndices { uint i[]; };
|
||||
layout(buffer_reference, std430) readonly buffer ShadowViews { ShadowView v[]; };
|
||||
layout(buffer_reference, std430) readonly buffer Froxels { uvec2 f[]; }; // the fog grid, integrated: rgb light, a transmittance (halves)
|
||||
// A GI probe (gi.glsl): the light reaching it as L1 spherical harmonics (sh[0..3].rgb: band 0,
|
||||
// then y, z, x; sh[0].w how much to trust it), the mean distance to what's around it and its
|
||||
// square along ±x ±y ±z, and the world cell it holds (w 1 = filled).
|
||||
struct GiProbe { vec4 sh[4]; vec4 dist[3]; vec4 tag; };
|
||||
layout(buffer_reference, std430) readonly buffer GiProbes { GiProbe p[]; };
|
||||
layout(buffer_reference, std430) readonly buffer FrameData {
|
||||
FRAME_FIELDS
|
||||
Lights lights;
|
||||
Cells cells;
|
||||
LightIndices lightIndices;
|
||||
ShadowViews shadowViews;
|
||||
Froxels froxels;
|
||||
GiProbes giProbes;
|
||||
};
|
||||
PUSH_BLOCK {
|
||||
mat4 viewProj;
|
||||
Vertices vertices;
|
||||
Skin skin; // skinned draws only
|
||||
Joints joints; // skinned draws only; already include the model transform
|
||||
DrawData draw;
|
||||
FrameData frame;
|
||||
uint textures; // albedo | lightmap << 16
|
||||
uint tint; // RGBA8
|
||||
uint material; // texture: rgb tangent-space normal, a gloss (0 = flat, matte-ish) | sky visibility << 16
|
||||
} pc;
|
||||
#define VERTEX(n_) pc.vertices.v[n_]
|
||||
#define SKIN(n_) pc.skin.s[n_]
|
||||
#define JOINT(n_) pc.joints.m[n_]
|
||||
#define DRAW pc.draw
|
||||
#define FRAME pc.frame
|
||||
#define LIGHT(n_) pc.frame.lights.l[n_]
|
||||
#define CELL(n_) pc.frame.cells.c[n_]
|
||||
#define LIGHT_INDEX(n_) pc.frame.lightIndices.i[n_]
|
||||
#define SHADOW_VIEW(n_) pc.frame.shadowViews.v[n_]
|
||||
#endif
|
||||
#endif
|
||||
21
engine/shaders/skinned.vert
Normal file
21
engine/shaders/skinned.vert
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#version 460
|
||||
#include "model.glsl"
|
||||
|
||||
layout(location = 0) out vec3 v_normal;
|
||||
layout(location = 1) out vec2 v_uv;
|
||||
layout(location = 2) out vec2 v_lightmapUv;
|
||||
layout(location = 3) out vec3 v_world;
|
||||
|
||||
void main() {
|
||||
Vertex vx = VERTEX(VERTEX_ID);
|
||||
uvec2 sk = SKIN(VERTEX_ID);
|
||||
vec4 w = unpackUnorm4x8(sk.y);
|
||||
mat4 m = JOINT(sk.x & 0xFFu) * w.x + JOINT((sk.x >> 8) & 0xFFu) * w.y
|
||||
+ JOINT((sk.x >> 16) & 0xFFu) * w.z + JOINT(sk.x >> 24) * w.w;
|
||||
vec4 world = m * vec4(vx.px, vx.py, vx.pz, 1.0);
|
||||
v_normal = normalize(mat3(m) * vec3(vx.nx, vx.ny, vx.nz));
|
||||
v_uv = vec2(vx.u, vx.v);
|
||||
v_lightmapUv = vec2(0.0);
|
||||
v_world = world.xyz;
|
||||
gl_Position = pc.viewProj * world;
|
||||
}
|
||||
58
engine/shaders/sky.frag
Normal file
58
engine/shaders/sky.frag
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#version 460
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
mat4 invViewProj;
|
||||
vec4 sunDir; // xyz towards the sun
|
||||
vec4 sunColor; // rgb, w = 1 when there is a sun
|
||||
vec4 sky; // x texture, y intensity, z top level, w yaw
|
||||
vec4 weather; // x cloud cover, y the photo's own sun's height (z; 2 = none), z night, w lightning flash
|
||||
} pc;
|
||||
#include "sky.glsl"
|
||||
|
||||
layout(location = 0) in vec2 v_ndc;
|
||||
layout(location = 0) out vec4 o_color;
|
||||
|
||||
void main() {
|
||||
vec4 far = pc.invViewProj * vec4(v_ndc, 1.0, 1.0), near = pc.invViewProj * vec4(v_ndc, 0.0, 1.0);
|
||||
vec3 dir = normalize(far.xyz / far.w - near.xyz / near.w);
|
||||
vec3 c = skyRadiance(uint(pc.sky.x), pc.sky.y, pc.sky.w, dir, 0.0);
|
||||
// Overcast: the photo's sky greys over (a flat, brighter-low-down cloud deck).
|
||||
float cloud = pc.weather.x;
|
||||
// The photo's own sun (turned to the real sun's bearing, but at the height it was shot at):
|
||||
// when the time of day has moved the sun away from it, it's painted over with the sky beside it.
|
||||
if (pc.weather.y < 1.5 && length(pc.sunDir.xy) > 1e-3) {
|
||||
vec3 photoSun = vec3(normalize(pc.sunDir.xy) * sqrt(max(1.0 - pc.weather.y * pc.weather.y, 0.0)), pc.weather.y);
|
||||
float moved = 1.0 - smoothstep(0.9990, 0.9998, dot(normalize(pc.sunDir.xyz), photoSun));
|
||||
float near = smoothstep(0.975, 0.997, dot(dir, photoSun)) * moved;
|
||||
if (near > 0.0) { // the same height a little round to the side: clean sky like what's round it
|
||||
const float t = 0.38; // ~22°
|
||||
vec3 beside = vec3(dir.x * cos(t) - dir.y * sin(t), dir.x * sin(t) + dir.y * cos(t), dir.z);
|
||||
c = mix(c, skyRadiance(uint(pc.sky.x), pc.sky.y, pc.sky.w, beside, 1.0), near);
|
||||
}
|
||||
}
|
||||
// The photo blurred right out (its top level): no sun in it, for the cloud deck and the night.
|
||||
vec3 soft = (cloud > 0.0 || pc.weather.z > 0.0) ? skyRadiance(uint(pc.sky.x), pc.sky.y, pc.sky.w, dir, pc.sky.z) : c;
|
||||
if (cloud > 0.0) {
|
||||
float l = dot(soft, vec3(0.2126, 0.7152, 0.0722));
|
||||
vec3 deck = vec3(l) * vec3(0.92, 0.95, 1.0) * mix(1.0, 0.7, smoothstep(0.0, 0.6, dir.z));
|
||||
c = mix(c, deck, cloud);
|
||||
}
|
||||
if (pc.sunColor.w > 0.5 && cloud < 0.95) {
|
||||
// The sun itself (taken out of the photo so it matches the shadows): a disc and a glow.
|
||||
float d = dot(dir, pc.sunDir.xyz);
|
||||
c += pc.sunColor.rgb * (smoothstep(0.99995, 0.99999, d) * 60.0 + pow(max(d, 0.0), 600.0) * 1.5 + pow(max(d, 0.0), 40.0) * 0.08) * (1.0 - cloud) * (1.0 - cloud);
|
||||
}
|
||||
// Night: the photo's sky goes dark blue, and stars come out (fixed to the sky).
|
||||
float night = pc.weather.z;
|
||||
if (night > 0.0) {
|
||||
c = mix(c, vec3(0.004, 0.006, 0.014) + soft * 0.15, night);
|
||||
vec3 d = dir * 220.0;
|
||||
vec3 cell = floor(d), f = fract(d) - 0.5;
|
||||
float h = fract(sin(dot(cell, vec3(12.9898, 78.233, 37.719))) * 43758.5453);
|
||||
float star = step(0.985, h) * smoothstep(0.12, 0.0, length(f)) * smoothstep(-0.05, 0.25, dir.z);
|
||||
float bright = 0.3 + 0.7 * fract(h * 97.0); // stars of different brightness
|
||||
c += vec3(0.8, 0.85, 1.0) * star * bright * night * 0.6;
|
||||
}
|
||||
c *= 1.0 + pc.weather.w * 4.0; // lightning lights the clouds
|
||||
o_color = vec4(c, 1.0);
|
||||
}
|
||||
16
engine/shaders/sky.glsl
Normal file
16
engine/shaders/sky.glsl
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#ifndef KEEL_SKY_GLSL
|
||||
#define KEEL_SKY_GLSL
|
||||
// The sky (.ksky, engine/asset/ksky.h): equirectangular RGBM; level k is the sky as a surface
|
||||
// of roughness k / top reflects it.
|
||||
const float kSkyRange = 16.0;
|
||||
|
||||
// Radiance from direction `dir` (world space). yaw turns the photo so its sun lines up with
|
||||
// the map's.
|
||||
vec3 skyRadiance(uint tex, float intensity, float yaw, vec3 dir, float level) {
|
||||
float c = cos(yaw), s = sin(yaw);
|
||||
dir = vec3(c * dir.x + s * dir.y, -s * dir.x + c * dir.y, dir.z);
|
||||
vec2 uv = vec2(atan(dir.y, dir.x) * 0.15915494 + 0.5, acos(clamp(dir.z, -1.0, 1.0)) * 0.31830989);
|
||||
vec4 m = sampleTexLod(tex, uv, level);
|
||||
return m.rgb * m.a * kSkyRange * intensity;
|
||||
}
|
||||
#endif
|
||||
10
engine/shaders/sky.vert
Normal file
10
engine/shaders/sky.vert
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#version 460
|
||||
// The sky: a full-screen triangle on the far plane, drawn where nothing else is (depth test
|
||||
// less-or-equal against the cleared 1.0).
|
||||
#include "backend.glsl"
|
||||
layout(location = 0) out vec2 v_ndc;
|
||||
void main() {
|
||||
vec2 uv = vec2((VERTEX_ID << 1) & 2, VERTEX_ID & 2);
|
||||
v_ndc = uv * 2.0 - 1.0;
|
||||
gl_Position = vec4(v_ndc, 1.0, 1.0);
|
||||
}
|
||||
30
engine/shaders/smoke_sky.frag
Normal file
30
engine/shaders/smoke_sky.frag
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#version 460
|
||||
// Smoke in front of the sky (Vulkan): the sky pass has no fog (its photo has its own haze), so
|
||||
// smoke against open sky is laid over it here, from the fog grid's smoke-only column totals
|
||||
// (froxel_main.glsl: light, and how much of what's behind gets through). Drawn on the far plane
|
||||
// after the sky, where nothing else is: colour × through + light.
|
||||
#include "backend.glsl"
|
||||
#define KEEL_FRAME_ONLY
|
||||
#include "model.glsl"
|
||||
layout(buffer_reference, std430) readonly buffer SmokeSky { uvec2 f[]; };
|
||||
PUSH_BLOCK {
|
||||
FrameData frame;
|
||||
SmokeSky sky; // 160 × 90 (the fog grid's columns)
|
||||
} pc;
|
||||
#define FRAME pc.frame
|
||||
layout(location = 0) in vec2 v_ndc;
|
||||
layout(location = 0) out vec4 o_color;
|
||||
|
||||
const ivec2 kCols = ivec2(160, 90);
|
||||
vec4 column(ivec2 c) {
|
||||
uvec2 v = pc.sky.f[clamp(c.y, 0, kCols.y - 1) * kCols.x + clamp(c.x, 0, kCols.x - 1)];
|
||||
return vec4(unpackHalf2x16(v.x), unpackHalf2x16(v.y));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 g = gl_FragCoord.xy / FRAME.screen.xy * vec2(kCols) - 0.5;
|
||||
ivec2 b = ivec2(floor(g));
|
||||
vec2 f = g - vec2(b);
|
||||
vec4 s = mix(mix(column(b), column(b + ivec2(1, 0)), f.x), mix(column(b + ivec2(0, 1)), column(b + ivec2(1, 1)), f.x), f.y);
|
||||
o_color = vec4(s.rgb, s.a); // (blended: src + dst × src.a)
|
||||
}
|
||||
19
engine/shaders/sprite.frag
Normal file
19
engine/shaders/sprite.frag
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#version 460
|
||||
// Particles: unlit, the texture times the colour (fire, sparks, flashes added on; smoke blended).
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
mat4 viewProj;
|
||||
vec4 posSize;
|
||||
vec4 color;
|
||||
vec4 right;
|
||||
vec4 up;
|
||||
} pc;
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 0) out vec4 o_color;
|
||||
void main() {
|
||||
vec4 t = sampleTex(floatBitsToUint(pc.up.w), v_uv);
|
||||
// Block compression leaves a little alpha where there should be none; bright sprites
|
||||
// would show it as a square: cut it.
|
||||
float a = clamp((t.a - 0.03) / 0.97, 0.0, 1.0);
|
||||
o_color = vec4(t.rgb * pc.color.rgb, a * pc.color.a);
|
||||
}
|
||||
21
engine/shaders/sprite.vert
Normal file
21
engine/shaders/sprite.vert
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#version 460
|
||||
// Particles: a camera-facing quad per sprite, from the vertex index (6 vertices, no buffer).
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
mat4 viewProj;
|
||||
vec4 posSize; // xyz centre, w half size
|
||||
vec4 color; // rgb (HDR: can be bright), a
|
||||
vec4 right; // camera right, w = rotation (radians)
|
||||
vec4 up; // camera up, w = texture index (bits)
|
||||
} pc;
|
||||
layout(location = 0) out vec2 v_uv;
|
||||
const vec2 kQuad[6] = vec2[](vec2(-1,-1), vec2(1,-1), vec2(1,1), vec2(-1,-1), vec2(1,1), vec2(-1,1));
|
||||
void main() {
|
||||
vec2 q = kQuad[VERTEX_ID];
|
||||
float c = cos(pc.right.w), s = sin(pc.right.w);
|
||||
vec2 r = vec2(c * q.x - s * q.y, s * q.x + c * q.y);
|
||||
vec3 world = pc.posSize.xyz + (pc.right.xyz * r.x + pc.up.xyz * r.y) * pc.posSize.w;
|
||||
v_uv = q * 0.5 + 0.5;
|
||||
v_uv.y = 1.0 - v_uv.y;
|
||||
gl_Position = pc.viewProj * vec4(world, 1.0);
|
||||
}
|
||||
63
engine/shaders/ssao.frag
Normal file
63
engine/shaders/ssao.frag
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#version 460
|
||||
// Ambient occlusion from the depth buffer alone (Alchemy-style, half resolution): for each
|
||||
// pixel, view-space points around it in a spiral; those above the surface's plane and close
|
||||
// enough darken it. Rows are the depth buffer's own (uv from gl_FragCoord), on both backends.
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
uint depth; // scene depth slot
|
||||
uint w, h; // its size
|
||||
uint outW, outH; // this target's
|
||||
float radius; // world units
|
||||
float strength;
|
||||
float pad;
|
||||
vec4 proj; // tan(half fov x), tan(half fov y), near, far
|
||||
} pc;
|
||||
layout(location = 0) out vec4 o_ao;
|
||||
|
||||
// The point seen at uv: its depth from the pixel there, and its place across the screen from
|
||||
// that same pixel's centre (not uv itself: on a slanted wall the two would disagree by a part of
|
||||
// a pixel, in a pattern that depends on how the window's size halves; stripes on walls).
|
||||
vec3 viewPos(vec2 uv) {
|
||||
ivec2 p = clamp(ivec2(uv * vec2(pc.w, pc.h)), ivec2(0), ivec2(pc.w - 1, pc.h - 1));
|
||||
float d = fetchTex(pc.depth, p).r;
|
||||
float z = pc.proj.z * pc.proj.w / (pc.proj.w - d * (pc.proj.w - pc.proj.z));
|
||||
vec2 at = (vec2(p) + 0.5) / vec2(pc.w, pc.h);
|
||||
return vec3((at * 2.0 - 1.0) * pc.proj.xy * z, z);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / vec2(pc.outW, pc.outH);
|
||||
ivec2 full = clamp(ivec2(uv * vec2(pc.w, pc.h)), ivec2(0), ivec2(pc.w - 1, pc.h - 1));
|
||||
if (fetchTex(pc.depth, full).r >= 1.0) { o_ao = vec4(1.0); return; } // sky
|
||||
vec3 p = viewPos(uv);
|
||||
// The surface normal from neighbouring depths (the smaller step on each axis, so edges hold).
|
||||
vec2 texel = 1.0 / vec2(pc.outW, pc.outH);
|
||||
vec3 px1 = viewPos(uv + vec2(texel.x, 0)) - p, px2 = p - viewPos(uv - vec2(texel.x, 0));
|
||||
vec3 py1 = viewPos(uv + vec2(0, texel.y)) - p, py2 = p - viewPos(uv - vec2(0, texel.y));
|
||||
// The smaller step, but never one off the screen's edge (there it clamps onto this pixel,
|
||||
// a zero step, and the normal goes wrong: a dark line along the border).
|
||||
bool right = uv.x + texel.x < 1.0, left = uv.x - texel.x > 0.0, down = uv.y + texel.y < 1.0, up = uv.y - texel.y > 0.0;
|
||||
vec3 dx = !right ? px2 : !left ? px1 : dot(px1, px1) < dot(px2, px2) ? px1 : px2;
|
||||
vec3 dy = !down ? py2 : !up ? py1 : dot(py1, py1) < dot(py2, py2) ? py1 : py2;
|
||||
vec3 n = normalize(cross(dx, dy));
|
||||
if (dot(n, p) > 0.0) n = -n; // towards the camera
|
||||
// Interleaved gradient noise turns the spiral per pixel; the blur evens it out.
|
||||
float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
||||
const int kSamples = 12;
|
||||
float occlusion = 0.0;
|
||||
for (int i = 0; i < kSamples; ++i) {
|
||||
float t = (float(i) + noise) / float(kSamples);
|
||||
float a = (float(i) * 2.39996 + noise * 6.2831853);
|
||||
float r = pc.radius * t;
|
||||
vec2 off = vec2(cos(a), sin(a)) * r / (p.z * pc.proj.xy * 2.0);
|
||||
vec2 q = uv + off;
|
||||
if (any(lessThan(q, vec2(0.0))) || any(greaterThan(q, vec2(1.0)))) continue; // off screen: nothing known there
|
||||
vec3 v = viewPos(q) - p;
|
||||
float vv = dot(v, v);
|
||||
float falloff = max(0.0, 1.0 - vv / (pc.radius * pc.radius));
|
||||
occlusion += max(0.0, dot(v, n) - 0.01 * p.z) / (vv + 1.0) * falloff;
|
||||
}
|
||||
float ao = max(0.0, 1.0 - 2.0 * occlusion / float(kSamples) * pc.radius * 0.6);
|
||||
o_ao = vec4(mix(1.0, ao * ao, pc.strength));
|
||||
}
|
||||
34
engine/shaders/ssao_blur.frag
Normal file
34
engine/shaders/ssao_blur.frag
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#version 460
|
||||
// Smooths the occlusion (4×4, weighted by how close each sample's depth is to this pixel's).
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
uint ao, depth;
|
||||
uint w, h; // depth size
|
||||
uint outW, outH; // this target's (= the occlusion's)
|
||||
float pad0, pad1;
|
||||
vec4 proj;
|
||||
} pc;
|
||||
layout(location = 0) out vec4 o_ao;
|
||||
|
||||
float linearDepth(vec2 uv) {
|
||||
ivec2 p = clamp(ivec2(uv * vec2(pc.w, pc.h)), ivec2(0), ivec2(pc.w - 1, pc.h - 1));
|
||||
float d = fetchTex(pc.depth, p).r;
|
||||
return pc.proj.z * pc.proj.w / (pc.proj.w - d * (pc.proj.w - pc.proj.z));
|
||||
}
|
||||
|
||||
void main() {
|
||||
ivec2 o = ivec2(gl_FragCoord.xy);
|
||||
vec2 uv = gl_FragCoord.xy / vec2(pc.outW, pc.outH);
|
||||
float z = linearDepth(uv);
|
||||
float sum = 0.0, weight = 0.0;
|
||||
for (int y = -2; y < 2; ++y)
|
||||
for (int x = -2; x < 2; ++x) {
|
||||
ivec2 q = clamp(o + ivec2(x, y), ivec2(0), ivec2(pc.outW - 1, pc.outH - 1));
|
||||
float zq = linearDepth((vec2(q) + 0.5) / vec2(pc.outW, pc.outH));
|
||||
float wgt = max(0.0, 1.0 - abs(zq - z) / (0.05 * z + 1.0));
|
||||
sum += fetchTex(pc.ao, q).r * wgt;
|
||||
weight += wgt;
|
||||
}
|
||||
o_ao = vec4(weight > 0.0 ? sum / weight : 1.0);
|
||||
}
|
||||
82
engine/shaders/ssr.frag
Normal file
82
engine/shaders/ssr.frag
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#version 460
|
||||
// Screen-space reflections (half resolution): from each pixel, the view ray mirrored about its
|
||||
// normal (the mesh pass's G-buffer) is marched through view space against the depth buffer;
|
||||
// on a hit (refined by bisection) it takes this frame's lit scene there. Fades with roughness,
|
||||
// distance, the screen's edges and rays towards the camera, so it blends into the sky and
|
||||
// probe reflections the mesh pass already gave. TAA (or the tonemap) adds it to the scene.
|
||||
// Rows are the targets' own (uv from gl_FragCoord), on both backends.
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
PUSH_BLOCK {
|
||||
uint depth, gbuf, scene;
|
||||
uint w, h; // full size
|
||||
uint outW, outH; // this target's
|
||||
float strength;
|
||||
vec4 proj; // tan(half fov x), tan(half fov y), near, far
|
||||
} pc;
|
||||
layout(location = 0) out vec4 o_ssr;
|
||||
|
||||
float viewZ(vec2 uv) {
|
||||
ivec2 p = clamp(ivec2(uv * vec2(pc.w, pc.h)), ivec2(0), ivec2(pc.w - 1, pc.h - 1));
|
||||
float d = fetchTex(pc.depth, p).r;
|
||||
return d >= 1.0 ? 1e9 : pc.proj.z * pc.proj.w / (pc.proj.w - d * (pc.proj.w - pc.proj.z));
|
||||
}
|
||||
vec3 viewPos(vec2 uv) { // across the screen from the centre of the pixel its depth came from (as ssao.frag)
|
||||
float z = viewZ(uv);
|
||||
ivec2 p = clamp(ivec2(uv * vec2(pc.w, pc.h)), ivec2(0), ivec2(pc.w - 1, pc.h - 1));
|
||||
vec2 at = (vec2(p) + 0.5) / vec2(pc.w, pc.h);
|
||||
return vec3((at * 2.0 - 1.0) * pc.proj.xy * z, z);
|
||||
}
|
||||
vec2 toUv(vec3 v) { return v.xy / (v.z * pc.proj.xy) * 0.5 + 0.5; }
|
||||
vec3 octDecode(vec2 e) {
|
||||
e = e * 2.0 - 1.0;
|
||||
vec3 n = vec3(e, 1.0 - abs(e.x) - abs(e.y));
|
||||
if (n.z < 0.0) n.xy = (1.0 - abs(n.yx)) * vec2(n.x >= 0.0 ? 1.0 : -1.0, n.y >= 0.0 ? 1.0 : -1.0);
|
||||
return normalize(n);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / vec2(pc.outW, pc.outH);
|
||||
o_ssr = vec4(0.0);
|
||||
vec4 g = fetchTex(pc.gbuf, clamp(ivec2(uv * vec2(pc.w, pc.h)), ivec2(0), ivec2(pc.w - 1, pc.h - 1)));
|
||||
float rough = g.z, weight = g.w;
|
||||
if (weight <= 0.01 || rough > 0.65) return;
|
||||
vec3 p = viewPos(uv);
|
||||
if (p.z > 1e8) return;
|
||||
vec3 n = octDecode(g.xy);
|
||||
vec3 v = normalize(p);
|
||||
vec3 r = reflect(v, n);
|
||||
if (r.z < -0.2) return; // back at the camera: nothing on screen to see there
|
||||
// March: steps grow with distance; thickness grows with them.
|
||||
float jitter = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
||||
float t = 4.0 + 6.0 * jitter, stepLen = 6.0;
|
||||
const int STEPS = 40;
|
||||
vec2 hit = vec2(-1.0);
|
||||
float hitT = 0.0;
|
||||
for (int i = 0; i < STEPS; ++i) {
|
||||
vec3 q = p + r * t;
|
||||
if (q.z <= pc.proj.z) break;
|
||||
vec2 quv = toUv(q);
|
||||
if (any(lessThan(quv, vec2(0.0))) || any(greaterThan(quv, vec2(1.0)))) break;
|
||||
float sceneZ = viewZ(quv);
|
||||
float behind = q.z - sceneZ;
|
||||
if (behind > 0.0 && behind < stepLen * 2.0 + 8.0) {
|
||||
float lo = t - stepLen, hi = t; // refine between the last two steps
|
||||
for (int k = 0; k < 5; ++k) {
|
||||
float mid = (lo + hi) * 0.5;
|
||||
vec3 m = p + r * mid;
|
||||
if (m.z - viewZ(toUv(m)) > 0.0) hi = mid; else lo = mid;
|
||||
}
|
||||
hitT = hi;
|
||||
hit = toUv(p + r * hi);
|
||||
break;
|
||||
}
|
||||
t += stepLen;
|
||||
stepLen *= 1.12;
|
||||
}
|
||||
if (hit.x < 0.0) return;
|
||||
vec2 edge = smoothstep(vec2(0.0), vec2(0.12), hit) * smoothstep(vec2(0.0), vec2(0.12), 1.0 - hit);
|
||||
float fade = edge.x * edge.y * (1.0 - smoothstep(0.3, 0.65, rough)) * (1.0 - smoothstep(1500.0, 3000.0, hitT)) * smoothstep(-0.2, 0.1, r.z + 0.1);
|
||||
vec3 c = min(sampleTexLod(pc.scene, hit, 0.0).rgb, vec3(16.0));
|
||||
o_ssr = vec4(c * weight * fade * pc.strength, fade);
|
||||
}
|
||||
69
engine/shaders/taa.frag
Normal file
69
engine/shaders/taa.frag
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#version 460
|
||||
// Temporal anti-aliasing (as id Tech 6's TSSAA, in spirit): the scene is drawn a sub-pixel off
|
||||
// each frame; each pixel finds where it was last frame (its depth, last frame's camera) and
|
||||
// blends with what was there, after clamping that to the colours around it now (variance
|
||||
// clipping in YCoCg) so moved things don't leave ghosts. Weighted by brightness so highlights
|
||||
// don't flicker. Rows are the targets' own (uv from gl_FragCoord), on both backends.
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
#include "upsample.glsl"
|
||||
PUSH_BLOCK {
|
||||
mat4 reproject; // this frame's clip space → last frame's
|
||||
uint scene, history, depth;
|
||||
uint w, h;
|
||||
uint reset; // no usable history (first frame, resized, teleported)
|
||||
uint ssr; // reflections to add first (half size), 0 = none
|
||||
float pad1;
|
||||
} pc;
|
||||
layout(location = 0) out vec4 o_color;
|
||||
|
||||
// The scene with the half-size reflections / volumetric light on it: depth-aware for the pixel
|
||||
// itself (no lines along edges), a plain read for its neighbours (only the colour clamp uses them).
|
||||
vec3 sceneAt(ivec2 q) {
|
||||
vec3 c = fetchTex(pc.scene, q).rgb;
|
||||
if (pc.ssr != 0u) c += sampleTexLod(pc.ssr, (vec2(q) + 0.5) / vec2(pc.w, pc.h), 0.0).rgb;
|
||||
return c;
|
||||
}
|
||||
vec3 sceneAtCentre(ivec2 q) {
|
||||
vec3 c = fetchTex(pc.scene, q).rgb;
|
||||
if (pc.ssr != 0u) c += upsampleDepthAware(pc.ssr, pc.depth, q).rgb;
|
||||
return c;
|
||||
}
|
||||
|
||||
vec3 toYCoCg(vec3 c) { return vec3(dot(c, vec3(0.25, 0.5, 0.25)), dot(c, vec3(0.5, 0.0, -0.5)), dot(c, vec3(-0.25, 0.5, -0.25))); }
|
||||
vec3 fromYCoCg(vec3 c) { return vec3(c.x + c.y - c.z, c.x + c.z, c.x - c.y - c.z); }
|
||||
float weight(vec3 c) { return 1.0 / (1.0 + dot(c, vec3(0.2126, 0.7152, 0.0722))); }
|
||||
|
||||
void main() {
|
||||
ivec2 p = ivec2(gl_FragCoord.xy);
|
||||
ivec2 last = ivec2(pc.w - 1, pc.h - 1);
|
||||
vec3 cur = sceneAtCentre(p).rgb;
|
||||
if (pc.reset != 0u) { o_color = vec4(cur, 1.0); return; }
|
||||
// The colours around this pixel now (mean ± deviation), and the nearest depth among them
|
||||
// (so an edge's history comes from the thing in front).
|
||||
vec3 m1 = vec3(0.0), m2 = vec3(0.0);
|
||||
float nearest = 1.0;
|
||||
ivec2 at = p;
|
||||
for (int y = -1; y <= 1; ++y)
|
||||
for (int x = -1; x <= 1; ++x) {
|
||||
ivec2 q = clamp(p + ivec2(x, y), ivec2(0), last);
|
||||
vec3 c = toYCoCg(sceneAt(q).rgb);
|
||||
m1 += c;
|
||||
m2 += c * c;
|
||||
float d = fetchTex(pc.depth, q).r;
|
||||
if (d < nearest) nearest = d, at = q;
|
||||
}
|
||||
vec3 mu = m1 / 9.0, sigma = sqrt(abs(m2 / 9.0 - mu * mu));
|
||||
vec3 lo = mu - 1.25 * sigma, hi = mu + 1.25 * sigma;
|
||||
vec2 uv = (vec2(p) + 0.5) / vec2(pc.w, pc.h);
|
||||
vec4 prev = pc.reproject * vec4(((vec2(at) + 0.5) / vec2(pc.w, pc.h)) * 2.0 - 1.0, nearest, 1.0);
|
||||
vec2 puv = prev.xy / prev.w * 0.5 + 0.5 + (uv - (vec2(at) + 0.5) / vec2(pc.w, pc.h));
|
||||
if (any(lessThan(puv, vec2(0.0))) || any(greaterThan(puv, vec2(1.0)))) { o_color = vec4(cur, 1.0); return; }
|
||||
vec3 hist = sampleTex(pc.history, puv).rgb;
|
||||
hist = fromYCoCg(clamp(toYCoCg(hist), lo, hi));
|
||||
// Less history when things move a lot on screen (it smears), more when still.
|
||||
float moved = length((puv - uv) * vec2(pc.w, pc.h));
|
||||
float keep = mix(0.92, 0.7, clamp(moved / 24.0, 0.0, 1.0));
|
||||
float wc = (1.0 - keep) * weight(cur), wh = keep * weight(hist);
|
||||
o_color = vec4((cur * wc + hist * wh) / (wc + wh), 1.0);
|
||||
}
|
||||
78
engine/shaders/tonemap.frag
Normal file
78
engine/shaders/tonemap.frag
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#version 460
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
#include "upsample.glsl"
|
||||
// HDR scene → display: ambient occlusion, bloom, exposure, ACES filmic curve, a light grade
|
||||
// (saturation, vignette), then sRGB encoding.
|
||||
PUSH_BLOCK {
|
||||
uint scene;
|
||||
float exposure;
|
||||
uint ao; // 0 = none
|
||||
uint bloom; // 0 = none
|
||||
float bloomStrength;
|
||||
float saturation;
|
||||
float vignette;
|
||||
float sharpen; // after TAA: give back a little crispness (0 = none)
|
||||
uint depth; // scene depth (1 = sky)
|
||||
uint ssr; // reflections to add (when TAA didn't), 0 = none
|
||||
uint adapt; // eye adaptation (1×1 exposure multiplier), 0 = none
|
||||
uint pad2;
|
||||
vec4 shafts; // sun shafts: the sun's uv, strength (0 = none)
|
||||
vec4 sunColor;
|
||||
} pc;
|
||||
|
||||
// Sun shafts (screen-space light scattering, GPU Gems 3 ch. 13): march towards the sun's spot
|
||||
// on screen, adding up how much open sky lies on the way; air lit by the sun glows there,
|
||||
// fading with distance from the sun. Started at a per-pixel offset so the steps don't band.
|
||||
vec3 sunShafts(vec2 uv) {
|
||||
const int N = 32;
|
||||
vec2 toSun = pc.shafts.xy - uv;
|
||||
vec2 stepUv = toSun * (0.85 / float(N));
|
||||
float jitter = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
||||
vec2 p = uv + stepUv * jitter;
|
||||
vec3 lit = vec3(0.0);
|
||||
float w = 1.0, total = 0.0;
|
||||
for (int i = 0; i < N; ++i, p += stepUv) {
|
||||
bool inside = all(greaterThanEqual(p, vec2(0.0))) && all(lessThanEqual(p, vec2(1.0)));
|
||||
if (inside && sampleTexLod(pc.depth, p, 0.0).r >= 1.0) // open sky: its light, the sun's brightest
|
||||
lit += max(sampleTexLod(pc.scene, p, 0.0).rgb - 1.0, vec3(0.0)) * w;
|
||||
total += w;
|
||||
w *= 0.97;
|
||||
}
|
||||
vec2 aspect = vec2(float(texSize(pc.scene).x) / float(texSize(pc.scene).y), 1.0);
|
||||
float fall = exp(-length(toSun * aspect) * 1.5);
|
||||
return min(lit / total, vec3(8.0)) * fall * pc.shafts.z;
|
||||
}
|
||||
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 0) out vec4 o_color;
|
||||
|
||||
vec3 aces(vec3 x) {
|
||||
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
|
||||
}
|
||||
|
||||
vec3 toSrgb(vec3 c) {
|
||||
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 hdr = sampleTex(pc.scene, v_uv).rgb;
|
||||
const ivec2 px = ivec2(v_uv * vec2(texSize(pc.depth)));
|
||||
if (pc.ssr != 0u) hdr += upsampleDepthAware(pc.ssr, pc.depth, px).rgb;
|
||||
if (pc.sharpen > 0.0) { // unsharp mask against the 4 neighbours, limited so edges don't ring
|
||||
vec2 t = 1.0 / vec2(texSize(pc.scene));
|
||||
vec3 n = sampleTex(pc.scene, v_uv + vec2(t.x, 0)).rgb + sampleTex(pc.scene, v_uv - vec2(t.x, 0)).rgb
|
||||
+ sampleTex(pc.scene, v_uv + vec2(0, t.y)).rgb + sampleTex(pc.scene, v_uv - vec2(0, t.y)).rgb;
|
||||
hdr = max(hdr + clamp((hdr - n * 0.25) * pc.sharpen, -0.1 * (hdr + 0.05), 0.1 * (hdr + 0.05)), 0.0);
|
||||
}
|
||||
if (pc.ao != 0u) hdr *= upsampleDepthAware(pc.ao, pc.depth, px).r;
|
||||
if (pc.bloom != 0u) hdr = mix(hdr, sampleTex(pc.bloom, v_uv).rgb, pc.bloomStrength);
|
||||
if (pc.shafts.z > 0.0) hdr += sunShafts(v_uv);
|
||||
float exposure = pc.exposure * (pc.adapt != 0u ? sampleTexLod(pc.adapt, vec2(0.5), 0.0).r : 1.0);
|
||||
vec3 c = aces(hdr * exposure);
|
||||
float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
||||
c = max(mix(vec3(l), c, pc.saturation), 0.0);
|
||||
vec2 d = v_uv - 0.5;
|
||||
c *= 1.0 - pc.vignette * dot(d, d) * 2.0;
|
||||
o_color = vec4(toSrgb(c), 1.0);
|
||||
}
|
||||
28
engine/shaders/upsample.glsl
Normal file
28
engine/shaders/upsample.glsl
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Depth-aware ("bilateral") upsampling of a half-size target (ambient occlusion, reflections,
|
||||
// volumetric light): the four nearest texels, each weighted by how close its depth is to this
|
||||
// pixel's, so what's behind an edge doesn't bleed onto it (a plain bilinear read leaves dark or
|
||||
// bright lines along corners and silhouettes).
|
||||
float linearDepth(float d) {
|
||||
const float n = 2.0, f = 16384.0; // the camera's near and far planes (render/renderer.cpp)
|
||||
return d >= 1.0 ? f : n * f / (f - d * (f - n));
|
||||
}
|
||||
vec4 upsampleDepthAware(uint tex, uint depthTex, ivec2 px) {
|
||||
ivec2 full = texSize(depthTex), half_ = texSize(tex);
|
||||
vec2 hp = (vec2(px) + 0.5) * vec2(half_) / vec2(full) - 0.5;
|
||||
ivec2 b = ivec2(floor(hp));
|
||||
vec2 f = hp - vec2(b);
|
||||
float z = linearDepth(fetchTex(depthTex, clamp(px, ivec2(0), full - 1)).r);
|
||||
vec4 sum = vec4(0.0);
|
||||
float wsum = 0.0;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
ivec2 o = ivec2(k & 1, k >> 1);
|
||||
ivec2 t = clamp(b + o, ivec2(0), half_ - 1);
|
||||
ivec2 tf = clamp((t * full) / half_ + (full / half_) / 2, ivec2(0), full - 1); // the full-size texel it stands for
|
||||
float zt = linearDepth(fetchTex(depthTex, tf).r);
|
||||
float bil = (o.x == 1 ? f.x : 1.0 - f.x) * (o.y == 1 ? f.y : 1.0 - f.y);
|
||||
float w = bil / (0.002 + abs(zt - z) / max(z, 1.0));
|
||||
sum += fetchTex(tex, t) * w;
|
||||
wsum += w;
|
||||
}
|
||||
return wsum > 0.0 ? sum / wsum : fetchTex(tex, clamp(b, ivec2(0), half_ - 1));
|
||||
}
|
||||
56
engine/shaders/volumetric.frag
Normal file
56
engine/shaders/volumetric.frag
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#version 460
|
||||
// Volumetric light (half resolution): the sun and the dynamic lights scattered towards the eye
|
||||
// by the air along each view ray, where they reach it (12 jittered steps between the eye and the
|
||||
// depth buffer; each takes the sun's shadow and, for every light in that spot's cluster, its
|
||||
// falloff, cone and a one-tap shadow; Henyey-Greenstein phase), so window frames cut shafts out of
|
||||
// the sun and lamps glow in the haze (like Source 2's volumetric fog). Once per pixel, whatever overdraw the scene had. Added onto the reflections
|
||||
// target (TAA, or the tonemap, adds that to the scene). Rows: the targets' own, both backends.
|
||||
#define KEEL_POST // screen-space: render targets, sampled clamped
|
||||
#include "backend.glsl"
|
||||
#define KEEL_FRAME_ONLY
|
||||
#include "model.glsl"
|
||||
PUSH_BLOCK {
|
||||
#ifndef KEEL_GL
|
||||
FrameData frame;
|
||||
#endif
|
||||
uint depth;
|
||||
uint w, h, outW, outH;
|
||||
float pad0, pad1, pad2;
|
||||
vec4 proj; // tan(half fov x), tan(half fov y), near, far
|
||||
} pc;
|
||||
#ifndef KEEL_GL
|
||||
#define FRAME pc.frame
|
||||
#endif
|
||||
layout(location = 0) out vec4 o_light;
|
||||
|
||||
#include "airlight.glsl"
|
||||
|
||||
void main() {
|
||||
o_light = vec4(0.0);
|
||||
if (FRAME.volumetric.x <= 0.0) return;
|
||||
const bool sun = FRAME.sunColor.w > 0.5 && FRAME.volumetric.w > 0.5;
|
||||
vec2 uv = gl_FragCoord.xy / vec2(pc.outW, pc.outH);
|
||||
ivec2 px = clamp(ivec2(uv * vec2(pc.w, pc.h)), ivec2(0), ivec2(pc.w - 1, pc.h - 1));
|
||||
float d = fetchTex(pc.depth, px).r;
|
||||
float z = d >= 1.0 ? 1e9 : pc.proj.z * pc.proj.w / (pc.proj.w - d * (pc.proj.w - pc.proj.z));
|
||||
// The view ray in world space (view: x right, y down the screen, z forward).
|
||||
vec3 fwd = FRAME.camFwd.xyz, right = normalize(cross(fwd, vec3(0.0, 0.0, 1.0))), up = cross(right, fwd);
|
||||
vec2 s = (uv * 2.0 - 1.0) * pc.proj.xy;
|
||||
vec3 rd = normalize(fwd + right * s.x - up * s.y);
|
||||
float dist = z * length(vec3(s, 1.0));
|
||||
float len = min(dist, FRAME.volumetric.y);
|
||||
float jitter = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
||||
float lit = 0.0;
|
||||
vec3 lamps = vec3(0.0);
|
||||
const int N = 12;
|
||||
for (int i = 0; i < N; ++i) {
|
||||
vec3 w = FRAME.camPos.xyz + rd * len * ((float(i) + jitter) / float(N));
|
||||
if (sun) lit += sunlitAt(w);
|
||||
lamps += lightsAt(w, rd, uv);
|
||||
}
|
||||
float scattered = 1.0 - exp(-len * FRAME.volumetric.z);
|
||||
vec3 sunPart = sun ? FRAME.sunColor.rgb * (lit / float(N)) * phaseHG(dot(rd, FRAME.sunDir.xyz), 0.6) : vec3(0.0);
|
||||
// Lamps: each step's light over the stretch it stands for (the air between them is lit too).
|
||||
vec3 lampPart = lamps / float(N) * scattered * 7.0;
|
||||
o_light = vec4((sunPart * scattered + lampPart) * FRAME.volumetric.x, 0.0);
|
||||
}
|
||||
Loading…
Reference in a new issue