第八章 更复杂的光照
Posted xiegaosen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第八章 更复杂的光照相关的知识,希望对你有一定的参考价值。
目录
@
Unity的光源类型
在前面的例子中,我们场景中都仅仅只有一个光源且光源类型是平行光(如果你的场景不是这样的话,可能会得到错误的结果)。只有一个平行光的世界很美好,但美梦总有行的那一天,这是我们就要在Unity Shader中处理更复杂的光源类型以及数目更多的光源。在本节中,我们将会学习如何在Unity中处理点光源(point light)和聚光灯(spot light)。
Unity一共支持4中光源类型:平行光、点光源、聚光灯和面光源(area light)。面光源尽在烘焙时才可发挥作用,因此我们不讨它。由于每种光源的几何定义不同,因此它们对应的光源属性也就不同。这就要求我们要区别对待它们。幸运的是Unity提供了很多内置函数来帮我们处理这些光源,我们后面会说到。现在我们要先来理解它们的原理。
1. 光源类型有什么影响
我们来看一下光源类型的不同到底会给Shader带来哪些影响。我们可以考虑Shader中使用了哪些属性。最常见的光源属性有光源的位置、方向(更具体的说就是,到某点的方向)、颜色、强度以及衰减(更具体的说是,到某点的衰减,与该点到光源的距离有关)这5个属性。而这些属性和它们的几何定义息息相关。
1.1 平行光
对于我们之前使用的平行光来说,它的几何定义是最简单的。平行光可以照亮的范围是没有限制的,它通常是作为太阳这样的角色在场景中出现的。下图给出了在Unity中平行光在Scene视图中的表示以及Light组件的面板。
平行光之所以简单,是因为它没有一个唯一的位置,也就说它可以放在场景中的任何位置(回忆一下,小时候我们是不是总觉得太阳和我们一起移动)。它的几何属性只有方向,我们可以调整Transform组件中的Rotation属性来改变它的光源方向,而且平行光到场景中所有点的方向都是一样的,这也是平行光名字的由来。除此之外,由于平行光没有一个具体的位置,因此也没有衰减的概念,也就是说,光照强度不会随着距离而发生改变。
1.2 点光源
点光源的照亮空间则是有限的,它是由空间中的一个球体定义的。点光源可以表示由一个点发出的、向所有方向延伸的光。下图给出了点光源在Scene视图中的表示以及Light组件的面板。
需要提醒读者的一点是,我们需要在Scene视图中开启光照才能看到预览光源是如何影响场景中的物体的。下图给出了开启Scene视图光照的按钮。
球体半径可以由面板中的Range属性来调整,也可以在Scene视图中直接拖拉点光源的线框(如球体上的黄色控制点)来修改它的属性。点光源是有位置属性的,它是由点光源的Transform组件中的Position属性定义的。对于方向属性,我们需要用点光源的位置减去某点的位置来得到它到该点的方向。而点光源的颜色和强度可以在Light组件面板中调整。同时,点光源也是会衰减的,随着物体逐渐远离点光源,它接受到的光照强度也会逐渐减小。点光源球心处的光照强度最强,球体边界处最弱,值为0。其中的衰减值可以由一个函数定义。
1.3 聚光灯
聚光灯是这3种光源类型中最复杂的一种。它的照亮空间同样是有限的,但不再是简单的球体,而是由空间中的一块锥形区域定义的。聚光灯可以用于表示由一个特定位置出发、向特定方向延伸的光。下图给出了Unity聚光灯在Scene视图中的表示以及Light组件的面板。
这块锥形区域的半径由面板中的Range属性决定,而锥体的张开角度由SpotAngle属性决定。我们同样也可以在Scene视图中直接拖拉聚光灯的线框(如中间的黄色控制点以及四周的黄色控制点)来修改它的属性。聚光灯的位置同样是由Transform组件中的Position属性定义的。对于方向属性,我们需要用聚光灯的位置减去某点的位置来得到它到该点的方向。聚光灯的衰减也是随着物体远离点光源而逐渐减小,在锥形的顶点处光照强度最强,在锥形的边界处强度为0。其中的衰减值可以由一个函数定义,这个函数相对于点光源衰减计算公式更加复杂,以为我们需要判断一个点是否在锥体的范围内。
2.在前向渲染中处理不同的光源类型
在了解3种光源的几何定义后,我们来看一下如何在Unity Shader中访问它们的5个属性:位置、方向、颜色、强度以及衰减。需要注意的是,本节均建立在使用前向渲染路径的基础上。
学完本节后,我们可以得到类似下图的效果:
2.1 实践
为了让物体受多个光源的影响 ,我们再新建一个点光源,把其颜色设为绿色,以和平行光进行区分。
我们的代码使用了Blinn-Phong光照模型,并为前向渲染定义了Base Pass和Additional Pass来处理多个光源。在这里我们只给出其中关键的代码。
(1)我们首先定义第一个Pass——Base Pass。为此,我们需要设置该Pass的渲染路径标签:
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)在Base Pass的片元着色器中,我们首先计算了场景中的环境光:
//Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz
(3)然后,我们在Base Pass中处理了场景中的最重要的平行光。在这个例子中,场景中只有一个平行光。如果场景中包含了多个平行光。Unity会选择最亮的平行光传递给Base Pass进行逐像素处理,其它平行光会按照逐顶点或在Additional Pass中按逐像素的方式处理。如果场景中没有任何平行光,那么Base Pass会当成全黑的光源处理。我们提到过,每一个光源有5个属性:位置、方向、颜色强度以及衰减。对于Base Pass来说,它处理的逐像素光源一定是平行光。我们可以使用_WorldSpaceLightPos0来得到这个平行光的方向(位置对平行光来说没有意义),使用_LightColor0来得到它的颜色和强度(_LightColor0已经是颜色和强度相乘后的结果),由于平行光可以认为是没有衰减的,这里我们直接令衰减值为1.0。相关代码如下:
//Compute diffuse term
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*max(0,dot(worldNormal,worldLightDir));
......
//Compute specular term
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 fixed(ambient+(diffuse+specular)*atten,1.0)
至此,Base Pass的工作就完成了
(4)接下来,我们需要为场景中其它逐像素光源定义Additional Pass。为此我们首先需要设置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的光照处理和Base Pass的处理方式是一样的,因此我们只需要把Base Pass的顶点和片元着色器代码粘贴到Additional Pass中,然后再稍微修改一下即可。这些修改往往是为了去掉Base Pass中的环境光、自发光、逐顶点光照、SH光照的部分,并添加一些对不同光源的支持。因此在Additional Pass的片元着色器中,我们没有再计算场景中的环境光。由于Additional Pass处理的光源类型可能是平行光、点光源或是聚光灯,因此在计算光源的5个属性——位置、方向、颜色、强度和衰减时,颜色和强度我们仍可以使用_LightColor0来得到,但对于位置、方向和衰减属性,我们就需要根据光源的类型分别计算。首先,我们来看如何计算不同光源的方向:
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz-i.worldPostion.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的衰减值我们后面会说到。
我们可以在场景中添加更多的逐像素光源来照亮物体。需要注意的是,本节只是为了讲解处理其他类型光源的实现原理,上述的代码并不会真正的用于项目中。
2.2 实验:Base Pass和Additional Pass的调用
创建四个点光源,把它们的颜色设为相同的红色,我们可以得到类似下图的效果:
那么,这样的结果是怎么来的呢?当我们创建一个光源时,默认情况下它的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中打开帧调试器,如下图所示
从帧调试其中可以看出,渲染这个场景的Unity一共进行了6个渲染事件,由于本例中只包含了一个物体,一次这6个渲染事件几乎都是用于渲染该物体的光照结果。我们可以通过依次单击帧调试器中的渲染事件,来查看Unity是怎样渲染物体的。下图给出了本例中Unity进行的6个渲染事件。
从图中可以看出,Unity是如何一步步将不同光照渲染到物体上的:在第一个渲染事件中,Unity首先清楚颜色、深度和模板缓冲,为后面的渲染做准备;在第二个渲染事件中,Unity利用Chapter9->ForwardRendering的第一个Pass,即Base Pass,将平行光的光照渲染到帧缓存中;在后面的4个渲染事件中,Unity使用Chapter9->ForwardRendering的第二个Pass,即Additional Pass,一次将四个点光源应用到物体上,得到最后的渲染结果。
可以注意到,Unity处理这些点光源的顺序是按照它们的重要度排序的。在这个例子中,由于所有的点光源颜色和强度都相同,因此它们的重要度取决于它们距离胶囊体的远近,因此上图中首先绘制的是距离胶囊体较近的光源。但是如果光源的强度和亚瑟互不相同,那么距离就不再是唯一的衡量标准。例如,如果我们把现在距离最近的点光源的强度设为0.2,那么从帧调试器中我们可以发现绘制顺序发生了变化。此时,首先绘制的是距离胶囊体第二近的光源,最近的点光源会在最后被渲染。Unity官方文档中并没有给出光源强度、颜色和距离物体的远近是如何具体影响光源的重要度排序的,我们仅知道排序结果和这三者都有关系。
对于场景中的一个物体,如果它不在一个光源的光照范围内,Unity是不会为这个物体调用Pass来处理这个光源的。我们可以把本例中距离最远的点光源的范围调小,使得胶囊体在它的照亮范围外。此时,再查看帧调试器,我们可以发现渲染事件比之前少了一个,如下图所示:
同样,如果一个物体不在某个聚光灯范围内,Unity也不会为该物体调用相关的渲染事件的。
我们知道,如果逐像素光源的数目很多的话,该物体的Additional Pass就会被调用多次,影响性能,我们可以通过把光源的Render Mode设为Not Important来告诉Unity,我们不希望把该光源当成逐像素处理。在本例中,我们可以把4个点光源的Render Mode都设为Not Important,可以得到下图的结果:
由于我们在Chapter-ForwardRendering中没有在Base Pass中计算逐顶点和SH光源,因此场景中的4个点光源实际上不会对物体造成任何影响。同样,如果我们把平行光的Render Mode也设为Not Important,物体就会仅显示环境光照的结果。
那么,我们如何在前向渲染路径的Base Pass中计算逐顶点和SH光呢?我们可以使用前面提到的内置变量和函数来计算这些光源的光照效果。
以上是关于第八章 更复杂的光照的主要内容,如果未能解决你的问题,请参考以下文章