如何构建透视投影矩阵(无 API)

Posted

技术标签:

【中文标题】如何构建透视投影矩阵(无 API)【英文标题】:How to build perspective projection matrix (no API) 【发布时间】:2013-08-26 15:02:27 【问题描述】:

我开发了一个简单的 3D 引擎(不使用任何 API),成功地将我的场景转换为世界和视图空间,但是使用透视投影矩阵(OpenGL 样式)投影我的场景(从视图空间)时遇到了问题。我不确定 fov、near 和 far 值,而且我得到的场景是扭曲的。 我希望有人可以指导我如何通过示例代码正确构建和使用透视投影矩阵。提前感谢您的帮助。

矩阵构建:

double f = 1 / Math.Tan(fovy / 2);
return new double[,]  

     f / Aspect, 0, 0, 0 ,
     0, f, 0, 0 ,
     0, 0, (Far + Near) / (Near - Far),  (2 * Far * Near) / (Near - Far) , 
     0, 0, -1, 0  
;

矩阵用途:

foreach (Point P in T.Points)
     
    .
    .     // Transforming the point to homogen point matrix, to world space, and to view space (works fine)
    .     

    // projecting the point with getProjectionMatrix() specified in the previous code :      

    double[,] matrix = MatrixMultiply( GetProjectionMatrix(Fovy, Width/Height, Near, Far) , viewSpacePointMatrix );

    // translating to Cartesian coordinates (from homogen):

    matrix [0, 0] /= matrix [3, 0];
    matrix [1, 0] /= matrix [3, 0];
    matrix [2, 0] /= matrix [3, 0];
    matrix [3, 0] = 1;
    P = MatrixToPoint(matrix);

    // adjusting to the screen Y axis:

    P.y = this.Height - P.y;

    // Printing...

【问题讨论】:

scratchapixel.com/lessons/3d-basic-rendering/…我也推荐之前的课程(投影点和3D查看)。 【参考方案1】:

以下是透视投影矩阵的典型实现。 这是一个很好的链接来解释一切OpenGL Projection Matrix

void ComputeFOVProjection( Matrix& result, float fov, float aspect, float nearDist, float farDist, bool leftHanded /* = true */ )

    //
    // General form of the Projection Matrix
    //
    // uh = Cot( fov/2 ) == 1/Tan(fov/2)
    // uw / uh = 1/aspect
    // 
    //   uw         0       0       0
    //    0        uh       0       0
    //    0         0      f/(f-n)  1
    //    0         0    -fn/(f-n)  0
    //
    // Make result to be identity first

    // check for bad parameters to avoid divide by zero:
    // if found, assert and return an identity matrix.
    if ( fov <= 0 || aspect == 0 )
    
        Assert( fov > 0 && aspect != 0 );
        return;
    

    float frustumDepth = farDist - nearDist;
    float oneOverDepth = 1 / frustumDepth;

    result[1][1] = 1 / tan(0.5f * fov);
    result[0][0] = (leftHanded ? 1 : -1 ) * result[1][1] / aspect;
    result[2][2] = farDist * oneOverDepth;
    result[3][2] = (-farDist * nearDist) * oneOverDepth;
    result[2][3] = 1;
    result[3][3] = 0;

【讨论】:

对不起,这里的 uh 和 uw 是什么?用户宽度和用户高度? @ReX357 uw = near/right, and uh = near/top,其中right是右裁剪平面的坐标,top是顶部裁剪平面的坐标。由于上面的透视投影是对称的,所以right =水平宽度的一半,top =垂直高度的一半,那么uw/uh = top/right = height/width = 1/aspect 在opengl中z不是通常乘以-1吗? @racarate 我认为 opengl 使用右手坐标系,但 directx 使用左手。如果您将opengl 转到左手坐标系以与directx 保持一致,那么您将z 乘以-1。上面的计算已经解决了这个问题。 (你传递给函数的leftHand参数) @WayneWang,我正在阅读您的链接,但我卡在“接下来,我们将 xp 和 yp 映射到 NDC 的 xn 和 yn 与线性关系;[l, r] ⇒ [-1, 1] 和 [b, t] ⇒ [-1, 1]."here is the equation 你能解释一下为什么我们要添加 beta 以及为什么我们要替换 xp = r 和 xn = 1? 【参考方案2】:

另一个可能有用的功能。

这个基于left/right/top/bottom/near/far参数(在OpenGL中使用):

static void test()
    float projectionMatrix[16];

    // width and height of viewport to display on (screen dimensions in case of fullscreen rendering)
    float ratio = (float)width/height;
    float left = -ratio;
    float right = ratio;
    float bottom = -1.0f;
    float top = 1.0f;
    float near = -1.0f;
    float far = 100.0f;

    frustum(projectionMatrix, 0, left, right, bottom, top, near, far);



static void frustum(float *m, int offset,
                     float left, float right, float bottom, float top,
                     float near, float far) 

    float r_width  = 1.0f / (right - left);
    float r_height = 1.0f / (top - bottom);
    float r_depth  = 1.0f / (far - near);
    float x =  2.0f * (r_width);
    float y =  2.0f * (r_height);
    float z =  2.0f * (r_depth);
    float A = (right + left) * r_width;
    float B = (top + bottom) * r_height;
    float C = (far + near) * r_depth;
    m[offset + 0] = x;
    m[offset + 3] = -A;
    m[offset + 5] = y;
    m[offset + 7] = -B;
    m[offset + 10] = -z;
    m[offset + 11] = -C;
    m[offset +  1] = 0.0f;
    m[offset +  2] = 0.0f;
    m[offset +  4] = 0.0f;
    m[offset +  6] = 0.0f;
    m[offset +  8] = 0.0f;
    m[offset +  9] = 0.0f;
    m[offset + 12] = 0.0f;
    m[offset + 13] = 0.0f;
    m[offset + 14] = 0.0f;
    m[offset + 15] = 1.0f;


【讨论】:

以上是关于如何构建透视投影矩阵(无 API)的主要内容,如果未能解决你的问题,请参考以下文章

相机矩阵(Camera Matrix)

∑GL-透视投影矩阵的推导

∑GL-透视投影矩阵的推导

PyOpenGL 透视投影

视锥体剔除(Frustum Culling)算法详解-透视投影矩阵直接推导

视锥体剔除(Frustum Culling)算法详解-透视投影矩阵直接推导