总览
在这次编程任务中,我们会进一步模拟现代图形技术。我们在代码中添加了Object Loader (用于加载三维模型), Vertex Shader 与 Fragment Shader ,并且支持了纹理映射。
而在本次实验中,你需要完成的任务是:
- 修改函数
rasterize_triangle(const Triangle& t) in rasterizer.cpp: 在此处实现与作业 2 类似的插值算法,实现法向量、颜色、纹理颜色的插值。 - 修改函数
get_projection_matrix() in main.cpp: 将你自己在之前的实验中实现的投影矩阵填到此处,此时你可以运行 ./Rasterizer output.png normal 来观察法向量实现结果。 - 修改函数
phong_fragment_shader() in main.cpp: 实现 Blinn-Phong 模型计算 Fragment Color. - 修改函数
texture_fragment_shader() in main.cpp: 在实现 Blinn-Phong的基础上,将纹理颜色视为公式中的 kd,实现 TextureShading Fragment Shader. - 修改函数
bump_fragment_shader() in main.cpp: 在实现 Blinn-Phong 的基础上,仔细阅读该函数中的注释,实现 Bump mapping. - 修改函数
displacement_fragment_shader() in main.cpp: 在实现 Bump mapping 的基础上,实现 displacement mapping
开始编写
编译与使用
在课程提供的虚拟机上,下载本次实验的基础代码之后,请在 SoftwareRasterizer 目录下按照如下方式构建程序:
mkdir build
cd ./build
cmake ..
make
这将会生成命名为 Rasterizer 的可执行文件。使用该可执行文件时,你传入的第二个参数将会是生成的图片文件名,而第三个参数可以是如下内容:
texture : 使用代码中的 texture shader. 使用举例: ./Rasterizer output.png texture normal : 使用代码中的 normal shader. 使用举例: ./Rasterizer output.png normal phong : 使用代码中的 blinn-phong shader. 使用举例: ./Rasterizer output.png phong bump : 使用代码中的 bump shader. 使用举例: ./Rasterizer output.png bump displacement : 使用代码中的 displacement shader. 使用举例: ./Rasterizer output.png displacement 当你修改代码之后,你需要重新 make 才能看到新的结果。
框架代码说明
相比上次实验,我们对框架进行了如下修改:
- 我们引入了一个第三方.obj 文件加载库来读取更加复杂的模型文件,这部分库文件在 OBJ_Loader.h file. 你无需详细理解它的工作原理,只需知道这个库将会传递给我们一个被命名被 TriangleList 的 Vector,其中每个三角形都有对应的点法向量与纹理坐标。此外,与模型相关的纹理也将被一同加载。
注意:如果你想尝试加载其他模型,你目前只能手动修改模型路径。 - 我们引入了一个新的 Texture 类以从图片生成纹理,并且提供了查找纹理颜色的接口:
Vector3f getColor(float u, float v) - 我们创建了 Shader.hpp 头文件并定义
fragment_shader_payload ,其中包括了 Fragment Shader 可能用到的参数。目前 main.cpp 中有三个 Fragment Shader,其中 fragment_shader 是按照法向量上色的样例 Shader,其余两个将由你来实现。 - 主渲染流水线开始于
rasterizer::draw(std::vector<Triangle> &TriangleList) .我们再次进行一系列变换,这些变换一般由 Vertex Shader 完成。在此之后,我们调用函数 rasterize_triangle . rasterize_triangle 函数与你在作业 2 中实现的内容相似。不同之处在于被设定的数值将不再是常数,而是按照 Barycentric Coordinates 对法向量、颜色、纹理颜色与底纹颜色 (Shading Colors) 进行插值。回忆我们上次为了计算z value 而提供的 [alpha, beta, gamma] ,这次你将需要将其应用在其他参数的插值上。你需要做的是计算插值后的颜色,并将 Fragment Shader 计算得到的颜色写入 framebuffer ,这要求你首先使用插值得到的结果设置 fragment shader payload ,并调用 fragment shader 得到计算结果。
运行与结果
在你按照上述说明将上次作业的代码复制到对应位置,并作出相应修改之后(请务必认真阅读说明),你就可以运行默认的 normal shader 并观察到如下结果: 实现 Blinn-Phong 反射模型之后的结果应该是: 实现纹理之后的结果应该是: 实现 Bump Mapping 后,你将看到可视化的凹凸向量: 实现 Displacement Mapping 后,你将看到如下结果:
作业代码
插值
使用重心坐标插值出对应像素的法向量、颜色、纹理坐标、以及其三维空间中的坐标,然后计算经过着色模型之后的颜色作为该像素的颜色
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos)
{
auto v = t.toVector4();
int boundingbox_x_left = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));
int boundingbox_x_right = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));
int boundingbox_y_left = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));
int boundingbox_y_right = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));
for (auto x=boundingbox_x_left; x<=boundingbox_x_right; ++x)
for (auto y=boundingbox_y_left; y<=boundingbox_y_right; ++y) {
if (insideTriangle(x, y, t.v)) {
auto[alpha, beta, gamma] = computeBarycentric2D(1.0*x+0.5, 1.0*y+0.5, 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)]) {
auto interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1.0);
auto interpolated_normal = interpolate(alpha, beta, gamma, t.normal[0], t.normal[1], t.normal[2], 1.0);
auto interpolated_texcoords = interpolate(alpha, beta, gamma, t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1.0);
auto interpolated_shadingcoords = interpolate(alpha, beta, gamma, view_pos[0], view_pos[1], view_pos[2], 1.0);
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);
depth_buf[get_index(x,y)] = zp;
set_pixel(Eigen::Vector2i(x, y), pixel_color);
}
}
}
}
这里三维空间中的坐标 payload.view_pos 表示经过模型变换与视图变换之后的物体三维坐标插值。这个三维空间坐标插值主要用于着色模型中,计算
l
?
\vec{l}
l
与
v
?
\vec{v}
v
,至于为什么二维重心可以插值三维坐标,其实这里是一种近似,因为误差不是很大,就直接使用二维重心坐标插值三维坐标了。需要注意的是在 rst::rasterizer::draw 代码中,对于初始法向量要经过 inv_trans 变换,这是因为开始给出的法向量是未经过模型变换与视图变换的,因此变换的法向量
n
′
?
\vec{n'}
n′
要满足
v
′
?
T
?
n
′
?
=
0
\vec{v'}^{T}*\vec{n'}=0
v′
T?n′
=0,而
v
?
T
?
n
?
=
0
\vec{v}^{T}*\vec{n}=0
v
T?n
=0,所以
(
v
i
e
w
?
m
o
d
e
l
?
v
?
)
T
?
(
M
?
n
?
)
=
0
(view*model*\vec{v})^{T}*(M*\vec{n})=0
(view?model?v
)T?(M?n
)=0,我们就是要知道这个
M
M
M,而上面的式子可以写为
v
?
T
?
(
v
i
e
w
?
m
o
d
e
l
)
T
?
M
?
n
?
\vec{v}^{T}*(view*model)^{T}*M*\vec{n}
v
T?(view?model)T?M?n
,所以可以得出
M
=
(
v
i
e
w
?
m
o
d
e
l
)
T
?
1
M = (view*model)^{T^{-1}}
M=(view?model)T?1
Eigen::Matrix4f inv_trans = (view * model).inverse().transpose();
Eigen::Vector4f n[] = {
inv_trans * to_vec4(t->normal[0], 0.0f),
inv_trans * to_vec4(t->normal[1], 0.0f),
inv_trans * to_vec4(t->normal[2], 0.0f)
};
法向量实现
法向量对应颜色这里使用 法向量+
(
0.5
,
0.5
,
0.5
)
(0.5,0.5,0.5)
(0.5,0.5,0.5)
Eigen::Vector3f normal_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f return_color = (payload.normal.head<3>().normalized() + Eigen::Vector3f(1.0f, 1.0f, 1.0f)) / 2.f;
Eigen::Vector3f result;
result << return_color.x() * 255, return_color.y() * 255, return_color.z() * 255;
return result;
}
运行 ./Rasterizer output.png normal 其结果如下
Blinn-Phong 模型
Blinn-Phong 反射模型的公式如下 在代码实现过程中,需要注意,角度的计算需要为单位向量。其次这里我把相机位置改为
(
0
,
0
,
0
)
(0,0,0)
(0,0,0),我的想法是,这里的插值三维空间坐标是已经经过模型变换和视图变换之后的了,那么视图变换之后,相机按道理就应该在
(
0
,
0
,
0
)
(0,0,0)
(0,0,0) 这个位置,不知道我的理解是否有问题,欢迎大佬指正。
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, 0};
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)
{
auto ambient = amb_light_intensity;
Eigen::Vector3f l = light.position - point;
auto diffuse = (light.intensity/l.dot(l)) * std::max(0.f, normal.dot(l.normalized()));
Eigen::Vector3f v = eye_pos - point;
Eigen::Vector3f h = (v + l).normalized();
auto specular = (light.intensity/l.dot(l)) * std::max(0.f, std::pow(normal.dot(h), p));
result_color = result_color + Eigen::Vector3f(ambient[0]*ka[0],ambient[1]*ka[1],ambient[2]*ka[2]) + Eigen::Vector3f(diffuse[0]*kd[0],diffuse[1]*kd[1],diffuse[2]*kd[2]) + Eigen::Vector3f(specular[0]*ks[0],specular[1]*ks[1],specular[2]*ks[2]);
}
return result_color * 255.f;
}
运行 ./Rasterizer output.png phong 结果如下
应用纹理
我们之前已经插值出该采样点对应的纹理坐标,使用 payload.texture->getColor(u,v) 就能获得
(
u
,
v
)
(u,v)
(u,v) 对应的纹理颜色,并将其传给 kd 。
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f return_color = {0, 0, 0};
if (payload.texture)
{
float u = std::min(1.0f, std::max(0.0f, payload.tex_coords[0]));
float v = std::min(1.0f, std::max(0.0f, payload.tex_coords[1]));
return_color = payload.texture->getColor(u, v);
}
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, 0};
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)
{
auto ambient = amb_light_intensity;
Eigen::Vector3f l = light.position - point;
auto diffuse = (light.intensity/l.dot(l)) * std::max(0.f, normal.dot(l.normalized()));
Eigen::Vector3f v = eye_pos - point;
Eigen::Vector3f h = (v + l).normalized();
auto specular = (light.intensity/l.dot(l)) * std::max(0.f, std::pow(normal.dot(h), p));
result_color = result_color + Eigen::Vector3f(ambient[0]*ka[0],ambient[1]*ka[1],ambient[2]*ka[2]) + Eigen::Vector3f(diffuse[0]*kd[0],diffuse[1]*kd[1],diffuse[2]*kd[2]) + Eigen::Vector3f(specular[0]*ks[0],specular[1]*ks[1],specular[2]*ks[2]);
}
return result_color * 255.f;
}
运行 ./Rasterizer output.png texture 结果如下
凹凸贴图
TVB切向量空间具体可以看这篇文章 这里我感觉代码框架里的 t 只是做一个近似,正常 t 也是根据三角形三个顶点的 t 插值产生。代码框架给出的方法就是按照上图的方法,求出一个 t ,而且这里还有点错误,
t
=
(
?
x
y
x
2
+
z
2
,
x
2
+
z
2
,
?
y
z
x
2
+
z
2
)
t=(-\frac{xy}{\sqrt{x^{2}+z^{2}}}, \sqrt{x^{2}+z^{2}},-\frac{yz}{\sqrt{x^{2}+z^{2}}})
t=(?x2+z2
?xy?,x2+z2
?,?x2+z2
?yz?)
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;
float u = std::min(1.0f, std::max(0.0f, payload.tex_coords[0]));
float v = std::min(1.0f, std::max(0.0f, payload.tex_coords[1]));
float x = normal.x();
float y = normal.y();
float z = normal.z();
Vector3f t(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
Vector3f b = normal.cross(t);
Matrix3f TBN;
TBN<<t.x(),b.x(),normal.x(),t.y(),b.y(),normal.y(),t.z(),b.z(),normal.z();
float dU = kh * kn * (h(u+1.0f/payload.texture->width,v,payload)-h(u,v,payload));
float dV = kh * kn * (h(u,v+1.0f/payload.texture->height,payload)-h(u,v,payload));
Eigen::Vector3f ln(-dU, -dV, 1.0f);
normal = (TBN * ln).normalized();
Eigen::Vector3f result_color = {0, 0, 0};
result_color = normal;
return result_color * 255.f;
}
h 函数用于计算
(
u
,
v
)
(u,v)
(u,v) 对应高度值,这里用对应 RGB 颜色的平方和的平方根表示,其代码如下:
float h(float u, float v, const fragment_shader_payload& payload)
{
return payload.texture->getColor(u, v).norm();
}
运行 ./Rasterizer output.png bump 结果如下
位移贴图
位移贴图这里除了类似凹凸贴图,改变了法向量,并且实际移动了三维空间中点的位置,进而直接影响 Blinn-Phong 模型中的
l
?
\vec{l}
l
与
v
?
\vec{v}
v
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;
float u = std::min(1.0f, std::max(0.0f, payload.tex_coords[0]));
float v = std::min(1.0f, std::max(0.0f, payload.tex_coords[1]));
float x = normal.x();
float y = normal.y();
float z = normal.z();
Vector3f t(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
Vector3f b = normal.cross(t);
Matrix3f TBN;
TBN<<t.x(),b.x(),normal.x(),t.y(),b.y(),normal.y(),t.z(),b.z(),normal.z();
float dU = kh * kn * (h(u+1.0f/payload.texture->width,v,payload)-h(u,v,payload));
float dV = kh * kn * (h(u,v+1.0f/payload.texture->height,payload)-h(u,v,payload));
Eigen::Vector3f ln(-dU, -dV, 1.0f);
point = point + kn * normal * h(u, v, payload);
normal = (TBN * ln).normalized();
Eigen::Vector3f result_color = {0, 0, 0};
for (auto& light : lights)
{
auto ambient = amb_light_intensity;
Eigen::Vector3f l = light.position - point;
auto diffuse = (light.intensity/l.dot(l)) * std::max(0.f, normal.dot(l.normalized()));
Eigen::Vector3f v = eye_pos - point;
Eigen::Vector3f h = (v + l).normalized();
auto specular = (light.intensity/l.dot(l)) * std::max(0.f, std::pow(normal.dot(h), p));
result_color = result_color + Eigen::Vector3f(ambient[0]*ka[0],ambient[1]*ka[1],ambient[2]*ka[2]) + Eigen::Vector3f(diffuse[0]*kd[0],diffuse[1]*kd[1],diffuse[2]*kd[2]) + Eigen::Vector3f(specular[0]*ks[0],specular[1]*ks[1],specular[2]*ks[2]);
}
return result_color * 255.f;
}
运行 ./Rasterizer output.png displacement 结果如下
进阶代码
更换模型
更换模型就是把对应 obj 以及图片(.png 、.jpg 、.tif )文件路径进行修改
这里以 cube 模型为例,加上纹理之后结果好像有问题,不知道什么原因,欢迎大佬指正
双线性纹理插值
首先下采样为 200x200 像素的图,这里 cv::pyrDown 进行一次 2x2 的下采样
down_width = width / 4;
down_height = height / 4;
cv::pyrDown(image_data, image_down2_data);
cv::pyrDown(image_down2_data, image_down4_data);
其结果为 然后采用双线性插值,我们根据当前位置离哪块像素点接近,判断取左上,左下,还是右上或右下的像素进行双线性插值,需要注意的是opencv的坐标系是x轴朝右y轴朝下的
Eigen::Vector3f getBilinearInterpolationColor(float u, float v)
{
float u_down_img = std::min((down_width - 1) * 1.0f, std::max(0.0f, u * down_width));
float v_down_img = std::min((down_height - 1)* 1.0f, std::max(0.0f, (1 - v) * down_height));
cv::Vec3b bilinear_interpolate_color;
float u_down_img_round = std::min((down_width - 1) * 1.0f, std::max(0.0f, round(u_down_img)));
float v_down_img_round = std::min((down_height - 1) * 1.0f, std::max(0.0f, round(v_down_img)));
if(u_down_img - u_down_img_round >= 0.0f)
{
if(v_down_img - v_down_img_round >= 0.0f)
{
float s = u_down_img - u_down_img_round + 0.5f;
float t = v_down_img - v_down_img_round + 0.5f;
int x2 = std::max(0, std::min(down_width-1, (int)u_down_img_round));
int x1 = std::max(0, std::min(down_width-1, x2 - 1));
int y2 = std::max(0, std::min(down_height-1, (int)v_down_img_round));
int y1 = std::max(0, std::min(down_height-1, y2 - 1));
bilinear_interpolate_color = lerp(t, lerp(s, image_down4_data.at<cv::Vec3b>(y1, x1), image_down4_data.at<cv::Vec3b>(y1, x2)),
lerp(s, image_down4_data.at<cv::Vec3b>(y2, x1), image_down4_data.at<cv::Vec3b>(y2, x2)));
} else {
float s = u_down_img - u_down_img_round + 0.5f;
float t = v_down_img_round - v_down_img + 0.5f;
int x2 = std::max(0, std::min(down_width-1, (int)u_down_img_round));
int x1 = std::max(0, std::min(down_width-1, x2 - 1));
int y2 = std::max(0, std::min(down_height-1, (int)v_down_img_round));
int y1 = std::max(0, std::min(down_height-1, y2 - 1));
bilinear_interpolate_color = lerp(t, lerp(s, image_down4_data.at<cv::Vec3b>(y2, x1), image_down4_data.at<cv::Vec3b>(y2, x2)),
lerp(s, image_down4_data.at<cv::Vec3b>(y1, x1), image_down4_data.at<cv::Vec3b>(y1, x2)));
}
} else {
if(v_down_img - v_down_img_round >= 0.0f)
{
float s = u_down_img_round - u_down_img + 0.5f;
float t = v_down_img - v_down_img_round + 0.5f;
int x2 = std::max(0, std::min(down_width-1, (int)u_down_img_round));
int x1 = std::max(0, std::min(down_width-1, x2 - 1));
int y2 = std::max(0, std::min(down_height-1, (int)v_down_img_round));
int y1 = std::max(0, std::min(down_height-1, y2 - 1));
bilinear_interpolate_color = lerp(t, lerp(s, image_down4_data.at<cv::Vec3b>(y1, x2), image_down4_data.at<cv::Vec3b>(y1, x1)),
lerp(s, image_down4_data.at<cv::Vec3b>(y2, x2), image_down4_data.at<cv::Vec3b>(y2, x1)));
} else {
float s = u_down_img_round - u_down_img + 0.5f;
float t = v_down_img_round - v_down_img + 0.5f;
int x2 = std::max(0, std::min(down_width-1, (int)u_down_img_round));
int x1 = std::max(0, std::min(down_width-1, x2 - 1));
int y2 = std::max(0, std::min(down_height-1, (int)v_down_img_round));
int y1 = std::max(0, std::min(down_height-1, y2 - 1));
bilinear_interpolate_color = lerp(t, lerp(s, image_down4_data.at<cv::Vec3b>(y2, x2), image_down4_data.at<cv::Vec3b>(y2, x1)),
lerp(s, image_down4_data.at<cv::Vec3b>(y1, x2), image_down4_data.at<cv::Vec3b>(y1, x1)));
}
}
return Eigen::Vector3f(bilinear_interpolate_color[0], bilinear_interpolate_color[1], bilinear_interpolate_color[2]);
}
}
lerp 函数如下
cv::Vec3b lerp(float ratio, cv::Vec3b color1, cv::Vec3b color2)
{
return cv::Vec3b((1.0f-ratio)*color1[0] + ratio*1.0f*color2[0], (1.0f-ratio)*color1[1] + ratio*1.0f*color2[1], (1.0f-ratio)*color1[2] + ratio*1.0f*color2[2]);
}
其结果如下
|