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>
相关推荐
晴空雨8 分钟前
React Media 深度解析:从使用到 window.matchMedia API 详解
前端·react.js
一个有故事的男同学8 分钟前
React性能优化全景图:从问题发现到解决方案
前端
探码科技10 分钟前
2025年20+超实用技术文档工具清单推荐
前端
Juchecar13 分钟前
Vue 3 推荐选择组合式 API 风格(附录与选项式的代码对比)
前端·vue.js
uncleTom66616 分钟前
# 从零实现一个Vue 3通用建议选择器组件:设计思路与最佳实践
前端·vue.js
影i16 分钟前
iOS WebView 异步跳转解决方案
前端
Nicholas6817 分钟前
flutter滚动视图之ScrollController源码解析(三)
前端
爪洼守门员17 分钟前
安装electron报错的解决方法
前端·javascript·electron
web前端进阶者24 分钟前
electron-vite_19配置环境变量
前端·javascript·electron
棒棒的唐27 分钟前
nodejs安装后 使用npm 只能在cmd 里使用 ,但是不能在poowershell使用,只能用npm.cmd
前端·npm·node.js