如何将 3D 点转换为 2D 透视投影? [英] How to convert a 3D point into 2D perspective projection?

查看:65
本文介绍了如何将 3D 点转换为 2D 透视投影?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前正在使用贝塞尔曲线和曲面来绘制著名的犹他州茶壶.使用 16 个控制点的 Bezier 补丁,我已经能够绘制茶壶并使用世界到相机"功能显示它,该功能能够旋转生成的茶壶,目前我正在使用正交投影.

I am currently working with using Bezier curves and surfaces to draw the famous Utah teapot. Using Bezier patches of 16 control points, I have been able to draw the teapot and display it using a 'world to camera' function which gives the ability to rotate the resulting teapot, and am currently using an orthographic projection.

结果是我有一个扁平"的茶壶,这是预期的,因为正交投影的目的是保留平行线.

The result is that I have a 'flat' teapot, which is expected as the purpose of an orthographic projection is to preserve parallel lines.

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

However, I would like to use a perspective projection to give the teapot depth. My question is, how does one take the 3D xyz vertex returned from the 'world to camera' function, and convert this into a 2D coordinate. I am wanting to use the projection plane at z=0, and allow the user to determine the focal length and image size using the arrow keys on the keyboard.

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

I am programming this in java and have all of the input event handler set up, and have also written a matrix class which handles basic matrix multiplication. I've been reading through wikipedia and other resources for a while, but I can't quite get a handle on how one performs this transformation.

推荐答案

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

I see this question is a bit old, but I decided to give an answer anyway for those who find this question by searching.
The standard way to represent 2D/3D transformations nowadays is by using homogeneous coordinates. [x,y,w] for 2D, and [x,y,z,w] for 3D. Since you have three axes in 3D as well as translation, that information fits perfectly in a 4x4 transformation matrix. I will use column-major matrix notation in this explanation. All matrices are 4x4 unless noted otherwise.
The stages from 3D points and to a rasterized point, line or polygon looks like this:

  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 缓冲区中并用于深度测试.
  1. Transform your 3D points with the inverse camera matrix, followed with whatever transformations they need. If you have surface normals, transform them as well but with w set to zero, as you don't want to translate normals. The matrix you transform normals with must be isotropic; scaling and shearing makes the normals malformed.
  2. Transform the point with a clip space matrix. This matrix scales x and y with the field-of-view and aspect ratio, scales z by the near and far clipping planes, and plugs the 'old' z into w. After the transformation, you should divide x, y and z by w. This is called the perspective divide.
  3. Now your vertices are in clip space, and you want to perform clipping so you don't render any pixels outside the viewport bounds. Sutherland-Hodgeman clipping is the most widespread clipping algorithm in use.
  4. Transform x and y with respect to w and the half-width and half-height. Your x and y coordinates are now in viewport coordinates. w is discarded, but 1/w and z is usually saved because 1/w is required to do perspective-correct interpolation across the polygon surface, and z is stored in the z-buffer and used for depth testing.

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

This stage is the actual projection, because z isn't used as a component in the position any more.

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

This calculates the field-of view. Whether tan takes radians or degrees is irrelevant, but angle must match. Notice that the result reaches infinity as angle nears 180 degrees. This is a singularity, as it is impossible to have a focal point that wide. If you want numerical stability, keep angle less or equal to 179 degrees.

fov = 1.0 / tan(angle/2.0)

还要注意 1.0/tan(45) = 1.这里有人建议只除以 z.这里的结果很明显.您将获得 90 度 FOV 和 1:1 的纵横比.像这样使用齐次坐标还有其他几个优点;例如,我们可以对近平面和远平面进行裁剪,而无需将其视为特例.

Also notice that 1.0 / tan(45) = 1. Someone else here suggested to just divide by z. The result here is clear. You would get a 90 degree FOV and an aspect ratio of 1:1. Using homogeneous coordinates like this has several other advantages as well; we can for example perform clipping against the near and far planes without treating it as a special case.

这是剪辑矩阵的布局.aspectRatio 是宽度/高度.所以 x 分量的 FOV 是基于 y 的 FOV 缩放的.far 和near 是系数,是远近裁剪平面的距离.

This is the layout of the clip matrix. aspectRatio is Width/Height. So the FOV for the x component is scaled based on FOV for y. Far and near are coefficients which are the distances for the near and far clipping planes.

[fov * aspectRatio][        0        ][        0              ][        0       ]
[        0        ][       fov       ][        0              ][        0       ]
[        0        ][        0        ][(far+near)/(far-near)  ][        1       ]
[        0        ][        0        ][(2*near*far)/(near-far)][        0       ]

屏幕投影

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

Screen Projection

After clipping, this is the final transformation to get our screen coordinates.

new_x = (x * Width ) / (2.0 * w) + halfWidth;
new_y = (y * Height) / (2.0 * w) + halfHeight;

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;
}

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

If you still ponder about this, the OpenGL specification is a really nice reference for the maths involved. The DevMaster forums at http://www.devmaster.net/ have a lot of nice articles related to software rasterizers as well.

这篇关于如何将 3D 点转换为 2D 透视投影?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆