Mesh Transmission Material for React Three Fiber — powered by WebGPU & Three.js TSL
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:
- Clean pass — hide all glass meshes, render the scene into
cleanTarget(HalfFloat + mipmaps). Geometry onTRANSMISSION_BACKDROP_LAYERcan appear only in this buffer (e.g. bubbles inside the gelatinous cube). - Backside pass (optional) — flip glass materials to
BackSide, temporarily usebacksideThickness, refract the clean buffer intobacksideTarget. - 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
sampleschanges) 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
triNoise3Doffset 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:
- Creates a
GlassPhysicalNodeMaterialwith transmission uniforms. - Registers itself with the backdrop manager for the current WebGPU renderer.
- Captures the backdrop each frame.
- Syncs live values from
glassSettings(and the config panel /__glassSettingsin 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 |
1 – 2.5 |
Index of refraction (air ≈ 1, water ≈ 1.33, glass ≈ 1.5) |
thickness |
number |
3.5 |
typically -2 – 2 in UI (scene-dependent) |
Optical path length through the volume; drives refraction offset and attenuation distance |
dispersion |
number |
6.5 |
0 – 20 |
Chromatic aberration strength (per-channel IOR spread) |
transmission |
number |
1 |
0 – 1 |
How much light is transmitted vs. opaque |
anisotropicBlur |
number |
0.47 |
0 – 1 |
Extra thickness smear / frost independent of roughness (internalBlur alias) |
attenuationDistance |
number |
0.5 |
0 – 10 |
Distance over which attenuation color fully absorbs |
attenuationColor |
string (hex) |
'#ffffff' |
— | Beer–Lambert absorption tint |
envMapIntensity |
number |
0.85 |
0 – 5 |
Strength of environment reflections / IBL |
specularIntensity |
number |
1 |
0 – 1 |
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 |
0 – 1 |
Microfacet roughness; also raises backdrop mip LOD |
metalness |
number |
0 |
0 – 1 |
Metallic blend |
clearcoat |
number |
0.2 |
0 – 1 |
Clearcoat layer weight |
clearcoatRoughness |
number |
0 |
0 – 1 |
Clearcoat roughness |
iridescence |
number |
0.45 |
0 – 1 |
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 |
0 – 1 |
Strength of 3D noise bent into the refraction normal |
distortionScale |
number |
0.3 |
0 – 2 |
Spatial frequency of the distortion noise |
temporalDistortion |
number |
0.2 |
0 – 1 |
How fast the noise scrolls over time (gel / liquid feel) |
Performance / backdrop
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
samples |
number (int) |
8 |
1 – 16 |
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 |
0 – 10 |
Thickness used only during the backside pass |
backdropResolutionScale |
number |
0.75 |
0.25 – 1 |
Resolution scale of the (backside / final) backdrop target |
backsideResolutionScale |
number |
0.5 |
0.25 – 1 |
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(seesrc/canvas/createWebGPURenderer.js). Callawait 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
samplesorspectralDispersionrebuilds 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.
