SwissGL / WebGPU / WASM

Tiny and beautiful programs living on the Web

Alexander Mordvintsev, Google, 2024

znah.net/alife24

The "joy" of GPU programming
WebGL2 state diagram
swissgl.js (28Kb)

							<!DOCTYPE html>
							<title>Tiny SwissGL example</title>
							<meta charset="utf-8">
							<meta name="viewport" content="width=device-width,initial-scale=1">
							<script src="swissgl.js"></script>
							<style>
								body, html {margin: 0; padding: 0; overflow: hidden;}
								#c {width:100vw; height: 100vh}
							</style>
							<canvas id="c"></canvas>
							<script>
								"use strict";
								const canvas = document.getElementById('c');
								const glsl = SwissGL(canvas);
								glsl.loop(({time})=>{
									glsl.adjustCanvas();        
									glsl({time, Aspect:'cover',FP:`
										sin(length(XY)*vec3(30,30.5,31)
										-time+atan(XY.x,XY.y)*3.),1`});
								});
							</script>
						

Simple things must be easy

Fullscreen quad fragment shader
(short syntax)
Fullscreen quad fragment shader
(full syntax)
  • This function runs in parallel on GPU for every pixel of the canvas (super fast)
  • vec2 UV – quad coordinates [0..1]x[0..1]
  • vec4 FOut – output color (RGBA)
  • (0,0) is lower left corner in WebGL 😡

We also have vec2 XY (same as UV, but [-1..1]x[-1..1]), and full GLSL ES to use

ivec2 I - integer pixel coordinates

* beware of `devicePixelRatio` * useful for rendering into data-textures
Maintaining the aspect ratio

Options: "fit", "cover", "x", "y", "mean"

(I'm not 100% happy about this design, might change in th future)

Maintaining the aspect ratio

Options: "fit", "cover", "x", "y", "mean"

(I'm not 100% happy about this design, might change in th future)

We can easily pass uniform values from JS to GLSL
* Any keyword is assumed to be a uniform, except: * `Inc`, `VP`, `FP`,`Clear`, `Blend`, `View`, `Grid`, `Mesh`, `Aspect`, `DepthTest`, `AlphaCoverage`, `Face`

But why are image top and bottom clipped?

SwissGL is a quad drawing library
  • By default it draws a single [-1..1]x[-1..1] quad
  • Without Aspect it covers the full view
  • Aspect:'fit' makes this quad to fit the view
  • Fragment Program (FP) only controls what to draw in this quad, not the quad position

Enter the Vertex Program!

Enter the Vertex Program!

Full syntax

Spin me!

* `mat2 rot2(float)` - handy tool to make 2D rotation matrices

Is quad boring? 🥱

Tesselate!

Add texture with FP

Tesselate 2D

Why such a strange triangulation?

(to make the triangular grid if needed) * `ivec2 VID` – vertex index in a mesh * `ivec2 Mesh` – mesh size

Drawing many things at once

for(...) {}

Instancing

* `ivec3 Grid` – instance grid size * `ivec3 ID` – instance index

Passing data VP -> FP

Quad color is computer by the Vertex Program and passed to the Fragment Program using `varying`

Adding some randomness

* `vec3 hash(ivec3)` maps an integer vector into a "random" vector in [0..1]^3 * `TAU = 2.0*PI` 🙃

Grid and Mesh work together

3D

Spinning 3D Cube

(SwissGL way)
`vec3 cubeVert(vec2 xy, int face)`

Problem?

Z-buffer to the rescue!

In simple cases we may also use
Face:'front' or Face:'back'

No perspective?

Perspective division

* GL does [x,y,z,w] -> [x/w, y/w, z/w, 1] * we adjust `w` to magnify `xy` if z>0.0 and shrink if z<0.0 (z-axis looks at the camera) * downscale `z` to prevent clipping out of [-1..1] * I think I'll add this function to SwissGL soon * canonical way to do this is "Projection matrix" * use [glMatrix](https://github.com/toji/gl-matrix) for the canonical way

1000 cubes

Color cube

We can also pass per-vertex colors

Surface Normals

* `SURF` – marco that uses finite differences to estimate the normal of `surface_f` * in this example we obtain the view-space normal

Back to the plane for now...

Let's draw some fireflies

Fragment program draws a radially decaying sprite, but it's still a opaque quad

Enter Blend

* `Blend` string is an expression using a small embedded language * `s`/`sa` - source (incoming) rgb/alpha * `d`/`da` - destination (existing) rgb/alpha * Examples: * `d+s` - additive blending * `d*(1-sa)+s*sa` - standard alpha overlay (assume far to near draw order) * `d*(1-sa)+s` - premultiplied alpha * `max(s,d)`/`min(s,d)` - useful for making cellular Voronoi-patterns

Voronoi bubbles

Stateful computation

(textures!)

The second argument

* The first argument tells what to draw, the second - where to store the result * We can also omit drawing completely and push `data` to GPU * Each texture must have a unique `tag` * SwissGL will update existing texture if a `tag` was already seen * Way to stream bulk data JS->GPU * Textures are passed like uniforms and queried like functions

Streaming videos

(or loading images, canvases)

Render-to-texture

Clear

Sampling control

Data textures (float32)

Vertex Texture Fetch

Note the use of integer pixel coordinates

Preserving the history

(ping-pong buffers)

Making particles interact

Yet Another Particle Lenia

More at Swiss.GL
Next: SwissGPU 🚧
(WebGPU experiment)
WebBFF (paper)
Z80 Life