fix(camera): 视图矩阵俯仰符号翻转——视角不跟随鼠标上下移动的根因
视图矩阵误用正向旋转 R 代替 Rᵀ(RotX(-pitch)·RotY(-yaw)), 低头渲染成抬头、抬头渲染成低头;yaw 因 RotY 对称性侥幸正确。 新增 TestViewPitchSteep/Up/Forward 验证陡俯仰与方向映射。
This commit is contained in:
@@ -32,27 +32,27 @@ func (c *Camera) Right() [3]float64 {
|
||||
}
|
||||
|
||||
// View 视图矩阵(列主序 4×4,GL 用)。
|
||||
// 推导:相机旋转 R_cam = RotY(yaw)·RotX(pitch);视图 = R_camᵀ·T(-pos)。
|
||||
// 旧实现矩阵布局错误导致非零视角下画面全灰(白屏/灰屏根因之一)。
|
||||
// 推导:相机旋转 R = RotY(yaw)·RotX(pitch)(世界→相机为 Rᵀ = RotX(-pitch)·RotY(-yaw))。
|
||||
// 视图 = Rᵀ·T(-pos)。
|
||||
// 旧实现误用正向旋转 R:俯仰角符号被翻转(低头渲染成抬头),
|
||||
// 表现是「视角不跟随鼠标上下移动」(上下视角失效根因)。
|
||||
func (c *Camera) View() [16]float32 {
|
||||
cp, sp := math.Cos(c.Pitch), math.Sin(c.Pitch)
|
||||
cy, sy := math.Cos(c.Yaw), math.Sin(c.Yaw)
|
||||
|
||||
// V = RotX(-pitch)·RotY(-yaw),按列主序存储(列 0/1/2 为相机基在世界的分量)
|
||||
var m [16]float32
|
||||
// 列 0
|
||||
m[0] = float32(cy)
|
||||
m[1] = 0
|
||||
m[2] = float32(sy)
|
||||
// 列 1
|
||||
m[4] = float32(-sy * sp)
|
||||
m[5] = float32(cp)
|
||||
m[6] = float32(cy * sp)
|
||||
// 列 2
|
||||
m[8] = float32(-sy * cp)
|
||||
m[9] = float32(-sp)
|
||||
m[10] = float32(cy * cp)
|
||||
m[0] = float32(cy) // col0.x
|
||||
m[1] = float32(sp * sy) // col0.y
|
||||
m[2] = float32(cp * sy) // col0.z
|
||||
m[4] = 0 // col1.x
|
||||
m[5] = float32(cp) // col1.y
|
||||
m[6] = float32(-sp) // col1.z
|
||||
m[8] = float32(-sy) // col2.x
|
||||
m[9] = float32(sp * cy) // col2.y
|
||||
m[10] = float32(cp*cy) // col2.z
|
||||
m[15] = 1
|
||||
// 平移:t = -R·pos
|
||||
// 平移:t = -V·pos
|
||||
px, py, pz := float32(c.Pos[0]), float32(c.Pos[1]), float32(c.Pos[2])
|
||||
m[12] = -(m[0]*px + m[4]*py + m[8]*pz)
|
||||
m[13] = -(m[1]*px + m[5]*py + m[9]*pz)
|
||||
|
||||
77
internal/camera/pitch_test.go
Normal file
77
internal/camera/pitch_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user