Lights
Single-Pass Forward Rendering
- 实现 diffuse shading.
- 支持 directional(方向光), point(点光源), and spotlights(聚光灯).
- 每帧可允许最多16个可见光参与渲染
- 每个物体可以最多由4个像素光和4个顶点光参与计算光照。
这是本系列教程的第三篇,在这一篇中,我们将实现每个物体由8个光源进行shading且仅消耗一个draw call。
1. Shading With a Light
为了渲染光照,我们得为我们的渲染管线加入一个最基础的lit shader。光照渲染可以非常简单,比如只包括光的漫反射,也可以非常非常复杂,比如基于物理的渲染(PBS)。我们现在先从最基础的开始,只计算方向光的漫反射,不考虑阴影。
1.1 Lit Shader
复制Unlit.hlsl并重命名为Lit.hlsl. 在文件中,用lit代替unlit,尤其是定义vertex和fragment 函数的名字。
同样复制Unlit.shader并重命名为Lit.shader. 在文件中,用lit代替unlit。
现在我们可以通过新建的lit shader创建material了,虽然目前渲染的效果和unlit一样(没写呢还当然一样)
1.2 Normal Vectors
为了计算方向光,我们需要知道表面的法线。我们为vertext函数的输入和输出结构体添加法线信息。
我们假设物体使用统一的scale,因此用3X3 模型矩阵简化法线的坐标变换,如果不是统一的scale,我们需要用world to object的转置矩阵进行计算(具体原理可以搜索其他资料,法线的坐标变换)。坐标变换后在fragment函数中进行归一化。
为了证明我们获得了正确的法线信息,我们在fragment函数中输出法线看看效果
1.3 Diffuse Light
漫反射由光照与表面法线的角度夹角决定,目前我们先硬编码,将光照的方向设置为 (0 ,1,0)。
2. Visible Lights
为了使用场景中的光源,我们的渲染管线需要将光源数据传输到GPU中,场景中可能存在多光源,所以我们也需要支持多光源的渲染。Unity中默认的渲染管线会为每个物体每个光源分配一个pass进行渲染(M X N 即M个物体N个光源需要M X N个Pass)。LWRP渲染管线则对每个物体只使用一个Pass渲染所有光源。HDRP则使用Deferred rendering,先渲染所有物体的表面信息,再对每个光源使用一个pass进行渲染。
在本文里,我们使用和LWRP相同的策略,对每个物体用一个pass渲染所有光源,所以要求我们将当前可见的所有光源信息传输到GPU,那些虽然在场景中,但是对物体没有产生任何影响的光源将被忽略不参与计算。
2.1 Light Buffer
在一个Pass中渲染所有光源意味着所有光源的信息必须在同时都准备好,我们目前暂且将所有光源类型限制为方向光,这意味着我们需要知道每个光源的颜色和方向信息。为了支持多光源,我们采用数组来存储。我们用一个单独的buffer存储光源的信息,给这个buffer命名为_LightBuffer.
然而我们并不能够在定义数组时不指定数组大小,我们声明一个宏来定义最大可见光源数量,用它来指定数组大小
加入一个DIffuseLight函数,它用传入进来的光源信息计算Diffuse光照
在LitPassFragment函数中加入for循环来支持多光源的渲染
2.2 Filling the Buffer
现在我们渲染出来的东西还是一片漆黑,这是因为我们还没把光源数据传进GPU来,我们需要在我们的渲染管线MyPipeline中声明同样大小的数组,再使用Shader.PropertyToID方法获取shader中相关属性的引用,
通过函数SetGlobalVectorArray操作command buffer,可以将数组数据传入到GPU中。
2.3 Configuring the Lights
我们现在是可以将光源数据每帧传输到GPU中了,但是现在确依然显示漆黑,这是因为我们还得先设置数据,我们声明个ConfigureLights函数来完成这项工作。
在culling剪裁中,Unity同时指出了哪些光源是可见的。这一信息可以从cull结果中获得,这一信息以visibleLights名字的list变量存储在cull结果中。
finalColor字段存储了光源的颜色,该颜色数据是由光源的color属性和intensity属性相乘后的结果,并经过了颜色空间的校正,所以我们可以直接使用该信息将其赋值给visibleLightColors数组。
然后,unity默认的渲染管线中,intensity定义在gamma空间,我们工作中线性空间,所以通过GraphicsSettings.lightsUseLinearIntensity 属性我们将其设置为线性空间。
方向光的光源方向信息可以通过光源的旋转信息获得,光源的方向是它的z轴方向。我们可以通过VisibleLight.localtoWorld矩阵获取在世界坐标系中的该信息。这个矩阵的第三列定义了光源的本地Z轴方向。
在shader中我们使用从物体朝向光源的向量方向进行计算,所以将获得的光源方向进行取反操作。
我们的shader目前将会计算四个光源,即使场景中没有四个光源,也将会计算四次。在场景中加入四个光源后,渲染的效果如下。
在frame debugger中可以查看到传入GPU的light data。
2.4 Varying the Number of Lights
当可见光的数量大于我们设定的maxVisibleLights时,会产生越界的错误,所以我们要对边界条件进行处理,当可见光数量大于maxVisibleLights时,忽略掉多出的那些光源(Unity光源排序的规则可以参考其他资料,简单来讲是通过光源的重要程度排序)
我们还要处理的一种情形是当光源数目由多变少,这时候需要清理重置光源的信息,确保下一帧的正确渲染。
3. Point Lights
这一节我们将实现渲染管线中的点光源。
3.1 Light Position
和方向光不同,点光源不关心光的方向而关心光源的位置。我们不另外开辟新数组存储位置信息,而是使用之前声明用于存储方向光方向信息的数组来存储点光源的位置数据。在Mypipeline中重新命名该数组
使用VisibleLight.lightType来判断当前光源的类型,当是方向光时存入方向信息,当是点光源时存入位置信息。
在shader函数中,使用该数据信息获取光源位置信息,并传入worldPos,两者相减即可获得光线的方向。
当是方向光时,w是0,当是点光源时w是1,我们利用该性质将worldPos 与 w分量相乘,这样就可以用同一个公式计算点光源和方向光的信息。
为了获取片段的位置信息,我们需要在shader中进行处理,由vertex函数输出到fragement函数。
至此,我们就可以看到点光源的效果了。
3.2 Distance Attenuation
和方向光不同,点光源要考虑光源强度随着距离而衰减。这里的衰减关系是距离平方的倒数。为了避免除数是0出现错误,因此加入一个极小的值0.00001
3.3 Light Range
点光源还有个属性是光照范围。 在范围外的物体将不会受该光源的影响,虽然在事实上它们可能会被物体照亮,但是用范围这个属性,我们可以更好的规定哪些物体受到该光源到影响,没有这个范围属性限制,所有的光源都会被认为是可见的。
范围属性不是突变的而是平滑渐变的,其公式为:
范围属性是场景中的数据,所以我们也需要将其传入GPU,这回我们将使用一个新的数组来存储它。
像之前做的一样,把数据用command buffer输入到GPU中
填充数据时,我们计算好,将结果存入数组后传入GPU,这样可以减少GPU的工作。
在shader中计算范围的影响,进行着色
4. Spotlights
接下来我们添加聚光灯光源.聚光灯和点光源很像,但是有方向的限制
4.1 pot Direction
像方向光源,聚光灯也是沿着它的z方向发射光,但是是一个圆锥形范围,它也有个位置属性,所以我们得新添加个数组来支持聚光灯。
判断光源类型,如果是聚光灯,将方向信息填入新的数组中。
在shader中添加方向数据。
4.2 Angle Falloff
聚光灯类型光源也是渐变的衰减,这个范围可以被定义为一个内层的角度和一个外层的角度,从内层的角度开始衰减,直到外层衰减到0.
Unity LWRP中,spot light类型光源只允许我们控制其外层角度,其衰减的方法被假定为与外层的角度有一个固定算法。
为了得到fallof,先把spot 光源的角度的一半由角度转换成弧度,并计算其cosine值。
根据外层的角度计算内层的角度的公式以及衰减函数的公式和计算如下所示:
其中衰减函数可以进行简化:
最后在shader中用计算出来的光照进行着色
为了保证不同类型的光照计算的一致性(用同样的shader代码),将w分量设置为1
5. Lights Per Object
目前我们支持了对一个物体用四个光源进行光照,实际上,无论有几个光源,目前每个物体都将计算4次,但其实很多时候是不必要的。不如如下的例子。9乘9的方格,共有81个球体,场景中有4个光源在四个角,当光源的范围并不是很大时,大多数球体只受到一个光源的影响,甚至有的球体不受到任何影响。
目前81个球体在开启GPU Instaing的时候将只会消耗一个draw call,但是球体的每个fragment将在fragment shader中计算4次光照,我们应该改进成只计算影响该fragment的光源。
5.1 Light Indices
在Culling期间,Unity也会计算出哪些光是可见的,每个物体受哪些光源的影响的信息可以以光照索引list的形式传输到GPU
Unity目前支持两种形式的光源索引,第一种是对每个物体,将其受影响的光源存入两个float4类型变量中。第二种是将所有物体受光源影响的信息以list形式一起存入单独的buffer中。然而目前Unity 2018.3版本只支持第一种,因此我们采用第一种。
设置rendererConfiguration字段为RendererConfiguration.PerObjectLightIndices8来开启光源的索引功能。
Unity现在需要为每个物体设置额外的数据以提供给GPU,这将会影响到GPU instancing。相较于根据受影响的光源分组,Unity更倾向于根据距离分组,另外光源的重要性也会影响到索引的排序,这些都会影响到合批。在我们的这个例子中,会由30个draw call,远大于1,当然也远小于81.
索引通过unity_4LightIndices0
and unity_4LightIndices1
引通变量可以获得,它们应该存在UnityPerDraw Buffer中。另外
unity_LightIndicesOffsetAndCount变量中的Y分量存有当前物体受多少光源影响的数量。
现在我们可以限制调用DiffuseLight着色的次数为实际需要的了,但是我们还需要取出正确的索引来使用。我们目前限制灯光数量最多为4个,所以只需从unity_4LightIndices0变量中获取。
限制GPU的开销变小了,我们只需要计算真正影响到物体的光源,通过frame debugger我们可以查看传入的光源的数量以及索引。
现在不在需要使用固定的数值来循环计算了,也不需要再去每次清除data。
5.2 More Visible Lights
现在可以支持更多可见的光源,让我们把场景中最大的可见光源数量提升到16,但是大部分物体只会受少量光源的影响。修改变量值为16:
对于unity_4LightIndices0变量,最多只能存储4个值,所以我们要注意不要越界:
但是我们可以不必限制单个物体最多受4个光源的影响,因为我们还可以用unity_4LightIndices1变量。但是我们不能超过8个,这已经是对当个物体来讲,目前能够支持最多数量的光源了:
光源的索引是按照重要程度排序的,对于大多数物体,后四个光源的影响其实很小,关掉前四个光源的效果,可以查看后四个光源的效果:
5.3 Vertex Lights
由于后四个光源其实并没有那么重要,我们可以将其计算从fragment函数中移到vertex函数中,也就是从逐像素光照改为逐顶点光照,这样虽然着色的精度会损失一些,但是可以减少GPU的消耗。现在,意味着我们支持4个逐像素光照,4个逐顶点光照,注逐顶点光照的结果要传入到fragment函数中,作为初始值参与光照的计算,和逐像素光照相加后输出:
5.4 Too Many Visible Lights
尽管目前我们已经支持到场景中最多16个光源,但是依然无法避免有可能会存在更多光源的情况。当超出时,我们需要告诉Unity需要将一些光源舍弃以避免数组的越界。
我们可以通过GetLightIndexMap函数获得光源索引的list,修改该list后再通过SetLightIndexMap函数存回去。Unity将对索引数组中为-1的值进行忽略,所以我们可以将超出的光源的索引改为-1:
进一步优化,我们可以只需要当数量确实超出时进行该操作:
5.5 Zero Visible Lights
另一个可能性是场景中没有一个光源,这时为了避免错误崩溃,我们需要先判断场景中的光源数量大于0再设置drawSettings.rendererConfiguration变量,同时只有在场景中光源数量大于0的情况下才设置光源数据:
不设置光源数据的一个副作用是这些数据将一直保持最后一个物体的数据,为了避免这个问题,我们需要手动将unity_LightIndicesOffsetAndCount设置为0: