Skip to content

Vec:既是位置,也是箭头

动工之前,老盖得在图纸上把数算清:彗星离太阳多远?朝哪个方向飞?这些演算用的数学类型就是 Transform::translation 的类型本身——Vec3(三维向量,x、y、z 三个 f32),以及它的平面兄弟 Vec2。它们是普通的 Copy 值,不是组件、不需要 App,在 main 里就能把脾气摸清:

rust
// 老盖摊开图纸:三个天体的位置,各用一个 Vec2 表示
    let sun = Vec2::new(0.0, 0.0); // 太阳钉在图纸原点
    let earth = Vec2::new(180.0, 0.0); // 地球在太阳正右方 180
    let comet = Vec2::new(-120.0, 160.0); // 彗星在左上方游荡

Listing 12-3(其一):三个天体的位置,各用一个 Vec2(examples/listing-12-03.rs)

同一个 Vec2 有两种解读。写成 comet 时它是位置——图纸上的一个点;而两个位置相减,得到的是箭头——从一点指向另一点的位移,有方向、有长度:

rust
// 位置相减得到“从 B 指向 A 的箭头”,箭头的长度就是距离
    let sun_to_comet = comet - sun;
    println!("老盖量彗星:离太阳 {},离地球 {}", sun_to_comet.length(), comet.distance(earth));

Listing 12-3(其二):位置相减得箭头,箭头的长度是距离

length() 给出箭头的长度,distance() 是“相减再取长度”的快捷写法。游戏里的“索敌半径”“拾取范围”,本质都是这一行。

性能提示:比较距离用 length_squared()(长度的平方)更省——开平方是这几个操作里唯一贵的一步,而“谁更近”用平方比较结果一样。

归一化:把箭头压成纯方向

箭头携带两份信息:方向和长度。很多场合只要方向——“朝那边走,速度我自己定”。normalize() 把箭头压成长度为 1 的单位向量,只留方向:

rust
// 归一化:把箭头压成长度 1,只留方向
    let comet_bearing = sun_to_comet.normalize();
    println!("彗星的方位(单位向量):{comet_bearing:.1},长度 {:.2}", comet_bearing.length());

Listing 12-3(其三):normalize 只留方向

拿到单位方向后乘上标量就是“沿这个方向走多远”:bearing * 120.0 是沿方向 120 单位的位移。第 2 章方块的运动公式、日后所有“朝目标移动”的代码,骨架都是 位置 += 方向 * 速度 * dt

小心一个坑:零向量没有方向Vec2::ZERO.normalize() 给出的是 NaN(非数),带着它继续算会污染一切。拿不准输入是否为零时用 normalize_or_zero(),或者用下一节的 Dir2 类型把检查交给类型系统。

点积:方向之间的问答

两个单位向量的点积dot)是一个标量,直接回答“它们有多同向”:同向得 1,垂直得 0,反向得 −1。观测站靠它判断彗星的来意:

rust
// 点积:两个单位向量同向得 1,垂直得 0,反向得 -1
    let to_sun = (sun - comet).normalize();
    let dive = Vec2::new(0.6, -0.8); // 第一晚测得的彗星速度方向
    let graze = Vec2::new(0.8, 0.6); // 第二晚测得的速度方向
    println!("第一晚:速度·朝日方向 = {}(全速冲着太阳来!)", dive.dot(to_sun));
    println!("第二晚:速度·朝日方向 = {}(虚惊一场,正横着掠过)", graze.dot(to_sun));

Listing 12-3(其四):点积判断“冲着来还是横着过”

“敌人在我身前还是身后”“子弹算不算迎面命中”,都是一次点积的事。

lerp 与 2D/3D 互转

最后两件随手用的小工具——按比例取中间点的 lerp(线性插值),和 Vec2Vec3 的互转:

rust
// lerp:在两点之间按比例取点,0.5 就是正中
    println!("日地连线的中点:{}", sun.lerp(earth, 0.5));

    // 2D 图纸钉进 3D 世界:补一个 z 就是 Vec3,截掉 z 又回到 Vec2
    let comet_3d: Vec3 = comet.extend(5.0);
    println!("彗星入册:{comet_3d},截回平面:{}", comet_3d.truncate());

Listing 12-3(其五):lerp 取中点;extend 补 z、truncate 截 z

extend(z) 给平面坐标补上第三维,truncate() 把它截回去。这对组合在 2D 游戏里出场率极高:游戏逻辑在平面上用 Vec2 思考,而 translationVec3——上一节刚讲过,那个 z 是图层号,不该被平面运算搅动。“xy 归逻辑,z 归图层”的纪律,靠它们维持。

console
cargo run -p ch12-transforms --example listing-12-03
text
老盖量彗星:离太阳 200,离地球 340
彗星的方位(单位向量):[-0.6, 0.8],长度 1.00
第一晚:速度·朝日方向 = 1(全速冲着太阳来!)
第二晚:速度·朝日方向 = 0(虚惊一场,正横着掠过)
日地连线的中点:[90, 0]
彗星入册:[-120, 160, 5],截回平面:[-120, 160]

这页演算摊在图纸上就是 Figure 12-4:三个点是位置,太阳到彗星的实线是相减得到的箭头,虚线只报距离,lerp 的空心圈停在日地正中。

图纸示意:太阳 (0,0)、地球 (180,0)、彗星 (−120,160) 三个点;从太阳指向彗星的橙色箭头标注 comet − sun 与 length() = 200;彗星与地球之间的灰色虚线标注 distance() = 340;日地连线中点处有 lerp 的空心圆点;右下角图例画着 normalize 之后只剩方向的单位箭头

Figure 12-4:图纸演算的几何形状——位置是点,相减是箭头,归一化只留方向

图纸演算完毕。Vec 家族还有不少成员——整数版的 IVec2/UVec2、四维的 Vec4、双精度的 DVec3——API 形状全一样,用到再查。下一节正式开机:用 Transform 把太阳装上转轴。