Reference
Engines
The three engines a piece can run on, three.js, GLSL and p5.js, and the shape of each one’s code.
Every piece runs on one of three engines. The studio calls them WebGL 3D (three.js), GLSL (a fragment shader) and Canvas 2D (p5.js), and the gallery's filters call them 3D, Shader and 2D. Each has its own shape of code, which this page sets out, so you can write a piece by hand or change what Perlin wrote without breaking it.
Whichever engine it is, the runtime owns the canvas, its size, the frame loop, pausing and capture. Your code draws; the runtime decides when. What a piece's code can use beyond the engine itself (controls, the pointer, sound, assets and saving) is on The runtime API.
Which engine to use
| Engine | Language | Good for | Can use |
|---|---|---|---|
| WebGL 3D | JavaScript module, three.js r186 | Anything spatial: objects, materials, lighting, particles in space, cameras, post-processing | Controls, pointer, sound, textures, every kind of asset, saving files, VR and AR |
| GLSL | GLSL ES 3.00 fragment shader | Raymarched worlds, fractals, noise fields, abstract motion graphics | Controls from annotated uniforms, the pointer, the previous frame |
| Canvas 2D | JavaScript, p5.js 1.11 in global mode | Flow fields, line work, patterns, typography, particle trails | Controls, pointer, sound, textures, images, fonts and video, saving files |
To pick one before Perlin builds, use the switch in the prompt box: Auto, 3D, Shader or 2D. When you change a piece, Perlin keeps its engine unless you ask for another.
Only the WebGL 3D and Canvas 2D engines can use your assets in their code, and only WebGL 3D can load a 3D model or go into VR and AR. Any engine can carry a timeline with text, image and video layers over it.
WebGL 3D
The code is an ES module. Its default export is a function that sets the scene up once and returns what to do each frame.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
export default function sketch({ scene, camera, renderer, gui }) {
const params = { speed: 0.4, colour: '#ff7a45' };
scene.background = new THREE.Color('#07080c');
camera.position.set(0, 1, 5);
const orbit = new OrbitControls(camera, renderer.domElement);
orbit.enableDamping = true;
const material = new THREE.MeshStandardMaterial({ color: params.colour, roughness: 0.3 });
const knot = new THREE.Mesh(new THREE.TorusKnotGeometry(1, 0.32, 200, 32), material);
scene.add(knot, new THREE.HemisphereLight('#ffffff', '#202030', 1.5));
gui.add(params, 'speed', 0, 2).name('Speed');
gui.addColor(params, 'colour').name('Colour').onChange((v) => material.color.set(v));
return {
update({ delta }) {
knot.rotation.y += delta * params.speed;
orbit.update();
},
};
}
What the function receives
| Name | What it is |
|---|---|
THREE | The three.js module, the same one import * as THREE from 'three' gives |
scene | A THREE.Scene, rendered for you every frame |
camera | A PerspectiveCamera: field of view 50, near 0.1, far 2000, at (0, 0, 6) facing the origin |
renderer | A WebGLRenderer, antialiased, sized to the canvas. Colour output is sRGB, with no tone mapping and shadows off until you turn them on |
canvas | The canvas it draws on, for pointer listeners |
size | { width, height, pixelRatio }, kept current |
width, height | The current size in CSS pixels |
pointer | Where the mouse, pen or finger is; see Pointer and size |
gui | The controls; see Controls |
fonts | Addresses of built-in typefaces for FontLoader: helvetiker, helvetikerRegular, optimer, gentilis, droidSans, droidSerif, mono |
sfx, textures, assets | Sound, images and your uploaded files; see The runtime API |
xr | VR and AR; see VR and AR in code |
The function may be async, for example to await a model or a typeface before the first frame.
What it returns
Every member is optional.
| Member | When it runs |
|---|---|
update({ time, delta, frame, pointer }) | Every frame, before rendering. time and delta are in seconds and stop while the piece is paused. delta is never more than 0.1 |
render() | Instead of the default renderer.render(scene, camera), for example composer.render() with post-processing |
resize({ width, height, pixelRatio }) | After the runtime has resized the renderer and updated the camera's aspect |
dispose() | When the page goes away |
camera | A different camera to render with |
Rules of the engine
- Import only from
threeandthree/addons/…. They resolve to three.js r186. - Never create a renderer, call
requestAnimationFrameorsetAnimationLoop, or add a canvas of your own. The runtime does all of that. - Only the camera you're given is updated on resize. An
OrthographicCameraof your own, anEffectComposeror a render target needs its own sizing inresize(). - The pixel ratio is capped at 2.
GLSL
The code is one fragment shader, Shadertoy style. Define mainImage; the #version line, precision and main() are added for you.
uniform float uSpeed; // @control value=0.5 min=0.0 max=2.0 label="Speed"
uniform int uRings; // @control value=8 min=2 max=24 label="Rings"
uniform vec3 uTint; // @control value=#4cc9f0 label="Tint"
uniform bool uPulse; // @control value=true label="Pulse"
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
float d = length(uv);
float wave = sin(d * float(uRings) * 6.2831 - iTime * uSpeed * 4.0);
float glow = smoothstep(0.0, 1.0, wave) * (uPulse ? 0.6 + 0.4 * sin(iTime) : 1.0);
vec3 col = uTint * glow * (1.0 - d);
fragColor = vec4(pow(max(col, 0.0), vec3(0.4545)), 1.0);
}
Built-in uniforms
Use these without declaring them.
| Uniform | Type | What it holds |
|---|---|---|
iResolution | vec3 | The canvas size in device pixels (z is 1) |
iTime | float | Seconds since the piece started; stops while paused |
iTimeDelta | float | Seconds since the last frame |
iFrameRate | float | Frames a second |
iFrame | int | Frames drawn so far |
iMouse | vec4 | xy is the pointer in pixels, followed on hover and starting at the centre. zw is where it was last pressed: positive while pressed, negative once released |
iDate | vec4 | Year, month (counting from 0), day, and seconds into the day |
iChannel0 | sampler2D | The previous frame. Mentioning it turns feedback on; sample it with texture(iChannel0, fragCoord / iResolution.xy) |
Controls from uniforms
A uniform becomes a control when its line ends in a // @control comment. Uniforms without one aren't set by anything, and names starting with i and a capital are kept for the built-ins.
| Type | Control | Example |
|---|---|---|
float | Slider | uniform float uZoom; // @control value=1.0 min=0.5 max=4.0 |
int | Slider in whole steps | uniform int uSteps; // @control value=96 min=24 max=160 |
bool | Switch | uniform bool uGrain; // @control value=true |
vec3 or vec4 | Colour picker | uniform vec3 uGlow; // @control value=#ff7a45 |
The keys after @control:
valueis where it starts. A colour takes a hex value, or three numbers from 0 to 1 such asvalue=1.0,0.5,0.2. Avec4colour's alpha is always 1.minandmaxset a slider's range, andstepits increments.label="…"names it in the panel. Without one, the uniform's name is used, minus a leadingubefore a capital:uSpeedshows as Speed.folder="…"groups it with other controls of the same folder.
A control's key, for keyframes and for the embed API, is the uniform's name, after its folder and a dot when it has one: uSpeed, or Light.uGlow.
Shader details
- GLSL ES 3.00 is strict: write
1.0rather than1, convert withfloat(i), usetexture(), and give every loop a constant bound. - The colour you write is shown as it is, with alpha ignored. Do your own tone mapping and gamma.
- The engine draws at a lower resolution when a shader can't keep up, down to half, and comes back up when it can. Build coordinates from
iResolutionand the piece stays right at any resolution and shape. - The pixel ratio is capped at 1.5.
Canvas 2D
The code is a plain script for p5.js 1.11 in global mode: no import or export, with setup() and draw().
const params = { count: 400, speed: 1, ink: '#f1d72c' };
let dots = [];
function setup() {
createCanvas(windowWidth, windowHeight);
gui.add(params, 'count', 50, 2000, 1).name('Dots').onChange(reset);
gui.add(params, 'speed', 0.1, 4).name('Speed');
gui.addColor(params, 'ink').name('Ink');
reset();
}
function reset() {
dots = Array.from({ length: params.count }, () => createVector(random(width), random(height)));
}
function draw() {
background(10, 30);
stroke(params.ink);
for (const p of dots) {
const a = noise(p.x * 0.003, p.y * 0.003, frameCount * 0.004) * TWO_PI * 2;
p.x = (p.x + cos(a) * params.speed + width) % width;
p.y = (p.y + sin(a) * params.speed + height) % height;
point(p.x, p.y);
}
}
Canvas details
gui,sfx,textures,assetsandvybeare globals. A variable of your own with the same name hides the runtime's.- A canvas made with
createCanvas(windowWidth, windowHeight)is resized for you, and keeps its last frame, scaled to cover, so trails don't flash to black. DefinewindowResized()yourself when anything else depends on the size. mouseX,mouseY,mouseIsPressed,mousePressed(),mouseDragged()andmouseReleased()answer to touch as well as the mouse. For several fingers, readtouches.- Images come from
textures.get(id), which you can call insetup()or later. Don't reach forloadImage,loadFontorloadSound: the frame can't fetch files from other sites. Usetextures,assetsandsfxinstead. - A still piece draws in
draw(), callsnoLoop(), and callsredraw()from its controls'onChange.
What no engine can do
A piece runs in a sealed frame, so it can't reach the page around it or the rest of the internet. It can't send anything out or fetch from other sites: only the engine's own library and your assets are loaded for it. It has no localStorage, cookies or clipboard, can't open windows or show alert, and a frame that navigates away is taken down. Draw any interface on the canvas, take values through gui, and give the viewer files with vybe.save.
A loop that runs for more than 3 seconds is stopped, and the piece reports which line it was on.
Related
Still stuck, or something here out of date? Tell us