nx_vnormals_lib.nx
buildroot/runtime/nx_vnormals_lib.nx
about
nx_vnormals_lib.nx -- ONE OWNER for AREA-WEIGHTED smooth per-vertex normals over an AoS vertex buffer
and a flat triangle-index buffer. Retires the copy in nx_gltf_export.gl_vnormals and the copy that was
INLINE inside nx_obj_export.write_obj.
WHY THIS FILE EXISTS AND WHY IT IS NOT THE OBVIOUS MERGE. The estate had THREE per-vertex-normal
computations. Two of them are the same computation and are retired here. The THIRD IS NOT THE SAME
COMPUTATION AND IS DELIBERATELY LEFT WHERE IT IS -- merging it would have silently changed shipped
geometry, which is how this estate once deleted live capability by consolidating things that merely
looked alike:
sc_vertex_normals (nx_shellclose.nx) NORMALISES EACH FACE NORMAL TO UNIT LENGTH BEFORE ACCUMULATING
it -- `nrm[vi] = nrm[vi] + (nx * SC_Q) / nl` -- so every incident face contributes EQUALLY regardless
of its area. That is UNIFORM (equal-weight) smoothing.
The two copies retired here accumulate the RAW cross product, whose magnitude is twice the triangle's
area, so a large face dominates a small one. That is AREA-WEIGHTED smoothing.
They agree only on a mesh whose incident triangles all have equal area, and they diverge without bound
otherwise. nx_vnormals_gate MEASURES that divergence against the real shipped sc_vertex_normals rather
than asserting it: on two triangles meeting at a vertex with a 100:1 area ratio, area weighting yields a
normal 99.5 percent of the way toward the large face while uniform weighting sits at exactly 45 degrees
between them, and the gate proves the two results are not even PARALLEL. Three further differences are
recorded for the same reason -- a future reader will be tempted by this merge again:
* output scale 1000 here versus SC_Q = 256 there,
* a vertex with no usable incident face becomes +Y up here and stays the ZERO vector there,
* sc_isqrt is a fixed 24-iteration Newton with its seed clamped to 65536, which for a non-perfect
square can settle on either side of the true root, where vm_isqrt iterates to convergence and is an
exact floor. Two rulers, not one.
THE OUTPUT SCALE IS NOT A CHOICE MADE HERE. VN_SCALE is 1000 because that is the scale BOTH retired
copies already emitted and both of their consumers already expect. Changing it here would be a silent
format change to two shipped exporters, so it is named as inherited rather than picked.
CORRECTION 2026-08-25 -- THE HALF OF THAT SENTENCE THAT WAS A COMMENT, NOT A MEASUREMENT. This header
used to justify the scale with "glTF renderers re-normalise after the node's inverse-transpose". That is
FALSE as a statement about the FORMAT: glTF 2.0 requires the NORMAL accessor to carry unit vectors, and
KhronosGroup/glTF-Validator ranks a non-unit one an Error, not a warning -- 33,656 of them across the 12
.glb files nx_gltf_export had published. The OBJ half of the sentence stands (importers do rescale), and
THAT is why the scale is still right here and the defect was never in this function: it was at the glTF
write site, and it is fixed there, in nx_glbnorm_lib.gn_unit3. This file is deliberately unchanged in
dependencies 1 imports · 8 importers
imports: nx_vecmath.nx
imported by: nx_glbnorm_gate.nxnx_gltf_export.nxnx_gltf_export_material_candidate_t340.nxnx_gltf_export_multipart_candidate_t345.nxnx_morf_draw_core.nxnx_obj_cohort_qualification_t282.nxnx_obj_export.nxnx_vnormals_gate.nx
structs
| none |
consts
| 47 | const VN_SCALE: i64 = 1000 |
| 48 | const VN_V3: i64 = 3 // components per vertex in the estate's AoS vertex buffers |
| 49 | const VN_TRI_I: i64 = 3 // vertex indices per triangle in the estate's flat index buffers |
| 50 | const VN_X: i64 = 0 |
| 51 | const VN_Y: i64 = 1 |
| 52 | const VN_Z: i64 = 2 |
| 58 | const VN_FALLBACK_X: i64 = 0 |
| 59 | const VN_FALLBACK_Y: i64 = 1000 // == VN_SCALE, and deliberately spelled out beside its siblings |
| 60 | const VN_FALLBACK_Z: i64 = 0 |
| 64 | const VN_MIN_LEN: i64 = 1 |
| 66 | const VN_OK: i64 = 0 |
| 67 | const VN_E_ARGS: i64 = 0 - 48 // clear of nx_editstack_lib (-1..-10), nx_vpick_lib (-16..-26) |
functions
| 71 | func vn_code_name(c: i64) -> *u8 |
| 86 | func vn_smooth_area(vbuf: *i64, fbuf: *i64, nv: i64, nf: i64, nrm: *i64) -> i64 |