Skip to content

Renderer feature matrix — what works where, and how we fall back

Framework. Game-agnostic. Consumer-facing. The exhaustive answer to “does feature X work on both backends?” and, if not, “what does the framework do about it?”. Lives next to renderer-configuration.md (the how-to-configure surface) and below webgl-constraints.md (the hard platform limits both backends sit inside).

Living document — gaps and edge cases land here as they’re discovered. Per adr/0005-dual-renderer-backend.md.

For every feature listed below the framework guarantees one of three outcomes when running on either backend:

  • Single implementation. The feature works identically on both backends. The consumer writes one component / one material / one shader; Three.js + TSL handle the rest. No special handling.
  • Graceful degrade (Pattern A). The feature exists on both backends with different internal routes. WebGPU path uses compute / indirect-draw / storage textures; WebGL 2 path uses CPU / transform feedback / reduced fidelity. The component API is identical; framework picks the route.
  • Feature gate (Pattern B). The feature exists only on WebGPU. On WebGL 2 it’s silently absent — the game still works without it. Consumers branch on caps.<feature> to opt in.

A fourth outcome — backend requirement (Pattern C) — is reserved for the rare project that pins renderer.prefer = "webgpu-required" and refuses to boot on WebGL 2. Not a default; not how most features ship.

See adr/0005-dual-renderer-backend.md for the decision context.

The framework exposes a capability surface via @vibesmith/renderer:

import { useRendererCapabilities } from '@vibesmith/renderer';
function FancyEffect() {
const caps = useRendererCapabilities();
if (!caps.compute) return null; // Pattern B feature gate
return <VolumetricFog density={0.5} />;
}

Rule of thumb: branch on caps.<capability>, never on handle.backend. The capability surface is the stable contract; the backend identity is a transient implementation detail and shouldn’t leak into consumer code.

The capabilities currently surfaced (more added as features land):

CapabilityTrue whenUsed by
caps.computeWebGPU adapter activePattern A / B features that need compute shaders
caps.indirectDrawWebGPU adapter activeGPU-driven culling, indirect crowd rendering
caps.storageTexturesWebGPU adapter activeFluid / SDF / custom compute-output effects
caps.timestampQueryWebGPU + timestamp-query featureGPU-side profiling
caps.parallelShaderCompileWebGL 2 + KHR_parallel_shader_compile, or always on WebGPUAsync material precompile
caps.maxTextureSizeplatform-reportedTexture sizing decisions
caps.maxComputeWorkgroupSizeWebGPU adapter activeCompute kernel dispatch

FeatureOutcomeNotes
Indexed mesh drawSingle implUniversal
InstancedMeshSingle implSame API both backends
Skinned meshSingle implWebGPU uses storage buffer; WebGL 2 uses bone texture. Both via Three.js; transparent
Morph targetsSingle impl
OrthographicCamera (2D-style)Single impl
PerspectiveCameraSingle impl
Render-to-textureSingle implAPI differs internally; Three.js abstracts
Depth textureSingle impl
Stencil bufferSingle impl
MSAASingle impl
MaterialOutcomeNotes
MeshBasicMaterialSingle impl
MeshLambertMaterialSingle impl
MeshStandardMaterial (PBR)Single implIdentical output
MeshPhysicalMaterialSingle implClearcoat, sheen, iridescence
MeshToonMaterialSingle impl
NodeMaterial (TSL)Single implThe canonical custom-material path
ShaderMaterial (raw GLSL)Not supported on WebGPUUse NodeMaterial + TSL. Raw GLSL is gated behind renderer-config.json’s allowCustomShaders: "raw-glsl-and-wgsl" escape hatch

Framework policy: consumers write custom materials in TSL via NodeMaterial. Raw ShaderMaterial works only on WebGL 2 and requires opt-in. See material-system.md and renderer-configuration.md § Custom shader policy.

FeatureOutcomeNotes
Ambient / hemisphere lightSingle impl
Directional + cascaded shadowsSingle impl
Point lights + cubemap shadowsSingle impl
Spot lights + perspective shadowsSingle impl
PCFSoft shadow filteringSingle impl
VSM shadow filteringSingle impl
Light probes / image-based lightingSingle impl
Contact shadows / SSAOSingle implVia postprocessing v3+
Real-time global illuminationPattern BCompute-driven; WebGPU only

Via @react-three/postprocessing v3+. Effects below mount inside <EffectComposer>:

EffectOutcomeNotes
Tone mappingSingle implACES / Cineon / Neutral
BloomSingle impl
SSAOSingle impl
Colour grading (LUT)Single impl
FXAA / SMAASingle impl
Depth of fieldSingle impl
Motion blurPattern AWebGL 2 approximation; WebGPU does it properly
Volumetric / atmospheric fogPattern BCompute-driven
Custom compute-based post-FXPattern B
FeatureOutcomeNotes
Sprite particles, small count (≤ ~5k)Single implInstancedMesh; both backends
Compute particles, large count (10k+)Pattern A<ComputeParticles>; WebGPU uses compute shader, WebGL 2 caps fidelity to ~5k CPU particles
Crowd rendering, small (≤ ~200 chars)Single implSkinned-instanced
Crowd rendering, large (≥ 1000 chars)Pattern AWebGL 2 uses billboard impostors; WebGPU uses indirect-draw
Trail / ribbon effectsSingle impl
FeatureNotes
GPU frustum cullingWebGL 2 uses CPU culling; this is the WebGPU upside
GPU occlusion cullingWebGPU only
Fluid simulationStable on WebGPU; no realistic WebGL 2 path
Cloth / soft-body simulationSame
Procedural mesh generation in computeWebGL 2 alternative: workers + JS
ML inference (gesture recognition, pose estimation, etc.)WebGPU compute; not in scope for the render path, but uses the same adapter
FormatOutcomeNotes
glTF (.glb / .gltf)Single implIdentical loader
KTX2 (UASTC / ETC1S)Single implTranscode targets differ slightly per backend; loader picks
Draco-compressed meshesSingle impl
Audio (PCM / OGG / WAV / etc.)Single implBrowser-handled; renderer-irrelevant
HDR environment mapsSingle impl
Compressed textures (BC / ETC / ASTC)Single implWebGPU coverage broader; framework targets the intersection
Probe fieldWebGL 2 sourceWebGPU sourceNormalised at
Draw call countgl.info.render.callsrenderer.info.render.drawCallshandle.info()
Triangle countgl.info.render.trianglesrenderer-sidehandle.info()
Geometries countgl.info.memory.geometriesrenderer-sidehandle.info()
Textures countgl.info.memory.texturesrenderer-sidehandle.info()
Shader programs countgl.info.programs.lengthrenderer-sidehandle.info()
Frame time (wall clock)consumer-suppliedconsumer-suppliedunchanged
GPU timestamp queriesNot availablecaps.timestampQuery if feature requestedoptional

packages/r3f-probes/src/renderer-probe.ts reads handle.info() in the normalised form rather than raw gl.info. Telemetry captures carry a meta.renderingKind of webgl or webgpu (per the RenderingKind enum in @vibesmith/runtime-introspection) so downstream tooling knows which backend produced the data.

BackendMechanismBehaviour
WebGL 2KHR_parallel_shader_compile (extension)Async when extension present; serial otherwise
WebGPUBuilt into pipeline creationAlways async

The framework’s material-precompile pass per material-system.md uses async compilation where available. No consumer-facing difference.


Cookbook: choosing between Patterns A / B / C

Section titled “Cookbook: choosing between Patterns A / B / C”

When a feature lands that doesn’t have a WebGL 2 equivalent, the framework author picks one of the three patterns. This section is the heuristic.

If the feature’s visual identity could exist on WebGL 2 at reduced fidelity, write both implementations and pick the route inside the framework component. Consumer writes one component.

Use Pattern A when:

  • The visual is recognisable on both backends (a particle is still a particle at 5k instead of 100k).
  • The WebGL 2 implementation is finite — small enough to maintain.
  • The framework intends to ship the feature in the default tier table for LOW/MEDIUM.
  • Building a WebGL 2 equivalent would be substantially more code than the feature itself (e.g., 64-slice fragment-shader fog vs a 200-line compute shader).
  • The visual identity requires compute (a fluid sim that becomes “blue rectangles” on WebGL 2 isn’t gracefully degraded — it’s broken).
  • The feature only ships in the ULTRA tier; LOW/MEDIUM never see it and the game doesn’t depend on it.

Choose Pattern C (backend requirement) only when

Section titled “Choose Pattern C (backend requirement) only when”
  • The game’s identity is a compute-driven simulation; without WebGPU there is no game.
  • The project is kiosk / demo / internal-tooling and the hardware is fixed.

Pattern C is the rarest choice. Frameworks ship Pattern C features only on explicit consumer opt-in via renderer.prefer = "webgpu-required".

  • Don’t expose handle.backend to consumer code. It’s a leak of internal state. Use caps.<capability> instead.
  • Don’t write per-backend forks in the consumer’s game code. If a feature needs different implementations per backend, that belongs inside a framework component, not in the consumer’s source tree.
  • Don’t add features to the default tier table that only work on WebGPU. That breaks the WebGL 2 path silently. Use Pattern B + opt-in instead.

Refresh this matrix when:

  1. Three.js ships a feature with backend-specific behaviour. Each new feature gets a row in the appropriate table.
  2. TSL closes a gap. Idioms previously requiring raw GLSL/WGSL move to single-implementation.
  3. A consumer ships a Pattern A or B feature. Reference implementations move into the framework’s repository and get linked from the relevant row.
  4. A WebGPU-mobile reliability issue is confirmed. Add the pattern + workaround to the row.