Unity shader入门精要笔记(十六)~(十七)
1.1.1 移动平台的特点
游戏优化不仅是程序员的工作,同时还需要美工人员在游戏的美术上进行一定的权衡,比如避免使用全屏的屏幕特效、避免使用计算复杂的shader、减少透明混合造成的overdraw等。
移动设备上的GPU专注于尽可能使用更小的带宽和功能,也由此带来许多和PC不同的现象。overdraw(一个像素被绘制多次)将会可能造成性能的瓶颈。
相比Android平台,ios平台硬件条件则相对较一。
1.1.2 影响性能的因素
对于一个游戏来说,它主要使用两种计算资源:GPU和CPU。CPU主要负责保证帧率,GPU主要负责分辨率相关。
造成游戏性能瓶颈的主要原因分成以下几方面:
[1]CPU:
过多的drawcall;
复杂的脚本或者物理模拟;
[2]GPU:
顶点处理:
过多的顶点;
过多的逐顶点计算;
片元处理:
过多的片元(可能是由于分辨率造成,也有可能是overdraw);
过多的逐片元计算;
[3]带宽:
使用了尺寸很大且未压缩的纹理;
分辨率过高的帧缓存;
优化技术:
[1]CPU优化
使用批处理技术减少draw call数量
[2]GPU优化
减少需要处理的顶点数目:(1)优化几何体 (2)使用模型的LOD(Level of Detail)技术 (3)使用遮挡剔除(Occlusion Culling)技术
减少需要处理的片元数目:(1)控制绘制顺序 (2)警惕透明物 (3)减少实施光照
减少计算复杂度:(1)使用shader的LOD(Level of Detail)技术 (2)代码方面的优化
[3]节省内存带宽
减少纹理大小
利用分辨率缩放
1.1.3 Unity中的渲染分析工具
在Unity5中,这些工具包括了渲染统计窗口(Rendering Statistics Windows)、性能分析器(Profiler),以及帧调试器(Frame Debugger)。而不同的目标平台上,这些工具中显示的数据也会发生变化。
Unity5的渲染统计窗口:
渲染统计窗口(Rendering Statistic Windows)显示当前游戏的各个渲染统计变量,开发者可以通过Game视图右上方的菜单上点击Stats按钮来打开。
其中包含:音频(Audio)、图像(Graphics)和网络(Network)。而这里我们只需要关注第二方面,即图像相关的渲染统计结果。
性能分析器的渲染区域:
单击Windows -> Profile打开Unity的性能分析器(Profiler)。性能分析器中的渲染区域(Rendering Area)提供了更多关于渲染的统计信息。
而性能分析器给出的draw call数目和批处理数目、Pass数目并不相等,并且看起来要大雨开发者所估算的数目,这是因为Unity在背后需要进行很多工作,例如,初始化各个缓存、为阴影更新深度纹理和阴影映射纹理等,因此需要花费比“预期”更多的draw call。而Unity5引入了一个新的工具来帮助开发者查看每一个draw call的工作,该工具就是帧调试器。
再谈帧调试器:
通过Windows -> Frame Debugger打开该窗口。
在该窗口中可以看到每一个draw call的工作和结果,同时可看到事件为16,其中包含draw call事件数量为12。
在Unity的渲染统计窗口、分析器和帧调试器这3个利器的帮助下,开发者可以获得很多有用的优化信息。
其他性能分析工具:
对于Android工具来说,高通的Adreno分析工具可以对不同的测试机进行详细的性能分析,英伟达提供了NVPerfHUD工具来帮助开发者得到几乎所有需要的性能分析数据,例如每个draw call的CPU的事件,每个shader花费的cycle数目等。
而对于iOS平台,Unity内置的分析器可以得到整个场景话费的GPU事件。PowerVRAM的PVRUniSCo shader分析器也可以给出一个大致的性能评估。Xcode中的OpenGL ES Driver Instruments可以给出一些宏观上的性能信息,例如,设备利用率,渲染器利用率等。但对于Android平台,对iOS的性能分析更加困难。
1.1.4 减少draw call的数目
批处理的实现原理就是为了减少每一帧需要的draw call的数量,为了把一个对象渲染到屏幕上,CPU需要检查哪些光源影响了该物体,绑定shader并设置它的参数,再把渲染命令发送给GPU。当场景中包含了大量对象时,这些操作会非常耗时。
而使用同一个材质的物体可以一起处理。对于使用同一个材质的物体,它们之间的不同仅仅在于顶点数据的差别,开发者可以把这些顶点数据合并在一起,在一起发送给GPU,从而完成一次批处理。
动态批处理:
模型共享了同一个材质并满足一些条件,Unity就可以自动把它们进行批处理,从而只需要花费一个draw call就可以渲染所有的物体。而动态批处理的基本原理是,每一帧把可以进行批处理的模型网格进行合并,再把合并后模型数据传递给GPU,然后使用同一个材质对其渲染。处理实现方便,动态批处理的另一个好处是,经过批处理的物体仍然可以移动,这是因为由于在处理每帧时Unity都会重新合并一次网格。
合并的主要的条件限制;
[1]能够进行动态批处理的网格的顶点属性要小于900。
[2]所有对象需要使用同一个缩放尺度(可以是(1,1,1)、(1,2,3)、(1.5,1.4,1.3)等,但必须一致)
[3]使用光照纹理(lightingmap)的物体需要小心处理。
[4]多Pass的shader会中断批处理。
使用多个Pass的shader在需要应用多个光照的情况下破坏了动态批处理的机制,导致Unity不能对这些物体进行动态批处理。
静态批处理:
相对于动态批处理来说,静态批处理适合于任何大小的几何模型。其实现原理是,只在运行开始阶段,把需要进行静态批处理的模型和冰岛一个新的挽歌结构中,意味着这些模型不可以在运行时刻被移动。静态批处理的缺点在于,它往往需要占用更多的内存来存储合并后的几何结构。因为在静态批处理前一些物体共享了相同的网格,那么在内存中每一个物体都会对应一个该网格的复制品,即一个网格会变成多个网格再发给GPU。如果这一类使用同一网格的对象很多那么将会成为一个性能瓶颈。
静态批处理的实现只需要在物体面板上的static复选框构选上即可(事实上只需要勾选Batching static即可)。
(而虽然场景中的物体材质不一样,但是Unity合并后的网格包含多个网格,即场景中的多个对象。对于合并后的网格,Unity会判断其中使用同一个材质的字网格,然后对它们进行批处理)
对于使用同一材质的物体,Unity只需要调用一个draw call就可以绘制全部物体。而对于使用了不同材质的物体,静态批处理仍然可以提升渲染性能。尽管这些物体仍然需要调用多个draw call,但是静态批处理可以减少这些draw call的状态切换。但静态批处理将会让VBO(Vertex Buffer Object,顶点缓冲对象)的数目变大,即它需要更多的内存存储合并后的几何结构,即如果一些物体共享了相同的网格,那么在内存中每一个物体都会对应一个该网格的复制品。
而场景中包含了除了平行光以外的其他光源,并且在shader中定义了额外的pass来处理它们,这些pass将不会被批处理,而平行光的base pass仍然可以被静态批处理。
共享材质:
如果两个材质之间只有使用的纹理不同,开发者可以把这些纹理合并到一张更大的纹理中,这张更大的纹理被称为一张图集(atlas)。一旦使用了同一张纹理,开发者可以使用同一个材质,再使用不同的采样坐标对纹理采样即可。
而除了纹理不同外,不同的物体在材质上还有一些微小的参数变化。例如颜色不同或某些浮点属性不同。而不管是动态还是静态批处理,他们的前提都是使用同一个材质,而不是同一个shader的材质,即他们指向的材质必须是同一个实体。
批处理注意事项:
[1]选择静态批处理,要时刻小心对内存的消耗,记住经过静态批处理的物体不可以再被移动
[2]如果无法进行静态批处理而需要动态批处理时,小心各种限制条件
[3]游戏中的小道具可以使用动态批处理
[4]对于包含动画的物体,无法全部使用静态批处理,而如果不动的部分可以把这部分标示为static
批处理需要把多个模型变换到世界空间下合并他们,从而如果shader存在一些再模型空间下的坐标的运算,那么将会可能得到错误的结果,而可以在这部分的shader中使用DisableBatching标签来强制使用该shader的材质不会被批处理。
1.1.5 减少处理的顶点数目
常用的顶点优化策略:
优化几何体:
尽可能减少模型中三角面片的数目,一些对于模型没有影响、或是肉眼非常难察觉到区别的顶点都要尽可能去掉。为了经可能减少模型的顶点数目,美工往往需要优化网格结构,在很多3d软件中都有相应的优化选项,可以自动优化网格结构。
而相较于建模软件,开发者应该更加关心于在Unity中显示的数目。而Unity中所显示的顶点数目之所以往往会多于建模软件的顶点数目,这是因为在GPU看来,有时候一个顶点需要拆分成多个顶点,其原因主要有:(1)为了分离纹理坐标(uv splits);(2)为了产生平滑的边界(smoothing splits)。
减少顶点数目才是最为关心的事情,从而还可以移除不必要的硬边以及纹理衔接,避免边界平滑和纹理分离。
模型的LOD技术:
减少顶点数目的方法是使用LOD(Level of Detail)技术。该技术的原理是:当一个物体离摄像机很远时,模型上的很多细节是无法被察觉到的,因此LOD允许当对象逐渐远离摄像机时,减少模型上的面片数量,从而提高性能。
在Unity中,可以使用LOD Group组件来为一个物体构建一个LOD。当需要为一个对象准备多个包含不同细节成都的模型,然后把它们赋给LOD Group组件中的不同等级,Unity会自动判断当前位置上使用哪个等级的模型。
遮挡剔除技术:
顶点优化策略是遮挡剔除技术(Occlusion Culling)。遮挡剔除可以用来消除哪些在其他物体后面看不到的物件,这意味着资源不会浪费在计算那些看不到的顶点上,从而提高性能。
模型的LOD技术和遮挡剔除技术可以同时减少CPU和GPU的负荷,CPU可以提交更少的draw call,而GPU可以处理更少的顶点和片元数目。
1.1.6 减少处理的片元数目
控制绘制顺序:
为了最大限度地避免overdraw,一个重要的优化策略就是控制绘制顺序。由于深度测试的存在,从而如果保证了所有物体都是从前往后进行绘制,从很大程度上减少overdraw。因为后面绘制的物体无法通过深度测试,从而无法被绘制出来。
在Unity中,渲染队列数目小于2500(如“Background”、“Geometry”、“AlphaTest”的对象都被认为是不透明(opaque))的物体,这些物体总体上是从前到后绘制的。而使用其他队列(“Transparent”、“Overlay”)的物体,则是从后到前,意味着开发者尽可能把物体的队列设置为不透明物体的渲染队列,而经可能避免使用半透明队列。
以射击游戏为例,通常游戏中的主要角色会出现在屏幕的绝大部分,从而在渲染的时候可以先绘制它们(使用更小的渲染队列);而对于敌人,则往往会藏在遮挡物后面,从而可以把所有常规不透明物体都渲染完后再渲染他们;而对于天空盒,因为永远在后面,从而起队列可以设置为“Geometry+1”,从而保证永远不会造成overdraw。
时刻警惕透明物体:
半透明物体则需要从后往前进行渲染,这意味着一定会产生overdraw现象。对于上述GUI的这种情况,可以尽量减少窗口中的GUI所占的面积,如果没办法,可以把GUI的绘制和三维场景的绘制交给不同的摄像机,而其中负责三维场景的摄像机的视角范围经历不要与GUI的相互重叠。
而透明度测试也会影响游戏性能,虽然透明度测试没有关闭深度写入,但是比如它会在片元着色器中使用discard函数改变了片元是否会被渲染的结果,也就是说,只有在执行了所有的片元着色器后,GPU才知道哪些片元会被真正渲染到屏幕上,从而原先那些可以减少overdraw的优化都无效了,此时使用混合度混合的性能往往比使用透明度测试更好。
减少实施光照和阴影:
如果场景中包含了过多的点光源,并且使用了多个Pass的shader,那么很有可能将会造成性能下降。因为计算光照是一个非常复杂的过程。
而为了节省资源但又需要得到出色的画面效果,往往开发者会使用烘培技术,把光照提前烘培到一张光照纹理(lightmap)中,然后在运行时刻只需要根据纹理采样得到光照效果即可。另一种模拟光源的方法是使用God Ray。场景中很多小型光源的效果都是靠该方法模拟。它们一般不是真正的光源,很多时候通过透明纹理模拟得到的。
1.1.7 节省带宽
减少纹理大小:
使用纹理图集可以帮助开发者减少draw call的数目,而这些纹理的大小同样是需要考虑的问题。所有纹理的长宽比最好是正方形,而且长宽值最好是2的整数幂(即2、4、8、16、31、64等)。
除此之外,还应该尽可能使用多级渐变纹理技术(mipmapping)和纹理压缩。
利用分辨率缩放:
过大的屏幕分辨率和糟糕的GPU是游戏性能的两个瓶颈。因此需要对于特定机器进行分辨率缩放。
1.1.8 减少计算复杂度
shader的LOD技术:
与模型的LOD技术类似,Shader的LOD技术可以控制使用的Shader等级,它的原理是,只有shader的LOD值小于某个设定的值,这个Shader才会被使用。而使用了那些超过设定值的Shader的物体将不会被渲染。
通常使用以下值来指明该shader的LOD值:
SubShader{ Tags{ “RenderType” = “Opaque” } LOD 200 }
Unity内置的shader使用了不同的LOD值,如:Diffuse的LOD为200,Bumped Specular的LOD为400。
代码方面的优化:
游戏需要计算的对象、顶点和像素排序是:对象 < 顶点数 < 像素数。因此需要尽可能需要把采样坐标的计算放在对象或顶点上,例如在实现高斯模糊或边缘检测,把采样坐标的计算放在顶点着色器中,该做法远好于原片着色器中(就是有时候带来的效果可能没有片元着色器那么细腻,牺牲质量提高性能)。
尽可能使用低精度的浮点值进行运算,最高精度的float/highp适用于存储如顶点坐标等变量,但它的计算速度是最慢,从而应该避免在片元着色器中使用这种精度进行计算。而half/mediump适用于一些标量、纹理坐标等变量,它的计算速度大约是float的两倍。而fixed/lowp适用于绝大多数颜色变量和归一化后的方向矢量。其计算速度大约是float的4倍。
对于绝大多数GPU来说,在使用插值寄存器吧数值从顶点着色器传递给下一个阶段时,应该使用尽可能少的差值变量。例如如果需要对两个纹理坐标进行插值,通常会把它们打包到同一个float4类型的变量中,例如如果需要对两个纹理坐标进行插值,通常会把它们打包到同一个float4类型的变量中,两个纹理坐标分别对应xy分量和zw分量。
尽可能不要使用全屏的屏幕后处理效果。若实在是需要,啧尽量使用fixed/lowp进行低精度运算。同时尽量吧多个特效合并在一个shader中。例如可以吧颜色校正和添加噪声等屏幕特效在Bloom特效的最后一个Pass中合成。
同时代码优化规则:
尽可能不要使用分支语句和循环语句;
尽可能避免使用类似sin、tan、pow、log等较复杂的数学运算,利用查找表来代替;
尽可能不要使用discard操作,这会影响硬件的某些优化。
1.2 Unity shader入门精要笔记(十七)
1.2.1 表面着色器的一个例子
表面着色器代码:
Shader "Unity Shaders Book/Chapter 17/Bumped Diffuse" { Properties { _Color ("Main Color", Color) = (1,1,1,1) _MainTex ("Base (RGB)", 2D) = "white" {} _BumpMap ("Normalmap", 2D) = "bump" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 300 CGPROGRAM #pragma surface surf Lambert #pragma target 3.0 sampler2D _MainTex; sampler2D _BumpMap; fixed4 _Color; struct Input { float2 uv_MainTex; float2 uv_BumpMap; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 tex = tex2D(_MainTex, IN.uv_MainTex); o.Albedo = tex.rgb * _Color.rgb; o.Alpha = tex.a * _Color.a; o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); } ENDCG } FallBack "Legacy Shaders/Diffuse" }
还有一种定义为,Unity应该划分为表面着色器+光照模型+光照着色器,而表面着色器定义了模型表面的反射率、法线和高光等,使用表面着色器,开发者只需要告诉shader应该用什么纹理去填充颜色,用什么法线纹理去填充表面法线,使用兰伯特模型还是Blinn-Phong等模型,光照着色器则计算光照衰减、阴影等。
像上述代码,表面着色器的代码是直接而且必须写在subshader中,Unity会在背后生成多个pass,而表面着色器最重要的两个结构体以及它的编译指令。两个结构体是表面着色器中不同函数之间信息传递的桥梁,而编译指令是开发者和Unity沟通的重要手段。
1.2.2 编译指令
编译指令最重要的作用是指明该表面着色器使用的表面函数和光照函数,并设置一些可选参数。编译指令的一般格式如下:
#pragma surface surfaceFunction lightModel [optionalparams]
#pragma surface用于指明该编译指令是定义表面着色器,在其后面需要指明使用的表面函数(surfaceFunction)和光照模型(lightModel)。
表面着色器:
一个对象的表面属性定义了它的反射率、光滑度、透明度等值。而编译指令中的surfaceFunction用于定义这些表面属性。其通常是名为surf的函数,如:
void surf (Input IN, inout SurfaceOutput o) void surf (Input IN, inout SurfaceOutputStandard o) void surf (Input IN, inout SurfaceOutputStandardSpecular o)
SurfaceOutput和SurfaceOutputStandard还有SurfaceOutputStandardSpecular是Unity内置的结构体,它们需要配合不同的光照模型使用。
而在表面函数中,开发者会使用输入结构体Input IN来设置各种表面属性,并将这些属性存储输出当上述的结构体中,在传递给光照函数计算光照结果。
光照函数:
光照函数会使用表面函数中设置的各种表面属性来应用某些光照模型,进而模拟物体表面的光照效果。Unity内置了基于物理的光照模型函数Standard和StandSpecular,以及简单的非基于物理的光照模型函数Blinn-Phong和Lambert模型。
同时也可以自己定义光照模型:
//不依赖于视角的光照模型,如漫反射 half4 Lighting<Name>(SurfaceOutput s, half3 lightDir, half atten); //依赖于视角的光照模型,如高光反射 half4 Lighting<Name>(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten);
其他可选参数:
在编译指令的最后,开发者可以设置一些可选参数(optionalparams),这些可选参数包含了很多非常有用的指令类型,例如,开启/设置透明度混合/透明度测试,指明自定义的顶点和颜色修改函数,控制生成的代码等。
[1]自定义修改的函数:除了表面函数和光照模型外,表面着色器还可以支持其他两种自定义的函数:顶点修改函数(VertexFunction)和最后的颜色修改函数(ColorFunction)。顶点修改函数允许开发者自定义一些顶点属性,例如,把顶点颜色传递给表面函数,或是修改顶点位置,实现某些顶点动画等。最后的颜色修改函数则可以在颜色会知道屏幕前,最后一次修改颜色值,例如实现自定义的雾效等。
[2]阴影:可以通过一些指令来控制和阴影相关的代码,例如addshadow参数回味表面着色器生成一个阴影投射的pass。
[3]透明度混合和透明度测试:通过alpha和alphatest指令来控制透明度混合和透明度测试。
[4]光照:一些指令可以控制光照对物体的影响,例如,noambient参数会告诉Unity不要应用任何环境光照或光照探针(light probe)等指令参数。
[5]控制代码的生成:一些指令还可以控制由表面着色器自动生成的代码。
1.2.3 两个结构体
表面着色器支持最多自定义4种关键的函数:表面函数(用于设置各种表面性质,如反射率、法线等),光照函数(自定义表面使用的光照模型),顶点修改函数(修改或传递顶点属性),最后的颜色修改函数(对最后的颜色进行修改)。
一个表面着色器需要使用两个结构体:表面函数的输入结构体Input,以及存储了表面属性的结构体SurfaceOutput。
数据来源-Input结构体:Input结构体包含了很多表面属性的数据来源,其支持很多内置的变量名,通过这些变量名,开发者告诉Unity需要使用的数据信息。
表面属性-SurfaceOutput结构体:SurfaceOutput、SurfaceOutputStandard和SurfaceOutputStandardSpecular结构体用于存储这些表面属性的结构体,它会作为表面函数的输出,随后作为光照函数的输入来进行各种光照计算。相比于Input结构体的自由性,该结构体里面的变量是提前就声明好,不可以增加也不会减少。
如果使用了非基于物理的光照模型Blinn-Phong或Lambert,则常使用SurfaceOutput结构体,而如果使用了基于物理的光照模型Standard或StandardSpecular,则通常使用SurfaceOutputStandard或SurfaceOutputStandardSpecular结构体。
(时刻记住,表面着色器的本质就是包含了许多Pass的顶点着色器和片元着色器)
1.2.4 Unity 背后做了什么
1.2.5 表面着色器实例分析
表面着色器:
Shader "Unity Shaders Book/Chapter 17/Normal Extrusion" { Properties { _ColorTint ("Color Tint", Color) = (1,1,1,1) _MainTex ("Base (RGB)", 2D) = "white" {} _BumpMap ("Normalmap", 2D) = "bump" {} _Amount ("Extrusion Amount", Range(-0.5, 0.5)) = 0.1 } SubShader { Tags { "RenderType"="Opaque" } LOD 300 CGPROGRAM // surf - which surface function. // CustomLambert - which lighting model to use. // vertex:myvert - use custom vertex modification function. // finalcolor:mycolor - use custom final color modification function. // addshadow - generate a shadow caster pass. Because we modify the vertex position, the shder needs special shadows handling. // exclude_path:deferred/exclude_path:prepas - do not generate passes for deferred/legacy deferred rendering path. // nometa - do not generate a “meta” pass (that’s used by lightmapping & dynamic global illumination to extract surface information). #pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa //因为修改了顶点位置,从而要对其他物体产生正确的阴影效果不能直接依赖于FallBack中找到阴影投射Pass,addshadow参数告诉Unity生成一个该表面着色器对应的阴影投射Pass //而为了缩小自动生成代码量,从而使用exclude_path:deferred和exclude_path:prepass告诉Unity不要为了延迟渲染路径生成相应的pass,而nometa 取消对提取元数据的Pass的生成 #pragma target 3.0 fixed4 _ColorTint; sampler2D _MainTex; sampler2D _BumpMap; half _Amount; struct Input { float2 uv_MainTex; float2 uv_BumpMap; }; void myvert (inout appdata_full v) { //顶点法线对顶点位置进行膨胀 v.vertex.xyz += v.normal * _Amount; } void surf (Input IN, inout SurfaceOutput o) { //表面函数使用主纹理设置了表面属性中的反射率 fixed4 tex = tex2D(_MainTex, IN.uv_MainTex); o.Albedo = tex.rgb; o.Alpha = tex.a; o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); //并使用法线纹理设置了表面法线的方向 } half4 LightingCustomLambert (SurfaceOutput s, half3 lightDir, half atten) { //光照函数实现了简单的兰伯特反射光照模型 half NdotL = dot(s.Normal, lightDir); half4 c; c.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten); c.a = s.Alpha; return c; } void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) { //颜色修改函数中,简单使用了颜色参数对输出颜色进行调整 color *= _ColorTint; } ENDCG } FallBack "Legacy Shaders/Diffuse" }
1.2.6 surface shader缺点
任何在表面着色器中完成的事情,开发者都可以在顶点/片元着色器中重现。但顶点/片元着色器能做的事情却不一定可以在表面着色器中实现。
表面着色器虽然可以快速实现各种光照效果,但是开发者失去了对各种优化和各种特效实现的控制。因此,使用表面着色器往往会对性能造成一定影响,而内置的shader,例如Diffuse、Bumped Specular等都是使用表面着色器编写的。而这些版本(移动平台的相应版本)的shader值时去掉了额外的逐像素pass、不计算全局光照和其它一些光照计算上的优化。但想要更深沉的优化,表面着色器就不能满足开发者的需求。
除了性能比较差外,表面着色器还无法完成一些自定义的渲染效果,如透明玻璃的效果。
如果需要和各种光源打交道,可能会更适合使用表面着色器,但需要注意小心它的性能。
而如果需要处理的光源数目很少,例如只有一个平行光,则使用顶点/片元着色器是一个更好的选择。
(即多光源,表面着色器更方便;但如果是少光源甚至单光源,则顶点/片元着色器所呈现的效果会更多更好)
如果有很多自定义效果,则需要选择顶点/片元着色器。