复刻URPLit
考虑到项目移植各个平台的顺利,URP还是目前最实用的管线了,但自带的lit还是缺少很多功能,比如对VR很重要的几何高光抗锯齿,混合SSR也不是很方便,各向异性,环境法线,高光遮蔽,视差映射等等功能都是缺少的。
为了功能的收缩与扩张方便,还是有必要手撸一个PBR的。
而且Lit的代码为了兼容各种环境,比如Dot,VR,LODCross,做了大量的判断,代码很杂乱,很难修改。整理一遍也是为了更好的理解URP的Lit着色器。
以上存粹是为了说服自己造这个轮子。。。
需求
先从简单的搞起,大致输入就先仿照URP最精简的Lit
- 按金属粗糙的工作流
- 不透明
- 仅正面
- alpha不裁切
- 接收阴影
- 阴影不级联
- BaseMap(RGBA)
- BaseColor(RGBA)
- Metallic
- MetallicMap(R金属 A光滑)
- Smoothness
- NormalMap
- 先不考虑高度图
- OcclusionMap
- OcclusionStrength
- EmissionColor
- EmissionMap
- 先不考虑细节贴图
- 先不考虑清漆
- 实时光仅平行光
- 有环境反射
- 先不考虑烘焙
开搞
这里就没什么好说的了,公式就翻翻URP的lit的实现,先尽量汇总到一个文件里,方便之后比对。
Properties
就参照上面的需求,大致列一下属性,不够之后再加。
按照URP文档的要求,要配置一些通用的Tag
Pass
URP要求一个Shader中要实现多个Pass,分别包含了
- UniversalForward
- ShadowCaster
- UniversalGBuffer
- DepthOnly
- DepthNormals
- Meta
- Universal2D
顾名思义,不一一介绍了,最为最简单的仅前向的shader,这里就挑出以下五个实现
- UniversalForward
- ShadowCaster
- DepthOnly
- DepthNormals
- Meta
关键字
参照urp的lit,分为材质的关键字、管线的关键字、unity的关键字和GPUInstancing。
材质的关键字主要用来定义一些局部的属性,如是否启用法线贴图,是否启用高度图,光滑的从哪里采样等等
这部分就先省略,之后优化时再加。
然后是管线的关键字,主要定义一些是否启用了主光源阴影,屏幕阴影,附加光阴影,软阴影等等。
这里就也先省略了,需要对应的功能再加。
然后是untiy的关键字。包含了光照贴图,阴影的shadowMask,雾气等等。
存储结构
参考lit,把输入以及一些常用的采样分到一个文件中,具体的着色器函数在另一个文件,方便引用,那这里也依葫芦画瓢。
值得一提的是URP中还把常见的一些贴图又分到了一个SurfaceInpute中,这里就没有必要了,合并到一起。
都是很常见的属性。
顶点着色器
那精简之后得到
片元
然后就是最复杂的片元了
URP的SurfaceData略显复杂,精简一下
然后是初始化SurfaceData,就是采样一堆贴图,然后根据系数得出最终的每个片元的属性。简化一下如下
接着是初始化顶点的输入,主要涉及各种空间下的坐标以及GI、阴影、雾气等。
GI
依据不同的光照贴图的编码,有不同的解码方式,这里先取巧用FULL_HDR,就可以不用解码了。
大致流程就是采样贴图,得出方向与强度,用halfLambert得出最终的GI。
阴影
这里仅算主光源阴影,Untiy还会采样烘焙的阴影贴图,去混合实时光阴影与烘焙阴影。
PBR
整个计算流程就是分为算AO,实际的URP会在这里取看有没有SSAO,我们先省略。
然后得出主光源的信息,这里就包含了阴影。
然后就是计算GI,主光源,附加光,顶点光。
最后两项先省略。
代码如下:
详细的计算GI与实时光的代码就不贴了,总之这样就算有了一个改动方便的PBR。后续再想增加一些SSR、环境法线之类的功能就方便很多了。
当然这个还很不完善,比如级联阴影,透明,等等都没有考虑。
很多分支都被删减了,先看看好不好用吧,如果可行那就以后再慢慢补充。
分文件
还是有必要按照原来逻辑把hlsl代码分开,不然后期不太好维护。
大致分为以下几个文件
- AmbientOcclusion AO计算,包括直接光与间接光,SSAO得出的结果在前向渲染里也是在这里采样的。
- BRDF 这里就是PBR那一套公式了
- EntityLighting 涉及球谐函数以及光照贴图的采样(包括解码)
- GlobalIllumination 在对EntityLighting封装的基础上,添加了诸如BoxProjected,混合,根据光照贴图编码方式选择具体的解码,采样反射探针等
- ImageBasedLighting 包含了一些基于图像的光照工具函数
- Lighting 主要的光照计算,除了调用上面的BRDF,其实还包含了Unlit,BlinnPhong,BakedLit等多种光照模型。
- LitForwardPass 包含顶点与片元。
- LitInput 包含从材质传递过来的全部输入
- RealtimeLights 实时光的获取,并调用阴影
- Shadows 阴影相关,阴影贴图的采样,级联,也包含了采样光照贴图里的阴影贴图并混合
- SurfaceData 一个结构体,包含了一个要进入光照计算的片元的表面数据,如漫反射,金属,粗糙,遮蔽等。
- InputData 一个结构体,包含了一个要进入光照计算的片元的几何数据,如位置坐标,世界法线,阴影坐标,BakedGI等。
根据这些文件的调用关系,再梳理一下就是
LitForwardPass是总入口,汇总并梳理了全部的输入,整合成两个输入即InputData与SurfaceData。
当然,出于性能的考虑,这里的阴影坐标也提前在顶点着色器中获取了。
然后选择Lighting中的一个光照模型。类似与Lighting实现了一个通用的光照模型的接口。
这里就选用PBR的模型。
然后就开始采用PBR的公式,分以下几步,
- 首先是从InputData与SurfaceData构造BRDFData。
- BRDFData与SurfaceData很类似,主要是增加了一些诸如roughness2MinusOne等方便计算的属性。
- 取AO,从AmbientOcclusion取出AO,如果有SSAO,也会在这里处理。
- 取主光照数据(数据结构Light),从RealtimeLights获取实时光的数据。
- 这里又会调用Shadows来包含对应的阴影
- Light包含 方向,颜色,阴影。
- 然后是构造LightingData,包含了GI,主光源,附加光,顶点光,自发光。
- GI是调用BRDF的工具函数由公式计算得出,主要的输入如下
- 间接漫反射就是直接取的光照贴图,或者球谐函数。
- 间接高光就是采用的反射贴图了
- 主光源就单纯的用BRDF公式,也是分为直接漫反射和直接高光。
- 出于简化的目的,其余的先不看了。
- 最后就是将这些来自各种光源的光叠加,得出最终的输出。
这应该就是URP下的最简单的PBR实现了
阴影投射Pass
只需在顶点着色器中应用一下法线和深度偏移,其余没有什么特殊的。
球谐函数
根据LIGHTMAP_ON这个关键字,来判断GI是用贴图还是球谐函数。
级联阴影
通过_MAIN_LIGHT_SHADOWS_CASCADE来判断是否级联,然后根据距离各个级联球的中心的距离与设定距离的差值,来判断应该取哪一个级联。
总结
说不上是从零开始手撸PBR造轮子,只能算是精简了URP的原本的lit的着色器,主要目的还是理清其各个文件的逻辑关系,方便之后实现各种效果。