Skip to content

Expose skeletal animation data (mesh.bones, scene.animations) - #19

Merged
ranjian0 merged 4 commits into
masterfrom
feat/skeletal-animation
Sep 8, 2026
Merged

ranjian0 merged 4 commits into
masterfrom
feat/skeletal-animation

Conversation

@ranjian0

@ranjian0 ranjian0 commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Closes #14

API

class Bone:
    name: str
    offset_matrix: tuple[tuple[float, ...], ...]   # 4x4 bind pose, like Node.transformation
    num_weights: int
    weights: memoryview            # float32, N
    weight_vertex_ids: memoryview  # uint32, N (parallel)
    armature_name: str | None      # populated only with Process_PopulateArmatureData
    node_name: str | None

class Animation:
    name: str
    duration: float                # in ticks
    ticks_per_second: float        # 0.0 = unspecified (duration is seconds)
    num_channels: int
    channels: list[NodeAnim]

class NodeAnim:
    node_name: str
    num_position_keys: int;  position_key_times: memoryview   # float64, N
                             position_key_values: memoryview  # float32, N*3
    num_rotation_keys: int;  rotation_key_times / rotation_key_values  # N*4 (x,y,z,w)
    num_scaling_keys: int;   scaling_key_times / scaling_key_values    # N*3
    pre_state: int                 # AnimBehaviour_* (before first key)
    post_state: int                # after last key

Mesh  += num_bones: int, bones: list[Bone]           # empty list when absent
Scene += num_animations: int, animations: list[Animation]

new constants: Process_PopulateArmatureData, AnimBehaviour_DEFAULT/_CONSTANT/_LINEAR/_REPEAT

Also included

  • Memoryview lifetime fix: exported memoryviews (vertices, weights, keys, ...) are now backed by an internal buffer-keeper object, so data lives as long as any view references it. Previously buffers were freed with their owner while views could still exist (use-after-free; present since 1.1.0). Pinned by test_memoryviews_outlive_scene.
  • scripts/memprof.py rewritten as an RSS leak harness (cyborg / planet / fox plain / fox skeletal / retained-memoryviews cases, allocator warmup + confirmation pass). All cases clean.
  • examples/demo.py (pip install assimp-py[demo], make demo): pygame + moderngl demo — textured cyborg + GPU-skinned animated fox, animation evaluated in numpy from the new API (lerp/slerp keyframes, hierarchy walk, global(node) @ offset_matrix palette), orbit camera, HUD, skinned wireframe + skeleton overlays, --screenshot headless mode.
  • Tests: tests/test_skeletal.py + fox fixture — bones (counts, names, offset matrices, parallel weight views, per-vertex weight sums), animations (3 Fox cycles with pinned durations/keys), armature/node names with and without the flag, empty-list behavior for static meshes. Fixture: Fox.glb from KhronosGroup/glTF-Sample-Assets (license included).
  • README: skeletal animation usage + demo sections; .pyi stubs updated.

Out of scope (follow-ups)

Morph-target/mesh-swap animation channels (needs aiMesh::mAnimMeshes exposure), key-level interpolation data.

Closes #14

- New Bone type: name, offset_matrix (4x4 tuple), parallel memoryviews
  weights (float32) / weight_vertex_ids (uint32), and armature_name /
  node_name (populated only with Process_PopulateArmatureData)
- New Animation type: name, duration, ticks_per_second, channels
- New NodeAnim type: node_name, position/rotation/scaling key tracks as
  parallel memoryviews (float64 times + float32 values), pre/post states
- Mesh gains bones/num_bones; Scene gains animations/num_animations
  (empty lists when absent)
- New constants: Process_PopulateArmatureData, AnimBehaviour_*
- Stubs and README skeletal animation section
- Tests: tests/test_skeletal.py + fox fixture in test_models.py using
  Fox.glb from KhronosGroup/glTF-Sample-Assets (license included)
memoryviews were created with a NULL base object, so their backing
buffers were freed with the owning Mesh/Bone/NodeAnim even while views
still referenced them - reading mesh.vertices after dropping the Scene
was undefined behavior (present since 1.1.0; caught by the new
test_memoryviews_outlive_scene).

Buffers now transfer ownership to an internal _BufferKeeper type that
exports the buffer protocol; views hold a strong reference to the
keeper, and the data is freed when the last view is released.

Also rewrite scripts/memprof.py as an RSS leak harness covering the
skeletal paths (fox plain/skeletal/retained-views cases, global warmup,
confirmation pass against allocator step noise); --lines keeps the old
memory_profiler line mode.
- examples/demo.py: pygame + moderngl demo rendering the textured cyborg
  next to the GPU-skinned animated fox. Animation is evaluated from the
  new bones/animations API in numpy (keyframe lerp/slerp, hierarchy
  walk, palette = global(node) @ offset_matrix) and skinned in the
  vertex shader. Orbit camera, HUD, wireframe (skinned) and skeleton
  overlays; --screenshot for headless verification.
  Wire via 'pip install assimp-py[demo]' / 'make demo'.
- scripts/memprof.py: RSS-based leak harness over cyborg/planet/fox
  (plain, skeletal, retained-memoryviews) cases with allocator warmup
  and a confirmation pass; --lines keeps memory_profiler line mode.
- tests: memoryviews must stay valid after the Scene is released.
- To guarantee that, exported memoryviews are now backed by an internal
  buffer-keeper object so the data lives as long as any view (previously
  buffers died with their owner while views could still exist).
Channel order comes from an unordered_map in assimp and differs across
platforms (MSVC vs libstdc++); pinning channels[0] broke on Windows.
@ranjian0
ranjian0 merged commit 7bd0972 into master Sep 8, 2026
9 checks passed
@ranjian0
ranjian0 deleted the feat/skeletal-animation branch September 8, 2026 14:38
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.

Feature Request: Expose skeletal animation data (mesh.bones and scene.animations) to Python objects

1 participant