Files
HartOMat/render-worker/scripts/_blender_scene_setup.py
T
Hartmut c054236d22 fix: material override pipeline — pass --material-override CLI arg to Blender scripts
The initial implementation only overrode the material_map dict in the task,
but the Blender USD primvar path bypassed it. Now:
- Added --material-override named CLI arg parsed in _blender_args.py
- Both Mode A (factory) and Mode B (template) in _blender_scene_setup.py
  override usd_material_lookup and material_map when set
- Passed through full chain: task → step_processor → render_blender → CLI → Blender
- Tested: 175-part bearing rendered with single Steel-Bare material (1/1 materials)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:19:21 +01:00

187 lines
7.7 KiB
Python

"""MODE A / MODE B scene setup for blender_render.py.
MODE A — factory settings (no template): auto-camera + auto-lights
MODE B — template file: load .blend, import into named collection
"""
import time as _time
from typing import Callable
import bpy # type: ignore[import]
from _blender_camera import setup_auto_camera, setup_auto_lights
from _blender_import import import_glb, apply_rotation, import_usd_file
from _blender_materials import (
assign_failed_material,
build_mat_map_lower,
apply_material_library,
apply_material_library_direct,
)
from _blender_scene import (
ensure_collection,
apply_smooth_batch,
apply_sharp_edges_from_occ,
setup_shadow_catcher,
)
def setup_scene(args, lap_fn: Callable[[str], None]) -> None:
"""Set up the Blender scene according to args (MODE A or B).
Handles import, rotation, smooth shading, material assignment, shadow
catcher, and auto-camera/lights. lap_fn is called with a label string
at each timing checkpoint.
"""
if args.use_template:
_setup_mode_b(args, lap_fn)
else:
_setup_mode_a(args)
def _setup_mode_b(args, lap_fn: Callable[[str], None]) -> None:
"""MODE B: Template-based render — load .blend, import into collection."""
print(f"[blender_render] Opening template: {args.template_path}")
bpy.ops.wm.open_mainfile(filepath=args.template_path)
lap_fn("template_load")
target_col = ensure_collection(args.target_collection)
usd_material_lookup: dict = {}
if args.usd_path:
parts, usd_material_lookup = import_usd_file(args.usd_path)
else:
parts = import_glb(args.glb_path)
lap_fn("glb_import")
apply_rotation(parts, args.rotation_x, args.rotation_y, args.rotation_z)
lap_fn("rotation")
for part in parts:
for col in list(part.users_collection):
col.objects.unlink(part)
target_col.objects.link(part)
apply_smooth_batch(parts, args.smooth_angle)
if not args.usd_path:
_occ_pairs = args.mesh_attributes.get("sharp_edge_pairs") or []
if _occ_pairs:
apply_sharp_edges_from_occ(parts, _occ_pairs)
lap_fn("smooth_shading")
# Apply material override: replace all material lookups with a single material
if getattr(args, 'material_override', None):
print(f"[blender_render] material_override active: all parts → {args.material_override}", flush=True)
if usd_material_lookup:
usd_material_lookup = {k: args.material_override for k in usd_material_lookup}
if args.material_map:
args.material_map = {k: args.material_override for k in args.material_map}
if args.material_library_path and usd_material_lookup:
# USD primvar path: direct material assignment (no name-matching needed)
apply_material_library_direct(
parts, args.material_library_path, usd_material_lookup,
)
# Fall back to name-matching for any parts missing primvars
if args.material_map:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and
p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
if _unassigned:
print(f"[blender_render] {len(_unassigned)} parts without USD primvar — "
f"falling back to name-matching", flush=True)
apply_material_library(
_unassigned, args.material_library_path,
build_mat_map_lower(args.material_map), args.part_names_ordered,
)
elif args.material_library_path and args.material_map:
apply_material_library(
parts, args.material_library_path,
build_mat_map_lower(args.material_map), args.part_names_ordered,
)
else:
for part in parts:
assign_failed_material(part)
lap_fn("material_assign")
if args.shadow_catcher:
setup_shadow_catcher(parts)
needs_auto_camera = (
(args.lighting_only and not args.shadow_catcher)
or not bpy.context.scene.camera
)
if args.lighting_only and not args.shadow_catcher:
print("[blender_render] lighting_only mode: using template World/HDRI, forcing auto-camera")
elif needs_auto_camera:
print("[blender_render] WARNING: template has no camera — will create auto-camera")
if not needs_auto_camera and bpy.context.scene.camera:
bpy.context.scene.camera.data.clip_start = 0.001
print(f"[blender_render] template mode: {len(parts)} parts imported into collection '{args.target_collection}'")
if needs_auto_camera:
setup_auto_camera(parts, args.width, args.height,
lens_mm=args.focal_length_mm,
sensor_width_mm=args.sensor_width_mm)
def _setup_mode_a(args) -> None:
"""MODE A: Factory settings — auto-camera + auto-lights."""
bpy.ops.wm.read_factory_settings(use_empty=True)
usd_material_lookup: dict = {}
if args.usd_path:
parts, usd_material_lookup = import_usd_file(args.usd_path)
else:
parts = import_glb(args.glb_path)
apply_rotation(parts, args.rotation_x, args.rotation_y, args.rotation_z)
_t = _time.time()
apply_smooth_batch(parts, args.smooth_angle)
if not args.usd_path:
_occ_pairs = args.mesh_attributes.get("sharp_edge_pairs") or []
if _occ_pairs:
apply_sharp_edges_from_occ(parts, _occ_pairs)
for part in parts:
assign_failed_material(part)
print(f"[blender_render] smooth+fallback-material: {len(parts)} parts ({_time.time()-_t:.2f}s)", flush=True)
# Apply material override: replace all material lookups with a single material
if getattr(args, 'material_override', None):
print(f"[blender_render] material_override active (Mode A): all parts → {args.material_override}", flush=True)
if usd_material_lookup:
usd_material_lookup = {k: args.material_override for k in usd_material_lookup}
if args.material_map:
args.material_map = {k: args.material_override for k in args.material_map}
if args.material_library_path and usd_material_lookup:
# USD primvar path: direct material assignment
apply_material_library_direct(
parts, args.material_library_path, usd_material_lookup,
)
# Fall back to name-matching for parts without primvars
if args.material_map:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and
p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
if _unassigned:
apply_material_library(
_unassigned, args.material_library_path,
build_mat_map_lower(args.material_map), args.part_names_ordered,
)
elif args.material_library_path and args.material_map:
apply_material_library(
parts, args.material_library_path,
build_mat_map_lower(args.material_map), args.part_names_ordered,
)
bbox_center, bsphere_radius = setup_auto_camera(parts, args.width, args.height,
lens_mm=args.focal_length_mm,
sensor_width_mm=args.sensor_width_mm)
setup_auto_lights(bbox_center, bsphere_radius)
world = bpy.data.worlds.new("World")
bpy.context.scene.world = world
world.use_nodes = True
bg = world.node_tree.nodes["Background"]
bg.inputs["Color"].default_value = (0.96, 0.96, 0.97, 1.0)
bg.inputs["Strength"].default_value = 0.15