美文网首页
UnityShader精要笔记九 不同光源处理

UnityShader精要笔记九 不同光源处理

作者: 合肥黑 | 来源:发表于2022-01-09 19:12 被阅读0次

本文继续对《UnityShader入门精要》——冯乐乐 第九章 更复杂的光照 9.2节进行学习

一、光源类型

Unity一共支持4中光源类型:平行光、点光源、聚光灯和面光源(area light)。面光源尽在烘焙时才可发挥作用,因此我们不讨它。

1.平行光

平行光之所以简单,是因为它没有一个唯一的位置,也就说它可以放在场景中的任何位置(回忆一下,小时候我们是不是总觉得太阳和我们一起移动)。它的几何属性只有方向,我们可以调整Transform组件中的Rotation属性来改变它的光源方向,而且平行光到场景中所有点的方向都是一样的,这也是平行光名字的由来。除此之外,由于平行光没有一个具体的位置,因此也没有衰减的概念,也就是说,光照强度不会随着距离而发生改变。

2.点光源

点光源的照亮空间则是有限的,它是由空间中的一个球体定义的。点光源可以表示由一个点发出的、向所有方向延伸的光。需要提醒读者的一点是,我们需要在Scene视图中开启光照才能看到预览光源是如何影响场景中的物体的。


图9.7 开启Scene视图中的光照

球体半径可以由面板中的Range属性来调整,也可以在Scene视图中直接拖拉点光源的线框(如球体上的黄色控制点)来修改它的属性。点光源是有位置属性的,它是由点光源的Transform组件中的Position属性定义的。对于方向属性,我们需要用点光源的位置减去某点的位置来得到它到该点的方向。而点光源的颜色和强度可以在Light组件面板中调整。同时,点光源也是会衰减的,随着物体逐渐远离点光源,它接受到的光照强度也会逐渐减小。点光源球心处的光照强度最强,球体边界处最弱,值为0。其中的衰减值可以由一个函数定义。

3.聚光灯

聚光灯是这3种光源类型中最复杂的一种。它的照亮空间同样是有限的,但不再是简单的球体,而是由空间中的一块锥形区域定义的。聚光灯可以用于表示由一个特定位置出发、向特定方向延伸的光。下图给出了Unity聚光灯在Scene视图中的表示以及Light组件的面板。


图9.8 聚光灯

这块锥形区域的半径由面板中的Range属性决定,而锥体的张开角度由SpotAngle属性决定。我们同样也可以在Scene视图中直接拖拉聚光灯的线框(如中间的黄色控制点以及四周的黄色控制点)来修改它的属性。聚光灯的位置同样是由Transform组件中的Position属性定义的。对于方向属性,我们需要用聚光灯的位置减去某点的位置来得到它到该点的方向。聚光灯的衰减也是随着物体远离点光源而逐渐减小,在锥形的顶点处光照强度最强,在锥形的边界处强度为0。其中的衰减值可以由一个函数定义,这个函数相对于点光源衰减计算公式更加复杂,因为我们需要判断一个点是否在锥体的范围内。

二、在前向渲染中处理不同的光源类型

回顾下9.1节的知识:对于前向渲染来说,一个UnityShader通常会定义一个Base Pass(Base Pass也可以被定义多次,例如需要双面渲染的情况)以及一个Additional Pass。一个Base Pass仅会执行一次(定义了多个Base Pass的情况除外),而一个Additional Pass会根据影响该物体的其他逐像素光源数目被多次调用,即每个逐像素光源会执行一次Additional Pass。
环境光和自发光也是在Base pass中计算的。这是因为对于一个物体来说,环境光和自发光我们只希望计算一次即可,而如果我们在Additional Pass中计算这两种光照,就会造成叠加多次环境光和自发光,这不是我们想要的。

图9.9 使用一个平行光和一个点光源共同照亮物体。右图显示了胶囊体、平行光和点光源在场景中的相对位置

为了让物体受多个光源的影响 ,我们再新建一个点光源,把其颜色设为绿色,以和平行光进行区分。我们的代码使用了Blinn-Phong光照模型,并为前向渲染定义了Base Pass和Additional Pass来处理多个光源。在这里我们只给出其中关键的代码。

1.定义第一个Pass——Base Pass
Shader "Unity Shaders Book/Chapter 9/Forward Rendering" {
    Properties {
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
        _Specular ("Specular", Color) = (1, 1, 1, 1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        
        Pass {
            // Pass for ambient light & first pixel light (directional light)
            Tags { "LightMode"="ForwardBase" }
        
            CGPROGRAM
            
            // Apparently need to add this declaration 
            #pragma multi_compile_fwdbase   

需要注意的是,我们除了设置渲染路径外,还使用了#pragma编译指令。#pragma multi_compile_fwdbase指令可以保证我们在shader中使用光照衰减等光照变量可以被正确赋值。这是不可缺少的。

2.计算场景中的环境光和自发光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

我们希望环境光计算一次即可,因此在后面的Additional Pass中就不会再计算这个部分。与之类似,还有物体的自发光,但在本例中,我们假设胶囊体没有自发光效果。

3.处理场景中最重要的平行光

在这个例子中,场景中只有一个平行光。如果场景中包含了多个平行光。Unity会选择最亮的平行光传递给Base Pass进行逐像素处理,其它平行光会按照逐顶点或在Additional Pass中按逐像素的方式处理。如果场景中没有任何平行光,那么Base Pass会当成全黑的光源处理。我们提到过,每一个光源有5个属性:位置、方向、颜色强度以及衰减。对于Base Pass来说,它处理的逐像素光源一定是平行光。我们可以使用_WorldSpaceLightPos0来得到这个平行光的方向(位置对平行光来说没有意义),使用_LightColor0来得到它的颜色和强度(_LightColor0已经是颜色和强度相乘后的结果),由于平行光可以认为是没有衰减的,这里我们直接令衰减值为1.0。相关代码如下:

fixed4 frag(v2f i) : SV_Target {
    fixed3 worldNormal = normalize(i.worldNormal);
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
    fixed3 halfDir = normalize(worldLightDir + viewDir);
    fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
    //The attenuation of directional light is always 1
    fixed atten = 1.0;
    
    return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

至此,Base Pass的工作就完成了。可以看出,和之前普通的Blinn-Phong没啥区别。

4.定义第二个Pass——Additional Pass
        Pass {
            // Pass for other pixel lights
            Tags { "LightMode"="ForwardAdd" }
            
            Blend One One
        
            CGPROGRAM
            
            // Apparently need to add this declaration
            #pragma multi_compile_fwdadd

除了设置渲染路径标签外,我们同样使用了#pragma multi_compile_fwdadd指令,如前面所说,这个指令可以保证我们在Addition Pass中访问到正确的环境变量。与Base Pass不同的是,我们还使用了Blend命令开启和设置了混合模式。这是因为我们希望Additional Pass计算得到的光照结果可以在帧缓存中与之前的光照结果进行叠加。如果没有使用Blend命令的话,Additional Pass会直接覆盖掉之前的光照结果。在本例中,我们选择的混合系数Blend One One,这不是必须的,我们可以设置成Unity支持的任何混合系数。常见的还有Blend SrcAlpha One。

5.Additional Pass的光照处理

通常来说,Additional Pass的光照处理和Base Pass的处理方式是一样的,因此我们只需要把Base Pass的顶点和片元着色器代码粘贴到Additional Pass中,然后再稍微修改一下即可。

这些修改往往是为了去掉Base Pass中的环境光、自发光、逐顶点光照、SH光照的部分,并添加一些对不同光源的支持。因此在Additional Pass的片元着色器中,我们没有再计算场景中的环境光。

由于Additional Pass处理的光源类型可能是平行光、点光源或是聚光灯,因此在计算光源的5个属性——位置、方向、颜色、强度和衰减时,颜色和强度我们仍可以使用_LightColor0来得到,但对于位置、方向和衰减属性,我们就需要根据光源的类型分别计算。首先,我们来看如何计算不同光源的方向:

fixed4 frag(v2f i) : SV_Target {
    fixed3 worldNormal = normalize(i.worldNormal);
    #ifdef USING_DIRECTIONAL_LIGHT
        fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    #else
        fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
    #endif

在上面的代码中,我们首先判断了当前处理的逐像素光源的类型,这是通过使用#ifdef指令判断是否定义了USING_DIRECTIONAL_LIGHT来得到的。如果前向渲染的Pass处理的光源类型是平行光,那么Unity的底层渲染引擎就会定义USING_DIRECTIONAL_LIGHT。如果判断得知是平行光的话,光源方向就可以直接由_WorldSpaceLightPos0.xyz得到;如果是点光源或聚光灯,那么_WorldSpaceLightPos0.xyz表示的是世界空间下的光源位置,而想要得到光源方向的话,我们就需要用这个位置减去世界空间下的顶点位置。

6.光源的衰减
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten =1.0;
#else
float3 lightCoord=mul(_LightMatrix0,float4(i.worldPosition,1)).xyz;
fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif

我们同样通过判断是否定义了USING_DIRECTIONAL_LIGHT来决定当前处理的光源类型。如果是平行光的话,衰减值为1.0。如果是其它的光源类型,那么处理更复杂一些。尽管我们可以使用数学表达式来计算给顶点相对于点光源和聚光灯的衰减,但这些计算往往涉及开根号、除法等计算量较大的操作,因此Unity选择了使用一张纹理作为查找表(Lookup Table,LUT),以在片元着色器中得到光源的衰减。我们首先得到光源空间下的坐标,然后利用该坐标对衰减纹理进行采样得到衰减值。关于Unity的衰减值,参考9.3节。

我们可以在场景中添加更多的逐像素光源来照亮物体。需要注意的是,本节只是为了讲解处理其他类型光源的实现原理,上述的代码并不会真正的用于项目中。

三、实验:Base Pass和Additional Pass的调用

在9.1.1节中给出了前向渲染中Unity是如何决定哪些光源是逐像素光,哪些是逐顶点或SH光。为了更加直观的理解,我们可以在Unity中进行一个实验。

在上个示例的基础上,新建4个红色的点光源,效果如下:


图9.10 使用1个平行光 + 4个点光源照亮一个物体

那么,这样的结果是怎么来的呢?当我们创建一个光源时,默认情况下它的Render Mode(可以在Light组件中设置)是Auto。这意味着,Unity会在背后为我们判断哪些光源会按逐像素处理,而哪些光源按顶点或SH的方式处理。由于我们没有更改Edit->Project Settings->Quality->Pixel Light Count中的数值,因此默认情况下一个物体可以接收除最亮的平行光外的4个逐像素光照。在这个例子中,场景中共包含了5个光源,其中一个是平行光,它会在Chapter9-Forward Rendering的Base Pass中按逐像素的方式进行处理;其余4个都是点光源,由于它们的Render Mode为Auto且数目正好等于4,因此都会在Chapter9-Forward Rendering的Additional Pass中按逐像素的方式被处理,每个光源会调用一次Additional Pass。

在Unity5中,我们还可以使用帧调试器(Frame Debugger)工具来查看场景的绘制过程,使用方法是:在window->Franme Debugger中打开帧调试器,如下图所示


图9.11 打开帧调试器查看场景的绘制事件

从帧调试其中可以看出,渲染这个场景的Unity一共进行了6个渲染事件,由于本例中只包含了一个物体,一次这6个渲染事件几乎都是用于渲染该物体的光照结果。我们可以通过依次单击帧调试器中的渲染事件,来查看Unity是怎样渲染物体的。下图给出了本例中Unity进行的6个渲染事件。


图9.12 本例中的6个渲染事件,绘制顺序是从左到右、从上到下进行的
从图中可以看出,Unity是如何一步步将不同光照渲染到物体上的:
  • 在第一个渲染事件中,Unity首先清楚颜色、深度和模板缓冲,为后面的渲染做准备;
  • 在第二个渲染事件中,Unity利用Chapter9->ForwardRendering的第一个Pass,即Base Pass,将平行光的光照渲染到帧缓存中;
  • 在后面的4个渲染事件中,Unity使用Chapter9->ForwardRendering的第二个Pass,即Additional Pass,一次将四个点光源应用到物体上,得到最后的渲染结果。

可以注意到,Unity处理这些点光源的顺序是按照它们的重要度排序的。在这个例子中,由于所有的点光源颜色和强度都相同,因此它们的重要度取决于它们距离胶囊体的远近,因此上图中首先绘制的是距离胶囊体较近的光源。

但是如果光源的强度和颜色互不相同,那么距离就不再是唯一的衡量标准。例如,如果我们把现在距离最近的点光源的强度设为0.2,那么从帧调试器中我们可以发现绘制顺序发生了变化。此时,首先绘制的是距离胶囊体第二近的光源,最近的点光源会在最后被渲染。Unity官方文档中并没有给出光源强度、颜色和距离物体的远近是如何具体影响光源的重要度排序的,我们仅知道排序结果和这三者都有关系。

对于场景中的一个物体,如果它不在一个光源的光照范围内,Unity是不会为这个物体调用Pass来处理这个光源的。我们可以把本例中距离最远的点光源的范围调小,使得胶囊体在它的照亮范围外。此时,再查看帧调试器,我们可以发现渲染事件比之前少了一个,如下图所示:


图9.13 如果物体不在一个光源的光照范围内(从右图可以看出,胶囊体不在最左方的点光源的照明范围内),Unity是不会调用Additional Pass来为该物体处理该光源的

同样,如果一个物体不在某个聚光灯范围内,Unity也不会为该物体调用相关的渲染事件的。
我们知道,如果逐像素光源的数目很多的话,该物体的Additional Pass就会被调用多次,影响性能,我们可以通过把光源的Render Mode设为Not Important来告诉Unity,我们不希望把该光源当成逐像素处理。在本例中,我们可以把4个点光源的Render Mode都设为Not Important,可以得到下图的结果:


图9.14 当把光源的Render Mode设为Not Important时,这些光源就不会按逐像素光来处理

由于我们在Chapter-ForwardRendering中没有在Base Pass中计算逐顶点和SH光源,因此场景中的4个点光源实际上不会对物体造成任何影响。同样,如果我们把平行光的Render Mode也设为Not Important,物体就会仅显示环境光照的结果。

那么,我们如何在前向渲染路径的Base Pass中计算逐顶点和SH光呢?我们可以使用9.1.1节中提到的内置变量和函数来计算这些光源的光照效果。

四、Unity的光照衰减

在前面,我们提到Unity使用一张纹理作为查找表在片元着色器中计算逐像素光照的衰减。这样的好处在于,计算衰减不依赖于数学公式的复杂性,我们只要使用一个参数值去纹理中采样即可。但使用纹理查找来计算衰减也有一些弊端。
●需要预处理得到采样纹理,而且纹理的大小也会影响衰减的精度
●不直观,同时也不方便,因此一旦把数据存储到查找表中,我们就无法使用其它数学公式来计算衰减

但由于这种方法可以在一定程度上提升性能,而且得到的效果在大部分情况下都是良好的,因此Unity默认的就是使用这种纹理查找方式来计算逐像素的点光源和聚光灯的衰减的。

1.用于光照衰减的纹理

Unity在内部使用了一张名为_LightTexture0的纹理来计算光源衰减。需要注意的是,如果我们对该光源使用了cookie,那么衰减查找纹理是_LightTextureB0,但这里不讨论这种情况。我们通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了在光源空间中不同位置的点的衰减值。例如,(0,0)点表明了与光源位置重合的点的衰减值,而(1,1)点表明了在光源空间中所关心的距离最远点的衰减。

为了对_LightTexture0纹理采样得到给定点到该光源的衰减值,我们首先需要得到该点在光源空间中的位置,这是通过_LightMatrix0变换矩阵得到的。在前面我们已经知道_LightMatrix0可以把顶点从世界空间变换到光源空间。因此,我们只需把_LightMatrix0和世界空间中的顶点坐标相乘即可得到光源空间中的相应位置。

float3 lightCoord=mul(_LightMatrix0,float4(i.worldPosition,1)).xyz;

然后,我们可以使用这个坐标模的平方对衰减纹理进行采样,得到衰减值:

fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;

可以发现,在上面的代码中,我们使用了光源空间中顶点距离的平方(通过dot函数来得到)来对纹理采样,之所以没有使用距离值来采样是因为这种方法可以避免开方操作。最后,我们使用宏UNITY_ATTEN_CHANNEL来得到衰减纹理中衰减值所在的分量,以得到最终的衰减值。

2.使用数学公式进行计算

尽管纹理衰减的方法可以减少计算衰减时的复杂度,但有时我们希望可以在代码中利用公式来计算光源的衰减。例如下面的代码可以计算光源的线性衰减。

float distance = length(_WorldSpaceLightPos.xyz-i.worldPosition.xyz);
atten=1.0/distance;//linear attenuation

可惜的是,Unity没有在文档中给出内置衰减计算的说明。尽管我们仍可以在片元着色器中利用一些数学公式来计算衰减,但由于我们无法在Shader中通过内置变量得到光源的范围、聚光灯的朝向、张开角度等信息,因此得到的效果有些时候不尽人意,尤其在物体离开光源的照明范围时会发生突变(这是因为,如果物体不在该光源的照明范围内,Unity就不会为物体执行一个Additional Pass)。当然,我们可以利用脚本将光源的相关信息传递给Shader,但这样的灵活性很低。我们只能期待未来版本中Unity可以完善文档并开发更多的参数给开发者使用。

相关文章

网友评论

      本文标题:UnityShader精要笔记九 不同光源处理

      本文链接:https://www.haomeiwen.com/subject/omsycrtx.html