GitHub - ektogamat/webgpu-mesh-transmission-material: A high-end refractive glass material that brings the spirit of Drei’s Mesh Transmission Material into the WebGPU era. Built with Three.js node materials (TSL), a custom volume-refraction lighting model, and a multi-pass backdrop pipeline that samples the real scene behind the glass — including an optional backside pass for true thickness.

9 min read Original article ↗

Mesh Transmission Material for React Three Fiber — powered by WebGPU & Three.js TSL

WebGPU Glass Material preview

A high-end refractive glass material that brings the spirit of Drei’s Mesh Transmission Material into the WebGPU era. Built with Three.js node materials (TSL), a custom volume-refraction lighting model, and a multi-pass backdrop pipeline that samples the real scene behind the glass — including an optional backside pass for true thickness.

Live demo: webgpu-glass-material.vercel.app


Why this glass looks incredible

Most “glass” in the browser is either a cheap opacity trick or a screen-space refraction that ignores thickness, attenuation, and chromatic spread. This material treats transmission as a physical volume:

What you see What’s happening under the hood
Distorted checkerboard through the mesh Ray-exit refraction projected into screen UVs, then sampled from a dedicated backdrop buffer
Rainbow edges / prism split Per-channel IOR spread (R / G / B) driven by dispersion, with an optional spectral sampling mode
Soft frosted look Multi-sample thickness smear + roughness-aware LOD on the backdrop mips
Colored depth / tint in thick regions Beer–Lambert style volume attenuation (attenuationColor × attenuationDistance)
Reflections that sit on top of the refraction Full MeshPhysicalNodeMaterial stack (Fresnel via DFG LUT, clearcoat, iridescence, sheen, anisotropy)
Internal “second surface” Optional backside pass: back faces refract the clean scene into a second target that the front faces then sample

All of that runs on WebGPU via THREE.WebGPURenderer and TSL nodes — not a hand-rolled GLSL string glued onto WebGL.


Stack

  • React 19 + React Three Fiber 9
  • Three.js (three/webgpu + three/tsl)
  • @react-three/drei for helpers / environment
  • Vite for the demo app
  • Custom UI panel for live tuning (GlassPanel)

Requires a browser with WebGPU support (Chrome / Edge / Safari recent builds, etc.).


Advanced techniques

1. Custom transmission lighting model

GlassPhysicalNodeMaterial extends MeshPhysicalNodeMaterial and swaps in TransmissionPhysicalLightingModel. Instead of Three’s default IBL volume refraction, the lighting model injects a custom backdrop node into the builder context and blends it with the physical transmission term. Specular / Fresnel still come from the physical stack; only the transmitted light is replaced by the screen-space volume path.

2. Multi-pass transmission backdrop

Every frame (priority -1, before the main draw), TransmissionBackdropManager captures what sits behind the glass:

  1. Clean pass — hide all glass meshes, render the scene into cleanTarget (HalfFloat + mipmaps). Geometry on TRANSMISSION_BACKDROP_LAYER can appear only in this buffer (e.g. bubbles inside the gelatinous cube).
  2. Backside pass (optional) — flip glass materials to BackSide, temporarily use backsideThickness, refract the clean buffer into backsideTarget.
  3. Main pass — front faces sample the final backdrop texture through the volume refraction node.

Tone mapping is disabled during backdrop capture so the buffer stays linear and composable with the final ACES-tonemapped frame.

3. Volume refraction in TSL

transmissionNodes.js builds the refraction path entirely with TSL (Fn, Loop, refract, screen projection, etc.):

  • Volume transmission ray — refract view direction by IOR, scale by thickness × model scale, exit the volume in world space, project to NDC → backdrop UVs.
  • Chromatic dispersion — spread IOR across R/G/B (classic three-tap) or walk a stratified spectrum with lobe weights (spectralDispersion).
  • Multi-sample blur — Vogel disk jitter in a tangent frame + thickness smear for frosted / anisotropic blur. Sample count is baked into the loop bounds (recompiled when samples changes) so unused iterations never run.
  • Explicit backdrop LOD — screen-space footprint of the refracted UV is measured once in uniform control flow (dFdx / dFdy) before the sample loop. Implicit derivatives inside dynamic loops produced speckled mip noise; this avoids that.
  • Hardware trilinear taps — roughness maps into mip LOD via applyIorToRoughness, preferring a single .level() read over expensive bicubic reconstruction.
  • Ordered dither — interleaved gradient noise phases the thickness smear so high-contrast backdrops don’t sparkle.
  • Temporal / spatial distortion — optional triNoise3D offset on the normal for gel / liquid motion.

4. Physical surface stack

On top of transmission, the material exposes the full physical surface toolkit:

  • Roughness / metalness / specular
  • Clearcoat + clearcoat roughness
  • Iridescence (IOR + thickness range)
  • Sheen
  • Anisotropy
  • Environment map intensity
  • Base color tinting the transmitted light

5. Performance knobs that actually matter

  • backdropResolutionScale / backsideResolutionScale — render the expensive offscreen passes at a fraction of the canvas size.
  • samples (1–16) — quality vs. cost; loop is recompiled to the exact count.
  • forceSinglePass — skip double-sided material complications when you don’t need them.
  • Half-float render targets with generated mipmaps for cheap roughness blur.

6. Demo extras

  • Three scene presets: Gelatinous Cube, Aquarium, Shader Ball — each with its own glass defaults.
  • Instanced transmission bubbles that live on the backdrop-only layer so they refract inside the volume.
  • PCSS contact shadows (TSL) for soft grounding under the glass.
  • Live WebGPU Glass Config panel with a refraction / critical-angle chart.

Quick start

Open the local Vite URL in a WebGPU-capable browser.

npm run build    # production build
npm run preview  # preview the build

Using the glass material

Attach GlassMaterial as a child of any mesh (R3F attach="material" pattern):

import GlassMaterial from "./glass/GlassMaterial";

function MyGlassMesh() {
  return (
    <mesh>
      <sphereGeometry args={[1, 64, 64]} />
      <GlassMaterial materialName="MyGlass" />
    </mesh>
  );
}

The component:

  1. Creates a GlassPhysicalNodeMaterial with transmission uniforms.
  2. Registers itself with the backdrop manager for the current WebGPU renderer.
  3. Captures the backdrop each frame.
  4. Syncs live values from glassSettings (and the config panel / __glassSettings in dev).

Global defaults live in src/glass/glassSettings.js. Mutate via setGlassSetting(key, value) or the panel; subscribers (including the material) update automatically.

import { setGlassSetting, glassSettings } from "./glass/glassSettings";

setGlassSetting("ior", 1.45);
setGlassSetting("dispersion", 8);
console.log(glassSettings.thickness);

In development, globalThis.__glassSettings mirrors the same object for console tweaking.


Material parameters

Defaults below match glassSettings / glassDefaults. Ranges follow the live config panel schema.

Optics / transmission

Parameter Type Default Range Description
color string (hex) '#c9ffa1' Base albedo; tints transmitted light
ior number 1.25 12.5 Index of refraction (air ≈ 1, water ≈ 1.33, glass ≈ 1.5)
thickness number 3.5 typically -22 in UI (scene-dependent) Optical path length through the volume; drives refraction offset and attenuation distance
dispersion number 6.5 020 Chromatic aberration strength (per-channel IOR spread)
transmission number 1 01 How much light is transmitted vs. opaque
anisotropicBlur number 0.47 01 Extra thickness smear / frost independent of roughness (internalBlur alias)
attenuationDistance number 0.5 010 Distance over which attenuation color fully absorbs
attenuationColor string (hex) '#ffffff' Beer–Lambert absorption tint
envMapIntensity number 0.85 05 Strength of environment reflections / IBL
specularIntensity number 1 01 Specular reflectance strength (panel label: Reflectivity)
specularColor string (hex) '#ffffff' Specular tint
spectralDispersion boolean false If true, stratified spectral lobes instead of fixed R/G/B taps (recompiles the node graph)

Surface

Parameter Type Default Range Description
roughness number 0 01 Microfacet roughness; also raises backdrop mip LOD
metalness number 0 01 Metallic blend
clearcoat number 0.2 01 Clearcoat layer weight
clearcoatRoughness number 0 01 Clearcoat roughness
iridescence number 0.45 01 Thin-film iridescence weight
iridescenceIOR number 1.14 Thin-film IOR
iridescenceThicknessMin number 0 Thin-film thickness range (nm-style units used by Three)
iridescenceThicknessMax number 1000 Thin-film thickness range max
sheen number 0 Sheen weight
sheenRoughness number 0.29 Sheen roughness
sheenColor string (hex) '#ffffff' Sheen tint
anisotropy number 0.1 Specular anisotropy
anisotropyRotation number 0 Anisotropy angle

Distortion

Parameter Type Default Range Description
distortion number 0 01 Strength of 3D noise bent into the refraction normal
distortionScale number 0.3 02 Spatial frequency of the distortion noise
temporalDistortion number 0.2 01 How fast the noise scrolls over time (gel / liquid feel)

Performance / backdrop

Parameter Type Default Range Description
samples number (int) 8 116 Transmission sample count; baked into the TSL loop (recompile on change)
backside boolean true Enable second pass that renders back faces into the backdrop
backsideThickness number 1.5 010 Thickness used only during the backside pass
backdropResolutionScale number 0.75 0.251 Resolution scale of the (backside / final) backdrop target
backsideResolutionScale number 0.5 0.251 Resolution scale of the clean pass when backside is on
forceSinglePass boolean false Force single-pass material rendering
bg string (hex) '#839681' Solid fill used as scene background while capturing the backdrop

Fresnel helpers (settings object)

Parameter Type Default Description
fresnelIntensity number 0.12 Extra fresnel emphasis used by tooling / presets
fresnelColor string (hex) '#f2f7ff' Fresnel highlight tint for presets

Architecture (source map)

src/glass/
  GlassMaterial.jsx                 # R3F material component + GlassPhysicalNodeMaterial
  glassSettings.js                  # Shared defaults, apply + subscribe API
  transmissionBackdrop.js           # Clean + backside render-target manager
  transmissionNodes.js              # TSL volume refraction / dispersion / sampling
  TransmissionPhysicalLightingModel.js  # Injects backdrop into PhysicalLightingModel

src/glass-panel/                    # Live WebGPU Glass Config UI
src/scenes/                         # Gelatinous Cube / Aquarium / Shader Ball
src/bubbles/                        # Instanced bubbles on TRANSMISSION_BACKDROP_LAYER
src/canvas/                         # WebGPU renderer, lighting, PCSS shadows

Key exports for reuse

Export From Role
GlassMaterial glass/GlassMaterial.jsx Drop-in R3F material
glassSettings / setGlassSetting / applyGlassSettings glass/glassSettings.js Live parameter store
TRANSMISSION_BACKDROP_LAYER glass/transmissionBackdrop.js Layer for geometry visible only in the backdrop
captureTransmissionBackdrop glass/transmissionBackdrop.js Manual capture hook (already called from GlassMaterial)
buildTransmissionBackdropNode glass/transmissionNodes.js Build the TSL refraction node yourself

Scenes in the demo

Scene Idea
Gelatinous Cube Soft organic volume, internal bubbles on the backdrop layer, heavy distortion-friendly look
Aquarium Clearer water-like IOR / thickness, scene content refracted through a tank
Shader Ball Classic studio shader ball on a checker pedestal — best for judging dispersion, roughness, and reflections

Presets live in src/scenes/scenePresets.js and overwrite glassSettings when you switch scenes.


Browser / renderer notes

  • Create the canvas with THREE.WebGPURenderer (see src/canvas/createWebGPURenderer.js). Call await renderer.init() before the first frame.
  • Tone mapping: ACES Filmic on the main path; NoToneMapping during backdrop capture.
  • Glass meshes are temporarily hidden while the clean buffer is rendered, then restored — do not rely on them being visible mid-capture.
  • Changing samples or spectralDispersion rebuilds the material node graph; expect a short hitch.

Credits

Created by Anderson Mancini — a WebGPU evolution of the Mesh Transmission Material approach popularized for React Three Fiber / Drei.


License

Check the repository license file for usage terms. If none is present, contact the author before redistributing the material in a commercial product.