mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-06-23 16:29:25 +08:00
BVH refactor
This commit is contained in:
parent
cf7c5a0bde
commit
6280ba29a3
@ -713,6 +713,12 @@ class File3DFBX(ComfyTypeIO):
|
||||
Type = File3D
|
||||
|
||||
|
||||
@comfytype(io_type="FILE_3D_BVH")
|
||||
class File3DBVH(ComfyTypeIO):
|
||||
"""BVH format 3D file - skeletal motion capture animation (no geometry)."""
|
||||
Type = File3D
|
||||
|
||||
|
||||
@comfytype(io_type="FILE_3D_OBJ")
|
||||
class File3DOBJ(ComfyTypeIO):
|
||||
"""OBJ format 3D file - simple geometry format."""
|
||||
@ -2349,6 +2355,7 @@ __all__ = [
|
||||
"File3DGLB",
|
||||
"File3DGLTF",
|
||||
"File3DFBX",
|
||||
"File3DBVH",
|
||||
"File3DOBJ",
|
||||
"File3DSTL",
|
||||
"File3DUSDZ",
|
||||
|
||||
@ -33,12 +33,7 @@ from comfy_extras.sam3d_body.utils import image_to_uint8
|
||||
|
||||
|
||||
SAM3TrackData = io.Custom("SAM3_TRACK_DATA")
|
||||
# MHRPoseData = SAM3DBody_Predict's native output (carries mhr_model_params,
|
||||
# shape_params, expr_params, MHR70 keypoint layout, canonical_colors keyed to
|
||||
# MHR mesh, hand_vert_mask from MHR LBS). The export-side consumers
|
||||
# (BuildPoseGLB / SavePoseBVH in comfy_extras/nodes_save_3d.py) also accept
|
||||
# KIMODO_POSE_DATA via a MultiType union — those types are mirrored there.
|
||||
MHRPoseData = io.Custom("MHR_POSE_DATA")
|
||||
MHRPoseData = io.Custom("MHR_POSE_DATA") # mhr_model_params, shape_params, expr_params, MHR70 keypoint layout, canonical_colors keyed to MHR mesh, hand_vert_mask from MHR LBS).
|
||||
SAM3DBodyModel = io.Custom("SAM3D_BODY_MODEL")
|
||||
|
||||
# Loader
|
||||
@ -151,7 +146,7 @@ class SAM3DBody_Predict(io.ComfyNode):
|
||||
),
|
||||
),
|
||||
io.Int.Input(
|
||||
"chunk_size", #TODO: automate?
|
||||
"batch_size", #TODO: automate?
|
||||
default=64, min=1, max=512, step=1, advanced=True,
|
||||
tooltip=(
|
||||
"Max frames to process as a batch. Larger values utilize more VRAM for faster inference."
|
||||
@ -162,7 +157,7 @@ class SAM3DBody_Predict(io.ComfyNode):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, sam3d_body_model, image, sam3_track_data=None, bboxes=None, run_hand_refinement=True, fov=0.0, chunk_size=64) -> io.NodeOutput:
|
||||
def execute(cls, sam3d_body_model, image, track_data=None, bboxes=None, run_hand_refinement=True, fov=0.0, batch_size=64) -> io.NodeOutput:
|
||||
comfy.model_management.load_model_gpu(sam3d_body_model)
|
||||
inner: SAM3DBody = sam3d_body_model.model
|
||||
|
||||
@ -171,8 +166,8 @@ class SAM3DBody_Predict(io.ComfyNode):
|
||||
|
||||
# Precedence: SAM3 track (masks + boxes) > detector boxes > full-frame fallback.
|
||||
per_frame_bboxes, per_frame_masks = (None, None)
|
||||
if sam3_track_data is not None:
|
||||
per_frame_bboxes, per_frame_masks = inputs_from_sam3_track(sam3_track_data, B, H, W)
|
||||
if track_data is not None:
|
||||
per_frame_bboxes, per_frame_masks = inputs_from_sam3_track(track_data, B, H, W)
|
||||
if per_frame_bboxes is None and bboxes:
|
||||
per_frame_bboxes = _per_frame_bboxes_from_detections(bboxes, B)
|
||||
per_frame_masks = None
|
||||
@ -209,7 +204,7 @@ class SAM3DBody_Predict(io.ComfyNode):
|
||||
image_size, inference_type,
|
||||
cam_int=cam_int,
|
||||
pbar=pbar,
|
||||
crops_per_chunk=int(chunk_size),
|
||||
crops_per_chunk=int(batch_size),
|
||||
)
|
||||
else:
|
||||
# Mixed K per frame — call the batched path once per frame.
|
||||
@ -459,7 +454,7 @@ class SAM3DBody_Smooth(io.ComfyNode):
|
||||
io.Combo.Input(
|
||||
"method",
|
||||
options=["gaussian", "savgol"],
|
||||
default="gaussian", advanced=True,
|
||||
default="savgol", advanced=True,
|
||||
tooltip=(
|
||||
"gaussian: symmetric weighted average, best general-purpose smoother./n"
|
||||
"savgol: sliding polynomial fit, preserves sharp peaks."
|
||||
@ -471,7 +466,7 @@ class SAM3DBody_Smooth(io.ComfyNode):
|
||||
tooltip="Temporal window in frames (odd values).",
|
||||
),
|
||||
io.Float.Input(
|
||||
"rotation_threshold_deg",
|
||||
"rotation_threshold_degrees",
|
||||
default=30.0, min=0.0, max=90.0, step=1.0, advanced=True,
|
||||
tooltip=(
|
||||
"Disables smoothing for this root rotation rate (degree/frame) to preserve fast spins. "
|
||||
@ -484,7 +479,7 @@ class SAM3DBody_Smooth(io.ComfyNode):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, mhr_pose_data, method, window, strength, rotation_threshold_deg) -> io.NodeOutput:
|
||||
def execute(cls, mhr_pose_data, method, window, strength, rotation_threshold_degrees) -> io.NodeOutput:
|
||||
if strength <= 0.0 or window <= 1:
|
||||
return io.NodeOutput(mhr_pose_data)
|
||||
|
||||
@ -514,7 +509,7 @@ class SAM3DBody_Smooth(io.ComfyNode):
|
||||
smoothed = [list(f) for f in frames]
|
||||
|
||||
base_blend = float(strength)
|
||||
rot_thresh = float(np.deg2rad(max(0.0, rotation_threshold_deg)))
|
||||
rot_thresh = float(np.deg2rad(max(0.0, rotation_threshold_degrees)))
|
||||
|
||||
for pid in range(max_p):
|
||||
valid = np.array([pid < len(f) for f in frames], dtype=bool)
|
||||
@ -861,13 +856,7 @@ class SAM3DBody_Render(io.ComfyNode):
|
||||
"camera_info", optional=True,
|
||||
tooltip=(
|
||||
"Free 6DOF camera override. When wired, the pose is re-projected through this camera "
|
||||
"(position/target/zoom) instead of the predicted one. "
|
||||
),
|
||||
),
|
||||
io.Float.Input(
|
||||
"fov", default=0.0, min=0.0, max=170.0, step=0.5, advanced=True,
|
||||
tooltip=(
|
||||
"Override the vertical FoV of the camera_info. Ignored when camera_info is empty. 0 = keep the FoV of the camera_info."
|
||||
"(position/target/zoom/rotation/FoV) instead of the predicted one. "
|
||||
),
|
||||
),
|
||||
io.DynamicCombo.Input(
|
||||
@ -893,7 +882,7 @@ class SAM3DBody_Render(io.ComfyNode):
|
||||
|
||||
|
||||
@classmethod
|
||||
def execute(cls, mhr_pose_data, background=None, width=0, height=0, camera_info=None, fov=0.0, render_style=None) -> io.NodeOutput:
|
||||
def execute(cls, mhr_pose_data, background=None, width=0, height=0, camera_info=None, render_style=None) -> io.NodeOutput:
|
||||
render_style = render_style or {"render_style": "mesh"}
|
||||
mode_key = render_style.get("render_style", "mesh")
|
||||
|
||||
@ -912,7 +901,7 @@ class SAM3DBody_Render(io.ComfyNode):
|
||||
px_scale = min(new_W / native_W, new_H / native_H)
|
||||
|
||||
if camera_info is not None:
|
||||
mhr_pose_data = apply_camera_override(mhr_pose_data, camera_info, H, W, fov_deg=float(fov))
|
||||
mhr_pose_data = apply_camera_override(mhr_pose_data, camera_info, H, W)
|
||||
|
||||
B = len(mhr_pose_data["frames"])
|
||||
if B == 0:
|
||||
|
||||
@ -342,6 +342,7 @@ class SaveGLB(IO.ComfyNode):
|
||||
IO.File3DGLTF,
|
||||
IO.File3DOBJ,
|
||||
IO.File3DFBX,
|
||||
IO.File3DBVH,
|
||||
IO.File3DSTL,
|
||||
IO.File3DUSDZ,
|
||||
IO.File3DPLY,
|
||||
@ -428,7 +429,7 @@ def rainbow_tilt_inputs():
|
||||
|
||||
|
||||
def camera_translation_input():
|
||||
"""Shared camera_translation combo (BuildPoseGLB + SavePoseBVH)."""
|
||||
"""Shared camera_translation combo (Create 3D Animation glb + bvh paths)."""
|
||||
return IO.Combo.Input(
|
||||
"camera_translation",
|
||||
options=["off", "centered", "absolute"],
|
||||
@ -442,15 +443,16 @@ def camera_translation_input():
|
||||
)
|
||||
|
||||
|
||||
class BuildPoseGLB(IO.ComfyNode):
|
||||
"""Convert pose_data to an in-memory animated GLB"""
|
||||
class BuildPoseFile(IO.ComfyNode):
|
||||
"""Build an animated GLB from pose data, or save it as a BVH mocap file."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="BuildPoseGLB",
|
||||
display_name="Build Pose GLB",
|
||||
description="Convert pose data to an animated GLB",
|
||||
node_id="BuildPoseFile",
|
||||
display_name="Create 3D Animation File",
|
||||
description="Build an animated GLB from pose data, or save a BVH mocap file.",
|
||||
search_aliases=["pose animation", "mocap", "glb", "bvh", "build pose file", "save pose file"],
|
||||
category="3d",
|
||||
inputs=[
|
||||
IO.MultiType.Input(
|
||||
@ -459,188 +461,208 @@ class BuildPoseGLB(IO.ComfyNode):
|
||||
),
|
||||
SAM3DBodyModel.Input("sam3d_body_model", optional=True),
|
||||
IO.DynamicCombo.Input(
|
||||
"mesh_style",
|
||||
"format",
|
||||
options=[
|
||||
IO.DynamicCombo.Option("body_mesh", [
|
||||
IO.DynamicCombo.Option("glb", [
|
||||
IO.DynamicCombo.Input(
|
||||
"bone_vis",
|
||||
"mesh_style",
|
||||
options=[
|
||||
IO.DynamicCombo.Option("off", []),
|
||||
IO.DynamicCombo.Option("octahedrons", [
|
||||
IO.Float.Input(
|
||||
"bone_vis_radius_m",
|
||||
default=0.02, min=0.005, max=0.5, step=0.005, advanced=True,
|
||||
tooltip="Radius in m (sphere radius / octahedron half-width).",
|
||||
IO.DynamicCombo.Option("body_mesh", [
|
||||
IO.DynamicCombo.Input(
|
||||
"bone_vis",
|
||||
options=[
|
||||
IO.DynamicCombo.Option("off", []),
|
||||
IO.DynamicCombo.Option("octahedrons", [
|
||||
IO.Float.Input(
|
||||
"bone_vis_radius_m",
|
||||
default=0.02, min=0.005, max=0.5, step=0.005, advanced=True,
|
||||
tooltip="Radius in m (sphere radius / octahedron half-width).",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"bone_vis_color",
|
||||
options=["white", "rainbow_y"],
|
||||
default="rainbow_y",
|
||||
tooltip=(
|
||||
"Per-bone vertex colors (unlit material). "
|
||||
"'white' = none, 'rainbow_y' = head→toe jet."
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
tooltip=("Bone vis shape, rigidly skinned to each joint. "),
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"bone_vis_color",
|
||||
options=["white", "rainbow_y"],
|
||||
default="rainbow_y",
|
||||
IO.DynamicCombo.Input(
|
||||
"shader",
|
||||
options=[
|
||||
IO.DynamicCombo.Option("default", []),
|
||||
IO.DynamicCombo.Option("rainbow", [
|
||||
*rainbow_tilt_inputs(),
|
||||
IO.Float.Input(
|
||||
"person_palette_falloff",
|
||||
default=0.6, min=0.1, max=1.0, step=0.05,
|
||||
tooltip="Per-person desaturation: each track gets (1 - falloff^k) pastel mix.",
|
||||
),
|
||||
]),
|
||||
IO.DynamicCombo.Option("rainbow_face_normal", [
|
||||
*rainbow_tilt_inputs(),
|
||||
IO.Float.Input(
|
||||
"person_palette_falloff",
|
||||
default=0.6, min=0.1, max=1.0, step=0.05,
|
||||
tooltip="Per-person desaturation: each track gets (1 - falloff^k) pastel mix.",
|
||||
),
|
||||
]),
|
||||
IO.DynamicCombo.Option("rainbow_face_semantic", [
|
||||
*rainbow_tilt_inputs(),
|
||||
IO.Float.Input(
|
||||
"person_palette_falloff",
|
||||
default=0.6, min=0.1, max=1.0, step=0.05,
|
||||
tooltip="Per-person desaturation: each track gets (1 - falloff^k) pastel mix.",
|
||||
),
|
||||
]),
|
||||
],
|
||||
tooltip=(
|
||||
"Per-bone vertex colors (unlit material). "
|
||||
"'white' = none, 'rainbow_y' = head→toe jet."
|
||||
"Bake per-vertex colors matching the Render node's shaders "
|
||||
"(COLOR_0 + KHR_materials_unlit). 'default' = no colors."
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
tooltip=("Bone vis shape, rigidly skinned to each joint. "),
|
||||
),
|
||||
IO.DynamicCombo.Input(
|
||||
"shader",
|
||||
options=[
|
||||
IO.DynamicCombo.Option("default", []),
|
||||
IO.DynamicCombo.Option("rainbow", [
|
||||
*rainbow_tilt_inputs(),
|
||||
IO.Float.Input(
|
||||
"person_palette_falloff",
|
||||
default=0.6, min=0.1, max=1.0, step=0.05,
|
||||
tooltip="Per-person desaturation: each track gets (1 - falloff^k) pastel mix.",
|
||||
IO.DynamicCombo.Option("bones_only", [
|
||||
IO.DynamicCombo.Input(
|
||||
"bone_vis",
|
||||
options=[
|
||||
IO.DynamicCombo.Option("octahedrons", [
|
||||
IO.Float.Input(
|
||||
"bone_vis_radius_m",
|
||||
default=0.02, min=0.005, max=0.5, step=0.005, advanced=True,
|
||||
tooltip="Radius in m (sphere radius / octahedron half-width).",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"bone_vis_color",
|
||||
options=["white", "rainbow_y"],
|
||||
default="rainbow_y",
|
||||
tooltip=(
|
||||
"Per-bone vertex colors (unlit material). "
|
||||
"'white' = none, 'rainbow_y' = head→toe jet."
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
tooltip=(
|
||||
"Bone vis shape, rigidly skinned to each joint. "
|
||||
"'octahedrons' = Blender-style directional bones (joint → "
|
||||
"primary child)."
|
||||
),
|
||||
),
|
||||
]),
|
||||
IO.DynamicCombo.Option("rainbow_face_normal", [
|
||||
*rainbow_tilt_inputs(),
|
||||
IO.DynamicCombo.Option("openpose", [
|
||||
IO.Float.Input(
|
||||
"person_palette_falloff",
|
||||
default=0.6, min=0.1, max=1.0, step=0.05,
|
||||
tooltip="Per-person desaturation: each track gets (1 - falloff^k) pastel mix.",
|
||||
"marker_radius_m", default=0.010, min=0.005, max=0.1, step=0.001, advanced=True,
|
||||
tooltip="Sphere radius in m.",
|
||||
),
|
||||
]),
|
||||
IO.DynamicCombo.Option("rainbow_face_semantic", [
|
||||
*rainbow_tilt_inputs(),
|
||||
IO.Float.Input(
|
||||
"person_palette_falloff",
|
||||
default=0.6, min=0.1, max=1.0, step=0.05,
|
||||
tooltip="Per-person desaturation: each track gets (1 - falloff^k) pastel mix.",
|
||||
"stick_radius_m", default=0.008, min=0.002, max=0.05, step=0.001, advanced=True,
|
||||
tooltip="Limb half-width in m. Auto-clamped to bone_length x 0.1.",
|
||||
),
|
||||
IO.Boolean.Input(
|
||||
"include_hands", default=False,
|
||||
tooltip=(
|
||||
"Append 21+21 OpenPose hands (wrist + 5 fingers x 4 joints, "
|
||||
"base→tip) sourced from pred_keypoints_3d."
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
tooltip=(
|
||||
"Bake per-vertex colors matching the Render node's shaders "
|
||||
"(COLOR_0 + KHR_materials_unlit). 'default' = no colors."
|
||||
),
|
||||
),
|
||||
]),
|
||||
IO.DynamicCombo.Option("bones_only", [
|
||||
IO.DynamicCombo.Input(
|
||||
"bone_vis",
|
||||
options=[
|
||||
IO.DynamicCombo.Option("octahedrons", [
|
||||
IO.Float.Input(
|
||||
"bone_vis_radius_m",
|
||||
default=0.02, min=0.005, max=0.5, step=0.005, advanced=True,
|
||||
tooltip="Radius in m (sphere radius / octahedron half-width).",
|
||||
"hand_marker_radius_m", default=0.005, min=0.001, max=0.1, step=0.001, advanced=True,
|
||||
tooltip="Hand sphere radius in m.",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"hand_stick_radius_m", default=0.003, min=0.001, max=0.05, step=0.001, advanced=True,
|
||||
tooltip="Hand limb half-width in m.",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"bone_vis_color",
|
||||
options=["white", "rainbow_y"],
|
||||
default="rainbow_y",
|
||||
"face_style",
|
||||
options=["disabled", "full", "eyes_mouth"],
|
||||
default="disabled",
|
||||
tooltip=(
|
||||
"Per-bone vertex colors (unlit material). "
|
||||
"'white' = none, 'rainbow_y' = head→toe jet."
|
||||
"Face-contour landmarks sampled from pred_vertices at fixed "
|
||||
"head-mesh vertex IDs (needs canonical_colors on pose_data). "
|
||||
"'full' = all ~30 points; 'eyes_mouth' = eyes + outer lips only."
|
||||
),
|
||||
),
|
||||
IO.Float.Input(
|
||||
"face_marker_radius_m", default=0.0, min=0.0, max=0.05, step=0.0005, advanced=True,
|
||||
tooltip="Face dot radius. 0 = auto = 0.3 x marker_radius_m.",
|
||||
),
|
||||
]),
|
||||
IO.DynamicCombo.Option("scail", [
|
||||
IO.Float.Input(
|
||||
"stick_radius_m", default=0.022, min=0.002, max=0.1, step=0.001, advanced=True,
|
||||
tooltip=(
|
||||
"Cylinder radius in m. Bones are open cylinders at constant "
|
||||
"radius; joint spheres (auto-sized to match) cap the open ends. "
|
||||
"SCAIL reference = 0.0215 m."
|
||||
),
|
||||
),
|
||||
IO.Float.Input(
|
||||
"marker_radius_m", default=0.0, min=0.0, max=0.1, step=0.001, advanced=True,
|
||||
tooltip="Joint sphere radius. 0 = auto = stick_radius_m (flush cap).",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"material_roughness", default=0.3, min=0.0, max=1.0, step=0.05, advanced=True,
|
||||
tooltip="PBR roughness. SCAIL ref = 0.3. 1 = matte; 0 = chrome.",
|
||||
),
|
||||
IO.Boolean.Input(
|
||||
"include_hands", default=False,
|
||||
tooltip="Append 21+21 hand keypoints + capsule sticks per track.",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"hand_marker_radius_m", default=0.005, min=0.001, max=0.05, step=0.001, advanced=True,
|
||||
tooltip="Hand sphere radius in m.",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"hand_stick_radius_m", default=0.003, min=0.001, max=0.05, step=0.001, advanced=True,
|
||||
tooltip="Hand cylinder radius in m.",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"face_style",
|
||||
options=["disabled", "full", "eyes_mouth"],
|
||||
default="disabled",
|
||||
tooltip=(
|
||||
"Face-contour landmarks sampled from pred_vertices (needs "
|
||||
"canonical_colors on pose_data). 'full' = all ~30 points; "
|
||||
"'eyes_mouth' = eyes + outer lips only."
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
tooltip=(
|
||||
"Bone vis shape, rigidly skinned to each joint. "
|
||||
"'octahedrons' = Blender-style directional bones (joint → "
|
||||
"primary child)."
|
||||
"'body_mesh' = real Armature (127 bones, skinning, TRS keyframes, 72 face morphs; needs model). "
|
||||
"'bones_only' = bone-shape primitives at each joint (preview armature). "
|
||||
"'openpose' = OpenPose-18 3D skeleton from keypoints "
|
||||
"'scail' = SCAIL 3D capsule rig (open cylinders capped flush by joint spheres)."
|
||||
),
|
||||
),
|
||||
IO.Int.Input(
|
||||
"bone_smooth_window",
|
||||
default=0, min=0, max=51, step=2,
|
||||
tooltip=(
|
||||
"Gaussian smoothing window on per-bone rotation keyframes / keypoint "
|
||||
"tracks. 0 = off. 7-15 calms spins/jitter where upstream Smooth misses spikes."
|
||||
),
|
||||
),
|
||||
]),
|
||||
IO.DynamicCombo.Option("openpose", [
|
||||
IO.Float.Input(
|
||||
"marker_radius_m", default=0.010, min=0.005, max=0.1, step=0.001, advanced=True,
|
||||
tooltip="Sphere radius in m.",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"stick_radius_m", default=0.008, min=0.002, max=0.05, step=0.001, advanced=True,
|
||||
tooltip="Limb half-width in m. Auto-clamped to bone_length x 0.1.",
|
||||
),
|
||||
IO.Boolean.Input(
|
||||
"include_hands", default=False,
|
||||
tooltip=(
|
||||
"Append 21+21 OpenPose hands (wrist + 5 fingers x 4 joints, "
|
||||
"base→tip) sourced from pred_keypoints_3d."
|
||||
),
|
||||
),
|
||||
IO.Float.Input(
|
||||
"hand_marker_radius_m", default=0.005, min=0.001, max=0.1, step=0.001, advanced=True,
|
||||
tooltip="Hand sphere radius in m.",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"hand_stick_radius_m", default=0.003, min=0.001, max=0.05, step=0.001, advanced=True,
|
||||
tooltip="Hand limb half-width in m.",
|
||||
),
|
||||
IO.DynamicCombo.Option("bvh", [
|
||||
IO.Combo.Input(
|
||||
"face_style",
|
||||
options=["disabled", "full", "eyes_mouth"],
|
||||
default="disabled",
|
||||
tooltip=(
|
||||
"Face-contour landmarks sampled from pred_vertices at fixed "
|
||||
"head-mesh vertex IDs (needs canonical_colors on pose_data). "
|
||||
"'full' = all ~30 points; 'eyes_mouth' = eyes + outer lips only."
|
||||
),
|
||||
),
|
||||
IO.Float.Input(
|
||||
"face_marker_radius_m", default=0.0, min=0.0, max=0.05, step=0.0005, advanced=True,
|
||||
tooltip="Face dot radius. 0 = auto = 0.3 x marker_radius_m.",
|
||||
),
|
||||
]),
|
||||
IO.DynamicCombo.Option("scail", [
|
||||
IO.Float.Input(
|
||||
"stick_radius_m", default=0.022, min=0.002, max=0.1, step=0.001, advanced=True,
|
||||
tooltip=(
|
||||
"Cylinder radius in m. Bones are open cylinders at constant "
|
||||
"radius; joint spheres (auto-sized to match) cap the open ends. "
|
||||
"SCAIL reference = 0.0215 m."
|
||||
),
|
||||
),
|
||||
IO.Float.Input(
|
||||
"marker_radius_m", default=0.0, min=0.0, max=0.1, step=0.001, advanced=True,
|
||||
tooltip="Joint sphere radius. 0 = auto = stick_radius_m (flush cap).",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"material_roughness", default=0.3, min=0.0, max=1.0, step=0.05, advanced=True,
|
||||
tooltip="PBR roughness. SCAIL ref = 0.3. 1 = matte; 0 = chrome.",
|
||||
),
|
||||
IO.Boolean.Input(
|
||||
"include_hands", default=False,
|
||||
tooltip="Append 21+21 hand keypoints + capsule sticks per track.",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"hand_marker_radius_m", default=0.005, min=0.001, max=0.05, step=0.001, advanced=True,
|
||||
tooltip="Hand sphere radius in m.",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"hand_stick_radius_m", default=0.003, min=0.001, max=0.05, step=0.001, advanced=True,
|
||||
tooltip="Hand cylinder radius in m.",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"face_style",
|
||||
options=["disabled", "full", "eyes_mouth"],
|
||||
default="disabled",
|
||||
tooltip=(
|
||||
"Face-contour landmarks sampled from pred_vertices (needs "
|
||||
"canonical_colors on pose_data). 'full' = all ~30 points; "
|
||||
"'eyes_mouth' = eyes + outer lips only."
|
||||
),
|
||||
"units",
|
||||
options=["cm", "m"],
|
||||
default="cm",
|
||||
tooltip="BVH OFFSET/position units. 'cm' is the mocap standard.",
|
||||
),
|
||||
]),
|
||||
],
|
||||
tooltip=(
|
||||
"'body_mesh' = real Armature (127 bones, skinning, TRS keyframes, 72 face morphs; needs model). "
|
||||
"'bones_only' = bone-shape primitives at each joint (preview armature). "
|
||||
"'openpose' = OpenPose-18 3D skeleton from keypoints "
|
||||
"'scail' = SCAIL 3D capsule rig (open cylinders capped flush by joint spheres)."
|
||||
),
|
||||
),
|
||||
IO.Int.Input(
|
||||
"bone_smooth_window",
|
||||
default=0, min=0, max=51, step=2,
|
||||
tooltip=(
|
||||
"Gaussian smoothing window on per-bone rotation keyframes / keypoint "
|
||||
"tracks. 0 = off. 7-15 calms spins/jitter where upstream Smooth misses spikes."
|
||||
"Output format, both fed to Save 3D Model to write to disk. "
|
||||
"'glb' = animated GLB (mesh / bones / openpose / scail). "
|
||||
"'bvh' = BVH mocap clip (one skeleton; needs the model)."
|
||||
),
|
||||
),
|
||||
IO.Float.Input(
|
||||
@ -653,12 +675,32 @@ class BuildPoseGLB(IO.ComfyNode):
|
||||
tooltip="-1 = all tracks; ≥0 = single track.",
|
||||
),
|
||||
],
|
||||
outputs=[IO.File3DGLB.Output("model_3d")],
|
||||
outputs=[IO.File3DAny.Output("model_3d")],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, pose_data, mesh_style, sam3d_body_model=None, bone_smooth_window=0, fps=24.0, camera_translation="off", track_index=-1) -> IO.NodeOutput:
|
||||
mesh_style = mesh_style or {"mesh_style": "body_mesh"}
|
||||
def execute(cls, pose_data, format, sam3d_body_model=None, fps=24.0, camera_translation="off", track_index=-1) -> IO.NodeOutput:
|
||||
format = format or {"format": "glb"}
|
||||
fmt = format.get("format", "glb")
|
||||
|
||||
if fmt == "bvh":
|
||||
if sam3d_body_model is None:
|
||||
raise ValueError("Create 3D Animation: 'bvh' format needs the `sam3d_body_model` input.")
|
||||
# BVH carries one skeleton; -1 (all tracks) collapses to the first.
|
||||
ti = int(track_index)
|
||||
if ti < 0:
|
||||
ti = 0
|
||||
bvh_bytes = build_bvh(
|
||||
pose_data, sam3d_body_model,
|
||||
fps=float(fps),
|
||||
camera_translation=str(camera_translation),
|
||||
track_index=ti,
|
||||
units=str(format.get("units", "cm")),
|
||||
)
|
||||
return IO.NodeOutput(Types.File3D(BytesIO(bvh_bytes), file_format="bvh"))
|
||||
|
||||
mesh_style = format.get("mesh_style") or {"mesh_style": "body_mesh"}
|
||||
bone_smooth_window = int(format.get("bone_smooth_window", 0))
|
||||
mode_key = mesh_style["mesh_style"]
|
||||
# `shader` is nested in body_mesh; absent for bones_only.
|
||||
shader_dict = mesh_style.get("shader") or {}
|
||||
@ -678,7 +720,7 @@ class BuildPoseGLB(IO.ComfyNode):
|
||||
has_external_rig = isinstance(pose_data, dict) and ("_skeleton_override" in pose_data)
|
||||
if sam3d_body_model is None and not has_external_rig:
|
||||
raise ValueError(
|
||||
f"BuildPoseGLB: '{mode_key}' mode needs the `sam3d_body_model` input OR a "
|
||||
f"BuildPoseFile: '{mode_key}' mode needs the `sam3d_body_model` input OR a "
|
||||
"`_skeleton_override` dict in pose_data. Connect the SAM3DBody model "
|
||||
"or feed pose_data from a node that supplies the override (e.g. KimodoSample)."
|
||||
)
|
||||
@ -748,78 +790,10 @@ class BuildPoseGLB(IO.ComfyNode):
|
||||
return IO.NodeOutput(Types.File3D(BytesIO(glb_bytes), file_format="glb"))
|
||||
|
||||
|
||||
class SavePoseBVH(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="SavePoseBVH",
|
||||
description="Save pose data as BVH mocap file",
|
||||
display_name="Save Pose BVH",
|
||||
category="3d",
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
IO.MultiType.Input(
|
||||
"pose_data", types=[MHRPoseData, KimodoPoseData],
|
||||
tooltip=(
|
||||
"MHR pose data from SAM3DBody_Predict, or external-rig "
|
||||
"pose data from Kimodo."
|
||||
),
|
||||
),
|
||||
SAM3DBodyModel.Input("sam3d_body_model"),
|
||||
IO.String.Input("filename_prefix", default="3d/ComfyUI"),
|
||||
IO.Float.Input(
|
||||
"fps", default=24.0, min=1.0, max=240.0, step=1.0,
|
||||
tooltip="Animation frame rate (BVH `Frame Time`).",
|
||||
),
|
||||
camera_translation_input(),
|
||||
IO.Combo.Input(
|
||||
"units",
|
||||
options=["cm", "m"],
|
||||
default="cm",
|
||||
tooltip="BVH OFFSET/position units. 'cm' is the mocap standard.",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"track_index", default=0, min=0, max=15,
|
||||
tooltip="Track to export. BVH carries one skeleton; export multi-person clips one at a time.",
|
||||
),
|
||||
],
|
||||
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
|
||||
outputs=[],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, pose_data, sam3d_body_model, filename_prefix="3d/ComfyUI",
|
||||
fps=24.0, camera_translation="off", units="cm",
|
||||
track_index=0) -> IO.NodeOutput:
|
||||
bvh_bytes = build_bvh(
|
||||
pose_data, sam3d_body_model,
|
||||
fps=float(fps),
|
||||
camera_translation=str(camera_translation),
|
||||
track_index=int(track_index),
|
||||
units=str(units),
|
||||
)
|
||||
|
||||
full_output_folder, filename, counter, subfolder, _ = \
|
||||
folder_paths.get_save_image_path(
|
||||
filename_prefix, folder_paths.get_output_directory(),
|
||||
)
|
||||
f = f"{filename}_{counter:05}_.bvh"
|
||||
out_path = os.path.join(full_output_folder, f)
|
||||
with open(out_path, "wb") as fh:
|
||||
fh.write(bvh_bytes)
|
||||
|
||||
return IO.NodeOutput(ui={"3d": [{
|
||||
"filename": f,
|
||||
"subfolder": subfolder,
|
||||
"type": "output",
|
||||
}]})
|
||||
|
||||
|
||||
class Save3DExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||
return [SaveGLB, BuildPoseGLB, SavePoseBVH]
|
||||
return [SaveGLB, BuildPoseFile]
|
||||
|
||||
|
||||
async def comfy_entrypoint() -> Save3DExtension:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user