3D数据点到2D数据点

3D data point to 2D data point

我正在使用 GDI+ 实现一些简单的图形,我已经从这个示例中获取代码 http://www.vcskicks.com/3d_gdiplus_drawing.php 并且可以让它做我想做的事,但我不明白它是如何做的从 3D 数据点到 2D 数据点的转换:

            //Convert 3D Points to 2D
            Math3D.Point3D vec;
            for (int i = 0; i < point3D.Length; i++)
            {
                vec = cubePoints[i];
                if (vec.Z - camera1.Position.Z >= 0)
                {
                    point3D[i].X = (int)((double)-(vec.X - camera1.Position.X) / (-0.1f) * zoom) + drawOrigin.X;
                    point3D[i].Y = (int)((double)(vec.Y - camera1.Position.Y) / (-0.1f) * zoom) + drawOrigin.Y;
                }
                else
                {
                    tmpOrigin.X = (int)((double)(cubeOrigin.X - camera1.Position.X) / (double)(cubeOrigin.Z - camera1.Position.Z) * zoom) + drawOrigin.X;
                    tmpOrigin.Y = (int)((double)-(cubeOrigin.Y - camera1.Position.Y) / (double)(cubeOrigin.Z - camera1.Position.Z) * zoom) + drawOrigin.Y;

                    point3D[i].X = (float)((vec.X - camera1.Position.X) / (vec.Z - camera1.Position.Z) * zoom + drawOrigin.X);
                    point3D[i].Y = (float)(-(vec.Y - camera1.Position.Y) / (vec.Z - camera1.Position.Z) * zoom + drawOrigin.Y);

                    point3D[i].X = (int)point3D[i].X;
                    point3D[i].Y = (int)point3D[i].Y;
                }
            }

我找到了一些讨论从 3d 数据点到 2d 数据点转换的资源:

但是 none 这些资源似乎详细说明了上例中使用的数学。

如果有人能指出我的数学推导,我将不胜感激and/or解释上面的代码是如何工作的。

这篇文章和代码确实有点混乱。在我们开始之前,让我们对其余代码做一些修改。通过这些修改,您可能会更容易地看到发生了什么。让我们指定一个静态相机位置。而不是这个奇怪的公式:

double cameraZ = -(((anchorPoint.X - cubeOrigin.X) * zoom) / cubeOrigin.X) + anchorPoint.Z;

让我们这样做:

cameraZ = 200;
zoom = 100;

之后,我们保持

camera1.Position = new Math3D.Point3D(cubeOrigin.X, cubeOrigin.Y, cameraZ);

这会将相机定位在 200 深度,使其 x/y 坐标与立方体中心重合。我会回到 zoom.

的意思

相机模型使用透视投影和右手坐标系。这意味着相机朝负 z 方向看,远处的东西会显得更小。

下面一步一步仔细看一下3D->2D的转换代码:

if (vec.Z - camera1.Position.Z >= 0)

vec就是我们要投射的点。更直观的写法是:

if (vec.Z >= camera1.Position.Z)

因此,此分支适用于相机后面的所有点(请记住相机朝负 z 方向看)。这个分支中发生的事情有点老套。它与实际预测无关。你真正想要做的是切断这些点(因为它们不可见)。幸运的是,在示例中,none 个点位于相机后面。所以,我们不需要关心这个。我稍后再谈。

让我们继续 else 分支。

tmpOrigin = ...

这个变量没有在任何地方使用,所以我们可以忽略它。

point3D[i].X = (float)((vec.X - camera1.Position.X) / (vec.Z - camera1.Position.Z) * zoom + drawOrigin.X);

这是实际投影(我只考虑X部分,Y部分也是如此)。让我们来看看各个部分:

vec.X - camera1.Position.X

这是从相机位置到绘制点的向量。相机左边的所有东西都有一个负坐标,相机右边的所有东西都有一个正坐标。

vec.Z - camera1.Position.Z

这是相机的负景深。不确定为什么在这里使用负深度。这会给你一个镜像。你真正想做的是(由于相机看着负 z 轴)

camera1.Position.Z - vec.Z

然后,

(vec.X - camera1.Position.X) / (vec.Z - camera1.Position.Z)

是透视分界线。差异向量按其反深度缩放(即远处的物体变小)。

* zoom 

这会将图像从世界 space(非常小)缩放到图像 space(将世界单位转换为像素)。该因素是任意的(这就是我们刚刚指定 100 的原因)。更多涉及的相机模型使用视野。

  • drawOrigin.X 最后,我们将相机中心对准 drawOrigin。请记住,相机左侧的点坐标为负。有了这个,这些将得到一个正坐标(但仍然留在drawOrigin)。

    point3D[i].X = (int)point3D[i].X; 这只是对 int.

  • 的转换

对于y坐标,还有一个额外的-。这样就把y轴转过来了(在图像的像素坐标系中,y轴指向下方)。

让我们回到 hacky if 分支。你看公式是完全一样的。除了之前点的负深度部分现在有(-0.1f)。因此,这些点将被视为具有 0.1 的恒定深度。非常可疑,与实际预测相去甚远。

基本上就是这样。还有一点要注意:这篇文章有一节是关于 Gimbal lock 的。事实是,那里描述的矩阵乘法的属性与万向节锁无关。所以,不要过分依赖这篇文章。这是一个很好的实用应用程序,但它有很多缺陷。