引言
- 在学习GAMES101的路上,我总是会心血来潮,想要做出一个3D渲染器。所谓3D渲染器,就是可以加载模型,并且通过调整参数来渲染出不同的视觉效果。
- 最近看视频看到了第9课,想着做个作业,于是通过作业1接触到了Eigen库,就不可停止的开始了我的3D渲染器制作之路。
- 本文仅用于记录3D渲染器1.0开发心得,欢迎所有大佬大神批评指教!
变换矩阵的计算
可参考GAMES101的知识点。
平移矩阵
任何矩阵乘以单位矩阵都等于它本身,因此我们将平移变换矩阵trsModel初始化为单位矩阵。
变换矩阵第4列前三个数表示平移,我们根据平移量trs设置它即可。
最终返回的应该是原变换矩阵model经过平移变换trs后得到的新模型矩阵trs*model
// 平移后产生新模型矩阵
Eigen::Matrix4f get_model_matrix_translation(Eigen::Matrix4f model, Eigen::Vector3f trs)
{Eigen::Matrix4f trsModel = Eigen::Matrix4f::Identity();for (int i = 0; i < 3; i++)trsModel(i, 3) = trs(i);return trsModel * model;
}
缩放矩阵
缩放矩阵很简单,设置对角线上前三个元素为对应轴的缩放倍率即可。
需要注意的一件事是,如果我们要把x轴缩放0.5倍,我们通常会传入的scale_size为(1,0,0),也就是说我们默认认为传入0的缩放倍率表示不进行缩放,因此我们需要判断scale_size的分量如果为0则设置为1(缩放倍率为1即不缩放)。但是scale_size为浮点数,浮点数比较相等无法精确,因此我们判断scale_size是否接近0,如果非常接近0,则对应轴不进行缩放。
// 缩放后产生新模型矩阵
Eigen::Matrix4f get_model_matrix_scale(Eigen::Matrix4f model, Eigen::Vector3f scale_size)
{Eigen::Matrix4f scaleModel = Eigen::Matrix4f::Identity();for (int i = 0; i < 3; i++)scaleModel(i, i) = ( abs(scale_size(i) - 0) > 0.1 ? scale_size(i) : 1);return scaleModel * model;
}
镜像矩阵
其实镜像矩阵就是缩放矩阵,只不过缩放倍率为负数,如对x轴缩放倍率为-1,就变成了关于x轴对称。因此下面的代码和缩放矩阵一样。
镜像矩阵我们可以认为不进行缩放,即倍率只能为1或-1,因此我们将参数axle设为整数int类型,这样判断其值是否为0而不进行操作,也很简单。
// 镜像后产生新模型矩阵
Eigen::Matrix4f get_model_matrix_Reflection(Eigen::Matrix4f model, Eigen::Vector3i reflection_axle)
{Eigen::Matrix4f reflectionModel = Eigen::Matrix4f::Identity();for (int i = 0; i < 3; i++)reflectionModel(i, i) = (reflection_axle(i) != 0 ? reflection_axle(i) : 1);Eigen::Matrix4f newModle = reflectionModel * model;return newModle;
}
旋转矩阵
- 可参考GAMES101知识点。
绕X轴旋转
// 旋转后产生新模型矩阵
Eigen::Matrix4f get_model_matrix_RotateX(Eigen::Matrix4f model, float rotation_angle)
{Eigen::Matrix4f rotateModel = Eigen::Matrix4f::Identity();rotateModel(1, 1) = cos(rotation_angle);rotateModel(1, 2) = -sin(rotation_angle);rotateModel(2, 1) = -rotateModel(1, 2);rotateModel(2, 2) = rotateModel(1, 1);return rotateModel * model;
}
绕Y轴旋转
Eigen::Matrix4f get_model_matrix_RotateY(Eigen::Matrix4f model, float rotation_angle)
{Eigen::Matrix4f rotateModel = Eigen::Matrix4f::Identity();rotateModel(0, 0) = cos(rotation_angle);rotateModel(0, 2) = sin(rotation_angle);rotateModel(2, 0) = -rotateModel(0, 2);rotateModel(2, 2) = rotateModel(0, 0);return rotateModel * model;
}
绕Z轴旋转
Eigen::Matrix4f get_model_matrix_RotateZ(Eigen::Matrix4f model, float rotation_angle)
{Eigen::Matrix4f rotateModel = Eigen::Matrix4f::Identity();rotateModel(0, 0) = cos(rotation_angle);rotateModel(0, 1) = -sin(rotation_angle);rotateModel(1, 0) = -rotateModel(0, 1);rotateModel(1, 1) = rotateModel(0, 0);return rotateModel * model;
}
绕任意轴旋转
绕任意轴旋转根据闫老师推导的公式即可,可参考绕3D空间任意轴旋转。
要注意,这里axis表示的旋转轴是指过原点(0,0,0)方向为axis的轴。
Eigen::Matrix4f get_model_matrix_Rotate(Eigen::Matrix4f model,Eigen::Vector3f axis, float rotation_angle)
{axis /= axis.sum();Eigen::Matrix4f rotateModel = Eigen::Matrix4f::Identity();rotateModel << (1 - cos(rotation_angle)) * (axis(0) * axis(0)) + cos(rotation_angle),(1 - cos(rotation_angle))* (axis(0) * axis(1)) - axis(2) * sin(rotation_angle),(1 - cos(rotation_angle))* (axis(0) * axis(2)) + axis(1) * sin(rotation_angle),(1 - cos(rotation_angle))* (axis(0) * axis(1)) + axis(2) * sin(rotation_angle),(1 - cos(rotation_angle))* (axis(1) * axis(1)) + cos(rotation_angle),(1 - cos(rotation_angle))* (axis(1) * axis(2)) - axis(0) * sin(rotation_angle),(1 - cos(rotation_angle))* (axis(0) * axis(2)) - axis(1) * sin(rotation_angle),(1 - cos(rotation_angle))* (axis(1) * axis(2)) + axis(0) * sin(rotation_angle),(1 - cos(rotation_angle))* (axis(2) * axis(2)) + cos(rotation_angle);return rotateModel * model;
}
当旋转轴axis的起点pos不为原点时,我们先对原模型矩阵进行平移,将pos移到原点(0,0,0)上,使得旋转轴axis过原点。然后在原点绕axis轴旋转angle角度,再将pos移回原来的位置。
igen::Matrix4f get_model_matrix_Rotate(Eigen::Matrix4f model, Eigen::Vector3f pos,Eigen::Vector3f axis, float rotation_angle)
{Eigen::Matrix4f trsed_1 = get_model_matrix_translation(model, -(pos));Eigen::Matrix4f rotated = get_model_matrix_Rotate(trsed_1, axis, rotation_angle);Eigen::Matrix4f trsed_2 = get_model_matrix_translation(rotated, pos);return trsed_2;
}
观察矩阵的计算
可参考GAMES101知识点。
要注意的变换的顺序,我们要先将摄像机平移到原点,然后再将摄像机角度调整为标准角度。
// 获得观察矩阵
Eigen::Matrix4f get_view_matrix(Eigen::Vector3f eye_pos, Eigen::Vector3f front,Eigen::Vector3f up)
{Eigen::Matrix4f view = Eigen::Matrix4f::Identity();Eigen::Matrix4f translate;translate << 1, 0, 0, -eye_pos[0], 0, 1, 0, -eye_pos[1], 0, 0, 1,-eye_pos[2], 0, 0, 0, 1;Eigen::Matrix4f rotate;Eigen::Vector3f gxt = front.cross(up);rotate << gxt[0], gxt[1], gxt[2], 0,up[0], up[1], up[2], 0,-front[0], -front[1], -front[2], 0,0, 0, 0, 1;view = rotate * translate * view;return view;
}
投影矩阵的计算
看参考投影矩阵的计算。
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,float zNear, float zFar)
{Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();//完全按照课程里的参数取值,这道题的相机就在(0,0,0),因此远近平面都是在z的负半轴,所以n和f的值应该为负float f, n, l, r, b, t, fov;fov = eye_fov / 180 * MY_PI;n = -zNear; //znear是正值f = zFar;t = tan(fov/2) * zNear;b = -t;r = t * aspect_ratio;l = -r;//透视->正交 perspective->orthographicEigen::Matrix4f pertoorth;pertoorth << n, 0, 0, 0,0, n, 0, 0,0, 0, n + f, -n*f,0, 0, 1, 0;//正交——移动Eigen::Matrix4f orth1;orth1 << 1, 0, 0, -(r + l) / 2,0, 1, 0, -(t + b) / 2,0, 0, 1, -(n + f) / 2,0, 0, 0, 1;//正交——缩放Eigen::Matrix4f orth2;orth2 << 2 / (r - l), 0, 0, 0,0, 2 / (t - b), 0, 0,0, 0, 2 / (n - f), 0,0, 0, 0, 1;projection = orth2*orth1 * pertoorth;//注意矩阵顺序,变换从右往左依次进行return projection;
}
Element图元
#pragma once
#include <Eigen/Dense>
#include <Eigen/Core>
// 图元基类
class Element
{
public:// 枚举值记录图元的类型enum Type{POINT,LINE,TRIANGLE};Element() {};Type type;
};
// 图元-点
class Point :Element
{
public:Point(){}// 点具有一个齐次坐标和一个四维颜色Eigen::Vector4f position;Eigen::Vector4f color;Point(Eigen::Vector4f position, Eigen::Vector4f color){this->position = position;this->color = color;this->type = POINT;}
};
// 图元-线
class Line :Element
{
public:Line(){}// 线具有两个齐次坐标和一个四维颜色Eigen::Vector4f position[2];Eigen::Vector4f color;Line(Eigen::Vector4f position[2], Eigen::Vector4f color){this->position[0] = position[0];this->position[1] = position[1];this->color = color;this->type = LINE;}
};
class Triangle :Element
{
public:Triangle(){}// 三角形具有三个齐次坐标和一个四维颜色Eigen::Vector4f position[3];Eigen::Vector4f color;Triangle(Eigen::Vector4f position[3], Eigen::Vector4f color){this->position[0] = position[0];this->position[1] = position[1];this->position[2] = position[2];this->color = color;this->type = TRIANGLE;}
};
ScreenWindow
#pragma once
#include <Eigen/Dense>
#include <Eigen/Core>
#include "Element.h"
#include "MatrixTransformation.h"
#include <graphics.h>// 定义屏幕的最大像素值(VS STDIO需要设置堆栈保留大小很大才可运行)
#define MAX_SCREEN_WIDTH 1920
#define MAX_SCREEN_HEIGHT 1080// 定义颜色缓冲(借鉴OpenGL)
class ColorBuffer
{
public:// 颜色缓冲具有大小int row, line;ColorBuffer(){}// 小白不会动态申请空间,直接申请最大的数组,存储每个像素的四维颜色值Eigen::Vector4f buffer[MAX_SCREEN_WIDTH][MAX_SCREEN_HEIGHT];// 存储清空颜色缓冲所用颜色(借鉴OpenGL)Eigen::Vector4f clearColor;ColorBuffer(int row, int line){this->row = row;this->line = line;clearColor << 0, 0, 0, 1;}// 清空颜色缓冲void clear(){for (int i = 0; i < row; i++)for (int j = 0; j < line; j++)buffer[i][j] = clearColor;}
};
// 定义深度缓冲(借鉴OpenGL)
class DepthBuffer
{
public:int row, line;DepthBuffer(){}// 对每个像素深度缓冲存储的一个浮点数深度值float buffer[MAX_SCREEN_WIDTH][MAX_SCREEN_HEIGHT];// 清空深度缓冲的值(构造函数中设置其为负无穷大)float clearDepth;DepthBuffer(int row, int line){this->row = row;this->line = line;clearDepth = -FLT_MAX;}// 清空深度缓冲void clear(){for (int i = 0; i < row; i++)for (int j = 0; j < line; j++)buffer[i][j] = clearDepth;}
};// 着色类(主要负责使用图元更新缓冲区)
class Shader
{
public:// 定义着色器中的图元数据(包括图元数量和原始数据)int points, lines, triangles;Point point[100];Line line[100];Triangle triangle[100];// 定义变换矩阵Eigen::Matrix4f model;Eigen::Matrix4f view;Eigen::Matrix4f projection;Shader(){}Shader(int points, int lines, int triangles){for (int i = 0; i < points; i++){this->point[i] = Point();this->line[i] = Line();this->triangle[i] = Triangle();}}// 更新颜色缓冲区void Draw(ColorBuffer &colorBuffer,int width, int height){// 创建新的三角形数组,记录模型中三角形经过变换后的位置Triangle triangle[100];// 对每一个三角形for (int i = 1; i <= triangles; i++){// 先计算出三角形通过 模型、视图、投影变换后的顶点位置for (int j = 0; j < 3; j++)triangle[i].position[j] = projection * view * model * this->triangle[i].position[j];// 对于屏幕上的每个像素进行光栅化for (int j = 0; j < width; j++){for (int k = 0; k < height; k++){// 创建pos记录像素位置,position记录三角形变换后的顶点位置(为了后面三维做叉积方便在此转换一下)Eigen::Vector3f pos(j, k, 0);Eigen::Vector3f position[3];// 提取出三角形顶点的三维坐标for (int r = 0; r < 3; r++){position[r][0] = triangle[i].position[r][0];position[r][1] = triangle[i].position[r][1];position[r][2] = triangle[i].position[r][2];}// 判断像素是否在三角形中bool isIn = isInside(pos, position);// 如果在就更新颜色缓冲的颜色值if (isIn)colorBuffer.buffer[j][k] = this->triangle[i].color;}}}}// 判断像素点pos是否在三角形顶点position中bool isInside(Eigen::Vector3f pos, Eigen::Vector3f position[3]){// 三次叉乘Eigen::Vector3f res1 = (pos - position[0]).cross(position[1] - position[0]);Eigen::Vector3f res2 = (pos - position[1]).cross(position[2] - position[1]);Eigen::Vector3f res3 = (pos - position[2]).cross(position[0] - position[2]);// 要求叉积同向if (res1[2] * res2[2] > 0 && res2[2] * res3[2] > 0 && res1[2] * res3[2] >0 )return true;elsereturn false;}
};// 屏幕控制类(借鉴glfw)
class ScreenWindow
{
public:// 屏幕的尺寸int width, height;// 清空缓冲区时是否清空颜色缓冲、深度缓冲bool clearColor, clearDepth;// 当前屏幕使用的缓冲区号int index;// 前后缓冲区ColorBuffer colorBuffer[2];DepthBuffer depthBuffer[2];// shader负责处理图元更新缓冲Shader shader;ScreenWindow(){}ScreenWindow(int width, int height){this->index = 0;this->width = width;this->height = height;colorBuffer[0] = ColorBuffer(width, height);colorBuffer[1] = ColorBuffer(width, height);depthBuffer[0] = DepthBuffer(width, height);depthBuffer[0].clear();depthBuffer[1] = DepthBuffer(width, height);depthBuffer[1].clear();// 默认不清空缓冲clearColor = false;clearDepth = false;std::cout << "窗口初始化完毕。" << std::endl;// 使用EasyX绘制,初始化窗口initgraph(width, height);}// 将图元加载到颜色缓冲中(借鉴OpenGL)void Draw(){// 调用shader,使用shader中的图元更新未被渲染的!index缓冲(index要么为0,要么为1)shader.Draw(colorBuffer[!index], width, height);std::cout << "更新数据到缓冲区:" << !index << std::endl;}// 清空颜色缓冲void clearBuffer(){if (clearColor)colorBuffer[!index].clear();if (clearDepth)depthBuffer[!index].clear();std::cout << "清空缓冲区:" << !index << std::endl;}// 交换前后缓冲区(借鉴OpenGL)void swapBuffer(){// 交换缓冲区index = !index;std::cout << "交换缓冲区完毕,当前使用缓冲区:" << index << std::endl;// 绘制新替换的默认缓冲DrawScreen();}void DrawScreen(){// 使用Easy的双缓冲BeginBatchDraw();// 绘制每一个像素处的颜色值for (int i = 0; i < width; i++){for (int j = 0; j < height; j++){COLORREF color = RGB(colorBuffer[index].buffer[i][j][0],colorBuffer[index].buffer[i][j][1],colorBuffer[index].buffer[i][j][2]);putpixel(i, j, color);}}std::cout << "屏幕渲染完毕,当前使用缓冲区:" << index << std::endl;FlushBatchDraw();}
};
Main函数
#include <iostream>
#include <Eigen/Dense>
#include <Eigen/Core>
#include <graphics.h>
#include <iostream>
#include "ScreenWindow.h"
#include "Element.h"
#include "MatrixTransformation.h"int main(int argc, const char** argv)
{int width = 800, height = 600;int row = 4, line = 4;// 创建窗口ScreenWindow window(width,height);// 设置窗口选项window.colorBuffer[0].clearColor = Eigen::RowVector4f(127, 254, 212, 1);window.colorBuffer[1].clearColor = Eigen::RowVector4f(127, 254, 212, 1);window.clearColor = true;// 设置图元Eigen::Vector4f position[3];Eigen::Vector4f color;position[0] << 0.0f, 0.0f, 0.0f, 1.0f;position[1] << 240.0f, 0.0f, 0.0f, 1.0f;position[2] << 0.0f, 240.0f, 0.0f, 1.0f;color << 0, 255, 0, 1;Triangle triangle(position, color);window.shader.triangles = 1;window.shader.triangle[1] = triangle;clock_t start_time = clock();while (1){// 计算时间clock_t end_time = clock();double deltTime = static_cast<double>(end_time - start_time) / CLOCKS_PER_SEC * 1000;std::cout << std::endl << "Game Loop:" << std::endl;std::cout << "当前运行时间:" << deltTime/1000 << std::endl;std::cout << "当前使用缓冲区:" << window.index << std::endl;// 清空缓冲区window.clearBuffer();// 向Shdaer传入变换矩阵window.shader.model = Eigen::Matrix4f::Identity();window.shader.model = get_model_matrix_translation(window.shader.model,Eigen::Vector3f(300, 200, 0));Eigen::Vector3f eye_pos(0, 0, 1);Eigen::Vector3f front(0, 0, -1);Eigen::Vector3f up(0, 1, 0);window.shader.view = get_view_matrix(eye_pos, front, up);window.shader.projection = Eigen::Matrix4f::Identity();// 将图元加载到缓冲区中window.Draw();// 交换前后缓冲区window.swapBuffer();}// 释放资源closegraph(); return 0;
}