You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

透视投影矩阵乘法实现求助:3D顶点转屏幕坐标问题

问题解决步骤

一、先修正代码里的基础错误

1. 修复Object3D的成员定义

当前vertices和triangles是单个float类型,无法存储顶点与三角形数据,改成合适的容器:

#include <vector>
#include <array>

struct Object3D {
    // 存储8个4维齐次坐标顶点
    std::vector<std::array<float, 4>> vertices = {
        {1.0f, 1.0f, -1.0f, 1.0f}, {1.0f, -1.0f, -1.0f, 1.0f},
        {1.0f, 1.0f, 1.0f, 1.0f}, {1.0f, -1.0f, 1.0f, 1.0f},
        {-1.0f, 1.0f, -1.0f, 1.0f}, {-1.0f, -1.0f, -1.0f, 1.0f},
        {-1.0f, 1.0f, 1.0f, 1.0f}, {-1.0f, -1.0f, 1.0f, 1.0f}
    };
    // 存储12个三角形的顶点索引
    std::vector<std::array<int, 3>> triangles = {
        {4, 2, 0}, {2, 7, 3}, {6, 5, 7}, {1, 7, 5},
        {0, 3, 1}, {4, 1, 5}, {4, 6, 2}, {2, 6, 7},
        {6, 4, 5}, {1, 3, 7}, {0, 2, 3}, {4, 0, 1}
    };
};

2. 修复PerspectiveProjectionCamera的init函数

init里重新定义了局部矩阵,导致类成员变量未初始化,直接给成员赋值即可:

class PerspectiveProjectionCamera {
public:
    float projectionMatrix[4][4];
    float toScreenMatrix[4][4];
    float position[3] = {0.0f, 0.0f, 5.0f}; // 默认相机位置
    int hwidth;
    int hheight;
    void init(float nearplane, float farplane, int width, int height, float FOV) {
        float aspectRatio = static_cast<float>(height) / width;
        float vFOV = FOV * aspectRatio;
        float right = tanf(FOV / 2.0f);
        float left = -right;
        float top = tanf(vFOV / 2.0f);
        float bottom = -top;
        hwidth = width / 2;
        hheight = height / 2;

        float m00 = 2.0f / (right - left);
        float m11 = 2.0f / (top - bottom);
        float m22 = (farplane + nearplane) / (farplane - nearplane);
        float m32 = -2.0f * nearplane * farplane / (farplane - nearplane);

        // 直接赋值给类成员矩阵
        projectionMatrix[0][0] = m00; projectionMatrix[0][1] = 0.0f; projectionMatrix[0][2] = 0.0f; projectionMatrix[0][3] = 0.0f;
        projectionMatrix[1][0] = 0.0f; projectionMatrix[1][1] = m11; projectionMatrix[1][2] = 0.0f; projectionMatrix[1][3] = 0.0f;
        projectionMatrix[2][0] = 0.0f; projectionMatrix[2][1] = 0.0f; projectionMatrix[2][2] = m22; projectionMatrix[2][3] = 1.0f;
        projectionMatrix[3][0] = 0.0f; projectionMatrix[3][1] = 0.0f; projectionMatrix[3][2] = m32; projectionMatrix[3][3] = 0.0f;

        toScreenMatrix[0][0] = static_cast<float>(width) / 2.0f; toScreenMatrix[0][1] = 0.0f; toScreenMatrix[0][2] = 0.0f; toScreenMatrix[0][3] = 0.0f;
        toScreenMatrix[1][0] = 0.0f; toScreenMatrix[1][1] = -static_cast<float>(height) / 2.0f; toScreenMatrix[1][2] = 0.0f; toScreenMatrix[1][3] = 0.0f;
        toScreenMatrix[2][0] = 0.0f; toScreenMatrix[2][1] = 0.0f; toScreenMatrix[2][2] = 1.0f; toScreenMatrix[2][3] = 0.0f;
        toScreenMatrix[3][0] = static_cast<float>(width) / 2.0f; toScreenMatrix[3][1] = static_cast<float>(height) / 2.0f; toScreenMatrix[3][2] = 0.0f; toScreenMatrix[3][3] = 1.0f;
    }
private:
    // 改用float减少计算开销
    float vFOV;
    float right;
    float left;
    float top;
    float bottom;
    float m00;
    float m11;
    float m22;
    float m32;
};

二、正确的顶点变换流程与矩阵乘法实现

1. 变换顺序纠正

原公式projectionMatrix * (vertices+cameraPos) * toScreenMatrix完全错误,正确的3D顶点到屏幕坐标变换顺序为:

世界空间顶点 → 视图空间(顶点 - 相机位置)→ 裁剪空间(投影矩阵乘视图顶点)→ 归一化设备坐标(透视除法)→ 屏幕坐标(屏幕矩阵乘NDC顶点)

顶点用列向量时,矩阵乘法顺序为从右到左:屏幕坐标 = toScreenMatrix * projectionMatrix * 视图空间顶点(透视除法需放在投影之后、屏幕矩阵之前)

2. 矩阵乘4维向量的实现

用inline函数减少调用开销:

// 4x4矩阵乘4维列向量,结果存入out
inline void multiplyMatrixVector(const float mat[4][4], const float vec[4], float out[4]) {
    out[0] = mat[0][0] * vec[0] + mat[0][1] * vec[1] + mat[0][2] * vec[2] + mat[0][3] * vec[3];
    out[1] = mat[1][0] * vec[0] + mat[1][1] * vec[1] + mat[1][2] * vec[2] + mat[1][3] * vec[3];
    out[2] = mat[2][0] * vec[0] + mat[2][1] * vec[1] + mat[2][2] * vec[2] + mat[2][3] * vec[3];
    out[3] = mat[3][0] * vec[0] + mat[3][1] * vec[1] + mat[3][2] * vec[2] + mat[3][3] * vec[3];
}

3. 完整顶点变换代码

// 输入世界空间顶点与相机,输出屏幕坐标(x,y)
void transformVertexToScreen(const float worldVertex[4], const PerspectiveProjectionCamera& cam, float& screenX, float& screenY) {
    // 1. 转换到视图空间:世界顶点 - 相机位置,保持w=1
    float viewVertex[4];
    viewVertex[0] = worldVertex[0] - cam.position[0];
    viewVertex[1] = worldVertex[1] - cam.position[1];
    viewVertex[2] = worldVertex[2] - cam.position[2];
    viewVertex[3] = 1.0f;

    // 2. 投影矩阵变换,得到裁剪空间坐标
    float clipVertex[4];
    multiplyMatrixVector(cam.projectionMatrix, viewVertex, clipVertex);

    // 3. 透视除法,得到NDC坐标
    if (clipVertex[3] == 0.0f) clipVertex[3] = 1e-6f; // 避免除以0
    float ndcVertex[4];
    ndcVertex[0] = clipVertex[0] / clipVertex[3];
    ndcVertex[1] = clipVertex[1] / clipVertex[3];
    ndcVertex[2] = clipVertex[2] / clipVertex[3];
    ndcVertex[3] = 1.0f;

    // 4. 屏幕矩阵变换,得到最终屏幕坐标
    float screenVertex[4];
    multiplyMatrixVector(cam.toScreenMatrix, ndcVertex, screenVertex);

    screenX = screenVertex[0];
    screenY = screenVertex[1];
}

三、十万三角形的性能优化建议

  1. 减少内存访问开销:将Object3D的vertices和triangles改为连续数组(如float vertices[8*4];),比vector内存访问更高效;或提前预分配vector容量。
  2. 循环内复用变量:遍历三角形时,提前声明临时变量并复用,避免频繁创建销毁。
  3. SIMD指令优化:使用x86的SSE/AVX指令集批量处理顶点,一次处理2-4个顶点的变换,大幅提升计算速度。
  4. 多线程并行处理:将三角形分成多个批次,用std::thread或OpenMP分配给多核CPU并行处理,充分利用硬件资源。
  5. 提前剔除不可见三角形:变换前做视锥体剔除,跳过完全在相机视锥体外部的三角形,减少无效计算。

内容的提问来源于stack exchange,提问作者user21956960

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 10:24:57