Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68fa3fed57 |
@@ -14,7 +14,7 @@ from CRM.util.renderer import Renderer
|
||||
from CRM.model.archs.mlp_head import SdfMlp, RgbMlp
|
||||
import xatlas
|
||||
|
||||
from mesh_processor.mesh import Mesh
|
||||
from mesh_processer.mesh import Mesh
|
||||
|
||||
|
||||
class Dummy:
|
||||
|
||||
@@ -6,6 +6,6 @@ except ImportError:
|
||||
try:
|
||||
from .mesh_inpaint_processor_fallback import meshVerticeInpaint
|
||||
print("[WARNING] Using Python fallback mesh_inpaint_processor (slower)")
|
||||
print("[INFO] To use faster C++ version, run: compile_mesh_painter.bat or compile_mesh_painter.sh")
|
||||
print("[INFO] To use faster C++ version, run: compile_mesh_painter.bat")
|
||||
except ImportError:
|
||||
print("[ERROR] Neither C++ nor Python mesh_inpaint_processor found!")
|
||||
|
||||
@@ -6,9 +6,6 @@ import base64
|
||||
import io
|
||||
import tempfile
|
||||
import os
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "../../../"))
|
||||
from mesh_processor.fastmesh import FastMesh
|
||||
|
||||
|
||||
def combine_metallic_roughness(metallic_path, roughness_path, output_path):
|
||||
@@ -85,8 +82,7 @@ def create_glb_with_pbr_materials(obj_path, textures_dict, output_path):
|
||||
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_mr_file:
|
||||
mr_combined_path = temp_mr_file.name
|
||||
temp_files.append(mr_combined_path)
|
||||
# combine_metallic_roughness(textures_dict["metallic"], textures_dict["roughness"], mr_combined_path)
|
||||
combine_metallic_roughness_fast(textures_dict["metallic"], textures_dict["roughness"], mr_combined_path)
|
||||
combine_metallic_roughness(textures_dict["metallic"], textures_dict["roughness"], mr_combined_path)
|
||||
textures_dict["metallicRoughness"] = mr_combined_path
|
||||
|
||||
# 6. 添加图像到GLTF
|
||||
@@ -162,17 +158,3 @@ def create_glb_with_pbr_materials(obj_path, textures_dict, output_path):
|
||||
os.remove(temp_file)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to remove temporary file {temp_file}: {e}")
|
||||
|
||||
|
||||
# FastMesh-based functions (new optimized versions)
|
||||
def combine_metallic_roughness_fast(metallic_path, roughness_path, output_path):
|
||||
"""Fast combine metallic and roughness maps using FastMesh"""
|
||||
combined_array = FastMesh._combine_metallic_roughness(metallic_path, roughness_path)
|
||||
combined = Image.fromarray(combined_array)
|
||||
combined.save(output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
def create_glb_with_pbr_materials_fast(obj_path, textures_dict, output_path):
|
||||
"""Fast create GLB with PBR materials using FastMesh"""
|
||||
FastMesh.create_glb_with_pbr_materials(obj_path, textures_dict, output_path)
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@ except ImportError:
|
||||
try:
|
||||
from .mesh_inpaint_processor_fallback import meshVerticeInpaint
|
||||
print("[WARNING] Using Python fallback mesh_inpaint_processor (slower)")
|
||||
print("[INFO] To use faster C++ version, run: compile_mesh_painter.bat or compile_mesh_painter.sh")
|
||||
print("[INFO] To use faster C++ version, run: compile_mesh_painter.bat")
|
||||
except ImportError:
|
||||
print("[ERROR] Neither C++ nor Python mesh_inpaint_processor found!")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from setuptools import setup, find_packages
|
||||
import platform
|
||||
|
||||
# Определяем платформу для правильного именования wheel
|
||||
if platform.system() == "Windows":
|
||||
plat_name = "win_amd64"
|
||||
elif platform.system() == "Darwin":
|
||||
@@ -12,14 +13,14 @@ setup(
|
||||
name="mesh_inpaint_processor",
|
||||
version="1.0.0",
|
||||
description="Fast C++ mesh inpainting processor with Python fallback",
|
||||
long_description="Compiled module for fast mesh inpainting processing",
|
||||
long_description="Компилированный модуль для быстрой обработки инпейнтинга мешей",
|
||||
author="Hunyuan3D Team",
|
||||
packages=find_packages(),
|
||||
package_data={
|
||||
'mesh_inpaint_processor': [
|
||||
'*.pyd',
|
||||
'*.so', # For Linux version
|
||||
'*.py' # Python fallback files
|
||||
'*.pyd', # Включаем .pyd файлы
|
||||
'*.so', # На случай Linux версии
|
||||
'*.py' # Python fallback файлы
|
||||
]
|
||||
},
|
||||
include_package_data=True,
|
||||
@@ -40,6 +41,7 @@ setup(
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS",
|
||||
],
|
||||
# Указываем что это binary distribution
|
||||
zip_safe=False,
|
||||
has_ext_modules=lambda: True,
|
||||
has_ext_modules=lambda: True, # Указывает что есть нативные модули
|
||||
)
|
||||
@@ -29,7 +29,7 @@ from .utils.pipeline_utils import ViewProcessor
|
||||
from .utils.image_super_utils import imageSuperNet
|
||||
from .utils.uvwrap_utils import mesh_uv_wrap
|
||||
from .DifferentiableRenderer.mesh_utils import convert_obj_to_glb
|
||||
from .convert_utils import create_glb_with_pbr_materials, create_glb_with_pbr_materials_fast
|
||||
from .convert_utils import create_glb_with_pbr_materials
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
@@ -209,9 +209,7 @@ class Hunyuan3DPaintPipeline:
|
||||
textures_dict['normal'] = normal_path
|
||||
|
||||
try:
|
||||
# create_glb_with_pbr_materials(output_mesh_path, textures_dict, output_glb_path)
|
||||
print("Creating GLB with full PBR materials using FastMesh")
|
||||
create_glb_with_pbr_materials_fast(output_mesh_path, textures_dict, output_glb_path)
|
||||
create_glb_with_pbr_materials(output_mesh_path, textures_dict, output_glb_path)
|
||||
print(f"Created GLB with full PBR materials: {output_glb_path}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to create GLB with PBR materials: {e}")
|
||||
|
||||
@@ -15,8 +15,8 @@ from kiui.op import inverse_sigmoid
|
||||
from kiui.cam import orbit_camera, get_perspective
|
||||
from kiui.nn import MLP, trunc_exp
|
||||
|
||||
from mesh_processor.mesh import Mesh
|
||||
from mesh_processor.mesh_utils import marching_cubes_density_to_mesh, color_func_to_albedo
|
||||
from mesh_processer.mesh import Mesh
|
||||
from mesh_processer.mesh_utils import marching_cubes_density_to_mesh, color_func_to_albedo
|
||||
from LGM.core.options import Options
|
||||
from LGM.core.gs import GaussianRenderer
|
||||
from LGM.core.utils import get_rays
|
||||
|
||||
@@ -540,7 +540,7 @@ def prepare_tg2mv_pipeline(
|
||||
if hasattr(pipe, 'enable_model_cpu_offload'):
|
||||
if not use_mmgp:
|
||||
pipe.enable_model_cpu_offload()
|
||||
print("[INFO] CPU offload for memory optimization")
|
||||
print("[INFO] Включен CPU offload для экономии VRAM")
|
||||
|
||||
if use_mmgp and MMGP_AVAILABLE:
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
@@ -48,7 +48,7 @@ def _try_import_sageattention() -> bool:
|
||||
try:
|
||||
import torch.nn.functional as F
|
||||
from sageattention import sageattn
|
||||
#F.scaled_dot_product_attention = sageattn
|
||||
F.scaled_dot_product_attention = sageattn
|
||||
#import sageattention
|
||||
return True
|
||||
except ImportError:
|
||||
|
||||
@@ -13,7 +13,7 @@ from ..utils.ops import scale_tensor
|
||||
from .networks import MLP
|
||||
from einops import rearrange, reduce
|
||||
|
||||
from mesh_processor.mesh_utils import construct_list_of_gs_attributes, write_gs_ply
|
||||
from mesh_processer.mesh_utils import construct_list_of_gs_attributes, write_gs_ply
|
||||
|
||||
inverse_sigmoid = lambda x: np.log(x / (1 - x))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import torch
|
||||
import torch.nn as nn
|
||||
from mcubes import marching_cubes
|
||||
|
||||
from mesh_processor.mesh_utils import switch_vector_axis
|
||||
from mesh_processer.mesh_utils import switch_vector_axis
|
||||
|
||||
|
||||
class IsosurfaceHelper(nn.Module):
|
||||
|
||||
@@ -12,7 +12,7 @@ from pytorch3d.renderer import (
|
||||
FoVOrthographicCameras,
|
||||
)
|
||||
from pytorch3d.renderer import MeshRasterizer
|
||||
from mesh_processor.mesh import Mesh
|
||||
from mesh_processer.mesh import Mesh
|
||||
|
||||
def get_camera(world_to_cam, fov_in_degrees=60, focal_length=1 / (2**0.5), cam_type='fov'):
|
||||
# pytorch3d expects transforms as row-vectors, so flip rotation: https://github.com/facebookresearch/pytorch3d/issues/1183
|
||||
|
||||
@@ -9,7 +9,7 @@ import nvdiffrast.torch as dr
|
||||
|
||||
from kiui.op import inverse_sigmoid
|
||||
|
||||
from mesh_processor.mesh import safe_normalize
|
||||
from mesh_processer.mesh import safe_normalize
|
||||
|
||||
def scale_img_nhwc(x, size, mag='bilinear', min='bilinear'):
|
||||
assert (x.shape[1] >= size[0] and x.shape[2] >= size[1]) or (x.shape[1] < size[0] and x.shape[2] < size[1]), "Trying to magnify image in one dimension and minify in the other"
|
||||
|
||||
@@ -13,7 +13,7 @@ from .util import SimpleMesh
|
||||
from .loss import sdf_reg_loss
|
||||
|
||||
from shared_utils.camera_utils import OrbitCamera
|
||||
from mesh_processor.mesh import Mesh
|
||||
from mesh_processer.mesh import Mesh
|
||||
|
||||
|
||||
def lr_schedule(iter):
|
||||
|
||||
@@ -15,8 +15,8 @@ from kornia.geometry.conversions import (
|
||||
from kiui.op import inverse_sigmoid
|
||||
|
||||
from shared_utils.sh_utils import eval_sh, SH2RGB, RGB2SH
|
||||
from mesh_processor.mesh import Mesh, PointCloud
|
||||
from mesh_processor.mesh_utils import construct_list_of_gs_attributes, write_gs_ply, read_gs_ply, K_nearest_neighbors_func
|
||||
from mesh_processer.mesh import Mesh, PointCloud
|
||||
from mesh_processer.mesh_utils import construct_list_of_gs_attributes, write_gs_ply, read_gs_ply, K_nearest_neighbors_func
|
||||
|
||||
def get_expon_lr_func(
|
||||
lr_init, lr_final, lr_delay_steps=0, lr_delay_mult=1.0, max_steps=1000000
|
||||
|
||||
@@ -20,8 +20,8 @@ This is an extensive node suite that enables ComfyUI to process 3D inputs (Mesh
|
||||
- [Pre-builds](https://github.com/MrForExample/Comfy3D_Pre_Builds) are available for:
|
||||
- Windows 10/11
|
||||
- Python 3.12
|
||||
- CUDA 12.8
|
||||
- torch 2.7.0+cu128
|
||||
- CUDA 12.4
|
||||
- torch 2.5.1+cu124
|
||||
- [install.py](install.py) will download & install Pre-builds automatically according to your runtime environment, if it couldn't find corresponding Pre-builds, then [build script](_Pre_Builds/_Build_Scripts/auto_build_all.py) will start automatically, if automatic build doesn't work for you, then please check out [Semi-Automatic Build Guide](_Pre_Builds/README.md#build-required-packages-semi-automatically)
|
||||
- If you have any missing node in any open Comfy3D workflow, try simply click [Install Missing Custom Nodes](https://github.com/ltdrdata/ComfyUI-Manager?tab=readme-ov-file#support-of-missing-nodes-installation) in ComfyUI-Manager
|
||||
- In case there is missing python library, you can check [all the python dependencies of my dev environment](my-reqs.txt)
|
||||
@@ -127,7 +127,7 @@ Your ComfyUI Root Directory\python_embeded\python.exe install.py
|
||||
1. Single image to 3D Mesh shape only
|
||||
2. 3D Mesh shape + Single reference image to 3D Mesh with RGB texture
|
||||
- Model weights: https://huggingface.co/tencent/Hunyuan3D-2/tree/main
|
||||
- [Workflows](./example_workflows/Hunyuan3D_V2)
|
||||
- [Workflows](https://github.com/DenisKochetov/ComfyUI-3D-Pack/tree/dev/example_workflows/Hunyuan3D_V2)
|
||||
|
||||
<video controls autoplay loop src="https://github.com/user-attachments/assets/da6a8f8b-8dab-4460-90a6-44769ec9bb09"></video>
|
||||
|
||||
|
||||
@@ -5,22 +5,13 @@ import platform
|
||||
import subprocess
|
||||
import time
|
||||
import glob
|
||||
import shutil
|
||||
|
||||
PYTHON_PATH = sys.executable
|
||||
|
||||
# Set to False to disable uv usage
|
||||
ENABLE_UV = True
|
||||
|
||||
def get_pip_install_cmd(args):
|
||||
if ENABLE_UV and shutil.which("uv"):
|
||||
return ["uv", "pip", "install", "--python", PYTHON_PATH] + args
|
||||
return [PYTHON_PATH, "-m", "pip", "install"] + args
|
||||
|
||||
try:
|
||||
from omegaconf import OmegaConf
|
||||
except ImportError as e:
|
||||
subprocess.run(get_pip_install_cmd(["OmegaConf"]))
|
||||
subprocess.run([PYTHON_PATH, "-s", "-m", "pip", "install", "OmegaConf"])
|
||||
from omegaconf import OmegaConf
|
||||
|
||||
BUILD_SCRIPT_ROOT_ABS_PATH = dirname(os.path.abspath(__file__))
|
||||
@@ -230,9 +221,10 @@ def install_remote_packages(package_names):
|
||||
if hasattr(package_attr, "url"):
|
||||
url_option = package_attr.url_option if hasattr(package_attr, "url_option") else "--index-url"
|
||||
|
||||
subprocess.run(get_pip_install_cmd([
|
||||
subprocess.run([
|
||||
PYTHON_PATH, "-s", "-m", "pip", "install",
|
||||
package_name, url_option, package_attr.url
|
||||
]))
|
||||
])
|
||||
continue
|
||||
else:
|
||||
# Check if package is already installed
|
||||
@@ -242,7 +234,7 @@ def install_remote_packages(package_names):
|
||||
|
||||
print(f"Installing {original_package_name}...")
|
||||
|
||||
subprocess.run(get_pip_install_cmd([package_name]))
|
||||
subprocess.run([PYTHON_PATH, "-s", "-m", "pip", "install", package_name])
|
||||
|
||||
def install_platform_packages():
|
||||
if hasattr(build_config, 'platform_packages') and OS_TYPE in build_config.platform_packages:
|
||||
@@ -256,7 +248,7 @@ def install_platform_packages():
|
||||
continue
|
||||
|
||||
print(f"Installing platform package {package}...")
|
||||
subprocess.run(get_pip_install_cmd([package]))
|
||||
subprocess.run([PYTHON_PATH, "-s", "-m", "pip", "install", package])
|
||||
|
||||
def install_isolated_packages(package_names):
|
||||
"""Install packages with special flags like --no-build-isolation"""
|
||||
@@ -273,7 +265,7 @@ def install_isolated_packages(package_names):
|
||||
|
||||
if hasattr(package_attr, "url"):
|
||||
# Build command with install flags
|
||||
cmd = get_pip_install_cmd([])
|
||||
cmd = [PYTHON_PATH, "-s", "-m", "pip", "install"]
|
||||
if hasattr(package_attr, "install_flags"):
|
||||
cmd.extend(package_attr.install_flags)
|
||||
cmd.append(package_attr.url)
|
||||
|
||||
+1
-3
@@ -8,12 +8,10 @@ import logging
|
||||
ROOT_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
MODULE_PATH = os.path.join(ROOT_PATH, "Gen_3D_Modules")
|
||||
MV_ALGO_PATH = os.path.join(ROOT_PATH, "MVs_Algorithms")
|
||||
MESH_PROCESSOR_PATH = os.path.join(ROOT_PATH, "mesh_processor")
|
||||
|
||||
sys.path.insert(0, ROOT_PATH)
|
||||
sys.path.append(ROOT_PATH)
|
||||
sys.path.append(MODULE_PATH)
|
||||
sys.path.append(MV_ALGO_PATH)
|
||||
sys.path.append(MESH_PROCESSOR_PATH)
|
||||
|
||||
import shutil
|
||||
import __main__
|
||||
|
||||
+2
-28
@@ -24,9 +24,6 @@ try:
|
||||
install_platform_packages,
|
||||
install_isolated_packages,
|
||||
wheels_dir_exists_and_not_empty,
|
||||
is_package_installed,
|
||||
get_pip_install_cmd,
|
||||
ENABLE_UV,
|
||||
build_config,
|
||||
PYTHON_PATH,
|
||||
WHEELS_ROOT_ABS_PATH,
|
||||
@@ -40,13 +37,6 @@ try:
|
||||
except ImportError:
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "PyGithub"])
|
||||
|
||||
# Install uv if enabled
|
||||
if ENABLE_UV:
|
||||
try:
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "uv"])
|
||||
except Exception:
|
||||
print("Failed to install uv, continuing with standard pip...")
|
||||
|
||||
def install_local_wheels(builds_dir):
|
||||
"""Install all wheels from local directory"""
|
||||
wheel_files = glob.glob(os.path.join(builds_dir, "**/*.whl"), recursive=True)
|
||||
@@ -56,23 +46,7 @@ try:
|
||||
|
||||
success_count = 0
|
||||
for wheel_path in wheel_files:
|
||||
# Try to parse package name and version from filename to check if installed
|
||||
# Wheel filename format: {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
|
||||
try:
|
||||
filename = os.path.basename(wheel_path)
|
||||
parts = filename.split('-')
|
||||
package_name = parts[0]
|
||||
version = parts[1] if len(parts) > 1 else None
|
||||
|
||||
# Check if installed (check both raw name and normalized name with dashes)
|
||||
if is_package_installed(package_name, version) or is_package_installed(package_name.replace('_', '-'), version):
|
||||
cstr(f"Wheel {filename} is already installed, skipping...").msg.print()
|
||||
success_count += 1
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result = subprocess.run(get_pip_install_cmd(["--no-deps", wheel_path]),
|
||||
result = subprocess.run([PYTHON_PATH, "-s", "-m", "pip", "install", "--no-deps", "--force-reinstall", wheel_path],
|
||||
text=True, capture_output=True)
|
||||
if result.returncode == 0:
|
||||
cstr(f"Successfully installed wheel: {os.path.basename(wheel_path)}").msg.print()
|
||||
@@ -151,7 +125,7 @@ try:
|
||||
except ImportError:
|
||||
cstr(f"Installing {tool}...").msg.print()
|
||||
result = subprocess.run(
|
||||
get_pip_install_cmd(["--upgrade", tool]),
|
||||
[PYTHON_PATH, "-m", "pip", "install", "--upgrade", tool],
|
||||
text=True, capture_output=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import os
|
||||
import cv2
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import NamedTuple
|
||||
import torch
|
||||
from torch import Tensor
|
||||
import trimesh
|
||||
import numpy as np
|
||||
|
||||
@@ -12,11 +11,6 @@ from kiui.typing import *
|
||||
from shared_utils.sh_utils import SH2RGB
|
||||
from shared_utils.image_utils import prepare_torch_img
|
||||
|
||||
# new loader
|
||||
from .io_gltf import load_gltf_or_glb, get_binary_data
|
||||
from .mesh_ops import get_all_meshes_triangles
|
||||
from .accessors import access_data
|
||||
|
||||
|
||||
class Mesh:
|
||||
"""
|
||||
@@ -71,7 +65,7 @@ class Mesh:
|
||||
self.ori_scale = 1
|
||||
|
||||
@classmethod
|
||||
def load(cls, path, resize=True, renormal=True, retex=False, clean=False, bound=0.5, front_dir='+z', use_new_gltf_loader=False, **kwargs):
|
||||
def load(cls, path, resize=True, renormal=True, retex=False, clean=False, bound=0.5, front_dir='+z', **kwargs):
|
||||
"""load mesh from path.
|
||||
|
||||
Args:
|
||||
@@ -82,7 +76,6 @@ class Mesh:
|
||||
clean (bool, optional): perform mesh cleaning at load (e.g., merge close vertices). Defaults to False.
|
||||
bound (float, optional): bound to resize. Defaults to 0.9.
|
||||
front_dir (str, optional): front-view direction of the mesh, should be [+-][xyz][ 123]. Defaults to '+z'.
|
||||
use_new_gltf_loader (bool, optional): use new mesh_processor-based GLTF loader instead of trimesh. Defaults to False.
|
||||
device (torch.device, optional): torch device. Defaults to None.
|
||||
|
||||
Note:
|
||||
@@ -95,9 +88,6 @@ class Mesh:
|
||||
# obj supports face uv
|
||||
if path.endswith(".obj"):
|
||||
mesh = cls.load_obj(path, **kwargs)
|
||||
# Select loader for GLB/GLTF
|
||||
elif use_new_gltf_loader and (path.endswith(".glb") or path.endswith(".gltf")):
|
||||
mesh = cls.load_gltf(path, **kwargs)
|
||||
# trimesh only supports vertex uv, but can load more formats
|
||||
else:
|
||||
mesh = cls.load_trimesh(path, **kwargs)
|
||||
@@ -448,211 +438,13 @@ class Mesh:
|
||||
)
|
||||
|
||||
return mesh
|
||||
|
||||
@classmethod
|
||||
def from_trimesh(cls, trimesh_obj, device=None, use_new_converter=False):
|
||||
"""Create Mesh object from trimesh object.
|
||||
|
||||
Args:
|
||||
trimesh_obj: trimesh.Trimesh or trimesh.Scene
|
||||
device: torch device
|
||||
use_new_converter: use new converter (experimental)
|
||||
|
||||
Returns:
|
||||
Mesh: Mesh object
|
||||
"""
|
||||
if use_new_converter:
|
||||
# New way - try to extract data directly without full dependency on trimesh
|
||||
return cls._from_trimesh_new(trimesh_obj, device)
|
||||
else:
|
||||
# Old way - use load_trimesh
|
||||
return cls.load_trimesh(given_mesh=trimesh_obj, device=device)
|
||||
|
||||
|
||||
@classmethod
|
||||
def load_gltf(cls, path=None, device=None):
|
||||
"""Load mesh with using mesh_processor (new implementation).
|
||||
|
||||
Alternative to load_trimesh() for GLB/GLTF files without dependency on trimesh.
|
||||
|
||||
Args:
|
||||
path (str): path to GLB/GLTF file.
|
||||
device (torch.device, optional): torch device. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Mesh: loaded Mesh object.
|
||||
"""
|
||||
mesh = cls()
|
||||
|
||||
# device
|
||||
if device is None:
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
mesh.device = device
|
||||
|
||||
if path is None:
|
||||
print(f"[load_gltf] path is required")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Load GLTF document
|
||||
doc = load_gltf_or_glb(path)
|
||||
print(f"[load_gltf] loaded GLTF document with {len(doc.meshes())} meshes")
|
||||
|
||||
# Extract geometry of all meshes
|
||||
vertices, faces, mesh_groups = get_all_meshes_triangles(doc, transform_to_global=True)
|
||||
|
||||
if len(vertices) == 0 or len(faces) == 0:
|
||||
print(f"[load_gltf] no valid geometry found")
|
||||
return None
|
||||
|
||||
print(f"[load_gltf] extracted {len(vertices)} vertices, {len(faces)} faces")
|
||||
|
||||
# Convert to torch tensors
|
||||
mesh.v = torch.tensor(vertices, dtype=torch.float32, device=device)
|
||||
mesh.f = torch.tensor(faces, dtype=torch.int32, device=device)
|
||||
|
||||
# Try to extract UV coordinates and normals from the first mesh
|
||||
texcoords = None
|
||||
normals = None
|
||||
vertex_colors = None
|
||||
|
||||
if doc.meshes():
|
||||
first_mesh = doc.meshes()[0]
|
||||
for primitive in first_mesh.get("primitives", []):
|
||||
attributes = primitive.get("attributes", {})
|
||||
|
||||
# UV coordinates
|
||||
if "TEXCOORD_0" in attributes and texcoords is None:
|
||||
try:
|
||||
texcoord_accessor_idx = int(attributes["TEXCOORD_0"])
|
||||
texcoords = access_data(doc, texcoord_accessor_idx).astype(np.float32)
|
||||
# Flip V coordinate (as in trimesh)
|
||||
texcoords[:, 1] = 1.0 - texcoords[:, 1]
|
||||
print(f"[load_gltf] extracted UV coordinates: {texcoords.shape}")
|
||||
except Exception as e:
|
||||
print(f"[load_gltf] failed to extract UV coordinates: {e}")
|
||||
|
||||
# Normals
|
||||
if "NORMAL" in attributes and normals is None:
|
||||
try:
|
||||
normal_accessor_idx = int(attributes["NORMAL"])
|
||||
normals = access_data(doc, normal_accessor_idx).astype(np.float32)
|
||||
print(f"[load_gltf] extracted normals: {normals.shape}")
|
||||
except Exception as e:
|
||||
print(f"[load_gltf] failed to extract normals: {e}")
|
||||
|
||||
# Vertex colors
|
||||
if "COLOR_0" in attributes and vertex_colors is None:
|
||||
try:
|
||||
color_accessor_idx = int(attributes["COLOR_0"])
|
||||
vertex_colors = access_data(doc, color_accessor_idx).astype(np.float32)
|
||||
if vertex_colors.shape[1] > 3:
|
||||
vertex_colors = vertex_colors[:, :3] # Remove alpha channel
|
||||
print(f"[load_gltf] extracted vertex colors: {vertex_colors.shape}")
|
||||
except Exception as e:
|
||||
print(f"[load_gltf] failed to extract vertex colors: {e}")
|
||||
|
||||
# Set UV coordinates
|
||||
mesh.vt = (
|
||||
torch.tensor(texcoords, dtype=torch.float32, device=device)
|
||||
if texcoords is not None
|
||||
else None
|
||||
)
|
||||
mesh.ft = mesh.f if texcoords is not None else None
|
||||
|
||||
# Set normals
|
||||
mesh.vn = (
|
||||
torch.tensor(normals, dtype=torch.float32, device=device)
|
||||
if normals is not None
|
||||
else None
|
||||
)
|
||||
mesh.fn = mesh.f if normals is not None else None
|
||||
|
||||
# Set vertex colors
|
||||
mesh.vc = (
|
||||
torch.tensor(vertex_colors, dtype=torch.float32, device=device)
|
||||
if vertex_colors is not None
|
||||
else None
|
||||
)
|
||||
|
||||
# Try to extract textures and materials
|
||||
should_create_empty_albedo = True
|
||||
|
||||
if doc.materials() and doc.textures() and doc.images():
|
||||
try:
|
||||
# Take the first material
|
||||
material = doc.materials()[0]
|
||||
pbr = material.get("pbrMetallicRoughness", {})
|
||||
|
||||
# Base texture (albedo)
|
||||
base_color_texture = pbr.get("baseColorTexture")
|
||||
if base_color_texture:
|
||||
texture_idx = base_color_texture.get("index", 0)
|
||||
if texture_idx < len(doc.textures()):
|
||||
texture = doc.textures()[texture_idx]
|
||||
image_idx = texture.get("source", 0)
|
||||
if image_idx < len(doc.images()):
|
||||
image_info = doc.images()[image_idx]
|
||||
|
||||
# Extract image from buffer
|
||||
buffer_view_idx = image_info.get("bufferView")
|
||||
if buffer_view_idx is not None:
|
||||
buffer_view = doc.bufferViews()[buffer_view_idx]
|
||||
buffer_idx = buffer_view.get("buffer", 0)
|
||||
byte_offset = buffer_view.get("byteOffset", 0)
|
||||
byte_length = buffer_view.get("byteLength", 0)
|
||||
|
||||
binary_data = get_binary_data(doc, buffer_idx)
|
||||
image_bytes = binary_data[byte_offset:byte_offset + byte_length]
|
||||
|
||||
# Decode image
|
||||
image_array = np.frombuffer(image_bytes, dtype=np.uint8)
|
||||
texture_image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
|
||||
|
||||
if texture_image is not None:
|
||||
texture_image = cv2.cvtColor(texture_image, cv2.COLOR_BGR2RGB)
|
||||
texture_image = texture_image.astype(np.float32) / 255.0
|
||||
mesh.albedo = torch.tensor(texture_image, dtype=torch.float32, device=device).contiguous()
|
||||
should_create_empty_albedo = False
|
||||
print(f"[load_gltf] loaded albedo texture: {texture_image.shape}")
|
||||
|
||||
# Metallic-Roughness texture
|
||||
metallic_roughness_texture = pbr.get("metallicRoughnessTexture")
|
||||
if metallic_roughness_texture and not should_create_empty_albedo:
|
||||
texture_idx = metallic_roughness_texture.get("index", 0)
|
||||
if texture_idx < len(doc.textures()):
|
||||
# Similarly for metallic-roughness
|
||||
# (simplified version for now)
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
print(f"[load_gltf] failed to extract textures: {e}")
|
||||
|
||||
# Create empty texture if not able to load
|
||||
if should_create_empty_albedo:
|
||||
mesh.set_new_albedo(1024, 1024)
|
||||
print(f"[load_gltf] created empty albedo texture with shape: {mesh.albedo.shape}")
|
||||
|
||||
return mesh
|
||||
|
||||
except Exception as e:
|
||||
print(f"[load_gltf] failed to load {path}: {e}")
|
||||
return None
|
||||
|
||||
def set_new_albedo(self, res_H, res_W):
|
||||
if self.albedo is None:
|
||||
texture = np.ones((res_H, res_W, 3), dtype=np.float32) * np.array([0.5, 0.5, 0.5])
|
||||
self.albedo = torch.tensor(texture, dtype=torch.float32, device=self.device)
|
||||
else:
|
||||
self.albedo = prepare_torch_img(self.albedo.unsqueeze(0), res_H, res_W, self.device).squeeze(0).permute(1, 2, 0).contiguous() # (1, 3, H, W) -> (H, W, 3)
|
||||
|
||||
def _create_empty_albedo_fast(self):
|
||||
"""Create empty albedo texture quickly (without prepare_torch_img)"""
|
||||
if self.albedo is None:
|
||||
texture = np.ones((1024, 1024, 3), dtype=np.float32) * np.array([0.5, 0.5, 0.5])
|
||||
self.albedo = torch.tensor(texture, dtype=torch.float32, device=self.device)
|
||||
print(f"[Mesh] Fast empty texture: {self.albedo.shape}")
|
||||
|
||||
# aabb
|
||||
def aabb(self):
|
||||
@@ -833,30 +625,8 @@ class Mesh:
|
||||
v_np = self.v.detach().cpu().numpy()
|
||||
f_np = self.f.detach().cpu().numpy()
|
||||
|
||||
# Simple PLY writing without trimesh
|
||||
self._write_ply_simple(path, v_np, f_np)
|
||||
|
||||
def _write_ply_simple(self, path, vertices, faces):
|
||||
"""Simple PLY writing without trimesh"""
|
||||
with open(path, 'w') as f:
|
||||
# PLY header
|
||||
f.write("ply\n")
|
||||
f.write("format ascii 1.0\n")
|
||||
f.write(f"element vertex {len(vertices)}\n")
|
||||
f.write("property float x\n")
|
||||
f.write("property float y\n")
|
||||
f.write("property float z\n")
|
||||
f.write(f"element face {len(faces)}\n")
|
||||
f.write("property list uchar int vertex_indices\n")
|
||||
f.write("end_header\n")
|
||||
|
||||
# Vertices
|
||||
for v in vertices:
|
||||
f.write(f"{v[0]} {v[1]} {v[2]}\n")
|
||||
|
||||
# Faces
|
||||
for face in faces:
|
||||
f.write(f"3 {face[0]} {face[1]} {face[2]}\n")
|
||||
_mesh = trimesh.Trimesh(vertices=v_np, faces=f_np)
|
||||
_mesh.export(path)
|
||||
|
||||
|
||||
def write_glb(self, path):
|
||||
@@ -1127,9 +897,9 @@ class Mesh:
|
||||
shs = np.random.random((num_pts, 3)) / 255.0
|
||||
normals = np.zeros((num_pts, 3))
|
||||
pcd = PointCloud(points=xyz, colors=SH2RGB(shs), normals=normals)
|
||||
return pcd
|
||||
|
||||
|
||||
return pcd
|
||||
|
||||
|
||||
class PointCloud(NamedTuple):
|
||||
points: np.array
|
||||
colors: np.array
|
||||
@@ -598,66 +598,6 @@ def K_nearest_neighbors_func(
|
||||
else:
|
||||
return nn[0, :, :, :], idx[0, :, :], dist[0, :, :]
|
||||
|
||||
|
||||
def reduce_mesh_faces_inplace(mesh_obj, max_faces: int = 40000, backend="pymeshlab", optimalplacement=True, verbose=True):
|
||||
"""
|
||||
Reduce faces for Mesh/FastMesh objects using decimate_mesh (in-place modification)
|
||||
|
||||
Args:
|
||||
mesh_obj: Mesh or FastMesh object (modified in-place)
|
||||
max_faces: maximum number of faces
|
||||
backend: algorithm backend ("pymeshlab" or "pyfqmr")
|
||||
optimalplacement: optimal placement for decimation
|
||||
verbose: print progress
|
||||
|
||||
Returns:
|
||||
Same mesh object (modified in-place)
|
||||
"""
|
||||
|
||||
faces_before = mesh_obj.f.shape[0]
|
||||
|
||||
# Skip if already under limit
|
||||
if faces_before <= max_faces:
|
||||
if verbose:
|
||||
print(f"Mesh already has {faces_before} faces (≤ {max_faces}) - skipping")
|
||||
return mesh_obj
|
||||
|
||||
try:
|
||||
|
||||
# Convert to numpy
|
||||
vertices_np = mesh_obj.v.detach().cpu().numpy().astype(np.float32)
|
||||
faces_np = mesh_obj.f.detach().cpu().numpy().astype(np.int32)
|
||||
|
||||
# Apply decimation
|
||||
reduced_vertices, reduced_faces = decimate_mesh(
|
||||
vertices_np, faces_np,
|
||||
target=max_faces,
|
||||
backend=backend,
|
||||
optimalplacement=optimalplacement,
|
||||
verbose=verbose
|
||||
)
|
||||
|
||||
# Update existing mesh object (in-place)
|
||||
device = mesh_obj.v.device
|
||||
mesh_obj.v = torch.from_numpy(reduced_vertices).contiguous().float().to(device)
|
||||
mesh_obj.f = torch.from_numpy(reduced_faces).contiguous().int().to(device)
|
||||
|
||||
# Recalculate normals
|
||||
mesh_obj.auto_normal()
|
||||
|
||||
# Stats
|
||||
if verbose:
|
||||
faces_after = mesh_obj.f.shape[0]
|
||||
reduction_ratio = ((faces_before - faces_after) / faces_before) * 100
|
||||
|
||||
return mesh_obj
|
||||
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
print(f"Face reduction error: {e}, keep original mesh")
|
||||
return mesh_obj
|
||||
|
||||
|
||||
def interpolate_texture_map_attr(mesh, texture_size: int = 256, batch_size: int = 64, interpolate_color=True, interpolate_position=False):
|
||||
# Get UV coordinates and faces
|
||||
if mesh.vt is None:
|
||||
@@ -1,85 +0,0 @@
|
||||
from .io_gltf import (
|
||||
GltfDocument,
|
||||
load_gltf_or_glb,
|
||||
save_glb,
|
||||
)
|
||||
from .accessors import (
|
||||
access_data,
|
||||
update_accessor_binary_data,
|
||||
append_accessor_and_bufferview,
|
||||
recompute_accessor_min_max,
|
||||
)
|
||||
from .transforms import (
|
||||
compose_matrix,
|
||||
quaternion_matrix,
|
||||
direction_to_quaternion,
|
||||
parse_node_transform,
|
||||
compute_global_transform,
|
||||
compute_mesh_global_transforms,
|
||||
transform_positions_inplace,
|
||||
set_node_trs_identity,
|
||||
transform_local_vertices_to_world,
|
||||
)
|
||||
from .mesh_ops import (
|
||||
get_all_meshes_triangles,
|
||||
merge_all_meshes_to_single,
|
||||
update_nodes_for_merged_mesh,
|
||||
)
|
||||
from .rigging_ops import (
|
||||
check_input_file_compatibility,
|
||||
clean_rigging_data_from_gltf,
|
||||
clean_skin_data_from_gltf,
|
||||
insert_generated_data_to_gltf,
|
||||
)
|
||||
from .validate import validate_before_save
|
||||
from .mesh import (
|
||||
Mesh,
|
||||
PointCloud,
|
||||
)
|
||||
from .fastmesh import FastMesh
|
||||
|
||||
__all__ = [
|
||||
"GltfDocument",
|
||||
"load_gltf_or_glb",
|
||||
"save_glb",
|
||||
"access_data",
|
||||
"update_accessor_binary_data",
|
||||
"append_accessor_and_bufferview",
|
||||
"recompute_accessor_min_max",
|
||||
"compose_matrix",
|
||||
"quaternion_matrix",
|
||||
"direction_to_quaternion",
|
||||
"parse_node_transform",
|
||||
"compute_global_transform",
|
||||
"compute_mesh_global_transforms",
|
||||
"transform_positions_inplace",
|
||||
"set_node_trs_identity",
|
||||
"transform_local_vertices_to_world",
|
||||
"get_all_meshes_triangles",
|
||||
"merge_all_meshes_to_single",
|
||||
"update_nodes_for_merged_mesh",
|
||||
"check_input_file_compatibility",
|
||||
"clean_rigging_data_from_gltf",
|
||||
"clean_skin_data_from_gltf",
|
||||
"insert_generated_data_to_gltf",
|
||||
"validate_before_save",
|
||||
"Mesh",
|
||||
"FastMesh",
|
||||
"PointCloud",
|
||||
]
|
||||
|
||||
try:
|
||||
from .export_utils import (
|
||||
export_to_fastmesh,
|
||||
export_to_mesh,
|
||||
export_to_mesh_ultra_fast,
|
||||
)
|
||||
__all__.extend([
|
||||
"export_to_fastmesh",
|
||||
"export_to_mesh",
|
||||
"export_to_mesh_ultra_fast",
|
||||
])
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .io_gltf import GltfDocument, get_binary_data
|
||||
from .utils import ensure_little_endian, as_uint8_buffer
|
||||
|
||||
|
||||
_ELEMENT_SHAPES: Dict[str, Tuple[int, ...]] = {
|
||||
"SCALAR": (1,),
|
||||
"VEC2": (2,),
|
||||
"VEC3": (3,),
|
||||
"VEC4": (4,),
|
||||
"MAT2": (2, 2),
|
||||
"MAT3": (3, 3),
|
||||
"MAT4": (4, 4),
|
||||
}
|
||||
|
||||
_ITEM_TYPES: Dict[int, Any] = {
|
||||
5120: np.int8,
|
||||
5121: np.uint8,
|
||||
5122: np.int16,
|
||||
5123: np.uint16,
|
||||
5125: np.uint32,
|
||||
5126: np.float32,
|
||||
}
|
||||
|
||||
|
||||
def _dtype_itemsize(component_type: int) -> int:
|
||||
return np.dtype(_ITEM_TYPES[component_type]).itemsize
|
||||
|
||||
|
||||
def _validate_stride(stride: int) -> None:
|
||||
if stride != 0 and stride < 4:
|
||||
raise ValueError("Stride is too small.")
|
||||
if stride > 252:
|
||||
raise ValueError("Stride is too big.")
|
||||
|
||||
|
||||
def access_data(doc: GltfDocument, accessor_index: int) -> np.ndarray:
|
||||
accessor = doc.accessors()[accessor_index]
|
||||
buffer_view_index = accessor.get("bufferView")
|
||||
if buffer_view_index is None:
|
||||
raise NotImplementedError("Undefined buffer view")
|
||||
|
||||
accessor_byte_offset = int(accessor.get("byteOffset", 0))
|
||||
component_type = int(accessor["componentType"]) # glTF enum
|
||||
element_count = int(accessor["count"]) # number of elements
|
||||
element_type = accessor["type"]
|
||||
|
||||
if accessor.get("sparse") is not None:
|
||||
raise NotImplementedError("Sparse accessors are not supported")
|
||||
|
||||
buffer_view = doc.bufferViews()[buffer_view_index]
|
||||
buffer_index = int(buffer_view["buffer"]) # which buffer
|
||||
buffer_byte_length = int(buffer_view["byteLength"]) # length of this view
|
||||
element_byte_offset = int(buffer_view.get("byteOffset", 0))
|
||||
element_byte_stride = int(buffer_view.get("byteStride", 0))
|
||||
_validate_stride(element_byte_stride)
|
||||
|
||||
element_shape = _ELEMENT_SHAPES[element_type]
|
||||
item_dtype = np.dtype(_ITEM_TYPES[component_type])
|
||||
item_count = int(np.prod(element_shape))
|
||||
item_size = item_dtype.itemsize
|
||||
|
||||
size = element_count * item_count * item_size
|
||||
if size > buffer_byte_length:
|
||||
raise ValueError("Buffer did not have enough data for the accessor")
|
||||
|
||||
buffers = doc.buffers()
|
||||
if buffer_index >= len(buffers):
|
||||
raise ValueError("Invalid buffer index")
|
||||
|
||||
binary_data = get_binary_data(doc, buffer_index)
|
||||
if len(binary_data) < buffers[buffer_index].get("byteLength", 0):
|
||||
raise ValueError("Not enough binary data for the buffer")
|
||||
|
||||
if element_byte_stride == 0:
|
||||
element_byte_stride = item_size * item_count
|
||||
if element_byte_stride < item_size * item_count:
|
||||
raise ValueError("Items should not overlap")
|
||||
|
||||
# force little-endian
|
||||
item_dtype_str = np.dtype(item_dtype).newbyteorder("<").str
|
||||
|
||||
dtype = np.dtype(
|
||||
{
|
||||
"names": ["element"],
|
||||
"formats": [str(element_shape) + item_dtype_str],
|
||||
"offsets": [0],
|
||||
"itemsize": element_byte_stride,
|
||||
}
|
||||
)
|
||||
|
||||
byte_offset = accessor_byte_offset + element_byte_offset
|
||||
if byte_offset % item_size != 0:
|
||||
raise ValueError("Misaligned data")
|
||||
byte_length = element_count * element_byte_stride
|
||||
|
||||
view = binary_data[byte_offset : byte_offset + byte_length].view(dtype)["element"]
|
||||
if element_type in ("MAT2", "MAT3", "MAT4"):
|
||||
view = np.transpose(view, (0, 2, 1))
|
||||
return view
|
||||
|
||||
|
||||
def update_accessor_binary_data(doc: GltfDocument, accessor_idx: int, new_data: np.ndarray) -> None:
|
||||
accessor = doc.accessors()[accessor_idx]
|
||||
buffer_view_idx = accessor.get("bufferView")
|
||||
if buffer_view_idx is None:
|
||||
raise NotImplementedError("Undefined buffer view")
|
||||
|
||||
buffer_view = doc.bufferViews()[buffer_view_idx]
|
||||
buffer_idx = int(buffer_view["buffer"]) # which buffer
|
||||
byte_offset = int(buffer_view.get("byteOffset", 0))
|
||||
accessor_byte_offset = int(accessor.get("byteOffset", 0))
|
||||
|
||||
binary_data = doc.binary(buffer_idx)
|
||||
# ensure little-endian float32 or appropriate component
|
||||
new_bytes = ensure_little_endian(new_data).tobytes()
|
||||
|
||||
start_pos = byte_offset + accessor_byte_offset
|
||||
end_pos = start_pos + len(new_bytes)
|
||||
|
||||
new_binary_data = binary_data.copy()
|
||||
new_binary_data[start_pos:end_pos] = as_uint8_buffer(np.frombuffer(new_bytes, dtype=np.uint8))
|
||||
doc.set_binary(buffer_idx, new_binary_data)
|
||||
doc.update_buffer_length(buffer_idx)
|
||||
|
||||
|
||||
def recompute_accessor_min_max(doc: GltfDocument, accessor_idx: int) -> None:
|
||||
accessor = doc.accessors()[accessor_idx]
|
||||
if accessor.get("type") != "VEC3":
|
||||
return
|
||||
data = access_data(doc, accessor_idx)
|
||||
accessor["min"] = data.min(axis=0).tolist()
|
||||
accessor["max"] = data.max(axis=0).tolist()
|
||||
|
||||
|
||||
def append_accessor_and_bufferview(
|
||||
doc: GltfDocument,
|
||||
array: np.ndarray,
|
||||
component_type: int,
|
||||
element_type: str,
|
||||
target: Optional[int] = None,
|
||||
) -> Tuple[int, int]:
|
||||
"""Append array bytes into buffer 0 and create bufferView + accessor.
|
||||
|
||||
Returns: (accessor_index, buffer_view_index)
|
||||
"""
|
||||
if element_type not in _ELEMENT_SHAPES:
|
||||
raise ValueError("Invalid element type")
|
||||
if component_type not in _ITEM_TYPES:
|
||||
raise ValueError("Invalid component type")
|
||||
|
||||
array_le = ensure_little_endian(array, np.dtype(_ITEM_TYPES[component_type]))
|
||||
element_shape = _ELEMENT_SHAPES[element_type]
|
||||
|
||||
# validate last dimension matches element
|
||||
num_components = int(np.prod(element_shape))
|
||||
if array_le.size % num_components != 0:
|
||||
raise ValueError("Array size is not divisible by element components")
|
||||
|
||||
if array_le.ndim == 1:
|
||||
count = array_le.size // num_components
|
||||
else:
|
||||
count = array_le.shape[0]
|
||||
|
||||
# append to binary buffer 0
|
||||
buf0 = doc.binary(0)
|
||||
byte_offset = int(buf0.nbytes)
|
||||
bytes_data = array_le.tobytes()
|
||||
buf0_new = np.concatenate([buf0, np.frombuffer(bytes_data, dtype=np.uint8)])
|
||||
doc.set_binary(0, buf0_new)
|
||||
|
||||
# create bufferView
|
||||
bv_index = len(doc.bufferViews())
|
||||
byte_length = len(bytes_data)
|
||||
bufferview: Dict[str, Any] = {
|
||||
"buffer": 0,
|
||||
"byteOffset": byte_offset,
|
||||
"byteLength": byte_length,
|
||||
}
|
||||
if target is not None:
|
||||
bufferview["target"] = target
|
||||
doc.bufferViews().append(bufferview)
|
||||
|
||||
# create accessor
|
||||
acc_index = len(doc.accessors())
|
||||
accessor: Dict[str, Any] = {
|
||||
"bufferView": bv_index,
|
||||
"componentType": int(component_type),
|
||||
"count": int(count),
|
||||
"type": element_type,
|
||||
}
|
||||
# if POSITION like, store min/max
|
||||
if element_type == "VEC3":
|
||||
arr2d = array_le.reshape((-1, 3))
|
||||
accessor["min"] = arr2d.min(axis=0).tolist()
|
||||
accessor["max"] = arr2d.max(axis=0).tolist()
|
||||
doc.accessors().append(accessor)
|
||||
|
||||
doc.update_buffer_length(0)
|
||||
return acc_index, bv_index
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
"""
|
||||
Utilities for exporting from pipelines to our mesh objects
|
||||
Direct functions WITHOUT trimesh dependency
|
||||
"""
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
from .mesh import Mesh
|
||||
from .fastmesh import FastMesh
|
||||
|
||||
|
||||
def export_to_fastmesh(mesh_output, device=None):
|
||||
"""
|
||||
Export from pipeline to FastMesh
|
||||
|
||||
Args:
|
||||
mesh_output: object with mesh_v and mesh_f from pipeline
|
||||
device: torch device
|
||||
|
||||
Returns:
|
||||
FastMesh object or list of FastMesh objects
|
||||
"""
|
||||
|
||||
if device is None:
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
if isinstance(mesh_output, list):
|
||||
outputs = []
|
||||
for mesh in mesh_output:
|
||||
if mesh is None:
|
||||
outputs.append(None)
|
||||
else:
|
||||
fastmesh = _convert_single_mesh_to_fastmesh(mesh, device)
|
||||
outputs.append(fastmesh)
|
||||
return outputs
|
||||
else:
|
||||
return _convert_single_mesh_to_fastmesh(mesh_output, device)
|
||||
|
||||
|
||||
def export_to_mesh(mesh_output, device=None):
|
||||
"""
|
||||
Export from pipeline to standard Mesh
|
||||
|
||||
Args:
|
||||
mesh_output: object with mesh_v and mesh_f from pipeline
|
||||
device: torch device
|
||||
|
||||
Returns:
|
||||
Mesh object or list of Mesh objects
|
||||
"""
|
||||
|
||||
if device is None:
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
if isinstance(mesh_output, list):
|
||||
outputs = []
|
||||
for mesh in mesh_output:
|
||||
if mesh is None:
|
||||
outputs.append(None)
|
||||
else:
|
||||
standard_mesh = _convert_single_mesh_to_mesh(mesh, device)
|
||||
outputs.append(standard_mesh)
|
||||
return outputs
|
||||
else:
|
||||
return _convert_single_mesh_to_mesh(mesh_output, device)
|
||||
|
||||
|
||||
def _convert_single_mesh_to_fastmesh(mesh_obj, device):
|
||||
"""Convert one mesh object to FastMesh"""
|
||||
|
||||
try:
|
||||
# Extract vertices and faces
|
||||
vertices = mesh_obj.mesh_v
|
||||
faces = mesh_obj.mesh_f
|
||||
|
||||
# Flip faces (as in original function)
|
||||
faces = faces[:, ::-1]
|
||||
|
||||
# Convert to numpy if needed
|
||||
if hasattr(vertices, 'cpu'):
|
||||
vertices = vertices.cpu().numpy()
|
||||
if hasattr(faces, 'cpu'):
|
||||
faces = faces.cpu().numpy()
|
||||
|
||||
vertices = np.array(vertices, dtype=np.float32)
|
||||
faces = np.array(faces, dtype=np.int32)
|
||||
|
||||
print(f"[export_to_fastmesh] Convert: {vertices.shape[0]} vertices, {faces.shape[0]} faces")
|
||||
|
||||
fastmesh = FastMesh(device=device)
|
||||
fastmesh.v = torch.tensor(vertices, dtype=torch.float32, device=device)
|
||||
fastmesh.f = torch.tensor(faces, dtype=torch.int32, device=device)
|
||||
|
||||
fastmesh.auto_normal()
|
||||
|
||||
fastmesh._create_empty_albedo()
|
||||
|
||||
print(f"[export_to_fastmesh] FastMesh created successfully")
|
||||
return fastmesh
|
||||
|
||||
except Exception as e:
|
||||
print(f"[export_to_fastmesh] Error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _convert_single_mesh_to_mesh(mesh_obj, device):
|
||||
"""Convert one mesh object to standard Mesh"""
|
||||
|
||||
try:
|
||||
vertices = mesh_obj.mesh_v
|
||||
faces = mesh_obj.mesh_f
|
||||
|
||||
faces = faces[:, ::-1]
|
||||
|
||||
if hasattr(vertices, 'cpu'):
|
||||
vertices = vertices.cpu().numpy()
|
||||
if hasattr(faces, 'cpu'):
|
||||
faces = faces.cpu().numpy()
|
||||
|
||||
vertices = np.array(vertices, dtype=np.float32)
|
||||
faces = np.array(faces, dtype=np.int32)
|
||||
|
||||
print(f"[export_to_mesh] Convert: {vertices.shape[0]} vertices, {faces.shape[0]} faces")
|
||||
|
||||
mesh = Mesh(device=device)
|
||||
mesh.v = torch.tensor(vertices, dtype=torch.float32, device=device)
|
||||
mesh.f = torch.tensor(faces, dtype=torch.int32, device=device)
|
||||
|
||||
mesh.auto_normal()
|
||||
|
||||
mesh._create_empty_albedo_fast()
|
||||
|
||||
print(f"[export_to_mesh] Mesh created successfully")
|
||||
return mesh
|
||||
|
||||
except Exception as e:
|
||||
print(f"[export_to_mesh] Error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def export_to_mesh_ultra_fast(mesh_output, device=None):
|
||||
"""
|
||||
Ultra-fast export to standard Mesh WITHOUT any slow operations
|
||||
"""
|
||||
|
||||
if device is None:
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
try:
|
||||
# Process list or single object
|
||||
if isinstance(mesh_output, list):
|
||||
return [_convert_single_mesh_to_mesh_ultra_fast(mesh, device) for mesh in mesh_output]
|
||||
else:
|
||||
return _convert_single_mesh_to_mesh_ultra_fast(mesh_output, device)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[export_to_mesh_ultra_fast] Error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _convert_single_mesh_to_mesh_ultra_fast(mesh_obj, device):
|
||||
"""Convert one mesh object to standard Mesh Ultra-fast"""
|
||||
|
||||
try:
|
||||
# Extract vertices and faces
|
||||
vertices = mesh_obj.mesh_v
|
||||
faces = mesh_obj.mesh_f
|
||||
|
||||
# Flip faces (as in original function)
|
||||
faces = faces[:, ::-1]
|
||||
|
||||
# Convert to numpy if needed
|
||||
if hasattr(vertices, 'cpu'):
|
||||
vertices = vertices.cpu().numpy()
|
||||
if hasattr(faces, 'cpu'):
|
||||
faces = faces.cpu().numpy()
|
||||
|
||||
vertices = np.array(vertices, dtype=np.float32)
|
||||
faces = np.array(faces, dtype=np.int32)
|
||||
|
||||
print(f"[export_to_mesh_ultra_fast] Convert: {vertices.shape[0]} vertices, {faces.shape[0]} faces")
|
||||
|
||||
# Create standard Mesh Minimal
|
||||
mesh = Mesh(device=device)
|
||||
mesh.v = torch.tensor(vertices, dtype=torch.float32, device=device)
|
||||
mesh.f = torch.tensor(faces, dtype=torch.int32, device=device)
|
||||
|
||||
print(f"[export_to_mesh_ultra_fast] Mesh created instantly")
|
||||
return mesh
|
||||
|
||||
except Exception as e:
|
||||
print(f"[export_to_mesh_ultra_fast] Error: {e}")
|
||||
return None
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,42 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .io_gltf import GltfDocument, load_gltf_or_glb, save_glb
|
||||
from .mesh_ops import get_all_meshes_triangles, merge_all_meshes_to_single
|
||||
from .transforms import (
|
||||
compose_matrix,
|
||||
quaternion_matrix,
|
||||
direction_to_quaternion,
|
||||
compute_mesh_global_transforms,
|
||||
transform_local_vertices_to_world,
|
||||
preprocess_transforms_inplace,
|
||||
)
|
||||
from .rigging_ops import (
|
||||
check_input_file_compatibility,
|
||||
clean_rigging_data_from_gltf,
|
||||
insert_generated_data_to_gltf,
|
||||
)
|
||||
from .validate import validate_before_save
|
||||
|
||||
|
||||
__all__ = [
|
||||
"GltfDocument",
|
||||
"load_gltf_or_glb",
|
||||
"save_glb",
|
||||
"get_all_meshes_triangles",
|
||||
"merge_all_meshes_to_single",
|
||||
"compose_matrix",
|
||||
"quaternion_matrix",
|
||||
"direction_to_quaternion",
|
||||
"compute_mesh_global_transforms",
|
||||
"transform_local_vertices_to_world",
|
||||
"preprocess_transforms_inplace",
|
||||
"check_input_file_compatibility",
|
||||
"clean_rigging_data_from_gltf",
|
||||
"insert_generated_data_to_gltf",
|
||||
"validate_before_save",
|
||||
]
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
from base64 import b64decode
|
||||
from typing import Any, BinaryIO, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .utils import NpEncoder, pad_to_4_bytes, warn
|
||||
|
||||
|
||||
GLTF_MAGIC = 0x46546C67
|
||||
JSON_CHUNK_TYPE = 0x4E4F534A
|
||||
BINARY_CHUNK_TYPE = 0x004E4942
|
||||
DATA_URI_PREFIX = "data:application/octet-stream;base64,"
|
||||
|
||||
|
||||
def read_header(stream: BinaryIO) -> Optional[Tuple[int, int]]:
|
||||
header = stream.read(12)
|
||||
if len(header) != 12:
|
||||
return None
|
||||
magic, version, length = struct.unpack("<III", header)
|
||||
if magic != GLTF_MAGIC:
|
||||
return None
|
||||
return version, length
|
||||
|
||||
|
||||
def read_chunks(stream: BinaryIO, length: int) -> Optional[Tuple[Dict[str, Any], np.ndarray]]:
|
||||
json_data: Optional[Dict[str, Any]] = None
|
||||
binary_data: Optional[np.ndarray] = None
|
||||
|
||||
while stream.tell() < length:
|
||||
chunk_header = stream.read(8)
|
||||
if len(chunk_header) != 8:
|
||||
warn(f"Invalid chunk header size: {len(chunk_header)} bytes (expected 8)")
|
||||
break
|
||||
chunk_length, chunk_type = struct.unpack("<II", chunk_header)
|
||||
chunk_data = stream.read(chunk_length)
|
||||
if len(chunk_data) != chunk_length:
|
||||
warn(
|
||||
f"Incomplete chunk data: {len(chunk_data)} bytes (expected {chunk_length})"
|
||||
)
|
||||
return None
|
||||
|
||||
if chunk_type == JSON_CHUNK_TYPE:
|
||||
json_data = json.loads(chunk_data)
|
||||
elif chunk_type == BINARY_CHUNK_TYPE:
|
||||
binary_data = np.frombuffer(chunk_data, dtype=np.uint8)
|
||||
else:
|
||||
warn("Unsupported chunk type")
|
||||
return None
|
||||
|
||||
if json_data is None:
|
||||
raise ValueError("Missing json header")
|
||||
|
||||
if binary_data is None:
|
||||
binary_data = np.array([], dtype=np.uint8)
|
||||
|
||||
return json_data, binary_data
|
||||
|
||||
|
||||
class GltfDocument:
|
||||
"""Minimal GLTF/GLB container with helpers over core collections."""
|
||||
|
||||
def __init__(self, json_data: Dict[str, Any], binary_buffers: Optional[Dict[int, np.ndarray]] = None):
|
||||
self._json = json_data
|
||||
self._bin: Dict[int, np.ndarray] = binary_buffers or {0: np.array([], dtype=np.uint8)}
|
||||
|
||||
# Accessors to top-level collections
|
||||
def json(self) -> Dict[str, Any]:
|
||||
return self._json
|
||||
|
||||
def buffers(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("buffers", [])
|
||||
|
||||
def bufferViews(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("bufferViews", [])
|
||||
|
||||
def accessors(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("accessors", [])
|
||||
|
||||
def nodes(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("nodes", [])
|
||||
|
||||
def meshes(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("meshes", [])
|
||||
|
||||
def scenes(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("scenes", [])
|
||||
|
||||
def materials(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("materials", [])
|
||||
|
||||
def images(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("images", [])
|
||||
|
||||
def textures(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("textures", [])
|
||||
|
||||
def samplers(self) -> List[Dict[str, Any]]:
|
||||
return self._json.setdefault("samplers", [])
|
||||
|
||||
def binary(self, index: int = 0) -> np.ndarray:
|
||||
return self._bin.setdefault(index, np.array([], dtype=np.uint8))
|
||||
|
||||
def set_binary(self, index: int, data: np.ndarray) -> None:
|
||||
self._bin[index] = data
|
||||
|
||||
def update_buffer_length(self, buffer_index: int = 0) -> None:
|
||||
if not self.buffers():
|
||||
self._json["buffers"] = [{"byteLength": int(self.binary(buffer_index).nbytes)}]
|
||||
else:
|
||||
self.buffers()[buffer_index]["byteLength"] = int(self.binary(buffer_index).nbytes)
|
||||
|
||||
|
||||
def get_binary_data(doc: GltfDocument, buffer_index: int) -> np.ndarray:
|
||||
"""Return binary data for a buffer index.
|
||||
|
||||
Supports embedded base64 buffers in GLTF as well as GLB's main buffer.
|
||||
"""
|
||||
buffers = doc.buffers()
|
||||
if buffer_index >= len(buffers):
|
||||
raise IndexError("Buffer index out of range")
|
||||
binary = doc._bin.get(buffer_index)
|
||||
if binary is not None and binary.size > 0:
|
||||
return binary
|
||||
uri = buffers[buffer_index].get("uri")
|
||||
if uri and uri.startswith(DATA_URI_PREFIX):
|
||||
binary_data = b64decode(uri[len(DATA_URI_PREFIX) :])
|
||||
arr = np.frombuffer(binary_data, dtype=np.uint8)
|
||||
doc.set_binary(buffer_index, arr)
|
||||
return arr
|
||||
# if no uri, return existing buffer (may be empty for GLB until provided)
|
||||
return doc.binary(buffer_index)
|
||||
|
||||
|
||||
def load_gltf_or_glb(path: str) -> GltfDocument:
|
||||
with open(path, "rb") as f:
|
||||
stream = f
|
||||
version_and_length = read_header(stream)
|
||||
if version_and_length is None: # GLTF JSON
|
||||
stream.seek(0)
|
||||
json_data = json.load(stream)
|
||||
# embedded or external buffers will be resolved lazily
|
||||
return GltfDocument(json_data=json_data)
|
||||
version, length = version_and_length
|
||||
if version != 2:
|
||||
raise ValueError(f"Only GLB version 2 is supported, found version {version}")
|
||||
json_data, binary_data = read_chunks(stream, length)
|
||||
return GltfDocument(json_data=json_data, binary_buffers={0: binary_data})
|
||||
|
||||
|
||||
def save_glb(doc: GltfDocument, stream: BinaryIO) -> None:
|
||||
"""Write GLB v2 with proper 4-byte alignment for JSON and BIN chunks."""
|
||||
# ensure buffers length is correct for buffer 0
|
||||
doc.update_buffer_length(0)
|
||||
|
||||
json_bytes = json.dumps(doc.json(), cls=NpEncoder, separators=(",", ":")).encode("utf-8")
|
||||
json_padded = pad_to_4_bytes(json_bytes, b"\x20")
|
||||
|
||||
bin_bytes = bytes(doc.binary(0))
|
||||
bin_padded = pad_to_4_bytes(bin_bytes, b"\x00")
|
||||
|
||||
version = 2
|
||||
file_header_size = 12
|
||||
chunk_header_size = 8
|
||||
file_length = (
|
||||
file_header_size
|
||||
+ chunk_header_size
|
||||
+ len(json_padded)
|
||||
+ chunk_header_size
|
||||
+ len(bin_padded)
|
||||
)
|
||||
|
||||
# write GLB header
|
||||
stream.write(struct.pack("<III", GLTF_MAGIC, version, file_length))
|
||||
|
||||
# JSON chunk
|
||||
stream.write(struct.pack("<II", len(json_padded), JSON_CHUNK_TYPE))
|
||||
stream.write(json_padded)
|
||||
|
||||
# BIN chunk
|
||||
stream.write(struct.pack("<II", len(bin_padded), BINARY_CHUNK_TYPE))
|
||||
stream.write(bin_padded)
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .io_gltf import GltfDocument
|
||||
from .accessors import access_data, append_accessor_and_bufferview
|
||||
from .transforms import compute_mesh_global_transforms, transform_local_vertices_to_world, set_node_trs_identity
|
||||
|
||||
|
||||
def get_all_meshes_triangles(doc: GltfDocument, transform_to_global: bool = True):
|
||||
if not doc.meshes():
|
||||
raise ValueError("No meshes found in the GLB/GLTF.")
|
||||
|
||||
all_vertices_groups: List[Dict[str, Any]] = []
|
||||
vertices_count = 0
|
||||
all_pos_data: List[np.ndarray] = []
|
||||
all_indices_data: List[np.ndarray] = []
|
||||
|
||||
mesh_transforms: Dict[int, np.ndarray] = {}
|
||||
if transform_to_global:
|
||||
mesh_transforms = compute_mesh_global_transforms(doc)
|
||||
|
||||
for i_m, mesh_json in enumerate(doc.meshes()):
|
||||
for i_p, primitive in enumerate(mesh_json.get("primitives", [])):
|
||||
if "attributes" in primitive and "indices" in primitive:
|
||||
attributes = primitive["attributes"]
|
||||
if "POSITION" in attributes:
|
||||
pos_accessor_index = int(attributes["POSITION"])
|
||||
pos_data = access_data(doc, pos_accessor_index)
|
||||
if pos_data.dtype != np.float32:
|
||||
pos_data = pos_data.astype(np.float32)
|
||||
|
||||
if transform_to_global and i_m in mesh_transforms:
|
||||
transform_matrix = mesh_transforms[i_m]
|
||||
identity = np.eye(4, dtype=np.float32)
|
||||
if not np.allclose(transform_matrix, identity, atol=1e-6):
|
||||
fake_node = {"matrix": transform_matrix.flatten(order="F").tolist()}
|
||||
pos_data = transform_local_vertices_to_world(pos_data, fake_node)
|
||||
|
||||
all_vertices_groups.append({
|
||||
"mesh_index": i_m,
|
||||
"primitive_index": i_p,
|
||||
"vertices_range": (vertices_count, vertices_count + pos_data.shape[0]),
|
||||
"global_transform_applied": transform_to_global and i_m in mesh_transforms,
|
||||
})
|
||||
all_pos_data.append(pos_data)
|
||||
|
||||
indices_accessor_index = int(primitive["indices"])
|
||||
indices_data = access_data(doc, indices_accessor_index)
|
||||
if indices_data.dtype != np.int64:
|
||||
indices_data = indices_data.astype(np.int64)
|
||||
all_indices_data.append(indices_data + vertices_count)
|
||||
|
||||
vertices_count += pos_data.shape[0]
|
||||
|
||||
return (
|
||||
np.concatenate(all_pos_data, axis=0),
|
||||
np.concatenate(all_indices_data, axis=0).reshape(-1, 3),
|
||||
all_vertices_groups,
|
||||
)
|
||||
|
||||
|
||||
def _choose_index_component_type(num_vertices: int) -> int:
|
||||
# 5123 = UNSIGNED_SHORT, 5125 = UNSIGNED_INT
|
||||
return 5123 if num_vertices <= 65535 else 5125
|
||||
|
||||
|
||||
def merge_all_meshes_to_single(doc: GltfDocument) -> bool:
|
||||
if len(doc.meshes()) <= 1:
|
||||
return True
|
||||
|
||||
mesh_transforms = compute_mesh_global_transforms(doc)
|
||||
|
||||
merged_vertices: List[np.ndarray] = []
|
||||
merged_indices: List[np.ndarray] = []
|
||||
vertex_offset = 0
|
||||
|
||||
for mesh_index, mesh in enumerate(doc.meshes()):
|
||||
for primitive in mesh.get("primitives", []):
|
||||
if "attributes" in primitive and "POSITION" in primitive["attributes"]:
|
||||
pos_accessor_index = int(primitive["attributes"]["POSITION"])
|
||||
vertices = access_data(doc, pos_accessor_index).astype(np.float32)
|
||||
if mesh_index in mesh_transforms:
|
||||
transform_matrix = mesh_transforms[mesh_index]
|
||||
identity = np.eye(4, dtype=np.float32)
|
||||
if not np.allclose(transform_matrix, identity, atol=1e-6):
|
||||
pos_h = np.ones((vertices.shape[0], 4), dtype=np.float32)
|
||||
pos_h[:, :3] = vertices
|
||||
vertices = (pos_h @ transform_matrix.T)[:, :3].astype(np.float32)
|
||||
|
||||
if "indices" in primitive:
|
||||
indices_accessor_index = int(primitive["indices"])
|
||||
indices = access_data(doc, indices_accessor_index).astype(np.int64)
|
||||
indices = indices + vertex_offset
|
||||
else:
|
||||
indices = np.arange(len(vertices), dtype=np.int64) + vertex_offset
|
||||
indices = indices.reshape(-1, 3)
|
||||
|
||||
merged_vertices.append(vertices)
|
||||
merged_indices.append(indices)
|
||||
vertex_offset += len(vertices)
|
||||
|
||||
if not merged_vertices:
|
||||
return False
|
||||
|
||||
final_vertices = np.concatenate(merged_vertices, axis=0)
|
||||
final_indices = np.concatenate(merged_indices, axis=0)
|
||||
|
||||
acc_pos, _ = append_accessor_and_bufferview(
|
||||
doc,
|
||||
final_vertices.astype(np.float32),
|
||||
component_type=5126,
|
||||
element_type="VEC3",
|
||||
target=34962,
|
||||
)
|
||||
|
||||
index_component_type = _choose_index_component_type(final_vertices.shape[0])
|
||||
index_dtype = np.uint16 if index_component_type == 5123 else np.uint32
|
||||
acc_idx, _ = append_accessor_and_bufferview(
|
||||
doc,
|
||||
final_indices.astype(index_dtype).reshape(-1),
|
||||
component_type=index_component_type,
|
||||
element_type="SCALAR",
|
||||
target=34963,
|
||||
)
|
||||
|
||||
merged_mesh = {
|
||||
"name": "Merged_Mesh",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {"POSITION": acc_pos},
|
||||
"indices": acc_idx,
|
||||
"mode": 4,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
doc.json()["meshes"] = [merged_mesh]
|
||||
|
||||
update_nodes_for_merged_mesh(doc)
|
||||
return True
|
||||
|
||||
|
||||
def update_nodes_for_merged_mesh(doc: GltfDocument) -> None:
|
||||
nodes = doc.nodes()
|
||||
meshes = doc.meshes()
|
||||
merged_node = {"name": "Merged_Mesh_Node", "mesh": 0}
|
||||
nodes.append(merged_node)
|
||||
merged_node_index = len(nodes) - 1
|
||||
|
||||
scene_index = doc.json().get("scene", 0)
|
||||
scenes = doc.scenes()
|
||||
if not scenes:
|
||||
doc.json()["scenes"] = [{"nodes": [merged_node_index]}]
|
||||
doc.json()["scene"] = 0
|
||||
else:
|
||||
scenes[scene_index].setdefault("nodes", [])
|
||||
scenes[scene_index]["nodes"].append(merged_node_index)
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .io_gltf import GltfDocument
|
||||
from .transforms import (
|
||||
compute_mesh_global_transforms,
|
||||
compose_matrix,
|
||||
direction_to_quaternion,
|
||||
)
|
||||
from .accessors import append_accessor_and_bufferview
|
||||
|
||||
_EPS = np.finfo(float).eps * 4.0
|
||||
|
||||
|
||||
def check_input_file_compatibility(doc: GltfDocument) -> Dict[str, Any]:
|
||||
info = {
|
||||
"has_skins": False,
|
||||
"has_animations": False,
|
||||
"skin_count": 0,
|
||||
"animation_count": 0,
|
||||
"issues": [],
|
||||
"recommendations": [],
|
||||
"mesh_transforms_non_identity": [],
|
||||
}
|
||||
|
||||
if "skins" in doc.json():
|
||||
info["has_skins"] = True
|
||||
info["skin_count"] = len(doc.json()["skins"])
|
||||
info["issues"].append(f"File already contains {info['skin_count']} skin(s)")
|
||||
info["recommendations"].append("Remove existing skin data before auto-rigging")
|
||||
|
||||
if "animations" in doc.json():
|
||||
info["has_animations"] = True
|
||||
info["animation_count"] = len(doc.json()["animations"])
|
||||
info["issues"].append(
|
||||
f"File already contains {info['animation_count']} animation(s)"
|
||||
)
|
||||
info["recommendations"].append(
|
||||
"Existing animations may conflict with generated rig"
|
||||
)
|
||||
|
||||
mesh_transforms = compute_mesh_global_transforms(doc)
|
||||
for mesh_index, transform in mesh_transforms.items():
|
||||
identity = np.eye(4, dtype=np.float32)
|
||||
if not np.allclose(transform, identity, atol=1e-6):
|
||||
mesh_name = doc.meshes()[mesh_index].get("name", f"Mesh_{mesh_index}")
|
||||
info["mesh_transforms_non_identity"].append(
|
||||
{"mesh_index": mesh_index, "mesh_name": mesh_name, "transform": transform.tolist()}
|
||||
)
|
||||
|
||||
if info["mesh_transforms_non_identity"]:
|
||||
info["issues"].append(
|
||||
f"{len(info['mesh_transforms_non_identity'])} mesh(es) have non-identity transformations"
|
||||
)
|
||||
info["recommendations"].append(
|
||||
"Mesh positions will be transformed to global space for auto-rigging"
|
||||
)
|
||||
return info
|
||||
|
||||
|
||||
def clean_rigging_data_from_gltf(doc: GltfDocument) -> Dict[str, Any]:
|
||||
cleanup_info = {
|
||||
"removed_skins": 0,
|
||||
"removed_animations": 0,
|
||||
"removed_skin_refs": 0,
|
||||
"removed_joint_attrs": 0,
|
||||
"removed_weight_attrs": 0,
|
||||
"removed_accessors": 0,
|
||||
"removed_buffer_views": 0,
|
||||
"binary_buffer_rebuilt": False,
|
||||
"operations": [],
|
||||
}
|
||||
|
||||
# collect accessors to remove
|
||||
joint_weight_accessors = set()
|
||||
for mesh in doc.meshes():
|
||||
for primitive in mesh.get("primitives", []):
|
||||
attrs = primitive.get("attributes", {})
|
||||
if "JOINTS_0" in attrs:
|
||||
joint_weight_accessors.add(int(attrs["JOINTS_0"]))
|
||||
cleanup_info["removed_joint_attrs"] += 1
|
||||
if "WEIGHTS_0" in attrs:
|
||||
joint_weight_accessors.add(int(attrs["WEIGHTS_0"]))
|
||||
cleanup_info["removed_weight_attrs"] += 1
|
||||
|
||||
inverse_bind_accessors = set()
|
||||
if "skins" in doc.json():
|
||||
for skin in doc.json()["skins"]:
|
||||
if "inverseBindMatrices" in skin:
|
||||
inverse_bind_accessors.add(int(skin["inverseBindMatrices"]))
|
||||
|
||||
all_skin_accessors = joint_weight_accessors | inverse_bind_accessors
|
||||
|
||||
skin_buffer_views = set()
|
||||
for accessor_idx in all_skin_accessors:
|
||||
if accessor_idx < len(doc.accessors()):
|
||||
accessor = doc.accessors()[accessor_idx]
|
||||
if "bufferView" in accessor:
|
||||
skin_buffer_views.add(int(accessor["bufferView"]))
|
||||
|
||||
# remove animations
|
||||
if "animations" in doc.json():
|
||||
cleanup_info["removed_animations"] = len(doc.json()["animations"])
|
||||
del doc.json()["animations"]
|
||||
cleanup_info["operations"].append(
|
||||
f"Removed 'animations' section with {cleanup_info['removed_animations']} elements"
|
||||
)
|
||||
|
||||
# remove skins
|
||||
if "skins" in doc.json():
|
||||
cleanup_info["removed_skins"] = len(doc.json()["skins"])
|
||||
del doc.json()["skins"]
|
||||
cleanup_info["operations"].append(
|
||||
f"Removed 'skins' section with {cleanup_info['removed_skins']} elements"
|
||||
)
|
||||
|
||||
# remove node skin refs
|
||||
for idx, node in enumerate(doc.nodes()):
|
||||
if "skin" in node:
|
||||
node.pop("skin")
|
||||
cleanup_info["removed_skin_refs"] += 1
|
||||
cleanup_info["operations"].append(f"Removed skin reference from node index {idx}")
|
||||
|
||||
# remove JOINTS_0/WEIGHTS_0 attributes
|
||||
for m_idx, mesh in enumerate(doc.meshes()):
|
||||
for p_idx, primitive in enumerate(mesh.get("primitives", [])):
|
||||
attrs = primitive.get("attributes", {})
|
||||
removed = []
|
||||
if "JOINTS_0" in attrs:
|
||||
attrs.pop("JOINTS_0")
|
||||
removed.append("JOINTS_0")
|
||||
if "WEIGHTS_0" in attrs:
|
||||
attrs.pop("WEIGHTS_0")
|
||||
removed.append("WEIGHTS_0")
|
||||
if removed:
|
||||
cleanup_info["operations"].append(
|
||||
f"Removed attributes {removed} from mesh {m_idx} primitive {p_idx}"
|
||||
)
|
||||
|
||||
# rebuild accessors without skin ones
|
||||
if all_skin_accessors:
|
||||
old_accessors = doc.accessors()
|
||||
new_accessors = []
|
||||
old_to_new = {}
|
||||
for i, acc in enumerate(old_accessors):
|
||||
if i in all_skin_accessors:
|
||||
cleanup_info["removed_accessors"] += 1
|
||||
else:
|
||||
old_to_new[i] = len(new_accessors)
|
||||
new_accessors.append(acc)
|
||||
doc.json()["accessors"] = new_accessors
|
||||
|
||||
# remap references
|
||||
for mesh in doc.meshes():
|
||||
for primitive in mesh.get("primitives", []):
|
||||
attrs = primitive.get("attributes", {})
|
||||
for key, acc_idx in list(attrs.items()):
|
||||
if acc_idx in old_to_new:
|
||||
attrs[key] = old_to_new[acc_idx]
|
||||
else:
|
||||
# invalid -> drop attribute
|
||||
attrs.pop(key)
|
||||
if "indices" in primitive:
|
||||
idx = primitive["indices"]
|
||||
if idx in old_to_new:
|
||||
primitive["indices"] = old_to_new[idx]
|
||||
else:
|
||||
primitive.pop("indices", None)
|
||||
|
||||
# bufferViews cleanup requires rebuilding binary; keep it simple for now
|
||||
# caller may opt to rebuild or leave holes; we just report counts
|
||||
cleanup_info["removed_buffer_views"] = len(skin_buffer_views)
|
||||
return cleanup_info
|
||||
|
||||
|
||||
def clean_skin_data_from_gltf(doc: GltfDocument) -> Dict[str, Any]:
|
||||
return clean_rigging_data_from_gltf(doc)
|
||||
|
||||
|
||||
@dataclass
|
||||
class JointData:
|
||||
joint_node_index: int
|
||||
joint_name: str
|
||||
parent_index: Optional[int] = None
|
||||
children_indices: Tuple[int, ...] = ()
|
||||
forward_bind_matrix: np.ndarray = field(default_factory=lambda: np.eye(4, dtype=np.float32))
|
||||
inverse_bind_matrix: np.ndarray = field(default_factory=lambda: np.eye(4, dtype=np.float32))
|
||||
local_transform_matrix: np.ndarray = field(default_factory=lambda: np.eye(4, dtype=np.float32))
|
||||
original_index: Optional[int] = None
|
||||
|
||||
|
||||
def insert_generated_data_to_gltf(
|
||||
doc: GltfDocument,
|
||||
joints: np.ndarray,
|
||||
conns: np.ndarray,
|
||||
skins: np.ndarray,
|
||||
all_vertices_groups: List[Dict[str, Any]],
|
||||
) -> None:
|
||||
# A) collect used joints and build remap
|
||||
used = set()
|
||||
for v_group in all_vertices_groups:
|
||||
v0, v1 = v_group["vertices_range"]
|
||||
skins_data = skins[v0:v1]
|
||||
top_idx = np.argsort(-skins_data, axis=1)[:, :4]
|
||||
rows = np.arange(skins_data.shape[0])[:, None]
|
||||
top_w = skins_data[rows, top_idx]
|
||||
mask = top_w > _EPS
|
||||
used.update(top_idx[mask].tolist())
|
||||
used_sorted = sorted(used)
|
||||
old_to_new = {old: new for new, old in enumerate(used_sorted)}
|
||||
|
||||
# B) build joint nodes and skin
|
||||
current_nodes_len = len(doc.nodes())
|
||||
joints_nodes_indices: List[int] = []
|
||||
all_joints_data: List[JointData] = []
|
||||
joints_global_pos: List[np.ndarray] = []
|
||||
for new_joint_idx, old_joint_idx in enumerate(used_sorted):
|
||||
parent_index = conns[old_joint_idx] if old_joint_idx < len(conns) else old_joint_idx
|
||||
joint_node_index = current_nodes_len + new_joint_idx
|
||||
joint_name = f"Joint_{new_joint_idx}"
|
||||
jd = JointData(joint_node_index, joint_name)
|
||||
jd.original_index = int(old_joint_idx)
|
||||
joints_nodes_indices.append(joint_node_index)
|
||||
all_joints_data.append(jd)
|
||||
parent_pick = int(parent_index) if parent_index < len(joints) else int(old_joint_idx)
|
||||
joints_global_pos.append(joints[parent_pick])
|
||||
|
||||
# build parent/children links
|
||||
for jd in all_joints_data:
|
||||
old_idx = int(jd.original_index)
|
||||
parent_old = int(conns[old_idx]) if old_idx < len(conns) else old_idx
|
||||
if parent_old == old_idx:
|
||||
continue
|
||||
if parent_old in old_to_new:
|
||||
parent_new = old_to_new[parent_old]
|
||||
parent_node_index = all_joints_data[parent_new].joint_node_index
|
||||
jd.parent_index = parent_node_index
|
||||
parent_children = list(all_joints_data[parent_new].children_indices)
|
||||
if jd.joint_node_index not in parent_children:
|
||||
parent_children.append(jd.joint_node_index)
|
||||
all_joints_data[parent_new].children_indices = tuple(parent_children)
|
||||
|
||||
# compute matrices
|
||||
inverse_bind_mats: List[np.ndarray] = []
|
||||
for i_name, jd in enumerate(all_joints_data):
|
||||
global_scale = np.array([1.0, 1.0, 1.0], dtype=np.float32)
|
||||
global_translation = np.asarray(joints_global_pos[i_name], dtype=np.float32)
|
||||
if jd.children_indices:
|
||||
offset = np.zeros(3, dtype=np.float32)
|
||||
for child_index in jd.children_indices:
|
||||
offset += (
|
||||
joints_global_pos[child_index - current_nodes_len] - global_translation
|
||||
)
|
||||
global_rotation = direction_to_quaternion(offset)
|
||||
elif jd.parent_index is not None:
|
||||
offset_from_parent = (
|
||||
global_translation
|
||||
- joints_global_pos[jd.parent_index - current_nodes_len]
|
||||
)
|
||||
global_rotation = direction_to_quaternion(offset_from_parent)
|
||||
else:
|
||||
global_rotation = np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float32)
|
||||
forward = compose_matrix(global_scale, global_rotation, global_translation)
|
||||
jd.forward_bind_matrix = forward
|
||||
jd.inverse_bind_matrix = np.linalg.inv(forward)
|
||||
inverse_bind_mats.append(jd.inverse_bind_matrix)
|
||||
|
||||
inverse_bind_mats = np.stack(inverse_bind_mats, axis=0).astype(np.float32)
|
||||
# glTF expects column-major arrays in JSON; we transpose like original code
|
||||
inverse_bind_mats = np.transpose(inverse_bind_mats, (0, 2, 1)).copy(order="C")
|
||||
|
||||
# C) add vertex joint/weight attributes per groups
|
||||
for v_group in all_vertices_groups:
|
||||
v0, v1 = v_group["vertices_range"]
|
||||
skins_data = skins[v0:v1]
|
||||
top_idx = np.argsort(-skins_data, axis=1)[:, :4]
|
||||
rows = np.arange(skins_data.shape[0])[:, None]
|
||||
weights_data = skins_data[rows, top_idx].astype(np.float32)
|
||||
|
||||
joints_data = np.zeros_like(top_idx, dtype=np.uint16)
|
||||
for i_row in range(top_idx.shape[0]):
|
||||
for i_col in range(4):
|
||||
old = int(top_idx[i_row, i_col])
|
||||
if old in old_to_new:
|
||||
joints_data[i_row, i_col] = np.uint16(old_to_new[old])
|
||||
else:
|
||||
joints_data[i_row, i_col] = np.uint16(0)
|
||||
weights_data[i_row, i_col] = 0.0
|
||||
|
||||
# D) normalize weights
|
||||
sums = np.sum(weights_data, axis=1, keepdims=True)
|
||||
mask_zero = sums < _EPS
|
||||
weights_data = np.where(mask_zero, 0.25, weights_data / sums)
|
||||
|
||||
mesh_index = v_group["mesh_index"]
|
||||
prim_index = v_group["primitive_index"]
|
||||
primitive = doc.meshes()[mesh_index]["primitives"][prim_index]
|
||||
|
||||
acc_j, _ = append_accessor_and_bufferview(
|
||||
doc, joints_data.astype(np.uint16), component_type=5123, element_type="VEC4", target=34962
|
||||
)
|
||||
acc_w, _ = append_accessor_and_bufferview(
|
||||
doc, weights_data.astype(np.float32), component_type=5126, element_type="VEC4", target=34962
|
||||
)
|
||||
primitive.setdefault("attributes", {})
|
||||
primitive["attributes"]["JOINTS_0"] = acc_j
|
||||
primitive["attributes"]["WEIGHTS_0"] = acc_w
|
||||
|
||||
# skin JSON
|
||||
acc_ibm, _ = append_accessor_and_bufferview(
|
||||
doc, inverse_bind_mats.astype(np.float32), component_type=5126, element_type="MAT4", target=None
|
||||
)
|
||||
doc.json()["skins"] = [
|
||||
{"joints": joints_nodes_indices, "inverseBindMatrices": acc_ibm, "name": "Generated_Skin"}
|
||||
]
|
||||
|
||||
# add joint nodes
|
||||
root_joint_nodes: List[int] = []
|
||||
for jd in all_joints_data:
|
||||
node_json = {
|
||||
"name": jd.joint_name,
|
||||
"matrix": np.transpose(jd.local_transform_matrix, (1, 0)).copy(order="C").flatten().tolist(),
|
||||
}
|
||||
if jd.children_indices:
|
||||
node_json["children"] = list(jd.children_indices)
|
||||
doc.nodes().append(node_json)
|
||||
if jd.parent_index is None:
|
||||
root_joint_nodes.append(jd.joint_node_index)
|
||||
|
||||
# joints root
|
||||
doc.nodes().append({"name": "Joints_Root", "children": root_joint_nodes})
|
||||
joints_root_index = len(doc.nodes()) - 1
|
||||
scene_index = doc.json().get("scene", 0)
|
||||
doc.scenes()[scene_index].setdefault("nodes", [])
|
||||
doc.scenes()[scene_index]["nodes"].append(joints_root_index)
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .io_gltf import GltfDocument
|
||||
from .accessors import access_data, update_accessor_binary_data, recompute_accessor_min_max
|
||||
|
||||
|
||||
def quaternion_matrix(quaternion: np.ndarray) -> np.ndarray:
|
||||
x, y, z, w = quaternion
|
||||
x2 = x * x
|
||||
y2 = y * y
|
||||
z2 = z * z
|
||||
xy = x * y
|
||||
xz = x * z
|
||||
yz = y * z
|
||||
wx = w * x
|
||||
wy = w * y
|
||||
wz = w * z
|
||||
return np.array(
|
||||
[
|
||||
[1.0 - 2.0 * (y2 + z2), 2.0 * (xy - wz), 2.0 * (xz + wy), 0.0],
|
||||
[2.0 * (xy + wz), 1.0 - 2.0 * (x2 + z2), 2.0 * (yz - wx), 0.0],
|
||||
[2.0 * (xz - wy), 2.0 * (yz + wx), 1.0 - 2.0 * (x2 + y2), 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def compose_matrix(scale: np.ndarray, rotation: np.ndarray, translation: np.ndarray) -> np.ndarray:
|
||||
S = np.diag([scale[0], scale[1], scale[2], 1.0])
|
||||
R = quaternion_matrix(rotation)
|
||||
T = np.eye(4)
|
||||
T[:3, 3] = translation
|
||||
return T @ (R @ S)
|
||||
|
||||
|
||||
def direction_to_quaternion(direction: np.ndarray) -> np.ndarray:
|
||||
if not np.issubdtype(direction.dtype, np.floating):
|
||||
direction = direction.astype(np.float32)
|
||||
norm = np.linalg.norm(direction)
|
||||
if norm < 1e-10:
|
||||
return np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float32)
|
||||
direction = direction / norm
|
||||
forward = np.array([0.0, 1.0, 0.0], dtype=np.float32)
|
||||
up = np.array([0.0, 0.0, 1.0], dtype=np.float32)
|
||||
dot = float(np.clip(np.dot(forward, direction), -1.0, 1.0))
|
||||
if dot > 0.9999:
|
||||
return np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float32)
|
||||
if dot < -0.9999:
|
||||
return np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float32)
|
||||
axis = np.cross(forward, direction)
|
||||
axis_norm = np.linalg.norm(axis)
|
||||
if axis_norm < 1e-10:
|
||||
if abs(direction[2]) < 0.9:
|
||||
axis = np.cross(up, direction)
|
||||
else:
|
||||
axis = np.cross(np.array([1.0, 0.0, 0.0], dtype=np.float32), direction)
|
||||
axis = axis / np.linalg.norm(axis)
|
||||
else:
|
||||
axis = axis / axis_norm
|
||||
angle = np.arccos(dot)
|
||||
half = angle * 0.5
|
||||
s = np.sin(half)
|
||||
c = np.cos(half)
|
||||
quat = np.array([axis[0] * s, axis[1] * s, axis[2] * s, c], dtype=np.float32)
|
||||
return quat / np.linalg.norm(quat)
|
||||
|
||||
|
||||
def parse_node_transform(node_json: Dict[str, Any]) -> np.ndarray:
|
||||
if "matrix" in node_json:
|
||||
m = np.array(node_json["matrix"], dtype=np.float32).reshape(4, 4)
|
||||
return m.T
|
||||
t = np.array(node_json.get("translation", [0.0, 0.0, 0.0]), dtype=np.float32)
|
||||
r = np.array(node_json.get("rotation", [0.0, 0.0, 0.0, 1.0]), dtype=np.float32)
|
||||
s = np.array(node_json.get("scale", [1.0, 1.0, 1.0]), dtype=np.float32)
|
||||
return compose_matrix(s, r, t)
|
||||
|
||||
|
||||
def compute_global_transform(doc: GltfDocument, node_index: int, parent_transform: Optional[np.ndarray] = None) -> np.ndarray:
|
||||
node_json = doc.nodes()[node_index]
|
||||
local = parse_node_transform(node_json)
|
||||
return parent_transform @ local if parent_transform is not None else local
|
||||
|
||||
|
||||
def compute_mesh_global_transforms(doc: GltfDocument) -> Dict[int, np.ndarray]:
|
||||
mesh_to_transform: Dict[int, np.ndarray] = {}
|
||||
scene_index = doc.json().get("scene")
|
||||
if scene_index is None:
|
||||
return mesh_to_transform
|
||||
root_nodes = doc.scenes()[scene_index].get("nodes", [])
|
||||
|
||||
def traverse(node_idx: int, parent: Optional[np.ndarray] = None) -> None:
|
||||
global_m = compute_global_transform(doc, node_idx, parent)
|
||||
node = doc.nodes()[node_idx]
|
||||
if "mesh" in node:
|
||||
mesh_to_transform[int(node["mesh"])] = global_m
|
||||
for child in node.get("children", []) or []:
|
||||
traverse(int(child), global_m)
|
||||
|
||||
for root in root_nodes:
|
||||
traverse(int(root))
|
||||
return mesh_to_transform
|
||||
|
||||
|
||||
def transform_positions_inplace(doc: GltfDocument, mesh_idx: int, matrix: np.ndarray) -> None:
|
||||
mesh = doc.meshes()[mesh_idx]
|
||||
for primitive in mesh.get("primitives", []):
|
||||
attrs = primitive.get("attributes", {})
|
||||
if "POSITION" not in attrs:
|
||||
continue
|
||||
acc_idx = int(attrs["POSITION"])
|
||||
data = access_data(doc, acc_idx)
|
||||
pos_h = np.ones((data.shape[0], 4), dtype=np.float32)
|
||||
pos_h[:, :3] = data.astype(np.float32)
|
||||
transformed = (pos_h @ matrix.T)[:, :3].astype(np.float32)
|
||||
update_accessor_binary_data(doc, acc_idx, transformed)
|
||||
recompute_accessor_min_max(doc, acc_idx)
|
||||
|
||||
|
||||
def transform_local_vertices_to_world(pos_data: np.ndarray, node: Dict[str, Any]) -> np.ndarray:
|
||||
if "matrix" in node:
|
||||
M = np.array(node["matrix"], dtype=np.float32).reshape(4, 4).T
|
||||
else:
|
||||
t = np.array(node.get("translation", [0.0, 0.0, 0.0]), dtype=np.float32)
|
||||
r = np.array(node.get("rotation", [0.0, 0.0, 0.0, 1.0]), dtype=np.float32)
|
||||
s = np.array(node.get("scale", [1.0, 1.0, 1.0]), dtype=np.float32)
|
||||
M = compose_matrix(s, r, t)
|
||||
pos_h = np.concatenate([pos_data, np.ones((pos_data.shape[0], 1), dtype=np.float32)], axis=1)
|
||||
out = (M @ pos_h.T).T[:, :3]
|
||||
return out
|
||||
|
||||
|
||||
def set_node_trs_identity(doc: GltfDocument, node_idx: int) -> None:
|
||||
node = doc.nodes()[node_idx]
|
||||
for key in ["matrix", "translation", "rotation", "scale"]:
|
||||
if key in node:
|
||||
node.pop(key)
|
||||
|
||||
|
||||
def preprocess_transforms_inplace(doc: GltfDocument) -> None:
|
||||
"""Apply node transforms to vertex positions and set TRS for all nodes.
|
||||
|
||||
Idea: for each mesh, we calculate the global matrix, transform the POSITION of all
|
||||
primitives of this mesh and remove the transform for the nodes referring to this mesh.
|
||||
"""
|
||||
mesh_transforms = compute_mesh_global_transforms(doc)
|
||||
# quick exit if all identity
|
||||
any_non_identity = any(
|
||||
not np.allclose(M, np.eye(4), atol=1e-6) for M in mesh_transforms.values()
|
||||
)
|
||||
if not any_non_identity:
|
||||
return
|
||||
|
||||
for mesh_idx, M in mesh_transforms.items():
|
||||
if np.allclose(M, np.eye(4), atol=1e-6):
|
||||
continue
|
||||
# transform all positions in the mesh
|
||||
mesh = doc.meshes()[mesh_idx]
|
||||
for primitive in mesh.get("primitives", []):
|
||||
attrs = primitive.get("attributes", {})
|
||||
if "POSITION" not in attrs:
|
||||
continue
|
||||
acc_idx = int(attrs["POSITION"])
|
||||
data = access_data(doc, acc_idx)
|
||||
pos_h = np.ones((data.shape[0], 4), dtype=np.float32)
|
||||
pos_h[:, :3] = data.astype(np.float32)
|
||||
transformed = (pos_h @ M.T)[:, :3].astype(np.float32)
|
||||
update_accessor_binary_data(doc, acc_idx, transformed)
|
||||
recompute_accessor_min_max(doc, acc_idx)
|
||||
|
||||
# reset TRS for all nodes referring to this mesh
|
||||
for node_idx, node in enumerate(doc.nodes()):
|
||||
if node.get("mesh") == mesh_idx:
|
||||
set_node_trs_identity(doc, node_idx)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import json
|
||||
import warnings
|
||||
from typing import Any, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def pad_to_4_bytes(data: bytes, pad_byte: bytes = b"\x20") -> bytes:
|
||||
"""Pad given bytes to the next 4-byte boundary.
|
||||
|
||||
JSON chunks must be padded with spaces (0x20),
|
||||
binary chunks must be padded with zeros (0x00).
|
||||
"""
|
||||
if not data:
|
||||
return data
|
||||
remainder = len(data) % 4
|
||||
if remainder == 0:
|
||||
return data
|
||||
return data + pad_byte * (4 - remainder)
|
||||
|
||||
|
||||
def ensure_little_endian(array: np.ndarray, dtype: Optional[np.dtype] = None) -> np.ndarray:
|
||||
"""Return an array with little-endian dtype (copy if needed)."""
|
||||
if dtype is not None:
|
||||
array = array.astype(dtype, copy=False)
|
||||
|
||||
# Big-endian → Little-endian
|
||||
if array.dtype.byteorder == ">":
|
||||
return array.byteswap().view(array.dtype.newbyteorder("<"))
|
||||
|
||||
# "|" = non-byte-order type → treat as big-endian-compatible
|
||||
if array.dtype.byteorder == "|":
|
||||
return array.view(array.dtype.newbyteorder("<"))
|
||||
|
||||
# Already little-endian
|
||||
if array.dtype.byteorder == "<":
|
||||
return array
|
||||
|
||||
# Native ("=") without explicit endianness → convert explicitly
|
||||
return array.view(array.dtype.newbyteorder("<"))
|
||||
|
||||
|
||||
def as_uint8_buffer(array: np.ndarray) -> np.ndarray:
|
||||
"""View array bytes as uint8 vector."""
|
||||
if not isinstance(array, np.ndarray):
|
||||
raise TypeError("as_uint8_buffer expects a numpy array")
|
||||
return np.frombuffer(array.tobytes(), dtype=np.uint8)
|
||||
|
||||
|
||||
def warn(message: str) -> None:
|
||||
warnings.warn(message)
|
||||
|
||||
|
||||
class NpEncoder(json.JSONEncoder):
|
||||
"""JSON encoder that converts numpy scalars/arrays to native Python types.
|
||||
|
||||
Important: cast numpy integers to int, not to string.
|
||||
"""
|
||||
|
||||
def default(self, obj: Any) -> Any:
|
||||
# scalars
|
||||
if isinstance(obj, (np.integer,)):
|
||||
return int(obj)
|
||||
if isinstance(obj, (np.floating,)):
|
||||
return float(obj)
|
||||
if isinstance(obj, (np.bool_,)):
|
||||
return bool(obj)
|
||||
# arrays
|
||||
if isinstance(obj, np.ndarray):
|
||||
return obj.tolist()
|
||||
return super().default(obj)
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .io_gltf import GltfDocument
|
||||
|
||||
|
||||
def validate_before_save(doc: GltfDocument) -> List[str]:
|
||||
errors: List[str] = []
|
||||
# buffers length and 4-byte alignment will be handled by writer, but sanity-check
|
||||
for i, buf in enumerate(doc.buffers()):
|
||||
expect = int(doc.binary(i).nbytes)
|
||||
if buf.get("byteLength", -1) != expect:
|
||||
errors.append(f"Buffer {i} byteLength mismatch: json={buf.get('byteLength')} actual={expect}")
|
||||
# bufferViews within ranges
|
||||
for i, bv in enumerate(doc.bufferViews()):
|
||||
b = int(bv.get("buffer", 0))
|
||||
off = int(bv.get("byteOffset", 0))
|
||||
ln = int(bv.get("byteLength", 0))
|
||||
if off + ln > int(doc.binary(b).nbytes):
|
||||
errors.append(f"bufferView {i} out of range: {off}+{ln}>{doc.binary(b).nbytes}")
|
||||
if off % 4 != 0:
|
||||
errors.append(f"bufferView {i} is not 4-byte aligned: offset={off}")
|
||||
# accessors consistency
|
||||
for i, acc in enumerate(doc.accessors()):
|
||||
if "bufferView" not in acc:
|
||||
continue
|
||||
bv = doc.bufferViews()[acc["bufferView"]]
|
||||
component_type = acc.get("componentType")
|
||||
acc_type = acc.get("type")
|
||||
if component_type not in (5120, 5121, 5122, 5123, 5125, 5126):
|
||||
errors.append(f"accessor {i} invalid componentType {component_type}")
|
||||
if acc_type not in ("SCALAR", "VEC2", "VEC3", "VEC4", "MAT2", "MAT3", "MAT4"):
|
||||
errors.append(f"accessor {i} invalid type {acc_type}")
|
||||
# position min/max
|
||||
for i, acc in enumerate(doc.accessors()):
|
||||
if acc.get("type") == "VEC3":
|
||||
if "min" not in acc or "max" not in acc:
|
||||
errors.append(f"accessor {i} missing min/max for VEC3")
|
||||
# primitives mode
|
||||
for m_idx, mesh in enumerate(doc.meshes()):
|
||||
for p_idx, prim in enumerate(mesh.get("primitives", [])):
|
||||
if prim.get("mode", 4) != 4:
|
||||
errors.append(f"mesh {m_idx} prim {p_idx} mode must be 4 (TRIANGLES)")
|
||||
return errors
|
||||
|
||||
@@ -37,10 +37,8 @@ from plyfile import PlyData
|
||||
import trimesh
|
||||
from PIL import Image
|
||||
|
||||
from mesh_processor.export_utils import export_to_fastmesh, export_to_mesh
|
||||
from mesh_processor.fastmesh import FastMesh
|
||||
from mesh_processor.mesh import Mesh
|
||||
from mesh_processor.mesh_utils import (
|
||||
from .mesh_processer.mesh import Mesh
|
||||
from .mesh_processer.mesh_utils import (
|
||||
ply_to_points_cloud,
|
||||
get_target_axis_and_scale,
|
||||
switch_ply_axis_and_scale,
|
||||
@@ -50,7 +48,6 @@ from mesh_processor.mesh_utils import (
|
||||
color_func_to_albedo,
|
||||
interpolate_texture_map_attr,
|
||||
decimate_mesh,
|
||||
reduce_mesh_faces_inplace
|
||||
)
|
||||
|
||||
from FlexiCubes.flexicubes_trainer import FlexiCubesTrainer
|
||||
@@ -131,7 +128,6 @@ from Gen_3D_Modules.PartCrafter.partcrafter_src.pipelines.pipeline_partcrafter i
|
||||
from Gen_3D_Modules.PartCrafter.partcrafter_src.utils.data_utils import get_colored_mesh_composition
|
||||
from Gen_3D_Modules.PartCrafter.partcrafter_src.utils.render_utils import explode_mesh
|
||||
import zipfile
|
||||
from typing import Union
|
||||
|
||||
|
||||
os.environ['SPCONV_ALGO'] = 'native'
|
||||
@@ -289,7 +285,6 @@ class Load_3D_Mesh:
|
||||
return {
|
||||
"required": {
|
||||
"mesh_file_path": ("STRING", {"default": '', "multiline": False}),
|
||||
"use_fastmesh": ("BOOLEAN", {"default": True, "tooltip": "Use FastMesh for GLB/GLTF files"}),
|
||||
"resize": ("BOOLEAN", {"default": False},),
|
||||
"renormal": ("BOOLEAN", {"default": True},),
|
||||
"retex": ("BOOLEAN", {"default": False},),
|
||||
@@ -308,7 +303,7 @@ class Load_3D_Mesh:
|
||||
FUNCTION = "load_mesh"
|
||||
CATEGORY = "Comfy3D/Import|Export"
|
||||
|
||||
def load_mesh(self, mesh_file_path, use_fastmesh, resize, renormal, retex, optimizable, clean, resize_bound):
|
||||
def load_mesh(self, mesh_file_path, resize, renormal, retex, optimizable, clean, resize_bound):
|
||||
mesh = None
|
||||
|
||||
if not os.path.isabs(mesh_file_path):
|
||||
@@ -318,10 +313,7 @@ class Load_3D_Mesh:
|
||||
folder, filename = os.path.split(mesh_file_path)
|
||||
if filename.lower().endswith(SUPPORTED_3D_EXTENSIONS):
|
||||
with torch.inference_mode(not optimizable):
|
||||
if use_fastmesh and mesh_file_path.endswith((".glb", ".gltf")):
|
||||
mesh = FastMesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound)
|
||||
else:
|
||||
mesh = Mesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound)
|
||||
mesh = Mesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound)
|
||||
else:
|
||||
cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print()
|
||||
else:
|
||||
@@ -371,7 +363,6 @@ class Save_3D_Mesh:
|
||||
"required": {
|
||||
"mesh": ("MESH",),
|
||||
"save_path": ("STRING", {"default": 'Mesh_%Y-%m-%d-%M-%S-%f.glb', "multiline": False}),
|
||||
"use_fastmesh": ("BOOLEAN", {"default": True, "tooltip": "Use FastMesh for fast saving GLB"}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -385,26 +376,11 @@ class Save_3D_Mesh:
|
||||
FUNCTION = "save_mesh"
|
||||
CATEGORY = "Comfy3D/Import|Export"
|
||||
|
||||
def save_mesh(self, mesh, save_path, use_fastmesh):
|
||||
def save_mesh(self, mesh, save_path):
|
||||
save_path = parse_save_filename(save_path, comfy_paths.output_directory, SUPPORTED_3D_EXTENSIONS, self.__class__.__name__)
|
||||
|
||||
if save_path is not None:
|
||||
|
||||
if use_fastmesh and save_path.endswith((".glb", ".ply", ".obj")):
|
||||
if hasattr(mesh, '_write_glb_fast'):
|
||||
mesh.write(save_path)
|
||||
else:
|
||||
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)
|
||||
else:
|
||||
mesh.write(save_path)
|
||||
mesh.write(save_path)
|
||||
|
||||
return (save_path, )
|
||||
|
||||
@@ -4593,6 +4569,7 @@ class Hunyuan3D_V2_ShapeGen_MV:
|
||||
|
||||
return (Mesh.load_trimesh(given_mesh=mesh),)
|
||||
|
||||
#--------------------------------
|
||||
class Load_StableGen_Trellis_Pipeline:
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
RETURN_TYPES = ("DIFFUSERS_PIPE",)
|
||||
@@ -4683,6 +4660,7 @@ class Load_StableGen_StableX_Pipeline:
|
||||
pipe = self.__class__._build_pipe(repo, use_fp16)
|
||||
return (pipe,)
|
||||
|
||||
|
||||
class StableGen_Trellis_Image_To_3D:
|
||||
"""
|
||||
3D generation pipeline using Trellis model.
|
||||
@@ -4788,6 +4766,7 @@ class StableGen_Trellis_Image_To_3D:
|
||||
except Exception as e:
|
||||
raise Exception(f"[StableGen_Trellis_Image_To_3D] 3D generation failed: {str(e)}")
|
||||
|
||||
|
||||
class StableGen_StableX_Process_Image:
|
||||
"""
|
||||
Image processing pipeline using StableX model.
|
||||
@@ -4835,7 +4814,7 @@ class StableGen_StableX_Process_Image:
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"[StableGen_StableX_Process_Image] Image processing failed: {str(e)}")
|
||||
|
||||
# --- MV START ----
|
||||
class Load_MVAdapter_IG2MV_Pipeline:
|
||||
"""Loader pipeline for MV-Adapter (Image to Multi-View)"""
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
@@ -4872,6 +4851,7 @@ class Load_MVAdapter_IG2MV_Pipeline:
|
||||
vae_model = None if vae_model == "None" else vae_model
|
||||
lora_model = None if not lora_model else lora_model
|
||||
|
||||
# Подготавливаем параметры для пайплайна
|
||||
pipeline_kwargs = {
|
||||
"base_model": base_model,
|
||||
"vae_model": vae_model,
|
||||
@@ -4890,7 +4870,6 @@ class Load_MVAdapter_IG2MV_Pipeline:
|
||||
print("MV-Adapter IG2MV pipeline loaded successfully")
|
||||
return (pipe,)
|
||||
|
||||
|
||||
class MVAdapter_IG2MV:
|
||||
"""Generate multi-view images from single image and 3D mesh"""
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
@@ -4952,7 +4931,6 @@ class MVAdapter_IG2MV:
|
||||
return_images = pils_to_torch_imgs(images, device=DEVICE_STR)
|
||||
return (return_images,)
|
||||
|
||||
|
||||
class Load_MVAdapter_TG2MV_Pipeline:
|
||||
"""Loader pipeline for MV-Adapter Text-Guided to Multi-View (TG2MV)"""
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
@@ -5066,7 +5044,6 @@ class MVAdapter_TG2MV:
|
||||
return_images = pils_to_torch_imgs(images, device=DEVICE_STR)
|
||||
return (return_images,)
|
||||
|
||||
|
||||
class Load_MVAdapter_Texture_Pipeline:
|
||||
"""Load texture projection pipeline for MVAdapter"""
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
@@ -5229,7 +5206,6 @@ class MVAdapter_Texture_Projection:
|
||||
os.remove(temp_grid_path)
|
||||
raise e
|
||||
|
||||
|
||||
class Load_Hunyuan3D_21_ShapeGen_Pipeline:
|
||||
"""Load Hunyuan3D-2.1 Shape Generation Pipeline"""
|
||||
|
||||
@@ -5298,7 +5274,6 @@ class Load_Hunyuan3D_21_ShapeGen_Pipeline:
|
||||
|
||||
return (pipeline,)
|
||||
|
||||
|
||||
class Load_Hunyuan3D_21_TexGen_Pipeline:
|
||||
"""Load Hunyuan3D-2.1 Texture Generation Pipeline"""
|
||||
|
||||
@@ -5419,9 +5394,8 @@ class Load_Hunyuan3D_21_TexGen_Pipeline:
|
||||
print(f"[TexGen-Loader] Cached new pipeline {cache_key}")
|
||||
return (pipeline,)
|
||||
|
||||
|
||||
class Hunyuan3D_21_ShapeGen:
|
||||
"""Hunyuan3D-2.1 Shape Generation """
|
||||
"""Hunyuan3D-2.1 Shape Generation with automatic pipeline cleanup"""
|
||||
|
||||
CATEGORY = "Comfy3D/Algorithm/Hunyuan3D-2.1"
|
||||
RETURN_TYPES = ("MESH", "IMAGE")
|
||||
@@ -5440,20 +5414,20 @@ class Hunyuan3D_21_ShapeGen:
|
||||
"octree_resolution": ("INT", {"default": 256, "min": 64, "max": 512}),
|
||||
"remove_background": ("BOOLEAN", {"default": True}),
|
||||
"auto_cleanup": ("BOOLEAN", {"default": True}),
|
||||
"max_faces": ("INT", {"default": 40000, "min": 1000, "max": 1000000}),
|
||||
}
|
||||
}
|
||||
|
||||
@torch.no_grad()
|
||||
def generate(self, shapegen_pipe, image, seed, steps, guidance_scale, octree_resolution, remove_background, auto_cleanup, max_faces):
|
||||
pil_image = torch_imgs_to_pils(image)[0]
|
||||
if remove_background:
|
||||
bg_remover = BackgroundRemover_2_1()
|
||||
if bg_remover is not None:
|
||||
pil_image = bg_remover(pil_image)
|
||||
del bg_remover
|
||||
|
||||
generator = torch.Generator(device=shapegen_pipe.device).manual_seed(seed)
|
||||
def generate(self, shapegen_pipe, image, seed, steps, guidance_scale, octree_resolution, remove_background, auto_cleanup):
|
||||
pil_image = torch_imgs_to_pils(image)[0].convert("RGBA")
|
||||
|
||||
if remove_background or pil_image.mode == "RGB":
|
||||
rmbg_worker = BackgroundRemover_2_1()
|
||||
pil_image = rmbg_worker(pil_image.convert('RGB'))
|
||||
del rmbg_worker
|
||||
|
||||
generator = torch.Generator(device=shapegen_pipe.device)
|
||||
generator = generator.manual_seed(int(seed))
|
||||
|
||||
outputs = shapegen_pipe(
|
||||
image=pil_image,
|
||||
@@ -5465,23 +5439,36 @@ class Hunyuan3D_21_ShapeGen:
|
||||
output_type='mesh'
|
||||
)
|
||||
|
||||
mesh_objects = export_to_fastmesh(outputs)
|
||||
mesh_out = mesh_objects[0] if isinstance(mesh_objects, list) else mesh_objects
|
||||
mesh = export_to_trimesh_2_1(outputs)[0]
|
||||
|
||||
if mesh_out is None:
|
||||
raise Exception("Failed to create mesh from pipeline")
|
||||
|
||||
reduce_mesh_faces_inplace(mesh_out, max_faces=max_faces, verbose=True)
|
||||
face_reduce_worker = FaceReducer_2_1()
|
||||
mesh = face_reduce_worker(mesh)
|
||||
del face_reduce_worker
|
||||
|
||||
# Auto cleanup pipeline if enabled
|
||||
if auto_cleanup:
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
try:
|
||||
shapegen_pipe.to('cpu')
|
||||
if hasattr(shapegen_pipe, 'unet'):
|
||||
del shapegen_pipe.unet
|
||||
if hasattr(shapegen_pipe, 'vae'):
|
||||
del shapegen_pipe.vae
|
||||
if hasattr(shapegen_pipe, 'scheduler'):
|
||||
del shapegen_pipe.scheduler
|
||||
del outputs
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
print("Shape pipeline cleaned up")
|
||||
except Exception as e:
|
||||
print(f"Error during pipeline cleanup: {e}")
|
||||
|
||||
mesh_out = Mesh.load_trimesh(given_mesh=mesh)
|
||||
mesh_out.auto_normal()
|
||||
|
||||
processed_image_tensor = pils_to_torch_imgs([pil_image])
|
||||
|
||||
return (mesh_out, processed_image_tensor)
|
||||
|
||||
|
||||
class Hunyuan3D_21_TexGen:
|
||||
"""Hunyuan3D-2.1 Texture Generation"""
|
||||
|
||||
@@ -5561,8 +5548,15 @@ class Hunyuan3D_21_TexGen:
|
||||
|
||||
if os.path.exists(glb_path):
|
||||
try:
|
||||
print(f"[Hunyuan3D] Loading GLB with PBR materials")
|
||||
mesh_out = FastMesh.load(glb_path, load_pbr=True)
|
||||
glb_scene = trimesh.load(glb_path)
|
||||
|
||||
if hasattr(glb_scene, 'geometry') and glb_scene.geometry:
|
||||
mesh_name = list(glb_scene.geometry.keys())[0]
|
||||
glb_mesh = glb_scene.geometry[mesh_name]
|
||||
else:
|
||||
glb_mesh = glb_scene
|
||||
|
||||
mesh_out = Mesh.load_trimesh(given_mesh=glb_mesh)
|
||||
mesh_out.auto_normal()
|
||||
print(f"Loaded GLB mesh with PBR materials from: {glb_path}")
|
||||
except Exception as e:
|
||||
@@ -5571,7 +5565,8 @@ class Hunyuan3D_21_TexGen:
|
||||
|
||||
# If PBR failed or not requested, load regular textured mesh
|
||||
if mesh_out is None:
|
||||
mesh_out = FastMesh.load(result_path, load_pbr=create_pbr)
|
||||
textured_mesh = trimesh.load(result_path)
|
||||
mesh_out = Mesh.load_trimesh(given_mesh=textured_mesh)
|
||||
mesh_out.auto_normal()
|
||||
if create_pbr:
|
||||
print("Warning: PBR creation failed, loaded regular textured mesh")
|
||||
@@ -5592,6 +5587,7 @@ class Hunyuan3D_21_TexGen:
|
||||
except:
|
||||
pass
|
||||
|
||||
# --------------------- PARTCRAFTER ---------------------
|
||||
|
||||
class Load_PartCrafter_Pipeline:
|
||||
"""Load PartCrafter Pipeline"""
|
||||
@@ -5794,7 +5790,7 @@ class PartCrafter_Generate:
|
||||
print(f"GLB mesh path for Preview_3DMesh: {relative_scene_path}")
|
||||
|
||||
return (zip_path, relative_scene_path, processed_image_tensor)
|
||||
|
||||
#------ partcrafter scene ---------------------
|
||||
|
||||
class Load_PartCrafter_Scene_Pipeline:
|
||||
"""Load PartCrafter Scene Pipeline"""
|
||||
@@ -5998,3 +5994,4 @@ class PartCrafter_Generate:
|
||||
print(f"GLB mesh path for Preview_3DMesh: {relative_scene_path}")
|
||||
|
||||
return (zip_path, relative_scene_path, processed_image_tensor)
|
||||
|
||||
|
||||
+7
-23
@@ -8,7 +8,7 @@ PyGithub
|
||||
requests
|
||||
|
||||
# computational libraries
|
||||
numpy>=2.2.6
|
||||
numpy
|
||||
einops
|
||||
scipy
|
||||
kornia
|
||||
@@ -19,16 +19,12 @@ slangtorch
|
||||
cumm==0.7.11
|
||||
spconv-cu126
|
||||
|
||||
utils3d @ git+https://github.com/EasternJournalist/utils3d.git@e87d195da7270f575d9680d884d6aad2588864aa
|
||||
git+https://github.com/EasternJournalist/utils3d.git#egg=utils3d
|
||||
git+https://github.com/ashawkey/kiuikit.git#egg=kiui
|
||||
|
||||
# for use ML models
|
||||
hf_transfer
|
||||
hf_xet
|
||||
peft==0.17.1
|
||||
diffusers==0.31.0
|
||||
transformers==4.46.2
|
||||
accelerate==1.1.1
|
||||
diffusers>=0.26.1
|
||||
transformers>=4.36.2
|
||||
safetensors
|
||||
open_clip_torch
|
||||
mmgp
|
||||
@@ -37,6 +33,7 @@ mmgp
|
||||
torchmetrics
|
||||
pytorch_msssim
|
||||
pytorch-lightning
|
||||
peft
|
||||
|
||||
# for process images & videos
|
||||
rembg
|
||||
@@ -53,7 +50,7 @@ plyfile
|
||||
pygltflib
|
||||
xatlas
|
||||
pymeshlab
|
||||
gpytoolbox==0.3.7
|
||||
gpytoolbox
|
||||
PyMCubes
|
||||
libigl
|
||||
pyvista
|
||||
@@ -69,17 +66,4 @@ packaging
|
||||
OmegaConf
|
||||
pyhocon
|
||||
iopath
|
||||
easydict
|
||||
|
||||
# comfyui
|
||||
comfyui-workflow-templates
|
||||
comfyui-frontend-package
|
||||
comfyui-embedded-docs
|
||||
torchsde
|
||||
sentencepiece
|
||||
alembic
|
||||
SQLAlchemy
|
||||
av>=14.2.0
|
||||
spandrel
|
||||
pydantic~=2.0
|
||||
pydantic-settings~=2.0
|
||||
easydict
|
||||
Reference in New Issue
Block a user