本系列文章由优快云@萌萌的一天 出品,未经博主允许不得转载。
接触shader编程已经有很长一段时间,最近有很多初学者问我许多关于Unity3D Shader方面的问题,我打算写一篇关于shader教学的文章,方便解决大家初入图像编程~大坑时遇到的麻烦。
简单来说,学习Shader Programming就是学习如何利用GPU的力量。刚开始我会详细解释shader的作用、创建方式、基本语法等等,之后我会详细教给大家如何写出一个简单有效的shader案例。
神马是Shader?
Shader并不神秘,它只是很小的一段程序,包含着数学计算和算法模型,运行在计算机的图像管道层面,告诉计算机图像上的每一个像素应该怎样显示在屏幕上,它将一个个输入的网格绘制到屏幕之上,就能得到一个Material(材质),之后通过渲染器进行一系列的渲染输出操作后,我们就可以观察到客观的物体。Shader一般被称作着色器,它可以通过改变自身的属性来改变材质渲染到屏幕上的效果。以下是3中不同的Shader渲染到同一个材质上的例子:
Unity3D中编写的Shader是通过.shader文件来进行实现的,它使用ShaderLab语言,极其类似Cg/HLSL。ShaderLab语言很类似于C语言,如果你有不错的C/C++编程功底的话,学习ShaderLab将会很容易。
Shader分为三种类型:
1、Fixed Function Shader( 固定渲染管线着色器)
2、Surface Shader(表面着色器)
3、Vertex Shader&Fragment Shader (顶点着色器&片段着色器)
接下来我们在Unity中创建Shader和Material,详细介绍每一种Shader的创建,用法等。
在Unity中创建第一个Shader
首先,我们新建一个场景,在场景下的Project面板中新建一个叫做“Shader”的文件夹,方便我们管理今后创建的Shader文件。然后右键点击空白处--->Creat--->Shader,这样操作后,我们就能得到一个名为“NewShader”的.shader类型的文件了。
之前说过,shader文件只有依附在Material,我们才能看到正确渲染出来的图像,所以我们还应创建一个新的Material,和之前类似,右键点击空白处--->Creat--->Material,创建一个新的Material,命名为“TestMaterial_1”。
那么怎么将新创建的Shader赋予到Material上呢,我们点击“TestMaterial_1”,观察它的Inspector面板,在Shader-->Custom下就能找到我们刚刚创建的"NewShader"文件了。
Shader的基本框架
双击Shader文件,就能看到Shader的基本写法。总体来说,我们可以概括Shader的框架写法:
Shader "name" { [Properties] Subshaders [Fallback] [CustomEditor] }
用图形可以这样表示:
用编辑器打开刚才的“NewShader”文件后,代码大概是如下的样子(可能会因为Unity版本略有不同):
Shader "Custom/NewShader" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
- 首先,第一行Shader "Custom/NewShader"为我们说明了这是一个.Shader文件,紧接的“Custom/NewShader”表示了此Shader文件的目录位置和名称。注意,.shader文件不会自动更新文件名,当我们更改一个Shader文件的名称之后,也要对这里的路径下的名称进行更改。
- 接下来的Properties后的{}表示这个Shader的属性。属性类似于c/c#里的变量,我们可以定义它的名称、类型以及属性数值。关于Properties的相关语法说明可以参考:
我们可以定义如下的类型:_Name("Display Name", type) = defaultValue[{options}]
1、name ("display name", Range (min, max)) = number
自定义浮点型属性,在面板上通过介于最大/最小值间的滑条修改
2、name ("display name", Float) = number
自定义浮点型属性,在面板上可以手动输入浮点值
3、name ("display name", Int) = number
自定义整型数值,在面板上可以手动输入整型数值
4、name ("display name", Color) = (number,number,number,number)
自定义颜色属性,在面板上可以打开颜色选择器
5、name ("display name", Vector) = (number,number,number,number)
自定义四维向量数值,在面板上可以手动输入四维向量(Float类型)
6、name ("display name", 2D) = "defaulttexture" {}
自定义2D Texture
7、name ("display name", Cube) = "defaulttexture" {}
自定义Cubemap
8、name ("display name", 3D) = "defaulttexture" {}
自定义3D Texture
- 接下来是SubShader,它代表一个子着色器,里边所有声明的变量和输入输出都不会影响到其他的SubShader。我们看看它的第一行做了什么。
Tags { "RenderType"="Opaque" }
Tags{ "TagName1" = "Value1" "TagName2" = "Value2" }
Transparent
半透明着色器,包括绝大多数粒子Shader,地形Shader等;TransparentCutout半透明着色器补充类(Cutout Shader类型不允许绘制部分透明的区域),包括双通道(Pass)植被ShaderBackground背景着色器,比如典型的天空盒;Overlay覆盖类着色器,常用于GUITexture和Flare Shaders;TreeOpaque不透明树木着色器;
GrassTreeTransparentCutout透明树木类着色器,比如树叶的渲染;
草地类渲染着色器;
除了RenderTyper,还有一些其它常用的标签类型,例如:
"DisableBatching tag"="true",表示对此着色器禁用批处理绘制方式;
"ForceNoShadowCasting tag"="true" 表示该SubShader不会对着色物体产生阴影;
"IgnoreProjector tag"="true"表示该SubShader产生的阴影不受投影机(Projector)的影响;
"Queue"=" "表示制定的的渲染顺序队列;
这里需要详细解释Queue这个属性。可以想象,在一个大型游戏中,游戏物体的渲染大致顺序应该是由近到远逐步进行,从而保证游戏镜头逐步进行呈现,透明的水后边应该绘制不透明的物体等等。这样我们可以通过制定渲染顺序队列,来确保让透明的着色器类型能渲染在不透明物体前边。
Unity为我们提供了四种预定义的渲染队列,当然,我们也可以自行添加自定义的渲染方式。官方提供的预定义渲染队列如下:
- BackGroud:背景渲染队列,它优先于其他队列,主要用于渲染天空盒等背景元素;
- Geometry(default):这是默认渲染队列,一般来说,场景中的大部分不透明物体都用此方式进行渲染。
- AlphaTest:Alpha渲染通道,它是一种比较特殊的队列,用于渲染Alpha-tested的对象。
- Transparent:透明物体渲染队列(Alpha值小于100),适合用于玻璃、粒子效果等。
- Overlay:这个队列用来处理覆盖效果,一般来说最后呈现的渲染特效都在这里进行处理。
- 说完Tags标签作用和类型,下边是一句LOD声明 :
LOD 200
- 接下来是Shader中的主体部分
CGPROGRAM
#pragma surface surf Lambert
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o) {
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
首先,CGPROGRAM表示开始位,与结束位ENDCG相对应,表明这之中是子着色器编译命令,所有的输入输出操作在其中进行。之后是一句#pragma surface指令,它的语法是:
#paragma target 3.0//<span style="font-family: Arial, Helvetica, sans-serif;">是Unity5.X版本新加入的指令,用来处理某些2.0版本的Surface shader指令集限制。</span>
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o) {
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
现在到了最后的关键函数surf,在surf里首先对_MainTex进行采样,将它和_Color叠加后进行输出。这里的tex2D函数是Cg中用于2D纹理采样,它的函数原型很多:
float4 tex2D(sampler2D samp, float2 s)
float4 tex2D(sampler2D samp, float2 s, inttexelOff)
float4 tex2D(sampler2D samp, float3 s)
float4 tex2D(sampler2D samp, float3 s, inttexelOff)
float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy)
float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy)
float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy, int texelOff)
int4 tex2D(isampler2D samp, float2 s)
int4 tex2D(isampler2D samp, float2 s, inttexelOff)
int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy)
int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
unsigned int4 tex2D(usampler2D samp, float2s)
unsigned int4 tex2D(usampler2D samp, float2s, int texelOff)
unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy)
unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy,int texelOff)
tex2D函数的返回值是采样后的纹理,这样就能得到最终的着色效果。