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:
Nexoniarz 2026-10-19 21:18:35 +02:00
commit f3e8eca87b
135 changed files with 23423 additions and 0 deletions

View 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);
}