☰
从 mag() 到 normalize():深入理解向量大小与归一化(The Nature of Code 第 1.4 节)
2026/10/14 15:34:10 网站建设 项目流程
  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

本文基于仓库归档文档 _more/archive/nature-of-code/1-Vectors/1.4-Vector-Math-II.md(Coding Train《The Nature of Code》第 1 章第 4 个视频配套笔记)展开。该视频延续向量数学的讨论,重点讲解两个在物理模拟中最基础也最常用的 PVector 操作:计算向量的大小(magnitude,mag())与归一化向量(normalize,normalize())。读完本文,你将理解"向量长度"与"方向"这对核心概念的区别,掌握mag()、normalize()、setMag()、limit()的用法差异,并看到它们如何在仓库的加速度、摩擦力、阻力、转向行为等真实示例中驱动物体运动。

为什么需要"大小"与"方向"两个维度

在前几节中我们已经知道,向量是一个同时携带**大小(magnitude)和方向(direction)**的量。在 Processing / p5.js 中,PVector(或p5.Vector)就是用来承载这两个属性的数据结构,例如v = (3, 4)表示向右 3 个单位、向下 4 个单位(在屏幕坐标系中 y 轴向下),其大小为 5,方向指向右下。

  • 视频系列起点可参考归档笔记 1.1-Vectors.md 与 1.2-PVector-class.md;
  • 向量加法与减法的操作可参考 1.3-Vector-Math.md。

但在做运动模拟时,我们经常面临两个相反的需求:

  1. 我想知道这个向量有多长(例如物体当前速度的大小);
  2. 我想把这个向量只当作方向用,长度无所谓(例如"朝鼠标方向移动",但速度要统一为固定值)。

第 1 个需求由mag()解决,第 2 个需求由normalize()解决。这正是本视频的核心。

用 mag() 计算向量的大小

一个二维向量v = (x, y)的大小(模长)定义为:

mag = sqrt(x * x + y * y)

也就是勾股定理:x与y是直角三角形的两条直角边,向量本身是斜边。

在 p5.js 中,调用实例方法v.mag()即可获得该向量的大小(返回浮点数):

let v = createVector(3, 4); console.log(v.mag()); // 输出 5

PVector类在 Processing 中同样提供mag()方法。仓库中多个示例都依赖这一操作来读取"当前运动状态的强度":

  • 在 5.5-wander/main/vehicle.js 的arrive()行为中,先通过p5.Vector.sub(target, this.pos)得到指向目标的向量,再用force.mag()取得"距目标还有多远"(距离即向量大小),当距离小于减速半径slowRadius时,用map()把距离映射为期望速度,实现"靠近时减速、到达时停止"的缓停效果;
  • 在 2.4_drag/P5/mover.js 的阻力模拟中,使用this.vel.magSq()获取速度大小的平方,用来计算流体阻力的强度(drag.setMag(c * speedSq))。magSq()是mag()的平方版本,省去一次开平方运算,在性能敏感场景中更高效,且在比较大小(如magSq < 半径²)时结果等价。

用 normalize() 得到单位向量

归一化(normalize)就是把一个向量的大小变成 1,但保持方向不变。归一化后的向量被称为单位向量(unit vector)。

数学上,归一化就是让每个分量除以向量的大小:

unit.x = v.x / mag(v) unit.y = v.y / mag(v)

归一化后的向量长度恒为 1,此时它纯粹表达方向,不再携带"多快/多远"的信息。这正是运动模拟中最常见的需求:把"方向"和"速度大小"解耦,随后再单独乘以想要的速度值。

p5.js 中直接调用v.normalize()即可就地归一化。仓库里 learning/nature-of-code/1.5_unit_vector/P5/sketch.js 给出了一个完整的可运行示例,绘制一条"始终从画布中心指向鼠标、长度固定为 50"的线段:

function setup() { createCanvas(400, 400); } function draw() { background(0); let pos = createVector(200, 200); let mouse = createVector(mouseX, mouseY); let v = p5.Vector.sub(mouse, pos); // 指向鼠标的向量 v.normalize().mult(50); // 归一化后放大到长度 50 translate(width / 2, height / 2); strokeWeight(4); stroke(255); line(0, 0, v.x, v.y); }

这段代码的妙处在于:无论鼠标在屏幕哪个位置,v.normalize()都先把向量压成长度为 1 的纯方向,随后.mult(50)把它拉长到固定长度 50。因此线段永远"指向鼠标且长度恒定",不会因为鼠标距离画布中心忽远忽近而忽长忽短。

手动归一化:mag() 与 div() 的组合

normalize()只是封装了"除以自身大小"这一操作。想理解它的本质,可以在不使用normalize()的情况下手写等价代码。示例源码中保留了被注释的等价写法(见 1.5_unit_vector/P5/sketch.js):

// 方式一:手动归一化 let m = v.mag(); v.div(m); // 方式二:链式归一化 + 缩放 v.normalize().mult(50); // 方式三:一步到位 v.setMag(50);

三种写法完全等价:v.div(m)把x、y各除以大小,效果与normalize()相同。理解这层关系后,遇到"向量为零向量(0, 0)"的情况就要特别小心——此时mag()为 0,除以 0 会产生NaN(Infinity/NaN分量),导致后续所有基于该向量的计算失效。实际工程中应避免对零向量做归一化,或在调用前先判断if (v.mag() > 0)。

setMag():方向与大小的"一步合成"

归一化最典型的用途,是"先定方向、再定速度"。由于这个组合太常见,p5.js 提供了setMag(len)方法:在保持方向不变的前提下,直接把向量长度设置为len,等价于normalize().mult(len)的组合调用。

仓库源码中有大量setMag()的实战用法:

  • 在 1.6_acceleration_vector/P5/mover.js 中,Mover 先通过p5.Vector.sub(mouse, this.pos)获得指向鼠标的向量,再this.acc.setMag(1)把加速度大小固定为 1,让物体以恒定加速度冲向鼠标;
  • 在 5.2-seek/seek/vehicle.js 的转向(seek)行为中,force.setMag(this.maxSpeed)先把期望速度向量设置为最大速度,再减去当前速度、用force.limit(this.maxForce)限制每帧可施加的转向力,这是 Craig Reynolds 经典 Steering Behavior 的核心一行;
  • 在 5.5-wander/main/vehicle.js 的漫游行为中,wanderPoint.setMag(100)沿当前速度方向前推 100 像素,构造出"前方虚拟圆"的圆心,从而产生随机而平滑的转向。

归一化在力模拟中的典型应用:摩擦与阻力

归一化的价值在物理力模拟中体现得最充分。摩擦力与阻力的方向都与运动方向相反,但它们的"方向"和"大小"需要分开计算。

摩擦力(见 2.3_friction/P5/mover.js):

// 摩擦力的方向:与速度方向相反 let friction = this.vel.copy(); friction.normalize(); // 只保留方向 friction.mult(-1); // 反转为反向 // 摩擦力的大小:mu * normal let normal = this.mass; friction.setMag(mu * normal); // 重新设置大小 this.applyForce(friction);

这里先normalize()提取速度方向并取反,再用setMag(mu * normal)重新赋予符合物理公式的大小——方向与大小的计算完全解耦。注释中还提供了链式简写this.vel.copy().normalize().mult(-1)。

阻力(见 2.4_drag/P5/mover.js):

let drag = this.vel.copy(); drag.normalize(); // 方向:与速度相反 drag.mult(-1); let speedSq = this.vel.magSq(); // 大小:与速度平方成正比 drag.setMag(c * speedSq); this.applyForce(drag);

注意这里用magSq()而非mag()读取速度强度——因为阻力公式F = c * v²需要的正是速度大小的平方,直接用平方值省去开方再平方的往返损耗。

零向量与"除以零"的边界处理

综合mag()与normalize()的使用,一个值得铭记的边界情况是零向量。当v = (0, 0)时:

  • v.mag()返回 0,这在数学上是合法的;
  • 但v.normalize()内部执行v.x / 0、v.y / 0,会产生NaN;
  • 后续v.mult(50)、v.add(...)都会把NaN传播到位置、速度上,导致物体"瞬移消失"或渲染异常。

因此凡是可能对零向量(例如两物体完全重合、速度恰好为 0)调用归一化或setMag()的地方,建议先做保护:

if (v.mag() > 0) { v.normalize(); }

在 p5.js 实现中,setMag()、limit()等同样基于mag(),对零向量的处理同样需要留意。

小结与延伸阅读

本节的核心可以浓缩为三条:

  1. mag()返回向量的长度(勾股定理),用来读取"运动有多强/距离有多远";追求性能时可使用平方版本magSq();
  2. normalize()把向量长度压缩为 1、保留方向,得到纯方向向量(单位向量),是"方向与速度解耦"的关键操作;
  3. setMag(len)是normalize().mult(len)的便捷等价,在"给定方向、设定大小"的物理模拟中高频出现,仓库的加速度、摩擦、阻力、seek、arrive、wander 示例均直接使用它。

本视频的归档笔记位于 _more/archive/nature-of-code/1-Vectors/1.4-Vector-Math-II.md,其配套章节目录见 _more/archive/nature-of-code/1-Vectors/index.md。紧接着的下一节 1.5-Acceleration.md 将把本节的方向向量(归一化、setMag)应用于加速度,实现"物体持续加速"的经典 Mover 示例——完整可运行代码就在仓库的 learning/nature-of-code/1.6_acceleration_vector/P5/mover.js 中。动手把上面的三种归一化写法逐一替换运行,观察线段长度是否保持不变,你会对"大小与方向分离"获得最直观的体感。

  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询