npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

three-gtvbao

v0.2.1

Published

GT-VBAO ambient occlusion for three.js WebGPU and WebGL2 (TSL): visibility-bitmask AO with the GT-VBAO corrections, a depth-MIP prefilter, a denoiser and a depth-aware upsample

Readme

three-gtvbao

GT-VBAO ambient occlusion showcase

English | 日本語

GT-VBAO screen-space ambient occlusion for three.js WebGPU and WebGL2, written in TSL.

Based on three.js's SSGINode AO path and DenoiseNode, with Mirko Salm's GT-VBAO corrections. It applies AO to indirect lighting through builtinAOContext and RenderPipeline.

  • Visibility bitmasks with cosine-weighted sectors and perspective-correct slice directions.
  • Linear-depth MIPs, reduced-resolution rendering and depth-aware upsampling to reduce edge bleeding.
  • Temporal sampling with TRAA, or fixed sampling with an edge-aware denoiser.
  • Five quality presets, debug views and TypeScript declarations.
  • Native ESM with tree shaking; import only the named exports you need. Runtime options do not remove bundled code.

Live demo

Open the demo to explore the presets and AO controls. The active backend is shown in the demo; force WebGL2 to try that backend.

Requirements

  • three.js r184 or newer (three/webgpu and three/tsl). Verified on WebGPU and WebGL2: 0.184.0 and 0.185.1 in the lighting compatibility regression, and 0.186.0 with the output-switch timing fix.
  • WebGPURenderer with its WebGPU or WebGL2 backend; the legacy WebGLRenderer is unsupported.
  • EXT_color_buffer_float when using WebGL2.
  • A PerspectiveCamera or OrthographicCamera.

Install

npm install three three-gtvbao

Quick start

Start with an initialized WebGPURenderer, a scene and a perspective or orthographic camera. The opaque pre-pass provides depth and normals; the lit pass applies AO to materials that use three.js's AO lighting hook, such as MeshStandardNodeMaterial. Unlit materials are unaffected.

import * as THREE from "three/webgpu";
import { builtinAOContext, mrt, normalView, pass, positionView, screenUV, velocity } from "three/tsl";
import { traa } from "three/examples/jsm/tsl/display/TRAANode.js";
import { gtvbao, applyGtvbaoPreset, renderPassAfter, createPassthroughAoContext } from "three-gtvbao";

// 1. Opaque pre-pass: depth, view-space normals and velocity for TRAA.
const prePass = pass(scene, camera);
prePass.transparent = false;
prePass.setMRT(mrt({ output: normalView, velocity }));
prePass.contextNode = createPassthroughAoContext();
const preNormal = prePass.getTextureNode("output");
const preDepth = prePass.getTextureNode("depth");
const preVelocity = prePass.getTextureNode("velocity");

// 2. AO reads the completed pre-pass.
const aoNode = gtvbao(preDepth, preNormal, camera);
applyGtvbaoPreset(aoNode, "Balanced");
renderPassAfter(aoNode, prePass);

// 3. Apply AO at each lit fragment's surface.
const scenePass = pass(scene, camera);
renderPassAfter(scenePass, prePass);
scenePass.contextNode = builtinAOContext(
  aoNode.createDepthAwareAo(aoNode.getTextureNode(), {
    screenUv: screenUV,
    viewPosition: positionView,
    viewNormal: normalView,
  })
);

// 4. Resolve temporal sampling with TRAA.
const pipeline = new THREE.RenderPipeline(renderer);
pipeline.outputNode = traa(scenePass, preDepth, preVelocity, camera);
aoNode.setVariantChangeCallback(() => {
  pipeline.needsUpdate = true;
});

renderer.setAnimationLoop(() => pipeline.render());

Keep the renderPassAfter calls and the pre-pass's createPassthroughAoContext(): they ensure the depth inputs are ready and prevent the lit pass's AO hook from affecting the pre-pass. The variant callback updates the pipeline when settings change the compiled shader.

Without temporal anti-aliasing

Before starting the render loop, use a No Temporal preset, feed the denoised texture to the lit pass, and replace the TRAA output with the scene pass:

import { gtvbaoDenoise } from "three-gtvbao";

const denoiseNode = gtvbaoDenoise(aoNode.getTextureNode(), preDepth, preNormal, camera, {
  linearDepthSource: aoNode,
});
renderPassAfter(denoiseNode, aoNode);
applyGtvbaoPreset(aoNode, "No Temporal Low", denoiseNode);

scenePass.contextNode = builtinAOContext(
  aoNode.createDepthAwareAo(denoiseNode.getTextureNode(), {
    screenUv: screenUV,
    viewPosition: positionView,
    viewNormal: normalView,
  })
);
pipeline.outputNode = scenePass;

Presets

| Preset | Resolution scale | Slices × steps | Temporal | Denoise | | --- | --- | --- | --- | --- | | Low | 0.5 | 1 × 8 | yes | no | | Balanced | 0.5 | 2 × 6 | yes | no | | High | 1 | 3 × 12 | yes | no | | No Temporal Low | 0.5 | 2 × 6 | no | yes | | No Temporal High | 1 | 3 × 12 | no | yes |

applyGtvbaoPreset(aoNode, presetOrName, denoiseNode?) applies settings and returns the resolved preset. It does not wire the render graph: temporal presets need TRAA or another temporal resolve; denoising presets need the denoiser's output connected as above. DEFAULT_GTVBAO_PRESET is "Balanced"; available names and settings are in GTVBAO_PRESET_NAMES and GTVBAO_PRESETS.

API

gtvbao(depthNode, normalNode, camera, options?)

Also available as new GTVBAONode(...). Pass a pre-pass depth texture node, a view-space normal texture node (or null to reconstruct normals from depth), and a PerspectiveCamera or OrthographicCamera. Logarithmic depth buffers are supported. options accepts initial values for the properties below.

Defaults here are constructor defaults, before applying a preset. Set plain properties directly and uniform / variant properties through .value. Variant changes can rebuild the shader; batchVariantChanges(fn) groups them into one rebuild. Set normalEncoding at construction.

| Property | Kind | Default | Description | | --- | --- | --- | --- | | resolutionScale | plain | 1 | AO resolution relative to the drawing buffer. | | sliceCount | variant | 2 | Slices per pixel (1–8). | | stepCount | variant | 8 | Steps per direction (1–32); up to sliceCount × stepCount × 2 samples per pixel. | | radius | uniform | 3 | Sampling radius control; screen-relative by default, world-space when useScreenSpaceSampling is false. | | useScreenSpaceSampling | variant | true | Choose screen-relative rather than world-space sampling radius. | | thickness | uniform | 0.12 | Base occluder thickness in world units, before depth scaling and clamping. | | useLinearThickness | variant | true | Scale thickness with sample view depth relative to camera.far. | | linearThicknessScale | uniform | 100 | Multiplier for depth-scaled thickness. | | maxThickness | uniform | 0.35 | Maximum effective thickness and depth range for the MIP prefilter. | | aoIntensity | variant | 1 | Visibility exponent; rebuilds only when entering or leaving the 1 fast path. | | expFactor | variant | 2 | Step distribution exponent; rebuilds only when entering or leaving the 2 fast path. | | sectorMeasure | variant | "cosine" | Sector weighting: "cosine", "solidAngle" or "angle"; see below. | | usePerspectiveCorrectSlice | variant | true | Distribute slices around the view vector and project them to the screen. | | useDepthMips | variant | true | Sample the linear-depth MIP chain instead of scene depth. | | useDepthAwareUpsample | plain | true | Enable depth-aware upsampling when resolutionScale < 1 and depth MIPs are enabled. | | useTemporalFiltering | plain | true | Rotate samples each frame; requires a temporal resolve. | | normalEncoding | plain | "view" | Raw view-space normals, or "directionToColor" for normals packed into [0,1]. | | debugMode | variant | 0 | Select a view from GTVBAO_DEBUG_MODE_OPTIONS. |

| Method | Description | | --- | --- | | getTextureNode() | AO texture; use createDepthAwareAo for depth-aware upsampling. | | createDepthAwareAo(aoTexture, { screenUv, viewPosition, viewNormal }) | Sample AO at a lit fragment using its own positionView and normalView. | | createDepthAwareAoFromBuffers(aoTexture, depthTexture, normalTexture, screenUv) | Equivalent for a full-screen pass, reconstructing the surface from buffers. | | getDepthMipNodes() | Five linear-depth textures; level 0 is at AO resolution. Updated only while depth MIPs are enabled. | | isDepthAwareUpsampleActive() | Whether reduced resolution and the current settings enable depth-aware upsampling. | | setVariantChangeCallback(fn) | Register the pipeline update callback shown above. | | batchVariantChanges(fn) | Apply multiple variant changes with one rebuild. | | setSize(width, height) | Called automatically with the drawing buffer size. | | dispose() | Release the node's render targets, materials and depth prefilter. |

Both camera types use the same API, including denoising, depth MIPs and depth-aware upsampling. Pass the same camera used to render the depth/normal buffers. After changing zoom or the frustum, call camera.updateProjectionMatrix() as usual. With an orthographic camera, world-space sampling (useScreenSpaceSampling: false) has a depth-independent screen radius. useLinearThickness still explicitly scales thickness with view depth; set it to false for constant world-space thickness.

gtvbaoDenoise(aoTexture, depthNode, normalNode, camera, options?)

Also available as new GTVBAODenoiseNode(...). A 16-tap edge-aware denoiser that runs at AO resolution. getTextureNode() returns its output; dispose() releases its resources.

Adjust radius (AO texels), lumaPhi, depthPhi and normalPhi through .value. Options are normalEncoding, linearDepthSource (an AO node whose linear depth can be reused), and useTemporalDenoiseRotation (rotate the kernel each frame; use only with a temporal resolve).

Pass ordering and exports

renderPassAfter(passNode, dependency) renders the dependency first each frame. createPassthroughAoContext() isolates the opaque pre-pass from the lit pass's AO context. See the quick start for their placement.

For all exports, including debug helpers, sector measures and render-pass names for timestamp queries, see src/index.js and the TypeScript declarations.

Algorithm and limitations

Each slice tracks occluded directions in a 32-bit mask. GT-VBAO remaps horizon angles before quantization, uses shared dithering for both sector edges, and distributes slices around the per-pixel view vector. Clear sectors determine visibility; cosine-weighted slices are combined using their integral weights.

  • cosine uses GTAO-style weighting to reduce camera-pitch bias. solidAngle uses solid-angle weighting; angle keeps the original equal-angle VBAO sectors.
  • cosine can be noisier in dark regions at the same sample count. Try angle if lower noise matters more than reducing pitch bias.
  • AO uses screen-space depth, so hidden or off-screen geometry cannot contribute.
  • Thickness is an approximation; the perspective-correct offset along each sample's own view ray is not implemented.

Development

In a repository checkout, use Node.js 20+:

npm install
npm run dev
npm run typecheck
npm test
npm run build

Open the local URL printed by Vite (normally http://localhost:5173). The example shows the active backend; append ?backend=webgl to force WebGL2. In your app, use new THREE.WebGPURenderer({ forceWebGL: true }); otherwise WebGL2 is selected when WebGPU is unavailable.

npm test includes the tree-shaking checks; npm run test:treeshake runs those alone. Browser GPU checks are separate: see WebGPU / WebGL2 regressions.

Use the Projection selector to choose Perspective or Orthographic. Switching projection reloads the example; ?camera=orthographic opens it directly, and can be combined with &backend=webgl. Camera views and orbit/zoom work in both modes.

The example is published to GitHub Pages by GitHub Actions after each push to master. The Pages build uses npm run build -- --base=/three-gtvbao/ and publishes dist/.

Credits

  • Based on three.js's SSGINode.js and DenoiseNode.js (MIT). SSGINode is a port of Olivier Therrien's SSRT3 (MIT), from Screen Space Indirect Lighting with Visibility Bitmask (Therrien, Levesque, Gilet, 2023).
  • GT-VBAO corrections: Mirko Salm's Shadertoy (CC0 / MIT).
  • Depth MIP prefilter: Intel's XeGTAO (MIT).

License

MIT