- added renderer
- Added undo
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
# Realistic Render View — Implementation Plan
|
||||
|
||||
## Context
|
||||
|
||||
Add a **"Render"** feature to Fluency CAD that opens a separate window for photorealistic rendering of the selected component or assembly (like KeyShot/Cacles).
|
||||
|
||||
**Constraints:**
|
||||
- Open in a **new window** — don't clutter the workspace
|
||||
- **Keep existing OCCRenderer** for the interactive 3D viewport — untouched
|
||||
- Render backend must be a **separate, swappable module** so we can change the renderer later
|
||||
- Use **Mitsuba 3** as the initial backend (`pip install mitsuba`, ~50MB)
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Main Fluency Window (existing OCCRenderer — untouched) │
|
||||
│ │
|
||||
│ [Select body/assembly] → [Click "Render"] │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ RenderWindow (separate QMainWindow)│ │
|
||||
│ │ │ │
|
||||
│ │ ┌───────────────────────────────┐ │ │
|
||||
│ │ │ RenderBackend (ABC) │ │ │
|
||||
│ │ │ ├─ MitsubaBackend ← current │ │ │
|
||||
│ │ │ ├─ (future: BlenderBackend) │ │ │
|
||||
│ │ │ └─ (future: CyclesBackend) │ │ │
|
||||
│ │ └───────────────────────────────┘ │ │
|
||||
│ │ │ │\n│ │ [Image preview] [Progress bar] │ │
|
||||
│ │ [Material ▾] [Quality ▾] [Render] │ │
|
||||
│ │ [Export PNG] │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Swappable Backend Interface
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
@dataclass
|
||||
class RenderMaterial:
|
||||
name: str
|
||||
color: tuple[float, float, float] = (0.7, 0.7, 0.7)
|
||||
metallic: float = 0.0 # 0.0–1.0
|
||||
roughness: float = 0.5 # 0.0–1.0
|
||||
bsdf_type: str = "diffuse" # diffuse | roughconductor | roughdielectric | plastic
|
||||
|
||||
@dataclass
|
||||
class RenderCamera:
|
||||
origin: tuple[float, float, float] = (100, 100, 100)
|
||||
target: tuple[float, float, float] = (0, 0, 0)
|
||||
up: tuple[float, float, float] = (0, 0, 1)
|
||||
fov: float = 45.0
|
||||
|
||||
@dataclass
|
||||
class RenderSettings:
|
||||
width: int = 1920
|
||||
height: int = 1080
|
||||
spp: int = 256 # samples per pixel
|
||||
max_depth: int = 8 # path tracer bounces
|
||||
|
||||
class RenderBackend(ABC):
|
||||
"""Swap this to change the rendering engine."""
|
||||
@abstractmethod
|
||||
def render(self, obj_path: str, material: RenderMaterial,
|
||||
camera: RenderCamera, settings: RenderSettings) -> np.ndarray: ...
|
||||
@abstractmethod
|
||||
def render_preview(self, obj_path: str, material: RenderMaterial,
|
||||
camera: RenderCamera, settings: RenderSettings) -> np.ndarray: ...
|
||||
@abstractmethod
|
||||
def name(self) -> str: ...
|
||||
```
|
||||
|
||||
Switching backends later = write a new class implementing `RenderBackend`. One import change.
|
||||
|
||||
---
|
||||
|
||||
## Mitsuba 3 Backend
|
||||
|
||||
### Why Mitsuba
|
||||
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| `pip install mitsuba` | Single install, no system deps |
|
||||
| True path tracing | GI, caustics, spectral rendering |
|
||||
| PBR materials | `roughconductor`, `roughdielectric`, `diffuse`, `plastic` |
|
||||
| Python dict API | Build scenes programmatically, no XML |
|
||||
| CPU + GPU backends | `scalar_rgb` (CPU), `cuda_rgb` (NVIDIA) |
|
||||
| Output formats | PNG, EXR (HDR) with tonemapping |
|
||||
|
||||
### OCC → OBJ Conversion Path
|
||||
|
||||
```python
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.StlAPI import StlAPI_Writer
|
||||
from OCP.BRep import BRep_Builder
|
||||
import tempfile, os
|
||||
|
||||
def occ_shape_to_obj(shape, obj_path: str, linear_deflection: float = 0.1):
|
||||
"""Tessellate OCC shape and write as OBJ for Mitsuba."""
|
||||
tess = BRepMesh_IncrementalMesh(shape, linear_deflection, False, 0.5, True)
|
||||
tess.Perform()
|
||||
# Write STL (reliable), then convert to OBJ via trimesh or direct
|
||||
writer = StlAPI_Writer()
|
||||
writer.SetASCIIMode(False)
|
||||
stl_path = obj_path.replace(".obj", ".stl")
|
||||
writer.Write(shape, stl_path)
|
||||
# Mitsuba can read STL directly, or we convert to OBJ
|
||||
return stl_path
|
||||
```
|
||||
|
||||
### Mitsuba Scene Construction
|
||||
|
||||
```python
|
||||
import mitsuba as mi
|
||||
mi.set_variant("scalar_rgb")
|
||||
|
||||
def build_scene(mesh_path: str, material: RenderMaterial,
|
||||
camera: RenderCamera, settings: RenderSettings) -> mi.Scene:
|
||||
# Map our material to Mitsuba BSDF
|
||||
bsdf_map = {
|
||||
"diffuse": {"type": "diffuse", "reflectance": {"type": "rgb", "value": material.color}},
|
||||
"roughconductor": {
|
||||
"type": "roughconductor",
|
||||
"material": "copper", # or铝, 钢, etc.
|
||||
"alpha": material.roughness,
|
||||
},
|
||||
"roughdielectric": {
|
||||
"type": "roughdielectric",
|
||||
"int_ior": 1.5,
|
||||
"alpha": material.roughness,
|
||||
},
|
||||
"plastic": {
|
||||
"type": "plastic",
|
||||
"diffuse_reflectance": {"type": "rgb", "value": material.color},
|
||||
"int_ior": 1.5,
|
||||
},
|
||||
}
|
||||
|
||||
return mi.load_dict({
|
||||
"type": "scene",
|
||||
"integrator": {"type": "path", "max_depth": settings.max_depth},
|
||||
"sensor": {
|
||||
"type": "perspective",
|
||||
"fov": camera.fov,
|
||||
"to_world": mi.ScalarTransform4f.look_at(
|
||||
origin=camera.origin, target=camera.target, up=camera.up
|
||||
),
|
||||
"film": {"type": "hdrfilm", "width": settings.width, "height": settings.height},
|
||||
"sampler": {"type": "independent", "sample_count": settings.spp},
|
||||
},
|
||||
"emitter": {"type": "constant"},
|
||||
"shape": {
|
||||
"type": "stl", # or "obj"
|
||||
"filename": mesh_path,
|
||||
"bsdf": bsdf_map.get(material.bsdf_type, bsdf_map["diffuse"]),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `src/fluency/rendering/render_backend.py` | **NEW** | Abstract `RenderBackend`, `RenderMaterial`, `RenderCamera`, `RenderSettings` |
|
||||
| `src/fluency/rendering/mitsuba_backend.py` | **NEW** | `MitsubaBackend(RenderBackend)` implementation |
|
||||
| `src/fluency/rendering/occ_to_mesh.py` | **NEW** | OCC `TopoDS_Shape` → STL/OBJ tessellation |
|
||||
| `src/fluency/rendering/material_presets.py` | **NEW** | Preset library: Steel, Aluminum, Brass, Chrome, Plastic, Rubber, Wood |
|
||||
| `src/fluency/ui/render_window.py` | **NEW** | `RenderWindow(QMainWindow)` — image preview, material/quality controls, render/export |
|
||||
| `src/fluency/ui/main_window.py` | MODIFY | Add "Render" button → get selected shapes → open `RenderWindow` |
|
||||
|
||||
---
|
||||
|
||||
## UI: RenderWindow
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Render — [Part Name] [─][□][×] │
|
||||
├──────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ Rendered Image Preview │ │
|
||||
│ │ (QLabel with QPixmap) │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Material: [Steel ▾] │
|
||||
│ Quality: [256 SPP ▾] │
|
||||
│ Resolution: [1920×1080 ▾] │
|
||||
│ │
|
||||
│ [▶ Render] [⏹ Cancel] [💾 Export PNG] │
|
||||
│ │
|
||||
│ ████████████████░░░░░░ 65% (23s left) │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Preview**: progressive refinement (low SPP first, then ramp)
|
||||
- **Cancel**: kill Mitsuba render thread
|
||||
- **Export**: save to PNG/EXR
|
||||
|
||||
---
|
||||
|
||||
## Material Presets
|
||||
|
||||
| Preset | Color | Metallic | Roughness | BSDF |
|
||||
|--------|-------|----------|-----------|------|
|
||||
| Brushed Steel | (0.65, 0.67, 0.72) | 0.9 | 0.35 | roughconductor |
|
||||
| Polished Chrome | (0.8, 0.8, 0.8) | 1.0 | 0.05 | roughconductor |
|
||||
| Brushed Aluminum | (0.75, 0.75, 0.75) | 0.85 | 0.25 | roughconductor |
|
||||
| Copper | (0.95, 0.64, 0.54) | 0.95 | 0.15 | roughconductor |
|
||||
| Gold | (1.0, 0.76, 0.33) | 1.0 | 0.1 | roughconductor |
|
||||
| Blackened Steel | (0.15, 0.15, 0.17) | 0.8 | 0.4 | roughconductor |
|
||||
| Matte Plastic | (0.2, 0.5, 0.8) | 0.0 | 0.6 | plastic |
|
||||
| Glossy Plastic | (0.2, 0.5, 0.8) | 0.0 | 0.1 | plastic |
|
||||
| White Nylon | (0.85, 0.85, 0.83) | 0.0 | 0.45 | plastic |
|
||||
| Black ABS | (0.05, 0.05, 0.05) | 0.0 | 0.35 | plastic |
|
||||
| Red PA12 | (0.75, 0.08, 0.08) | 0.0 | 0.4 | plastic |
|
||||
| Rubber | (0.1, 0.1, 0.1) | 0.0 | 0.9 | diffuse |
|
||||
| Ceramic White | (0.92, 0.91, 0.88) | 0.0 | 0.15 | dielectric |
|
||||
| Glass | (0.95, 0.95, 0.95) | 0.0 | 0.0 | dielectric |
|
||||
| Wood | (0.6, 0.4, 0.2) | 0.0 | 0.7 | diffuse |
|
||||
|
||||
**Note:** Mitsuba pip installs don't include spectral metal data files (iron.spd, copper.spd, etc.), so metal presets use `material="none"` with `specular_reflectance` set to the metal color instead.
|
||||
|
||||
---
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| Mitsuba not installed | Graceful error: "pip install mitsuba" shown in UI |
|
||||
| Slow CPU rendering | Default to low SPP (64) for preview; offer GPU variant if CUDA available |
|
||||
| Large meshes slow to tessellate | Progress indicator; optional mesh decimation |
|
||||
| Mitsuba STL/OCC compatibility | Test tessellation quality; tune `linear_deflection` |
|
||||
|
||||
---
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- **Phase 1** (abstract backend + OCC→mesh + Mitsuba impl): ~4-6 hours
|
||||
- **Phase 2** (render window UI + material presets): ~3-4 hours
|
||||
- **Phase 3** (polish, export, swap test): ~2-3 hours
|
||||
- **Total**: ~9-13 hours
|
||||
Reference in New Issue
Block a user