🌌 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";
3
4/* ============================================================
5 AURORA ENGINE — a raw WebGPU fragment shader (WGSL), with an
6 automatic WebGL2 (GLSL) fallback for older browsers.
7 WebGPU became Baseline (all major browsers) in January 2026.
8 No libraries. The full render pipeline is ~120 lines.
9 ============================================================ */
10
11const 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;
14
15fn hash(p: vec2f) -> f32 {
16 return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453);
17}
18fn noise(p: vec2f) -> f32 {
19 let i = floor(p); let f = fract(p);
20 let s = f * f * (3.0 - 2.0 * f);
21 return mix(mix(hash(i), hash(i + vec2f(1.0, 0.0)), s.x),
22 mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), s.x), s.y);
23}
24fn fbm(p0: vec2f) -> f32 {
25 var p = p0; var v = 0.0; var a = 0.5;
26 for (var i = 0; i < 5; i++) { v += a * noise(p); p *= 2.03; a *= 0.5; }
27 return v;
28}
29
30@vertex
31fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
32 var pos = array<vec2f, 3>(vec2f(-1.0, -3.0), vec2f(3.0, 1.0), vec2f(-1.0, 1.0));
33 return vec4f(pos[i], 0.0, 1.0);
34}
35
36@fragment
37fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
38 let uv = vec2f(frag.x / u.w, 1.0 - frag.y / u.h);
39 let m = vec2f(u.mx, u.my);
40 let t = u.time * 0.12;
41 // domain-warped fbm = silky aurora curtains
42 let q = vec2f(fbm(uv * 3.0 + t), fbm(uv * 3.0 - t));
43 let r = vec2f(fbm(uv * 3.0 + q * 1.7 + m * 0.6), fbm(uv * 3.0 + q * 1.7 - m * 0.6));
44 let v = fbm(uv * 3.0 + r * 2.0);
45 let curtain = pow(smoothstep(0.25, 0.95, v + uv.y * 0.4), 1.6);
46 let violet = vec3f(0.49, 0.23, 0.93);
47 let cyan = vec3f(0.02, 0.71, 0.83);
48 let gold = vec3f(1.00, 0.65, 0.00);
49 var col = mix(violet, cyan, clamp(q.x * 1.6, 0.0, 1.0));
50 col = mix(col, gold, pow(r.y, 3.0) * 0.6);
51 col *= curtain;
52 col += vec3f(0.02, 0.01, 0.05); // deep-space base
53 return vec4f(col, 1.0);
54}
55`;
56
57const GLSL_FRAG = `#version 300 es
58precision 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);
62 return 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(){
65 vec2 uv = gl_FragCoord.xy / res; float t = time*0.12;
66 vec2 q = vec2(fbm(uv*3.+t), fbm(uv*3.-t));
67 vec2 r = vec2(fbm(uv*3.+q*1.7+mouse*.6), fbm(uv*3.+q*1.7-mouse*.6));
68 float v = fbm(uv*3.+r*2.);
69 float curtain = pow(smoothstep(.25,.95, v+uv.y*.4), 1.6);
70 vec3 col = mix(vec3(.49,.23,.93), vec3(.02,.71,.83), clamp(q.x*1.6,0.,1.));
71 col = mix(col, vec3(1.,.65,0.), pow(r.y,3.)*.6);
72 O = vec4(col*curtain + vec3(.02,.01,.05), 1.);
73}`;
74
75export default function Aurora() {
76 const ref = useRef(null);
77 const [engine, setEngine] = useState("…");
78
79 useEffect(() => {
80 const canvas = ref.current;
81 let stop = false;
82 let cleanup = () => {};
83 const mouse = { x: 0.5, y: 0.5 };
84 const onMove = (e) => {
85 const b = canvas.getBoundingClientRect();
86 mouse.x = (e.clientX - b.left) / b.width;
87 mouse.y = (e.clientY - b.top) / b.height;
88 };
89 canvas.addEventListener("pointermove", onMove);
90
91 const size = () => {
92 const dpr = Math.min(window.devicePixelRatio || 1, 2);
93 canvas.width = canvas.clientWidth * dpr;
94 canvas.height = canvas.clientHeight * dpr;
95 };
96 size();
97
98 async function startWebGPU() {
99 const adapter = await navigator.gpu.requestAdapter();
100 if (!adapter) throw new Error("no adapter");
101 const device = await adapter.requestDevice();
102 const ctx = canvas.getContext("webgpu");
103 const format = navigator.gpu.getPreferredCanvasFormat();
104 ctx.configure({ device, format, alphaMode: "opaque" });
105
106 const shaderModule = device.createShaderModule({ code: WGSL });
107 const pipeline = device.createRenderPipeline({
108 layout: "auto",
109 vertex: { module: shaderModule, entryPoint: "vs" },
110 fragment: { module: shaderModule, entryPoint: "fs", targets: [{ format }] },
111 primitive: { topology: "triangle-list" },
112 });
113 const ubuf = device.createBuffer({
114 size: 32,
115 usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
116 });
117 const bind = device.createBindGroup({
118 layout: pipeline.getBindGroupLayout(0),
119 entries: [{ binding: 0, resource: { buffer: ubuf } }],
120 });
121
122 const t0 = performance.now();
123 const frame = () => {
124 if (stop) return;
125 device.queue.writeBuffer(
126 ubuf,
127 0,
128 new Float32Array([
129 (performance.now() - t0) / 1000,
130 canvas.width,
131 canvas.height,
132 mouse.x,
133 mouse.y,
134 0, 0, 0,
135 ])
136 );
137 const enc = device.createCommandEncoder();
138 const pass = enc.beginRenderPass({
139 colorAttachments: [
140 {
141 view: ctx.getCurrentTexture().createView(),
142 loadOp: "clear",
143 storeOp: "store",
144 clearValue: { r: 0, g: 0, b: 0, a: 1 },
145 },
146 ],
147 });
148 pass.setPipeline(pipeline);
149 pass.setBindGroup(0, bind);
150 pass.draw(3);
151 pass.end();
152 device.queue.submit([enc.finish()]);
153 requestAnimationFrame(frame);
154 };
155 frame();
156 setEngine("WebGPU · WGSL");
157 cleanup = () => device.destroy();
158 }
159
160 function startWebGL() {
161 const gl = canvas.getContext("webgl2");
162 const vsrc = `#version 300 es
163 void main(){ vec2 p[3]=vec2[3](vec2(-1.,-3.),vec2(3.,1.),vec2(-1.,1.));
164 gl_Position=vec4(p[gl_VertexID],0.,1.);}`;
165 const mk = (type, src) => {
166 const s = gl.createShader(type);
167 gl.shaderSource(s, src);
168 gl.compileShader(s);
169 return s;
170 };
171 const prog = gl.createProgram();
172 gl.attachShader(prog, mk(gl.VERTEX_SHADER, vsrc));
173 gl.attachShader(prog, mk(gl.FRAGMENT_SHADER, GLSL_FRAG));
174 gl.linkProgram(prog);
175 gl.useProgram(prog);
176 const uT = gl.getUniformLocation(prog, "time");
177 const uR = gl.getUniformLocation(prog, "res");
178 const uM = gl.getUniformLocation(prog, "mouse");
179 const t0 = performance.now();
180 const frame = () => {
181 if (stop) return;
182 gl.viewport(0, 0, canvas.width, canvas.height);
183 gl.uniform1f(uT, (performance.now() - t0) / 1000);
184 gl.uniform2f(uR, canvas.width, canvas.height);
185 gl.uniform2f(uM, mouse.x, mouse.y);
186 gl.drawArrays(gl.TRIANGLES, 0, 3);
187 requestAnimationFrame(frame);
188 };
189 frame();
190 setEngine("WebGL2 fallback · GLSL");
191 }
192
193 if (navigator.gpu) {
194 startWebGPU().catch(() => startWebGL());
195 } else {
196 startWebGL();
197 }
198 window.addEventListener("resize", size);
199 return () => {
200 stop = true;
201 cleanup();
202 window.removeEventListener("resize", size);
203 canvas.removeEventListener("pointermove", onMove);
204 };
205 }, []);
206
207 return (
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">
211 engine: {engine} · move your cursor
212 </p>
213 </div>
214 );
215}
216