引言
GAMES101现代图形学入门是由闫令琪老师教授。本次作业我们会进一步模拟现代图形技术,主要是关于 Blinn-Phong 着色模型以及纹理贴图。
GAMES101 Spring 2021课程作业三现在发布!大家可以在SmartChair上提交了,注意截止日期为2021年7月10日。
在课程中段,我们会为同学们开启补提交通道。百度云链接
提取码: r3pc请大家根据网络环境自行下载。
总览
在这次编程任务中,我们会进一步模拟现代图形技术。我们在代码中添加了 Object Loader(用于加载三维模型), Vertex Shader 与 Fragment Shader,并且支持了纹理映射。
而在本次实验中,你需要完成的任务是:
- 修改函数
rasterize_triangle(const Triangle& t)
inrasterizer.cpp
: 在此处实现与作业 2 类似的插值算法,实现法向量、颜色、纹理颜色的插值。 - 修改函数
get_projection_matrix()
inmain.cpp
: 将你自己在之前的实验中实现的投影矩阵填到此处,此时你可以运行./Rasterizer output.png normal
来观察法向量实现结果。 - 修改函数
phong_fragment_shader()
inmain.cpp
: 实现 Blinn-Phong 模型计算 Fragment Color. - 修改函数
texture_fragment_shader()
inmain.cpp
: 在实现 Blinn-Phong 的基础上,将纹理颜色视为公式中的 kd,实现 Texture Shading Fragment Shader. - 修改函数
bump_fragment_shader()
inmain.cpp
: 在实现 Blinn-Phong 的基础上,仔细阅读该函数中的注释,实现 Bump mapping. - 修改函数
displacement_fragment_shader()
inmain.cpp
: 在实现 Bump mapping 的基础上,实现 displacement mapping.
开始编写
编译与使用
在课程提供的虚拟机上,下载本次实验的基础代码之后,请在 SoftwareRasterizer 目录下按照如下方式构建程序:
mkdir build |
这将会生成命名为 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 后,你将看到如下结果:
Frequently Asked Questions
- bump mapping 部分的
h(u,v)=texture_color(u,v).norm
, 其中u
,v
是tex_coords
,w
,h
是texture
的宽度与高度 rasterizer.cpp
中v = t.toVector4()
get_projection_matrix
中的eye_fov
应该被转化为弧度制- bump 与 displacement 中修改后的
normal
仍需要 normalize - 可能用到的 eigen 方法:
norm()
,normalized()
,cwiseProduct()
- 实现
h(u+1/w,v)
的时候要写成h(u+1.0/w,v)
- 正规的凹凸纹理应该是只有一维参量的灰度图,而本课程为了框架使用的简便性而使用了一张 RGB 图作为凹凸纹理的贴图,因此需要指定一种规则将彩色投影到灰度,而我只是「恰好」选择了 norm 而已。为了确保你们的结果与我一致,我才要求你们都使用 norm 作为计算方法
- bump mapping & displacement mapping 的计算的推导日后将会在光线追踪部分详细介绍,目前请按照注释实现
评分与提交
评分
[5 分] 提交格式正确,包括所有需要的文件。代码可以正常编译、执行。
[10 分] 参数插值: 正确插值颜色、法向量、纹理坐标、位置 (Shading Position) 并将它们传递给 fragment_shader_payload.
[20 分] Blinn-phong 反射模型: 正确实现 phong_fragment_shader 对应的 反射模型。
[5 分] Texture mapping: 将 phong_fragment_shader 的代码拷贝到 texture_fragment_shader, 在此基础上正确实现 Texture Mapping.
[10 分] Bump mapping 与 Displacement mapping: 正确实现 Bump mapping 与 Displacement mapping.
[Bonus 3 分] 尝试更多模型: 找到其他可用的.obj 文件,提交渲染结果并 把模型保存在 /models 目录下。这些模型也应该包含 Vertex Normal 信息。
[Bonus 5 分] 双线性纹理插值: 使用双线性插值进行纹理采样, 在 Texture 类中实现一个新方法 Vector3f getColorBilinear(float u, float v) 并 通过 fragment shader 调用它。为了使双线性插值的效果更加明显,你应该考虑选择更小的纹理图。请同时提交纹理插值与双线性纹理插值的结果,并进行比较。
[-2 分] 惩罚分数:
未删除 /build、/.vscode、Assignment3.pdf 等与代码无关的文件; 未提交或未按要求完成 README.md;
未按照要求完成/images 目录;
代码相关文件和 README.md 文件不在你提交的文件夹下的第一层。
提交
当你完成作业后,请清理你的项目 (删去:/build、/.vscode、/Assignment3.pdf 等),在你的文件夹中包含 CMakeLists.txt 和所有的程序文件 (无论是否修改);
同时,请新建一个 /images 目录,将所有实验结果图片保存在该目录下;
再添加一个 README.md 文件写清楚自己完成了以上七个得分点中的哪几点 (如果完成了,也请同时提交一份结果图片),并简要描述你在各个函数中实现的功能;
最后,将上述内容打包,并用“姓名 Homework3.zip”的命名方式提交到 SmartChair 平台。
实现
代码框架
|
投影函数
填写在 main.cpp
下的 get_projection_matrix()
投影函数:
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar) |
插值函数
与上一次作业相比,这次插值多出了法向和纹理:
//Screen space rasterization |
接着运行终端代码:
mkdir build |
Blinn-Phong模型
如果 Blinn-Phong 模型还是有些搞不清楚,可以去看一下 LearnOpenGL 的 基础光照,讲得非常清楚可以配套课程食用。
Ambient Lighting
ambient 环境光照为常量,计算如下:
$$
L_a = k_a I_a
$$
Eigen::Vector3f ambient = ka.cwiseProduct(amb_light_intensity); |
Diffuse Lighting
diffuse 漫反射光照计算如下:
$$
L_d = k_d (I / r^2) max(0, n \cdot l)
$$
Eigen::Vector3f diffuse = kd.cwiseProduct(light.intensity/r.dot(r)) * std::max(0.0f, n.dot(l)); |
Specular Lighting
$$
L_s = k_s(I / r^2) max(0, n \cdot h)^p
$$
Eigen::Vector3f specular = ks.cwiseProduct(light.intensity/r.dot(r)) * std::pow(std::max(0.0f, n.dot(h)), p); |
不要忘记指数 p
,我一开始就是忘记这一点导致渲染出来的模型偏亮,凸显不出高光项。
最终完整的 phong_fragment_shader()
函数如下:
Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload) |
接着运行终端代码:
./Rasterizer output.png phong |
Texture
texture_fragment_shader()
大同小异,只是需要先获取纹理坐标:
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload) |
接着运行终端代码:
./Rasterizer output.png texture |
Bump mapping
有关法线贴图课上还是有些一笔带过,还可以参考 LearnOpenGL 的 法线贴图 补充。
kh * kn
是影响系数(是常数,上面已经定义了值),表示纹理法线对真实物体的影响程度,和课上的 c1
、c2
是同一个东西。
Eigen::Vector3f bump_fragment_shader(const fragment_shader_payload& payload) |
norm()
是 Eigen 库里定义的一个求范数的函数,就是求所有元素²的和再开方。 向量的范数则表示的是原有集合的大小,范数的本质是距离,存在的意义是为了实现比较。getColor()
返回的是一个储存颜色值的向量:(color[0],color[1],color[2])
对应的是 RGB 值,dU
和 dV
都是一个 float
值,并不是 Vector
,想要实现 h()
表示的实数高度值,就要用到 norm.()
将向量映射成实数。
接着运行终端代码:
./Rasterizer output.png bump |
Displacement mapping
与 bump 相比位移贴图多了一个修改 point 的步骤:
point += kn * n * payload.texture->getColor(u, v).norm(); |
在根据凹凸贴图 UV 计算新的法向量后,顶点要实际移动,按照注释多写一行,然后再修改法向量。
Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload) |
接着运行终端代码:
./Rasterizer output.png displacement |
问题汇总
C++标准设置
这个问题我没有遇到,但是在查阅其他问题时在论坛有同学有讨论无法使用一些新的语言特性。
如果使用 VisualStudio 需要进入项目设置中选择 C++17/20 等较高版本;如果像我一样使用虚拟机的同学则可以考虑修改 CMake 配置文件:
cmake_minimum_required(VERSION 3.10) |
模型纹理的路径设置
框架代码给定的是相对路径,如果不加以修改会有以下报错:
OpenCV Error: Assertion failed (scn == 3 || scn == 4) in cvtColor, file /build/opencv-L2vuMj/opencv-3.2.0+dfsg/modules/imgproc/src/color.cpp, line 9716 |
相对路径修改为绝对路径即可,不要忘记最后的 /
:
int main(int argc, const char** argv) |
模型倒置问题
如果按照前几次作业的投影函数直接复制拷贝,会出现模型尾部正对摄像机的情况:
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar) |
这种情况我参考了 GAMES101作业3-遇到的各种问题及解决方法 的解决方案,加入一个 mirror
矩阵将 Z 轴旋转 180 度:
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar) |
纹理归一化
框架给定的模型贴图一部分是超出 [0, 1] 范围的,所以需要我们在 Texture.h
中加入归一化操作:
// |
切线空间矩阵转置
对着注释写 TBN 矩阵写成了转置形式:
Eigen::Matrix3f TBN; |
后面浏览参考答案时发现不妥,模型身上都是哑光。应该更改为:
Eigen::Matrix3f TBN; |
计算结果未累加
逛论坛的时候发现大家还有一种错误就是没有累加最后的结果,选择了赋值:
首先是 Blinn-Phong 模型中 result_color
结果需要累加所有光源:
for (auto& light : lights) |
displacement mapping 处顶点 point
这里也是累加而非赋值施加贴图的影响:
// Vector ln = (-dU, -dV, 1) |
这会导致模型渲染结果会比正常情况偏暗。