WebGL2D文件架构和代码

21 阅读5分钟

📁 项目目录结构

webgl2d-renderer/
│
├── index.html                    # 主页面
├── package.json                  # 可选:用于引入 gl-matrix
│
└── src/
    ├── core/
    │   ├── CanvasCore.js
    │   └── utils.js
    │
    ├── modules/
    │   ├── View.js
    │   ├── ShaderProgram.js
    │   ├── GLBuffer.js
    │   ├── VertexArray.js
    │   ├── RenderGeometry.js
    │   ├── Renderer2D.js
    │   └── Event.js
    │
    └── main.js                   # 入口文件

✅ 所有代码使用 ES6 模块语法import / export),需通过本地服务器运行(如 Live Server)


📦 代码文件


1. index.html

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>WebGL2 2D 渲染引擎</title>
  <style>
    body {
      margin: 0;
      background: #1a1a1a;
      color: #eee;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    h2 {
      margin: 20px 0;
      color: #bbf;
    }
    canvas {
      border: 1px solid #333;
      margin: 10px;
      box-shadow: 0 0 10px rgba(0,0,0,0.3);
    }
    .controls {
      margin: 10px;
    }
    button {
      margin: 0 8px;
      padding: 10px 16px;
      font-size: 14px;
      background: #333;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    button:hover {
      background: #555;
    }
  </style>
</head>
<body>
  <h2>🎨 WebGL2 2D 图形渲染器</h2>
  <canvas id="render-canvas" width="800" height="600"></canvas>
  <div class="controls">
    <button onclick="startRect()">画矩形</button>
    <button onclick="startLine()">画线段</button>
    <button onclick="startPoint()">画点</button>
    <button onclick="clearAll()">清空画布</button>
  </div>

  <!-- 引入 gl-matrix(CDN) -->
  <script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.3/+esm"></script>

  <!-- 主入口 -->
  <script type="module" src="./src/main.js"></script>
</body>
</html>

2. src/core/utils.js

// src/core/utils.js
export function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error('Shader 编译失败:', gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
  return shader;
}

export function resizeCanvasToDisplaySize(canvas) {
  const { clientWidth, clientHeight } = canvas;
  const dpr = window.devicePixelRatio || 1;
  const width = clientWidth * dpr;
  const height = clientHeight * dpr;

  let needResize = false;
  if (canvas.width !== width) {
    canvas.width = width;
    needResize = true;
  }
  if (canvas.height !== height) {
    canvas.height = height;
    needResize = true;
  }

  if (needResize) {
    const gl = canvas.getContext('webgl2');
    gl.viewport(0, 0, canvas.width, canvas.height);
  }

  return needResize;
}

3. src/core/CanvasCore.js

// src/core/CanvasCore.js
import { resizeCanvasToDisplaySize } from './utils.js';

export class CanvasCore {
  constructor(canvasId) {
    const canvas = document.getElementById(canvasId);
    this.canvas = canvas;
    this.gl = canvas.getContext('webgl2', { alpha: false });
    if (!this.gl) throw new Error('WebGL2 不支持,请检查浏览器');

    this.view = null;
    this.renderer2D = null;
    this.running = false;
  }

  init(view, renderer2D) {
    this.view = view;
    this.renderer2D = renderer2D;
    this.running = true;
    this.renderLoop();
  }

  clear() {
    const gl = this.gl;
    gl.clearColor(0.05, 0.05, 0.05, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  }

  renderFrame() {
    const gl = this.gl;

    // 自动适配高 DPI 与尺寸变化
    if (resizeCanvasToDisplaySize(this.canvas)) {
      this.view.setViewport(this.canvas.width, this.canvas.height);
    }

    this.clear();
    this.renderer2D.render(this.view);
  }

  renderLoop = () => {
    if (this.running) {
      this.renderFrame();
      requestAnimationFrame(this.renderLoop);
    }
  };
}

4. src/modules/View.js

// src/modules/View.js
import { mat4 } from 'gl-matrix';

export class View {
  constructor(canvasWidth = 800, canvasHeight = 600) {
    this.width = canvasWidth;
    this.height = canvasHeight;
    this.viewMatrix = mat4.create();
    this.projectionMatrix = mat4.create();

    this.translateX = 0;
    this.translateY = 0;
    this.scale = 1.0;

    this.initMatrices();
  }

  initMatrices() {
    const aspect = this.width / this.height;
    const w = this.width / 2;
    const h = this.height / 2;
    mat4.ortho(this.projectionMatrix, -w, w, -h, h, -1, 1);

    mat4.identity(this.viewMatrix);
    mat4.translate(this.viewMatrix, this.viewMatrix, [-this.width / 2, -this.height / 2, 0]);
  }

  setViewport(width, height) {
    this.width = width;
    this.height = height;
    this.initMatrices();
  }

  pan(dx, dy) {
    this.translateX += dx;
    this.translateY += dy;
    this.updateViewMatrix();
  }

  zoom(scale, centerX = 0, centerY = 0) {
    this.scale = Math.max(0.1, Math.min(10, scale));
    this.updateViewMatrix(centerX, centerY);
  }

  updateViewMatrix(centerX = 0, centerY = 0) {
    mat4.identity(this.viewMatrix);
    mat4.translate(this.viewMatrix, this.viewMatrix, [
      -this.width / 2 + this.translateX,
      -this.height / 2 + this.translateY,
      0,
    ]);
    mat4.translate(this.viewMatrix, this.viewMatrix, [centerX, centerY, 0]);
    mat4.scale(this.viewMatrix, this.viewMatrix, [this.scale, this.scale, 1]);
    mat4.translate(this.viewMatrix, this.viewMatrix, [-centerX, -centerY, 0]);
  }
}

5. src/modules/ShaderProgram.js

// src/modules/ShaderProgram.js
import { createShader } from '../core/utils.js';

export class ShaderProgram {
  constructor(gl) {
    this.gl = gl;
    this.program = null;
    this.programInfo = null;
    this.init();
  }

  init() {
    const gl = this.gl;

    const vsSource = `
      attribute vec2 a_position;
      uniform mat4 u_viewMatrix;
      uniform mat4 u_projectionMatrix;
      uniform vec3 u_color;
      uniform float u_pointSize;
      void main() {
        gl_Position = u_projectionMatrix * u_viewMatrix * vec4(a_position, 0, 1);
        gl_PointSize = u_pointSize;
      }
    `;

    const fsSource = `
      precision mediump float;
      uniform vec3 u_color;
      void main() {
        gl_FragColor = vec4(u_color, 1.0);
      }
    `;

    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);

    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error('着色器程序链接失败:', gl.getProgramInfoLog(program));
      return;
    }

    this.program = program;

    this.programInfo = {
      program: program,
      attribLocations: {
        position: gl.getAttribLocation(program, 'a_position'),
      },
      uniformLocations: {
        viewMatrix: gl.getUniformLocation(program, 'u_viewMatrix'),
        projectionMatrix: gl.getUniformLocation(program, 'u_projectionMatrix'),
        color: gl.getUniformLocation(program, 'u_color'),
        pointSize: gl.getUniformLocation(program, 'u_pointSize'),
      },
    };
  }
}

6. src/modules/GLBuffer.js

// src/modules/GLBuffer.js
export class GLBuffer {
  constructor(gl, type, data, usage = gl.STATIC_DRAW) {
    this.gl = gl;
    this.buffer = gl.createBuffer();
    this.type = type;
    this.bind();
    gl.bufferData(type, data, usage);
  }

  bind() {
    this.gl.bindBuffer(this.type, this.buffer);
  }

  unbind() {
    this.gl.bindBuffer(this.type, null);
  }

  dispose() {
    this.gl.deleteBuffer(this.buffer);
  }
}

7. src/modules/VertexArray.js

// src/modules/VertexArray.js
export class VertexArray {
  constructor(gl) {
    this.gl = gl;
    this.vao = gl.createVertexArray();
    this.bind();
  }

  bind() {
    this.gl.bindVertexArray(this.vao);
  }

  unbind() {
    this.gl.bindVertexArray(null);
  }

  configure(attributes) {
    const gl = this.gl;
    this.bind();

    for (const attr of attributes) {
      gl.enableVertexAttribArray(attr.location);
      gl.bindBuffer(gl.ARRAY_BUFFER, attr.buffer.buffer);
      gl.vertexAttribPointer(
        attr.location,
        attr.size,
        gl.FLOAT,
        false,
        0,
        0
      );
    }

    this.unbind();
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }

  dispose() {
    this.gl.deleteVertexArray(this.vao);
  }
}

8. src/modules/RenderGeometry.js

// src/modules/RenderGeometry.js
export class RenderGeometry {
  constructor(type, config = {}) {
    this.type = type;
    this.position = [];
    this.indices = [];
    this.color = config.color || [1, 1, 1];
    this.pointSize = config.pointSize || 5;
    this.lineWidth = config.lineWidth || 2;

    this.generate(config);
  }

  generate(config) {
    const { x, y, width, height, radius, startAngle, endAngle, segments = 32, points } = config;

    switch (this.type) {
      case 'point':
        this.position = [x, y];
        break;

      case 'line':
        this.position = [x[0], y[0], x[1], y[1]];
        this.indices = [0, 1];
        break;

      case 'rect':
        const left = x, right = x + width;
        const top = y, bottom = y + height;
        this.position = [
          left, top,
          right, top,
          right, bottom,
          left, bottom
        ];
        this.indices = [0, 1, 2, 3, 0];
        break;

      case 'circle':
        for (let i = 0; i <= segments; i++) {
          const a = (i / segments) * Math.PI * 2;
          this.position.push(x + radius * Math.cos(a), y + radius * Math.sin(a));
        }
        this.indices = Array.from({ length: segments + 1 }, (_, i) => i);
        break;

      case 'polygon':
        if (points) {
          for (const p of points) this.position.push(p[0], p[1]);
          this.indices = Array.from({ length: points.length + 1 }, (_, i) => i % points.length);
        }
        break;

      case 'arc':
        const diff = endAngle - startAngle;
        for (let i = 0; i <= segments; i++) {
          const a = startAngle + (i / segments) * diff;
          this.position.push(x + radius * Math.cos(a), y + radius * Math.sin(a));
        }
        this.indices = Array.from({ length: segments + 1 }, (_, i) => i);
        break;

      default:
        console.warn(`未知图形类型: ${this.type}`);
    }
  }
}

9. src/modules/Renderer2D.js

// src/modules/Renderer2D.js
import { GLBuffer } from './GLBuffer.js';
import { VertexArray } from './VertexArray.js';

export class Renderer2D {
  constructor(gl, shaderProgram) {
    this.gl = gl;
    this.program = shaderProgram.programInfo;
    this.geometryList = [];

    this.buffers = new WeakMap();  // geometry → GLBuffer
    this.vaos = new WeakMap();    // geometry → VertexArray
  }

  render(view) {
    const gl = this.gl;
    const prog = this.program;

    gl.useProgram(prog.program);
    gl.uniformMatrix4fv(prog.uniformLocations.viewMatrix, false, view.viewMatrix);
    gl.uniformMatrix4fv(prog.uniformLocations.projectionMatrix, false, view.projectionMatrix);

    for (const geom of this.geometryList) {
      this.drawGeometry(geom);
    }
  }

  drawGeometry(geom) {
    const gl = this.gl;
    const prog = this.program;

    let posBuffer = this.buffers.get(geom);
    if (!posBuffer) {
      posBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, new Float32Array(geom.position));
      this.buffers.set(geom, posBuffer);
    }

    let vao = this.vaos.get(geom);
    if (!vao) {
      vao = new VertexArray(gl);
      vao.configure([{
        location: prog.attribLocations.position,
        buffer: posBuffer,
        size: 2
      }]);
      this.vaos.set(geom, vao);
    }

    gl.uniform3fv(prog.uniformLocations.color, geom.color);
    gl.uniform1f(prog.uniformLocations.pointSize, geom.pointSize);

    vao.bind();

    if (geom.indices && geom.indices.length > 0) {
      const ebo = new GLBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(geom.indices));
      gl.drawElements(gl.LINE_STRIP, geom.indices.length, gl.UNSIGNED_SHORT, 0);
      ebo.dispose();
    } else {
      gl.drawArrays(gl.POINTS, 0, 1);
    }

    vao.unbind();
  }
}

10. src/modules/Event.js

// src/modules/Event.js
export class Event {
  constructor(canvas, view, renderer2D, geometryList) {
    this.canvas = canvas;
    this.view = view;
    this.renderer2D = renderer2D;
    this.geometryList = geometryList;

    this.isDrawing = false;
    this.drawingType = null;
    this.startPos = [0, 0];
    this.tempGeometry = null;

    this.bindEvents();
  }

  bindEvents() {
    const canvas = this.canvas;
    const rect = canvas.getBoundingClientRect();
    const scale = window.devicePixelRatio || 1;

    const toCanvasCoords = (clientX, clientY) => {
      const x = (clientX - rect.left) * scale;
      const y = (clientY - rect.top) * scale;
      return [x, y];
    };

    let lastX = 0, lastY = 0;

    canvas.addEventListener('mousedown', (e) => {
      if (e.button === 0 && this.drawingType) {
        const [x, y] = toCanvasCoords(e.clientX, e.clientY);
        this.startPos = [x, y];
        this.isDrawing = true;
      }
    });

    canvas.addEventListener('mousemove', (e) => {
      const [x, y] = toCanvasCoords(e.clientX, e.clientY);
      const dx = x - lastX;
      const dy = y - lastY;

      if (this.isDrawing && this.drawingType) {
        const [sx, sy] = this.startPos;
        const config = { color: [0.3, 0.8, 1], pointSize: 6 };

        switch (this.drawingType) {
          case 'rect':
            config.x = sx;
            config.y = sy;
            config.width = x - sx;
            config.height = y - sy;
            break;
          case 'line':
            config.x = [sx, x];
            config.y = [sy, y];
            break;
          case 'point':
            config.x = x;
            config.y = y;
            break;
        }

        if (this.tempGeometry) {
          this.geometryList[this.geometryList.length - 1] = new RenderGeometry(this.drawingType, config);
        } else {
          this.tempGeometry = new RenderGeometry(this.drawingType, config);
          this.geometryList.push(this.tempGeometry);
        }
      } else if (e.buttons === 4) { // 右键拖拽平移
        this.view.pan(dx, dy);
      }

      lastX = x;
      lastY = y;
    });

    canvas.addEventListener('mouseup', () => {
      if (this.isDrawing) {
        this.isDrawing = false;
        this.tempGeometry = null;
      }
    });

    canvas.addEventListener('wheel', (e) => {
      e.preventDefault();
      const cx = (e.clientX - rect.left) * scale;
      const cy = (e.clientY - rect.top) * scale;
      const delta = Math.max(-1, Math.min(1, -e.wheelDelta || e.deltaY));
      const newScale = this.view.scale * (delta > 0 ? 1.1 : 0.9);
      this.view.zoom(newScale, cx, cy);
    });
  }

  setDrawingMode(type) {
    this.drawingType = type;
    this.isDrawing = false;
    this.tempGeometry = null;
  }
}

11. src/main.js

// src/main.js
import { CanvasCore } from './core/CanvasCore.js';
import { View } from './modules/View.js';
import { ShaderProgram } from './modules/ShaderProgram.js';
import { Renderer2D } from './modules/Renderer2D.js';
import { RenderGeometry } from './modules/RenderGeometry.js';
import { Event } from './modules/Event.js';

let geometryList = [];
let eventSystem = null;

function startApp() {
  const canvas = document.getElementById('render-canvas');
  const gl = canvas.getContext('webgl2');
  if (!gl) {
    alert('您的浏览器不支持 WebGL2');
    return;
  }

  const view = new View(canvas.width, canvas.height);
  const shaderProgram = new ShaderProgram(gl);
  const renderer2D = new Renderer2D(gl, shaderProgram);
  renderer2D.geometryList = geometryList;

  const canvasCore = new CanvasCore('render-canvas');
  canvasCore.init(view, renderer2D);

  eventSystem = new Event(canvas, view, renderer2D, geometryList);

  // 添加默认图形
  geometryList.push(
    new RenderGeometry('point', { x: 100, y: 100, color: [1, 0, 0], pointSize: 10 }),
    new RenderGeometry('line', { x: [200, 300], y: [150, 250], color: [0, 1, 0] }),
    new RenderGeometry('rect', { x: 400, y: 100, width: 100, height: 80, color: [0, 0.5, 1] }),
    new RenderGeometry('circle', { x: 600, y: 150, radius: 50, color: [1, 1, 0] }),
    new RenderGeometry('arc', { x: 300, y: 300, radius: 60, startAngle: 0, endAngle: Math.PI * 1.5, color: [1, 0.5, 0] }),
    new RenderGeometry('polygon', { 
      points: [[500, 200], [550, 180], [580, 220], [560, 270], [510, 260]], 
      color: [0.8, 0.2, 0.8] 
    })
  );
}

// 提供给 HTML 按钮调用
window.startRect = () => eventSystem?.setDrawingMode('rect');
window.startLine = () => eventSystem?.setDrawingMode('line');
window.startPoint = () => eventSystem?.setDrawingMode('point');
window.clearAll = () => geometryList.length = 0;

window.onload = startApp;

🚀 如何运行?

  1. 将以上文件保存到本地目录 webgl2d-renderer/
  2. 使用 本地服务器 打开 index.html(不能双击直接打开)

推荐方式:

  • VS Code + Live Server 插件
  • 或运行命令:
    npx serve
    
  • 或 Python:
    python3 -m http.server 8000
    

访问 http://localhost:8000 即可看到运行效果!


✅ 功能演示

操作效果
点击“画点”下次点击画布生成一个点
点击“画线段”拖拽绘制一条线
点击“画矩形”拖拽绘制矩形边框
右键拖拽平移视图
滚轮以鼠标为中心缩放
“清空画布”删除所有图形

📦 下一步建议

  • ✅ 添加撤销功能(维护 history 栈)
  • ✅ 支持图形选中与编辑
  • ✅ 导出为 JSON 或 SVG
  • ✅ 添加网格背景