Nodes/ComfyUI-WanAnimatePreprocessV2/Wan Face Controller 3D
ComfyUI Node

Wan Face Controller 3D

Twelve expression dials, 3-DoF head rotation, gaze offset — all on the pose data, before the sampler

By Code2Collapse·Created 8 months ago·Updated 8 days ago· 17
Wan Face Controller 3D
  • pose_data
  • images
  • model
  • reference_pose_data
  • pose_data
  • info
  • coeff_time_series_json
  • preview_image
  • overlay_video
  • keyframes_csv
  • pose_diff_json
  • lp_rotate_pitch
  • lp_rotate_yaw
  • lp_rotate_roll
  • lp_pupil_x
  • lp_pupil_y
detection_threshold0.30
pose_threshold0.30
use_clahetrue
detect_rescale1.25
fallback_to_full_frametrue
fc3d_config_json{"expression_coeffs_json":"","expression_strength":1.0,"expression_clamp":1.5,"expression_clamp_per_axis_json":"","propagate_expression":"off","head_pose_json":"","head_yaw_deg":0.0,"head_pitch_deg":0.0,"head_roll_deg":0.0,"head_tx":0.0,"head_ty":0.0,"head_tz":0.0,"head_scale":1.0,"jaw_rot_deg":0.0,"neck_yaw_deg":0.0,"neck_pitch_deg":0.0,"propagate_head":"off","propagate_gaze":"off","gaze_json":"","gaze_yaw_deg":0.0,"gaze_pitch_deg":0.0,"blend_strength":0.0,"blend_mouth":true,"blend_brows":false,"blend_eyes":false,"blend_jaw":false,"use_metas":"edited","frame_start":-1,"frame_end":-1,"preview_frame_idx":0,"preview_size":512,"preview_max_video_frames":120}
landmark_overrides_json
pose_overrides_json
gaze_overrides_json

WanFaceController3DV2 is the pack's all-in-one face editor, and it does what the other nodes won't: it changes the detected performance instead of just measuring or rendering it. Four independent stages, applied in order - a reference-shape blend, twelve FACS-inspired expression dials, a 3-DoF head rotation, and a gaze offset - and leaving any stage at its defaults makes it a no-op, so the same node covers a full edit or a single tweak.

The mental model: this node sits between detection and drawing. It takes pose_data, mutates the face landmarks / head pose / gaze, and hands the edited bundle downstream. If you then wire the edited pose_data into DrawViTPoseV2 with apply_pose_edits_to_face="warp", the actual face-crop pixels get warped to match - which is the only way Wan Animate's pixel-driven face encoder actually sees the edit. Miss that wiring and your expression dials change a skeleton that does nothing visible.

What it can do

  • Reference-shape blend - blend the detected face toward the shape of a reference frame's pose_data (reference_pose_data input), per region: mouth, brows, eyes, jaw. This is how you nudge a performance toward a specific character's resting face.
  • Expression dials - 12 FACS-inspired coefficients driven through expression_coeffs_json (the ARKit-52 blendshapes measured by the detection node). expression_strength, expression_clamp, per-axis clamps, and a propagate_expression option for extending an edit across frames.
  • Head rotation - yaw/pitch/roll plus translation and scale, applied using a canonical iBUG-68 depth map so the face rotates in 3D rather than skewing. There are neck controls too, and propagate_head to push a keyframe pose across a range.
  • Gaze offset - yaw/pitch on the iris data, with propagate_gaze for sustained redirection.

All of that is steered from fc3d_config_json, a big editor-owned JSON that the in-canvas viewer (face_controller_3d.js) writes for you when you drag the iBUG-68 face landmarks, OpenPose-18 body joints, or gaze handles. There are also raw landmark_overrides_json, pose_overrides_json and gaze_overrides_json inputs if you want to inject edits programmatically.

The node can even do its own detection: wire images + model and it runs ViTPose internally (with detection_threshold, pose_threshold, use_clahe, detect_rescale, fallback_to_full_frame), so you don't need a separate detection node. pose_data stays optional for that reason.

Outputs worth knowing

  • pose_data - the edited bundle; the one you wire forward.
  • preview_image and overlay_video - the in-canvas feedback, so you can see edits without a full render.
  • keyframes_csv / pose_diff_json / coeff_time_series_json - edit provenance, useful when you need to know exactly what changed and when.
  • lp_rotate_pitch / lp_rotate_yaw / lp_rotate_roll / lp_pupil_x / lp_pupil_y - clamped outputs specifically shaped for LivePortrait's ExpressionEditor (head rotations clamped to ±20°, pupil to ±15). If you're feeding a LivePortrait pipeline downstream instead of Wan Animate, these drop straight onto its inputs.

The honest take

This is the most powerful and the most fiddly node in the pack, and it's unapologetically aimed at people who want frame-accurate performance editing. The in-canvas viewer is the intended interface - editing the raw JSON by hand is a punishment. Two practical notes: the whole thing is optional wiring, so it slots into an existing graph without breaking it (defaults are all no-ops), and if your edits seem to have zero effect on the render, check that apply_pose_edits_to_face="warp" is on in DrawViTPoseV2 and that face_images is wired - that's the delivery path, and it's the first thing to go wrong.

Install is the shared pack path: Manager search "WanAnimatePreprocessV2", or git clone https://github.com/Code2Collapse/ComfyUI-WanAnimatePreprocessV2 into custom_nodes, pip install -r requirements.txt, restart. No extra models for the editor itself; the detection it runs internally reuses the ONNX models from the loader node.

CategoryWanAnimatePreprocessV2/extras

Inputs (13)

NameTypeDefaultDescription
pose_dataoptPOSEDATAPre-detected pose bundle (iBUG-68 face landmarks). OPTIONAL if you wire `images` + `model` to detect inside this node.
imagesoptIMAGEVideo frames (B,H,W,3). Wire a video loader here to run ViTPose pose estimation INSIDE this node — no separate detector node needed.
modeloptPOSEMODELViTPose+YOLO bundle from 'ONNX Detection Model Loader (V2)'. Used for internal detection (when `images` is wired and `pose_data` is empty).
detection_thresholdoptFLOAT0.300–1Internal detection: YOLO person-detection confidence threshold.
pose_thresholdoptFLOAT0.300–1Internal detection: per-keypoint confidence threshold.
use_claheoptBOOLEANtrueInternal detection: CLAHE contrast enhancement on the pose crop.
detect_rescaleoptFLOAT1.251–2Internal detection: bbox padding factor before the ViTPose crop.
fallback_to_full_frameoptBOOLEANtrueInternal detection: run ViTPose on the full frame when YOLO finds no person.
reference_pose_dataoptPOSEDATAOptional single-frame POSEDATA used as a region-wise shape target.
fc3d_config_jsonoptSTRING{"expression_coeffs_json":"","expression_strength":1.0,"expression_clamp":1.5,"expression_clamp_per_axis_json":"","propagate_expression":"off","head_pose_json":"","head_yaw_deg":0.0,"head_pitch_deg":0.0,"head_roll_deg":0.0,"head_tx":0.0,"head_ty":0.0,"head_tz":0.0,"head_scale":1.0,"jaw_rot_deg":0.0,"neck_yaw_deg":0.0,"neck_pitch_deg":0.0,"propagate_head":"off","propagate_gaze":"off","gaze_json":"","gaze_yaw_deg":0.0,"gaze_pitch_deg":0.0,"blend_strength":0.0,"blend_mouth":true,"blend_brows":false,"blend_eyes":false,"blend_jaw":false,"use_metas":"edited","frame_start":-1,"frame_end":-1,"preview_frame_idx":0,"preview_size":512,"preview_max_video_frames":120}Editor-owned JSON for expression/head/gaze/blend/preview params. Synced automatically by face_controller_3d.js.
landmark_overrides_jsonoptSTRINGJSON {"frames":{"<idx>":{"<lm>":[x_img_norm,y_img_norm]}}} from the in-canvas face viewer (image-normalised 0..1). Empty = no override.
pose_overrides_jsonoptSTRINGJSON {"frames":{"<idx>":{"<joint>":[x_img_norm,y_img_norm]}}} from the in-canvas pose viewer (OpenPose-18).
gaze_overrides_jsonoptSTRINGJSON {"frames":{"<idx>":{"l":[yaw_rad,pitch_rad],"r":[yaw_rad,pitch_rad]}}} from the in-canvas gaze handles.

Outputs (12)

NameTypeDescription
pose_dataPOSEDATA
infoSTRING
coeff_time_series_jsonSTRING
preview_imageIMAGE
overlay_videoIMAGE
keyframes_csvSTRING
pose_diff_jsonSTRING
lp_rotate_pitchFLOATHead pitch for LivePortrait ExpressionEditor (clamped ±20°). Wire to rotate_pitch.
lp_rotate_yawFLOATHead yaw for ExpressionEditor (clamped ±20°). Wire to rotate_yaw.
lp_rotate_rollFLOATHead roll for ExpressionEditor (clamped ±20°). Wire to rotate_roll.
lp_pupil_xFLOATGaze→pupil_x for ExpressionEditor (clamped ±15). Wire to pupil_x.
lp_pupil_yFLOATGaze→pupil_y for ExpressionEditor (clamped ±15). Wire to pupil_y.