3D 矩阵和向量空间

基础转换矩阵:3D 基础矩阵推导 矩阵的变换是线性代数的基础,也是图形相关的技术的基础,如果做好图形渲染,仅仅了解这些基础是远远不够的。还需要比较清楚的是3D中的6次向量空间的变换的几何意义,以及变换矩阵的推导。

 

向量空间转换和顶点坐标转换的区别:其实没区别都是把顶点从一个坐标空间转换到另一个。只不过模型的转换只针对模型本身,例如模型转换到世界空间,你可以说它是从模型空间转换到世界空间,也可以说它就是模型在世界空间里面的转换,因为世界中的所有模型转换的规则并不全都一样。但是,模型从世界空间转换到视图空间就是空间坐标系的转换了,因为空间里面的所有顶点的转换规则都一样。

 

视点变换矩阵:

世界空间(World Space)转换到视图空间(Eye Space)的矩阵

 

// Lookat 表示是view转换矩阵 LH 是左手系 
// pEye 摄像机的坐标位置  pAt 前方,这也算是一个方向向量了 
// pUp 上方也是一个方向向量
D3DXMATRIX* WINAPI D3DXMatrixLookAtLH ( D3DXMATRIX *pOut, CONST D3DXVECTOR3 *pEye, CONST D3DXVECTOR3 *pAt, CONST D3DXVECTOR3 *pUp );

方法体的实现:
zaxis = normal(At - Eye)  
xaxis = normal(cross(Up, zaxis))
yaxis = cross(zaxis, xaxis)
    
 xaxis.x           yaxis.x           zaxis.x          0
 xaxis.y           yaxis.y           zaxis.y          0
 xaxis.z           yaxis.z           zaxis.z          0
-dot(xaxis, eye)  -dot(yaxis, eye)  -dot(zaxis, eye)  1

 1.pEye是视点的位置,也就是摄像机的位置,pAt是一个视角方向上的一个点,而不是描述视角方向的方向向量,这是因为

 

 zaxis = normal(At - Eye) 方法向量是通过 At - Eye 计算出来的,而不是直接为at。

2.蛋疼的是Up又是一个方向向量,因为xaxis = normal(cross(Up, zaxis)),两个向量的叉乘能够得到他们的法向量。这里利用的是up和zaxis 组成的是一个垂直的平面,而它的法向量肯定是指向xaxis x轴了。

3.cross(zaxis, xaxis)根据已知的两个轴求出第三个轴来,我这里有个疑问up难道不跟yaxis 相等吗?或许这里强调的是一种更灵活的操作,就是up它只是在yz平面上,但并不要求它与zaxis 垂直。

4.我们知道3x3矩阵只是对顶点做了相应的旋转和缩放,而-dot(xaxis, eye) -dot(yaxis, eye) -dot(zaxis, eye)则是对位置的移动,保证世界坐标的原点移动到视图坐标的原点,也就是相机的位置。

5.视点变换矩阵的获取:

 

void CameraControl::updateView()
{
	float3 up, position, lookAt;
	float yaw, pitch, roll;
	D3DXMATRIX rotationMatrix;

	position = mBean.position;
	lookAt = mBean.lookAt;
	up = mBean.up;

	pitch = mBean.angleX;
	yaw   = mBean.angleY;
	roll  = 0;
        
        // 1.根据旋转因子,获取旋转矩阵 angleX,angleY,这里不以z为轴进行旋转
	D3DXMatrixRotationYawPitchRoll(&rotationMatrix, yaw, pitch, roll);
        // 2.这里对两个方向向量进行旋转
	D3DXVec3TransformCoord(&lookAt, &lookAt, &rotationMatrix);
	D3DXVec3TransformCoord(&up, &up, &rotationMatrix);
        // 3.这个完全是因为LookAtLH的参数不是方向向量而是一个点
	lookAt = position + lookAt; 

	// Finally create the view matrix from the three updated vectors.
	D3DXMatrixLookAtLH(&mView, &position, &lookAt, &up);
}

 

透视投影矩阵的推导:

投影空间:在投影空间中,视图空间的视锥变成为一个长方体,首先这里视图空间本身是一个与世界空间一样的大空间,它也不是被截成了一个视锥。只不过转换到投影空间的,只是视图空间里的视锥部分的内容。视图空间到投影空间的转换,主要是把透视空间椎体里面的内容转换到投影空间,投影空间本身是一个矩形的。

矩形的左裁剪面为x = -1,右为x = 1,上为 y = 1,下为 y = -1,近裁剪面为 z = 0 (这里是0不是-1,这个主要是对应深度模板来的),远裁剪面为 z = 1。


从这个图可以看出P是视锥里面的一个点,而P1是映射到近裁剪面的一个点,最终P在透视空间里面的结果Pt为(P1.x,P1.y,P.z)当然还要对Pt进行缩放,使其比例为上面说道的(1,1,0),(-1,-1,-1)这个矩阵里面。

如图,现在已知Py,Pz和近裁剪面zn求P1y。 P1y/zn = Py/Pz,所以最终结果为:


又因为

 所以



 同样的可以求出



 继续求出P1z来


然后把这些式子转换成矩阵:



 视图空间转换到透视空间,做了两件事,一个是等比例缩放,把视锥缩放到一个矩形里面,然后就是调整比例,把比例调整到一个(1,1,0)(-1,-1,-1)里面。

需要注意的是:此转换没有对结果进行归一化。

// fovy 投影角度  Aspect 宽和高的比率 zn 近截面的z值 zf 远截面的z值
D3DXMATRIX* WINAPI D3DXMatrixPerspectiveFovLH ( D3DXMATRIX *pOut, FLOAT fovy, FLOAT Aspect, FLOAT zn, FLOAT zf );

屏幕空间:

从透视空间转换到屏幕空间的这个过程,是在光栅化的时候进行的,所以,你在PS里面获取到的float4 piPosition : SV_POSITION;这个顶点位置信息已经不是透视空间里的了。所以,如果想获取透视空间的position请不要使用SV_POSITION这个语义来进行描述。

 

总结:

1.在所有需要变换的内容当中,有顶点和向量之分,对于顶点来说位置的移动是有意义的,而对于向量来说,位置的移动反而会改变向量的方向。原则上来说,位置使用4x4矩阵进行转换,而向量用3x3的矩阵进行转换,因为第四行和第四列是用于平移的。

 

2.摄像机位置和方向,光源的位置和方向,是不需要进行世界矩阵变换的,因为他们本身就是身处世界空间之中。这个如果不清楚,很容易在VS里面乘一个世界转换矩阵。当然,也有特殊情况,就是你的模型够复杂,并且里面有相对模型不变的光源,这个时候光源的位置是模型空间的,那么在转换的时候是需要进行世界坐标转换的。从这里可以看出,事无绝对,最根本的还是得清楚你在干什么。

 

3.原则上来说,对于一些类似光照计算,或者其他的会用到顶点位置信息或光源,摄像机的信息的计算。都应该在世界空间,或者视角空间里面进行处理。因为这个时候,顶点之间的相对位置没有异变,放在透视空间或者屏幕空间会使结果不可控。

 

4.所有D3DX的工具方法,都是这样一个原则:1.左手系,2.顺时针为正,3.向量*矩阵。它对应了最终描述的结果。

5.在使用基础矩阵进行转换的时候:先平移,再旋转,后缩放。

6.每次转换过方向向量后,都有必要对其进行归一化。

7.通过D3DX提供的方法获取的世界矩阵,视图矩阵,透视矩阵,在shader里面转换顶点前都需要进行转置,这是我最费解的地方。不是说好了,左手系,向量*矩阵的么,为啥用的时候还需要进行一次转置呢?先这样做吧 - -。

 

 

 

 

网上的一段文字,难道方向向量,非等比例缩放也不行吗?

之前看过一些文章,说道mesh顶点的空间变换,从本地坐标系变换到世界坐标系,位置信息是乘以世界矩阵,而点的法向量要变换到世界空间下,是要乘以世界矩阵的逆矩阵的转置矩阵。网上有关于这样做的证明。只是之前在项目中,一开始我并不知道要这样做,都是乘以世界矩阵,但是用于光照方程,或者说计算球形贴图坐标的时候,也并没有出错,最后的效果都是满意的,疑惑了很久。

最近翻看《实时计算机图形学》一说,才明白其中原因。因为项目中我用到的世界矩阵中,大部分都是只包括了平移信息和旋转信息,没有去做scale的变换,都是1:1的比例在绘制。问题就在这里,因为法向量是一个方向向量,不是矢量,在变换的过程中,把法向量变为齐次坐标的时候,W分量要设置为0,也就是忽略了平移变换。所以这个世界矩阵就可以看成是一个3*3的旋转举证,而所有的旋转矩阵都是正交矩阵。正交矩阵有一个特性,就是正交举证的逆矩阵等于他的转置矩阵。所以正交矩阵的逆矩阵的转置矩阵就是正交矩阵的转置矩阵的转置矩阵,也就是正交矩阵自己(上面一段话很绕口啊)!

所以,这时候的世界矩阵的逆矩阵的转置矩阵就是世界矩阵,所以我得到了想要的结果。如果出现了scale信息,且各个轴不是1:1的比例的话。这时候的世界矩阵就不能算是正交的,就会出现问题。

在 **Panda3D** 中进行 3D 空间变换时,正确使用 **矩阵(Matrix)** **向量(Vector/Point)** 是实现相机跟随、物体旋转、路径计算等高级功能的基础。以下是全面的指南。 --- ### ✅ 一、核心概念:向量矩阵类型 | 类型 | 含义 | 示例 | |------|------|------| | `LVector3` / `LVector3f` | 方向或偏移(无位置) | `(0, -30, 15)` 相机偏移 | | `LPoint3` / `LPoint3f` | 空间中的一个点(有位置) | 飞机坐标 `(100, 200, -50)` | | `Mat3` / `LMatrix3f` | 3×3 矩阵,用于旋转变换(无平移) | 旋转方向向量 | | `Mat4` / `LMatrix4f` | 4×4 矩阵,包含旋转 + 平移 + 缩放 | 完整模型变换 | > ⚠️ 注意:`LVector3` 表示方向,不能直接参与“平移”;`LPoint3` 才能表示位置。 --- ### ✅ 二、常见操作及正确方法 #### 1. ✅ 旋转向量(如相机偏移) ```python from panda3d.core import * # 原始偏移向量(后方上方) offset = LVector3(0, -30, 15) # 创建绕 Z 轴转 90 度的矩阵(heading) rot_mat = Mat3.rotateMat(90, LVector3(0, 0, 1)) # ✅ 正确方式:使用 xform() rotated_offset = rot_mat.xform(offset) print(rotated_offset) # 输出类似 (30, 0, 15) ``` > ❌ 错误写法:`offset * rot_mat` → 报错! --- #### 2. ✅ 对点应用完整变换(含平移) ```python point = LPoint3(10, 20, 30) transform = Mat4.translateMat(5, 5, 5) * Mat4.rotateMat(45, LVector3(0, 0, 1)) new_point = transform.xform_point(point) ``` - 使用 `xform_point()` 处理带位置的点。 - 支持平移仿射变换。 --- #### 3. ✅ 只对方向应用旋转(忽略平移) ```python direction = LVector3(1, 0, 0) rotation_only_mat = Mat4.rotateMat(45, LVector3(0, 0, 1)) new_dir = rotation_only_mat.xform_vec(direction) ``` - 使用 `xform_vec()` 保证只应用旋转缩放,不加平移。 --- ### ✅ 三、构建常用变换矩阵 | 操作 | 方法 | |------|------| | 绕某轴旋转 | `Mat3.rotateMat(angle, axis)` 或 `Mat4.rotateMat(...)` | | 平移 | `Mat4.translateMat(x, y, z)` | | 缩放 | `Mat4.scaleMat(s)` 或 `scaleMat(sx, sy, sz)` | | 复合变换 | `matA * matB`(注意顺序:先右后左) | #### 示例:先旋转再平移 ```python rotate = Mat4.rotateMat(45, LVector3(0, 0, 1)) translate = Mat4.translateMat(10, 0, 0) transform = translate * rotate # 先旋转,再平移 ``` > 🔁 顺序很重要!`A * B` 表示先应用 `B`,再应用 `A` --- ### ✅ 四、实战应用:让相机随飞机姿态旋转 ```python def update_camera(self): h, p, r = self.player.hpr # heading, pitch, roll(度) # 构建旋转矩阵:先 heading(Z),再 pitch(X) heading_rot = Mat3.rotateMat(h, LVector3(0, 0, 1)) pitch_rot = Mat3.rotateMat(p, LVector3(1, 0, 0)) total_rot = heading_rot * pitch_rot # 应用到原始偏移向量 camera_offset_world = total_rot.xform(self.camera_follow_offset) # 设置相机位置 cam_pos = self.player.pos + camera_offset_world base.camera.setPos(cam_pos) base.camera.lookAt(self.player.node) ``` ✅ 这是解决你之前错误的核心方案。 --- ### ✅ 五、推荐最佳实践 | 场景 | 推荐方法 | |------|----------| | 旋转向量(如偏移、速度) | `Mat3.xform(vector)` | | 变换空间点(如目标位置) | `Mat4.xform_point(point)` | | 变换方向(不带位移) | `Mat4.xform_vec(vector)` | | 组合多个变换 | 使用 `*` 连乘,注意顺序 | | 获取节点当前变换 | `node.getMat(render)` | | 将局部坐标转世界坐标 | `node.getNetTransform().getMat().xform_point(local_pos)` | --- ### ✅ 六、调试技巧 打印向量更清晰: ```python print(f"Pos: {pos}, HPR: {hpr}") print(f"Offset: {self.camera_follow_offset:.2f}") ``` 可视化辅助: ```python # 显示坐标轴(调试用) debug_axis = loader.loadModel("zup-axis") debug_axis.reparentTo(render) debug_axis.setScale(5) debug_axis.setPos(self.player.pos) ``` --- ### ✅ 总结:关键规则 | ✅ 正确做法 | ❌ 错误做法 | |-----------|------------| | `mat.xform(vec)` | `vec * mat` | | `mat4.xform_point(p)` | `p * mat4` | | 使用 `LVector3` 表示方向 | 用它做位置加减 | | 使用 `Mat4` 做完整变换 | 用 `Mat3` 加平移 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值