Skip to content

Commit 1ea727b

Browse files
committed
support additional solid texture outputs
1 parent 858977a commit 1ea727b

1 file changed

Lines changed: 58 additions & 16 deletions

File tree

comfy_api_nodes/nodes_hunyuan3d.py

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import zipfile
22
from io import BytesIO
33

4+
import torch
45
from typing_extensions import override
56

67
from comfy_api.latest import IO, ComfyExtension, Input, Types
@@ -41,27 +42,66 @@ def _is_tencent_rate_limited(status: int, body: object) -> bool:
4142
)
4243

4344

44-
async def download_and_extract_obj_zip(url: str) -> tuple[Types.File3D, Input.Image | None]:
45-
"""The Tencent API returns OBJ results as ZIP archives containing the .obj mesh, and a texture image."""
45+
class ObjZipResult:
46+
__slots__ = ("obj", "texture", "metallic", "normal", "roughness")
47+
48+
def __init__(
49+
self,
50+
obj: Types.File3D,
51+
texture: Input.Image | None = None,
52+
metallic: Input.Image | None = None,
53+
normal: Input.Image | None = None,
54+
roughness: Input.Image | None = None,
55+
):
56+
self.obj = obj
57+
self.texture = texture
58+
self.metallic = metallic
59+
self.normal = normal
60+
self.roughness = roughness
61+
62+
63+
async def download_and_extract_obj_zip(url: str) -> ObjZipResult:
64+
"""The Tencent API returns OBJ results as ZIP archives containing the .obj mesh, and texture images.
65+
66+
When PBR is enabled, the ZIP may contain additional metallic, normal, and roughness maps
67+
identified by their filename suffixes.
68+
"""
4669
data = BytesIO()
4770
await download_url_to_bytesio(url, data)
4871
data.seek(0)
4972
if not zipfile.is_zipfile(data):
5073
data.seek(0)
51-
return Types.File3D(source=data, file_format="obj"), None
74+
return ObjZipResult(obj=Types.File3D(source=data, file_format="obj"))
5275
data.seek(0)
5376
obj_bytes = None
54-
texture_tensor = None
77+
textures: dict[str, Input.Image] = {}
5578
with zipfile.ZipFile(data) as zf:
5679
for name in zf.namelist():
5780
lower = name.lower()
5881
if lower.endswith(".obj"):
5982
obj_bytes = zf.read(name)
6083
elif any(lower.endswith(ext) for ext in (".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".webp")):
61-
texture_tensor = bytesio_to_image_tensor(BytesIO(zf.read(name)), mode="RGB")
84+
stem = lower.rsplit(".", 1)[0]
85+
tensor = bytesio_to_image_tensor(BytesIO(zf.read(name)), mode="RGB")
86+
matched_key = "texture"
87+
for suffix, key in {
88+
"_metallic": "metallic",
89+
"_normal": "normal",
90+
"_roughness": "roughness",
91+
}.items():
92+
if stem.endswith(suffix):
93+
matched_key = key
94+
break
95+
textures[matched_key] = tensor
6296
if obj_bytes is None:
6397
raise ValueError("ZIP archive does not contain an OBJ file.")
64-
return Types.File3D(source=BytesIO(obj_bytes), file_format="obj"), texture_tensor
98+
return ObjZipResult(
99+
obj=Types.File3D(source=BytesIO(obj_bytes), file_format="obj"),
100+
texture=textures.get("texture"),
101+
metallic=textures.get("metallic"),
102+
normal=textures.get("normal"),
103+
roughness=textures.get("roughness"),
104+
)
65105

66106

67107
def get_file_from_response(
@@ -180,16 +220,14 @@ async def execute(
180220
response_model=To3DProTaskResultResponse,
181221
status_extractor=lambda r: r.Status,
182222
)
183-
obj_file, texture_image = await download_and_extract_obj_zip(
184-
get_file_from_response(result.ResultFile3Ds, "obj").Url
185-
)
223+
obj_result = await download_and_extract_obj_zip(get_file_from_response(result.ResultFile3Ds, "obj").Url)
186224
return IO.NodeOutput(
187225
f"{task_id}.glb",
188226
await download_url_to_file_3d(
189227
get_file_from_response(result.ResultFile3Ds, "glb").Url, "glb", task_id=task_id
190228
),
191-
obj_file,
192-
texture_image,
229+
obj_result.obj,
230+
obj_result.texture,
193231
)
194232

195233

@@ -243,6 +281,9 @@ def define_schema(cls):
243281
IO.File3DGLB.Output(display_name="GLB"),
244282
IO.File3DOBJ.Output(display_name="OBJ"),
245283
IO.Image.Output(display_name="texture_image"),
284+
IO.Image.Output(display_name="optional_metallic"),
285+
IO.Image.Output(display_name="optional_normal"),
286+
IO.Image.Output(display_name="optional_roughness"),
246287
],
247288
hidden=[
248289
IO.Hidden.auth_token_comfy_org,
@@ -336,16 +377,17 @@ async def execute(
336377
response_model=To3DProTaskResultResponse,
337378
status_extractor=lambda r: r.Status,
338379
)
339-
obj_file, texture_image = await download_and_extract_obj_zip(
340-
get_file_from_response(result.ResultFile3Ds, "obj").Url
341-
)
380+
obj_result = await download_and_extract_obj_zip(get_file_from_response(result.ResultFile3Ds, "obj").Url)
342381
return IO.NodeOutput(
343382
f"{task_id}.glb",
344383
await download_url_to_file_3d(
345384
get_file_from_response(result.ResultFile3Ds, "glb").Url, "glb", task_id=task_id
346385
),
347-
obj_file,
348-
texture_image,
386+
obj_result.obj,
387+
obj_result.texture,
388+
obj_result.metallic if obj_result.metallic is not None else torch.zeros(1, 1, 1, 3),
389+
obj_result.normal if obj_result.normal is not None else torch.zeros(1, 1, 1, 3),
390+
obj_result.roughness if obj_result.roughness is not None else torch.zeros(1, 1, 1, 3),
349391
)
350392

351393

0 commit comments

Comments
 (0)