webgl_demo / test.js
Severian's picture
Update test.js
993eabd verified
raw
history blame
8.77 kB
<!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">
"use strict";
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>