nx_castview.nx source
↩ module page · 106 lines · 5669 B
1// nx_castview.nx -- render a mesh3 by RAY-CASTING into (textured image, EXACT depth map).
2//
3// WHY: every reconstruction test in this arc has used a flat synthetic plane. That validates the maths but
4// not the case that matters -- a real machined part with curvature, facets, holes and silhouettes. Real
5// PHOTOGRAPHS are the eventual gate, but they are not the only way past a plane: the as1 STEP file gives
6// REAL CAD part geometry, and ray-casting it produces views WITH exact ground-truth depth per pixel. That
7// turns the whole chain into SCAN-vs-CAD dimensional inspection -- reconstruct a part, compare the result
8// against the CAD it came from -- which is a named twin capability and needs no camera.
9//
10// ⚠SCALE: r3_ray returns a Q14 (R3_Q) unit direction; mtk_ray_tri works in fx256. Converting between them
11// is mandatory -- feeding a Q14 direction to the fx256 caster silently mis-scales every hit distance.
12// license_tier: ORIGINAL
13import "nx_planesweep.nx"
14import "nx_meshthick.nx"
15
16const CV_TEXQ: i64 = 40 // texture block size in world (fx256) units -- the historic floor
17// ★pixels a texture block should span. MEASURED AND BRACKETED on the real as1 bracket (nx_texscale_gate),
18// not guessed: sweeping 0.4 -> 256 px the inlier fraction rises 220 -> 404 -> 194 per thousand, peaking at
19// 32 px, with error degrading on BOTH sides. The two ends fail for DIFFERENT reasons -- below ~1 px the
20// texture DECORRELATES between views (the two cameras sample different world points per pixel, so noise
21// carries no shared signal, and 10.6% of pixels end up with no discrimination at all), while above the
22// frame size the whole image is one block and there is nothing left to match.
23// ⚠the optimum is LARGER than the 9x9 matching patch, which looks wrong until you see why: a patch does not
24// need texture at every pixel, it needs ONE UNAMBIGUOUS EDGE crossing it, and blocks ~3.5x the patch width
25// maximise the number of patches containing exactly one.
26const CV_TARGET_PX: i64 = 32
27
28// procedural surface texture keyed on ALL THREE axes, so faces of any orientation carry detail. Keying on
29// (x,y) alone leaves surfaces parallel to the view axis flat and therefore unmatchable.
30func cv_tex(x: i64, y: i64, z: i64) -> i64 {
31 return cv_tex_q(x, y, z, CV_TEXQ)
32}
33// ⚠⚠SAME CLASS AS THE SOLVER PROBE STEP: CV_TEXQ is a block size in WORLD units, and whether the texture is
34// usable depends on how many PIXELS a block covers -- f*q/dist. At the real-part geometry scanvcad actually
35// uses (f 150, dist 30000) a 40-unit block projects to 0.2 PIXELS, i.e. FIVE BLOCKS PER PIXEL. nx_planesweep
36// already documents exactly this failure for ps_tex: texture finer than a pixel ALIASES, the patch sees noise
37// instead of structure, and the match score stops being meaningful. So the block size must be chosen in
38// pixels, not world units.
39func cv_tex_q(x: i64, y: i64, z: i64, q: i64) -> i64 {
40 var qq: i64 = q
41 if qq < 1 { qq = 1 }
42 var a: i64 = (x + z) / qq
43 var b: i64 = (y - z) / qq
44 if x + z < 0 { a = a - 1 }
45 if y - z < 0 { b = b - 1 }
46 var h: i64 = (a * 7919 + b * 104729) % 1021
47 if h < 0 { h = 0 - h }
48 var c: i64 = (x - y) / (qq * 4)
49 if x - y < 0 { c = c - 1 }
50 var g: i64 = (c * 40503) % 733
51 if g < 0 { g = 0 - g }
52 return 40 + (h % 150) + (g % 60)
53}
54// block size that projects to CV_TARGET_PX pixels at this working distance, floored at the historic constant
55func cv_texq_for(dist: i64, f: i64) -> i64 {
56 if f <= 0 { return CV_TEXQ }
57 var q: i64 = (dist * CV_TARGET_PX) / f
58 if q < CV_TEXQ { q = CV_TEXQ }
59 return q
60}
61// Ray-cast `mesh` from a camera. img gets the textured intensity, depth gets the EXACT ray distance
62// (-1 where the ray misses). Returns the number of pixels that hit the part.
63// ★the default now SIZES THE TEXTURE TO THE VIEW instead of trusting a world-unit constant. The working
64// distance is taken from the camera to the mesh's own AABB centre, so no caller has to know or pass it.
65func cv_render(mesh: i64, cx: i64, cy: i64, cz: i64, basis: *i64, f: i64, w: i64, h: i64, img: *i64, depth: *i64) -> i64 {
66 let bx: *i64 = sys_mmap(64) as *i64
67 var q: i64 = CV_TEXQ
68 if m3_aabb(mesh, bx) == 0 {
69 let mx: i64 = (bx[0] + bx[3]) / 2 - cx
70 let my: i64 = (bx[1] + bx[4]) / 2 - cy
71 let mz: i64 = (bx[2] + bx[5]) / 2 - cz
72 q = cv_texq_for(r3_isqrt(mx * mx + my * my + mz * mz), f)
73 }
74 return cv_render_q(mesh, cx, cy, cz, basis, f, w, h, img, depth, q)
75}
76func cv_render_q(mesh: i64, cx: i64, cy: i64, cz: i64, basis: *i64, f: i64, w: i64, h: i64, img: *i64, depth: *i64, q: i64) -> i64 {
77 let d3: *i64 = sys_mmap(32) as *i64
78 var hits: i64 = 0
79 var py: i64 = 0
80 while py < h {
81 var px: i64 = 0
82 while px < w {
83 r3_ray(basis, f, px - w / 2, py - h / 2, d3)
84 // Q14 unit dir -> fx256 unit dir for the Moller-Trumbore caster
85 let dx: i64 = (d3[0] * 256) / R3_Q
86 let dy: i64 = (d3[1] * 256) / R3_Q
87 let dz: i64 = (d3[2] * 256) / R3_Q
88 let t: i64 = mtk_cast(mesh, cx, cy, cz, dx, dy, dz, 0 - 1)
89 var val: i64 = 0
90 var dep: i64 = 0 - 1
91 if t > 0 {
92 let wx: i64 = cx + (d3[0] * t) / R3_Q
93 let wy: i64 = cy + (d3[1] * t) / R3_Q
94 let wz: i64 = cz + (d3[2] * t) / R3_Q
95 val = cv_tex_q(wx, wy, wz, q)
96 dep = t
97 hits = hits + 1
98 }
99 img[py * w + px] = val
100 depth[py * w + px] = dep
101 px = px + 1
102 }
103 py = py + 1
104 }
105 return hits
106}