@kieransimkin/dance-rudiments 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ # Rigify character motion on the DanceRudiments clock
2
+
3
+ This extension binds the existing native **N-dimensional Recording** to a
4
+ versioned Blender Rigify control profile. Each model has a semantic type URL;
5
+ each physical parameter has a fixed, one-based channel number. A compiled motion
6
+ retains the profile, source timing, provenance and normalized samples together.
7
+
8
+ The core authoring code is standard-library Python. Live playback still uses
9
+ C++ `SampledRecording` at **64 integer pips per quarter-note beat**. Blender is
10
+ the optional environment for importing, evaluating and driving a rigged mesh.
11
+ Existing XYZ patterns, the 1,731-pattern catalogue and their looping semantics
12
+ are unchanged. Rig motions are **non-looping** and reject out-of-range pips.
13
+
14
+ ## Models and types
15
+
16
+ The checked-in inventory was generated using the actual **Blender 5.2.2 LTS**
17
+ Rigify implementation. It covers eight metarigs and 28 component types. The
18
+ [Rigify catalogue guide](rigify-catalogue.md) records the exact build, source
19
+ hashes, channel counts, component contexts and regeneration command.
20
+
21
+ | Rigify preset | Motion type |
22
+ | --- | --- |
23
+ | Human | `syn://type/danceflow/motion/rigify/metarig/human` |
24
+ | Basic human | `syn://type/danceflow/motion/rigify/metarig/basic/basic_human` |
25
+ | Basic quadruped | `syn://type/danceflow/motion/rigify/metarig/basic/basic_quadruped` |
26
+ | Bird | `syn://type/danceflow/motion/rigify/metarig/animals/bird` |
27
+ | Cat | `syn://type/danceflow/motion/rigify/metarig/animals/cat` |
28
+ | Horse | `syn://type/danceflow/motion/rigify/metarig/animals/horse` |
29
+ | Shark | `syn://type/danceflow/motion/rigify/metarig/animals/shark` |
30
+ | Wolf | `syn://type/danceflow/motion/rigify/metarig/animals/wolf` |
31
+
32
+ Component types have a parallel hierarchy, for example
33
+ `syn://type/danceflow/motion/rigify/component/limbs/arm` and
34
+ `syn://type/danceflow/motion/rigify/component/spines/super_spine`.
35
+ `synny://` URLs resolve to the **same identity** as their `syn://` equivalents.
36
+ The tree is an interchange convention; it does not add a Potbot dependency or
37
+ modify another repository's type registry.
38
+
39
+ Rigify's presets are configurable. A rig with edited proportions, extra bones,
40
+ different generation settings or another Blender build needs its **own exported
41
+ profile**. The model type can remain the same while its profile fingerprint
42
+ changes. Neither bone count nor matching names establishes rig compatibility.
43
+
44
+ ## Numbered channel contract
45
+
46
+ A `dancerudiments.rig-profile` document declares:
47
+
48
+ | Field | Meaning |
49
+ | --- | --- |
50
+ | `type_uri`, `model`, `version` | Semantic model identity and explicit profile version |
51
+ | `channels[].number` | Persistent one-based parameter number; column index is `number - 1` |
52
+ | `name` | Unique Recording dimension name |
53
+ | `bone`, `data_path`, `array_index` | Exact Blender control and component to drive |
54
+ | `unit` | Blender units, radians, unitless values or other explicitly declared units |
55
+ | `minimum`, `maximum`, `neutral` | Physical range and neutral value |
56
+ | `value_type`, `interpolation` | Float, integer or Boolean; linear, quaternion or step behavior |
57
+ | `enum_items` | Valid numeric choices and labels for discrete enum controls, when present |
58
+ | `rotation_mode` | The actual generated control's Euler order, quaternion or axis-angle mode |
59
+ | `source` | Blender/Rigify identity, rest matrices, parents, locks and scene-unit metadata |
60
+
61
+ The initial export enumerates actual controls deterministically. When extending
62
+ an exported profile, `--previous` preserves the old channel numbers and appends
63
+ new parameters. Removed or changed parameter definitions fail rather than
64
+ silently reusing an old number. The explicit version and SHA-256 fingerprint
65
+ accompany every capture and motion. A fingerprint detects mismatched data; it is
66
+ not a digital signature or a guarantee that a source is trustworthy.
67
+
68
+ The generated manifests cover writable **animator control transforms and numeric
69
+ custom properties**, including IK/FK blends and parent selectors where present.
70
+ Rigify's internal `ORG-`, `DEF-`, `MCH-` and `VIS-` bones are excluded from direct
71
+ control. The existing rig evaluates its constraints and drives the bound mesh.
72
+ Static metarig-generation parameters are inventoried separately: segment counts,
73
+ widget choices and similar settings are not time-varying motion channels.
74
+
75
+ Local transforms remain in Blender's coordinate convention. Exported presets use
76
+ explicit bounds: location ±10 Blender units, Euler angles ±4π radians, scale
77
+ 0..4, quaternion components −1..1, and custom properties' declared ranges.
78
+ These are encoding limits, not anatomical joint limits. Set suitable bounds when
79
+ exporting a real character, especially one with large root translation. Values
80
+ outside the chosen range fail; they are never silently clamped or independently
81
+ renormalized per clip.
82
+
83
+ Physical values encode into the existing bipolar Recording contract:
84
+
85
+ ```text
86
+ normalized = 2 * (physical - minimum) / (maximum - minimum) - 1
87
+ physical = minimum + (normalized + 1) * (maximum - minimum) / 2
88
+ ```
89
+
90
+ Every stored scalar remains finite float64 within `[-1,1]`, rounded to twelve
91
+ decimals. Quaternions are complete `w,x,y,z` groups and must have unit magnitude.
92
+ Boolean/integer selectors use held values, never fractional intermediate choices.
93
+ Quaternion interpolation uses shortest-path SLERP during compilation. Existing
94
+ Euler controls keep their actual rotation order and sampled winding; unwrap
95
+ source discontinuities before capture. The profile does not force Euler-based
96
+ Rigify controls into quaternion mode.
97
+
98
+ ## Inspect or export the mappings
99
+
100
+ From a source checkout, the CLI works without installing a native extension for
101
+ inspection and authoring:
102
+
103
+ ```sh
104
+ python tools/rig_motion.py catalogue
105
+ python tools/rig_motion.py profile human --output generated/human.profile.json
106
+ python tools/rig_motion.py channels generated/human.profile.json --output generated/human.channels.csv
107
+ python tools/rig_motion.py profile limbs.arm --kind component --output generated/arm.profile.json
108
+ python tools/rig_motion.py sources
109
+ ```
110
+
111
+ After installing this source revision, `dancerudiments-rig` is the same command.
112
+ The catalogue JSON and all profile JSON files are shipped inside the Python
113
+ package. The CSV includes every numbered parameter; no XYZ truncation occurs.
114
+ Existing outputs are preserved unless `--overwrite` is explicit. Conversion
115
+ commands refuse output paths that are also their inputs.
116
+
117
+ ## Import and capture real animation
118
+
119
+ The Blender bridge is available as the packaged
120
+ `dancerudiments_authoring.blender_bridge` module and through
121
+ `tools/blender_rigify_bridge.py`. The latter wrapper is useful with Blender's
122
+ `--python` command. Playback requires the native DanceRudiments extension for
123
+ the **Python ABI used by that Blender**. Authoring-only import/profile/capture
124
+ operations do not require the native extension.
125
+
126
+ First import a local file using Blender's supported interchange importers:
127
+
128
+ ```sh
129
+ blender --background --python tools/blender_rigify_bridge.py -- import-source dance.bvh --save imported-source.blend
130
+ ```
131
+
132
+ FBX and glTF/GLB are also accepted. This imports the recording's skeleton and
133
+ animation. Use an explicit retargeting workflow in Blender to map it onto the
134
+ chosen generated Rigify rig, correct the rest-pose/bone bases and root scale,
135
+ choose the appropriate FK/IK controls, and **bake the control animation**. Save
136
+ that result as, for example, `dancer-retargeted.blend`.
137
+
138
+ Export the exact character profile and capture its controls:
139
+
140
+ ```sh
141
+ blender --background dancer-retargeted.blend --python tools/blender_rigify_bridge.py -- export-profile --armature rig --model my_dancer --output generated/dancer.profile.json
142
+ blender --background dancer-retargeted.blend --python tools/blender_rigify_bridge.py -- capture-action --armature rig --profile generated/dancer.profile.json --frame-start 1 --frame-end 121 --output generated/dance.capture.json
143
+ ```
144
+
145
+ `--model` is your rig instance's stable descriptive identifier. `--kind metarig`
146
+ with the original model identifier is appropriate when preserving a preset's
147
+ model type. `--location-range`, `--rotation-range` and `--scale-max` choose the
148
+ export contract; modifying these later requires a new capture/profile identity.
149
+
150
+ Capture times are in seconds relative to the first selected Blender frame. The
151
+ source musical beat-zero offset must use **that same origin**. FPS and captured
152
+ frame range are retained in provenance. The bridge restores the previous scene
153
+ frame after capture. BVH import preserves its source FPS. Fractional frame bounds
154
+ allow precise imported endpoints; see `capture-action --help` for frame-step and
155
+ sample-rate controls.
156
+
157
+ The bridge reads baked control values. It does not infer a foreign skeleton's
158
+ mapping, embed arbitrary Python in motion JSON, or replace Rigify's constraints.
159
+ Object placement, mesh shape keys and third-party feature-set controls require
160
+ an explicit appropriate rig/profile integration; they are not silently inferred
161
+ from humanoid motion.
162
+
163
+ ## Beat matching and compilation
164
+
165
+ A `dancerudiments.rig-capture` stores physical frame values and timestamps without
166
+ binding them to music yet. A source `BeatMap` supplies strictly increasing
167
+ `(seconds, beat)` anchors, or a constant BPM plus a separately supplied beat-zero
168
+ time. There is no automatic tempo estimate or first-beat guess.
169
+
170
+ ```sh
171
+ python tools/rig_motion.py compile generated/dance.capture.json generated/dance.motion.json --name dance_phrase --source-bpm 100 --source-beat-zero 0.18 --bpm 124 --samples 512
172
+ ```
173
+
174
+ That example creates an eight-beat grid (512 pip rows). It requires sufficient
175
+ source motion after its 0.18-second beat origin. At every absolute output pip `p`:
176
+
177
+ ```text
178
+ source_beat = source_beat_offset + motion_rate * p / 64
179
+ source_seconds = source_beat_map.inverse(source_beat)
180
+ ```
181
+
182
+ `--source-beat-offset` selects a source excerpt. `--motion-rate 1/2` uses half a
183
+ source beat per playback beat; `2` uses two. `--start-pip` is an absolute output
184
+ beat position, including negative pickups; it is not an independent clip-local
185
+ offset. For a desired source beat `s` at start pip `p0`, use
186
+ `source_beat_offset = s - motion_rate * p0 / 64`.
187
+
188
+ For drifting tempo or manually marked motion accents, use
189
+ `--source-beat-map beats.json` instead of `--source-bpm`/`--source-beat-zero`.
190
+ Anchors interpolate linearly in accumulated beat position; the inverse maps back
191
+ to source time. A measured map is bounded unless extrapolation is explicitly
192
+ enabled. Source frame bounds are always enforced. There is no automatic looping,
193
+ endpoint extension, foot-plant correction or seam repair.
194
+
195
+ `--bpm` records the **nominal target tempo** for standard Recording compatibility.
196
+ The compiled samples are indexed by musical position and can subsequently play
197
+ under a different fixed or variable target tempo. The original source tempo map
198
+ is retained separately in provenance; it is not replaced by the target BPM.
199
+
200
+ ## Native playback and Blender baking
201
+
202
+ ```python
203
+ from dancerudiments_authoring import BeatMap, load_rig_motion
204
+
205
+ motion = load_rig_motion("generated/dance.motion.json")
206
+ player = motion.player() # actual C++ SampledRecording
207
+ values = player.sample(64) # physical values in channel order
208
+ numbered = player.by_channel(64) # {one_based_channel_number: value}
209
+ target = BeatMap.from_bpm(124, beat_zero_seconds=0, source="song tempo")
210
+ values_at_time = player.sample_time(0.5, target)
211
+ ```
212
+
213
+ The target transport determines each pip from **absolute** audio time. A variable
214
+ tempo map integrates successive beat intervals instead of multiplying all
215
+ elapsed time by the latest BPM. Repeated seeks do not leave an accumulated phase.
216
+ `pip_at()` uses floor for negative positions. There is no Python playback fallback
217
+ when the native extension is missing.
218
+
219
+ To apply a native sample or bake the whole motion into a new Blender Action:
220
+
221
+ ```sh
222
+ blender --background dancer-retargeted.blend --python tools/blender_rigify_bridge.py -- apply --armature rig --motion generated/dance.motion.json --pip 64 --save posed.blend
223
+ blender --background dancer-retargeted.blend --python tools/blender_rigify_bridge.py -- bake --armature rig --motion generated/dance.motion.json --action-name DanceRudimentsDance --save danced.blend
224
+ ```
225
+
226
+ `bake --target-beat-map beats.json` supplies a variable target tempo. The output
227
+ frame start anchors the recording's first pip; subsequent pip times use the
228
+ target map's inverse. Without a target map, the recording's nominal fixed tempo
229
+ sets spacing. This places a clip at `--frame-start`, including for negative or
230
+ nonzero starting pips, rather than preserving the original transport's absolute
231
+ beat-zero time. Baked keyframes use constant interpolation to retain the exact
232
+ integer-pip contract. Any smoother rendering is an explicit consumer choice.
233
+ `apply` creates a one-pose Action so the requested pose survives reopening the
234
+ saved scene. Both output operations use full-influence replacement and disable
235
+ NLA evaluation for that armature; previous Actions and NLA data are retained.
236
+ Output `.blend` files must be new paths. Rig/profile mismatches fail before
237
+ applying values.
238
+
239
+ ## Interchange and existing consumers
240
+
241
+ `dancerudiments.rig-motion`, schema 1, contains `type_uri`, `profile_sha256`,
242
+ `profile` and an unchanged `dancerudiments.recording` schema-1 document. Loaders
243
+ reject unknown fields, duplicate JSON keys, non-finite values, invalid discrete
244
+ controls/quaternions and mismatched columns or profile hashes.
245
+
246
+ ```sh
247
+ python tools/rig_motion.py inspect generated/dance.motion.json
248
+ python tools/rig_motion.py recording generated/dance.motion.json generated/dance.recording.json
249
+ ```
250
+
251
+ The extracted recording works with the existing native C++/Python N-dimensional
252
+ sampler and shared recording harness. Its bar view can inspect all normalized
253
+ channels. It is not a mesh viewer. The typed Python layer performs physical-unit
254
+ decoding and target-profile checking. Existing C#/TypeScript XYZ wrappers remain
255
+ unchanged; this update does not claim new typed mesh renderers in DanceMoves or
256
+ FrameRudiments.
257
+
258
+ The existing recording bounds apply: at most **8,388,608 scalar values** and
259
+ **64 MiB JSON** per document. Long full-body recordings may require explicit
260
+ excerpts. No channel is discarded to make a file fit. Native sampling can run
261
+ without Blender; evaluating a full Rigify constraint rig requires Blender or a
262
+ separately implemented consumer capable of the same rig semantics.
263
+
264
+ ## Examples, tests and next imports
265
+
266
+ The [synthetic quickstart](../examples/rig-motion/README.md) produces a capture,
267
+ source beat map and typed motion, then optionally samples it through C++ under a
268
+ variable tempo. It contains no third-party choreography or music.
269
+
270
+ ```sh
271
+ python -m pip install .
272
+ python -m unittest discover -s tests/python -p 'test_motion*.py' -v
273
+ python -m unittest discover -s tests/python -p 'test_rig*.py' -v
274
+ python examples/rig-motion/quickstart.py --output generated/rig-example --native
275
+ ```
276
+
277
+ The dedicated Rigify workflow installs the pinned official `bpy` runtime and
278
+ checks an actual generated armature, native sampling, skin deformation and
279
+ save/reopen persistence. Its synthetic BVH, FBX and GLB importer fixtures also
280
+ verify source timing, fractional capture endpoints and retained skin bindings.
281
+ See [motion-source research](motion-sources.md) for 100STYLE, CMU, Mixamo,
282
+ Rokoko, AIST++, AMASS, LaFAN1, Motorica and animal-motion candidates, with separate
283
+ format/access/licence notes. Dataset-specific SMPL conversion, general automatic
284
+ retargeting, automatic beat inference, contacts and loop/transition repair remain
285
+ future adapters rather than implied capabilities of this first implementation.
@@ -0,0 +1,246 @@
1
+ # Rigify model types and numbered control channels
2
+
3
+ DanceRudiments can describe and drive a generated Blender Rigify armature through
4
+ a versioned `dancerudiments.rig-profile`. A mesh bound to that armature deforms
5
+ through Blender's normal Armature modifier. The profile describes animator
6
+ controls; it does not replace skin weights, rig constraints or inverse kinematics.
7
+
8
+ The checked-in catalogue was extracted by **running Blender 5.2.2 LTS**, build
9
+ `d13f752e3b9c`, and generating its actual built-in rigs. It contains **8 metarig
10
+ presets and 28 component sample profiles**. Every entry has a hierarchical type
11
+ URL, a concrete numbered channel manifest and a SHA-256 fingerprint. The Blender
12
+ build, normalized Rigify Python-source digest and source URL are retained in each
13
+ profile. No third-party character mesh, motion recording or music is bundled.
14
+
15
+ ## Complete metarig presets
16
+
17
+ | Blender model | Semantic type suffix after `syn://type/danceflow/motion/rigify/metarig/` | Channels |
18
+ | --- | --- | ---: |
19
+ | `human` | `human` | 1,967 |
20
+ | `Basic.basic_human` | `basic/basic_human` | 653 |
21
+ | `Basic.basic_quadruped` | `basic/basic_quadruped` | 799 |
22
+ | `Animals.bird` | `animals/bird` | 1,327 |
23
+ | `Animals.cat` | `animals/cat` | 2,379 |
24
+ | `Animals.horse` | `animals/horse` | 1,286 |
25
+ | `Animals.shark` | `animals/shark` | 579 |
26
+ | `Animals.wolf` | `animals/wolf` | 2,669 |
27
+
28
+ There are **11,659 channels across these eight preset manifests**. Numbers are
29
+ local to a profile: channel 265 in one model need not be the same parameter as
30
+ channel 265 in another. The profile type and fingerprint accompany the data.
31
+ `synny://` and `syn://` resolve to the same type identity; serialization uses
32
+ `syn://`. They are not two separate registries.
33
+
34
+ ## Component samples
35
+
36
+ Rigify constructs complete metarigs from reusable component rig types. The
37
+ component profiles use `syn://type/danceflow/motion/rigify/component/` followed
38
+ by the dotted Blender name expressed as a path, for example `limbs/arm`.
39
+
40
+ | Component | Channels in the generated sample |
41
+ | --- | ---: |
42
+ | `basic.copy_chain` | 40 |
43
+ | `basic.pivot` | 20 |
44
+ | `basic.raw_copy` | 10 |
45
+ | `basic.super_copy` | 20 |
46
+ | `experimental.super_chain` | 70 |
47
+ | `face.basic_tongue` | 41 |
48
+ | `face.skin_eye` | 84 |
49
+ | `face.skin_jaw` | 132 |
50
+ | `faces.super_face` | 892 |
51
+ | `limbs.arm` | 113 |
52
+ | `limbs.front_paw` | 144 |
53
+ | `limbs.leg` | 146 |
54
+ | `limbs.paw` | 144 |
55
+ | `limbs.rear_paw` | 144 |
56
+ | `limbs.simple_tentacle` | 70 |
57
+ | `limbs.spline_tentacle` | 115 |
58
+ | `limbs.super_finger` | 52 |
59
+ | `limbs.super_limb` | 113 |
60
+ | `limbs.super_palm` | 14 |
61
+ | `skin.anchor` | 20 |
62
+ | `skin.basic_chain` | 38 |
63
+ | `skin.glue` | 20, in the explicitly declared containing-rig fixture |
64
+ | `skin.stretchy_chain` | 38 |
65
+ | `skin.transform.basic` | 20 |
66
+ | `spines.basic_spine` | 108 |
67
+ | `spines.basic_tail` | 69 |
68
+ | `spines.super_head` | 44 |
69
+ | `spines.super_spine` | 195 |
70
+
71
+ These are concrete upstream sample configurations, including their generated
72
+ root and containing controls. Changing chain length, component settings or the
73
+ metarig composition can change the controls; export a profile from the resulting
74
+ rig instead of assuming a sample's channel layout still applies.
75
+
76
+ `skin.glue` has **no independent animator control**: it connects to a skin control
77
+ created by another component. Its fixture adds a real `skin.anchor` at the glue
78
+ head, successfully generates both components and records their containing rig's
79
+ root/anchor channels. Both the catalogue and profile explicitly mark this context.
80
+ The channels are not represented as new controls created by glue. Similarly,
81
+ the default `basic.raw_copy` sample exposes its generated root while preserving
82
+ its deformation bone internally.
83
+
84
+ The catalogue also enumerates each component's **generation settings**—chain
85
+ segments, rotation-axis options and similar configuration. Those settings are
86
+ metadata, not animated control channels. They must be applied before generating
87
+ the rig. The runtime profile covers the generated result.
88
+
89
+ ## Numbering and physical meaning
90
+
91
+ Each channel defines its stable **one-based number**, safe Recording dimension
92
+ name, exact bone name, Blender property path, array component, unit, minimum,
93
+ maximum, neutral value, value type and interpolation policy. Quaternion channels
94
+ retain Blender's **w, x, y, z** ordering. Euler controls retain the actual Euler
95
+ order chosen by Rigify; the bridge does not change a rig's rotation mode.
96
+ Integer selectors also retain their allowed numeric enum choices and labels.
97
+
98
+ For example, the shipped `Basic.basic_human` profile assigns:
99
+
100
+ | Channel | Exact control parameter |
101
+ | ---: | --- |
102
+ | 203–205 | `hand_ik.L.location` X, Y, Z |
103
+ | 206–209 | `hand_ik.L.rotation_quaternion` W, X, Y, Z |
104
+ | 210–212 | `hand_ik.L.scale` X, Y, Z |
105
+ | 265–267 | `root.location` X, Y, Z |
106
+ | 268–271 | `root.rotation_quaternion` W, X, Y, Z |
107
+ | 272–274 | `root.scale` X, Y, Z |
108
+ | 617 | `upper_arm_parent.L["IK_FK"]` |
109
+ | 619 | `upper_arm_parent.L["IK_parent"]` |
110
+ | 621 | `upper_arm_parent.L["pole_vector"]` |
111
+
112
+ These are exact assignments from the checked-in profile, not a generic humanoid
113
+ bone convention. The complete list is machine readable and can be exported to
114
+ CSV with the motion CLI.
115
+
116
+ Local location is measured in Blender units; the scene's `scale_length` is part
117
+ of the profile. Angles are radians and scales are dimensionless. Controls use
118
+ local pose/basis space relative to the retained bone rest matrices and hierarchy.
119
+ Armature-object animation is not captured as a bone channel; bake locomotion to
120
+ the rig's root control first when it is part of the motion.
121
+
122
+ The built-in transform ranges are ±10 Blender units for local translation,
123
+ ±4π radians for Euler/axis-angle rotation and 0–4 for scale. Quaternion components
124
+ use −1–1. Numeric custom properties use their declared limits; custom enums use
125
+ the actual allowed choices rather than Blender's generic int32 storage range.
126
+ The exporter accepts explicit range overrides. A capture that exceeds its
127
+ profile's bounds fails visibly; it is never silently clipped.
128
+
129
+ The encoding into the existing normalized Recording contract is:
130
+
131
+ ```text
132
+ encoded = 2 × (physical − minimum) / (maximum − minimum) − 1
133
+ physical = minimum + (encoded + 1) × (maximum − minimum) / 2
134
+ ```
135
+
136
+ The existing float64/twelve-decimal contract remains in force. Quaternion groups
137
+ must form a unit quaternion, and native output is normalized when applied in
138
+ Blender to remove small numerical drift. Boolean/integer/enum channels use step
139
+ sampling and reject invalid intermediate values; float blends remain continuous.
140
+ Euler curves preserve their sampled winding. Correct discontinuous Euler wraps
141
+ in the source action before capture, or use an appropriate quaternion control.
142
+
143
+ Initial exports enumerate bones deterministically and their writable transforms
144
+ and numeric custom properties in fixed order. Re-export with `--previous` to
145
+ retain every existing channel number and append new bindings. Removed channels,
146
+ changed ranges or changed parameter semantics require an explicit new profile;
147
+ old recordings are not silently reinterpreted. Rest-pose changes also change the
148
+ fingerprint even when names and numbers remain identical.
149
+
150
+ ## Export and capture a real character
151
+
152
+ The helper lives in the installable package as
153
+ `dancerudiments_authoring.blender_bridge`; `tools/blender_rigify_bridge.py` is its
154
+ checkout entry point. Capture and profile discovery use Blender plus the Python
155
+ authoring package. Native apply/bake additionally require a DanceRudiments
156
+ extension built for the Python ABI used by that Blender installation.
157
+
158
+ ```sh
159
+ blender --background character.blend --python tools/blender_rigify_bridge.py -- export-profile --armature rig --model my_character --output my-character.profile.json
160
+ blender --background character.blend --python tools/blender_rigify_bridge.py -- capture-action --armature rig --profile my-character.profile.json --frame-start 1 --frame-end 241 --output my-character.capture.json
161
+ ```
162
+
163
+ Use `--location-range`, `--rotation-range` and `--scale-max` on `export-profile`
164
+ when the character needs different explicit transform bounds. Use `--previous`
165
+ with a prior profile to preserve numbering when adding controls. A saved profile
166
+ fingerprints the actual rest matrices, modes, source version and unit scale, but
167
+ does not include the current pose or random Rigify instance ID. Applying to a
168
+ different rig fails with a fingerprint mismatch.
169
+
170
+ Capture accepts fractional `--frame-start`, `--frame-end` and `--frame-step`
171
+ values. It always retains the explicit last endpoint. `--sample-fps` selects an
172
+ explicit sampling rate instead of `--frame-step`, including subframes. Time zero
173
+ is the first captured frame; source frames, FPS and time origin are recorded in
174
+ provenance. Supply musical beat anchors relative to this origin when compiling
175
+ the capture. Captures are bounded by the existing scalar-count and file-size
176
+ limits; select an excerpt when a long, high-channel-count motion exceeds them.
177
+
178
+ ## Import and retarget motion
179
+
180
+ ```sh
181
+ blender --background --factory-startup --python tools/blender_rigify_bridge.py -- import-source dance.bvh --save dance-source.blend
182
+ ```
183
+
184
+ The bridge delegates `.bvh`, `.fbx`, `.gltf` and `.glb` to Blender's own importers.
185
+ BVH import preserves the source FPS in the scene and updates its duration.
186
+ FBX/glTF may have fractional keyframe endpoints; choose explicit capture bounds
187
+ and sampling rates after retargeting. Imports retain their original skeleton
188
+ until an explicit retargeting/baking step maps them to the generated Rigify
189
+ **controls**. Bone-name copying is not sufficient to compensate for rest-pose
190
+ axes, scale, IK, poles or foot contacts. See [motion sources and tools](motion-sources.md).
191
+
192
+ After retargeting, export the exact destination rig's profile and use
193
+ `capture-action`; the regular motion compiler maps this capture to 64 pips per
194
+ beat. The adapter does not infer musical tempo or foot-contact events.
195
+
196
+ ## Native playback and saved Blender actions
197
+
198
+ ```sh
199
+ blender --background character.blend --python tools/blender_rigify_bridge.py -- apply --armature rig --motion dance.rig-motion.json --pip 64 --save pose-at-beat-one.blend
200
+ blender --background character.blend --python tools/blender_rigify_bridge.py -- bake --armature rig --motion dance.rig-motion.json --target-beat-map song-beats.json --frame-start 1 --save song-matched-character.blend
201
+ ```
202
+
203
+ Both commands sample the existing native C++ recording engine. `apply` writes a
204
+ new one-pose Action, so the requested pose persists after reopening the file.
205
+ `bake` writes a new Action with a key at every recorded pip and explicitly sets
206
+ constant interpolation, retaining the integer-pip playback contract without
207
+ Bezier overshoot. It anchors the recording's first pip at `--frame-start`.
208
+ With `--target-beat-map`, each following pip uses the map's inverse beat-to-time
209
+ conversion; otherwise it uses the recording's nominal tempo.
210
+
211
+ The saved output uses the new Action at full replacement influence and disables
212
+ previous NLA evaluation, preventing accidental mixing with existing animation.
213
+ Previous Action and NLA data are retained. A failed operation restores the old
214
+ Action and animation settings. Output `.blend` paths must be new files.
215
+
216
+ The bridge never animates `DEF-`, `MCH-`, `ORG-` or visual helper bones directly.
217
+ It drives animator controls and lets the generated rig evaluate its constraints,
218
+ drivers and skin deformation. Mesh shape keys, arbitrary external feature-set
219
+ semantics and automatic universal retargeting are outside this profile contract.
220
+ For another Blender/Rigify version or a customized metarig, discover the actual
221
+ rig rather than assuming these pinned manifests remain valid.
222
+
223
+ ## Reproduce the manifests and integration check
224
+
225
+ ```sh
226
+ blender --background --factory-startup --python tools/blender_rigify_bridge.py -- catalogue --output-dir python/dancerudiments_authoring/rig_profiles
227
+ PYTHONPATH=python python -m unittest discover -s tests/python -p test_rigs.py -v
228
+ python tools/test_blender_rigify_bridge.py --output-dir .cache/rigify-smoke
229
+ ```
230
+
231
+ The last command runs in a Python environment with `bpy==5.2.2` and the matching
232
+ compiled DanceRudiments extension. It builds an actual Rigify basic human,
233
+ captures synthetic movement, compiles beat samples, drives a weighted test mesh,
234
+ checks fractional capture and selector handling, bakes variable-tempo native
235
+ motion, reopens saved apply/bake outputs in fresh Blender processes to verify
236
+ their control values and skin deformation, and verifies the contextual glue
237
+ sample. It uses no third-party motion.
238
+
239
+ ## Primary references
240
+
241
+ - [Pinned Blender 5.2.2 Rigify source](https://github.com/blender/blender/tree/v5.2.2/scripts/addons_core/rigify).
242
+ - [Rigify component model and generation stages](https://developer.blender.org/docs/features/animation/rigify/).
243
+ - [Blender Rigify rig types](https://docs.blender.org/manual/en/latest/addons/rigify/rig_types/index.html).
244
+ - [Blender Rigify metarig construction](https://docs.blender.org/manual/en/latest/addons/rigify/metarigs.html).
245
+ - [Blender PoseBone API](https://docs.blender.org/api/current/bpy.types.PoseBone.html).
246
+ - [Official bpy distribution](https://pypi.org/project/bpy/5.2.2/).
@@ -0,0 +1,21 @@
1
+ """Inspect one selected package and sample only the chosen clip natively."""
2
+ import argparse
3
+ from pathlib import Path
4
+ import sys
5
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'python'))
6
+ from dancerudiments_authoring import MotionPackage, load_profile
7
+
8
+ parser = argparse.ArgumentParser(description=__doc__)
9
+ parser.add_argument('package', type=Path)
10
+ parser.add_argument('--clip')
11
+ parser.add_argument('--profile', type=Path, help='Exact profile saved with the target scene')
12
+ args = parser.parse_args()
13
+ profile = load_profile(args.profile) if args.profile else None
14
+ with MotionPackage(args.package, expected_profile=profile) as package:
15
+ for clip in package.catalogue():
16
+ print(clip['id'], clip['start_pip'], clip['sample_count'], clip['byte_length'])
17
+ if args.clip:
18
+ motion = package.load(args.clip)
19
+ player = motion.player()
20
+ print('type:', motion.profile.type_uri)
21
+ print('first native pose by numbered channel:', player.by_channel(player.start_pip))