Skip to content

Retargeting

Remap the 15 visemes onto another rig's blendshape names and weights — ARKit, Rhubarb, Preston-Blair, VRM, CC4, Ready Player Me, and more — via the built-in presets or a custom mapping. See the Retargeting guide for the preset tables and worked examples.

openfacefx.retarget

Retarget viseme channels onto another rig's blendshape convention.

A mapping sends each Oculus-15 viseme to one or more target shapes with a weight scale::

{"PP": [("mouthClose", 0.9), ("mouthPressLeft", 0.35)], ...}

retarget resamples on the union of the contributing channels' key times (linear interpolation, matching intended playback), sums the scaled contributions per target, clamps to [0, 1], and returns a new FaceTrack. Presets for common rigs live in PRESETS; they are plain data — copy and tweak for your character.

A rig that lacks some of a preset's target shapes can pass available= (an iterable of the shapes it actually has). Any mapped target not in that set is rerouted through a per-preset PRESET_FALLBACKS table so its weight redistributes instead of vanishing (e.g. a tongue-less ARKit rig sends tongueOut to a small jawOpen); a target with no fallback rule is dropped. Provenance and the fallback tables: docs/retargeting.md.

Mapping = Dict[str, Sequence[Tuple[str, float]]] module-attribute

PRESETS: Dict[str, Mapping] = {'arkit': _ARKIT, 'rhubarb': _RHUBARB, 'preston_blair': _PRESTON_BLAIR, 'vrm': _VRM, 'vrm0': _VRM0, 'cc4': _CC4, 'readyplayerme': _READY_PLAYER_ME} module-attribute

PRESET_FALLBACKS: Dict[str, Mapping] = {'rhubarb': {'G': (('A', 1.0),), 'H': (('C', 1.0),), 'X': (('A', 1.0),)}, 'arkit': {'tongueOut': (('jawOpen', 0.2),)}} module-attribute

retarget(track: FaceTrack, mapping: Mapping, available: Optional[Iterable[str]] = None, fallbacks: Optional[Mapping] = None) -> FaceTrack

Return a new FaceTrack with channels renamed/combined per mapping.

Source channels absent from the mapping are dropped (deliberately: a rig that lacks a shape should not receive its weight).

available is an iterable of the target shape names the rig actually has; any mapped target outside it reroutes through fallbacks (a {target: [(replacement, scale), ...]} table, typically PRESET_FALLBACKS[name]) so weight redistributes rather than dropping silently. available=None (default) disables filtering and is identical to a plain rename/combine.

Source code in src/openfacefx/retarget.py
def retarget(track: FaceTrack, mapping: Mapping,
             available: Optional[Iterable[str]] = None,
             fallbacks: Optional[Mapping] = None) -> FaceTrack:
    """Return a new FaceTrack with channels renamed/combined per ``mapping``.

    Source channels absent from the mapping are dropped (deliberately: a rig
    that lacks a shape should not receive its weight).

    ``available`` is an iterable of the target shape names the rig actually has;
    any mapped target outside it reroutes through ``fallbacks`` (a
    ``{target: [(replacement, scale), ...]}`` table, typically
    ``PRESET_FALLBACKS[name]``) so weight redistributes rather than dropping
    silently. ``available=None`` (default) disables filtering and is identical
    to a plain rename/combine.
    """
    avail = set(available) if available is not None else None
    # target name -> list of (sampler, key times, scale)
    contributors: Dict[str, List[tuple]] = {}
    for ch in track.channels:
        times = [k.time for k in ch.keys]
        for target, scale in mapping.get(ch.name, ()):
            for final, s in _resolve_target(target, scale, avail, fallbacks):
                contributors.setdefault(final, []).append((_sampler(ch), times, s))

    channels: List[Channel] = []
    for target, sources in contributors.items():
        times = sorted({t for _, ts, _ in sources for t in ts})
        keys = []
        for t in times:
            v = sum(sample(t) * scale for sample, _, scale in sources)
            keys.append(Keyframe(t, round(min(max(v, 0.0), 1.0), 4)))
        channels.append(Channel(target, keys))
    channels.sort(key=lambda c: c.name)
    # Declared vocabulary = every target the mapping can actually produce for
    # this rig (fallback-resolved), so an available= run advertises only the
    # shapes it emits.
    all_targets = sorted({ft for targets in mapping.values() for t, _ in targets
                          for ft, _ in _resolve_target(t, 1.0, avail, fallbacks)})
    return FaceTrack(fps=track.fps, channels=channels, target_set=all_targets)

rename_only(prefix: str = '', names: Dict[str, str] = None) -> Mapping

Build a 1:1 mapping that renames each viseme, e.g. viseme_PP.

Source code in src/openfacefx/retarget.py
def rename_only(prefix: str = "", names: Dict[str, str] = None) -> Mapping:
    """Build a 1:1 mapping that renames each viseme, e.g. ``viseme_PP``."""
    names = names or {}
    return {v: [(names.get(v, prefix + v), 1.0)] for v in VISEMES}