{ "cells": [ { "cell_type": "markdown", "id": "a1b2c3d4e5f6", "metadata": {}, "source": [ "# Writing Custom Renderers\n", "\n", "When the built-in renderers don't fit your data, you can subclass `Renderer` to create your own.\n", "A custom renderer needs three things:\n", "\n", "- **`get_shader_code()`** — return a WGSL shader string\n", "- **`get_bounding_box()`** — return the spatial extent for camera auto-fit\n", "- **`n_vertices`** — set the number of vertices in the draw call" ] }, { "cell_type": "markdown", "id": "b2c3d4e5f6a7", "metadata": {}, "source": [ "## Minimal custom renderer\n", "\n", "The simplest renderer: a hardcoded color triangle. The shader uses `#import camera` to get\n", "the `cameraMapPoint` function, which transforms world-space positions into clip space." ] }, { "cell_type": "code", "execution_count": null, "id": "c3d4e5f6a7b8", "metadata": {}, "outputs": [], "source": [ "from webgpu.renderer import Renderer\n", "from webgpu.jupyter import Draw\n", "\n", "shader_code = \"\"\"\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.0),\n", " vec3f(-0.5, -0.5, 0.0),\n", " vec3f( 0.5, -0.5, 0.0)\n", " );\n", " var col = array(\n", " vec4f(1.0, 0.2, 0.2, 1.0),\n", " vec4f(0.2, 1.0, 0.2, 1.0),\n", " vec4f(0.2, 0.2, 1.0, 1.0)\n", " );\n", " return FragmentInput(cameraMapPoint(pos[vi]), col[vi]);\n", "}\n", "\n", "@fragment\n", "fn fragment_main(input: FragmentInput) -> @location(0) vec4f {\n", " return input.color;\n", "}\n", "\"\"\"\n", "\n", "class ColorTriangle(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\n", "\n", "Draw(ColorTriangle())" ] }, { "cell_type": "markdown", "id": "d4e5f6a7b8c9", "metadata": {}, "source": [ "## Data via buffer bindings\n", "\n", "For data-driven renderers, pass GPU buffers using `BufferBinding`. Use `buffer_from_array`\n", "to upload NumPy arrays to the GPU, then declare matching `var` bindings in your shader\n", "with `@group(0) @binding(N)`. Binding numbers must be >= 100 (below 100 is reserved for the framework).\n", "\n", "This example renders a colored quad (two triangles, 6 vertices) from storage buffers:" ] }, { "cell_type": "code", "execution_count": null, "id": "e5f6a7b8c9d0", "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 = \"\"\"\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[4u*vi], colors[4u*vi+1u], colors[4u*vi+2u], colors[4u*vi+3u]);\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", "class QuadRenderer(Renderer):\n", " def __init__(self):\n", " super().__init__()\n", " self.n_vertices = 6\n", "\n", " def update(self, options):\n", " self.verts = buffer_from_array(np.array([\n", " -0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0,\n", " -0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0,\n", " ], dtype=np.float32))\n", " self.cols = buffer_from_array(np.array([\n", " 1,0,0,1, 0,1,0,1, 0,0,1,1,\n", " 1,0,0,1, 0,0,1,1, 1,1,0,1,\n", " ], dtype=np.float32))\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\n", "\n", " def get_bindings(self):\n", " return [BufferBinding(101, self.verts), BufferBinding(102, self.cols)]\n", "\n", "Draw(QuadRenderer())" ] }, { "cell_type": "markdown", "id": "f6a7b8c9d0e1", "metadata": {}, "source": [ "## Key methods reference\n", "\n", "| Method / Attribute | Description |\n", "|---|---|\n", "| `get_shader_code()` | Return a WGSL source string. Use `#import camera` for 3D camera support, `#import lighting` for lighting. |\n", "| `get_bindings()` | Return a list of `BufferBinding`, `UniformBinding`, etc. Binding numbers must be >= 100 (below 100 is reserved for the framework). |\n", "| `get_bounding_box()` | Return `((min_x, min_y, min_z), (max_x, max_y, max_z))` for camera auto-fit. |\n", "| `update(options)` | Called when data changes. Create or update GPU buffers here. |\n", "| `n_vertices` | Number of vertices per draw call. |\n", "| `n_instances` | Number of instances per draw call. |\n", "| `set_needs_update()` | Mark the renderer for re-upload on the next frame. |" ] }, { "cell_type": "markdown", "id": "a7b8c9d0e1f2", "metadata": {}, "source": [ "## Combining with built-in renderers\n", "\n", "Custom renderers can be used alongside built-in renderers in the same scene. Just pass a list of renderers to `Draw`:" ] }, { "cell_type": "code", "execution_count": null, "id": "b8c9d0e1f2a3", "metadata": {}, "outputs": [], "source": [ "from webgpu.shapes import generate_cylinder, ShapeRenderer\n", "\n", "quad = QuadRenderer()\n", "cylinder = ShapeRenderer(\n", " generate_cylinder(n=16, radius=0.05, height=0.5),\n", " positions=[[0, 0, 0.1]],\n", " directions=[[0, 0, 1]],\n", " colors=[[1, 0.5, 0, 1]],\n", ")\n", "Draw([quad, cylinder])" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11.0" } }, "nbformat": 4, "nbformat_minor": 5 }