08 · 二维叉积:左右、转弯与有向面积
1. 二维为什么也能谈叉积
把二维向量放进三维 XY 平面:a=(ax,ay,0),b=(bx,by,0)。三维叉积只剩 z 分量:
因此二维语境常把这个 z 分量称为二维叉积。Three.js Vector2.cross(v) 正是返回该标量,不修改接收向量,也不返回 Vector2。严格说它是二维行列式或有向面积,不是在二维平面内构造一个垂直向量。
2. 左右的前提是轴向与顺序
在 X 向右、Y 向上的平面,a 从共同起点出发作为参考:
正值表示 b 在 a 的逆时针侧,通常称为左侧;负值为顺时针侧,通常称为右侧。交换 a、b 会改变符号。
import * as THREE from 'three';
const east = new THREE.Vector2(1, 0);
const north = new THREE.Vector2(0, 1);
console.log(east.cross(north)); // 1:从东向北是左转
console.log(north.cross(east)); // -1
浏览器 client 坐标中 Y 向下,同样的代数正值在屏幕上表现为顺时针。不是函数错误,而是显示坐标轴的朝向变了。
3. 道路哪一侧:先把点变成位移
一条有向道路从 A=(1,1) 走向 B=(5,1),行人位于 P=(3,3)。道路向量 B-A=(4,0),道路起点到行人的位移 P-A=(2,2),叉积为 8,所以行人在道路左侧。
const road = new THREE.Vector2();
const toPoint = new THREE.Vector2();
function sideOfRoad(a, b, p, tolerance = 0.01) {
road.subVectors(b, a);
const length = road.length();
if (length < 1e-8) return 'invalid-road';
toPoint.subVectors(p, a);
const signedDistance = road.cross(toPoint) / length;
if (Math.abs(signedDistance) <= tolerance) return 'on-line';
return signedDistance > 0 ? 'left' : 'right';
}
叉积的量纲是长度平方,除以道路长度后才是点到无限直线的有符号垂直距离。这样 tolerance=0.01 才能解释为 1 厘米(假定世界单位是米),而不会随道路长短改变容差。
此函数判断的是道路所在的无限直线。要判断是否在道路线段附近,还需计算沿道路投影参数 t=(P-A)·(B-A)/|B-A|²,检查 t 是否在 [0,1],或求最近端点距离。
4. 转弯角度同时需要点积
叉积为零可能是同向,也可能是反向,还可能存在零向量。用点积区分前两者,并先排除零长度。需要有符号最短转角时:
function signedTurn(a, b) {
if (a.lengthSq() < 1e-12 || b.lengthSq() < 1e-12) return null;
return Math.atan2(a.cross(b), a.dot(b));
}
无需分别归一化,因为叉积与点积都带相同的长度乘积。恰好反向时,左右绕行同样短,正负 π 还受带符号零等影响;业务应选择稳定策略,例如沿上一次转向。
5. 面积与地图绕序
叉积绝对值是两向量张成的平行四边形面积,所以三角形面积是:
保留符号还能判断三个顶点的绕序。简单多边形可以把连续顶点的叉积累加后除以 2,得到鞋带公式的有向面积;自交多边形则涉及有向区域抵消,不能直接当成普通占地面积。
在 Three.js 的 XZ 地面上,如果把 (x,z) 映射到 Vector2,两维叉积与原三维叉积的 Y 分量符号相反:cross3(a,b).y = az*bx - ax*bz。实际导航应先明确你用的是地图二维约定还是世界 +Y 的旋转约定。
练习(不附答案)
- 道路 A=(2,1)、B=(2,5),判断 P=(0,3)、Q=(4,3)、R=(2,8) 的位置关系,并区分直线与线段。
- 将 sideOfRoad 改用于浏览器 client 坐标,确保 left/right 与屏幕上的观察一致。
- 编写返回三角形面积和绕序的函数,处理共线点。
- 用三维 XZ 向量验证二维叉积与三维叉积 Y 分量的符号关系。