终于要到shading了,2333~
先复习下单词
shading, [ˈʃeɪdɪŋ], noun. The darkening or coloring of an illustration or diagram with parallel lines or a block of color.
理论回顾
模型的顶点
OBJ模型文件
# "#"号开头是注释行
# v(vertex)数据段: 模型顶点列表
# 顶点位置信息,是xyz三维坐标
# v开头的每一行描述一个顶点,行数等于顶点数。8个顶点所以有8行
v 1.00 -1.00 -1.00
v 1.00 1.00 1.00
......
# vt(vertex texture)数据段:模型顶点的纹理坐标列表
# 顶点的纹理坐标信息,是xy二维坐标
# vt开头的每一行描述一个纹理坐标,行数大于等于顶点数,因为一个模型顶点在贴图的UV坐标系中很可能对应多个顶点/纹理坐标。且坐标值范围是在0~1之间,这个模型中有14行。
# 关于纹理坐标看图,本文不多解释纹理坐标,可参考文献[2]或自行百度
vt 0.74 0.75
vt 0.29 0.55
......
# vn(vertex normal)数据段:顶点法线列表
# 三维法向量,xyz
# vn开头的每一行描述一个法向量,行数大于等于顶点数。 前面介绍了,法线是与面相关的概念,但是现在的面是靠顶点来描述,拿示意图中的点"1"为例,它与参与构成了三个面,所以"顶点1"对应有3条法线
# 可能你已经发现了,在这个立方体模型中,共面顶点的法向量的方向是相同的,也就是说这里面的数据会重复,所以在建模软件导出obj文件时有个优化的选项,勾选后的导出的法线列表数据中就不会有重复项,这里的例子优化后有6条法线
vn -1.00 0.00 0.00
vn 1.00 0.00 0.00
vn 0.00 1.00 0.00
......
# f(face):模型的三角面列表
# f开头的每一行描述一个面 ,关键的来了,三个点组成一个面,怎样拿到这三个点呢?通过从1开始的索引,去前面的v、vt、vn列表中去取。
# 总结一下就是:每一行定义1个面,1个面包含3个点,1个点具有“顶点/纹理坐标/法线”3个索引值,索引的是前面3个列表的信息。
f 1/1/1 2/2/1 3/3/1 # 顶点1、顶点2、顶点3 组成的面
f 2/2/1 3/3/1 4/4/1 # 顶点2、顶点3、顶点4 组成的面
f 1/1/1 5/10/1 8/14/6 # 顶点1、顶点5、顶点8 组成的面
# f (v)/(vt)/(vn) 同一个顶点可能有多个纹理和法线坐标,这个与它关联的面有关
......
通常在模型文件中会保存每个顶点的法线,即与之关联的面的法向向量。所以模型文件中一个顶点会有好几个法线,这并不是错误。在渲染之前,会将这些向量加权平均,得到顶点法线。计算如下:
着色时会根据片元的法向向量来计算光照。片元法线又是根据顶点法线按照质心法插值得到的。
此外obj文件里面还有一些非核心的数据段,如下:
- o 对象名
- g 组名
- s 平滑组
- usemtl 材质名
- mtllib 材质库.mtl
着色的级别
第三种方法对每个像素点都进行了处理,精确度更高。
模型的纹理
着色时实际上是在不同的片元上按照纹理坐标设置属性,所以首先需要加载模型的纹理。只要是模型的某种属性就可以做成纹理的形式,因此纹理的种类很多。纹理的生成方法有专门的研究,这里不说明纹理是怎么做的,而且只介绍常用的几种纹理。
Texture Mapping
材质纹理是最简单、最常用的,其作用是给片元提供颜色。
-
Planar Projection(平面投影)
这对于大部分平坦的表面来说效果很好,表面法线变化不大,通过取平均法线可以找到良好的投影方向。
在封闭物体上使用平面投影总是会导致投影方向与表面相切的点附近的非单射、一对多映射和极端失真。
-
Spherical Map(球面贴图)
也称为标准环境映射,泛用性较好,但问题是描述不够均匀,在靠近极点的位置存在扭曲的现象(图像会变大)
-
Cube Map(立方体贴图)
相当于先用一个包围盒包住模型,然后在每个面上映射,这种方法的好处是极大的较小了纹理扭曲,但是查找纹理的时候,需要先计算是在Cube的哪个面上面。
Normal Maps
法线贴图。虽然模型文件中保存了法线,但是为了让物体看起来更加真实,有时候需要改变物体的表面,所以会而外加载一个法线贴图,用来替换原来的法线。这样可以改变光照形成的阴影,达到让物体看起来更真实的效果。
实际应用中,法线贴图通常是按照标准坐标系生成的,也可以说是在法线空间里定义的,实际中模型中的面会经过顶点变换,因此不能直接用法线贴图里的坐标进行替换。
因此需要对法线进行变换,方法是先计算点所在位置的切向空间,可以用TBN矩阵描述,TBN矩阵定义了一个以模型顶点为中心的局部坐标系,然后把贴图中的法线变换到这个坐标系下。
TBN矩阵指的是切线-副切线-顶点法线,而且TBN矩阵是一个正交单位阵。切线变换的公式如下(推导参考虎书5th-7.2.2):
令M=TBN-Matrix,因为M是正交单位阵,有 M = ( M − 1 ) T M=(M^{-1})^T M=(M−1)T,所以变换公式为 n N = M n n_N=Mn nN=Mn。
计算TBN矩阵时,首先只知道表面的法向量,计算两个切向量的时候通常会使其对其纹理方向,然后对矩阵进行施密特正交化。
法线贴图更全面的理解以后学习Shader编程的时候再深入吧,这里先了解有什么用,其实作业中的计算方法是不严谨的。
Bump Mapping
法线贴图可以让物体看起来更加真实,但有更简单的实现方法,即凹凸贴图,纹理中保存的相对高度场,着色时需要计算当前位置的高度变化来对法线施加扰动。
没有凹凸贴图的球体(左)。 要应用于球体的凹凸贴图(中)。 应用了凹凸贴图的球体(右)似乎有一个类似于橙色的斑驳表面。 凹凸贴图通过更改照明表面对光的反应方式来实现此效果,而无需修改表面的大小或形状。
凹凸贴图的局限性在于它不会修改底层对象的形状。 在左侧,定义凹凸贴图的数学函数模拟了球体上破碎的表面,但对象的轮廓和阴影仍然是完美球体的轮廓。 在右侧,相同的函数用于通过生成等值面来修改球体的表面。 这模拟了一个表面凹凸不平的球体,结果是它的轮廓和阴影都被真实地渲染了。
凹凸贴图和法线贴图的联系:法线贴图可以由凹凸贴图求导得到。
Displacement Mapping
位移贴图通常使用和凹凸贴图一样的纹理,只不过用从纹理中获得的法向量去计算顶点变化。
光照模型
Blinn-Phong reflection Model
- k a k_a ka是环境光反射常数, I a I_a Ia是环境光强度;
- k d k_d kd是漫反射参数;
- k s k_s ks是镜面反射常数;
代码展示
Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload)
{
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;
Eigen::Vector3f result_color = {0, 0, 0};
Eigen::Vector3f bump_norm = {0, 0, 0};
Vector3f texture_norm;
// 按照凹凸谱进行位移贴图
if (payload.bumpmap) {
Texture *t = payload.bumpmap;
//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, y, z;
x = normal.x();
y = normal.y();
z = normal.z();
// 按照注释里的方法计算TBN矩阵
Vector3f T = Vector3f(x * y / sqrt(x * x + z * z),
sqrt(x * x + z * z),
z * y / sqrt(x * x + z * z));
Vector3f B = normal.cross(T);
Matrix<float, 3, 3> TBN;
TBN << T, B, normal;
// u,v是凹凸贴图的坐标;w,h是贴图的大小
float u = payload.bump_coords.x();
float v = payload.bump_coords.y();
int w = payload.bumpmap->width;
int h = payload.bumpmap->height;
// 之所以要u+1.f/w而不是u+1,是因为在读取纹理的时候uv已经归一化了
Vector3f t_h1 = t->getColor(u + 1.f / w, v);
Vector3f t_h2 = t->getColor(u, v);
Vector3f t_h3 = t->getColor(u, v + 1.f / h);
// 计算数值微分
float dU = kh * kn * (t_h1.norm() - t_h2.norm());
float dV = kh * kn * (t_h3.norm() - t_h2.norm());
// 求切平面的法向量,也就是这个点在切线空间的法向量
Vector3f ln = Vector3f(-dU, -dV, 1);
// 用TBN矩阵将法线变换到世界坐标下
bump_norm = TBN * ln;
bump_norm.normalize();
// 根据该点的相对高度大小和法线方向对点施加扰动
point += kn * t_h2.norm() * bump_norm;
}
// 按照Blinn-Phong模型计算光照
if (payload.texture){
result_color=payload.texture->getColor(
payload.tex_coords.x(),payload.tex_coords.y());
}
Eigen::Vector3f texture_color;
texture_color << result_color.x(), result_color.y(), result_color.z();
// kd是漫反射常数,这里取决于色彩贴图,符合现实场景
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = texture_color/255.0;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
// cwiseProduct()是按元素相乘
result_color=ka.cwiseProduct(amb_light_intensity);
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 *bump_norm* object.
Vector3f l_vec = light.position-point;
Vector3f v_vec= eye_pos-point;
float light_dis=l_vec.norm();
l_vec.normalize();
v_vec.normalize();
// 镜面反射的半程向量
// 两个单位向量线性组合并不一定得到单位向量!!
Vector3f half_vec=(l_vec+v_vec).normalized();
// L_diffuse
float inner=std::max(0.f, normal.dot(l_vec));
Vector3f kDiffuse=kd.cwiseProduct(light.intensity)/(light_dis*light_dis);
Vector3f Ld = kDiffuse * inner;
result_color+=Ld;
// L_specular
inner=std::max(0.f,normal.dot(half_vec));
inner=std::pow(inner,p);
Vector3f kSpec=ks.cwiseProduct(light.intensity)/(light_dis*light_dis);
Vector3f Ls=kSpec*inner;
result_color+=Ls;
}
return result_color * 255.f;
}
从左到右,依次是纹理贴图、凹凸贴图和位移贴图。
参考
[1] https://zhuanlan.zhihu.com/p/38052123
[2] https://blog.csdn.net/kaitiren/article/details/47280221
[3] https://games-cn.org/forums/topic/frequently-asked-questionskeep-updating/