2.2 点与向量的区别
站内的 图形数学基础
把点与向量都当成三元组来算,浮点库不区分二者的语义;本节要的是可断言的区别:同一个平移矩阵,为什么对点生效、对向量却完全无效。答案藏在齐次坐标的第四分量 w 里——点取 w=1,向量取 w=0,仅这一位之差,就让平移项在向量上被乘 0 抹掉。
w 是坐标的「类型标签」
齐次坐标给每个三维量补一个第四分量。点的 w=1,向量的 w=0。这不是记号,而是运算规则的一部分:
- 点 - 点 = 向量:
w: 1 - 1 = 0,结果自然是向量。两点相减得到的位移与原点无关。 - 点 + 向量 = 点:
w: 1 + 0 = 1,把点沿向量挪一段,落点仍是点。 - 向量 + 向量 = 向量:
w: 0 + 0 = 0,两个方向相加仍是方向。
这三条不是约定俗成,而是 w 在加减下的自然结果。下面的例子逐条断言它们。
平移矩阵为什么对向量无效
mat_model 的平移分量落在矩阵的第 4 列:
scale[0] 0 0 t_x
0 scale[1] 0 t_y
0 0 scale[2] t_z
0 0 0 ONE
mat_vec4 的第 r 行做的是 m[r,0]·x + m[r,1]·y + m[r,2]·z + m[r,3]·w。平移项 t_x 乘的是 w:点(w=1)拿到整段平移,向量(w=0)拿到 t_x · 0 = 0,平移被消去。这正是「平移只移动位置、不改变方向」的代数表达。
可跑的演示与断言
S 层脚本复用 sim/cglib.py 的 v_add / v_sub / mat_model / mat_vec4:
#!/usr/bin/env python3
# file: lantern/sim/cg_raster_02_2_point_vector.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/cg_raster_02_2_point_vector.py
"""2.2 点与向量的区别:齐次坐标里 w=1 是点、w=0 是向量。
可跑的演示与断言:
- 点 - 点 = 向量(w: 1 - 1 = 0)
- 点 + 向量 = 点(w: 1 + 0 = 1)
- 向量 + 向量 = 向量(w: 0 + 0 = 0)
- 平移矩阵对点有效、对向量无效(w=0 让平移项乘 0)
- 把点当向量变换(w=0)会丢掉平移——这是最常见的坐标类型错用
产物:out/cg_raster_02_2_result.json
"""
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cglib # noqa: E402
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ONE = cglib.ONE
def show(name, v):
print(f"{name:22s}: {v}")
def main():
# 点:w = 1;向量:w = 0
p1 = (1 * ONE, 2 * ONE, 3 * ONE)
p2 = (4 * ONE, 6 * ONE, 3 * ONE)
vec = (1 * ONE, 0, 0)
print(f"ONE : {ONE} (w=1 表示点, w=0 表示向量)")
show("点 p1", p1)
show("点 p2", p2)
show("向量 v", vec)
# --- 点 - 点 = 向量 --------------------------------------------------
d = cglib.v_sub(p2, p1)
show("p2 - p1 (应为向量)", d)
assert d == (3 * ONE, 4 * ONE, 0), "两点相减应得向量 (3,4,0)"
# 齐次意义:w: 1 - 1 = 0,结果自然是向量
print(f" w: 1 - 1 = {1 - 1} -> 向量")
# --- 点 + 向量 = 点 --------------------------------------------------
p3 = cglib.v_add(p1, d)
show("p1 + (p2-p1) (应为点)", p3)
assert p3 == p2, "点加向量应回到原起点"
print(f" w: 1 + 0 = {1 + 0} -> 点")
# --- 向量 + 向量 = 向量 ----------------------------------------------
v2 = cglib.v_add(vec, vec)
show("v + v (应为向量)", v2)
assert v2 == (2 * ONE, 0, 0), "向量相加仍是向量"
print(f" w: 0 + 0 = {0 + 0} -> 向量")
# --- 平移矩阵:对点有效、对向量无效 -----------------------------------
t = (10 * ONE, 0, 0)
T = cglib.mat_model(translate=t)
# 点按 w=1 变换
pt_h = cglib.mat_vec4(T, (p1[0], p1[1], p1[2], ONE))
show("T·p1 (w=1)", pt_h[:3])
assert pt_h[:3] == (11 * ONE, 2 * ONE, 3 * ONE), "平移应作用于点"
print(f" w' = {pt_h[3]} (= ONE,仍是点)")
# 向量按 w=0 变换
vc_h = cglib.mat_vec4(T, (vec[0], vec[1], vec[2], 0))
show("T·v (w=0)", vc_h[:3])
assert vc_h[:3] == vec, "平移不应作用于向量"
print(f" w' = {vc_h[3]} (= 0,仍是向量)")
# 平移项为何消失:第 4 列乘的是 w
print(f" 平移项 t={t} 在 w=0 时乘 0,故被消去")
# --- 错用:把点当向量变换,平移丢失 -----------------------------------
wrong = cglib.mat_vec4(T, (p1[0], p1[1], p1[2], 0))
show("T·p1 错用 w=0", wrong[:3])
assert wrong[:3] == p1, "w=0 时平移丢失,点停在原地"
assert wrong[:3] != pt_h[:3], "与正确结果必须分叉"
out = os.path.join(ROOT, "out")
os.makedirs(out, exist_ok=True)
result = {
"p1": list(p1),
"p2": list(p2),
"v": list(vec),
"p2_minus_p1": list(d),
"p1_plus_d": list(p3),
"v_plus_v": list(v2),
"translate": list(t),
"T_point_w1": list(pt_h),
"T_vector_w0": list(vc_h),
"T_point_w0_wrong": list(wrong),
}
with open(os.path.join(out, "cg_raster_02_2_result.json"), "w", encoding="utf-8") as fh:
json.dump(result, fh, ensure_ascii=False, indent=2)
print("out : cg_raster_02_2_result.json")
return 0
if __name__ == "__main__":
sys.exit(main())
P 层把这六条断言逐条搬进 node --test,包括那个「把点当向量」的负例:
// file: lantern/core/test/cg_raster_02_2_point_vector.test.ts
// tier: P
// verify: node --test 'core/test/cg_raster_02_2_point_vector.test.ts'
// 2.2 的 P 层:齐次坐标 w=1 是点、w=0 是向量,平移矩阵只作用于点。
// 与 S 侧 sim/cg_raster_02_2_point_vector.py 的断言逐条对应。
import assert from "node:assert/strict";
import { test } from "node:test";
import { ONE } from "../src/lantern/fixed.ts";
import { type Mat4, matModel, matVec4 } from "../src/lantern/mat.ts";
import { type Vec3, vAdd, vSub } from "../src/lantern/vec.ts";
const p1: Vec3 = [1n * ONE, 2n * ONE, 3n * ONE];
const p2: Vec3 = [4n * ONE, 6n * ONE, 3n * ONE];
const v: Vec3 = [1n * ONE, 0n, 0n];
const T: Mat4 = matModel([10n * ONE, 0n, 0n]);
test("点 - 点 = 向量(w: 1 - 1 = 0)", () => {
const d = vSub(p2, p1);
assert.deepEqual(d, [3n * ONE, 4n * ONE, 0n]);
});
test("点 + 向量 = 点(w: 1 + 0 = 1)", () => {
const d = vSub(p2, p1);
assert.deepEqual(vAdd(p1, d), p2);
});
test("向量 + 向量 = 向量(w: 0 + 0 = 0)", () => {
assert.deepEqual(vAdd(v, v), [2n * ONE, 0n, 0n]);
});
test("平移矩阵对点有效:w=1 时平移项生效", () => {
const r = matVec4(T, [p1[0], p1[1], p1[2], ONE]);
assert.deepEqual(r, [11n * ONE, 2n * ONE, 3n * ONE, ONE]);
});
test("平移矩阵对向量无效:w=0 时平移项被乘 0", () => {
const r = matVec4(T, [v[0], v[1], v[2], 0n]);
assert.deepEqual(r, [v[0], v[1], v[2], 0n]);
});
test("负例:把点当向量变换(w=0)-> 平移丢失,与正确结果分叉", () => {
const right = matVec4(T, [p1[0], p1[1], p1[2], ONE]);
const wrong = matVec4(T, [p1[0], p1[1], p1[2], 0n]);
assert.deepEqual([wrong[0], wrong[1], wrong[2]], p1, "w=0 时点停在原地");
assert.notDeepEqual(wrong, right, "错用 w=0 必须与正确结果分叉");
});
真跑:
# snippet: 复现命令(在 lantern 工作区根执行)
cd $HOME/.cgbook/lantern
/opt/homebrew/bin/python3 sim/cg_raster_02_2_point_vector.py
node --test 'core/test/cg_raster_02_2_point_vector.test.ts'
S 侧输出:
ONE : 65536 (w=1 表示点, w=0 表示向量)
p2 - p1 (应为向量) : (196608, 262144, 0)
w: 1 - 1 = 0 -> 向量
p1 + (p2-p1) (应为点) : (262144, 393216, 196608)
w: 1 + 0 = 1 -> 点
v + v (应为向量) : (131072, 0, 0)
w: 0 + 0 = 0 -> 向量
T·p1 (w=1) : (720896, 131072, 196608)
T·v (w=0) : (65536, 0, 0)
T·p1 错用 w=0 : (65536, 131072, 196608)
P 侧结果:
ℹ tests 6
ℹ pass 6
ℹ fail 0
注意 T·p1 得到 (720896, …) = (11, …),而错用 w=0 的 T·p1 停在 (65536, …) = (1, …) 原处——平移项 t = (655360, 0, 0) 在 w=0 时乘 0 被抹掉。这就是把点误当向量时最典型的症状:模型「飘不动」。
阅读导航:上一节:2.1 向量运算的定点化 · 下一节:2.3 齐次坐标与投影 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。