add test texture loading

This commit is contained in:
DenisKochetov
2025-09-05 12:15:32 +02:00
parent f8067fce42
commit 4349dfaa1a
3 changed files with 190 additions and 273 deletions
-269
View File
@@ -1,269 +0,0 @@
class Preview_3DMesh:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh_file_path": ("STRING", {"default": '', "multiline": False}),
},
}
OUTPUT_NODE = True
RETURN_TYPES = ()
FUNCTION = "preview_mesh"
CATEGORY = "Comfy3D/Visualize"
def preview_mesh(self, mesh_file_path):
mesh_folder_path, filename = os.path.split(mesh_file_path)
if not os.path.isabs(mesh_file_path):
mesh_file_path = os.path.join(comfy_paths.output_directory, mesh_folder_path, filename)
if not filename.lower().endswith(SUPPORTED_3D_EXTENSIONS):
cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print()
mesh_file_path = ""
print(f"[Preview_3DMesh] Final mesh path: {mesh_file_path}")
print(f"[Preview_3DMesh] File exists: {os.path.exists(mesh_file_path) if mesh_file_path else False}")
previews = [
{
"filepath": mesh_file_path,
}
]
return {"ui": {"previews": previews}, "result": ()}
class Load_3D_Mesh:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh_file_path": ("STRING", {"default": '', "multiline": False}),
"use_fastmesh": ("BOOLEAN", {"default": True, "tooltip": "Использовать FastMesh для GLB/GLTF файлов"}),
"resize": ("BOOLEAN", {"default": False},),
"renormal": ("BOOLEAN", {"default": True},),
"retex": ("BOOLEAN", {"default": False},),
"optimizable": ("BOOLEAN", {"default": False},),
"clean": ("BOOLEAN", {"default": False},),
"resize_bound": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1000.0, "step": 0.001}),
},
}
RETURN_TYPES = (
"MESH",
)
RETURN_NAMES = (
"mesh",
)
FUNCTION = "load_mesh"
CATEGORY = "Comfy3D/Import|Export"
def load_mesh(self, mesh_file_path, use_fastmesh, resize, renormal, retex, optimizable, clean, resize_bound):
import time
mesh = None
if not os.path.isabs(mesh_file_path):
mesh_file_path = os.path.join(comfy_paths.input_directory, mesh_file_path)
if os.path.exists(mesh_file_path):
folder, filename = os.path.split(mesh_file_path)
if filename.lower().endswith(SUPPORTED_3D_EXTENSIONS):
with torch.inference_mode(not optimizable):
start_time = time.time()
if use_fastmesh and mesh_file_path.endswith((".glb", ".gltf")):
mesh = FastMesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound)
print(f"[FastMesh] Loading completed in {time.time() - start_time:.3f}s")
else:
mesh = Mesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound)
print(f"[Mesh] Loading completed in {time.time() - start_time:.3f}s")
else:
cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print()
else:
cstr(f"[{self.__class__.__name__}] File {mesh_file_path} does not exist").error.print()
return (mesh, )
class Save_3D_Mesh:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh": ("MESH",),
"save_path": ("STRING", {"default": 'Mesh_%Y-%m-%d-%M-%S-%f.glb', "multiline": False}),
"use_fastmesh": ("BOOLEAN", {"default": True, "tooltip": "Использовать FastMesh для быстрого сохранения GLB/PLY/OBJ"}),
},
}
OUTPUT_NODE = True
RETURN_TYPES = (
"STRING",
)
RETURN_NAMES = (
"save_path",
)
FUNCTION = "save_mesh"
CATEGORY = "Comfy3D/Import|Export"
def save_mesh(self, mesh, save_path, use_fastmesh):
import time
save_path = parse_save_filename(save_path, comfy_paths.output_directory, SUPPORTED_3D_EXTENSIONS, self.__class__.__name__)
if save_path is not None:
start_time = time.time()
# Используем FastMesh методы для быстрого сохранения
if use_fastmesh and save_path.endswith((".glb", ".ply", ".obj")):
# Если это уже FastMesh - используем напрямую
print(f"[Save_3D_Mesh] FastMesh detected")
if hasattr(mesh, '_write_glb_fast'):
mesh.write(save_path)
save_time = time.time() - start_time
print(f"[FastMesh] Saving completed in {save_time:.3f}s")
else:
# Если обычный Mesh - создаем временный FastMesh для быстрых методов
print(f"[Save_3D_Mesh] REGULAR Mesh detected")
fast_mesh = FastMesh(
v=mesh.v, f=mesh.f, vn=mesh.vn, fn=mesh.fn,
vt=mesh.vt, ft=mesh.ft, vc=mesh.vc,
albedo=mesh.albedo, metallicRoughness=getattr(mesh, 'metallicRoughness', None),
device=mesh.device
)
fast_mesh.ori_center = getattr(mesh, 'ori_center', 0)
fast_mesh.ori_scale = getattr(mesh, 'ori_scale', 1)
fast_mesh.write(save_path)
save_time = time.time() - start_time
print(f"[FastMesh] Saving completed in {save_time:.3f}s")
else:
# Обычное сохранение через стандартный метод
mesh.write(save_path)
save_time = time.time() - start_time
print(f"[Mesh] Saving completed in {save_time:.3f}s")
return (save_path, )
class Preview_3D_FastMesh:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh_file_path": ("STRING", {"default": '', "multiline": False}),
},
}
OUTPUT_NODE = True
RETURN_TYPES = ()
FUNCTION = "preview_mesh"
CATEGORY = "Comfy3D/Visualize"
def preview_mesh(self, mesh_file_path):
mesh_folder_path, filename = os.path.split(mesh_file_path)
if not os.path.isabs(mesh_file_path):
mesh_file_path = os.path.join(comfy_paths.output_directory, mesh_folder_path, filename)
if not filename.lower().endswith(SUPPORTED_3D_EXTENSIONS):
cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print()
mesh_file_path = ""
print(f"[Preview_3D_FastMesh] Final mesh path: {mesh_file_path}")
print(f"[Preview_3D_FastMesh] File exists: {os.path.exists(mesh_file_path) if mesh_file_path else False}")
previews = [
{
"filepath": mesh_file_path,
}
]
return {"ui": {"previews": previews}, "result": ()}
class Load_3D_FastMesh:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh_file_path": ("STRING", {"default": '', "multiline": False}),
"use_fastmesh": ("BOOLEAN", {"default": True, "tooltip": "Использовать FastMesh загрузчик"}),
"resize": ("BOOLEAN", {"default": False},),
"renormal": ("BOOLEAN", {"default": True},),
"retex": ("BOOLEAN", {"default": False},),
"optimizable": ("BOOLEAN", {"default": False},),
"clean": ("BOOLEAN", {"default": False},),
"resize_bound": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1000.0, "step": 0.001}),
},
}
RETURN_TYPES = (
"MESH",
)
RETURN_NAMES = (
"mesh",
)
FUNCTION = "load_mesh"
CATEGORY = "Comfy3D/Import|Export"
def load_mesh(self, mesh_file_path, use_fastmesh, resize, renormal, retex, optimizable, clean, resize_bound):
import time
mesh = None
if not os.path.isabs(mesh_file_path):
mesh_file_path = os.path.join(comfy_paths.input_directory, mesh_file_path)
if os.path.exists(mesh_file_path):
folder, filename = os.path.split(mesh_file_path)
if filename.lower().endswith(SUPPORTED_3D_EXTENSIONS):
with torch.inference_mode(not optimizable):
start_time = time.time()
if use_fastmesh and mesh_file_path.endswith((".glb", ".gltf")):
from mesh_processor.mesh import FastMesh
mesh = FastMesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound)
print(f"[FastMesh] Loading completed in {time.time() - start_time:.3f}s")
else:
mesh = Mesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound)
mesh_type = "FastMesh" if use_fastmesh else "Mesh"
print(f"[{mesh_type}] Loading completed in {time.time() - start_time:.3f}s")
else:
cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print()
else:
cstr(f"[{self.__class__.__name__}] File {mesh_file_path} does not exist").error.print()
return (mesh, )
class Save_3D_FastMesh:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh": ("MESH",),
"save_path": ("STRING", {"default": 'FastMesh_%Y-%m-%d-%M-%S-%f.glb', "multiline": False}),
},
}
OUTPUT_NODE = True
RETURN_TYPES = (
"STRING",
)
RETURN_NAMES = (
"save_path",
)
FUNCTION = "save_mesh"
CATEGORY = "Comfy3D/Import|Export"
def save_mesh(self, mesh, save_path):
import time
save_path = parse_save_filename(save_path, comfy_paths.output_directory, SUPPORTED_3D_EXTENSIONS, self.__class__.__name__)
if save_path is not None:
start_time = time.time()
mesh.write(save_path)
save_time = time.time() - start_time
mesh_type = "FastMesh" if hasattr(mesh, '_create_empty_albedo') else "Mesh"
print(f"[{mesh_type}] Saving completed in {save_time:.3f}s")
return (save_path, )
+190 -4
View File
@@ -130,10 +130,7 @@ class FastMesh:
if colors is not None:
mesh.vc = colors
print(f"[FastMesh] Colors: {mesh.vc.shape}")
if not lazy_texture:
cls._extract_gltf_textures(mesh, doc, device)
else:
mesh._lazy_texture_doc = doc
cls._extract_gltf_textures_fast(mesh, doc, device, lazy_empty=lazy_texture)
print(f"[FastMesh] Loaded: {len(vertices)} vertices, {len(faces)} faces")
return mesh
@@ -287,6 +284,50 @@ class FastMesh:
except Exception as e:
print(f"[FastMesh] Error colors: {e}")
@classmethod
def _extract_gltf_textures_fast(cls, mesh, doc, device, lazy_empty=True):
"""Fast texture extraction - only load if exists, don't create empty if lazy_empty=True"""
if not (doc.materials() and doc.textures() and doc.images()):
if not lazy_empty:
mesh._create_empty_albedo()
print(f"[FastMesh] No textures found - created empty albedo")
else:
print(f"[FastMesh] No textures found - skipping empty albedo creation")
return
texture_loaded = False
try:
material = doc.materials()[0]
pbr = material.get("pbrMetallicRoughness", {})
# Albedo texture
base_color_texture = pbr.get("baseColorTexture")
if base_color_texture:
albedo_texture = cls._extract_texture_by_index(doc, base_color_texture.get("index", 0))
if albedo_texture is not None:
albedo_float = albedo_texture.astype(np.float32) / 255.0
mesh.albedo = torch.tensor(albedo_float, dtype=torch.float32, device=device).contiguous()
texture_loaded = True
print(f"[FastMesh] Albedo texture loaded: {mesh.albedo.shape}")
# Metallic-Roughness texture
mr_texture_info = pbr.get("metallicRoughnessTexture")
if mr_texture_info and texture_loaded:
mr_texture = cls._extract_texture_by_index(doc, mr_texture_info.get("index", 0))
if mr_texture is not None:
mr_float = mr_texture.astype(np.float32) / 255.0
mesh.metallicRoughness = torch.tensor(mr_float, dtype=torch.float32, device=device).contiguous()
print(f"[FastMesh] MetallicRoughness texture loaded: {mesh.metallicRoughness.shape}")
except Exception as e:
print(f"[FastMesh] Error extracting textures: {e}")
if not texture_loaded and not lazy_empty:
mesh._create_empty_albedo()
print(f"[FastMesh] Created empty albedo texture")
@classmethod
def _extract_gltf_textures(cls, mesh, doc, device):
"""Extract textures from GLTF"""
@@ -528,6 +569,11 @@ class FastMesh:
def _write_glb_fast(self, path):
"""Fast GLB writing using mesh_processor"""
try:
print(f"[FastMesh] Starting GLB save to: {path}")
print(f"[FastMesh] Mesh has albedo: {self.albedo is not None}")
print(f"[FastMesh] Mesh has UV: {self.vt is not None}")
print(f"[FastMesh] Mesh has metallicRoughness: {self.metallicRoughness is not None}")
# Prepare data
vertices = self.v.detach().cpu().numpy().astype(np.float32)
faces = self.f.detach().cpu().numpy().astype(np.uint32)
@@ -572,6 +618,23 @@ class FastMesh:
primitive["attributes"] = {"POSITION": pos_accessor}
primitive["indices"] = idx_accessor
# Add UV coordinates if available
if self.vt is not None:
uv_data = self.vt.detach().cpu().numpy().astype(np.float32)
uv_accessor, _ = append_accessor_and_bufferview(
doc, uv_data,
component_type=5126, # FLOAT
element_type="VEC2",
target=34962 # ARRAY_BUFFER
)
primitive["attributes"]["TEXCOORD_0"] = uv_accessor
print(f"[FastMesh] Added UV coordinates: {uv_data.shape}")
# Add texture if available
if self.albedo is not None and self.vt is not None:
self._add_texture_to_gltf(doc, primitive)
print(f"[FastMesh] Added albedo texture: {self.albedo.shape}")
# Save to file
with open(path, "wb") as f:
save_glb(doc, f)
@@ -582,6 +645,129 @@ class FastMesh:
print(f"[FastMesh] Fast GLB save failed: {e}, using fallback")
self._write_fallback(path)
def _add_texture_to_gltf(self, doc, primitive):
"""Add texture data to GLTF document using fast approach"""
try:
# Convert albedo texture to PNG bytes
albedo_np = self.albedo.detach().cpu().numpy()
albedo_uint8 = (albedo_np * 255).astype(np.uint8)
# Encode as PNG
import cv2
albedo_bgr = cv2.cvtColor(albedo_uint8, cv2.COLOR_RGB2BGR)
success, png_bytes = cv2.imencode('.png', albedo_bgr)
if not success:
print(f"[FastMesh] Failed to encode texture as PNG")
return
png_data = png_bytes.tobytes()
# Add image data to buffer using helper function
image_accessor, _ = append_accessor_and_bufferview(
doc, np.frombuffer(png_data, dtype=np.uint8),
component_type=5121, # UNSIGNED_BYTE
element_type="SCALAR",
target=None # No specific target for image data
)
# Create GLTF structures
gltf_json = doc.json()
# Initialize arrays if they don't exist
if "materials" not in gltf_json:
gltf_json["materials"] = []
if "textures" not in gltf_json:
gltf_json["textures"] = []
if "images" not in gltf_json:
gltf_json["images"] = []
if "samplers" not in gltf_json:
gltf_json["samplers"] = []
# Add image
image_idx = len(gltf_json["images"])
gltf_json["images"].append({
"bufferView": image_accessor,
"mimeType": "image/png"
})
# Add sampler
sampler_idx = len(gltf_json["samplers"])
gltf_json["samplers"].append({
"magFilter": 9729, # LINEAR
"minFilter": 9987, # LINEAR_MIPMAP_LINEAR
"wrapS": 10497, # REPEAT
"wrapT": 10497 # REPEAT
})
# Add texture
texture_idx = len(gltf_json["textures"])
gltf_json["textures"].append({
"sampler": sampler_idx,
"source": image_idx
})
# Add material
material_idx = len(gltf_json["materials"])
material = {
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": texture_idx,
"texCoord": 0
},
"metallicFactor": 0.0,
"roughnessFactor": 1.0
},
"alphaMode": "OPAQUE",
"doubleSided": True
}
# Add metallic-roughness texture if available
if self.metallicRoughness is not None:
mr_np = self.metallicRoughness.detach().cpu().numpy()
mr_uint8 = (mr_np * 255).astype(np.uint8)
mr_bgr = cv2.cvtColor(mr_uint8, cv2.COLOR_RGB2BGR)
mr_success, mr_png_bytes = cv2.imencode('.png', mr_bgr)
if mr_success:
mr_png_data = mr_png_bytes.tobytes()
mr_image_accessor, _ = append_accessor_and_bufferview(
doc, np.frombuffer(mr_png_data, dtype=np.uint8),
component_type=5121, # UNSIGNED_BYTE
element_type="SCALAR",
target=None
)
# Add MR image and texture
mr_image_idx = len(gltf_json["images"])
gltf_json["images"].append({
"bufferView": mr_image_accessor,
"mimeType": "image/png"
})
mr_texture_idx = len(gltf_json["textures"])
gltf_json["textures"].append({
"sampler": sampler_idx, # Reuse same sampler
"source": mr_image_idx
})
# Update material
material["pbrMetallicRoughness"]["metallicRoughnessTexture"] = {
"index": mr_texture_idx,
"texCoord": 0
}
material["pbrMetallicRoughness"]["metallicFactor"] = 1.0
material["pbrMetallicRoughness"]["roughnessFactor"] = 1.0
print(f"[FastMesh] Added metallic-roughness texture: {mr_np.shape}")
gltf_json["materials"].append(material)
# Link material to primitive
primitive["material"] = material_idx
except Exception as e:
print(f"[FastMesh] Error adding texture to GLTF: {e}")
def _write_ply_fast(self, path):
"""Fast PLY writing"""
vertices = self.v.detach().cpu().numpy()