本文所有知识点均来源于《图形渲染实战 2D架构设计与实现》这本书,作者关于向量数学知识点的讲解是我目前看到的最全面、最容易理解的。这里只是抛砖引玉,感兴趣请阅读书籍

1. 概念

向量的定义:向量是具有方向(Direction)和大小(Length / Magnitude)的空间变量。

向量的大小:已知一个向量a = [ x , y ],那么该向量的大小可以使用|| a ||来表示,其值为一个标量(Scalar),可以由Math.sqrt(x^2 y^2)这个公式计算得出。

2.向量加减法2.1加法

canvas的坐标怎么算(Canvas之向量数学)(1)

在图6.2中,向量a(实线且大小为200)与向量b(实线且大小为282.84)相加的几何解释就是:平移向量,使向量b的尾部(向量b的圆点部分)连接向量a的头部(向量a箭头部分),接着从向量a的尾部向向量b的头部画一个向量(大小为446.21),该向量就是向量a b形成的新的向量,这就是向量加法的三角形法则。而在图6.2所示的向量加法几何特性图示中,使用的是向量加法的平行四边形法则绘制的。

向量加法很有用。举个例子,在力学中,如果有两个力(向量)同时施加在某个物体上,那么该物体受到的合力就是这两个力(向量)相加。

2.2减法

canvas的坐标怎么算(Canvas之向量数学)(2)

向量减法的几何含义,如图6.3所示,可以固定任意一个向量,平移另外一个向量,让两个向量的尾部重合,此时如果是:会发现,向量的减法是具有方向性的,并不满足交换律。

2.3向量与标量乘法

向量与标量不能相加,但是它们能相乘。当一个标量和一个向量相乘时,将得到一个新的向量,该向量与原向量平行,但长度不同或方向相反。如图6.5所示,画布中心左侧的向量的方向都是相同的(平行),向量的大小则以每20个单位递增。而画布中心右侧的向量,方向相反(仍旧平行),其大小也是以每20个单位递增。

canvas的坐标怎么算(Canvas之向量数学)(3)

向量与标量相乘的本质是缩放向量,因此实现的静态方法名为scale。现在大家应该知道标量的英文为什么叫Scalar了,因为标量(Scalar)用来缩放(Scale)向量(Vector)

2.4向量的点乘

向量能与标量相乘,向量也能和向量相乘。两个向量相乘被称为点乘(也常称为点积或内积)

canvas的坐标怎么算(Canvas之向量数学)(4)

两个向量a和b,夹角为θ,根据余弦定律:

|| a - b ||²= || a ||² || b ||²-2 || a || || b || cosθ。

将上述表达式的左侧|| a - b ||²展开,写成|| a ||² || b ||²-2 ( a·b ),则可以得到:

|| a ||² || b ||²-2 ( a·b ) = || a ||2 || b ||2-2 || a || || b || cosθ。

从而就可以导出如下公式:

a·b = || a || || b || cosθ。

根据上面的公式,可以写成如下形式:

cosθ = a·b / ( || a || || b || ) 其中 ( || a || || b || ) 的值总是正数。

由此得到如下重要的信息:

2.5向量夹角

根据这个公式:cosθ = a·b / ( || a || || b || ),那么能够很容易计算出向量a与向量b之间的夹角,伪代码如下:

// 根据公式:cosθ = a·b / ( || a || || b || ),计算向量a和向量b的夹角 // 根据isRadian参数的取值来判断是返回弧度还是返回角度 public static getAngle(a: Vec2, b: Vec2, isRadian: boolean = false): number { let dot: number = Vec2.dotProduct(a, b); let radian: number = Math.acos(dot / (a.length * b.length)); if (isRadian === false) { radian = Math2D.toDegree(radian); } return radian; }

2.6向量朝向

为了与夹角区分,使用朝向来表示物体的方向。具体代码如下:

// 计算两个向量的连起来与x轴的夹角 public static getOrientation( from: Vec2, to: Vec2, isRadian: boolean = false ): number { let diff: Vec2 = Vec2.difference(to, from); let radian = Math.atan2(diff.y, diff.x); if (isRadian === false) { radian = Math2D.toDegree(radian); } return radian; }

3.向量投影Demo

canvas的坐标怎么算(Canvas之向量数学)(5)

背景描述:当鼠标指针的位置在向量的区域范围之外(如图6.7所示),则正常显示该向量。但当鼠标指针的位置移动到向量区域范围内,则会加粗显示该向量,并且会:

(1)标记出鼠标指针位置(圆圈表示)、坐标信息,以及线段起点到鼠标指针处的向量。

(2)标记出鼠标指针位置在向量上的投影点(圆圈),坐标信息,以及从投影点到鼠标指针位置之间的向量。

(3)标记出鼠标指针位置与线段起点之间以角度表示的夹角。

(4)所有的坐标信息是相对全局坐标系原点(左上角)的偏移表示。

canvas的坐标怎么算(Canvas之向量数学)(6)

canvas的坐标怎么算(Canvas之向量数学)(7)

3.1向量投影算法

图6.7和图6.8所示的效果是经典的向量投影算法。简单地说,就是将一个点(鼠标位置表示)投影到由起点和终点所形成的向量上。该算法比较常用,伪代码如下:

/** * 判断点是否在线上 * @param pt * @param start * @param end * @param closePoint * @returns */ public static projectPointOnLineSegment( pt: vec2, start: vec2, end: vec2, closePoint: vec2 ): boolean { let v0: vec2 = vec2.create(); let v1: vec2 = vec2.create(); let d: number = 0; // 向量起点到鼠标位置的方向向量 vec2.difference(pt, start, v0); // 向量起点到向量终点的方向向量 vec2.difference(end, start, v1); // 原向量变成单位向量,并返回原向量的长度 d = v1.normalize(); // 将v0投影到v1上,获取投影长度 // v0*v1 = ||v0|| * ||v1|| * cosθ // v1是单位向量,所有 ||v1|| = 1 // 于是 v0*v1 = ||v0|| * cosθ,也就是v0在v1上的投影长度 let t: number = vec2.dotProduct(v0, v1); // 如果t < 0,说明鼠标在起始点之外,返回起始点 if (t < 0) { closePoint.x = start.x; closePoint.y = start.y; return false; } else if (t > d) { // 投影长度 > 线段长度,说明鼠标位置超过线段终点范围 closePoint.x = end.x; closePoint.y = end.y; return false; } else { // 鼠标点位于线段中间 // 使用scaleAdd计算出相对于全局坐标的坐标偏远信息 // start 起点向量 // v1 * t 投影向量 // start v1(单位向量)*t(标量) 相对于全局坐标的向量 vec2.scaleAdd(start, v1, t, closePoint); return true; } }

3.2向量的两种夹角

为了更好地了解getOrientation(向量朝向)和getAngle(向量夹角)方法之间的区别,参考如图6.10所示的效果,可以知道:

canvas的坐标怎么算(Canvas之向量数学)(8)

3.3碰撞检测算法3.3.1点与圆的碰撞检测

如果一个点在圆的半径范围之内,则说明发生了碰撞

/** * 判断坐标是否在圆内部 * @param pt 鼠标坐标 * @param center 圆中心坐标 * @param radius 半径 * @returns */ public static isPointInCircle( pt: vec2, center: vec2, radius: number ): boolean { let diff: vec2 = vec2.difference(pt, center); let len2: number = diff.squaredLength; // 避免使用Math.sqrt方法 if (len2 <= radius * radius) { return true; } return false; }

3.3.2点与线段的碰撞检测

点与线段的碰撞检测是一个比较基础和重要的算法,该算法可以由两部分组成:

/** * 判断坐标是否在线附近 * @param pt 鼠标位置 * @param start 线段起始点 * @param end 线段终止点 * @param radius 碰撞半径 * @returns */ public static isPointOnLineSegment( pt: vec2, start: vec2, end: vec2, radius: number = 2 ): boolean { let closePt: vec2 = vec2.create(); if (Math2D.projectPointOnLineSegment(pt, start, end, closePt) === false) { return false; } return Math2D.isPointInCircle(pt, closePt, radius); }

3.3.3点与矩形的碰撞检测

关于点与矩形的碰撞检测算法非常简单

/** * 判断坐标是否在矩形内部 * @param ptX 鼠标位置 * @param ptY 鼠标位置 * @param x 矩形左上角 * @param y 矩形左上角 * @param w 矩形宽度 * @param h 矩形高度 * @returns */ public static isPointInRect( ptX: number, ptY: number, x: number, y: number, w: number, h: number ): boolean { if (ptX >= x && ptX <= x w && ptY >= y && ptY <= y h) { return true; } return false; }

3.3.4点与椭圆的碰撞检测

假设椭圆的中心点定义的坐标值为[ centerX,centerY ],并且半径分别为[ radiusX , radiusY ],在这种情况下,一个点P ( pX ,pY )如果在椭圆的内部,那么要满足如下公式:

canvas的坐标怎么算(Canvas之向量数学)(9)

有了上述公式,就可以实现isPointInEllipse方法。

/** * 判断坐标是否在椭圆内部 * @param ptX * @param ptY * @param centerX * @param centerY * @param radiusX * @param radiusY * @returns */ public static isPointInEllipse( ptX: number, ptY: number, centerX: number, centerY: number, radiusX: number, radiusY: number ): boolean { let diffX = ptX - centerX; let diffY = ptY - centerY; let n: number = (diffX * diffX) / (radiusX * radiusX) (diffY * diffY) / (radiusY * radiusY); return n <= 1.0; }

3.3.5点与三角形的碰撞检测

如图6.11所示为点与三角形的关系,从图中可以知道,点P与[ v0 , v1 , v2 ]形成的三角形之间的关系有两种,要么点P在三角形内部,要么点P在三角形的外部。

canvas的坐标怎么算(Canvas之向量数学)(10)

来观察一下它们之间的区别,你会发现:

3.3.5.1向量叉乘

向量叉乘比较特别,只能使用3D向量,现在假设有两个3D向量 a=[x0,y0, z0]和b=[x1, y1, z1],那么

canvas的坐标怎么算(Canvas之向量数学)(11)

为了将2D向量vec2以3D向量的形式来表示,可以将3D向量的z分量设置为0,例如a=[x0, y0,0], b=[x1, y1,0],套用上述叉积公式,会得到:

canvas的坐标怎么算(Canvas之向量数学)(12)

可以看到,对于2D向量的叉积来说,其x和y分量总是为0,但是对于点与三角形碰撞检测算法来说,叉积后的z分量x0y1-y0x1才是最关键的,先把z分量的计算作为vec2的一个静态方法。

/** * 计算三角形两条边向量的叉乘 * @param v0 * @param v1 * @param v2 * @returns */ public static sign(v0: vec2, v1: vec2, v2: vec2): number { // v2->v0边向量 let e1: vec2 = vec2.difference(v0, v2); // v2->v1边向量 let e2: vec2 = vec2.difference(v1, v2); return vec2.crossProduct(e1, e2); } /** * 判断鼠标是否在三角形内部 * @param pt * @param v0 * @param v1 * @param v2 * @returns */ public static isPointInTriangle(pt: vec2, v0: vec2, v1: vec2, v2: vec2) { // 计算三角形的三个定点与鼠标形成的三个子三角形的边向量的叉乘 let b1: boolean = Math2D.sign(v0, v1, pt) < 0.0; let b2: boolean = Math2D.sign(v1, v2, pt) < 0.0; let b3: boolean = Math2D.sign(v2, v0, pt) < 0.0; // 三个三角形的方向一致,说明点在三角形内部 // 否则点在三角形外部 return b1 === b2 && b2 === b3; }

通过上面的代码,结合上图6.11会发现,实际上并不关心子三角形两条边向量叉积的数值大小,更关心的是叉积的正负性,只要三个子三角形的两条边向量的叉积的正负性都一致的话,表示三个子三角形的顶点排列顺序一致,那么该点P肯定在[v0 , v1 , v2 ]形成的三角形的内部,否则肯定在外部。

3.3.5点与任意凸多边形的碰撞检测

如图6.12所示,可以将任意的凸多边形很方便地分解成三角形,然后依次调用上一节实现的点与三角形碰撞检测算法,这样就能获得点与任意凸多边形碰撞检测的算法。

canvas的坐标怎么算(Canvas之向量数学)(13)

/** * 判断坐标是否在凸多边形内部 * @param pt * @param points * @returns */ public static isPointInPolygon(pt: vec2, points: vec2[]): boolean { if (points.length < 3) { return false; } // 以point[0]为共享点,遍历多边形点集,构成三角形,判断点是否在三角形内部, // 一旦点与某个三角形发生碰撞,就返回true for (let i: number = 2; i < points.length; i ) { if (Math2D.isPointInTriangle(pt, points[0], points[i - 1], points[i])) { return true; } } return false; }

3.3.5.1凸多边形判断

canvas的坐标怎么算(Canvas之向量数学)(14)

参考图6.13会发现,左侧的凸多边形(六边形)顶点形成的6个子三角形分别是[ v0, v1 , v2 ]、[ v1 , v2 , v3 ]、[ v2 , v3 , v4 ]、[ v3 , v4 , v5 ]、[ v4 , v5 , v0 ]和[v5 , v0 , v1 ],这6个子三角形顶点的顺序都是顺时针排列的。

而观察上图右侧的凹多边形,由5个顶点组成,形成的5个子三角形分别是[ v0 , v1, v2 ]、[ v1 , v2 , v3 ]、[ v2 , v3 , v4 ]、[ v3 , v4 , v0 ]和[ v4 , v0 , v1 ],会发现最后一个三角形[ v4 , v0 , v1]的顶点顺序是逆时针排列,而其他的三角形都是顺时针排列。

如何判断一个三角形的顶点顺序,已经在上一节中了解过了,那么直接来看一下判断凸多边形的算法。

/** * 判断是否凸多边形 * @param points * @returns */ public static isConvex(points: vec2[]): boolean { // 算出第一个三角形的定点顺序 let sign: boolean = Math2D.sign(points[0], points[1], points[2]) < 0; let j: number, k: number; for (let i: number = 1; i < points.length; i ) { j = (i 1) % points.length; k = (i 2) % points.length; // 如果当前多边形的顶点顺序和第一个多边形的顶点顺序不一致,则说明是凹边形 if (sign !== Math2D.sign(points[i], points[j], points[k]) < 0) { return false; } } // 凸多边形 return true; }

4.总结

向量是具有大小和方向的空间变量。而以前常用的数值都是标量,其仅有大小,而没有方向。在向量一节中,知道了如何计算向量的大小、向量的方向、向量的加减法、负向量、向量的缩放,以及向量的点乘。同时更加详细地解释了向量的上述这些操作相对应的几何含义,这是本文的关键点。只有深刻地理解向量的几何含义,才能灵活地应用向量来解决问题。

核心关注点与基本几何形体之间的碰撞检测算法,涉及点与线段、点与圆、点与矩形、点与椭圆、点与三角形,以及点与凸多边形之间的碰撞检测,并且讲解了多边形的三角形化,以及如何判断凸多边形的算法。

,