261 lines
8.4 KiB
Python
261 lines
8.4 KiB
Python
|
|
import bpy
|
|||
|
|
import sys
|
|||
|
|
import os
|
|||
|
|
import argparse
|
|||
|
|
import tempfile
|
|||
|
|
|
|||
|
|
|
|||
|
|
def clean_scene():
|
|||
|
|
bpy.ops.object.select_all(action='SELECT')
|
|||
|
|
bpy.ops.object.delete(use_global=False)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _parse_args_blender(argv):
|
|||
|
|
"""Blender 参数在 `--` 之后。"""
|
|||
|
|
if "--" in argv:
|
|||
|
|
return argv[argv.index("--") + 1:]
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _is_glb_gltf(path: str) -> bool:
|
|||
|
|
ext = os.path.splitext(path)[1].lower()
|
|||
|
|
return ext in [".glb", ".gltf"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _is_obj(path: str) -> bool:
|
|||
|
|
return os.path.splitext(path)[1].lower() == ".obj"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def import_mesh_any(input_path: str):
|
|||
|
|
"""
|
|||
|
|
导入 OBJ 或 GLB/GLTF,并将场景中所有 MESH 合并为一个 active mesh 对象返回。
|
|||
|
|
"""
|
|||
|
|
clean_scene()
|
|||
|
|
|
|||
|
|
if _is_glb_gltf(input_path):
|
|||
|
|
print(f"[import] GLB/GLTF: {input_path}")
|
|||
|
|
bpy.ops.import_scene.gltf(filepath=input_path)
|
|||
|
|
elif _is_obj(input_path):
|
|||
|
|
print(f"[import] OBJ: {input_path}")
|
|||
|
|
bpy.ops.wm.obj_import(filepath=input_path)
|
|||
|
|
else:
|
|||
|
|
raise RuntimeError(f"Unsupported input format: {input_path}")
|
|||
|
|
|
|||
|
|
meshes = [o for o in bpy.context.scene.objects if o.type == "MESH"]
|
|||
|
|
if not meshes:
|
|||
|
|
raise RuntimeError("No mesh objects found after import.")
|
|||
|
|
|
|||
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|||
|
|
for o in meshes:
|
|||
|
|
o.select_set(True)
|
|||
|
|
bpy.context.view_layer.objects.active = meshes[0]
|
|||
|
|
|
|||
|
|
if len(meshes) > 1:
|
|||
|
|
bpy.ops.object.join()
|
|||
|
|
|
|||
|
|
obj = bpy.context.view_layer.objects.active
|
|||
|
|
obj.select_set(True)
|
|||
|
|
return obj
|
|||
|
|
|
|||
|
|
|
|||
|
|
def export_obj_selected(output_obj: str):
|
|||
|
|
out_dir = os.path.dirname(output_obj)
|
|||
|
|
if out_dir:
|
|||
|
|
os.makedirs(out_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
bpy.ops.wm.obj_export(
|
|||
|
|
filepath=output_obj,
|
|||
|
|
export_selected_objects=True,
|
|||
|
|
export_normals=True,
|
|||
|
|
export_uv=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fix_mesh(obj):
|
|||
|
|
"""修复网格使其更符合 QuadriFlow 的输入要求"""
|
|||
|
|
bpy.context.view_layer.objects.active = obj
|
|||
|
|
obj.select_set(True)
|
|||
|
|
|
|||
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|||
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|||
|
|
|
|||
|
|
bpy.ops.mesh.remove_doubles(threshold=0.001)
|
|||
|
|
bpy.ops.mesh.delete_loose()
|
|||
|
|
|
|||
|
|
bpy.ops.mesh.select_all(action='DESELECT')
|
|||
|
|
bpy.ops.mesh.select_non_manifold()
|
|||
|
|
|
|||
|
|
selected_count = sum(1 for v in obj.data.vertices if v.select)
|
|||
|
|
if selected_count > 0:
|
|||
|
|
print(f"[fix_mesh] found {selected_count} non-manifold verts, deleting...")
|
|||
|
|
bpy.ops.mesh.delete(type='VERT')
|
|||
|
|
|
|||
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|||
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
|||
|
|
|
|||
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|||
|
|
bpy.ops.mesh.fill_holes(sides=0)
|
|||
|
|
|
|||
|
|
bpy.ops.mesh.remove_doubles(threshold=0.001)
|
|||
|
|
bpy.ops.mesh.dissolve_degenerate(threshold=0.0001)
|
|||
|
|
|
|||
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|||
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
|||
|
|
|
|||
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def quadriflow_remesh_obj(
|
|||
|
|
input_obj: str,
|
|||
|
|
output_obj: str,
|
|||
|
|
face_count: int = 8000,
|
|||
|
|
use_mesh_symmetry: bool = False,
|
|||
|
|
use_preserve_sharp: bool = False,
|
|||
|
|
use_preserve_boundary: bool = True,
|
|||
|
|
use_voxel_preprocess: bool = True,
|
|||
|
|
voxel_size: float = 0.008,
|
|||
|
|
):
|
|||
|
|
"""只处理 OBJ 输入(你的原流程)"""
|
|||
|
|
obj = import_mesh_any(input_obj) # 这里会走 OBJ import
|
|||
|
|
|
|||
|
|
print(f"[import] object={obj.name}, verts={len(obj.data.vertices)}, faces={len(obj.data.polygons)}")
|
|||
|
|
print("[mesh] fixing...")
|
|||
|
|
fix_mesh(obj)
|
|||
|
|
|
|||
|
|
if use_voxel_preprocess:
|
|||
|
|
print(f"[voxel] preprocess voxel_size={voxel_size} ...")
|
|||
|
|
voxel_mod = obj.modifiers.new(name="Voxel", type='REMESH')
|
|||
|
|
voxel_mod.mode = 'VOXEL'
|
|||
|
|
voxel_mod.voxel_size = voxel_size
|
|||
|
|
voxel_mod.use_smooth_shade = True
|
|||
|
|
voxel_mod.use_remove_disconnected = False
|
|||
|
|
bpy.ops.object.modifier_apply(modifier=voxel_mod.name)
|
|||
|
|
|
|||
|
|
print(f"[voxel] after: verts={len(obj.data.vertices)}, faces={len(obj.data.polygons)}")
|
|||
|
|
print("[mesh] fixing after voxel...")
|
|||
|
|
fix_mesh(obj)
|
|||
|
|
print(f"[mesh] after fix2: verts={len(obj.data.vertices)}, faces={len(obj.data.polygons)}")
|
|||
|
|
|
|||
|
|
print(f"[quadriflow] target_faces={face_count} ...")
|
|||
|
|
bpy.context.view_layer.objects.active = obj
|
|||
|
|
obj.select_set(True)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
bpy.ops.object.quadriflow_remesh(
|
|||
|
|
use_mesh_symmetry=use_mesh_symmetry,
|
|||
|
|
use_preserve_sharp=use_preserve_sharp,
|
|||
|
|
use_preserve_boundary=use_preserve_boundary,
|
|||
|
|
smooth_normals=False,
|
|||
|
|
mode='FACES',
|
|||
|
|
target_faces=face_count,
|
|||
|
|
seed=0
|
|||
|
|
)
|
|||
|
|
print("[quadriflow] done.")
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[quadriflow] error: {e} (continue to export)")
|
|||
|
|
|
|||
|
|
print(f"[export] {output_obj}")
|
|||
|
|
export_obj_selected(output_obj)
|
|||
|
|
print(f"[done] verts={len(obj.data.vertices)}, faces={len(obj.data.polygons)}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def glb_to_tmp_obj(input_glb: str) -> str:
|
|||
|
|
"""
|
|||
|
|
在 Blender 内把 GLB/GLTF 导入后导出为临时 OBJ(按你的要求“先转OBJ再走后续”)。
|
|||
|
|
"""
|
|||
|
|
obj = import_mesh_any(input_glb) # 会走 gltf import
|
|||
|
|
tmp_obj = tempfile.mktemp(suffix=".obj")
|
|||
|
|
print(f"[glb2obj] export tmp obj -> {tmp_obj}")
|
|||
|
|
export_obj_selected(tmp_obj)
|
|||
|
|
return tmp_obj
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run_pipeline(
|
|||
|
|
input_path: str,
|
|||
|
|
output_obj: str,
|
|||
|
|
face_count: int,
|
|||
|
|
mesh_symmetry: bool,
|
|||
|
|
preserve_sharp: bool,
|
|||
|
|
preserve_boundary: bool,
|
|||
|
|
no_voxel_preprocess: bool,
|
|||
|
|
voxel_size: float,
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
统一入口:
|
|||
|
|
- 输入 OBJ:直接 remesh
|
|||
|
|
- 输入 GLB/GLTF:先转临时 OBJ,再 remesh
|
|||
|
|
"""
|
|||
|
|
tmp_obj = None
|
|||
|
|
try:
|
|||
|
|
if _is_glb_gltf(input_path):
|
|||
|
|
tmp_obj = glb_to_tmp_obj(input_path)
|
|||
|
|
quadriflow_remesh_obj(
|
|||
|
|
input_obj=tmp_obj,
|
|||
|
|
output_obj=output_obj,
|
|||
|
|
face_count=face_count,
|
|||
|
|
use_mesh_symmetry=mesh_symmetry,
|
|||
|
|
use_preserve_sharp=preserve_sharp,
|
|||
|
|
use_preserve_boundary=preserve_boundary,
|
|||
|
|
use_voxel_preprocess=(not no_voxel_preprocess),
|
|||
|
|
voxel_size=voxel_size,
|
|||
|
|
)
|
|||
|
|
elif _is_obj(input_path):
|
|||
|
|
quadriflow_remesh_obj(
|
|||
|
|
input_obj=input_path,
|
|||
|
|
output_obj=output_obj,
|
|||
|
|
face_count=face_count,
|
|||
|
|
use_mesh_symmetry=mesh_symmetry,
|
|||
|
|
use_preserve_sharp=preserve_sharp,
|
|||
|
|
use_preserve_boundary=preserve_boundary,
|
|||
|
|
use_voxel_preprocess=(not no_voxel_preprocess),
|
|||
|
|
voxel_size=voxel_size,
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
raise RuntimeError(f"Unsupported input format: {input_path}")
|
|||
|
|
finally:
|
|||
|
|
if tmp_obj and os.path.exists(tmp_obj):
|
|||
|
|
try:
|
|||
|
|
os.remove(tmp_obj)
|
|||
|
|
print(f"[cleanup] removed tmp obj: {tmp_obj}")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
parser = argparse.ArgumentParser(description="GLB/GLTF/OBJ -> (optional tmp OBJ) -> QuadriFlow remesh -> OBJ")
|
|||
|
|
parser.add_argument("-i", "--input", default="trellis_out/sample.glb", help="Input file path (.obj/.glb/.gltf).")
|
|||
|
|
parser.add_argument("-o", "--output", default="output/test.obj", help="Output OBJ file path.")
|
|||
|
|
parser.add_argument("--face_count", type=int, default=8000, help="Target quad face count.")
|
|||
|
|
parser.add_argument("--mesh_symmetry", action="store_true", help="Enable mesh symmetry.")
|
|||
|
|
parser.add_argument("--preserve_sharp", action="store_true", help="Preserve sharp edges.")
|
|||
|
|
parser.add_argument("--preserve_boundary", action="store_true", help="Preserve boundary edges.")
|
|||
|
|
parser.add_argument("--no_voxel_preprocess", action="store_true", help="Disable voxel preprocess.")
|
|||
|
|
parser.add_argument("--voxel_size", type=float, default=0.008, help="Voxel size for preprocess (smaller=denser).")
|
|||
|
|
|
|||
|
|
args = parser.parse_args(_parse_args_blender(sys.argv))
|
|||
|
|
|
|||
|
|
if not os.path.exists(args.input):
|
|||
|
|
raise FileNotFoundError(f"Input not found: {args.input}")
|
|||
|
|
|
|||
|
|
run_pipeline(
|
|||
|
|
input_path=args.input,
|
|||
|
|
output_obj=args.output,
|
|||
|
|
face_count=args.face_count,
|
|||
|
|
mesh_symmetry=args.mesh_symmetry,
|
|||
|
|
preserve_sharp=args.preserve_sharp,
|
|||
|
|
preserve_boundary=args.preserve_boundary,
|
|||
|
|
no_voxel_preprocess=args.no_voxel_preprocess,
|
|||
|
|
voxel_size=args.voxel_size,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
try:
|
|||
|
|
main()
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[fatal] {e}")
|
|||
|
|
import traceback
|
|||
|
|
|
|||
|
|
traceback.print_exc()
|
|||
|
|
raise
|