我用 WebGL 从「画个点」一路画到「贪吃蛇」

我用 WebGL 从「画个点」一路画到「贪吃蛇」

大家好,我是tk 最近闲着没事干,学习ai学的脑子发蒙,想着学点可视化的换换脑子 所以就学了学webgl玩玩,下面是我学了几天总结下来的小经验


目录

  1. [第 1 步:画出人生第一个点](#第 1 步:画出人生第一个点 "#%E7%AC%AC-1-%E6%AD%A5%E7%94%BB%E5%87%BA%E4%BA%BA%E7%94%9F%E7%AC%AC%E4%B8%80%E4%B8%AA%E7%82%B9")
  2. [第 2 步:把点连成线](#第 2 步:把点连成线 "#%E7%AC%AC-2-%E6%AD%A5%E6%8A%8A%E7%82%B9%E8%BF%9E%E6%88%90%E7%BA%BF")
  3. [第 3 步:让图形动起来(着色器变量)](#第 3 步:让图形动起来(着色器变量) "#%E7%AC%AC-3-%E6%AD%A5%E8%AE%A9%E5%9B%BE%E5%BD%A2%E5%8A%A8%E8%B5%B7%E6%9D%A5%E7%9D%80%E8%89%B2%E5%99%A8%E5%8F%98%E9%87%8F%E7%89%88")
  4. [第 4 步:换矩阵来做变换](#第 4 步:换矩阵来做变换 "#%E7%AC%AC-4-%E6%AD%A5%E6%8D%A2%E7%9F%A9%E9%98%B5%E6%9D%A5%E5%81%9A%E5%8F%98%E6%8D%A2")
  5. [第 5 步:综合实战------贪吃蛇](#第 5 步:综合实战——贪吃蛇 "#%E7%AC%AC-5-%E6%AD%A5%E7%BB%BC%E5%90%88%E5%AE%9E%E6%88%98%E8%B4%AA%E5%90%83%E8%9B%87")
  6. 附录:踩坑清单

第 1 步:画出人生第一个点

目标:点击画布,出现一个红点。

这一步要把 WebGL 的「最小闭环」全部打通------后面每一步都在这上面加料。

1.1 拿到 WebGL 上下文

一切从 <canvas> 开始:

ts 复制代码
const canvas = document.getElementById('canvas') as HTMLCanvasElement
const gl = canvas.getContext('webgl')
if (!gl) throw new Error('WebGL not supported')

getContext('webgl') 拿到的是一个状态机:你绑什么缓冲、用什么着色器、设什么颜色,它都记着。后面几乎所有操作都是在「改状态 → 下指令」。

1.2 画布尺寸:新手第一坑

CSS 写 width: 800px 只影响显示多大

WebGL 真正画画的「纸」由 canvas.width / height 决定,默认只有 300×150

ts 复制代码
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
gl.viewport(0, 0, canvas.width, canvas.height)
  • canvas.width/height:绘图缓冲分辨率
  • viewport:告诉 GPU「裁剪空间 [-1,1] 映射到缓冲的哪块矩形」

只设 CSS、不设这两样 → 经典黑屏 / 位置错乱套餐。我为此贡献了整整一下午。

1.3 什么是着色器?为什么要两个?

WebGL 不帮你「画个圆」「画个方」。它只认:

  1. 顶点着色器(Vertex Shader) :每个顶点跑一遍,算出最终位置 gl_Position
  2. 片元着色器(Fragment Shader) :每个像素(片元)跑一遍,算出颜色 gl_FragColor

可以粗暴理解成:

顶点着色器管「画在哪」,片元着色器管「涂啥色」。

顶点着色器
glsl 复制代码
attribute vec2 a_position; // 每个顶点自己的数据(从缓冲里来)
uniform mat4 u_matrix;     // 所有顶点共享的数据(投影矩阵)

void main() {
  // 必须输出到裁剪空间
  gl_Position = u_matrix * vec4(a_position, 0.0, 1.0);
  gl_PointSize = 10.0; // 只有画 POINTS 时才有用
}
片元着色器
glsl 复制代码
precision mediump float;   // WebGL1 片元着色器必须写精度
uniform vec4 u_color;

void main() {
  gl_FragColor = u_color;
}
学习点:attribute vs uniform
attribute uniform
谁有一份 每个顶点各一份 整次绘制共用一份
典型用途 位置、颜色、UV 矩阵、时间、全局颜色
数据从哪来 顶点缓冲(ARRAY_BUFFER) JS 里 uniformXxx 塞进去

1.4 着色器怎么变成能跑的 Program

GLSL 源码本身 GPU 不吃,要走一套仪式:

ts 复制代码
// 1. 创建着色器对象
const vs = gl.createShader(gl.VERTEX_SHADER)!
const fs = gl.createShader(gl.FRAGMENT_SHADER)!

// 2. 塞源码 + 编译
gl.shaderSource(vs, vertexSource)
gl.compileShader(vs)
gl.shaderSource(fs, fragmentSource)
gl.compileShader(fs)

// 3. 创建程序,挂上两个着色器,链接
const program = gl.createProgram()!
gl.attachShader(program, vs)
gl.attachShader(program, fs)
gl.linkProgram(program)

// 4. 启用
gl.useProgram(program)

记住口诀:创建 → 源码 → 编译 → 挂载 → 链接 → 使用

链接失败 / 编译失败时建议查 getShaderInfoLog / getProgramInfoLog,否则它就静默让你对着黑屏发呆。

1.5 裁剪空间与正交投影

GPU 最终只认 裁剪空间x/y/z 都在大约 [-1, 1]

可人点鼠标拿到的是像素坐标,比如 (320, 480)

所以需要 投影矩阵 当翻译官。2D 常用正交投影:

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

const projectMat4 = mat4.create()
mat4.ortho(projectMat4, 0, width, height, 0, -1, 1)

gl-matrix是大佬封装的开源数学库,有了这个我们就不用库库计算了。 感谢大佬✋︎😭✋︎

上面这个设定的含义:

  • 左上角是 (0, 0)
  • X 向右增大,Y 向下增大(和 DOM 点击坐标一致)
  • 像素 (x, y) 会被映射进裁剪空间

然后把矩阵交给着色器:

ts 复制代码
const uMatrix = gl.getUniformLocation(program, 'u_matrix')
gl.uniformMatrix4fv(uMatrix, false, projectMat4)
// 第二个参数 false:不要转置(gl-matrix 已经是列主序)

1.6 顶点缓冲:数据住在 GPU 里

JS 数组 GPU 直接用不了,要上传到 Buffer

ts 复制代码
const buffer = gl.createBuffer()!
gl.bindBuffer(gl.ARRAY_BUFFER, buffer) // 绑定到「当前 ARRAY_BUFFER 槽位」

WebGL 很多 API 不直接传 buffer 对象,而是操作「当前绑定的那个」------所以你会反复看到 bindBuffer

告诉 GPU:怎么从缓冲里读 a_position

ts 复制代码
const aPosition = gl.getAttribLocation(program, 'a_position')
gl.enableVertexAttribArray(aPosition) // 打开这个属性通道
gl.vertexAttribPointer(
  aPosition,
  2,        // 每个顶点读 2 个 float(x, y)
  gl.FLOAT,
  false,    // 不归一化
  0,        // stride:0 表示紧密排列
  0,        // offset:从缓冲开头读
)

学习点:vertexAttribPointer 的 size 必须和数据布局一致。

你存的是 x,y,x,y...,就写 2;写 4 会把别人的数据当自己的坐标,图形当场变形。

1.7 颜色:那个差点击败我的 u_color

ts 复制代码
const uColor = gl.getUniformLocation(program, 'u_color')
gl.uniform4f(uColor, 1, 0, 0, 1) // RGBA,大红

忘了设的话,uniform 默认接近 (0,0,0,0)

黑底 + 透明黑 = 你画了,宇宙当没看见。新手排查第一招:先涂大红保命。

1.8 点击交互:动态更新缓冲

ts 复制代码
const points: number[] = []

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top
  points.push(x, y)

  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.DYNAMIC_DRAW)

  draw()
})

学习点:bufferData 的 usage 提示

  • STATIC_DRAW:基本不变(几何轮廓)
  • DYNAMIC_DRAW:经常改(点击加点、每帧更新蛇身)

1.9 真正开画:clear + drawArrays

ts 复制代码
function draw() {
  gl.clearColor(0, 0, 0, 1)          // 设置清屏颜色
  gl.clear(gl.COLOR_BUFFER_BIT)      // 清掉颜色缓冲
  gl.drawArrays(gl.POINTS, 0, points.length / 2)
  //                          ↑ 从第 0 个顶点
  //                             ↑ 画几个顶点(不是 float 个数!)
}

学习点:图元类型 POINTS

  • 每个顶点画成一个点
  • 点的大小看顶点着色器里的 gl_PointSize

学习点:count 怎么算

数据是 [x0,y0, x1,y1, x2,y2] → 6 个 float → 3 个顶点count = length / 2

1.10 我踩的坑:空缓冲硬画

一度同时挂了两套点击逻辑:旧的直接 drawArrays(..., 0, 1),新的才写数据。

旧逻辑先冲上去,对着空缓冲喊「画一个!」:

text 复制代码
GL_INVALID_OPERATION: Vertex buffer is not big enough for the draw call

翻译:你让我读顶点,缓冲里没那么多数据。

bufferData,再 draw

第 1 步过关标准

点几下,冒出红点 → 上下文、着色器、缓冲、投影、绘制,整条管线通了。


第 2 步:把点连成线

目标:用一组顶点画出闭合折线。

新东西:索引缓冲drawElementsLINE_LOOP、NDC 坐标。

2.1 为啥需要索引?

假设画矩形,四个角,两个三角形:

text 复制代码
顶点:A B C D
三角形1:A B C
三角形2:A C D

如果不按索引,C 和 A 要存两遍。顶点一多就浪费。

索引缓冲(ELEMENT_ARRAY_BUFFER) 存的是「番号顺序」,顶点只存一份。

2.2 换一套坐标系:NDC

这一步我改用裁剪空间直接写顶点:

ts 复制代码
mat4.ortho(projectMat4, -1, 1, -1, 1, -1, 1)

顶点可以直接写:

ts 复制代码
const points = [
  0, 0.5,
  0.15, 0.15,
  0.5, 0,
  // ...
]

学习点:两种常用 2D 坐标

方式 ortho 设定 顶点怎么写 适合
像素坐标 (0,w, h,0) (320, 200) 这种 跟鼠标、格子对齐
NDC (-1,1, -1,1) (-0.5, 0.3) 这种 纯几何演示

2.3 创建索引缓冲

ts 复制代码
const indices = new Uint16Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

const indexBuffer = gl.createBuffer()!
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)

注意绑定目标是 ELEMENT_ARRAY_BUFFER ,不是 ARRAY_BUFFER

两个槽位互不抢:顶点走 ARRAY,索引走 ELEMENT。

2.4 drawElements + LINE_LOOP

ts 复制代码
gl.drawElements(
  gl.LINE_LOOP,          // 图元:闭合折线
  indices.length,        // 索引个数
  gl.UNSIGNED_SHORT,     // 和 Uint16Array 对应
  0,                     // 从索引缓冲开头读
)

学习点:常见线相关图元

图元 含义
LINES 两两一对:0-1,2-3,4-5...
LINE_STRIP 依次连:0-1-2-3...,不闭合
LINE_LOOP 依次连,并且自动把末尾接回起点

2.5 和画点的对比(巩固)

线
API drawArrays drawElements
要不要索引 不需要 需要
图元 POINTS LINE_LOOP
gl_PointSize 有用 没用

2.6 我踩的坑

  1. 数据按 4 分量存,pointer 按 2 分量读 → 形状像被洗衣机搅过

第 2 步过关标准

屏幕上出现红色闭合折线 → 你已经会「按索引组装图元」了。


第 3 步:让图形动起来(着色器变量版)

目标:画字母「F」,用按钮平移 / 旋转 / 缩放。

关键思想:顶点数据不动,只改 uniform,让 GPU 每帧重算位置。

3.1 先把 F 用三角形拼出来

F 可以拆成三块矩形,每块矩形 = 2 个三角形 = 6 个索引:

ts 复制代码
// 顶点(像素坐标)
const f = [
  0,0,  30,0,  0,150,  30,150,   // 左竖
  100,0, 30,30, 100,30,          // 上横相关点
  30,60, 67,60, 30,90, 67,90,    // 中横
]

const indices = new Uint16Array([
  0,1,2, 2,1,3,     // 左竖两个三角
  1,4,5, 5,4,6,     // 上横
  7,8,9, 9,8,10,    // 中横
])

学习点:图元 TRIANGLES

每 3 个顶点(或 3 个索引)组成一个实心三角。

WebGL 没有「直接画矩形」的图元,矩形都是两个三角拼的。

3.2 在顶点着色器里手写变换

glsl 复制代码
attribute vec2 a_position;
uniform mat4 u_matrix;
uniform vec2 u_rotation;    // (sinθ, cosθ)
uniform vec2 u_translation;
uniform vec2 u_scale;

void main() {
  // 1) 旋转
  vec2 rotated = vec2(
    a_position.x * u_rotation.y + a_position.y * u_rotation.x,
    a_position.y * u_rotation.y - a_position.x * u_rotation.x
  );
  // 2) 缩放
  vec2 scaled = rotated * u_scale;
  // 3) 平移
  vec2 world = u_translation + scaled;

  gl_Position = u_matrix * vec4(world, 0.0, 1.0);
}

学习点:变换顺序很重要

这里是 先旋转 → 再缩放 → 最后平移

换顺序结果会不一样。以后用矩阵时,顺序问题会更明显。

学习点:为啥旋转传 sin/cos

三角函数在 GPU 里也能算,但每顶点都算一遍浪费。

JS 算一次 sin/cos,当 uniform 传进去,所有顶点共享------更划算。

3.3 JS 侧:状态 + 按钮 + 重绘

ts 复制代码
const transform = {
  angle: 0,
  x: 0,
  y: 0,
  scaleX: 1,
  scaleY: 1,
}

function draw() {
  gl.uniform2f(uRotation, Math.sin(transform.angle), Math.cos(transform.angle))
  gl.uniform2f(uScale, transform.scaleX, transform.scaleY)
  gl.uniform2f(uTranslation, transform.x, transform.y)

  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0)
}

// 按钮只改状态,然后 draw
translateX.onclick = () => { transform.x += 20; draw() }
rotate.onclick = () => { transform.angle += Math.PI / 12; draw() }
scale.onclick = () => {
  transform.scaleX *= 1.1
  transform.scaleY *= 1.1
  draw()
}

学习点:CPU 改参数,GPU 算位置

顶点数组始终是那个 F。动起来靠的是每帧改 uniform 再画一遍------这就是最原始的「动画」。

3.4 我踩的坑

draw 里一度写死:

ts 复制代码
gl.uniform2f(uTranslation, 0, 0) // 💀

按钮狂点,F 纹丝不动。不是事件没绑上,是状态传到 GPU 的路上被我掐断了。

3.5 这一步的收获和局限

收获:理解了「变换可以发生在着色器里」。

局限:公式一多就崩(绕任意点转、父子节点......)。

所以下一步请矩阵出山。

第 3 步过关标准

点按钮,F 会平移/旋转/缩放 → GPU 侧变换直觉建立完成。


第 4 步:换矩阵来做变换

目标:还是 F,但用 模型矩阵 表达变换。

你会发现着色器突然变短,JS 里开始「叠 buff」。

4.1 为啥需要矩阵?

二维里一个点的变换,其实都能写成:

text 复制代码
新位置 = 矩阵 × 旧位置

平移、旋转、缩放,各自对应一个矩阵。

组合变换 = 矩阵相乘。

引擎里乱七八糟的变换,最后都是在乘矩阵。

我们用 gl-matrixmat4(4×4,兼容 3D,2D 也能用)。

4.2 着色器变得很干净

glsl 复制代码
attribute vec2 a_position;
uniform mat4 u_matrix; // 投影矩阵
uniform mat4 u_model;  // 模型矩阵

void main() {
  gl_Position = u_matrix * u_model * vec4(a_position, 0.0, 1.0);
}

学习点:矩阵乘法顺序(从右往左作用)

text 复制代码
gl_Position = 投影 × 模型 × 顶点

含义:先对顶点做模型变换(挪到世界里),再投影到裁剪空间。

写反了,画面会怪到你怀疑人生。

4.3 JS 里累乘模型矩阵

ts 复制代码
const modelMat4 = mat4.create() // 单位矩阵起步

mat4.translate(modelMat4, modelMat4, [20, 0, 0])
mat4.rotateZ(modelMat4, modelMat4, angleInRad)
mat4.scale(modelMat4, modelMat4, [1.1, 1.1, 1])

gl.uniformMatrix4fv(modelLocation, false, modelMat4)

学习点:这些 API 都是「在现有矩阵上继续乘」

每次点击都在原矩阵上叠加,所以会越移越远、越转越多------符合「按钮累加」的直觉。

学习点:角度要用弧度

ts 复制代码
const degToRad = (deg: number) => (deg * Math.PI) / 180

rotateZ 吃的是弧度,传 60 以为转 60°,其实转上天了。

4.4 绕中心旋转

默认旋转是绕原点 (0,0)。想绕 F 自己的中心转,套路是:

text 复制代码
平移到中心 → 旋转 → 平移回去
ts 复制代码
mat4.translate(model, model, [cx, cy, 0])
mat4.rotateZ(model, model, rad)
mat4.translate(model, model, [-cx, -cy, 0])

学习点:矩阵乘法不交换

A×B 不等于 B×A。顺序写反,F 可能原地转,也可能飞出屏幕去度假。

4.5 和第 3 步对比

着色器变量硬算 模型矩阵
变换写哪 GLSL 公式 JS mat4 累乘
着色器 又长又具体 一行乘完
组合变换 越写越乱 继续乘就行
像不像引擎 演示向 更接近真实做法

第 4 步过关标准

会平移、旋转、缩放、绕中心转 → 你已经掌握 2D 模型变换的核心武器。

盘点:现在你手里有啥

  • 点 / 线 / 三角形
  • 顶点缓冲 + 索引缓冲
  • attribute / uniform
  • 投影矩阵 + 模型矩阵
  • 清屏与多种图元

可以拼游戏了。


第 5 步:综合实战------贪吃蛇

目标:不追新 API,把前面积木拼成能玩的东西。

这一步重点是:同一套管线,怎么服务多个物体、怎么每帧更新、怎么处理游戏逻辑。

5.1 把需求映射回 WebGL

游戏需求 WebGL 怎么做
网格地图 LINES 画横竖线
蛇身 每节格子 → 两个三角形 → TRIANGLES
食物 一个小方块 → TRIANGLE_FAN 也能画
不同颜色 画之前改 u_color
持续动 requestAnimationFrame 循环里 update + draw
格子坐标 逻辑用格子,绘制时乘 cellSize 变像素

5.2 多物体 = 多次 draw(换缓冲 / 换颜色)

蛇、地图、食物顶点不同、颜色不同。新手最朴素的做法:

ts 复制代码
// 画地图
bindBuffer(mapBuffer)
setColor([0.25, 0.25, 0.25, 1])
gl.drawArrays(gl.LINES, 0, mapVertexCount)

// 画蛇
bindBuffer(snakeBuffer)
setColor([0, 0, 1, 1])
gl.drawArrays(gl.TRIANGLES, 0, snakeVertexCount)

// 画食物
bindBuffer(foodBuffer)
setColor([1, 0.15, 0.15, 1])
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)

学习点:一次 draw 用的是「当前状态」

当前 program、当前 ARRAY_BUFFER 绑定、当前 attribute 指针、当前 uniform 值......

换东西画之前,把该绑的绑好、该改的改好。

学习点:TRIANGLE_FAN

从第一个顶点出发扇形展开。四个点刚好能围成一个方块,写食物很方便。

5.3 格子坐标 → 像素顶点

逻辑层蛇头在格子 (5, 3),cellSize=20,则像素矩形是:

ts 复制代码
const x = 5 * 20
const y = 3 * 20
// 矩形四个角:
// (x,y) (x+20,y) (x+20,y+20) (x,y+20)
// 再拆成两个三角形的 6 个顶点

学习点:逻辑与渲染分离

  • 逻辑:格子、方向、碰撞,全是整数
  • 渲染:只负责把格子变成三角形顶点丢给 GPU

这样调试简单:游戏错了查逻辑,画面错了查顶点生成。

5.4 游戏循环与固定时间步

屏幕刷新率可能 60/120/144,但蛇不能因此跑速翻倍。做法:

ts 复制代码
let last = 0
let acc = 0
const STEP = 150 // 毫秒走一格

function loop(now: number) {
  const dt = now - last
  last = now
  acc += dt

  while (acc >= STEP) {
    update()      // 只在这里改蛇的格子位置
    acc -= STEP
  }

  const progress = acc / STEP // 0~1,用于插值
  render(progress)
  requestAnimationFrame(loop)
}

学习点:固定时间步

逻辑更新频率固定,画面怎么刷都不影响手感。

5.5 插值:让移动不那么「蹦」

蛇逻辑上是一格一格跳的,但渲染可以在两格之间插值:

ts 复制代码
const x = (prev.x + (curr.x - prev.x) * progress) * cellSize
const y = (prev.y + (curr.y - prev.y) * progress) * cellSize

学习点:previousBody

每走一步前先备份身体,渲染时用「旧位置 → 新位置」插值,看起来就丝滑了。

5.6 碰撞、生长、输入(游戏规则,但影响你怎么画)

ts 复制代码
// 下一格是食物 → 生长:头 unshift,尾巴不 pop
// 出界 / 撞自己 → Game Over,停掉 rAF
// 输入与当前方向相反 → 忽略(防止当场表演 U 转咬自己)

吃到食物后要 bufferData 更新蛇的顶点(更长了),食物也要重新随机并更新缓冲。

5.7 一帧里到底发生了啥(串起来)

text 复制代码
requestAnimationFrame
  → 累积时间,必要时 update(改格子数据)
  → 根据格子 + progress 生成像素顶点
  → clear
  → 绑地图缓冲,设灰色,draw LINES
  → 绑蛇缓冲,设蓝色,draw TRIANGLES
  → 绑食物缓冲,设红色,draw TRIANGLE_FAN

回头看:全部都是第 1~4 步的零件,没有魔法。

展示一下成果


附录:踩坑清单

现象 人话原因 处理
整页黑屏 绘图缓冲还是 300×150,viewport 对不上 canvas.width/heightviewport
画了看不见 忘了 u_color,透明黑画在黑底上 uniform4f(...,1,0,0,1)
buffer not big enough 空缓冲就 draw,或 count 大于实际顶点 bufferData;count=顶点数
形状扭曲 vertexAttribPointer 的 size/stride 和数据不一致 vec2 就写 size 2
Y 轴反了 投影 Y 方向和点击换算不一致 统一「Y 向下」或统一换算
按钮点了没动 uniform 写死了 / 改完没 draw 用当前状态更新 uniform 再画
绕中心转飞了 矩阵乘顺序反了 先移到中心,再转,再移回
角度离谱 把角度当弧度传了 deg * Math.PI / 180

调试三板斧

  1. 看控制台有没有 GL 报错(WebGL 很爱默默失败)
  2. 打印 canvas.widthcanvas.clientWidth,不一样先别往下写
  3. 先画一个固定三角形证明管线通,再加点击、动画、游戏逻辑

五步串讲(合上书前回看)

text 复制代码
第1步 点
  学会:上下文、画布尺寸、着色器、Program、
        attribute/uniform、投影、VBO、POINTS、clear/draw

第2步 线
  学会:索引缓冲、drawElements、LINE_LOOP、NDC

第3步 着色器变换
  学会:TRIANGLES 拼几何、用 uniform 做平移旋转缩放、
        CPU 改状态 GPU 算位置

第4步 矩阵变换
  学会:模型矩阵、矩阵乘法顺序、绕中心旋转、mat4 累乘

第5步 贪吃蛇
  学会:多物体多次 draw、换颜色、格子到像素、
        rAF、固定时间步、插值动画

一句话:

WebGL = 把数据塞进缓冲,让着色器算位置和颜色,然后按图元喊「画!」

矩阵只是让「算位置」少写点神仙公式。


最后强烈推荐一个开源的学习笔记[webglfundamentals.org/webgl/lesso...](https://link.juejin.cn?target=https%3A%2F%2Fwebglfundamentals.org%2Fwebgl%2Flessons%2Fzh_cn%2F "https://webglfundamentals.org/webgl/lessons/zh_cn/")

相关推荐
xiaobaoyu1 小时前
前端代码常用规范
前端
xiaobaoyu1 小时前
img,word,excel,pdf文件在线预览
前端
lichenyang4531 小时前
从 H5 到 HarmonyOS:我做了一个 npm + OHPM 双端发布的 ArkWeb JSBridge 框架
前端
用户84298142418101 小时前
AI时代,JS混淆加密还有用吗?
前端·javascript
xiaobaoyu1 小时前
如何理解前端项目中的绝对路径和相对路径
前端
xiaobaoyu1 小时前
vue3+vite+vant4项目实战总结
前端
咖啡星人k1 小时前
【无标题】
前端·ai·github
颜进强2 小时前
LangChain 从入门到实践:用最小案例理解 RAG 的 5 个核心抽象
前端·后端·ai编程
颜进强2 小时前
MCP 从入门到实践:用 TypeScript 实现第一个 MCP Server
前端·后端·ai编程