03. GL workstation · 1993
Retro torus
About
A torus tessellated into chunky triangles with per-face normals, lit by a mint key and a cool fill. Edges are drawn as a second line pass. A ground grid locks it to the SGI demo-reel floor. Matrix math is inline, no three.js. Drag to orbit. It auto-rotates until you take over.
Drag on the preview to orbit. Auto-rotation stops after the first drag.
Browser APIs
- WebGL 1
- GLSL ES 1.0
- Pointer Events
- requestAnimationFrame
- matchMedia
If WebGL is missing, the demo draws a message on the canvas instead of a blank frame. prefers-reduced-motion freezes the first still.
Source
(function () {
const PARAMS = {
color: "#c7cccf",
accent: "#6ee7b7",
speed: 1
};
function hexToRgb(hex) {
const n = parseInt(String(hex).replace("#", ""), 16);
if (isNaN(n)) return [0.43, 0.91, 0.72];
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
}
const canvas = document.getElementById("c");
const fallback = document.getElementById("fallback");
function fail(message) {
fallback.hidden = false;
fallback.textContent = message;
}
const gl = canvas.getContext("webgl", { alpha: false, antialias: true });
if (!gl) {
fail("WebGL is not available in this browser. Try a current Firefox, Chrome, Safari, or Edge.");
return;
}
const VERT = `attribute vec3 a_pos;
attribute vec3 a_nrm;
uniform mat4 u_mvp;
uniform mat4 u_model;
varying vec3 v_nrm;
varying vec3 v_pos;
void main() {
vec4 world = u_model * vec4(a_pos, 1.0);
v_pos = world.xyz;
v_nrm = mat3(u_model) * a_nrm;
gl_Position = u_mvp * vec4(a_pos, 1.0);
}`;
const FRAG = `precision mediump float;
varying vec3 v_nrm;
varying vec3 v_pos;
uniform vec3 u_cam;
uniform vec3 u_color;
uniform vec3 u_accent;
void main() {
vec3 n = normalize(v_nrm);
vec3 v = normalize(u_cam - v_pos);
vec3 l1 = normalize(vec3(0.55, 0.85, 0.35));
vec3 l2 = normalize(vec3(-0.75, 0.25, 0.2));
float d1 = max(dot(n, l1), 0.0);
float d2 = max(dot(n, l2), 0.0);
float rim = pow(1.0 - max(dot(n, v), 0.0), 2.8);
vec3 col = u_color * (0.10 + 0.78 * d1);
col += u_accent * d2 * 0.38;
col += u_accent * rim * 0.22;
gl_FragColor = vec4(col, 1.0);
}`;
const LINE_VERT = `attribute vec3 a_pos;
uniform mat4 u_mvp;
void main() {
gl_Position = u_mvp * vec4(a_pos, 1.0);
}`;
const LINE_FRAG = `precision mediump float;
uniform vec3 u_accent;
void main() {
gl_FragColor = vec4(u_accent, 0.28);
}`;
function compile(type, src) {
const sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(sh) || "Shader compile failed");
}
return sh;
}
function program(vs, fs) {
const p = gl.createProgram();
gl.attachShader(p, compile(gl.VERTEX_SHADER, vs));
gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fs));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(p) || "Program link failed");
}
return p;
}
function mul(a, b) {
const o = new Float32Array(16);
for (let c = 0; c < 4; c++) {
for (let r = 0; r < 4; r++) {
o[c * 4 + r] =
a[r] * b[c * 4] +
a[4 + r] * b[c * 4 + 1] +
a[8 + r] * b[c * 4 + 2] +
a[12 + r] * b[c * 4 + 3];
}
}
return o;
}
function perspective(fovy, aspect, near, far) {
const f = 1 / Math.tan(fovy / 2);
const nf = 1 / (near - far);
const o = new Float32Array(16);
o[0] = f / aspect;
o[5] = f;
o[10] = (far + near) * nf;
o[11] = -1;
o[14] = 2 * far * near * nf;
return o;
}
function translate(x, y, z) {
const o = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);
o[12] = x; o[13] = y; o[14] = z;
return o;
}
function rotateXY(ax, ay) {
const cx = Math.cos(ax), sx = Math.sin(ax);
const cy = Math.cos(ay), sy = Math.sin(ay);
const rx = new Float32Array([1,0,0,0, 0,cx,sx,0, 0,-sx,cx,0, 0,0,0,1]);
const ry = new Float32Array([cy,0,-sy,0, 0,1,0,0, sy,0,cy,0, 0,0,0,1]);
return mul(ry, rx);
}
function torus(major, minor, R, r) {
const pos = [];
const nrm = [];
const lines = [];
function point(i, j) {
const u = (i / major) * Math.PI * 2;
const v = (j / minor) * Math.PI * 2;
const cx = Math.cos(u), sx = Math.sin(u);
const cy = Math.cos(v), sy = Math.sin(v);
return [
(R + r * cy) * cx,
r * sy,
(R + r * cy) * sx,
];
}
function tri(a, b, c) {
const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2];
const vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
let nx = uy * vz - uz * vy;
let ny = uz * vx - ux * vz;
let nz = ux * vy - uy * vx;
const len = Math.hypot(nx, ny, nz) || 1;
nx /= len; ny /= len; nz /= len;
for (const p of [a, b, c]) {
pos.push(p[0], p[1], p[2]);
nrm.push(nx, ny, nz);
}
}
for (let i = 0; i < major; i++) {
for (let j = 0; j < minor; j++) {
const a = point(i, j);
const b = point(i + 1, j);
const c = point(i + 1, j + 1);
const d = point(i, j + 1);
tri(a, b, c);
tri(a, c, d);
lines.push(a[0], a[1], a[2], b[0], b[1], b[2]);
lines.push(a[0], a[1], a[2], d[0], d[1], d[2]);
}
}
return {
pos: new Float32Array(pos),
nrm: new Float32Array(nrm),
count: pos.length / 3,
lines: new Float32Array(lines),
lineCount: lines.length / 3,
};
}
function grid(size, step) {
const pts = [];
for (let i = -size; i <= size; i += step) {
pts.push(-size, 0, i, size, 0, i);
pts.push(i, 0, -size, i, 0, size);
}
return { data: new Float32Array(pts), count: pts.length / 3 };
}
const mesh = torus(16, 10, 1.05, 0.42);
const floor = grid(4.5, 0.5);
const meshProg = program(VERT, FRAG);
const lineProg = program(LINE_VERT, LINE_FRAG);
function bindAttrib(prog, name, buffer, size) {
const loc = gl.getAttribLocation(prog, name);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0);
}
function makeBuf(data) {
const b = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, b);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
return b;
}
const posBuf = makeBuf(mesh.pos);
const nrmBuf = makeBuf(mesh.nrm);
const lineBuf = makeBuf(mesh.lines);
const gridBuf = makeBuf(floor.data);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0.043, 0.047, 0.055, 1);
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let yaw = 0.7;
let pitch = 0.45;
let auto = true;
let dragging = false;
let lastX = 0, lastY = 0;
let last = 0;
let raf = 0;
canvas.addEventListener("pointerdown", function (e) {
dragging = true;
auto = false;
lastX = e.clientX;
lastY = e.clientY;
canvas.setPointerCapture(e.pointerId);
});
canvas.addEventListener("pointerup", function () { dragging = false; });
canvas.addEventListener("pointermove", function (e) {
if (!dragging) return;
yaw += (e.clientX - lastX) * 0.008;
pitch += (e.clientY - lastY) * 0.008;
pitch = Math.max(-0.2, Math.min(1.2, pitch));
lastX = e.clientX;
lastY = e.clientY;
});
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
}
gl.viewport(0, 0, canvas.width, canvas.height);
}
function frame(now) {
const dt = Math.min(0.1, last ? (now - last) * 0.001 : 0.016);
last = now;
if (auto && !reduced) yaw += dt * 0.45 * PARAMS.speed;
resize();
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const aspect = canvas.width / canvas.height;
const proj = perspective(Math.PI / 4, aspect, 0.1, 40);
const view = mul(translate(0, -0.15, -5.2), rotateXY(-0.35, 0));
const model = mul(translate(0, 0.55, 0), rotateXY(pitch, yaw));
const mvp = mul(mul(proj, view), model);
const gridModel = translate(0, -0.85, 0);
const gridMvp = mul(mul(proj, view), gridModel);
const cam = [0, 1.6, 5.2];
gl.useProgram(meshProg);
gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, "u_mvp"), false, mvp);
gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, "u_model"), false, model);
gl.uniform3fv(gl.getUniformLocation(meshProg, "u_cam"), cam);
gl.uniform3fv(gl.getUniformLocation(meshProg, "u_color"), hexToRgb(PARAMS.color));
gl.uniform3fv(gl.getUniformLocation(meshProg, "u_accent"), hexToRgb(PARAMS.accent));
bindAttrib(meshProg, "a_pos", posBuf, 3);
bindAttrib(meshProg, "a_nrm", nrmBuf, 3);
gl.enable(gl.CULL_FACE);
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
gl.useProgram(lineProg);
gl.uniform3fv(gl.getUniformLocation(lineProg, "u_accent"), hexToRgb(PARAMS.accent));
gl.uniformMatrix4fv(gl.getUniformLocation(lineProg, "u_mvp"), false, mvp);
bindAttrib(lineProg, "a_pos", lineBuf, 3);
gl.disable(gl.CULL_FACE);
gl.drawArrays(gl.LINES, 0, mesh.lineCount);
gl.uniformMatrix4fv(gl.getUniformLocation(lineProg, "u_mvp"), false, gridMvp);
bindAttrib(lineProg, "a_pos", gridBuf, 3);
gl.drawArrays(gl.LINES, 0, floor.count);
if (!reduced) raf = requestAnimationFrame(frame);
}
canvas.addEventListener("webglcontextlost", function (e) {
e.preventDefault();
cancelAnimationFrame(raf);
fail("The WebGL context was lost. Reload to restore the demo.");
});
try {
if (reduced) frame(performance.now());
else raf = requestAnimationFrame(frame);
} catch (err) {
fail(err && err.message ? err.message : String(err));
}
})();