From c054236d224c23aafc2dfa0c76b26ba8a7eb50f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sat, 14 Mar 2026 14:19:21 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20material=20override=20pipeline=20?= =?UTF-8?q?=E2=80=94=20pass=20--material-override=20CLI=20arg=20to=20Blend?= =?UTF-8?q?er=20scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../domains/pipeline/tasks/render_order_line.py | 14 ++++++++++++-- backend/app/services/render_blender.py | 6 ++++++ backend/app/services/step_processor.py | 2 ++ render-worker/scripts/_blender_args.py | 6 ++++++ render-worker/scripts/_blender_scene_setup.py | 16 ++++++++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/backend/app/domains/pipeline/tasks/render_order_line.py b/backend/app/domains/pipeline/tasks/render_order_line.py index 637d332..29c7dfc 100644 --- a/backend/app/domains/pipeline/tasks/render_order_line.py +++ b/backend/app/domains/pipeline/tasks/render_order_line.py @@ -201,8 +201,16 @@ def render_order_line_task(self, order_line_id: str): # Apply global material override from OutputType (e.g. x-ray mode) if line.output_type and line.output_type.material_override: override_mat = line.output_type.material_override - material_map = {k: override_mat for k in material_map} - emit(order_line_id, f"Material override active: all parts → {override_mat}") + # Build override map from existing material_map keys or from parsed STEP parts + override_keys = set() + if material_map: + override_keys = set(material_map.keys()) + if cad_file and cad_file.parsed_objects: + for part_name in cad_file.parsed_objects.get("objects", []): + override_keys.add(part_name) + material_map = {k: override_mat for k in override_keys} + use_materials = True + emit(order_line_id, f"Material override active: {len(material_map)} parts → {override_mat}") if template: emit(order_line_id, f"Using render template: {template.name} (collection={template.target_collection}, material_replace={template.material_replace_enabled}, lighting_only={template.lighting_only})") @@ -352,6 +360,7 @@ def render_order_line_task(self, order_line_id: str): usd_path=usd_render_path, focal_length_mm=focal_length_mm, sensor_width_mm=sensor_width_mm, + material_override=line.output_type.material_override if line.output_type else None, ) success = True render_log = { @@ -410,6 +419,7 @@ def render_order_line_task(self, order_line_id: str): rotation_z=rotation_z, focal_length_mm=focal_length_mm, sensor_width_mm=sensor_width_mm, + material_override=line.output_type.material_override if line.output_type else None, job_id=order_line_id, order_line_id=order_line_id, noise_threshold=noise_threshold, diff --git a/backend/app/services/render_blender.py b/backend/app/services/render_blender.py index 74232af..6642b27 100644 --- a/backend/app/services/render_blender.py +++ b/backend/app/services/render_blender.py @@ -94,6 +94,7 @@ def render_still( tessellation_engine: str = "occ", focal_length_mm: float | None = None, sensor_width_mm: float | None = None, + material_override: str | None = None, ) -> dict: """Convert STEP → GLB (OCC or GMSH) → PNG (Blender subprocess). @@ -185,6 +186,8 @@ def render_still( cmd += ["--focal-length", str(focal_length_mm)] if sensor_width_mm is not None: cmd += ["--sensor-width", str(sensor_width_mm)] + if material_override: + cmd += ["--material-override", material_override] return cmd def _run(eng: str) -> tuple[int, list[str], list[str]]: @@ -322,6 +325,7 @@ def render_turntable_to_file( tessellation_engine: str = "occ", focal_length_mm: float | None = None, sensor_width_mm: float | None = None, + material_override: str | None = None, ) -> dict: """Render a turntable animation: STEP → STL → N frames (Blender) → mp4 (ffmpeg). @@ -408,6 +412,8 @@ def render_turntable_to_file( cmd += ["--focal-length", str(focal_length_mm)] if sensor_width_mm is not None: cmd += ["--sensor-width", str(sensor_width_mm)] + if material_override: + cmd += ["--material-override", material_override] log_lines: list[str] = [] diff --git a/backend/app/services/step_processor.py b/backend/app/services/step_processor.py index 70479b9..40a461c 100644 --- a/backend/app/services/step_processor.py +++ b/backend/app/services/step_processor.py @@ -893,6 +893,7 @@ def render_to_file( tessellation_engine: str | None = None, focal_length_mm: float | None = None, sensor_width_mm: float | None = None, + material_override: str | None = None, ) -> tuple[bool, dict]: """Render a STEP file to a specific output path using current system settings. @@ -1031,6 +1032,7 @@ def render_to_file( tessellation_engine=tessellation_engine or settings["tessellation_engine"], focal_length_mm=focal_length_mm, sensor_width_mm=sensor_width_mm, + material_override=material_override, ) rendered_png = tmp_png if tmp_png.exists() else None except Exception as exc: diff --git a/render-worker/scripts/_blender_args.py b/render-worker/scripts/_blender_args.py index 7c7f5dd..7be5ba3 100644 --- a/render-worker/scripts/_blender_args.py +++ b/render-worker/scripts/_blender_args.py @@ -73,6 +73,11 @@ def parse_args() -> SimpleNamespace: _sw_idx = sys.argv.index("--sensor-width") sensor_width_mm_override = float(sys.argv[_sw_idx + 1]) if _sw_idx + 1 < len(sys.argv) else None + material_override = None + if "--material-override" in sys.argv: + _mo_idx = sys.argv.index("--material-override") + material_override = sys.argv[_mo_idx + 1] if _mo_idx + 1 < len(sys.argv) else None + if template_path and not os.path.isfile(template_path): print(f"[blender_render] ERROR: template not found: {template_path}") sys.exit(1) @@ -108,4 +113,5 @@ def parse_args() -> SimpleNamespace: use_template=bool(template_path), focal_length_mm=focal_length_mm, sensor_width_mm=sensor_width_mm_override, + material_override=material_override, ) diff --git a/render-worker/scripts/_blender_scene_setup.py b/render-worker/scripts/_blender_scene_setup.py index f6d59b5..827b586 100644 --- a/render-worker/scripts/_blender_scene_setup.py +++ b/render-worker/scripts/_blender_scene_setup.py @@ -65,6 +65,14 @@ def _setup_mode_b(args, lap_fn: Callable[[str], None]) -> None: 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( @@ -136,6 +144,14 @@ def _setup_mode_a(args) -> None: 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(