Files
fluencyCAD/docs/render_view_plan.md
T
bklronin 9f1387fe68 - added renderer
- Added undo
2026-07-12 22:21:43 +02:00

11 KiB
Raw Blame History

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

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.01.0
    roughness: float = 0.5       # 0.01.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

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

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