学习进度
0/9 已勾只保存在这台浏览器;勾满 9 项即视为本题完成。
先把题目说成人话
守卫只需要判断目标是否走进面前的扇形区域,手电筒只需要判断光束是否对准物体,瞄准提示还可能要分清目标在左还是在右。这些现实问题都可以先看成“两支箭头朝得有多像”。但箭头长短会干扰比较,长度几乎为零的箭头甚至没有方向;若忽略这些前提,边界附近就会闪烁或突然得到无效结果。
同时带方向和长度的箭头叫向量(Vector),把它缩成长度为一而不改方向叫归一化(Normalize)。两支单位箭头的重合分数叫点积(Dot Product):同向接近一、垂直接近零、反向接近负一。只有确实需要显示具体角度时,才把这个分数交给反余弦函数(acos);只做范围门槛时,直接比较分数更合适。
这是虚构教学故事,不是个人项目经历。
场景:先让问题变得看得见
假设聚光判定要求目标进入角色前方的一小块扇区。先准备三种能直接画出来的情况:一支箭正对前方,一支箭与它垂直,还有一支箭短到没有可用方向。前两种有明确几何关系,最后一种不能硬塞进任何角度。
(右脑)画面:把两支箭放在同一张透明纸上,先沿各自方向把它们缩成一样长,再让尾端重合。两支箭完全叠住时,方向最相近;张成直角时互不偏向;箭头相背时方向最远。守卫面前还画着一条固定边界线,只要目标箭落在边界以内,就已经得到“看得见或看不见”的答案,不必再量整段圆弧。若其中一支箭缩成了一个点,纸上根本没有可比较的朝向,应该把这次输入交给上层决定,而不是伪造一个角度。
(左脑)对表:本题对象是同一坐标空间中的 a、b,长度平方、单位方向、点积和可选参考轴 n。前向与正上应为 90°、零向量没有方向是定义和基线事实;“acos(dot(a,b)) 对任意输入都成立”是假设。证据来自 CPU 参考值、Shader 非法输入染色和目标平台 half/float 对照。向量可有长度单位,归一化方向、余弦和点积门槛无量纲,acos/atan2 输出通常是弧度;eps2 必须与长度平方同量纲。有向角还要求投影平面和单位 n。两箭头类比解释方向关系,但不等于证明未归一化、异空间或零长度输入也满足公式。
小明:先形成一个完整猜测
小明直接写 acos(dot(a,b))。当向量长度不是 1 时,点积不再是余弦;接近同向时,舍入又可能给出 1.0000001,acos 返回 NaN。失败说明:公式的前提没有满足,不能把函数名当证明。
小红:把猜测放回现实检查
小红先把“没有方向”变成显式返回值,再处理正常输入。函数成功时输出弧度;任一向量过短时返回 false,调用方必须决定跳过、沿用上一方向还是使用项目约定的备用值:
bool TryUnsignedAngle(float3 a, float3 b, float eps2,
out float angleRadians)
{
float la2 = dot(a, a);
float lb2 = dot(b, b);
if (la2 < eps2 || lb2 < eps2)
{
angleRadians = 0.0; // 只是初始化,不代表零向量夹角为 0
return false;
}
float3 an = a * rsqrt(la2);
float3 bn = b * rsqrt(lb2);
float c = clamp(dot(an, bn), -1.0, 1.0);
angleRadians = acos(c); // HLSL acos 返回弧度
return true;
}
若只判断 30° 扇区,在通过同一长度检查并得到 an/bn 后,直接比较 dot(an,bn) >= cos(radians(30)),无需计算角度。
我:用检查结果修正模型
我区分无符号角与有向角。length(cross(a,b)) 只有面积大小,没有左右符号。若需要绕参考法线 n 的有向角,先把 n 归一化,把 a/b 投影到与 n 正交的平面并检查投影长度不为零;归一化为 an/bn 后再使用
atan2(dot(n, cross(an, bn)), dot(an, bn))。
若只需更稳定的无符号角,也可用 atan2(length(cross(an,bn)), dot(an,bn))。有向公式只对“绕 n 的平面内角”成立;未投影的三维向量会把轴外分量混进结果。所有量都明确在同一坐标空间中计算;法线在世界空间、视线在视图空间时,点积即使数值合法也没有几何意义。最终在 Unity Shader 的目标精度、平台编译结果、零长度与投影后退化输入下回归,而不是只验证一对整齐向量。
(右脑)画面修正:小明直接写 acos(dot(a,b)),非单位输入先让点积失去余弦意义,近同向舍入到 1.0000001 又让 acos 产生 NaN;零向量案例还说明“没有方向”不能硬塞成 0°。小红先检查长度平方,退化时显式返回 false,正常时归一化并把点积夹回 [-1,1],于是模型改成“先取得合法方向,再选择阈值、无符号角或绕指定轴的有向角”。clamp 只防舍入越界,不能替代长度、空间和投影平面的前提检查。
(左脑)严格对表:先算 la2=a·a、lb2=b·b;若任一小于与长度平方同量纲的 eps2,返回无有效角。否则 an=a/√la2、bn=b/√lb2,c=clamp(an·bn,−1,1)。无符号角可取 acos(c),或用 atan2(|an×bn|,c);只判阈值 α 时直接比较 c≥cos(α)。绕参考轴 n 的有向角须先归一化 n,将 a、b 投影到 n 的正交平面并再次检查长度,再算 atan2(n·(an×bn),an·bn)。验收覆盖同向、反向、垂直、零向量、近边界、异空间防护和目标 Shader 精度;clamp 不能替代前提检查。
老师解惑
- 点积公式:
a·b = |a||b|cosθ,所以θ = acos((a·b)/(|a||b|))。 - 合法域:理论余弦在
[-1,1],浮点计算后应 clamp;但 clamp 不能替代零长度检查。 acos的代价与必要性:阈值判定比较余弦即可;只有输出角度时才需要反三角函数。- 有向角:符号必须来自参考轴与叉积的点积;单独的叉积长度无法分左右。
- 空间一致性:参与点积、叉积的向量必须处在同一坐标空间并有明确单位/归一化约定。
工具分工
- vector-angle-precision Lab:观察点积、叉积、零向量与近共线边界。
- Shader 调试颜色:把非法输入、阈值内外和左右符号分别染色。
- CPU 参考实现与单元测试:覆盖同向、反向、垂直、零长度和近边界输入。
- 目标平台抓帧:核对
half/float与编译器优化后的实际结果。
记忆钩子
先检查长度和空间,再把点积夹回合法域;只做门槛判断,就别多算一个 acos。
这是教学场景,不是目标项目的真实捕获或个人经历。
下一步:验证这条因果
学会先查空间与长度,再安全地计算夹角。
教学边界:它只验证这一条画面因果,不替代完整答案、项目经历或目标设备数据。