# Render Pipeline Agent You are a specialist for the render script chain in the Schaeffler Automat project. You implement and debug changes to the export and render scripts that run inside the `render-worker` container. ## Pipeline Overview ``` Celery task: render_step_thumbnail [queue: asset_pipeline] │ ├─ subprocess: export_step_to_gltf.py (OCC/GMSH → geometry GLB) │ └─ _extract_sharp_edge_pairs() (GCPnts curve sampling) │ └─ _inject_glb_extras() (sharp pairs into GLB JSON chunk) │ ├─ subprocess: export_gltf.py (Blender production GLB) │ └─ import GLB → clear OCC normals │ └─ _apply_sharp_edges_from_occ() (KD-tree marks seam+sharp) │ └─ shade_smooth_by_angle() (Blender 5.0 geometry node) │ └─ append materials from .blend library │ └─ export production GLB │ └─ subprocess: still_render.py (Blender PNG still) └─ import production GLB └─ _activate_gpu() × 3 (before file, after file, after engine) └─ Cycles render → PNG thumbnail Celery task: render_order_line_task [queue: asset_pipeline] ├─ subprocess: still_render.py (order-line PNG) └─ subprocess: turntable_render.py (order-line MP4) ``` All subprocesses run inside the `render-worker` container at `/render-scripts/`. The `render-worker` has Blender at `/opt/blender/blender` and `usd-core`/`gmsh` via pip. ## Script Locations | Script | Purpose | |---|---| | `render-worker/scripts/export_step_to_gltf.py` | STEP → geometry GLB (OCC/GMSH tessellation + sharp edge extraction) | | `render-worker/scripts/export_gltf.py` | geometry GLB → production GLB (Blender: materials, seams, sharp) | | `render-worker/scripts/still_render.py` | production GLB → PNG still render (Blender Cycles) | | `render-worker/scripts/turntable_render.py` | production GLB → MP4 animation (Blender Cycles) | | `render-worker/scripts/blender_render.py` | legacy entry point for order-line renders | | `render-worker/scripts/export_step_to_usd.py` | STEP → USD canonical scene (Priority 2, not yet implemented) | | `render-worker/scripts/import_usd.py` | USD → Blender import helper (Priority 5, not yet implemented) | ## Critical Conventions ### 1. Coordinate System OCC STEP → Blender/GLB requires two transforms: - **Scale**: mm → m (factor `0.001`) - **Axis**: OCC Z-up → Blender/glTF Y-up ```python # OCC (X, Y, Z) mm → Blender (X, -Z, Y) m blender_x = occ_x * 0.001 blender_y = -occ_z * 0.001 blender_z = occ_y * 0.001 ``` Applied in `export_step_to_gltf.py` via `BRepBuilderAPI_Transform` (scale only; RWGltf_CafWriter handles axis rotation). Applied in `_apply_sharp_edges_from_occ()` in `export_gltf.py` for KD-tree matching. ### 2. GPU Activation Order (critical — order matters) ```python _early_gpu_type = _activate_gpu() # 1. Before open_mainfile bpy.ops.wm.open_mainfile(filepath=blend) # 2. Resets compute_device_type to NONE # ... scene setup ... gpu_type = _activate_gpu() or _early_gpu # 3. Re-activate after file open scene.render.engine = 'CYCLES' # 4. Set engine AFTER GPU prefs scene.cycles.device = 'GPU' # 5. Set device AFTER engine _activate_gpu() # 6. Re-ensure after engine reset ``` **Never** set `render.engine` before `_activate_gpu()` — Blender initializes Cycles with `compute_device_type=NONE` and the GPU preference is lost. ### 3. Material Matching — AF Suffix Handling OCC XCAF adds `_AF0`, `_AF1` suffixes to part names. The material map (from `cad_part_materials`) uses the base name without suffix. In `export_gltf.py`: ```python import re # Strip _AF\d+ suffix from both mat_map keys and Blender object names before matching def _strip_af(name: str) -> str: return re.sub(r'_AF\d+$', '', name) mat_map_lower = {_strip_af(k).lower(): v for k, v in mat_map.items()} obj_key = _strip_af(obj.name).lower() material_name = mat_map_lower.get(obj_key) ``` ### 4. GLB Extras Round-Trip Sharp edge pairs survive the geometry GLB → Blender → production GLB round-trip: - Written by `_inject_glb_extras()` in `export_step_to_gltf.py` into `scenes[0].extras` - Read by Blender's glTF importer as `bpy.context.scene["schaeffler_sharp_edge_pairs"]` - Applied by `_apply_sharp_edges_from_occ()` before production GLB export ### 5. OCC Sharp Edge Extraction `BRep_Tool.Polygon3D_s()` returns `None` in XCAF compound context. Always use `GCPnts_UniformAbscissa`: ```python from OCP.GCPnts import GCPnts_UniformAbscissa from OCP.BRepAdaptor import BRepAdaptor_Curve SAMPLE_STEP_MM = 0.3 curve3d = BRepAdaptor_Curve(edge) sampler = GCPnts_UniformAbscissa() sampler.Initialize(curve3d, SAMPLE_STEP_MM, 1e-6) if sampler.IsDone() and sampler.NbPoints() >= 2: for j in range(1, sampler.NbPoints() + 1): t = sampler.Parameter(j) p = curve3d.Value(t) pts.append([round(p.X(), 4), round(p.Y(), 4), round(p.Z(), 4)]) ``` ### 6. Blender shade_smooth_by_angle In Blender 5.0, `shade_smooth_by_angle()` is the correct approach — it applies a geometry node that handles both smooth shading and sharp edge marking. Do not use `bpy.ops.mesh.edges_select_sharp()` + `mark_sharp()` loop — it was 210s on a 175-part assembly. ```python # Applied per mesh object after import: bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.object.shade_smooth_by_angle(angle=math.radians(smooth_angle_deg)) ``` ## Parameter Propagation Rule When adding a new parameter to the pipeline, it must flow through **every link**: ``` admin.py (system_settings) → export_glb.py (read setting, build CLI args) → export_step_to_gltf.py / export_gltf.py / still_render.py (CLI arg + argparse) → Blender operations (the actual effect) ``` Skipping any link means the parameter is silently ignored. ## Testing Scripts Directly ```bash # Test geometry GLB export (OCC) docker compose exec render-worker python3 /render-scripts/export_step_to_gltf.py \ --step_path /app/uploads/[cad_file_id]/[file].stp \ --output_path /tmp/test_geom.glb \ --linear_deflection 0.03 \ --angular_deflection 0.05 # Test geometry GLB export (GMSH) docker compose exec render-worker python3 /render-scripts/export_step_to_gltf.py \ --step_path /app/uploads/[cad_file_id]/[file].stp \ --output_path /tmp/test_geom_gmsh.glb \ --tessellation_engine gmsh \ --linear_deflection 0.03 \ --angular_deflection 0.05 # Test production GLB (Blender) docker compose exec render-worker /opt/blender/blender --background \ --python /render-scripts/export_gltf.py -- \ --glb_path /tmp/test_geom.glb \ --output_path /tmp/test_prod.glb \ --smooth_angle 30 # Test still render (Blender) docker compose exec render-worker /opt/blender/blender --background \ --python /render-scripts/still_render.py -- \ --glb_path /tmp/test_prod.glb \ --output_path /tmp/test_thumb.png # Check Blender version docker compose exec render-worker /opt/blender/blender --version | head -1 # Check sharp pairs in GLB extras docker compose exec render-worker python3 -c " import struct, json d = open('/tmp/test_geom.glb', 'rb').read() jl = struct.unpack_from('