作业2主要是片段是否在屏幕上显示和处理,也就是画一个图,图是由像素组成的,需要判断哪些像素需要颜色,这样才能画出来。
首先我们需要判断哪些像素在三角形内部
判断像素是否在三角形内部,需要使用叉乘的方式,判断的方向是否一致,比如统一指向屏幕向外,还是指向屏幕向里
static bool insideTriangle(float x, float y, const Vector3f* _v)
{
int sign=-2;
for(int i=0;i<3;i++){
Eigen::Vector3f point(x,y,1.0f);
auto v1=_v[i]-_v[(i+1)%3];
auto v2=_v[i]-point;
float z_value=v1.cross(v2).z();
if(z_value==0) continue;
int pas=z_value>0?1:-1;
if(sign==-2) sign=pas;
else if(sign!=pas) return false;
}
return true;
// TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
}
然后我们需要逐像素的去判断是否该像素点需要着色,同时需要深度判断,如果该三角形在该像素点上的表现更加靠前,则我们更像该像素点的颜色。
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
float min_x=v[0].x();
float max_x=v[0].x();
float min_y=v[0].y();
float max_y=v[0].y();
for(int i=0;i<3;i++){
min_x=std::min(min_x,v[i].x());
max_x=std::max(max_x,v[i].x());
min_y=std::min(min_y,v[i].y());
max_y=std::max(max_y,v[i].y());
}//找到边界
for(int x=min_x;x<max_x;x++){
for(int y=min_y;y<max_y;y++){//per piexs
if(insideTriangle(x+0.5,y+0.5,t.v)){
auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
int idx=get_index(x,y);
if(z_interpolated<depth_buf[idx]){//在当前的像素上的最靠近我们的深度,如果小于,则应该显示这个颜色到像素上,因为可能会有多个三角形
Eigen::Vector3f p;
p<<x,y,z_interpolated;
set_pixel(p,t.getColor());
depth_buf[idx]=z_interpolated;
}
}
}
}
// TODO : Find out the bounding box of current triangle.
// iterate through the pixel and find if the current pixel is inside the triangle
// If so, use the following code to get the interpolated z value.
//auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
//float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
//float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
//z_interpolated *= w_reciprocal;
// TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
}
main中,只是加上了生成projection矩阵的逻辑
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
float zNear, float zFar)
{
// Students will implement this function
float half_eye_fov=eye_fov/2.0;
half_eye_fov=half_eye_fov/180.0*MY_PI;
float h=std::tan(half_eye_fov)*std::fabs(zNear);
float w=aspect_ratio*h;
float lh=-1.0*h;
float lw=-1.0*w;
Eigen::Matrix4f perspect;
perspect<<zNear,0,0,0,
0,zNear,0,0,
0,0,zNear+zFar,-1.0*zFar*zNear,
0,0,1,0;
Eigen::Matrix4f oth_move;
oth_move<<1,0,0,-(w+lw)/2,
0,1,0,-(h+lh)/2,
0,0,1,-(zNear+zFar)/2,
0,0,0,1;
Eigen::Matrix4f oth_chan;
oth_chan<<2/(w-lw),0,0,0,
0,2/(h-lh),0,0,
0,0,2/(zNear-zFar),0,
0,0,0,1;
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
projection=oth_chan*oth_move*perspect*projection;
// TODO: Implement this function
// Create the projection matrix for the given parameters.
// Then return it.
return projection;
}
但是这样会有一个问题,此时的三角形是倒着的
此时需要修改model矩阵,在opengl中我们得知,这个矩阵就是对我们的渲染物体进行平移旋转缩放等操作。,这里我们让它绕着z轴旋转了180度同时,沿着x轴向左平移了一段距离。(具体参考坐标系,z轴正方向指向我们,y方向向上,正方向视角向z轴时,x正方向指向右边,此处的向左为面向屏幕的参考)
Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
float c_z=std::cos(rotation_angle/180.0f*MY_PI);
float s_z=std::sin(rotation_angle/180.0f*MY_PI);
model<<c_z,-s_z,0,0,
s_z,c_z,0,0.0f,
0,0,1,0,
0,0,0,1;
Eigen::Matrix4f trans;
trans<<1.0f,0.0f,0.0f,2.0f,
0.0f,1.0f,0.0f,0.0f,
0.0f,0.0f,1.0f,0.0f,
0.0f,0.0f,0.0f,1.0f;
return trans*model;
}
然而此刻放大这个图片发现有锯齿状的现象。
反走样
MSAA与SSAA的区别
区别在于,MSAA的是直接的否定,比如说,一个像素,它的中心属于上面的三角形和下面的三角形,那么这个像素只能着色为上面三角形的颜色,这就导致,如果覆盖分像素较小,该像素就会出现黑边的现象。
SSAA不是,如果这个像素同时属于上下两个三角形,那么在该处的像素,根据每个小像素的深度属于哪个三角形得到每个小像素的值,然后在最后的绘制阶段,取所有小像素的平均,得到该处的颜色值。
SSAA
这部分课上没有介绍,这里是根据别人的笔记进行学习。
首先为什么会走样,因为采样率太低了。
那我们只需要加大采样率,是否就可以解决这个问题?
SSAA就是单纯的将像素进行拆分,然后记录每个像素的颜色。
既然采样频率低,我们就提高采样率,这里我们把一个像素均等分成4份,变成4个子样本(sub_pixel),就相当于原本一个像素只采样一次,现在我们采样4次,采样率直接提高至4倍。
针对每一个子采样点,重复母像素的流程,判定是否在三角形内、深度检测、着色,最后每个子采样点都会得到各自的颜色,母像素的颜色就是子采样点颜色的平均。(最终还是母像素进行着色)
在代码中frame_buf是存储像素颜色的缓冲区
depth_buf是存储像素深度的缓冲区
我们主要需要解决的问题就是上面的两个缓冲区的内容。代码中已经初始化了这两个buf的颜色,我们需要自己创建两个对应的buff,来存储小像素点的信息。
std::vector<Eigen::Vector3f> frame_buf;
std::vector<float> depth_buf;
std::vector<Eigen::Vector3f> fram_buf_ssaa;
std::vector<float> depth_buf_ssaa;
这里每个像素分为2*2,也就是说需要开4倍的像素内存
int mass_w=2;
int mass_h=2;
int max_count=mass_h*mass_w;
float pixel_step=1.0/mass_w;
float start_point=pixel_step/2.0;
rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
frame_buf.resize(w * h);
depth_buf.resize(w * h);
depth_buf_ssaa.resize(w*h*max_count);
fram_buf_ssaa.resize(w*h*max_count);
}
然后我们需要分别处理每一个小像素,就需要有get_index的机制,这个是为了得到唯一的小像素。
源代码中的x,y应该是x代表横向的x轴,y代表纵向的y轴。然后模拟下它的idx得到的方式,对应上我们得到idx的方式。
int rst::rasterizer::get_index(int x, int y)
{
return (height-1-y)*width + x;
}
int rst::rasterizer::get_index_ssaa(int x, int y,float x_s,float y_s)
{
int mass_height=mass_h*height;
int mass_width=mass_w*width;
int res_x=int((x_s-start_point)/pixel_step);
int res_y=int((y_s-start_point)/pixel_step);
return (mass_height-1-y*mass_h+res_y)*mass_width+x*mass_w+res_x;
}
记得修改clear
void rst::rasterizer::clear(rst::Buffers buff)
{
if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
{
std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
std::fill(frame_buf_ssaa.begin(),frame_buf_ssaa.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());
std::fill(depth_buf_ssaa.begin(), depth_buf_ssaa.end(), std::numeric_limits<float>::infinity());
}
}
然后在处理像素的时候预先处理所有的像素,存在buf中
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
float min_x=v[0].x();
float max_x=v[0].x();
float min_y=v[0].y();
float max_y=v[0].y();
for(int i=0;i<3;i++){
min_x=std::min(min_x,v[i].x());
max_x=std::max(max_x,v[i].x());
min_y=std::min(min_y,v[i].y());
max_y=std::max(max_y,v[i].y());
}//找到边界
for(int x=min_x;x<max_x;x++){
for(int y=min_y;y<max_y;y++){//per piexs
float num=0.0f;
for(float x_step=start_point;x_step<1;x_step+=pixel_step){
for(float y_step=start_point;y_step<1;y_step+=pixel_step){
if(insideTriangle(x+x_step,y+y_step,t.v)){
auto[alpha,beta,gamma]=computeBarycentric2D(x+x_step,y+y_step,t.v);
float w_reciprocal = 1.0/(alpha / v[0].w()+beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated*=w_reciprocal;
int idx=get_index_ssaa(x,y,x_step,y_step);
if(z_interpolated<depth_buf_ssaa[idx]){
depth_buf_ssaa[idx]=z_interpolated;
fram_buf_ssaa[idx]=t.getColor();
}
}
}
}
}
}
// TODO : Find out the bounding box of current triangle.
// iterate through the pixel and find if the current pixel is inside the triangle
// If so, use the following code to get the interpolated z value.
//auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
//float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
//float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
//z_interpolated *= w_reciprocal;
// TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
}
然后在draw函数中对每个大像素进行处理。
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;
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)
};
//Homogeneous division
for (auto& vec : v) {
vec /= vec.w();
}
//Viewport transformation
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);
}
for(int x=0;x<height;x++){
for(int y=0;y<width;y++){
//当前的像素的颜色,由子像素决定
Eigen::Vector3f color={0.0,0.0,0.0};
for(float x_s=start_point;x_s<1.0f;x_s+=pixel_step){
for(float y_s=start_point;y_s<1.0f;y_s+=pixel_step){
int idx=get_index_ssaa(x,y,x_s,y_s);
color+=frame_buf_ssaa[idx];
}
}
Eigen::Vector3f p;
p<<x,y,0.0f;//设置最近的距离0
set_pixel(p,color/(max_count));//设置平均颜色
}
}
}
可以看到锯齿状基本上没有了
MSAA
利用更多的采样点,也就是将一个像素分解为多个像素,此时需要判断该像素点分解为的每个小像素点是否在三角形内部,比如说将一个像素分为2x2的像素格子,如果其中有3个小像素在三角形内部则最终该像素的颜色为:
根据这个思想进行编码:
int mass_w=4;
int mass_h=4;
int max_count=mass_h*mass_w;
float pixel_step=1.0/mass_w;
float start_point=pixel_step/2.0;
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
float min_x=v[0].x();
float max_x=v[0].x();
float min_y=v[0].y();
float max_y=v[0].y();
for(int i=0;i<3;i++){
min_x=std::min(min_x,v[i].x());
max_x=std::max(max_x,v[i].x());
min_y=std::min(min_y,v[i].y());
max_y=std::max(max_y,v[i].y());
}//找到边界
for(int x=min_x;x<max_x;x++){
for(int y=min_y;y<max_y;y++){//per piexs
float num=0.0f;
for(float x_step=start_point;x_step<1;x_step+=pixel_step){
for(float y_step=start_point;y_step<1;y_step+=pixel_step){
if(insideTriangle(x+x_step,y+y_step,t.v)){
num+=1.0f;//计算在三角形内部的像素点的数量
}
}
}
if(insideTriangle(x+0.5f,y+0.5f,t.v)){//有像素分点在三角形内部
auto[alpha,beta,gamma]=computeBarycentric2D(x,y,t.v);
float w_reciprocal = 1.0/(alpha / v[0].w()+beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated*=w_reciprocal;
int idx=get_index(x,y);
if(z_interpolated<depth_buf[idx]){
Eigen::Vector3f p;
p<<x,y,z_interpolated;
set_pixel(p,t.getColor()*(num/max_count));
depth_buf[idx]=z_interpolated;
}
}
}
}
// TODO : Find out the bounding box of current triangle.
// iterate through the pixel and find if the current pixel is inside the triangle
// If so, use the following code to get the interpolated z value.
//auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
//float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
//float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
//z_interpolated *= w_reciprocal;
// TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
}
得到的结果发现,锯齿状小了很多
但是在三角形交叉处会出现黑边的问题
下图2为MSAA的处理结果,图1为不做反走样结果
图1 | 图2 |
| |
明显看出图2在三角形的重叠处有黑边
黑边问题
为什么会出现黑边呢,参考为什么出现黑边问题
也就是说,在绿色三角形的边缘位置,该位置的像素点属于绿色三角形,所以覆盖蓝色区域,但是由于像素被包含的占比较低,比如说1/100,此时就会接近黑色的颜色,导致了黑边的现象发生。
那么该如何解决这个现象呢?思考一个问题,这个像素我基于MSAA的思想,它只要整体在前面的三角形上更加靠近我们,那么他就显示这个三角形的颜色,但是边界上我希望是两个颜色的插值。
那我只需要每次处理小像素的时候,更新小像素目前的深度值,然后大像素只要小像素发生了更新,则大像素就必须发生更新。更新为每个小像素的插值。
这样我在遍历完所有三角形后,最后一个更改该大像素的小像素的三角形一定会更改完成这个大像素的值。(但是这不就是ssaa吗?哈哈哈,所以就不写了,有一个优化思路就是只是到处理边界的时候,使用ssaa的算法思想。其它情况直接使用msaa。)