Blog ·

Photoreal PCB rendering in the browser: metal, solder, glass mask

Sergey Alekseev

PCBKit draws every picture of a board with its own renderer: the 2D canvas, the 3D view, the PNGs the CLI writes and the frames of our films. It is a Rust crate on wgpu, so the same passes run natively on Metal or Vulkan and in the browser on WebGPU or WebGL2. This week we worked on making the 3D PCB view read like a photograph of a board: metal that looks plated, solder that wets its pads and every frame at full resolution while the camera moves. Below is what changed, what it costs in frame time and what we tried and took out again on the same day.

How the 3D PCB view is drawn

The board itself is baked. For each side the renderer draws colour, a material map (clear coat, roughness, metalness, occlusion), normals and annotations with its own 2D pipelines, combined per fabrication option, so a change of mask colour or finish re-bakes the board instead of rebuilding geometry. Holes are distance ramps in the colour’s alpha and stay round at any zoom. Close to the board, a detail bake at screen resolution covers what the view shows.

Parts are meshes: our own solids for packages we know, STEP models converted from the person’s KiCad library or our hosted library otherwise. The shading is physically based: Burley diffuse, GGX with height-correlated Smith visibility, multiple-scattering compensation, a clear-coat layer over the base, light from a studio of prefiltered cube maps, a shadow-casting key light and AgX tone mapping. The scene is drawn into a 4× multisampled target and composited with a fine film grain. Nothing in it needs more than WebGL2 has: no compute, no storage buffers. The board’s shader binds 11 of WebGL2’s 16 textures.

Full resolution while the camera moves

Until this week the live view drew differently while the camera moved. Since ADR 0076 (2026-10-29) a moving frame drew at one device pixel per CSS pixel, half the resolution on a 2× screen, and stretched the picture. At rest the 3D view averaged 16 jittered frames, the close-up bake waited for the camera to rest 150 ms, the demo tour lowered its resolution to as little as 0.75 when frames ran long and the film grain changed every moving frame. It saved GPU time. It also made everything soften on every drag and sharpen a quarter of a second after it.

ADR 0120 removed all five paths, with no switch kept. A frame of the live view is the same picture pcbkit render draws of the same view. A test (moving_frames_are_pictures) checks it on frames of a pan, a zoom and an orbit. Without temporal averaging, polished metal would sparkle: a highlight narrower than a pixel lands on one pixel, then the next. So the shader widens roughness by how much the normal turns across the pixel (Tokuyoshi and Kaplanyan’s specular anti-aliasing). The grain is fixed on the screen.

The cost is real. Measured on an Apple M2 (10 GPU cores) in Chrome with WebGPU, the doom-handheld demo at 2× (fps, GPU mean ms):

3D view Moving frames at half resolution Every pixel
orbit, whole board 59–60 · 1.6–3.3 60 · 2.5
wheel zoom in to 45 mm 53–57 · 2.4–2.8 44 · 3.0–3.3
wheel zoom out 54 · 2.3–2.7 42 · 2.7

Orbiting and panning held 60 fps at full resolution. Zooms lost frames, because the close-up bake now runs during the zoom and one bake cost 30 to 60 ms of GPU time. So the bake is now made in steps, one per frame, in bands of rows. Its maps got three mip levels, which took a settled close-up’s scene pass from 13.2 to 6.2 ms. Then the bands were sized by how much room the last frames left (up to 0.15 Mpx, less as frames near 11 ms of GPU time) instead of a fixed 0.15 Mpx. Natively on the same M2 (Metal, 1470×733 at 2×, doom rp2350-handheld, 180 frames each, GPU mean ms):

Native, Metal Fixed bands Bands sized by the frame
orbit 1.64 1.33
zoom in and out 2.93 2.11
orbit at 1/8 the distance 6.10 2.54
frames over 16.7 ms 0 of 180 0 of 180

In Chrome the same frames had measured 3 to 4 times that GPU time. 3D zooms there were at 42 to 54 fps before the last change. We have not yet measured the browser after it, so 58 fps or better on zooms in Chrome is still a target.

Metals that read as metal

The materials were already metallic in the shading model. They looked like satin grey because they mirrored an evenly lit grey ceiling with wear on top. Two things changed. Roughness went down: tin and solder 0.2 to 0.13, gold 0.12 to 0.08, cans 0.2 to 0.12, the ENIG finish 0.18 to 0.1, HASL 0.16 to 0.11. And the studio got contrast: a darker ceiling, brighter soft boxes, a crisp strip overhead that a flat can or pad mirrors as a bright band sliding across it, tall horizon panels with dark gaps that standing pins mirror as streaks.

Gold pins still looked ochre and the plated board edge brown. A vertical metal face turned to the camera mirrors what is below and behind it, which was a floor at 0.09. A photographer lays a white card on the table for that, so the studio has one now, low in front and below the horizon where the board’s own surface never sees it. The floor went to 0.17. The shields on modules and the USB-C shell looked blotchy and galvanized. We found the cause by elimination: the grain of the wear texture in tin’s normal and roughness, seen in the mirrored soft boxes. Plated metal is now an even satin with streaks only close up.

Solder that wets the pads

A chip capacitor stood on a flat slab with no fillet. The joint’s height came from the model’s vertices within 0.35 mm of the board, which on a block termination are only its rounded bottom edge. The meniscus came out at 0.045 mm, hidden under a 0.05 mm slab. Now a block termination’s meniscus climbs two fifths of its end face (0.05 to 0.3 mm) and the slab is a low pillow wetted to the pad’s edges. Chip ends get concave fillets and gull-wing feet stand on smooth wetted lands.

Reflections of the parts: added and removed the same day

A glossy mask mirrors the parts standing on it. Ours mirrored only the studio. The board is flat, so its reflections are those of a plane mirror, which a rasterizer can draw exactly. ADR 0124 drew the parts a second time each frame, mirrored in the board’s plane at half the picture’s size. On the ESP32-C6 dev board fixture (M2, 1920×1080 at 2×) it cost about 5 % of the GPU: 2.62 to 2.77 ms orbiting, 4.29 to 4.49 ms close up, with no frame over 16.7 ms.

It came out the same day (ADR 0125). Every chip, can and lead doubled in black gloss made the boards look wet and busy. The wetted solder, the metals and the studio from that work stayed. The board’s coat and pads reflect the studio again and the frame has one pass less.

A glass mask

The newest finish is mask = .glass: the board as if on clear glass. Nothing is baked for the mask. The copper of both sides shows bare and lacquered. The glass between is black (clear glass scatters no light) except for its flaws: seeds, a bubble’s rim catching the light, a dark inclusion, a handling scratch, a slight waviness. It is 14 % opaque plus what the flaws stop. Its faces are blended premultiplied after everything else so what the glass reflects is not dimmed by what it lets through. The far face shows only its copper’s matte back. An earlier version lit that face from the studio floor and laid a white veil over the board. In the app it is “Glass (clear board)” in the Fabrication menu.

Display pictures on their glass

A running device’s screen is drawn on the display module in 3D. On every socket-mounted module of the registry the picture floated 8.5 to 11 mm above the glass, because the screen’s height came from a number typed into the part sheet while the body was built from the module’s own stack. Now the pixels are laid on the highest upward surface under the screen’s middle, found by casting a ray against the component’s own bodies. We drew all 28 registry boards with a screen to check it. The fix then fought the glass for depth from far away and drew stripes, so the screen’s pipeline now draws with a depth bias towards the eye instead.

Heavy boards: geometry, not pixels

On delta-s02, a home computer of plain logic chips with thousands of DIP pins, the problem was triangles: 2,505,114 of them, mostly through-hole joints and leads merged per material. The scene pass hardly got faster at a quarter of the pixels. Joints and leads are now instanced copies of shared meshes with three levels of detail chosen per copy by its size on screen. Copies outside the view are left out. In the tour on an Apple M-series GPU that brought it to 591,262 triangles and the frame time p99 from 33.8 to 18.8 ms.

What is still behind

A board’s first frame on a phone is about 1 s in production, down from 11.5 s. The same renderer draws the running devices of the lab. For running firmware on these boards, see the post about firmware at its corners.