表面着色器的CG代码是直接而且也必须写在SubShader块中,Unity 会在背后为我们生成多个Pass。当然,可以在SubShader一开始处使用Tags来设置该表面着色器使用的标签。我们也可以使用LOD命令设置该表面着色器的LOD值。然后,我们使用CGPROGRAM和ENDCG定义了表面着色器的具体代码。
Shader "Custom/test"
{
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
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)
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
}
FallBack "Diffuse"
}
1 编译指令
编译指令最重要的作用是指明该表面着色器使用的表面函数和光照函数,并设置一些可选参数。表面着色器的CG块中的第一句代码往往就是它的编译指令。编译指令的一般格式如下:
#pragma surface surfaceFunction lightModel [optionalparams ]
其中,#pragmasurface用于指明该编译指令是用于定义表面着色器的,在它的后面需要指明使用的表面函数(surfaceFunction) 和光照模型(lightModel), 同时,还可以使用一"些可选参数来控制表面着色器的一些行为。
表面函数:
surfaceFunction就用于定义这些表面属性。surfaceFunction通常就是名为surf的函数(函数名可以是任意的),它的函数格式是固定的:
void surf (Input IN,inout SurfaceOutput o)
void surf (Input IN, inout SurfaceOutputStandard o)
void surf (Input IN, inout SurfaceOutputStandardSpecular o)
在表面函数中,会使用输入结构体InputIN来设置各种表面属性,并把这些属性存储在输出结构体SurfaceOutput、SurfaceOutputStandard 或SurfaceOutputStandardSpecular中,再传递给光照
函数计算光照结果。
光照函数:
光照函数会使用表面函数中设置的各种表面属性,来应用某些光照模型,进而模拟物体表面的光照效果。Unity内置了基于物理的光照模型函数Standard和StandardSpecular (UnityPBSLighting.cginc文件中被定义),以及简单的非基于物理的光照模型函数Lambert和BlinnPhong (在Lighting.cginc文件中被定义)。
自定义用于前向渲染的函数:
//用于不依赖视角的光照模型,例如漫反射
half4 Lighting (SurfaceOutput s,half3 lightDir, half atten) ;
//用于依赖视角的光照模型,例如高光反射
half4 Lighting (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten) ;
自定义光照模型https://docs.unity3d.com/Manual/SL-SurfaceShaderLightingExamples.html可选参数:
可选参数https://docs.unity3d.com/Manual/SL-SurfaceShaders.html
顶点修改函数(vertex:VertexFunction):允许我们自定义一些顶点属性,例如,把顶点颜色传递给表面函数,或是修改顶点位置,实现某些顶点动画等。
最后的颜色修改函数(finalcolor:ColorFunction):在颜色绘制到屏幕前,最后一次修改颜色值,例如实现自定义的雾效等。
addshadow参数会为表面着色器生成一个阴影投射的Pass。
fullforwardshadows参数则可以在前向渲染路径中支持所有光源类型的阴影。
noshadow参数来禁用阴影。
通过alpha和alphatest指令来控制透明度混合和透明度测试。例如,alphatest:VariableName 指令会使用名为VariableName的变量来剔除不满足条件的片元。此时,我们可能还需要使用上面提到的addshadow参数来生成正确的阴影投射的Pass。
noambient 参数会告诉Unity不要应用任何环境光照或光照探针(light probe)。
novertexlights参数告诉Unity不要应用任何逐顶点光照。
noforwardadd 会去掉所有前向渲染中的额外的Pass。也就是说,这个Shader只会支持一个逐像素的平行光,而其他的光源会按照逐顶点或SH的方法来计算光照影响。这个参数通常会用于移动平台版本的表面着色器中。
nolightmap控制光照烘焙。
nofog雾效模拟。
默认情况下,Unity会为一个表面着色器生成相应的前向渲染路径、延迟渲染路径使用的Pass, 这会导致生成的Shader文件比较大。如果我们确定该表面着色器只会在某些渲染路径中使用,就可以exclude_ path:deferred、exclude_path:forward和exclude_path:prepass来告诉Unity不需要为某些渲染路径生成代码。
2 两个结构体
Input结构体:
Input结构体包含了许多表面属性的数据来源,因此,它会作为表面函数的输入结构体(如果自定义了顶点修改函数,它还会是顶点修改函数的输出结构体)。
Input结构体中包含了主纹理和法线纹理的采样坐标uv_ MainTex和uv_ BumpMap。 这些采样坐标必须以“uv”为前缀(实际上也可用“uv2” 为前缀,表明使用次纹理坐标集合),后面紧跟纹理名称。以主纹理_MainTex为例,如果需要使用它的采样坐标,就需要在Input结构体中声明float2 uv_ MainTex来对应它的采样坐标。表17.1列出了Input结构体中内置的其他变量。
使用时按照上述名称严格声明变量。一个例外情况是,我们自定义了顶点修改函数,并需要向表面函数中传递一些自定义的数据。例如,为了自定义雾效,我们可能需要在顶点修改函数中根据顶点在视角空间下的位置信息计算雾效混合系数,这样我们就可以在Input 结构体中定义一个名为half fog的变量,把计算结果存储在该变量后进行输出。
SurfaceOutput结构体:
作为表面函数的输出,随后会作为光照函数的输入来进行各种光照计算。相比与Input结构体的自由性,这个结构体里面的变量是提前就声明好的,不可以增加也不会减少(如果没有对某些变量赋值,就会使用默认值)。SurfaceOutput 的声明可以在Lighting.cginc文件中找到:
struct SurfaceOutput {
fixed3 Albedo; //对光源的反射率
fixed3 Normal; //表面法线方向
fixed3. Emission; //自发光
half Specular; //高光反射的指数部分
fixed Gloss; //高光反射中的强度系数
fixed Alpha; //透明通道
};
而SurfaceOutputStandard和SurfaceOutputStandardSpecular的声明可以UnityPBSLighting.cginc中找到:
struct Sur faceOutputStandard
{
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;另一种是Unity 5添加的、基于物理的光照模型,包括Standard和StandardSpecular,这种模型会更加符合物理规律,但计算也会复杂很多。
如果使用了非基于物理的光照模型,我们通常会使用SurfaceOutput 结构体,而如果使用了基于物理的光照模型Standard或StandardSpecular, 我们会分别使用SurfaceOutputStandard 或SurfaceOutputStandardSpecular结构体。其中,SurfaceOutputStandard结构体用于默认的金属工作流程(Metallic Workflow),对应了Standard 光照函数;而SurfaceOutputStandardSpecular结构体用于高光工作流程(Specular Workflow),对应了StandardSpecular 光照函数。
表面着色器本质上就是包含了很多Pass的顶点/片元着色器。
3 Unity在背后做了什么
我们之前说过,Unity 在背后会根据表面着色器生成一个包含了很多Pass 的顶点/片元着色器。这些Pass有些是为了针对不同的渲染路径,例如,默认情况下Unity 会为前向渲染路径生成LightMode为ForwardBase和ForwardAdd的Pass,为Unity 5之前的延迟渲染路径生成LightMode为PrePassBase和PrePassFinal的Pass,为Unity 5之后的延迟渲染路径生成LightMode为Deferred的Pass。还有一些Pass是用于产生额外的信息,例如,为了给光照映射和动态全局光照提取表面信息,Unity 会生成一个LightMode为Meta的Pass。有些表面着色器由于修改了顶点位置,因此,我们可以利用adddshadow编译指令为它生成相应的LightMode为ShadowCaster的阴影投射Pass。这些Pass的生成都是基于我们在表面着色器中的编译指令和自定义的函数,这是有规律可循的。
点击shader面板上的Show generated code按钮可以查看表面着色器生成的顶点/片元着色器。
以Unity生成的LightMode为ForwardBase的Pass (用于前向渲染)为例,Unity的自动生成过程如下:
(1)直接将表面着色器中CGPROGRAM和ENDCG之间的代码复制过来,这些代码包括了我们对Input结构体、表面函数、光照函数(如果自定了的话)等变量和函数的定义。这些函数和变量会在之后的处理过程中被当成正常的结构体和函数进行调用。
(2) Unity会分析上述代码,并据此生成项点着色器的输出——v2f_ surf 结构体,用于在顶点着色器和片元着色器之间进行数据传递。
(3)接着,生成顶点着色器。
①如果我们自定义了顶点修改函数,Unity 会首先调用顶点修改函数来修改顶点数据,或填充自定义的Input结构体中的变量。然后,Unity会分析顶点修改函数中修改的数据,在需要时通过Input结构体将修改结果存储到v2f_surf相应的变量中。
②计算v2f_surf中其他生成的变量值。这主要包括了顶点位置、纹理坐标、法线方向、逐顶点光照、光照纹理的采样坐标等。当然,我们可以通过编译指令来控制某些变量是否需要计算。
③最后,将v2f_surf传递给接下来的片元着色器。
(4)生成片元着色器。
①使用v2f_ surf 中的对应变量填充Input结构体,例如,纹理坐标、视角方向等。
②调用我们自定义的表面函数填充SurfaceOutput结构体。
③调用光照函数得到初始的颜色值。如果使用的是内置的Lambert或BlinnPhong光照函数,Unity还会计算动态全局光照,并添加到光照模型的计算中。
④进行其他的颜色叠加。例如,如果没有使用光照烘焙,还会添加逐顶点光照的影响。
⑤最后,如果自定义了最后的颜色修改函数,Unity就会调用它进行最后的颜色修改。
4 Surface Shader的缺点
失去了对各种优化和各种特效实现的控制,对性能有一定的影响。
表面着色器还无法完成一些自定义的渲染效果
Shader "Unity Shaders Book/Chapter 17/Normal Extrusion" {
Properties {
_ColorTint ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
_Amount ("Extrusion Amount", Range(-0.5, 0.5)) = 0.1
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
CGPROGRAM
// surf - which surface function.
// CustomLambert - which lighting model to use.
// vertex:myvert - use custom vertex modification function.
// finalcolor:mycolor - use custom final color modification function.
// addshadow - generate a shadow caster pass. Because we modify the vertex position, the shder needs special shadows handling.
// exclude_path:deferred/exclude_path:prepas - do not generate passes for deferred/legacy deferred rendering path.
// nometa - do not generate a “meta” pass (that’s used by lightmapping & dynamic global illumination to extract surface information).
#pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa
#pragma target 3.0
fixed4 _ColorTint;
sampler2D _MainTex;
sampler2D _BumpMap;
half _Amount;
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
};
//顶点修改函数
void myvert (inout appdata_full v) {
v.vertex.xyz += v.normal * _Amount;
}
//表面函数
void surf (Input IN, inout SurfaceOutput o) {
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = tex.rgb;
o.Alpha = tex.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
}
//光照函数
half4 LightingCustomLambert (SurfaceOutput s, half3 lightDir, half atten) {
half NdotL = dot(s.Normal, lightDir);
half4 c;
c.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten);
c.a = s.Alpha;
return c;
}
//最后的颜色修改函数
void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) {
color *= _ColorTint;
}
ENDCG
}
FallBack "Legacy Shaders/Diffuse"
}