Skip to content

v0.3.0

Choose a tag to compare

@cdcseacave cdcseacave released this 27 Aug 20:03
· 5 commits to develop since this release

Half-edge–primary mesh processing

The half-edge is now the working representation and faces is a derived
snapshot, so a multi-stage pipeline pays one HalfMesh::Build instead of one
per family transition. On a 5 M-face mesh a native clean (spurious → spikes →
holes → unref) performs exactly one build and one face harvest; tests/perf
asserts both counts.

  • Representation contract — Mesh has exactly three valid states
    (arrays-only, half-edge-only, both-and-consistent), enforced by
    ValidateInvariants(), with InvalidateFaces() / SyncFaces() /
    InvalidateHalfMesh() and a BeginHalfEdgePipeline() scope that defers
    per-stage snapshots to one final harvest.
  • New HalfMesh primitives — FRemoveBulk, FAddDisk,
    VRemoveUnreferenced; FAdd accepts isolated corners and unwinds through an
    undo log, so a rejected add leaves every array byte-identical.
  • Native repair/hole stages — the whole repair, spike, hole-closing and
    unreferenced-vertex family mutates the live half-edge instead of clearing it.
  • Simplify and RemeshIsotropic read topology from halfMesh only.

Performance

HalfMesh::Build pairs twins through a flat open-addressing table keyed by a
packed (min,max) vertex pair instead of std::unordered_map: 3.24× faster
(0.608 s vs. 1.969 s) at 495.8 MiB vs. 904.2 MiB peak on a 5,003,552-face mesh
(Release, MSVC 14.51, x64, i7-13700KF). Half-edge numbering, rejection behavior
and goldens are unchanged.

New

  • RemoveSpuriousComponents(factor), RemoveSpikes(maxIterations),
    RemoveVerticesAndFill(vertexRemoves), RemoveFacesHalfEdge,
    ComputeMeanEdgeLength.
  • RectPacking.h — PackRectangles / EstimateSquareTextureSize: the atlas
    packer's two-tier skyline+shelf core over integer pixel rectangles, with no
    mesh involved.
  • HalfMesh::BuildCount() / FFacesCount() so a pipeline can assert it
    rebuilds connectivity once.
  • Mesh::vertexNormals — authored per-vertex normals, transported rather
    than maintained. Parallel to vertices under the same contract as
    vertexColors, but explicitly not a cache: nothing recomputes it, so an
    empty array means "derive them yourself". It is remapped through every
    operation that only renumbers vertices and dropped by every operation that
    moves one. The openMVS bridge now carries normals in both directions, so a
    round trip no longer replaces an artist's normals with geometric ones.
  • BakeOntoAtlas(source, target, params) — RebakeTexture's counterpart
    for a target whose UV layout is authored and must be preserved: it reads
    target.faceTexcoords instead of generating a new atlas, so only the texels
    change.
  • BakeParams::faceMask — restrict rasterization to selected target faces.
    Under a mask each page is seeded from target.texturesDiffuse rather than
    black, so unselected texels keep what they had and the bake is a true
    in-place edit of one region of an existing texture.

Breaking changes

  • CloseHoles changed meaning — the first parameter is now maxHoleEdges
    (default 30), a size threshold, not the old nCloseHoles = 200 count.
    A scanned surface's large open boundary now stays open by default.
  • vertexColors is a contractual parallel array — empty, or exactly as long
    as vertices, enforced by ValidateInvariants().
  • faceTexblobs is std::vector<Mesh::TexIndex> (uint8), matching openMVS's
    MVS::Mesh::TexIndex; MAX_TEXBLOBS is 255. The PLY on-disc property stays
    int32, so existing files are unaffected.
  • SaveGLTF takes imageFormat and embedImages (defaulted to the previous
    embedded-JPEG behavior).
  • glTF save → load is now an identity. LoadGLTF previously applied
    SaveGLTF's z-up → y-up root-node matrix like any other node transform and
    returned the mesh rotated 90° about X, so the library could not read back what
    it had just written. It now converts back to z-up after flattening the node
    hierarchy. The rotation stays in the file, so exports remain upright in
    Blender, three.js and Cesium. halfmesh is z-up in memory, its glTF files are
    y-up
    is now a stated, fixed contract rather than an implied one. A y-up glTF
    from any exporter loads correctly; a glTF carrying z-up data under an identity
    node — non-conformant, but emitted by some writers, including openMVS's own
    pre-halfmesh exporter — loads rotated and must be corrected by its producer.
    PLY is unaffected.
  • A caller that hand-edits the public faces array must call
    InvalidateHalfMesh()
    — the old count heuristic is gone.
  • Not ABI-compatible with 0.2.0 — recompile against the new headers.

Also

  • openMVS interop gained consuming ConvertMesh overloads (rvalue source), so a
    conversion peaks at the larger mesh plus one array rather than both at once.
  • vcpkg builtin-baseline advanced to 0ac8df3b; the vcpkg-overlay-ports/
    tinygltf hotfix is gone — a cold-cache build resolves everything from the
    registry.
  • LoadPLY kept the previous mesh's optional arrays whenever the new file
    carried none, leaving them the wrong length against the new vertex and face
    counts; both loaders now reset the optional set up front.
  • tinygltf is now built with TINYGLTF_NOEXCEPTION / JSON_NOEXCEPTION, so glTF
    JSON parsing no longer needs exceptions. OpenCV and tinyply still throw, so the
    library as a whole is not yet buildable with exceptions disabled.
  • glTF textures are decoded and encoded with OpenCV imgcodecs instead of the
    bundled stb codecs (TINYGLTF_NO_STB_IMAGE / _WRITE) — one image path for PLY
    sidecars and glTF textures alike, and no stb compiled into the build at all.
    JPEG export keeps the quality stb hardcoded, so file output is unchanged.

Full detail: CHANGELOG.md.

Python wheels

manylinux_2_28 x86_64 wheels for CPython 3.10–3.13 are attached below:

pip install https://github.com/cdcseacave/halfmesh/releases/download/v0.3.0/halfmesh-0.3.0-cp312-cp312-manylinux_2_28_x86_64.whl