nx_texpaint_lib.nx
buildroot/runtime/nx_texpaint_lib.nx
about
nx_texpaint_lib.nx -- TEXTURE PAINTING: a brush stroke placed on the SURFACE that lands in the correct
texel of the baked PBR map set, and that does not TEAR where the UV parameterization is cut.
/compare/dcc DC7, and the board says why it matters: texture-painting is the ONE axis on that board where
all four rivals are Best -- Blender paints directly onto the mesh, ZBrush PolyPaint needs no UVs assigned
in advance, Maya has the 3D Paint Tool, and ArmorPaint exists for precisely this.
CHECK-BEFORE-BUILD, MEASURED NOT ASSUMED. nx_capsearch over 6892 organs (1375 tools + 5517 libs,
corpus_complete=1) returned a glyph STROKE PEN, four 2D FRAMEBUFFER painters and one mesh SCULPT brush --
nothing that writes a texel of a texture. nx_absent then returned tp_stroke ABSENT-PROVEN at
coverage_complete=1 corpus_complete=1 over buildroot/runtime. The estate BAKES maps (ntb_bake) and
GENERATES a whole PBR set per genome (ntb_bake_pbr); it has never PAINTED one.
THE ATLAS CONTRACT IS READ OUT OF THE INCUMBENTS, NEVER INVENTED. Two files own it and both were read:
nx_nxa_texc_lib.nx -- the TEXC section. Payload words [nv, stride=3, grid g, reserved] then per vertex
[u q16 0..65535][v q16 0..65535][owning joint]. Joint j owns ONE TILE of a g x g
grid, tile width tw = 65536/g in UV units, so charts from different bones are
DISJOINT BY CONSTRUCTION. The unwrap is CYLINDRICAL around the bone: u is the
angle (a full circle mapped onto the tile) and v is the clamped position along
the parent->joint segment.
nx_nxa_texbake_lib.nx -- the atlas itself: res*res*3 u8 RGB row-major, the TM_IMG contract, and a map
set of albedo, specular, gloss and tangent-space normal.
EVERYTHING BELOW FOLLOWS FROM THOSE TWO FACTS. In particular the map COUNT is a parameter of the map set
rather than a constant in this file: a second copy of NTB_NMAP here would be a duplicate ruler that drifts
the day the PBR set grows a fifth map.
WHERE THE SEAM IS, AND WHY THE NAIVE PAINTER TEARS. Because the unwrap is cylindrical, EVERY chart carries
its own wrap seam: the texel column at the left edge of a tile and the column at the right edge are the
SAME PLACE ON THE BODY, cut apart so the cylinder could be laid flat. A brush disc centred near that cut
covers surface on both sides of it, but in the atlas the far side is a whole tile-width away. A painter
that clips the disc to the tile rectangle -- which is what a painter must do, or paint leaks onto another
bone -- writes one side and drops the other, and the asset renders with a hard-edged half-stroke: THE
TEAR. This is the defect that makes naive 3D painting unusable and it is the reason DC7 exists.
THE FIX IS A METRIC, NOT A HEURISTIC. tp_stroke_core paints with the PLANAR metric and therefore tears --
it is shipped, callable and named, because a negative control you can still run is worth more than a
paragraph saying you once deleted the fix and watched a test fail. tp_seam_bleed then writes exactly the
texels the planar pass could not reach: those whose CYLINDRICAL distance to the centre is inside the brush
while their planar distance is not. The two sets are disjoint by that definition, so no texel is ever
written twice, and their union is the full disc. tp_stroke is the composition of the two.
u WRAPS AND v DOES NOT, and that asymmetry is the geometry rather than a preference: u is an angle around
dependencies 3 imports · 1 importers
imports: nx_syscalls.nxnx_vecmath.nxnx_editstack_lib.nx
imported by: nx_texpaint_gate.nx
structs
| none |
consts
| 78 | const TP_I64_BYTES: i64 = 8 |
| 79 | const TP_CHAN: i64 = 3 // RGB. The TM_IMG contract read from nx_nxa_texbake_lib: res*res*3 u8. |
| 80 | const TP_CH_SPAN: i64 = 8 // 2^TP_CHAN: the number of distinct channel masks. Guarded by a tooth |
| 82 | const TP_VALUE_MAX: i64 = 255 // the u8 channel range of the atlas element type |
| 83 | const TP_ONE: i64 = 1000 // permil, the estate's fixed-point unit for a normalised 0..1 quantity |
| 84 | const TP_STRENGTH_MAX: i64 = 1000 // == TP_ONE, so full strength reaches the target value EXACTLY |
| 85 | const TP_BARY_ONE: i64 = 1000 // barycentric coordinates in permil; the triple must sum to this |
| 86 | const TP_UV_SPAN: i64 = 65536 // NT_Q16 from nx_nxa_texc_lib -- the codomain of a TEXC u or v word |
| 87 | const TP_HALF: i64 = 2 // the divisor in W/2; named so the margin derivation reads as arithmetic |
| 88 | const TP_BANDS: i64 = 2 // a cylinder is cut ONCE, which leaves TWO edges in the atlas -- so the |
| 92 | const TP_SMOOTH_A: i64 = 3 // the 3 in S(t) = 3t^2 - 2t^3 |
| 93 | const TP_SMOOTH_B: i64 = 2 // the 2 in S(t) = 3t^2 - 2t^3 |
| 94 | const TP_TEXEL_XY: i64 = 2 // a texel address is a pair |
| 95 | const TP_TRI_V: i64 = 3 // a triangle names three vertices |
| 101 | const TP_H_RES: i64 = 0 |
| 102 | const TP_H_NMAPS: i64 = 1 |
| 103 | const TP_H_GRID: i64 = 2 |
| 104 | const TP_H_ERR: i64 = 3 |
| 105 | const TP_HDR: i64 = 4 |
| 109 | const TP_M_PLANAR: i64 = 0 |
| 110 | const TP_M_WRAPONLY: i64 = 1 |
| 114 | const TP_OK: i64 = 0 |
| 115 | const TP_E_ARGS: i64 = 0 - 1 |
| 116 | const TP_E_RES: i64 = 0 - 2 |
| 117 | const TP_E_GRID: i64 = 0 - 3 |
| 118 | const TP_E_MAP: i64 = 0 - 4 |
| 119 | const TP_E_TEXEL: i64 = 0 - 5 |
| 120 | const TP_E_BARY: i64 = 0 - 6 |
| 121 | const TP_E_BARYSUM: i64 = 0 - 7 |
| 122 | const TP_E_TRI: i64 = 0 - 8 |
| 123 | const TP_E_VERT: i64 = 0 - 9 |
| 124 | const TP_E_RADIUS: i64 = 0 - 10 |
| 125 | const TP_E_RADIUS_CHART: i64 = 0 - 11 |
| 126 | const TP_E_STRENGTH: i64 = 0 - 12 |
| 127 | const TP_E_VALUE: i64 = 0 - 13 |
| 128 | const TP_E_CHMASK: i64 = 0 - 14 |
| 129 | const TP_E_CHART: i64 = 0 - 15 |
| 130 | const TP_E_GEOM: i64 = 0 - 16 |
| 131 | const TP_E_FULL: i64 = 0 - 17 |
| 132 | const TP_E_PUSH: i64 = 0 - 18 |
| 133 | const TP_E_MODE: i64 = 0 - 19 |
functions
| 135 | func tp_err_name(e: i64) -> *u8 called by 1: main |
| 160 | func tp_value_span() -> i64 { return TP_VALUE_MAX + 1 } |
| 161 | func tp_strength_span() -> i64 { return TP_STRENGTH_MAX + 1 } |
| 164 | func tp_ch_span_derived() -> i64 called by 1: main |
| 171 | func tp_ch_on(chmask: i64, c: i64) -> i64 called by 1: tp_put |
| 188 | func tp_smoothstep(t: i64) -> i64 |
| 195 | func tp_falloff(d: i64, r: i64) -> i64 |
| 203 | func tp_hdr_bytes() -> i64 { return TP_HDR * TP_I64_BYTES } |
| 204 | func tp_map_bytes(res: i64) -> i64 { return res * res * TP_CHAN } |
| 205 | func tp_set_bytes(res: i64, nmaps: i64) -> i64 { return tp_hdr_bytes() + nmaps * tp_map_bytes(res) } |
| 212 | func tp_geom_ok(res: i64, nmaps: i64, grid: i64) -> i64 |
| 223 | func tp_tile_uv(g: i64) -> i64 { return TP_UV_SPAN / g } |
| 226 | func tp_chart_x0(res: i64, g: i64, t: i64) -> i64 { return t * tp_tile_uv(g) * res / TP_UV_SPAN } called by 6: tg_out_of_discmaintp_chart_axistp_stroke_validtp_bleed_margintp_stroke_at calls 1: tp_tile_uv |
| 227 | func tp_chart_x1(res: i64, g: i64, t: i64) -> i64 { return (t + 1) * tp_tile_uv(g) * res / TP_UV_SPAN } called by 6: tg_out_of_discmaintp_chart_axistp_stroke_validtp_bleed_margintp_stroke_at calls 1: tp_tile_uv |
| 231 | func tp_chart_axis(res: i64, g: i64, x: i64) -> i64 |
| 241 | func tp_chart_of_texel(res: i64, g: i64, x: i64, y: i64) -> i64 |
| 252 | func tp_bleed_margin_w(w: i64, r: i64) -> i64 |
| 261 | func tp_maps_new(res: i64, nmaps: i64, grid: i64) -> *i64 |
| 271 | func tp_res(ms: *i64) -> i64 { return ms[TP_H_RES] } |
| 272 | func tp_nmaps(ms: *i64) -> i64 { return ms[TP_H_NMAPS] } |
| 273 | func tp_grid(ms: *i64) -> i64 { return ms[TP_H_GRID] } called by 1: tg_out_of_disc |
| 275 | func tp_map_ptr(ms: *i64, m: i64) -> *u8 called by 7: tg_filltp_gettp_settp_maps_copytp_maps_equaltp_maps_digest+1 calls 2: tp_hdr_bytestp_map_bytes |
| 280 | func tp_texel_off(res: i64, x: i64, y: i64, c: i64) -> i64 { return (y * res + x) * TP_CHAN + c } |
| 284 | func tp_get(ms: *i64, m: i64, x: i64, y: i64, c: i64) -> i64 |
| 297 | func tp_set(ms: *i64, m: i64, x: i64, y: i64, c: i64, v: i64) -> i64 |
| 306 | func tp_geom_same(a: *i64, b: *i64) -> i64 |
| 314 | func tp_maps_copy(dst: *i64, src: *i64) -> i64 |
| 326 | func tp_maps_equal(a: *i64, b: *i64) -> i64 |
| 338 | func tp_maps_digest(ms: *i64) -> i64 |
| 360 | func tp_bary_ok(b0: i64, b1: i64, b2: i64) -> i64 called by 1: tp_surface_to_uv |
| 371 | func tp_surface_to_uv(uv: *i64, uvstride: i64, tris: *i64, tri: i64, ntri: i64, nvert: i64, |
| 409 | func tp_uv_to_texel(u: i64, v: i64, res: i64, out2: *i64) -> i64 |
| 423 | func tp_surface_to_texel(uv: *i64, uvstride: i64, tris: *i64, tri: i64, ntri: i64, nvert: i64, |
| 433 | func tp_stroke_valid(ms: *i64, map_id: i64, chmask: i64, cx: i64, cy: i64, r: i64, |
| 462 | func tp_bleed_margin(ms: *i64, cx: i64, cy: i64, r: i64) -> i64 |
| 480 | func tp_put(m: *u8, res: i64, chmask: i64, x: i64, y: i64, dx: i64, dy: i64, r: i64, |
| 503 | func tp_stroke_at(ms: *i64, map_id: i64, chmask: i64, cx: i64, cy: i64, r: i64, |
| 570 | func tp_stroke_core(ms: *i64, map_id: i64, chmask: i64, cx: i64, cy: i64, r: i64, |
| 580 | func tp_seam_bleed(ms: *i64, map_id: i64, chmask: i64, cx: i64, cy: i64, r: i64, |
| 586 | func tp_stroke(ms: *i64, map_id: i64, chmask: i64, cx: i64, cy: i64, r: i64, |
| 606 | func tp_pack_what(map_id: i64, chmask: i64, value: i64) -> i64 |
| 609 | func tp_what_map(p: i64) -> i64 { return p / tp_value_span() / TP_CH_SPAN } |
| 610 | func tp_what_chmask(p: i64) -> i64 { return p / tp_value_span() % TP_CH_SPAN } |
| 611 | func tp_what_value(p: i64) -> i64 { return p % tp_value_span() } |
| 612 | func tp_pack_centre(res: i64, cx: i64, cy: i64) -> i64 { return cy * res + cx } |
| 613 | func tp_centre_x(res: i64, p: i64) -> i64 { return p % res } |
| 614 | func tp_centre_y(res: i64, p: i64) -> i64 { return p / res } |
| 615 | func tp_pack_brush(r: i64, strength: i64) -> i64 { return r * tp_strength_span() + strength } |
| 616 | func tp_brush_r(p: i64) -> i64 { return p / tp_strength_span() } |
| 617 | func tp_brush_strength(p: i64) -> i64 { return p % tp_strength_span() } |
| 620 | func tp_session_new(cap_strokes: i64) -> *i64 { return es_new(cap_strokes, cap_strokes) } |
| 621 | func tp_strokes(st: *i64) -> i64 { return es_head(st) } |
| 628 | func tp_record(st: *i64, ms: *i64, map_id: i64, chmask: i64, cx: i64, cy: i64, r: i64, called by 2: tg_record_sessionmain calls 7: tp_stroke_valides_heades_capes_pushtp_pack_whattp_pack_centre+1 |
| 645 | func tp_reeval(st: *i64, ms: *i64, idx: i64, map_id: i64, chmask: i64, cx: i64, cy: i64, r: i64, |
| 665 | func tp_render(st: *i64, base: *i64, out: *i64) -> i64 |