WebGL 入门到进阶全解析:从 Canvas 上下文到 3D 绘制与 WebGL2 新特性

🌀 一、动画与 requestAnimationFrame

1. 早期的定时动画

概念

  • requestAnimationFrame 出现之前,开发者常用 setIntervalsetTimeout 来驱动动画。

原理

  • setInterval(fn, 16) → 每 16ms 调用一次函数,理想状态下约等于 60fps。
  • 但问题是 浏览器渲染节奏不受控制,有时会掉帧或提前调用。

对比

  • 定时器

    • 固定时间触发,与屏幕刷新率不匹配。
    • 页面不可见时仍然运行(浪费性能)。
  • rAF

    • 自动匹配刷新率(通常 60Hz,即每 16.6ms)。
    • 页面不可见时会自动暂停(节省性能)。

实践(定时器动画示例)

ini 复制代码
let x = 0;
function moveBox() {
  const box = document.getElementById("box");
  x += 2;
  box.style.transform = `translateX(${x}px)`;
}

// 每 16ms 调用一次
setInterval(moveBox, 16);

2. 时间间隔问题(精度)

概念

  • setTimeoutsetInterval实际间隔不可靠,可能受以下因素影响:

    1. 浏览器负载(CPU 忙时会延迟)。
    2. 最小时间粒度(现代浏览器最小约 4ms)。
    3. 不同设备刷新率不同(60Hz / 120Hz)。

rAF 的优势

  • 始终在下一帧绘制之前执行,跟屏幕刷新同步
  • 内部提供时间戳参数,方便计算动画进度。

实践(rAF 动画)

ini 复制代码
let x = 0;
function animate() {
  const box = document.getElementById("box");
  x += 2;
  box.style.transform = `translateX(${x}px)`;

  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

3. cancelAnimationFrame

概念

  • 类似 clearInterval,用于取消 requestAnimationFrame

原理

  • requestAnimationFrame(fn) 返回一个 id
  • 调用 cancelAnimationFrame(id) 即可停止。

实践

ini 复制代码
let animId;
function animate() {
  const box = document.getElementById("box");
  let x = parseInt(box.style.left || 0);
  box.style.left = (x + 2) + "px";

  animId = requestAnimationFrame(animate);
}

// 启动动画
animId = requestAnimationFrame(animate);

// 3 秒后停止
setTimeout(() => cancelAnimationFrame(animId), 3000);
注释:
  1. animId 保存了当前动画的 id。
  2. cancelAnimationFrame(animId) → 停止循环调用。

4. rAF 的使用 ------ 节流(以 scroll 为例)

概念

  • 节流(throttle) :避免在高频事件中反复触发昂贵操作。

  • 典型场景:scrollresize

  • 思路:

    1. scroll 事件可能一秒触发几十次。
    2. 用一个布尔标记 + rAF → 保证每帧只处理一次。

实践:scroll 事件节流

ini 复制代码
let ticking = false;

window.addEventListener("scroll", () => {
  // 如果已经有 rAF 在等待,就不再重复申请
  if (!ticking) {
    requestAnimationFrame(() => {
      // 这里是安全的绘制逻辑(每帧最多执行一次)
      console.log("Scroll Y:", window.scrollY);

      // 标记重置,允许下一帧继续
      ticking = false;
    });
    ticking = true;
  }
});
逐行注释:
  1. let ticking = false; → 标记是否已经有一个 rAF 在队列中。
  2. scroll 回调可能触发 30~100 次/秒。
  3. if (!ticking) → 保证只触发一次 rAF
  4. requestAnimationFrame(...) → 把任务放到下一帧渲染时执行。
  5. ticking = false; → 动作完成后,释放标记,允许下次继续。

🎨 二、Canvas 基本画布功能

0. toDataURL() ------ 导出画布为图片

概念

  • canvas.toDataURL(type, quality?) → 将当前画布的像素数据导出为 Base64 图片字符串

  • 常用格式:

    • "image/png"(默认,带透明)
    • "image/jpeg"(可调节质量)

原理

  • Canvas 内部维护一个位图(像素缓冲区)。
  • toDataURL 会把这块内存编码为指定格式的图片,再转成 Base64 字符串。

对比

  • toDataURL() → 返回字符串,适合预览。
  • toBlob() → 返回二进制数据,适合上传/下载。

示例

ini 复制代码
<canvas id="c1" width="200" height="200"></canvas>
<img id="preview">

<script>
const canvas = document.getElementById("c1");
const ctx = canvas.getContext("2d");

// 绘制一个红色矩形
ctx.fillStyle = "red";
ctx.fillRect(20, 20, 150, 150);

// 导出为 PNG Base64
const imgURL = canvas.toDataURL("image/png");

// 显示到 <img> 标签
document.getElementById("preview").src = imgURL;
</script>
注释:
  1. fillStyle = "red" → 设置填充颜色为红色。
  2. fillRect(20,20,150,150) → 绘制一个矩形。
  3. toDataURL("image/png") → 导出图片数据。
  4. 把导出的字符串赋值给 <img>.src,即可显示结果。

1. 2D 绘图上下文(CanvasRenderingContext2D

概念

  • 通过 canvas.getContext("2d") 获取的对象,提供 绘图 API

  • 它是一个 状态机,存储:

    • 当前颜色 / 线条样式
    • 当前路径
    • 变换矩阵(旋转、缩放)
    • 全局透明度、字体、阴影等

对比

  • "2d" → 主要用于图形绘制。
  • "webgl" → 用于 GPU 3D 渲染。

示例

ini 复制代码
const canvas = document.getElementById("c1");
const ctx = canvas.getContext("2d");

ctx.fillStyle = "blue";
ctx.fillRect(50, 50, 100, 100);

2. 填充与描边

概念

  • fillStyle → 设置填充颜色或渐变。
  • strokeStyle → 设置描边颜色。
  • lineWidth → 设置描边宽度。

示例

ini 复制代码
ctx.fillStyle = "green";   // 填充为绿色
ctx.strokeStyle = "black"; // 描边为黑色
ctx.lineWidth = 5;         // 线宽 5px

ctx.fillRect(30, 30, 120, 80);   // 绘制填充矩形
ctx.strokeRect(30, 30, 120, 80); // 绘制描边矩形

3. 绘制矩形

概念

  • fillRect(x,y,w,h) → 绘制填充矩形。
  • strokeRect(x,y,w,h) → 绘制描边矩形。
  • clearRect(x,y,w,h) → 擦除矩形区域。

示例:两个重叠的矩形 + 清除重叠的一小块

ini 复制代码
ctx.fillStyle = "blue";
ctx.fillRect(20, 20, 150, 100);   // 蓝色矩形

ctx.fillStyle = "red";
ctx.fillRect(80, 60, 150, 100);   // 红色矩形,部分重叠

// 清除重叠的一小块区域 (40×40)
ctx.clearRect(100, 80, 40, 40);

4. 绘制路径(表盘示例)

概念

  • beginPath() → 开始新路径。
  • arc(x,y,r,start,end) → 绘制圆。
  • moveTo(x,y) / lineTo(x,y) → 绘制线。
  • stroke() / fill() → 渲染路径。
  • isPointInPath(x,y) → 判断点是否在路径内。

示例:简易时钟表盘

scss 复制代码
// 外圆
ctx.beginPath();
ctx.arc(150, 150, 100, 0, Math.PI * 2);
ctx.stroke();

// 内圆(中心点)
ctx.beginPath();
ctx.arc(150, 150, 5, 0, Math.PI * 2);
ctx.fill();

// 时针
ctx.beginPath();
ctx.moveTo(150, 150);
ctx.lineTo(150, 90);
ctx.stroke();

// 分针
ctx.beginPath();
ctx.moveTo(150, 150);
ctx.lineTo(200, 150);
ctx.stroke();

判断点击位置

ini 复制代码
canvas.addEventListener("click", e => {
  const x = e.offsetX, y = e.offsetY;

  ctx.beginPath();
  ctx.arc(150, 150, 100, 0, Math.PI * 2);

  if (ctx.isPointInPath(x, y)) {
    console.log("点击在表盘内");
  }
});
多个对象如何区分?

👉 通常需要 手动维护对象列表,例如:

ini 复制代码
const objects = [
  { type: "circle", x: 150, y: 150, r: 100 },
  { type: "hand",   x1: 150, y1: 150, x2: 200, y2: 150 }
];

canvas.addEventListener("click", e => {
  const x = e.offsetX, y = e.offsetY;

  for (let obj of objects) {
    ctx.beginPath();
    if (obj.type === "circle") {
      ctx.arc(obj.x, obj.y, obj.r, 0, Math.PI * 2);
      if (ctx.isPointInPath(x, y)) console.log("点中表盘");
    }
  }
});

5. 绘制文本

概念

  • ctx.font → 设置字体,如 "24px serif"
  • ctx.fillText(text, x, y) → 填充文本。
  • ctx.strokeText(text, x, y) → 描边文本。
  • ctx.textAlign → 控制水平对齐(left, center, right)。
  • ctx.textBaseline → 控制垂直对齐(top, middle, bottom, alphabetic)。
  • ctx.measureText(text) → 获取文本宽度。

示例

ini 复制代码
ctx.font = "24px serif";         // 设置字体大小和样式
ctx.fillStyle = "purple";        // 设置填充颜色
ctx.fillText("Hello Canvas", 50, 200); // 绘制填充文本

ctx.strokeStyle = "black";       // 设置描边颜色
ctx.strokeText("Outline Text", 50, 240); // 绘制描边文本

🎭 三、Canvas 变换 (Transformation)

Canvas 提供了一套 变换矩阵 API ,让我们可以对绘制内容进行 平移、旋转、缩放、组合变换,而不是手动计算每个点的位置。

这些变换操作不会直接作用在图像像素上,而是作用在 坐标系统 上。

👉 也就是说:变换后再绘制,图形会自动跟随新的坐标系。


1. 平移 (translate)

概念

  • ctx.translate(dx, dy) → 将画布原点 (0,0) 平移到 (dx,dy)
  • 之后绘制的图形都会基于新的坐标系。

示例

ini 复制代码
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 50, 50); // 在 (0,0) 处绘制

ctx.translate(100, 50);     // 平移坐标系
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 50); // 实际位置在 (100,50)
注释:
  1. 第一个矩形在原点绘制。
  2. translate(100,50) → 原点移动到 (100,50)。
  3. 第二个矩形看似在 (0,0),其实已经被平移。

2. 旋转 (rotate)

概念

  • ctx.rotate(angle) → 旋转坐标系,单位是 弧度
  • 旋转中心是 当前原点

示例

ini 复制代码
ctx.translate(100, 100);     // 把原点移到 (100,100)
ctx.rotate(Math.PI / 4);     // 旋转 45°
ctx.fillStyle = "green";
ctx.fillRect(0, 0, 80, 40);  // 矩形被旋转
注释:
  1. 旋转前通常要 translate,否则默认绕 (0,0) 转。
  2. Math.PI/4 → 45 度。
  3. 所有后续绘制都会在旋转后的坐标系下进行。

3. 缩放 (scale)

概念

  • ctx.scale(sx, sy) → 缩放坐标系。
  • sx > 1 → 放大,0 < sx < 1 → 缩小。
  • sy 控制垂直缩放。

示例

ini 复制代码
ctx.fillStyle = "orange";
ctx.fillRect(10, 10, 50, 50); // 正常大小

ctx.scale(2, 1.5);            // X 方向放大 2 倍,Y 方向放大 1.5 倍
ctx.fillStyle = "purple";
ctx.fillRect(10, 10, 50, 50); // 实际大小变成 100×75

4. 综合变换 (transform)

概念

  • ctx.transform(a, b, c, d, e, f)矩阵相乘,改变当前变换矩阵。

  • 矩阵含义:

    css 复制代码
    [ a  c  e ]
    [ b  d  f ]
    [ 0  0  1 ]
  • 坐标 (x,y) 变换结果:

    ini 复制代码
    x' = ax + cy + e
    y' = bx + dy + f

参数意义

  • a = 水平方向缩放
  • b = 垂直倾斜
  • c = 水平倾斜
  • d = 垂直缩放
  • e = 水平平移
  • f = 垂直平移

示例

ini 复制代码
// 水平放大 1.5 倍,垂直放大 1 倍,右移 50
ctx.transform(1.5, 0, 0, 1, 50, 0);
ctx.fillStyle = "cyan";
ctx.fillRect(0, 0, 50, 50);

5. 设置变换 (setTransform)

概念

  • ctx.setTransform(a,b,c,d,e,f)直接重置 当前变换矩阵为指定值。

  • transform 的区别:

    • transform叠加
    • setTransform覆盖(重新设定)。

示例

ini 复制代码
// 先平移再旋转
ctx.translate(100, 100);
ctx.rotate(Math.PI / 6);

// 直接重置为单位矩阵 (无变换)
ctx.setTransform(1, 0, 0, 1, 0, 0);

ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 50); // 在原始坐标系绘制

6. 保存与恢复 (save / restore)

概念

  • ctx.save() → 保存当前绘制状态(样式、变换矩阵、裁剪路径等)。
  • ctx.restore() → 恢复到最近一次保存的状态。
  • 常用于:多个对象有不同变换,不想互相干扰。

示例:三个矩形不同位置

ini 复制代码
// 第一个矩形
ctx.fillStyle = "blue";
ctx.fillRect(10, 10, 50, 50);

// 第二个矩形(平移+旋转)
ctx.save();                   // 保存状态
ctx.translate(120, 50);
ctx.rotate(Math.PI / 4);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 50);
ctx.restore();                // 恢复到之前的状态

// 第三个矩形(缩放)
ctx.save();
ctx.translate(200, 50);
ctx.scale(2, 0.5);
ctx.fillStyle = "green";
ctx.fillRect(0, 0, 50, 50);
ctx.restore();
注释:
  1. 第一个矩形不受影响。
  2. 第二个矩形只在 save/restore 内旋转。
  3. 第三个矩形只在 save/restore 内缩放。
  4. restore 确保每个矩形独立。

🎨 四、Canvas 进阶功能


1. 绘制图像 (drawImage)

概念

  • ctx.drawImage() → 将图像绘制到画布上。

  • 常见用法:

    1. ctx.drawImage(img, dx, dy) → 绘制原图。
    2. ctx.drawImage(img, dx, dy, dw, dh) → 缩放绘制。
    3. ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)裁剪 + 缩放绘制

原理

  • 浏览器把 <img> 或另一个 <canvas> 的像素数据复制到目标区域。

示例:绘制与裁剪

ini 复制代码
<canvas id="c1" width="300" height="200"></canvas>
<img id="source" src="example.jpg" hidden>

<script>
const canvas = document.getElementById("c1");
const ctx = canvas.getContext("2d");
const img = document.getElementById("source");

img.onload = () => {
  // 直接绘制原图
  ctx.drawImage(img, 0, 0);

  // 缩放绘制到 (150,0),大小 100×100
  ctx.drawImage(img, 150, 0, 100, 100);

  // 裁剪 (50,50,100,100),放到 (0,100),缩放为 120×120
  ctx.drawImage(img, 50, 50, 100, 100, 0, 100, 120, 120);
};
</script>

2. 阴影 (shadowColor, shadowBlur, shadowOffsetX, shadowOffsetY)

概念

  • shadowColor → 阴影颜色。
  • shadowBlur → 模糊半径。
  • shadowOffsetX / shadowOffsetY → 阴影偏移。

示例

ini 复制代码
ctx.shadowColor = "rgba(0,0,0,0.5)"; // 半透明黑色
ctx.shadowBlur = 10;                 // 模糊半径
ctx.shadowOffsetX = 10;              // X 偏移
ctx.shadowOffsetY = 5;               // Y 偏移

ctx.fillStyle = "orange";
ctx.fillRect(50, 50, 120, 80);

3. 渐变 (createLinearGradient, createRadialGradient)

概念

  • createLinearGradient(x1,y1,x2,y2) → 线性渐变。
  • createRadialGradient(x1,y1,r1,x2,y2,r2) → 径向渐变。
  • addColorStop(offset, color) → 添加渐变色,offset ∈ [0,1]

示例

ini 复制代码
// 线性渐变
let grad1 = ctx.createLinearGradient(0, 0, 200, 0);
grad1.addColorStop(0, "red");
grad1.addColorStop(1, "blue");

ctx.fillStyle = grad1;
ctx.fillRect(10, 10, 200, 80);

// 径向渐变
let grad2 = ctx.createRadialGradient(150, 150, 10, 150, 150, 80);
grad2.addColorStop(0, "yellow");
grad2.addColorStop(1, "green");

ctx.fillStyle = grad2;
ctx.fillRect(100, 100, 120, 120);

4. 图案 (createPattern)

概念

  • createPattern(image, repeatType) → 用图像创建平铺模式。
  • repeatTyperepeat | repeat-x | repeat-y | no-repeat

示例

ini 复制代码
const img = document.getElementById("source");
img.onload = () => {
  const pattern = ctx.createPattern(img, "repeat");
  ctx.fillStyle = pattern;
  ctx.fillRect(0, 0, 300, 200);
};

5. 像素操作 (getImageData, putImageData)

概念

  • ctx.getImageData(x,y,w,h) → 获取像素数据对象。
  • ctx.putImageData(imageData, dx, dy) → 放回像素数据。
  • imageData.dataUint8ClampedArray,每 4 个数代表一个像素(RGBA)。

示例:灰度化

ini 复制代码
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let data = imgData.data;

for (let i = 0; i < data.length; i += 4) {
  let r = data[i], g = data[i+1], b = data[i+2];
  let gray = 0.3*r + 0.59*g + 0.11*b;
  data[i] = data[i+1] = data[i+2] = gray; // 设置为灰度
}

ctx.putImageData(imgData, 0, 0);

6. 合成操作 (globalCompositeOperation)

概念

  • 控制新绘制内容如何与已有内容混合。

  • 常用模式:

    • "source-over"(默认,上层覆盖下层)
    • "destination-over"(下层覆盖上层)
    • "lighter"(颜色相加)
    • "multiply"(颜色相乘,变暗)
    • "xor"(异或,交集透明)

示例

ini 复制代码
// 绘制蓝色矩形
ctx.fillStyle = "blue";
ctx.fillRect(20, 20, 120, 120);

// 改变合成模式
ctx.globalCompositeOperation = "lighter"; // 颜色相加

// 绘制红色矩形
ctx.fillStyle = "red";
ctx.fillRect(80, 80, 120, 120);

🟥 7,WebGL 基础

(0)概念与使用场景

  • WebGL (Web Graphics Library)

    • 是基于 OpenGL ES 2.0 的 JavaScript API,可以在 HTML5 Canvas 中直接调用 GPU 渲染 2D/3D 图形。
    • 特点:跨平台、跨浏览器、无插件。
  • 使用场景

    1. 3D 游戏(浏览器内运行,无需安装客户端)。
    2. 数据可视化(如三维图表、地理地图、医学成像)。
    3. 图形编辑器(类似 Photoshop 在线版)。
    4. AI/科学计算(利用 GPU 的并行计算能力)。

(1)WebGL 上下文

概念

  • WebGL 运行在 <canvas> 上,必须通过 getContext("webgl")getContext("webgl2") 获取绘图上下文。
  • 这个上下文就是和 GPU 交互的接口。

示例:获取上下文

xml 复制代码
<canvas id="glcanvas" width="400" height="400"></canvas>
<script>
// 获取 Canvas
const canvas = document.getElementById("glcanvas");

// 获取 WebGL 上下文
// 如果浏览器支持 WebGL2,优先用 webgl2
let gl = canvas.getContext("webgl2") || canvas.getContext("webgl");

// 检查是否成功
if (!gl) {
  alert("你的浏览器不支持 WebGL");
} else {
  console.log("WebGL 上下文获取成功:", gl);
}
</script>

(2)WebGL 基础绘图

初始化参数

arduino 复制代码
const gl = canvas.getContext("webgl", {
  alpha: true,              // 是否支持透明度
  depth: true,              // 是否启用深度缓冲区 (3D 计算前后遮挡)
  stencil: false,           // 是否启用模板缓冲区
  antialias: true,          // 是否抗锯齿
  premultipliedAlpha: true, // 是否使用预乘透明度
  preserveDrawingBuffer: false // 是否保留上一次绘制内容
});

(2.1)常量

WebGL 使用很多常量:

  • 清除缓冲区

    • gl.COLOR_BUFFER_BIT → 清除颜色缓冲区
    • gl.DEPTH_BUFFER_BIT → 清除深度缓冲区
    • gl.STENCIL_BUFFER_BIT → 清除模板缓冲区
  • 绘制模式

    • gl.TRIANGLES → 三角形
    • gl.LINES → 直线
    • gl.POINTS → 点

(2.2)方法命名 (uniform 变量传递)

  • uniform3iv(location, Int32Array) → 传入 3个整型数组
  • uniform4f(location, x, y, z, w) → 传入 4个浮点数
  • uniform3i(location, x, y, z) → 传入 3个整型数

总结:

  • i = int
  • f = float
  • v = vector (数组)

(2.3)绘制前准备

scss 复制代码
// 设置清屏颜色 (r,g,b,a) = 黑色
gl.clearColor(0.0, 0.0, 0.0, 1.0);

// 清除颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

解释:

  • clearColor → 设置画布底色。
  • clear → 按照设置的缓冲区常量清空。

(2.4)视口与坐标

arduino 复制代码
// 设置视口:左下角 (0,0),宽高为整个 Canvas
gl.viewport(0, 0, canvas.width, canvas.height);
  • 视口 (viewport) :WebGL 绘制的区域。
  • 坐标映射:WebGL 内部坐标系是 裁剪坐标 [-1,1] → 再映射到视口范围。

(2.5)缓冲区

arduino 复制代码
// 创建一个缓冲区
const buffer = gl.createBuffer();

// 绑定缓冲区为当前操作对象 (ARRAY_BUFFER 专门存顶点数据)
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

// 传入顶点数据(Float32Array)
const vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 删除缓冲区
// gl.deleteBuffer(buffer);

说明:

  • createBuffer() → 创建显存缓冲区对象。
  • bindBuffer(target, buffer) → 绑定目标(ARRAY_BUFFER / ELEMENT_ARRAY_BUFFER)。
  • bufferData(target, data, usage) → 把数据传给 GPU(usage 一般是 STATIC_DRAW)。
  • deleteBuffer(buffer) → 释放缓冲区。

(2.6)错误处理

go 复制代码
const error = gl.getError();
if (error !== gl.NO_ERROR) {
  console.error("WebGL 错误代码:", error);
}

常见错误常量:

  • gl.NO_ERROR → 没错误
  • gl.INVALID_ENUM → 无效枚举
  • gl.INVALID_VALUE → 无效值
  • gl.INVALID_OPERATION → 当前状态下不能执行
  • gl.OUT_OF_MEMORY → 内存不足

(3)着色器

(3.1)attribute vs uniform

  • attribute → 每个顶点不同的数据(位置、颜色、法线)。
  • uniform → 全局统一数据(变换矩阵、光照参数)。

(3.2)最简单 GLSL 着色器

csharp 复制代码
// 顶点着色器
attribute vec4 a_Position; 
void main() {
  gl_Position = a_Position; // 直接传递顶点位置
}

// 片元着色器
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
}

(3.3)创建着色器程序

ini 复制代码
function createShader(gl, type, source) {
  const shader = gl.createShader(type);       // 创建着色器对象
  gl.shaderSource(shader, source);            // 传入 GLSL 源码
  gl.compileShader(shader);                   // 编译
  return shader;
}

const vs = createShader(gl, gl.VERTEX_SHADER, vertexSource);
const fs = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);

const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
gl.useProgram(program);

(3.4)给着色器传递数据

arduino 复制代码
// 获取变量位置
const a_Position = gl.getAttribLocation(program, "a_Position");

// 指定如何解析缓冲区中的数据
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

// 启用变量
gl.enableVertexAttribArray(a_Position);

(3.5)调试着色器

less 复制代码
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
  console.error(gl.getShaderInfoLog(vs));
}
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  console.error(gl.getProgramInfoLog(program));
}

(3.6)GLSL 100 → GLSL 300

  • GLSL 100 (WebGL1) → attribute / varying
  • GLSL 300 (WebGL2) → in / out

示例:

ini 复制代码
// GLSL 100
attribute vec4 a_Position;
varying vec4 v_Color;

// GLSL 300
in vec4 a_Position;
out vec4 v_Color;

(3.7)绘制图形

scss 复制代码
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);

// 使用索引绘制
gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);

(3.8)纹理

ini 复制代码
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
              gl.RGBA, gl.UNSIGNED_BYTE, imageData);

(3.9)读取像素

arduino 复制代码
const pixels = new Uint8Array(canvas.width * canvas.height * 4);
gl.readPixels(0, 0, canvas.width, canvas.height,
              gl.RGBA, gl.UNSIGNED_BYTE, pixels);
console.log(pixels);

(4)WebGL1 vs WebGL2

  • WebGL1 基于 OpenGL ES 2.0

  • WebGL2 基于 OpenGL ES 3.0,新特性:

    1. GLSL 300 es 支持 → in/out 替代 attribute/varying
    2. 支持多重采样渲染(MSAA)。
    3. 支持浮点纹理。
    4. 支持 VAO (Vertex Array Object)

带纹理的小球弹跳动画(WebGL 版

js 复制代码
<canvas id="glCanvas"></canvas>
<script>
const canvas = document.getElementById("glCanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// 获取 WebGL 上下文
const gl = canvas.getContext("webgl");
if (!gl) {
  alert("WebGL 不支持,请换浏览器~");
}

// ========================
// 1. 定义着色器(GLSL)
// ========================

// 顶点着色器:负责计算顶点位置,并传递纹理坐标
const vsSource = `
  attribute vec2 a_position;   // 顶点坐标
  attribute vec2 a_texCoord;   // 纹理坐标
  varying vec2 v_texCoord;     // 传递给片元着色器

  uniform vec2 u_translation;  // 平移(小球位置)
  uniform float u_scale;       // 缩放(小球半径)

  void main() {
    gl_Position = vec4((a_position * u_scale) + u_translation, 0.0, 1.0);
    v_texCoord = a_texCoord;
  }
`;

// 片元着色器:负责渲染像素(这里使用纹理)
const fsSource = `
  precision mediump float;
  varying vec2 v_texCoord;
  uniform sampler2D u_sampler;  // 纹理采样器

  void main() {
    gl_FragColor = texture2D(u_sampler, v_texCoord);
  }
`;

// ========================
// 2. 创建着色器程序
// ========================
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(gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
  return shader;
}

function createProgram(gl, vsSource, fsSource) {
  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 null;
  }
  return program;
}

const program = createProgram(gl, vsSource, fsSource);
gl.useProgram(program);

// ========================
// 3. 定义小球(用圆形近似)
// ========================
const numSegments = 100;  // 圆的细分数(越大越圆)
const positions = [];
const texCoords = [];

// 圆心
positions.push(0, 0);
texCoords.push(0.5, 0.5);

// 圆边
for (let i = 0; i <= numSegments; i++) {
  const angle = i * Math.PI * 2 / numSegments;
  const x = Math.cos(angle);
  const y = Math.sin(angle);

  positions.push(x, y);
  texCoords.push(x * 0.5 + 0.5, y * 0.5 + 0.5);
}

// 创建缓冲区并传数据
function createBuffer(data, attribName, size) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);

  const attribLoc = gl.getAttribLocation(program, attribName);
  gl.enableVertexAttribArray(attribLoc);
  gl.vertexAttribPointer(attribLoc, size, gl.FLOAT, false, 0, 0);
}

createBuffer(positions, "a_position", 2);
createBuffer(texCoords, "a_texCoord", 2);

// ========================
// 4. 加载纹理
// ========================
function loadTexture(url) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // 先用 1x1 像素填充,避免图像未加载时报错
  gl.texImage2D(
    gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0,
    gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])
  );

  const image = new Image();
  image.onload = () => {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA,
                  gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.generateMipmap(gl.TEXTURE_2D);
  };
  image.src = url;
  return texture;
}

const texture = loadTexture("https://picsum.photos/200"); // 随机纹理

// ========================
// 5. 小球物理参数
// ========================
let ball = {
  x: 0, y: 0,          // 小球位置(范围 -1 ~ 1)
  dx: 0.01, dy: 0.02,  // 速度
  radius: 0.2,         // 半径(控制缩放)
  gravity: -0.0008,    // 重力
  friction: 0.8        // 碰撞摩擦
};

// 获取 uniform 位置
const u_translation = gl.getUniformLocation(program, "u_translation");
const u_scale = gl.getUniformLocation(program, "u_scale");
const u_sampler = gl.getUniformLocation(program, "u_sampler");

// ========================
// 6. 动画循环
// ========================
function animate() {
  gl.clearColor(0, 0, 0, 1);  // 背景黑色
  gl.clear(gl.COLOR_BUFFER_BIT);

  // 更新小球位置
  ball.dy += ball.gravity; // 受重力影响
  ball.x += ball.dx;
  ball.y += ball.dy;

  // 边界检测(-1~1 范围)
  if (ball.y - ball.radius < -1) {
    ball.y = -1 + ball.radius;
    ball.dy = -ball.dy * ball.friction;
  }
  if (ball.y + ball.radius > 1) {
    ball.y = 1 - ball.radius;
    ball.dy = -ball.dy * ball.friction;
  }
  if (ball.x - ball.radius < -1 || ball.x + ball.radius > 1) {
    ball.dx = -ball.dx * ball.friction;
  }

  // 传 uniform(平移 + 缩放)
  gl.uniform2f(u_translation, ball.x, ball.y);
  gl.uniform1f(u_scale, ball.radius);
  gl.uniform1i(u_sampler, 0);

  // 绑定纹理并绘制
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, texture);

  gl.drawArrays(gl.TRIANGLE_FAN, 0, positions.length / 2);

  requestAnimationFrame(animate);
}

animate();
</script>
相关推荐
代码猎人几秒前
forEach和map方法有哪些区别
前端
恋猫de小郭1 分钟前
Google DeepMind :RAG 已死,无限上下文是伪命题?RLM 如何用“代码思维”终结 AI 的记忆焦虑
前端·flutter·ai编程
m0_4711996310 分钟前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
编程大师哥11 分钟前
Java web
java·开发语言·前端
A小码哥12 分钟前
Vibe Coding 提示词优化的四个实战策略
前端
Murrays12 分钟前
【React】01 初识 React
前端·javascript·react.js
大喜xi16 分钟前
ReactNative 使用百分比宽度时,aspectRatio 在某些情况下无法正确推断出高度,导致图片高度为 0,从而无法显示
前端
helloCat16 分钟前
你的前端代码应该怎么写
前端·javascript·架构
电商API_1800790524717 分钟前
大麦网API实战指南:关键字搜索与详情数据获取全解析
java·大数据·前端·人工智能·spring·网络爬虫
康一夏18 分钟前
CSS盒模型(Box Model) 原理
前端·css