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