1. unity的光照类型

2. unity中的渲染路径

3. 平行光处理

4. 附加光源处理

5. 阴影

1:Unity中的光照类型
在unity编辑器中,有四种光源类型:Directional Light 平行光;Point Light 点光源;Spot Light 聚光灯;Area Light面光源。这些光源都有一些共同的属性,分别是:光源位置;光源方向;光源颜色;光源强度;和光源衰减。
1.1平行光的属性:
平行光照亮的范围是没有限制的,通常是被想象成现实世界中的太阳一样的光源体。所以在3D世界中,假象平行光的位置属性是没有意义的。因此平行光的衰减不会随着距离发生改变,所以计算平行光的衰减也是没有意义的。基于以上平行光的属性是只具有光源颜色,光源强度,和光源方向三个属性。
1.2 点光源的属性:
点光源的光源范围是有限的。点光源可以由一个圆形的点,向四周散发所有方向延伸的光来表示。因此,点光源的属性是有光源位置;光源方向;光源颜色;光源强度;和光源衰减五个属性的。点光源衰减是指:满能量的点光源原点,到无能量的点光源范围边界的值。假设光源原点的值是1,那么光源范围边界的值是0。调整光源强度可以调整光源原点到光源边界的能量值的变化。
1.3聚光灯的属性:
聚光灯的范围也是有限的,聚光灯可以由一个起始位置点,到一个特定方向延伸的锥形光来表示,同时还有半径和张开角度的设定。因此聚光灯和点光源一样,也是具有光源位置;光源方向;光源颜色;光源强度;和光源衰减五个属性。聚光灯的光源衰减和点光源类似,由起始位置点能量为1,到半径位置终点能量为0为止。同时还需要注意一个聚光灯的角度。超出角度边界之后的灯光能量会快速递减为0。
1.4面光源的属性:
面光源的范围也是有限的。不过不同的是面光源的起始光源是一个矩形面,由于光照计算对处理器性能消耗较大,因此面光源不可实时处理,只能烘焙到光照贴图中。面光源也是有光源位置;光源方向;光源颜色;光源强度;和光源衰减五个属性。面光源的方向就是由起始光源矩形位置所代表的面向正前方的方向。可以通过调整面光源的长和宽来调整起始矩形方向的大小。物体远离面光源设定的衰减半径距离时开始衰减光源强度。
2: Unity中的渲染路径
了解Unity中光照类型之后,就可以选择需要的灯光渲染在物体上了。在执行灯光渲染之前,还需要了解一下Unity中的渲染路径。在Unity中主要有两种渲染路径,前向渲染路径和延迟渲染路径。
2.1:前向渲染路径
前向渲染路径决定了物体如何渲染到屏幕上的规则。前向渲染路径的主要需要计算的信息有颜色和深度。也就是颜色缓冲区,和深度缓冲区。在前向渲染路径下,划分了A区用来保存当前屏幕上的某个像素的颜色信息;用B区来保存当前屏幕上的某个像素的深度信息。当屏幕上某一个像素的颜色有好几个的时候,用深度信息来判断,应该渲染哪一个颜色。距离摄像机也就是屏幕最近的颜色是应该被渲染出来的。确定好一帧画面的整个像素的颜色后,就输出给屏幕,让我们能直观的看见。
众所周知,计算光源是非常消耗性能的,因此,光照在前向渲染路径中,有三种处理方式:逐顶点处理、逐像素处理、球谐函数处理。逐像素处理就是根据屏幕上显示这个物体的像素,来计算接受光照后的像素是什么样的。这是效果最好的一种处理。逐顶点处理就是在物体的网格顶点上计算接受光照后的颜色,再输出到屏幕的像素上。这是效果比较一般的处理。球谐函数处理是由函数变换来进行的,优点是速度快,成本可以很低。缺点是频率很低,仅影响漫反射光照,可以应付小型动态物体,因此效果是最差的。
因此选择前向渲染路径,还需要设定那些灯光会进行优先逐像素渲染。一般默认前4个灯光会逐像素,或者手动选择灯光的Render mode为important。接着的四个灯光为逐顶点处理,其他的为球谐处理。可以在Quality中设置逐像素处理灯光的数量。
Unity中默认选择的是前向渲染路径。可以在shader内部设置渲染路径:Tags{“LightMode” = “ForwardBase”}
2.2 延迟渲染路径
除了前向渲染路径,还有延迟渲染路径。延迟渲染的目的是为了解决大量无效灯光计算产生的性能浪费问题。延迟路径对场景中的光源数量没有限制,所有光源都逐像素处理。且在延迟渲染路径中,光照是和屏幕空间的大小有关。并且是根据“G缓冲区”和“深度缓冲区”来计算。使用延迟渲染路径是有门槛的,而且不支持抗锯齿,并且无法处理半透明物体。在unity中设置了延迟渲染路径之后,却无法处理的情况下会自动使用低一级渲染路径来渲染。
在延迟渲染中,首选会使用深度缓冲区得到能够在屏幕上显示出来的一些几何信息,储存在G缓冲区中,然后在迭代渲染G缓冲区中的可见像素的光照。需要注意的是在延迟渲染路径中,计算光照只能使用同一个光照算法,原因就是渲染到G缓冲区的是需要在屏幕上显示的像素,到这一步已经无法知道哪个点属于哪个mesh网格了,所以只能使用统一的光照算法。
3.0 平行光处理(Base Pass)
上面介绍了unity中目前最常用的两种渲染路径,接下来将以前向渲染路径为基础,看看unity中的平行光是如何被渲染的。至于为何从平行光开始,是由于默认的场景中必定会有一个平行光,且平行光的计算相对简单,光照模型基本默认的是基于平行光的计算。
正式开始进入具体光源处理前,还需要介绍一下一些经验模型,也就是前人根据经验总结出来的一套用来渲染光照模型算法。主要有以下几个:
3.1:光照模型介绍
3.1.1兰伯特(Lambert)经验模型1760年
兰伯特光照模型,模拟了理想环境下的漫反射效果。
3.1.2 Phong 经验模型 1973年
也是理想状态下的经验模型,在兰伯特光照模型的基础上增加了镜面反射部分。
3.1.3 Blinn-Phong 经验模型 1977年
在Phong模型的基础上改进了镜面反射部分,提出了半程向量h,对Phong模型的结果进行了优化,使得到的结果在某些方面更接近实验结果。
了解到这三种承接关系的基础光照经验模型之后,已经可以写出标准光照模型了,接下来正式开始描述Unity是如何处理多种光源的。经验模型的理论上是只考虑一个平行光的处理的情况下该是什么样的。所以我们从简单的入手,首先是平行光的处理,分别实现平行光的漫反射,高光反射,和环境光。
3.2.1 平行光的漫反射部分
兰伯特经验模型计算漫反射的公式:
此公式中,kd是漫反射系数,对物体表面吸收率调节,可以用来模拟人眼看到的物体是什么颜色。通常人眼看到的颜色是物体不吸收,被反射出来的颜色。例如绿色的树叶,吸收了除绿色之外的颜色,那么人眼看树叶就是绿色的。I/r^2 是衰减公式。由于unity中的平行光没有光源衰减,所以暂时可以不用考虑。Max函数用来限定向量N和向量L点乘的数值,不低于0,低于零是黑色,反正都是黑色,数值多少就无所谓,还能避免出现奇怪的情况,使用max()函数可以直接舍弃负数。 向量N是法线向量,L是光源方向向量。
可以看出,漫反射公式核心的内容其实时法线向量和光源方向的点乘。公式推理的背后隐藏着许多有趣的内容,有兴趣深挖背后原理的,可以参考底部参考学习资料。暂且此文不深挖背后原理,只描述清楚具体实现过程。
核心代码:
    
    
worldNormal = normalize ( mul ( v . normal , ( float3x3 ) unity_WorldToObject ) ) ; worldLightDir = normalize ( _WorldSpaceLightPos0 . xyz ) ; diffse = _LightColor0 . rgb * _Diffuse . rgb * max ( 0 , dot ( worldNormal , worldLightDir ) ) ;
worldNormal的计算过程:
normalize函数用来归一化。Mul()函数用来矩阵相乘,使用了normal矩阵和模型空间到世界空间的逆矩阵相乘,使法线转换到世界坐标。
worldLightDir的计算过程:
直接使用了内置函数 _WorldSpaceLightPos0.xyz 来获得当前场景的首位平行光的位置方向信息。
Diffse的计算过程:
使用Unity内置变量 _LightColor0.rgb 来获取处理的光源的强度和颜色信息。以及自定义的材质漫反射颜色信息 _Diffuse_LightColor0_Diffuse 就是计算公式中的kd漫反射系数。最后使用max()函数限定数值范围0到法线方向和灯光方向的点乘数值。
这样就完成了一个平行光漫反射计算。但是兰伯特计算公式有一个缺点,光的背面会完全是黑色。因此提出了半兰伯特光照模型的概念。也就是,把worldNormal和worldLightDir的点乘数值从-1到1的范围,通过一个乘以0.5再加0.5的操作方式,转换成0到1的范围之间。看起来和现实世界的情况会有所差距,但是物体的暗部也有明暗变化了。最终效果会比普通兰伯特经验模型会更亮。
    
    
halfLambert = dot ( worldNormal , worldLightDir ) * 0.5 + 0.5
3.2.2平行光的高光反射,和高光反射计算的近似算法
现实世界中,基本上所有物体都会有高光反射现象,由于物体表面的粗糙度不同,会反射不同程度的高光能量,物体表面越光滑,则高光反射范围越小且越亮。基于现实生活中的例子,以及上面描述的Phong 经验光照模型,我们可以为已经计算了漫反射的物体添加高光。
高光反射的计算公式:
通过公式可以发现,与上边的漫反射公式很类似。不同的是,增加了一个Mgloss 高光反射系数,可以通过此系数调整高光区域大小。还有漫反射的法线和灯光方向点乘改成了视角方向和反射方向点乘。等式后面的第一个括号其实就是漫反射方程里的kd 这个概念。
反射方向计算公式:
看不懂反射方向计算公式也没关系,在Unity当中,提供了一个reflect()函数来计算反射方向,传入入射方向i,法线方向n,就可以计算出反射方向。
    
    
reflectDir = normalize ( reflect ( - worldLight , worldNormal ) ) ;
这里使用的是负的世界空间中的灯光方向-worldLight,这是由于reflect()函数要求的入射方向是光源指向交点。因此需要取反。
法线方向直接使用漫反射中获取的法线方向,传入reflect()函数即可获得反射方向。
高光反射公式中还有一个参数是视角方向,可以使用内置变量 _WorldSpaceCameraPos 来获取摄像机的位置,使用摄像机的位置和物体顶点的位置做向量减法,再进行归一化,即可获得视角方向。
    
    
viewDir = normalize ( _WorldSpaceCameraPos . xyz - i . worldPos . xyz ) ;
最后组装成高光反射公式
    
    
specular = _LightColor0 . rgb * _Specular . rgb * pow ( saturate ( dot ( reflectDir , viewDir ) ) , _Gloss ) ;
pow()函数传入两个参数,参数二是参数一的幂,通过自定义变量_Gloss来控制高光范围的大小。
至此我们使用phong经验模型的理论公式实现了一个高光反射模型,可以直接用此模型公式结果+上兰伯特漫反射公式计算的结果,来得到一个拥有漫反射和高光的模型。也就是类似各种3d软件默认材质的效果了。
在某些情况下,Phong模型存在一些问题,这些问题在Blinn Phong中得到改善,例如计算反射方向r向量使用reflect函数,比较与计算半程向量h使用,消耗的性能更大。上面介绍Blinn Phong光照模型的时候有提到,是由于Blinn Phong引入了一个新的矢量h;半程向量。
下面是blinn phong的计算公式:
半程向量h的公式:
来看Blinn Phong计算公式,Blinn Phong计算公式对比Phong计算公式使用了N法线向量,和H半程向量的点乘运算来计算。半程向量h的计算公式很简单,归一化的v+l向量,就是上面提到过的视角方向,和灯光方向相加再归一化。
    
    
h = normalize ( worldLight + viewDir ) ;
得到h之后一切都很简单了,其他的向量N,V,L向量咱在兰伯特和phong模型的计算中,都已经得到过。
    
    
Blinn specular = _LightColor0 . rgb * _Specular . rgb * pow ( Max ( 0 , dot ( worldNormal , h ) ) , _Gloss ) ;
目前常用的基本光照就计算完毕了。但是在实际项目中还会需要添加环境光, 添加环境光,直接使用Unity内置变量 UNITY_LIGHTMODEL_AMBIENT 就可以获得;
    
    
ambient = UNITY_LIGHTMODEL_AMBIENT . xyz
最终的平行光计算就是上面计算的【环境光+漫反射+反射高光】
4.0 附加光源(Additional Pass)
在unity中实际我们需要计算的光源除了上面已经计算了的平行光以外,其实只需要再考虑点光源,和聚光灯。为什么不考虑面光源呢,由于面光源只能烘焙成灯光贴图使用,所以无需在附加光源中计算它,所以下面重点讨论点光源和聚光灯。
附加光源,顾名思义。是独立与上面计算基本光照之外的,单独计算然后添加进物体上的光源计算内容。由于平行光的光源位置和光源衰减是没有意义的,所以在前一部分内容并有没计算位置和衰减,而是直接通过函数获得位置。而点光源和聚光灯都有光源衰减,和光源方向需要计算。
好,概念的是这个概念,该如何写呢。Unity接触较多的朋友肯定会了解到一个神奇的东西—Shader,没错以上代码都是在Shader中编写的,但是又不是真正的Shader。Unity为了方便开发者提供了高层级的渲染抽象层 ShaderLab语言。使用此语言可以很轻松的在Unity中开发着色器。了解完shader这个概念,还需要稍微深入了解一下,shader中的pass这个概念。真正实现着色器功能的代码会写在pass中,shader中可以包含很多pass,当然尽量不要很多,越少pass渲染性能越佳。上面讲的经验模型代码pass,默认接受的灯光是场景中最亮的平行光。而我们其他光源需要写在另外一个pass,这个pass就是 Additional Pass ,如果场景中有多个灯光,就会重复计算 Additional Pass 。为了区分,首先pass中将会添加 Tags{“LightMode” = “ForwardAdd”} 渲染路径标签来区分Pass的类型。添加编译指令 #pragma multi_compile_fwdadd 来确保能正确获取正确内置光照变量。还需要定义混合模式 Blend One One ,确保在计算多个光源的时候,后一个光源是叠加,而不是覆盖掉之前计算好了的光源。
Additional Pass 计算中,由于点光源和聚光灯具有相似的计算过程,我们还需要区分平行光和点光源聚光灯。可以使用 #ifdef USING_DIRECTIONAL_LIGHT 判断语句来判断,如果当前Pass计算的是平行光,则会直接定义 USING_DIRECTIONAL_LIGHT 并不需要操心具体定义过程,Unity已经为我们做好了这一切。前面提到平行光在unity中并没有衰减,所以光源位置并不重要,只需获取平行光的位置就可以,衰减直接为1就可以。我们可以通过内置变量 _WorldSpaceLightPos0.xyz 的归一化直接获取平行光方向。
其他光源也类似,当 USING_DIRECTIONAL_LIGHT 没有被定义的时候,也就是当前灯光不是平行光的时候,则进入 #eles 语块,在这里计算其他光源的位置信息,由于点光源和聚光灯的灯光位置和灯光方向还有衰减都是有意义的,所以我们需要先计算当前光源位置到接受当前光源位置的物体的方向。通过 _WorldSpaceLightPos0.xyz 也可以直接获得当前光源位置,然后减去当前物体的位置,再归一化即可获得接受灯光物体对于灯光的方向。
附加光照计算完后,还是需要计算当前灯光的漫反射和高光反射的内容,漫反射和高光反射上一节已经讲过。接着呢就要计算光源衰减。在Unity中,计算光源衰减默认是使用光照衰减的纹理获得。好处是可以不依赖复杂的数学公式计算,获得比较好的性能结果,大部分情况下是不错结果。缺点也是无法使用数学公式计算光源衰减,而且纹理的大小会影响精度。由于大部分情况下是能得到不错的结果的,所以在此处我们使用unity默认的办法来进行光照衰减的获得。
同样,在计算衰减的时候也要区分平行光和其他光源,还记得上面说的吗,平行光没有光源衰减,或者说平行光的光源衰减永远是1。因此使用的判断语句和上面的类似,也是 #ifdef USING_DIRECTIONAL_LIGHT 来判断是否是平行光,如果是,则衰减的变量为1,如果不是, #eles 开始计算点光源或聚光灯的衰减。上面提到的光照衰减纹理就在这里排上了用场,首先需要获得物体在光源空间中的位置,以方便计算,使用一个矩阵和物体的世界坐标相乘即可; lightCoord = mul(unity_WorldToLight,float4(i.worldPos,1)).xyz; 这一步使用了lightCoord变量来储存这个位置的信息。Mul()就是用来计算矩阵相乘的函数。获得了需要计算衰减的坐标之后,使用坐标的模的平方来对光源衰减纹理采样,得到具体的衰减值,在用一个变量去储存即可。
    
    
atten = tex2D ( _LightTexture0 , dot ( lightCoord , lightCoord ) . rr ) . UNITY_ATTEN_CHANNEL ;
_LightTexture0 即是那个对应的,储存光源衰减的纹理。使用的是tex2D函数来计算此纹理的值,最后使用 UNITY_ATTEN_CHANNEL 宏,来获得衰减纹理中衰减值的分量,得到最终的衰减。
最后漫反射和高光分别乘以衰减值即可获得附加光源最终的灯光效果。
5.0阴影
Unity中阴影的计算,需要分两步,一步是需要计算物体本身投射的阴影,另外一步则是需要接受来子其他物体的投射阴影。
在Unity中投射阴影其实是相当简单的,如果是仅仅应用的话。直接在灯光的Inspactor面板中开启阴影设置,在Pass中回调一个基础的shader即可。
而接受阴影呢则需要在Shader代码中添加一些内容:
  1. pass中添加一个包含文件 #include “AutoLight.cginc” ,就可以使用包含文件中的宏了。
  2. 在顶点着色输出结构体中添加内置宏 SHADOW_COORDS(0) ,声明一个阴影纹理采样的坐标,括号内的参数需要一个可用的插值寄存器的索引。
  3. 在顶点着色器返回前添加另外一个宏 TRANSFER_SHADOW(o) ,用来计算上一步声明的阴影纹理坐标。
  4. 在片元着色器中再使用一个宏 SHADOW_ATTENUATION(i) ,在片元着色器中计算阴影值。
在片元着色器的最后,与其他漫反射,环境光,高光反射,衰减值相乘,这样就完成阴影计算了。
计算阴影其实是一个很费劲的过程,之所以在unity中可以使用简单的操作获得阴影,unity在背后为我们做了很多事。比如映射阴影的时候用的屏幕空间的阴影映射技术,已经在内置的一些shader中包含进去了。直接Fallback 就可以直接找到,无需操心过多。
完。
参考资料:
https://zhuanlan.zhihu.com/p/102134614
《Unity shader 入门精要》
https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_reflection_model
闫令琪-GAMES101-现代计算机图形学入门
https://paroj.github.io/gltut/Illumination/Tut11%20BlinnPhong%20Model.html
https://zhuanlan.zhihu.com/p/102135703
Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐