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

@hology/webgpu-renderer

v0.1.6

Published

WebGPU renderer for the Hology game engine

Downloads

1,038

Readme

@hology/webgpu-renderer

TypeScript-first WebGPU renderer scaffold aimed at teams migrating from an existing three.js codebase.

The current slice keeps three scene/content classes as the authoring surface and adapts them into a small WGSL-first renderer:

  • Mesh
  • SkinnedMesh
  • InstancedMesh
  • BatchedMesh
  • BufferGeometry and BufferAttribute
  • Texture / CanvasTexture / DataTexture
  • Material via a custom WGSLShaderMaterial

What this version does

  • Initializes a raw WebGPU device and canvas context.
  • Traverses a three Scene.
  • Skins SkinnedMesh content in the WGSL vertex stage from cached GPU bone palettes; CPU work is limited to one palette update per changed skeleton plus conservative bone-envelope culling.
  • Extracts standard meshes, instanced meshes, and batched meshes into a shared per-frame instance buffer.
  • Renders standard meshes, true instanced draws for InstancedMesh, and batched draws for BatchedMesh without per-draw object-uniform uploads.
  • Runs the frame through an explicit render graph with pass dependencies, resource reads/writes, and ordered execution.
  • Builds a Hi-Z depth pyramid with compute passes.
  • Uses compute to cull indexed opaque draws against the Hi-Z pyramid and write indirect indexed draw arguments on the GPU.
  • Temporally GPU-culls every instance in a StaticDrawSet, compacts visible source-instance indices before the static depth prepass, and builds its indirect instance counts without per-frame CPU visibility uploads.
  • Renders directional-light and point-light shadow maps.
  • Exposes a reusable ComputeKernel abstraction for general-purpose compute work.
  • Exposes both render() and renderAsync() so graph passes can opt into async prepare() work before execution.
  • Exposes compile() and compileAsync() so scenes can prewarm geometry, textures, and material pipelines before the first visible frame.
  • Can capture per-pass render-graph timings, with CPU timings everywhere and GPU timestamp-query timings when the device supports them.
  • Supports custom render-graph nodes, including fullscreen post-processing passes that can read the renderer's scene color target and write to a post-process target before presentation.
  • Exposes a higher-level FrameGraphBuilder so passes can be wired with named outputs and inputs instead of raw GPUTextureView plumbing.
  • Lets you write custom WGSL shaders and pass uniforms/textures through a material that plays the role of a WebGPU-era ShaderMaterial.
  • Supports sampled 2D, 2D-array, 3D, cube, compressed, and compressed-array texture uploads, including KTX2-friendly CompressedTexture, CompressedTextureArray, CompressedCubeTexture, DataArrayTexture, and Data3DTexture paths.
  • Ships with a demo scene that proves the pipeline works with Mesh, BufferAttribute, Texture, InstancedMesh, BatchedMesh, and dynamic shadows.

What this version does not do yet

  • Built-in PBR or lighting materials.
  • GPU-driven culling for transparent draws or non-indexed draws. The current visibility path targets indexed opaque draws first.
  • GPU compaction into a single multi_draw_indirect style stream. The renderer writes one indirect indexed argument record per extracted indexed draw and then submits them individually.
  • Full GPU-driven aggregation for mutable BatchedMesh content. Dynamic batches still use the CPU extraction path; immutable batches can use StaticDrawSet for per-instance GPU culling and compaction.
  • Cascaded directional shadows, clustered lighting, point-light arrays, shadow atlases, or any of the bigger AAA-scale scheduling systems yet.
  • Storage textures, external textures, and the more specialized renderer-owned texture flows from three's node/WebGPU stack.
  • Post-processing, temporal anti-aliasing/upscaling, terrain, particles, or material-template systems.

Install and run

npm install
npm run dev

Useful scripts:

  • npm run build
  • npm run typecheck
  • npm run demo:build

Public API

import {
  FrameGraphBuilder,
  Mesh,
  Scene,
  PerspectiveCamera,
  BoxGeometry,
  WGSLShaderMaterial,
  WebGpuRenderer,
} from "@hology/webgpu-renderer";

const renderer = new WebGpuRenderer({ canvas });
await renderer.initialize();

const material = new WGSLShaderMaterial({
  vertexShader: `...`,
  fragmentShader: `...`,
  uniforms: {
    time: { type: "f32", value: 0 },
  },
});

const mesh = new Mesh(new BoxGeometry(1, 1, 1), material);
const scene = new Scene();
scene.add(mesh);

const camera = new PerspectiveCamera(50, canvas.width / canvas.height, 0.1, 100);
camera.position.z = 4;

renderer.render(scene, camera);

await renderer.renderAsync(scene, camera);

renderer.compile(scene, camera);
await renderer.compileAsync(scene, camera);

compile() is synchronous and assumes initialize() has already completed. compileAsync() will initialize on demand if needed and uses async pipeline compilation for WGSL materials.

For incremental migrations, a renderer-wide fallback can draw meshes whose source materials have not been ported yet. The source materials remain untouched, and native WGSLShaderMaterial instances still take precedence:

const renderer = new WebGpuRenderer({
  canvas,
  fallbackMaterial: diagnosticMaterial,
  // Disable this for scenes whose callbacks require THREE.WebGLRenderer APIs.
  invokeObjectRenderCallbacks: false,
});

The fallback applies to regular, instanced, and batched mesh extraction, including static draw sets. For imported THREE.SkinnedMesh objects, the renderer instead uses its built-in skin-aware lit fallback automatically, so standard Three.js materials and their skin attributes work without a WGSL material per submesh. Set fallbackSkinnedMaterial when a project needs a custom GPU-skinned appearance. Batched meshes from both the Three.js r169 _drawInfo layout and newer _instanceInfo layout are supported.

Hi-Z occlusion culling is currently optional and defaults to off while the GPU visibility path is still settling:

const renderer = new WebGpuRenderer({
  canvas,
  hiZOcclusionCulling: false,
  sampleCount: 1,
  renderGraphProfiling: true,
});

renderer.setHiZOcclusionCullingEnabled(true);
renderer.setRenderGraphProfilingEnabled(true);

Whether Hi-Z culling is actually active also depends on device support for WebGPU indirect draws with non-zero firstInstance:

renderer.getHiZOcclusionCullingEnabled(); // requested
renderer.getHiZOcclusionCullingActive(); // requested + supported on this device
renderer.getLastFrameProfile(); // latest CPU/GPU pass timings
renderer.getSampleCount(); // current MSAA sample count

Manually synchronized scene transforms

By default, render() updates the scene's world matrices before extraction, as Three renderers normally do. Editors that already track transform dirtiness can avoid that full traversal by synchronizing dirty subtrees and then disabling the automatic renderer update:

scene.matrixWorldAutoUpdate = false;

function synchronizeTransform(object: THREE.Object3D): void {
  // Recompose this object's local matrix when position/quaternion/scale changed.
  if (object.matrixAutoUpdate) object.updateMatrix();

  // Update its ancestors as needed, then propagate the new world transform to
  // descendants. Call once for each highest dirty root, not for every child.
  object.updateWorldMatrix(true, true);
}

A camera parented into the scene is covered by its dirty subtree update. For an unparented camera, either leave camera.matrixWorldAutoUpdate enabled or disable it and call camera.updateMatrixWorld() after changing its transform. The same manual synchronization rule applies to directional and spot-light targets whose matrixWorldAutoUpdate has been disabled.

Static GPU Visibility

For immutable environment content, snapshot the authored Three objects once and remove the source root from the dynamic scene. The static path supports indexed opaque Mesh, InstancedMesh, and BatchedMesh content with WGSLShaderMaterial:

await renderer.initialize();

const staticEnvironment = renderer.createStaticDrawSet(environmentRoot, {
  label: "environment",
});
scene.remove(environmentRoot);

With Hi-Z active, the renderer keeps frozen instance data and per-instance bounds on the GPU. It first culls against the previous frame's depth pyramid, compacts the survivors, and uses those indirect commands for the static depth prepass. It then builds the current pyramid and retests only the initial rejects before forward rendering, which keeps camera motion and newly exposed geometry correct. CPU work does not scale with the static set's instance count.

The first frame after creation, resize, or toggling Hi-Z is deliberately frustum-only because there is no valid depth history yet. Static transforms, membership, geometry, materials, and visibility are frozen; dispose and recreate the set after changing any of those properties. Material uniforms and textures remain live.

MSAA is currently disabled by default. You can enable it with:

const renderer = new WebGpuRenderer({
  canvas,
  sampleCount: 4,
});

or update it later with:

renderer.setSampleCount(4);

This first slice applies MSAA to the built-in forward pass and resolves back into the normal scene color target, while the rest of the render graph remains single-sampled.

Meshes can also provide mesh.customDepthMaterial using WGSLShaderMaterial. In this slice, that override is used for directional and spot shadows so vertex-deformed casters can render correctly into the shadow map. The shared depth prepass intentionally skips those meshes for now to keep the prepass on its fast shared pipeline.

Optional renderer hooks:

  • renderer.setRenderGraphBuilder((graph, context) => { ... })
  • renderer.setHiZOcclusionCullingEnabled(true | false)
  • renderer.getHiZOcclusionCullingActive()
  • renderer.setRenderGraphProfilingEnabled(true | false)
  • renderer.getLastFrameProfile()
  • renderer.createComputeKernel({ ... })

Inside setRenderGraphBuilder(...), the context can now:

  • add the built-in shadow, depth, Hi-Z, visibility, forward, and present passes
  • add a custom pass via context.addCustomPass({ ... })
  • choose whether the present pass samples from sceneColor or postProcessColor

For a simpler authoring layer, use FrameGraphBuilder:

renderer.setRenderGraphBuilder((_graph, context) => {
  const frameGraph = new FrameGraphBuilder(context);
  const vignetteColor = frameGraph.createColorTarget("vignetteColor");

  frameGraph.addStandardScenePasses();
  frameGraph.addFullscreenPass({
    name: "VignettePostPass",
    inputs: {
      sceneColor: frameGraph.sceneColor,
    },
    outputs: vignetteColor,
    fragmentShader: `...`,
  });
  frameGraph.present(vignetteColor);
});

This keeps the internal renderer explicit, but lets normal usage feel closer to named render targets flowing from one pass to the next.

Shader contract

The renderer prepends a small WGSL header before your shaders. Your WGSL code is expected to provide vsMain and fsMain.

For porting existing three.js ShaderMaterial code, see Migrating Three ShaderMaterial Shaders. For material shaders that write multiple render targets, see Multiple Render Targets. WGSLShaderMaterial accepts three-style uniform entries like { value }, infers common WGSL types, and moves texture uniforms into texture bindings automatically.

WGSLShaderMaterial also runs a small preprocessor before WebGPU compilation. You can use defines plus common GLSL-style directives such as #define, #ifdef, #ifndef, #if, #elif, #else, and #endif in WGSL shader strings:

const material = new WGSLShaderMaterial({
  defines: {
    USE_BATCH_PARAMS: true,
    MAX_LIGHT_STEPS: 8,
  },
  vertexShader,
  fragmentShader,
});

The preprocessor is a migration helper, not native WGSL. It supports conditional compilation and simple object-like/function-like macro expansion, but it does not provide three's GLSL chunks or convert GLSL syntax to WGSL.

The renderer also provides a small compatibility define set by default:

  • HIGH_PRECISION
  • WEBGPU_RENDERER
  • WGSL_SHADER_MATERIAL
  • USE_BATCHING
  • USE_COLOR
  • USE_COLOR_ALPHA
  • USE_SHADOWMAP when compiling a shadow-receiving forward variant
  • DOUBLE_SIDED or FLIP_SIDED from material.side
  • texture-name defines such as USE_MAP, USE_NORMALMAP, and USE_ALPHAMAP when matching texture bindings exist
  • light-count constants such as NUM_DIR_LIGHTS, NUM_POINT_LIGHTS, and NUM_SPOT_LIGHTS

You can disable a default compatibility define per material by setting it to false in defines.

Available frame resources in every shader:

struct RendererCameraUniforms {
  viewProj: mat4x4<f32>,
  cameraPosition: vec4<f32>,
  viewportAndTime: vec4<f32>,
  view: mat4x4<f32>,
  projection: mat4x4<f32>,
};

Also available:

  • rendererElapsedTime() — the timeSeconds argument passed to render(), in seconds
  • rendererWorldPosition(localPosition, instanceIndex)
  • rendererClipPosition(localPosition, instanceIndex)
  • rendererWorldNormal(localNormal, instanceIndex)
  • rendererObjectColor(instanceIndex)
  • rendererBatchingId(instanceIndex)
  • modelMatrix(instanceIndex)
  • modelViewMatrix(instanceIndex)
  • projectionMatrix()
  • normalMatrix(instanceIndex)
  • cameraPosition()
  • rendererDirectionalLightDirection()
  • rendererDirectionalLightColor()
  • rendererPointLightPosition()
  • rendererPointLightRange()
  • rendererPointLightColor()
  • rendererAmbientLight()
  • rendererSampleDirectionalShadow(worldPosition)
  • rendererSamplePointShadow(worldPosition)

If you define material uniforms, they are exposed as:

@group(1) @binding(0) var<uniform> materialUniforms: MaterialUniforms;

For example, a uniform named time is read as materialUniforms.time in WGSL rather than as a global time variable.

For renderer-wide animation time, prefer rendererElapsedTime() over a material time uniform. The renderer uploads it once per frame through the shared scene or shadow-camera bind group, so it works for forward rendering, depth/shadow passes, and compatible StaticDrawSet render bundles without per-material uniform uploads or bundle rebuilds.

For BatchedMesh, rendererBatchingId(instanceIndex) returns the original three.js batched instance id from BatchedMesh.addInstance(...). This is the migration replacement for shaders that used three's batching id path to look up additional per-batch data from a custom texture or buffer.

If you define textures like { albedo: texture }, they are exposed with the correct WGSL texture type for the bound three.js texture. A regular 2D texture looks like:

@group(1) @binding(1) var albedoSampler: sampler;
@group(1) @binding(2) var albedoTexture: texture_2d<f32>;

For array, cube, 3D, integer, or depth textures, the declaration changes automatically to the matching WGSL type such as texture_2d_array<f32>, texture_cube<f32>, or texture_3d<f32>.

Vertex attribute locations are fixed for this first slice:

  • @location(0) position vec3<f32>
  • @location(1) normal vec3<f32>
  • @location(2) uv vec2<f32>
  • @location(3) color vec4<f32>
  • @builtin(instance_index) instance index for shared instance-buffer lookups

Missing geometry attributes are auto-filled with defaults.

GPU skinning contract

SkinnedMesh no longer has a CPU-skinned fallback. Imported Three.js meshes use the renderer's built-in skin-aware fallback material automatically. For custom WGSL, the renderer sets USE_SKINNING from the draw object and supplies the two GPU vertex streams. The material only needs the conditional shader path; no editor-facing skinning setting is required:

struct VertexInput {
  @location(0) position: vec3<f32>,
  @location(1) normal: vec3<f32>,
#ifdef USE_SKINNING
  @location(4) skinIndex: vec4<u32>,
  @location(5) skinWeight: vec4<f32>,
#endif
  @builtin(instance_index) instanceIndex: u32,
};

#ifdef USE_SKINNING
let skinnedPosition = rendererSkinPosition(input.position, input.skinIndex, input.skinWeight);
let skinnedNormal = rendererSkinNormal(input.normal, input.skinIndex, input.skinWeight);
#else
let skinnedPosition = input.position;
let skinnedNormal = input.normal;
#endif
output.position = rendererClipPosition(skinnedPosition, input.instanceIndex);
output.worldNormal = rendererWorldNormal(skinnedNormal, input.instanceIndex);

The renderer shares one GPU palette per THREE.Skeleton, uploads it only when its bone world matrices or inverses change, and retains a per-mesh bind-matrix uniform. It also culls against a conservative envelope built from each bone's influenced vertices before a palette upload is needed.

Demo

The demo is wired directly to the source library and shows:

  • a regular Mesh using a custom BufferAttribute color stream
  • a procedural SkinnedMesh example with animated bones
  • a realistic 20-character X Bot walking crowd loaded from the supplied FBX mesh and animation
  • a shared texture sampled from WGSL
  • animated material uniforms
  • an InstancedMesh
  • a BatchedMesh
  • a shadow-casting directional light
  • a shadow-casting point light
  • a render graph that includes depth, Hi-Z, visibility, and forward passes
  • a live HUD that visualizes per-pass frame timings
  • an example picker with both a baseline scene and a custom vignette post-processing example
  • one example module per file under demo/examples, so new demos can be added without growing demo/main.ts

Structure

  • src/materials: WGSL material and uniform packing
  • src/renderer: WebGPU renderer core, extraction, pass execution, and shader header generation
  • src/rendergraph: frame graph primitives
  • src/compute: reusable compute pipeline wrapper
  • demo/examples: one file per selectable example, plus shared demo scene helpers
  • demo/ui: small reusable demo UI helpers like the profiling HUD
  • demo: Vite demo app shell using the library exports directly

Next steps

Natural follow-ups from here:

  • add a three bridge layer explicitly named and documented as the migration boundary
  • compact visible draws into fewer GPU-driven submission streams and reduce CPU draw iteration further
  • add cascaded directional shadows, light arrays, and shadow atlases
  • add material templates and pipeline caches for larger shader families
  • separate extraction, render graph nodes, and resource registries into dedicated modules