Spaces:
Running
Running
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>3D Cymatic Display in WebGL2</title> | |
<style> | |
body, html { margin: 0; height: 100%; overflow: hidden; background: #000; } | |
canvas { width: 100%; height: 100%; display: block; } | |
</style> | |
</head> | |
<body> | |
<canvas id="canvas"></canvas> | |
<!-- Include glMatrix and webgl-utils scripts --> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/gl-matrix-min.js"></script> | |
<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script> | |
<script type="text/javascript"> | |
; | |
function main() { | |
const canvas = document.querySelector("#canvas"); | |
const gl = canvas.getContext("webgl2"); | |
if (!gl) { | |
console.error("WebGL 2 not supported"); | |
return; | |
} | |
// Vertex shader: displaces a grid using two vibrational modes and computes normals | |
const vsSource = `#version 300 es | |
precision highp float; | |
in vec3 a_position; | |
uniform float u_time; | |
uniform mat4 u_MVP; | |
out vec3 v_normal; | |
out vec3 v_position; | |
const float PI = 3.14159; | |
void main() { | |
// Amplitudes for two modes | |
float A1 = 0.1; | |
float A2 = 0.05; | |
// Mode 1: Fundamental vibration (nodal lines at the boundaries) | |
float mode1 = A1 * sin(PI * (a_position.x + 1.0) / 2.0) | |
* sin(PI * (a_position.y + 1.0) / 2.0) | |
* cos(3.0 * u_time); | |
// Mode 2: A higher-order vibration for additional detail | |
float mode2 = A2 * sin(2.0 * PI * (a_position.x + 1.0) / 2.0) | |
* sin(PI * (a_position.y + 1.0) / 2.0) | |
* cos(5.0 * u_time); | |
float z = mode1 + mode2; | |
// Compute partial derivatives for normal calculation | |
float dx1 = A1 * (PI/2.0) * cos(PI*(a_position.x+1.0)/2.0) | |
* sin(PI*(a_position.y+1.0)/2.0) * cos(3.0 * u_time); | |
float dx2 = A2 * (2.0*PI/2.0) * cos(2.0*PI*(a_position.x+1.0)/2.0) | |
* sin(PI*(a_position.y+1.0)/2.0) * cos(5.0 * u_time); | |
float dfdx = dx1 + dx2; | |
float dy1 = A1 * (PI/2.0) * sin(PI*(a_position.x+1.0)/2.0) | |
* cos(PI*(a_position.y+1.0)/2.0) * cos(3.0 * u_time); | |
float dy2 = A2 * (PI/2.0) * sin(2.0*PI*(a_position.x+1.0)/2.0) | |
* cos(PI*(a_position.y+1.0)/2.0) * cos(5.0 * u_time); | |
float dfdy = dy1 + dy2; | |
vec3 displacedPos = vec3(a_position.x, a_position.y, z); | |
// The normal is computed as the normalized cross of the tangent derivatives. | |
// For a height field, an approximate normal is: (-dfdx, -dfdy, 1) | |
vec3 normal = normalize(vec3(-dfdx, -dfdy, 1.0)); | |
v_normal = normal; | |
v_position = displacedPos; | |
gl_Position = u_MVP * vec4(displacedPos, 1.0); | |
}`; | |
// Fragment shader: uses Phong shading for realistic lighting | |
const fsSource = `#version 300 es | |
precision highp float; | |
in vec3 v_normal; | |
in vec3 v_position; | |
uniform vec3 u_lightPos; | |
uniform vec3 u_viewPos; | |
out vec4 outColor; | |
void main() { | |
vec3 normal = normalize(v_normal); | |
vec3 lightDir = normalize(u_lightPos - v_position); | |
float diff = max(dot(normal, lightDir), 0.0); | |
vec3 ambient = vec3(0.2); | |
vec3 diffuse = diff * vec3(0.7, 0.7, 0.8); | |
vec3 viewDir = normalize(u_viewPos - v_position); | |
vec3 reflectDir = reflect(-lightDir, normal); | |
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); | |
vec3 specular = vec3(0.3) * spec; | |
vec3 color = ambient + diffuse + specular; | |
outColor = vec4(color, 1.0); | |
}`; | |
// Create the shader program using webgl-utils | |
const program = webglUtils.createProgramFromSources(gl, [vsSource, fsSource]); | |
// Look up attribute and uniform locations | |
const positionAttribLocation = gl.getAttribLocation(program, "a_position"); | |
const timeUniformLocation = gl.getUniformLocation(program, "u_time"); | |
const mvpUniformLocation = gl.getUniformLocation(program, "u_MVP"); | |
const lightPosUniformLocation = gl.getUniformLocation(program, "u_lightPos"); | |
const viewPosUniformLocation = gl.getUniformLocation(program, "u_viewPos"); | |
// Create a grid covering [-1,1]x[-1,1] | |
const gridSize = 150; | |
const positions = []; | |
for (let j = 0; j <= gridSize; j++) { | |
for (let i = 0; i <= gridSize; i++) { | |
// Map grid coordinates to [-1,1] | |
let x = (i / gridSize) * 2 - 1; | |
let y = (j / gridSize) * 2 - 1; | |
positions.push(x, y, 0); | |
} | |
} | |
// Generate indices for triangles | |
const indices = []; | |
for (let j = 0; j < gridSize; j++) { | |
for (let i = 0; i < gridSize; i++) { | |
let a = j * (gridSize + 1) + i; | |
let b = a + 1; | |
let c = a + (gridSize + 1); | |
let d = c + 1; | |
indices.push(a, b, c); | |
indices.push(b, d, c); | |
} | |
} | |
// Create and bind a Vertex Array Object | |
const vao = gl.createVertexArray(); | |
gl.bindVertexArray(vao); | |
// Create and fill the position buffer | |
const positionBuffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(positionAttribLocation); | |
gl.vertexAttribPointer(positionAttribLocation, 3, gl.FLOAT, false, 0, 0); | |
// Create and fill the index buffer | |
const indexBuffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); | |
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW); | |
// Set up camera matrices using glMatrix | |
const fieldOfView = 45 * Math.PI / 180; | |
const near = 0.1; | |
const far = 100; | |
let projectionMatrix = glMatrix.mat4.create(); | |
let viewMatrix = glMatrix.mat4.create(); | |
let modelMatrix = glMatrix.mat4.create(); | |
const eye = [0, -2.5, 2.5]; // Camera position | |
const center = [0, 0, 0]; // Look at center of plate | |
const up = [0, 0, 1]; | |
glMatrix.mat4.perspective(projectionMatrix, fieldOfView, canvas.clientWidth / canvas.clientHeight, near, far); | |
glMatrix.mat4.lookAt(viewMatrix, eye, center, up); | |
// Optionally, rotate the model slightly for a dramatic view | |
glMatrix.mat4.rotateX(modelMatrix, modelMatrix, -0.5); | |
let mvpMatrix = glMatrix.mat4.create(); | |
glMatrix.mat4.multiply(mvpMatrix, viewMatrix, modelMatrix); | |
glMatrix.mat4.multiply(mvpMatrix, projectionMatrix, mvpMatrix); | |
// Set light and view positions (for the shader) | |
const lightPos = [2.0, -2.0, 3.0]; | |
const viewPos = eye; | |
// Animation loop | |
let startTime = null; | |
function render(now) { | |
if (!startTime) startTime = now; | |
const timeInSeconds = (now - startTime) * 0.001; | |
// Resize canvas if needed | |
webglUtils.resizeCanvasToDisplaySize(gl.canvas); | |
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); | |
gl.enable(gl.DEPTH_TEST); | |
gl.clearColor(0.0, 0.0, 0.0, 1.0); | |
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); | |
// Update the projection in case the canvas size changed | |
glMatrix.mat4.perspective(projectionMatrix, fieldOfView, gl.canvas.clientWidth / gl.canvas.clientHeight, near, far); | |
glMatrix.mat4.lookAt(viewMatrix, eye, center, up); | |
glMatrix.mat4.multiply(mvpMatrix, viewMatrix, modelMatrix); | |
glMatrix.mat4.multiply(mvpMatrix, projectionMatrix, mvpMatrix); | |
// Use our program and bind the VAO | |
gl.useProgram(program); | |
gl.bindVertexArray(vao); | |
// Set shader uniforms | |
gl.uniform1f(timeUniformLocation, timeInSeconds); | |
gl.uniformMatrix4fv(mvpUniformLocation, false, mvpMatrix); | |
gl.uniform3fv(lightPosUniformLocation, lightPos); | |
gl.uniform3fv(viewPosUniformLocation, viewPos); | |
// Draw the grid | |
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0); | |
requestAnimationFrame(render); | |
} | |
requestAnimationFrame(render); | |
} | |
main(); | |
</script> | |
</body> | |
</html> | |