“A good picture is equivalent to a good deed.”
—Vincent Van Gogh
1 着色模型(Shading Models)
第一步首先确定使用哪种着色模型:
本章示例中采用的是 Gooch着色模型:
结合灯光后可以总结为:
2 光源
本节描述光照和常见的灯光类型,可以参考之前写过的DX12笔记:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 — 第八章:光照
3 光照模型的实现
3.1 频率的评估(Frequency of Evaluation)
在设计着色实现的时候,计算需要根据频率的评估进行划分。
首先,评估计算的结果是否在整个Draw Call中是恒定的,如果是的话,计算应该有应用,特别是CPU来进行,如果放到GPU会有更大的开销。然后将结果通过图形API放到着色器输入参数中。
比如是否有只需要执行一次的运算,比如shader的编译,可以放到加载程序或者安装的时候进行离线预编译。
另一种情况是,某些着色计算是随着应用一直改变,但是改变得很慢,而且计算量非常大。比如游戏中的24小时天气效果。它的计算就可以划分到多个帧中分批计算。
另一种情况是,基于每帧都需要的计算,比如连接view和perspective矩阵;或者逐模型的,比如基于位置更新模型光照参数;或者逐Draw Call的,比如更新模型每个材质的参数。将这些输入参数,根据更新频率来进行分组,可以帮助GPU constant参数更新的性能。
如果着色计算的结果是根据Draw Call改变,那么它就不能由shader input进行传入。它比如有可编程着色阶段来计算。理论上,着色计算可以在任何可编程阶段进行,每个阶段对于的频率如下:
- Vertex shader—Evaluation per pre-tessellation vertex.
- Hull shader—Evaluation per surface patch.
- Domain shader—Evaluation per post-tessellation vertex.
- Geometry shader—Evaluation per primitive.
- Pixel shader—Evaluation per pixel.
在实际应用中,着色计算一般都是逐像素进行的。逐顶点运算主要用以变换和编写。下图展示了逐像素和逐顶点计算的差别。
需要注意的是,vertex shader经常创建单位法向量,但是线性差值会改变它的长度,所以在pixel shader中又需要重新将向量标准化,如下图。即使差值后长度会变化,在vertex shader中还是需要单位化法向量,因为如果长度不同,差值出来的法向量的方向会发生偏差,如下图左右对比:
与表面法向量不同,点朝向的相连个,比如view向量和光照向量,它们不会做差值运算。
在pixel shader中通过减法计算出来的向量不要进行标准化,否则会导致计算出现偏差,如下图:
之前有提到过,vexter shader将位置变换到合适的坐标系,但是哪个坐标系比较合适呢?这个需要根据实际需求来确定,比如场景中如果有大量灯光,那么变换到世界坐标系就比较合适,因为不需要变换灯光的位置;否则变换到camera坐标系比较合适。
上面已经讨论了大部分实现的思路,但是也有一些例外,比如一些逐primitive着色公式的风格化效果:flat shading:
flat shading可以基于geometry shader来实现,但是最近大部分还是基于vertex shader来实现。它让primitive使用第一个vertex的属性,并且关闭线性差值。
3.2 实现的例子
本节实现的例子是从Gooch模型扩展出来的,它的公式在