欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵~~
这篇文章主要讲着色器的实现,顺带提一下图形管线
代码参考:
github.com/Quanwei1992…
实现效果
基于一个小奶牛模型实现各种贴图。
模型:
纹理贴图:
冯氏光照:
法线贴图:
凹凸贴图:
唯一贴图:
图形管线
OpenGL中,将固定的流程封装好,对开发者透明,只暴露顶点着色器、片元着色器供定制。这是非常好的设计。以冯氏光照为例,在片元着色器中实现”环境光”、”漫反射”、”镜面反射”,渲染出来的很像塑料制品的效果。
图形管线不是难点,重点分析着色器的实现
着色器
简单的场景,顶点着色器一般不做复杂的计算,仅作为参数的入口,逻辑主要在片元着色器中。更复杂的场景还可能用到几何着色器。
顶点着色器的代码非常简单,返回模型的position。
Eigen::Vector3f vertex_shader(const vertex_shader_payload& payload)
{
return payload.position;
}
复制代码
顶点着色器的计算量一般远小于片元着色器。因为组成三角形的顶点相对有限,而片元需要基于顶点组成的三角形进行插值。
比如,一个100 * 100大小的矩形平面,有四个顶点,计算四次,但是片元着色器需要计算10000次。
所以,能在顶点着色器里计算的工作,就不要放在片元着色器中。
此处我没有做科学的测试,因为GPU的高并发特点,实际性能也不应该按次数来对比。
纹理贴图
纹理贴图比较简单,模型中对每个顶点已经生成好了对应的纹理坐标,直接获取对应的color即可
纹理模型一般是用工具按照一定的规则设计好的
冯氏光照
冯氏光照比较简单,网上有大量的资料。
冯氏光照
法线贴图
也比较简单,模型自带了法线。注意,法线取值范围[-1, 1],需要映射到[0,1],再转换到[0, 255]
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;
}
复制代码
凹凸贴图
到这里就略复杂了。在冯氏光照的基础上,加上凹凸纹理贴图,用凹凸纹理来替代奶牛模型表面的法线,来模拟真实物体表面凹凸不平的效果。
凹凸纹理中法线用RGB来存储,大多数法线垂直表面朝上,则B分量较大,所以整体成蓝色。
凹凸贴图原理
- 基于凹凸纹理,计算出具体点扰动后的法向量。需用到微积分的偏导。
- dp/du = c1 * [h(u+1) – h(u)]
- dp/dv = c2 * [h(v+1) – h(v)]
- n = (-dp/du, -dp/dv, 1).normalized()
- 计算TBN(切线矩阵)
切线矩阵可以将法线贴图对齐到模型表面上,则一张法线贴图可以复用。
TBN的理解完全理解有一定难度,学习的过程中,我参考了不少资料
此处不再赘述。
实际代码中可以用更巧妙的方式来计算TBN
- 将法线n投影到xz平面、y轴,得到两个向量A(xz)和B(y)
- 将A(xz)和B(y)同时旋转,使得A(xz)对齐y轴,B(y)对齐xz平面,新得到的两个向量组成了T向量,即切向量
- T和法向量n叉乘,得到复切向量
我也是从这篇帖子得到启发 TBN快速算法的解释
注意,代码里的实现是错误的,t的各分量正负号不对,得到的TBN并不是两垂直。
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);
复制代码
求出TBN,对法线做变换,得到view空间的法线
Vector3f n = (TBN * ln).normalized();
复制代码
此外,法向量随着模型一起,需要经过mv矩阵计算,但是直接这么算会导致法线并不能垂直模型表面,比如平移会改变法线的方向。实际法线的变换矩阵是逆矩阵的转置,原理参考
OpenGL Normal Vector Transformation
位移贴图
位移贴图是在凹凸贴图的基础上进一步优化, 对比上图,凹凸贴图是通过法向量的方向来模拟凹凸不平,实际上是影响了光线的反射,位移贴图是真的将顶点做了位移。
凹凸贴图的外轮廓还是光滑的,位移贴图外轮廓也呈现凹凸不平,更真实。
// 改变原始点的位置,按照法向量的方向做变化
Vector3f ln(-dU,-dV,1);
Vector3f n = (TBN * ln).normalized();
Vector3f p = point + n * huv * kn;
// 然后冯氏光照渲染
...
复制代码
此处的位移贴图比较简单,可以参考
learnopengl-cn.github.io/
上对位移贴图的讲解,非常详细。
补充,判断点在三角形内
实际代码中,光栅化逻辑中,判断点在三角形内的算法做了优化,初次理解还挺费劲。
常见的“判断点在三角形内的”算法都很好理解,基于向量叉乘或点乘。
本篇文章代码中的实现:
static bool insideTriangle(int x, int y, const Vector4f* _v){
Vector3f v[3];
for(int i=0;i<3;i++)
v[i] = {_v[i].x(),_v[i].y(), 1.0};
Vector3f f0,f1,f2;
f0 = v[1].cross(v[0]);
f1 = v[2].cross(v[1]);
f2 = v[0].cross(v[2]);
Vector3f p(x,y,1.);
if((p.dot(f0)*f0.dot(v[2])>0) && (p.dot(f1)*f1.dot(v[0])>0) && (p.dot(f2)*f2.dot(v[1])>0))
return true;
return false;
}
复制代码
参考下图及说明:
可以动的图地址:
www.geogebra.org/3d/qf2fcj9t
理解:
- 三角形ABC分别看作起点在原点处的向量
- 叉乘计算OA、OC的法向量,为f0
- 如此,OB和f0的夹角 > 90°,如果点F和B在AC的同一侧,那么OB.dot(f0) 与 OF.dot(f0)符号相同,同为正或者负
原理都是利用叉乘来判断,这个算法少了三次减法,即求向量AB BC CA。确实比上一篇文章里的insideTriangle算法实现更优,就是理解不太直观,此处本人磕了好几个小时才搞明白。
欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵~~
参考
[1]
Gouraud着色法: zh.wikipedia.org/wiki/Gourau…
[2]
冯氏光照: learnopengl-cn.github.io/02%20Lighti…
[3]
切线空间与法向量变换: juejin.cn/post/696790…
[4]
Tangent Space: www.opengl.org/archives/re…
[5]
Derivation of the Tangent Space Matrix: www.blacksmith-studios.dk/projects/do…
[6]
TBN问题: www.zhihu.com/question/43…
[7]
法线贴图、TBN: learnopengl-cn.github.io/05%20Advanc…
[8]
opengl-tutorial 法线贴图: www.opengl-tutorial.org/cn/intermed…
[9]
TBN快速算法的解释: games-cn.org/forums/topi…
[10]
OpenGL Normal Vector Transformation: www.songho.ca/opengl/gl_n…
[11]
判断点是否在三角形内: www.cnblogs.com/graphics/ar…
[12]
Cross product: en.wikipedia.org/wiki/Cross_…
[13]
伴随矩阵: zh.wikipedia.org/wiki/伴随矩阵
[14]
逆矩阵的推导过程: mp.weixin.qq.com/s/NWmLZnIDA…