赞
踩
本次作业的任务是填写一个旋转矩阵和一个透视投影矩阵。给定三维下三个点 v0(2.0, 0.0, −2.0), v1(0.0, 2.0, −2.0), v2(−2.0, 0.0, −2.0), 你需要将这三个点的坐标变换为屏幕坐标并在屏幕上绘制出对应的线框三角形 (在代码框架中,我们已经提供了 draw_triangle 函数,所以你只需要去构建变换矩阵即可)。简而言之,我们需要进行模型、视图、投影、视口等变换来将三角形显示在屏幕上。在提供的代码框架中,我们留下了模型变换和投影变换的部分给你去完成。
以下是你需要在 main.cpp 中修改的函数(请不要修改任何的函数名和其他已经填写好的函数,并保证提交的代码是已经完成且能运行的):
• get_model_matrix(float rotation_angle):
逐个元素地构建模型变换矩阵并返回该矩阵。在此函数中,你只需要实现三维中绕 z 轴旋转的变换矩阵,而不用处理平移与缩放。• get_projection_matrix(float eye_fov, float aspect_ratio, floatzNear, float zFar): 使用给定的参数逐个元素地构建透视投影矩阵并返回该矩阵。
• [Optional] main(): 自行补充你所需的其他操作。
当你在上述函数中正确地构建了模型与投影矩阵,光栅化器会创建一个窗口显示出线框三角形。由于光栅化器是逐帧渲染与绘制的,所以你可以使用 A 和 D 键去将该三角形绕 z 轴旋转(此处有一项提高作业,将三角形绕任意过原点的轴旋转)。当你按下 Esc 键时,窗口会关闭且程序终止。
另外,你也可以从命令行中运行该程序。你可以使用以下命令来运行和传递旋转角给程序,在这样的运行方式下,是不会生成任何的窗口,输出的结果图像会被存储在给定的文件中 (若未指定文件名,则默认存储在 output.png 中)。图像的存储位置在可执行文件旁,所以如果你的可执行文件是在 build 文件夹中,那么图像也会在该文件夹内。
命令行的使用命令如下:
./Rasterizer //循环运行程序,创建一个窗口显示,且你可以使 用A键 和D键 旋 转 三 角 形。
./Rasterizer −r 20 //运行程序并将三角形旋转20度,然后将结果存在output.png中
./Rasterizer −r 20 image.png //运行程序并将三角形旋转20度,然后将结果存在image.png中。
Eigen::Matrix4f get_model_matrix(float rotation_angle)//模型变换矩阵 { Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); // TODO: Implement this function // Create the model matrix for rotating the triangle around the Z axis. // Then return it. Eigen::Matrix4f rotation; double fangle = rotation_angle / 180 * MY_PI;//角度转弧度,便于计算 rotation << cos(fangle), -sin(fangle), 0, 0, sin(fangle), cos(fangle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1;//模型旋转矩阵(绕z轴) model = rotation * model; return model; }
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)//投影变换矩阵 { // Students will implement this function Eigen::Matrix4f projection = Eigen::Matrix4f::Identity(); // TODO: Implement this function // Create the projection matrix for the given parameters. // Then return it. Eigen::Matrix4f proj, ortho; proj << zNear, 0, 0, 0, 0, zNear, 0, 0, 0, 0, zNear + zFar, -zNear * zFar, 0, 0, 1, 0;//透视投影矩阵 double w, h, z; h = zNear * tan(eye_fov / 2) * 2; w = h * aspect_ratio; z = zFar - zNear; ortho << 2 / w, 0, 0, 0, 0, 2 / h, 0, 0, 0, 0, 2 / z, -(zFar+zNear) / 2, 0, 0, 0, 1;//正交投影矩阵,因为在观测投影时x0y平面视角默认是中心,所以这里的正交投影就不用平移x和y了 projection = ortho * proj * projection; return projection; }
这次作业本身难度并不大,更多是用来温习MVP的流程和公式,具体要点的都在代码注释里写出了,这里稍微多讲一些我对提高部分的实现,首先看到提高部分的要求:在 main.cpp 中构造一个函数,该函数的作用是得到绕任意
过原点的轴的旋转变换矩阵。Eigen::Matrix4f get_rotation(Vector3f axis, float angle)
这就很明显是lecture 04里提到过的罗德里格斯旋转公式了:
根据给定的参数写出公式即可,这里要注意齐次坐标在这个公式应用时第四维的计算结果可能会影响最终成像,要记得改过来(推导过程见附录)。
Eigen::Matrix4f get_rotation(Vector3f axis, float angle) {//任意轴旋转矩阵(罗德里格斯旋转公式,默认轴过原点) double fangle = angle / 180 * MY_PI; Eigen::Matrix4f I, N, Rod; Eigen::Vector4f axi; Eigen::RowVector4f taxi; axi << axis.x(), axis.y(), axis.z(), 0; I.Identity(); N << 0, -axis.z(), axis.y(), 0, axis.z(), 0, -axis.x(), 0, -axis.y(), axis.x(), 0, 0, 0, 0, 0, 1; Rod = cos(fangle) * I + (1 - cos(fangle)) * axi * axi.transpose() + sin(fangle) * N; Rod(3, 3) = 1;//这里要注意,非齐次坐标的公式应用在齐次坐标上时记得运算完成后把矩阵的右下角改为1,否则会导致图形比例错误 return Rod; }
但是现在我有了函数输出的矩阵,应该怎么操作才能让它真正的应用到图像上呢?
当然就是加入一个类似mvp矩阵的传递接口,在main.cpp文件中我们可以看到类似set_projection这种函数,他的作用是将内部的投影矩阵设为给定矩阵 p,并传递给光栅化器,同样的我们也可以在rasterizer.cpp加入接口函数
void rst::rasterizer::set_rodrigues(const Eigen::Matrix4f& r)
{
rodrigues = r;
}
接下来在draw函数里加入mvp矩阵的计算:
Eigen::Matrix4f mvp = projection * view * model * rodrigues;//计算mvp矩阵,因为绕任意轴旋转也是模型变换的一种,所以放在model矩阵的相邻位置
注意这步之前要在rasterizer.hpp里声明接口函数和矩阵:
class rasterizer
{
...
public:
void set_rodrigues(const Eigen::Matrix4f& r);
private:
Eigen::Matrix4f rodrigues;
...
};
最后在main.cpp的main函数里面加入旋转轴和角度的参数输入:
std::cout << "Please enter the axis and angle:" << std::endl;
std::cin >> raxis.x() >> raxis.y() >> raxis.z() >> ra;//定义罗德里格斯旋转轴和角
加入按下键盘’r’键就绕指定轴旋转的判定:
if (rflag) //如果按下r了,就开始绕给定任意轴旋转
r.set_rodrigues(get_rotation(raxis, rangle));
else
r.set_rodrigues(get_rotation({ 0,0,1 }, 0));
...
else if (key == 'r') {//按下r,再次绕给定任意轴旋转
rflag = true;
rangle += ra;
}
我们输入时设定每按一次’R’就绕z轴(0,0,1)旋转90度:
这是初始的图像:
这是按下三次‘A’(即绕z轴逆时针旋转30°)时的图像:
这是再按下四次‘D’(即顺时针旋转10°)时的图像:
这是再按下一次’R’(即逆时针旋转80°)时的图像:
可以自己尝试,比如绕(1,2,3)旋转10°的图像:
这份作业框架包含了许多功能,提前了解的话有助于我们后续的学习,为方便c++基础一般的朋友理解,给大部分代码做了注释:
int main(int argc, const char** argv) { float angle = 0;//定义角度 bool command_line = false;//定义命令行开关标志,默认为关 std::string filename = "output.png";//定义文件名,默认为output.png" Eigen::Vector3f raxis(0, 0, 1); double rangle = 0, ra; if (argc >= 3) {//接收到的参数大于三个,即检测到通过命令行传入参数时 command_line = true;//设命令行开关标志为开 angle = std::stof(argv[2]); //从命令行获取角度参数 if (argc == 4) {//接收到的参数为四个,那么说明命令行输入了文件名参数 filename = std::string(argv[3]);//从命令行获取文件名 } } rst::rasterizer r(700, 700);//设定700*700像素的光栅器视口 Eigen::Vector3f eye_pos = { 0, 0, 5 };//设定相机位置 std::vector<Eigen::Vector3f> pos{ {2, 0, -2}, {0, 2, -2}, {-2, 0, -2} };//设定三顶点位置 std::vector<Eigen::Vector3i> ind{ {0, 1, 2} };//设定三顶点序号,用于画图时确定需要处理几个顶点,这里表示的是三个顶点 auto pos_id = r.load_positions(pos); auto ind_id = r.load_indices(ind);//保存多个图形的顶点和序号,本次作业只涉及一个图形,可以不管 int key = 0;//键盘输入 int frame_count = 0;//帧序号 if (command_line) {//如果命令行开关标志为开(这一段if代码是为了应用命令行传入的参数,比如初始角度和文件名) r.clear(rst::Buffers::Color | rst::Buffers::Depth);//初始化帧缓存和深度缓存(本次作业本次作业只涉及一个图形,所以不涉及深度,可以不管) r.set_model(get_model_matrix(angle)); r.set_view(get_view_matrix(eye_pos)); r.set_projection(get_projection_matrix(45, 1, 0.1, 50));//向光栅器传入MVP矩阵 r.set_rodrigues(get_rotation(raxis, rangle)); r.draw(pos_id, ind_id, rst::Primitive::Triangle);//开始画图 cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data()); image.convertTo(image, CV_8UC3, 1.0f); cv::imwrite(filename, image);//写入文件名 return 0; } bool rflag = false; std::cout << "Please enter the axis and angle:" << std::endl; std::cin >> raxis.x() >> raxis.y() >> raxis.z() >> ra;//定义罗德里格斯旋转轴和角 while (key != 27) {//只要没有检测到按下ESC就循环(ESC的ASCII码是27) r.clear(rst::Buffers::Color | rst::Buffers::Depth); r.set_model(get_model_matrix(angle)); r.set_view(get_view_matrix(eye_pos)); r.set_projection(get_projection_matrix(45, 1, 0.1, 50)); if (rflag) //如果按下r了,就开始绕给定任意轴旋转 r.set_rodrigues(get_rotation(raxis, rangle)); else r.set_rodrigues(get_rotation({ 0,0,1 }, 0)); r.draw(pos_id, ind_id, rst::Primitive::Triangle); cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data()); image.convertTo(image, CV_8UC3, 1.0f); cv::imshow("image", image);//显示图像 key = cv::waitKey(10);//等待10号码接收键盘输入,没有输入就为空,图像不做调整,保持原状 std::cout << "frame count: " << frame_count++ << '\n';//显示当前是第几帧画面 if (key == 'a') {//按下a,逆时针旋转10° angle += 10; } else if (key == 'd') {//按下d,顺时针旋转10° angle -= 10; } else if (key == 'r') {//按下r,绕给定任意轴旋转 rflag = true; rangle += ra; } } return 0; }
#include <algorithm> #include "rasterizer.hpp" #include <opencv2/opencv.hpp> #include <math.h> #include <stdexcept> rst::pos_buf_id rst::rasterizer::load_positions(const std::vector<Eigen::Vector3f> &positions) { auto id = get_next_id(); pos_buf.emplace(id, positions); return {id}; } rst::ind_buf_id rst::rasterizer::load_indices(const std::vector<Eigen::Vector3i> &indices) { auto id = get_next_id(); ind_buf.emplace(id, indices); return {id}; } // Bresenham's line drawing algorithm // Code taken from a stack overflow answer: https://stackoverflow.com/a/16405254 void rst::rasterizer::draw_line(Eigen::Vector3f begin, Eigen::Vector3f end)//直线扫描画线,本次作业没有用到,这段代码的原理是用的中点画线算法 { auto x1 = begin.x(); auto y1 = begin.y(); auto x2 = end.x(); auto y2 = end.y();//获得传入的线段起始点坐标 Eigen::Vector3f line_color = {255, 255, 255};//设置默认线段颜色 int x,y,dx,dy,dx1,dy1,px,py,xe,ye,i; dx=x2-x1; dy=y2-y1; dx1=fabs(dx); dy1=fabs(dy); px=2*dy1-dx1; py=2*dx1-dy1; if(dy1<=dx1)//如果线段斜率的绝对值小于等于1就执行下列代码,这样区分是因为像素点坐标是整数,而如果斜率的绝对值小于1,在对线段进行采样时,每次x坐标加1,y坐标要么是加1,要么是减1,要么不变,简化了计算,而当斜率的绝对值大于1时,斜率的倒数的绝对值就小于1,就可以每次对y坐标加1,也是一样的效果 { if(dx>=0) { x=x1; y=y1; xe=x2; } else { x=x2; y=y2; xe=x1; }//这一段if else是为了保证x,y是起始点的坐标(从左到右,最左为起始点) Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f);//转换齐次坐标 set_pixel(point,line_color);//设置起始点颜色 for(i=0;x<xe;i++)//从起始点开始遍历处理线段上每个点 { x=x+1; if(px<0)//若px小于0,说明中点在线段之上,y坐标还不用加1 { px=px+2*dy1; } else { if((dx<0 && dy<0) || (dx>0 && dy>0))//根据斜率判断x坐标加1时y坐标是加还是减 { y=y+1; } else { y=y-1; } px=px+2*(dy1-dx1); } // delay(0); Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f); set_pixel(point,line_color);//将得到的新点画上颜色 } } else//如果线段斜率的绝对值大于1,基本处理与上面相似,只是是以y坐标为基准处理,不再赘述 { if(dy>=0) { x=x1; y=y1; ye=y2; } else { x=x2; y=y2; ye=y1; } Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f); set_pixel(point,line_color); for(i=0;y<ye;i++) { y=y+1; if(py<=0) { py=py+2*dx1; } else { if((dx<0 && dy<0) || (dx>0 && dy>0)) { x=x+1; } else { x=x-1; } py=py+2*(dx1-dy1); } // delay(0); Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f); set_pixel(point,line_color); } } } auto to_vec4(const Eigen::Vector3f& v3, float w = 1.0f)//转换齐次坐标 { return Vector4f(v3.x(), v3.y(), v3.z(), w); } void rst::rasterizer::draw(rst::pos_buf_id pos_buffer, rst::ind_buf_id ind_buffer, rst::Primitive type) { if (type != rst::Primitive::Triangle) { throw std::runtime_error("Drawing primitives other than triangle is not implemented yet!"); } auto& buf = pos_buf[pos_buffer.pos_id]; //根据传入的id参数获取图形顶点 auto& ind = ind_buf[ind_buffer.ind_id];//根据传入的id参数获取图形顶点序号 float f1 = (100 - 0.1) / 2.0; float f2 = (100 + 0.1) / 2.0; Eigen::Matrix4f mvp = projection * view * model * rodrigues;//计算mvp矩阵 for (auto& i : ind)//顺序处理所有图形顶点 { Triangle t; Eigen::Vector4f v[] = { mvp * to_vec4(buf[i[0]], 1.0f), mvp * to_vec4(buf[i[1]], 1.0f), mvp * to_vec4(buf[i[2]], 1.0f) };//计算获得三个顶点经过mvp转换后的坐标 for (auto& vec : v) {//齐次坐标除以第四维转常规坐标 vec /= vec.w(); } for (auto & vert : v) { vert.x() = 0.5*width*(vert.x()+1.0); vert.y() = 0.5*height*(vert.y()+1.0); vert.z() = vert.z() * f1 + f2; }//视口变换 for (int i = 0; i < 3; ++i) { t.setVertex(i, v[i].head<3>()); t.setVertex(i, v[i].head<3>()); t.setVertex(i, v[i].head<3>()); } t.setColor(0, 255.0, 0.0, 0.0); t.setColor(1, 0.0 ,255.0, 0.0); t.setColor(2, 0.0 , 0.0,255.0); rasterize_wireframe(t); } } void rst::rasterizer::rasterize_wireframe(const Triangle& t)//画出三角形的框架(三条边) { draw_line(t.c(), t.a()); draw_line(t.c(), t.b()); draw_line(t.b(), t.a()); } void rst::rasterizer::set_model(const Eigen::Matrix4f& m) { model = m; } void rst::rasterizer::set_view(const Eigen::Matrix4f& v) { view = v; } void rst::rasterizer::set_projection(const Eigen::Matrix4f& p) { projection = p; } void rst::rasterizer::set_rodrigues(const Eigen::Matrix4f& r) { rodrigues = r; } void rst::rasterizer::clear(rst::Buffers buff)//初始化,设置帧缓冲内所有像素颜色为(0,0,0),深度缓冲的所有像素深度为无限大 { if ((buff & rst::Buffers::Color) == rst::Buffers::Color) { std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0}); } if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth) { std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity()); } } rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)//根据宽高比设置帧缓冲大小和深度缓冲大小(大小就是像素个数) { frame_buf.resize(w * h); depth_buf.resize(w * h); } int rst::rasterizer::get_index(int x, int y)//根据坐标求像素在缓冲区的序号 { return (height-y)*width + x; } void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)//将屏幕像素点 (x, y) 设 为 (r, g, b) 的颜色,并写入相应的帧缓冲区位置。 { //old index: auto ind = point.y() + point.x() * width; if (point.x() < 0 || point.x() >= width || point.y() < 0 || point.y() >= height) return;//如果像素点坐标超出屏幕范围,不处理 auto ind = (height-1-point.y())*width + point.x();//这一步是根据坐标求像素在帧缓冲区的序号 frame_buf[ind] = color;//将屏幕像素点 (x, y) 设 为 (r, g, b) 的颜色,并写入相应的帧缓冲区位置。 }
首先第一步,可以将原向量S分解为与旋转轴a平行和垂直的两个向量S∥和S⊥,其中,S∥为S在a轴上的投影,自然可以表示为
S
∥
=
a
∗
(
S
⋅
a
)
,
S_∥ = a * (S·a),
S∥=a∗(S⋅a),
又因为列向量间的点乘可以表示为一个向量的转置(行向量)和另一个向量的乘积,所以
S
∥
=
a
∗
a
T
∗
S
,
S_∥ = a * a^T * S ,
S∥=a∗aT∗S,
自然也就可以得到
S
⊥
=
S
−
S
∥
=
S
−
a
∗
a
T
∗
S
接下来再关注旋转后的向量
S
R
O
T
S_{ROT}
SROT,我们也可以将它分解为与旋转轴
a
a
a平行和垂直的两个向量
S
R
O
T
∥
S_{ROT∥}
SROT∥和
S
R
O
T
⊥
S_{ROT⊥}
SROT⊥,其中
S
R
O
T
∥
S_{ROT∥}
SROT∥自然是与
S
∥
S_{∥}
S∥相等的(因为是绕轴旋转,所以平行与旋转轴的分量不会变化),所以接下来的关键就是在于求出
S
R
O
T
⊥
S_{ROT⊥}
SROT⊥了,同样的,我们可以用垂直于旋转轴
a
a
a的平面上两个互相垂直的向量
b
b
b、
c
c
c去表示它,其中一个向量是
S
⊥
S_{⊥}
S⊥,而另一个向量我们则可以利用
a
×
b
a × b
a×b得到,就此,我们得到了一个局部坐标系,他们的轴的单位向量分别为
a
a
a,
b
=
S
⊥
/
∣
S
⊥
∣
,
b = S⊥ / |S⊥|,
b=S⊥/∣S⊥∣,
c
=
a
×
b
=
a
×
S
⊥
∣
S
⊥
∣
=
a
×
S
∣
S
⊥
∣
其中
c
c
c的最后一步转换的依据为:
a
×
S
⊥
=
∣
a
∣
∗
∣
S
⊥
∣
∗
s
i
n
θ
∗
n
(因为
a
和
S
⊥
的夹角
θ
为
90
°
)
=
∣
a
∣
∗
∣
S
⊥
∣
∗
n
a
×
S
=
∣
a
∣
∗
∣
S
∣
∗
s
i
n
θ
1
∗
n
(因为
∣
S
∣
∗
s
i
n
θ
1
=
∣
S
⊥
∣
)
=
∣
a
∣
∗
∣
S
⊥
∣
∗
n
那么
S
R
O
T
⊥
S_{ROT⊥}
SROT⊥就可以表示为
S
R
O
T
⊥
=
∣
S
⊥
∣
∗
c
o
s
θ
∗
b
+
∣
S
⊥
∣
∗
s
i
n
θ
∗
c
=
∣
S
⊥
∣
∗
c
o
s
θ
∗
S
⊥
∣
S
⊥
∣
+
∣
S
⊥
∣
∗
s
i
n
θ
∗
(
a
×
b
)
=
c
o
s
θ
∗
S
⊥
+
∣
S
⊥
∣
∗
s
i
n
θ
∗
(
a
×
S
⊥
∣
S
⊥
∣
)
=
c
o
s
θ
∗
S
⊥
+
s
i
n
θ
∗
(
a
×
S
)
=
c
o
s
θ
∗
(
S
−
a
∗
a
T
∗
S
)
+
s
i
n
θ
∗
(
a
×
S
)
=
c
o
s
θ
∗
(
S
−
a
∗
a
T
∗
S
)
+
s
i
n
θ
∗
(
R
a
∗
S
)
(
R
a
为向量
a
的叉乘矩阵)
最后我们求得
S
R
O
T
=
S
R
O
T
⊥
+
S
R
O
T
∥
=
c
o
s
θ
∗
(
S
−
a
∗
a
T
∗
S
)
+
s
i
n
θ
∗
(
R
a
∗
S
)
+
a
∗
a
T
∗
S
=
c
o
s
θ
∗
S
+
(
1
−
c
o
s
θ
)
∗
(
a
∗
a
T
∗
S
)
+
s
i
n
θ
∗
(
R
a
∗
S
)
中点算法会在后面的课程提到,想提前了解的朋友可以看下这个ppt,看看中点算法是怎么推导的:
直线扫描转换-中点算法
附上源代码,网页看不清或者想要自己运行尝试修改的朋友可以自行下载:
CSDN:【GAMES101】作业1(提高)
GITHUB:【GAMES101】作业合集
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。