从0开始做一个软渲染器——透视投影和投影矫正
从0开始做一个软渲染器——透视投影和投影矫正
已经做了一段时间了,一直都没记录。最近实现了一个透视投影的相机,从这一部分记录。
项目地址:https://github.com/DogWealth/PIRenderer
需要注意的是:
- 以下代码的运算都将向量考虑成行向量,进行从左往右的乘法运算。相比列向量,对应的相乘矩阵需要进行转置
- 右手坐标系
透视投影
参考文章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});
}
结果如下,地板被严重扭曲,这是因为透视插值没有进行矫正
透视矫正
参考资料:
- 透视矫正插值 Perspective-Correct Interpolation - straywriter - 博客园 (cnblogs.com)
- 【软渲染】六 光栅化二:属性插值和透视投影矫正_哔哩哔哩_bilibili
这里要记住的插值公式如下:
\[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);
}
}
这样就完成了透视矫正,最终结果如下