5ee4b2e3b5
- bpy.ops.import_mesh.stl → bpy.ops.wm.stl_import (removed in Blender 4.0) - mesh.use_auto_smooth = True → bpy.ops.object.shade_smooth_by_angle() (use_auto_smooth removed in Blender 4.1) - Apply smooth shading to all objects unconditionally after scale, so GLB is smooth-shaded even when no sharp edge midpoints are present Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
145 lines
4.8 KiB
Python
145 lines
4.8 KiB
Python
"""Blender headless script: export a STEP-derived scene as a production GLB.
|
|
|
|
Usage:
|
|
blender --background --python export_gltf.py -- \\
|
|
--stl_path /path/to/file.stl \\
|
|
--output_path /path/to/output.glb \\
|
|
[--asset_library_blend /path/to/library.blend] \\
|
|
[--material_map '{"SrcMat": "LibMat"}']
|
|
|
|
The script:
|
|
1. Imports the STL file (with mm→m scale).
|
|
2. Optionally applies asset library materials from a .blend.
|
|
3. Exports as GLB (Draco-compressed if available, otherwise standard).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import traceback
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
argv = sys.argv
|
|
if "--" not in argv:
|
|
print("No arguments after --", file=sys.stderr)
|
|
sys.exit(1)
|
|
rest = argv[argv.index("--") + 1:]
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--stl_path", required=True)
|
|
parser.add_argument("--output_path", required=True)
|
|
parser.add_argument("--asset_library_blend", default=None)
|
|
parser.add_argument("--material_map", default="{}")
|
|
parser.add_argument("--sharp_edges_json", default="[]",
|
|
help="JSON array of [x, y, z] midpoints (mm) to mark as sharp edges")
|
|
return parser.parse_args(rest)
|
|
|
|
|
|
def mark_sharp_edges_by_proximity(midpoints_mm: list, threshold_mm: float = 1.0) -> None:
|
|
"""Mark Blender mesh edges as sharp based on proximity to OCC-derived midpoints.
|
|
|
|
midpoints_mm: list of [x, y, z] in mm (from OCC coordinate space).
|
|
After STL import + scale-apply (mm→m), Blender vertices are in meters, so we
|
|
convert the edge midpoint back to mm before comparing.
|
|
threshold_mm: snap distance in mm (default 1.0 mm).
|
|
"""
|
|
if not midpoints_mm:
|
|
return
|
|
|
|
import bpy # type: ignore[import]
|
|
import math
|
|
|
|
for obj in bpy.data.objects:
|
|
if obj.type != "MESH":
|
|
continue
|
|
mesh = obj.data
|
|
# Blender 4.1+ removed use_auto_smooth — use shade_smooth_by_angle instead
|
|
bpy.context.view_layer.objects.active = obj
|
|
obj.select_set(True)
|
|
try:
|
|
bpy.ops.object.shade_smooth_by_angle(angle=math.radians(30))
|
|
except Exception:
|
|
pass # fallback: stay flat-shaded
|
|
mw = obj.matrix_world
|
|
for edge in mesh.edges:
|
|
v1 = mw @ mesh.vertices[edge.vertices[0]].co
|
|
v2 = mw @ mesh.vertices[edge.vertices[1]].co
|
|
# Convert Blender meters → mm for comparison
|
|
mid_mm = [
|
|
(v1.x + v2.x) / 2 * 1000,
|
|
(v1.y + v2.y) / 2 * 1000,
|
|
(v1.z + v2.z) / 2 * 1000,
|
|
]
|
|
for hint in midpoints_mm:
|
|
dist_sq = sum((a - b) ** 2 for a, b in zip(mid_mm, hint))
|
|
if dist_sq < threshold_mm ** 2:
|
|
edge.use_edge_sharp = True
|
|
break
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
material_map: dict = json.loads(args.material_map)
|
|
sharp_edge_midpoints: list = json.loads(args.sharp_edges_json)
|
|
|
|
import bpy # type: ignore[import]
|
|
|
|
# Clean scene
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
# Import STL (bpy.ops.wm.stl_import is the Blender 4.0+ API)
|
|
bpy.ops.wm.stl_import(filepath=args.stl_path)
|
|
|
|
# Scale mm → m
|
|
for obj in bpy.context.selected_objects:
|
|
obj.scale = (0.001, 0.001, 0.001)
|
|
bpy.context.view_layer.objects.active = obj
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
|
|
# Apply smooth shading with 30° angle threshold (Blender 4.1+ API)
|
|
import math as _math
|
|
for obj in bpy.data.objects:
|
|
if obj.type == "MESH":
|
|
bpy.context.view_layer.objects.active = obj
|
|
obj.select_set(True)
|
|
try:
|
|
bpy.ops.object.shade_smooth_by_angle(angle=_math.radians(30))
|
|
except Exception:
|
|
pass
|
|
|
|
# Mark sharp edges for better UV seams
|
|
if sharp_edge_midpoints:
|
|
mark_sharp_edges_by_proximity(sharp_edge_midpoints)
|
|
print(f"Marked sharp edges from {len(sharp_edge_midpoints)} hint points")
|
|
|
|
# Apply asset library materials if provided
|
|
if args.asset_library_blend and material_map:
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from asset_library import apply_asset_library_materials
|
|
apply_asset_library_materials(args.asset_library_blend, material_map)
|
|
|
|
# Export GLB
|
|
try:
|
|
bpy.ops.export_scene.gltf(
|
|
filepath=args.output_path,
|
|
export_format="GLB",
|
|
export_apply=True,
|
|
use_selection=False,
|
|
)
|
|
except Exception as exc:
|
|
print(f"GLB export failed: {exc}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(f"GLB exported to {args.output_path}")
|
|
|
|
|
|
try:
|
|
main()
|
|
except SystemExit:
|
|
raise
|
|
except Exception:
|
|
traceback.print_exc()
|
|
sys.exit(1)
|