我目前正在使用贝塞尔曲线和曲面绘制著名的犹他茶壶。使用具有16个控制点的Bezier补丁,我已经能够绘制茶壶并使用“向相机拍照”功能来显示茶壶,该功能可以旋转生成的茶壶,并且目前正在使用正交投影。

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

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

我正在用Java对此进行编程,并设置了所有输入事件处理程序,并且还编写了处理基本矩阵乘法的矩阵类。我已经阅读了Wikipedia和其他资源一段时间,但我不太了解如何执行此转换。

最佳答案

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

  • 使用逆相机矩阵变换3D点,然后进行所需的任何变换。如果您具有表面法线,则也可以对其进行变换,但是将w设置为零,因为您不想平移法线。用来转换法线的矩阵必须是各向同性的。缩放和剪切会使法线变形。
  • 使用剪辑空间矩阵变换点。该矩阵使用视场和宽高比缩放x和y,通过近和远裁剪平面缩放z,并将“旧” z插入w。转换后,应将x,y和z除以w。这称为透视鸿沟。
  • 现在,您的顶点在剪贴空间中,并且您想要执行剪贴,这样就不会在视口(viewport)边界之外渲染任何像素。 Sutherland-Hodgeman裁剪是使用最广泛的裁剪算法。
  • 将w和半角与半高转换为x和y。您的x和y坐标现在处于视口(viewport)坐标中。 w被丢弃,但是通常保存1/w和z,因为在多边形表面进行透视校正插值需要1/w,并且z存储在z缓冲区中并用于深度测试。

  • 这个阶段是实际的投影,因为z不再用作该位置的分量。
    算法:
    视场的计算
    这将计算视野。棕褐色采用弧度还是度无关紧要,但是角度必须匹配。请注意,当角度接近180度时,结果达到无穷大。这是一个奇异之处,因为不可能有这么宽的焦点。如果需要数值稳定性,请使角度保持小于或等于179度。
    fov = 1.0 / tan(angle/2.0)
    
    还要注意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       ]
    
    屏幕投影
    裁剪后,这是获取屏幕坐标的最终转换。
    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论坛上也有很多与软件光栅化器相关的好文章。

    关于java - 如何将3D点转换为2D透视投影?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/724219/

    10-11 04:51
    查看更多