从0开始做一个软渲染器——透视投影和投影矫正

从0开始做一个软渲染器——透视投影和投影矫正

已经做了一段时间了,一直都没记录。最近实现了一个透视投影的相机,从这一部分记录。

项目地址:https://github.com/DogWealth/PIRenderer

需要注意的是:

  • 以下代码的运算都将向量考虑成行向量,进行从左往右的乘法运算。相比列向量,对应的相乘矩阵需要进行转置
  • 右手坐标系

透视投影

correct_gloor.gif

参考文章The Perspective and Orthographic Projection Matrix (scratchapixel.com)

阅读完这一大章节(七个小节)的知识点,跟着手推一遍,就能完全掌握透视投影和正交投影。

直接给出投影矩阵的代码

Matrix4 Matrix4::Orthographic(float l, float r, float b, float t, float f, float n)
{
    Matrix4 mat4;
    mat4.m_Mat[0][0] = 2 / (r - l);
    mat4.m_Mat[0][1] = 0;
    mat4.m_Mat[0][2] = 0;
    mat4.m_Mat[0][3] = 0;

    mat4.m_Mat[1][0] = 0;
    mat4.m_Mat[1][1] = 2 / (t - b);
    mat4.m_Mat[1][2] = 0;
    mat4.m_Mat[1][3] = 0;

    mat4.m_Mat[2][0] = 0;
    mat4.m_Mat[2][1] = 0;
    mat4.m_Mat[2][2] = 2 / (n - f);
    mat4.m_Mat[2][3] = 0;

    mat4.m_Mat[3][0] = -(r + l) / (r - l);
    mat4.m_Mat[3][1] = -(t + b) / (t - b);
    mat4.m_Mat[3][2] = -(n + f) / (n - f);
    mat4.m_Mat[3][3] = 1;

    return mat4;
}

Matrix4 Matrix4::Perspective(float n, float f, float fov, float aspectRatio)
{
    float t = tan(PI * fov / (2 * 180)) * n;
    float b = -t;
    float r = t * aspectRatio;
    float l = -r;

    Matrix4 mat4;
    mat4.m_Mat[0][0] = n;
    mat4.m_Mat[0][1] = 0;
    mat4.m_Mat[0][2] = 0;
    mat4.m_Mat[0][3] = 0;

    mat4.m_Mat[1][0] = 0;
    mat4.m_Mat[1][1] = n;
    mat4.m_Mat[1][2] = 0;
    mat4.m_Mat[1][3] = 0;

    mat4.m_Mat[2][0] = 0;
    mat4.m_Mat[2][1] = 0;
    mat4.m_Mat[2][2] = f + n;
    mat4.m_Mat[2][3] = -1;

    mat4.m_Mat[3][0] = 0;
    mat4.m_Mat[3][1] = 0;
    mat4.m_Mat[3][2] = f * n;
    mat4.m_Mat[3][3] = 0;

    mat4 =  mat4 * Matrix4::Orthographic(l, r, b, t, f, n);

    return mat4;
}

透视相机

相机的作用是得到VP矩阵,建立一个Camera类,其主要作用就是计算VP矩阵

class Camera
{
public:
    Camera(Matrix4 projectionMatrix, Matrix4 viewMatrix)
        : m_ProjectionMatrix(projectionMatrix), m_ViewMatrix(viewMatrix)
    {
    }


    void LookAt(const Vector3f& eyePos, const Vector3f& lookAt, const Vector3f& upAxis)
    {

        m_ViewMatrix = Matrix4::LookAt(eyePos, lookAt, upAxis);
        m_ViewProjectionMatrix = m_ViewMatrix * m_ProjectionMatrix;
    }

    void SetPosition(const Vector3f& position)
    {
        m_Position = position;
        RecalculateViewMatrix();
    }
    const Vector3f& GetPosition() const { return m_Position; }

    void SetLookDir(const Vector3f& lookDir)
    {
        m_LookDir = lookDir;
        RecalculateViewMatrix();
    }
    const Vector3f& GetLookDir() const { return m_LookDir; }

    void SetRotation(Vector3f rotation)
    {
        m_Rotation = rotation;
        RecalculateViewMatrix();
    }
    Vector3f GetRotation() const { return m_Rotation; }

    const Matrix4& GetProjectionMatrix() const { return m_ProjectionMatrix; }
    const Matrix4& GetViewMatrix() const { return m_ViewMatrix; }
    const Matrix4& GetViewProjectionMatrix() const { return m_ViewProjectionMatrix; }


protected:
    void RecalculateViewMatrix()
    {
        Matrix4 transform = Matrix4::Translate(-m_Position.x, -m_Position.y, -m_Position.z);
        Matrix4 rotation = Matrix4::RotateEuler(m_Rotation.y, m_Rotation.x, m_Rotation.z);

        m_ViewMatrix = transform * Matrix4::Transpose(rotation);
        m_ViewProjectionMatrix = m_ViewMatrix * m_ProjectionMatrix;
    }

protected:
    Matrix4 m_ProjectionMatrix;
    Matrix4 m_ViewMatrix;
    Matrix4 m_ViewProjectionMatrix;

    Vector3f m_Position = { 0.0f, 0.0f, 2.0f };
    Vector3f m_LookDir = { 0.0f, 0.0f, -1.0f };
    Vector3f m_Rotation = { 0.0f, 0.0f, 0.0f };
};

通过继承这个类来实现正交相机或者投影相机

//Camera.h
class OrthographicCamera : public Camera
{
public:
    OrthographicCamera(float left, float right, float bottom, float top, float far, float near);

    void SetProjection(float left, float right, float bottom, float top, float far, float near);
};


class PerspectiveCamera : public Camera
{
public:
    PerspectiveCamera(float n, float f, float fov, float aspectRatio);

    void SetProjection(float n, float f, float fov, float aspectRatio);
};

OrthographicCamera::OrthographicCamera(float left, float right, float bottom, float top, float far, float near)
		: Camera(Matrix4::Orthographic(left, right, bottom, top, far, near), Matrix4::Identity())
	{
		RecalculateViewMatrix();
	}


//Camera.cpp
void OrthographicCamera::SetProjection(float left, float right, float bottom, float top, float far, float near)
{
    m_ProjectionMatrix = Matrix4::Orthographic(left, right, bottom, top, far, near);
    RecalculateViewMatrix();
}

PerspectiveCamera::PerspectiveCamera(float n, float f, float fov, float aspectRatio)
    : Camera(Matrix4::Perspective(n, f, fov, aspectRatio), Matrix4::Identity())
{
    RecalculateViewMatrix();
}

void PerspectiveCamera::SetProjection(float n, float f, float fov, float aspectRatio)
{
    m_ProjectionMatrix = Matrix4::Perspective(n, f, fov, aspectRatio);
    RecalculateViewMatrix();
}

在这里通过改变相机位置和旋转相机都能够改变View Matrix,或者通过LookAt函数让相机看向某个方向

LookAt矩阵参考文章:图形学:观察矩阵/LookUp矩阵的推导 - 知乎 (zhihu.com)

Matrix4 Matrix4::LookAt(const Vector3f& eyePos, const Vector3f& lookAt, const Vector3f& upAxis)
{
    Vector3f lookDir = lookAt;
    lookDir.Normalize();

    Vector3f rightDir = Vector3f::CrossProduct(upAxis, lookDir);
    rightDir.Normalize();

    Vector3f upDir = Vector3f::CrossProduct(lookDir, rightDir);
    upDir.Normalize();

    Matrix4 mat4;
    mat4.m_Mat[0][0] = rightDir.x;
    mat4.m_Mat[0][1] = upDir.x;
    mat4.m_Mat[0][2] = lookDir.x;
    mat4.m_Mat[0][3] = 0;

    mat4.m_Mat[1][0] = rightDir.y;
    mat4.m_Mat[1][1] = upDir.y;
    mat4.m_Mat[1][2] = lookDir.y;
    mat4.m_Mat[1][3] = 0;

    mat4.m_Mat[2][0] = rightDir.z;
    mat4.m_Mat[2][1] = upDir.z;
    mat4.m_Mat[2][2] = lookDir.z;
    mat4.m_Mat[2][3] = 0;

    mat4.m_Mat[3][0] = 0;
    mat4.m_Mat[3][1] = 0;
    mat4.m_Mat[3][2] = 0;
    mat4.m_Mat[3][3] = 1;

    return Matrix4::Translate(-eyePos.x, -eyePos.y, -eyePos.z) * mat4;
}

轨道相机

为了更好的观察物体,需要建立一个CameraController来控制相机的运动。轨道相机能更方便的展示模型

可以参考的文章:

建立一个Controller类来控制相机的运动

class PerspectiveCameraController
{
public:
    PerspectiveCameraController(float n, float f, float fov, float aspectRatio);

    virtual void OnUpdate() = 0;

    PerspectiveCamera& GetCamera() { return m_Camera; }
    const PerspectiveCamera& GetCamera() const { return m_Camera; }

protected:
    PerspectiveCamera m_Camera;
    Vector3f m_CameraPosition = { 0.f, 0.f, 30.f };
    Vector3f m_CameraRotation = { 0.f, 0.f, 0.f };

    float m_CameraTranslationSpeed = 0.05f;
    float m_CameraRotationSpeed = 1.f;
};

class OrbitController : public PerspectiveCameraController
{
public:
    OrbitController(float n, float f, float fov, float aspectRatio)
        : PerspectiveCameraController(n, f, fov, aspectRatio)
    {

    }

    virtual void OnUpdate() override;

private:
    float Radius = 50;
    float Theta = 0;
    float Phi = 0;

};

void OrbitController::OnUpdate()
{
    if (Input::IsKeyPressed(SDL_SCANCODE_LEFT))
    {
        Theta -= m_CameraRotationSpeed;
    }

    if (Input::IsKeyPressed(SDL_SCANCODE_RIGHT))
    {
        Theta += m_CameraRotationSpeed;
    }

    if (Input::IsKeyPressed(SDL_SCANCODE_UP))
    {
        Phi += m_CameraRotationSpeed;
    }

    if (Input::IsKeyPressed(SDL_SCANCODE_DOWN))
    {
        Phi -= m_CameraRotationSpeed;
    }

    if (Input::IsKeyPressed(SDL_SCANCODE_W))
        Radius -= m_CameraTranslationSpeed;

    if (Input::IsKeyPressed(SDL_SCANCODE_S))
        Radius += m_CameraTranslationSpeed;


    m_CameraPosition.x = Radius * sin(Theta * PI / 180.0f) * cos(Phi * PI / 180.0f);
    m_CameraPosition.y = Radius * sin(Phi * PI / 180.0f);
    m_CameraPosition.z = Radius * cos(Theta * PI / 180.0f) * cos(Phi * PI / 180.0f);

    m_Camera.SetPosition(m_CameraPosition);
    m_Camera.LookAt(m_CameraPosition, m_CameraPosition, {0, 1, 0});
}

结果如下,地板被严重扭曲,这是因为透视插值没有进行矫正

floor.gif

透视矫正

参考资料:

这里要记住的插值公式如下:

\[I_{t}=\left(\frac{I_{1}}{Z_{1}}+s\left(\frac{I_{2}}{Z_{2}}-\frac{I_{1}}{Z_{1}}\right)\right) / \frac{1}{Z_{t}} \]

在顶点变换中,先对顶点的各个属性预先除以Z值,这一步在函数Vertex_rhw_Init中进行

//顶点着色器
void BasicShader::VertexShader(Vertex* v1, Vertex* v2, Vertex* v3)
{
    v1->m_Position = v1->m_Position * m_VPMatrix;
    v2->m_Position = v2->m_Position * m_VPMatrix;
    v3->m_Position = v3->m_Position * m_VPMatrix;

    v1->m_Position.x /= v1->m_Position.w;
    v2->m_Position.x /= v2->m_Position.w;
    v3->m_Position.x /= v3->m_Position.w;

    v1->m_Position.y /= v1->m_Position.w;
    v2->m_Position.y /= v2->m_Position.w;
    v3->m_Position.y /= v3->m_Position.w;

    v1->m_Position.z = -v1->m_Position.w;
    v2->m_Position.z = -v2->m_Position.w;
    v3->m_Position.z = -v3->m_Position.w;

    v1->m_Position.w = 1.0f;
    v2->m_Position.w = 1.0f;
    v3->m_Position.w = 1.0f;

    Vertex_rhw_Init(v1);
    Vertex_rhw_Init(v2);
    Vertex_rhw_Init(v3);
}

void BasicShader::Vertex_rhw_Init(Vertex* v)
{
    float rhw_z = 1.0f / v->m_Position.z;

    v->m_Position.z = rhw_z;
    v->m_Color *= rhw_z;
    v->m_Normal *= rhw_z;
    v->m_TexCoord *= rhw_z;
}

完成插值之后再除以\(\frac{1}{Z_t}\)

void Renderer::DrawScanline(Vertex* v, Vertex* v1, Vertex* v2)
{
    if (v1->m_Position.x > v2->m_Position.x)
        std::swap(v1, v2);

    int x1 = v1->m_Position.x;
    int x2 = v2->m_Position.x;

    for (int x = x1; x < x2; x++)
    {
        float t = (float)(x - x1) / (x2 - x1);
        Vertex::Interpolate(v, *v1, *v2, t);

        //透视矫正
        float rhw_z = v->m_Position.z;
        v->m_Position.z = 1.0f / rhw_z;
        v->m_Color /= rhw_z;
        v->m_Normal /= rhw_z;
        v->m_TexCoord /= rhw_z;

        //像素着色器
        m_Shader->FragmentShader(v);

        SetPixel(v->m_Position.x, v->m_Position.y, v->m_Position.z, v->m_Color);
    }
}

这样就完成了透视矫正,最终结果如下

correct_gloor.gif

posted @ 2024-03-01 15:43  DogWealth~  阅读(61)  评论(0编辑  收藏  举报