webgl_demo / test.js
Severian's picture
Update test.js
337fbee verified
raw
history blame
10 kB
"use strict";
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();