JavaScript Graphics

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.

By JavaScript Document Team
webgl3d-graphicsshadersgpurenderinggraphics-programming

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.