Skip to content

Commit e308a67

Browse files
Merge pull request #804 from fernandotonon/feat/anim-slice-b-vertex-alembic
feat(#519): Anim Slice B — vertex animation + Alembic (.abc) import
2 parents d861af7 + aa139fd commit e308a67

35 files changed

Lines changed: 4784 additions & 357 deletions

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ qtmesh info model.fbx --json # show mesh info (JSON)
4242
qtmesh convert model.fbx -o model.gltf2 # convert between formats
4343
qtmesh fix model.fbx -o fixed.fbx # re-import/export with standard optimizations
4444
qtmesh fix model.fbx --all # apply all extra fixes (remove degenerates, merge materials)
45+
qtmesh anim cache.abc --info # Alembic vertex-cache metadata (frames/verts/fps/duration; needs -DENABLE_ALEMBIC)
46+
qtmesh anim cache.abc --info --json # same, as JSON
4547
qtmesh anim model.fbx --list # list animations
4648
qtmesh anim model.fbx --list --json # list animations (JSON)
4749
qtmesh anim model.fbx --rename "Take 001" "Idle" -o out.fbx # rename an animation
@@ -245,6 +247,15 @@ Three singletons manage core state. All run on the main thread. Access via `Clas
245247
- **Subdivide**: 1-to-4 triangle split. Adjacent non-selected faces are retriangulated against the new midpoints to avoid T-junctions (1/2/3 split-edge cases). Wired to a toolbar button (⊞) — face mode subdivides selected tris, edge mode subdivides every triangle incident to a selected edge.
246248
- **Fill**: vertex mode fan-triangulates the selected verts (3 → triangle, 4 → quad, N → N-2 tris); edge mode detects a closed boundary loop via degree-2 walk and caps it. Toolbar button (◆) and `F` shortcut. Cross-submesh inputs and duplicates of existing triangles are rejected.
247249

250+
### Animation systems beyond skeletal (epic #517)
251+
252+
The animation pipeline started skeleton-only; the #517 epic broadens it. Slices A/C/D shipped: **MorphAnimationManager** (`src/MorphAnimationManager.{h,cpp}`, morph targets / blend shapes — Ogre `Pose` + `VAT_POSE` tracks, `qtmesh morph` CLI, MCP, undo via `commands/MorphCommands`), **NodeAnimationManager** (non-skinned node TRS), **PoseLibrary** (named poses). All are `QML_SINGLETON`s registered in `mainwindow.cpp`.
253+
254+
- **Morph authoring UX (Blender-parity, #519):** morph targets are authored/edited/reordered in the Edit-Mode **"Vertex Morph Animation"** Inspector group (NOT Animation Mode — Animation Mode's list shows only real clips, morph animations are filtered out of `PropertiesPanelController::animationData()` by name). Authoring is a **non-destructive sculpt session**: `EditModeController::beginMorphSculpt()` snapshots base positions; `+ Add` captures the current edit as a target (delta vs base); `endMorphSculpt()` / exiting Edit Mode **restores the base** (the target lives on as a `Pose` + weight). `EditableMesh::commitToEntity` refreshes the pose buffer (`AnimationStateSet::_notifyDirty()` + `entity->_updateAnimation()`) for vertex-animated entities so edits render live while the frame loop is paused. Reorder via `MorphAnimationManager::moveMorphTarget`/`moveMorphTargetToIndex` (undoable `ReorderMorphTargetsCommand` — VAT_POSE keyframes reference poses by index, so it rebuilds all targets in the new order). **Weight keyframing over time** (Slice 2): `setMorphWeightKeyframe(name, time, weight)` writes to one shared clip `MorphAnimationManager::kWeightClipName` ("MorphAnim") — a VAT_POSE track per target's pose, each keyframe referencing the pose at influence == weight; the per-target "◈ Key" button records the weight at the timeline playhead (`AnimationControlController.sliderValue`), diamonds show in the dope sheet (`allMorphRows()` prefers the MorphAnim track's per-pose keyframe times). **glTF export:** `buildAiScene` emits blend-shape targets (`aiMesh::mAnimMeshes` via `attachMorphTargetsToAiMesh`) AND a morph-weights animation (`aiMeshMorphAnim` from the MorphAnim clip). FBX blend-shape weight export is a follow-up.
255+
256+
- **VertexAnimationManager** (`src/VertexAnimationManager.{h,cpp}`, Slice B #519): full-mesh per-vertex animation (cloth / sims / fluid bakes / Alembic caches — every vertex moves, no skeleton). Reuses Ogre's `VAT_POSE` path so the existing timeline/dope-sheet/loop play it with no new playback code. `FrameSet`/`FrameData` are the source-agnostic decoded-cache types; `buildClipFromFrames(mesh, name, frames)` reads submesh-0 bind positions and builds one `Ogre::Pose` per frame (delta vs bind) + one `VAT_POSE` track keyed per frame time. `sampleHeuristic(frameCount)` (< 32 → poses, else stream — the issue's rule) is static + unit-tested. Sentry `scene.anim.vertex_anim`.
257+
- **AlembicImporter** (`src/AlembicImporter.{h,cpp}`, Slice B2): Alembic (.abc) reader behind `-DENABLE_ALEMBIC` (default OFF; `cmake/Alembic.cmake` FetchContents Imath 3.1.12 + Alembic 1.8.8, both BSD-3, fully static, all optional components off — Imath install stays ON so Alembic's unconditional `install(EXPORT)` finds it in an export set). `readFrameSet` decodes the first `IPolyMesh` into a `FrameSet` (pure data — rejects variable-topology caches, fan-triangulates n-gon faces); `importToScene` builds the base mesh + `VAT_POSE` clip + entity. `.abc` routes through `MeshImporterExporter::importer` (guarded — a non-Alembic build logs a clear "rebuild with -DENABLE_ALEMBIC" and skips). The reader is `#ifdef ENABLE_ALEMBIC`-guarded so the default build is unaffected; a round-trip test writes+reads a synthetic `.abc` (only compiled/run in the Alembic-on coverage CI lane). **B3 (shipped):** `readInfo(path)` reads cache metadata (frames/verts/tris/fps/duration/storage) from the schema header + first sample without decoding all frames → **`qtmesh anim <file>.abc --info [--json]`** (`CLIPipeline::cmdAnim`); MCP **`import_alembic`** (heavy — decodes into the live scene, reports node/entities/vertexClips) + **`play_vertex_animation`** (delegates to `toolPlayAnimation` since a vertex clip is an ordinary `AnimationState`). Frame cap: `readFrameSet(maxFrames)` and `importToScene` cap the decode at 512 frames and set `ReadResult::truncated` / log a warning when it bites (no silent cap) — VAT_POSE holds every frame resident, so true per-frame vertex-buffer streaming remains future work.
258+
248259
### Undo/Redo System
249260

250261
- **UndoManager** (`src/UndoManager.h/cpp`): Singleton wrapping `QUndoStack`. Push commands, undo/redo via `Ctrl+Z`/`Ctrl+Shift+Z`.

CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,24 @@ if(ENABLE_ONNX)
306306
message(STATUS "AI PBR map synthesis enabled with ONNX Runtime")
307307
endif()
308308
##############################################################
309+
# Alembic — mesh / vertex animation import (#519, Anim Slice B)
310+
##############################################################
311+
# Vendored (BSD) reader for baked per-vertex caches (cloth / sims / fluids /
312+
# Houdini + Blender exports). Default OFF: the Alembic + Imath build is
313+
# non-trivial across the three target platforms. VertexAnimationManager + Ogre
314+
# VAT_POSE playback work WITHOUT Alembic; this flag adds the .abc reader
315+
# (cmake/Alembic.cmake, FetchContent). The C++ #ifdef ENABLE_ALEMBIC guards the
316+
# reader so a build without it still compiles and skips .abc with a clear message.
317+
option(ENABLE_ALEMBIC "Enable Alembic (.abc) vertex-animation import" OFF)
318+
319+
if(ENABLE_ALEMBIC)
320+
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Alembic.cmake)
321+
add_definitions(-DENABLE_ALEMBIC)
322+
message(STATUS "Alembic vertex-animation import enabled")
323+
else()
324+
message(STATUS "Alembic vertex-animation import disabled (VAT_POSE playback still available)")
325+
endif()
326+
##############################################################
309327
# PS1 runtime geometry extraction (experimental)
310328
##############################################################
311329
option(ENABLE_PS1_RIP "Enable experimental PS1 runtime geometry extraction" OFF)

cmake/Alembic.cmake

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Alembic (.abc) vertex-animation import — Anim epic Slice B (#519), sub-slice B2.
2+
#
3+
# Vendors Imath 3 + Alembic (both BSD-3-Clause) via FetchContent, statically,
4+
# with every optional component OFF (no HDF5, Python, tests, binaries, install).
5+
# Exposes an imported target `qtmesh_alembic` that the app links; the C++ side
6+
# is #ifdef ENABLE_ALEMBIC-guarded so a build without this still compiles.
7+
#
8+
# Why build from source rather than find_package: Alembic + Imath system
9+
# packages are absent or version-skewed across our three targets (macOS via
10+
# brew, Ubuntu CI, Windows MinGW). FetchContent gives one reproducible version
11+
# everywhere, matching how the project already vendors ONNX Runtime / libsodium
12+
# / tinyexr.
13+
#
14+
# Dependency chain: Alembic FIND_PACKAGE(Imath) → we build Imath as a
15+
# subproject first, point Imath_DIR at its generated build-tree config so
16+
# Alembic's find_package(Imath CONFIG) resolves to the target we just built.
17+
18+
if(TARGET qtmesh_alembic)
19+
return()
20+
endif()
21+
22+
include(FetchContent)
23+
24+
set(QTMESH_IMATH_TAG "v3.1.12" CACHE STRING "Imath git tag")
25+
set(QTMESH_ALEMBIC_TAG "1.8.8" CACHE STRING "Alembic git tag")
26+
27+
# ---- Imath ---------------------------------------------------------------
28+
# Static, no tests/python. IMATH_INSTALL stays ON: Alembic's own
29+
# INSTALL(EXPORT AlembicTargets) is unconditional and references Imath, so
30+
# Imath MUST be in an export set too or CMake's generate step fails
31+
# ("target Alembic requires target Imath that is not in any export set").
32+
# We never actually run `make install` from the app build, so an ON install
33+
# rule is harmless — it just keeps the export sets consistent.
34+
set(IMATH_INSTALL ON CACHE BOOL "" FORCE)
35+
set(IMATH_INSTALL_PKG_CONFIG OFF CACHE BOOL "" FORCE)
36+
set(PYTHON OFF CACHE BOOL "" FORCE)
37+
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
38+
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
39+
40+
FetchContent_Declare(
41+
qtmesh_imath
42+
GIT_REPOSITORY https://github.com/AcademySoftwareFoundation/Imath.git
43+
GIT_TAG ${QTMESH_IMATH_TAG}
44+
GIT_SHALLOW TRUE
45+
)
46+
FetchContent_MakeAvailable(qtmesh_imath)
47+
48+
# Alembic's find_package(Imath) resolves against a CONFIG. Imath-as-subproject
49+
# writes its package config under the build dir; point find_package there.
50+
if(NOT DEFINED Imath_DIR)
51+
set(Imath_DIR "${qtmesh_imath_BINARY_DIR}/config" CACHE PATH "" FORCE)
52+
endif()
53+
54+
# ---- Alembic -------------------------------------------------------------
55+
# Everything optional OFF: no HDF5 backend (we only read the modern Ogawa
56+
# backend), no tests/binaries/python/prman/maya/arnold, static lib.
57+
set(USE_HDF5 OFF CACHE BOOL "" FORCE)
58+
set(USE_TESTS OFF CACHE BOOL "" FORCE)
59+
set(USE_BINARIES OFF CACHE BOOL "" FORCE)
60+
set(USE_EXAMPLES OFF CACHE BOOL "" FORCE)
61+
set(USE_PYALEMBIC OFF CACHE BOOL "" FORCE)
62+
set(USE_ARNOLD OFF CACHE BOOL "" FORCE)
63+
set(USE_PRMAN OFF CACHE BOOL "" FORCE)
64+
set(USE_MAYA OFF CACHE BOOL "" FORCE)
65+
set(ALEMBIC_SHARED_LIBS OFF CACHE BOOL "" FORCE)
66+
set(ALEMBIC_ILMBASE_LINK_STATIC ON CACHE BOOL "" FORCE)
67+
set(ALEMBIC_LIB_INSTALL_DIR "lib" CACHE STRING "" FORCE)
68+
69+
FetchContent_Declare(
70+
qtmesh_alembic
71+
GIT_REPOSITORY https://github.com/alembic/alembic.git
72+
GIT_TAG ${QTMESH_ALEMBIC_TAG}
73+
GIT_SHALLOW TRUE
74+
)
75+
FetchContent_MakeAvailable(qtmesh_alembic)
76+
77+
# Alembic's core library target is `Alembic` (with an `Alembic::Alembic` alias
78+
# in recent versions). Wrap whichever exists behind our stable name so the app
79+
# links `qtmesh_alembic` regardless.
80+
if(TARGET Alembic::Alembic)
81+
add_library(qtmesh_alembic INTERFACE)
82+
target_link_libraries(qtmesh_alembic INTERFACE Alembic::Alembic Imath::Imath)
83+
elseif(TARGET Alembic)
84+
add_library(qtmesh_alembic INTERFACE)
85+
target_link_libraries(qtmesh_alembic INTERFACE Alembic Imath::Imath)
86+
# The plain `Alembic` target's public include dirs cover its own headers;
87+
# Imath::Imath carries the Imath headers Alembic's public API exposes.
88+
else()
89+
message(FATAL_ERROR "Alembic.cmake: neither Alembic nor Alembic::Alembic target was created")
90+
endif()
91+
92+
message(STATUS "Alembic ${QTMESH_ALEMBIC_TAG} + Imath ${QTMESH_IMATH_TAG} vendored (static)")

0 commit comments

Comments
 (0)