{ "cells": [ { "cell_type": "markdown", "id": "b1a2c3d4e5f60001", "metadata": {}, "source": [ "# WebGPU Rendering Basics" ] }, { "cell_type": "markdown", "id": "b1a2c3d4e5f60002", "metadata": {}, "source": [ "The `webgpu` package is a thin Python layer over the WebGPU API. You write shaders in WGSL\n", "(WebGPU Shading Language), manage GPU buffers, and build render pipelines — all from Python.\n", "The `Renderer` base class handles device/canvas boilerplate; you focus on shaders and data.\n", "\n", "This package gives you full access to WebGPU. You write WGSL shaders directly and control\n", "GPU buffers from Python. Everything you can do with WebGPU, you can do through this layer.\n", "\n", "**References**\n", "\n", "- WGSL spec: \n", "- WGSL tour: \n", "- WebGPU spec: " ] }, { "cell_type": "markdown", "id": "b1a2c3d4e5f60003", "metadata": {}, "source": [ "## The render pipeline\n", "\n", "A WebGPU render pipeline has two programmable stages:\n", "\n", "- **Vertex shader** — runs once per vertex, outputs a clip-space position (`vec4f`)\n", "- **Fragment shader** — runs once per pixel covered by a primitive, outputs a color\n", "\n", "A `Renderer` subclass wraps these into a pipeline by returning WGSL source from\n", "`get_shader_code()`. The framework compiles the shader, creates the pipeline, and\n", "issues draw calls based on `n_vertices` and `n_instances`." ] }, { "cell_type": "markdown", "id": "b1a2c3d4e5f60004", "metadata": {}, "source": [ "## Hello World — hardcoded triangle\n", "\n", "The simplest custom `Renderer`. Vertices and colors are hardcoded in the WGSL shader.\n", "No camera — positions are directly in clip space." ] }, { "cell_type": "code", "execution_count": null, "id": "b1a2c3d4e5f60005", "metadata": {}, "outputs": [], "source": [ "from webgpu.renderer import Renderer\n", "from webgpu.jupyter import Draw\n", "\n", "shader_code = \"\"\"\n", "struct FragmentInput {\n", " @builtin(position) p: vec4,\n", " @location(0) color: vec4,\n", "};\n", "\n", "@vertex\n", "fn vertex_main(@builtin(vertex_index) vi: u32) -> FragmentInput {\n", " var pos = array(\n", " vec4f( 0.0, 0.5, 0., 1.),\n", " vec4f(-0.5, -0.5, 0., 1.),\n", " vec4f( 0.5, -0.5, 0., 1.)\n", " );\n", " var color = array(\n", " vec4f(1., 0., 0., 1.),\n", " vec4f(0., 1., 0., 1.),\n", " vec4f(0., 0., 1., 1.)\n", " );\n", " return FragmentInput(pos[vi], color[vi]);\n", "}\n", "\n", "@fragment\n", "fn fragment_main(input: FragmentInput) -> @location(0) vec4f {\n", " return input.color;\n", "}\n", "\"\"\"\n", "\n", "\n", "class Triangle(Renderer):\n", " def __init__(self):\n", " super().__init__()\n", " self.n_vertices = 3\n", "\n", " def get_bounding_box(self):\n", " return ((0, 0, 0), (1, 1, 1))\n", "\n", " def get_shader_code(self):\n", " return shader_code\n", "\n", "\n", "Draw(Triangle())" ] }, { "cell_type": "markdown", "id": "b1a2c3d4e5f60006", "metadata": {}, "source": [ "## Camera integration\n", "\n", "The triangle above doesn't rotate when you drag the canvas because its positions are\n", "in clip space. To get 3D camera support:\n", "\n", "1. Add `#import camera` at the top of the shader.\n", "2. Use `vec3f` positions and pass them through `cameraMapPoint()`.\n", "\n", "This imports the framework's camera uniform buffer into your shader. Try dragging\n", "the canvas below — the triangle now rotates." ] }, { "cell_type": "code", "execution_count": null, "id": "b1a2c3d4e5f60007", "metadata": {}, "outputs": [], "source": [ "from webgpu.renderer import Renderer\n", "from webgpu.jupyter import Draw\n", "\n", "shader_code_camera = \"\"\"\n", "#import camera\n", "\n", "struct FragmentInput {\n", " @builtin(position) p: vec4,\n", " @location(0) color: vec4,\n", "};\n", "\n", "@vertex\n", "fn vertex_main(@builtin(vertex_index) vi: u32) -> FragmentInput {\n", " var pos = array(\n", " vec3f( 0.0, 0.5, 0.),\n", " vec3f(-0.5, -0.5, 0.),\n", " vec3f( 0.5, -0.5, 0.)\n", " );\n", " var color = array(\n", " vec4f(1., 0., 0., 1.),\n", " vec4f(0., 1., 0., 1.),\n", " vec4f(0., 0., 1., 1.)\n", " );\n", " return FragmentInput(cameraMapPoint(pos[vi]), color[vi]);\n", "}\n", "\n", "@fragment\n", "fn fragment_main(input: FragmentInput) -> @location(0) vec4f {\n", " return input.color;\n", "}\n", "\"\"\"\n", "\n", "\n", "class TriangleCamera(Renderer):\n", " def __init__(self):\n", " super().__init__()\n", " self.n_vertices = 3\n", "\n", " def get_bounding_box(self):\n", " return ((-0.5, -0.5, 0), (0.5, 0.5, 0))\n", "\n", " def get_shader_code(self):\n", " return shader_code_camera\n", "\n", "\n", "Draw(TriangleCamera())" ] }, { "cell_type": "markdown", "id": "b1a2c3d4e5f60008", "metadata": {}, "source": [ "## GPU buffers — sending data from Python\n", "\n", "Real renderers need dynamic data. The pattern:\n", "\n", "1. Declare `@group(0) @binding(N) var` in WGSL (use N ≥ 101 to avoid\n", " collisions with internal bindings).\n", "2. In `update()`, call `buffer_from_array()` to upload NumPy arrays.\n", "3. In `get_bindings()`, return `BufferBinding(N, buffer)` for each buffer." ] }, { "cell_type": "code", "execution_count": null, "id": "b1a2c3d4e5f60009", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from webgpu.renderer import Renderer\n", "from webgpu.utils import BufferBinding, buffer_from_array\n", "from webgpu.jupyter import Draw\n", "\n", "shader_code_buffers = \"\"\"\n", "#import camera\n", "\n", "@group(0) @binding(101) var vertices: array;\n", "@group(0) @binding(102) var colors: array;\n", "\n", "struct FragmentInput {\n", " @builtin(position) p: vec4,\n", " @location(0) color: vec4,\n", "};\n", "\n", "@vertex\n", "fn vertex_main(@builtin(vertex_index) vi: u32) -> FragmentInput {\n", " let pos = vec3f(vertices[3u*vi], vertices[3u*vi+1u], vertices[3u*vi+2u]);\n", " let col = vec4f(colors[3u*vi], colors[3u*vi+1u], colors[3u*vi+2u], 1.0);\n", " return FragmentInput(cameraMapPoint(pos), col);\n", "}\n", "\n", "@fragment\n", "fn fragment_main(input: FragmentInput) -> @location(0) vec4f {\n", " return input.color;\n", "}\n", "\"\"\"\n", "\n", "\n", "class TriangleBuffers(Renderer):\n", " def __init__(self, points, colors):\n", " super().__init__()\n", " self.n_vertices = 3\n", " self.points = np.array(points, dtype=np.float32).reshape(-1, 3)\n", " self.colors = np.array(colors, dtype=np.float32).reshape(-1, 3)\n", "\n", " def update(self, options):\n", " self.gpu_points = buffer_from_array(self.points)\n", " self.gpu_colors = buffer_from_array(self.colors)\n", "\n", " def get_bounding_box(self):\n", " return np.min(self.points, axis=0), np.max(self.points, axis=0)\n", "\n", " def get_shader_code(self):\n", " return shader_code_buffers\n", "\n", " def get_bindings(self):\n", " return [BufferBinding(101, self.gpu_points), BufferBinding(102, self.gpu_colors)]\n", "\n", "\n", "points = [[0, 0.5, 0], [-0.5, -0.5, 0], [0.5, -0.5, 0]]\n", "colors = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]\n", "Draw(TriangleBuffers(points, colors))" ] }, { "cell_type": "markdown", "id": "b1a2c3d4e5f60013", "metadata": {}, "source": [ "## Summary\n", "\n", "The `Renderer` API:\n", "\n", "| Method / Attribute | Purpose |\n", "|---|---|\n", "| `get_shader_code()` | Return WGSL source (use `#import camera`, `#import lighting`) |\n", "| `get_bindings()` | Return list of `BufferBinding` / `UniformBinding` (binding ≥ 101) |\n", "| `get_bounding_box()` | Return `((min_x, min_y, min_z), (max_x, max_y, max_z))` |\n", "| `update(options)` | Create / update GPU buffers before each draw |\n", "| `n_vertices` | Number of vertices in the draw call |\n", "| `n_instances` | Number of instances in the draw call |\n", "\n", "The subsequent notebooks show the convenience renderers built on top of this layer\n", "for scientific visualization." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.13.11" } }, "nbformat": 4, "nbformat_minor": 5 }