本文同时发布在我的个人博客上:https://dragon_boy.gitee.io
表面着色器是一种对艺术家友好的着色器,它不同于传统的顶点着色器和片元着色器等流程,而是添加了一层抽象,整个渲染流程的着色器被分为三个:表面着色器、光照模型和光照着色器。其中,表面着色器定义了模型表面的反射率、法线和高光等,光照模型则选择兰伯特、Phong和Bliin-Phong等模型。光照着色器负责计算光照衰减和阴影等。绝大部分时候只需要关注表面着色器,例如纹理和颜色等,光照模型是提前定义的,光照着色其由系统定义,不会轻易更改。
表面着色器例子
Shader代码如下:
Shader "Unlit/BumpedDiffuse"
{
Properties
{
_Color ("Color",Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_BumpMap("Normal Map", 2D) = "bump" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
CGPROGRAM
#pragma surface surf Lambert
#pragma target 3.0
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
};
float4 _Color;
sampler2D _MainTex;
sampler2D _BumpMap;
void surf(Input IN, inout SurfaceOutput o)
{
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = tex.rgb * _Color.rgb;
o.Alpha = tex.a * _Color.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
}
ENDCG
}
FallBack "Diffuse"
}
效果如下:
编译指令
编译指令最重要的作用是指明该表面着色器使用的表面函数和光照函数,并设置一些可选参数。编译指令一般格式如下:
#pragma surface surfaceFunction lightModel [optionalparams]
表面函数
表面函数的输入结构体由自己定义,输出结构体有三种:
void surf(Input IN, inout surfaceOutput o);
void surf(Input IN, inout surfaceOutputStandard o);
void surf(Input IN, inout surfaceOutputStandardSpecular o);
光照函数
内置的光照函数有Standard
、StandardSpecular
,Lambert
,BlinnPhong
,我们也可以定义自己的光照函数,使用下面这种格式:
//不依赖视角的光照模型
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten)
//依赖视角
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
更多的例子参考官方文档。
其它可选参数。
这些参数包括是否开启透明度测试,透明度混合,指明自定义函数,控制生成的代码等。
- 自定义的修改函数。表面着色器支持另外两种自定义的函数:顶点修改函数(vertex:VertexFunction)和最后颜色修改函数(final:ColorFunction)。顶点修改函数允许我们自定义一些顶点属性,最后颜色修改函数则可以最后修改一次颜色值。
其它的可选参数参考官方文档。
两个结构体
Input
表面函数的输入结构体Input,它定义了许多内置的变量,一些不同空间下的的顶点坐标,视角方向,法线方向,反射方向等。然后可以自定义一些变量,这些变量可以在自定义的函数中计算。
SurfaceOutPut结构体
相关定义在Lighting.cginc
和UnityPBSLighting.cginc
中寻找。
表面着色器的实现
Unity在背后会根据表面着色器生成一个包含很多Pass的顶点\片元着色器,这些着色器会根据设置的渲染路径设置标签。同时还会根据编译指令的额外参数生成相应的Pass,我们可以在Shader面板点击Show generated code
按钮来查看Unity自动生成的顶点\片元着色器。
Unity生成代码的过程如下:
-
直接将表面着色器中CGPROGRAM和ENDCG之间的代码复制过来,这些代码包括我们对Input结构体、表面幻术、光照函数等变量和函数的定义。这些函数和变量会在之后的处理过程中被当成正常的结构体和函数进行调用。
-
Unity会分析上述代码,并据此生成顶点着色器的输出——
v2f_surf
结构体,用于在顶点着色器和片元着色器之间进行数据传递。Unity会分析我们在自定义函数中所使用的变量,如果需要就会在结构体中生成。 -
接着生成顶点着色器。
-
如果我们自定义了顶点修改函数,Unity会首先调用定点修改函数来修改顶点数据,或填充自定义的Input结构体中的变量。然后,Unity会分析顶点修改函数中修改的数据,在需要通过Input结构体将修改结果存储到
v2f_surf
结构体中。 -
计算
v2f_surf
中其它生成的变量值。这主要包括顶点位置、纹理坐标等。 -
最后,将
v2f_surf
传递给接下来的片元着色器。
- 生成片元着色器
-
使用
v2f_sur
中对应变量填充Input结构体。 -
调用我们自定义的表面函数填充SurfaceOuput结构体,或其它。
-
调用光照函数得到初始的颜色值。
-
进行其它的颜色叠加。
-
最后,如果自定义了最后的颜色修改函数,Unity就会调用它进行最后的颜色修改。