从这一篇开始,我会对Unity Shader相关的进行简单的探讨学习,当然,也是很表层的了,毕竟对于不是专门做Shader方面的来说,能够简单学习到里面所实现的原理,并理解它也是很厉害的了。
Tips:这里我使用的Unity版本是5.6.0f3,因为不同版本的Unity在编写Shader的时候,一些接口变量可能会有所不同。
对于Shader,可能有些人会有些陌生,翻译一下就是着色器的意思,我们可以通过不同硬件所支持的不同的着色器语言可以来实现我们需要的图形效果。如果读者学过计算机图形学,相信对着色器并不会陌生。关于Shader的简介,这里就不多介绍。
Unity中,可以使用哪种Shader语言来编写呢,其实使用哪种语言都可以编写Shader,比如英伟达公司的CG,微软的HLSL以及SGI公司的GLSL语言都是可以的,具体使用什么语言来开发,就根据开发者开发中所支持的目标平台的不同来决定。而Unity中又对这些不同硬件所支持的不同Shader进行了统一的封装,我们只要按照它所封装好的规则来编写,就可以实现不同平台的支持,我们这里把这个封装所写的Shader称为ShaderLab。话不多说,我们现在就开始编写Unity中最简单的一个ShaderLab。
先来看看我们要实现过程的效果:
好,现在再看看代码实现:
//Shader模块定义 Shader "xiaolezi/SimpleShader" { //属性设置 Properties { //定义一个物体表面颜色,格式:[属性名]([Inspector面板显示名字],属性类型)=[初始值] _Color("Tint Color", Color) = (1,1,1,1) } //第一个SubShader块 SubShader { //第一个Pass块 Pass { //开启CG着色器编辑模块 CGPROGRAM //定义顶点着手器函数名 #pragma vertex vert //定义片段着色器函数名 #pragma fragment frag //定义一个从应用程序到顶点数据的结构体 struct appdata { float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据 }; //定义一个从顶点数据到片段数据的结构体 struct v2f { float4 vertex : SV_POSITION;//SV_POSITION语义:得到输入片元着色器中的顶点数据 }; //从属性模块中取得该变量 fixed4 _Color; //顶点着色器函数实现 v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标 return o; } //片段着色器函数实现 fixed4 frag (v2f i) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色 { return _Color; } //结束CG着色器编辑模块 ENDCG } } }看起来挺复杂的,不过不要担心,我们一个个来分析。
首先我们先来了解一下关于ShaderLab的一个模板,如下:
Shader "名称/.."{ Properties{ //... } SubShader{ Pass{ CGPROGRAM //... ENDCG } } Fallback "默认着色器" }
这个模板就是ShaderLab封装的一个模板,我们以后就会根据这个模板来编写我们的shader。
我们再来了解一下几个关键字:
Shader关键字,后面跟着一条字符串,当你在材质选择面板时,你可以通过使用正斜杠符"/"在子集菜单放置你的shader文件。
Properties关键字,在语句块中包含了shader的变量,例如(color,texture等等),它将会保存在我们的材质中,并且显示在材质面板上。
SubShader关键字,着色器中可以包含一个或多个Shader文件,它会根据不同的GPU的兼容来决定使用那个SubShader。
Pass关键字,每个SubShader会编写很多Pass块,每个Pass块代表对拥有相同着色器材质被渲染的对象的一个可执行的顶点片元代码。很多简单的着色器只使用一个Pass,但是对于一些被光照相互的着色器就需要多个Pass。Pass中的一个名字通常会设置一些固定功能的状态,比如混合模式,这个后面再探讨。
CGPROGRAM...ENDCG关键字:顶点片元着色器代码需要写在这两个关键字里面。
好,我们现在来分析一下上面所给的代码。
Shader "xiaolezi/SimpleShader"这里我定义了我的材质面板所存放该shader的目录,如图:
//属性设置 Properties { //定义一个物体表面颜色,格式:[属性名]([Inspector面板显示名字],属性类型)=[初始值] _Color("Tint Color", Color) = (1, 1, 1, 1) }这里定义了一个颜色属性,关于属性类型,还有哪一些呢,我们常有的有以下种:
整型 Int 浮点型 Float 随机数 Range(min,max) 颜色属性 Color 向量属性 Vector 纹理属性 2D 3D Cube具体我们到时用到再说。
然后我们再来看看Pass语句下的顶点片元函数的实现:
首先我们需要定义顶点片元着色器函数,通过如下代码定义:
#pragma vertex vert #pragma fragment fragvert和frag分别为它们的函数名,这个可以自定义。
然后我们需要对顶点片元函数进行实现,而实现之前需要定义相关结构体来得到一些相关值。
所以,这里还要对渲染管线进行分析,我们来看一张图简单了解一下关于GPU是怎么对图像进行渲染的:
我们知道,想要图像完整的被硬件所渲染,其实步骤还是很复杂的。首先要获取顶点数据,再通过几何光栅化阶段最终才能获得我们的屏幕图像。之前硬件限制,实现的Shader是固定的渲染管线,基本很多不可编程的,所以实现起来也很麻烦。现在的Shader很多可以编程,这样让Shader实现起来会很方便。所以我们现在所实现的过程就是上面绿色部分当中的内容。
好,我们这里关注点有两个,一个是顶点着色器,一个是片元着色器。这里有两个问题:
1.顶点数据在Unity中从哪里获取得到;
2.顶点数据在怎么输入与输出到顶点着色器和片段着色器中。
我们先来解决第一个问题,Unity中,获取顶点数据会从当前物体的Mesh Filter组件中获取得到,这里面保存了大量的顶点信息。我们在Wireframe模式下观察各个顶点的连线信息,如图所示:
第二个问题,顶点信息是怎么输入和输出的,这里通过语义(Semantics)来获取和输出。
在顶点着色器中,我们可以使用哪些语义来得到顶点数据呢?如下:
POSITION 顶点位置,一般为一个float3或float4类型 NORMAL 顶点法线,一般为一个float3类型 TEXCOORD0 纹理坐标,一般为float2,float3或float4类型 TEXCOORD1, TEXCOORD2 and TEXCOORD3 分别是 第二,第三,第四级纹理坐标 TANGENT 切线向量(通常被使用作为法线贴图),一般为float4类型 COLOR 逐顶点颜色,一般为float4类型
而片元着色器中,可以使用以下语义获得顶点数据:
SV_POSITION COLOR0 Color1 TEXCOORD0~7最终从片元着色器中输出的语义为:
SV_Target好,现在继续再来看看我们的代码, 先来看看这两个结构体:
//定义一个从应用程序到顶点数据的结构体 struct appdata { float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据 }; //定义一个从顶点数据到片段数据的结构体 struct v2f { float4 vertex : SV_POSITION;//SV_POSITION语义:得到输入片元着色器中的顶点数据 };一个是需要在顶点着色器中使用的,所以我们定义的结构体需要使用顶点着色器中的获取顶点数据的语义,这里通过POSITION得到了顶点位置信息,是模型空间下的。
接着,第二个结构体是需要在片元着色器中使用的,所以我们也是需要使用片元着色器获取顶点数据的语义,这里也是得到顶点数据。
好,最后来看下顶点片元函数的实现了:
//从属性模块中取得该变量 fixed4 _Color; //顶点着色器函数实现 v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标 return o; } //片段着色器函数实现 fixed4 frag(v2f i) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色 { return _Color; }首先,我们看下第一行,取得变量_Color,虽然我们的变量在属性面板中有定义,但是如果要在Pass中使用,你就必须重新定义一下,这是Shader和其他高级语言不一样的地方。所以这一点知道就好。
然后 顶点着色器函数返回我们的片元着色器结构体数据,自然,在函数中需要定义一个片元着色器结构体,然后关键的一步就是把你通过POSITION语义去的顶点位置信息从模型空间转化为剪裁空间。上面的渲染管线图有这具体的一步。而在Unity中,只要调用封装好的这个方法UnityObjectToClipPos,然后把顶点坐标传入即可,接着我们把它转化后的坐标保存在o这个结构体中方便在片元着色器中访问得到。
片元着色器中,需要返回具体的图像颜色值,所以我们设置返回值为 fixed4这个类型,然后直接把我们当前属性的颜色返回。一般,颜色值得数据都设置为fixed类型,这样对性能好一些。
以上就是对Shader中最基本编写进行解剖。希望能够启发读者对Unity中Shader的使用。
这里我创建了一个GitHub仓库,把本章所有的源码以及以后面章节代码都会放在上面,
链接:Github仓库地址
Happy Coding...