TS2d渲染引擎

基于 TypeScript 的轻量级 2D渲染引擎

✅ 所有代码为纯逻辑层封装,适用于 WebWorker 或与任意 UI 框架集成


📦 安装依赖(提示)

bash 复制代码
npm install gl-matrix

🔧 基础类型定义(common.ts)

ts 复制代码
import { mat4, vec2, vec3 } from 'gl-matrix';

export type Vec2 = vec2;
export type Vec3 = vec3;
export type Mat4 = mat4;

export interface CanvasRect {
  x: number;
  y: number;
  width: number;
  height: number;
}

🎨 CanvasConfig.ts

ts 复制代码
import { CanvasRect } from './common';

export class CanvasConfig {
  public canvasClientRect: CanvasRect;

  constructor(rect: CanvasRect) {
    this.canvasClientRect = rect;
  }
}

🖼️ View.ts

ts 复制代码
import { mat4, vec2, vec3 } from 'gl-matrix';
import { CanvasRect, Vec2, Mat4 } from './common';

export class View {
  public viewMatrix: Mat4 = mat4.create();
  public projectionMatrix: Mat4 = mat4.create();

  private canvasRect: CanvasRect;

  constructor(canvasRect: CanvasRect) {
    this.canvasRect = canvasRect;
    this.updateProjectionMatrix(); // 默认正交投影
  }

  updateViewMatrix(eye: Vec2 = [0, 0], center: Vec2 = [0, 0], up: Vec2 = [0, 1]): void {
    const eye3: Vec3 = [eye[0], eye[1], 0];
    const center3: Vec3 = [center[0], center[1], 0];
    const up3: Vec3 = [up[0], up[1], 1]; // z 轴轻微上扬避免退化
    mat4.lookAt(this.viewMatrix, eye3, center3, up3);
  }

  updateProjectionMatrix(near: number = -1, far: number = 1): void {
    const { width, height } = this.canvasRect;
    mat4.ortho(this.projectionMatrix, 0, width, height, 0, near, far);
  }

  // 屏幕坐标 → NDC (-1 ~ 1)
  screenToNDC(screenPos: Vec2): Vec2 {
    const { width, height } = this.canvasRect;
    return [
      (screenPos[0] / width) * 2 - 1,
      (screenPos[1] / height) * 2 - 1
    ];
  }

  // NDC → 世界坐标(假设 Z=0)
  ndcToWorld(ndc: Vec2): Vec2 {
    const invViewProj = mat4.create();
    mat4.multiply(invViewProj, this.projectionMatrix, this.viewMatrix);
    mat4.invert(invViewProj, invViewProj);

    const pt4 = vec3.fromValues(ndc[0], ndc[1], 0);
    vec3.transformMat4(pt4, pt4, invViewProj);
    return [pt4[0], pt4[1]];
  }

  // 屏幕坐标 → 世界坐标
  screenToWorld(screenPos: Vec2): Vec2 {
    const ndc = this.screenToNDC(screenPos);
    return this.ndcToWorld(ndc);
  }
}

⚙️ GLBuffer.ts

ts 复制代码
export enum BufferType {
  ARRAY_BUFFER = 0x8892,        // position, color, etc.
  ELEMENT_ARRAY_BUFFER = 0x8893 // indices
}

export enum UsageHint {
  STATIC_DRAW = 0x88e4,
  DYNAMIC_DRAW = 0x88e8,
  STREAM_DRAW = 0x88e0
}

export class GLBuffer {
  private buffer: WebGLBuffer | null = null;
  private gl: WebGL2RenderingContext;

  constructor(
    gl: WebGL2RenderingContext,
    type: BufferType,
    usage: UsageHint = UsageHint.STATIC_DRAW
  ) {
    this.gl = gl;
    this.buffer = gl.createBuffer();
    this.bind(type);
    this.unbind(type);
  }

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

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

  data(data: Float32Array | Uint16Array | Uint32Array, type: BufferType, usage: UsageHint): void {
    this.bind(type);
    this.gl.bufferData(type, data, usage);
    this.unbind(type);
  }

  destroy(): void {
    if (this.buffer) {
      this.gl.deleteBuffer(this.buffer);
      this.buffer = null;
    }
  }
}

🧱 VertexLayout.ts

ts 复制代码
export interface AttributeInfo {
  name: string;
  size: number;     // components per vertex (e.g., 2 for vec2)
  type: number;     // gl.FLOAT, etc.
  normalized: boolean;
  stride: number;
  offset: number;
}

export class VertexLayout {
  private attributes: AttributeInfo[] = [];

  addAttribute(info: Omit<AttributeInfo, 'stride' | 'offset'>): this {
    this.attributes.push({ ...info, stride: 0, offset: 0 });
    return this;
  }

  computeOffsetsAndStride(): number {
    let offset = 0;
    let stride = 0;

    this.attributes.forEach(attr => {
      attr.offset = offset;
      const typeSize = this.getTypeSize(attr.type);
      offset += attr.size * typeSize;
    });

    stride = offset;

    this.attributes.forEach(attr => {
      attr.stride = stride;
    });

    return stride;
  }

  getAttributes(): readonly AttributeInfo[] {
    return this.attributes;
  }

  private getTypeSize(type: number): number {
    switch (type) {
      case 0x1406: return 4; // FLOAT
      case 0x1404: return 4; // INT
      case 0x1405: return 4; // UNSIGNED_INT
      default: throw new Error(`Unsupported GL type: ${type}`);
    }
  }
}

🧩 VertexArray.ts

ts 复制代码
export class VertexArray {
  private vao: WebGLVertexArrayObjectOES;
  private vbo: GLBuffer;
  private ibo: GLBuffer | null = null;
  private layout: VertexLayout;
  private gl: WebGL2RenderingContext;

  constructor(
    gl: WebGL2RenderingContext,
    vbo: GLBuffer,
    layout: VertexLayout,
    ibo?: GLBuffer
  ) {
    this.gl = gl;
    const ext = gl.getExtension('OES_vertex_array_object');
    if (!ext) throw new Error('OES_vertex_array_object not supported');

    this.vao = ext.createVertexArray()!;
    this.vbo = vbo;
    this.layout = layout;
    this.ibo = ibo || null;

    this.bind();
    this.vbo.bind(BufferType.ARRAY_BUFFER);

    const stride = layout.computeOffsetsAndStride();

    for (const attr of layout.getAttributes()) {
      this.gl.enableVertexAttribArray(this.gl.getAttribLocation(this.getCurrentProgram(), attr.name));
      this.gl.vertexAttribPointer(
        this.gl.getAttribLocation(this.getCurrentProgram(), attr.name),
        attr.size,
        attr.type,
        attr.normalized,
        attr.stride,
        attr.offset
      );
    }

    if (this.ibo) {
      this.ibo.bind(BufferType.ELEMENT_ARRAY_BUFFER);
    }

    this.unbind();
  }

  bind(): void {
    const ext = this.gl.getExtension('OES_vertex_array_object');
    ext!.bindVertexArray(this.vao);
  }

  unbind(): void {
    const ext = this.gl.getExtension('OES_vertex_array_object');
    ext!.bindVertexArray(null);
  }

  drawElements(mode: number, count: number, type: number, offset: number): void {
    this.bind();
    this.gl.drawElements(mode, count, type, offset);
    this.unbind();
  }

  destroy(): void {
    const ext = this.gl.getExtension('OES_vertex_array_object');
    ext!.deleteVertexArray(this.vao);
    this.vbo.destroy();
    this.ibo?.destroy();
  }

  private getCurrentProgram(): WebGLProgram {
    return this.gl.getParameter(this.gl.CURRENT_PROGRAM);
  }
}

🎯 Shader.ts

ts 复制代码
export enum ShaderType {
  VERTEX = 0x8b31,
  FRAGMENT = 0x8b30
}

export class Shader {
  private shader: WebGLShader;
  private gl: WebGL2RenderingContext;

  constructor(gl: WebGL2RenderingContext, source: string, type: ShaderType) {
    this.gl = gl;
    this.shader = gl.createShader(type)!;
    gl.shaderSource(this.shader, source);
    gl.compileShader(this.shader);

    if (!gl.getShaderParameter(this.shader, gl.COMPILE_STATUS)) {
      const log = gl.getShaderInfoLog(this.shader);
      console.error(`Shader compilation failed:\n${log}`);
      throw new Error(`Shader compile error: ${log}`);
    }
  }

  getRaw(): WebGLShader {
    return this.shader;
  }

  destroy(): void {
    this.gl.deleteShader(this.shader);
  }
}

🧩 ShaderProgram.ts

ts 复制代码
export class ShaderProgram {
  private program: WebGLProgram;
  private gl: WebGL2RenderingContext;

  constructor(gl: WebGL2RenderingContext, vertexShader: Shader, fragmentShader: Shader) {
    this.gl = gl;
    this.program = gl.createProgram()!;
    gl.attachShader(this.program, vertexShader.getRaw());
    gl.attachShader(this.program, fragmentShader.getRaw());
    gl.linkProgram(this.program);

    if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
      const log = gl.getProgramInfoLog(this.program);
      console.error(`Program link failed: ${log}`);
      throw new Error(`Program link error: ${log}`);
    }
  }

  use(): void {
    this.gl.useProgram(this.program);
  }

  getUniformLocation(name: string): WebGLUniformLocation | null {
    return this.gl.getUniformLocation(this.program, name);
  }

  setUniformMatrix4fv(name: string, value: Float32Array): void {
    const loc = this.getUniformLocation(name);
    if (loc) this.gl.uniformMatrix4fv(loc, false, value);
  }

  destroy(): void {
    this.gl.deleteProgram(this.program);
  }
}

🧱 RenderGeometry.ts

ts 复制代码
export abstract class RenderGeometry {
  protected positions: Float32Array = new Float32Array(0);
  protected indices: Uint16Array = new Uint16Array(0);

  getPositions(): Float32Array {
    return this.positions;
  }

  getIndices(): Uint16Array {
    return this.indices;
  }

  getCount(): number {
    return this.indices.length;
  }
}

// 示例:用两个三角形模拟一条线段(加宽)
export class RenderSegment extends RenderGeometry {
  constructor(start: [number, number], end: [number, number], thickness: number = 2) {
    super();
    const dx = end[0] - start[0];
    const dy = end[1] - start[1];
    const len = Math.hypot(dx, dy);
    if (len === 0) return;

    const nx = dy / len * thickness * 0.5;
    const ny = -dx / len * thickness * 0.5;

    const p0 = [start[0] - nx, start[1] - ny];
    const p1 = [start[0] + nx, start[1] + ny];
    const p2 = [end[0] + nx, end[1] + ny];
    const p3 = [end[0] - nx, end[1] - ny];

    this.positions = new Float32Array([
      p0[0], p0[1],
      p1[0], p1[1],
      p2[0], p2[1],
      p3[0], p3[1]
    ]);

    this.indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
  }
}

🖌️ Renderer2D.ts

ts 复制代码
import { mat4 } from 'gl-matrix';

export class Renderer2D {
  private program: ShaderProgram;
  private uProjection: WebGLUniformLocation | null = null;
  private uView: WebGLUniformLocation | null = null;

  constructor(program: ShaderProgram) {
    this.program = program;
    this.program.use();
    this.uProjection = program.getUniformLocation('uProjection');
    this.uView = program.getUniformLocation('uView');
  }

  render(geometries: RenderGeometry[], viewMatrix: mat4, projectionMatrix: mat4): void {
    this.program.use();

    if (this.uProjection) this.program.setUniformMatrix4fv('uProjection', projectionMatrix);
    if (this.uView) this.program.setUniformMatrix4fv('uView', viewMatrix);

    for (const geom of geometries) {
      const vbo = new GLBuffer(this.program.gl, BufferType.ARRAY_BUFFER);
      vbo.data(geom.getPositions(), BufferType.ARRAY_BUFFER, UsageHint.STATIC_DRAW);

      const ibo = new GLBuffer(this.program.gl, BufferType.ELEMENT_ARRAY_BUFFER);
      ibo.data(geom.getIndices(), BufferType.ELEMENT_ARRAY_BUFFER, UsageHint.STATIC_DRAW);

      const layout = new VertexLayout()
        .addAttribute({
          name: 'aPosition',
          size: 2,
          type: this.program.gl.FLOAT,
          normalized: false
        });

      const vao = new VertexArray(this.program.gl, vbo, layout, ibo);
      vao.drawElements(this.program.gl.TRIANGLES, geom.getCount(), this.program.gl.UNSIGNED_SHORT, 0);
      vao.destroy(); // 简单示例中每帧重建,实际可用对象池优化
    }
  }
}

🧠 CanvasCore.ts

ts 复制代码
export class CanvasCore {
  public readonly gl: WebGL2RenderingContext;
  public readonly renderer: Renderer2D;
  public readonly config: CanvasConfig;

  private geometries: RenderGeometry[] = [];
  public isDrawing: boolean = false;

  constructor(
    gl: WebGL2RenderingContext,
    renderer: Renderer2D,
    config: CanvasConfig
  ) {
    this.gl = gl;
    this.renderer = renderer;
    this.config = config;
  }

  renderFrame(viewMatrix: Float32Array, projectionMatrix: Float32Array): void {
    this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
    this.renderer.render(this.geometries, viewMatrix, projectionMatrix);
  }

  addGeometry(geom: RenderGeometry): void {
    if (this.isDrawing) {
      this.geometries.push(geom);
    }
  }

  clearGeometries(): void {
    this.geometries = [];
  }
}

🖱️ EventModule.ts(支持 zoom/pan)

ts 复制代码
import { View } from './View';
import { CanvasCore } from './CanvasCore';
import { RenderSegment } from './RenderGeometry';

export class EventModule {
  private view: View;
  private core: CanvasCore;
  private lastPanPoint: [number, number] | null = null;

  constructor(view: View, core: CanvasCore) {
    this.view = view;
    this.core = core;
  }

  onPanStart(x: number, y: number): void {
    this.lastPanPoint = [x, y];
  }

  onPanMove(x: number, y: number): void {
    if (!this.lastPanPoint) return;

    const delta = [x - this.lastPanPoint[0], y - this.lastPanPoint[1]];
    const startWorld = this.view.screenToWorld(this.lastPanPoint);
    const currentWorld = this.view.screenToWorld([x, y]);

    const panDelta = [startWorld[0] - currentWorld[0], startWorld[1] - currentWorld[1]];

    const translation = [panDelta[0], panDelta[1]];
    const translateMat = mat4.create();
    mat4.translate(translateMat, this.view.viewMatrix, translation);
    mat4.copy(this.view.viewMatrix, translateMat);

    this.lastPanPoint = [x, y];
  }

  onZoom(scale: number, centerX: number, centerY: number): void {
    const worldCenterBefore = this.view.screenToWorld([centerX, centerY]);

    // 缩放 view matrix(可改为修改投影)
    const scaleMat = mat4.create();
    mat4.scale(scaleMat, this.view.viewMatrix, [scale, scale, 1]);
    mat4.copy(this.view.viewMatrix, scaleMat);

    const worldCenterAfter = this.view.screenToWorld([centerX, centerY]);

    const fix = [
      worldCenterBefore[0] - worldCenterAfter[0],
      worldCenterBefore[1] - worldCenterAfter[1]
    ];

    mat4.translate(this.view.viewMatrix, this.view.viewMatrix, [fix[0], fix[1], 0]);
  }

  onMouseDown(x: number, y: number): void {
    this.core.isDrawing = true;
    this.onPanStart(x, y);
  }

  onMouseMove(x: number, y: number): void {
    if (this.core.isDrawing && this.lastPanPoint) {
      const seg = new RenderSegment(this.lastPanPoint, [x, y], 2);
      this.core.addGeometry(seg);
      this.lastPanPoint = [x, y];
    } else {
      this.onPanMove(x, y);
    }
  }

  onMouseUp(): void {
    this.core.isDrawing = false;
    this.lastPanPoint = null;
  }
}

✅ 使用示例(外部调用)

ts 复制代码
// 假设你已获取 gl 上下文
const canvas = document.getElementById('render-canvas') as HTMLCanvasElement;
const gl = canvas.getContext('webgl2')!;

// 配置
const config = new CanvasConfig({ x: 0, y: 0, width: canvas.width, height: canvas.height });
const view = new View(config.canvasClientRect);

// 着色器
const vs = `
  attribute vec2 aPosition;
  uniform mat4 uView;
  uniform mat4 uProjection;
  void main() {
    gl_Position = uProjection * uView * vec4(aPosition, 0, 1);
  }
`;

const fs = `
  precision mediump float;
  void main() {
    gl_FragColor = vec4(1, 0, 0, 1);
  }
`;

const vertexShader = new Shader(gl, vs, ShaderType.VERTEX);
const fragmentShader = new Shader(gl, fs, ShaderType.FRAGMENT);
const program = new ShaderProgram(gl, vertexShader, fragmentShader);
const renderer = new Renderer2D(program);

const core = new CanvasCore(gl, renderer, config);
const eventModule = new EventModule(view, core);

// 绑定事件(此处仅为示意)
canvas.addEventListener('mousedown', e => {
  const rect = canvas.getBoundingClientRect();
  eventModule.onMouseDown(e.clientX - rect.left, e.clientY - rect.top);
});

// 动画循环
function animate() {
  core.renderFrame(view.viewMatrix, view.projectionMatrix);
  requestAnimationFrame(animate);
}
animate();

🌟 特点总结

模块 职责说明
CanvasCore 核心协调器,持有状态
Renderer2D 渲染几何体列表
RenderGeometry 几何数据生成(如线段)
GLBuffer/VAO GPU 数据管理
ShaderProgram 着色器编译与使用
View 相机控制,坐标转换
EventModule 支持交互(zoom/pan/draw)

相关推荐
Robet5 小时前
WebGL2D渲染引擎
webgl
goodName1 天前
如何实现精准操控?Cesium模型移动旋转控件实现
webgl·cesium
丫丫7237344 天前
Three.js 模型树结构与节点查询学习笔记
javascript·webgl
allenjiao6 天前
WebGPU vs WebGL:WebGPU什么时候能完全替代WebGL?Web 图形渲染的迭代与未来
前端·图形渲染·webgl·threejs·cesium·webgpu·babylonjs
mapvthree7 天前
mapvthree Engine 设计分析——二三维一体化的架构设计
webgl·数字孪生·mapvthree·jsapi2d·jsapigl·引擎对比
GISer_Jing8 天前
3D Cesium渲染架剖析
javascript·3d·webgl
Swift社区8 天前
用 Chrome DevTools 深度分析 Vue WebGL 内存泄漏(进阶篇)
vue.js·webgl·chrome devtools
GISer_Jing10 天前
3DThreeJS渲染核心架构深度解析
javascript·3d·架构·webgl
ThreePointsHeat10 天前
Unity 关于打包WebGL + jslib录制RenderTexture画面
unity·c#·webgl