向量关系基础概念
在几何和图形学中,首尾相连的向量序列和旋转顺序有以下标准术语:
1. 闭合路径
首尾相连的向量现象,被称为:多边形环(Polygon Loop)或闭合路径(Closed Path)
- 定义:一系列向量按顺序连接,且最后一个向量的终点与第一个向量的起点重合,形成闭合图形。
- 相关概念:
- 顶点顺序(Vertex Order):描述顶点排列的方向性(顺时针/逆时针)。
- 有向边(Directed Edges):每个向量代表一条有方向的边。
关键特性
- 在计算几何中,这种结构是判断多边形性质(如凸性、自相交)的基础。
- Three.js 的
THREE.Shape和THREE.Path依赖闭合路径定义形状。
2. 缠绕顺序(顶点顺序)
按闭合路径顺序访问顶点的顺序,被称为:缠绕顺序(Winding Order)
- 定义:顶点围绕多边形中心排列的方向(顺时针/CW 或逆时针/CCW)。
- 数学判定:通过叉积和(如
ShapeUtils.isClockWise)确定:- 顺时针(Clockwise, CW):叉积和为负。
- 逆时针(Counter-Clockwise, CCW):叉积和为正。
应用场景
- 图形渲染:
决定多边形的正面(Front Face)和背面(Back Face),影响剔除(Culling)行为。// Three.js 中设置正面剔除 material.side = THREE.FrontSide; // 默认渲染正面(逆时针) - 孔洞处理:
在复杂多边形中,外围路径和孔洞需有相反的缠绕顺序(如外围CCW,孔洞CW)。
3.向量缠绕顺序的作用
- 渲染正确性:错误的缠绕顺序会导致面片不可见(被错误剔除)。
- 几何操作:
- 三角剖分(Tessellation)需统一缠绕顺序。
- 物理引擎碰撞检测依赖法线方向(由顶点顺序推导)。
4. 二维法线的定义
对于二维向量 (vec{v} = (x, y)),其法线向量有两个方向(垂直于原向量):
- 左手法线(Outward Normal):((-y, x))(逆时针旋转90度)。
- 右手法线(Inward Normal):((y, -x))(顺时针旋转90度)。
我们常用逆时针旋转90°得到“左手法线”(或者叫“外法线”)
5. 闭合路径的缠绕顺序与法线方向的关系
关键结论:
- 顺时针(CW)缠绕:
- 边向量的法线指向路径外部(假设路径是闭合多边形)。
- 逆时针(CCW)缠绕:
- 边向量的法线指向路径内部。
为什么?
-
顺时针路径(CW):
- 每条边的方向是“顺时针”排列的。
- 对边向量 (vec{v} = (x_i - x_{i-1}, y_i - y_{i-1})),其左手法线 ((-(y_i - y_{i-1}), x_i - x_{i-1})) 会指向外侧(因为整体旋转方向是“收紧”的)。
- 例如:矩形的顺时针顶点顺序会使得所有边的法线向外。
-
逆时针路径(CCW):
- 边向量的左手法线会指向内侧(因为整体旋转方向是“扩张”的)。
6. 数学原理
-
叉积符号决定法线方向:
二维叉积 (vec{a} X vec{b} = a_x b_y - a_y b_x) 的符号隐含了旋转方向。- 顺时针路径的叉积和为负,法线自然朝外。
- 逆时针路径的叉积为正,法线朝内。
-
右手定则:
在右手坐标系中(Y轴向上),逆时针路径的法线指向屏幕外(Z轴正方向),但二维下简化为“指向内部”。
7. 应用场景
- 图形渲染:
- 逆时针(CCW)为默认“正面”,法线朝内时渲染可见。
- 通过
material.side = THREE.DoubleSide可强制渲染双面。
- 物理引擎:
- 碰撞检测中,法线方向决定碰撞响应(如反弹方向)。
- 路径偏移:
- 沿法线方向平移路径(如生成描边效果)需考虑缠绕顺序。
| 缠绕顺序 | 边向量法线方向 | 典型用途 |
|---|---|---|
| 顺时针(CW) | 指向路径外部 | 孔洞、布尔运算的裁剪区域 |
| 逆时针(CCW) | 指向路径内部 | 默认的多边形填充区域 |
顺时针路径的法线朝外,逆时针路径的法线朝内。
8. 二维向量的叉积(scalar cross product)
它的公式就是:
cross(v1, v2) = v1.x * v2.y - v1.y * v2.x
它表示什么?
1.向量围成的三角形的有符号面积(面积的2倍)
- 值的绝对值 |cross| 是 平行四边形的面积(二维)
- 所以三角形面积是 0.5 * |cross|
2.向量之间的相对方向(顺时针 or 逆时针)
- 如果 cross > 0:逆时针
- 如果 cross < 0:顺时针
- 如果 cross === 0:共线(同方向或相反)
建议你记住这句话: 在 XOY 平面,v1 × v2 > 0 表示 v2 在 v1 的逆时针方向。
方向判断遵循右手螺旋定则:
比如 c = a X b
让四指沿着叉积左值(a向量)的方向,然后四指向叉积右值(b向量)可以弯曲,
(如果无法弯曲,就翻转大拇指的朝向,然后再往右值弯曲),
那么大拇指的指向方向是向量c的方向。
举个例子,向量a和向量b在xoy平面,如果按右手螺旋定则,
-
得知向量c方向朝下,即为z轴负方向,那么就说明叉积的值是负值,那么a向量到b向量,向量b再到向量a,组成的环形布局,是按顺时针排列的
-
如果向量c的方向朝上,即为z轴正方向,那么就说明叉积的值是正值,那么a向量到b向量的,向量b再到向量a,组成的环形布局,是按逆时针排列的
注意,叉乘不满足交换律,当你需要判断多个向量方向相对关系时,左侧点击值需要保持统一
叉乘不满足交换律
你通过比较向量a叉乘b与向量b叉乘a区别,顺序不同结果不同,也就是说叉乘不满足交换律。
// a叉乘b
c.crossVectors(a,b);
// b叉乘a
c.crossVectors(b,a);
叉积的应用
叉积对于判断点的排列方向很关键,比如:
- 判断 p1 -> p2 -> p3 是左转还是右转
- 判断 Shape 的轮廓是否逆时针
它和法线的关系?
二维中:
-
法线(normal)是垂直于方向向量的单位向量
-
而 cross 只是标量(不在一个概念维度)
-
但是:
- 在 3D 中,向量叉积是返回一个垂直的向量(法线)****
- 而在 2D 中,这个标量 cross 的正负值反映了两向量叉积的Z轴方向(垂直于2D平面)
所以可以说:
二维 cross 的正负 ≈ 三维向量叉积结果 z 分量的正负。
9. 向量点积
const dot = v1.dot(v2); // v1.x * v2.x + v1.y * v2.y
它表示两个向量的夹角关系,例如是否垂直(dot=0)、是否同向(dot > 0)。
| 计算值 | 类型 | 含义 | 应用 |
|---|---|---|---|
| cross(二维) | 叉积标量 | 有符号面积 / 方向判断 | 判断点的顺时针/逆时针,面积 |
| dot | 点积标量 | 夹角的余弦、方向是否相似 | 判断是否垂直/平行 |
| v1.cross(v2) (Vector3) | 三维向量 | 得到垂直于两个向量的向量 | 法线计算、旋转轴计算等 |
10.实践
1. 判断相邻点组成的闭合路径的旋转顺序
import { ShapeUtils } from 'three';
if (ShapeUtils.isClockWise(points)) {
points.reverse(); // 将顺时针转为逆时针
}
在 Three.js 中,ShapeUtils.isClockWise(points) 是一个用于 判断二维多边形顶点顺序是否为顺时针方向 的实用工具函数。它通过计算多边形的有向面积来确定顶点排列顺序,常用于几何处理中的方向校验。
输入与输出
- 输入:一个二维点数组
points(格式为THREE.Vector2[]或[x, y, ...])。 - 输出:布尔值
true表示顺时针,false表示逆时针。
数学原理
函数基于 鞋带公式(Shoelace Formula) 计算多边形的有向面积:
- 面积为正 → 顶点逆时针排列。
- 面积为负 → 顶点顺时针排列。
源码实现
ShapeUtils.isClockWise(points) 的核心原理正是通过 向量叉积之和 来判断多边形的顶点顺序方向。
Three.js 源码验证
查看 Three.js 的 ShapeUtils.isClockWise 源码,确实如此实现:
function isClockWise( pts ) {
let sum = 0;
for ( let i = 0, il = pts.length - 1; i < il; i ++ ) {
sum += ( pts[ i + 1 ].x - pts[ i ].x ) * ( pts[ i + 1 ].y + pts[ i ].y );
}
return sum < 0;
}
(注:实际源码通过优化后的鞋带公式计算,数学本质与叉积和一致。)
2. 判断不明顺序的点集合的闭合路径和旋转顺序
用凸包算法(Convex Hull)得到一个不相交、封闭的轮廓
import convexHull from 'convex-hull-2d'; // npm i convex-hull-2d
const points = [p1, p2, p3, p4];
const pointArray = points.map(p => [p.x, p.y]);
const hull = convexHull(pointArray); // 返回索引对,如 [[0, 1], [1, 2], ...]
const ordered = hull.map(([i]) => points[i]); // 提取点顺序
⚠️ 注意:这是二维算法,只适用于 XY、XZ 等平面点的处理。
3.尽量按相邻点和旋转顺序缓存点集合
在开发过程中,为了后续构造轮廓(Shape)、挤出(Extrude)、布尔操作、网格计算等操作更容易,应该尽可能:
- 按相邻和特定排列顺序缓存点
- 或者 使用可推导相邻关系的结构缓存点(如边、环、网格)
为什么要“按相邻顺序缓存点”?
因为三维几何绘制通常需要你提供:
-
顺序的点列表(用于轮廓)
-
首尾相接(封闭性)
-
方向一致(顺/逆时针)
这三点决定了:
- Shape 能否填充正确的面
- Shape.holes 能否识别镂空区域
- ExtrudeGeometry 是否能成功挤出体积
- ShapeGeometry 能否渲染预期区域
- 动态绘图是否无缝衔接
实战建议
| 使用场景 | 点数据设计建议 |
|---|---|
| 墙体绘制、封闭区域绘制 | 用数组保持相邻顺序 |
| 多边形轮廓、Shape 几何 | 使用顺序数组,并保证首尾闭合 |
| 用户点击自由建图 | 实时更新相邻点顺序,辅助用线段或边缓存 |
| 后续要挖洞、合并面 | 提前按“外轮廓 + 内轮廓”结构缓存点 |
总结一句话:
在几何处理和三维绘图中,维护清晰、顺序性强、结构合理的点/边数据结构,是构造复杂模型的前提。否则后期每一步都需要猜测顺序和逻辑,效率极低。