A Three.js WebGPU TSL surface material with built-in parallax occlusion mapping (POM). Drop in your standard PBR maps (albedo, normal, roughness, AO, height, metalness) and get a node material with a real ray-marched height field — not just a normal map fake — running entirely on the GPU.
Note
POM uses the parallaxDirection TSL node and runs on the WebGPU renderer. It
requires a browser with WebGPU enabled and a Three.js build that includes the
three/webgpu and three/tsl entry points.
Live demo: https://promontis.github.io/threejs-pom/
The demo lets you browse Poly Haven and PBRPX textures, swap PBR maps live, and play with depth, tiling, and lighting. POM is automatically disabled for assets without a displacement map.
npm install @promontis/threejs-pom threethree is a peer dependency — bring your own version (>=0.180.0).
import * as THREE from 'three/webgpu';
import { createPomMaterial } from '@promontis/threejs-pom';
const loader = new THREE.TextureLoader();
const material = createPomMaterial({
albedo: loader.load('/textures/brick_albedo.jpg'),
normal: loader.load('/textures/brick_normal.jpg'),
roughness: loader.load('/textures/brick_roughness.jpg'),
height: loader.load('/textures/brick_height.png'),
// ao, metalness are also supported and optional
depth: 0.025, // parallax depth in UV units (default 0.025)
repeat: 1, // UV tiling multiplier (default 1)
});
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
mesh.rotation.x = -Math.PI / 2;
scene.add(mesh);
// Tweak live without rebuilding the material:
material.pom.depth.value = 0.05;
material.pom.repeat.value = 2;Tip
The renderer must be a THREE.WebGPURenderer. POM relies on the TSL
parallaxDirection node, which is only available on the WebGPU pipeline.
Returns a THREE.MeshStandardNodeMaterial wired up for PBR with optional
parallax occlusion mapping. Texture color space, wrapping, and anisotropy are
configured for you — pass raw THREE.Texture instances straight from your
loader.
type PomMaterialOptions = {
albedo: THREE.Texture;
normal?: THREE.Texture;
roughness?: THREE.Texture;
ao?: THREE.Texture;
height?: THREE.Texture;
metalness?: THREE.Texture;
/** Parallax depth in UV units. Default 0.025. Useful range: 0.005 – 0.1. */
depth?: number;
/** UV repeat multiplier (tiling). Default 1. */
repeat?: number;
};POM is enabled automatically when height is supplied, and skipped otherwise.
The returned material has a .pom field with live uniforms — assign to
.value to tweak at runtime without rebuilding the shader graph:
material.pom.depth.value = 0.04;
material.pom.repeat.value = 4;The shader is implemented entirely in TSL:
- The view direction is transformed into tangent space using the built-in
parallaxDirectionnode. - A
Loop(24–40 layers depending on grazing angle) ray-marches into the height field until it intersects the surface. - A 5-step refinement loop bisects the last segment for sub-layer precision.
- UVs that escape the
[0, 1]range after marching arediscard-ed, so the silhouette correctly reveals occlusion at the geometry edges.
See src/pomMaterial.ts for the full implementation —
it is intentionally short and self-contained (~150 LOC).
import { Canvas, useLoader } from '@react-three/fiber';
import { useEffect, useMemo } from 'react';
import * as THREE from 'three/webgpu';
import { createPomMaterial } from '@promontis/threejs-pom';
function Surface({ urls, depth, repeat }) {
const [albedo, normal, roughness, height] = useLoader(THREE.TextureLoader, [
urls.albedo,
urls.normal,
urls.roughness,
urls.height,
]);
// Recreate only when the texture set changes; depth and repeat live as uniforms.
const material = useMemo(
() => createPomMaterial({ albedo, normal, roughness, height, depth, repeat }),
[albedo, normal, roughness, height],
);
useEffect(() => { material.pom.depth.value = depth; }, [material, depth]);
useEffect(() => { material.pom.repeat.value = repeat; }, [material, repeat]);
return (
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[10, 10]} />
<primitive object={material} attach="material" />
</mesh>
);
}@promontis/threejs-pom runs on the WebGPU renderer. Today that means:
| Browser | Status |
|---|---|
| Chrome / Edge ≥113 | ✅ |
| Safari TP / 17.4+ | ✅ |
| Firefox | Behind dom.webgpu.enabled |
Chrome can hide navigator.gpu on http://0.0.0.0, LAN URLs, embedded
previews, or when hardware acceleration is disabled. Use http://localhost
for development.
PRs welcome! See CONTRIBUTING.md for the project layout and dev setup. The repo is split into two folders:
src/— the published library (@promontis/threejs-pom)demo/— the WebGPU viewer deployed to GitHub Pages
MIT © Promontis. See LICENSE for the full text and THIRD_PARTY_NOTICES.md for upstream acknowledgements.