package camera import ( "math" "testing" ) // apply 用视图矩阵变换世界点(列主序语义,与 GL 一致)。 func apply(m *[16]float32, wx, wy, wz float64) (vx, vy, vz float64) { vx = float64(m[0])*wx + float64(m[4])*wy + float64(m[8])*wz + float64(m[12]) vy = float64(m[1])*wx + float64(m[5])*wy + float64(m[9])*wz + float64(m[13]) vz = float64(m[2])*wx + float64(m[6])*wy + float64(m[10])*wz + float64(m[14]) return } // TestViewPitchSteep 验证陡俯仰角(低头 60°)下: // 相机正下方地面 → 相机前方(z<0)且屏幕下方(y<0); // 相机前方地面 → 相机前方;远处地面 → 相机前方且低于视轴下方。 // 旧实现俯仰符号翻转:正下方/正前方地面点被映射到相机背后(z>0), // 低头时画面只剩天空——「视角不跟随鼠标」的根因。 func TestViewPitchSteep(t *testing.T) { c := New([3]float64{8.5, 66.66, 8.5}, 0, -60*math.Pi/180, 70) m := c.View() cases := []struct { name string wx, wy, wz float64 wantBelow bool // 期望屏幕中心下方 }{ {"below", 8.5, 64.0, 8.5, true}, // 正下方地面 {"ahead", 8.5, 64.0, 5.0, false}, // 前方地面 {"far-ahead", 8.5, 64.0, 0.0, false}, // 远处地面 } for _, tc := range cases { vx, vy, vz := apply(&m, tc.wx, tc.wy, tc.wz) t.Logf("%s: view=(%.2f,%.2f,%.2f)", tc.name, vx, vy, vz) if vz >= 0 { t.Errorf("%s: 地面点应在相机前方 (z<0),得到 z=%.2f", tc.name, vz) } if tc.wantBelow && vy >= 0 { t.Errorf("%s: 正下方地面应在屏幕下方 (y<0),得到 y=%.2f", tc.name, vy) } } } // TestViewPitchUp 抬头 60°:远处地面应仍在相机前方;正下方(与视线夹角 150°)应在相机后方。 func TestViewPitchUp(t *testing.T) { c := New([3]float64{8.5, 66.66, 8.5}, 0, 60*math.Pi/180, 70) m := c.View() // 远处地面点(平视前方远处,y 比眼睛低) _, _, vz := apply(&m, 8.5, 66.0, -50) if vz >= 0 { t.Errorf("抬头时远处地面应在相机前方 (z<0),得到 z=%.2f", vz) } // 正下方点:与视线方向夹角 150°>90°,应在相机后方(z>0) _, _, vz = apply(&m, 8.5, 50, 8.5) if vz <= 0 { t.Errorf("抬头时正下方应在相机后方 (z>0),得到 z=%.2f", vz) } } // TestViewForward 相机前方向(Forward() 与世界原点前方一点)应映射到视图空间 -z 且 x≈0。 func TestViewForward(t *testing.T) { for _, tc := range []struct{ yaw, pitch float64 }{ {0, 0}, {0, -0.6}, {0.9, 0.3}, {-2.1, 0.77}, } { c := New([3]float64{1, 2, 3}, tc.yaw, tc.pitch, 70) f := c.Forward() m := c.View() vx, vy, vz := apply(&m, 1+f[0]*5, 2+f[1]*5, 3+f[2]*5) if vz >= 0 { t.Errorf("yaw=%.2f pitch=%.2f: 前方点应在相机前方,得到 z=%.2f", tc.yaw, tc.pitch, vz) } if math.Abs(vx) > 1e-3 || math.Abs(vy) > 1e-3 { t.Errorf("yaw=%.2f pitch=%.2f: 前方点应位于视轴中心,得到 (%.3f,%.3f)", tc.yaw, tc.pitch, vx, vy) } } }