Wan Face Controller 3D
Twelve expression dials, 3-DoF head rotation, gaze offset — all on the pose data, before the sampler
- 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
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_datainput), 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 apropagate_expressionoption 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_headto push a keyframe pose across a range. - Gaze offset - yaw/pitch on the iris data, with
propagate_gazefor 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_imageandoverlay_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'sExpressionEditor(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.
Inputs (13)
| Name | Type | Default | Description |
|---|---|---|---|
| pose_dataopt | POSEDATA | Pre-detected pose bundle (iBUG-68 face landmarks). OPTIONAL if you wire `images` + `model` to detect inside this node. | |
| imagesopt | IMAGE | Video frames (B,H,W,3). Wire a video loader here to run ViTPose pose estimation INSIDE this node — no separate detector node needed. | |
| modelopt | POSEMODEL | ViTPose+YOLO bundle from 'ONNX Detection Model Loader (V2)'. Used for internal detection (when `images` is wired and `pose_data` is empty). | |
| detection_thresholdopt | FLOAT | 0.300–1 | Internal detection: YOLO person-detection confidence threshold. |
| pose_thresholdopt | FLOAT | 0.300–1 | Internal detection: per-keypoint confidence threshold. |
| use_claheopt | BOOLEAN | true | Internal detection: CLAHE contrast enhancement on the pose crop. |
| detect_rescaleopt | FLOAT | 1.251–2 | Internal detection: bbox padding factor before the ViTPose crop. |
| fallback_to_full_frameopt | BOOLEAN | true | Internal detection: run ViTPose on the full frame when YOLO finds no person. |
| reference_pose_dataopt | POSEDATA | Optional single-frame POSEDATA used as a region-wise shape target. | |
| fc3d_config_jsonopt | STRING | {"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_jsonopt | STRING | JSON {"frames":{"<idx>":{"<lm>":[x_img_norm,y_img_norm]}}} from the in-canvas face viewer (image-normalised 0..1). Empty = no override. | |
| pose_overrides_jsonopt | STRING | JSON {"frames":{"<idx>":{"<joint>":[x_img_norm,y_img_norm]}}} from the in-canvas pose viewer (OpenPose-18). | |
| gaze_overrides_jsonopt | STRING | JSON {"frames":{"<idx>":{"l":[yaw_rad,pitch_rad],"r":[yaw_rad,pitch_rad]}}} from the in-canvas gaze handles. |
Outputs (12)
| Name | Type | Description |
|---|---|---|
| pose_data | POSEDATA | — |
| info | STRING | — |
| coeff_time_series_json | STRING | — |
| preview_image | IMAGE | — |
| overlay_video | IMAGE | — |
| keyframes_csv | STRING | — |
| pose_diff_json | STRING | — |
| lp_rotate_pitch | FLOAT | Head pitch for LivePortrait ExpressionEditor (clamped ±20°). Wire to rotate_pitch. |
| lp_rotate_yaw | FLOAT | Head yaw for ExpressionEditor (clamped ±20°). Wire to rotate_yaw. |
| lp_rotate_roll | FLOAT | Head roll for ExpressionEditor (clamped ±20°). Wire to rotate_roll. |
| lp_pupil_x | FLOAT | Gaze→pupil_x for ExpressionEditor (clamped ±15). Wire to pupil_x. |
| lp_pupil_y | FLOAT | Gaze→pupil_y for ExpressionEditor (clamped ±15). Wire to pupil_y. |