mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-19 12:58:15 +08:00
Resolve review comments and suggestions
This commit is contained in:
parent
4cf6a777f6
commit
1f23bcb27b
@ -10,6 +10,7 @@ import math
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import struct
|
import struct
|
||||||
import torch
|
import torch
|
||||||
|
import logging
|
||||||
|
|
||||||
import zlib
|
import zlib
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
@ -89,6 +90,149 @@ class ImageCropV2(IO.ComfyNode):
|
|||||||
return IO.NodeOutput(img, ui=UI.PreviewImage(img))
|
return IO.NodeOutput(img, ui=UI.PreviewImage(img))
|
||||||
|
|
||||||
|
|
||||||
|
def _crop_image_with_mask(item_image, item_mask, max_image_size=1024, pad_factor=1.1,
|
||||||
|
mask_offset=0, mask_threshold=0.05, bg_rgb=(0.0, 0.0, 0.0),
|
||||||
|
aspect_ratio=1.0):
|
||||||
|
img = item_image.permute(2, 0, 1).unsqueeze(0).cpu().float().clamp(0, 1)
|
||||||
|
mask = item_mask.unsqueeze(0).unsqueeze(0).cpu().float().clamp(0, 1)
|
||||||
|
|
||||||
|
# Detect and correct an inverted mask, only when border and center have opposite polarity.
|
||||||
|
m2d = mask[0, 0]
|
||||||
|
h, w = m2d.shape
|
||||||
|
border = torch.cat([m2d[0, :], m2d[-1, :], m2d[:, 0], m2d[:, -1]])
|
||||||
|
center = m2d[h // 4:h - h // 4, w // 4:w - w // 4]
|
||||||
|
if float(border.mean()) > 0.5 and float(center.mean()) < 0.5:
|
||||||
|
mask = 1.0 - mask
|
||||||
|
|
||||||
|
if mask_offset > 0:
|
||||||
|
r = mask_offset
|
||||||
|
mask = torch.nn.functional.max_pool2d(mask, kernel_size=2 * r + 1, stride=1, padding=r)
|
||||||
|
elif mask_offset < 0:
|
||||||
|
r = -mask_offset
|
||||||
|
mask = 1.0 - torch.nn.functional.max_pool2d(1.0 - mask, kernel_size=2 * r + 1, stride=1, padding=r)
|
||||||
|
|
||||||
|
if mask_threshold > 0.0:
|
||||||
|
mask = torch.where(mask < mask_threshold, torch.zeros_like(mask), mask)
|
||||||
|
|
||||||
|
H, W = img.shape[-2:]
|
||||||
|
if max(H, W) > max_image_size:
|
||||||
|
scale = max_image_size / max(H, W)
|
||||||
|
new_w, new_h = int(W * scale), int(H * scale)
|
||||||
|
img = comfy.utils.common_upscale(img, new_w, new_h, "lanczos", "disabled")
|
||||||
|
mask = comfy.utils.common_upscale(mask, new_w, new_h, "lanczos", "disabled")
|
||||||
|
# common_upscale's lanczos path drops the singleton channel dim for masks (utils.py:1062).
|
||||||
|
if mask.ndim == 3:
|
||||||
|
mask = mask.unsqueeze(1)
|
||||||
|
H, W = new_h, new_w
|
||||||
|
scene_size = (W, H)
|
||||||
|
|
||||||
|
alpha_u8 = (mask[0, 0].clamp(0, 1) * 255.0).to(torch.uint8)
|
||||||
|
fg_pixels = (alpha_u8 > 204).nonzero()
|
||||||
|
if fg_pixels.numel() == 0:
|
||||||
|
# Try the inverted mask — auto-invert above may have been too conservative.
|
||||||
|
inv_fg = ((255 - alpha_u8) > 204).nonzero()
|
||||||
|
if inv_fg.numel() > 0:
|
||||||
|
logging.info("Trellis2 preprocess: mask bbox empty, using inverted mask.")
|
||||||
|
mask = 1.0 - mask
|
||||||
|
fg_pixels = inv_fg
|
||||||
|
if fg_pixels.numel() > 0:
|
||||||
|
y_min, x_min = fg_pixels.min(dim=0).values.tolist()
|
||||||
|
y_max, x_max = fg_pixels.max(dim=0).values.tolist()
|
||||||
|
center_y, center_x = (y_min + y_max) / 2.0, (x_min + x_max) / 2.0
|
||||||
|
bw = x_max - x_min
|
||||||
|
bh = y_max - y_min
|
||||||
|
# Grow the bbox so its aspect matches `aspect_ratio` (width/height),
|
||||||
|
# anchored on the max side. Then apply pad_factor.
|
||||||
|
if bw / max(bh, 1) >= aspect_ratio:
|
||||||
|
crop_w = int(bw * pad_factor)
|
||||||
|
crop_h = int(bw / aspect_ratio * pad_factor)
|
||||||
|
else:
|
||||||
|
crop_h = int(bh * pad_factor)
|
||||||
|
crop_w = int(bh * aspect_ratio * pad_factor)
|
||||||
|
half_w, half_h = crop_w // 2, crop_h // 2
|
||||||
|
crop_x1 = int(center_x - half_w)
|
||||||
|
crop_y1 = int(center_y - half_h)
|
||||||
|
crop_x2 = crop_x1 + 2 * half_w
|
||||||
|
crop_y2 = crop_y1 + 2 * half_h
|
||||||
|
else:
|
||||||
|
logging.warning("Mask for the image is empty; a clean foreground mask is required for best quality.")
|
||||||
|
crop_x1, crop_y1, crop_x2, crop_y2 = 0, 0, W, H
|
||||||
|
crop_bbox = (crop_x1, crop_y1, crop_x2, crop_y2)
|
||||||
|
|
||||||
|
# Zero-pad out-of-bounds slice (PIL.crop semantics).
|
||||||
|
pad_l = max(0, -crop_x1)
|
||||||
|
pad_t = max(0, -crop_y1)
|
||||||
|
pad_r = max(0, crop_x2 - W)
|
||||||
|
pad_b = max(0, crop_y2 - H)
|
||||||
|
if pad_l or pad_t or pad_r or pad_b:
|
||||||
|
img = torch.nn.functional.pad(img, (pad_l, pad_r, pad_t, pad_b), value=0.0)
|
||||||
|
mask = torch.nn.functional.pad(mask, (pad_l, pad_r, pad_t, pad_b), value=0.0)
|
||||||
|
crop_x1 += pad_l
|
||||||
|
crop_x2 += pad_l
|
||||||
|
crop_y1 += pad_t
|
||||||
|
crop_y2 += pad_t
|
||||||
|
cropped_img = img [..., crop_y1:crop_y2, crop_x1:crop_x2]
|
||||||
|
cropped_mask = mask[..., crop_y1:crop_y2, crop_x1:crop_x2]
|
||||||
|
|
||||||
|
bg = torch.tensor(bg_rgb, dtype=cropped_img.dtype, device=cropped_img.device).view(1, 3, 1, 1)
|
||||||
|
composite = (cropped_img * cropped_mask + bg * (1.0 - cropped_mask)).clamp(0, 1)
|
||||||
|
return composite, crop_bbox, scene_size
|
||||||
|
|
||||||
|
|
||||||
|
class ImageCropToMask(IO.ComfyNode):
|
||||||
|
"""Crop an image to its mask's bounding box (centered square, with pad_factor
|
||||||
|
margin), then composite `img * mask` and resize to a square. Handles OOB crops
|
||||||
|
with zero-padding. Useful for 3D pipelines that expect a centered, background-free
|
||||||
|
subject at a fixed input resolution (Trellis2, Pixal3D, Hunyuan3D, TripoSR, etc.)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ImageCropToMask",
|
||||||
|
display_name="Crop Image to Mask",
|
||||||
|
category="image/transform",
|
||||||
|
search_aliases=["crop to mask", "mask crop", "crop mask", "mask crop resize", "crop mask resize", "trellis2", "pixal3d"],
|
||||||
|
inputs=[
|
||||||
|
IO.Image.Input("images"),
|
||||||
|
IO.Mask.Input("masks"),
|
||||||
|
IO.Int.Input("width", default=1024, min=64, max=4096, step=8, tooltip="Output width in pixels."),
|
||||||
|
IO.Int.Input("height", default=1024, min=64, max=4096, step=8, tooltip="Output height in pixels."),
|
||||||
|
IO.Float.Input("pad_factor", default=1.0, min=1.0, max=2.0, step=0.01, tooltip="Extra margin around the mask bounding box as a multiplier."),
|
||||||
|
IO.Int.Input("grow_mask", default=0, min=-32, max=32, step=1, tooltip="Grow or shrink the mask by this many pixels before cropping."),
|
||||||
|
IO.Color.Input("background", default="#000000", tooltip="Background color behind the masked subject."),
|
||||||
|
],
|
||||||
|
outputs=[IO.Image.Output(display_name="images")],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, images, masks, width, height, pad_factor, grow_mask, background) -> IO.NodeOutput:
|
||||||
|
h = background.lstrip("#")
|
||||||
|
bg_rgb = (int(h[0:2], 16) / 255.0, int(h[2:4], 16) / 255.0, int(h[4:6], 16) / 255.0) if len(h) == 6 else (0.0, 0.0, 0.0)
|
||||||
|
images = images[..., :3]
|
||||||
|
batch_size = images.shape[0]
|
||||||
|
if masks.shape[0] == 1 and batch_size > 1:
|
||||||
|
masks = masks.expand(batch_size, -1, -1)
|
||||||
|
elif masks.shape[0] != batch_size:
|
||||||
|
raise ValueError(f"Mask batch {masks.shape[0]} does not match image batch {batch_size}")
|
||||||
|
if masks.shape[-2:] != images.shape[1:3]:
|
||||||
|
masks = comfy.utils.common_upscale(masks.unsqueeze(1).float(), images.shape[2], images.shape[1], "bilinear", "disabled").squeeze(1)
|
||||||
|
|
||||||
|
out_images = []
|
||||||
|
for b in range(batch_size):
|
||||||
|
composite, _, _ = _crop_image_with_mask(
|
||||||
|
images[b], masks[b], max_image_size=max(width, height), pad_factor=pad_factor,
|
||||||
|
mask_offset=grow_mask, bg_rgb=bg_rgb, aspect_ratio=width / height,
|
||||||
|
)
|
||||||
|
composite = comfy.utils.common_upscale(composite, width, height, "lanczos", "disabled")
|
||||||
|
out_images.append(composite.movedim(-3, -1))
|
||||||
|
|
||||||
|
result = torch.cat(out_images, dim=0).to(
|
||||||
|
device=comfy.model_management.intermediate_device(),
|
||||||
|
dtype=comfy.model_management.intermediate_dtype(),
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(result)
|
||||||
|
|
||||||
|
|
||||||
class BoundingBox(IO.ComfyNode):
|
class BoundingBox(IO.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
@ -1233,6 +1377,7 @@ class ImagesExtension(ComfyExtension):
|
|||||||
return [
|
return [
|
||||||
ImageCrop,
|
ImageCrop,
|
||||||
ImageCropV2,
|
ImageCropV2,
|
||||||
|
ImageCropToMask,
|
||||||
BoundingBox,
|
BoundingBox,
|
||||||
RepeatImageBatch,
|
RepeatImageBatch,
|
||||||
ImageFromBatch,
|
ImageFromBatch,
|
||||||
|
|||||||
@ -1329,7 +1329,7 @@ class BakeTextureFromVoxel(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="BakeTextureFromVoxel",
|
node_id="BakeTextureFromVoxel",
|
||||||
display_name="Bake Texture From Voxel",
|
display_name="Bake Texture From Voxel",
|
||||||
category="3d/mesh/texturing",
|
category="3d/texturing",
|
||||||
description=(
|
description=(
|
||||||
"Bakes PBR textures onto the mesh's existing UV layout (trilinear-sample the "
|
"Bakes PBR textures onto the mesh's existing UV layout (trilinear-sample the "
|
||||||
"sparse voxel volume). Does NOT unwrap — connect a UV unwrap node upstream. Outputs "
|
"sparse voxel volume). Does NOT unwrap — connect a UV unwrap node upstream. Outputs "
|
||||||
@ -1339,7 +1339,7 @@ class BakeTextureFromVoxel(IO.ComfyNode):
|
|||||||
inputs=[
|
inputs=[
|
||||||
IO.Mesh.Input("mesh"),
|
IO.Mesh.Input("mesh"),
|
||||||
IO.Voxel.Input("voxel_colors"),
|
IO.Voxel.Input("voxel_colors"),
|
||||||
IO.Int.Input("texture_size", default=2048, min=64, max=8192,
|
IO.Int.Input("texture_size", display_name="resolution", default=2048, min=64, max=8192,
|
||||||
tooltip="Square UV atlas resolution."),
|
tooltip="Square UV atlas resolution."),
|
||||||
IO.Mesh.Input("reference_mesh", optional=True,
|
IO.Mesh.Input("reference_mesh", optional=True,
|
||||||
tooltip=(
|
tooltip=(
|
||||||
@ -1432,7 +1432,7 @@ class MeshTextureToImage(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="MeshTextureToImage",
|
node_id="MeshTextureToImage",
|
||||||
display_name="Mesh Texture to Image",
|
display_name="Mesh Texture to Image",
|
||||||
category="3d/mesh/texturing",
|
category="3d/texturing",
|
||||||
description=(
|
description=(
|
||||||
"Extracts a mesh's baked textures as individual IMAGEs: base_color, metallic, "
|
"Extracts a mesh's baked textures as individual IMAGEs: base_color, metallic, "
|
||||||
"roughness, occlusion and normal_map. Channels with nothing baked come back "
|
"roughness, occlusion and normal_map. Channels with nothing baked come back "
|
||||||
@ -1490,7 +1490,7 @@ class ApplyTextureToMesh(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ApplyTextureToMesh",
|
node_id="ApplyTextureToMesh",
|
||||||
display_name="Apply Texture to Mesh",
|
display_name="Apply Texture to Mesh",
|
||||||
category="3d/mesh/texturing",
|
category="3d/texturing",
|
||||||
description=(
|
description=(
|
||||||
"Attaches baked texture IMAGEs to a mesh's UV layout for SaveGLB. Feed the SAME mesh you baked"
|
"Attaches baked texture IMAGEs to a mesh's UV layout for SaveGLB. Feed the SAME mesh you baked"
|
||||||
),
|
),
|
||||||
@ -1583,12 +1583,12 @@ class BakeNormalMapFromMesh(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="BakeNormalMapFromMesh",
|
node_id="BakeNormalMapFromMesh",
|
||||||
display_name="Bake Normal Map from Mesh",
|
display_name="Bake Normal Map from Mesh",
|
||||||
category="3d/mesh/texturing",
|
category="3d/texturing",
|
||||||
description=(
|
description=(
|
||||||
"Bakes a tangent-space normal map (glTF/OpenGL +Y) from a high-poly mesh into a "
|
"Bakes a tangent-space normal map (glTF/OpenGL +Y) from a high-poly mesh into a "
|
||||||
"low-poly's UV layout, capturing detail lost to decimation. Feed the UV-unwrapped "
|
"low-poly's UV layout, capturing detail lost during decimation. Feed the UV-unwrapped "
|
||||||
"low_poly and the same-frame high_poly it was decimated from. Outputs an IMAGE for "
|
"low_poly and the high_poly it was decimated from. Outputs an image for "
|
||||||
"ApplyTextureToMesh's normal_map input."
|
"Apply Texture To Mesh's normal_map input."
|
||||||
),
|
),
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mesh.Input("low_poly"),
|
IO.Mesh.Input("low_poly"),
|
||||||
@ -1596,7 +1596,7 @@ class BakeNormalMapFromMesh(IO.ComfyNode):
|
|||||||
IO.Int.Input("resolution", default=1024, min=64, max=8192, step=64,
|
IO.Int.Input("resolution", default=1024, min=64, max=8192, step=64,
|
||||||
display_name="resolution"),
|
display_name="resolution"),
|
||||||
IO.Float.Input("cage_distance", default=0.05, min=0.001, max=0.5, step=0.001,
|
IO.Float.Input("cage_distance", default=0.05, min=0.001, max=0.5, step=0.001,
|
||||||
tooltip="Surface search band, as a fraction of the bbox diagonal. "
|
tooltip="Surface search band, as a fraction of the bounding box diagonal. "
|
||||||
"Raise for wrong/missing patches under heavy decimation; "
|
"Raise for wrong/missing patches under heavy decimation; "
|
||||||
"lower if it grabs across gaps."),
|
"lower if it grabs across gaps."),
|
||||||
IO.Boolean.Input("ignore_backfaces", default=True,
|
IO.Boolean.Input("ignore_backfaces", default=True,
|
||||||
@ -1664,12 +1664,12 @@ class BakeAmbientOcclusion(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="BakeAmbientOcclusion",
|
node_id="BakeAmbientOcclusion",
|
||||||
display_name="Bake Ambient Occlusion",
|
display_name="Bake Ambient Occlusion",
|
||||||
category="3d/mesh/texturing",
|
category="3d/texturing",
|
||||||
description=(
|
description=(
|
||||||
"Bakes an ambient-occlusion map from a high-poly mesh into a low-poly's UV "
|
"Bakes an ambient-occlusion map from a high-poly mesh into a low-poly's UV "
|
||||||
"layout (white = open, dark = crevices). Feed the UV-unwrapped low_poly and the "
|
"layout (white = open, dark = crevices). Feed the UV-unwrapped low_poly and the "
|
||||||
"high_poly it was decimated from. Outputs a grayscale IMAGE for "
|
"high_poly it was decimated from. Outputs a grayscale image for "
|
||||||
"ApplyTextureToMesh's occlusion input (packed into the ORM map / occlusionTexture)."
|
"Apply Texture To Mesh's occlusion input (packed into the ORM map / occlusionTexture)."
|
||||||
),
|
),
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mesh.Input("low_poly"),
|
IO.Mesh.Input("low_poly"),
|
||||||
@ -1678,12 +1678,12 @@ class BakeAmbientOcclusion(IO.ComfyNode):
|
|||||||
IO.Int.Input("samples", default=64, min=4, max=1024, step=4,
|
IO.Int.Input("samples", default=64, min=4, max=1024, step=4,
|
||||||
tooltip="Rays per texel. More = smoother, slower. Raise if grainy."),
|
tooltip="Rays per texel. More = smoother, slower. Raise if grainy."),
|
||||||
IO.Float.Input("max_distance", default=0.5, min=0.01, max=2.0, step=0.01,
|
IO.Float.Input("max_distance", default=0.5, min=0.01, max=2.0, step=0.01,
|
||||||
tooltip="Ray length, as a fraction of the bbox diagonal. "
|
tooltip="Ray length, as a fraction of the bounding box diagonal. "
|
||||||
"Smaller = tighter, more local occlusion."),
|
"Smaller = tighter, more local occlusion."),
|
||||||
IO.Float.Input("strength", default=1.0, min=0.0, max=2.0, step=0.05,
|
IO.Float.Input("strength", default=1.0, min=0.0, max=2.0, step=0.05,
|
||||||
tooltip="Scales the occlusion. >1 darkens, <1 lightens."),
|
tooltip="Scales the occlusion. >1 darkens, <1 lightens."),
|
||||||
IO.Float.Input("bias", default=0.01, min=0.0001, max=0.2, step=0.0005,
|
IO.Float.Input("bias", default=0.01, min=0.0001, max=0.2, step=0.0005,
|
||||||
tooltip="Ray origin lift off the surface, as a fraction of the bbox "
|
tooltip="Ray origin lift off the surface, as a fraction of the bounding box "
|
||||||
"diagonal. Raise if even surfaces show dark blotches/holes."),
|
"diagonal. Raise if even surfaces show dark blotches/holes."),
|
||||||
],
|
],
|
||||||
outputs=[IO.Image.Output(display_name="occlusion")],
|
outputs=[IO.Image.Output(display_name="occlusion")],
|
||||||
@ -2202,7 +2202,7 @@ class DecimateMesh(IO.ComfyNode):
|
|||||||
IO.Float.Input("feature_edge_quadric_weight", default=0.0, min=0.0, max=1000.0, step=1.0,
|
IO.Float.Input("feature_edge_quadric_weight", default=0.0, min=0.0, max=1000.0, step=1.0,
|
||||||
tooltip="Extra quadric weight on dihedral feature edges (creases). 0 = off."),
|
tooltip="Extra quadric weight on dihedral feature edges (creases). 0 = off."),
|
||||||
IO.Float.Input("feature_edge_min_dihedral_deg", default=30.0, min=0.0, max=180.0, step=1.0,
|
IO.Float.Input("feature_edge_min_dihedral_deg", default=30.0, min=0.0, max=180.0, step=1.0,
|
||||||
tooltip="Min dihedral angle (deg) to count an edge as a feature edge."),
|
tooltip="Minimum dihedral angle (degree) to count an edge as a feature edge."),
|
||||||
IO.Boolean.Input("clamp_v_to_edge", default=True,
|
IO.Boolean.Input("clamp_v_to_edge", default=True,
|
||||||
tooltip="Project the QEM-optimal position onto the collapsed edge segment."),
|
tooltip="Project the QEM-optimal position onto the collapsed edge segment."),
|
||||||
]),
|
]),
|
||||||
@ -2214,7 +2214,7 @@ class DecimateMesh(IO.ComfyNode):
|
|||||||
description=(
|
description=(
|
||||||
"Simplifies a mesh to a target face count using QEM, on the active compute "
|
"Simplifies a mesh to a target face count using QEM, on the active compute "
|
||||||
"device. 'midpoint' is the cumesh-faithful preset (best quality, preserves thin "
|
"device. 'midpoint' is the cumesh-faithful preset (best quality, preserves thin "
|
||||||
"features / hair); 'qem' places verts at the QEM optimum with line/feature-edge "
|
"features / hair); 'qem' places vertices at the QEM optimum with line/feature-edge "
|
||||||
"controls. Output stays welded."
|
"controls. Output stays welded."
|
||||||
),
|
),
|
||||||
inputs=[
|
inputs=[
|
||||||
@ -2285,7 +2285,7 @@ class RemeshMesh(IO.ComfyNode):
|
|||||||
sign_mode_options = [
|
sign_mode_options = [
|
||||||
IO.DynamicCombo.Option(key="udf", inputs=[
|
IO.DynamicCombo.Option(key="udf", inputs=[
|
||||||
IO.Boolean.Input("qef", default=False, advanced=True,
|
IO.Boolean.Input("qef", default=False, advanced=True,
|
||||||
tooltip="QEF dual-vertex placement for sharper edges."),
|
tooltip="QEF (Quadratic Error Function) dual-vertex placement for sharper edges."),
|
||||||
IO.Boolean.Input("drop_inverted_components", default=False, advanced=True,
|
IO.Boolean.Input("drop_inverted_components", default=False, advanced=True,
|
||||||
tooltip="Drop inward-normal (negative-volume) closed components — the UDF inner shell."),
|
tooltip="Drop inward-normal (negative-volume) closed components — the UDF inner shell."),
|
||||||
IO.Boolean.Input("drop_enclosed_components", default=False, advanced=True,
|
IO.Boolean.Input("drop_enclosed_components", default=False, advanced=True,
|
||||||
@ -2293,7 +2293,7 @@ class RemeshMesh(IO.ComfyNode):
|
|||||||
]),
|
]),
|
||||||
IO.DynamicCombo.Option(key="sdf", inputs=[
|
IO.DynamicCombo.Option(key="sdf", inputs=[
|
||||||
IO.Boolean.Input("qef", default=True,
|
IO.Boolean.Input("qef", default=True,
|
||||||
tooltip="QEF dual-vertex placement (recovers sharp features) vs edge-crossing centroid."),
|
tooltip="QEF (Quadratic Error Function) dual-vertex placement (recovers sharp features) vs edge-crossing centroid."),
|
||||||
IO.Boolean.Input("manifold", default=False,
|
IO.Boolean.Input("manifold", default=False,
|
||||||
tooltip="Manifold Dual Contouring: 1-4 dual verts/voxel for multi-sheet cases. Slower."),
|
tooltip="Manifold Dual Contouring: 1-4 dual verts/voxel for multi-sheet cases. Slower."),
|
||||||
]),
|
]),
|
||||||
@ -2305,25 +2305,25 @@ class RemeshMesh(IO.ComfyNode):
|
|||||||
description=(
|
description=(
|
||||||
"Re-extracts a uniformly tessellated mesh via a narrow-band distance field + Dual "
|
"Re-extracts a uniformly tessellated mesh via a narrow-band distance field + Dual "
|
||||||
"Contouring, on the active compute device. Normalizes messy / non-manifold / "
|
"Contouring, on the active compute device. Normalizes messy / non-manifold / "
|
||||||
"self-intersecting topology; run before DecimateMesh to hit an exact face count. "
|
"self-intersecting topology; run before Decimate Mesh to hit an exact face count. "
|
||||||
"Output stays welded."
|
"Output stays welded."
|
||||||
),
|
),
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mesh.Input("mesh"),
|
IO.Mesh.Input("mesh"),
|
||||||
IO.Int.Input("resolution", default=512, min=32, max=1024,
|
IO.Int.Input("resolution", default=512, min=32, max=1024,
|
||||||
tooltip="Voxel grid resolution (output density). 256 ~ 100k faces, 512 ~ 1M. "
|
tooltip="Voxel grid resolution (output density). 256 ~ 100k faces, 512 ~ 1M. "
|
||||||
"For an exact face count, follow with DecimateMesh."),
|
"For an exact face count, follow with Decimate Mesh."),
|
||||||
IO.DynamicCombo.Input("sign_mode", options=sign_mode_options, display_name="sign_mode",
|
IO.DynamicCombo.Input("sign_mode", options=sign_mode_options, display_name="sign_mode",
|
||||||
tooltip="udf: robust to messy/non-manifold input. sdf: clean single "
|
tooltip="udf: robust to messy/non-manifold input. sdf: clean single "
|
||||||
"surface with QEF sharp-feature recovery, but needs consistent winding."),
|
"surface with QEF (Quadratic Error Function) sharp-feature recovery, but needs consistent winding."),
|
||||||
IO.Float.Input("band", default=1.0, min=0.5, max=4.0, step=0.1, advanced=True,
|
IO.Float.Input("band", default=1.0, min=0.5, max=4.0, step=0.1, advanced=True,
|
||||||
tooltip="Narrow-band width in voxel units. In UDF mode also offsets the surface."),
|
tooltip="Narrow-band width in voxel units. In UDF mode also offsets the surface."),
|
||||||
IO.Float.Input("project_back", default=0.0, min=0.0, max=1.0, step=0.05, advanced=True,
|
IO.Float.Input("project_back", default=0.0, min=0.0, max=1.0, step=0.05, advanced=True,
|
||||||
tooltip="Lerp verts toward the original surface (0 = pure DC, 1 = snapped)."),
|
tooltip="Linearly interpolate vertices toward the original surface (0 = pure DC, 1 = snapped)."),
|
||||||
IO.Boolean.Input("fix_poles", default=False, advanced=True,
|
IO.Boolean.Input("fix_poles", default=False, advanced=True,
|
||||||
tooltip="Collapse valence-3 vertex pairs (DC T-junction artifact)."),
|
tooltip="Collapse valence-3 vertex pairs (DC T-junction artifact)."),
|
||||||
IO.Int.Input("smooth_iters", default=0, min=0, max=20,
|
IO.Int.Input("smooth_iters", default=0, min=0, max=20,
|
||||||
tooltip="Taubin smoothing iters (0 = off). 2-3 cleans DC stairstepping; higher rounds off QEF edges."),
|
tooltip="Taubin smoothing iterations (0 = off). 2-3 cleans DC staircase-like artifacts; higher over-smooth QEF edges."),
|
||||||
IO.Float.Input("drop_small_components", default=0.01, min=0.0, max=0.5, step=0.005, advanced=True,
|
IO.Float.Input("drop_small_components", default=0.01, min=0.0, max=0.5, step=0.005, advanced=True,
|
||||||
tooltip="Drop components below this fraction of the largest's face count. 0 disables."),
|
tooltip="Drop components below this fraction of the largest's face count. 0 disables."),
|
||||||
IO.Int.Input("precluster_max_verts", default=8_000_000, min=0, max=50_000_000, advanced=True,
|
IO.Int.Input("precluster_max_verts", default=8_000_000, min=0, max=50_000_000, advanced=True,
|
||||||
@ -2631,11 +2631,11 @@ class UnwrapMesh(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="UnwrapMesh",
|
node_id="UnwrapMesh",
|
||||||
display_name="Unwrap Mesh UVs",
|
display_name="Unwrap Mesh UVs",
|
||||||
category="3d/mesh/texturing",
|
category="3d/texturing",
|
||||||
description=(
|
description=(
|
||||||
"Generates a UV atlas (pure-torch, no xatlas): segments the surface into charts, "
|
"Generates a UV atlas: segments the surface into charts, parameterizes each, packs into a [0,1] atlas. "
|
||||||
"parameterizes each, packs into a [0,1] atlas. Seam verts duplicated. Run after "
|
"Vertices on chart seams are duplicated (one copy per chart, same position, own UV), so the output mesh "
|
||||||
"DecimateMesh/RemeshMesh, before texture baking."
|
"has more vertices than the input. Run after Remesh/Decimate, before texture baking."
|
||||||
),
|
),
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mesh.Input("mesh"),
|
IO.Mesh.Input("mesh"),
|
||||||
@ -2865,7 +2865,7 @@ class RenderUVAtlas(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="RenderUVAtlas",
|
node_id="RenderUVAtlas",
|
||||||
display_name="Render UV Atlas",
|
display_name="Render UV Atlas",
|
||||||
category="3d/mesh/texturing",
|
category="3d/texturing",
|
||||||
description=("Renders a mesh's UV layout as an image."),
|
description=("Renders a mesh's UV layout as an image."),
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mesh.Input("mesh"),
|
IO.Mesh.Input("mesh"),
|
||||||
@ -2909,10 +2909,10 @@ class FillHoles(IO.ComfyNode):
|
|||||||
IO.Float.Input("max_perimeter", default=0.03, min=0.0, step=0.0001,
|
IO.Float.Input("max_perimeter", default=0.03, min=0.0, step=0.0001,
|
||||||
tooltip="Max hole perimeter to fill. 0 disables."),
|
tooltip="Max hole perimeter to fill. 0 disables."),
|
||||||
IO.Float.Input("weld_epsilon_rel", default=1e-5, min=0.0, step=1e-6,
|
IO.Float.Input("weld_epsilon_rel", default=1e-5, min=0.0, step=1e-6,
|
||||||
tooltip="Pre-weld tolerance (fraction of bbox diagonal); boundary detection "
|
tooltip="Pre-weld tolerance (fraction of bounding box diagonal); boundary detection "
|
||||||
"needs welded verts. 0 skips."),
|
"needs welded vertices. 0 skips."),
|
||||||
IO.Int.Input("max_verts", default=16, min=3, max=1024,
|
IO.Int.Input("max_vertices", default=16, min=3, max=1024,
|
||||||
tooltip="Cap boundary verts per cycle; centroid-fan only works for small "
|
tooltip="Cap boundary vertices per cycle; centroid-fan only works for small "
|
||||||
"near-planar holes. Keep ≤16."),
|
"near-planar holes. Keep ≤16."),
|
||||||
IO.Boolean.Input("fill_chains", default=False,
|
IO.Boolean.Input("fill_chains", default=False,
|
||||||
tooltip="Also fill open chains (not just cycles). Noisy; OFF matches cumesh."),
|
tooltip="Also fill open chains (not just cycles). Noisy; OFF matches cumesh."),
|
||||||
@ -2984,7 +2984,7 @@ class MeshSmoothNormals(IO.ComfyNode):
|
|||||||
inputs=[
|
inputs=[
|
||||||
IO.Mesh.Input("mesh"),
|
IO.Mesh.Input("mesh"),
|
||||||
IO.Float.Input("crease_angle", default=180.0, min=0.0, max=180.0, step=1.0,
|
IO.Float.Input("crease_angle", default=180.0, min=0.0, max=180.0, step=1.0,
|
||||||
tooltip="Edges whose dihedral angle exceeds this (degrees) stay "
|
tooltip="Edges whose dihedral angle exceeds this value (degrees) stay "
|
||||||
"hard (vertices are split). 180 = fully smooth; lower "
|
"hard (vertices are split). 180 = fully smooth; lower "
|
||||||
"preserves sharp edges (e.g. ~30-60 for hard-surface)."),
|
"preserves sharp edges (e.g. ~30-60 for hard-surface)."),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -7,7 +7,6 @@ from comfy_extras.nodes_mesh_postprocess import pack_variable_mesh_batch
|
|||||||
import comfy.latent_formats
|
import comfy.latent_formats
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
import logging
|
|
||||||
import math
|
import math
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
@ -102,7 +101,7 @@ class VaeDecodeShapeTrellis(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="VaeDecodeShapeTrellis",
|
node_id="VaeDecodeShapeTrellis",
|
||||||
category="latent/3d",
|
category="model/latent/trellis",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Latent.Input("samples"),
|
IO.Latent.Input("samples"),
|
||||||
IO.Vae.Input("vae"),
|
IO.Vae.Input("vae"),
|
||||||
@ -174,7 +173,7 @@ class VaeDecodeTextureTrellis(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="VaeDecodeTextureTrellis",
|
node_id="VaeDecodeTextureTrellis",
|
||||||
category="latent/3d",
|
category="model/latent/trellis",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Latent.Input("samples"),
|
IO.Latent.Input("samples"),
|
||||||
IO.Vae.Input("vae"),
|
IO.Vae.Input("vae"),
|
||||||
@ -248,7 +247,7 @@ class VaeDecodeStructureTrellis2(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="VaeDecodeStructureTrellis2",
|
node_id="VaeDecodeStructureTrellis2",
|
||||||
category="latent/3d",
|
category="model/latent/trellis",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Latent.Input("samples"),
|
IO.Latent.Input("samples"),
|
||||||
IO.Vae.Input("vae"),
|
IO.Vae.Input("vae"),
|
||||||
@ -618,7 +617,7 @@ class EmptyTrellis2LatentStructure(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="EmptyTrellis2LatentStructure",
|
node_id="EmptyTrellis2LatentStructure",
|
||||||
category="latent/3d",
|
category="model/latent/trellis",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Int.Input("batch_size", default=1, min=1, max=4096, tooltip="The number of latent images in the batch."),
|
IO.Int.Input("batch_size", default=1, min=1, max=4096, tooltip="The number of latent images in the batch."),
|
||||||
],
|
],
|
||||||
@ -648,95 +647,6 @@ def _dinov3_patches_to_2d(tokens, image_size, patch_size=16):
|
|||||||
return patches.transpose(1, 2).reshape(tokens.shape[0], -1, h_p, w_p).contiguous()
|
return patches.transpose(1, 2).reshape(tokens.shape[0], -1, h_p, w_p).contiguous()
|
||||||
|
|
||||||
|
|
||||||
def _crop_image_with_mask(item_image, item_mask, max_image_size=1024, pad_factor=1.1,
|
|
||||||
mask_offset=0, mask_threshold=0.05, bg_rgb=(0.0, 0.0, 0.0),
|
|
||||||
aspect_ratio=1.0):
|
|
||||||
img = item_image.permute(2, 0, 1).unsqueeze(0).cpu().float().clamp(0, 1)
|
|
||||||
mask = item_mask.unsqueeze(0).unsqueeze(0).cpu().float().clamp(0, 1)
|
|
||||||
|
|
||||||
# Detect and correct an inverted mask, only when border and center have opposite polarity.
|
|
||||||
m2d = mask[0, 0]
|
|
||||||
h, w = m2d.shape
|
|
||||||
border = torch.cat([m2d[0, :], m2d[-1, :], m2d[:, 0], m2d[:, -1]])
|
|
||||||
center = m2d[h // 4:h - h // 4, w // 4:w - w // 4]
|
|
||||||
if float(border.mean()) > 0.5 and float(center.mean()) < 0.5:
|
|
||||||
mask = 1.0 - mask
|
|
||||||
|
|
||||||
if mask_offset > 0:
|
|
||||||
r = mask_offset
|
|
||||||
mask = torch.nn.functional.max_pool2d(mask, kernel_size=2 * r + 1, stride=1, padding=r)
|
|
||||||
elif mask_offset < 0:
|
|
||||||
r = -mask_offset
|
|
||||||
mask = 1.0 - torch.nn.functional.max_pool2d(1.0 - mask, kernel_size=2 * r + 1, stride=1, padding=r)
|
|
||||||
|
|
||||||
if mask_threshold > 0.0:
|
|
||||||
mask = torch.where(mask < mask_threshold, torch.zeros_like(mask), mask)
|
|
||||||
|
|
||||||
H, W = img.shape[-2:]
|
|
||||||
if max(H, W) > max_image_size:
|
|
||||||
scale = max_image_size / max(H, W)
|
|
||||||
new_w, new_h = int(W * scale), int(H * scale)
|
|
||||||
img = comfy.utils.common_upscale(img, new_w, new_h, "lanczos", "disabled")
|
|
||||||
mask = comfy.utils.common_upscale(mask, new_w, new_h, "lanczos", "disabled")
|
|
||||||
# common_upscale's lanczos path drops the singleton channel dim for masks (utils.py:1062).
|
|
||||||
if mask.ndim == 3:
|
|
||||||
mask = mask.unsqueeze(1)
|
|
||||||
H, W = new_h, new_w
|
|
||||||
scene_size = (W, H)
|
|
||||||
|
|
||||||
alpha_u8 = (mask[0, 0].clamp(0, 1) * 255.0).to(torch.uint8)
|
|
||||||
fg_pixels = (alpha_u8 > 204).nonzero()
|
|
||||||
if fg_pixels.numel() == 0:
|
|
||||||
# Try the inverted mask — auto-invert above may have been too conservative.
|
|
||||||
inv_fg = ((255 - alpha_u8) > 204).nonzero()
|
|
||||||
if inv_fg.numel() > 0:
|
|
||||||
logging.info("Trellis2 preprocess: mask bbox empty, using inverted mask.")
|
|
||||||
mask = 1.0 - mask
|
|
||||||
fg_pixels = inv_fg
|
|
||||||
if fg_pixels.numel() > 0:
|
|
||||||
y_min, x_min = fg_pixels.min(dim=0).values.tolist()
|
|
||||||
y_max, x_max = fg_pixels.max(dim=0).values.tolist()
|
|
||||||
center_y, center_x = (y_min + y_max) / 2.0, (x_min + x_max) / 2.0
|
|
||||||
bw = x_max - x_min
|
|
||||||
bh = y_max - y_min
|
|
||||||
# Grow the bbox so its aspect matches `aspect_ratio` (width/height),
|
|
||||||
# anchored on the max side. Then apply pad_factor.
|
|
||||||
if bw / max(bh, 1) >= aspect_ratio:
|
|
||||||
crop_w = int(bw * pad_factor)
|
|
||||||
crop_h = int(bw / aspect_ratio * pad_factor)
|
|
||||||
else:
|
|
||||||
crop_h = int(bh * pad_factor)
|
|
||||||
crop_w = int(bh * aspect_ratio * pad_factor)
|
|
||||||
half_w, half_h = crop_w // 2, crop_h // 2
|
|
||||||
crop_x1 = int(center_x - half_w)
|
|
||||||
crop_y1 = int(center_y - half_h)
|
|
||||||
crop_x2 = crop_x1 + 2 * half_w
|
|
||||||
crop_y2 = crop_y1 + 2 * half_h
|
|
||||||
else:
|
|
||||||
logging.warning("Mask for the image is empty; a clean foreground mask is required for best quality.")
|
|
||||||
crop_x1, crop_y1, crop_x2, crop_y2 = 0, 0, W, H
|
|
||||||
crop_bbox = (crop_x1, crop_y1, crop_x2, crop_y2)
|
|
||||||
|
|
||||||
# Zero-pad out-of-bounds slice (PIL.crop semantics).
|
|
||||||
pad_l = max(0, -crop_x1)
|
|
||||||
pad_t = max(0, -crop_y1)
|
|
||||||
pad_r = max(0, crop_x2 - W)
|
|
||||||
pad_b = max(0, crop_y2 - H)
|
|
||||||
if pad_l or pad_t or pad_r or pad_b:
|
|
||||||
img = torch.nn.functional.pad(img, (pad_l, pad_r, pad_t, pad_b), value=0.0)
|
|
||||||
mask = torch.nn.functional.pad(mask, (pad_l, pad_r, pad_t, pad_b), value=0.0)
|
|
||||||
crop_x1 += pad_l
|
|
||||||
crop_x2 += pad_l
|
|
||||||
crop_y1 += pad_t
|
|
||||||
crop_y2 += pad_t
|
|
||||||
cropped_img = img [..., crop_y1:crop_y2, crop_x1:crop_x2]
|
|
||||||
cropped_mask = mask[..., crop_y1:crop_y2, crop_x1:crop_x2]
|
|
||||||
|
|
||||||
bg = torch.tensor(bg_rgb, dtype=cropped_img.dtype, device=cropped_img.device).view(1, 3, 1, 1)
|
|
||||||
composite = (cropped_img * cropped_mask + bg * (1.0 - cropped_mask)).clamp(0, 1)
|
|
||||||
return composite, crop_bbox, scene_size
|
|
||||||
|
|
||||||
|
|
||||||
def _dino_encode_batch(clip_vision_model, image, out_device, *, want_patches=False):
|
def _dino_encode_batch(clip_vision_model, image, out_device, *, want_patches=False):
|
||||||
"""Encode an already-preprocessed image through DINOv3 at 512 and 1024.
|
"""Encode an already-preprocessed image through DINOv3 at 512 and 1024.
|
||||||
|
|
||||||
@ -774,59 +684,6 @@ def _dino_encode_batch(clip_vision_model, image, out_device, *, want_patches=Fal
|
|||||||
out["composites"] = composite_list
|
out["composites"] = composite_list
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
class ImageCropToMask(IO.ComfyNode):
|
|
||||||
"""Crop an image to its mask's bounding box (centered square, with pad_factor
|
|
||||||
margin), then composite `img * mask` and resize to a square. Handles OOB crops
|
|
||||||
with zero-padding. Useful for 3D pipelines that expect a centered, background-free
|
|
||||||
subject at a fixed input resolution (Trellis2, Pixal3D, Hunyuan3D, TripoSR, etc.)."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def define_schema(cls):
|
|
||||||
return IO.Schema(
|
|
||||||
node_id="ImageCropToMask",
|
|
||||||
display_name="Image Crop to Mask",
|
|
||||||
category="image/transform",
|
|
||||||
search_aliases=["crop to mask", "mask crop", "crop mask", "mask crop resize", "crop mask resize", "trellis2", "pixal3d"],
|
|
||||||
inputs=[
|
|
||||||
IO.Image.Input("image"),
|
|
||||||
IO.Mask.Input("mask"),
|
|
||||||
IO.Int.Input("width", default=1024, min=64, max=4096, step=8, tooltip="Output width in pixels."),
|
|
||||||
IO.Int.Input("height", default=1024, min=64, max=4096, step=8, tooltip="Output height in pixels."),
|
|
||||||
IO.Float.Input("pad_factor", default=1.0, min=1.0, max=2.0, step=0.01, tooltip="Extra margin around the mask bbox as a multiplier."),
|
|
||||||
IO.Int.Input("mask_offset", default=0, min=-32, max=32, step=1, tooltip="Grow or shrink the mask by this many pixels before cropping."),
|
|
||||||
IO.Color.Input("background", default="#000000", tooltip="Fill color behind the masked subject."),
|
|
||||||
],
|
|
||||||
outputs=[IO.Image.Output(display_name="image")],
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def execute(cls, image, mask, width, height, pad_factor, mask_offset, background) -> IO.NodeOutput:
|
|
||||||
h = background.lstrip("#")
|
|
||||||
bg_rgb = (int(h[0:2], 16) / 255.0, int(h[2:4], 16) / 255.0, int(h[4:6], 16) / 255.0) if len(h) == 6 else (0.0, 0.0, 0.0)
|
|
||||||
image = image[..., :3]
|
|
||||||
batch_size = image.shape[0]
|
|
||||||
if mask.shape[0] == 1 and batch_size > 1:
|
|
||||||
mask = mask.expand(batch_size, -1, -1)
|
|
||||||
elif mask.shape[0] != batch_size:
|
|
||||||
raise ValueError(f"Mask batch {mask.shape[0]} does not match image batch {batch_size}")
|
|
||||||
|
|
||||||
out_images = []
|
|
||||||
for b in range(batch_size):
|
|
||||||
composite, _, _ = _crop_image_with_mask(
|
|
||||||
image[b], mask[b], max_image_size=max(width, height), pad_factor=pad_factor,
|
|
||||||
mask_offset=mask_offset, bg_rgb=bg_rgb, aspect_ratio=width / height,
|
|
||||||
)
|
|
||||||
composite = comfy.utils.common_upscale(composite, width, height, "lanczos", "disabled")
|
|
||||||
out_images.append(composite.movedim(-3, -1))
|
|
||||||
|
|
||||||
result = torch.cat(out_images, dim=0).to(
|
|
||||||
device=comfy.model_management.intermediate_device(),
|
|
||||||
dtype=comfy.model_management.intermediate_dtype(),
|
|
||||||
)
|
|
||||||
return IO.NodeOutput(result)
|
|
||||||
|
|
||||||
|
|
||||||
class Pixal3DConditioning(IO.ComfyNode):
|
class Pixal3DConditioning(IO.ComfyNode):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -839,7 +696,7 @@ class Pixal3DConditioning(IO.ComfyNode):
|
|||||||
IO.Image.Input("image", tooltip="Preprocessed image from ImageCropToMask (pad_factor=1.1 for Pixal3D)."),
|
IO.Image.Input("image", tooltip="Preprocessed image from ImageCropToMask (pad_factor=1.1 for Pixal3D)."),
|
||||||
IO.Float.Input(
|
IO.Float.Input(
|
||||||
"camera_angle_x", display_name="fov",
|
"camera_angle_x", display_name="fov",
|
||||||
default=49.13, min=1.0, max=170.0, step=0.01, advanced=True,
|
default=49.13, min=1.0, max=170.0, step=0.01,
|
||||||
tooltip="Horizontal FOV in degrees. Wire a MoGeGeometryToFOV "
|
tooltip="Horizontal FOV in degrees. Wire a MoGeGeometryToFOV "
|
||||||
"(axis='horizontal', unit='degrees') for a per-image FoV (matches upstream default).",
|
"(axis='horizontal', unit='degrees') for a per-image FoV (matches upstream default).",
|
||||||
),
|
),
|
||||||
@ -930,7 +787,6 @@ class Trellis2Extension(ComfyExtension):
|
|||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
return [
|
return [
|
||||||
ImageCropToMask,
|
|
||||||
Trellis2Conditioning,
|
Trellis2Conditioning,
|
||||||
Pixal3DConditioning,
|
Pixal3DConditioning,
|
||||||
Trellis2ShapeStage,
|
Trellis2ShapeStage,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user