Spaces:
Running
Running
; | |
function main() { | |
// Get a WebGL2 context | |
const canvas = document.querySelector("#canvas"); | |
const gl = canvas.getContext("webgl2"); | |
if (!gl) { | |
return; | |
} | |
//ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
// Vertex shader: simply pass the vertex positions along. | |
const vs = `#version 300 es | |
in vec4 a_position; | |
void main() { | |
gl_Position = a_position; | |
} | |
`; | |
//ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
// Fragment shader: a scientificallyβinspired 3D cymatic display. | |
// This shader rayβmarches a vibrating βplateβ (whose height is defined | |
// by the sum of two sinusoidal (mode) functions) and then shades it with | |
// diffuse and specular lighting. The palette() function is used to inject | |
// a pleasing color variation based on the local vibration amplitude. | |
const fs = `#version 300 es | |
precision highp float; | |
uniform vec2 iResolution; | |
uniform vec2 iMouse; | |
uniform float iTime; | |
out vec4 outColor; | |
// A color palette function (from Shadertoy) to add some βpopβ | |
vec3 palette( float t ) { | |
vec3 a = vec3(0.5, 0.5, 0.5); | |
vec3 b = vec3(0.5, 0.5, 0.5); | |
vec3 c = vec3(1.0, 1.0, 1.0); | |
vec3 d = vec3(0.263, 0.416, 0.557); | |
return a + b * cos( 6.28318 * (c * t + d) ); | |
} | |
// The vibrating plate β defined on the xzβplane (with x,z in [-1,1]) | |
// and with vertical displacement given by y = plate(x,z,t). | |
// Two modes are added (a βfundamentalβ and a secondβharmonic mode) to mimic | |
// realistic cymatic (Chladni) patterns on a clamped plate. | |
float plate(vec2 pos, float t) { | |
// Map pos from [-1,1] to [0,1] (for clampedβedge conditions) | |
vec2 uv = (pos + 1.0) * 0.5; | |
float mode1 = sin(3.14159 * uv.x) * sin(3.14159 * uv.y) * cos(3.14159 * t); | |
float mode2 = sin(2.0 * 3.14159 * uv.x) * sin(2.0 * 3.14159 * uv.y) * cos(2.0 * 3.14159 * t); | |
return 0.2 * (mode1 + mode2); | |
} | |
// Compute the normal of the heightfield (the vibrating plate) using finite differences. | |
vec3 calcNormal(vec2 pos, float t) { | |
float eps = 0.001; | |
float h = plate(pos, t); | |
float hx = plate(pos + vec2(eps, 0.0), t) - h; | |
float hz = plate(pos + vec2(0.0, eps), t) - h; | |
return normalize(vec3(-hx, 1.0, -hz)); | |
} | |
// Given a 3D point p, return its vertical distance to the plate surface. | |
// (If p is exactly on the surface then p.y = plate(p.xz,t) and the result is zero.) | |
float mapHeight(vec3 p, float t) { | |
// Outside the domain x,z β [-1,1] we assume a flat floor at y=0. | |
if (abs(p.x) > 1.0 || abs(p.z) > 1.0) { | |
return p.y; | |
} | |
return p.y - plate(vec2(p.x, p.z), t); | |
} | |
// A simple raycast function that marches a ray from the camera and | |
// returns the distance along the ray at which the plate is hit. | |
float raycast(vec3 ro, vec3 rd, float t) { | |
float tMin = 0.0; | |
float tMax = 20.0; | |
float tCurrent = tMin; | |
float stepSize = 0.02; | |
bool hit = false; | |
for (int i = 0; i < 500; i++) { | |
vec3 pos = ro + rd * tCurrent; | |
float d = mapHeight(pos, t); | |
if (d < 0.001) { | |
hit = true; | |
break; | |
} | |
tCurrent += stepSize; | |
if (tCurrent > tMax) break; | |
} | |
if (!hit) return -1.0; | |
// Refine the hit point with a short binary search. | |
float tA = tCurrent - stepSize; | |
float tB = tCurrent; | |
for (int i = 0; i < 10; i++) { | |
float tMid = (tA + tB) * 0.5; | |
float dMid = mapHeight(ro + rd * tMid, t); | |
if (dMid > 0.0) { | |
tA = tMid; | |
} else { | |
tB = tMid; | |
} | |
} | |
return (tA + tB) * 0.5; | |
} | |
void main() { | |
// Compute normalized screen coordinates (centered on 0) | |
vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y; | |
// Use the mouse to control the cameraβs azimuth and pitch. | |
// Horizontal movement rotates 0β2Ο; vertical movement adjusts pitch. | |
float angle = iMouse.x / iResolution.x * 6.28318; // full rotation | |
float pitch = mix(0.4, 1.2, iMouse.y / iResolution.y); | |
float radius = 4.0; | |
vec3 ro = vec3( | |
radius * cos(pitch) * cos(angle), | |
radius * sin(pitch), | |
radius * cos(pitch) * sin(angle) | |
); | |
vec3 target = vec3(0.0, 0.0, 0.0); | |
// Construct a simple camera coordinate system. | |
vec3 forward = normalize(target - ro); | |
vec3 right = normalize(cross(forward, vec3(0.0, 1.0, 0.0))); | |
vec3 up = cross(right, forward); | |
// Compute the ray direction using a basic perspective projection. | |
vec3 rd = normalize(forward + uv.x * right + uv.y * up); | |
// March the ray to see if and where it hits the vibrating plate. | |
float tHit = raycast(ro, rd, iTime); | |
vec3 color; | |
if (tHit > 0.0) { | |
vec3 pos = ro + rd * tHit; | |
// Get the local normal from the heightfield | |
vec3 normal = calcNormal(vec2(pos.x, pos.z), iTime); | |
// Standard lighting: diffuse + specular | |
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.8)); | |
float diff = max(dot(normal, lightDir), 0.0); | |
vec3 viewDir = normalize(ro - pos); | |
vec3 halfDir = normalize(lightDir + viewDir); | |
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0); | |
// Base color comes from the palette β modulated by the local vibration amplitude. | |
float h = plate(vec2(pos.x, pos.z), iTime); | |
vec3 baseColor = palette(h * 5.0); | |
color = baseColor * diff + vec3(0.1) * spec + vec3(0.1); | |
} else { | |
// If no hit, use a subtle background gradient. | |
color = mix(vec3(0.0, 0.0, 0.1), vec3(0.0), uv.y + 0.5); | |
} | |
outColor = vec4(color, 1.0); | |
} | |
`; | |
//ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
// Create and compile the shader program using webgl-utils. | |
const program = webglUtils.createProgramFromSources(gl, [vs, fs]); | |
// Look up attribute and uniform locations. | |
const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); | |
const resolutionLocation = gl.getUniformLocation(program, "iResolution"); | |
const mouseLocation = gl.getUniformLocation(program, "iMouse"); | |
const timeLocation = gl.getUniformLocation(program, "iTime"); | |
// Create a vertex array object (VAO) and bind it. | |
const vao = gl.createVertexArray(); | |
gl.bindVertexArray(vao); | |
// Create a buffer and put a fullβscreen quad in it. | |
const positionBuffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
gl.bufferData( | |
gl.ARRAY_BUFFER, | |
new Float32Array([ | |
-1, -1, | |
1, -1, | |
-1, 1, | |
-1, 1, | |
1, -1, | |
1, 1, | |
]), | |
gl.STATIC_DRAW | |
); | |
// Enable the position attribute. | |
gl.enableVertexAttribArray(positionAttributeLocation); | |
gl.vertexAttribPointer( | |
positionAttributeLocation, | |
2, // 2 components per vertex | |
gl.FLOAT, // data type is float | |
false, | |
0, | |
0 | |
); | |
//ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
// Setup mouse / touch interactions. | |
const playpauseElem = document.querySelector(".playpause"); | |
const inputElem = document.querySelector(".divcanvas"); | |
inputElem.addEventListener("mouseover", requestFrame); | |
inputElem.addEventListener("mouseout", cancelFrame); | |
let mouseX = 0; | |
let mouseY = 0; | |
function setMousePosition(e) { | |
const rect = inputElem.getBoundingClientRect(); | |
mouseX = e.clientX - rect.left; | |
mouseY = rect.height - (e.clientY - rect.top) - 1; | |
} | |
inputElem.addEventListener("mousemove", setMousePosition); | |
inputElem.addEventListener("touchstart", (e) => { | |
e.preventDefault(); | |
playpauseElem.classList.add("playpausehide"); | |
requestFrame(); | |
}, { passive: false }); | |
inputElem.addEventListener("touchmove", (e) => { | |
e.preventDefault(); | |
setMousePosition(e.touches[0]); | |
}, { passive: false }); | |
inputElem.addEventListener("touchend", (e) => { | |
e.preventDefault(); | |
playpauseElem.classList.remove("playpausehide"); | |
cancelFrame(); | |
}, { passive: false }); | |
//ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
// Animation loop variables and functions. | |
let requestId; | |
function requestFrame() { | |
if (!requestId) { | |
requestId = requestAnimationFrame(render); | |
} | |
} | |
function cancelFrame() { | |
if (requestId) { | |
cancelAnimationFrame(requestId); | |
requestId = undefined; | |
} | |
} | |
let then = 0; | |
let time = 0; | |
function render(now) { | |
requestId = undefined; | |
now *= 0.001; // convert milliseconds to seconds | |
const elapsedTime = Math.min(now - then, 0.1); | |
time += elapsedTime; | |
then = now; | |
// Resize canvas if needed. | |
webglUtils.resizeCanvasToDisplaySize(gl.canvas); | |
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); | |
// Use our program and bind our VAO. | |
gl.useProgram(program); | |
gl.bindVertexArray(vao); | |
// Set the uniforms. | |
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height); | |
gl.uniform2f(mouseLocation, mouseX, mouseY); | |
gl.uniform1f(timeLocation, time); | |
// Draw the fullβscreen quad. | |
gl.drawArrays(gl.TRIANGLES, 0, 6); | |
requestFrame(); | |
} | |
requestFrame(); | |
requestAnimationFrame(cancelFrame); | |
} | |
main(); | |