2.2 点与向量的区别

齐次坐标里 w=1 是点、w=0 是向量,这一位之差决定了平移矩阵对谁生效。本节用可跑的例子演示点减点得向量、点加向量得点、向量加向量仍是向量,并逐条给出断言;最后把点当向量按 w=0 变换,展示平移项如何被乘 0 抹掉,以及模型为何飘不动。

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 齐次坐标与投影 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「cg」更多文章

  1. 14.3 中间缓冲的可视化
  2. 14.2 覆盖率与线框
  3. 14.1 深度图与法线图