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)

让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 ? ? ? ? ? =? ? ? ? ? ? ? ? ? ? xw yw zw 1 ? ? ? ? ? ? ? ? ? ?

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

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

[x y z 1 ]?? ? ? ? ? M11M21M31M41 M12M22M32M42 M13M23M33M43 M14M24M34M44 ? ? ? ? ?

这个过程被称为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+OyZ ′ 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 ))x ′ ?(M12+M14?(?YW +Oy?Height2 ))+y ′ ?(M22+M24?(?YW +Oy?Height2 ))+z ′ ?(M32+M34?(?YW +Oy?Height2 ))x ′ ?(M13?M14?(ZW ?(far?near)+near)+y ′ ?(M23?M24?(ZW ?(far?near)+near)+z ′ ?(M33?M34?(ZW ?(far?near)+near) =W?(XW +Ox?Width2 )?X=?W?(?YW +Oy?Height2 )?Y=W?(ZW ?(far?near)+near)?Z

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

? ? ? L1M1N1 L2M2N2 L3M3N3 ? ? ?  ? ? ? ? x ′ y ′ z ′  ? ? ?  = ? ? ? A1A2A3 ? ? ?

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

? ? ? ? L1M1N1 L2M2N2 L3M3N3 A1A2A3  ? ? ? ?

最后直接给出消元结果:

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;
            var 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系数应该是
           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);
            但一般情况下viewport始终是在0点创建所以near=0,far=1,可以简化
           */
            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
            */
            var A3 = 0;

            /*
            可以不考虑z坐标,直接用2x2消元结果代替
            var y1 = (A2 * L1 - A1 * M1) / (M2 * L1 - L2 * M1);
            var x1 = (A1 / L1 - y * L2 / L1);
            这样上面的系数L3,M3,N1,N2,N3,A3都不用计算,下面返回的时候z可以取0
            */
            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效果。

时间: 2024-10-29 00:25:45

D3D坐标系统下3D世界坐标映射到2D屏幕坐标的平移矩阵的相关文章

NGUI研究之3D模型坐标转2D屏幕坐标-血条

?? 刚好今天有朋友问我,比較典型的样例就是游戏里面人物的血条. 原理非常easy就是把3D点换算成2D的点.可是因为NGUI自身是3D所以我们须要先把NGUI下的点转成2D点.然后在把他转成3D的点. 听起来有点绕,不要紧我直接上代码. 对屏幕自适应不明确的看NGUI研究之怎样自适应屏幕 眼下我一直都是用NGUI来做人物血条,可是2D血条都会有个限制.就是它不能和模型有遮挡关系.只是血条能够依据人物的位置调节.比方远一点的人物血条会小一些,近一点的人物血条会大一些. 最好让美术做FBX的时候直

第1部分: 游戏引擎介绍, 渲染和构造3D世界

原文作者:Jake Simpson译者: 向海Email:[email protected] ------------------------------------------------------------第1部分: 游戏引擎介绍, 渲染和构造3D世界 介绍 自Doom游戏时代以来我们已经走了很远. DOOM不只是一款伟大的游戏,它同时也开创了一种新的游戏编程模式: 游戏 "引擎". 这种模块化,可伸缩和扩展的设计观念可以让游戏玩家和程序设计者深入到游戏核心,用新的模型,场景和

NeHe OpenGL教程 第十课:3D世界

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第十课:3D世界 加载3D世界,并在其中漫游: 在这一课中,你将学会如何加载3D世界,并在3D世界中漫游.这一课使用第一课的代码,当然在课程说明中我只介绍改变了代码. 这一课是由Lionel Brits (βtelgeuse)所写的

第10课 OpenGL 3D世界

加载3D世界,并在其中漫游: 在这一课中,你将学会如何加载3D世界,并在3D世界中漫游.这一课使用第一课的代码,当然在课程说明中我只介绍改变了代码. 这一课是由Lionel Brits (βtelgeuse)所写的.在本课中我们只对增加的代码做解释.当然只添加课程中所写的代码,程序是不会运行的.如果您有兴趣知道下面的每一行代码是如何运行的话,请下载完整的源码,并在浏览这一课的同时,对源码进行跟踪.好了现在欢迎来到名不见经传的第十课.到现在为止,您应该有能力创建一个旋转的立方体或一群星星了,对3D

【Stage3D学习笔记续】真正的3D世界(一):透视矩阵

如果各位看官跟着我的学习笔记一路看过来的话,一定会吐槽我的,这都是什么3D啊?从头到尾整个都是在使用GPU绘制一堆2D图像而已,的确,之前我们一直使用正交矩阵利用GPU加速来实现2D世界的展示,算不上真3D,但是正是由于有了之前的学习我们实现真3D世界的学习才会更加轻松,下面的笔记就让我们真正的进入3D世界吧! 补充一下,我们的这部分学习笔记是基于<Adobe Flash 11 Stage3D(Molehill) 游戏编程初学者指南>一书的学习而来的. 赶快进入我们的主题,是否还记得之前的2D

【Stage3D学习笔记续】真正的3D世界(二):显示模型

虽然我们进入真3D世界了,但是上一章的Demo仍然是显示的一个平面,尽管我们的平面在3D空间中旋转可以看出一点3D透视的效果,但是既然是真3D,就要拿出点3D的样子来! 如果要显示3D模型,我们就要告别直接使用代码编写顶点数据的时代了,想想上一节,平面的四边形手写顶点或许还过得去,但是如果手写一个立方体的8个顶点数据就有点难了吧,如果是显示更加复杂的形状呢? 要显示一个3D模型,就需要一个特定格式的3D模型文件,3D模型文件一般使用3D建模工具创建,美术完成建模后会将模型导出为特定格式的3D模型

使用ivx的3D世界实现跑马灯效果的经验总结

之前的案例涉及的动画效果都是平面展示,但是ivx中也可以通过3D世界组件展示3D的效果.今天我们就以跑马灯为例来讲一下ivx中的3D世界是如何使用的.一.3D世界3D世界最基础的组成部分就是坐标系和摄像机.坐标系是一个空间直角坐标系,3D世界下的所有组件都会有一个XYZ坐标来决定它在3D世界中的位置,而摄像机负责控制我们的视角,下图中红圈处就是摄像机的位置,黄线框起来的区域就是我们的视角范围.另外我们还可以在3D世界中添加各种光源,字体,图片,图片序列和物体模型这些具有展示效果的组件,除此之外还

win7 64下暗黑世界V1.1 服务器端及客户端的安装及运行 成功

原地址:http://bbs.gameres.com/thread_223717.html 屌丝一枚,没有MAC 更没有XCODE 潜心整了星期六与星期天两天才安装运行成功...只能说安装太复杂了,,,新手入门不易...总结如下: 一.win7 64位服务端的安装与运行 1. 安装python2.7      E:\software\other\python-2.7.5.amd64.msi2.安装mySQL     E:\software\DB\mysql-5.5.15-winx64.msi3.

【Stage3D学习笔记续】真正的3D世界(六):空间大战

这就是书上的最终效果了,一个完整的空间大战游戏: 点击查看源码 这里并没有太多的新知识,所涉及的东西更多的是游戏开发方面的优化和技巧,下面我们大家一起来看看: 飞船: 类似粒子效果中的粒子创建方法,我们的游戏飞船也需要一样的创建机制进行创建,大家可以点击查看源码: GameActor GameActorPool 需要额外说明的是,添加了进行碰撞检测的机制,代码里写了基于球形的距离的碰撞检测(简单高效)和基于AABB盒的碰撞检测,但AABB盒的碰撞检测在实际的游戏中并没有使用. 另外添加了一点优化