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