如何将3D点转换为2D透视投影?

Zac*_*ght 51 java 3d graphics bezier

我目前正在使用Bezier曲线和曲面来绘制着名的犹他州茶壶.使用16个控制点的Bezier贴片,我已经能够绘制茶壶并使用"世界到相机"功能显示它,这使得能够旋转生成的茶壶,并且目前正在使用正交投影.

结果是我有一个"扁平"茶壶,预计正投影的目的是保持平行线.

但是,我想使用透视投影来给出茶壶深度.我的问题是,如何从"世界到相机"函数返回3D xyz顶点,并将其转换为2D坐标.我想在z = 0时使用投影平面,并允许用户使用键盘上的箭头键确定焦距和图像大小.

我在java中编程并设置了所有输入事件处理程序,并且还编写了一个处理基本矩阵乘法的矩阵类.我已经阅读了维基百科和其他资源一段时间,但我无法完全了解如何执行此转换.

小智 88

我看到这个问题有点老了,但无论如何我决定给那些通过搜索找到这个问题的人一个答案.
现在表示2D/3D变换的标准方法是使用齐次坐标.[X,Y,w]的用于2D,和[X,Y,Z,W]为3D.由于您在3D和平移中都有三个轴,因此该信息完全适合4x4变换矩阵.我将在此解释中使用列主矩阵表示法.除非另有说明,否则所有矩阵均为4x4.
从3D点到栅格化点,线或多边形的阶段如下所示:

  1. 使用逆摄像机矩阵转换3D点,然后进行所需的任何转换.如果你有曲面法线,也可以将它们变换,但是将w设置为零,因为你不想平移法线.变换法线的矩阵必须是各向同性的 ; 缩放和剪切会使法线变形.
  2. 使用剪辑空间矩阵变换点.该矩阵使用视场和纵横比来缩放x和y,通过近和远剪裁平面缩放z,并将'旧'z插入到w中.转换后,应将x,y和z除以w.这被称为透视鸿沟.
  3. 现在您的顶点位于剪辑空间中,并且您希望执行剪裁,因此您不会渲染视口边界之外的任何像素.Sutherland-Hodgeman裁剪是使用中最广泛的裁剪算法.
  4. 相对于w以及半宽和半高变换x和y.您的x和y坐标现在位于视口坐标中.w被丢弃,但通常保存1/w和z,因为在多边形表面上进行透视校正插值需要1/w,并且z存储在z缓冲区中并用于深度测试.

此阶段是实际投影,因为z不再用作位置中的组件.

算法:

视野的计算

这会计算视野.tan取弧度或度数是无关紧要的,但角度必须匹配.请注意,当角度接近180度时,结果会达到无穷大.这是一个奇点,因为不可能有一个广泛的焦点.如果您想要数值稳定性,请保持角度小于或等于179度.

fov = 1.0 / tan(angle/2.0)
Run Code Online (Sandbox Code Playgroud)

还要注意1.0/tan(45)= 1.这里的其他人建议除以z.结果很清楚.您将获得90度FOV和1:1的宽高比.使用像这样的齐次坐标还有其他几个优点; 例如,我们可以对近处和远处的平面执行剪裁,而不将其视为特殊情况.

剪辑矩阵的计算

这是剪辑矩阵的布局.aspectRatio是宽度/高度.因此,基于y的FOV来缩放x分量的FOV.远近是系数,它们是近剪裁平面和远剪裁平面的距离.

[fov * aspectRatio][        0        ][        0              ][        0       ]
[        0        ][       fov       ][        0              ][        0       ]
[        0        ][        0        ][(far+near)/(far-near)  ][        1       ]
[        0        ][        0        ][(2*near*far)/(near-far)][        0       ]
Run Code Online (Sandbox Code Playgroud)

屏幕投影

剪切后,这是获取屏幕坐标的最终转换.

new_x = (x * Width ) / (2.0 * w) + halfWidth;
new_y = (y * Height) / (2.0 * w) + halfHeight;
Run Code Online (Sandbox Code Playgroud)

C++中的简单示例实现

#include <vector>
#include <cmath>
#include <stdexcept>
#include <algorithm>

struct Vector
{
    Vector() : x(0),y(0),z(0),w(1){}
    Vector(float a, float b, float c) : x(a),y(b),z(c),w(1){}

    /* Assume proper operator overloads here, with vectors and scalars */
    float Length() const
    {
        return std::sqrt(x*x + y*y + z*z);
    }

    Vector Unit() const
    {
        const float epsilon = 1e-6;
        float mag = Length();
        if(mag < epsilon){
            std::out_of_range e("");
            throw e;
        }
        return *this / mag;
    }
};

inline float Dot(const Vector& v1, const Vector& v2)
{
    return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
}

class Matrix
{
    public:
    Matrix() : data(16)
    {
        Identity();
    }
    void Identity()
    {
        std::fill(data.begin(), data.end(), float(0));
        data[0] = data[5] = data[10] = data[15] = 1.0f;
    }
    float& operator[](size_t index)
    {
        if(index >= 16){
            std::out_of_range e("");
            throw e;
        }
        return data[index];
    }
    Matrix operator*(const Matrix& m) const
    {
        Matrix dst;
        int col;
        for(int y=0; y<4; ++y){
            col = y*4;
            for(int x=0; x<4; ++x){
                for(int i=0; i<4; ++i){
                    dst[x+col] += m[i+col]*data[x+i*4];
                }
            }
        }
        return dst;
    }
    Matrix& operator*=(const Matrix& m)
    {
        *this = (*this) * m;
        return *this;
    }

    /* The interesting stuff */
    void SetupClipMatrix(float fov, float aspectRatio, float near, float far)
    {
        Identity();
        float f = 1.0f / std::tan(fov * 0.5f);
        data[0] = f*aspectRatio;
        data[5] = f;
        data[10] = (far+near) / (far-near);
        data[11] = 1.0f; /* this 'plugs' the old z into w */
        data[14] = (2.0f*near*far) / (near-far);
        data[15] = 0.0f;
    }

    std::vector<float> data;
};

inline Vector operator*(const Vector& v, const Matrix& m)
{
    Vector dst;
    dst.x = v.x*m[0] + v.y*m[4] + v.z*m[8 ] + v.w*m[12];
    dst.y = v.x*m[1] + v.y*m[5] + v.z*m[9 ] + v.w*m[13];
    dst.z = v.x*m[2] + v.y*m[6] + v.z*m[10] + v.w*m[14];
    dst.w = v.x*m[3] + v.y*m[7] + v.z*m[11] + v.w*m[15];
    return dst;
}

typedef std::vector<Vector> VecArr;
VecArr ProjectAndClip(int width, int height, float near, float far, const VecArr& vertex)
{
    float halfWidth = (float)width * 0.5f;
    float halfHeight = (float)height * 0.5f;
    float aspect = (float)width / (float)height;
    Vector v;
    Matrix clipMatrix;
    VecArr dst;
    clipMatrix.SetupClipMatrix(60.0f * (M_PI / 180.0f), aspect, near, far);
    /*  Here, after the perspective divide, you perform Sutherland-Hodgeman clipping 
        by checking if the x, y and z components are inside the range of [-w, w].
        One checks each vector component seperately against each plane. Per-vertex
        data like colours, normals and texture coordinates need to be linearly
        interpolated for clipped edges to reflect the change. If the edge (v0,v1)
        is tested against the positive x plane, and v1 is outside, the interpolant
        becomes: (v1.x - w) / (v1.x - v0.x)
        I skip this stage all together to be brief.
    */
    for(VecArr::iterator i=vertex.begin(); i!=vertex.end(); ++i){
        v = (*i) * clipMatrix;
        v /= v.w; /* Don't get confused here. I assume the divide leaves v.w alone.*/
        dst.push_back(v);
    }

    /* TODO: Clipping here */

    for(VecArr::iterator i=dst.begin(); i!=dst.end(); ++i){
        i->x = (i->x * (float)width) / (2.0f * i->w) + halfWidth;
        i->y = (i->y * (float)height) / (2.0f * i->w) + halfHeight;
    }
    return dst;
}
Run Code Online (Sandbox Code Playgroud)

如果你仍然在思考这个问题,那么OpenGL规范对于所涉及的数学来说是一个非常好的参考.http://www.devmaster.net/上的DevMaster论坛也有很多与软件光栅化器相关的好文章.

  • 这是什么意思?"你通过检查x,y和z分量是否在[-w,w]的范围内来执行Sutherland-Hodgeman裁剪." 即-w和w.这些价值来自哪里? (3认同)
  • 这个“琐碎”的例子甚至无法编译。 (3认同)

rof*_*kel 13

我想可能会回答你的问题.这是我在那里写的:

这是一个非常一般的答案.假设相机处于(Xc,Yc,Zc)并且您要投影的点是P =(X,Y,Z).从摄像机到投影到的2D平面的距离为F(因此平面的方程为Z-Zc = F).投影到平面上的P的2D坐标是(X',Y').

然后,非常简单:

X'=((X-Xc)*(F/Z))+ Xc

Y'=((Y-Yc)*(F/Z))+ Yc

如果您的相机是原点,那么这简化为:

X'= X*(F/Z)

Y'= Y*(F/Z)


小智 8

在此输入图像描述

\n\n

从顶部看屏幕,您将获得 x 和 z 轴。\n
从侧面看屏幕,您将获得 y 和 z 轴。

\n\n

使用三角学计算顶视图和侧视图的焦距,即眼睛与屏幕中间之间的距离,该距离由屏幕的视野决定。\n这使得两个直角三角形的形状变回原来的形状回来。

\n\n

hw = 屏幕宽度 / 2

\n\n

hh = 屏幕高度 / 2

\n\n

fl_top = hw / tan(\xce\xb8/2)

\n\n

fl_side = hh / tan(\xce\xb8/2)

\n\n


\n然后取平均焦距。

\n\n

fl_average = (fl_top + fl_side) / 2

\n\n


\n现在用基本算术计算新的 x 和新 y,因为由 3d 点和视点构成的较大直角三角形与由 2d 点和视点构成的较小三角形全等。

\n\n

x\' = (x * fl_top) / (z + fl_top)

\n\n

y\' = (y * fl_top) / (z + fl_top)

\n\n


\n或者你可以简单地设置

\n\n

x\' = x / (z + 1)

\n\n

\n\n

y\' = y / (z + 1)

\n


Dan*_*eón 6

您可以使用以下方式在2D中投影3D点:Commons Math:Apache Commons数学库,只有两个类.

Java Swing的示例.

import org.apache.commons.math3.geometry.euclidean.threed.Plane;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;


Plane planeX = new Plane(new Vector3D(1, 0, 0));
Plane planeY = new Plane(new Vector3D(0, 1, 0)); // Must be orthogonal plane of planeX

void drawPoint(Graphics2D g2, Vector3D v) {
    g2.drawLine(0, 0,
            (int) (world.unit * planeX.getOffset(v)),
            (int) (world.unit * planeY.getOffset(v)));
}

protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    drawPoint(g2, new Vector3D(2, 1, 0));
    drawPoint(g2, new Vector3D(0, 2, 0));
    drawPoint(g2, new Vector3D(0, 0, 2));
    drawPoint(g2, new Vector3D(1, 1, 1));
}
Run Code Online (Sandbox Code Playgroud)

现在你只需要更新planeXplaneY更改透视投影,就可以得到这样的结果:

在此输入图像描述在此输入图像描述


j_r*_*ker 5

要获得透视校正的坐标,只需除以z坐标即可:

xc = x / z
yc = y / z
Run Code Online (Sandbox Code Playgroud)

上面的工作原理假设相机位于,(0, 0, 0)并且您要在上投影到平面上,z = 1否则您需要相对于相机平移坐标。

曲线存在一些复杂性,因为投影3D Bezier曲线的点通常不会提供与通过投影点绘制2D Bezier曲线相同的点。