code wiki / (root) / nx_castview.nx

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}