JavaScript WebGL Graphics: 3D Rendering, Shaders, and Interactive Graphics
Master WebGL with JavaScript for 3D graphics, shader programming, and interactive visualizations. Learn graphics programming and GPU optimization.
WebGL (Web Graphics Library) is a JavaScript API for rendering interactive 3D and 2D graphics within web browsers. It provides direct access to the GPU through OpenGL ES, enabling high-performance graphics applications. This comprehensive guide covers WebGL fundamentals, shader programming, and building interactive graphics applications.
WebGL Fundamentals
WebGL Context and Basic Setup
// WebGL Context Manager
class WebGLManager {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
if (!this.canvas) {
throw new Error(`Canvas with id "${canvasId}" not found`);
}
// Get WebGL context
this.gl =
this.canvas.getContext('webgl') ||
this.canvas.getContext('experimental-webgl');
if (!this.gl) {
throw new Error('WebGL not supported');
}
// Set up canvas dimensions
this.resizeCanvas();
// Initialize WebGL state
this.initWebGL();
// Track resources for cleanup
this.resources = {
programs: [],
buffers: [],
textures: [],
framebuffers: [],
};
// Bind resize handler
window.addEventListener('resize', () => this.resizeCanvas());
}
initWebGL() {
const gl = this.gl;
// Set clear color
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// Enable depth testing
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
// Enable blending
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Set viewport
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
}
resizeCanvas() {
const displayWidth = this.canvas.clientWidth;
const displayHeight = this.canvas.clientHeight;
if (
this.canvas.width !== displayWidth ||
this.canvas.height !== displayHeight
) {
this.canvas.width = displayWidth;
this.canvas.height = displayHeight;
if (this.gl) {
this.gl.viewport(0, 0, displayWidth, displayHeight);
}
}
}
// Shader compilation utilities
createShader(type, source) {
const gl = this.gl;
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const error = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error(`Shader compilation error: ${error}`);
}
return shader;
}
createProgram(vertexShaderSource, fragmentShaderSource) {
const gl = this.gl;
const vertexShader = this.createShader(
gl.VERTEX_SHADER,
vertexShaderSource
);
const fragmentShader = this.createShader(
gl.FRAGMENT_SHADER,
fragmentShaderSource
);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const error = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error(`Program linking error: ${error}`);
}
// Clean up shaders
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
// Track program for cleanup
this.resources.programs.push(program);
return program;
}
// Buffer utilities
createBuffer(data, usage = this.gl.STATIC_DRAW) {
const gl = this.gl;
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), usage);
// Track buffer for cleanup
this.resources.buffers.push(buffer);
return buffer;
}
createElementBuffer(data, usage = this.gl.STATIC_DRAW) {
const gl = this.gl;
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(data), usage);
// Track buffer for cleanup
this.resources.buffers.push(buffer);
return buffer;
}
// Texture utilities
createTexture(image, options = {}) {
const gl = this.gl;
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set texture parameters
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_S,
options.wrapS || gl.CLAMP_TO_EDGE
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_T,
options.wrapT || gl.CLAMP_TO_EDGE
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MIN_FILTER,
options.minFilter || gl.LINEAR
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MAG_FILTER,
options.magFilter || gl.LINEAR
);
// Upload image data
if (image) {
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
image
);
// Generate mipmaps if power of 2
if (this.isPowerOf2(image.width) && this.isPowerOf2(image.height)) {
gl.generateMipmap(gl.TEXTURE_2D);
}
}
// Track texture for cleanup
this.resources.textures.push(texture);
return texture;
}
isPowerOf2(value) {
return (value & (value - 1)) === 0;
}
// Cleanup resources
cleanup() {
const gl = this.gl;
// Delete programs
this.resources.programs.forEach((program) => {
gl.deleteProgram(program);
});
// Delete buffers
this.resources.buffers.forEach((buffer) => {
gl.deleteBuffer(buffer);
});
// Delete textures
this.resources.textures.forEach((texture) => {
gl.deleteTexture(texture);
});
// Delete framebuffers
this.resources.framebuffers.forEach((framebuffer) => {
gl.deleteFramebuffer(framebuffer);
});
// Clear tracking arrays
Object.keys(this.resources).forEach((key) => {
this.resources[key] = [];
});
}
}
// Matrix utilities for 3D transformations
class Matrix4 {
constructor() {
this.elements = new Float32Array([
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
]);
}
static identity() {
return new Matrix4();
}
static perspective(fov, aspect, near, far) {
const matrix = new Matrix4();
const f = 1.0 / Math.tan(fov / 2);
const rangeInv = 1.0 / (near - far);
matrix.elements[0] = f / aspect;
matrix.elements[5] = f;
matrix.elements[10] = (near + far) * rangeInv;
matrix.elements[11] = -1;
matrix.elements[14] = near * far * rangeInv * 2;
matrix.elements[15] = 0;
return matrix;
}
static lookAt(eye, center, up) {
const matrix = new Matrix4();
const f = this.normalize(this.subtract(center, eye));
const s = this.normalize(this.cross(f, up));
const u = this.cross(s, f);
matrix.elements[0] = s[0];
matrix.elements[1] = u[0];
matrix.elements[2] = -f[0];
matrix.elements[4] = s[1];
matrix.elements[5] = u[1];
matrix.elements[6] = -f[1];
matrix.elements[8] = s[2];
matrix.elements[9] = u[2];
matrix.elements[10] = -f[2];
matrix.elements[12] = -this.dot(s, eye);
matrix.elements[13] = -this.dot(u, eye);
matrix.elements[14] = this.dot(f, eye);
return matrix;
}
translate(x, y, z) {
const translation = new Matrix4();
translation.elements[12] = x;
translation.elements[13] = y;
translation.elements[14] = z;
return this.multiply(translation);
}
rotateX(angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const rotation = new Matrix4();
rotation.elements[5] = cos;
rotation.elements[6] = sin;
rotation.elements[9] = -sin;
rotation.elements[10] = cos;
return this.multiply(rotation);
}
rotateY(angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const rotation = new Matrix4();
rotation.elements[0] = cos;
rotation.elements[2] = -sin;
rotation.elements[8] = sin;
rotation.elements[10] = cos;
return this.multiply(rotation);
}
rotateZ(angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const rotation = new Matrix4();
rotation.elements[0] = cos;
rotation.elements[1] = sin;
rotation.elements[4] = -sin;
rotation.elements[5] = cos;
return this.multiply(rotation);
}
scale(x, y, z) {
const scaling = new Matrix4();
scaling.elements[0] = x;
scaling.elements[5] = y;
scaling.elements[10] = z;
return this.multiply(scaling);
}
multiply(other) {
const result = new Matrix4();
const a = this.elements;
const b = other.elements;
const c = result.elements;
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
c[i * 4 + j] =
a[i * 4 + 0] * b[0 * 4 + j] +
a[i * 4 + 1] * b[1 * 4 + j] +
a[i * 4 + 2] * b[2 * 4 + j] +
a[i * 4 + 3] * b[3 * 4 + j];
}
}
return result;
}
// Vector utilities
static normalize(v) {
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
return [v[0] / length, v[1] / length, v[2] / length];
}
static subtract(a, b) {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}
static cross(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
}
static dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
}
// Simple 3D renderer
class SimpleRenderer {
constructor(webglManager) {
this.gl = webglManager.gl;
this.webglManager = webglManager;
// Shader sources
this.vertexShaderSource = `
attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord;
attribute vec3 a_normal;
uniform mat4 u_modelViewMatrix;
uniform mat4 u_projectionMatrix;
uniform mat4 u_normalMatrix;
varying vec4 v_color;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_position;
void main() {
gl_Position = u_projectionMatrix * u_modelViewMatrix * a_position;
v_color = a_color;
v_texCoord = a_texCoord;
v_normal = mat3(u_normalMatrix) * a_normal;
v_position = vec3(u_modelViewMatrix * a_position);
}
`;
this.fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
uniform vec3 u_lightDirection;
uniform vec3 u_lightColor;
uniform vec3 u_ambientColor;
uniform bool u_useTexture;
varying vec4 v_color;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_position;
void main() {
vec3 normal = normalize(v_normal);
vec3 lightDir = normalize(-u_lightDirection);
float diffuse = max(dot(normal, lightDir), 0.0);
vec3 lighting = u_ambientColor + (u_lightColor * diffuse);
vec4 texColor = u_useTexture ? texture2D(u_texture, v_texCoord) : vec4(1.0);
gl_FragColor = vec4(v_color.rgb * texColor.rgb * lighting, v_color.a * texColor.a);
}
`;
// Create shader program
this.program = this.webglManager.createProgram(
this.vertexShaderSource,
this.fragmentShaderSource
);
// Get attribute and uniform locations
this.locations = {
attributes: {
position: this.gl.getAttribLocation(this.program, 'a_position'),
color: this.gl.getAttribLocation(this.program, 'a_color'),
texCoord: this.gl.getAttribLocation(this.program, 'a_texCoord'),
normal: this.gl.getAttribLocation(this.program, 'a_normal'),
},
uniforms: {
modelViewMatrix: this.gl.getUniformLocation(
this.program,
'u_modelViewMatrix'
),
projectionMatrix: this.gl.getUniformLocation(
this.program,
'u_projectionMatrix'
),
normalMatrix: this.gl.getUniformLocation(
this.program,
'u_normalMatrix'
),
texture: this.gl.getUniformLocation(this.program, 'u_texture'),
lightDirection: this.gl.getUniformLocation(
this.program,
'u_lightDirection'
),
lightColor: this.gl.getUniformLocation(this.program, 'u_lightColor'),
ambientColor: this.gl.getUniformLocation(
this.program,
'u_ambientColor'
),
useTexture: this.gl.getUniformLocation(this.program, 'u_useTexture'),
},
};
// Set up projection matrix
this.projectionMatrix = Matrix4.perspective(
Math.PI / 4, // 45 degrees FOV
this.gl.canvas.width / this.gl.canvas.height,
0.1,
100.0
);
// Set up lighting
this.lightDirection = [0.5, 0.7, 1.0];
this.lightColor = [1.0, 1.0, 1.0];
this.ambientColor = [0.2, 0.2, 0.2];
}
render(objects) {
const gl = this.gl;
// Clear canvas
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Use shader program
gl.useProgram(this.program);
// Set projection matrix
gl.uniformMatrix4fv(
this.locations.uniforms.projectionMatrix,
false,
this.projectionMatrix.elements
);
// Set lighting uniforms
gl.uniform3fv(this.locations.uniforms.lightDirection, this.lightDirection);
gl.uniform3fv(this.locations.uniforms.lightColor, this.lightColor);
gl.uniform3fv(this.locations.uniforms.ambientColor, this.ambientColor);
// Render each object
objects.forEach((object) => {
this.renderObject(object);
});
}
renderObject(object) {
const gl = this.gl;
// Set model-view matrix
gl.uniformMatrix4fv(
this.locations.uniforms.modelViewMatrix,
false,
object.modelViewMatrix.elements
);
// Set normal matrix (inverse transpose of model-view)
gl.uniformMatrix4fv(
this.locations.uniforms.normalMatrix,
false,
object.normalMatrix.elements
);
// Bind and set up vertex attributes
if (object.positionBuffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, object.positionBuffer);
gl.enableVertexAttribArray(this.locations.attributes.position);
gl.vertexAttribPointer(
this.locations.attributes.position,
3,
gl.FLOAT,
false,
0,
0
);
}
if (object.colorBuffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, object.colorBuffer);
gl.enableVertexAttribArray(this.locations.attributes.color);
gl.vertexAttribPointer(
this.locations.attributes.color,
4,
gl.FLOAT,
false,
0,
0
);
}
if (object.texCoordBuffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, object.texCoordBuffer);
gl.enableVertexAttribArray(this.locations.attributes.texCoord);
gl.vertexAttribPointer(
this.locations.attributes.texCoord,
2,
gl.FLOAT,
false,
0,
0
);
}
if (object.normalBuffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, object.normalBuffer);
gl.enableVertexAttribArray(this.locations.attributes.normal);
gl.vertexAttribPointer(
this.locations.attributes.normal,
3,
gl.FLOAT,
false,
0,
0
);
}
// Set texture
if (object.texture) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, object.texture);
gl.uniform1i(this.locations.uniforms.texture, 0);
gl.uniform1i(this.locations.uniforms.useTexture, 1);
} else {
gl.uniform1i(this.locations.uniforms.useTexture, 0);
}
// Draw
if (object.indexBuffer) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, object.indexBuffer);
gl.drawElements(gl.TRIANGLES, object.indexCount, gl.UNSIGNED_SHORT, 0);
} else {
gl.drawArrays(gl.TRIANGLES, 0, object.vertexCount);
}
}
}
// 3D Object class
class Object3D {
constructor(webglManager, geometry, options = {}) {
this.webglManager = webglManager;
this.gl = webglManager.gl;
// Transform properties
this.position = options.position || [0, 0, 0];
this.rotation = options.rotation || [0, 0, 0];
this.scale = options.scale || [1, 1, 1];
// Create buffers from geometry
this.createBuffers(geometry);
// Load texture if provided
if (options.textureUrl) {
this.loadTexture(options.textureUrl);
}
// Initialize matrices
this.modelViewMatrix = Matrix4.identity();
this.normalMatrix = Matrix4.identity();
this.updateTransform();
}
createBuffers(geometry) {
// Position buffer
if (geometry.vertices) {
this.positionBuffer = this.webglManager.createBuffer(geometry.vertices);
this.vertexCount = geometry.vertices.length / 3;
}
// Color buffer
if (geometry.colors) {
this.colorBuffer = this.webglManager.createBuffer(geometry.colors);
}
// Texture coordinate buffer
if (geometry.texCoords) {
this.texCoordBuffer = this.webglManager.createBuffer(geometry.texCoords);
}
// Normal buffer
if (geometry.normals) {
this.normalBuffer = this.webglManager.createBuffer(geometry.normals);
}
// Index buffer
if (geometry.indices) {
this.indexBuffer = this.webglManager.createElementBuffer(
geometry.indices
);
this.indexCount = geometry.indices.length;
}
}
loadTexture(url) {
const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = () => {
this.texture = this.webglManager.createTexture(image);
};
image.src = url;
}
updateTransform() {
// Create model matrix
this.modelViewMatrix = Matrix4.identity()
.translate(this.position[0], this.position[1], this.position[2])
.rotateX(this.rotation[0])
.rotateY(this.rotation[1])
.rotateZ(this.rotation[2])
.scale(this.scale[0], this.scale[1], this.scale[2]);
// Normal matrix is the inverse transpose of the model-view matrix
// For simplicity, we'll use the model matrix itself (works for uniform scaling)
this.normalMatrix = Matrix4.identity()
.rotateX(this.rotation[0])
.rotateY(this.rotation[1])
.rotateZ(this.rotation[2]);
}
setPosition(x, y, z) {
this.position = [x, y, z];
this.updateTransform();
}
setRotation(x, y, z) {
this.rotation = [x, y, z];
this.updateTransform();
}
setScale(x, y, z) {
this.scale = [x, y, z];
this.updateTransform();
}
translate(x, y, z) {
this.position[0] += x;
this.position[1] += y;
this.position[2] += z;
this.updateTransform();
}
rotate(x, y, z) {
this.rotation[0] += x;
this.rotation[1] += y;
this.rotation[2] += z;
this.updateTransform();
}
}
// Geometry utilities
class GeometryUtils {
static createCube(size = 1) {
const s = size / 2;
return {
vertices: [
// Front face
-s,
-s,
s,
s,
-s,
s,
s,
s,
s,
-s,
s,
s,
// Back face
-s,
-s,
-s,
-s,
s,
-s,
s,
s,
-s,
s,
-s,
-s,
// Top face
-s,
s,
-s,
-s,
s,
s,
s,
s,
s,
s,
s,
-s,
// Bottom face
-s,
-s,
-s,
s,
-s,
-s,
s,
-s,
s,
-s,
-s,
s,
// Right face
s,
-s,
-s,
s,
s,
-s,
s,
s,
s,
s,
-s,
s,
// Left face
-s,
-s,
-s,
-s,
-s,
s,
-s,
s,
s,
-s,
s,
-s,
],
colors: [
// Front face - red
1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0,
0.0, 1.0,
// Back face - green
0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0,
0.0, 1.0,
// Top face - blue
0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0,
1.0, 1.0,
// Bottom face - yellow
1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0,
0.0, 1.0,
// Right face - purple
1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0,
1.0, 1.0,
// Left face - cyan
0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0,
1.0, 1.0,
],
normals: [
// Front face
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
// Back face
0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0,
// Top face
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
// Bottom face
0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0,
// Right face
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,
// Left face
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0,
],
indices: [
0,
1,
2,
0,
2,
3, // front
4,
5,
6,
4,
6,
7, // back
8,
9,
10,
8,
10,
11, // top
12,
13,
14,
12,
14,
15, // bottom
16,
17,
18,
16,
18,
19, // right
20,
21,
22,
20,
22,
23, // left
],
};
}
static createSphere(radius = 1, segments = 16) {
const vertices = [];
const colors = [];
const normals = [];
const texCoords = [];
const indices = [];
// Generate vertices
for (let lat = 0; lat <= segments; lat++) {
const theta = (lat * Math.PI) / segments;
const sinTheta = Math.sin(theta);
const cosTheta = Math.cos(theta);
for (let lon = 0; lon <= segments; lon++) {
const phi = (lon * 2 * Math.PI) / segments;
const sinPhi = Math.sin(phi);
const cosPhi = Math.cos(phi);
const x = cosPhi * sinTheta;
const y = cosTheta;
const z = sinPhi * sinTheta;
vertices.push(radius * x, radius * y, radius * z);
normals.push(x, y, z);
const u = 1 - lon / segments;
const v = 1 - lat / segments;
texCoords.push(u, v);
// Color based on position
colors.push(Math.abs(x), Math.abs(y), Math.abs(z), 1.0);
}
}
// Generate indices
for (let lat = 0; lat < segments; lat++) {
for (let lon = 0; lon < segments; lon++) {
const first = lat * (segments + 1) + lon;
const second = first + segments + 1;
indices.push(first, second, first + 1);
indices.push(second, second + 1, first + 1);
}
}
return { vertices, colors, normals, texCoords, indices };
}
static createPlane(width = 1, height = 1) {
const w = width / 2;
const h = height / 2;
return {
vertices: [-w, -h, 0, w, -h, 0, w, h, 0, -w, h, 0],
colors: [
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0,
],
normals: [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
texCoords: [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0],
indices: [0, 1, 2, 0, 2, 3],
};
}
}
// Usage example
const canvas = document.createElement('canvas');
canvas.id = 'webgl-canvas';
canvas.width = 800;
canvas.height = 600;
canvas.style.border = '1px solid #ccc';
document.body.appendChild(canvas);
// Initialize WebGL
const webglManager = new WebGLManager('webgl-canvas');
const renderer = new SimpleRenderer(webglManager);
// Create 3D objects
const cube = new Object3D(webglManager, GeometryUtils.createCube(1), {
position: [-2, 0, -5],
});
const sphere = new Object3D(webglManager, GeometryUtils.createSphere(1, 20), {
position: [2, 0, -5],
});
const plane = new Object3D(webglManager, GeometryUtils.createPlane(4, 4), {
position: [0, -2, -5],
rotation: [-Math.PI / 2, 0, 0],
});
// Animation loop
let time = 0;
function animate() {
time += 0.016; // ~60 FPS
// Rotate cube
cube.setRotation(time, time * 0.7, 0);
// Move sphere
sphere.setPosition(2 + Math.sin(time) * 0.5, Math.cos(time * 0.5) * 0.5, -5);
// Render scene
renderer.render([cube, sphere, plane]);
requestAnimationFrame(animate);
}
animate();
Advanced Shader Programming
Vertex and Fragment Shaders
// Advanced Shader Manager
class ShaderManager {
constructor(webglManager) {
this.gl = webglManager.gl;
this.webglManager = webglManager;
this.shaders = new Map();
this.programs = new Map();
}
// Phong lighting shader
createPhongShader() {
const vertexShader = `
attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec2 a_texCoord;
uniform mat4 u_modelMatrix;
uniform mat4 u_viewMatrix;
uniform mat4 u_projectionMatrix;
uniform mat4 u_normalMatrix;
varying vec3 v_worldPosition;
varying vec3 v_normal;
varying vec2 v_texCoord;
void main() {
vec4 worldPosition = u_modelMatrix * vec4(a_position, 1.0);
v_worldPosition = worldPosition.xyz;
v_normal = normalize(mat3(u_normalMatrix) * a_normal);
v_texCoord = a_texCoord;
gl_Position = u_projectionMatrix * u_viewMatrix * worldPosition;
}
`;
const fragmentShader = `
precision mediump float;
uniform vec3 u_lightPosition;
uniform vec3 u_lightColor;
uniform vec3 u_viewPosition;
uniform vec3 u_materialAmbient;
uniform vec3 u_materialDiffuse;
uniform vec3 u_materialSpecular;
uniform float u_materialShininess;
uniform sampler2D u_texture;
uniform bool u_useTexture;
varying vec3 v_worldPosition;
varying vec3 v_normal;
varying vec2 v_texCoord;
void main() {
vec3 normal = normalize(v_normal);
vec3 lightDir = normalize(u_lightPosition - v_worldPosition);
vec3 viewDir = normalize(u_viewPosition - v_worldPosition);
vec3 reflectDir = reflect(-lightDir, normal);
// Ambient
vec3 ambient = u_materialAmbient * u_lightColor;
// Diffuse
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * u_materialDiffuse * u_lightColor;
// Specular
float spec = pow(max(dot(viewDir, reflectDir), 0.0), u_materialShininess);
vec3 specular = spec * u_materialSpecular * u_lightColor;
vec3 result = ambient + diffuse + specular;
if (u_useTexture) {
vec4 texColor = texture2D(u_texture, v_texCoord);
result *= texColor.rgb;
}
gl_FragColor = vec4(result, 1.0);
}
`;
const program = this.webglManager.createProgram(
vertexShader,
fragmentShader
);
this.programs.set('phong', program);
return program;
}
// Particle system shader
createParticleShader() {
const vertexShader = `
attribute vec3 a_position;
attribute vec3 a_velocity;
attribute float a_size;
attribute float a_life;
attribute vec4 a_color;
uniform mat4 u_viewMatrix;
uniform mat4 u_projectionMatrix;
uniform float u_time;
uniform vec3 u_gravity;
varying vec4 v_color;
varying float v_life;
void main() {
float t = u_time - a_life;
vec3 position = a_position + a_velocity * t + 0.5 * u_gravity * t * t;
vec4 viewPosition = u_viewMatrix * vec4(position, 1.0);
gl_Position = u_projectionMatrix * viewPosition;
gl_PointSize = a_size * (1.0 / length(viewPosition.xyz));
v_color = a_color;
v_life = clamp(1.0 - t / 5.0, 0.0, 1.0); // 5 second lifetime
}
`;
const fragmentShader = `
precision mediump float;
uniform sampler2D u_particleTexture;
varying vec4 v_color;
varying float v_life;
void main() {
vec2 coords = gl_PointCoord;
float alpha = texture2D(u_particleTexture, coords).a;
alpha *= v_life;
gl_FragColor = vec4(v_color.rgb, alpha * v_color.a);
}
`;
const program = this.webglManager.createProgram(
vertexShader,
fragmentShader
);
this.programs.set('particle', program);
return program;
}
// Post-processing shader
createPostProcessShader() {
const vertexShader = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
`;
const fragmentShader = `
precision mediump float;
uniform sampler2D u_colorTexture;
uniform sampler2D u_depthTexture;
uniform vec2 u_resolution;
uniform float u_time;
varying vec2 v_texCoord;
// Blur function
vec4 blur(sampler2D tex, vec2 uv, vec2 resolution, float radius) {
vec4 color = vec4(0.0);
vec2 texelSize = 1.0 / resolution;
for (int x = -2; x <= 2; x++) {
for (int y = -2; y <= 2; y++) {
vec2 offset = vec2(float(x), float(y)) * texelSize * radius;
color += texture2D(tex, uv + offset);
}
}
return color / 25.0;
}
// Vignette effect
float vignette(vec2 uv, float intensity) {
vec2 center = vec2(0.5);
float dist = distance(uv, center);
return 1.0 - smoothstep(0.0, 0.8, dist * intensity);
}
void main() {
vec2 uv = v_texCoord;
vec4 color = texture2D(u_colorTexture, uv);
// Apply blur
vec4 blurred = blur(u_colorTexture, uv, u_resolution, 1.0);
// Mix original and blurred based on depth
float depth = texture2D(u_depthTexture, uv).r;
float blurAmount = smoothstep(0.1, 0.9, depth);
color = mix(color, blurred, blurAmount * 0.3);
// Apply vignette
float vig = vignette(uv, 1.2);
color.rgb *= vig;
// Color grading
color.rgb = pow(color.rgb, vec3(0.8)); // Gamma correction
color.rgb = mix(color.rgb, vec3(dot(color.rgb, vec3(0.299, 0.587, 0.114))), 0.1); // Slight desaturation
gl_FragColor = color;
}
`;
const program = this.webglManager.createProgram(
vertexShader,
fragmentShader
);
this.programs.set('postprocess', program);
return program;
}
// Water shader with animated waves
createWaterShader() {
const vertexShader = `
attribute vec3 a_position;
attribute vec2 a_texCoord;
uniform mat4 u_modelMatrix;
uniform mat4 u_viewMatrix;
uniform mat4 u_projectionMatrix;
uniform float u_time;
uniform float u_waveHeight;
uniform float u_waveFrequency;
varying vec3 v_worldPosition;
varying vec2 v_texCoord;
varying vec3 v_normal;
void main() {
vec3 position = a_position;
// Generate waves
float wave1 = sin(position.x * u_waveFrequency + u_time * 2.0) * u_waveHeight;
float wave2 = sin(position.z * u_waveFrequency * 0.7 + u_time * 1.5) * u_waveHeight * 0.5;
position.y += wave1 + wave2;
// Calculate normal for lighting
float dx = cos(position.x * u_waveFrequency + u_time * 2.0) * u_waveFrequency * u_waveHeight;
float dz = cos(position.z * u_waveFrequency * 0.7 + u_time * 1.5) * u_waveFrequency * 0.7 * u_waveHeight * 0.5;
v_normal = normalize(vec3(-dx, 1.0, -dz));
vec4 worldPosition = u_modelMatrix * vec4(position, 1.0);
v_worldPosition = worldPosition.xyz;
v_texCoord = a_texCoord;
gl_Position = u_projectionMatrix * u_viewMatrix * worldPosition;
}
`;
const fragmentShader = `
precision mediump float;
uniform vec3 u_lightPosition;
uniform vec3 u_lightColor;
uniform vec3 u_viewPosition;
uniform vec3 u_waterColor;
uniform float u_time;
uniform sampler2D u_normalMap;
varying vec3 v_worldPosition;
varying vec2 v_texCoord;
varying vec3 v_normal;
void main() {
vec2 uv = v_texCoord;
// Animate texture coordinates for flowing water
vec2 flowUV1 = uv + vec2(u_time * 0.1, u_time * 0.05);
vec2 flowUV2 = uv + vec2(-u_time * 0.08, u_time * 0.12);
// Sample normal map
vec3 normalMap1 = texture2D(u_normalMap, flowUV1).rgb * 2.0 - 1.0;
vec3 normalMap2 = texture2D(u_normalMap, flowUV2).rgb * 2.0 - 1.0;
vec3 normal = normalize(v_normal + normalMap1 * 0.2 + normalMap2 * 0.1);
// Lighting calculations
vec3 lightDir = normalize(u_lightPosition - v_worldPosition);
vec3 viewDir = normalize(u_viewPosition - v_worldPosition);
vec3 reflectDir = reflect(-lightDir, normal);
// Fresnel effect
float fresnel = pow(1.0 - max(dot(normal, viewDir), 0.0), 2.0);
// Diffuse lighting
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * u_waterColor * u_lightColor;
// Specular lighting
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = spec * u_lightColor;
// Combine colors
vec3 result = diffuse + specular * fresnel;
// Add some transparency
float alpha = 0.8 + fresnel * 0.2;
gl_FragColor = vec4(result, alpha);
}
`;
const program = this.webglManager.createProgram(
vertexShader,
fragmentShader
);
this.programs.set('water', program);
return program;
}
getProgram(name) {
return this.programs.get(name);
}
}
// Particle System
class ParticleSystem {
constructor(webglManager, maxParticles = 1000) {
this.gl = webglManager.gl;
this.webglManager = webglManager;
this.maxParticles = maxParticles;
this.particles = [];
this.particlePool = [];
// Initialize particle pool
for (let i = 0; i < maxParticles; i++) {
this.particlePool.push({
position: [0, 0, 0],
velocity: [0, 0, 0],
size: 1.0,
life: 0.0,
color: [1, 1, 1, 1],
active: false,
});
}
this.createBuffers();
this.createParticleTexture();
}
createBuffers() {
// Create vertex buffer for all particles
const positions = new Float32Array(this.maxParticles * 3);
const velocities = new Float32Array(this.maxParticles * 3);
const sizes = new Float32Array(this.maxParticles);
const lives = new Float32Array(this.maxParticles);
const colors = new Float32Array(this.maxParticles * 4);
this.buffers = {
position: this.webglManager.createBuffer(positions, this.gl.DYNAMIC_DRAW),
velocity: this.webglManager.createBuffer(
velocities,
this.gl.DYNAMIC_DRAW
),
size: this.webglManager.createBuffer(sizes, this.gl.DYNAMIC_DRAW),
life: this.webglManager.createBuffer(lives, this.gl.DYNAMIC_DRAW),
color: this.webglManager.createBuffer(colors, this.gl.DYNAMIC_DRAW),
};
}
createParticleTexture() {
// Create a simple circular particle texture
const size = 32;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// Create radial gradient
const gradient = ctx.createRadialGradient(
size / 2,
size / 2,
0,
size / 2,
size / 2,
size / 2
);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
this.particleTexture = this.webglManager.createTexture(canvas);
}
emit(position, velocity, count = 1) {
for (let i = 0; i < count; i++) {
const particle = this.getParticle();
if (particle) {
particle.position = [...position];
particle.velocity = [
velocity[0] + (Math.random() - 0.5) * 2,
velocity[1] + (Math.random() - 0.5) * 2,
velocity[2] + (Math.random() - 0.5) * 2,
];
particle.size = 10 + Math.random() * 20;
particle.life = Date.now() / 1000;
particle.color = [Math.random(), Math.random(), Math.random(), 1.0];
particle.active = true;
this.particles.push(particle);
}
}
}
getParticle() {
for (let i = 0; i < this.particlePool.length; i++) {
const particle = this.particlePool[i];
if (!particle.active) {
return particle;
}
}
return null;
}
update(deltaTime) {
const currentTime = Date.now() / 1000;
// Update active particles
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
const age = currentTime - particle.life;
if (age > 5.0) {
// 5 second lifetime
particle.active = false;
this.particles.splice(i, 1);
}
}
this.updateBuffers();
}
updateBuffers() {
const gl = this.gl;
const positions = new Float32Array(this.maxParticles * 3);
const velocities = new Float32Array(this.maxParticles * 3);
const sizes = new Float32Array(this.maxParticles);
const lives = new Float32Array(this.maxParticles);
const colors = new Float32Array(this.maxParticles * 4);
// Fill buffers with active particles
for (let i = 0; i < this.particles.length; i++) {
const particle = this.particles[i];
positions[i * 3] = particle.position[0];
positions[i * 3 + 1] = particle.position[1];
positions[i * 3 + 2] = particle.position[2];
velocities[i * 3] = particle.velocity[0];
velocities[i * 3 + 1] = particle.velocity[1];
velocities[i * 3 + 2] = particle.velocity[2];
sizes[i] = particle.size;
lives[i] = particle.life;
colors[i * 4] = particle.color[0];
colors[i * 4 + 1] = particle.color[1];
colors[i * 4 + 2] = particle.color[2];
colors[i * 4 + 3] = particle.color[3];
}
// Update buffers
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.position);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, positions);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.velocity);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, velocities);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.size);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, sizes);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.life);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, lives);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.color);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, colors);
}
render(program, viewMatrix, projectionMatrix) {
const gl = this.gl;
if (this.particles.length === 0) return;
gl.useProgram(program);
// Set uniforms
gl.uniformMatrix4fv(
gl.getUniformLocation(program, 'u_viewMatrix'),
false,
viewMatrix.elements
);
gl.uniformMatrix4fv(
gl.getUniformLocation(program, 'u_projectionMatrix'),
false,
projectionMatrix.elements
);
gl.uniform1f(gl.getUniformLocation(program, 'u_time'), Date.now() / 1000);
gl.uniform3fv(gl.getUniformLocation(program, 'u_gravity'), [0, -9.8, 0]);
// Bind texture
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.particleTexture);
gl.uniform1i(gl.getUniformLocation(program, 'u_particleTexture'), 0);
// Set vertex attributes
const positionLocation = gl.getAttribLocation(program, 'a_position');
const velocityLocation = gl.getAttribLocation(program, 'a_velocity');
const sizeLocation = gl.getAttribLocation(program, 'a_size');
const lifeLocation = gl.getAttribLocation(program, 'a_life');
const colorLocation = gl.getAttribLocation(program, 'a_color');
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.position);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.velocity);
gl.enableVertexAttribArray(velocityLocation);
gl.vertexAttribPointer(velocityLocation, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.size);
gl.enableVertexAttribArray(sizeLocation);
gl.vertexAttribPointer(sizeLocation, 1, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.life);
gl.enableVertexAttribArray(lifeLocation);
gl.vertexAttribPointer(lifeLocation, 1, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.color);
gl.enableVertexAttribArray(colorLocation);
gl.vertexAttribPointer(colorLocation, 4, gl.FLOAT, false, 0, 0);
// Enable blending for particles
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Render particles
gl.drawArrays(gl.POINTS, 0, this.particles.length);
// Disable vertex arrays
gl.disableVertexAttribArray(positionLocation);
gl.disableVertexAttribArray(velocityLocation);
gl.disableVertexAttribArray(sizeLocation);
gl.disableVertexAttribArray(lifeLocation);
gl.disableVertexAttribArray(colorLocation);
}
}
// Usage example with advanced shaders
const canvas2 = document.createElement('canvas');
canvas2.id = 'advanced-webgl-canvas';
canvas2.width = 800;
canvas2.height = 600;
canvas2.style.border = '1px solid #ccc';
canvas2.style.marginTop = '20px';
document.body.appendChild(canvas2);
const webglManager2 = new WebGLManager('advanced-webgl-canvas');
const shaderManager = new ShaderManager(webglManager2);
// Create shaders
const phongProgram = shaderManager.createPhongShader();
const particleProgram = shaderManager.createParticleShader();
// Create particle system
const particleSystem = new ParticleSystem(webglManager2, 500);
// Animation loop with particles
let time2 = 0;
function animateAdvanced() {
time2 += 0.016;
// Emit particles
if (Math.random() < 0.3) {
particleSystem.emit(
[Math.sin(time2) * 2, 1, -5],
[Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5],
Math.floor(Math.random() * 5) + 1
);
}
// Update particles
particleSystem.update(0.016);
// Clear canvas
const gl = webglManager2.gl;
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Set up view and projection matrices
const viewMatrix = Matrix4.lookAt([0, 0, 0], [0, 0, -1], [0, 1, 0]);
const projectionMatrix = Matrix4.perspective(
Math.PI / 4,
canvas2.width / canvas2.height,
0.1,
100.0
);
// Render particles
particleSystem.render(particleProgram, viewMatrix, projectionMatrix);
requestAnimationFrame(animateAdvanced);
}
animateAdvanced();
Interactive Graphics and User Input
// Interactive 3D Scene Manager
class InteractiveScene {
constructor(webglManager) {
this.gl = webglManager.gl;
this.webglManager = webglManager;
this.canvas = webglManager.canvas;
// Camera properties
this.camera = {
position: [0, 0, 5],
target: [0, 0, 0],
up: [0, 1, 0],
fov: Math.PI / 4,
near: 0.1,
far: 100.0,
};
// Mouse/touch interaction
this.mouse = {
x: 0,
y: 0,
lastX: 0,
lastY: 0,
isDown: false,
button: 0,
};
this.touch = {
x: 0,
y: 0,
lastX: 0,
lastY: 0,
isDown: false,
scale: 1,
lastScale: 1,
};
// Interaction state
this.isRotating = false;
this.isPanning = false;
this.isZooming = false;
// Scene objects
this.objects = [];
this.selectedObject = null;
// Performance monitoring
this.fps = 0;
this.frameCount = 0;
this.lastTime = performance.now();
this.setupEventListeners();
}
setupEventListeners() {
// Mouse events
this.canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
this.canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
this.canvas.addEventListener('wheel', (e) => this.onWheel(e));
// Touch events
this.canvas.addEventListener('touchstart', (e) => this.onTouchStart(e));
this.canvas.addEventListener('touchmove', (e) => this.onTouchMove(e));
this.canvas.addEventListener('touchend', (e) => this.onTouchEnd(e));
// Keyboard events
document.addEventListener('keydown', (e) => this.onKeyDown(e));
document.addEventListener('keyup', (e) => this.onKeyUp(e));
// Prevent context menu
this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
}
onMouseDown(event) {
this.mouse.isDown = true;
this.mouse.button = event.button;
this.mouse.lastX = event.clientX;
this.mouse.lastY = event.clientY;
// Check for object selection
this.selectObject(event.clientX, event.clientY);
if (event.button === 0) {
// Left button
this.isRotating = true;
} else if (event.button === 2) {
// Right button
this.isPanning = true;
}
}
onMouseMove(event) {
if (!this.mouse.isDown) return;
const deltaX = event.clientX - this.mouse.lastX;
const deltaY = event.clientY - this.mouse.lastY;
if (this.isRotating) {
this.rotateCamera(deltaX * 0.01, deltaY * 0.01);
} else if (this.isPanning) {
this.panCamera(deltaX * 0.01, deltaY * 0.01);
}
this.mouse.lastX = event.clientX;
this.mouse.lastY = event.clientY;
}
onMouseUp(event) {
this.mouse.isDown = false;
this.isRotating = false;
this.isPanning = false;
}
onWheel(event) {
event.preventDefault();
const zoomSpeed = 0.1;
const zoomDelta = event.deltaY > 0 ? zoomSpeed : -zoomSpeed;
this.zoomCamera(zoomDelta);
}
onTouchStart(event) {
event.preventDefault();
if (event.touches.length === 1) {
this.touch.isDown = true;
this.touch.lastX = event.touches[0].clientX;
this.touch.lastY = event.touches[0].clientY;
} else if (event.touches.length === 2) {
// Pinch gesture
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const distance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
this.touch.lastScale = distance;
this.isZooming = true;
}
}
onTouchMove(event) {
event.preventDefault();
if (event.touches.length === 1 && this.touch.isDown) {
const deltaX = event.touches[0].clientX - this.touch.lastX;
const deltaY = event.touches[0].clientY - this.touch.lastY;
this.rotateCamera(deltaX * 0.01, deltaY * 0.01);
this.touch.lastX = event.touches[0].clientX;
this.touch.lastY = event.touches[0].clientY;
} else if (event.touches.length === 2 && this.isZooming) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const distance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
const scale = distance / this.touch.lastScale;
this.zoomCamera((1 - scale) * 0.5);
this.touch.lastScale = distance;
}
}
onTouchEnd(event) {
this.touch.isDown = false;
this.isZooming = false;
}
onKeyDown(event) {
const moveSpeed = 0.1;
switch (event.code) {
case 'KeyW':
this.moveCamera(0, 0, -moveSpeed);
break;
case 'KeyS':
this.moveCamera(0, 0, moveSpeed);
break;
case 'KeyA':
this.moveCamera(-moveSpeed, 0, 0);
break;
case 'KeyD':
this.moveCamera(moveSpeed, 0, 0);
break;
case 'KeyQ':
this.moveCamera(0, moveSpeed, 0);
break;
case 'KeyE':
this.moveCamera(0, -moveSpeed, 0);
break;
case 'KeyR':
this.resetCamera();
break;
}
}
onKeyUp(event) {
// Handle key release events if needed
}
rotateCamera(deltaX, deltaY) {
// Implement orbital camera rotation
const radius = this.getCameraDistance();
// Horizontal rotation (around Y axis)
const angleY = deltaX;
const cosY = Math.cos(angleY);
const sinY = Math.sin(angleY);
const newX =
this.camera.position[0] * cosY - this.camera.position[2] * sinY;
const newZ =
this.camera.position[0] * sinY + this.camera.position[2] * cosY;
this.camera.position[0] = newX;
this.camera.position[2] = newZ;
// Vertical rotation (around X axis)
const angleX = deltaY;
const currentRadius = Math.sqrt(
this.camera.position[0] * this.camera.position[0] +
this.camera.position[2] * this.camera.position[2]
);
this.camera.position[1] = Math.max(
-radius * 0.9,
Math.min(radius * 0.9, this.camera.position[1] + angleX * currentRadius)
);
}
panCamera(deltaX, deltaY) {
// Move camera parallel to the screen
const right = this.getCameraRight();
const up = this.getCameraUp();
this.camera.position[0] += right[0] * deltaX + up[0] * deltaY;
this.camera.position[1] += right[1] * deltaX + up[1] * deltaY;
this.camera.position[2] += right[2] * deltaX + up[2] * deltaY;
this.camera.target[0] += right[0] * deltaX + up[0] * deltaY;
this.camera.target[1] += right[1] * deltaX + up[1] * deltaY;
this.camera.target[2] += right[2] * deltaX + up[2] * deltaY;
}
zoomCamera(delta) {
const direction = this.getCameraDirection();
const zoomSpeed = Math.max(0.1, this.getCameraDistance() * 0.1);
this.camera.position[0] += direction[0] * delta * zoomSpeed;
this.camera.position[1] += direction[1] * delta * zoomSpeed;
this.camera.position[2] += direction[2] * delta * zoomSpeed;
// Prevent camera from going through the target
const minDistance = 0.5;
if (this.getCameraDistance() < minDistance) {
const newDirection = this.normalize([
this.camera.position[0] - this.camera.target[0],
this.camera.position[1] - this.camera.target[1],
this.camera.position[2] - this.camera.target[2],
]);
this.camera.position[0] =
this.camera.target[0] + newDirection[0] * minDistance;
this.camera.position[1] =
this.camera.target[1] + newDirection[1] * minDistance;
this.camera.position[2] =
this.camera.target[2] + newDirection[2] * minDistance;
}
}
moveCamera(deltaX, deltaY, deltaZ) {
this.camera.position[0] += deltaX;
this.camera.position[1] += deltaY;
this.camera.position[2] += deltaZ;
this.camera.target[0] += deltaX;
this.camera.target[1] += deltaY;
this.camera.target[2] += deltaZ;
}
resetCamera() {
this.camera.position = [0, 0, 5];
this.camera.target = [0, 0, 0];
this.camera.up = [0, 1, 0];
}
getCameraDistance() {
return Math.sqrt(
Math.pow(this.camera.position[0] - this.camera.target[0], 2) +
Math.pow(this.camera.position[1] - this.camera.target[1], 2) +
Math.pow(this.camera.position[2] - this.camera.target[2], 2)
);
}
getCameraDirection() {
return this.normalize([
this.camera.target[0] - this.camera.position[0],
this.camera.target[1] - this.camera.position[1],
this.camera.target[2] - this.camera.position[2],
]);
}
getCameraRight() {
const direction = this.getCameraDirection();
return this.normalize(this.cross(direction, this.camera.up));
}
getCameraUp() {
const direction = this.getCameraDirection();
const right = this.getCameraRight();
return this.cross(right, direction);
}
selectObject(screenX, screenY) {
// Convert screen coordinates to normalized device coordinates
const rect = this.canvas.getBoundingClientRect();
const x = ((screenX - rect.left) / rect.width) * 2 - 1;
const y = -((screenY - rect.top) / rect.height) * 2 + 1;
// Create ray from camera through mouse position
const ray = this.createRay(x, y);
// Test intersection with objects
let closestObject = null;
let closestDistance = Infinity;
this.objects.forEach((object) => {
const distance = this.rayObjectIntersection(ray, object);
if (distance !== null && distance < closestDistance) {
closestDistance = distance;
closestObject = object;
}
});
this.selectedObject = closestObject;
}
createRay(ndcX, ndcY) {
// Create view and projection matrices
const viewMatrix = Matrix4.lookAt(
this.camera.position,
this.camera.target,
this.camera.up
);
const projectionMatrix = Matrix4.perspective(
this.camera.fov,
this.canvas.width / this.canvas.height,
this.camera.near,
this.camera.far
);
// Compute ray in world space
const rayStart = this.camera.position;
const rayEnd = this.unproject(
ndcX,
ndcY,
1.0,
viewMatrix,
projectionMatrix
);
return {
origin: rayStart,
direction: this.normalize([
rayEnd[0] - rayStart[0],
rayEnd[1] - rayStart[1],
rayEnd[2] - rayStart[2],
]),
};
}
unproject(x, y, z, viewMatrix, projectionMatrix) {
// This is a simplified unproject function
// In a real implementation, you'd need proper matrix inversion
const invViewProj = this.multiply4x4(projectionMatrix, viewMatrix);
// Apply inverse transformation
const point = [x, y, z, 1.0];
const result = this.multiplyMatrixVector(invViewProj, point);
return [
result[0] / result[3],
result[1] / result[3],
result[2] / result[3],
];
}
rayObjectIntersection(ray, object) {
// Simple sphere collision detection
// In a real implementation, you'd use the object's actual geometry
const center = object.position;
const radius = 1.0; // Assume unit sphere
const oc = [
ray.origin[0] - center[0],
ray.origin[1] - center[1],
ray.origin[2] - center[2],
];
const a = this.dot(ray.direction, ray.direction);
const b = 2 * this.dot(oc, ray.direction);
const c = this.dot(oc, oc) - radius * radius;
const discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return null; // No intersection
}
const t1 = (-b - Math.sqrt(discriminant)) / (2 * a);
const t2 = (-b + Math.sqrt(discriminant)) / (2 * a);
return t1 > 0 ? t1 : t2 > 0 ? t2 : null;
}
updatePerformance() {
this.frameCount++;
const currentTime = performance.now();
if (currentTime - this.lastTime >= 1000) {
this.fps = Math.round(
(this.frameCount * 1000) / (currentTime - this.lastTime)
);
this.frameCount = 0;
this.lastTime = currentTime;
}
}
render(renderer) {
this.updatePerformance();
// Update view matrix
const viewMatrix = Matrix4.lookAt(
this.camera.position,
this.camera.target,
this.camera.up
);
renderer.setViewMatrix(viewMatrix);
// Render objects
renderer.render(this.objects);
// Highlight selected object
if (this.selectedObject) {
this.renderSelectionHighlight(this.selectedObject);
}
// Render UI
this.renderUI();
}
renderSelectionHighlight(object) {
// Render wireframe or outline for selected object
// This would require additional shader programs
}
renderUI() {
// Render performance and control information
const uiText = `FPS: ${this.fps} | Camera: ${this.camera.position.map((v) => v.toFixed(2)).join(', ')}`;
// You would typically render this with a 2D canvas overlay or WebGL text rendering
console.log(uiText);
}
// Utility functions
normalize(v) {
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
return length > 0
? [v[0] / length, v[1] / length, v[2] / length]
: [0, 0, 0];
}
cross(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
}
dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
multiply4x4(a, b) {
// Matrix multiplication - simplified
return a; // Placeholder
}
multiplyMatrixVector(matrix, vector) {
// Matrix-vector multiplication - simplified
return vector; // Placeholder
}
}
// Usage example
const canvas3 = document.createElement('canvas');
canvas3.id = 'interactive-webgl-canvas';
canvas3.width = 800;
canvas3.height = 600;
canvas3.style.border = '1px solid #ccc';
canvas3.style.marginTop = '20px';
document.body.appendChild(canvas3);
const webglManager3 = new WebGLManager('interactive-webgl-canvas');
const interactiveScene = new InteractiveScene(webglManager3);
const renderer3 = new SimpleRenderer(webglManager3);
// Add interactive objects
const interactiveCube = new Object3D(
webglManager3,
GeometryUtils.createCube(1),
{
position: [-2, 0, -5],
}
);
const interactiveSphere = new Object3D(
webglManager3,
GeometryUtils.createSphere(1, 20),
{
position: [2, 0, -5],
}
);
interactiveScene.objects.push(interactiveCube, interactiveSphere);
// Interactive animation loop
function animateInteractive() {
interactiveScene.render(renderer3);
requestAnimationFrame(animateInteractive);
}
animateInteractive();
// Add instructions
const instructions = document.createElement('div');
instructions.style.marginTop = '10px';
instructions.innerHTML = `
<h4>Controls:</h4>
<ul>
<li>Left mouse drag: Rotate camera</li>
<li>Right mouse drag: Pan camera</li>
<li>Mouse wheel: Zoom</li>
<li>WASD: Move camera</li>
<li>Q/E: Move up/down</li>
<li>R: Reset camera</li>
<li>Click objects to select them</li>
</ul>
`;
document.body.appendChild(instructions);
Conclusion
WebGL provides powerful capabilities for creating interactive 3D graphics and visualizations in web browsers. By mastering shader programming, understanding the graphics pipeline, and implementing proper interaction systems, you can build sophisticated graphics applications that run smoothly across different devices and platforms. The key to successful WebGL development is understanding the GPU architecture, optimizing for performance, and creating intuitive user interactions that enhance the visual experience.
When building WebGL applications, focus on efficient resource management, proper error handling, and progressive enhancement to ensure your graphics applications work well across different browsers and hardware configurations.