Scarlet Industries

BunKit Metal

3D scenes and a typed GPU API for BunKit apps.

Scene3D is a scene graph of shapes, a camera and a light. Under it, GPUView is a Metal surface where you write the passes yourself, and gpu() is the device that hands out buffers, shaders and pipelines. All three come from bunkit/metal.

A 3D scene

A Scene3D is a view, so it goes into a stack next to labels and buttons:

const scene = new Scene3D({
  grow: 1,
  camera: { position: [4, 2.6, 5], target: [0, 0.6, 0], fov: 50 },
  light: { direction: [0.5, 0.85, 0.35], ambient: "#5d6478" },
});

scene.add(plane({ size: 40, color: "#20202a" }));
const cube = scene.add(box({ size: 1.1, position: [0, 0.55, 0], color: "#aa091b" }));

scene.onFrame(({ dt }) => {
  cube.rotation.y += dt;
});

The shapes are box, sphere, plane, cylinder and cone, and mesh(geometry(...)) takes vertices of your own. Nodes that share a geometry and a material are drawn together in one instanced draw call:

for (let i = 0; i < 200; i++) {
  scene.add(box({ size: 0.2, position: [i % 20, 0, i / 20] }));
}

scene.batchCount;   // 1

Drawing a frame

A GPUView calls onFrame once per display refresh, and you record render passes into the frame it passes you:

const view = new GPUView({ grow: 1, sampleCount: 4 });

view.onFrame((frame) => {
  frame.render(
    {
      color: { texture: frame.colorTexture, clear: [0, 0, 0, 1], resolve: frame.resolveTexture },
      depth: frame.depthTexture,
    },
    (pass) => {
      pass.pipeline(pipeline).bind({ globals, instances });
      pass.drawIndexed(indices, count, { instances: 5000 });
    },
  );
});

bind matches inputs by the names the shader uses rather than by slot number, and a misspelt name is an error that lists the real ones. At most 2 frames are on the GPU at once, and when it falls behind the view skips a tick rather than block the run loop.

What else there is

bun run particles simulates 250,000 particles with a kernel, and bun run playground is a live shader editor. The API follows WebGPU closely, so porting from it is mostly renaming.