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

8.8 KiB
Raw Blame History

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
# 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)

_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:

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:

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.

# 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

# 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."