mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
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
218 lines
12 KiB
GLSL
218 lines
12 KiB
GLSL
// 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);
|
|
}
|