Skip to content
oframePublic

About

Minimal WebGPU Library

Resources

Stars

163 stars

Watchers

6 watching

Forks

Repository files navigation

OGPU

Minimal WebGPU framework.

Examples


OGPU continues the ethos of OGL for WebGPU: a thin, approachable layer that keeps the metal within reach. Part of the OFrame family, after the minimal-abstraction philosophy Nathan Gordon set with OGL.

Vanilla ES modules, Vite, a THREE-like API with far fewer features. Native WebGPU calls sit comfortably beside it, which also makes it a practical way to learn WebGPU.

Shaders are WGSL, always. No node graph, no TSL: you write WGSL, the framework reflects it. That's the contract.

A starter, not a package

Not on npm, on purpose: there's no stable API to import against. Treat it like a sourdough starter. Clone it, then grow it into your own engine (an LLM can do much of the reshaping).

git clone https://github.com/oframe/ogpu my-app
cd my-app
npm install
npm run dev        # http://localhost:5173

Built for coding agents

You grow it rather than import it, so it's laid out for agents (Claude Code, Codex, Cursor, Gemini CLI) to navigate:

  • AGENTS.md is the source of truth: architecture, cross-cutting model, conventions. CLAUDE.md, GEMINI.md and the Cursor rule are one-line bridges to it.

  • Per-directory CLAUDE.md files hold each area's footguns, next to the code.

  • api-digest.md (every public signature) and module-graph.json (the import graph, hubs ranked) are generated by npm run repomap; a pre-commit hook blocks stale copies.

  • Offline WGSL validation with naga: a PostToolUse hook checks each shader an agent writes and returns the compile error in the same turn; npm run validate:shaders checks the whole tree. Install naga-cli (brew install naga-cli or cargo install naga-cli); the script exits 2 without it.

    Homebrew's naga (no -cli) is an unrelated Snake game. In Claude Code, the naga-setup skill installs the hook, script and binaries; run it on a fresh clone, since the binary doesn't come with the repo.

Structure

Core (src/core/): a drawable is a Mesh = geometry + Shader + a state object + bind groups. The renderer derives and caches the pipeline; you never build one for a mesh.

  • Shader wraps a WGSL module with vs/fs, appends the engine block (engine.wgsl) and reflects it (webgpu-utils). Bind by WGSL name: shader.bindGroupLayout(0).createBindGroup({ uniforms }).
  • The engine owns group(1) and group(3). Shaders read camera.* (projection, view, position, viewport), object.* (model, normal matrix) and time.* (time, delta). Your resources go in groups 0 and 2.
  • Mesh holds no GPU objects. state (transparent, depthWrite, cullMode, blending, …) is frozen: swap the object to change it. One mesh renders into the canvas, an MSAA target or a shadow map with no per-target setup.
  • ComputeShader: every entry point of a compute module is a kernel, keyed by name.
// validate-prepend: ../../src/core/engine.wgsl
struct Uniforms {
  color: vec3f,
}

@group(0) @binding(0) var<uniform> uniforms: Uniforms;

struct Vertex {
  @location(0) position: vec3f,
  @location(1) normal: vec3f,
}

struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) vNormal: vec3f,
}

@vertex
fn vs(in: Vertex) -> VertexOutput {
  var out: VertexOutput;
  out.position = camera.projectionMatrix * (camera.viewMatrix * (object.modelMatrix * vec4f(in.position, 1.0)));
  out.vNormal = object.normalMatrix * in.normal;
  return out;
}

@fragment
fn fs(in: VertexOutput) -> @location(0) vec4f {
  return vec4f(uniforms.color * (0.5 + 0.5 * normalize(in.vNormal).y), 1.0);
}
const shader = new Shader(gpu, { label: 'box-shader', code });
const uniforms = new UniformBuffer(gpu, shader.uniforms, { color: [1, 0.4, 0.2] }, { label: 'box-uniforms' });

const mesh = new Mesh({
    label: 'box',
    geometry: new Box(gpu),
    shader,
    state: { transparent: true, depthWrite: false },
    bindGroups: [shader.bindGroupLayout(0).createBindGroup({ uniforms })],
});
mesh.setParent(scene);

uniforms.set({ color: [0.2, 0.6, 1] }); // uploads now

Engine and uniform values are one per submit: a mesh drawn in two passes of one submit sees its last transform and the last set() in both. Give each view its own UniformBuffer when they must differ. examples/hellowebgpu/ is the canonical sample; the details are in AGENTS.md and src/core/CLAUDE.md.

The rest of Core:

  • Renderer — device, frame loop, render queue. The canvas is the display's pixel grid; dpr caps it, or render into a smaller target and resample.
  • Transform, Camera
  • Geometry — vertex/index buffers
  • UniformBuffer — a reflected struct + its GPU buffer
  • PipelineRegistry — the cached Mesh pipelines (renderer.pipelines); renderer.compile() pre-warms them
  • RenderBundleGroup — replays a static subtree from a render bundle (examples/renderbundle)
  • PerDrawBuffer — the engine block's buffer
  • RenderPipeline — fullscreen passes and other non-Mesh draws
  • Texture, RenderTarget
  • GPUEnums — named constants for WebGPU's string enums, so a typo throws
  • ShaderReload — WGSL hot reload
  • reflect/, primitives/, skin/

Math (src/math/): a function layer (vec3, quat, mat4, …; wgpu-matrix plus local overrides) under chainable THREE-style classes (Vec3, Quat, Mat4, Euler, Color, …), each a Float32Array.

Modules (src/modules/), optional: Orbit, Raycast, GUI, Animation, GLTFLoader, CubeMap, VideoTexture, KTXTexture, gsplat/ (Gaussian splats), pbr/ (IBL shaders), text/ (MSDF text).

Utils (src/utils/): BufferUtils, RenderUtils (blit), mipmap, textureFromSource, IBLUtils, TimingHelper, JSONLoader, wgslOverrides (bakes override constants for Safari), and more.

Examples (examples/): the root is a gallery; ?example=<name> deep-links one, ?src=<name> loads it alone.

Fullscreen passes

Post-processing goes through blit (@utils/RenderUtils) and a RenderPipeline, over gpu.TRIANGLE (default) or gpu.QUAD (exact corner interpolation):

blit(encoder, { pipeline, geometry: gpu.TRIANGLE, targetView, bindGroup, clear: true });

The pipeline has no engine block: bind group(0) from pipeline.bindGroupLayout(0). examples/feedback ping-pongs two accumulation textures.

Multiple canvases

One device, one loop, any number of canvases. renderer.setContext(canvas) points the next render() at another canvas:

for (const view of views) {
    renderer.setContext(view.canvas);
    renderer.render({ scene, camera: view.camera });
}
renderer.render({ scene, camera }); // back on the default canvas

Meshes and scenes are shared. Extra canvases aren't observed: size them yourself and give each its own camera. To move only a final blit, target renderer.contextFor(canvas).getCurrentTexture().createView(). examples/multicanvas draws eight canvases in one submit.

PBR & IBL

src/modules/pbr/pbr.wgsl is a glTF metallic-roughness shader lit by IBL, ready for a Shader + Mesh. Maps follow t<Name> (tMap, tNormal, …). Inputs, built once (examples/pbrshader, examples/gltf):

  • Specular: loadIBLCubeMap(gpu, { url, faceSize, mipLevels }) → a GGX-prefiltered cube; pass its mipLevels as the roughnessLevels override.
  • Diffuse: loadSphericalHarmonics(url) → 9 SH coefficients.
  • BRDF LUT: brdflut.wgsl into a 512² rgba16float texture.

It tonemaps and gamma-encodes at the end; drop that when rendering into an intermediate target. In Claude Code, the pbr-shading skill wires it up.

Browser requirements

WebGPU ships by default in (status, October 2026):

  • Chrome / Edge: Windows, macOS, ChromeOS 113+; Android 121+ on most Android 12+ GPUs; Linux 144+ (Intel Gen12+) and 147+ (NVIDIA under Wayland). Before 135 there's no Float16Array: use f32 struct fields and 32float formats there.
  • Firefox: Windows 141+; Apple Silicon Macs 145+ on macOS 26, 147+ on older macOS. Elsewhere Nightly or a flag.
  • Safari: macOS / iOS / iPadOS / visionOS 26.

Optional features are feature-detected: anything the adapter lacks is dropped and logged, so the engine boots on any WebGPU adapter. Guard code that needs one.

Phones: navigator.gpu exists only in secure contexts, so http://192.168.x.x:5173 reports no WebGPU. Tunnel over HTTPS instead:

cloudflared tunnel --url http://localhost:5173 --http-host-header localhost:5173

Unlicense

Free and unencumbered software released into the public domain. See https://unlicense.org.

About

Minimal WebGPU Library

Resources

Stars

163 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages