Unity Shader分为表面着色器(Surface Shader)、顶点着色器(Vertex Shader)和片段着色器(Fragment Shader),这篇文档会为大家介绍表面着色器。
一.结构
和顶点/片元着色器不同的是,表面着色器的CG代码是直接而且也必须写在SubShader中,Unity会在背后为我们生成多个Pass。当然,可以在SubShader一开始处使用Tags来设置该表面着色器使用的标签。
对顶点着色器和片元着色器的进一层封装。主要部分为两个结构体(Input、SurfaceOutput)和编译指令(#pragma surface)。其中两个结构体是表面着色器中不同函数之间信息的传递的桥梁,而编译指令是我们和Unity沟通的重要手段。
表面着色器包括4个函数:
(1): 顶点变换函数;
(2): 表面着色函数;
(3): 光照模型;
(4): 最终颜色修改函数;
1.1.编译指令
表面着色器放在CGPROGRAM .. ENDCG块里面,表面着色器必须嵌在子着色器(SubShader)块里面,而不是Pass{}里面。因为表面着色器(Surface Shader)将在多重通道(multiple passes)内编译自己,而不是放在某个Pass中。
编译指令最重要的作用是指明该表面着色器使用的表面函数和光照函数,并设置一些可选参数。表面着色器的CG块中的第一句代码往往就是它的编译指令。一般格式如下:
#pragma surface surfaceFunction lightMode[optionalparams]
surfaceFunction:表示指定名称的Cg函数中有表面着色器(surface shader)代码。这个函数的格式应该是这样:void surf(Input IN,inout SurfaceOutput o),其中Input是我们自己定义的结构。Input结构中应该包含所需的纹理坐标(texture coordinates)和表面函数(surfaceFunction)所需要的额外的必须变量。
void surf (Input IN,inout SurfaceOutput o)
void surf (Input IN,inout SurfaceOutputStandard o)
void surf (Input IN,inout SurfaceOutputStandardSpecular o)
SurfaceOutput、SurfaceOutputStandard 和 SurfaceOutputStandardSpecular 都是Unity 内置的结构体,它们需要配合不同的光照模型使用。
LightModel: 使用的光照模式。内置一些基于物理的Standard和StandardSpecular光照模型,以及一些没有基于物理的Lambert(Diffuse)和BlinnPhong(specular)光照模型,当然也可以自己写光照模型。
例如,可以使用下面的函数来定义用于前向渲染中的光照函数:
//用于不依赖视角的光照模型,例如漫反射
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten);
//用于依赖视角的光照模型,例如高光反射
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir,half atten);
half4 Lighting<Name>(SurfaceOutput s, half4 light);
注意:要使用Lambert,就要搭配用SurfaceOutput,不能用SurfaceOutputStandard,Standard光照模型使用SurfaceOutputStandard作为输出结构体。
在编译指令的最后,我们还可以设置一些可选参数。这些可选参数包含了很多非常有用的指令类型,例如,开启/关闭透明度混合/测试,指明自定义的顶点和颜色修改函数,控制生成的代码等。
可选参数:
1:alpha: Alpha 混合模式,用户半透明着色器;
2: alphatest: varirableName Alpha测试模式,用户透明镂空着色器。
3: exclude_path:prepass 使用指定的渲染路径;
4: addshadow: 添加阴影投射器和集合通道;
5: dualforward: 将双重光照贴图用于正向渲染路径中;
6: fullforwardshadows 在正向渲染路径中支持的所有的阴影类型,在前向渲染路径中支持所有光源类型的阴影。默认是只有平行光,添加这个参数就可以让点光或聚光灯有阴影渲染。
7: decal: add 附加印花着色器;
8: decal: blend 附加半透明印花着色器;
9: softvegetation 使用表面着色器,仅在Soft Vegetation 开启时被渲染;
10: noambient 不使用任何光照
11: novertexlights 在正向渲染中不适用球面调和光照或逐点光照;
12: nolightmap 在这个着色器上禁用光照贴图;
13: nodirlightmap 在这个着色器上禁用方向光照贴图;
14: noforwardadd 禁用正向渲染添加通道,去掉所有前向渲染的额外Pass,即支持逐像素平行光,其他光源用逐顶点或SH计算。;
15: approxview: 对于有需要的着色器,逐顶点而不是逐像素计算规范化视线方向。
16: halfasview: 将半方向传递到光照函数中。
17: fullforwardshadows:在前向渲染路径中支持所有光源类型的阴影。默认是只有平
行光,添加这个参数就可以让点光或聚光灯有阴影渲染。
18: noshadow:取消所有阴影。
19: exclude_path:deferred, exclude_path:forward, exclude_path:prepass :不需要为特定渲染路径生成代码。
自定义修改函数:
除了表面函数和光照模型外,表面着色器还可以支持其他两种自定义的函数:顶点修改函数和最后的颜色修改函数。顶点修改函数允许我们自定义一些顶点属性,例如,把顶点颜色传递给表面函数,或是修改顶点位置,实现某些顶点动画等。最后的颜色修改函数则可以在颜色绘制到屏幕前,最后一次修改颜色值,例如实现自定义的雾效等。
1.vertex:VertexFunction:顶点修改,实现一些顶点动画等。
2.finalcolor:ColorFunction:最终颜色修改,实现雾效等。
3.finalgbuffer:ColorFunction:延迟渲染修改,实现边缘检测等。
4.finalprepass:ColorFunction:prepass base路径修改
1.2.两个结构体
Input结构体 包含了许多表面属性的数据来源,因此,它会作为表面函数的输入结构体。Input支持很多内置的变量名,通过这些变量名,我们告诉Unity需要使用的数据信息。下表给出了Input结构体中内置的变量。需要注意的是,我们并不需要自己计算上述的各个变量,而只需要在Input结构体中按上述名称严格声明这些变量即可,Unity会在背后为我们准备好这些数据,而我们只需要在表面函数中直接使用它们即可。一个例外的情况是,我们自定义了顶点修改函数,并需要向表面函数中传递一些自定义的数据。例如,为了自定义雾效,我们可能需要在顶点修改函数中根据顶点在视角空间下的位置信息计算雾效混合系数,这样我们就可以在Input结构体中定义一个名为half fog 的变量,把计算结果存储在该变量后进行输出。
有了Input结构体来提供所需要的数据后,我们就可以据此计算各种表面属性。因此,另一个结构体就是用于存储这些表面属性的结构体,即SurfaceOutput、SurfaceOutputStandard 和 SurfaceOutputStandardSpecular,它会作为表面函数的输出,随后会作为光照函数的输入来进行各种光照计算。相比于Input结构体的自由性,这个结构体里面的变量是提前声明好的,不可以增加也不会减少。SurfaceOutput 的声明可以在Lighting.cginc文件中找到:
struct SurfaceOutput {
fixed3 Albedo;
fixed3 Normal;
fixed3 Emission;
half Specular;
fixed Gloss;
fixed Alpha;
};
struct SurfaceOutputStandard
{
fixed3 Albedo; // base (diffuse or specular) color
fixed3 Normal; // tangent space normal, if written
half3 Emission;
half Metallic; // 0=non-metal, 1=metal
half Smoothness; // 0=rough, 1=smooth
half Occlusion; // occlusion (default 1)
fixed Alpha; // alpha for transparencies
};
struct SurfaceOutputStandardSpecular
{
fixed3 Albedo; // diffuse color
fixed3 Specular; // specular color
fixed3 Normal; // tangent space normal, if written
half3 Emission;
half Smoothness; // 0=rough, 1=smooth
half Occlusion; // occlusion (default 1)
fixed Alpha; // alpha for transparencies
};
在一个表面着色器中,只需要选择上述三者之一即可,这取决于我们选择使用的光照模型。Unity内置的光照模型有两种,一种是Unity5 之前的、简单的、非基于物理的光照模型,包含了Lambert 和 BlinnPhong;另一种是Unity5 添加的、基于物理的光照模型,包括Standard 和 StandardSpecular ,这种模型会更加符合物理规律,但计算也会复杂很多。如果使用了非基于物理的光照模型,就使用SurfaceOutputStad,否则分别使用SurfaceOutputStandard 和SurfaceOutputStandardSpecular 。其中,SurfaceOutputStandard 结构体用于默认的金属工作流程,对应了Standard 光照函数;而SurfaceOutputStandardSpecular 结构体用于高光工作流程,对应了StandardSpecular 光照函数。
在SurfaceOutput结构体中,部分表面属性有:
1)fixed3 Albedo 对光源的反射率。通常由纹理采样和颜色属性的乘积计算而得。
2)fixed3 Normal 表面法线方向
3)fixed3 Emission 自发光。Unity 通常会在片元着色器最后输出前,使用类似下面的语句进行简单的颜色相加。
c.rgb += o.Emission;
4)half Specular 高光反射中的指数部分的系数,影响高光反射的计算。例如,如果使用了内置的BlinnPhong 光照函数,它会使用如下语句计算高光反射的强度:
float spec = pow(nh,s.Specular*128.0)*s.Gloss;
5) fixed Gloss 高光反射中的强度系数。一般在包含了高光反射的光照模型里使用
6) fixed Alpha 透明通道
二.Unity 背后做了什么
Unity在背后会根据表面着色器生成一个包含了很多Pass的顶点/片元着色器。
这些Pass有些是为了针对不同的渲染路径,例如,默认情况下Unity 会为前向渲染路径生成LightMode 为 ForwardBase 和 ForwardAdd 的Pass,为Unity 5 之前的延迟渲染路径生成LightMode 为PrePassBase 和 PrePassFinal 的Pass,为Unity5之后的延迟渲染路径生成LightMode 为 Deferred 的Pass。
还有一些Pass 是用于产生额外的信息。例如,为了给光照映射和动态全局光照提取表面信息,Unity 会生成一个LightMode 为 Meta 的Pass。这些Pass 的生成都是基于我们再表面着色器中的编译指令和自定义的函数,这是由规律可循的。Unity 提供了一个功能,让我们可以对表面着色器自动生成的代码一探究竟:在每个编译完成的表面着色器的面板上,有一个“Show generated code” 按钮,如下图所示。我们只需要单击一下就可以看到Unity为这个表面着色器生成的所有顶点/片元着色器。
以Unity生成的LightMode 为ForwardBase 的Pass为例,它的渲染流水线如下图所示:
Unity对该Pass的自动生成过程大致如下:
- 将表面着色器中CGPROGRAM和ENDCG之间的代码复制过来。
- Unity根据上述代码生成结构体v2f_surf(顶点着色器的输出)。如果Input定义了一些变量但没有使用,生成的结构体也不会包含该变量。还会包含阴影纹理坐标、光照纹理坐标、逐顶点光照等。
- 生成顶点着色器。
3.1. 如果定义了顶点修改函数,会先调用,或填充自定义Input结构体中的变量。Unity会分析该函数修改的数据,通过Input结构体把修改结果存储到v2f_surf相应变量。
3.2. 计算v2f_surf中其他变量:顶点位置、纹理坐标、法线方向、逐顶点光照、光照纹理等。
3.3. 把v2f_surf传递给片元着色器。 - 生成片元着色器。
4.1. 将v2f_surf变量(纹理坐标、视角方向)填充到Input结构体。
4.2. 调用自定义表面函数,填充SurfaceOutput结构体。
4.3. 调用光照函数得到初始的颜色值。如果使用内置的Lambert或BlinnPhong光照函数,Unity还会计算动态全局光照,并添加到光照模型的计算。
4.4. 进行其他颜色叠加。例如没有光照烘培,会添加逐顶点光照的影响。
4.5. 调用最后的颜色修改函数。
三.CG函数讲解
UnpackNormal()函数
UnpackNormal接受一个fixed4的输入,并将其转换为所对应的法线值(fixed3),并将其赋值给输出的Normal,就可以参与到光线运算中完成接下来的的渲染工作。
调用示例:
o.Normal = UnpackNormal(tex2D(_BumpMap,IN.uv_BumpMap));
saturate()函数
saturate函数的作用是将取值转化为[0,1]之内的一个值。其可选的原型如下:
float saturate(float x);
float1 saturate(float1 x);
float2 saturate(float2 x);
float3 saturate(float3 x);
float4 saturate(float4 x);
half saturate(half x);
half1 saturate(half1 x);
half2 saturate(half2 x);
half3 saturate(half3 x);
half4 saturate(half4 x);
fixed saturate(fixed x);
fixed1 saturate(fixed1 x);
fixed2 saturate(fixed2 x);
fixed3 saturate(fixed3 x);
fixed4 saturate(fixed4 x);
返回值:
如果x取值小于0,则返回值为0
如果x取值大于1,则返回值为1
若x在0到1之间,则直接返回x的值
dot()函数
dot函数的作用用于返回两个向量的标量积,可选原型如下:
float dot(float a, float b);
float dot(float1 a, float1 b);
float dot(float2 a, float2 b);
float dot(float3 a, float3 b);
float dot(float4 a, float4 b);
half dot(half a, half b);
half dot(half1 a, half1 b);
half dot(half2 a, half2 b);
half dot(half3 a, half3 b);
half dot(half4 a, half4 b);
fixed dot(fixed a, fixed b);
fixed dot(fixed1 a, fixed1 b);
fixed dot(fixed2 a, fixed2 b);
fixed dot(fixed3 a, fixed3 b);
fixed dot(fixed4 a, fixed4 b);
tex2D()函数
该函数用于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)
参数简介
samp - 需要查找采样的对象
s - 需进行查找的纹理坐标
dx - 预计算的沿x轴方向的导数
dy - 预计算的沿y轴方向的导数
texeloff - 添加给最终纹理的偏移量
其返回值为查找到的纹理