不管你会不会写Unity3D的shader,估计你会知道,Unity3D编写shader有三种方式,这篇东西主要就是说一下这三种东西有什么区别,和大概是怎样用的。
先来列一下这三种方式:
fixed function shader
vertex and fragment shader
surface shader
为什么Unity3D要提供三种shader的编写方式呢?那是因为三种方式的编写的难易度有区别,对应着不同的使用人群。其实我觉得这是Uniy3D想得有点多了,着色器不单止是为了实现效果,还要效果实现的效率高,才适合真正使用在项目中,单纯实现一个效果,那只是demo级别的,只适合unity3D自身的产品推广,对实际使用者的意义不会很大。基于这种想法,我下面只会大概的介绍三种方式的使用方法,以后会通过其他文章有针对性的对某种方式进行详细的说明。
先来看看三种方式的说明:
fixed function shader (固定功能着色器)
最简单的着色器类型,只能使用Unity3D自带的固定语法和提供的方法,适用于任何硬件,使用难度最小
vertex and fragment shader (顶点片段程序着色器)
顶点片段着色器,效果最为丰富的着色器类型,使用Cg/HLSL语言规范,着色器由顶点程序和片段程序组成。所有效果都需要自己编写,使用难度相对较大。
surface shader (表面着色器)
同样使用Cg/HLSL语言规范的着色器类型,不过把光照模型提取出来,可以使用Unity3D自带的一些光照模型,也可以自己编写光照模型,着色器同样由顶点程序和片段程序组成,不过本身有默认的程序方法,使用者可以只针对自己关系的效果部分进行编写。由于选择性比较大,所以可以编写出较为丰富的效果,使用难度相对vertex and fragment shader小。
由上面的三种方式的说明可以看出来了,如果你是由丰富的的Cg开发经验的,好的选择就是直接使用vertex and fragment shader了。fixed function shader虽然简单,但能实现的效果非常有限。而surface shader是Unity3D提供的一种较为折中的方式,同样能实现较丰富的效果,但难度相对小很多。不过surface shader有一个问题,它不支持SubShader内部的多pass,所以某些需要多pass的效果要实现起来会比较困难。
接下来是主要内容,每一种方式的编写规范和举例:
1、Fixed Function shaders
1.标准范例:
Shader “VertexLit” {
Properties {
_Color (“Main Color”, Color) = (1,1,1,0.5)
_SpecColor (“Spec Color”, Color) = (1,1,1,1)
_Emission (“Emmisive Color”, Color) = (0,0,0,0)
_Shininess (“Shininess”, Range (0.01, 1)) = 0.7
_MainTex (“Base (RGB)”, 2D) = “white” { }
}
SubShader {
Pass {
Material {
Diffuse [_Color]
Ambient [_Color]
Shininess [_Shininess]
Specular [_SpecColor]
Emission [_Emission]
}
Lighting On
SeparateSpecular On
SetTexture [_MainTex] {
constantColor [_Color]
Combine texture * primary DOUBLE, texture * constant
}
}
}
}
2.具体说明
具体变量、Material模块参考《Unity3D的着色器介绍(二)——Unity3D的Shader基本结构说明》,主要区别是使用SetTexture 模块来控制贴图
SetTexture的具体写法:
SetTexture [贴图名称] {
ConstantColor color
Combine ColorPart, AlphaPart
}
ConstantColor color
定义一个常量颜色在combine command里面使用。
combine command
combine src1 * src2
将src1和src2相乘,结果会比两者更暗。
combine src1 + src2
将src1和src2相加,结果会比两者更亮
combine src1 - src2
从src1里减去src2
combine src1 +- src2
把src1加到src2,然后减去0.5
combine src1 lerp (src2) src3
src3和src1之间的插值,使用src2的透明值。插值是相对的。
当透明值是1时显示src1,当透明值是0时显示src3
combine src1 * src2 + src3
将src1和src2的透明度相乘,然后加上src3
combine src1 * src2 +- src3
将src1和src2的透明度相乘,然后与src3相加减去0.5
Multiplies src1 with the alpha component of src2, then does a signed add with src3.
combine src1 * src2 - src3
将src1和src2的透明度相乘,然后减去src3
matrix command
matrix [MatrixPropertyName]
Transforms texture coordinates used in this command with the given matrix
其中Combine部分是可选的,作用是设定颜色部分和透明部分的叠加方式。
其中透明部分如果不写,将会默认使用颜色部分相同的设置。
一般的写法是Combine texture * primary,这样的意思是把贴图乘以顶点颜色作为最终颜色。
也可以写成Combine texture * primary DOUBLE,加一个DOUBLE的作用是让光照强度增幅成2倍。primary代表的是初始的顶点颜色,也就是在没有加贴图之前的顶点颜色。
如果需要叠加第二张或者更多贴图,那么就不能再乘以primary了,要改为previous,也就是Combine texture * previous。previous代表的是之前的颜色,也就是你前一张贴图乘以初始颜色之后的颜色。
从Fixed Function shaders的写法可以看出,这种类型的shader非常简单易用,基本上所有平台设备都能支持,不过能做的东西确实不多。你可以用来简单的表现一些基本的贴图和颜色叠加的效果,也可以使用TexGen的贴图来实现一些贴图特效,不过不能控制顶点,也不能较为复杂的控制表面着色。
2、Vertex and Fragment Shader
1.标准例子:
Shader “Custom/Exam1” {
Properties {
_MainTex (“Texture”, 2D) = “white” { }
}
SubShader
{
pass
{
CGPROGRAM
sampler2D _MainTex;
float4 _MainTex_ST;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
} ;
v2f vert (appdata_base v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}
float4 frag (v2f i) : COLOR
{
float4 texCol = tex2D(_MainTex,i.uv);
float4 outp = texCol;
return outp;
}
ENDCG
}
}
}
2.细节解释:
1、Cg小片段
Cg小片段以CGPROGRAM开头,以ENDCG结尾。中间的内容使用Cg/HLSL语言。
Cg小片段的开始处,可以添加:
在Direct3D 9环境中:顶点着色器1.1和像索着色器2.0
ARB顶点程序 有128位指令限制,ARB片段程序 有96位指令限制(32位纹理 + 64位算术运算),16个临时寄存器和4个间接纹理。
在Direct3D 9环境中:顶点着色器3.0和像索着色器 3.0
ARB顶点程序 没有指令限制,ARB片段程序 有1024位指令限制(512位纹理 + 512位算术运算),32个临时寄存器和4个间接纹理。可以通过使用 #pragma profileoption 命令扩展限制值。例如: #pragma profileoption MaxTexIndirections=256增加间接纹理上限到256个。需要注意的是某些着色器模式3.0中的特性不支持ARB顶点程序和ARB片段程序,比如派生指令这样的。你可以使用 #pragma glsl命令转换到GLSL中,这样子限制较少。
?d3d9 - Direct3D 9.
?opengl - OpenGL.
?gles - OpenGL ES 2.0.
?xbox360 - Xbox 360.
?ps3 - PlayStation 3.
?flash - Flash.
2、参数对应声明
如有以下着色器输入参数
_MyColor (“Some Color”, Color) = (1,1,1,1)
_MyVector (“Some Vector”, Vector) = (0,0,0,0)
_MyFloat (“My float”, Float) = 0.5
_MyTexture (“Texture”, 2D) = “white” {}
_MyCubemap (“Cubemap”, CUBE) = “” {}
那么在cg程序里面应该再次声明这些参数,才能在cg程序里面使用
float4 _MyColor;
float4 _MyVector;
float _MyFloat;
sampler2D _MyTexture;
samplerCUBE _MyCubemap;
类型对应关系:
Color和Vector属性对应 float4 类型。
Range和Float属性对应 float 类型。
Texture属性对应 普通2D纹理的sampler2D 类型。
CUBE和RECT纹理对应 samplerCUBE 和 samplerRECT类型。
3、顶点程序
需要获取顶点的信息,可以先引用#include “UnityCG.cginc”
然后在这个包里面有2种类型包含了顶点的信息:
appdata_base: 包含顶点位置,法线 和一个纹理坐标。
appdata_tan:包含顶点位置,切线,法线 和一个纹理坐标。
其中包含以下的参数:
float4 vertex 是顶点位置
float3 normal 是顶点法线
float4 texcoord 是第一UV坐标
float4 texcoord1 是第二UV坐标
float4 tangent 是切线向量(用在法线贴图中)
float4 color 是每个顶点(per-vertex)颜色
当然也可以不引用#include “UnityCG.cginc”,直接自己定义appdata
struct appdata {
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
};
其中常用类型:
POSITION:顶点位置
COLOR:顶点颜色
NORMAL:法线
TANGENT:切线
TEXCOORD0:UV1
TEXCOORD1:UV2
声明顶点程序名称
下面就可以写顶点程序vert了
struct v2f {
float4 pos : SV_POSITION;
fixed4 color : COLOR;
};
v2f vert (appdata_base v)
{
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.color.xyz = v.normal * 0.5 + 0.5;
return o;
}
其中v2f是一个自定义块,里面定义了顶点程序返回时包含的信息,也可以传递给片段程序用,其中位置改成SV_POSITION。也可以自定义一些其他的属性。
在顶点程序里面的顶点与矩阵相乘得到最终位置,相关矩阵解释:
UNITY_MATRIX_MVP
当前模型视投影矩阵。(注:模型矩阵为 本地->世界)
UNITY_MATRIX_MV
当前模型*视图矩阵
UNITY_MATRIX_V
当前视图矩阵
UNITY_MATRIX_P
当前投影矩阵
UNITY_MATRIX_VP
当前视图*投影矩阵
UNITY_MATRIX_T_MV
转置模型*视图矩阵
UNITY_MATRIX_IT_MV
-逆转置模型*视矩阵
UNITY_MATRIX_TEXTURE0 to UNITY_MATRIX_TEXTURE3
纹理变换矩阵
当每一帧渲染的时候,每一个需要渲染的物体会自动的把顶点信息输入到shader的指定顶点程序,在vert顶点程序里面处理完,把顶点需要的信息赋值之后,就可以把v2f返回,这时候gpu会自动接收到顶点信息,并作处理。这个是GPU编程的特点。
3、片段程序
然后写
half4 frag (v2f i) : COLOR
{
half4 texcol = tex2D (_MainTex, i.uv);
return texcol * _Color;
}
片段程序不需要自定义块作为返回值,因为片段程序的返回值就直接是颜色了。
不过在片段程序里面,可以通过对之前顶点程序做处理获得的顶点位置、uv、法线等信息进行各种处理,让最终该点的颜色发生各种各样的改变。
3、Surface Shaders
1.标准例子:
Shader “Example/Diffuse Texture” {
Properties {
_MainTex (“Texture”, 2D) = “white” {}
}
SubShader {
Tags { “RenderType” = “Opaque” }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
};
sampler2D _MainTex;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
}
ENDCG
}
Fallback “Diffuse”
}
2.详细说明
程序声明写法:
surfaceFunction就是程序名称,该程序应该写成:
void surf (Input IN,inout SurfaceOutput o),
Input是你自己定义的结构。Input结构中应该包含所有纹理坐标(texture coordinates)和和表面函数(surfaceFunction)所需要的额外的必需变量。
lightModel 光照模型:
可选:
Lambert
BlinnPhong
或者自定义
Optional parameters:
可选参数:
alpha -透明( Alpha)混合模式。使用它可以写出半透明的着色器。
alphatest:VariableName -透明( Alpha)测试模式。使用它可以写出 镂空效果的着色器。镂空大小的变量(VariableName)是一个float型的变量。
finalcolor:ColorFunction - 自定义的最终颜色函数(final color function)。 请参考范例:表面着色器例子(Surface Shader Examples)。
exclude_path:prepass 或者 exclude_path:forward - 使用指定的渲染路径,不需要生成通道。
addshadow - 添加阴影投射 & 收集通道(collector passes)。通常用自定义顶点修改,使阴影也能投射在任何程序的顶点动画上。
dualforward - 在正向(forward)渲染路径中使用 双重光照贴图(dual lightmaps)。
fullforwardshadows - 在正向(forward)渲染路径中支持所有阴影类型。
decal:add - 添加贴花着色器(decal shader) (例如: terrain AddPass)。
decal:blend - 混合半透明的贴花着色器(Semitransparent decal shader)。
softvegetation - 使表面着色器(surface shader)仅能在Soft Vegetation打开时渲染。
noambient - 不适用于任何环境光照(ambient lighting)或者球面调和光照(spherical harmonics lights)。
novertexlights - 在正向渲染(Forward rendering)中不适用于球面调和光照(spherical harmonics lights)或者每个顶点光照(per-vertex lights)。
nolightmap - 在这个着色器上禁用光照贴图(lightmap)。(适合写一些小着色器)
nodirlightmap - 在这个着色器上禁用方向光照贴图(directional lightmaps)。 (适合写一些小着色器)。
noforwardadd - 禁用正向渲染添加通道(Forward rendering additive pass)。 这会使这个着色器支持一个完整的方向光和所有光照的per-vertex/SH计算。(也是适合写一些小着色器).
approxview - 着色器需要计算标准视图的每个顶点(per-vertex)方向而不是每个像索(per-pixel)方向。 这样更快,但是视图方向不完全是当前摄像机(camera) 所接近的表面。
halfasview - 在光照函数(lighting function)中传递进来的是half-direction向量,而不是视图方向(view-direction)向量。 Half-direction会计算且会把每个顶点(per vertex)标准化。这样做会很快,但不完全准确。
注意:surface由于使用了光照模型,所以是不能写pass,直接写在subshader里面的。
输入端:
uv_贴图变量名称 - 贴图的uv
float3 viewDir - 视图方向( view direction)值。为了计算视差效果(Parallax effects),边缘光照(rim lighting)等,需要包含视图方向( view direction)值。
float4 with COLOR semantic -每个顶点(per-vertex)颜色的插值。
float4 screenPos - 屏幕空间中的位置。 为了反射效果,需要包含屏幕空间中的位置信息。比如在Dark Unity中所使用的 WetStreet着色器。
float3 worldPos - 世界空间中的位置。
float3 worldRefl - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。 请参考这个例子:Reflect-Diffuse 着色器。
float3 worldNormal - 世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。
float3 worldRefl; INTERNAL_DATA - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的反射向量(reflection vector)需要使用世界反射向量(WorldReflectionVector (IN, o.Normal))。请参考这个例子: Reflect-Bumped着色器。
float3 worldNormal; INTERNAL_DATA -世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的法线向量(normal vector)需要使用世界法线向量(WorldNormalVector (IN, o.Normal))。
输出端:
struct SurfaceOutput {
half3 Albedo;
half3 Normal;
half3 Emission;
half Specular;
half Gloss;
half Alpha;
};
最后说两句我觉得比较重要的话:
1、着色器都有它规定的指令限制、寄存器、和纹理的限制,有一些复杂的效果需要使用到较多的寄存器和指令,可能会超出限制,比如使用着色器来实现骨骼动画就很容易超出限制了。不论在什么平台编写着色器(unity3D或者stage3D之类)都需要先了解清楚有什么限制,不然等到编译的时候就哭了。
2、着色器的限制有时候是跟发布平台的硬件支持有关系的,某些发布的平台(比如手机端)不支持你指定的高级别着色器模式,它有可能会根据实际情况降低级别,或者甚至显示不出来。所以编写着色器效果之前要考虑一下需要发布的平台,不要老是问一些诸如“为什么我在Unity3D的编辑器看的效果是正常的,为什么发布到手机就看不到效果”之类的问题了。
之前已经把概念性的东西讲得差不多了,下一次再来讲着色器相关的内容,就有2个选择,要么开始讲一些常用效果的写法,要么讲讲基础光照模型的写法。