📁 项目目录结构
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;
🚀 如何运行?
- 将以上文件保存到本地目录
webgl2d-renderer/ - 使用 本地服务器 打开
index.html(不能双击直接打开)
推荐方式:
- VS Code + Live Server 插件
- 或运行命令:
npx serve - 或 Python:
python3 -m http.server 8000
访问 http://localhost:8000 即可看到运行效果!
✅ 功能演示
| 操作 | 效果 |
|---|---|
| 点击“画点” | 下次点击画布生成一个点 |
| 点击“画线段” | 拖拽绘制一条线 |
| 点击“画矩形” | 拖拽绘制矩形边框 |
| 右键拖拽 | 平移视图 |
| 滚轮 | 以鼠标为中心缩放 |
| “清空画布” | 删除所有图形 |
📦 下一步建议
- ✅ 添加撤销功能(维护
history栈) - ✅ 支持图形选中与编辑
- ✅ 导出为 JSON 或 SVG
- ✅ 添加网格背景