Skip to content

ogpu 0.18.0

Choose a tag to compare

@DougLilliequist DougLilliequist released this 16 Oct 18:21
· 9 commits to main since this release

The first public release since 0.8.0 on main (the last GitHub Release was 0.5.0). Ten
releases' worth of work land at once: meshes are now geometry + WGSL shader + a plain state
object, and the renderer derives every render pipeline. Every Mesh shader can read a small
engine uniform block (camera, object, time). Bind groups are built by WGSL name, static
scenery can replay from render bundles, and there's an MSDF text module with SplitText-style
reveals.

Still not an npm package: fork it, grow it, make it yours. The full per-release detail is in
CHANGELOG.md (0.9.0 → 0.18.0).

Highlights

  • Shader + state meshes, derived pipelines. new Mesh({ geometry, shader, state, bindGroups })
    takes no gpu and builds no pipeline. The renderer's PipelineRegistry builds one per
    (shader, state, vertex layout, pass formats + sample count) on first draw and hands every
    mesh that asks the same object, so one mesh draws into the canvas, an MSAA target or a
    depth-only shadow map with no per-target setup. renderer.compile({ scene, target })
    pre-warms them.
  • An engine block every Mesh shader can read. camera.* (projection, view, position,
    quaternion, viewport), object.* (model, normal, object matrices) at group(1) and
    time.time / time.delta at group(3). It lives in one renderer-owned buffer: a fixed slot
    per mesh, rewritten only when the mesh moves, and a slot per render() for the camera.
    Fullscreen RenderPipelines and ComputeShaders don't get it.
  • Bind groups by WGSL name. shader.bindGroupLayout(0).createBindGroup({ uniforms, tMap }).
    BindGroupLayout is interned per device: shaders or kernels with the same bindings share
    one, and a hot reload that keeps the shape keeps your bind groups. UniformBuffer backs any
    uniform struct.
  • Render bundles: RenderBundleGroup. Parent a large static set under it, call
    record(), and render() replays the subtree from a GPURenderBundle. Children can still
    move. 10 000 static meshes: render() CPU about 0.8 ms recorded vs 6.3 ms live (M1 Pro,
    headless Chromium).
  • MSDF text (MSDFFont, MSDFText, TEXT_PRESETS): instanced glyph quads on an MTSDF
    atlas, SplitText-style reveals (chars/words/lines, masks, wipes, 12 easing curves)
    evaluated on the GPU, outlines, a wireframe view. Bundled font: Fira Sans Bold, as in
    OGL's msdf-text example.
  • Faster draws. Redundant pipeline/bind-group/buffer sets are skipped, group 3 and the
    empty group 2 are bound once per pass, and a static mesh costs no upload at all.
  • Per-node scissor (node.scissor = { x, y, width, height }); hot reload now rebuilds a
    Shader in place and the registry rebuilds its pipelines, with bind groups surviving when
    the binding shape doesn't change; a Float16Array shim for Chrome/Edge 113–134; and
    exact pins of webgpu-utils 2.1.1 (behind @utils/webgpu-utils) and wgpu-matrix
    3.4.2
    (behind @math/functions).
  • Compressed KTX textures now load: mip levels that aren't a whole number of compression
    blocks upload as whole blocks, as WebGPU requires.

New and updated examples

  • ?src=renderbundle: 6400 static meshes replayed from a bundle vs drawn live, with re-record,
    a state swap and a render() timer.
  • ?src=msdftext: OGL's "don't panic", black on white, with every reveal preset, outlines and
    a wireframe toggle.
  • ?src=highmeshcount&mode=recorded: up to 50 000 meshes, live or recorded.

Upgrading from 0.8.0

Every row maps 0.8.0 code straight to 0.18.0; the CHANGELOG has the stops in between.

0.8.0 0.18.0
new RenderPipeline(gpu, { code, constants, vertexBuffers, targets, sampleCount, cullMode, depthStencil, … }) + new Mesh(gpu, { pipeline, geometry, bindGroups }) const shader = new Shader(gpu, { code, constants }); new Mesh({ geometry, shader, state: { cullMode, depthWrite, depthCompare, depthBias, … }, bindGroups }). Drop targets, sampleCount, vertexBuffers and depth formats: they come from the pass. depthStencil becomes the flat state fields depthWrite / depthTest / depthCompare / depthBias / depthBiasSlopeScale / depthBiasClamp
projectionMatrix, viewMatrix, modelMatrix, modelViewMatrix, normalMatrix, objectMatrix, cameraPosition, cameraQuaternion, resolution, time in your Uniforms struct delete them; read camera.*, object.*, time.*. Add // validate-prepend: <path>/src/core/engine.wgsl
modelViewMatrix * p camera.viewMatrix * (object.modelMatrix * p) (or modelViewMatrix())
resolution camera.viewport
time time.time; the frame delta is time.delta
your own @group(1) or @group(3) bindings move them to group 2 (or 0); 1 and 3 are the engine's
a module-scope camera, object, time, Camera, Object, Time or modelViewMatrix of your own rename it: they collide with the engine block
mesh.uniforms.set(…), bindGroups: (uniformResource) => […] const uniforms = new UniformBuffer(gpu, shader.uniforms, values); bindGroups: [shader.bindGroupLayout(0).createBindGroup({ uniforms, … })]; uniforms.set(…)
gui.uniform(mesh, …) gui.uniform(uniforms, …)
device.createBindGroup({ layout: pipeline.bindGroupLayout(i), entries }) Mesh: shader.bindGroupLayout(i).createBindGroup({ name: resource }); fullscreen pass: pipeline.bindGroupLayout(i).createBindGroup({ … }). .layout is the raw GPUBindGroupLayout
pass.setBindGroup(0, bindGroup, [0]) on a RenderPipeline pass.setBindGroup(0, bindGroup): no dynamic offset any more (hasDynamicUniform is gone)
computeShader.bindGroupLayout(kernel) computeShader.bindGroupLayout('kernelName', group): both arguments required; it returns a BindGroupLayout, so use .createBindGroup({ … }) (or .layout for the raw one)
createUniformBuffer(…) new UniformBuffer(…) for a struct, createBuffer(gpu, { usage: UNIFORM | COPY_DST, data }) for raw bytes (createUniformBuffer still works but warns)
import { mat4, vec3 } from 'wgpu-matrix' from 'ogpu' or @math/functions. Only the functions ogpu uses are re-exported (mat4.create, vec3.sub, … may be missing): add what you need to src/math/functions/
import … from 'webgpu-utils' makeStructuredView from 'ogpu'; everything else from @utils/webgpu-utils (never the package directly)
generateMipmap(device, tex) generateMipmap(gpu, tex) from @utils/mipmap.js
a pass drawing a mesh with another shader (shadow casters) a second Mesh sharing the geometry, or swap mesh.shader and mesh.bindGroups together
one mesh, different uniform values per pass in one submit one UniformBuffer + bind group per pass, swapped via mesh.bindGroups
pbr.wgsl with a uniforms block at group(0) binding 0; loader.pipelines pbr.wgsl's group(0) starts at binding 1 (don't pass uniforms); loader.shader. GLTFLoader's targets / sampleCount options are ignored
a Mesh subclass overriding draw({ camera, pass, time, resolution }), reading mesh.modelViewMatrix / uniformResource / structSize draw({ pass, passInfo, renderer, camera, cameraOffset, bound }); those fields are gone. mesh.normalMatrix / objectMatrix can be stale on the CPU: call mesh.updateDerived() first
after a device loss rebuild your Shaders and their bind groups in renderer.addDeviceRestoredHandler(gpu => …)

Browsers: Chrome/Edge 113+, plus Firefox and Safari where they ship WebGPU (see the
README). Optional features (timestamp queries, texture compression families,
float32-filterable) are feature-detected.