Skip to content

dswhy/threejs-pom

 
 

Repository files navigation

@promontis/threejs-pom

npm version npm downloads bundle size license demo

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.

Try it

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.

Install

npm install @promontis/threejs-pom three

three is a peer dependency — bring your own version (>=0.180.0).

Quick start

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.

API

createPomMaterial(options): PomMaterial

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;

How the POM works

The shader is implemented entirely in TSL:

  1. The view direction is transformed into tangent space using the built-in parallaxDirection node.
  2. A Loop (24–40 layers depending on grazing angle) ray-marches into the height field until it intersects the surface.
  3. A 5-step refinement loop bisects the last segment for sub-layer precision.
  4. UVs that escape the [0, 1] range after marching are discard-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).

React Three Fiber example

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>
  );
}

Browser support

@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.

Contributing

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

License

MIT © Promontis. See LICENSE for the full text and THIRD_PARTY_NOTICES.md for upstream acknowledgements.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • TypeScript 100.0%