写在前面:时隔多年,研究生还是决定入坑图形学,亦是爱好使然,也是专业所需,希望研究生三年可以在图形学领域有所建树,更希望自己可以在这个领域坚持下去。
这次文章从闫老师GAMES101的作业3开始,之前的两个作业已经完成了,但由于比较懒,所以没有写成文章出来,看看后续有没有机会补上。
作业3内容很多,题目是Pipeline and Shading,图形管线和着色,实际上基本包含了之前作业的全部知识点,先把作业要求贴出来:
话不多说,逐一开始。
1.修改rasterize_triangle函数
该函数的作用是对模型中的每个三角形进行栅格化,与作业2相比,增加了法向量(normal)、颜色(color)、纹理(texcoords)以及(shadingcoords)的插值部分,shadingcoords的作用可能一开始不是很了解,之后再详细解释一下。
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos)
{
auto v = t.toVector4();
using namespace std;
// 求三角形最小外接矩形
float x_min = INT_MAX;
float x_max = INT_MIN;
float y_min = INT_MAX;
float y_max = INT_MIN;
for (auto & v: t.v) {
x_min = min(v.x(), x_min);
x_max = max(v.x(), x_max);
y_min = min(v.y(), y_min);
y_max = max(v.y(), y_max);
}
// 逐个像素点处理
for (int x = x_min; x <= x_max; x++)
{
for (int y = y_min; y <= y_max; y++)
{
if (insideTriangle((float)x + 0.5, (float)y + 0.5, t.v))
{
// 计算重心坐标
auto abg = computeBarycentric2D((float)x + 0.5, (float)y + 0.5, t.v);
// float alpha = std::get<0>(abg);
// float beta = std::get<1>(abg);
// float gamma = std::get<2>(abg);
float alpha, gamma, beta;
tie(alpha, beta, gamma) = abg;
//z-buffer插值
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;
if (z_interpolated < depth_buf[get_index(x, y)])
{
Eigen::Vector2i p = { (float)x,(float)y};
// 颜色插值
auto interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1);
// 法向量插值
auto interpolated_normal = interpolate(alpha, beta, gamma, t.normal[0], t.normal[1], t.normal[2], 1);
// 纹理颜色插值
auto interpolated_texcoords = interpolate(alpha, beta, gamma, t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1);
// 内部点位置插值
auto interpolated_shadingcoords = interpolate(alpha, beta, gamma, view_pos[0], view_pos[1], view_pos[2], 1);
fragment_shader_payload payload(interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
payload.view_pos = interpolated_shadingcoords;
auto pixel_color = fragment_shader(payload);
set_pixel(p, pixel_color); //设置颜色
depth_buf[get_index(x, y)] = z_interpolated;//更新z值
}
}
}
}
}
求三角形最小外接矩形时,由于三角形的顶点使用Vector4f定义,所以顶点值为浮点数,求x,y最大最小值时也需要定义为float类型,才能使用min和max函数进行比较。
逐个像素点处理时,需要判断外接矩形中每一个像素点是否再三角形内,对于每一个三角形内部的点,通过重心坐标公式求出alpha,beta,gamma,在进行插值求出z-buffer的结果。
如果当前像素的z_interpolated值小于深度缓冲中的值,则需要对该像素点进行着色和处理,interpolated插值函数已经在框架中写好了,通过重心坐标得到该像素位置的颜色、纹理、法向量以及shadingcoords的插值结果。
2.修改get_pojection_matrix函数
不必多说,作业1和作业2都使用到了,投影包含两个步骤,正交投影和透视投影。
透视投影可以理解为将视锥压缩为一个立方体
正交投影包含移动、归一化,将立方体移动到原点,在归一化为[-1, 1]³的规则立方体(cubic)
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
// TODO: Use the same projection matrix from the previous assignments
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
float fovY = eye_fov / 180 * MY_PI;
float t = abs(zNear) * tan(fovY / 2);
float b = -1.0 * t;
float r = aspect_ratio * t;
float l = -1.0 * r;
// orthoMatrix
Eigen::Matrix4f orthoScaleMatrix = Eigen::Matrix4f::Identity();
orthoScaleMatrix << 2 / (r - l), 0, 0, 0,
0, 2 / (t - b), 0, 0,
0, 0, 2 / (zNear - zFar), 0,
0, 0, 0, 1;
Eigen::Matrix4f orthoShiftMatrix = Eigen::Matrix4f::Identity();
orthoShiftMatrix << 1, 0, 0, -1.0 * (r + l) / 2,
0, 1, 0, -1.0 * (t + b) / 2,
0, 0, 1, -1.0 * (zNear + zFar) / 2,
0, 0, 0, 1;
// perspectMatrix
Eigen::Matrix4f perspMatrix = Eigen::Matrix4f::Identity();
perspMatrix << zNear, 0, 0, 0,
0, zNear, 0, 0,
0, 0, zNear + zFar, -zNear * zFar,
0, 0, 1, 0;
projection = orthoScaleMatrix * orthoShiftMatrix * perspMatrix;
return projection;
}
由于openCV的坐标系标准和课程中有所不同,所以结果的图像是上下颠倒的。
3.修改phong_fragment_shader函数
Blinn-Phong反射模型中,光照强度分为三个部分:环境光、漫反射光、高光,公式如上。
环境光 = 环境光系数 * 光照强度
漫反射光 = 漫反射系数 * 到达该点的光照强度(I/r²)* 光线向量与法线夹角
高光 = 高光系数 * 到达该点的光照强度(I/r²)* 半程向量与法线夹角,p为幂数项,p越大,高光面积越小
要注意的是,计算半程向量的过程中光线向量、视线向量要进行归一化(normalize)处理;计算点乘的过程中,法线向量、光线向量、半程向量要进行归一化处理。
Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005); // ambient
Eigen::Vector3f kd = payload.color; // diffuse
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937); // specular
auto l1 = light{{20, 20, 20}, {500, 500, 500}}; // {{position}, {intensity}}
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10}; // ambient light
Eigen::Vector3f eye_pos{0, 0, 10}; // v
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.
Eigen::Vector3f l = light.position - point;
Eigen::Vector3f v = eye_pos - point;
Eigen::Vector3f h = l.normalized() + v.normalized();
Eigen::Vector3f n = normal.normalized();
auto r_square = l.dot(l);
auto ambient = ka.cwiseProduct(amb_light_intensity);
auto diffuse = kd.cwiseProduct(light.intensity / r_square) * std::max(0.0f, n.dot(l.normalized()));
auto specular = ks.cwiseProduct(light.intensity / r_square) * std::pow(std::max(0.0f, n.dot(h.normalized())), p);
result_color += (ambient + diffuse + specular);
}
return result_color * 255.f;
}
简单说一下cwiseProduct函数,该函数返回两个矩阵同位置的元素分别相乘的新矩阵。
理论上Blinn-Phong反射模型得到的结果为一个值,但为了方便矩阵计算,所以phong_fragment_shader函数返回值为Vector3f类型,是一个1×3的向量,其中每个元素值相同,均等于反射模型的结果值。
4.修改函数texture_fragment_shader,将公式中的kd替换为纹理颜色
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.
Eigen::Vector3f l = light.position - point;
Eigen::Vector3f v = eye_pos - point;
Eigen::Vector3f h = l.normalized() + v.normalized();
Eigen::Vector3f n = normal.normalized();
auto r_square = l.dot(l);
auto ambient = ka.cwiseProduct(amb_light_intensity);
auto diffuse = kd.cwiseProduct(light.intensity / r_square) * std::max(0.0f, n.dot(l.normalized()));
auto specular = ks.cwiseProduct(light.intensity / r_square) * std::pow(std::max(0.0f, n.dot(h.normalized())), p);
result_color += (ambient + diffuse + specular);
}
return result_color * 255.f;
}
与Blinn-Phong反射模型函数差别不大,仅仅是修改了kd的值,变成了纹理颜色,其余代码全部照搬。关于怎么获取纹理颜色替代kd,涉及到纹理映射的知识点,纹理说到底就是一张图(贴图),每个三角形的顶点都对应纹理贴图上的一个颜色。
不过具体的映射函数就不再我们的考虑范围之内了。
5、6分别为实现凹凸贴图和位移贴图,在课程上闫老师也没有细说实现方法,需要自己在课外额外花时间完成这两个问题,打算放到之后再详细说说这两问,关于作业3的前4个任务就到此结束,下一篇文章再继续补充(争取不鸽)。