games101 作业笔记(作业3)
这次作业算是比较重要的一次,算是整合了前面学的东西,不过代码框架依然有些问题,
在 rasterize_triangle
函数中插值的注释里提到顶点的 w 分量是观察空间的深度 z,这点没有任何问题,经过透视投影的变换后,w 就变成了原本的 z
v[i].w() is the vertex view space depth value z.
但是助教的常见问题中却提到 v 是 toVector4
得到的,但事实在作业三框架中的 t.v 已经是 Vector4f 了(作业二是 Vector3f),而这个函数里直接强制把 w 分量赋值成了 1
v = t.toVector4()
std::array<Vector4f, 3> Triangle::toVector4() const
{
std::array<Vector4f, 3> res;
std::transform(std::begin(v), std::end(v), res.begin(), [](auto& vec) { return Vector4f(vec.x(), vec.y(), vec.z(), 1.f); });
return res;
}
这样做的话,就导致后面透视校正时除以 v[i].w() 变的毫无意义了,而且把 v[i].w() = 1带入式子后化简就可以发现,Z = 1(因为 α+β+γ = 1),zp = α * z1 +β * z2 + γ * z3。很显然 zp 退化成了直接进行一次线性插值。而老师上课说过透视投影后重心坐标会变,直接用变换后的 z 来做插值是不对的,同样的后面其他各种属性的插值都出问题了,网上大部分人的代码都没考虑到这个问题,可能是因为实际上这点透视投影导致的误差较小,实际效果和透视校正后的插值没什么区别,所有都没发现这个问题。
那么如何正确的进行插值呢,知乎上有位大佬有详细推导过程,可以看这篇矫正透视投影插值及属性插值详解,我这里就直接放结果了
当 v[i].w() 是正确的 view space depth value z 时,作业框架提供的 Z 的计算公式就是上面那个 zp 的计算公式,而下面那个公式就是对任意属性进行插值的通用公式,其实可以发现,作业框架的 zp 就是用下面的通用公式插值出来的,至于为什么要再进行插值算一遍深度,我没找到一个特别合适的理由,这篇《GAMES101》作业框架问题详解中的解释如下
有一种解释是:如果代入正确,因为透视投影矩阵的千差万别, 分量代入的很有可能是这个点在观察(摄像机)空间的 值的某个比值 ,这里 是一个常数(比如上文给出那个矩阵对应的 就是 ),所以可以认为 代入可能不是深度值。
这段话我是没有太理解,不过其实我们用 Z 还是二次插值出来的 zp 来做深度测试,应该都不会有什么太大影响,毕竟只要前后关系保证没有问题,其他并不影响结果,不过后面其他属性的插值,我们就应该使用 Z 来代入上面的公式进行插值了,下面以颜色的插值为例,其他基本相同,就是换一下后面插值的三个属性即可
auto interpolated_color = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), t.color[0], t.color[1], t.color[2], 1/Z);
代码思路
这一次在三角形内的判定已经帮我们写好了不需要再写了,光栅化其他的基本和上次是一样的了,只是多了几个其他属性的插值,以及最后的像素变成了着色后的像素
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos)
{
// 切记这里不能调用 toVector4
auto v = t.v;
// TODO: From your HW3, get the triangle rasterization code.
int x_min = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
int x_max = ceil(std::max(std::max(v[0].x(), v[1].x()), v[2].x()));
int y_min = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
int y_max = ceil(std::max(std::max(v[0].y(), v[1].y()), v[2].y()));
for(int x=x_min; x<x_max; x++)
for(int y=y_min; y<y_max; y++)
{
if(insideTriangle(x, y, t.v))
{
// TODO: Inside your rasterization loop:
// * v[i].w() is the vertex view space depth value z.
// * Z is interpolated view space depth for the current pixel
// * zp is depth between zNear and zFar, used for z-buffer
auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
zp *= Z;
if(zp < depth_buf[get_index(x, y)])
{
depth_buf[get_index(x, y)] = zp;
// TODO: Interpolate the attributes:
auto interpolated_color = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), t.color[0], t.color[1], t.color[2], 1/Z);
auto interpolated_normal = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), t.normal[0], t.normal[1], t.normal[2], 1/Z);
auto interpolated_texcoords = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1/Z);
auto interpolated_shadingcoords = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), view_pos[0], view_pos[1], view_pos[2], 1/Z);
fragment_shader_payload payload( interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
payload.view_pos = interpolated_shadingcoords;
// Use: Instead of passing the triangle's color directly to the frame buffer, pass the color to the shaders first to get the final color;
auto pixel_color = fragment_shader(payload);
set_pixel(Eigen::Vector2i(x, y), pixel_color);
}
}
}
}
然后记得把透视投影的代码从上几次作业复制过来,然后就可 ./Rasterizer output.png normal
跑一下看看效果,因为这次没用MSAA,所以边缘有明显锯齿,另外就是我发现牛脖子上有个小绿点格格不入,不知道是什么情况,但我看作业文档样例里的牛脸上有两个异常像素点,我这边只有脖子上一个,似乎效果还更好点,不知道有没用人知道这个异常像素产生的原因
后面就是修改不同的着色器函数来实现不同效果的着色了
首先是 phong_fragment_shader,其实就是照着老师上课的 blion-pong 光照模型的公式来即可,只需要记得归一化方向即可
这边的 cwiseProduct 函数作用就是,让矩阵点乘,每个位置和对应位置的值点对点相乘,结果依旧是个矩阵,因为 eigen 里向量就是特殊的矩阵,所以同样适用(注意向量点乘返回的结果是数值,我们需要的是个向量,两者是不一样的)
还需要注意的是环境光应该只被计算一次,因此应该放在循环的外面
Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
Eigen::Vector3f eye_pos{0, 0, 10};
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
Eigen::Vector3f result_color = {0, 0, 0};
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
auto l = (light.position - point).normalized();
auto v = (eye_pos - point).normalized();
auto h = (l + v).normalized();
auto r2 = (light.position - point).dot(light.position - point);
result_color += kd.cwiseProduct(light.intensity / r2) * std::max(0.f, normal.dot(l)) + ks.cwiseProduct(light.intensity / r2) * std::pow(std::max(0.f,normal.dot(h)), p);
}
result_color += ka.cwiseProduct(amb_light_intensity);
return result_color * 255.f;
}
可以看到那个坏点依旧存在,且在所有图中都存在,文档样例里的图也都是脸上有两个坏点
材质的着色器几乎完全一样,就是把颜色改成从材质中获取,循环里面一样的
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f return_color = {0, 0, 0};
if (payload.texture)
{
// TODO: Get the texture value at the texture coordinates of the current fragment
return_color = payload.texture->getColor(payload.tex_coords.x(), payload.tex_coords.y());
}
Eigen::Vector3f texture_color;
texture_color << return_color.x(), return_color.y(), return_color.z();
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = texture_color / 255.f;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
Eigen::Vector3f eye_pos{0, 0, 10};
float p = 150;
Eigen::Vector3f color = texture_color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
Eigen::Vector3f result_color = {0, 0, 0};
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
auto l = (light.position - point).normalized();
auto v = (eye_pos - point).normalized();
auto h = (l + v).normalized();
auto r2 = (light.position - point).dot(light.position - point);
result_color += kd.cwiseProduct(light.intensity / r2) * std::max(0.f, normal.dot(l)) + ks.cwiseProduct(light.intensity / r2) * std::pow(std::max(0.f,normal.dot(h)), p);
}
result_color += ka.cwiseProduct(amb_light_intensity);
return result_color * 255.f;
}
后面两个代码就相对难理解了,不过按照注释来就行,助教也说了相关的计算推导后面会讲,这里大概了解即可
bump mapping & displacement mapping 的计算的推导日后将会在光线追踪部分详细介绍,目前请按照注释实现。
注释里也就 h(u,v) 可能不清楚是什么,不过助教的常见问题里也给了
bump mapping 部分的 h(u,v)=texture_color(u,v).norm, 其中 u,v 是 tex_coords, w,h 是 texture 的宽度与高度
然后这里的 w 和 h 其实原本是 int 类型的,如果直接拿来计算的话,前面的 1 要改成 1.0,不然就是整数除法了,不过如果像我这样先赋值到一个 float 变量上的话改不改就无所谓了
Eigen::Vector3f bump_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
Eigen::Vector3f eye_pos{0, 0, 10};
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
float kh = 0.2, kn = 0.1;
// TODO: Implement bump mapping here
// Let n = normal = (x, y, z)
// Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
// Vector b = n cross product t
// Matrix TBN = [t b n]
// dU = kh * kn * (h(u+1/w,v)-h(u,v))
// dV = kh * kn * (h(u,v+1/h)-h(u,v))
// Vector ln = (-dU, -dV, 1)
// Normal n = normalize(TBN * ln)
float x = normal.x();
float y = normal.y();
float z = normal.z();
auto t = Eigen::Vector3f(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
auto b = normal.cross(t);
Eigen::Matrix3f TBN;
TBN.col(0) = t;
TBN.col(1) = b;
TBN.col(2) = normal;
float u = payload.tex_coords.x();
float v = payload.tex_coords.y();
float w = payload.texture->width;
float h = payload.texture->height;
float dU = kh * kn * (payload.texture->getColor(u+1.0/w,v).norm() - payload.texture->getColor(u,v).norm());
float dV = kh * kn * (payload.texture->getColor(u,v+1.0/h).norm() - payload.texture->getColor(u,v).norm());
Eigen::Vector3f ln(-dU, -dV, 1);
normal = (TBN * ln).normalized();
Eigen::Vector3f result_color = {0, 0, 0};
result_color = normal;
return result_color * 255.f;
}
总体效果不错,就是左前腿上方出现了疑似摩尔纹?
位移贴图着色器和凹凸贴图基本完全一样就是多了行修改 point
Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
Eigen::Vector3f eye_pos{0, 0, 10};
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
float kh = 0.2, kn = 0.1;
// TODO: Implement displacement mapping here
// Let n = normal = (x, y, z)
// Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
// Vector b = n cross product t
// Matrix TBN = [t b n]
// dU = kh * kn * (h(u+1/w,v)-h(u,v))
// dV = kh * kn * (h(u,v+1/h)-h(u,v))
// Vector ln = (-dU, -dV, 1)
// Position p = p + kn * n * h(u,v)
// Normal n = normalize(TBN * ln)
float x = normal.x();
float y = normal.y();
float z = normal.z();
auto t = Eigen::Vector3f(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
auto b = normal.cross(t);
Eigen::Matrix3f TBN;
TBN.col(0) = t;
TBN.col(1) = b;
TBN.col(2) = normal;
float u = payload.tex_coords.x();
float v = payload.tex_coords.y();
float w = payload.texture->width;
float h = payload.texture->height;
float dU = kh * kn * (payload.texture->getColor(u+1.0/w,v).norm() - payload.texture->getColor(u,v).norm());
float dV = kh * kn * (payload.texture->getColor(u,v+1.0/h).norm() - payload.texture->getColor(u,v).norm());
Eigen::Vector3f ln(-dU, -dV, 1);
point += kn * normal * payload.texture->getColor(u,v).norm();
normal = (TBN * ln).normalized();
Eigen::Vector3f result_color = {0, 0, 0};
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
auto l = (light.position - point).normalized();
auto v = (eye_pos - point).normalized();
auto h = (l + v).normalized();
auto r2 = (light.position - point).dot(light.position - point);
result_color += kd.cwiseProduct(light.intensity / r2) * std::max(0.f, normal.dot(l)) + ks.cwiseProduct(light.intensity / r2) * std::pow(std::max(0.f,normal.dot(h)), p);
}
return result_color * 255.f;
}
前几张图我和文档中的样例图片基本没区别,唯独这张图似乎暗处更暗一些,看上去更真实了一点,不清楚是透视校正的结果还是其他 bug 导致的莫名优化,但反正效果不错
提高
双线性插值参考课上的公式即可
Eigen::Vector3f getColorBilinear(float u, float v)
{
auto u_img = u * width;
auto v_img = (1 - v) * height;
float u_min = std::max(0.f, std::floor(u_img));
float u_max = std::min((float)width, std::ceil(u_img));
float v_min = std::max(0.f, std::floor(v_img));
float v_max = std::min((float)height, std::ceil(v_img));
auto u00 = image_data.at<cv::Vec3b>(v_min, u_min);
auto u01 = image_data.at<cv::Vec3b>(v_max, u_min);
auto u10 = image_data.at<cv::Vec3b>(v_min, u_max);
auto u11 = image_data.at<cv::Vec3b>(v_max, u_max);
auto u0 = u00 + (u_img - u_min) * (u10 - u00);
auto u1 = u01 + (u_img - u_min) * (u11 - u01);
auto color = u0 + (v_img - v_min) * (u1 - u0);
return Eigen::Vector3f(color[0], color[1], color[2]);
}
不过效果需要小一些的纹理图才行,不能直接改分辨率,那样只是模糊化,正确的方法应该用那张 svg 图改成小尺寸,因为 svg 图是矢量图缩放是不会丢失细节的,然后转成 png,下面是改成400*400的纹理图的对比效果
效果可能不太明显,上面的是双线性插值的,可以看到鼻子眼睛和蹄子的分界上杂乱的像素更少一点,但少的不多
最后是更换模型,其实没啥好说的,就是改一下路径,把main函数里所有的路径和文件名都改成对应的就行了,但给其他几个模型或多或少都有问题,直接用会有许多bug,我这边用下来是只有 cube 是最正常的
rock 似乎是里镜头太近了还是什么其他的,一部分在视线外面,如果光栅化的时候没有考虑越界情况的话,就会报错了,如果想显示在中间的话就需要调节摄像机位置和视野角了。
crate 的模型有问题,是四个顶点表示的,如果不修改模型就会是这个样子
可以参考论坛里这个帖子 作业3换模型—crate出大问题,修改obj文件即可修复,修改obj后效果如下
最后还有兔子,似乎是离摄像头太远了?非常非常小,我也没尝试修改