Files
HartOMat/.claude/commands/render-pipeline.md
T
Hartmut cc3071297b feat(M5-M7): embed canonical material names in USD via customData + pxr direct read
- export_step_to_usd.py: accept --material_map CLI arg, write
  schaeffler:canonicalMaterialName as customData on each Mesh prim,
  fix geometry transform (strip shape Location before face exploration,
  apply both face_loc and shape_loc sequentially)
- import_usd.py: after Blender USD import, use pxr to read customData
  directly from the USD file — builds {part_key: material_name} lookup
  (Blender ignores STRING primvars and customData, but pxr reads both)
- _blender_materials.py: add apply_material_library_direct() for exact
  dict-based material assignment without name-matching heuristics
- _blender_scene_setup.py: prefer direct USD lookup, fall back to
  name-matching for legacy USD files without material metadata
- export_glb.py (generate_usd_master_task): resolve material_map via
  material_service.resolve_material_map() and pass to subprocess;
  include material hash in cache key for invalidation
- ROADMAP.md: update P5 status, add M5-M7 milestones

Tested: 3/3 parts matched (ans_lfs120), 172/175 parts matched
(F-802007.TR4-D1-H122AG). Previous: 0/25 matched.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:04:26 +01:00

212 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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('<I', d, 12)[0]
j = json.loads(d[20:20+jl])
pairs = j.get('scenes', [{}])[0].get('extras', {}).get('schaeffler_sharp_edge_pairs', [])
print(f'{len(pairs)} sharp edge pairs in GLB extras')
if pairs: print('First pair:', pairs[0])
"
```
## Common Problems
| Symptom | Cause | Fix |
|---|---|---|
| No sharp edges in Blender after import | `Polygon3D_s()` returned None | Use `GCPnts_UniformAbscissa` (already in export_step_to_gltf.py) |
| GLB extras not read by Blender | `scenes[0].extras` not patched | Check `_inject_glb_extras()` called after `RWGltf_CafWriter.Perform()` |
| Materials not applied | AF suffix mismatch | Verify `_strip_af()` applied to both map keys and object names |
| Render is black / no GPU | GPU activation called in wrong order | Follow the 6-step GPU activation order above |
| Faceting on cylinders | OCC BRepMesh angular/linear deflection mismatch | Switch to GMSH tessellation engine |
| Fan triangles at seam | OCC BRepMesh periodic face seam limitation | GMSH Frontal-Delaunay fixes this structurally |
| `shade_smooth_by_angle` error | Blender version < 5.0 | Verify `BLENDER_VERSION=5.0.1` in render-worker |
| GMSH hangs | `gmsh.finalize()` not called | Wrap entire GMSH block in try/finally with `gmsh.finalize()` |
## Failure Protocol
If a script fails mid-pipeline:
1. Note the exact script name and error message
2. Add `[BLOCKED]` to the failing task in `plan.md`
3. Invoke `/plan` to refine — include: script name, CLI args used, and full traceback
## Completion
After changes to render scripts: "Render pipeline updated. Test with the commands above, then verify with `/check` and `/review`."