renpy-rigging 0.3.2
A 2D skeletal rigging, posing, and animation system
Requires Python: >=3.8
Keywords: 2d, animation, game-dev, renpy, rigging, skeletal, visual-novel

# renpy-rigging

A 2D skeletal rigging, posing, and animation system for Ren'Py — including a full visual rig editor built in Ren'Py itself.

## What is renpy-rigging?

- **A lightweight 2D cutout/skeletal animation runtime for Ren'Py**
- **A visual rig & animation editor** implemented as a Ren'Py project
- **A pip-installable Python package** that Ren'Py games can use to load and render rigs, poses, and animations at runtime
- **A JSON-based, human-readable asset format** for rigs, poses, animations, overlays, attachments, and scenes

### The Guiding Principle

> **What you see in the editor is exactly what ships in your game.**

No export pipelines. No mismatched math. No editor/runtime drift.

## Features

### Runtime Library (pip package)

- Load character rigs from JSON
- Load pose libraries and animation libraries
- Render characters using:
  - Hierarchical joints (forward kinematics)
  - Per-part pivots
  - Per-part rotation, offset, scale
  - Z-order layering
- Play animations:
  - Pose sequences with timing
  - Looping, one-shot, and ping-pong play modes
  - Easing functions (linear, ease_in, ease_out, ease_in_out)
  - Pose interpolation and blending
  - Multi-track composite animations with weighted additive blending
  - Variable playback speed
  - Per-frame horizontal/vertical flipping
- **Overlays**: Layer clothing, accessories, and alternate art on top of (or replacing) existing parts — with slot-based auto-eviction for mutually exclusive items
- **Attachments**: Attach fully-rigged independent objects (weapons, props) to character joints — with their own skeletons, poses, animations, and slot-based swapping
- **Scenes**: Compose backgrounds, layered props, placed characters, and attachments into full environments with z-ordered layers and ambient sound
- **Sound**: Three layers of audio — per-pose sounds, animation timeline SFX (up to 4 concurrent channels), and scene ambient audio
- **Outline/border rendering**: Per-rig configurable borders and runtime-adjustable outline overlays with color, width, and alpha controls
- Fully Ren'Py-native:
  - Uses Transform and displayables
  - No engine hacks or C extensions

### Editor (RigLab)

- Load a character rig and parts
- Visualize:
  - Joints
  - Bone hierarchy
  - Part pivots
  - Draw order
- Select and edit:
  - Joints (drag with mouse, nudge with arrow keys)
  - Parts (rotate, offset, adjust pivots)
- Save:
  - Poses to poses.json
  - Animations to animations.json
  - Overlays to overlays.json
  - Attachments to attachments.json
- Timeline editor:
  - Build animations from poses
  - Multi-track support with per-track weights and muting
  - Set timing and easing per step
  - Timeline sound placement
  - Per-frame overlay and attachment overrides
  - Preview loop, one-shot, or ping-pong playback
- Export:
  - PNG frame export (transparent, solid-color, or scene background)
  - Spritesheet export (all animations tiled)
  - GIF export (pure-Python, no PIL required)
  - Cloud render API for GIF/MP4
- **Same renderer as the runtime library**

## Installation

### Runtime Library

In your Ren'Py game's Python environment:

```bash
pip install renpy-rigging
```

Then in Ren'Py:

```python
init python:
    import renpy_rigging
```

### Editor (RigLab)

1. Open the `riglab/` folder in the Ren'Py Launcher
2. Launch the project
3. Select a sample character or add your own

## Quick Start

### Directory Layout

```
game/
  characters/
    vince/
      rig.json           # Joints, parts, canvas, border config
      poses.json         # Named poses (deltas from neutral)
      animations.json    # Named animations (frame sequences)
      overlays.json      # Named overlay sets (clothing, accessories)
      attachments.json   # Attachment bindings (weapons, props)
      parts/             # PNG images for each body part
      overlays/          # Overlay images per overlay set
      sounds/            # Sound files
  attachments/
    tommy-gun/
      rig.json           # Attachment rig (joints, parts, attachment config)
      poses.json
      animations.json
      parts/
  scenes/
    speakeasy/
      scene.json         # Background, layers, placed items/characters
      parts/             # Scene prop images
```

### Loading a Rig

```python
init python:
    from renpy_rigging import Rig, PoseLibrary, AnimationLibrary, RigRenderer

    rig = Rig.load("characters/vince/rig.json")
    poses = PoseLibrary.load("characters/vince/poses.json")
    anims = AnimationLibrary.load("characters/vince/animations.json")

    renderer = RigRenderer(rig, poses, anims)
```

Or load everything from a directory (including overlays and attachment bindings):

```python
init python:
    from renpy_rigging import RigRenderer

    vince = RigRenderer.from_directory("characters/vince")
```

### Showing a Character

```renpy
# Show a static pose
show expression renderer.show_pose("neutral") as vince:
    xalign 0.5
    yalign 0.7

# Play an animation
show expression renderer.play_animation("idle", loop=True) as vince:
    xalign 0.5
    yalign 0.7

# Play an animation mirrored
show expression renderer.play_animation("walk", loop=True, flip_h=True) as vince
```

### Using Overlays

```python
# Add an overlay (slot-aware — evicts any overlay in the same slot)
renderer.add_overlay("broadway-brawlers-shirt")

# Remove an overlay
renderer.remove_overlay("broadway-brawlers-shirt")

# Query by slot
renderer.get_overlay_by_slot("shirt")
```

### Using Attachments

```python
# Attach a weapon (slot-aware — evicts any attachment in the same slot)
renderer.add_attachment("tommy-gun")

# Set the attachment's pose
renderer.set_attachment_pose("tommy-gun", "firing")

# Play the attachment's own animation
renderer.play_attachment_animation("tommy-gun", "fire", loop=True)

# Remove
renderer.remove_attachment("tommy-gun")
```

### Setting Up Scenes

```python
init python:
    from renpy_rigging import SceneManager, register_channels

    register_channels()
    scene_mgr = SceneManager(os.path.join(renpy.config.gamedir, "scenes"))
    scene_mgr.register_images()  # enables "scene street" syntax
```

Then in your script:

```renpy
# Using the scene manager
$ scene_mgr.show("speakeasy")

# Or with the custom rigscene statement (if registered)
rigscene speakeasy with dissolve
```

Scenes automatically composite backgrounds, layered props, placed characters, and attachments at the correct z-orders, and start ambient sound if configured.

## Game Project Integration

You can integrate renpy-rigging into your Ren'Py game without pip installing the package. Add this to your `options.rpy`:

```python
init -100 python:
    import sys, os
    src_path = os.path.normpath(os.path.join(renpy.config.gamedir, "..", "..", "src"))
    sys.path.insert(0, src_path)
```

Then import and re-export into the Ren'Py store in a `rig_runtime.rpy` file:

```python
init -10 python:
    from renpy_rigging import (
        Rig, Pose, PoseLibrary, AnimationLibrary,
        RigRenderer, SceneManager, register_channels,
    )
    register_channels()
```

This is the pattern used by both the RigLab editor and the RigLabDemo game.

## Data Formats

### rig.json

Defines the skeleton (joints) and visual parts:

```json
{
  "canvas": [600, 900],

  "joints": {
    "root":   {"parent": null, "pos": [300, 700]},
    "pelvis": {"parent": "root", "pos": [0, 0]},
    "chest":  {"parent": "pelvis", "pos": [0, -200]}
  },

  "parts": {
    "torso": {
      "image": "parts/torso.png",
      "parent_joint": "pelvis",
      "pos": [0, 0],
      "pivot": [130, 135],
      "z": 10
    }
  },

  "border": {"enabled": true, "width": 3, "color": "#000000"},
  "slots": ["weapon"]
}
```

### poses.json

Poses store only deltas from the neutral rig:

```json
{
  "neutral": {
    "joints": {},
    "parts": {}
  },
  "wave": {
    "joints": {
      "shoulder_R": {"rot": -120},
      "elbow_R": {"rot": 90}
    },
    "parts": {
      "hand_R": {"visible": false}
    },
    "overlays": {"hand-pistol": true},
    "attachments": {
      "tommy-gun": {"pose": "firing", "visible": true}
    },
    "sound": "sounds/whoosh.wav"
  }
}
```

### animations.json

Animations are sequences of poses with timing. Three formats are supported (all backward-compatible):

**Simple (flat list):**
```json
{
  "idle": [
    {"pose": "neutral", "ms": 500},
    {"pose": "breathe_in", "ms": 800, "ease": "ease_in_out"},
    {"pose": "neutral", "ms": 500},
    {"pose": "breathe_out", "ms": 800, "ease": "ease_in_out"}
  ]
}
```

**With timeline sounds:**
```json
{
  "fire": {
    "frames": [
      {"pose": "firing", "ms": 80},
      {"pose": "neutral", "ms": 80}
    ],
    "sounds": [
      {"path": "sounds/gunshot.wav", "start_ms": 0},
      {"path": "sounds/shell_casing.wav", "start_ms": 100}
    ]
  }
}
```

**Multi-track composite:**
```json
{
  "walk_and_breathe": {
    "tracks": [
      {"name": "Walk", "frames": [...], "weight": 1.0},
      {"name": "Breathe", "frames": [...], "weight": 0.5, "muted": false}
    ],
    "sounds": [...]
  }
}
```

### overlays.json

Overlays associate extra art with body parts:

```json
{
  "broadway-brawlers-shirt": {
    "_slot": "shirt",
    "torso": {"path": "torso.png", "replace": true},
    "arm_upper_L": true,
    "arm_upper_R": {"z_delta": 2}
  }
}
```

A part set to `true` uses all defaults (same part name as the image filename, layered on top). The `_slot` field enables auto-eviction when activating a new overlay in the same slot.

### attachments.json (on the character)

Defines which attachments a character can use and how they connect:

```json
{
  "tommy-gun": {
    "path": "attachments/tommy-gun",
    "default_joint": "wrist_R",
    "pos": [-5.0, 29.0],
    "rot": 90.0,
    "slot": "weapon"
  }
}
```

### Attachment rig.json

An attachment's own rig.json includes an `attachment` config block:

```json
{
  "name": "tommy-gun",
  "canvas": [1051, 345],
  "joints": {"root": {"parent": null, "pos": [525, 172]}},
  "parts": {...},
  "attachment": {
    "is_attachment": true,
    "default_attach_joint": "wrist_R",
    "inherit_rotation": true,
    "inherit_scale": false,
    "z_offset": 10,
    "slot": "weapon"
  }
}
```

### scene.json

Defines a composed environment with backgrounds, layers, and placed entities:

```json
{
  "canvas": [1456, 816],
  "background": {
    "color": "#000000",
    "image": "parts/speakeasy.png",
    "image_offset": [0, 0],
    "image_scale": 1.0
  },
  "layers": {
    "background": {"z": -100},
    "foreground": {"z": 100}
  },
  "items": [
    {
      "name": "barrel",
      "image": "parts/barrel.png",
      "layer": "background",
      "pos": [300, 400],
      "scale": [1.5, 1.5],
      "z_offset": 5
    }
  ],
  "characters": [
    {
      "name": "Vince",
      "source": "Vince-New",
      "layer": "background",
      "pos": [728, 408],
      "scale": [0.4, 0.4]
    }
  ],
  "attachments": [
    {
      "name": "Tommy-Gun",
      "source": "Tommy-Gun",
      "layer": "background",
      "pos": [188, 578],
      "scale": [0.6, 0.6]
    }
  ],
  "sound": "sounds/jazz.ogg",
  "sound_loop": true
}
```

## Repository Structure

```
renpy-rigging/
  README.md
  pyproject.toml
  LICENSE

  src/
    renpy_rigging/
      __init__.py
      rig.py            # Rig data structures (joints, parts, canvas)
      pose.py           # Pose application, blending, interpolation
      animation.py      # Animation playback (multi-track, sounds, easing)
      overlay.py        # Overlay system (clothing, accessories)
      attachment.py     # Attachment system (weapons, props)
      scene.py          # Scene composition (backgrounds, layers, entities)
      render.py         # Ren'Py displayable builders
      audio.py          # Audio channel registration and management
      math2d.py         # Vec2, Transform2D, easing functions
      io.py             # JSON load/save helpers

  riglab/               # The Ren'Py editor app
    game/
      script.rpy
      options.rpy
      gui.rpy
      screens.rpy
      riglab/
        core/
          rig_runtime.rpy
          rig_io.rpy
          rig_math.rpy
        editor/
          editor_screen.rpy
          editor_input.rpy
          timeline_screen.rpy
        data/
          samples/
        main_menu.rpy

  riglabdemo/           # Demo game project
    game/
      characters/       # Character rigs
      attachments/      # Attachment rigs
      scenes/           # Scene definitions

  docs/
    rig_format.md
    authoring_guide.md
    renpy_integration.md
```

## Editor Keyboard Shortcuts

| Key | Action |
|-----|--------|
| J | Toggle joint visibility |
| B | Toggle bone visibility |
| P | Toggle pivot visibility |
| Arrow keys | Nudge selected joint (1px) |
| Shift + Arrow keys | Nudge selected joint (10px) |
| Space | Play/pause animation (in timeline) |

## Design Philosophy

- **FK-first**: Forward kinematics only in v1
- **No sprite sheets**: Everything is runtime-composed
- **Human-editable JSON**: All data formats are readable and editable
- **Editor and runtime share the same renderer**
- **Procedural-friendly**: Built for blending, aiming, reacting, not just canned loops
- **Slot-based resource management**: Overlays and attachments use named slots for clean swapping (e.g., only one "weapon" or "shirt" at a time)

## Development Roadmap

- [x] v0.1 — Viewer: Load rig.json, render neutral pose
- [x] v0.2 — Poses: Load poses.json, apply pose deltas
- [x] v0.3 — Editor: Select + drag joints, rotate parts, save poses
- [x] v0.4 — Animation: Timeline editor, playback system, export
- [x] v0.5 — Overlays, attachments, scene rendering, composite animations with sound

### Future

- [ ] IK helpers
- [ ] Symmetry tools
- [ ] Onion skinning
- [ ] Event markers in animations
- [ ] Physics jiggle bones
- [ ] Auto-z reordering helpers

## Non-Goals

This is **not** a Spine replacement. This is **not** a general-purpose animation suite.

This is a **Ren'Py-first, game-dev-first** rigging system.

## Who This Is For

- Visual novel devs who want **live characters**
- 2D game devs who want:
  - Aiming arms
  - Breathing idles
  - Reactive poses
  - Procedural animation
- Artists who think in **volumes and joints**, not sprite sheets

## Documentation

- [Rig Format Specification](docs/rig_format.md)
- [Character Authoring Guide](docs/authoring_guide.md)
- [Ren'Py Integration Guide](docs/renpy_integration.md)

## License

MIT License - see [LICENSE](LICENSE) for details.