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)

相关推荐
Aurora@Hui1 天前
WebGL & Three.js
webgl
CC码码3 天前
基于WebGPU实现canvas高级滤镜
前端·javascript·webgl·fabric
ct9783 天前
WebGL 图像处理核心API
图像处理·webgl
ct9785 天前
Cesium 矩阵系统详解
前端·线性代数·矩阵·gis·webgl
ct9788 天前
WebGL Shader性能优化
性能优化·webgl
棋鬼王8 天前
Cesium(一) 动态立体墙电子围栏,Wall墙体瀑布滚动高亮动效,基于Vue3
3d·信息可视化·智慧城市·webgl
Longyugxq11 天前
Untiy的Webgl端网页端视频播放,又不想直接mp4格式等格式的。
unity·音视频·webgl
花姐夫Jun11 天前
cesium基础学习-坐标系统相互转换及相应的场景
学习·webgl
ct97812 天前
WebGL开发
前端·gis·webgl
作孽就得先起床12 天前
unity webGL导出.glb模型
unity·c#·游戏引擎·webgl