3.前置知识学习
此文章用于详细拆分讲解内容。
1.JavaScript 进阶
1.1 ES6+(箭头函数、解构、模块import/export
、Promise/async/await
)
箭头函数:
- 语法简洁:
(参数) => 表达式
或(参数) => { 语句 }
- 没有自己的
this
:继承自外层作用域的this
(解决了传统函数中this
指向混乱的问题) - 不能用作构造函数(不能用
new
调用) - 没有
arguments
对象(可使用剩余参数...args
替代)
const add = (a, b) => a + b;const greet = (name) => {console.log(`Hello, ${name}!`); };
解构:
允许从数组或对象中提取值,并赋值给变量
数组:
const [a, b, c] = [1, 2, 3]; console.log(a); // 1 console.log(b); // 2// 跳过元素 const [x, , y] = [10, 20, 30]; console.log(x); // 10 console.log(y); // 30// 剩余参数 const [first, ...rest] = [1, 2, 3, 4]; console.log(rest); // [2, 3, 4]// 默认值 const [p = 0, q = 0] = [5]; console.log(q); // 0
对象:
const user = { name: "Alice", age: 30, city: "Beijing" };// 基本用法 const { name, age } = user; console.log(name); // "Alice"// 重命名变量 const { name: userName, age: userAge } = user; console.log(userName); // "Alice"// 默认值 const { gender = "female" } = user; console.log(gender); // "female"// 嵌套解构 const obj = { a: 1, b: { c: 2 } }; const { b: { c } } = obj; console.log(c); // 2
模块(import/export)
将代码分割为独立的文件(模块),通过 import
和 export
共享功能。
导出(export)
// module.js // 命名导出 export const PI = 3.14; export function add(a, b) { return a + b; }// 默认导出 export default class Person {constructor(name) {this.name = name;} }
导入(import)
// main.js // 导入命名导出(需用大括号) import { PI, add } from './module.js'; console.log(PI); // 3.14 console.log(add(2, 3)); // 5// 导入默认导出(无需大括号,可自定义名称) import MyPerson from './module.js'; const person = new MyPerson("Bob"); console.log(person.name); // "Bob"// 重命名导入 import { add as sum } from './module.js'; console.log(sum(2, 3)); // 5// 导入所有(命名空间导入) import * as MyModule from './module.js'; console.log(MyModule.PI); // 3.14
Promise 与 async/await
用于处理异步操作,解决了传统回调函数嵌套("回调地狱")的问题。
Promise 是一个对象,表示异步操作的最终完成或失败,有三种状态:
pending
(进行中)fulfilled
(已成功)rejected
(已失败)async/await
是 Promise 的语法糖,让异步代码看起来像同步代码
async
关键字修饰函数,使其返回一个 Promiseawait
关键字只能在async
函数中使用,用于等待 Promise 完成// 定义异步函数 async function handleData() {try {// 等待Promise完成const data = await fetchData; console.log(data); // "数据获取成功"const processedData = data + ",已处理";console.log(processedData);return processedData;} catch (error) {console.error(error); // 捕获失败} finally {console.log("操作完成");} }// 调用异步函数(返回Promise) handleData().then(result => {console.log("最终结果:", result); });
1.2 异步编程(事件循环、回调地狱解决)
异步编程是 JavaScript 处理非阻塞操作的核心机制,尤其适用于网络请求、文件读写等耗时操作.
事件循环(Event Loop):JavaScript 异步的底层原理
JavaScript 是单线程语言(同一时间只能只能执行一个任务),但浏览器和 Node.js 通过事件循环实现了非阻塞异步操作。
执行流程
- 同步代码直接进入调用栈执行,执行完后出栈。
- 遇到异步操作(如
setTimeout
),交给 Web API 处理,继续执行后续同步代码。 - 异步操作完成后,回调函数被放入任务队列(宏任务或微任务队列)。
- 当调用栈为空时,事件循环优先处理所有微任务,再处理一个宏任务,重复此过程。
任务类型
- 宏任务(Macro Task):
setTimeout
、setInterval
、DOM事件
、fetch
(网络请求完成后回调)、script
整体代码等。 - 微任务(Micro Task):
Promise.then/catch/finally
、async/await
(本质是 Promise 语法糖)、queueMicrotask
等。
console.log("1"); // 同步代码,直接执行setTimeout(() => {console.log("2"); // 宏任务,放入宏任务队列 }, 0);Promise.resolve().then(() => {console.log("3"); // 微任务,放入微任务队列 });console.log("4"); // 同步代码,直接执行// 输出顺序:1 → 4 → 3 → 2
回调地狱如何解决可以看看第一条Promise 链式调用和async/await
1.3 DOM/BOM 操作(canvas 元素控制、窗口 resize 监听)
DOM 用于操作网页内容,BOM 用于控制浏览器窗口
3.1 DOM 操作核心:canvas 元素控制
<canvas>
是 HTML5 新增的绘图元素,通过 JavaScript 可在其上绘制图形、动画、图像等,广泛用于游戏、数据可视化等场景。
学习参考中文网站:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API
基础使用步骤
<canvas id="myCanvas" width="400" height="300"></canvas>
// 1. 获取 canvas 元素 const canvas = document.getElementById('myCanvas');// 2. 获取绘图上下文(2D 绘图核心) const ctx = canvas.getContext('2d');// 3. 绘制基本图形 // 绘制矩形(填充) ctx.fillStyle = 'blue'; // 填充颜色 ctx.fillRect(50, 50, 100, 80); // x, y, 宽, 高// 绘制矩形(描边) ctx.strokeStyle = 'red'; // 描边颜色 ctx.lineWidth = 3; // 线条宽度 ctx.strokeRect(200, 50, 100, 80);// 绘制圆形 ctx.beginPath(); // 开始路径 ctx.arc(150, 200, 40, 0, Math.PI * 2); // 圆心(x,y), 半径, 起始角, 结束角 ctx.fillStyle = 'green'; ctx.fill(); ctx.stroke();
高级应用:动画与图像处理
- 动画原理:通过
requestAnimationFrame
循环重绘let x = 0; function animate() {// 清空画布ctx.clearRect(0, 0, canvas.width, canvas.height);// 绘制移动的圆ctx.beginPath();ctx.arc(x, 150, 30, 0, Math.PI * 2);ctx.fillStyle = 'purple';ctx.fill();x += 2;if (x > canvas.width) x = 0;requestAnimationFrame(animate); // 下一帧继续 } animate();
- 图像绘制:加载并绘制图片
const img = new Image(); img.src = 'image.jpg'; img.onload = () => {// 绘制完整图像ctx.drawImage(img, 0, 0);// 裁剪并绘制部分图像ctx.drawImage(img, 50, 50, 100, 100, 200, 200, 100, 100);// 参数:图像, 源图x, 源图y, 源图宽, 源图高, 目标x, 目标y, 目标宽, 目标高 };
扩展:canvas 实用场景
- 数据可视化(绘制图表)
- 网页截图(配合
html2canvas
库)- 像素级图像处理(滤镜效果)
- 小游戏开发(如贪吃蛇、五子棋)
3.2 BOM 操作核心:窗口 resize 监听
窗口大小变化(resize)是常见的浏览器事件,通过监听该事件可实现响应式布局调整、画布自适应等功能。
基础监听方法
// 监听窗口大小变化 window.addEventListener('resize', handleResize);// 处理函数 function handleResize() {// 获取当前窗口尺寸const width = window.innerWidth;const height = window.innerHeight;console.log(`窗口尺寸:${width}x${height}`);// 示例:让 canvas 自适应窗口大小canvas.width = width * 0.8; // 画布宽度为窗口的80%canvas.height = height * 0.6; }// 初始加载时执行一次 handleResize();
优化:防抖处理
窗口 resize 事件会频繁触发(如拖拽窗口边缘时),可能导致性能问题,需用防抖(debounce) 优化。
function debounce(func, delay = 100) {let timer;return (...args) => {clearTimeout(timer);timer = setTimeout(() => func.apply(this, args), delay);}; }// 使用防抖后的处理函数 window.addEventListener('resize', debounce(handleResize));
扩展:相关 BOM 事件与属性
- 窗口滚动(scroll):监听页面滚动位置
window.addEventListener('scroll', () => {const scrollTop = window.pageYOffset; // 滚动距离顶部的像素console.log('滚动位置:', scrollTop); });
- 窗口加载(load):页面资源全部加载完成后触发
window.addEventListener('load', () => {console.log('页面所有资源加载完成'); });
- 视口尺寸:除
innerWidth/innerHeight
外
document.documentElement.clientWidth
:可视区域宽度(不含滚动条)screen.width
:屏幕分辨率宽度
如何进行Canvas 性能优化?
优化方向 | 具体手段 | 核心原理 | 适用场景 | 注意事项 |
---|---|---|---|---|
减少重绘频率 | 1. 使用 requestAnimationFrame 代替 setInterval/setTimeout 2. 避免无意义的 clearRect + draw 循环3. 批量执行绘制操作(如合并多帧绘制为一帧) | 1. 与浏览器刷新频率(通常 60Hz,约 16.6ms / 帧)同步,避免丢帧 2. 减少不必要的画布清空和重复绘制,降低 GPU/CPU 负载 3. 减少绘制函数调用次数,降低函数执行开销 | 1. 所有需要动态刷新的 Canvas 场景(如动画、实时图表) 2. 绘制内容变化频率低的场景(如静态图表仅数据更新) 3. 多元素同时更新的场景(如粒子效果、多个图形同步移动) | 1. requestAnimationFrame 在页面隐藏时会暂停,需处理页面切换逻辑2. 批量绘制需控制单次绘制量,避免单帧耗时超过 16.6ms 3. 不要为了减少频率忽略用户交互的实时性(如鼠标跟随) |
优化绘制内容 | 1. 避免绘制不可见元素(如超出 Canvas 边界的内容) 2. 用 drawImage 绘制图片代替手动用图形 API 画复杂图案3. 简化路径复杂度(如减少曲线控制点、合并重复路径) | 1. 减少 GPU 无效渲染计算(不可见内容仍会消耗绘制资源) 2. 图片绘制由浏览器底层优化(比 Canvas API 手动绘制更高效) 3. 降低路径解析和渲染的计算量,减少 CPU 耗时 | 1. 元素频繁移动且可能超出画布的场景(如游戏角色、滚动图表) 2. 复杂图形 / 图标绘制(如自定义图标、装饰性图案) 3. 矢量图形绘制(如 SVG 转 Canvas、复杂折线图) | 1. 判定元素可见性时,用简单边界检测(如矩形碰撞)代替精确检测 2. 图片绘制需提前加载,避免绘制时阻塞 3. 路径简化需平衡性能与视觉精度,避免图形失真 |
合理使用画布分层 | 1. 将静态内容(如背景、固定图标)放在独立 Canvas 层 2. 动态内容(如动画、交互元素)单独分层 3. 按更新频率拆分多层 Canvas(高频层在上,低频层在下) | 1. 静态层仅绘制一次,避免随动态内容重复重绘 2. 动态层重绘时仅影响自身,不干扰其他层 3. 减少单次重绘的画布面积,降低渲染开销 | 1. 混合静态与动态内容的场景(如带背景的实时仪表盘、游戏场景) 2. 多元素更新频率差异大的场景(如静态表格 + 动态数据标签) 3. 复杂交互场景(如鼠标 hover 高亮、拖拽元素) | 1. 分层 Canvas 需通过 CSS 绝对定位叠加,确保坐标对齐 2. 避免过度分层(超过 3-5 层可能增加浏览器合成开销) 3. 低频层可设置 will-change: transform 提示浏览器优化 |
减少状态切换与 API 调用 | 1. 批量设置样式(如统一设置 fillStyle 后绘制所有同色元素)2. 复用路径(如用 save() /restore() 保存状态,避免重复设置)3. 避免频繁切换 globalAlpha /globalCompositeOperation | 1. 减少 Canvas 状态切换的内部开销(样式切换需重新初始化渲染参数) 2. 减少重复的状态配置代码,降低函数调用次数 3. 复杂混合模式会增加 GPU 计算量,频繁切换耗时更高 | 1. 多元素同样式的场景(如批量绘制同色矩形、同字体文本) 2. 需频繁切换状态的场景(如部分元素半透明、部分不透明) 3. 复杂图形组合场景(如多层叠加、裁剪绘制) | 1. 批量绘制前先排序元素(按样式 / 状态分组),减少跨组切换 2. save() /restore() 需成对使用,避免状态污染3. 非必要时避免使用 globalCompositeOperation ,优先用分层替代 |
硬件加速与资源控制 | 1. 避免 Canvas 尺寸过大(建议单画布像素不超过 4096*4096) 2. 用 transform 代替 offsetLeft/offsetTop 移动画布3. 避免频繁读取 Canvas 像素数据(如 getImageData /toDataURL ) | 1. 过大画布会超出 GPU 纹理缓存限制,触发 CPU 渲染(性能骤降) 2. transform 由 GPU 加速,而样式修改会触发重排重绘3. 像素数据读取需从 GPU 显存拷贝到 CPU 内存,属于高开销操作 | 1. 大尺寸 Canvas 场景(如全景图绘制、高分辨率图表) 2. 画布整体移动的场景(如滚动视图、地图平移) 3. 需像素级操作的场景(如图片滤镜、颜色检测) | 1. 大画布可拆分为多个小画布拼接(如地图瓦片) 2. 使用 transform 时,避免同时修改 width/height (会重置变换矩阵)3. 像素读取需批量执行,避免每帧多次调用,可缓存结果复用 |
2.数学基础
2.1 线性代数(向量运算:加减 / 点积 / 叉积;矩阵变换:平移 / 旋转 / 缩放;坐标系转换)
2.1.1向量运算:从 “箭头” 开始理解
向量(Vector)是线性代数的 “基本单位”,可以理解为 “有方向和长度的箭头”(比如物理中的 “力”“速度”)。在坐标系中,向量用一组数字表示(如 2D 向量
(x, y)
、3D 向量(x, y, z)
),这组数字对应箭头在各坐标轴上的 “投影长度”。
2.1.1.1 向量的表示:先搞懂 “位置” 与 “方向”
- 向量 vs 点:
点(Point):表示位置(在坐标系中 “在哪里”);向量(Vector):表示方向和长度(“从哪里到哪里” 的变化),例:点
P(2,3)
是坐标系中一个固定位置;向量v=(2,3)
可以理解为 “从原点出发,向右走 2,向上走 3” 的箭头,也可以平移到任意位置(比如从点(1,1)
出发,指向(3,4)
)。
- 向量的写法:
通常用加粗字母(如
v
)或带箭头的字母(如$\vec{v}$
)表示,2D 向量记为v = (v₁, v₂)
,3D 向量记为v = (v₁, v₂, v₃)
,其中v₁, v₂, v₃
是向量在 x、y、z 轴上的 “分量”
2.1.1.2 向量加减:“箭头拼接” 或 “分量对应算”
向量加减的核心是 “分量对应相加 / 减”,也可以用 “箭头拼接” 的直观方式理解(适合 2D/3D 场景)
- 向量加法:v + w
计算规则:两个向量的 “对应分量相加”
v = (x₁, y₁)
,w = (x₂, y₂)
,则v + w = (x₁+x₂, y₁+y₂)
3D 向量同理:
v = (x₁,y₁,z₁)
,w = (x₂,y₂,z₂)
,则v + w = (x₁+x₂, y₁+y₂, z₁+z₂)
直观理解(三角形法则):把向量
w
的起点平移到向量v
的终点,从v
的起点指向w
的终点的箭头,就是v + w
。例:你先向右走 2、向上走 3(向量
v=(2,3)
),再向右走 1、向上走 2(向量w=(1,2)
),最终位置变化就是(2+1, 3+2)=(3,5)
,对应向量v+w=(3,5)
。
向量减法:v - w
计算规则:两个向量的 “对应分量相减”,本质是
v + (-w)
(-w
是w
的 “反方向向量”,分量全取负)。
v=(2,3)
,w=(1,2)
,则v - w = (2-1, 3-2)=(1,1)
;-w=(-1,-2)
,v + (-w)=(2-1,3-2)=(1,1)
,结果一致。直观理解:
v - w
表示 “从w
的终点指向v
的终点” 的向量,也可以理解为 “两个向量起点重合时,从w
指向v
的箭头”。例:若
v
是 “从原点到 (2,3)”,w
是 “从原点到 (1,2)”,则v - w
是 “从 (1,2) 到 (2,3)” 的向量,分量为(1,1)
。
2.1.1.3 向量点积:判断 “方向相似度”
点积是向量的 “重要乘法”,结果不是向量,而是一个数字(标量),核心作用是:判断两个向量的 “方向是否相近”、计算向量长度、求夹角。
计算规则:分量相乘再求和
- 2D 向量:若
v=(x₁,y₁)
,w=(x₂,y₂)
,则点积v·w = x₁x₂ + y₁y₂
;- 3D 向量:若
v=(x₁,y₁,z₁)
,w=(x₂,y₂,z₂)
,则点积v·w = x₁x₂ + y₁y₂ + z₁z₂
- 例:
v=(2,3)
,w=(1,2)
,则v·w = 2×1 + 3×2 = 2 + 6 = 8
;核心意义:方向与长度的关联
- 点积的公式还可以表示为:
v·w = |v| × |w| × cosθ
v|
是向量v
的 “长度”(模长),计算方式是√(x₁² + y₁²)
(2D)或√(x₁² + y₁² + z₁²)
(3D);θ
是v
和w
之间的夹角(起点重合时)- 结论:
判断方向是否相近:
- 若
v·w > 0
:cosθ > 0
,θ < 90°
,两个向量 “大致同向”(比如v=(1,0)
,w=(1,1)
,点积 = 1>0,夹角 45°);- 若
v·w = 0
:cosθ = 0
,θ = 90°
,两个向量 “垂直”(比如 x 轴和 y 轴方向向量,点积 = 0);- 若
v·w < 0
:cosθ < 0
,θ > 90°
,两个向量 “大致反向”(比如v=(1,0)
,w=(-1,1)
,点积 =-1<0,夹角 135°)。计算向量长度:当
w = v
时,v·v = |v|² × cos0° = |v|²
,因此|v| = √(v·v)
(这就是模长的计算公式)。计算夹角:已知两个向量的点积和模长,可反求夹角
θ = arccos( (v·w) / (|v|×|w|) )
(常用于图形学中 “光照计算”,判断光线与物体表面的夹角)。
2.1.1.4 向量叉积:判断 “左右方向” 或 “法向量”
叉积仅在3D 向量中定义(2D 叉积可理解为 “3D 叉积的 z 分量”),结果是一个向量,核心作用是:判断两个向量的 “左右位置关系”、生成 “垂直于平面的法向量”(用于 3D 图形的面朝向判断)。
- 计算规则:行列式展开(记结论更简单)
对于 3D 向量
v=(x₁,y₁,z₁)
和w=(x₂,y₂,z₂)
,叉积v×w
的结果是一个新向量,计算公式为:
v×w = ( y₁z₂ - y₂z₁ , z₁x₂ - z₂x₁ , x₁y₂ - x₂y₁ )
- 例:
v=(1,0,0)
(x 轴正方向),w=(0,1,0)
(y 轴正方向),则v×w = (0×0 - 1×0, 0×0 - 0×1, 1×1 - 0×0) = (0, 0, 1)
(z 轴正方向);
若w=(0,1,0)
,v=(1,0,0)
,则w×v = (1×0 - 0×0, 0×1 - 0×0, 0×1 - 1×0) = (0, 0, -1)
(z 轴负方向)
核心意义:方向与面积
叉积的向量方向遵循 “右手定则”,且模长等于 “以两个向量为邻边的平行四边形面积”:
方向判断(右手定则):
- 伸出右手,让四指从
v
的方向弯曲到w
的方向(弯曲角度小于 180°),此时大拇指指向的方向就是v×w
的方向。- 例:x 轴向量 ×y 轴向量,四指从 x 弯向 y,大拇指指向 z 轴正方向;y 轴向量 ×x 轴向量,四指从 y 弯向 x,大拇指指向 z 轴负方向(因此叉积不满足 “交换律”,
v×w = -w×v
)。2D 场景的 “左右判断”:
2D 向量可看作 “z 分量为 0 的 3D 向量”,此时叉积的结果只有 z 分量(x、y 分量为 0),可通过 z 分量的正负判断两个向量的左右关系:
- 无序列表若
v×w
的 z 分量 > 0:w
在v
的 “左侧”;- 若
v×w
的 z 分量 < 0:w
在v
的 “右侧”;- 若 z 分量 = 0:
v
和w
“共线”(方向相同或相反)。- 例:
v=(1,0)
(向右),w=(0,1)
(向上),叉积 z 分量 = 1×1 - 0×0=1>0,因此w
在v
左侧;w=(0,-1)
(向下),叉积 z 分量 = 1×(-1) - 0×0=-1<0,因此w
在v
右侧。模长与面积:
|v×w| = |v| × |w| × sinθ
(θ
是v
和w
的夹角),这个值恰好等于 “以v
和w
为邻边的平行四边形的面积”(3D 图形中 “三角形面积” 是其一半)。
2.1.2 矩阵变换:用 “表格” 操作向量
矩阵(Matrix)是一个 “数字表格”,比如 2×2 矩阵(2 行 2 列)、3×3 矩阵、4×4 矩阵。在 linear algebra 中,矩阵的核心作用是 “变换向量”—— 把一个向量输入矩阵,输出一个 “被拉伸、旋转、平移后的新向量”(常用于图形学中 “物体变形”“视角移动”)。
矩阵与向量的乘法:核心运算
要让矩阵变换向量,必须先做 “矩阵 × 向量” 的运算(注意:向量要写成 “列向量” 形式,即 1 列多行,比如 2D 向量写成
$\begin{pmatrix}x \\ y\end{pmatrix}$
)。(1)2×2 矩阵 × 2D 向量(无平移,仅旋转 / 缩放)
假设矩阵
M = $\begin{pmatrix} a & b \\ c & d \end{pmatrix}$
,向量v = $\begin{pmatrix} x \\ y \end{pmatrix}$
,则乘法结果Mv
是一个新向量:
Mv = $\begin{pmatrix} a×x + b×y \\ c×x + d×y \end{pmatrix}$
计算规则:矩阵的 “行” 与向量的 “列” 对应相乘再求和,结果作为新向量的 “行”。
例:
M = $\begin{pmatrix} 2 & 0 \\ 0 & 3 \end{pmatrix}$
,v = $\begin{pmatrix} 1 \\ 2 \end{pmatrix}$
,则Mv = $\begin{pmatrix} 2×1 + 0×2 \\ 0×1 + 3×2 \end{pmatrix}$ = $\begin{pmatrix} 2 \\ 6 \end{pmatrix}$
(向量在 x 轴拉伸 2 倍,y 轴拉伸 3 倍)。(2)4×4 矩阵 × 3D 向量(含平移,完整变换)
2×2/3×3 矩阵无法实现 “平移”(因为向量乘法是 “线性运算”,
M(0) = 0
,无法把原点平移到其他点),因此在 3D 图形学中,通常用4×4 矩阵(齐次坐标)来包含 “平移、旋转、缩放” 三种变换,此时 3D 向量要写成 “4 维齐次向量”(x, y, z, 1)
(最后一位固定为 1,代表 “点”;若为 0,则代表 “方向向量”,平移对其无效)。4×4 矩阵的标准变换形式为:
M = $\begin{pmatrix} a & b & c & t_x \\ d & e & f & t_y \\ g & h & i & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}$
其中:
左上角 3×3 矩阵
$\begin{pmatrix} a & b & c \\ d & e & f \\ g & h & i \end{pmatrix}$
:负责旋转和缩放;第 4 列前 3 个元素
(t_x, t_y, t_z)
:负责平移(把点沿 x 轴移t_x
,y 轴移t_y
,z 轴移t_z
);最后一行
(0,0,0,1)
:保持齐次坐标的 “点 / 方向” 属性(固定不变)。4×4 矩阵 × 4D 齐次向量的计算规则:
若向量v = (x, y, z, 1)
,则Mv = (a x + b y + c z + t_x, d x + e y + f z + t_y, g x + h y + i z + t_z, 1)
。
例:平移矩阵
M = $\begin{pmatrix} 1 & 0 & 0 & 5 \\ 0 & 1 & 0 & 3 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$
(仅平移,无旋转缩放),向量v=(2,4,1,1)
,则Mv = (1×2+0×4+0×1+5, 0×2+1×4+0×1+3, 0×2+0×4+1×1+0, 1) = (7,7,1,1)
(点从 (2,4,1) 平移到 (7,7,1))。
三种核心矩阵变换:平移、旋转、缩放
(1)缩放变换(Scale):拉伸或压缩向量
作用:让向量在 x/y/z 轴上按比例放大或缩小(比如把物体 “变胖”“变高”)。
4×4 缩放矩阵形式(
s_x
=x 轴缩放比例,s_y
=y 轴,s_z
=z 轴):
M_scale = $\begin{pmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$
例:
s_x=2
(x 轴拉伸 2 倍),s_y=0.5
(y 轴压缩一半),s_z=1
(z 轴不变),则矩阵为$\begin{pmatrix} 2 & 0 & 0 & 0 \\ 0 & 0.5 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$
;
输入向量(1, 4, 3, 1)
,输出为(2×1, 0.5×4, 1×3, 1) = (2, 2, 3, 1)
(x 从 1→2,y 从 4→2,z 不变)。(2)旋转变换(Rotate):绕坐标轴转动向量
作用:让向量绕 x/y/z 轴旋转指定角度
θ
(右手定则:四指沿旋转方向,大拇指指向坐标轴正方向)。常见旋转矩阵(4×4 形式,仅列出关键部分,其他元素同标准变换矩阵):
绕 x 轴旋转
θ
:y/z 轴分量变化,x 轴不变
M_rot_x = $\begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & cosθ & -sinθ & 0 \\ 0 & sinθ & cosθ & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$
绕 y 轴旋转
θ
:x/z 轴分量变化,y 轴不变
M_rot_y = $\begin{pmatrix} cosθ & 0 & sinθ & 0 \\ 0 & 1 & 0 & 0 \\ -sinθ & 0 & cosθ & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$
绕 z 轴旋转
θ
:x/y 轴分量变化,z 轴不变
M_rot_z = $\begin{pmatrix} cosθ & -sinθ & 0 & 0 \\ sinθ & cosθ & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$
例:绕 z 轴旋转 90°(
θ=90°
,cos90°=0
,sin90°=1
),矩阵为$\begin{pmatrix} 0 & -1 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$
;
输入向量(1, 0, 0, 1)
(x 轴正方向),输出为(0×1 + (-1)×0, 1×1 + 0×0, 0, 1) = (0, 1, 0, 1)
(旋转后指向 y 轴正方向,符合 “绕 z 轴转 90°” 的直观效果)。(3)平移变换(Translate):移动向量位置
作用:把向量(点)沿 x/y/z 轴移动指定距离(比如把物体从左边移到右边)。
4×4 平移矩阵形式(
t_x
=x 轴平移距离,t_y
=y 轴,t_z
=z 轴):
M_trans = $\begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}$
例:
t_x=3
(向右移 3),t_y=-2
(向下移 2),t_z=1
(向前移 1),矩阵为$\begin{pmatrix} 1 & 0 & 0 & 3 \\ 0 & 1 & 0 & -2 \\ 0 & 0 & 1 & 1 \\ 0 & 0 & 0 & 1 \end{pmatrix}$
;
输入向量(2, 5, 0, 1)
,输出为(2+3, 5-2, 0+1, 1) = (5, 3, 1, 1)
(点从 (2,5,0) 移到 (5,3,1))。
变换的组合:矩阵乘法
如果要对一个向量做 “先缩放→再旋转→最后平移”,不需要单独计算三次,而是把三个变换矩阵相乘,得到一个 “组合矩阵”,再用组合矩阵 × 向量即可(一次计算完成所有变换)。
(1)矩阵乘法规则(以 4×4 矩阵为例)
假设矩阵
A
和B
都是 4×4 矩阵,组合矩阵C = A × B
(注意顺序:先应用B
,再应用A
,因为向量是 “右乘” 矩阵,Cv = A(Bv)
),则C
的每个元素C[i][j]
等于A
的第i
行与B
的第j
列对应元素相乘再求和。(2)变换顺序的重要性
矩阵乘法不满足交换律,即
A×B ≠ B×A
,因此变换顺序会直接影响结果:
例:“先平移再旋转” vs “先旋转再平移”:
先平移(
M_trans
)再旋转(M_rot
):组合矩阵M_rot × M_trans
;先旋转(
M_rot
)再平移(M_trans
):组合矩阵M_trans × M_rot
。这两个组合矩阵完全不同,比如:先把点 (1,0,0) 平移到 (1,0,0),再绕 z 轴转 90°,结果是 (0,1,0);而先绕 z 轴转 90°(点 (1,0,0)→(0,1,0)),再平移到 (1,0,0),结果是 (1,1,0),显然不同。
2.1.3 坐标系转换:从 “你的视角” 到 “我的视角”
在 3D 场景中,我们会遇到多个坐标系(比如 “物体自己的坐标系”“世界坐标系”“相机视角坐标系”),坐标系转换的核心是 “找到两个坐标系之间的变换矩阵”,把向量从一个坐标系转换到另一个坐标系。
先搞懂三个常用坐标系
-
1. 局部坐标系(Local Space):物体 “自己的坐标系”,原点通常在物体中心(比如一个立方体的中心),x 轴向右、y 轴向上、z 轴向前(方便描述物体自身的顶点位置,比如 “立方体的右上角顶点在 (1,1,1)”)。
-
2. 世界坐标系(World Space):整个场景的 “公共坐标系”,原点在场景的某个固定位置(比如场景中心),所有物体的位置都相对于这个坐标系(比如 “立方体在世界坐标系中的位置是 (5,0,3)”)。
-
3. 相机坐标系(View Space):相机 “视角的坐标系”,原点在相机位置,z 轴指向相机 “看的方向”,y 轴指向相机 “上方”(方便计算 “物体在相机视野中的位置”,比如 “物体在相机前方 5 米处”)。
坐标系转换的核心:“原点偏移”+“轴方向对齐”
任何两个坐标系之间的转换,都可以拆成两步:
-
平移:把原坐标系的原点,移动到目标坐标系的原点(抵消原点偏移);
-
旋转 / 缩放:把原坐标系的 x/y/z 轴,旋转 / 缩放到与目标坐标系的 x/y/z 轴对齐(统一轴方向和单位)。
(1)局部坐标系 → 世界坐标系(模型变换)
目的:把物体局部坐标系中的顶点,转换到世界坐标系中(确定物体在场景中的位置和姿态)。
转换过程:局部顶点先经过 “缩放→旋转”(调整物体大小和姿态),再经过 “平移”(把物体放到世界坐标系的指定位置),对应组合矩阵
M_model = M_trans × M_rot × M_scale
(顺序:先缩放,再旋转,最后平移)。例:一个立方体的局部顶点
v_local=(1,1,1,1)
(右上角顶点),若:
缩放:
s_x=s_y=s_z=2
(放大 2 倍),M_scale
;旋转:绕 y 轴转 90°,
M_rot
;平移:
t_x=5, t_y=0, t_z=3
(移到世界坐标系 (5,0,3) 处),M_trans
;
则世界坐标系中的顶点v_world = M_model × v_local = M_trans × M_rot × M_scale × v_local
。(2)世界坐标系 → 相机坐标系(视图变换)
目的:把世界坐标系中的物体,转换到相机坐标系中(确定物体相对于相机的位置,比如 “物体在相机左边还是右边”)。
转换逻辑:相机坐标系可以看作 “世界坐标系中一个特殊的物体”—— 因此,把世界坐标转换到相机坐标,等价于 “把相机坐标系移动到世界坐标系的原点,并让相机的轴与世界轴对齐”,对应矩阵
M_view
(视图矩阵)。
M_view
的计算步骤:
平移:把世界坐标系的原点,平移到相机的位置(即 “减去相机的世界坐标”,矩阵
M_trans_cam
,t_x=-cam_x, t_y=-cam_y, t_z=-cam_z
);旋转:把相机的 x/y/z 轴,旋转到与世界坐标系的 x/y/z 轴对齐(比如相机朝 z 轴负方向看,需要旋转 180°,矩阵
M_rot_cam
);组合矩阵:
M_view = M_rot_cam × M_trans_cam
(先平移,再旋转)。
总结:
-
先玩工具,再记公式:用可视化工具(如GeoGebra)手动拖动向量、矩阵,观察变换效果。
-
如果是为了学图形学 / 游戏开发,优先掌握 “4×4 矩阵变换” 和 “坐标系转换”
-
如果是为了机器学习,优先掌握 “向量点积”(用于计算相似度、线性回归)
-
零基础阶段不用深究 “线性空间”“秩” 等抽象概念,先会用 “向量运算”“矩阵变换” 解决具体问题(比如用点积判断光照强度,用矩阵旋转一个正方形),后续再回头补理论。
2.2 三角函数(正弦 / 余弦:控制旋转、角度计算)
三角函数是数学中描述 “角” 与 “边” 关系的核心工具,尤其正弦(sin)和余弦(cos),在图形旋转、角度计算等场景中应用极广.
2.2.1 先搞懂两个核心前提:直角三角形与单位圆
直角三角形
-
直角:90° 的角(用 “⊥” 标记),是三角形中最大的角;
-
锐角:除直角外的两个角(都小于 90°),我们研究的 “角” 就是指其中一个锐角(记为 θ,读作 “西塔”);
-
三边:根据与锐角 θ 的位置关系,分为 3 类(见下图):
-
对边:只与 θ 相对,不包含 θ 的那条边(记为 a);
-
邻边:包含 θ 且与直角相邻的边(记为 b);
-
斜边:最长的边,始终与直角相对(记为 c,斜边长度>对边 / 邻边)。
-
正弦(sin)和余弦(cos)的定义(直角三角形版)
正弦和余弦本质是 “直角三角形中,某条边与斜边的比值”,只和 “角的大小” 有关,和三角形的 “实际大小” 无关(比如同样 30° 角,小三角形和大三角形的 sin30° 比值完全相同)。
函数 | 定义(与锐角 θ 的关系) | 公式 | 记忆口诀 |
---|---|---|---|
正弦 sin | 对边长度 / 斜边长度 | sinθ = 对边 / 斜边 = a/c | 对正(对边对应正弦) |
余弦 cos | 邻边长度 / 斜边长度 | cosθ = 邻边 / 斜边 = b/c | 余邻(邻边对应余弦) |
单位圆:突破直角三角形,走向旋转应用
什么是单位圆?
圆心在坐标原点 (0,0),半径 = 1 的圆(“单位” 就是指半径为 1);
圆上任意一点的坐标记为 (x,y),根据圆的方程,满足x² + y² = 1(因为半径 = 1)。
正弦 / 余弦的定义(单位圆版):核心!核心!核心!
将 “角 θ” 与单位圆结合:让角的顶点在原点,角的始边(起始边)与 x 轴正方向重合,角的终边(结束边)与单位圆交于点 P (x,y)。
此时,正弦和余弦的定义直接与点 P 的坐标挂钩:
sinθ = 点 P 的纵坐标 = y
cosθ = 点 P 的横坐标 = x
这个定义彻底打破了 “直角三角形” 的限制,能描述任意角度(0°~360°、负角、大于 360° 的角),也是 “三角函数控制旋转” 的理论基础。
不同象限角的 sinθ、cosθ 正负(快速判断)
象限 | 角的范围(0°~360°) | 点 P 坐标 (x,y) | sinθ(y) | cosθ(x) |
---|---|---|---|---|
第一象限 | 0°~90° | 正,正 | 正 | 正 |
第二象限 | 90°~180° | 负,正 | 正 | 负 |
第三象限 | 180°~270° | 负,负 | 负 | 负 |
第四象限 | 270°~360° | 正,负 | 负 | 正 |
记忆口诀:“一全正,二正弦,三正切,四余弦”(只看正的情况:第一象限全正,第二象限只有正弦正,第四象限只有余弦正).
必背的特殊角度值:不用计算,直接用
角度 θ | 0° | 30° | 45° | 60° | 90° | 180° | 270° | 360° |
---|---|---|---|---|---|---|---|---|
sinθ | 0 | 1/2 | √2/2≈0.707 | √3/2≈0.866 | 1 | 0 | -1 | 0 |
cosθ | 1 | √3/2≈0.866 | √2/2≈0.707 | 1/2 | 0 | -1 | 0 | 1 |
规律总结:
sinθ:0°→90° 递增(0→1),90°→180° 递减(1→0),180°→270° 递减(0→-1),270°→360° 递增(-1→0);
cosθ:0°→90° 递减(1→0),90°→180° 递减(0→-1),180°→270° 递增(-1→0),270°→360° 递增(0→1)。
用正弦 / 余弦控制旋转(图形学基础)
这是正弦 / 余弦最经典的应用之一(比如游戏角色旋转、机器人关节转动、图像旋转),核心原理是 “通过角度计算旋转后的坐标”。
旋转的基本场景:点绕原点旋转
假设平面上有一个点 A (x₀,y₀),现在要让它绕坐标原点 (0,0) 顺时针旋转 θ 角,得到新点 A'(x',y'),如何计算 x' 和 y'?
答案就是用余弦和正弦推导的 “旋转公式”(这个公式不用推导,直接记应用即可):
新横坐标:x' = x₀×cosθ + y₀×sinθ
新纵坐标:y' = -x₀×sinθ + y₀×cosθ
如果是逆时针旋转,公式只需调整符号:
x' = x₀×cosθ - y₀×sinθ
y' = x₀×sinθ + y₀×cosθ
图形旋转的本质:
一个图形由无数个点组成,只要对图形上每一个点都用旋转公式计算出新坐标,再把这些新点连接起来,就能得到旋转后的图形 —— 这就是电脑、手机里 “图形旋转功能” 的底层数学逻辑,而正弦和余弦就是其中的 “核心工具”。
2.3 坐标系(2D→3D 的右手坐标系、屏幕坐标系)
坐标系是描述 “位置” 的数学工具,就像生活中用 “街道 + 门牌号” 定位住址一样,在数学、图形学(游戏、动画)、计算机视觉里,所有点的位置都要靠坐标系定义。下面从 0 基础出发,先讲简单的 2D 坐标系,再过渡到 3D 右手坐标系,最后讲和我们屏幕直接相关的屏幕坐标系,每个概念都配 “生活类比” 和 “画图指引”,保证能懂。
2.3.1 基础:2D 坐标系(平面坐标系)
2D 坐标系是 “在平面上定位点” 的工具,比如在纸上画点、手机屏幕上点图标,本质都是用 2D 坐标描述位置。最常用的是笛卡尔 2D 坐标系(也叫直角坐标系)
3 个核心要素
-
原点(Origin):坐标的 “起点”,记为 (0, 0)。可以理解为 “小区大门”—— 所有位置都从这里开始算。
-
坐标轴(Axes):2 条互相垂直的直线,用来确定 “方向”:
-
水平方向的轴叫 x 轴(x-axis),通常向右为 “正方向”(可以类比 “东方向”);
-
垂直方向的轴叫 y 轴(y-axis),通常向上为 “正方向”(可以类比 “北方向”);
-
-
单位长度(Unit Length):衡量 “距离” 的标准,比如 1 单位 = 1 厘米(纸上)或 1 像素(屏幕上)。
怎么用 2D 坐标表示一个点?
每个点的位置用 (x, y) 两个数字表示,规则很简单:
-
x 值:表示这个点到 “y 轴” 的水平距离(平行于 x 轴)——x 正,在 y 轴右边;x 负,在 y 轴左边;
-
y 值:表示这个点到 “x 轴” 的垂直距离(平行于 y 轴)——y 正,在 x 轴上方;y 负,在 x 轴下方。
2.3.2 进阶到 3D:右手坐标系(3D 的 “标准语言”)
2D 是 “平面”,3D 是 “空间”(比如房间、游戏里的 3D 场景)—— 要描述空间中一个点的位置,需要在 2D 的 x、y 轴基础上,加一条z 轴,形成 3 个互相垂直的轴,这就是 3D 坐标系。
而 “右手坐标系” 是 3D 领域的通用规则(游戏、CAD、3D 建模都用它),核心是 “确定 z 轴的正方向”—— 用右手就能快速记住,不用死记硬背。
右手坐标系的 “手型判断法”(关键!)
拿出你的右手,按照下面 3 步做,就能直观理解 x、y、z 轴的方向关系:
-
让右手手掌摊平,拇指指向右方(这是 x 轴的正方向,和 2D 的 x 轴一致);
-
让食指向上伸直(和拇指垂直),这是 y 轴的正方向(也和 2D 的 y 轴一致);
-
此时,你的中指会自然向 “自己的方向” 弯曲 / 伸直(和拇指、食指都垂直),这就是 z 轴的正方向!
简单总结:右手拇指 = x 正,食指 = y 正,中指 = z 正,三个轴两两垂直 —— 这就是右手坐标系的核心定义。
3D 坐标怎么表示?——(x, y, z)
和 2D 类似,3D 空间中每个点的位置用 (x, y, z) 三个数字表示,分别对应三个轴的 “距离”:
-
x:平行于 x 轴,到 y-z 平面的距离(x 正 = 右,x 负 = 左);
-
y:平行于 y 轴,到 x-z 平面的距离(y 正 = 上,y 负 = 下);
-
z:平行于 z 轴,到 x-y 平面的距离(z 正 = 靠近自己,z 负 = 远离自己)。
为什么必须是 “右手” 而不是 “左手”?
因为如果用左手(左手拇指 = x 正,食指 = y 正,中指 = z 正),z 轴的方向会和右手系相反(左手的中指会指向 “远离自己” 的方向)—— 如果大家不统一用 “右手”,同样一个 3D 模型,你看是 “向前”,别人看就会是 “向后”,会乱套。
比如游戏里的角色 “向前走”,在右手系里是 z 轴正方向;如果用左手系,角色就会 “向后退”—— 所以行业必须统一用右手坐标系。
2.3.3 和屏幕相关:屏幕坐标系(2D,但方向特殊)
前面讲的 2D 坐标系是 “数学标准”,但屏幕坐标系是 “设备标准” —— 它是专门用来描述 “屏幕上像素位置” 的坐标系,和我们平时用手机、电脑直接相关,有个关键特点:y 轴方向和数学 2D 坐标系相反。
屏幕坐标系的核心特点(和数学 2D 的区别)
对比项 | 数学 2D 坐标系(笛卡尔) | 屏幕坐标系(设备) |
---|---|---|
原点 (0,0) | 通常在平面中心 | 固定在屏幕左上角 |
x 轴正方向 | 向右 | 向右(和数学一致) |
y 轴正方向 | 向上 | 向下(和数学相反!) |
单位长度 | 厘米、米等 | 像素(Pixel) |
为什么屏幕坐标系的 y 轴向下?
这是历史原因:早期计算机屏幕(如 CRT 显示器)的 “扫描方式” 是从 “左上角” 开始,先向右扫一行(x 轴),扫完一行后 “向下移一行”(y 轴),再扫下一行 —— 为了和硬件扫描逻辑一致,屏幕坐标系就把 y 轴正方向设为 “向下”,一直沿用至今。
生活中的屏幕坐标系例子
-
手机屏幕(分辨率 1080×2340):
-
左上角是 (0, 0);
-
右上角是 (1079, 0)(因为 x 轴最大是 “分辨率宽度 - 1”,像素从 0 开始计数);
-
左下角是 (0, 2339)(y 轴最大是 “分辨率高度 - 1”);
-
如果你点击屏幕 “中间位置”,坐标大概是 (540, 1170)。
-
-
电脑游戏:比如《我的世界》里,你点击屏幕上的 “方块”,游戏就是通过 “屏幕坐标系” 获取你点击的 (x, y) 像素位置,再换算成 3D 场景中的方块位置。
2.3.4 总结:3 个坐标系的核心区别
坐标系类型 | 维度 | 原点位置 | 轴方向(正方向) | 单位 | 核心用途 |
---|---|---|---|---|---|
数学 2D 坐标系 | 2D | 通常在平面中心 | x = 右,y = 上 | 厘米、米等 | 数学计算、画图(如几何题) |
3D 右手坐标系 | 3D | 可自定义(如场景中心) | x = 右,y = 上,z = 靠近自己 | 米、厘米等 | 3D 建模、游戏、CAD 设计 |
屏幕坐标系 | 2D | 屏幕左上角 | x = 右,y = 下 | 像素 | 屏幕定位(图标、点击位置) |
3.图形学基础
3.1 计算机图形学核心概念(像素、纹理、着色器、渲染管线、光照模型)
计算机图形学本质是 “把数字变成图像” 的技术,比如游戏画面、电影特效、3D 建模都依赖它。
3.1.1 像素(Pixel):图像的 “最小积木”
像素是计算机图形的最基本单位,就像乐高积木 —— 再复杂的图像(照片、游戏画面、UI 界面),放大到极致后都是由无数个 “小方块” 组成的,每个小方块就是一个像素。
核心特点:
-
有固定位置:每个像素在屏幕上有唯一的 “坐标”(比如手机屏幕的 “第 100 行、第 200 列”),对应现实中 “第几排、第几列” 的位置。
-
有固定颜色:每个像素只能显示一种颜色,颜色由 “红(R)、绿(G)、蓝(B)” 三种基础色混合而成(比如纯红色是 R=255、G=0、B=0),有些像素还会加 “透明度(A)”(比如半透明的按钮)。
-
数量决定清晰度:屏幕 / 图像的 “分辨率” 本质就是像素总数。比如 “1920×1080” 表示画面横向有 1920 个像素、纵向有 1080 个像素,总像素数约 200 万;像素越多,画面越清晰(比如 4K 屏幕比 1080P 屏幕清晰,就是因为像素更多)。
3.1.2 纹理(Texture):给 3D 模型 “贴皮肤”
你可以把纹理理解为3D 模型的 “贴纸” 或 “皮肤”。比如游戏里的 “木质桌子”,3D 模型本身只是一个 “光秃秃的长方体”,要让它看起来像木头,就需要把一张 “木纹图片” 贴在长方体表面 —— 这张 “木纹图片” 就是纹理。
核心作用:
-
让 3D 模型更真实:没有纹理的模型是 “纯色块”(比如纯灰色的桌子),贴了纹理后能呈现细节(木纹、划痕、图案)。
-
降低模型复杂度:如果不用纹理,要做出木纹的凹凸感,需要给模型加无数个小面(增加计算量);用纹理只需一张图,既省资源又逼真。
常见类型:
-
2D 纹理:最常用的 “图片贴纸”,比如墙面的壁纸、人物的衣服图案。
-
法线纹理(Normal Map):特殊的 “凹凸贴纸”—— 看起来像模型有凸起(比如砖墙的凹陷),但实际模型还是平的,靠纹理欺骗眼睛,常用于游戏(既真实又不耗性能)。
3.1.3 着色器(Shader):图像的 “化妆师”
着色器是一段控制 “如何显示像素 / 模型” 的小程序,相当于给图像 / 3D 模型做 “化妆” 的工具:比如决定某个像素该显示什么颜色、模型表面该亮还是暗、要不要加光影效果。
核心分类(2 个最关键的着色器)
类型 | 作用 | 类比 |
---|---|---|
顶点着色器(Vertex Shader) | 处理 3D 模型的 “顶点”(比如长方体的 8 个角点):计算每个顶点在屏幕上的位置(比如模型移动后,顶点坐标怎么变)、传递顶点的基础信息(比如顶点的颜色、纹理坐标)。 | 给 “骨架” 定位置 —— 比如拍电影时,先确定演员的关节(顶点)在哪里,再调整姿势。 |
片段着色器(Fragment Shader) | 处理 “片段”(可以理解为 “像素的前身”):根据顶点着色器传递的信息,计算每个片段的最终颜色(比如结合纹理、光照,决定这个像素是木纹色还是阴影色)。 | 给 “皮肤” 上颜色 —— 比如根据演员的服装(纹理)、灯光(光照),决定脸上每个部位该涂什么色。 |
为什么重要?
所有你看到的 “酷炫效果”(比如游戏里的光影、电影里的爆炸特效、手机拍照的滤镜),本质都是着色器在工作。比如 “模糊滤镜” 就是片段着色器修改了每个像素的颜色,让相邻像素颜色更接近;“金属反光” 就是片段着色器计算了光线在金属表面的反射效果。
3.1.4 渲染管线(Rendering Pipeline):图像的 “生产流水线”
渲染管线是将 “3D 模型、纹理、光照” 等原始数据,一步步变成屏幕上 2D 图像的流程,就像工厂里 “从原材料到成品” 的流水线 —— 每个步骤只做一件事,依次传递处理。
核心步骤(简化版,从 3D 到 2D 的 7 个关键环节):
-
顶点输入:把 3D 模型的 “顶点数据”(位置、颜色、纹理坐标)输入管线(相当于把 “积木零件” 送到流水线起点)。
-
顶点着色器:处理每个顶点,计算其在屏幕上的 2D 位置(比如把 3D 的 “长方体角点” 转换成屏幕上的 “像素坐标”)。
-
图元装配:把处理后的顶点 “连成面”(比如把长方体的 8 个顶点,拼成 6 个矩形面),这些 “面” 叫 “图元”(三角形、矩形等)。
-
光栅化:把 “图元”(比如一个矩形面)拆分成无数个 “片段”(即对应屏幕上的像素位置,相当于把 “大积木” 拆成 “小像素碎片”)。
-
片段着色器:计算每个片段的最终颜色(结合纹理、光照,比如给这个片段涂成木纹色,或因为在阴影里涂成深色)。
-
逐片段操作:做 “最终检查”—— 比如判断这个片段是否在屏幕内(不在就丢弃)、是否被其他物体遮挡(被遮挡就不显示)、是否需要半透明混合。
-
像素输出:把最终确定的颜色 “画” 到屏幕对应的像素上,所有像素拼起来就是最终的 2D 图像。
3.1.5 光照模型(Lighting Model):让 3D 世界 “有亮有暗”
光照模型是模拟 “光线如何与物体交互” 的规则,目的是让 3D 模型看起来有 “立体感”—— 没有光照的话,所有物体都是 “纯色块”,分不清正面和背面;有了光照,就能看到明暗差异(比如阳光照到的面亮,背面暗)。
3 个最基础的光照模型(从简单到真实):
-
环境光(Ambient Light):“全局普照的光”,比如房间里的日光灯 —— 所有物体都会被均匀照亮,没有明暗差异,只能让物体 “不发黑”,无法体现立体感(相当于给所有物体涂一层 “基础色”)。
-
漫反射光(Diffuse Light):“光线照射到粗糙表面的反射”,比如阳光照在墙上 —— 光线会向各个方向散开,物体 “朝向光线的面亮,背对光线的面暗”,能体现基本的立体感(比如长方体的正面亮、背面暗)。
-
关键特点:只和 “物体表面与光线的夹角” 有关 —— 夹角越小(面越正对着光线),越亮。
-
-
镜面反射光(Specular Light):“光线照射到光滑表面的反射”,比如阳光照在镜子、金属上 —— 会形成 “亮斑”(高光),能体现物体的 “光滑度”(比如金属杯子的高光比木头桌子明显)。
-
关键特点:和 “观察角度” 有关 —— 只有当你的眼睛在 “光线反射方向” 上时,才能看到高光。
-
3.1.6 核心关联图
3D模型(顶点组成) → 贴纹理(穿皮肤) → 进入渲染管线(流水线)↓ ↓光照模型(算明暗) 顶点着色器(定位置)→ 片段着色器(算颜色)↓ ↓最终像素(有颜色、有明暗) → 屏幕显示2D图像
3.2 基础渲染流程(顶点→图元→光栅化→片元→屏幕像素)
渲染流程是将 “虚拟图形” 转化为 “屏幕可见像素” 的核心过程,就像 “把设计图做成实物” 的流水线。
渲染的本质
渲染的目标是把 3D/2D 空间中的 “几何形状”,转化为屏幕上一个个发光的 “像素点”。
比如你在游戏里看到的 “一个立方体”,最终会变成屏幕上几百个红色、蓝色的小方块(像素)—— 这个 “立方体→像素” 的转化过程,就是下面要讲的 5 步渲染流程。
3.2.1 顶点输入(Vertex Input):定义 “形状的骨架”
收集 “顶点数据”,这些顶点是构成图形的最基础 “锚点”,就像搭建房子前先确定 “墙角、房梁端点” 的位置。
-
顶点:只有 “位置信息” 的点(比如 3D 空间中的 X/Y/Z 坐标,2D 空间中的 X/Y 坐标),没有大小和颜色。
-
一个图形至少需要 3 个顶点(比如三角形,这是计算机最擅长处理的基础图形);复杂图形(比如立方体)由多个三角形拼接而成,需要更多顶点(立方体有 8 个顶点)。
技术细节
-
数据形式:顶点数据通常是 “数组”(比如
[x1,y1,z1, x2,y2,z2, x3,y3,z3]
),存储在计算机内存中。 -
核心作用:告诉计算机 “图形的轮廓在哪里”,是后续所有步骤的 “数据基础”。
实例
-
画一个 2D 三角形:需要输入 3 个顶点的坐标,比如
(100,200)
、(300,200)
、(200,100)
。 -
画一个 3D 立方体:需要输入 8 个顶点的 3D 坐标(比如
(0,0,0)
、(0,1,0)
、(1,1,0)
...),每个顶点代表立方体的一个角。
3.2.2 图元装配(Primitive Assembly):把 “骨架” 拼成 “基础形状”
将第一步输入的 “零散顶点”,按照规则 “连接成完整的基础图形”(计算机里叫 “图元”),最常用的图元是三角形(偶尔也用点、线段)。
-
为什么用三角形?因为三角形是 “最稳定的多边形”—— 无论怎么旋转、缩放,它的三个顶点始终能确定一个唯一的平面,不会出现 “变形” 或 “歧义”,计算机处理起来最高效。
类比理解
拼乐高时,你把刚才确定的 “连接点”(顶点)用 “乐高板块”(图元)连接起来,比如用 3 个连接点拼出一个三角形板块,再用多个三角形板块拼出立方体的一个面。
技术细节
-
装配规则:计算机按 “顶点输入顺序” 分组(比如每 3 个顶点组成一个三角形),或按 “索引” 指定分组(比如用
[0,1,2]
表示 “用第 0、1、2 个顶点拼三角形”)。 -
核心作用:把 “点” 变成 “可识别的形状”,为后续 “填充颜色” 做准备。
实例
-
2D 三角形:3 个顶点
(100,200)
、(300,200)
、(200,100)
被装配成一个完整的三角形图元。 -
3D 立方体:8 个顶点先被分成 6 个面(每个面是 2 个三角形),共 12 个三角形图元,最终拼成立方体的轮廓。
3.2.3 光栅化(Rasterization):把 “基础形状” 拆成 “像素格子”
将第二步拼好的 “图元(比如三角形)”,映射到 “屏幕的像素网格” 上,确定 “这个图元会覆盖屏幕上哪些像素”—— 就像给形状 “打格子”,标出需要上色的像素位置。
-
注意:这一步只确定 “哪些像素要处理”,还不给像素上色,相当于 “确定快递要送到哪些小区”,但还没装货
类比理解
你要在一张方格纸上画三角形:先确定三角形的轮廓,然后用铅笔圈出 “三角形内部包含的所有小方格”—— 这些小方格就是 “要处理的像素”。
技术细节
-
核心计算:计算机通过 “扫描线算法”(从图元顶部扫到底部),逐行判断每个像素是否在图元内部,最终输出一个 “像素列表”(包含所有需要上色的像素坐标)。
-
抗锯齿(可选):如果图元边缘的像素 “半在内部、半在外部”,会标记为 “半透明像素”,避免边缘出现 “锯齿状”(比如游戏里的 “抗锯齿” 功能就是干这个的)。
实例
-
2D 三角形:假设三角形覆盖屏幕上的
(101,201)
、(102,201)
、(101,202)
等 100 个像素,光栅化后就输出这 100 个像素的坐标。 -
3D 立方体:每个三角形面都会被光栅化,最终输出立方体在屏幕上 “投影后” 覆盖的所有像素坐标(比如总共 1000 个像素)。
3.2.4 片元着色(Fragment Shading):给 “像素格子” 上色
对第三步光栅化输出的 “每个像素”(这里叫 “片元”,因为可能包含透明、深度等额外信息),计算它的 “最终颜色”—— 这是渲染中 “最能决定视觉效果” 的一步,比如纹理、光影、颜色都在这里处理。
类比理解
你给刚才圈出的 “方格纸小方格” 上色:有的方格涂红色(比如立方体的正面),有的涂深红色(比如立方体的阴影部分),有的涂蓝色(比如反射的天空)—— 每个方格的颜色都是单独计算的。
技术细节
-
输入信息:每个片元会携带 “自身坐标”“所在图元的顶点颜色 / 纹理坐标”“光照信息” 等数据。
-
核心计算:通过 “片元着色器”(一段小程序)执行计算,比如:
-
颜色插值:如果三角形的三个顶点分别是红、绿、蓝,中间的像素颜色会 “渐变过渡”(红→黄→绿→青→蓝)。
-
纹理映射:把图片(比如 “木头纹理”)贴到图元上,计算每个像素对应的纹理位置,取图片上的颜色作为像素颜色。
-
光照计算:根据光源位置,判断像素是否在阴影里,调整颜色亮度(比如阴影部分颜色变暗)。
-
-
输出结果:每个片元最终会得到一个 “RGBA 颜色值”(比如
(255,0,0,255)
表示不透明的红色)。
实例
-
2D 三角形:假设顶点颜色是红、绿、蓝,片元着色后,三角形中心的像素颜色会是 “白色”(红 + 绿 + 蓝混合),边缘像素是红色、绿色或蓝色。
-
3D 立方体:给立方体贴 “木头纹理”,片元着色时会根据每个像素在立方体上的位置,取木头纹理图片中对应的颜色,同时计算 “阳光照射角度”,让朝向阳光的面更亮,背对阳光的面更暗。
3.2.5 逐片元操作(Per-Fragment Operations):确定 “最终显示哪些像素”
对第四步输出的 “带颜色的片元”,做最后一轮筛选和处理,最终决定 “哪些片元能显示在屏幕上”,并写入 “帧缓冲区”(屏幕的 “临时画布”)。
类比理解
你把上色后的方格纸 “整理成最终画面”:
-
如果两个方格重叠(比如立方体的正面挡住背面),只保留 “前面的方格”(背面的方格不显示)。
-
如果有的方格是半透明的(比如玻璃),就把它的颜色和 “下面的方格颜色混合”(比如玻璃的蓝色 + 下面的红色 = 紫色)。
技术细节
-
核心操作(按顺序执行):
-
深度测试:判断片元是否被 “更靠前的物体” 挡住(比如立方体正面的片元会挡住背面的片元),被挡住的片元直接丢弃,不显示。
-
模板测试(可选):按 “模板缓冲区” 的规则筛选片元(比如只在屏幕的某个区域显示图形,像游戏里的 “小地图”)。
-
混合:如果片元是透明的(RGBA 中的 A 值 <255),就把它的颜色和 “帧缓冲区中已有的颜色” 混合(比如半透明的玻璃会透出后面的物体)。
-
写入帧缓冲区:通过所有测试的片元,最终会把颜色写入 “帧缓冲区”—— 当一帧所有像素都处理完后,显示器会读取帧缓冲区的内容,显示到屏幕上。
-
实例
-
3D 立方体:正面的红色片元通过深度测试,写入帧缓冲区;背面的蓝色片元被正面挡住,深度测试失败,直接丢弃;如果立方体有一个半透明的玻璃面,玻璃的蓝色片元会和后面的红色片元混合,变成紫色后写入帧缓冲区。
-
游戏画面:玩家看到的 “角色 + 场景 + UI”,都是通过这一步判断 “谁在前、谁在后、谁透明”,最终合成一帧完整画面。