code wiki / (root) / nx_uvunwrap_lib.nx

nx_uvunwrap_lib.nx source

↩ module page · 1642 lines · 75241 B

1// nx_uvunwrap_lib.nx -- A GENERAL UV UNWRAP FOR AN ARBITRARY MESH, WITH A DISTORTION REFEREE (/compare/dcc DC5). 2// 3// WHAT WAS ABSENT, MEASURED NOT ASSERTED. nx_capsearch over 6,892 organs (1,375 tools + 5,517 libs, 4// corpus_complete=1) returned NO general parameterisation: the estate's only unwrap is ntx_apply in 5// nx_nxa_texc_lib, a bone-anchored CYLINDRICAL map that reads SKEL parents and SKIN weights and refuses 6// outright without them ("no SKIN section -- refusing to fabricate bone ownership"). That is the right 7// design for a humanoid NXA and it is unreachable for everything else: a sculpt, a boolean result, a 8// terrain patch, a retopologised prop has no skeleton to anchor to, so it could not be unwrapped AT ALL, 9// and texture painting was blocked for every non-humanoid asset in the estate. 10// 11// OUTPUT IS WIRE-COMPATIBLE WITH THE INCUMBENT BAKE PATH ON PURPOSE. uv_emit_texc writes exactly the TEXC 12// payload nx_nxa_texbake_lib already consumes -- header [nv][stride 3][atlas grid g][reserved 0] then per 13// vertex [u Q16][v Q16][chart], charts owning disjoint tiles of a g x g grid with a one-unit gutter, the 14// same shape and the same fixed-point scale ntx_apply emits. So a mesh unwrapped here bakes through the 15// existing sampler with nothing else changed. 16// BUT THE BRIDGE IS LOSSY AND IT SAYS SO. TEXC is PER VERTEX and a seam is exactly the place where one 17// mesh vertex needs TWO texture coordinates, so the incumbent wire format cannot represent a seam at 18// all. This library solves per CORNER (a mesh vertex split by seams into several UV vertices, which is 19// what makes a cut actually cut), and uv_emit_texc COUNTS the vertices whose extra coordinates the 20// per-vertex form drops, into UV_H_TEXCLOSS. A lossy conversion that reports zero loss would be the 21// defect; one that names its loss is a bridge. 22// 23// THE HONEST ALGORITHM. A least-squares conformal map (Levy et al., LSCM) needs a linear solve, and there 24// is no linear solver in this estate and no float type to build one on. So this is an ITERATIVE relaxation 25// toward the same stationary condition, and every part of that choice is declared rather than hidden: 26// * The energy is the LSCM energy in its Dirichlet-minus-area form, E = sum_edges W|dU|^2 - 2 sum_t A_t. 27// THAT SECOND TERM IS THE WHOLE REASON THIS DOES NOT COLLAPSE. A pure harmonic (Dirichlet-only) 28// relaxation with a free boundary and two pinned vertices has its minimum at the degenerate map that 29// puts every vertex on the segment between the pins -- a fold, scoring zero by every naive measure. 30// The -2*area term rewards area and makes the non-degenerate map the minimiser instead. 31// * Setting dE/dU_p = 0 gives a closed-form per-vertex update, so this is a damped Jacobi sweep over 32// the exact stationary condition rather than an invented heuristic. Interior vertices see a pure 33// cotangent-Laplacian average (their one-ring area terms telescope to zero); the area term acts only 34// on the boundary, pushing it outward. That is LSCM, iterated. 35// * TWO PINNED VERTICES PER CHART, the standard LSCM constraint: the conformal energy is invariant to 36// translation, rotation and scale, so two pins remove exactly the null space and no more. 37// * The iteration bound is DERIVED FROM THE CHART, the convergence criterion is DECLARED, and a chart 38// that reaches neither is REFUSED BY NAME (UV_E_NOCONVERGE) rather than emitted folded. 39// 40// THE REFEREE IS THE POINT. uv_distortion reports angle (quasi-conformal dilatation) and area distortion 41// in permil, mean and max, per chart and overall, measured on the COORDINATES ACTUALLY EMITTED -- after 42// atlas placement, not on some intermediate the caller never sees. Without it a fold is invisible: every 43// rival DCC shows a checker pattern and asks a person to look. A developable surface is exactly 44// parameterisable, so it must read at or near zero, and that is this file's own positive control. 45// 46// COMPOSES THE INCUMBENTS, ADDS NO SECOND RULER: mesh validity is nx_meshvalid_lib (mv_check / mv_count / 47// mv_manifold) -- there is no second topology checker here; integer square root and the fixed-point unit 48// are nx_vecmath (vm_isqrt, VM_ONE); the hash capacity rule and mixing function are meshvalid's mv_cap_for 49// and mv_mix rather than a second copy. 50// 51// ALL INTEGER, so every result is bit-exact and reproducible. No hardware writes (Rule 26). 52// license_tier: ORIGINAL 53import "nx_syscalls.nx" 54import "nx_vecmath.nx" 55import "nx_meshvalid_lib.nx" 56 57// ---- header slots ------------------------------------------------------------------------------ 58const UV_H_NV: i64 = 0 59const UV_H_NT: i64 = 1 60const UV_H_ECAP: i64 = 2 61const UV_H_SCAP: i64 = 3 62const UV_H_NC: i64 = 4 63const UV_H_NUV: i64 = 5 64const UV_H_ERR: i64 = 6 65const UV_H_STAGE: i64 = 7 66const UV_H_GRID: i64 = 8 67const UV_H_NSEAM: i64 = 9 68const UV_H_ITERS: i64 = 10 69const UV_H_MAXMOVE: i64 = 11 70const UV_H_FLIP: i64 = 12 71const UV_H_SING: i64 = 13 72const UV_H_UVDEGEN: i64 = 14 73const UV_H_NOCONV: i64 = 15 74const UV_H_ANGMEAN: i64 = 16 75const UV_H_ANGMAX: i64 = 17 76const UV_H_AREAMEAN: i64 = 18 77const UV_H_AREAMAX: i64 = 19 78const UV_H_OVERLAP: i64 = 20 79const UV_H_TEXCLOSS: i64 = 21 80const UV_H_MVERR: i64 = 22 81const UV_H_MVCLOSED: i64 = 23 82const UV_H_NCORN: i64 = 24 83// ---- array offsets, computed ONCE by uv_layout and stored, never recomputed by a caller ---------- 84const UV_O_VERT: i64 = 25 85const UV_O_TRI: i64 = 26 86const UV_O_ETAB: i64 = 27 87const UV_O_STAB: i64 = 28 88const UV_O_TCHART: i64 = 29 89const UV_O_STACK: i64 = 30 90const UV_O_CSTART: i64 = 31 91const UV_O_CORDER: i64 = 32 92const UV_O_UVF: i64 = 33 93const UV_O_UVSRC: i64 = 34 94const UV_O_UVCH: i64 = 35 95const UV_O_WU: i64 = 36 96const UV_O_WV: i64 = 37 97const UV_O_ACCU: i64 = 38 98const UV_O_ACCV: i64 = 39 99const UV_O_ACCW: i64 = 40 100const UV_O_ACCAU: i64 = 41 101const UV_O_ACCAV: i64 = 42 102const UV_O_CUV: i64 = 43 103const UV_O_TANG: i64 = 44 104const UV_O_TRAT: i64 = 45 105const UV_O_TSAR: i64 = 46 106const UV_O_CBOX: i64 = 47 107const UV_O_CCNT: i64 = 48 108const UV_O_CDIST: i64 = 49 109const UV_O_LIST: i64 = 50 110const UV_O_STAMP: i64 = 51 111const UV_O_TSEEN: i64 = 52 112const UV_O_PLACED: i64 = 53 113const UV_O_ROOT: i64 = 54 114// THE HEADER MUST OUTRANK EVERY SLOT IT USES. nx_meshvalid shipped with a header one word too short and 115// its first-offender slots silently ALIASED vertex 0, moving a corner; every topology class still passed 116// and the only symptom was one detector that could never fire. Highest slot in use here is 53, so 64 117// leaves ten words of headroom AND a structural tooth in the gate asserts the relation arithmetically. 118const UV_HDR: i64 = 64 119const UV_I64: i64 = 8 120const UV_TRI: i64 = 3 121const UV_EREC: i64 = 4 // edge record: [lo, hi, tri0+1, tri1+1] 122const UV_SREC: i64 = 3 // seam record: [lo, hi, used] 123// ...AND THE SLOT NAMES FOR BOTH, because a layout that lives only in the comment above gets hand-counted 124// at every access, which is the same shape as writing the width twice. The UV_ED_/UV_SM_ prefixes avoid 125// the UV_E_ refusal-code space deliberately: two constant families one character apart is its own hazard. 126const UV_ED_LO: i64 = 0 127const UV_ED_HI: i64 = 1 128const UV_ED_T0: i64 = 2 // first triangle, stored +1 so that 0 reads as EMPTY 129const UV_ED_T1: i64 = 3 // second triangle, same +1 bias 130const UV_SM_LO: i64 = 0 131const UV_SM_HI: i64 = 1 132const UV_SM_USED: i64 = 2 133const UV_ACCN: i64 = 5 // accU accV accW accAU accAV 134 135// THREE MORE RECORDS, WHOSE WIDTHS THE LAYOUT USED TO HAND-WRITE. UV_EREC and UV_SREC above set the 136// convention and these three were left as bare widths -- written once in uv_layout, again in 137// uv_words_used, and again at every accessor: four independent copies of one shape. A width written 138// beside a record is a second copy of that record's layout, and the two drift silently, because widening 139// the record leaves every bare + 2 / + 3 still compiling and still reading a word, just the wrong one, 140// with no diagnostic anywhere. The field names below are the single source of truth for the slot order. 141const UV_CUVREC: i64 = 2 // per-corner uv pair: [u, v] 142const UV_BOXREC: i64 = 4 // per-chart uv bounds: [u0, v0, u1, v1] 143const UV_DISTREC: i64 = 4 // per-chart distortion: [amean, amax, dmean, dmax] 144const UV_P_U: i64 = 0 145const UV_P_V: i64 = 1 146const UV_B_U0: i64 = 0 147const UV_B_V0: i64 = 1 148const UV_B_U1: i64 = 2 149const UV_B_V1: i64 = 3 150const UV_D_AMEAN: i64 = 0 151const UV_D_AMAX: i64 = 1 152const UV_D_DMEAN: i64 = 2 153const UV_D_DMAX: i64 = 3 154 155// THE TEXC WIRE HEADER, WHOSE WIDTH WAS HAND-WRITTEN AT EVERY SLOT THAT FOLLOWS IT. Four words -- 156// [nv][stride][grid][reserved] -- counted once as a size in uv_texc_words and then again as a bare + 4 157// payload base at each of the eight accesses in uv_emit_texc. Widen the header and every one of those 158// eight still compiles, still reads a word, and addresses the wrong one. 159const UV_X_HDR: i64 = 4 160const UV_X_NV: i64 = 0 161const UV_X_STRIDE: i64 = 1 162const UV_X_GRID: i64 = 2 163const UV_X_RSVD: i64 = 3 164const UV_X_U: i64 = 0 // per-vertex payload slots, relative to that vertex's record base 165const UV_X_V: i64 = 1 166const UV_X_CH: i64 = 2 167 168// ---- pipeline stages --------------------------------------------------------------------------- 169const UV_ST_NEW: i64 = 0 170const UV_ST_CHARTS: i64 = 1 171const UV_ST_SOLVED: i64 = 2 172const UV_ST_MEASURED: i64 = 3 173 174// ---- refusal codes. EVERY ONE IS ITS OWN CLASS: a compound refusal that will not name its failing 175// conjunct sends the reader to guess, and the reader always guesses the alarming one. ------------- 176const UV_OK: i64 = 0 177const UV_E_ARGS: i64 = 0 - 1 178const UV_E_TOPOLOGY: i64 = 0 - 2 179const UV_E_NOPIN: i64 = 0 - 3 180const UV_E_NOCONVERGE: i64 = 0 - 4 181const UV_E_FOLD: i64 = 0 - 5 182const UV_E_UVDEGEN: i64 = 0 - 6 183const UV_E_OVERLAP: i64 = 0 - 7 184const UV_E_STAGE: i64 = 0 - 8 185const UV_E_SPAN: i64 = 0 - 9 186const UV_E_NOTRUN: i64 = 0 - 10 187// A CODE OF ITS OWN, DELIBERATELY NOT UV_E_NOPIN. The rigid unfold leaving a chart vertex unplaced and a 188// chart whose two pins land on one point are different faults with different remedies, and while they 189// shared one number the first was unreadable from outside: the symptom said "this chart has no pins" about 190// a chart whose pins were fine. Two causes behind one code is what makes a defect expensive to find. 191const UV_E_UNPLACED: i64 = 0 - 11 192 193// ---- fixed point, every constant carrying the derivation that produced it ----------------------- 194// The TEXC wire scale. NOT a free choice: nx_nxa_texc_lib emits UVs as Q16 over the unit square 195// (NT_Q16 65536, NT_Q16M 65535) and nx_nxa_texbake_lib samples them at that scale, so adopting any 196// other scale would make this file a converter instead of a producer. 197const UV_Q16: i64 = 65536 198const UV_Q16M: i64 = 65535 199// The gutter, in Q16 units, left on every side of a chart's atlas tile. One unit is the smallest 200// non-zero separation the wire format can express, and it is what makes bilinear sampling at a tile 201// edge read the chart rather than its neighbour -- the same one-unit inset ntx_apply uses. 202const UV_GUTTER: i64 = 1 203// Weight scale for the cotangents. DERIVED: nx_vecmath VM_ONE is 4096 and is already the estate's 204// fixed-point unit (nx_itrig emits fx4096, nx_vmd decodes rotation at 4096). A second scale here would 205// make this file the place two scales meet. 206const UV_W_Q: i64 = 4096 207// The working span the solver relaxes in, and the u-coordinate the second pin is fixed at. DERIVED from 208// the output resolution: the final tile inner width is at most UV_Q16 - 2 = 65534, so a working span of 209// 2^20 gives at least 16 working units per emitted Q16 unit even for a single chart, i.e. the rounding 210// into the wire format costs strictly less than the convergence criterion below. Making it larger only 211// eats headroom in the sweep arithmetic; smaller and quantisation would dominate the measured distortion. 212const UV_PIN_SPAN: i64 = 1048576 213// The initial-guess space, before the similarity that lands the pins. Kept at 2^16 so that the similarity 214// multiply (guess coordinate * guess delta * UV_PIN_SPAN) stays inside i64: 2^16 * 2^17 * 2^20 = 2^53. 215const UV_GUESS_SPAN: i64 = 65536 216// Cotangent clamp. cot(1 degree) = 57.29, so a triangle whose angles are all at least one degree never 217// reaches 64; a triangle with an angle below one degree is a sliver whose weight would outrank a 218// well-formed neighbour's by three orders of magnitude and dominate the whole system. Clamping at 64 219// bounds the conditioning without touching any triangle a caller would consider usable. 220const UV_COT_CLAMP: i64 = 64 221// Convergence criterion, in working units, on the largest per-vertex move of a whole sweep (measured as 222// |du| + |dv|, the L1 move, which bounds the Euclidean one). DERIVED: 32 working units is 2 emitted Q16 223// units at the single-chart tile size, and integer division truncation makes a plus-or-minus one limit 224// cycle inevitable on each of two coordinates, so 2 emitted units is the finest criterion that can 225// actually be reached rather than orbited. At a 4096-square atlas -- the size the bake path uses -- one 226// texel is 16 Q16 units, so this is an eighth of a texel. 227const UV_CONV_EPS: i64 = 32 228// Sweep budget. DERIVED, not picked: damped Jacobi on a Laplacian-like operator over a two-dimensional 229// patch of n vertices has spectral radius about 1 - c/n, so cutting the initial error (bounded by 230// UV_PIN_SPAN) down to UV_CONV_EPS takes roughly n * ln(UV_PIN_SPAN / UV_CONV_EPS) = n * 15 sweeps. 231// 32 per vertex is a little over twice that. It is a BUDGET, not a promise: a chart that exhausts it is 232// refused by name, never emitted. 233const UV_ITER_PER_UVV: i64 = 32 234// Floor on that budget so a chart of three vertices still gets a real relaxation rather than two sweeps. 235const UV_ITER_MIN: i64 = 64 236// Damping. DERIVED: undamped Jacobi on this operator has iteration-matrix eigenvalues 1 - lambda with 237// lambda in (0,2], so the top of the spectrum oscillates at magnitude near 1. Damping by one half maps 238// them to 1 - lambda/2 in [0,1), which is the standard damped-Jacobi smoother, and no mode can amplify. 239// It is a divisor rather than a multiplier because integer division toward zero then makes the fixed 240// point exactly reachable: a move of one unit damps to zero and the sweep stops instead of orbiting. 241const UV_OMEGA_DEN: i64 = 2 242// Reduction bound for the first-fundamental-form triple (a,b,c) before the dilatation is computed. The 243// dilatation is a RATIO of singular values and is therefore invariant to a common scale on a,b,c, so 244// reducing costs nothing but overflow headroom. DERIVED: with a,b,c at or below 2^20, the products a*c 245// and b*b reach 2^40 and the permil form below reaches 2^22 * 10^6, all far inside i64. 246const UV_ABC_MAX: i64 = 1048576 247// Bound on any one component of the Ss / St vectors BEFORE they are squared. DERIVED: the sum of three 248// squares must stay inside i64, so a component is bounded by sqrt((2^63 - 1) / 3) = 1.75e9, and 1e9 keeps 249// a 1.75x margin. Reducing Ss and St by a COMMON divisor costs nothing for the same reason the (a,b,c) 250// reduction does: the dilatation is a ratio of singular values and is blind to a common scale. 251const UV_SS_MAX: i64 = 1000000000 252// Guard on the solved chart extent before atlas placement, so the placement multiply cannot overflow. 253// DERIVED: the product (chart span * tile inner width) must stay inside i64; at the largest possible 254// tile (65534) that bounds the span at 1.4e14, and 1e11 keeps three orders of headroom while remaining 255// five orders larger than any converged chart, whose span is on the order of UV_PIN_SPAN. 256const UV_SDEN_MAX: i64 = 100000000000 257const UV_PERMIL: i64 = 1000 258const UV_MIN_PINS: i64 = 2 259 260func uv_err_name(e: i64) -> *u8 { 261 if e == UV_OK { return "UNWRAPPED" as *u8 } 262 if e == UV_E_ARGS { return "REFUSED-BAD-ARGUMENTS" as *u8 } 263 if e == UV_E_TOPOLOGY { return "REFUSED-MESH-TOPOLOGY" as *u8 } 264 if e == UV_E_NOPIN { return "REFUSED-CHART-HAS-NO-TWO-PINS" as *u8 } 265 if e == UV_E_NOCONVERGE { return "REFUSED-CHART-DID-NOT-CONVERGE" as *u8 } 266 if e == UV_E_FOLD { return "REFUSED-FOLDED-CHART" as *u8 } 267 if e == UV_E_UVDEGEN { return "REFUSED-DEGENERATE-CHART-AREA" as *u8 } 268 if e == UV_E_OVERLAP { return "REFUSED-OVERLAPPING-CHARTS" as *u8 } 269 if e == UV_E_STAGE { return "REFUSED-PIPELINE-STAGE" as *u8 } 270 if e == UV_E_SPAN { return "REFUSED-CHART-EXTENT-UNPLACEABLE" as *u8 } 271 if e == UV_E_NOTRUN { return "REFUSED-UNWRAP-NOT-RUN" as *u8 } 272 if e == UV_E_UNPLACED { return "REFUSED-UNFOLD-LEFT-A-VERTEX-UNPLACED" as *u8 } 273 return "REFUSED-UNCLASSIFIED" as *u8 274} 275 276// ---- layout ------------------------------------------------------------------------------------ 277// THE LAYOUT ARITHMETIC EXISTS EXACTLY ONCE. A second copy written to compute the allocation size is the 278// duplicate-ruler defect in its purest form -- two expressions that must agree, with nothing making them. 279// So uv_new runs the REAL layout over a scratch header to learn the size, then runs it again over the 280// real arena. One function, one answer, and they cannot drift. 281func uv_layout(h: *i64) -> i64 { 282 let nv: i64 = h[UV_H_NV] 283 let nt: i64 = h[UV_H_NT] 284 let nk: i64 = nt * UV_TRI 285 h[UV_H_NCORN] = nk 286 var o: i64 = UV_HDR 287 h[UV_O_VERT] = o; o = o + nv * UV_TRI 288 h[UV_O_TRI] = o; o = o + nt * UV_TRI 289 h[UV_O_ETAB] = o; o = o + h[UV_H_ECAP] * UV_EREC 290 h[UV_O_STAB] = o; o = o + h[UV_H_SCAP] * UV_SREC 291 h[UV_O_TCHART] = o; o = o + nt 292 h[UV_O_STACK] = o; o = o + nt 293 h[UV_O_CSTART] = o; o = o + nt + 1 294 h[UV_O_CORDER] = o; o = o + nt 295 h[UV_O_UVF] = o; o = o + nk 296 h[UV_O_UVSRC] = o; o = o + nk 297 h[UV_O_UVCH] = o; o = o + nk 298 h[UV_O_WU] = o; o = o + nk 299 h[UV_O_WV] = o; o = o + nk 300 h[UV_O_ACCU] = o; o = o + nk 301 h[UV_O_ACCV] = o; o = o + nk 302 h[UV_O_ACCW] = o; o = o + nk 303 h[UV_O_ACCAU] = o; o = o + nk 304 h[UV_O_ACCAV] = o; o = o + nk 305 h[UV_O_LIST] = o; o = o + nk 306 // STAMP serves two different key spaces at two different times: class ids in [0,nuv) while charts are 307 // built and relaxed, and mesh vertex ids in [0,nv) in the TEXC bridge. It is sized for BOTH rather than 308 // for whichever looks larger, because a mesh may legally carry vertices no triangle references, and a 309 // shared scratch array sized for one of its two users is an aliasing bug waiting for that input. 310 h[UV_O_STAMP] = o; o = o + nk + nv 311 h[UV_O_TSEEN] = o; o = o + nt 312 h[UV_O_PLACED] = o; o = o + nk 313 // THE UNION-FIND ROOTS, RESOLVED ONCE AND KEPT. They cannot be resolved on demand out of UVF, because 314 // the relabel that consumes them REWRITES UVF -- one array cannot be a parent forest and the answer 315 // read off it at the same time. 316 h[UV_O_ROOT] = o; o = o + nk 317 h[UV_O_CUV] = o; o = o + nk * UV_CUVREC 318 h[UV_O_TANG] = o; o = o + nt 319 h[UV_O_TRAT] = o; o = o + nt 320 h[UV_O_TSAR] = o; o = o + nt 321 h[UV_O_CBOX] = o; o = o + nt * UV_BOXREC 322 h[UV_O_CCNT] = o; o = o + nt 323 h[UV_O_CDIST] = o; o = o + nt * UV_DISTREC 324 return o 325} 326 327func uv_new(nv: i64, nt: i64) -> *i64 { 328 if nv < UV_TRI { return 0 as *i64 } 329 if nt <= 0 { return 0 as *i64 } 330 let scratch: *i64 = sys_mmap(UV_HDR * UV_I64) as *i64 331 scratch[UV_H_NV] = nv 332 scratch[UV_H_NT] = nt 333 // capacity DERIVED from the element count by the incumbent rule (meshvalid mv_cap_for: next power of 334 // two at or above four times the count, load factor at or below a quarter), so neither table has a 335 // size to outgrow and neither can silently fill. An edge count is bounded by 3 per triangle, and a 336 // seam is an edge, so both tables take the same derivation. 337 scratch[UV_H_ECAP] = mv_cap_for(nt * UV_TRI) 338 scratch[UV_H_SCAP] = mv_cap_for(nt * UV_TRI) 339 let words: i64 = uv_layout(scratch) 340 let m: *i64 = sys_mmap(words * UV_I64) as *i64 341 if (m as i64) == 0 { return m } 342 m[UV_H_NV] = nv 343 m[UV_H_NT] = nt 344 m[UV_H_ECAP] = scratch[UV_H_ECAP] 345 m[UV_H_SCAP] = scratch[UV_H_SCAP] 346 uv_layout(m) 347 m[UV_H_ERR] = UV_E_NOTRUN 348 m[UV_H_STAGE] = UV_ST_NEW 349 m[UV_H_MVERR] = MV_E_NOTRUN 350 m[UV_H_MVCLOSED] = 0 - 1 351 return m 352} 353 354func uv_words_used(m: *i64) -> i64 { return m[UV_O_CDIST] + m[UV_H_NT] * UV_DISTREC } 355 356func uv_set_vert(m: *i64, i: i64, x: i64, y: i64, z: i64) -> i64 { 357 if i < 0 { return UV_E_ARGS } 358 if i >= m[UV_H_NV] { return UV_E_ARGS } 359 let o: i64 = m[UV_O_VERT] + i * UV_TRI 360 m[o + 0] = x 361 m[o + 1] = y 362 m[o + 2] = z 363 return UV_OK 364} 365 366func uv_set_tri(m: *i64, i: i64, a: i64, b: i64, c: i64) -> i64 { 367 if i < 0 { return UV_E_ARGS } 368 if i >= m[UV_H_NT] { return UV_E_ARGS } 369 let o: i64 = m[UV_O_TRI] + i * UV_TRI 370 m[o + 0] = a 371 m[o + 1] = b 372 m[o + 2] = c 373 return UV_OK 374} 375 376func uv_vx(m: *i64, v: i64) -> i64 { return m[m[UV_O_VERT] + v * UV_TRI + 0] } 377func uv_vy(m: *i64, v: i64) -> i64 { return m[m[UV_O_VERT] + v * UV_TRI + 1] } 378func uv_vz(m: *i64, v: i64) -> i64 { return m[m[UV_O_VERT] + v * UV_TRI + 2] } 379func uv_ti(m: *i64, t: i64, k: i64) -> i64 { return m[m[UV_O_TRI] + t * UV_TRI + k] } 380 381// ---- (1) SEAMS AND CHARTS ---------------------------------------------------------------------- 382// A seam is an undirected vertex pair. It does two distinct things and both matter: it blocks the flood 383// fill, so triangles on either side land in different charts when that is the only path between them; 384// and it blocks the corner merge, so a vertex ON the seam becomes two UV vertices. WITHOUT THE SECOND 385// HALF A SEAM DOES NOTHING USEFUL ON A CLOSED LOOP -- cutting a cylinder along one edge leaves the 386// triangles still connected the long way round, so the chart is unchanged, and only splitting the 387// vertices actually turns the annulus into a disk. That case is a gate fixture, not a hypothetical. 388func uv_seam_set(m: *i64, a: i64, b: i64) -> i64 { 389 if m[UV_H_STAGE] != UV_ST_NEW { m[UV_H_ERR] = UV_E_STAGE; return UV_E_STAGE } 390 if a < 0 { return UV_E_ARGS } 391 if b < 0 { return UV_E_ARGS } 392 if a >= m[UV_H_NV] { return UV_E_ARGS } 393 if b >= m[UV_H_NV] { return UV_E_ARGS } 394 if a == b { return UV_E_ARGS } 395 var lo: i64 = a 396 var hi: i64 = b 397 if lo > hi { lo = b; hi = a } 398 let cap: i64 = m[UV_H_SCAP] 399 let base: i64 = m[UV_O_STAB] 400 var s: i64 = mv_mix(lo, hi, cap) 401 var guard: i64 = 0 402 while guard < cap { 403 let o: i64 = base + s * UV_SREC 404 if m[o + UV_SM_USED] == 0 { 405 m[o + UV_SM_LO] = lo 406 m[o + UV_SM_HI] = hi 407 m[o + UV_SM_USED] = 1 408 m[UV_H_NSEAM] = m[UV_H_NSEAM] + 1 409 return UV_OK 410 } 411 if m[o + UV_SM_LO] == lo { if m[o + UV_SM_HI] == hi { return UV_OK } } 412 s = s + 1 413 if s >= cap { s = 0 } 414 guard = guard + 1 415 } 416 return UV_E_ARGS 417} 418 419func uv_is_seam(m: *i64, a: i64, b: i64) -> i64 { 420 var lo: i64 = a 421 var hi: i64 = b 422 if lo > hi { lo = b; hi = a } 423 let cap: i64 = m[UV_H_SCAP] 424 let base: i64 = m[UV_O_STAB] 425 var s: i64 = mv_mix(lo, hi, cap) 426 var guard: i64 = 0 427 while guard < cap { 428 let o: i64 = base + s * UV_SREC 429 if m[o + UV_SM_USED] == 0 { return 0 } 430 if m[o + UV_SM_LO] == lo { if m[o + UV_SM_HI] == hi { return 1 } } 431 s = s + 1 432 if s >= cap { s = 0 } 433 guard = guard + 1 434 } 435 return 0 436} 437 438// edge slot, inserting on first sight. Records the (at most two) triangles that use the edge; a third 439// user is a non-manifold edge, which meshvalid has already refused before this runs. 440func uvi_edge_slot(m: *i64, a: i64, b: i64, t: i64) -> i64 { 441 var lo: i64 = a 442 var hi: i64 = b 443 if lo > hi { lo = b; hi = a } 444 let cap: i64 = m[UV_H_ECAP] 445 let base: i64 = m[UV_O_ETAB] 446 var s: i64 = mv_mix(lo, hi, cap) 447 var guard: i64 = 0 448 while guard < cap { 449 let o: i64 = base + s * UV_EREC 450 if m[o + UV_ED_T0] == 0 { 451 m[o + UV_ED_LO] = lo 452 m[o + UV_ED_HI] = hi 453 m[o + UV_ED_T0] = t + 1 454 return s 455 } 456 if m[o + UV_ED_LO] == lo { if m[o + UV_ED_HI] == hi { 457 if m[o + UV_ED_T1] == 0 { m[o + UV_ED_T1] = t + 1 } 458 return s 459 } } 460 s = s + 1 461 if s >= cap { s = 0 } 462 guard = guard + 1 463 } 464 return 0 - 1 465} 466 467func uvi_edge_find(m: *i64, a: i64, b: i64) -> i64 { 468 var lo: i64 = a 469 var hi: i64 = b 470 if lo > hi { lo = b; hi = a } 471 let cap: i64 = m[UV_H_ECAP] 472 let base: i64 = m[UV_O_ETAB] 473 var s: i64 = mv_mix(lo, hi, cap) 474 var guard: i64 = 0 475 while guard < cap { 476 let o: i64 = base + s * UV_EREC 477 if m[o + UV_ED_T0] == 0 { return 0 - 1 } 478 if m[o + UV_ED_LO] == lo { if m[o + UV_ED_HI] == hi { return s } } 479 s = s + 1 480 if s >= cap { s = 0 } 481 guard = guard + 1 482 } 483 return 0 - 1 484} 485 486func uvi_find(f: *i64, base: i64, x: i64) -> i64 { 487 var r: i64 = x 488 while f[base + r] != r { f[base + r] = f[base + f[base + r]]; r = f[base + r] } 489 return r 490} 491 492func uvi_union(f: *i64, base: i64, a: i64, b: i64) -> i64 { 493 let ra: i64 = uvi_find(f, base, a) 494 let rb: i64 = uvi_find(f, base, b) 495 if ra == rb { return 0 } 496 if ra < rb { f[base + rb] = ra } else { f[base + ra] = rb } 497 return 1 498} 499 500// which corner of triangle t names vertex v, or -1 501func uvi_corner_of(m: *i64, t: i64, v: i64) -> i64 { 502 if uv_ti(m, t, 0) == v { return 0 } 503 if uv_ti(m, t, 1) == v { return 1 } 504 if uv_ti(m, t, 2) == v { return 2 } 505 return 0 - 1 506} 507 508// COMPOSE THE INCUMBENT REFEREE, DO NOT WRITE A SECOND ONE. nx_meshvalid_lib already names every mesh 509// defect class with a count and a first offender, so validity here is one call into it. 510// THE WHOLE-MESH VERDICT IS THE WRONG CONJUNCT AND USING IT WOULD BE A BUG. mv_manifold demands a 511// CLOSED surface, and every chart worth unwrapping is open -- a flat quad, a terrain patch and a cut 512// cylinder all have boundary edges by definition, so mv_manifold is 0 for all of them and reading it 513// as the gate would refuse every legitimate input. What actually blocks a parameterisation is a 514// non-manifold edge (no consistent neighbourhood), an out-of-range index, a degenerate triangle (no 515// defined normal) and inconsistent winding (no coherent orientation to preserve). Those four counters 516// are read; MV_C_BOUND is deliberately NOT, and mv_manifold is recorded as a reported fact instead. 517func uvi_validate(m: *i64) -> i64 { 518 let nv: i64 = m[UV_H_NV] 519 let nt: i64 = m[UV_H_NT] 520 let mv: *i64 = mv_new(nv, nt) 521 if (mv as i64) == 0 { return UV_E_ARGS } 522 var i: i64 = 0 523 while i < nv { 524 mv_set_vert(mv, i, uv_vx(m, i), uv_vy(m, i), uv_vz(m, i)) 525 i = i + 1 526 } 527 var t: i64 = 0 528 while t < nt { 529 mv_set_tri(mv, t, uv_ti(m, t, 0), uv_ti(m, t, 1), uv_ti(m, t, 2)) 530 t = t + 1 531 } 532 let rc: i64 = mv_check(mv) 533 m[UV_H_MVERR] = rc 534 m[UV_H_MVCLOSED] = mv_manifold(mv) 535 if mv_count(mv, MV_C_INDEX) > 0 { return UV_E_TOPOLOGY } 536 if mv_count(mv, MV_C_DEGEN) > 0 { return UV_E_TOPOLOGY } 537 if mv_count(mv, MV_C_NONMAN) > 0 { return UV_E_TOPOLOGY } 538 if mv_count(mv, MV_C_WIND) > 0 { return UV_E_TOPOLOGY } 539 return UV_OK 540} 541 542func uv_chart_build(m: *i64) -> i64 { 543 let vrc: i64 = uvi_validate(m) 544 if vrc != UV_OK { m[UV_H_ERR] = vrc; return vrc } 545 let nt: i64 = m[UV_H_NT] 546 let nk: i64 = m[UV_H_NCORN] 547 let tch: i64 = m[UV_O_TCHART] 548 let stk: i64 = m[UV_O_STACK] 549 let uvf: i64 = m[UV_O_UVF] 550 551 // edge table from the triangles 552 var t: i64 = 0 553 while t < nt { 554 var k: i64 = 0 555 while k < UV_TRI { 556 let a: i64 = uv_ti(m, t, k) 557 let b: i64 = uv_ti(m, t, (k + 1) % UV_TRI) 558 if uvi_edge_slot(m, a, b, t) < 0 { m[UV_H_ERR] = UV_E_ARGS; return UV_E_ARGS } 559 k = k + 1 560 } 561 t = t + 1 562 } 563 564 // flood fill triangles across shared NON-SEAM edges 565 t = 0 566 while t < nt { m[tch + t] = 0 - 1; t = t + 1 } 567 var nc: i64 = 0 568 var seed: i64 = 0 569 while seed < nt { 570 if m[tch + seed] < 0 { 571 var sp: i64 = 0 572 m[stk + sp] = seed 573 sp = sp + 1 574 m[tch + seed] = nc 575 while sp > 0 { 576 sp = sp - 1 577 let cur: i64 = m[stk + sp] 578 var k2: i64 = 0 579 while k2 < UV_TRI { 580 let a: i64 = uv_ti(m, cur, k2) 581 let b: i64 = uv_ti(m, cur, (k2 + 1) % UV_TRI) 582 if uv_is_seam(m, a, b) == 0 { 583 let es: i64 = uvi_edge_find(m, a, b) 584 if es >= 0 { 585 let eo: i64 = m[UV_O_ETAB] + es * UV_EREC 586 var w: i64 = 2 587 while w < UV_EREC { 588 let nb: i64 = m[eo + w] - 1 589 if nb >= 0 { if m[tch + nb] < 0 { 590 m[tch + nb] = nc 591 m[stk + sp] = nb 592 sp = sp + 1 593 } } 594 w = w + 1 595 } 596 } 597 } 598 k2 = k2 + 1 599 } 600 } 601 nc = nc + 1 602 } 603 seed = seed + 1 604 } 605 m[UV_H_NC] = nc 606 607 // UV VERTICES: union corners across shared NON-SEAM edges. Two corners naming the same mesh vertex 608 // are the same texture coordinate only if a path of non-seam edges joins them WITHIN the chart. This 609 // is what makes a cut cut, and it also handles a pinch vertex (one mesh vertex whose two triangle 610 // fans meet only at that point) correctly and for free -- the two fans simply never merge. 611 var kk: i64 = 0 612 while kk < nk { m[uvf + kk] = kk; kk = kk + 1 } 613 t = 0 614 while t < nt { 615 var k3: i64 = 0 616 while k3 < UV_TRI { 617 let a: i64 = uv_ti(m, t, k3) 618 let b: i64 = uv_ti(m, t, (k3 + 1) % UV_TRI) 619 if uv_is_seam(m, a, b) == 0 { 620 let es2: i64 = uvi_edge_find(m, a, b) 621 if es2 >= 0 { 622 let eo2: i64 = m[UV_O_ETAB] + es2 * UV_EREC 623 let t0: i64 = m[eo2 + UV_ED_T0] - 1 624 let t1: i64 = m[eo2 + UV_ED_T1] - 1 625 if t1 >= 0 { if t0 >= 0 { 626 let ca0: i64 = uvi_corner_of(m, t0, a) 627 let ca1: i64 = uvi_corner_of(m, t1, a) 628 let cb0: i64 = uvi_corner_of(m, t0, b) 629 let cb1: i64 = uvi_corner_of(m, t1, b) 630 if ca0 >= 0 { if ca1 >= 0 { uvi_union(m, uvf, t0 * UV_TRI + ca0, t1 * UV_TRI + ca1) } } 631 if cb0 >= 0 { if cb1 >= 0 { uvi_union(m, uvf, t0 * UV_TRI + cb0, t1 * UV_TRI + cb1) } } 632 } } 633 } 634 } 635 k3 = k3 + 1 636 } 637 t = t + 1 638 } 639 // relabel roots to dense class ids 0..nuv-1, and record each class's mesh vertex and chart 640 let uvsrc: i64 = m[UV_O_UVSRC] 641 let uvch: i64 = m[UV_O_UVCH] 642 let stamp: i64 = m[UV_O_STAMP] 643 let root: i64 = m[UV_O_ROOT] 644 // RESOLVE EVERY ROOT BEFORE REWRITING A SINGLE PARENT, AND KEEP THE TWO IN ARRAYS OF THEIR OWN. UVF is 645 // the union-find parent forest AND the corner-to-class map, and this relabel turns it from the first 646 // into the second IN PLACE. A find() run after the first corner has been rewritten therefore walks a 647 // CLASS ID as though it were a parent index -- and the old code hid the class behind a 0-2-cls 648 // encoding, which makes it NEGATIVE, so the walk indexed BEFORE the array and both read and wrote the 649 // CSTART and CORDER slots that sit underneath it. 650 // MEASURED ON THE FLAT QUAD, this file's own positive control, before the fix: corner 4 resolved to 651 // class 0 instead of class 2, so triangle 1 carried two corners in one class and could only ever emit 652 // a collapsed image; mesh vertex 0 lost its class entirely, which is why uv_emit_texc reported ONE 653 // lost corner on an UNSEAMED mesh -- arithmetically impossible when the map is the bijection an 654 // unseamed mesh guarantees; and UVSRC was corrupted underneath the solver, so a surface that has an 655 // exact isometry into the plane read 987 permil of angle distortion and refused UV_E_UVDEGEN. 656 // TWO JOBS SHARING ONE ARRAY IS THE WHOLE DEFECT. The extra nk words are the price of them not being 657 // able to interfere, and the relabel below never reads a slot it has already written. 658 kk = 0 659 while kk < nk { m[root + kk] = uvi_find(m, uvf, kk); kk = kk + 1 } 660 kk = 0 661 while kk < nk { m[stamp + kk] = 0 - 1; kk = kk + 1 } 662 var nuv: i64 = 0 663 kk = 0 664 while kk < nk { 665 let r: i64 = m[root + kk] 666 if m[stamp + r] < 0 { m[stamp + r] = nuv; nuv = nuv + 1 } 667 kk = kk + 1 668 } 669 kk = 0 670 while kk < nk { 671 let cls2: i64 = m[stamp + m[root + kk]] 672 m[uvf + kk] = cls2 673 m[uvsrc + cls2] = uv_ti(m, kk / UV_TRI, kk % UV_TRI) 674 m[uvch + cls2] = m[tch + kk / UV_TRI] 675 kk = kk + 1 676 } 677 m[UV_H_NUV] = nuv 678 679 // CSR of triangles by chart, plus the per-chart triangle count 680 let cst: i64 = m[UV_O_CSTART] 681 let cord: i64 = m[UV_O_CORDER] 682 let ccnt: i64 = m[UV_O_CCNT] 683 var c: i64 = 0 684 while c < nc + 1 { m[cst + c] = 0; c = c + 1 } 685 t = 0 686 while t < nt { m[cst + m[tch + t] + 1] = m[cst + m[tch + t] + 1] + 1; t = t + 1 } 687 c = 0 688 while c < nc { m[ccnt + c] = m[cst + c + 1]; c = c + 1 } 689 c = 0 690 while c < nc { m[cst + c + 1] = m[cst + c + 1] + m[cst + c]; c = c + 1 } 691 let fill: i64 = m[UV_O_LIST] 692 c = 0 693 while c < nc { m[fill + c] = m[cst + c]; c = c + 1 } 694 t = 0 695 while t < nt { 696 let cc: i64 = m[tch + t] 697 m[cord + m[fill + cc]] = t 698 m[fill + cc] = m[fill + cc] + 1 699 t = t + 1 700 } 701 m[UV_H_STAGE] = UV_ST_CHARTS 702 m[UV_H_ERR] = UV_OK 703 return UV_OK 704} 705 706func uv_chart_count(m: *i64) -> i64 { return m[UV_H_NC] } 707func uv_uvvert_count(m: *i64) -> i64 { return m[UV_H_NUV] } 708func uv_seam_count(m: *i64) -> i64 { return m[UV_H_NSEAM] } 709func uv_chart_of_tri(m: *i64, t: i64) -> i64 { 710 if t < 0 { return 0 - 1 } 711 if t >= m[UV_H_NT] { return 0 - 1 } 712 if m[UV_H_STAGE] < UV_ST_CHARTS { return 0 - 1 } 713 return m[m[UV_O_TCHART] + t] 714} 715func uv_chart_tris(m: *i64, c: i64) -> i64 { 716 if c < 0 { return 0 - 1 } 717 if c >= m[UV_H_NC] { return 0 - 1 } 718 return m[m[UV_O_CCNT] + c] 719} 720 721// ---- (2) CONFORMAL RELAXATION ------------------------------------------------------------------ 722// collect the distinct UV vertices of chart c into the LIST array; returns how many. 723func uvi_chart_classes(m: *i64, c: i64) -> i64 { 724 if c < 0 { return 0 } 725 if c >= m[UV_H_NC] { return 0 } 726 let stamp: i64 = m[UV_O_STAMP] 727 let list: i64 = m[UV_O_LIST] 728 let uvf: i64 = m[UV_O_UVF] 729 var i: i64 = 0 730 while i < m[UV_H_NUV] { m[stamp + i] = 0; i = i + 1 } 731 var n: i64 = 0 732 var p: i64 = m[m[UV_O_CSTART] + c] 733 let e: i64 = m[m[UV_O_CSTART] + c + 1] 734 while p < e { 735 let t: i64 = m[m[UV_O_CORDER] + p] 736 var k: i64 = 0 737 while k < UV_TRI { 738 let cls: i64 = m[uvf + t * UV_TRI + k] 739 if m[stamp + cls] == 0 { 740 m[stamp + cls] = 1 741 m[list + n] = cls 742 n = n + 1 743 } 744 k = k + 1 745 } 746 p = p + 1 747 } 748 return n 749} 750 751func uvi_d2(m: *i64, va: i64, vb: i64) -> i64 { 752 let dx: i64 = uv_vx(m, va) - uv_vx(m, vb) 753 let dy: i64 = uv_vy(m, va) - uv_vy(m, vb) 754 let dz: i64 = uv_vz(m, va) - uv_vz(m, vb) 755 return dx * dx + dy * dy + dz * dz 756} 757 758// TWO PINS, the standard LSCM constraint. Chosen as the chart's approximate 3D diameter by the usual 759// two-pass walk (farthest from an arbitrary seed, then farthest from that) -- an APPROXIMATION of the 760// diameter, not the diameter, and named as one. It only has to produce two well-separated vertices; a 761// worse pair costs conditioning, never correctness, because the energy itself is what is minimised. 762// Returns the number of pins established: 2, or 0 when the chart cannot supply two distinct positions. 763func uv_pins_for(m: *i64, c: i64, out: *i64) -> i64 { 764 let n: i64 = uvi_chart_classes(m, c) 765 if n < UV_MIN_PINS { return 0 } 766 let list: i64 = m[UV_O_LIST] 767 let uvsrc: i64 = m[UV_O_UVSRC] 768 let a: i64 = m[list + 0] 769 var best: i64 = 0 - 1 770 var i: i64 = 0 771 var pa: i64 = a 772 while i < n { 773 let d2: i64 = uvi_d2(m, m[uvsrc + m[list + i]], m[uvsrc + a]) 774 if d2 > best { best = d2; pa = m[list + i] } 775 i = i + 1 776 } 777 i = 0 778 best = 0 - 1 779 var pb: i64 = pa 780 while i < n { 781 let d3: i64 = uvi_d2(m, m[uvsrc + m[list + i]], m[uvsrc + pa]) 782 if d3 > best { best = d3; pb = m[list + i] } 783 i = i + 1 784 } 785 if best <= 0 { return 0 } 786 if pb == pa { return 0 } 787 out[0] = pa 788 out[1] = pb 789 return UV_MIN_PINS 790} 791 792// cotangent of the angle at vertex vk in the triangle (vi, vj, vk), in UV_W_Q scale, clamped. 793func uvi_cotq(m: *i64, vi: i64, vj: i64, vk: i64) -> i64 { 794 let ux: i64 = uv_vx(m, vi) - uv_vx(m, vk) 795 let uy: i64 = uv_vy(m, vi) - uv_vy(m, vk) 796 let uz: i64 = uv_vz(m, vi) - uv_vz(m, vk) 797 let wx: i64 = uv_vx(m, vj) - uv_vx(m, vk) 798 let wy: i64 = uv_vy(m, vj) - uv_vy(m, vk) 799 let wz: i64 = uv_vz(m, vj) - uv_vz(m, vk) 800 let dot: i64 = ux * wx + uy * wy + uz * wz 801 let cx: i64 = uy * wz - uz * wy 802 let cy: i64 = uz * wx - ux * wz 803 let cz: i64 = ux * wy - uy * wx 804 let cr: i64 = vm_isqrt(cx * cx + cy * cy + cz * cz) 805 let cmax: i64 = UV_COT_CLAMP * UV_W_Q 806 if cr <= 0 { return cmax } 807 var q: i64 = dot * UV_W_Q / cr 808 if q > cmax { q = cmax } 809 if q < 0 - cmax { q = 0 - cmax } 810 return q 811} 812 813// Fixed-point scale the rigid unfold works in, before the guess is normalised. DERIVED: it is UV_W_Q, 814// the estate's unit, reused rather than a fresh scale; at a mesh edge of 200 units a length becomes 8.2e5 815// and the law-of-cosines numerator below reaches 2e12, four million times inside i64. 816const UV_UNF_Q: i64 = 4096 817 818// squared 3D edge length lifted into the unfold's fixed point, in one square root rather than two. 819func uvi_len_q(m: *i64, va: i64, vb: i64) -> i64 { 820 return vm_isqrt(uvi_d2(m, va, vb) * UV_UNF_Q * UV_UNF_Q) 821} 822 823// Place the one vertex of triangle tn that its already-placed neighbour tp does not share, at its exact 824// 3D edge lengths from the two shared ones -- the law of cosines, done once per triangle. 825// THE SIDE MATTERS AND IS NOT A COIN FLIP: the new vertex goes on the OPPOSITE side of the shared edge 826// from the parent's own third vertex. Choosing the same side folds the chart back over the triangle it 827// just came from, which is a fold the distortion metric would then honestly report -- a self-inflicted 828// failure that would read exactly like the mesh being unwrappable. 829// A vertex already placed by another path KEEPS its first position: on a developable surface every path 830// agrees, and on one that is not, the disagreement IS the curvature, and it is the relaxation's job to 831// distribute it rather than this function's job to hide it. 832func uvi_place_third(m: *i64, tp: i64, tn: i64, va: i64, vb: i64) -> i64 { 833 let uvf: i64 = m[UV_O_UVF] 834 let uvsrc: i64 = m[UV_O_UVSRC] 835 let wu: i64 = m[UV_O_WU] 836 let wv: i64 = m[UV_O_WV] 837 let pl: i64 = m[UV_O_PLACED] 838 let ka: i64 = uvi_corner_of(m, tn, va) 839 let kb: i64 = uvi_corner_of(m, tn, vb) 840 if ka < 0 { return UV_E_ARGS } 841 if kb < 0 { return UV_E_ARGS } 842 let kc: i64 = UV_TRI - ka - kb // corner indices are 0,1,2 and two are known, so the third is 3 - a - b 843 let ca: i64 = m[uvf + tn * UV_TRI + ka] 844 let cb: i64 = m[uvf + tn * UV_TRI + kb] 845 let cc: i64 = m[uvf + tn * UV_TRI + kc] 846 if m[pl + cc] == 1 { return UV_OK } 847 let vc: i64 = m[uvsrc + cc] 848 let ax: i64 = m[wu + ca] 849 let ay: i64 = m[wv + ca] 850 let dx: i64 = m[wu + cb] - ax 851 let dy: i64 = m[wv + cb] - ay 852 let lab: i64 = vm_isqrt(dx * dx + dy * dy) 853 if lab <= 0 { return UV_E_ARGS } 854 let lac2: i64 = uvi_d2(m, va, vc) * UV_UNF_Q * UV_UNF_Q 855 let lbc2: i64 = uvi_d2(m, vb, vc) * UV_UNF_Q * UV_UNF_Q 856 let xq: i64 = (lab * lab + lac2 - lbc2) / (2 * lab) 857 var yq2: i64 = lac2 - xq * xq 858 if yq2 < 0 { yq2 = 0 } // rounding only; the triangle inequality guarantees the sign 859 let yq: i64 = vm_isqrt(yq2) 860 let pk: i64 = UV_TRI - uvi_corner_of(m, tp, va) - uvi_corner_of(m, tp, vb) 861 let pc: i64 = m[uvf + tp * UV_TRI + pk] 862 let cr: i64 = dx * (m[wv + pc] - ay) - dy * (m[wu + pc] - ax) 863 var sgn: i64 = 1 864 if cr > 0 { sgn = 0 - 1 } 865 m[wu + cc] = ax + dx * xq / lab - dy * yq * sgn / lab 866 m[wv + cc] = ay + dy * xq / lab + dx * yq * sgn / lab 867 m[pl + cc] = 1 868 return UV_OK 869} 870 871// THE RIGID UNFOLD. Walks the chart triangle by triangle from a seed, laying each one flat at its exact 872// 3D edge lengths. EXACT for any developable surface -- a flat quad, a cube face, a cut cylinder wall -- 873// and path-dependent for one that is not, which is the honest behaviour: a cone point cannot be flattened 874// and something has to give. 875func uvi_unfold(m: *i64, c: i64, n: i64) -> i64 { 876 let list: i64 = m[UV_O_LIST] 877 let uvf: i64 = m[UV_O_UVF] 878 let uvsrc: i64 = m[UV_O_UVSRC] 879 let wu: i64 = m[UV_O_WU] 880 let wv: i64 = m[UV_O_WV] 881 let pl: i64 = m[UV_O_PLACED] 882 let ts: i64 = m[UV_O_TSEEN] 883 let stk: i64 = m[UV_O_STACK] 884 let cord: i64 = m[UV_O_CORDER] 885 var i: i64 = 0 886 while i < n { m[pl + m[list + i]] = 0; i = i + 1 } 887 let p0: i64 = m[m[UV_O_CSTART] + c] 888 let p1: i64 = m[m[UV_O_CSTART] + c + 1] 889 var p: i64 = p0 890 while p < p1 { m[ts + m[cord + p]] = 0; p = p + 1 } 891 if p1 <= p0 { return UV_E_NOPIN } 892 let seed: i64 = m[cord + p0] 893 let a0: i64 = m[uvf + seed * UV_TRI + 0] 894 let b0: i64 = m[uvf + seed * UV_TRI + 1] 895 let c0: i64 = m[uvf + seed * UV_TRI + 2] 896 let lab: i64 = uvi_len_q(m, m[uvsrc + a0], m[uvsrc + b0]) 897 if lab <= 0 { return UV_E_NOPIN } 898 let lac2: i64 = uvi_d2(m, m[uvsrc + a0], m[uvsrc + c0]) * UV_UNF_Q * UV_UNF_Q 899 let lbc2: i64 = uvi_d2(m, m[uvsrc + b0], m[uvsrc + c0]) * UV_UNF_Q * UV_UNF_Q 900 let x0: i64 = (lab * lab + lac2 - lbc2) / (2 * lab) 901 var y0s: i64 = lac2 - x0 * x0 902 if y0s < 0 { y0s = 0 } 903 m[wu + a0] = 0 904 m[wv + a0] = 0 905 m[pl + a0] = 1 906 m[wu + b0] = lab 907 m[wv + b0] = 0 908 m[pl + b0] = 1 909 m[wu + c0] = x0 910 m[wv + c0] = vm_isqrt(y0s) 911 m[pl + c0] = 1 912 m[ts + seed] = 1 913 var sp: i64 = 0 914 m[stk + sp] = seed 915 sp = sp + 1 916 while sp > 0 { 917 sp = sp - 1 918 let t: i64 = m[stk + sp] 919 var k: i64 = 0 920 while k < UV_TRI { 921 let va: i64 = uv_ti(m, t, k) 922 let vb: i64 = uv_ti(m, t, (k + 1) % UV_TRI) 923 if uv_is_seam(m, va, vb) == 0 { 924 let es: i64 = uvi_edge_find(m, va, vb) 925 if es >= 0 { 926 let eo: i64 = m[UV_O_ETAB] + es * UV_EREC 927 var w: i64 = 2 928 while w < UV_EREC { 929 let nb: i64 = m[eo + w] - 1 930 if nb >= 0 { if nb != t { if m[ts + nb] == 0 { 931 // THE RETURN CODE IS THE POINT. Marking the triangle seen after a REFUSED 932 // placement is what turns a named refusal into a silent collapse: the vertex 933 // is never written, the walk never comes back for it because the triangle now 934 // reads as done, and the origin it was left holding is indistinguishable 935 // downstream from a coordinate the unfold actually chose. 936 let prc: i64 = uvi_place_third(m, t, nb, va, vb) 937 if prc != UV_OK { return prc } 938 m[ts + nb] = 1 939 m[stk + sp] = nb 940 sp = sp + 1 941 } } } 942 w = w + 1 943 } 944 } 945 } 946 k = k + 1 947 } 948 } 949 // THE WALK HAS TO ANSWER FOR THE WHOLE CHART, NOT FOR THE PART OF IT THAT WENT WELL. A class the walk 950 // never placed keeps whatever its slot held -- zero on a fresh arena -- and every step after this one 951 // reads that origin as a position the unfold chose: the similarity below can take it as a pin, the 952 // relaxation relaxes toward it, and the referee measures distortion against it. Refused under a code 953 // of its own so the reason travels with the refusal instead of arriving as somebody else's. 954 i = 0 955 var unplaced: i64 = 0 956 while i < n { 957 if m[pl + m[list + i]] == 0 { unplaced = unplaced + 1 } 958 i = i + 1 959 } 960 if unplaced > 0 { return UV_E_UNPLACED } 961 return UV_OK 962} 963 964// The initial guess: the rigid unfold, normalised uniformly into the guess space, then the 2D similarity 965// that lands pin A on the origin and pin B on (UV_PIN_SPAN, 0). 966// WHY AN UNFOLD AND NOT A PROJECTION. The obvious cheap guess is to drop the chart onto the two coordinate 967// axes it spreads along, and it is wrong in a way that matters: a cylinder wall projects onto a RING, two 968// vertices deep, and no local descent recovers the unrolled strip from there. The seam capability would 969// have shipped with a solver unable to use it, and the false refusal that followed would have read as the 970// mesh's fault rather than the guess's. 971// EVERY STEP HERE IS A SIMILARITY OR AN ISOMETRY, WHICH IS EXACTLY WHAT A CONFORMAL MAP IS ALLOWED TO BE, 972// so on a developable surface the guess is already the answer and the relaxation confirms it rather than 973// degrading it. That is what lets the positive control below be an exact claim instead of a hopeful one. 974func uvi_initial(m: *i64, c: i64, n: i64, pa: i64, pb: i64) -> i64 { 975 let list: i64 = m[UV_O_LIST] 976 let wu: i64 = m[UV_O_WU] 977 let wv: i64 = m[UV_O_WV] 978 let urc: i64 = uvi_unfold(m, c, n) 979 if urc != UV_OK { return urc } 980 var u0: i64 = 0 981 var u1: i64 = 0 982 var v0: i64 = 0 983 var v1: i64 = 0 984 var i: i64 = 0 985 while i < n { 986 let z: i64 = m[list + i] 987 if i == 0 { u0 = m[wu + z]; u1 = u0; v0 = m[wv + z]; v1 = v0 } 988 if m[wu + z] < u0 { u0 = m[wu + z] } 989 if m[wu + z] > u1 { u1 = m[wu + z] } 990 if m[wv + z] < v0 { v0 = m[wv + z] } 991 if m[wv + z] > v1 { v1 = m[wv + z] } 992 i = i + 1 993 } 994 // UNIFORM normalisation into the guess space -- one divisor on both axes. A per-axis fit is a shear, 995 // and a shear is precisely what the unfold just went to the trouble of not doing. 996 var sden: i64 = u1 - u0 997 if v1 - v0 > sden { sden = v1 - v0 } 998 if sden < 1 { sden = 1 } 999 if sden > UV_SDEN_MAX { return UV_E_SPAN } 1000 i = 0 1001 while i < n { 1002 let cls: i64 = m[list + i] 1003 m[wu + cls] = (m[wu + cls] - u0) * UV_GUESS_SPAN / sden 1004 m[wv + cls] = (m[wv + cls] - v0) * UV_GUESS_SPAN / sden 1005 i = i + 1 1006 } 1007 let gax: i64 = m[wu + pa] 1008 let gay: i64 = m[wv + pa] 1009 let dx: i64 = m[wu + pb] - gax 1010 let dy: i64 = m[wv + pb] - gay 1011 let nd: i64 = dx * dx + dy * dy 1012 // the two pins projecting onto the same guess point leaves the similarity undefined. REFUSE by name: 1013 // inventing a rotation here would silently choose the fold this file exists to prevent. 1014 if nd <= 0 { return UV_E_NOPIN } 1015 i = 0 1016 while i < n { 1017 let cls2: i64 = m[list + i] 1018 let px: i64 = m[wu + cls2] - gax 1019 let py: i64 = m[wv + cls2] - gay 1020 m[wu + cls2] = (px * dx + py * dy) * UV_PIN_SPAN / nd 1021 m[wv + cls2] = (py * dx - px * dy) * UV_PIN_SPAN / nd 1022 i = i + 1 1023 } 1024 return UV_OK 1025} 1026 1027// ONE CHART. Damped Jacobi over the exact LSCM stationary condition, pins held fixed. 1028func uvi_solve_chart(m: *i64, c: i64) -> i64 { 1029 let pins: *i64 = sys_mmap(UV_MIN_PINS * UV_I64) as *i64 1030 if uv_pins_for(m, c, pins) < UV_MIN_PINS { return UV_E_NOPIN } 1031 let pa: i64 = pins[0] 1032 let pb: i64 = pins[1] 1033 let n: i64 = uvi_chart_classes(m, c) 1034 let irc: i64 = uvi_initial(m, c, n, pa, pb) 1035 if irc != UV_OK { return irc } 1036 let list: i64 = m[UV_O_LIST] 1037 let uvf: i64 = m[UV_O_UVF] 1038 let uvsrc: i64 = m[UV_O_UVSRC] 1039 let wu: i64 = m[UV_O_WU] 1040 let wv: i64 = m[UV_O_WV] 1041 let au: i64 = m[UV_O_ACCU] 1042 let av: i64 = m[UV_O_ACCV] 1043 let aw: i64 = m[UV_O_ACCW] 1044 let aau: i64 = m[UV_O_ACCAU] 1045 let aav: i64 = m[UV_O_ACCAV] 1046 let p0: i64 = m[m[UV_O_CSTART] + c] 1047 let p1: i64 = m[m[UV_O_CSTART] + c + 1] 1048 var cap: i64 = n * UV_ITER_PER_UVV 1049 if cap < UV_ITER_MIN { cap = UV_ITER_MIN } 1050 var it: i64 = 0 1051 var used: i64 = 0 1052 var done: i64 = 0 1053 var maxmove: i64 = 0 1054 // THE LOOP CURSOR IS NOT THE ANSWER. Leaving early by writing the cap into the cursor would erase the 1055 // very number this function has to report, so the sweep count is carried in a counter of its own and 1056 // the cursor is free to be used as the exit sentinel. 1057 while it < cap { 1058 if done == 0 { 1059 var i: i64 = 0 1060 while i < n { 1061 let z: i64 = m[list + i] 1062 m[au + z] = 0 1063 m[av + z] = 0 1064 m[aw + z] = 0 1065 m[aau + z] = 0 1066 m[aav + z] = 0 1067 i = i + 1 1068 } 1069 var p: i64 = p0 1070 while p < p1 { 1071 let t: i64 = m[m[UV_O_CORDER] + p] 1072 let ci: i64 = m[uvf + t * UV_TRI + 0] 1073 let cj: i64 = m[uvf + t * UV_TRI + 1] 1074 let ck: i64 = m[uvf + t * UV_TRI + 2] 1075 let vi: i64 = m[uvsrc + ci] 1076 let vj: i64 = m[uvsrc + cj] 1077 let vk: i64 = m[uvsrc + ck] 1078 // weight on edge (i,j) is the cotangent at k, and so round the triangle 1079 let wk: i64 = uvi_cotq(m, vi, vj, vk) 1080 let wi: i64 = uvi_cotq(m, vj, vk, vi) 1081 let wj: i64 = uvi_cotq(m, vk, vi, vj) 1082 m[aw + ci] = m[aw + ci] + wk + wj 1083 m[aw + cj] = m[aw + cj] + wk + wi 1084 m[aw + ck] = m[aw + ck] + wi + wj 1085 m[au + ci] = m[au + ci] + wk * m[wu + cj] + wj * m[wu + ck] 1086 m[au + cj] = m[au + cj] + wk * m[wu + ci] + wi * m[wu + ck] 1087 m[au + ck] = m[au + ck] + wi * m[wu + cj] + wj * m[wu + ci] 1088 m[av + ci] = m[av + ci] + wk * m[wv + cj] + wj * m[wv + ck] 1089 m[av + cj] = m[av + cj] + wk * m[wv + ci] + wi * m[wv + ck] 1090 m[av + ck] = m[av + ck] + wi * m[wv + cj] + wj * m[wv + ci] 1091 // the area gradient: for corner i the neighbours in winding order are j (next) and k (prev) 1092 m[aau + ci] = m[aau + ci] + m[wv + cj] - m[wv + ck] 1093 m[aav + ci] = m[aav + ci] + m[wu + ck] - m[wu + cj] 1094 m[aau + cj] = m[aau + cj] + m[wv + ck] - m[wv + ci] 1095 m[aav + cj] = m[aav + cj] + m[wu + ci] - m[wu + ck] 1096 m[aau + ck] = m[aau + ck] + m[wv + ci] - m[wv + cj] 1097 m[aav + ck] = m[aav + ck] + m[wu + cj] - m[wu + ci] 1098 p = p + 1 1099 } 1100 maxmove = 0 1101 i = 0 1102 while i < n { 1103 let q: i64 = m[list + i] 1104 var skip: i64 = 0 1105 if q == pa { skip = 1 } 1106 if q == pb { skip = 1 } 1107 if m[aw + q] <= 0 { skip = 1; m[UV_H_SING] = m[UV_H_SING] + 1 } 1108 if skip == 0 { 1109 let den: i64 = m[aw + q] 1110 let tu: i64 = (m[au + q] + UV_W_Q * m[aau + q]) / den 1111 let tv: i64 = (m[av + q] + UV_W_Q * m[aav + q]) / den 1112 let du: i64 = (tu - m[wu + q]) / UV_OMEGA_DEN 1113 let dv: i64 = (tv - m[wv + q]) / UV_OMEGA_DEN 1114 m[wu + q] = m[wu + q] + du 1115 m[wv + q] = m[wv + q] + dv 1116 let mvd: i64 = vm_abs(du) + vm_abs(dv) 1117 if mvd > maxmove { maxmove = mvd } 1118 } 1119 i = i + 1 1120 } 1121 used = used + 1 1122 if maxmove <= UV_CONV_EPS { done = 1; it = cap } 1123 } 1124 it = it + 1 1125 } 1126 if used > m[UV_H_ITERS] { m[UV_H_ITERS] = used } 1127 if maxmove > m[UV_H_MAXMOVE] { m[UV_H_MAXMOVE] = maxmove } 1128 if done == 0 { 1129 m[UV_H_NOCONV] = m[UV_H_NOCONV] + 1 1130 return UV_E_NOCONVERGE 1131 } 1132 return UV_OK 1133} 1134 1135// ceil(sqrt(nc)): the atlas grid, exactly the rule ntx_apply uses so both producers tile alike. 1136func uv_grid_for(nc: i64) -> i64 { 1137 var g: i64 = 1 1138 while g * g < nc { g = g + 1 } 1139 return g 1140} 1141 1142// place every solved chart into its own tile of a g x g atlas, UNIFORMLY SCALED. A per-axis fit would 1143// squash the chart and INTRODUCE exactly the angle distortion the solver just spent its budget removing, 1144// so the same divisor is used on both axes and only the tile's spare margin is wasted. 1145func uvi_atlas(m: *i64) -> i64 { 1146 let nc: i64 = m[UV_H_NC] 1147 let g: i64 = uv_grid_for(nc) 1148 m[UV_H_GRID] = g 1149 let tw: i64 = UV_Q16 / g 1150 let inner: i64 = tw - 2 * UV_GUTTER 1151 if inner < 1 { return UV_E_SPAN } 1152 let wu: i64 = m[UV_O_WU] 1153 let wv: i64 = m[UV_O_WV] 1154 let list: i64 = m[UV_O_LIST] 1155 let cbox: i64 = m[UV_O_CBOX] 1156 var c: i64 = 0 1157 while c < nc { 1158 let n: i64 = uvi_chart_classes(m, c) 1159 var u0: i64 = 0 1160 var u1: i64 = 0 1161 var v0: i64 = 0 1162 var v1: i64 = 0 1163 var i: i64 = 0 1164 while i < n { 1165 let z: i64 = m[list + i] 1166 if i == 0 { u0 = m[wu + z]; u1 = u0; v0 = m[wv + z]; v1 = v0 } 1167 if m[wu + z] < u0 { u0 = m[wu + z] } 1168 if m[wu + z] > u1 { u1 = m[wu + z] } 1169 if m[wv + z] < v0 { v0 = m[wv + z] } 1170 if m[wv + z] > v1 { v1 = m[wv + z] } 1171 i = i + 1 1172 } 1173 var sden: i64 = u1 - u0 1174 if v1 - v0 > sden { sden = v1 - v0 } 1175 if sden < 1 { sden = 1 } 1176 if sden > UV_SDEN_MAX { m[UV_H_ERR] = UV_E_SPAN; return UV_E_SPAN } 1177 let tx: i64 = c % g 1178 let ty: i64 = c / g 1179 i = 0 1180 while i < n { 1181 let z2: i64 = m[list + i] 1182 m[wu + z2] = tx * tw + UV_GUTTER + (m[wu + z2] - u0) * inner / sden 1183 m[wv + z2] = ty * tw + UV_GUTTER + (m[wv + z2] - v0) * inner / sden 1184 i = i + 1 1185 } 1186 // chart box in EMITTED coordinates, which is what uv_overlap_check adjudicates 1187 i = 0 1188 while i < n { 1189 let z3: i64 = m[list + i] 1190 if i == 0 { 1191 m[cbox + c * UV_BOXREC + UV_B_U0] = m[wu + z3] 1192 m[cbox + c * UV_BOXREC + UV_B_V0] = m[wv + z3] 1193 m[cbox + c * UV_BOXREC + UV_B_U1] = m[wu + z3] 1194 m[cbox + c * UV_BOXREC + UV_B_V1] = m[wv + z3] 1195 } 1196 if m[wu + z3] < m[cbox + c * UV_BOXREC + UV_B_U0] { m[cbox + c * UV_BOXREC + UV_B_U0] = m[wu + z3] } 1197 if m[wv + z3] < m[cbox + c * UV_BOXREC + UV_B_V0] { m[cbox + c * UV_BOXREC + UV_B_V0] = m[wv + z3] } 1198 if m[wu + z3] > m[cbox + c * UV_BOXREC + UV_B_U1] { m[cbox + c * UV_BOXREC + UV_B_U1] = m[wu + z3] } 1199 if m[wv + z3] > m[cbox + c * UV_BOXREC + UV_B_V1] { m[cbox + c * UV_BOXREC + UV_B_V1] = m[wv + z3] } 1200 i = i + 1 1201 } 1202 c = c + 1 1203 } 1204 // publish per-corner coordinates, the actual product of this library 1205 let cuv: i64 = m[UV_O_CUV] 1206 let uvf: i64 = m[UV_O_UVF] 1207 var k: i64 = 0 1208 while k < m[UV_H_NCORN] { 1209 let cls: i64 = m[uvf + k] 1210 m[cuv + k * UV_CUVREC + UV_P_U] = m[wu + cls] 1211 m[cuv + k * UV_CUVREC + UV_P_V] = m[wv + cls] 1212 k = k + 1 1213 } 1214 return UV_OK 1215} 1216 1217func uv_conformal(m: *i64) -> i64 { 1218 if m[UV_H_STAGE] < UV_ST_CHARTS { m[UV_H_ERR] = UV_E_STAGE; return UV_E_STAGE } 1219 m[UV_H_NOCONV] = 0 1220 m[UV_H_SING] = 0 1221 m[UV_H_ITERS] = 0 1222 m[UV_H_MAXMOVE] = 0 1223 let nc: i64 = m[UV_H_NC] 1224 var c: i64 = 0 1225 while c < nc { 1226 let rc: i64 = uvi_solve_chart(m, c) 1227 if rc != UV_OK { m[UV_H_ERR] = rc; return rc } 1228 c = c + 1 1229 } 1230 let arc: i64 = uvi_atlas(m) 1231 if arc != UV_OK { m[UV_H_ERR] = arc; return arc } 1232 m[UV_H_STAGE] = UV_ST_SOLVED 1233 m[UV_H_ERR] = UV_OK 1234 return UV_OK 1235} 1236 1237func uv_iters(m: *i64) -> i64 { return m[UV_H_ITERS] } 1238func uv_maxmove(m: *i64) -> i64 { return m[UV_H_MAXMOVE] } 1239// NAMED FOR WHAT IT COUNTS, NOT FOR WHAT IT SOUNDS LIKE: this is vertex-SWEEPS skipped because the 1240// incident cotangent sum was non-positive, so one persistently obtuse vertex contributes once per sweep 1241// rather than once. Calling it a vertex count would overstate the population by the sweep budget. 1242func uv_singular_sweeps(m: *i64) -> i64 { return m[UV_H_SING] } 1243func uv_grid(m: *i64) -> i64 { return m[UV_H_GRID] } 1244func uv_corner_u(m: *i64, t: i64, k: i64) -> i64 { return m[m[UV_O_CUV] + (t * UV_TRI + k) * UV_CUVREC + UV_P_U] } 1245func uv_corner_v(m: *i64, t: i64, k: i64) -> i64 { return m[m[UV_O_CUV] + (t * UV_TRI + k) * UV_CUVREC + UV_P_V] } 1246func uv_corner_class(m: *i64, t: i64, k: i64) -> i64 { return m[m[UV_O_UVF] + t * UV_TRI + k] } 1247 1248// ---- (3) THE DISTORTION REFEREE ---------------------------------------------------------------- 1249// Measured on the EMITTED coordinates, after atlas placement -- a metric taken on an intermediate the 1250// caller never receives would be measuring a different subject than the one it names. 1251// 1252// ANGLE. The quasi-conformal dilatation k = (smax - smin) / (smax + smin) of the affine map each triangle 1253// induces, in permil. Zero exactly when the map is a similarity on that triangle, i.e. conformal. The two 1254// singular values come from the first fundamental form of the UV-to-3D map, following the standard 1255// Sander construction, so NO LOCAL FLATTENING OF THE TRIANGLE IS NEEDED and nothing but dot products of 1256// 3D edge vectors is involved. Because k is a RATIO of singular values it is invariant to a common scale 1257// on (a,b,c), which is what lets the triple be reduced into overflow-safe range for free. 1258// 1259// AREA. The ratio of emitted area to 3D area per triangle, then each triangle's deviation from its own 1260// CHART's mean ratio, in permil. Deliberately relative to the chart rather than absolute: a chart is 1261// scaled to fit its atlas tile, so its absolute scale carries no information and an absolute ratio would 1262// report the tile size as distortion. 1263func uvi_tri_metrics(m: *i64, t: i64) -> i64 { 1264 let vi: i64 = uv_ti(m, t, 0) 1265 let vj: i64 = uv_ti(m, t, 1) 1266 let vk: i64 = uv_ti(m, t, 2) 1267 // 3D edge vectors from corner 0. Working relative to a corner rather than absolutely keeps every 1268 // product below bounded by the EDGE length instead of the coordinate magnitude. 1269 let jx: i64 = uv_vx(m, vj) - uv_vx(m, vi) 1270 let jy: i64 = uv_vy(m, vj) - uv_vy(m, vi) 1271 let jz: i64 = uv_vz(m, vj) - uv_vz(m, vi) 1272 let kx: i64 = uv_vx(m, vk) - uv_vx(m, vi) 1273 let ky: i64 = uv_vy(m, vk) - uv_vy(m, vi) 1274 let kz: i64 = uv_vz(m, vk) - uv_vz(m, vi) 1275 let su: i64 = uv_corner_u(m, t, 0) 1276 let sv: i64 = uv_corner_v(m, t, 0) 1277 let ju: i64 = uv_corner_u(m, t, 1) - su 1278 let jv: i64 = uv_corner_v(m, t, 1) - sv 1279 let ku: i64 = uv_corner_u(m, t, 2) - su 1280 let kv: i64 = uv_corner_v(m, t, 2) - sv 1281 let tsar: i64 = m[UV_O_TSAR] 1282 let tang: i64 = m[UV_O_TANG] 1283 let trat: i64 = m[UV_O_TRAT] 1284 let twoa: i64 = ju * kv - ku * jv // twice the SIGNED emitted area 1285 m[tsar + t] = twoa 1286 // twice the 3D area, exact cross product then one integer square root 1287 let cx: i64 = jy * kz - jz * ky 1288 let cy: i64 = jz * kx - jx * kz 1289 let cz: i64 = jx * ky - jy * kx 1290 let a3: i64 = vm_isqrt(cx * cx + cy * cy + cz * cz) 1291 if twoa == 0 { 1292 // A COLLAPSED TRIANGLE IS NOT A ZERO-DISTORTION TRIANGLE. Scoring it zero is precisely how a 1293 // do-nothing unwrapper that maps every vertex to one point would score perfectly, so it is its 1294 // own class and it is counted, never averaged in. 1295 m[UV_H_UVDEGEN] = m[UV_H_UVDEGEN] + 1 1296 m[tang + t] = 0 - 1 1297 m[trat + t] = 0 - 1 1298 return UV_E_UVDEGEN 1299 } 1300 if a3 <= 0 { 1301 m[UV_H_UVDEGEN] = m[UV_H_UVDEGEN] + 1 1302 m[tang + t] = 0 - 1 1303 m[trat + t] = 0 - 1 1304 return UV_E_UVDEGEN 1305 } 1306 var tw2: i64 = twoa 1307 if tw2 < 0 { tw2 = 0 - tw2 } 1308 m[trat + t] = tw2 * UV_W_Q / a3 1309 // Ss and St, translated to corner 0 so the magnitudes are edge-sized 1310 var ssx: i64 = jx * kv - kx * jv 1311 var ssy: i64 = jy * kv - ky * jv 1312 var ssz: i64 = jz * kv - kz * jv 1313 var stx: i64 = kx * ju - jx * ku 1314 var sty: i64 = ky * ju - jy * ku 1315 var stz: i64 = kz * ju - jz * ku 1316 // REDUCE BEFORE SQUARING, never after: a component past UV_SS_MAX overflows the very sum that would 1317 // have reported it as too large, so the check has to run while the number is still readable. 1318 var smx: i64 = vm_abs(ssx) 1319 if vm_abs(ssy) > smx { smx = vm_abs(ssy) } 1320 if vm_abs(ssz) > smx { smx = vm_abs(ssz) } 1321 if vm_abs(stx) > smx { smx = vm_abs(stx) } 1322 if vm_abs(sty) > smx { smx = vm_abs(sty) } 1323 if vm_abs(stz) > smx { smx = vm_abs(stz) } 1324 if smx > UV_SS_MAX { 1325 let sd: i64 = smx / UV_SS_MAX + 1 1326 ssx = ssx / sd 1327 ssy = ssy / sd 1328 ssz = ssz / sd 1329 stx = stx / sd 1330 sty = sty / sd 1331 stz = stz / sd 1332 } 1333 var fa: i64 = ssx * ssx + ssy * ssy + ssz * ssz 1334 var fb: i64 = ssx * stx + ssy * sty + ssz * stz 1335 var fc: i64 = stx * stx + sty * sty + stz * stz 1336 // reduce by a common divisor -- free, because the dilatation is scale-invariant 1337 var mx: i64 = fa 1338 if fc > mx { mx = fc } 1339 let fbA: i64 = vm_abs(fb) 1340 if fbA > mx { mx = fbA } 1341 if mx > UV_ABC_MAX { 1342 let d: i64 = mx / UV_ABC_MAX + 1 1343 fa = fa / d 1344 fb = fb / d 1345 fc = fc / d 1346 } 1347 var det: i64 = fa * fc - fb * fb 1348 if det < 0 { det = 0 } // Cauchy-Schwarz guarantees this; the clamp is against rounding 1349 let gdet: i64 = vm_isqrt(det) 1350 let sp2: i64 = fa + fc + 2 * gdet // (smax + smin)^2 1351 var sm2: i64 = fa + fc - 2 * gdet // (smax - smin)^2 1352 if sm2 < 0 { sm2 = 0 } 1353 if sp2 <= 0 { 1354 m[UV_H_UVDEGEN] = m[UV_H_UVDEGEN] + 1 1355 m[tang + t] = 0 - 1 1356 return UV_E_UVDEGEN 1357 } 1358 m[tang + t] = vm_isqrt(sm2 * UV_PERMIL * UV_PERMIL / sp2) 1359 return UV_OK 1360} 1361 1362func uv_distortion(m: *i64) -> i64 { 1363 if m[UV_H_STAGE] < UV_ST_SOLVED { m[UV_H_ERR] = UV_E_STAGE; return UV_E_STAGE } 1364 let nt: i64 = m[UV_H_NT] 1365 let nc: i64 = m[UV_H_NC] 1366 m[UV_H_UVDEGEN] = 0 1367 m[UV_H_FLIP] = 0 1368 let tang: i64 = m[UV_O_TANG] 1369 let trat: i64 = m[UV_O_TRAT] 1370 let tsar: i64 = m[UV_O_TSAR] 1371 let cdist: i64 = m[UV_O_CDIST] 1372 var t: i64 = 0 1373 var bad: i64 = UV_OK 1374 while t < nt { 1375 let rc: i64 = uvi_tri_metrics(m, t) 1376 if rc != UV_OK { bad = rc } 1377 t = t + 1 1378 } 1379 // FOLD: a triangle whose emitted winding opposes its chart's majority. The chart's own signed-area 1380 // sum decides the majority, so an entirely mirrored chart is NOT reported as folded -- a global 1381 // reflection is a legitimate parameterisation, a local one is a fold. 1382 var c: i64 = 0 1383 while c < nc { 1384 var tot: i64 = 0 1385 var p: i64 = m[m[UV_O_CSTART] + c] 1386 let pe: i64 = m[m[UV_O_CSTART] + c + 1] 1387 while p < pe { tot = tot + m[tsar + m[m[UV_O_CORDER] + p]]; p = p + 1 } 1388 p = m[m[UV_O_CSTART] + c] 1389 while p < pe { 1390 let sa: i64 = m[tsar + m[m[UV_O_CORDER] + p]] 1391 if tot >= 0 { if sa < 0 { m[UV_H_FLIP] = m[UV_H_FLIP] + 1 } } 1392 if tot < 0 { if sa > 0 { m[UV_H_FLIP] = m[UV_H_FLIP] + 1 } } 1393 p = p + 1 1394 } 1395 c = c + 1 1396 } 1397 // per-chart angle mean/max, and area deviation from the chart's own mean ratio 1398 c = 0 1399 while c < nc { 1400 let p0: i64 = m[m[UV_O_CSTART] + c] 1401 let p1: i64 = m[m[UV_O_CSTART] + c + 1] 1402 var cnt: i64 = 0 1403 var asum: i64 = 0 1404 var amax: i64 = 0 1405 var rsum: i64 = 0 1406 var p2: i64 = p0 1407 while p2 < p1 { 1408 let tt: i64 = m[m[UV_O_CORDER] + p2] 1409 if m[tang + tt] >= 0 { 1410 cnt = cnt + 1 1411 asum = asum + m[tang + tt] 1412 if m[tang + tt] > amax { amax = m[tang + tt] } 1413 rsum = rsum + m[trat + tt] 1414 } 1415 p2 = p2 + 1 1416 } 1417 var amean: i64 = 0 1418 var rmean: i64 = 0 1419 if cnt > 0 { amean = asum / cnt; rmean = rsum / cnt } 1420 var dsum: i64 = 0 1421 var dmax: i64 = 0 1422 if rmean > 0 { 1423 p2 = p0 1424 while p2 < p1 { 1425 let tt2: i64 = m[m[UV_O_CORDER] + p2] 1426 if m[trat + tt2] >= 0 { 1427 let dev: i64 = vm_abs(m[trat + tt2] - rmean) * UV_PERMIL / rmean 1428 dsum = dsum + dev 1429 if dev > dmax { dmax = dev } 1430 } 1431 p2 = p2 + 1 1432 } 1433 } 1434 var dmean: i64 = 0 1435 if cnt > 0 { dmean = dsum / cnt } 1436 m[cdist + c * UV_DISTREC + UV_D_AMEAN] = amean 1437 m[cdist + c * UV_DISTREC + UV_D_AMAX] = amax 1438 m[cdist + c * UV_DISTREC + UV_D_DMEAN] = dmean 1439 m[cdist + c * UV_DISTREC + UV_D_DMAX] = dmax 1440 c = c + 1 1441 } 1442 // whole-mesh rollup: mean of the per-chart means weighted by triangle count, worst of the maxima 1443 var wa: i64 = 0 1444 var wd: i64 = 0 1445 var wn: i64 = 0 1446 var ma: i64 = 0 1447 var md: i64 = 0 1448 c = 0 1449 while c < nc { 1450 let k: i64 = m[m[UV_O_CCNT] + c] 1451 wa = wa + m[cdist + c * UV_DISTREC + UV_D_AMEAN] * k 1452 wd = wd + m[cdist + c * UV_DISTREC + UV_D_DMEAN] * k 1453 wn = wn + k 1454 if m[cdist + c * UV_DISTREC + UV_D_AMAX] > ma { ma = m[cdist + c * UV_DISTREC + UV_D_AMAX] } 1455 if m[cdist + c * UV_DISTREC + UV_D_DMAX] > md { md = m[cdist + c * UV_DISTREC + UV_D_DMAX] } 1456 c = c + 1 1457 } 1458 if wn > 0 { m[UV_H_ANGMEAN] = wa / wn; m[UV_H_AREAMEAN] = wd / wn } 1459 m[UV_H_ANGMAX] = ma 1460 m[UV_H_AREAMAX] = md 1461 m[UV_H_STAGE] = UV_ST_MEASURED 1462 if bad != UV_OK { m[UV_H_ERR] = bad; return bad } 1463 m[UV_H_ERR] = UV_OK 1464 return UV_OK 1465} 1466 1467func uv_angle_mean(m: *i64) -> i64 { return m[UV_H_ANGMEAN] } 1468func uv_angle_max(m: *i64) -> i64 { return m[UV_H_ANGMAX] } 1469func uv_area_mean(m: *i64) -> i64 { return m[UV_H_AREAMEAN] } 1470func uv_area_max(m: *i64) -> i64 { return m[UV_H_AREAMAX] } 1471func uv_flipped(m: *i64) -> i64 { return m[UV_H_FLIP] } 1472func uv_uvdegen(m: *i64) -> i64 { return m[UV_H_UVDEGEN] } 1473func uv_chart_angle_mean(m: *i64, c: i64) -> i64 { 1474 if c < 0 { return 0 - 1 } 1475 if c >= m[UV_H_NC] { return 0 - 1 } 1476 return m[m[UV_O_CDIST] + c * UV_DISTREC + UV_D_AMEAN] 1477} 1478func uv_chart_angle_max(m: *i64, c: i64) -> i64 { 1479 if c < 0 { return 0 - 1 } 1480 if c >= m[UV_H_NC] { return 0 - 1 } 1481 return m[m[UV_O_CDIST] + c * UV_DISTREC + UV_D_AMAX] 1482} 1483func uv_chart_area_mean(m: *i64, c: i64) -> i64 { 1484 if c < 0 { return 0 - 1 } 1485 if c >= m[UV_H_NC] { return 0 - 1 } 1486 return m[m[UV_O_CDIST] + c * UV_DISTREC + UV_D_DMEAN] 1487} 1488func uv_chart_area_max(m: *i64, c: i64) -> i64 { 1489 if c < 0 { return 0 - 1 } 1490 if c >= m[UV_H_NC] { return 0 - 1 } 1491 return m[m[UV_O_CDIST] + c * UV_DISTREC + UV_D_DMAX] 1492} 1493// THE ANTI-VACUITY MEASUREMENT. Summed unsigned emitted area of a chart, in squared Q16 units. An 1494// unwrapper that maps everything to one point reads exactly zero here while scoring zero on any 1495// distortion metric that averages only over non-degenerate triangles -- so this is the number that 1496// separates a perfect parameterisation from no parameterisation at all. 1497func uv_chart_uv_area(m: *i64, c: i64) -> i64 { 1498 if c < 0 { return 0 - 1 } 1499 if c >= m[UV_H_NC] { return 0 - 1 } 1500 if m[UV_H_STAGE] < UV_ST_MEASURED { return 0 - 1 } 1501 var s: i64 = 0 1502 var p: i64 = m[m[UV_O_CSTART] + c] 1503 let pe: i64 = m[m[UV_O_CSTART] + c + 1] 1504 while p < pe { 1505 s = s + vm_abs(m[m[UV_O_TSAR] + m[m[UV_O_CORDER] + p]]) 1506 p = p + 1 1507 } 1508 return s / 2 1509} 1510 1511// ---- (4) OVERLAP ------------------------------------------------------------------------------- 1512// Two questions, kept apart because they have different remedies and different strengths of proof. 1513// BETWEEN CHARTS the answer is exact: every chart owns one tile of the grid and the boxes are compared 1514// in emitted coordinates, so an overlap is a real overlap and REFUSED. Tile ownership makes this 1515// impossible by construction, which is exactly why the gate has to PLANT one to show the detector 1516// fires -- a check that has only ever been asked an impossible question has not been shown to work. 1517// WITHIN A CHART the honest answer is narrower and it is stated rather than dressed up: the flipped- 1518// triangle count is a NECESSARY condition for local injectivity, NOT a sufficient one for global 1519// injectivity. A chart can in principle wrap onto itself with every triangle correctly oriented, and 1520// this does not detect that. It detects the fold class a relaxation actually produces, and a caller 1521// who needs the global guarantee needs a pairwise test this file does not claim to have. 1522func uv_overlap_check(m: *i64) -> i64 { 1523 if m[UV_H_STAGE] < UV_ST_MEASURED { m[UV_H_ERR] = UV_E_STAGE; return UV_E_STAGE } 1524 let nc: i64 = m[UV_H_NC] 1525 let cbox: i64 = m[UV_O_CBOX] 1526 m[UV_H_OVERLAP] = 0 1527 var a: i64 = 0 1528 while a < nc { 1529 var b: i64 = a + 1 1530 while b < nc { 1531 var hit: i64 = 1 1532 if m[cbox + a * UV_BOXREC + UV_B_U1] < m[cbox + b * UV_BOXREC + UV_B_U0] { hit = 0 } 1533 if m[cbox + b * UV_BOXREC + UV_B_U1] < m[cbox + a * UV_BOXREC + UV_B_U0] { hit = 0 } 1534 if m[cbox + a * UV_BOXREC + UV_B_V1] < m[cbox + b * UV_BOXREC + UV_B_V0] { hit = 0 } 1535 if m[cbox + b * UV_BOXREC + UV_B_V1] < m[cbox + a * UV_BOXREC + UV_B_V0] { hit = 0 } 1536 if hit == 1 { m[UV_H_OVERLAP] = m[UV_H_OVERLAP] + 1 } 1537 b = b + 1 1538 } 1539 a = a + 1 1540 } 1541 if m[UV_H_FLIP] > 0 { m[UV_H_ERR] = UV_E_FOLD; return UV_E_FOLD } 1542 if m[UV_H_OVERLAP] > 0 { m[UV_H_ERR] = UV_E_OVERLAP; return UV_E_OVERLAP } 1543 m[UV_H_ERR] = UV_OK 1544 return UV_OK 1545} 1546 1547func uv_overlaps(m: *i64) -> i64 { return m[UV_H_OVERLAP] } 1548func uv_chart_box(m: *i64, c: i64, k: i64) -> i64 { 1549 if c < 0 { return 0 - 1 } 1550 if c >= m[UV_H_NC] { return 0 - 1 } 1551 if k < 0 { return 0 - 1 } 1552 if k >= UV_BOXREC { return 0 - 1 } 1553 return m[m[UV_O_CBOX] + c * UV_BOXREC + k] 1554} 1555// EXISTS FOR THE NEGATIVE CONTROL AND FOR NOTHING ELSE, and is named so no reader mistakes it for part 1556// of the pipeline. Tile ownership makes a genuine cross-chart overlap unreachable, so the only way to 1557// show uv_overlap_check can fire is to plant one; a detector that has never been given a positive case 1558// is indistinguishable from a detector that cannot fire at all. 1559func uv_force_box(m: *i64, c: i64, u0: i64, v0: i64, u1: i64, v1: i64) -> i64 { 1560 if c < 0 { return UV_E_ARGS } 1561 if c >= m[UV_H_NC] { return UV_E_ARGS } 1562 m[m[UV_O_CBOX] + c * UV_BOXREC + UV_B_U0] = u0 1563 m[m[UV_O_CBOX] + c * UV_BOXREC + UV_B_V0] = v0 1564 m[m[UV_O_CBOX] + c * UV_BOXREC + UV_B_U1] = u1 1565 m[m[UV_O_CBOX] + c * UV_BOXREC + UV_B_V1] = v1 1566 return UV_OK 1567} 1568 1569// EXISTS FOR THE ANTI-VACUITY CONTROL AND FOR NOTHING ELSE, and named so no reader mistakes it for part 1570// of the pipeline. The failure this file most needs to be unable to hide is the do-nothing unwrapper that 1571// maps every vertex to one point: it has zero angle distortion and zero area distortion by any metric that 1572// averages over non-degenerate triangles, because it HAS no non-degenerate triangles. No solver in here 1573// produces that map, so the only way to show the degeneracy detector fires is to plant it. 1574func uv_force_corner(m: *i64, t: i64, k: i64, u: i64, v: i64) -> i64 { 1575 if t < 0 { return UV_E_ARGS } 1576 if t >= m[UV_H_NT] { return UV_E_ARGS } 1577 if k < 0 { return UV_E_ARGS } 1578 if k >= UV_TRI { return UV_E_ARGS } 1579 m[m[UV_O_CUV] + (t * UV_TRI + k) * UV_CUVREC + UV_P_U] = u 1580 m[m[UV_O_CUV] + (t * UV_TRI + k) * UV_CUVREC + UV_P_V] = v 1581 return UV_OK 1582} 1583 1584// ---- the whole pipeline, in order -------------------------------------------------------------- 1585func uv_unwrap(m: *i64) -> i64 { 1586 let a: i64 = uv_chart_build(m) 1587 if a != UV_OK { return a } 1588 let b: i64 = uv_conformal(m) 1589 if b != UV_OK { return b } 1590 let c: i64 = uv_distortion(m) 1591 if c != UV_OK { return c } 1592 return uv_overlap_check(m) 1593} 1594 1595func uv_verdict(m: *i64) -> i64 { return m[UV_H_ERR] } 1596func uv_stage(m: *i64) -> i64 { return m[UV_H_STAGE] } 1597func uv_mv_verdict(m: *i64) -> i64 { return m[UV_H_MVERR] } 1598func uv_mv_closed(m: *i64) -> i64 { return m[UV_H_MVCLOSED] } 1599 1600// ---- the TEXC bridge --------------------------------------------------------------------------- 1601// Writes the payload nx_nxa_texbake_lib already reads: [nv][stride 3][grid g][0] then per vertex 1602// [u Q16][v Q16][chart]. RETURNS THE NUMBER OF TRIANGLE CORNERS WHOSE TEXTURE COORDINATE THE PER-VERTEX 1603// FORM CANNOT CARRY -- corners, counted where they occur, NOT vertices, because naming it a vertex count 1604// would overstate one number and understate the other. Every seam vertex produces some, since a 1605// per-vertex wire format has exactly one slot per vertex and a seam exists to need two. That loss is the 1606// honest cost of the bridge and it is counted rather than hidden: a caller baking a seamed mesh through 1607// the incumbent path needs to know before the bleed shows up in a render. 1608func uv_texc_words(m: *i64) -> i64 { return UV_X_HDR + m[UV_H_NV] * UV_TRI } 1609 1610func uv_emit_texc(m: *i64, out: *i64) -> i64 { 1611 if m[UV_H_STAGE] < UV_ST_SOLVED { return UV_E_STAGE } 1612 let nv: i64 = m[UV_H_NV] 1613 let nt: i64 = m[UV_H_NT] 1614 out[UV_X_NV] = nv 1615 out[UV_X_STRIDE] = UV_TRI 1616 out[UV_X_GRID] = m[UV_H_GRID] 1617 out[UV_X_RSVD] = 0 1618 var i: i64 = 0 1619 while i < nv { out[UV_X_HDR + i * UV_TRI + UV_X_CH] = 0 - 1; i = i + 1 } 1620 var loss: i64 = 0 1621 var t: i64 = 0 1622 while t < nt { 1623 var k: i64 = 0 1624 while k < UV_TRI { 1625 let v: i64 = uv_ti(m, t, k) 1626 let cls: i64 = uv_corner_class(m, t, k) 1627 if out[UV_X_HDR + v * UV_TRI + UV_X_CH] < 0 { 1628 out[UV_X_HDR + v * UV_TRI + UV_X_U] = uv_corner_u(m, t, k) 1629 out[UV_X_HDR + v * UV_TRI + UV_X_V] = uv_corner_v(m, t, k) 1630 out[UV_X_HDR + v * UV_TRI + UV_X_CH] = m[m[UV_O_UVCH] + cls] 1631 m[m[UV_O_STAMP] + m[UV_H_NCORN] + v] = cls 1632 } else { 1633 if m[m[UV_O_STAMP] + m[UV_H_NCORN] + v] != cls { loss = loss + 1 } 1634 } 1635 k = k + 1 1636 } 1637 t = t + 1 1638 } 1639 m[UV_H_TEXCLOSS] = loss 1640 return loss 1641} 1642func uv_texc_loss(m: *i64) -> i64 { return m[UV_H_TEXCLOSS] }