Unity着色器训练营(1):入门篇
对于游戏开发者而言,着色器一直充满神秘感。但只要你真正理解,就会发现着色器并不是特别复杂,而且它还可以实现你的奇思妙想。今天Unity大中华区技术经理鲍健运将为大家分享,由浅入深学习撰写Unity着色器脚本,实现美妙有趣的画面效果,带领大家叩开着色器开发的大门。
概述
列举一个现实生活中的例子,梵高的名画《向日葵》。它实在是太过有名,以至于不少人喜欢搞一件仿制品挂在墙头,为房间的装饰增添美感。
但是仿制品的生产依靠的是血汗工厂,因为制作这样一幅油画,需要一笔笔画出每个部分,非常地耗时耗力,因此我不得不以5倍速去播放它的绘制过程。如果观察右边的这幅《向日葵》,你会发现它是原有画作三角化的效果,分分钟从印象派风格变成了马赛克风格,而这个画面表现实际就是Unity着色器的效果。
事实上,着色器与中国古代四大发明之一的活字印刷术有点渊源。最早的活字印刷术虽然诞生于中国,但是兴盛于欧洲。因为字母文字系统得天独厚的优势,只需准备一些金属字模,然后根据文本的需求进行排布,就可高效无误地印刷安排工整书页。这种内容编排与批量印刷的过程,与着色器实现渲染的过程是极其相似的,即批量将像素绘制到屏幕上。
这个画面应该不少开发者碰到过吧,非常令人讨厌的粉红色Mesh。它意味着你的着色器出问题了。可能是着色器没有正确加载到,或者是设备太差而该着色器设置过高不适用等等。想要知道它为什么出错,就需要去了解什么是着色器,应该如何去写。
但是由于着色器会牵涉到些图形学和线性代数的知识,因此会出现大家不太熟知的数学公式,很可能会造成“Shader貌似非常复杂”的感觉。当然,写出复杂画面表现的Shader会耗费不少功夫,但是大家不需要担心,因为我们会带领大家以比较舒适的方式进入这个世界。
学习Shader的优势
內建Unity Shader仅仅是“通用”用例。这些通用的范式基本可以涵盖60%左右的需求和情况,但是不足以满足你所有的画面表现需求。
一旦掌握Shader,可以为游戏/应用创造独一无二的视觉享受。根据实际需求,为游戏和应用实现特定功能的Shader。
仅仅通过少量代码,就能实现非常有趣的效果。有时候仅需稍微处理一下顶点或片元函数。
它能大大帮助性能优化,因为通过Shader可以控制渲染什么以及如何渲染。
撰写Shader的能力对于游戏团队非常重要,掌握Shader技能的开发一直是炙手可热的职位。现在一个不争的事实就是,技术和美术永远是各大厂商的稀缺资源。
如果你已经掌握其他语言的编程,Shader对你而言不会很复杂。因为它本身不会比其他编程语言多些内容,主要处理的还是一些变量与方法。
Unity Shader 典型案例
让我们看看实际的案例吧。这款游戏就是大家非常熟知的暴雪大作《炉石传说》。它是暴雪第一款正式的移动游戏,也是Made with Unity的游戏。这里展示的就是Unity Shader的特效效果。卡牌的绿色勾边,角色爆炸的感觉,中间桌面轮廓的星河以及产生流动感的各种绚丽效果都是通过着色器实现出来的。给玩家带来的是不是一种非常震撼的享受?
介绍了这么多,那么Shader到底是什么呢?事实上,它是“一个告诉计算机以某种方式描绘物体的程序”。
Unity着色器构成
Unity着色器是怎么写的呢?到现在我们还没看到它的真面目。从编程语言层面来说,它所使用的是NVIDIA CG语言或微软的HLSL语言去编写,而它们都集合到我们专门的ShaderLab语言中。在应用中,点击T键可以切换显示。
这里就是一个典型的Unity Shader文件,包含了所有的基本元素。该脚本的整体其实就是ShaderLab代码内容,而其中被这个CGPROGRAM和ENDCG包裹的部分就是CG\HLSL语言部分。
常用 Shader 类型
自Unity 5.x起,基本常用的Shader就是上图这三种类型,针对一些旧型号GPU的固定函数着色器已经被时代所淘汰。
Vertex & Fragment Shader:顶点/片元着色器。它是最基本,也是非常强大的着色器类型。一般用于2D场景、特效之类的。从上图左边部分,大家可以看到它绘制出一个蜘蛛机器人的纹理,但是不受任何光线的影响。
Surface Shader:表面着色器。它拥有更多的光照运算,其实在系统内部它会被编译成一个比较复杂的顶点/片元着色器。从上图中间部分,与左边的比较我们不难发现,这个3D的蜘蛛机器人有明亮的部分,也有阴影,甚至还带有一定的金属光泽。
Standard Shader:标准着色器。它是表面着色器升级的版本,因为它使用了Physically Based Rendering(简称PBR)技术,即基于物理的渲染技术。所以在这个着色器中开放了更多处理光照与材质的参数。仔细观察上图右边部分的蜘蛛机器人,更多不同质感的部件被表现出来。机器人自带灯的光照,足部的金属质的甲片,机壳略微的锈迹丰富了这个物件的画面呈现。
创建Shader
Unity內建了一些Shader范式模板,开发者可以通过它们去创建所需类型的Shader,以此为基础开始撰写。点击Create → Shader我们便可以找到它们(不仅限于前面所提到的三种,在以后的系列文章中,将有机会分析其他类型的着色器,敬请大家期待)。
如果你想创建一个Vertex/Fragment Shader,可以选择Unlit Shader(无光照着色器),它是一个不包含光照(但包含雾效)的基本顶点/片元着色器。
如果你想创建一个Surface Shader,可以选择Standard Surface Shader(标准表面着色器),它是一个包含了标准光照模型的表面着色器模板。
如果你想使用Standard Shader,很可惜不能自行撰写,但是可以选择并使用Standard Shader。找到检视窗口(Inspector)中所需渲染对象的材质(Material),展开Shader选项卡,第一个Standard就是PBR的Standard Shader;第二个有Specular Setup的就是预制高光运算的Standard Shader。
Vertex/Fragment Shader 流程图
作为Unity着色器系列课程的入门,这篇主要介绍的是顶点/片元着色器,因为它是一切的基础。下面我们就具体拆解这个Shader的实现流程。
数据引入
在世界三维空间中,一开始传入Shader处理的数据其实就是网格数据(Mesh Data)。
在演示应用按T键,切换到下面的画面效果。
但是一般情况下,光是网格数据不能满足我们处理画面的需求,这时就需要引入一些常数属性数据(Properties)。点击打开“常数属性数据”窗口。
这些“属性”就是Shader的变量,可以有资源(Assets)、脚本(Scripts)和动画数据(Animation Data)来驱动表现效果,甚至是粒子系统(Particle System)也能作用(详见《Unity粒子遇上着色器》),而这些数据可在顶点(Vertex)函数和片元(Fragment)函数中使用。
属性的声明规则如下:
_Name(“Display Name”, type) = defaultValue[{options}]
_Name 是属性的名字,也就是变量名,在之后整个Shader代码中将使用这个名字来获取该属性的内容 ,切记要添加下划线。
Display Name 这个字符串将显示在Unity的Inspector中作为Shader的使用者可读的内容 ,即显示的名称。
type 属性的类型。常用的有这个几种:Color颜色,一般为RGBA的数组;2D纹理,宽高为2的幂次尺寸;Rect纹理,对应非2的幂次尺寸;Cube立方体,即6张2D纹理组成;Float和Range,都是浮点数,但是Range要求定义最大值和最小值,以Range(min,max)形式显示;Vector四维数。
defaultValue 默认值,与类型直接挂钩。一开始赋予该属性的初始值,但是在检视窗口中调整过属性值之后,不在有效。Color 以0~1定义的rgba颜色,比如(1,1,1,1);2D/Rect/Cube,对于纹理来说,默认值可以为一个代表默认tint颜色的字符串,可以是空字符串或者“white”,“black”等中的一个;Float和Range 为某个指定的浮点数;同样,Vector的是一个四维数值,写为(x,y,z,w)的形式。
Options 可选项,它只对2D,Rect或者Cube纹理有关,一般填入OpenGL中TexGen的模式,这篇的内容暂未涉及,就先以{}形式。
这样我们可以尝试解读上图中的那些属性声明的是什么了。比如_MainTex(“A Texture”, 2D) = “”{},就是声明了一个变量名为_MainTex的2次幂尺寸纹理,它在检视窗口中显示的名称是A Texture,默认是空的。
点击打开“网格数据”
Mesh网格如上图右边所示,它的数据需要组织成结构体(Struct)形式输入给顶点函数(Vertex Function)。而左边的就是输入结构体appdata,可以根据需要命名变量与类型,当然不能忘记添加冒号以及后面的语义。
语义是什么,语义(Semantics)是附加到着色器输入或输出的字符串,其传达关于参数的预期使用的信息。 对于在着色器级之间传递的所有变量,都需要语义。通常,在流水线级之间传递的数据是完全通用的,并且不被系统唯一地解释;允许任意语义没有特殊意义。 详细请看https://msdn.microsoft.com/en-us/library/windows/desktop/bb509647.aspx
将右边图片向上滑动:
如果要将网格数据直接渲染出来,我们应该可以看到像上图右边机器人顶点显示的效果。
2
顶点函数
点击“顶点函数”:
顶点函数是用来“构建”对象的,输入的是appdata,即组织好的网格数据。经过一定处理后,输出的将是顶点到片元结构体,即Vertex to Fragment,一般简称v2f。当然,这里的结构体与用于输入顶点函数的结构体都可以随便命名,只不过这里习惯以这种命名方式。
输入的顶点数据是需要从对象空间转换到屏幕空间,而顶点渲染到屏幕空间上就会以上图右边的情况显示出来。在Unity 5早期版本一般使用 mul(UNITY_MATRIX_MVP, IN.vertex)方式去处理,即 Model * View * Projection获得顶点对应到屏幕上的位置。但是这种方式效率不高,现在使用UnityObjectToClipPos函数方式直接处理顶点(vertex)信息。调用这个方法一般需要引入UnityCG.cginc预定义文件,通过#include “UnityCG.cginc”实现。向左滑动可以看到这个函数的具体实现方式。
3
顶点到片元结构体
点击“顶点到片元结构体”
这个结构体是中间数据,用于存储从顶点函数(Vertex Function)输出到片元函数(Fragment Function)输入的数据。这个结构体也可以添加其他的变量,比如normalAngle,calculatedLightingColor等。
向左滑动:
到这里你们应该会发现,用在Shader中的变量有些特殊,比如float就有float4,float3,float2等。数字就对应的维度数量,像float4代表这是一个四维的浮点数变量,对应的四个值可以分别对应X,Y,Z,W,或者是颜色值R,G,B,A。
再向左滑动:
浮点数也因精度的不同可以设置不同的变量。float是高精度的,一般为32位;half是中精度的,一般为16位;fixed是低精度的,一般为11位。在实际开发中,会根据性能需要选用合适的精度。比如颜色值RGBA,每个值域是0~1,而fixed值域是0~2,因此使用fixed4足够表现所有颜色值。
4
片元函数
点击“片元函数”:
片元函数(Fragment Function),通常用于将对象描绘到屏幕上,它输入的是v2f结构体数据,而输出的就是像素点。使用CG方法tex2D,输入参数纹理及UV坐标,就可以获得每个UV对应点的纹理的颜色。最后我们就可以看到如上图右边的机器人效果了。
动手修改你的第一个Unity着色器
这是一个最基本的Vertex/Fragment Shader,纯色渲染效果。可以看到上图中,蜘蛛机器人呈现的就是一片纯红色。大家可以点击【阅读原文】获取完整代码。现在打开对应的Unity着色器文件具体解读一下:
文件头部:
我们发现一件很有趣的事,这个一对引号里的Shader命名与地址与实际项目中物理命名与存放该着色器不太一样,这是为什么呢?因为Unity着色器在引擎内部有个自己的取用地址,而这个地址就是文件开头在Shader后面引号里的地址。这个文件的调取可以在检视窗口中材质组件的Shader选项找到,如下所示:
以后你写的Unity着色器也遵从这样的规则。
属性:
这里声明了一个属性,属性名为“_TintColor”,在检视窗口中显示为“Color”,类型为“颜色”,默认值为RGBA全1,即白色。查看一下检视窗口:
有Color这个选项,但是颜色却是红色的?因为这个属性的默认值会在编辑器状态下由设置的手动调整变化而变化,即自己设置了这个颜色。
标签Tags:
形式:Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
作用:控制渲染引擎“何时”、“如何”将子Shader内容进行呈现。
上图中所表示的是“渲染输出的是非透明物体”。
还有一个比较常用的,却容易引起混淆的就是“Queue”渲染队列。Tags { “Queue” = “Opaque” } 表示的是“指定在渲染非透明物体的顺序队列”。其实这两者最主要的区别在于“RenderType”表示的是渲染什么样的物体,而“Queue”表示的是在什么样的实际渲染物体。
Pass的开头部分:
CGPROGRAM与下文的ENDCG标记了在两者之间的是一段CG程序。使用的是NVIDIA的CG语言,一种类似于C的语言,其大多数内容基本与微软的HLSL语言是相似的。
#pragma的作用是指示编译对应的着色器函数。#pragma vertex vert 所表示的就是声明一个名为vert的顶点函数(Vertex Function),#pragma fragment frag 所表示的就是声明一个名为frag的片元函数(Fragment Function)。一点实现了这两个函数,实际上就是实现了顶点/片元着色器了。
#include “UnityCG.cginc”作用就是导入Unity通用CG预定义文件,后面的UnityObjectToClipPos函数就是在该文件里定义好的。
输入顶点函数的结构体:
appdata结构体只有一个参数,声明了一个名为vertex的四维浮点数,语义为网格的顶点坐标数据。
顶点函数实现:
声明了vert函数就需要实现它,这里一目了然,主要就是做了一件事:使用UnityObjectToClipPos方法,将输入网格顶点对象空间转换到屏幕剪裁平面。
顶点输出到片元输入的结构体:
v2f结构体中也就是一个参数,即网格顶点对应到屏幕上的坐标,而语义上的SV所代表的是System Value(系统值),SV_POSITION对应就是屏幕上的像素位置。
实例化声明:
这里可以发现这个float4的变量与属性里的名字一致了。这个float4变量是将属性(Properties)里的变量在Unity着色器内部进行数据绑定用的,为了CG 程序正常访问属性(Properties)的变量,CG程序中的变量必须使用和之前变量相同的名字进行声明。
片元函数:
SV_Target就是System Value Target,实际就是屏幕的像素,最后frag函数return的就是像素,即RGBA颜色,因此frag返回的类型就是fixed4类型。而这里return的就是_TintColor,含义就是屏幕上每一个像素点返回的都是_TintColor的颜色。
修改Color的颜色,在编辑器非运行时状态下,就能看到渲染的即刻变换。
如果要显示蜘蛛机器人的纹理,应该怎么做呢?
引入蜘蛛机器人的纹理
在Properties添加以下变量:
变量名_MainTex,检视窗口显示“Main Texture”,类型是宽高为2次幂的纹理,默认值为空。保存下看编辑器里的变化。
在Color下就多了个设置纹理的选项,点击Texture框内的Select按钮选择Bot,即引入了蜘蛛机器人的纹理。
2
结构体添加网格和纹理的UV值
UV是什么呢?UV(W) 是纹理空间中的多维坐标系,值域 0 到 1。这里使用 2D 纹理,因此是二维的。
分别在appdata和v2f结构体,添加变量uv0,用于记录引入纹理的UV坐标。
3
添加纹理的实例化声明
_TintColor后面添加如下新的实例化:
sampler2D是与纹理绑定的数据容器接口,为CG/HLSL中 2D贴图的类型,相应还有sampler1D、sampler3D、samplerCUBE等格式。
4
结构体UV赋值
在vert函数中,添加上图中的语句,将获取到的网格数据上的UV信息(网格平铺成二维与纹理的一一对应),赋值给v2f结构体中。
5
渲染纹理
将片元函数做以上修改,使用tex2D方法替代掉原来单纯返回颜色。
现在重新回到编辑器界面并运行:
蜘蛛机器人就可以显示对应纹理了,在运行状态下可以发现,纹理与网格是完全一致的。
其他有特色的顶点/片元着色器效果
双纹理混合(Texture Blending - Lerp!)
做双纹理混合肯定需要引入两张不同的纹理,这里分别声明了Main Texture和Second Texture,然后可以通过一个_Blend_Amount参数来调节两个纹理的混合比例。接着,在片元函数部分,分别获取两个纹理对应UV的像素颜色,通过Lerp函数进行混合。
Lerp的功能是基于权重返回两个标量或向量的线性差值。具体在CG中的实现如下:
调节_Blend_Amount,就可以获得双纹理混合的显示效果:
颜色渐变(Color Ramp - Texture Sample)
在输入参数部分,可以看到[Header(Color Ramp Sample)],它用于在检视窗口中添加一个标签文本。这里是显示Color Ramp Sample。一般引入一个2D的纹理,都会有Tiling和Offset显示,即可以调节纹理的缩放与平移,使用了[NoScaleOffset]就会将这两个参数设置禁用,仅获得纹理的原始比例与平移。
在检视窗口中的显示如上图所示。
参数_ColorRamp_Evaluation其实是获取这张渐变图的水平中心像素点的位置,从而得到该位置的像素。将主纹理的颜色与渐变纹理的颜色相乘,即可获得混合后的颜色。
颜色与颜色之间可以进行加减乘除进行混合运算。加法可以起到颜色叠加的效果,但是由于颜色值的值域为0~1,相加很容易达到1,就会颜色会愈发明亮,因此叠加建议使用乘法;减法可以进行反色处理,但是同样是值域的原因,数值达到0,颜色就很暗淡,因此要做反色建议使用除法。
纹理剔除(Texture Cutout)
在片元函数中使用了Clip方法,该方法的功能是当输入的参数小于等于0时,就会删除对应位置的像素。这里使用了一个有不同颜色分布的纹理作为剔除纹理,使用CutOut Value作为剔除参考值,当该纹理像素某位置上RGB分别减去这个CutOut Value有小于等于0的时候,这个蜘蛛机器人就会有镂空的效果。
世界坐标-梯度(World Space - Gradient)
在v2f结构体中,添加了世界坐标。而这个坐标是通过unity_ObjectToWorld,来获取每个顶点在世界空间中的坐标。在输入的部分加入了两个表示高低不同位置的颜色,片元函数中对两者根据踢动值进行了线性插值处理。最后以相乘的方式进行了像素混合,呈现出图中的画面效果。
法线挤压(Normal Extrusion)
可以看到在appdata结构体中引入了法线(Normal)。在顶点函数计算的时候,将发现xyz值乘以挤压值(Extrusion Amount),而后叠加到顶点的xyz上,这样就可以根据这个挤压值对于顶点的对象空间位置进行法线相关的偏移处理,最后可以得到“膨胀变胖”、“挤压变瘦”的有趣效果。
时间相关(Time)
_Time为Unity着色器默认载入UnityShaderVariables.cginc的变量,_Time.y表示游戏自启动开始后的时间。通过波速、波距和波频三个参数是将机器人进行波形化处理。
漫反射光照(Diffuse Lighting)
漫反射光照牵涉到一些光照运算的内容,首先就需要添加Tags的LightMode为ForwardBase,基于前向的光照模式,还引入了UnityLightingCommon.cginc预定义文件来辅助光照的运算。当然,appdata结构体中不会缺少网格的法线数据。在顶点函数中,通过UnityObjectToWorldNormal获取网格对象在世界坐标中的法线值。接着,通过dot方法得到法线值与世界空间光照坐标的点积值,作为漫反射参考值。然后,将这个漫反射参考值与光照颜色相乘获得光照的漫反射颜色。最后在片元函数中将主纹理颜色与之相乘混合,得到最终的像素颜色。
其他控制Unity着色器的方法
通过动画控制(Animation Clip)
正如上图所示,该示例没有使用额外的代码,仅仅依靠动画片段(Animation Clip)来控制着色器的参数值。
Unity着色器依附于材质(Material),而材质需要渲染器(Renderer)使之生效。因此通过动画控制Unity着色器只要找到对应的参数,就可以像制作其他角色动画一样,让策划或美术调整着色器显示的效果。
脚本控制(Scripting)
对于程序员而言,有时候通过代码的手段似乎更为便利,Unity着色器也给这方面的需求提供了方法。也正如前面在叙述动画片段控制Unity着色器的参数,只要通过Renderer → Material → Shader就可以访问到需要的参数。因此,可以通过Get和Set的方式去获取对应参数的值,或者去修改对应参数的值。
如果大家意犹未尽,可以点击【阅读原文】下载由Unity版PPT生成的单机应用程序和附带的部分场景的工程,进行体验。中间页面的切换通过键盘左右键进行,大多数内容使用到了UGUI。有些可滑动的部分使用鼠标拖动,还有一些内部的切换,需要使用键盘的T键,具体位置详见前文叙述。
常用Unity着色器辅助工具推荐
1. ShaderlabVS
https://github.com/wudixiaop/ShaderlabVS
开源Visual Studio插件,VS2013、2015、2017均可使用,支持ShaderLab着色器文件的语法高亮补全,支持.shader .cginc .glslinc .compute .cg .hlsl等文件类型。
2. Shader Unity Support
https://marketplace.visualstudio.com/items?itemName=MarcinODev.ShaderUnitySupport#overview
微软VS插件商店推荐工具,支持多种Unity着色器文件的代码补全。
3. ShaderlabVSCode
https://assetstore.unity.com/packages/tools/utilities/shaderlabvscode-94653
如果你使用Mac系统,并安装了Visual Studio Code,这款功能强大的ShaderLab辅助工具会是不错的选择。
4. Unity Shader Intellisense
https://assetstore.unity.com/packages/tools/utilities/unity-shader-intellisense-28508
这款基于Unity Editor开发的Shader编写插件,可以帮助你无论在那种开发平台上,都能便利的进行着色器代码的开发工作。
结语
相信学会修改自己的第一个Unity着色器是件很酷的事情。希望这篇文章能够帮助到正在使用Unity进行着色器开发的朋友们。我们还将继续分享Unity着色器训练营-第二期直播信息等内容在Unity官方中文社区(unitychina.cn),请保持关注!
推荐阅读
近期Unity官方活动
9月25日,“Unity全球技术校园行”将为你揭开顶尖佳作背后的神秘面纱。
9月,Unity面向莘莘学子特别推出Unity Plus加强版开学季专属特惠,及Asset Store资源商店5折促销。
点击“阅读原文”进入Unity官方中文社区!