DriverMappingRunner orchestrates bindings, what each BindingMode
does to a signal, and how to combine them into recipes that drive lights, sounds, blendshapes,
or anything else you can wire to a channel.
The DriverMappingRunner is the component that connects your DataBus channels (the live numbers flowing in from MediaPipe, audio analysis, time, manual sliders, etc.) to your scene objects (transforms, blendshapes, lights, materials, audio sources, animator parameters, ParticleSystems, even arbitrary reflected fields).
It does this through DriverBindings — small data records that say "read this channel, transform it, then push it to that target". The Runner holds a list of these and re-applies them every frame (in LateUpdate, after gameplay/animation have settled).
Three operating modes coexist on the same Runner. You can use one, two, or all three at once — they're processed in this order each frame: scene bindings first, asset mapping second. If they push the same target, the asset mapping wins (it runs last).
Direct DriverBinding entries living on the Runner itself (sceneBindings list). Scene refs go straight into each binding's targetObject / targetComponent fields. Everything saves with the scene file — open the scene tomorrow, hit Play, it just works.
Best for: one-off scenes, learning, iterative tweaking, anything where you don't need to share the mapping with another scene.
A DriverMapping ScriptableObject (a .asset file) holds a list of bindings. The Runner references one asset via assetMapping. Because the asset has no scene-specific references, it can be re-used in any scene or shared between projects.
Scene-specific wiring lives separately in assetTargets[] on the Runner — an array of BindingTarget structs that supply the actual scene objects each binding in the asset should drive. The asset is the recipe; the targets are the ingredients.
Best for: rigs you want to clone across multiple scenes ("the avatar mocap rig"), version-controlled mapping libraries, projects with multiple characters using the same mapping logic.
mappingLibrary[] is a list of multiple DriverMapping assets. The Runner cycles through them at runtime, driven by a single DataBus channel value via the swapKeyPath field (default "m").
When the channel's value rounds to 0, asset index 0 plays. When it rounds to 1, asset index 1 plays. And so on. This lets a performer flip between completely different mappings mid-show — same body, totally different effects routing.
assetTargets[] is shared across all assets in the library. So every asset in the library must use the same number of bindings in the same order, with the same expected target types. Plan the slot ordering up front.
| Field | Purpose |
|---|---|
| sceneBindings | List of DriverBindings with direct scene refs — saves with scene. |
| assetMapping | One DriverMapping ScriptableObject. Optional. |
| assetTargets | Per-binding scene-object wiring used by both assetMapping and mappingLibrary[]. |
| mappingLibrary | Multiple DriverMappings cycled by swapKeyPath's channel value. |
| swapKeyPath | Channel that picks the active library index. Default "m". |
| runInLateUpdate | If true (default) bindings apply after animation. If false, in Update. |
Every binding pushes one channel value through this six-step chain, then applies the result to its target. Understanding the order matters — most "why doesn't this respond the way I expect" issues come from an earlier stage clamping or smoothing the value out before later stages can react.
DataBus.ReadFloat(channelKey). Returns 0 if the key doesn't exist — bindings to mistyped keys silently produce 0 rather than crashing.
inputMin..inputMax → outputMin..outputMax. If clamp is on, values outside inputMin..inputMax get clipped to the output range. invert swaps high/low.
An AnimationCurve evaluated on the normalised 0..1 value before final scaling. Use it to shape ease-in / ease-out / threshold cliffs.
Replaces the value based on logic — passthrough, binary threshold, gated, latched, sequence step, or pulse. Detailed in section 4.
Lerp low-pass towards the target value. 0 = no smoothing (snap), 0.99 = heavy smoothing (slow). Frame-rate dependent — tune for your average fps.
Big switch on targetType — writes the final value to the correct property (transform.position.x, BlendShape weight, AudioSource.volume, MaterialColor hue, etc).
The BindingMode field on every DriverBinding decides what the binding does with its remapped value. Get this right and the whole pipeline falls into place; pick the wrong mode and you'll fight the signal forever.
modeThreshold from below. If a channel starts above the threshold, the first trigger won't fire until the channel dips below and rises again. Pre-process with a ThresholdProcessor or arrange the binding so the channel starts at 0.
The remapped value passes straight through to the target. No logic, no thresholds — just scaled channel → target property.
For anything continuous: position, scale, rotation, volume, blendshape weight, light intensity, material colour hue, animator float parameters. The default and most common mode by far.
pose/landmark/rightWrist/y. Remap -0.3..0.7 → 0..1. Target: AudioVolume on your main music source. Wrist drops below mid-chest → silence; arms-overhead → full volume.pose/body/centre/z. Remap to 0..3 with a slight ease-in curve. Target: MaterialFloat _EmissionStrength on a cube. Smoothing 0.4 so the glow doesn't pulse with every breath.pose/joint/jaw/openNorm. Remap 0..0.4 → 0..100 (blendshape weights are 0–100). Target: BlendShape on a face mesh. Smoothing 0.1 — too much and the mouth lags behind speech.Binary on/off based on whether the channel value crosses modeThreshold. Above → outputMax. Below → outputMin. The value between those is never visited — Switch produces a hard step.
For state-flips: play/stop, on/off, enabled/disabled. Light switches, audio play-stop, animator booleans, mute toggles, "is the user posing yes-or-no?".
pose/landmark/rightWrist/y. modeThreshold 0.4. outputMin=0, outputMax=1. Target: AudioPlayStop on a snare loop. Hand drops → stops. (This is exactly what your existing snare binding does.)The binding's main channel passes through only when a separate gateChannel reads above 0.5. When the gate is closed, output snaps to outputMin. When open, normal Direct-mode behaviour.
When you want one channel to arm another. Examples: "only let the right hand drive the synth pitch while the left hand is raised"; "only respond to facial blendshapes while the user is looking at the camera".
On the rising edge (channel crosses modeThreshold from below), captures the current value and holds it indefinitely — even if the channel changes. Only releases when a separate resetChannel reads above 0.5.
For snapshot behaviour. "When I clap, freeze the current pitch and hold it"; "When I tilt my head, remember that head angle until I reset". Anything where you want one moment captured and preserved.
pose/joint/leftElbow/raiseNorm. Move your wrist to pick a pitch, clap to freeze, raise the other elbow to release.Steps through the sequenceValues[] array, advancing one index every rising edge. Output is the value at the current index. Wraps to the beginning when it runs out.
For discrete state-cycling. Lighting cue changes (red → blue → green → black), audio loop swaps (intro → verse → chorus → outro), animator-state triggers (idle → guard → strike).
pose/landmark/rightAnkle/y with modeThreshold 0.2 (a foot-stomp briefly drops then rises). sequenceValues: [0, 0.33, 0.66, 1]. Target: AnimatorFloat cueIndex on a lighting director with 4 states.[0, 1, 2, 3] selecting which.On rising edge, snaps to outputMax. Then decays linearly back to outputMin over pulseDecay seconds — regardless of what the channel does afterwards.
For impact-feel events. Camera shake on punch, screen flash on clap, particle burst on jump, light strobe on snare hit, drum-trigger latency-free playback.
_Alpha. Sharp on the clap, gone before the next.
The targetType field decides which kind of property the binding pushes its final value into. Grouped here by category.
| Group · Target Type | What it writes to |
|---|---|
| Transform | |
| Transform | targetTransform.position / rotation / scale on one chosen axis |
| Mesh + Animator | |
| BlendShape | SkinnedMeshRenderer blendshape weight 0–100 (auto-remapped from 0–1) |
| AnimatorFloat | Animator float parameter |
| AnimatorBool | Animator bool parameter (true if value > 0.5) |
| AnimatorTrigger | Animator trigger — fired on rising edge only |
| Material | |
| MaterialFloat | Named shader float on any material (e.g. _Roughness) |
| MaterialColor | Interprets value as hue, applies via HSVToRGB. Falls back to _BaseColor → _Color if the named property doesn't exist. |
| Light | |
| LightIntensity | Light.intensity |
| LightRange | Light.range |
| LightColor | Light.color — interpreted as hue, like MaterialColor |
| Particles | |
| ParticleEmission | ParticleSystem.emission.rateOverTime |
| Audio (built-in) | |
| AudioVolume | AudioSource.volume |
| AudioPitch | AudioSource.pitch |
| AudioCrossfade | Multi-source crossfade index |
| AudioPlayStop | Play above threshold, Stop below |
| AudioSpatialX / Y / Z | 3D AudioSource world position component |
| AudioReverbZone | AudioReverbZone parameters |
| Audio (PaulXStretch — OSC to Reaper) | |
| PaulXTrackProperty | Any of the 9 PaulXStretch params (StretchAmount, PitchShift, FreqShift, FilterLow / High, Spread, Compress, MainVolume, Freeze) on a PaulXTrack component |
| Audio (UnityAudioFX — in-process) | |
| UnityAudioFXProperty | Auto-discovered FX on an AudioSource: AudioLowPassFilter, HighPassFilter, Echo, Distortion, Chorus, Reverb |
| Escape hatch | |
| Reflection | Arbitrary float/bool/int/double member by name on any component. Component lookup is case-insensitive Contains-match on short or full type name. |
Two bindings on the same MaterialFloat _Glow. First is Direct on body-centre-Z giving a resting glow (0..0.3). Second is Pulse on a clap-channel adding an outputMax=1 spike with 0.2s decay. The Direct holds the baseline, the Pulse adds drama. Listing order matters — the Pulse should come after the Direct so its result wins per frame.
A Latch binding captures the current head-pitch and holds it as a cameraTilt animator float. A second Direct binding on shoulder-Z applies live tweaks on top. The Latch establishes the base, the Direct nudges it. Reset the latch via clap or any chosen reset channel.
Sequence binding on a foot-stomp channel cycles AudioCrossfade across 4 loops. A separate Direct binding on right-wrist-Y controls the master volume of the cross-faded output. Performer can change track and adjust loudness independently with each hand.
Two body sources (rare but possible with two cameras + namespaced channels) feed the same scene. Use Gate to enable Performer A's bindings only when Performer A is on the left half of the frame, and B's bindings only when B is on the right. Channel: pose/body/centre/x with appropriate ranges as the gate channels.
One ChannelTrail (3D, scene-level — coming in a future stage) plus a Pulse binding on each major joint's velocity → ParticleEmission. The trail records the path; pulses spawn bursts of particles at moments of high velocity along it. Different particle systems per joint give per-limb "trails of action".
Three DriverMappings in mappingLibrary[], one per "set" of a performance. swapKeyPath bound to a manual ChannelSliderPanel slider — between sets the operator nudges the slider to advance. Same body, fundamentally different effect routing.
Open the F12 DataFlowMonitor panel and confirm the channel actually has a non-zero value. If it's flatlined, the source isn't writing — check the MediaPipePoseRunner is active, the camera frame is updating in the F10 preview, and the channel key spelling matches exactly (channel keys are case-sensitive).
→ if channel has values but binding stays at 0: check the binding's input range. A channel reading 0.2 with inputMin/Max of 0.5..1 maps to outputMin (your "off" value).
The remap is off. Open F11 ChannelSliderPanel, override the channel value, and watch what the target does. Then adjust inputMin/Max + outputMin/Max accordingly.
→ for blendshapes specifically, remember weights are 0–100 not 0–1. The editor auto-remaps but only if you haven't manually changed outputMax.
The rising-edge gotcha. Rising-edge modes need to see the signal cross modeThreshold from below. If the channel starts above the threshold, the first trigger is "swallowed" until the value dips and rises again.
→ fix by adding a ThresholdProcessor on the source channel that emits a clean 0 when below threshold, or by arranging the binding's input range so it starts below the threshold on scene-load.
The gateChannel test is a literal > 0.5 on the raw DataBus value — your binding's remap doesn't affect it.
→ inspect the gate channel directly in F12 DataFlowMonitor. If it never crosses 0.5, either pick a different channel or pre-process with ThresholdProcessor / SmoothProcessor to produce a clean 0–1 signal.
Increase smoothing on the binding (0.3–0.7 is a good first range). For very noisy channels (face landmarks especially), upstream SmoothProcessor in the Processors chain helps too — pre-smoothing the channel makes every binding feel calmer.
→ trade-off: higher smoothing = slower response. Tune visually with the F11 panel by sweeping the channel and watching the lag.
The channel is bouncing across the threshold — common with noisy face/hand channels. Increase the channel's upstream SmoothProcessor amount, raise modeThreshold above the noise floor, or add a Switch in front to debounce.
MaterialColor and LightColor interpret the binding's output value as a hue, not as a single RGB channel. Output ranges that span 0..1 sweep the full rainbow. If you wanted a fixed colour at variable intensity, use a MaterialFloat / LightIntensity binding instead and pre-set the colour manually.
Two things to check: (1) the swapKeyPath channel is actually being written — confirm in F12. (2) All assets in mappingLibrary[] use the same binding count + target order, since assetTargets[] is shared across them.
Standard Unity behaviour — anything you change in Play mode resets when Play stops. Two workarounds:
① Make changes when not in Play (the inspector accepts edits while the runner sleeps).
② In Play, right-click the changed component header → Copy Component, exit Play, right-click → Paste Component Values.