pcbjam/docs/features/fork-cleanup/10-3d-viewer.md
2026-06-15 11:32:12 +02:00

135 lines
9 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.

# 10 — Re-enable the 3D viewer
> **Verdict: port — and there's a cheap first step.** KiCad ships *two* 3D renderers: an
> OpenGL one (pure fixed-function GL 1.x — far from WebGL2) and a **pure-CPU raytracer that
> needs zero OpenGL**. The fork currently nulls the raytracer out. Turning 3D on via the
> raytracer (**Route C**) is low-risk and brings the render-to-PNG job and VRML export back
> nearly free; a WebGL2 port of the interactive renderer (**Route B**) is the follow-up.
## Why it's off (timeline, with primary sources)
| Date | Commit (kicad) | What |
|---|---|---|
| 2025-12-21 | `46bb16e86b` | "Enable 3D viewer build for WASM with OpenGL stubs" — compiled *only* so headers resolve. Message: "OpenGL rendering won't work at runtime … avoids complex stubbing." |
| 2026-01-03 | `a61d2ea406` | OpenGL GAL on `-sLEGACY_GL_EMULATION`; added the +190-line `kiglew.h` `__EMSCRIPTEN__` branch (display lists + lighting as **no-ops**). 3D "built" but couldn't draw. |
| 2026-01-10 | `d8a9ac4304` | "Disable 3D viewer functionality for WASM builds" — same day `WEBGL_GAL` landed and the build swapped emulation → `-sFULL_ES3`. Message: 3D "require[s] native OpenGL rendering (not WebGL)". Built with `KICAD_BUILD_3D_VIEWER_WASM=OFF`. |
| 2026-03-20 | `4ddb9b47f5` | "Migrate to pure WebGL 2.0 without FULL_ES3 emulation" — 2D GAL goes pure WebGL2; the only GL link flag today is `-sMAX_WEBGL_VERSION=2`. |
Recorded rationale: `features/gl-article/README.md:274-276,421-422` — the 3D viewer is "a
separate legacy-GL renderer, not GAL" needing "GLU + fixed-function", declared out of scope
when the strategy became "progressively remove the Emscripten crutch." So it was a deliberate
scoping decision when the emulation layer it depended on was removed — not a crash.
## What the OpenGL renderer actually uses
`3d-viewer/3d_rendering/opengl/` = 6 files, **4,555 lines**. It's pure GL 1.x fixed-function:
- **Immediate mode** — 153 tokens (`glBegin`/`glEnd` ×28, `glVertex*`/`glNormal3*`/`glTexCoord2*`).
- **Display lists** — 36 refs in 2 files, and they're the **primary board-geometry path**:
`layer_triangles.cpp:582-714` bakes every layer's triangles into `glGenLists`/`glNewList`
wrapping a `glDrawArrays`; `render_3d_opengl.cpp:1303-1315` for the grid.
- **Fixed-function matrix/lighting** — ~117 tokens (`glMaterialfv`, `glLightfv`,
`glMatrixMode`/`glPush/PopMatrix`, `GL_LIGHTING`, …).
- **Client-state arrays** — 49 tokens. **GLU** — 36 (`gluNewQuadric`/`gluCylinder`/`gluSphere`
for vias/pads).
- **Modern** — zero shaders, zero VAOs; only `3d_model.cpp` uses VBOs (GL 1.5 style).
Structurally friendlier than the raw counts suggest: geometry already lives in CPU triangle
containers, display lists are thin `glDrawArrays` wrappers, `3d_model.cpp` is already VBO,
camera/transforms are already glm.
## Route A — LEGACY_GL_EMULATION for the 3D viewer: REJECTED
- The flag is **module-global at link**: `GLEmulation.init()` replaces the module-wide JS
bindings (`glDrawArrays`, `glEnable`, `glBindBuffer`, `glGetString`, …) for *all* contexts,
including the flagship 2D `WEBGL_GAL`'s. Concretely it corrupts the GAL's ES3 shaders —
emscripten prepends `#extension GL_OES_standard_derivatives` to any fragment shader using
`dFdx` (`kicad_frag.glsl:138` does), and prepending anything before `#version 300 es` is a
guaranteed ES3 compile error. Separate canvases/contexts do **not** isolate this (hooks are
per-module).
- Even ignoring that: emscripten never implemented display lists (issue #688 — the main board
path draws nothing), GLU quadrics are absent, lighting emulation has `throw 'TODO'` holes.
You'd rewrite the display-list and quadric paths anyway (the hard half of Route B) while
regressing 2D and re-adding ~200 KB of the fragility the project already escaped. **Reject.**
## Route C — CPU raytracer + blit (do this first, low risk)
`RENDER_3D_RAYTRACE_RAM` (`render_3d_raytrace_ram.cpp`, 159 lines) is **100% GL-free**: it
renders into a plain `uint8_t` RGBA buffer (`initPbo()` is just `new uint8_t[w*h*4]`) and
exposes `GetBuffer()`. It's **progressive by design** — each `Redraw()` traces blocks up to a
400/750 ms budget then yields, so a serial browser loop still animates. Both existing
consumers already convert buffer → `wxImage` with no GL (`eda_3d_viewer_frame.cpp:846-866`
screenshot; `pcbnew_jobs_handler.cpp:817-885` render job).
Recipe:
1. `KICAD_BUILD_3D_VIEWER_WASM=ON`, and **genex-out the GL renderer TUs**
(`render_3d_opengl.cpp`, `layer_triangles.cpp`, …) the same way `render_3d_raytrace_gl.cpp`
is already excluded (`3d-viewer/CMakeLists.txt:49`) — this sidesteps the legacy-GL link
errors entirely. The raytracing subtree (12,232 lines) has no GL.
2. Have `EDA_3D_CANVAS` instantiate `RENDER_3D_RAYTRACE_RAM` under `__EMSCRIPTEN__` (it's
renderer-agnostic via `RENDER_3D_BASE`), and **blit** the buffer — either a plain wxWindow
painting the `wxImage` through the wx-wasm 2D canvas DC (zero GL), or a ~100-line WebGL2
textured quad (infra exists in `common/gal/webgl/fullscreen_quad.cpp`).
3. **Delete the fork gates** — they're keyed on `__EMSCRIPTEN__`, *not* the CMake option, so
flipping the flag alone isn't enough: `eda_3d_canvas.cpp` (raytracer nulled, raytracing
request early-returned), `pcbnew/pcbnew.cpp` (3D settings panels — see the
`PANEL_TOOLBAR_CUSTOMIZATION` bug in [07](07-native-build-bugs-and-tooling.md)),
`pcbnew_jobs_handler.cpp`/`.h` (render job), and `exporter_vrml.cpp` (in-file stub). Delete
the three big stub TUs too (`wasm/stubs/3d_viewer_stub.cpp`, `3d_canvas_stub.cpp`,
`3d_scenegraph_stub.cpp`, ~1,200 lines).
Caveats: main tracing runs **serial** under the `bs_thread_pool` inline patch (still usable,
because progressive); the preview / SSAO / DLAA passes use raw `std::thread` + a `sleep_for`
spin-wait — pthreads *are* enabled in the build (`-pthread -sUSE_PTHREADS=1
-sPTHREAD_POOL_SIZE=…`), so they run, but the spin-wait blocks the browser main thread per
pass and is worth reviewing for jank.
## Route B — WebGL2 port of the interactive renderer (follow-up)
Scope is ~4,555 lines / 6 files — roughly **1/5 of the 2D GAL port** and far less novel (no
compositor, no multi-FBO juggling). The work: display-list → VBO (mechanical, the lists wrap
`glDrawArrays` of CPU triangle containers), client-state arrays → generic vertex attribs,
fixed-function transforms → a small matrix-stack helper (already glm), ~2 shader pairs
(Phong-lit per-material + flat), GLU quadrics → a triangle helper (small; vias/pads/gizmo).
Reuse `common/gal/webgl/`'s shader infra + `convert_glsl_es3.py`.
Divergence: mirror the 2D precedent — new files (`3d_rendering/webgl/`), swapped via a CMake
genex like the existing `render_3d_raytrace_gl.cpp` exclusion. Near-zero upstream-file diff
beyond the swap + un-gating. Gives the **interactive** GPU-speed orbit/pan view. Risk: medium
(GPU correctness; the e2e screenshot harness pattern exists). Prereq: model loading (below).
## Model loading (needed for both B and C to show components)
Upstream loads `plugins/3d/{vrml,oce,idf}` as **runtime DSOs** —
`S3D_PLUGIN_MANAGER::loadPlugins()` scans a directory and `wxDynamicLibrary`-loads each. There
is **no static-registration path upstream**, and `wasm/stubs/3d_scenegraph_stub.cpp` (440
lines) currently stubs the whole `S3D_CACHE` + scenegraph + `S3D::WriteVRML`.
Static linking entails: compiling the plugin sources into the binary with **per-plugin symbol
prefixes** (they export identical C ABI names — `Load`, `GetFileFilter`, … — so they collide;
wrap each TU with `-Dname=vrml_name` or a wrapper), plus a small `__EMSCRIPTEN__` path (or a
subclassed `KICAD_PLUGIN_LDR_3D`) with a 3-entry static registry — cleanest as one new
wasm-layer file. Sizes: **vrml** (VRML1/2 + X3D, fully in-tree, **no external deps**) = 17,835
lines; **oce** (STEP/IGES — needs OCC, which **is already built for wasm**: OCC 7.8 via
`--with-occ`, verified `libTK*.a` in the sysroot) = 1,407; **idf** = 931. `kicad_3dsg` must
become STATIC for wasm (it's `add_library(SHARED)` today). Plus: ship 3D model assets to the
FS (models are *referenced*, not embedded — see
[`../libraries/0001-library-management.md`](../libraries/0001-library-management.md)).
Start with the VRML plugin (no deps) for `.wrl` models; add OCE for STEP once VRML works.
## Render job & VRML export — free with either route
Both only need the 3D model cache / scenegraph / raytracer, **not** the GL renderer:
- `JobExportRender` (`pcbnew_jobs_handler.cpp:641-898`) is `RENDER_3D_RAYTRACE_RAM` +
`BOARD_ADAPTER` + 3d_cache → board-render-to-PNG/JPEG works as soon as 3d-viewer links.
- `exporter_vrml.cpp`'s real implementation needs `S3D_CACHE` + the SGNODE/IFSG API — un-stub
by **deleting** the fork's `#else` block (a divergence *reduction*). Board-only export works
immediately; embedding component models additionally needs the static VRML plugin above.
## Sequencing
**C first** (working 3D view + render job + VRML export, low risk, mostly deletes gates) →
**B second** (interactive upgrade) → **A never**.