简述 矩阵-DirectX 原理,并详解世界坐标转屏幕坐标,附C++实现。

本文对比了DirectX与OpenGL在矩阵操作中的关键区别,包括行向量与列向量的使用、矩阵与向量相乘的顺序,以及平移、旋转和缩放变换矩阵的转换。重点介绍了变换矩阵在D3D和OpenGL中的不同形式,以及如何根据D3D坐标系实现屏幕坐标映射的平移矩阵计算。
摘要由CSDN通过智能技术生成

 

声明:以下内容摘录于不同博客。并进行了整合。文章中心段落,详细附加了原内容博客地址。

矩阵-DirectX与OpenGL的不同

矩阵是三维图形学中不可或缺的部分,几乎所有和变换相关的操作都涉及矩阵,世界变换,视图变换,投影变换,视口变换无一不需要矩阵,但是当今的两大主流图形库DirectX和OpenGL对矩阵操作却有着细微的差别,大多数的图形学书籍都以OpenGL为基础进行阐述,游戏编程类的书籍则更多使用DirectX,这就难免产生混淆,今天这篇主要讲讲两者在操作矩阵的时候有何不同。

矩阵

在三维图形学中,一般使用四维矩阵,也就是四行四列的方阵,下面是一个典型的四维矩阵

既然是三维图形学,为什么使用四维矩阵呢?主要有两个原因,第一,为了平移变换,第二,为了区别点和向量。

行向量与列向量

对于一个四维向量,它是行向量还是列向量呢?DirectX使用行向量,如下。

而OpenGL则使用列向量,如下

或者写成

矩阵与向量相乘

顶点进行几何变换的过程,从数学层面讲,就是顶点和矩阵相乘产生新顶点的过程,那么向量与矩阵相乘时顺序是怎样的呢?这取决于该向量是行向量还是列向量,我们知道两个矩阵Aij和Bxy若能相乘,则必须满足j=x才行,也就是说左边矩阵的列数要等于右边矩阵的行数,由于行向量和列向量本质上也是矩阵,也满足矩阵乘法的规律。

在DirectX中,使用行向量,所以向量和矩阵相乘的时候,向量在左,矩阵在右,如下。

而OpenGL中则使用列向量,相乘的时候矩阵在左,向量在右,如下。注意矩阵乘法中,若用^表示转置,则(AB)^=B^A^,所以下面的矩阵与DirectX中的矩阵互为转置矩阵,感谢网友ello指点。

比如对于平移变换来说,如果使用DirectX,那么m41,m42,m43分别对应三个平移分量,对应下面的Tx,Ty和Tz。

如果使用OpenGL,那么m11,m21,m31分别对应三个平移分量。

可以看出,对于同一个变换,DirectX中的矩阵和OpenGL中的矩阵互为转置矩阵。

矩阵连乘

如果有多个变换作用于一个顶点,那么可以先将所有的变换矩阵相乘,得到一个变换矩阵,最后将这个变换矩阵应用到顶点即可,这就涉及到矩阵的连乘,这时候如何安排矩阵的先后顺序呢?假设现在有三个变换,分别是平移变换,对应矩阵T,旋转变换,对应矩阵R,缩放变换,对应矩阵S,顺序是先平移,再旋转,后缩放,那么这个矩阵乘法该如何去写呢?

在DirectX中,矩阵乘法的顺序是从左到右,变换生效的先后顺序也是从左到右

而在OpenGL中,矩阵连乘的顺序是从右到左

不管是哪种方式,都是先产生作用的矩阵离顶点近(上面的T),后产生作用的矩阵离顶点远(上面的S)。

一个误区,左手系与右手系

矩阵乘法的顺序与坐标系是左手系还是右手系有关系么?根本没啥关系!

常用变换矩阵

下面给出几种常用的变换在DirectX和OpenGL中对应的矩阵,下图中左面是DirectX中的矩阵,右面是OpenGL中的矩阵。

平移变换

           

绕X轴旋转

           

绕Y轴旋转

           

绕Z轴旋转

           

缩放变换

缩放变换矩阵,两者是一致的,因为缩放变换的变换因子都在矩阵的对角线上,所以转置矩阵等于其自身。

 

上述内容来源转载于:

作者:zdd

出处:http://www.cnblogs.com/graphics/

 

以下内容来自于:本文链接:https://blog.csdn.net/guoyichao/article/details/50681009

 

在此基础上,再看 D3D坐标系统下3D世界坐标映射到2D屏幕坐标的平移矩阵

D3D中绘画3D模型基本上就是靠3个矩阵World, View, Projection来联合进行模型位置定位、视角定位及透视变形的,这与2D绘制一个图形只需要给出屏幕上的一个像素坐标就能进行定位有着非常大的不同。在某些场合,我们想根据屏幕上的像素坐标来绘制3D模型,一般可以通过用正交投影代替透视投影就能轻松进行绘制,但在一些极其特殊的情况下我们还想让绘制出的3D模型保持原先指定的3个矩阵所有变换,这就需要通过这3个矩阵及目标像素点反求出一个平移矩阵,这篇文章就是介绍该怎么做的。

先简单介绍一下在D3D里3D空间坐标在不做任何变化情况下是怎么转换成2D屏幕坐标。D3D的空间坐标在3个轴范围是[-1,1],D3D绘画到2D屏幕上的区域叫做ViewPort,ViewPort的横坐标范围是[Left, Right],纵坐标范围是[Top, Bottom],ViewPort还有近点(near)和远点(far)2个值代表z轴,则D3D空间坐标里的一点sp(x,y,z)的x坐标Xsp对应到屏幕上坐标sc(x,y)的x坐标Xsc的对应公式就是(这个详细解释网上有,这里不再赘述): 

Xsc−LeftRight−Left=Xsp−(−1)1−(−1)Xsc−LeftRight−Left=Xsp−(−1)1−(−1)


让Width = Right - Left,上面公式最后就变成: 
Xsc=(Xsp+1)∗Width2+Left 
同样得出y轴: 
Ysc=(Ysp+1)∗Height2+Top 
z轴(虽然屏幕上是没有z坐标的,但这里给出是为了后面推导的时候能统一处理): 
Zsc=Zsp∗(far−near)+near

 

接下来简单说一下齐次坐标,D3D系统里用得坐标并不只是(x,y,z),而是用4维坐标(x,y,z,w)来表示,这里齐次坐标与3维坐标的转换关系为: 

⎡⎣⎢⎢⎢xyzw⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢⎢x/wy/wz/w1⎤⎦⎥⎥⎥⎥[xyzw]=[x/wy/wz/w1]

 

也就是齐次坐标的每一项都除以w,这个过程叫做齐次坐标归一化(Normalize)也可以叫标准化,得到的坐标就是归一化坐标,当然要注意一点是当齐次坐标表示向量的时w=0,这时候则不用去除w,向量就代表了坐标,也就是当w=0时不用做任何处理。最后简单讲一下w的作用,w从绘画结果来看实质上就是一个非线性变形,透视投影就是利用了w来进行变换的。

齐次坐标是通过一个归一化3维坐标与变换矩阵相乘得到的,因为D3D是左手坐标系,是用变换矩阵左乘坐标,变换矩阵为4x4矩阵: 

⎡⎣⎢⎢⎢xyz1⎤⎦⎥⎥⎥∗⎡⎣⎢⎢⎢M11M12M13M14M21M22M23M24M31M32M33M34M41M42M43M44⎤⎦⎥⎥⎥[xyz1]∗[M11M21M31M41M12M22M32M42M13M23M33M43M14M24M34M44]


这个过程被称为Transform,结果是 
X = x * M11 + y * M21 + z * M31 + M41 
Y = x * M12 + y * M22 + z * M32 + M42 
Z = x * M13 + y * M23 + z * M33 + M43 
W = x * M14 + y * M24 + z * M34 + M44 
这个结果是等会推导要用到的。

 

接下来开始推导: 
我们已经知道空间坐标一点怎么通过变换矩阵得到齐次坐标,然后只要把齐次坐标正常化后就能知道变换后正确的空间坐标,再通过空间坐标到屏幕坐标的转换就能得到3D空间坐标映射到2D屏幕上的一点了,这个过程叫做投影(Project)。我们现在已知2D坐标一点P(x,y),可以简单通过反向刚才操作(Unproject)求出空间的一个坐标Q(x1,y1,z1),我们只要能让系统把这个Q坐标当成0坐标Z来进行绘画就大功告成了,问题是这个Q坐标的各个分量不仅是包含了平移信息也包含了旋转视角变换和投影信息,所以让0坐标Z对V的分量进行简单平移是不行的。我们假设Z通过对V(x’,y’,z’)进行平移后进行Project就能得到正确的P:

已知转换矩阵 M,设f(v)为齐次坐标转换成屏幕坐标函数 
则f( (Z + V) * M) = P 
其中Z为(x, y, z, 1), V为(x’,y’,z’,0), 
Z + V = (x + x’, y + y’, z + z’, 1) 
把这个带入之前Transform后就有: 
f( 
X’ = (x + x’) * M11 + (y + y’) * M21 + (z + z’) * M31 + M41 
Y’ = (x + x’) * M12 + (y + y’) * M22 + (z + z’) * M32 + M42 
Z’ = (x + x’) * M13 + (y + y’) * M23 + (z + z’) * M33 + M43 
W’ = (x + x’) * M14 + (y + y’) * M24 + (z + z’) * M34 + M44 
) = P 
简化后就是: 
f( 
X’ = x’ * M11 + y’ * M21 + z’ * M31 + X 
Y’ = x’ * M12 + y’ * M22 + z’ * M32 + Y 
Z’ = x’ * M13 + y’ * M23 + z’ * M33 + Z 
W’ = x’ * M14 + y’ * M24 + z’ * M34 + W 
) = P

而右边P可以表示为原先的0点坐标Z(x,y,z)通过Project操作后的平移,也就是 
P = f(Z* M) + Offset(x,y) 
化成Transform后的形式就是: 
P = f((X, Y, Z, W)) + (Ox, Oy)

这样就得到: 
f( 
X’ = x’ * M11 + y’ * M21 + z’ * M31 + X 
Y’ = x’ * M12 + y’ * M22 + z’ * M32 + Y 
Z’ = x’ * M13 + y’ * M23 + z’ * M33 + Z 
W’ = x’ * M14 + y’ * M24 + z’ * M34 + W 

= f((X, Y, Z, W)) + (Ox, Oy)

然后让我们把f这个函数展开,就是带入3d坐标映射成2d坐标的转换,当然在转换前还需要进行齐次坐标归一化: 
(X′W′+1)∗Width2+Left=(XW+1)∗Width2+Left+Ox 
(Y′W′+1)∗Height2+Top=(YW+1)∗Height2+Top+Oy 
Z′W′∗(far−near)+near=ZW∗(far−near)+near 
然后用刚才X’,Y’,Z’,W’公式右边分别替换X’,Y’,Z’,W’,整理后得到关于x’,y’,z’的方程组:

x′∗(M11−M14∗(XW+Ox∗Width2))+y′∗(M21−M24∗(XW+Ox∗Width2))+z′∗(M31−M34∗(XW+Ox∗Width2))=W∗(XW+Ox∗Width2)−X

x′∗(M12+M14∗(−YW+Oy∗Height2))+y′∗(M22+M24∗(−YW+Oy∗Height2))+z′∗(M32+M34∗(−YW+Oy∗Height2))=−W∗(−YW+Oy∗Height2)−Y

x′∗(M13−M14∗(ZW∗(far−near)+near)+y′∗(M23−M24∗(ZW∗(far−near)+near)+z′∗(M33−M34∗(ZW∗(far−near)+near)=W∗(ZW∗(far−near)+near)−Z

这是个标准的三元一次方程组,可用矩阵表示为: 

⎡⎣⎢L1L2L3M1M2M3N1N2N3⎤⎦⎥∗⎡⎣⎢x'y'z'⎤⎦⎥=⎡⎣⎢A1A2A3⎤⎦⎥[L1M1N1L2M2N2L3M3N3]∗[x′y′z′]=[A1A2A3]

 

解这个方程组只要用高斯-若尔当消元法对增广矩阵(如下)消元就可以了: 

⎡⎣⎢L1L2L3M1M2M3N1N2N3A1A2A3⎤⎦⎥[L1M1N1A1L2M2N2A2L3M3N3A3]

 

最后直接给出消元结果: 
z’ = ((A3 * L1 - A1 * N1) * (M2 * L1 - L2 * M1) - (A2 * L1 - A1 * M1) * (N2 * L1 - L2 * N1)) / ((N3 * L1 - L3 * N1) * (M2 * L1 - L2 * M1) - (M3 * L1 - L3 * M1) * (N2 * L1 - L2 * N1)) 
y’ = ((A2 * L1 - A1 * M1) - (M3 * L1 - L3 * M1) * z) / (M2 * L1 - L2 * M1) 
x’ = A1 / L1 - z * L3 / L1 - y * L2 / L1

这就是3x3矩阵的解,特别的当z=0,就得到2x2矩阵解为: 
y’= (A2 * L1 - A1 * M1) / (M2 * L1 - L2 * M1) 
x’= (A1 / L1 - y * L2 / L1)

至此终于求出了V(x’,y’,z’),我们只要对Z点平移V后就能正确Project到P点。

把以上过程转为编码(使用了SharpDX):


//pos为期望要绘画的屏幕坐标 
private Matrix Get2DTranslationMatrix(ViewportF viewPort, Vector2 pos, Matrix world, Matrix view, Matrix projection) { 
//先取空间0点坐标对应的屏幕坐标 
var screenZ = viewPort.Project(Vector3.Zero, projection, view, world); 

//相对于viewPort左上角求出期望坐标与0点坐标在屏幕坐标系的偏移值O(x,y) 
var diff = new Vector3(pos.X + viewPort.X, pos.Y + viewPort.Y, 0) - screenZ; 

//由于D3D是通过3个矩阵进行变换,而我推导只需要1个,所以就把3个矩阵相乘合成一个变换矩阵进行计算 
var projM = world * view * projection; 

//求出转换后的齐次坐标,并确保w不为0 
var transV = Vector3.Transform(Vector3.Zero, projM); 
if (MathUtil.IsZero(transV.W)) { transV.W = 1.0f; } 
var w = viewPort.Width; v
ar h = viewPort.Height; 

/* C1,C2在消元时已消去 C3,C4也没必要,理由见下 */ 
var C1 = viewPort.X; 
var C2 = viewPort.Y; 
var C3 = viewPort.MaxDepth; 
var C4 = viewPort.MinDepth; 
//设定增广矩阵所有系数 
var L1 = projM.M11 - projM.M14 * (transV.X / transV.W + diff.X * 2 / w); 
var L2 = projM.M21 - projM.M24 * (transV.X / transV.W + diff.X * 2 / w); 
var L3 = projM.M31 - projM.M34 * (transV.X / transV.W + diff.X * 2 / w); 

var M1 = projM.M12 + projM.M14 * (-transV.Y / transV.W + diff.Y * 2 / h); 
var M2 = projM.M22 + projM.M24 * (-transV.Y / transV.W + diff.Y * 2 / h); 
var M3 = projM.M32 + projM.M34 * (-transV.Y / transV.W + diff.Y * 2 / h); 

/* z系数应该是 near=0,far=1但一般情况下viewport始终是在0点创建所以,可以简化 */ 
var N1 = projM.M13 - projM.M14 * (transV.Z / transV.W * (C3 - C4) + C4); 
var N2 = projM.M23 - projM.M24 * (transV.Z / transV.W * (C3 - C4) + C4); 
var N3 = projM.M33 - projM.M34 * (transV.Z / transV.W * (C3 - C4) + C4); 

var N1 = projM.M13 - projM.M14 * (transV.Z / transV.W); 
var N2 = projM.M23 - projM.M24 * (transV.Z / transV.W); 
var N3 = projM.M33 - projM.M34 * (transV.Z / transV.W); 

var A1 = transV.W * (transV.X / transV.W + diff.X * 2 / w) - transV.X; 
var A2 = -transV.W * (-transV.Y / transV.W + diff.Y * 2 / h) - transV.Y; 
var A3 = transV.W * (transV.Z / transV.W * (C3 - C4) + C4) - transV.Z;

/* 由于near=0,far=1,A3可以简化为0
可以不考虑z坐标,直接用2x2消元结果代替这样上面的系数L3,M3,N1,N2,N3,A3都不用计算,下面返回的时候z可以取0  
var A3 = 0; 
var y1 = (A2 * L1 - A1 * M1) / (M2 * L1 - L2 * M1); 
*/

var z = ((A3 * L1 - A1 * N1) * (M2 * L1 - L2 * M1) - (A2 * L1 - A1 * M1) * (N2 * L1 - L2 * N1)) / ((N3 * L1 - L3 * N1) * (M2 * L1 - L2 * M1) - (M3 * L1 - L3 * M1) * (N2 * L1 - L2 * N1)); 
var y = ((A2 * L1 - A1 * M1) - (M3 * L1 - L3 * M1) * z) / (M2 * L1 - L2 * M1); 
var x = A1 / L1 - z * L3 / L1 - y * L2 / L1; 

//返回平移矩阵 
return Matrix.Translation(x, y, z); 
} 

最后使用的时候把world左乘平移矩阵得到一个新world就行了: 
world = Get2DTranslationMatrix(…) * world

至此完成了简单封装


摄像头实时采集画面后在VR眼镜(oculus rift)里播放,就是在D3D里做一个二维贴图,现在又对贴图进行了图形分析,在图像内匹配到的图形(利用openCV)上画出一个3D模型(也就是AR),而VR设备有着自己特殊的变换矩阵,所以通过这个可以轻松把模型画到侦测到的图形标记上,换句话说就是在没有AR库的辅助变换下完成了AR效果。

对于C++取整,可以使用math库中的round函数进行四舍五入取整,floor函数进行向下取整,ceil函数进行向上取整。 对于MPIP RawRaw图,需要首先了解MPIP Raw格式的数据结构,然后使用C语言的文件读写操作读取MPIP Raw格式的文件,再将读取到的数据换为Raw图像格式的数据结构,最后再写入到Raw图像文件中。 具体实现过程如下: 1. 读取MPIP Raw文件 ```c FILE *fp = fopen("input.mpip", "rb"); if (fp == NULL) { printf("Failed to open file."); return -1; } // 读取MPIP Raw文件头信息 struct MPIPHeader { uint32_t width; uint32_t height; uint32_t bits_per_pixel; uint32_t compression; uint32_t data_size; }; MPIPHeader header; fread(&header, sizeof(header), 1, fp); // 读取MPIP Raw数据 uint8_t *buffer = malloc(header.data_size); fread(buffer, header.data_size, 1, fp); fclose(fp); ``` 2. 将MPIP Raw数据换为Raw图像数据结构 ```c // 计算Raw图像数据大小 size_t raw_size = header.width * header.height * (header.bits_per_pixel / 8); // 分配Raw图像数据内存 uint8_t *raw_data = malloc(raw_size); // 将MPIP Raw数据解压缩,换为Raw图像数据 if (header.compression == 0) { memcpy(raw_data, buffer, raw_size); } else if (header.compression == 1) { uint8_t *src = buffer; uint8_t *dst = raw_data; for (uint32_t row = 0; row < header.height; row++) { memcpy(dst, src, header.width * (header.bits_per_pixel / 8)); src += header.width * (header.bits_per_pixel / 8); dst += header.width * (header.bits_per_pixel / 8); } } free(buffer); ``` 3. 写入Raw图像文件 ```c FILE *fp = fopen("output.raw", "wb"); if (fp == NULL) { printf("Failed to open file."); return -1; } // 写入Raw图像数据 fwrite(raw_data, raw_size, 1, fp); fclose(fp); free(raw_data); ``` 以上就是C语言实现MPIP RawRaw图的简要流程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值