CC5 HD: Read and reconstruct final evaluated facial morph weights with Python

Hi,

I am using a CC5 HD character in iClone 8 and would like to build an OpenPlugin that samples the character’s facial state at the current timeline time, exports the values to JSON, and later reconstructs the animation as expression keys.

I found these RIFaceComponent methods:

  • GetExpressionSetUid()
  • GetExpressionNames("", False)
  • GetExpressionWeights(time, [])
  • AddExpressionKeys(...)

Could you please clarify:

  1. Does GetExpressionWeights() return the final evaluated/blended facial state at that time, including Face Key expressions, Quick Set presets, expression clips and AccuFACE data?
  2. Does it include the active viseme/AccuLIPS contribution, or must visemes be read separately through RIVisemeComponent?
  3. Are all CC5 HD controls exposed, including jaw, tongue, facial bones and custom expressions?
  4. Can AddExpressionKeys() write all those CC5 HD controls back by name?
  5. Are values limited to 0–1, or are negative and greater-than-one values supported?
  6. Is there an API for enumerating or applying Quick Set expressions such as A1_Happy, or should we apply them manually and sample their evaluated weights?
  7. What UID should GetExpressionSetUid() return for a CC5 HD facial profile?
  8. Could you provide a minimal current CC5 HD example?

The eventual purpose is to build reusable viseme, emotion and character-persona JSON recipes for automated dialogue animation.

Hi,

I did a similar experiment for our other tools not too long ago, so here are two scripts for you to try out. To answer your questions:

  1. Yes, fully blended (base clip + presets + Face Key edits + expressiveness). One catch: it’s base_slider × expressiveness, not the raw slider — divide by GetExpressiveness(t) if you want the portable base value.

  2. Separate — visemes/AccuLIPS live under avatar.GetVisemeComponent(), not RIFaceComponent.

  3. Yes — jaw, tongue, neck, head, custom all show up in GetExpressionNames. Skip the constraint (C_*) group though, those are auto-driven correctives, not real controls.

  4. Yes, by name. It’s also the only write path — no SetExpressionWeights. Values clamp to [0, 1].

  5. Sliders: no, clamp to [0, 1]. Expressiveness itself: yes, unbounded — UI goes past 100%, confirmed same on CC5 and iClone.

  6. AddClip(t, name, length) resolves library presets by exact name, no error on a bad name though — verify by sampling right after.

  7. "CC5 MetaHuman" (literal string, not a GUID) on CC5 profile.

  8. Two minimal test scripts below (sample + reconstruct) — run them yourself and see the actual values.

test_sample_face_pose.py

"""Minimal test: sample the current facial pose + visemes using raw RLPy calls."""

import json

import RLPy

avatar = RLPy.RScene.GetAvatars()[0]
face = avatar.GetFaceComponent()
viseme = avatar.GetVisemeComponent()
t = RLPy.RGlobal.GetTime()

# -- expressions --
names = list(face.GetExpressionNames("", False))
weights = list(face.GetExpressionWeights(t, RLPy.WStringVector(names), False))
expressiveness = round(face.GetExpressiveness(t), 4)
# GetExpressionWeights returns base_weight * expressiveness; divide it back out so the
# stored pose is the raw base slider value (independent of whatever expressiveness is
# live when this gets loaded elsewhere).
scale = expressiveness if abs(expressiveness) > 1e-4 else 1.0
pose = {n: round(w / scale, 4) for n, w in zip(names, weights) if abs(w) > 1e-4}
print("active expression channels:", len(pose), " expressiveness:", expressiveness)
for n, w in sorted(pose.items()):
    print("  %-32s %.4f" % (n, w))

# -- visemes --
id_to_name = {int(getattr(RLPy, a)): a[len("EVisemeID_"):]
             for a in dir(RLPy) if a.startswith("EVisemeID_")}

keys = viseme.GetVisemeKeys()
visemes = []
for i in range(len(keys)):
    k = keys[i]
    if k.GetTime().ToFloat() != t.ToFloat():
        continue
    vid, w = int(k.GetID()), round(k.GetWeight(), 4)
    if vid == 0 and abs(w) < 1e-4:
        continue  # NONE placeholder key at clip boundaries, not a real viseme
    visemes.append({"id": id_to_name.get(vid, vid), "weight": w,
                    "transition": int(k.GetTransitionType())})
print("\nviseme keys at this frame:", visemes)

with open(r"C:\recipes\pose.json", "w") as f:
    json.dump({"expressions": pose, "expressiveness": expressiveness, "visemes": visemes}, f, indent=2)

test_load_face_pose.py

"""Minimal test: load a sampled pose + visemes back using raw RLPy calls."""

import json

import RLPy

with open(r"C:\recipes\pose.json") as f:
    data = json.load(f)

avatar = RLPy.RScene.GetAvatars()[0]
face = avatar.GetFaceComponent()
viseme = avatar.GetVisemeComponent()
t = RLPy.RGlobal.GetTime()

# -- expressions --
names = list(data["expressions"].keys())
weights = list(data["expressions"].values())

face.BeginKeyEditing()
face.AddExpressionKeys(t, RLPy.WStringVector(names), RLPy.FloatVector(weights), RLPy.RTime.FromValue(0))
face.AddExpressivenessKey(t, data["expressiveness"])
face.EndKeyEditing()
print("keyed %d expression channels at t=%s, expressiveness=%s" % (len(names), t.ToFloat(), data["expressiveness"]))

# -- visemes --
for v in data["visemes"]:
    k = RLPy.RVisemeKey()
    k.SetTime(t)
    k.SetID(getattr(RLPy, "EVisemeID_" + v["id"]))
    w100 = max(0.0, min(100.0, v["weight"] * 100))  # SetWeight is 0-100; outside that range it no-ops silently
    k.SetWeight(w100)
    k.SetTransitionType(v["transition"])
    viseme.AddVisemeKey(k)
print("keyed %d viseme(s)" % len(data["visemes"]))
1 Like