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:
-
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.
-
Separate — visemes/AccuLIPS live under avatar.GetVisemeComponent(), not RIFaceComponent.
-
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.
-
Yes, by name. It’s also the only write path — no SetExpressionWeights. Values clamp to [0, 1].
-
Sliders: no, clamp to [0, 1]. Expressiveness itself: yes, unbounded — UI goes past 100%, confirmed same on CC5 and iClone.
-
AddClip(t, name, length) resolves library presets by exact name, no error on a bad name though — verify by sampling right after.
-
"CC5 MetaHuman" (literal string, not a GUID) on CC5 profile.
-
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"]))