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)

相关推荐
烛阴9 小时前
拒绝配置地狱!5 分钟搭建 Three.js + Parcel 完美开发环境
前端·webgl·three.js
WebGISer_白茶乌龙桃21 小时前
Vue3 + Mapbox 加载 SHP 转换的矢量瓦片 (Vector Tiles)
javascript·vue.js·arcgis·webgl
ThreePointsHeat5 天前
Unity WebGL打包后启动方法,部署本地服务器
unity·游戏引擎·webgl
林枫依依6 天前
电脑配置流程(WebGL项目)
webgl
冥界摄政王8 天前
CesiumJS学习第四章 替换指定3D建筑模型
3d·vue·html·webgl·js·cesium
温宇飞10 天前
高效的线性采样高斯模糊
javascript·webgl
冥界摄政王11 天前
Cesium学习第一章 安装下载 基于vue3引入Cesium项目开发
vue·vue3·html5·webgl·cesium
光影少年13 天前
三维前端需要会哪些东西
前端·webgl
nnsix14 天前
Unity WebGL jslib 通信时,传入字符串,变成数值 问题
webgl
二狗哈14 天前
Cesium快速入门34:3dTile高级样式设置
前端·javascript·算法·3d·webgl·cesium·地图可视化