图形学中向量的运算

439 阅读8分钟

向量关系基础概念

在几何和图形学中,首尾相连的向量序列和旋转顺序有以下标准术语:

1. 闭合路径

首尾相连的向量现象,被称为:多边形环(Polygon Loop)或闭合路径(Closed Path)

  • 定义:一系列向量按顺序连接,且最后一个向量的终点与第一个向量的起点重合,形成闭合图形。
  • 相关概念
    • 顶点顺序(Vertex Order):描述顶点排列的方向性(顺时针/逆时针)。
    • 有向边(Directed Edges):每个向量代表一条有方向的边。

关键特性

  • 在计算几何中,这种结构是判断多边形性质(如凸性、自相交)的基础。
  • Three.js 的 THREE.ShapeTHREE.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)缠绕
    • 边向量的法线指向路径内部

为什么?

  1. 顺时针路径(CW)

    • 每条边的方向是“顺时针”排列的。
    • 对边向量 (vec{v} = (x_i - x_{i-1}, y_i - y_{i-1})),其左手法线 ((-(y_i - y_{i-1}), x_i - x_{i-1})) 会指向外侧(因为整体旋转方向是“收紧”的)。
    • 例如:矩形的顺时针顶点顺序会使得所有边的法线向外。
  2. 逆时针路径(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,组成的环形布局,是按逆时针排列的

注意,叉乘不满足交换律,当你需要判断多个向量方向相对关系时,左侧点击值需要保持统一

image.png

叉乘不满足交换律

你通过比较向量a叉乘b与向量b叉乘a区别,顺序不同结果不同,也就是说叉乘不满足交换律。

// a叉乘b
c.crossVectors(a,b);
// b叉乘a
c.crossVectors(b,a);

image.png

叉积的应用

叉积对于判断点的排列方向很关键,比如:

  • 判断 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)  计算多边形的有向面积:

image.png

  • 面积为正 → 顶点逆时针排列。
  • 面积为负 → 顶点顺时针排列。

源码实现

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 几何使用顺序数组,并保证首尾闭合
用户点击自由建图实时更新相邻点顺序,辅助用线段或边缓存
后续要挖洞、合并面提前按“外轮廓 + 内轮廓”结构缓存点

总结一句话:

在几何处理和三维绘图中,维护清晰、顺序性强、结构合理的点/边数据结构,是构造复杂模型的前提。否则后期每一步都需要猜测顺序和逻辑,效率极低。