使用相机将基本渲染的三维透视投影到二维屏幕上(不使用OpenGL)。

27

假设我有以下数据结构:

Camera {
   double x, y, z

   /** ideally the camera angle is positioned to aim at the 0,0,0 point */
   double angleX, angleY, angleZ;
}

SomePointIn3DSpace {
   double x, y, z
}

ScreenData {
   /** Convert from some point 3d space to 2d space, end up with x, y */
   int x_screenPositionOfPt, y_screenPositionOfPt

   double zFar = 100;

   int width=640, height=480
}

如何在没有屏幕截图或其他东西的情况下,计算给定空间中某个三维点的屏幕 x、y 位置。我想将该三维点投影到二维屏幕上。

Camera.x = 0
Camera.y = 10;
Camera.z = -10;


/** ideally, I want the camera to point at the ground at 3d space 0,0,0 */
Camera.angleX = ???;
Camera.angleY = ????
Camera.angleZ = ????;

SomePointIn3DSpace.x = 5;
SomePointIn3DSpace.y = 5;
SomePointIn3DSpace.z = 5;

ScreenData.x和y是空间中3D点的屏幕X位置。我该如何计算这些值?

我可能可以使用在此处找到的公式,但我不理解屏幕宽度/高度如何发挥作用。此外,我不明白维基百科条目中观察者位置相对于相机位置是什么意思。

http://en.wikipedia.org/wiki/3D_projection

8个回答

54
“完成的方式”是使用齐次变换和坐标。您可以采用以下步骤来处理空间中的点:
- 使用模型矩阵将其相对于相机定位。 - 使用投影矩阵将其正交或透视投影。 - 应用视口变换将其放置在屏幕上。
这有点模糊,但我会尝试涵盖重要部分并留下一些给您。我假设您了解矩阵数学的基础知识 :)。 齐次向量、点、变换 在三维空间中,齐次点是形如[x,y,z,1]的列矩阵。最后一个分量是“w”,一个缩放因子,对于向量而言是0:这样可以使向量不能平移,这在数学上是正确的。我们不会讨论这个问题,我们只谈论点。
齐次变换是4x4矩阵,使用它们可以将平移表示为矩阵乘法,而不是加法,这对于您的显卡非常快速方便。同时,我们可以通过将它们相乘来表示连续的变换。我们通过执行变换*点来对点应用变换。
主要有3种齐次变换:

还有其他一些值得探索的变换,尤其是“look at”变换。但是,我只想简要列出一些列表和几个链接。连续应用于点的移动、缩放和旋转被称为模型变换矩阵,并将它们相对于相机放置在场景中。重要的是要意识到我们所做的类似于将物体围绕相机移动,而不是相反。

正交投影和透视投影

要将世界坐标转换为屏幕坐标,您需要首先使用投影矩阵,通常有两种类型:

  • 正交投影,通常用于2D和CAD。
  • 透视投影,适用于游戏和3D环境。

正交投影矩阵构建如下:

An orthographic projection matrix, courtesy of Wikipedia.

参数包括:

  • Top: 可见空间顶部的Y坐标。
  • Bottom: 可见空间底部的Y坐标。
  • Left: 可见空间左侧的X坐标。
  • Right: 可见空间右侧的X坐标。

我认为这很简单。您所建立的是一个将出现在屏幕上的空间区域,您可以对其进行剪裁。这里很简单,因为可见空间的区域是一个矩形。透视剪裁更加复杂,因为出现在屏幕上或视野体积的区域是一个截头锥体

如果您在透视投影的维基百科上遇到了困难,这里有构建合适矩阵的代码,由geeks3D提供

void BuildPerspProjMat(float *m, float fov, float aspect,
float znear, float zfar)
{
  float xymax = znear * tan(fov * PI_OVER_360);
  float ymin = -xymax;
  float xmin = -xymax;

  float width = xymax - xmin;
  float height = xymax - ymin;

  float depth = zfar - znear;
  float q = -(zfar + znear) / depth;
  float qn = -2 * (zfar * znear) / depth;

  float w = 2 * znear / width;
  w = w / aspect;
  float h = 2 * znear / height;

  m[0]  = w;
  m[1]  = 0;
  m[2]  = 0;
  m[3]  = 0;

  m[4]  = 0;
  m[5]  = h;
  m[6]  = 0;
  m[7]  = 0;

  m[8]  = 0;
  m[9]  = 0;
  m[10] = q;
  m[11] = -1;

  m[12] = 0;
  m[13] = 0;
  m[14] = qn;
  m[15] = 0;
}

变量如下:

  • fov: 视角,π/4 弧度是一个好的值。
  • aspect: 高宽比。
  • znear, zfar: 用于裁剪,我会忽略这些。

生成的矩阵是列主序的,在上面的代码中按如下方式索引:

0   4   8  12
1   5   9  13
2   6  10  14
3   7  11  15

视口变换、屏幕坐标

这两个转换都需要另一个矩阵将物体放置在屏幕坐标中,称为视口变换。在这里描述了它,我不会涉及它(它非常简单)

因此,对于一个点p,我们将:

  • 执行模型变换矩阵* p,结果为pm。
  • 执行投影矩阵* pm,结果为pp。
  • 剪裁pp以适应视图体积。
  • 执行视口变换矩阵* pp,结果为ps:屏幕上的点。

摘要

我希望这大部分都包括在内了。上述内容存在漏洞,在某些地方含糊不清,请在下面发表任何问题。这个主题通常值得在教科书中撰写一整章,我已尽力概括这个过程,希望对您有所帮助!

我在上面链接到了这个,但我强烈建议您阅读并下载该二进制文件。它是一个很好的工具,可以进一步理解这些转换以及如何将点放在屏幕上:

http://www.songho.ca/opengl/gl_transform.html

关于实际工作,您需要实现一个4x4矩阵类进行同构变换,以及一个同构点类可以与其相乘应用变换(请记住,[x,y,z,1])。您需要按照上述描述和链接生成变换。一旦您理解了这个过程,它并不是很困难。祝你好运 :)。


10

一般而言,不应该将相机的旋转存储为X、Y和Z角度,因为这可能会导致歧义。

例如,x=60度与-300度相同。使用x、y和z时,模棱两可的可能性非常高。

相反,尝试在三维空间中使用两个点,x1、y1、z1用于相机位置,x2、y2、z2用于相机“目标”。角度可以向后计算到/从位置/目标,但在我看来,这是不推荐的。使用相机位置/目标允许您构造一个“LookAt”向量,它是指向相机方向(v')的单位向量。从这里,您还可以构造一个LookAt矩阵,它是用于将三维空间中的对象投影到二维空间中的像素的4×4矩阵。

请参见这个相关问题,其中我讨论了如何计算一个向量R,它位于与相机正交的平面上。

给定相机到目标的向量v = xi,yj,zk
将向量规范化,v' = xi,yj,zk / sqrt(xi^2 + yj^2 + zk^2)
让U = 全局世界上向量u = 0,0,1
然后我们可以计算R = 与相机视线方向平行的水平向量R = v' ^ U,
其中^是叉积,由以下公式给出
a ^ b = (a2b3 - a3b2)i + (a3b1 - a1b3)j + (a1b2 - a2b1)k

这将给您一个如下所示的向量。

Computing a vector orthogonal to the camera

这可能对你的问题有用,因为一旦你有了LookAt向量v'和正交向量R,你就可以开始将3D空间中的点投影到相机平面上。

基本上,所有这些3D操作问题都归结为将世界空间中的点转换为本地空间,其中本地x、y、z轴与相机方向相同。明白了吗?所以如果你有一个点Q=x,y,z,并且你知道R和v'(相机轴),那么你可以使用简单的向量操作将其投影到“屏幕”上。涉及的角度可以通过对向量进行点积运算来找到。

将Q投影到屏幕上


1
这是一个非常好的答案和简单的正交技术。然而,有一点需要注意:如果相机和目标不在同一XZ平面(同一高度),那么您不能使用全局“上”向量来投影点。相反,通过将V与U叉乘来推导R,然后通过将R与V叉乘来获得正交基向量来推导实际的上向量。 - Phrogz

5

按照维基百科,首先计算“d”:

http://upload.wikimedia.org/wikipedia/en/math/6/0/b/60b64ec331ba2493a2b93e8829e864b6.png

为此,在您的代码中建立这些矩阵。从示例到变量的映射:
θ = Camera.angle* a = 3D空间中的某个点 c = Camera.x | y | z 或者,只需单独执行方程而不使用矩阵,由您选择:

http://upload.wikimedia.org/wikipedia/en/math/1/c/8/1c89722619b756d05adb4ea38ee6f62b.png

现在我们计算“b”,一个二维点:

http://upload.wikimedia.org/wikipedia/en/math/2/5/6/256a0e12b8e6cc7cd71fa9495c0c3668.png

在这种情况下,ex和ey是观察者的位置。我相信在大多数图形系统中,屏幕大小的一半(0.5)被用来默认将(0,0)作为屏幕中心,但你可以使用任何值(尝试一下)。ez是视野角度所在的位置。那是你缺少的东西。选择一个fov角度并计算ez如下:
ez = 1 / tan(fov / 2)
最后,要将bx和by转换为实际像素,必须按与屏幕大小相关的因子进行缩放。例如,如果b从(0,0)映射到(1,1),则可以将x缩放1920倍,y缩放1080倍,以适应1920 x 1080显示器的任何屏幕大小。这样,任何屏幕大小都会显示相同的内容。当然,在实际的3D图形系统中还涉及许多其他因素,但这是基本版本。

4

将三维空间中的点转换为屏幕上的二维点可以通过使用矩阵来简单实现。使用矩阵来计算点的屏幕位置,这样可以节省很多工作。

在处理摄像机时,您应该考虑使用look-at-matrix并将其与投影矩阵相乘。


@BerlinBrown 很高兴能够帮助您。如果您在矩阵方面有任何问题,请在此提出,我会发布一些示例(我有一个可用的矩阵库)。 - Felix K.
你能否添加或者展示一下他们在矩阵乘法之后是如何转换为二维坐标的? - Berlin Brown
@BerlinBrown 看这里:http://answers.yahoo.com/question/index?qid=20090624084105AAmVf6q 这是一个简单的矩阵乘法,使用4x1矩阵(Vector4; x,y,z,w; w为1)。我现在不在我的工作电脑旁边,否则我会复制代码。 - Felix K.

3

假设相机在(0,0,0)位置并且朝向正前方,那么方程为:

ScreenData.x = SomePointIn3DSpace.x / SomePointIn3DSpace.z * constant;
ScreenData.y = SomePointIn3DSpace.y / SomePointIn3DSpace.z * constant;

其中"constant"是某个正值。将其设置为屏幕宽度(以像素为单位)通常会得到良好的结果。如果将其设置得更高,则场景将更加“缩放”,反之亦然。

如果您希望相机处于不同的位置或角度,则需要移动和旋转场景,使相机位于(0,0,0)并指向前方,然后您可以使用上述等式。

基本上您在计算穿过相机和3D点的直线与一个稍微悬浮在相机前面的垂直平面的交点。


2
您可能对了解GLUT的幕后操作感兴趣。所有这些方法都有类似的文档,展示了其中所涉及的数学知识。 UCSD的前三节课可能非常有帮助,并包含了关于这个主题的多个示例,我认为这才是您真正想要的。

1

通过光线追踪器运行它:

C#中的光线追踪器 - 他拥有的一些对象会让你感到熟悉 ;-)

只是为了好玩,还有一个LINQ版本

我不确定您的应用程序的更大目的是什么(您应该告诉我们,这可能会激发更好的想法),但是虽然投影和光线追踪是不同的问题集,但它们有很多重叠之处。

如果您的应用程序只是尝试绘制整个场景,那么这将非常棒。

解决问题 #1: 被遮挡的点不会被投影。
解决方案: 虽然我在博客页面上没有看到关于不透明度或透明度的任何内容,但是你可以添加这些属性和代码来处理一条反弹的光线(正常)和一条继续前进的光线(用于“透明度”)。

解决问题 #2: 投影单个像素将需要昂贵的全图轨迹追踪。
显然,如果你只想绘制对象,请使用光线跟踪器来做它的目的!但是,如果你想从随机对象的随机部分查找数千个像素(为什么?),每个请求进行完整的光线跟踪将是一个巨大的性能瓶颈。

幸运的是,通过更多地调整代码,你可能能够进行一次光线跟踪(具有透明度),并缓存结果直到对象改变。

如果你对光线追踪不熟悉,请阅读博客文章 - 我认为它解释了如何从每个2D像素开始向后工作,到达对象,然后到达灯光,以确定像素值。

你可以添加代码,使对象的交点被索引并构建列表,列表的项为当前跟踪的2D像素。

然后,当您想要投影一个点时,请转到该对象的列表中,找到最接近您想要投影的点的点,然后查找您关心的2D像素。这样做的数学计算比你文章中的方程式少得多。不幸的是,例如使用将对象+点结构映射到2D像素的字典,我不知道如何在运行整个映射点列表之外找到对象上的最近点。虽然这不会是世界上最慢的事情,而且你可能可以想出来,但我没有时间去想它。有人能帮忙吗?

祝好运!

"另外,我不明白维基词条中观众位置和相机位置的区别是什么"...... 我99%确定这是一回事。


0

你想使用类似于OpenGL的gluLookAt矩阵来转换你的场景,然后使用类似于OpenGL的gluPerspective投影矩阵来计算投影。

你可以尝试在软件中仅计算矩阵并进行乘法运算。


不是,但原理是相同的。如果您查看这些函数的文档,您将了解它们如何计算矩阵。也许您可以直接在您的程序中使用它们? - Krumelur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接