编写Unity3D着色器的三种方式

Posted AaronBlog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编写Unity3D着色器的三种方式相关的知识,希望对你有一定的参考价值。

不管你会不会写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 Shader "VertexLit" {
 2     Properties {
 3         _Color ("Main Color", Color) = (1,1,1,0.5)
 4         _SpecColor ("Spec Color", Color) = (1,1,1,1)
 5         _Emission ("Emmisive Color", Color) = (0,0,0,0)
 6         _Shininess ("Shininess", Range (0.01, 1)) = 0.7
 7         _MainTex ("Base (RGB)", 2D) = "white" { }
 8     }
 9 
10     SubShader {
11         Pass {
12             Material {
13                 Diffuse [_Color]
14                 Ambient [_Color]
15                 Shininess [_Shininess]
16                 Specular [_SpecColor]
17                 Emission [_Emission]
18             }
19             Lighting On
20             SeparateSpecular On
21             SetTexture [_MainTex] {
22                 constantColor [_Color]
23                 Combine texture * primary DOUBLE, texture * constant
24             }
25         }
26     }
27 } 
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 Shader "Custom/Exam1" {
 2 Properties {
 3 _MainTex ("Texture", 2D) = "white" { }
 4 }
 5 SubShader
 6 {
 7 pass
 8 {
 9 CGPROGRAM
10 #pragma vertex vert
11 #pragma fragment frag
12 #include "UnityCG.cginc"
13 sampler2D _MainTex;
14 float4 _MainTex_ST;
15 struct v2f {
16     float4  pos : SV_POSITION;
17     float2  uv : TEXCOORD0;
18 } ;
19 v2f vert (appdata_base v)
20 {
21     v2f o;
22    o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
23 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
24     return o;
25 }
26 float4 frag (v2f i) : COLOR
27 {
28 float4 texCol = tex2D(_MainTex,i.uv);
29     float4 outp = texCol;
30     return outp;
31 }
32 ENDCG
33 }
34 }
35 }
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了
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.标准例子:
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))。 
 
 
输出端:
struct SurfaceOutput {
    half3 Albedo;
    half3 Normal;
    half3 Emission;
    half Specular;
    half Gloss;
    half Alpha;
};
 
最后说两句我觉得比较重要的话:
1、着色器都有它规定的指令限制、寄存器、和纹理的限制,有一些复杂的效果需要使用到较多的寄存器和指令,可能会超出限制,比如使用着色器来实现骨骼动画就很容易超出限制了。不论在什么平台编写着色器(unity3D或者stage3D之类)都需要先了解清楚有什么限制,不然等到编译的时候就哭了。
2、着色器的限制有时候是跟发布平台的硬件支持有关系的,某些发布的平台(比如手机端)不支持你指定的高级别着色器模式,它有可能会根据实际情况降低级别,或者甚至显示不出来。所以编写着色器效果之前要考虑一下需要发布的平台,不要老是问一些诸如“为什么我在Unity3D的编辑器看的效果是正常的,为什么发布到手机就看不到效果”之类的问题了。
 
 原文地址:http://blog.csdn.net/a6627651/article/details/50545643

以上是关于编写Unity3D着色器的三种方式的主要内容,如果未能解决你的问题,请参考以下文章

unity3d游戏开发学习分享之表面着色器讲解

DirectX11--HLSL编译着色器的三种方法

传递给片段着色器的纹理坐标全部为 0

使用制服时 Xamarin OpenGL 片段着色器的奇怪行为

带有顶点/片段着色器的光。使用不同的变量。 (openGL)

顶点着色器的绘制操作