#2818·drei

[v11] Four root-exported components are classified agnostic but are WebGL-only

Author: DennisSmolekCreated Sep 2, 2026Updated Sep 2, 2026
Labelsbugwebgpuv11

"Agnostic" is an assumption about a directory, not a fact about the code

143 components: 110 are classified agnostic, meaning they live in core/, external/ or experimental/ and are therefore assumed to work on both renderers. Nothing has ever checked that assumption, and it does not hold. Four of the 110 are WebGL-only, and all four ship from the root entry — the entry that claims to work everywhere.

This is the same failure shape as webgpu/BakeShadows (#2665), just on the other side of the split: no error, no warning, the component simply does nothing on WebGPU.

1. core/Helpers/PointMaterial — silently loses its entire effect

PointMaterial.tsx:12 patches PointsMaterial's fragment shader through onBeforeCompile:

typescript
;(this as THREE.Material).onBeforeCompile = (shader, renderer) => {
  const { isWebGL2 } = renderer.capabilities
  shader.fragmentShader = shader.fragmentShader.replace(... gl_FragColor ...)

NodeMaterial never calls onBeforeCompile. On WebGPU the callback does not run, so points render as hard squares instead of the antialiased circles that are the component's whole purpose. Nothing throws.

Two independent problems in there: raw GLSL, and renderer.capabilities, which the WebGPU Renderer does not have at all (verified against three/src/renderers/common/Renderer.js). The second would throw — except the callback never fires, so it cannot.

Needs a TSL implementation. human-only.

2. experimental/Effects/Outlines — and Outlines is broken on WebGPU both ways

experimental/Effects/Outlines/Outlines.tsx:58 writes gl_FragColor — a GLSL ShaderMaterial. That is what the root entry exports.

The /webgpu entry exports the TSL version instead, and #2813 shows that one throws on every construction.

So: root entry on WebGPU gets a shader that never compiles; /webgpu entry gets a component that throws. There is no working path to Outlines under WebGPU today.

3. external/Geometry/Splat — needs a decision, not a port

Raw GLSL ShaderMaterial plus gl: THREE.WebGLRenderer typed into its public surface (Splat.tsx:33,246). The upstream technique is WebGL-only. Plausibly wont-port with a written reason and a runtime error, but that is a call to make deliberately rather than by leaving it misclassified.

4. core/Loaders/Preload — already tracked as #2809

gl.compile() is a getter aliasing compileAsync on the WebGPU renderer, so the call returns a promise nobody awaits and preloading does not happen. Not detectable by the automated check below, because compile exists on both — it just means something different. Recorded via an override so it stops reading as agnostic.

Guard added

scripts/audit-components.js now derives agnosticButNot and --check fails on it: a component classified agnostic whose core/external/experimental source contains GLSL, onBeforeCompile, ShaderMaterial, or a renderer member three/webgpu lacks (capabilities, extensions, isWebGL2). GLSL detection was widened to catch a raw shader body in a template literal, which is how the experimental/Outlines case had stayed invisible.

The check catches 1–3 automatically. Case 4 is the limit of it: an API that exists on both renderers with different semantics cannot be found by grep, only by reading. There may be more of those, and the 110 agnostic components have not been audited for them one by one.