🌌 WebGPU · shaders
Aurora Engine
WebGPU became Baseline in January 2026 — every major browser now ships native GPU shading. This is the post-Three.js era: a full render pipeline, by hand, in ~120 lines.
Use it for: Immersive hero sections · generative art · data visualization at scale
engine: … · move your cursor
The complete source
This is the exact file rendering the demo above — a single self-contained React client component with its styles inline. Copy or download it, drop it into any React / Next.js project, and it runs. No extra dependencies.
aurora.jsx
1"use client";2import { useEffect, useRef, useState } from "react";34/* ============================================================5AURORA ENGINE — a raw WebGPU fragment shader (WGSL), with an6automatic WebGL2 (GLSL) fallback for older browsers.7WebGPU became Baseline (all major browsers) in January 2026.8No libraries. The full render pipeline is ~120 lines.9============================================================ */1011const WGSL = `12struct U { time: f32, w: f32, h: f32, mx: f32, my: f32, pad0: f32, pad1: f32, pad2: f32 };13@group(0) @binding(0) var<uniform> u: U;1415fn hash(p: vec2f) -> f32 {16return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453);17}18fn noise(p: vec2f) -> f32 {19let i = floor(p); let f = fract(p);20let s = f * f * (3.0 - 2.0 * f);21return mix(mix(hash(i), hash(i + vec2f(1.0, 0.0)), s.x),22mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), s.x), s.y);23}24fn fbm(p0: vec2f) -> f32 {25var p = p0; var v = 0.0; var a = 0.5;26for (var i = 0; i < 5; i++) { v += a * noise(p); p *= 2.03; a *= 0.5; }27return v;28}2930@vertex31fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {32var pos = array<vec2f, 3>(vec2f(-1.0, -3.0), vec2f(3.0, 1.0), vec2f(-1.0, 1.0));33return vec4f(pos[i], 0.0, 1.0);34}3536@fragment37fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {38let uv = vec2f(frag.x / u.w, 1.0 - frag.y / u.h);39let m = vec2f(u.mx, u.my);40let t = u.time * 0.12;41// domain-warped fbm = silky aurora curtains42let q = vec2f(fbm(uv * 3.0 + t), fbm(uv * 3.0 - t));43let r = vec2f(fbm(uv * 3.0 + q * 1.7 + m * 0.6), fbm(uv * 3.0 + q * 1.7 - m * 0.6));44let v = fbm(uv * 3.0 + r * 2.0);45let curtain = pow(smoothstep(0.25, 0.95, v + uv.y * 0.4), 1.6);46let violet = vec3f(0.49, 0.23, 0.93);47let cyan = vec3f(0.02, 0.71, 0.83);48let gold = vec3f(1.00, 0.65, 0.00);49var col = mix(violet, cyan, clamp(q.x * 1.6, 0.0, 1.0));50col = mix(col, gold, pow(r.y, 3.0) * 0.6);51col *= curtain;52col += vec3f(0.02, 0.01, 0.05); // deep-space base53return vec4f(col, 1.0);54}55`;5657const GLSL_FRAG = `#version 300 es58precision highp float;59uniform float time; uniform vec2 res; uniform vec2 mouse; out vec4 O;60float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }61float noise(vec2 p){ vec2 i=floor(p),f=fract(p); vec2 s=f*f*(3.-2.*f);62return mix(mix(hash(i),hash(i+vec2(1,0)),s.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),s.x),s.y);}63float fbm(vec2 p){ float v=0.,a=.5; for(int i=0;i<5;i++){v+=a*noise(p);p*=2.03;a*=.5;} return v;}64void main(){65vec2 uv = gl_FragCoord.xy / res; float t = time*0.12;66vec2 q = vec2(fbm(uv*3.+t), fbm(uv*3.-t));67vec2 r = vec2(fbm(uv*3.+q*1.7+mouse*.6), fbm(uv*3.+q*1.7-mouse*.6));68float v = fbm(uv*3.+r*2.);69float curtain = pow(smoothstep(.25,.95, v+uv.y*.4), 1.6);70vec3 col = mix(vec3(.49,.23,.93), vec3(.02,.71,.83), clamp(q.x*1.6,0.,1.));71col = mix(col, vec3(1.,.65,0.), pow(r.y,3.)*.6);72O = vec4(col*curtain + vec3(.02,.01,.05), 1.);73}`;7475export default function Aurora() {76const ref = useRef(null);77const [engine, setEngine] = useState("…");7879useEffect(() => {80const canvas = ref.current;81let stop = false;82let cleanup = () => {};83const mouse = { x: 0.5, y: 0.5 };84const onMove = (e) => {85const b = canvas.getBoundingClientRect();86mouse.x = (e.clientX - b.left) / b.width;87mouse.y = (e.clientY - b.top) / b.height;88};89canvas.addEventListener("pointermove", onMove);9091const size = () => {92const dpr = Math.min(window.devicePixelRatio || 1, 2);93canvas.width = canvas.clientWidth * dpr;94canvas.height = canvas.clientHeight * dpr;95};96size();9798async function startWebGPU() {99const adapter = await navigator.gpu.requestAdapter();100if (!adapter) throw new Error("no adapter");101const device = await adapter.requestDevice();102const ctx = canvas.getContext("webgpu");103const format = navigator.gpu.getPreferredCanvasFormat();104ctx.configure({ device, format, alphaMode: "opaque" });105106const shaderModule = device.createShaderModule({ code: WGSL });107const pipeline = device.createRenderPipeline({108layout: "auto",109vertex: { module: shaderModule, entryPoint: "vs" },110fragment: { module: shaderModule, entryPoint: "fs", targets: [{ format }] },111primitive: { topology: "triangle-list" },112});113const ubuf = device.createBuffer({114size: 32,115usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,116});117const bind = device.createBindGroup({118layout: pipeline.getBindGroupLayout(0),119entries: [{ binding: 0, resource: { buffer: ubuf } }],120});121122const t0 = performance.now();123const frame = () => {124if (stop) return;125device.queue.writeBuffer(126ubuf,1270,128new Float32Array([129(performance.now() - t0) / 1000,130canvas.width,131canvas.height,132mouse.x,133mouse.y,1340, 0, 0,135])136);137const enc = device.createCommandEncoder();138const pass = enc.beginRenderPass({139colorAttachments: [140{141view: ctx.getCurrentTexture().createView(),142loadOp: "clear",143storeOp: "store",144clearValue: { r: 0, g: 0, b: 0, a: 1 },145},146],147});148pass.setPipeline(pipeline);149pass.setBindGroup(0, bind);150pass.draw(3);151pass.end();152device.queue.submit([enc.finish()]);153requestAnimationFrame(frame);154};155frame();156setEngine("WebGPU · WGSL");157cleanup = () => device.destroy();158}159160function startWebGL() {161const gl = canvas.getContext("webgl2");162const vsrc = `#version 300 es163void main(){ vec2 p[3]=vec2[3](vec2(-1.,-3.),vec2(3.,1.),vec2(-1.,1.));164gl_Position=vec4(p[gl_VertexID],0.,1.);}`;165const mk = (type, src) => {166const s = gl.createShader(type);167gl.shaderSource(s, src);168gl.compileShader(s);169return s;170};171const prog = gl.createProgram();172gl.attachShader(prog, mk(gl.VERTEX_SHADER, vsrc));173gl.attachShader(prog, mk(gl.FRAGMENT_SHADER, GLSL_FRAG));174gl.linkProgram(prog);175gl.useProgram(prog);176const uT = gl.getUniformLocation(prog, "time");177const uR = gl.getUniformLocation(prog, "res");178const uM = gl.getUniformLocation(prog, "mouse");179const t0 = performance.now();180const frame = () => {181if (stop) return;182gl.viewport(0, 0, canvas.width, canvas.height);183gl.uniform1f(uT, (performance.now() - t0) / 1000);184gl.uniform2f(uR, canvas.width, canvas.height);185gl.uniform2f(uM, mouse.x, mouse.y);186gl.drawArrays(gl.TRIANGLES, 0, 3);187requestAnimationFrame(frame);188};189frame();190setEngine("WebGL2 fallback · GLSL");191}192193if (navigator.gpu) {194startWebGPU().catch(() => startWebGL());195} else {196startWebGL();197}198window.addEventListener("resize", size);199return () => {200stop = true;201cleanup();202window.removeEventListener("resize", size);203canvas.removeEventListener("pointermove", onMove);204};205}, []);206207return (208<div className="relative w-full h-[70svh] rounded-3xl overflow-hidden border border-white/15">209<canvas ref={ref} className="w-full h-full block" />210<p className="absolute bottom-3 right-4 text-[1.2rem] text-white/60 bg-black/40 px-3 py-1 rounded-full">211engine: {engine} · move your cursor212</p>213</div>214);215}216