nx_sdf.nx source
↩ module page · 120 lines · 5661 B
1// nx_sdf.nx -- signed distance fields for solid primitives + boolean combinators,
2// the algebra under CSG (Constructive Solid Geometry). A primitive returns a Q14-mm
3// signed distance (negative = inside). Booleans are min/max combinators -- which is
4// why SDF CSG is UNCONDITIONALLY robust: union/difference/intersection can never fail
5// or self-intersect the way B-rep mesh booleans do on degenerate geometry. Sign-
6// correct fields (the zero-isosurface is the exact surface) are all marching needs.
7// Reuses nx_isqrt. No dup: grepped, no nx_sdf/csg existed. license_tier: ORIGINAL
8
9import "nx_syscalls.nx"
10import "nx_isqrt.nx"
11
12const NX_SDF_SPHERE: i64 = 0
13const NX_SDF_BOX: i64 = 1
14const NX_SDF_CYL: i64 = 2 // axis = Z
15const NX_SDF_TORUS: i64 = 3 // hole along Z; a = major R, b = minor r
16const NX_SDF_ROUNDBOX: i64 = 4 // a,b,c = half extents; d = corner round radius
17
18// kind + centre (cx,cy,cz) + params a,b,c:
19// sphere: a = radius
20// box: a,b,c = half extents (x,y,z)
21// cylinder: a = radius, b = half height (along Z)
22struct NxSdfPrim { kind: i64, cx: i64, cy: i64, cz: i64, a: i64, b: i64, c: i64, d: i64 }
23
24func sdf_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
25func sdf_max(a: i64, b: i64) -> i64 { if a > b { return a } return b }
26func sdf_min(a: i64, b: i64) -> i64 { if a < b { return a } return b }
27
28func nx_sdf_make(kind: i64, cx: i64, cy: i64, cz: i64, a: i64, b: i64, c: i64) -> *NxSdfPrim {
29 let p: *NxSdfPrim = (sys_mmap(64)) as *NxSdfPrim
30 p.kind = kind; p.cx = cx; p.cy = cy; p.cz = cz; p.a = a; p.b = b; p.c = c; p.d = 0
31 return p
32}
33
34// 4-param variant (the 4th param `d` feeds primitives that need an extra scalar,
35// e.g. the rounded box's corner radius). Existing callers keep using nx_sdf_make.
36func nx_sdf_make4(kind: i64, cx: i64, cy: i64, cz: i64, a: i64, b: i64, c: i64, d: i64) -> *NxSdfPrim {
37 let p: *NxSdfPrim = (sys_mmap(64)) as *NxSdfPrim
38 p.kind = kind; p.cx = cx; p.cy = cy; p.cz = cz; p.a = a; p.b = b; p.c = c; p.d = d
39 return p
40}
41
42// signed distance (Q14 mm) of point (x,y,z) to primitive p. <0 = inside.
43func nx_sdf_eval(p: *NxSdfPrim, x: i64, y: i64, z: i64) -> i64 {
44 let dx: i64 = x - p.cx
45 let dy: i64 = y - p.cy
46 let dz: i64 = z - p.cz
47 if p.kind == NX_SDF_SPHERE {
48 return nx_isqrt(dx * dx + dy * dy + dz * dz) - p.a
49 }
50 if p.kind == NX_SDF_BOX {
51 // sign-correct box field: zero-set is exactly the box surface
52 return sdf_max(sdf_max(sdf_abs(dx) - p.a, sdf_abs(dy) - p.b), sdf_abs(dz) - p.c)
53 }
54 if p.kind == NX_SDF_TORUS {
55 // distance from the ring of radius a (major) in the XY plane, then minus b (minor)
56 let q: i64 = nx_isqrt(dx * dx + dy * dy) - p.a
57 return nx_isqrt(q * q + dz * dz) - p.b
58 }
59 if p.kind == NX_SDF_ROUNDBOX {
60 // EXACT (Euclidean) box SDF minus the round radius d -> rounded edges/corners
61 let qx: i64 = sdf_abs(dx) - p.a
62 let qy: i64 = sdf_abs(dy) - p.b
63 let qz: i64 = sdf_abs(dz) - p.c
64 let ox: i64 = sdf_max(qx, 0)
65 let oy: i64 = sdf_max(qy, 0)
66 let oz: i64 = sdf_max(qz, 0)
67 let outside: i64 = nx_isqrt(ox * ox + oy * oy + oz * oz)
68 let inside: i64 = sdf_min(sdf_max(qx, sdf_max(qy, qz)), 0)
69 return outside + inside - p.d
70 }
71 // cylinder along Z: radial distance & axial slab, intersected
72 let dr: i64 = nx_isqrt(dx * dx + dy * dy) - p.a
73 let dh: i64 = sdf_abs(dz) - p.b
74 return sdf_max(dr, dh)
75}
76
77const NX_CSG_UNION: i64 = 0
78const NX_CSG_INTERSECT: i64 = 1
79const NX_CSG_DIFF: i64 = 2 // A minus B
80
81// combine two signed distances under a boolean op.
82func nx_csg_combine(op: i64, da: i64, db: i64) -> i64 {
83 if op == NX_CSG_UNION { return sdf_min(da, db) }
84 if op == NX_CSG_INTERSECT { return sdf_max(da, db) }
85 return sdf_max(da, 0 - db) // A - B (subtract B by intersecting with its complement)
86}
87
88// the combined field of a two-primitive CSG scene.
89func nx_csg_field(a: *NxSdfPrim, b: *NxSdfPrim, op: i64, x: i64, y: i64, z: i64) -> i64 {
90 return nx_csg_combine(op, nx_sdf_eval(a, x, y, z), nx_sdf_eval(b, x, y, z))
91}
92
93// ===== SMOOTH booleans (fillets) =================================
94//
95// Polynomial smooth-minimum (iquilezles form) in Q14: blends two fields over a
96// radius k_q14, producing a fillet at the junction. This is where SDF CSG is
97// genuinely AHEAD of B-rep: a fillet that is unconditionally robust + a single
98// scalar parameter, no edge-loop surgery, never fails on tight geometry.
99func nx_csg_smin(a: i64, b: i64, k: i64) -> i64 {
100 if k <= 0 { if a < b { return a } return b }
101 var hq: i64 = 8192 + (8192 * (b - a)) / k // h = clamp(0.5 + 0.5*(b-a)/k) in Q14
102 if hq < 0 { hq = 0 }
103 if hq > 16384 { hq = 16384 }
104 let term1: i64 = b + ((a - b) * hq) / 16384 // mix(b,a,h)
105 let hh: i64 = (hq * (16384 - hq)) / 16384 // h*(1-h) (Q14)
106 let term2: i64 = (k * hh) / 16384 // k*h*(1-h)
107 return term1 - term2
108}
109
110// smooth combine; k_q14<=0 falls back to the sharp boolean (so callers can pass 0).
111func nx_csg_combine_k(op: i64, da: i64, db: i64, k: i64) -> i64 {
112 if k <= 0 { return nx_csg_combine(op, da, db) }
113 if op == NX_CSG_UNION { return nx_csg_smin(da, db, k) }
114 if op == NX_CSG_INTERSECT { return 0 - nx_csg_smin(0 - da, 0 - db, k) } // smooth max
115 return 0 - nx_csg_smin(0 - da, db, k) // smooth difference A - B
116}
117
118func nx_csg_field_k(a: *NxSdfPrim, b: *NxSdfPrim, op: i64, k: i64, x: i64, y: i64, z: i64) -> i64 {
119 return nx_csg_combine_k(op, nx_sdf_eval(a, x, y, z), nx_sdf_eval(b, x, y, z), k)
120}