任务
图形管线
作业框架代码中的draw函数实现了顶点加工和三角形组装的功能,而下面三步需要自己代码实现。
Vertex Processing
对顶点进行加工,使其变换到屏幕空间坐标。
Triangle Processing
将加工后的顶点组装成三角形,用于下一步的光栅化。
void rst::rasterizer::draw(pos_buf_id pos_buffer, ind_buf_id ind_buffer, col_buf_id col_buffer, Primitive type)
{
//从缓冲区中读取顶点坐标,索引,颜色信息
auto& buf = pos_buf[pos_buffer.pos_id];
auto& ind = ind_buf[ind_buffer.ind_id];
auto& col = col_buf[col_buffer.col_id];
float f1 = (50 - 0.1) / 2.0;
float f2 = (50 + 0.1) / 2.0;
Eigen::Matrix4f mvp = projection * view * model; //计算得到mvp矩阵
for (auto& i : ind) //遍历索引数组,对顶点进行加工
{
Triangle t;
//这里对顶点进行mvp变换,并将结果存入v
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)
};
//因为得到的结果w值是原本的z值,所以除以w才是变换后在[-1,1]^3的空间中的坐标
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>());
}
//从颜色缓冲区中读取颜色
auto col_x = col[i[0]];
auto col_y = col[i[1]];
auto col_z = col[i[2]];
//为三角形的每个顶点附上颜色数据
t.setColor(0, col_x[0], col_x[1], col_x[2]);
t.setColor(1, col_y[0], col_y[1], col_y[2]);
t.setColor(2, col_z[0], col_z[1], col_z[2]);
//光栅化三角形
rasterize_triangle(t);
}
}
Rasterization
将三角形光栅化得到片元,这些片元最终会被渲染成屏幕上的像素。
FargmentProcessing
紧随光栅化(Rasterization)之后。在Fragment Processing阶段,主要对光栅化生成的每个片元(Fragment)进行进一步的处理,以确定其最终的颜色、深度和其他属性。而这里的各种属性可以根据线性插值和透视矫正插值得出。作业里要实现的是透视矫正插值。透视矫正插值需要用到重心坐标。重心坐标的计算作业框架中已经直接给出函数,直接调用即可。
经过透视变换后,三角形内所有的点分布都会发生变化,因此采用线性插值得到的结果是错误的,采用透视矫正插值才能得到正确的结果。
z坐标的透视矫正插值:
任意属性的透视矫正插值:
插值完成后利用Z-buffer判断该点是否能够看见。
该部分代码在下面的SSAA里。
Framebuffer Opeations
main.cpp这里通过框架的cv库,将frame_buffer的内容写入图片,从而得到最终的图片。
cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
image.convertTo(image, CV_8UC3, 1.0f);
cv::cvtColor(image, image, cv::COLOR_RGB2BGR);
cv::imwrite(filename, image);
提高项 SSAA
易错点:在光栅化的时候直接将像素分成四份并计算均值,这样子会导致每个三角形都独立计算,不考虑其他三角形,从而产生黑边。
解决方法:在Rasterizer.h里根据原先定义好的frame_buffer和depth_buffer,自己定义一个四倍大小的SSAA_frame_buffer和SSAA_depth_buffer,对三角形光栅化的时候不对frame_buffer,全都采用SSA_frame_buffer和SSAA_depth_buffer,等到要马上要输出图像的时候,再将SSAA_frame_buffer里的像素分组求平均再赋给frame_buffer输出。
std::vector<Eigen::Vector3f> frame_buf;
std::vector<float> depth_buf;
std::vector<Eigen::Vector3f> SSAA_frame_buf;
std::vector<float> SSAA_depth_buf;
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
//确定三角形的包围盒
int maxX,minX,maxY,minY;
maxX = (int)std::ceil(std::max(v[0].x(),std::max(v[1].x(),v[2].x())));
minX = (int)std::floor(std::min(v[0].x(),std::min(v[1].x(),v[2].x())));
maxY = (int)std::ceil(std::max(v[0].y(),std::max(v[1].y(),v[2].y())));
minY = (int)std::floor(std::min(v[0].y(),std::min(v[1].y(),v[2].y())));
//遍历三角形包围盒中的每个像素
for(int x =minX;x<maxX;x++){
for(int y=minY;y<maxY;y++){
int cnt = 0;
for(float i = 0.25;i<=0.75001;i+=0.5){
for(float j = 0.25;j<=0.75001;j+=0.5){
if(insideTriangle((float)x+i,(float)y+j,t.v)==true){
//计算重心坐标
auto temp = computeBarycentric2D((float)x+i,(float)y+j,t.v);
const float alpha = std::get<0>(temp);
const float beta = std::get<1>(temp);
const float gamma = std::get<2>(temp);
const float Za = v[0].z();
const float Zb = v[1].z();
const float Zc = v[2].z();
float finalZ = 1.0/(alpha/Za+beta/Zb+gamma/Zc); //这里采用了仅针对z轴的插值形式,任意属性插值在下个作业中
//判断是否离相机最近,最近则更新片元
if(SSAA_depth_buf[4 * get_index(x,y) + cnt]>finalZ ){
Eigen::Vector3f color = t.getColor();
Eigen::Vector3f pixelPoint((float)x,(float)y,finalZ);
SSAA_depth_buf[4 * get_index(x,y) + cnt]=finalZ;
SSAA_frame_buf[4 * get_index(x,y) + cnt]=color;
}
}
cnt++;
}
}
}
}
//将SSAA采样后的结果求平均赋给frame_buf
for(int x=minX;x<maxX;x++){
for(int y=minY;y<maxY;y++){
Eigen::Vector3f finalColor =( SSAA_frame_buf[4 * get_index(x,y) + 0] +
SSAA_frame_buf[4 * get_index(x,y) + 1] +
SSAA_frame_buf[4 * get_index(x,y) + 2] +
SSAA_frame_buf[4 * get_index(x,y) + 3] ) / 4.0;
set_pixel( {(float)x,(float)y,0.0} , finalColor );
}
}
}
结果
采用SSAA的结果
不考虑其他三角形时产生的黑边效果