This document provides an overview and usage examples for the tinygpu rendering library.
The Renderer class is the core of the rendering system. It handles WebGPU initialization, canvas setup, pipeline creation, and rendering scenes.
To use the renderer, you first need to create an instance and initialize it.
import { Renderer } from "./src/renderer"; // Adjust path as necessary
// Optional: Provide an existing canvas element
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
const renderer = new Renderer({ canvas });
async function main() {
try {
await renderer.init(); // Initializes WebGPU device and adapter
// If a canvas was not provided in the constructor, or you want to change it:
// renderer.initCanvas(newCanvasElement);
// ... rest of your setup and render loop
} catch (error) {
console.error("Failed to initialize renderer:", error);
// Handle initialization error (e.g., WebGPU not supported)
}
}
main();Key Steps:
new Renderer(options?):options.canvas(optional): An HTMLCanvasElement to render to. If not provided, it can be set later withinitCanvas.
await renderer.init():- Asynchronously requests a GPU adapter and device.
- Throws an error if WebGPU is not supported or an adapter/device cannot be obtained.
- If a canvas was provided in the constructor,
initCanvasis called internally.
renderer.initCanvas(canvasElement)(if canvas not set in constructor or needs changing):- Sets up the WebGPU context on the provided canvas.
- Configures the canvas format.
- Sets up a
ResizeObserverto handle canvas resizing automatically, adjusting the internalcanvasSizeand recreating the depth texture.
- Device and Adapter Management: Holds the
GPUDeviceandGPUAdapter. - Canvas Management: Manages the
HTMLCanvasElement,GPUCanvasContext, and preferredGPUTextureFormat. Handles resizing. - Depth Buffering: Creates and manages a
GPUTexturefor depth testing (depthTexture,depthTextureView). - Pipeline Caching: Implements a cache (
_pipelineCache) forGPURenderPipelineobjects to avoid redundant pipeline creation. The cache key is derived frommesh.cacheKey. - Resource Creation: Provides factory methods for creating common rendering objects.
This is the main rendering method. It should be called in your application's render loop.
// Assuming 'scene' is an instance of Scene and 'camera' is an instance of Camera
function animate() {
// Update scene objects, camera, etc.
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
// Start the render loop after initialization
animate();Steps within render:
- Gets the current texture from the canvas context.
- Sets up a
GPURenderPassDescriptorwith color and depth-stencil attachments. - Begins a render pass.
- Sets viewport and scissor rectangle.
- If canvas size has changed (
sizeDirty), updates the camera's aspect ratio viacamera.viewportResized(this.canvasSize). - Updates the scene and its objects:
scene.update(camera, this.canvasSize). - Sets the scene-level bind group.
- Traverses the scene graph (
scene.traverse):- For each
Meshobject:- Updates the mesh (
mesh.update()). - Gets or creates a
GPURenderPipelineusingpipelineFor(scene, mesh). - Sets the pipeline.
- Sets vertex buffers (position, UVs), index buffer.
- Sets mesh-specific and material-specific bind groups.
- Calls
drawIndexedto render the mesh.
- Updates the mesh (
- For each
- Ends the render pass.
- Submits the command buffer to the device queue.
Retrieves an existing GPURenderPipeline from the cache or creates a new one if not found.
- Cache Key:
mesh.cacheKey(likely a combination of geometry and material identifiers). - Pipeline Creation:
- Creates a
GPURenderPipelineLayoutusing bind group layouts from thescene,mesh, andmesh.material. - Uses
mesh.material.shaderCode(WGSL shader module) andmesh.geometry.bufferLayoutfor vertex and fragment stages. - Configures primitive topology (triangle-list), culling (back-face), and depth-stencil state.
- Creates a
A utility method to create and map a GPUBuffer with the given data and usage flags (e.g., GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST).
const vertices = new Float32Array([...]);
const vertexBuffer = renderer.createBuffer(vertices, GPUBufferUsage.VERTEX);The Renderer provides convenient factory methods to create core components, ensuring they are created with the correct GPUDevice.
-
createMaterial<T extends Material, O>(constructor: new (device: GPUDevice, o?: O) => T, options?: O): TCreates an instance of aMaterialsubclass.import { BasicMaterial } from "./src/materials/basic-material"; const material = renderer.createMaterial(BasicMaterial, { color: [1, 0, 0, 1], });
-
createGeometry<T extends Geometry>(constructor: new (renderer: Renderer) => T): TCreates an instance of aGeometrysubclass.import { CubeGeometry } from "./src/geometry/cube"; // Assuming CubeGeometry exists const cubeGeo = renderer.createGeometry(CubeGeometry);
-
createMesh(geometry: Geometry, material: Material): MeshCreates aMeshobject from aGeometryand aMaterial.const mesh = renderer.createMesh(cubeGeo, material);
-
createScene(): SceneCreates aSceneobject.const scene = renderer.createScene(); scene.add(mesh); // Add meshes and other objects to the scene
-
createPerspectiveCamera(options?: PerspectiveCameraProps): PerspectiveCameraCreates aPerspectiveCamera.const camera = renderer.createPerspectiveCamera({ fov: 45, near: 0.1, far: 100, }); camera.position.set([0, 1, 5]);
-
createOrthographicCamera(options?: OrthographicCameraProps): OrthographicCameraCreates anOrthographicCamera.const orthoCamera = renderer.createOrthographicCamera({ left: -2, right: 2, top: 2, bottom: -2, near: 0.1, far: 100, });
import { Renderer } from "./src/renderer";
import { CubeGeometry } from "./src/geometry/cube"; // Example
import { BasicMaterial } from "./src/materials/basic-material"; // Example
async function setup() {
const canvas = document.getElementById("webgpu-canvas") as HTMLCanvasElement;
const renderer = new Renderer({ canvas });
await renderer.init();
// 1. Create Scene
const scene = renderer.createScene();
// 2. Create Camera
const camera = renderer.createPerspectiveCamera({
fov: 75,
near: 0.1,
far: 1000,
});
camera.position.set([2, 2, 3]);
camera.lookAt([0, 0, 0]);
// 3. Create Geometry
const cubeGeometry = renderer.createGeometry(CubeGeometry);
// 4. Create Material
const redMaterial = renderer.createMaterial(BasicMaterial, {
color: [1, 0, 0, 1],
});
// 5. Create Mesh
const cubeMesh = renderer.createMesh(cubeGeometry, redMaterial);
scene.add(cubeMesh);
// Render loop
function renderLoop() {
cubeMesh.transform.rotateY(0.01); // Example animation
renderer.render(scene, camera);
requestAnimationFrame(renderLoop);
}
renderLoop();
}
setup().catch(console.error);This provides a basic structure for using the Renderer. Subsequent sections will detail other components like Scene, Camera, Mesh, Material, and Geometry.
The Scene class acts as a container for all objects you want to render. It extends Transform, meaning a scene itself can be positioned, rotated, and scaled, affecting all its children. It also manages scene-level uniforms like camera matrices, resolution, and time.
A Scene is created using the Renderer's factory method:
const renderer = new Renderer({ canvas });
await renderer.init();
const scene = renderer.createScene();When a Scene is constructed, it initializes a UniformManager with the following default uniforms:
projection matrix:mat4.create()view matrix:mat4.create()camera position:vec3.create()resolution:vec2.create(1, 1)time:performance.now() / 1000(current time in seconds)
- Object Container: Holds
Meshobjects and otherTransformnodes in a hierarchical structure. - Scene-Level Uniforms: Manages and updates uniforms that are common to all objects in the scene. This is handled by an internal
UniformManager.projectionMatrix: From the active camera.viewMatrix: From the active camera.cameraPosition: World-space position of the camera.resolution: Dimensions of the rendering canvas.time: Elapsed time, useful for animations.
- Transform Hierarchy: Inherits from
Transform, allowing the entire scene to be transformed.
This method is called by the Renderer during the render cycle. It updates the scene-level uniforms based on the current camera and canvas resolution.
- Updates
projection matrix,view matrix, andcamera positionfrom the providedcamera. - Updates
resolutionwith the canvas dimensions. - Updates
timewith the currentperformance.now() / 1000. - Calls
this._uniformManager.update()to write these values to the GPU buffer.
Inherited from Transform. Adds a child object (like a Mesh or another Transform group) to the scene.
const mesh = renderer.createMesh(geometry, material);
scene.add(mesh);
const group = new Transform(); // Or a custom class extending Transform
group.add(anotherMesh);
scene.add(group);Inherited from Transform. Removes a child object from the scene.
Inherited from Transform. Executes a callback function for the scene itself and all its descendants. The Renderer uses this to iterate through renderable objects.
Returns the GPUBindGroupLayout managed by the scene's UniformManager. This layout defines the structure of the scene-level uniforms for the shader.
Returns the GPUBindGroup managed by the scene's UniformManager. This bind group contains the actual GPU buffer for scene-level uniforms and is bound to slot 0 in the shaders.
// ... (renderer, camera, mesh setup from previous examples)
// Add mesh to scene
scene.add(cubeMesh);
// In the render loop, renderer.render(scene, camera) will automatically call:
// scene.update(camera, renderer.canvasSize);
// This keeps scene uniforms synchronized.The Scene object simplifies managing global shader parameters and the collection of objects to be rendered.
The Camera class is an abstract base class for all camera types in tinygpu. It extends Transform, allowing cameras to be positioned and oriented within the scene like any other object. Its primary role is to provide the projectionMatrix and viewMatrix necessary for rendering.
- Inheritance:
Camera extends Transform, so it hasposition,rotation,scale, and can be part of the scene graph hierarchy. - Matrices:
_projectionMatrix: Mat4: Defines how 3D points are mapped to 2D screen coordinates (e.g., perspective or orthographic projection)._viewMatrix: Mat4: Defines the camera's position and orientation in world space (effectively the inverse of the camera's world transform).
- Lazy Calculation: Both matrices are calculated on-demand using dirty flags (
_isProjectionDirty,_isViewDirty). This avoids redundant calculations if camera properties haven't changed. - Abstract Methods: Concrete camera types (like
PerspectiveCameraorOrthographicCamera) must implement:updateProjectionMatrix(): Logic to recalculate_projectionMatrix.updateViewMatrix(): Logic to recalculate_viewMatrix.viewportResized(size: Vec2): A hook for cameras to react to changes in the rendering canvas size (e.g., updating aspect ratio).
projectionMatrix: Mat4(getter): Returns the current projection matrix. If_isProjectionDirtyis true, it callsupdateProjectionMatrix()before returning.viewMatrix: Mat4(getter): Returns the current view matrix. If_isViewDirtyis true, it callsupdateViewMatrix()before returning.updateMatrices(): A public method to force an update of both matrices if they are dirty.position,rotation,scale: Inherited fromTransform. Modifying these (e.g.,camera.position.set([...])) will typically require the view matrix to be recalculated. Concrete camera implementations usually mark_isViewDirty = truewhen these transform properties change or when specific methods likelookAtare called.
A camera that uses perspective projection, making objects appear smaller as they move further away, simulating how human vision works.
Created using the Renderer's factory method:
const camera = renderer.createPerspectiveCamera({
fov: Math.PI / 4, // Field of View in radians (e.g., 45 degrees)
aspect: canvas.width / canvas.height, // Aspect ratio of the viewport
near: 0.1, // Near clipping plane
far: 1000, // Far clipping plane
position: vec3.fromValues(0, 5, 10), // Initial camera position
target: vec3.fromValues(0, 0, 0), // Point the camera looks at
up: vec3.fromValues(0, 1, 0), // Up direction for the camera
});Or with defaults:
const camera = renderer.createPerspectiveCamera();
camera.position.set([0, 2, 5]); // Set position using Transform properties
camera.lookAt([0, 0, 0]); // Point camera towards originfov: number: Vertical field of view in radians.aspect: number: Aspect ratio (width / height).near: number: Distance to the near clipping plane.far: number: Distance to the far clipping plane.target: Vec3: The world-space point the camera is looking at.up: Vec3: The world-space vector defining the "up" direction for the camera.
updateProjectionMatrix(): Recalculates_projectionMatrixusingmat4.perspective(this.fov, this.aspect, this.near, this.far).updateViewMatrix(): Recalculates_viewMatrixusingmat4.lookAt(this.position, this.target, this.up). Thepositionproperty is inherited fromTransform.
The PerspectiveCamera provides setter methods for its properties (e.g., setFov(), setTarget(), setPosition()). These methods update the corresponding property and set the appropriate dirty flag (_isProjectionDirty or _isViewDirty).
camera.setFov(Math.PI / 3); // Change FOV to 60 degrees
camera.setTarget([1, 1, 1]); // Look at a new point
// camera.position is part of Transform, can be set directly:
// camera.position.x += 1; camera._isViewDirty = true; (manual dirty or use setPosition)When the canvas (viewport) resizes, this method is called by the Renderer. For PerspectiveCamera, it updates the aspect ratio: this.setAspect(size[0] / size[1]).
A camera that uses orthographic projection, where objects appear the same size regardless of their distance from the camera. Useful for 2D rendering or specific 3D effects.
Created using the Renderer's factory method:
const orthoCamera = renderer.createOrthographicCamera({
left: -canvas.width / 2,
right: canvas.width / 2,
top: canvas.height / 2,
bottom: -canvas.height / 2,
near: 0.1,
far: 100,
position: vec3.fromValues(0, 0, 10), // Initial camera position
target: vec3.fromValues(0, 0, 0), // Point the camera looks at
up: vec3.fromValues(0, 1, 0), // Up direction for the camera
});Or with defaults:
const orthoCamera = renderer.createOrthographicCamera();
// Default frustum: left/bottom = -1, right/top = 1
orthoCamera.position.set([0, 0, 5]);
orthoCamera.lookAt([0, 0, 0]);left: number: Left boundary of the viewing frustum.right: number: Right boundary of the viewing frustum.top: number: Top boundary of the viewing frustum.bottom: number: Bottom boundary of the viewing frustum.near: number: Distance to the near clipping plane.far: number: Distance to the far clipping plane.target: Vec3: The world-space point the camera is looking at.up: Vec3: The world-space vector defining the "up" direction for the camera.
updateProjectionMatrix(): Recalculates_projectionMatrixusingmat4.ortho(this.left, this.right, this.bottom, this.top, this.near, this.far).updateViewMatrix(): Recalculates_viewMatrixusingmat4.lookAt(this.position, this.target, this.up).
Similar to PerspectiveCamera, OrthographicCamera has setters (e.g., setLeft(), setTarget(), setPosition()) that update properties and mark matrices as dirty.
orthoCamera.setLeft(-10);
orthoCamera.setRight(10);
// orthoCamera.position is part of TransformThe current implementation for OrthographicCamera is a no-op (nop). If you need the orthographic frustum to adapt to canvas size changes (e.g., to maintain a 1:1 pixel mapping), you would need to manually call its setter methods within a resize handler in your application code or extend/modify the camera.
// Example: Manual adjustment on resize
function onWindowResize() {
const newWidth = window.innerWidth;
const newHeight = window.innerHeight;
// renderer.canvas might need to be updated first
// renderer.canvas.width = newWidth;
// renderer.canvas.height = newHeight;
// renderer.canvasSize.set([newWidth, newHeight]); // If renderer handles this
// renderer.sizeDirty = true;
orthoCamera.setLeft(-newWidth / 2);
orthoCamera.setRight(newWidth / 2);
orthoCamera.setTop(newHeight / 2);
orthoCamera.setBottom(-newHeight / 2);
// No need to call camera.viewportResized if it's a nop
}
// window.addEventListener('resize', onWindowResize);Cameras are fundamental for defining what part of the 3D world is visible and how it's projected onto your 2D screen.
A Mesh is a fundamental renderable object in tinygpu. It combines Geometry (defining its shape and vertex data) with a Material (defining its appearance and shader). Like other visual objects, Mesh extends Transform, so it can be positioned, rotated, and scaled within the scene.
Meshes are created using the Renderer's factory method:
// Assuming 'renderer', 'cubeGeometry', and 'redMaterial' are already created
const cubeMesh = renderer.createMesh(cubeGeometry, redMaterial);
// Add to scene to make it renderable
scene.add(cubeMesh);When a Mesh is constructed:
- It stores references to the
GPUDevice,Material, andGeometry. - It initializes its own
UniformManagerspecifically for mesh-level uniforms.- The primary uniform managed here is the
modelmatrix (the mesh'sworldMatrixinherited fromTransform).
- The primary uniform managed here is the
- Combining Shape and Appearance: Links a specific
Geometryinstance with aMaterialinstance. - Transformable Object: Inherits from
Transform, allowing it to have its own local transformations (position, rotation, scale) and be part of the scene graph. Its final world transformation is available asthis.worldMatrix. - Mesh-Specific Uniforms: Manages its
modelmatrix (world transform) via an internalUniformManager. This matrix transforms the mesh's vertices from model space to world space. - Cache Key Generation: Provides a
cacheKeygetter used by theRendererto cacheGPURenderPipelineobjects. This key is typically a combination of the geometry's and material's cache keys.
material: Material: The material used to render the mesh.geometry: Geometry: The geometry defining the mesh's shape.cacheKey: string(getter): Returns a string key unique to the combination of this mesh's geometry and material. Used byRendererfor pipeline caching (${this.geometry.cacheKey}-${this.material.cacheKey}).update(): Called by theRendererduring the render cycle for each visible mesh.- Calls
this.material.update()to allow the material to update its own uniforms or state. - Updates its internal
UniformManagerwith the currentthis.worldMatrixfor the "model" uniform. - Calls
this._uniformManager.update()to write the model matrix to its GPU buffer.
- Calls
bindGroupLayout: GPUBindGroupLayout(getter): Returns theGPUBindGroupLayoutfrom its internalUniformManager. This layout defines the structure for mesh-specific uniforms (primarily the model matrix) for the shader.bindGroup: GPUBindGroup(getter): Returns theGPUBindGroupfrom its internalUniformManager. This bind group contains the GPU buffer for the model matrix and is typically bound to slot 1 in shaders.
// Create geometry and material
const myGeometry = renderer.createGeometry(MyCustomGeometry);
const myMaterial = renderer.createMaterial(MyCustomMaterial, {
/* ...material options... */
});
// Create mesh
const myMesh = renderer.createMesh(myGeometry, myMaterial);
// Position and rotate the mesh
myMesh.position.set([1, 2, 3]);
myMesh.rotation.x = Math.PI / 4;
myMesh.updateWorldMatrix(); // Important if not using a scene graph that does this automatically
// Add to scene
scene.add(myMesh);
// In the render loop, the Renderer will:
// 1. Traverse the scene.
// 2. For each Mesh found (like myMesh):
// a. Call myMesh.update() -> updates material and model matrix uniform.
// b. Get pipeline using myMesh.cacheKey.
// c. Set pipeline.
// d. Set scene, mesh, and material bind groups.
// e. Set vertex/index buffers from myMesh.geometry.
// f. Draw the mesh.Meshes are the primary building blocks for constructing visible elements in your 3D scene.
The Material class is an abstract base class that defines the appearance of a Mesh. It specifies the shader code to be used and manages any uniforms (like colors, textures, or other parameters) that the shader requires.
- Abstract Class: You don't instantiate
Materialdirectly. Instead, you use or create concrete subclasses likeBasicMaterial,UVMaterial, orShaderMaterial. - Shader Link: A material is primarily responsible for providing a
GPUShaderModule(shaderCodegetter) to the rendering pipeline. - Uniform Management: Materials can have their own set of uniforms (e.g., base color, texture maps). These are managed by an internal
UniformManagerinstance, if the material has uniforms. - Cache Key: Each material type (and sometimes its configuration) should provide a unique
cacheKeystring. This key is used by theRendererin conjunction with aGeometry's cache key to cacheGPURenderPipelineobjects, optimizing performance by avoiding redundant pipeline creation.
_uniformManager: UniformManager: An optional protected member. If a material has its own uniforms or textures, it will initialize and use aUniformManager.cacheKey: string(abstract getter): Concrete materials must implement this to return a unique string identifier for their type and configuration.shaderCode: GPUShaderModule(abstract getter): Concrete materials must implement this to return the compiledGPUShaderModulethey use.bindGroupLayout: GPUBindGroupLayout | undefined(getter): Returns theGPUBindGroupLayoutfrom its_uniformManager, if one exists. This defines the structure of material-specific uniforms for the shader.bindGroup: GPUBindGroup | undefined(getter): Returns theGPUBindGroupfrom its_uniformManager, if one exists. This contains the GPU buffers/textures for material-specific uniforms and is typically bound to slot 2 in shaders.update(): Called by theMesh'supdatemethod. It, in turn, callsthis._uniformManager?.update()to ensure any dynamic material uniforms are updated on the GPU.
A simple material that can render a solid color and optionally apply a texture.
- Initialization:
const material = renderer.createMaterial(BasicMaterial, { color: new Color(1, 0, 0), // Optional: Red color, defaults to white map: myTexture, // Optional: A Texture instance });
- Uniforms:
color: vec4f(defaults to white[1,1,1,1])map: texture_2d<f32>,mapSampler: sampler(if a texture is provided)
- Shader: Uses
basic-material.wgsl. This shader typically multiplies the sampled texture color (or white if no texture) by the uniform color. - Cache Key:
"basic-material" - Precompilation:
BasicMaterial.precompile(device)is called during construction to compile its WGSL shader into aGPUShaderModule(staticBasicMaterial.shaderModule). This is done once perGPUDevice.
A diagnostic material that visualizes the UV coordinates of a mesh.
- Initialization:
const uvMaterial = renderer.createMaterial(UVMaterial);
- Uniforms: None specific to the material itself (it directly uses UVs from geometry).
- Shader: Uses
uv-material.wgsl. This shader typically outputs the UV coordinates as colors (e.g., U for red channel, V for green channel). - Cache Key:
"UVMaterial" - Precompilation: Similar to
BasicMaterial,UVMaterial.precompile(device)compiles its shader once.
A flexible material that allows you to provide custom WGSL shader code directly.
-
Initialization:
const customCode = ` @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { return vec4f(in.uv, 0.0, 1.0); // Example: visualize UVs } `; const customUniforms = [{ name: "intensity", value: 0.8 }]; // Example uniform const customTextures = [myNoiseTexture]; // Example texture const shaderMaterial = renderer.createMaterial(ShaderMaterial, { code: customCode, uniforms: customUniforms, // Optional textures: customTextures, // Optional });
-
Shader Code: The
codeoption provides the fragment shader (and optionally vertex shader parts if not using the default header). The provided code is combined with a commonshaderHeader.wgsl(which includes standard bindings for scene, mesh uniforms, and vertex inputs). -
Uniforms & Textures: You can define an array of
UniformObjandTextureinstances that your custom shader will use. These are managed by theShaderMaterial'sUniformManager. -
Compilation: The shader code is compiled into a
GPUShaderModulewhen theShaderMaterialis constructed. -
Cache Key:
btoa(this._options.code)(Base64 encoding of the shader code string, ensuring uniqueness for different custom shaders).
Materials are crucial for defining how your 3D objects look, from simple colors to complex, custom-shaded surfaces.
The Geometry class is an abstract base class that defines the shape of a Mesh. It's responsible for holding vertex data (like positions, UV coordinates, normals) in GPU buffers and describing how this data is structured for the rendering pipeline.
- Abstract Class: You don't instantiate
Geometrydirectly. You use or create concrete subclasses likeCubeGeometryorBigTriangle. - GPU Buffers:
_vertexBuffer: GPUBuffer: Stores vertex attribute data (e.g., positions, normals)._indexBuffer: GPUBuffer: Stores indices that define how vertices are connected to form triangles._uvBuffer: GPUBuffer: Often stores UV coordinates. In some geometry implementations, UVs might be interleaved in the_vertexBuffer. TheBigTriangleexample shows a separate UV buffer.
- Vertex Data Description:
_indexCount: number: The number of indices in the_indexBuffer._vertexCount: number: The number of vertices.bufferLayout: GPUVertexBufferLayout[](abstract getter): This is crucial. It tells the GPU how to interpret the data in the vertex buffer(s). It defines attributes (like position, uv, normal), their shader locations, offsets, formats, and the stride of the vertex data.
- Cache Key: Provides a
cacheKey(abstract getter) used by theRenderer(along with the material's cache key) for pipeline caching.
_renderer: Renderer: A reference to theRendererinstance, used to access theGPUDevicefor buffer creation.device: GPUDevice(getter): Returns theGPUDevicefrom the renderer.vertexBuffer: GPUBuffer(getter): Returns the main vertex buffer.indexBuffer: GPUBuffer(getter): Returns the index buffer.uvBuffer: GPUBuffer(getter): Returns the UV buffer.indexCount: number(getter): Returns the number of indices.vertexCount: number(getter): Returns the number of vertices.cacheKey: string(abstract getter): Concrete geometry classes must provide a unique string identifier.bufferLayout: GPUVertexBufferLayout[](abstract getter): Concrete geometry classes must define the layout of their vertex data.
Defines a standard 1x1x1 cube centered at the origin.
- Initialization:
const cubeGeo = renderer.createGeometry(CubeGeometry);
- Vertex Data:
- Generates 24 unique vertices (4 for each of the 6 faces) because each vertex on a face needs unique UVs and potentially normals if smooth shading isn't desired across hard edges.
- Vertex attributes are interleaved in a single
vertexBuffer:[position (vec3f), uv (vec2f), normal (vec3f)]. floatsPerVertex = 8,arrayStride = 8 * 4 = 32 bytes.indexCount = 36(6 faces _ 2 triangles/face _ 3 indices/triangle).
- Buffers:
_vertexBuffer: Contains the interleaved position, UV, and normal data._indexBuffer: Contains the indices for the 36 triangles._uvBuffer: InCubeGeometry's current implementation, it seems to assign_vertexBufferto_uvBufferin thesupercall. This implies UVs are read from the main interleaved buffer, which is correct given its layout.
cacheKey:"CubeGeometry"bufferLayout: Defines a singleGPUVertexBufferLayoutfor the interleaved buffer:@location(0):position(float32x3)@location(1):uv(float32x2), offset after position.@location(2):normal(float32x3), offset after position and UVs.
A special-purpose geometry often used for full-screen shader effects. It's a single large triangle that can cover the entire viewport.
- Initialization:
const bigTriangleGeo = renderer.createGeometry(BigTriangle);
- Vertex Data:
- Defines 3 vertices that span a large area (e.g., from
(-1, -1)to(3, 3)in clip space if used directly, or can be adjusted by model/view/projection matrices). - Positions:
[-1, -1, 0], [3, -1, 0], [-1, 3, 0] - UVs:
[0, 0], [2, 0], [0, 2](these UVs go outside the 0-1 range, useful for specific texture addressing modes or procedural generation in shaders). indexCount = 3.
- Defines 3 vertices that span a large area (e.g., from
- Buffers:
_vertexBuffer: Contains only vertex positions (float32x3)._indexBuffer: Contains indices[0, 1, 2]._uvBuffer: A separate buffer containing UV coordinates (float32x2).
cacheKey:"big-triangle"bufferLayout: Defines twoGPUVertexBufferLayoutentries:- For
_vertexBuffer(positions at@location(0)). - For
_uvBuffer(UVs at@location(1)). This tells the pipeline to fetch positions from one buffer (bound to slot 0 by default for vertex attributes) and UVs from another buffer (which would be bound to slot 1 for vertex attributes). TheRenderer'srendermethod currently bindsmesh.geometry.vertexBufferto slot 0 andmesh.geometry.uvBufferto slot 1.
- For
Geometry classes encapsulate the data and structure of 3D shapes, making them ready for the GPU to render.
The Transform class is a fundamental building block for creating hierarchical structures (scene graphs) in tinygpu. Objects like Scene, Camera, and Mesh all extend Transform, allowing them to be positioned, rotated, and scaled in 3D space, and to have parent-child relationships.
- Local vs. World Space:
- Local Transform: Defined by
_position,_rotation(as aQuat), and_scale. These are relative to theTransform's parent. - Local Matrix (
_localMatrix): AMat4calculated from the local position, rotation, and scale. - World Matrix (
_worldMatrix): AMat4representing theTransform's final position, rotation, and scale in world space. It's calculated by multiplying the parent's world matrix by thisTransform's local matrix.
- Local Transform: Defined by
- Hierarchy:
_children: Transform[]: An array of childTransformobjects._parent?: Transform: A reference to the parentTransform.
- Dirty Flags:
_localDirty: boolean: True if local position, rotation, or scale has changed, requiring_localMatrixto be recalculated._worldDirty: boolean: True if_localMatrixhas changed or if any ancestor's_worldMatrixhas changed, requiring thisTransform's_worldMatrixto be recalculated.
- Lazy Updates: Matrices (
_localMatrix,_worldMatrix) are only recalculated when accessed (via their getters) and if they are marked as dirty. This avoids unnecessary computations.
position: Vec3(getter/setter): Gets or sets the local position. Setting it marks the transform as dirty.quaternion: Quat(getter/setter): Gets or sets the local rotation as a quaternion. Setting it marks the transform as dirty.scale: Vec3(getter/setter): Gets or sets the local scale. Setting it marks the transform as dirty.setRotation(x: number, y: number, z: number, order: RotationOrder = "xyz"): Sets the local rotation using Euler angles (converted to a quaternion). Marks the transform as dirty.
localMatrix: Mat4(getter): Returns the local transformation matrix. If_localDirtyis true, callsupdateLocalMatrix()first.worldMatrix: Mat4(getter): Returns the world transformation matrix. If_localDirtyor_worldDirtyis true, callsupdateWorldMatrix()first. This getter ensures that the entire chain of parent world matrices is up-to-date before calculating thisTransform's world matrix.
updateLocalMatrix(): Recalculates_localMatrixfrom_position,_rotation, and_scaleusing acomposefunction (similar tomat4.fromRotationTranslationScalebut directly composing into a matrix). Sets_localDirty = falseand_worldDirty = true.updateWorldMatrix():- Ensures
_localMatrixis up-to-date by callingupdateLocalMatrix()if_localDirty. - If a
_parentexists, multiplies_parent.worldMatrixbythis.localMatrixto get_worldMatrix. - If no
_parent,_worldMatrixis a copy of_localMatrix. - Sets
_worldDirty = false. - Crucially, marks all
_childrenas world-dirty, as their world matrices depend on this one.
- Ensures
makeDirty(): Sets_localDirty = trueand_worldDirty = true. Propagates world-dirtiness to children. Called when local transform components change.makeWorldDirty(): Sets_worldDirty = trueand propagates world-dirtiness to children. Called by parent when its world matrix changes, or when parent-child relationships change.
children: Transform[](getter): Returns the array of child transforms.add(child: Transform): Adds aTransformas a child.- Removes the child from its previous parent, if any.
- Sets
child._parent = this. - Marks the
childas world-dirty.
remove(child: Transform): Removes aTransformfrom its children.- Sets
child._parent = undefined. - Marks the
childas world-dirty (it's now relative to the scene root or needs re-parenting).
- Sets
clear(): Removes all children.traverse(fn: (transform: Transform) => void): Executes a callback function for thisTransformand recursively for all its descendants. This is used bySceneto iterate over all objects.
const scene = new Scene(device); // Scene is a Transform
const parentObj = new Transform();
parentObj.position.set([1, 0, 0]);
scene.add(parentObj);
const childObj = new Transform();
childObj.position.set([0, 1, 0]); // Local position relative to parentObj
parentObj.add(childObj);
// To get childObj's world position:
// childObj.worldMatrix will trigger updates if needed.
// The 4th column of childObj.worldMatrix (elements 12, 13, 14) is its world position.
// Expected world position of childObj: [1, 1, 0]
// If parentObj moves:
parentObj.position.x = 5; // This calls makeDirty() on parentObj
// Now, when childObj.worldMatrix is next accessed (e.g., by the renderer),
// it will correctly reflect the new parent position.
// Expected new world position of childObj: [5, 1, 0]
// A Mesh is also a Transform
const mesh = renderer.createMesh(geometry, material);
mesh.scale.set([0.5, 0.5, 0.5]); // Scale the mesh locally
childObj.add(mesh); // Mesh is now a child of childObj
// The renderer will use mesh.worldMatrix to render it.
// This matrix will be: scene.worldMatrix * parentObj.worldMatrix * childObj.worldMatrix * mesh.localMatrix
// (Assuming scene's worldMatrix is identity if it's the root)The Transform class provides the core mechanism for organizing objects in a 3D scene, handling complex spatial relationships and ensuring transformations are efficiently updated.
The UniformManager is a utility class responsible for managing a collection of uniforms (data like matrices, vectors, scalars) and textures that need to be passed to shaders. It handles the creation of GPUBuffers for uniform data, GPUSamplers and GPUTextureViews for textures, and the corresponding GPUBindGroupLayout and GPUBindGroup objects.
This class is used internally by Scene, Mesh, and Material to manage their respective shader inputs.
- Uniforms (
_uniforms: UniformObj[]): An array ofUniformObj(defined inuniform-utils.ts, typically{ name: string, value: Mat4 | Vec3 | Vec2 | number | number[] }).- These are packed into a single
ArrayBuffer(_uniformArr) and then uploaded to aGPUBuffer(_uniformBuffer).
- These are packed into a single
- Textures (
_textures: Texture[]): An array ofTextureinstances. Each texture has its ownGPUTextureandGPUTextureView. - Binding Structure:
- If uniforms exist, they are bound as a single
GPUBufferatbinding: 0. - If textures exist:
- A single
GPUSampler(linear filtering by default) is bound at the next available binding slot (e.g.,binding: 1if uniforms exist, orbinding: 0otherwise). - Each
GPUTextureViewis bound sequentially at subsequent binding slots.
- A single
- If uniforms exist, they are bound as a single
- Dirty Flags (
_uniformDirty,_texturesDirty): Track whether uniform data or textures need to be re-uploaded or reconfigured. - Lazy Initialization:
GPUBindGroupLayoutandGPUBindGroupare created on first access.
// Typically instantiated by Scene, Mesh, or Material constructors
const uniformManager = new UniformManager(
device, // GPUDevice
[
// Optional: Array of UniformObj
{ name: "modelMatrix", value: mat4.identity() },
{ name: "color", value: vec4.fromValues(1, 0, 0, 1) },
],
[
// Optional: Array of Texture instances
myDiffuseTexture,
myNormalTexture,
],
"MyObjectUniforms" // Optional: Label for debugging
);updateUniform(uniform: UniformObj): Updates the value of an existing uniform by name. Marks uniforms as dirty.uniformManager.updateUniform({ name: "modelMatrix", value: newModelMatrix });
updateTextures(textures?: Texture[]): Replaces the current set of textures. Marks textures as dirty.update(): This is the main update method.- If
_uniformDirty, it repacks_uniformsinto_uniformArr(usingpackUniforms) and re-uploads it to_uniformBuffer(usinguploadUniformBuffer). - If
_texturesDirty, it callstexture.upload(device)for each texture (this typically creates/updates theGPUTextureon the device if the texture's source data has changed). - Resets dirty flags.
- If
setUniformsDirty(),setTexturesDirty(),setDirty(): Manually mark parts or all of the managed resources as needing an update.sampler: GPUSampler(getter): Returns a sharedGPUSampler(linear filtering, created on first access).bindGroupLayoutDescriptor: GPUBindGroupLayoutDescriptor(getter): Dynamically generates the descriptor for the bind group layout based on the current set of uniforms and textures.bindGroupLayout: GPUBindGroupLayout(getter): Returns theGPUBindGroupLayout, creating it from the descriptor if it doesn't exist.bindGroupDescriptor: GPUBindGroupDescriptor(getter): Dynamically generates the descriptor for the bind group, referencing the_uniformBuffer,sampler, and texture views.bindGroup: GPUBindGroup(getter): Returns theGPUBindGroup, creating it from the descriptor if it doesn't exist.
When a UniformManager instance is used (e.g., by a Scene, Mesh, or Material), its bindGroup is bound to a specific group index in the shader pipeline. For example:
- Scene uniforms (projection, view matrices, time, etc.):
@group(0) - Mesh uniforms (model matrix):
@group(1) - Material uniforms (color, material-specific textures):
@group(2)
Within each group managed by a UniformManager:
- Uniform Buffer: If present, always at
@binding(0).// In shader, for a group using UniformManager @group(X) @binding(0) var<uniform> myUniforms: MyUniformStruct;
- Sampler: If textures are present, the sampler is at the next binding (e.g.,
@binding(1)if uniforms are also present, or@binding(0)if only textures).@group(X) @binding(Y) var mySampler: sampler;
- Textures: Sequentially after the sampler.
@group(X) @binding(Y+1) var texture1: texture_2d<f32>; @group(X) @binding(Y+2) var texture2: texture_2d<f32>; // ... and so on
The UniformManager abstracts away much of the boilerplate involved in setting up and updating shader resources, making it easier to define and use uniforms and textures across different parts of the rendering engine.
The Texture class is an abstract base class for different types of textures that can be used in materials. It defines a common interface for texture properties and operations like uploading to the GPU.
- Abstract Class: You don't instantiate
Texturedirectly. Use concrete subclasses likeDefaultTextureorImageTexture. - GPU Resource: Represents a
GPUTextureand itsGPUTextureViewon the device. - Data Source: Concrete implementations handle loading or generating pixel data.
- Descriptor: Each texture type provides a
GPUTextureDescriptorthat defines its size, format, usage flags, etc.
descriptor: GPUTextureDescriptor(getter): Returns the descriptor for creating the GPU texture.view: GPUTextureView(getter): Returns the view for the GPU texture, used for binding to shaders.upload(device: GPUDevice): void: Uploads the texture data to the GPU, creating theGPUTextureif it doesn't exist.dispose(): void: Releases GPU resources (destroys theGPUTexture).width: number(getter): Returns the width of the texture.height: number(getter): Returns the height of the texture.
A singleton texture representing a 1x1 white pixel. This is often used as a fallback or default texture in materials if no specific map is provided.
- Singleton: Accessed via
DefaultTexture.instance. - Data: A single white pixel
[255, 255, 255, 255]. - Size: 1x1.
- Format:
rgba8unorm. - Usage:
GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST. upload(device: GPUDevice): Creates a 1x1GPUTextureand writes the white pixel data to it usingdevice.queue.writeTexture. This happens only once.view: Returns aGPUTextureViewof the 1x1 white texture.
A texture loaded from an image URL.
- Initialization:
const myImageTexture = new ImageTexture("path/to/myimage.png"); // Must be loaded before it can be uploaded to GPU await myImageTexture.load();
load(): Promise<void>:- Fetches the image from the
srcURL. - Decodes it into an
ImageBitmap. - Stores the
ImageBitmapand itswidthandheight.
- Fetches the image from the
- Data: The
ImageBitmapobtained from loading the image. - Format:
rgba8unorm. - Usage:
GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT. TheRENDER_ATTACHMENTflag suggests it could potentially be used as a render target, though this might not be its primary use case if just used as a material map. upload(device: GPUDevice):- Requires
_imagedata(theImageBitmap) to be loaded first. - Creates a
GPUTexturebased on the image's dimensions and format. - Copies the
ImageBitmapdata to theGPUTextureusingdevice.queue.copyExternalImageToTexture. This method handles the conversion fromImageBitmapto the GPU texture format. It also includes aflipY: trueoption, which is common for web image loading as WebGL/WebGPU texture coordinates often have (0,0) at the bottom-left, while images have (0,0) at the top-left.
- Requires
dispose(): Closes theImageBitmapand destroys theGPUTexture.
Textures are essential for adding visual detail and realism to 3D scenes. The Texture classes in tinygpu provide a way to manage these resources.
This file contains helper functions and types primarily used by UniformManager for handling uniform data according to the WebGPU std140 memory layout rules.
export type UniformObj = { name: string; value: number | Float32Array | Color };A simple type definition for representing a named uniform value. The value can be a JavaScript number (for f32), a Float32Array (for vectors and matrices), or a Color instance (which likely provides a uniformValue() method returning a Float32Array).
packUniforms(items: UniformObj[], targetBuffer?: ArrayBuffer, targetOffset: number = 0): ArrayBuffer
This is the core function for packing JavaScript uniform data into an ArrayBuffer following std140 layout rules.
- Purpose: Takes an array of
UniformObjand arranges their data into a singleArrayBuffer, respecting the alignment and padding requirements of std140. This packed buffer can then be uploaded to aGPUBuffer. - std140 Layout: WebGPU (and other graphics APIs) require uniform data in buffers to follow specific memory layout rules (std140) to ensure compatibility across different hardware. Key rules include:
- Scalars (
f32) have an alignment and size of 4 bytes. vec2has an alignment and size of 8 bytes.vec3has an alignment of 16 bytes (meaning it often occupies 16 bytes even though its data is only 12 bytes).vec4,mat4columns, andmat3columns have an alignment of 16 bytes.- Arrays and structures have specific alignment rules based on their members.
- Scalars (
- How it Works:
- First Pass (Layout Calculation):
- Iterates through the input
items. - Determines the WGSL type (
f32,vec2,vec3,vec4,mat3,mat4,color) based on thevaluetype and size (usinggetDataTypehelper). - Looks up the std140 alignment, size, and padding requirements for that type from the
STD140_LAYOUT_INFOconstant. - Calculates the necessary padding before the current item to meet its alignment requirement based on the
currentOffset. - Stores the calculated offset (relative to the start of packing), size, type, and original data for each item.
- Advances
currentOffsetby the type's required size including padding (advanceAmount). - Keeps track of the
maxAlignmentneeded by any item.
- Iterates through the input
- Total Size Calculation: Calculates the
totalSizeNeededfor the packed data, ensuring it's a multiple of themaxAlignment. - Buffer Preparation:
- If a
targetBufferis provided, checks if it's large enough. - If no
targetBufferis provided or it's too small (though the current code throws an error if too small, it doesn't resize), it creates a newArrayBufferof thetotalSizeNeeded.
- If a
- Second Pass (Data Writing):
- Iterates through the stored layout information from the first pass.
- Calculates the absolute
writeOffsetin the target buffer (baseWriteOffset + relativeOffset). - Uses
DataView(forf32) orFloat32Arrayviews (for vectors/matrices/colors) to write thevaluefrom eachUniformObjinto thebufferToWriteat the correct calculatedwriteOffset. - Special handling is needed for
vec3(writes 3 floats into a 16-byte aligned slot) andmat3(writes columns individually respecting the 16-byte stride per column).
- First Pass (Layout Calculation):
- Return Value: Returns the
ArrayBuffercontaining the packed data (either the buffer passed in or the newly created one).
uploadUniformBuffer(packedUniforms: ArrayBuffer, device: GPUDevice, label: string = "Uniform Buffer", buffer?: GPUBuffer): GPUBuffer
A utility function to upload the packed ArrayBuffer data to a GPUBuffer.
- Purpose: Simplifies the process of creating or updating a
GPUBufferused for uniforms. - How it Works:
- If an existing
bufferis provided, it uses that. - If no
bufferis provided, it creates a newGPUBufferwith the size of thepackedUniformsdata and usage flagsGPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM. - Uses
device.queue.writeBufferto copy the data from thepackedUniformsArrayBufferinto theGPUBuffer.
- If an existing
- Return Value: Returns the
GPUBuffer(either the one passed in or the newly created one).
These utilities are essential for bridging the gap between JavaScript data structures and the memory layout required by the GPU for uniform buffers.
A simple class to represent an RGBA color.
const red = new Color(1, 0, 0); // Alpha defaults to 1
const semiTransparentBlue = new Color(0, 0, 1, 0.5);r: number: Red component (typically 0-1).g: number: Green component (typically 0-1).b: number: Blue component (typically 0-1).a: number: Alpha component (0-1).
uniformValue(): Vec4: Returns the color components as aVec4(which is aFloat32Arrayof length 4). It reuses an internal buffer (this.buffer) for efficiency. This is the format expected byUniformManagerwhen packing color uniforms (which are treated asvec4fin WGSL).
const myColor = new Color(0.2, 0.4, 0.6);
const colorUniformData = myColor.uniformValue(); // Returns Float32Array([0.2, 0.4, 0.6, 1.0])
// Used internally by UniformManager/packUniforms
const uniformObj = { name: "myColorUniform", value: myColor };
// packUniforms will call myColor.uniformValue() when processing this object.This file exports a simple object containing constants, likely intended for defining standard bind group indices used throughout the shaders and the engine.
export const Constants = {
BG_SCENE: 0, // Bind Group index for Scene-level uniforms
BG_MATERIAL: 1, // Bind Group index for Material-level uniforms/textures
BG_OBJECT: 2, // Bind Group index for Object/Mesh-level uniforms (e.g., model matrix)
};BG_SCENE = 0: Suggests that bind group 0 (@group(0)in WGSL) is reserved for uniforms managed by theScene(e.g., projection matrix, view matrix, time).BG_MATERIAL = 1: Suggests that bind group 1 (@group(1)in WGSL) is reserved for uniforms and textures managed by theMaterial.BG_OBJECT = 2: Suggests that bind group 2 (@group(2)in WGSL) is reserved for uniforms managed by theMesh(primarily the model matrix).
Note: There seems to be a discrepancy between these constants and the binding group indices mentioned in the UniformManager documentation section (which suggested Scene=0, Mesh=1, Material=2). The actual usage in Renderer.ts (passEncoder.setBindGroup(0, sceneBindGroup), passEncoder.setBindGroup(1, mesh.bindGroup), passEncoder.setBindGroup(2, mesh.material.bindGroup)) confirms the order: Scene=0, Mesh=1, Material=2. The constants file might be outdated or intended for a different binding scheme. For clarity, the documentation sections have been updated to reflect the Renderer.ts usage (0=Scene, 1=Mesh, 2=Material). It's recommended to update constants.ts to match the actual implementation:
// Recommended update for constants.ts to match Renderer.ts usage:
export const Constants = {
BG_SCENE: 0,
BG_OBJECT: 1, // Object/Mesh uniforms (model matrix)
BG_MATERIAL: 2, // Material uniforms/textures
};