unity-shader-Unity3D的着色器介绍(三)——编写Unity3D着色器的三种方式

原文链接:http://liweizhaolili.lofter.com/post/1cc70144_5afa8fa


过了2个星期,又来继续说着色器了。这两个星期发生的事情有点多。

不管你会不会写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.标准范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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.标准例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Shader "Custom/Exam1" {
Properties {
_MainTex ("Texture", 2D) = "white" { }
}
SubShader
{
pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
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小片段的开始处,可以添加:

#pragma vertex name - 表明这是个以name为名字的函数的顶点程序。

#pragma fragment name- 表明这是个以name为名字的函数的片段程序。

#pragma fragmentoption option - 添加选项到编译的OpenGL 片段程序。 通过 ARB 片段程序 可以查询到所允许的规范的选项列表。 这个指令对顶点程序或者不是以OpenGL为编译目标的程序无效。

#pragma target name - 着色器目标编译。

#pragma target default 编译默认目标:

在Direct3D 9环境中:顶点着色器1.1和像索着色器2.0

ARB顶点程序 有128位指令限制,ARB片段程序 有96位指令限制(32位纹理 + 64位算术运算),16个临时寄存器和4个间接纹理。

#pragma target 3.0 编译着色器模式3.0:

在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中,这样子限制较少。

#pragma only_renderers space separated names - 仅用给定的渲染器编译着色器。默认情况下用所有的渲染器都编译着色器。

•d3d9 - Direct3D 9.

•opengl - OpenGL.

•gles - OpenGL ES 2.0.

•xbox360 - Xbox 360.

•ps3 - PlayStation 3.

•flash - Flash.

#pragma exclude_renderers space separated names - 不用给定的渲染器编译着色器。 默认情况下用所有的渲染器都编译着色器。参考上一点。

#pragma glsl - 用桌面OpenGL平台编译着色器时,转换成GLSL里面的Cg/HLSL(而不是默认设置的ARB顶点/片段程序)。

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

声明顶点程序名称

#pragma vertex vert

下面就可以写顶点程序vert了

1
2
3
4
5
6
7
8
9
10
11
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、片段程序

#pragma fragment frag

然后写

half4 frag (v2f i) : COLOR

{

​ half4 texcol = tex2D (_MainTex, i.uv);

​ return texcol * _Color;

}

片段程序不需要自定义块作为返回值,因为片段程序的返回值就直接是颜色了。

不过在片段程序里面,可以通过对之前顶点程序做处理获得的顶点位置、uv、法线等信息进行各种处理,让最终该点的颜色发生各种各样的改变。

3、Surface Shaders

1.标准例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.详细说明

程序声明写法:

#pragma surface surfaceFunction lightModel [optionalparams]

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))。

输出端:

1
2
3
4
5
6
7
8
struct SurfaceOutput {
half3 Albedo;
half3 Normal;
half3 Emission;
half Specular;
half Gloss;
half Alpha;
};

最后说两句我觉得比较重要的话:

1、着色器都有它规定的指令限制、寄存器、和纹理的限制,有一些复杂的效果需要使用到较多的寄存器和指令,可能会超出限制,比如使用着色器来实现骨骼动画就很容易超出限制了。不论在什么平台编写着色器(unity3D或者stage3D之类)都需要先了解清楚有什么限制,不然等到编译的时候就哭了。

2、着色器的限制有时候是跟发布平台的硬件支持有关系的,某些发布的平台(比如手机端)不支持你指定的高级别着色器模式,它有可能会根据实际情况降低级别,或者甚至显示不出来。所以编写着色器效果之前要考虑一下需要发布的平台,不要老是问一些诸如“为什么我在Unity3D的编辑器看的效果是正常的,为什么发布到手机就看不到效果”之类的问题了。

之前已经把概念性的东西讲得差不多了,下一次再来讲着色器相关的内容,就有2个选择,要么开始讲一些常用效果的写法,要么讲讲基础光照模型的写法。>