Skip to content

Phase 3: native cvc::model + Assimp mesh/model loader - #158

Merged
transfix merged 3 commits into
masterfrom
feat/native-model-assimp-phase3
Jul 26, 2026
Merged

Phase 3: native cvc::model + Assimp mesh/model loader#158
transfix merged 3 commits into
masterfrom
feat/native-model-assimp-phase3

Conversation

@transfix

Copy link
Copy Markdown
Owner

Native import for standard mesh/model formats (obj, ply, stl, fbx, gltf/glb, dae, 3ds, …) into libcvc — VTK-free, mirroring the Phase-1 cvc::image + geometry_file_io registry pattern. Unblocks Phase 6 (volrover3 drops vtkGLTFReader for pycvc.load_model).

What's added

  • cvc::model — a small scene value type: meshes (each a cvc::geometry + material index) + cvc::material (PBR-ish base color / metallic / roughness / emissive + a base-color texture decoded to cvc::image below the VTK line). merged() flattens to one geometry (offsetting indices via geometry::merge); extents() unions bboxes.
  • model_file_io registryread_model/write_model + register_default_model_handlers(), a faithful mirror of geometry_file_io/image_file_io.
  • Assimp handlers (guarded by CVC_ENABLE_ASSIMP) — assimp_model_io (full multi-mesh scene) + assimp_geometry_io that flattens via model::merged() so read_geometry("mesh.obj") also works. Lowers positions/normals/vertex-colors/UV0/tangents (reconstructed handedness w) + materials incl. embedded (*N) and external textures → cvc::image (texture failures non-fatal).
  • BuildCVC_ENABLE_ASSIMP option + find_package(assimp CONFIG) with graceful fallback + PUBLIC link/define (mirrors ImageMagick).
  • Recipe+ assimp runtime dep (validated with --recipes-dir).

Heads-up: a real cvcpkg assimp-recipe bug, worked around here

The published assimp bundle's cmake export hardcodes an absolute build-sandbox zlib path (/tmp/cvcpkg-builder/…/libz.so) in INTERFACE_LINK_LIBRARIES — the recipe builds with -DASSIMP_BUILD_ZLIB=OFF and assimp's CMake bakes ${ZLIB_LIBRARIES} (a FindZLIB absolute path) into the export instead of the relocatable ZLIB::ZLIB. That path exists on no consumer, so link assimp::assimp fails everywhere including CI. This PR defensively rewrites that stale entry to ZLIB::ZLIB in libcvc's CMake (libcvc links zlib itself; the rewrite is a forward-compatible no-op once the recipe is fixed). Follow-up: fix the assimp recipe to export zlib via an imported target (libcvc-deps).

Verification

Built libcvc.so + model_test against a pristine assimp prefix (symlink workarounds removed) — links cleanly, and all 8 gtest cases pass (merged() index offsets, extents() union, unsupported-extension throw, registry-has-obj, OBJ+UVs+material+texture, read_geometry flatten, STL-no-UVs). clang-format clean; recipe validates.

Ships on the next family publish (compute-revisions auto-bumps libcvc's revision).

Joe Rivera added 3 commits July 26, 2026 16:49
Adds native import for standard mesh/model formats (obj, ply, stl, fbx,
gltf/glb, dae, 3ds, ...) into libcvc, VTK-free, mirroring the Phase-1
image + geometry_file_io registry pattern.

- cvc::model (inc/cvc/model/model.h, src/cvc/model/model.cpp): a small
  scene value type — meshes (each a cvc::geometry + material index) plus
  cvc::material (PBR-ish: base_color, metallic/roughness, emissive, and a
  base-color texture decoded to a cvc::image below the VTK line).
  merged() flattens to one geometry (offsetting indices via geometry::merge);
  extents() unions the mesh bboxes.
- model_file_io registry (inc/cvc/model/model_file_io.h + .cpp): read_model/
  write_model + register_default_model_handlers(), a faithful mirror of
  geometry_file_io/image_file_io (extension dispatch, first-handler-wins,
  lazy re-entrancy-guarded default registration).
- Assimp handlers (src/cvc/model/assimp_io.cpp, guarded by CVC_ENABLE_ASSIMP):
  an assimp_model_io (full multi-mesh scene) + an assimp_geometry_io that
  flattens via model::merged() so read_geometry("mesh.obj") works too.
  Lowers positions/normals/vertex-colors/UV0/tangents (with reconstructed
  handedness w) and materials incl. embedded (*N) and external base-color
  textures resolved to cvc::image (texture failures are non-fatal). When the
  flag is off, registration is a no-op and read_model raises 'no handler'.
- Build wiring (src/cvc/CMakeLists.txt): CVC_ENABLE_ASSIMP option +
  find_package(assimp CONFIG) with graceful fallback + PUBLIC link/define
  (mirrors ImageMagick). DEFENSIVE: rewrites assimp's exported ABSOLUTE
  build-sandbox zlib path (a cvcpkg assimp-recipe quirk from
  -DASSIMP_BUILD_ZLIB=OFF + FindZLIB) to the relocatable ZLIB::ZLIB, or CI
  would fail to link assimp::assimp.
- Recipe (cvcpkg/recipes/libcvc/recipe.yaml): + assimp runtime dep (validated).
- Tests (src/cvc/tests/model_test.cpp): 8 gtest cases — merged() index
  offsets, extents() union, unsupported-extension throw, registry has obj,
  OBJ+UVs+material+texture, read_geometry flatten, STL-no-UVs. All pass
  against a pristine assimp prefix; library links; clang-format clean.
Adversarial review of PR #158 confirmed one blocker + two minors; fixed:

1. BLOCKER — model::merged() desynced per-vertex attributes for meshes with
   heterogeneous attribute presence. geometry::merge appends normals/colors/
   uvs/tangents by raw insert WITHOUT padding to the vertex count (it only
   offsets the appended indices), so flattening a UV'd mesh together with a
   UV-less one (routine for multi-material OBJ/glTF) left the merged array
   shorter than points() and misaligned against the offset triangle indices —
   a consumer indexing uvs[vertexIndex] then reads out of bounds or the wrong
   vertex. Fixed in model::merged() (Phase-3-scoped, no change to shared
   geometry::merge): if ANY mesh carries an attribute, pad the meshes lacking
   it to full length with a neutral default before folding, so every present
   per-vertex array stays exactly num_points() long and index-aligned in either
   mesh order. New regression test MergedPadsHeterogeneousAttributes fails
   without the fix (uvs.size()==3 vs 6) and passes with it.

2. MINOR — embedded-texture temp filename used a plain 'static int counter'
   (collides across concurrent reads / processes sharing TMPDIR). Now an
   atomic counter qualified by getpid().

3. Strengthened ObjWithUvsMaterialTexture: assert the decoded texture's first
   pixel RGBA == authored (200,100,50,255) (guards channel order / decode, not
   just dims) and that CalcTangentSpace produced full-length tangents with
   |w|==1.

Not changed: the registry's first-call default-registration guard uses plain
static bools (a benign race on concurrent first use) — this mirrors the
existing geometry_file_io/image_file_io registries verbatim; making just this
one thread-safe would diverge from the established pattern (a proper fix is a
shared-registry follow-up). model_test: 9/9 pass; clang-format clean.
fetch-libcvc-deps installs libcvc's recipe dependency closure on EVERY
build platform (CI + publish), and assimp currently has no windows bundle
(only linux/macos-arm64/freebsd/netbsd). An unconditional 'assimp' dep
therefore breaks the windows deps-install — package-windows passes on
master, so this would be a regression. Scope the dep with
'platforms: [linux, macos, freebsd, netbsd]' (same mechanism as
pthreads4w's [windows]). Where assimp is absent, find_package(assimp
CONFIG) misses and CVC_ENABLE_ASSIMP flips OFF, so libcvc still builds
(without the native model loader). Add windows once an assimp windows
bundle exists.
@transfix
transfix merged commit 943cfe1 into master Jul 26, 2026
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant