unity shader development[7]

表面着色器

  在前两章中,我们解释了用于渲染的照明的基本理论,并在未照明的着色器中从头开始实现漫反射着色器和镜面反射着色器。在本章中,我们将把上一章中未照明的着色器转换为表面着色器,这将节省相当多的代码。

什么是表面着色器?

  表面着色器是Unity特有的一种着色器,它是用来计算 表面光照模型的着色器。从本章开始,我们将只使用Surface着色器进行照明。

  它们的主要优点是它们隐藏了相当多的模板代码。例如,考虑到上一章的着色器,你会记得你基本上要复制和粘贴整个着色器来使着色器支持一个以上的光。这是很麻烦的。表面着色器解决了这个问题,而代价是在灵活性方面的一些损失。你可能确实需要使用无灯着色器,因为你需要更多的 但对于我们的目的来说,Surface shaders可以很好地解决这个问题。

  Surface shaders的结构与Unlit shaders不同。在Unlit shader中,我们使用两个shader函数(顶点和片段),两个数据结构(一个用于输入顶点函数,另一个用于输出),如果你想支持一个以上的光,你需要写两个通道,ForwardAdd和ForwardBase。在Surface shader中,顶点函数是可选的,所以你仍然使用两个数据结构,但它们有不同的用途,而且你根本没有指定片段函数,但你必须写一个表面函数。此外,你还可以选择写你自己的照明模型函数。

默认的表面着色器

  让我们创建一个新的表面着色器,在项目窗格上点击右键,选择Create ➤ Shader ➤
Standard Surface Shader. 。清单7-1显示了我们得到的东西。

unity shader development[7]_第1张图片

unity shader development[7]_第2张图片
  正如承诺的那样,没有顶点函数和片段函数,但有一个新的表面函数和一个新的pragma。

Pragmas

  我们仍然有一个属性块,但是vert和fragma已经不在了。取而代之的是一个单一的surface pragma。surface pragma的第一个参数是surface 函数,这是你在着色器中写的责任(我们稍后会解释它的具体内容),然后是你要使用的照明模型,以及任何选项。

  这个默认文件中包含的surf pragma是。

#pragma surface surf Standard fullforwardshadows

  surf是表面函数,Standard是照明模型,fullforwardshadows是一个选项。

  如果你想使用一个不同于默认的顶点函数,你可以。你可以在Surface shader中编写你的自定义 顶点函数,指定顶点输入和输出的数据结构(由于历史原因,通常称为appdata和v2f,但你可以随心所欲地称呼它们)。由于历史原因,通常被称为appdata和v2f,但你可以随意称呼它们),最后 将其传递给surf pragma,这种方式。

#pragma surface surf Lambert vertex:vert

  surf还是表面函数,Lambert是一个内置的照明模型,而vertex:vert则指定了 顶点函数。

新的数据结构

  让我们暂时忽略关于实例化的部分,而研究表面函数。它需要一个数据结构 和一个叫做SurfaceOutputStandard的数据结构,其中inout为 类型限定符。这意味着它是一个输入,但也是一个输出,你不需要两个不同的数据结构 来实现。然后这个SurfaceOutputStandard数据结构将被发送到照明函数(Standard, BlinnPhong, Lambert,或者你可以写一个自定义的)。

  这个着色器中的输入结构只包括UV,承担了部分保留给顶点输出函数,v2f。

struct Input {
	float2 uv_MainTex;
};

  你可能注意到没有列出任何包含文件,但SurfaceOutputStandard来自通常的包含文件,特别是来自UnityPBSLighting.cginc(见清单7-2)。

清单7-2. 数据结构SurfaceOutputStandard

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
}; 

  这个数据结构的目的是向照明函数传递信息。

表面函数

  surf函数用于准备必要的数据,然后将其分配给数据结构(见清单7-3)。

unity shader development[7]_第3张图片
  正如你所看到的,在这个默认的着色器中,SurfaceOutputStandard数据结构的7个成员中有4个被填入。其中一些被大多数照明模型所使用,而另一些则是Unity所使用的标准照明模型所特有的。

  反照率是Unity中用来表示表面颜色的名称,通常来自于漫反射的 纹理。除非你要画一个透明的网格,否则不会真正使用Alpha。法线是来自法线贴图的数据。数据,如果网格要发光,则使用Emission。其他的都是特定于标准照明模型。

什么是光照模型?

  理解这个问题的最好方法是举个例子。标准照明函数非常复杂,所以让我们看看我们已经熟悉的一个函数,即Lambert照明函数(见清单7-4),它取自Lighting.cginc(但经过自由编辑,所以它适合于一个函数)。

清单 7-4. 作为照明模型函数实现的兰伯特

inline fixed4 LightingLambert (SurfaceOutput s, UnityGI gi)
{
	fixed4 c;
	UnityLight light = gi.light;
	fixed diff = max (0, dot (s.Normal, light.dir));
	c.rgb = s.Albedo * light.color * diff;
	c.a = s.Alpha;
	#ifdef UNITY_LIGHT_FUNCTION_APPLY_INDIRECT
	c.rgb += s.Albedo * gi.indirect.diffuse;
	#endif
	return c;
} 

  这是一个照明模型函数。任何你可以自己编写的自定义照明模型都会遵循这个模式。它返回一个fixed4,接收一个SurfaceOutput和一个UnityGI结构。SurfaceOutput与SurfaceOutputStandard相似,只是成员较少,因为Lambert是一个比较简单的照明模型。UnityGI是一个数据结构,用来传递由全局照明系统计算的间接光。全局照明基本上是解决计算间接光的一个更好的方法,在上一章中我们用一个简单的环境值粗略地解决了这个问题。

  你还不需要担心全局光照的问题。UnityGI这个主题的重要成员是光,它是另一个数据结构,UnityLight。UnityLight包括光的方向和光的颜色。你应该认识到我们在第五章中用来实现Lambert的计算:点积和与光和表面颜色的乘法。

  照明函数应该是模拟光线在表面上的行为。要做到这一点,正如你从Diffuse和Specular近似中了解到的,它需要一些信息。也就是光的方向、法线方向、表面和光的颜色,可能还有视图方向。

  SurfaceOutput和它的表亲SurfaceOutputStandard都包含法线和颜色(反照率) 的成员。光线方向和颜色是从UnityGI的数据结构中获得的。换句话说,一个照明函数会得到计算照明所需的所有数据,无论是通过输入的数据结构或其他参数。

  Lambert不需要视图方向,但如果它需要,函数签名将是。

half4 Lighting<Name> (SurfaceOutput s, half3 viewDir, UnityGI gi);

  使用这些函数签名将使编译器识别你的函数是一个照明模型的 函数,你就可以在 surface pragma中使用它。

  至此,我们已经了解了编写Surface着色器所需的所有部分,但它们究竟是如何组合的呢?

表面着色器的数据流

  Surface着色器的执行模式有些不直观。你应该考虑到,在幕后,Surface着色器被编译成与Unlit着色器非常相似的东西。在这一点上,一切又归结为一个顶点着色器和一个片段着色器。

  表面着色器去掉了一些灵活性,以节省你的时间和代码行。它们通过将片段着色器包装起来,放在一个接口后面来做到这一点。他们把它分解成一个表面函数和照明模型函数。此外,Unity给了你各种预制的照明功能,所以你只需要担心表面功能。

  但本书的重点是编写基于物理的照明模型函数,并使它们与Unity着色器基础设施的其他部分一起工作。所以,你以后会对光照函数相当熟悉。

  现在请注意,数据流从可选的顶点函数开始,你可以为其制作输入和输出数据结构,或者坚持使用标准库中的数据结构。然后,数据从顶点函数流向输入结构,该结构被传递给surface函数。在surface函数中,你要填入一个数据结构,其中包含计算光照所需的大部分数据,该结构一般被命名为SurfaceOutput或类似的结构。该结构被传递给光照函数,最后返回一种颜色(见图7-1)。

unity shader development[7]_第4张图片

编辑一个表面着色器

  现在你知道了什么是Surface shader以及它如何帮助你,让我们来看看几个使用Unity标准光照功能的自定义Surface shaders的例子。标准照明模型是基于物理的。在接下来的章节中,我们将解释这到底是什么意思,并展示哪些代码使其成为物理基础。

添加第二张反照率贴图

  使用标准光照模型不能解决的最常见的任务之一是当你需要更多的纹理,而不是它所提供的。让我们添加第二个反照率纹理,然后在两者之间添加一个滑块。

  首先,让我们添加第二个纹理和滑块的值(见清单7-5)。

清单 7-5. 添加第二个反照率纹理的属性

Properties {
	_Color ("Color", Color) = (1,1,1,1)
	_MainTex ("Albedo (RGB)", 2D) = "white" {}
	_SecondAlbedo ("Second Albedo (RGB)", 2D) = "white" {}
	_AlbedoLerp ("Albedo Lerp", Range(0,1)) = 0.5
	_Glossiness ("Smoothness", Range(0,1)) = 0.5
	_Metallic ("Metallic", Range(0,1)) = 0.0
}

  然后,像往常一样,我们需要将这些声明为变量。在输入结构之后就可以了(见清单7-6)。你 您可能认为我们需要为第二个纹理在输入结构中添加另一组 UV,但如果纹理有 但如果纹理具有相同的 UV(应该如此,因为它是为同一个模型设计的),你可以为两个纹理循环使用同一组 UVs。

清单 7-6. 为第二个反照率纹理添加变量声明

sampler2D _MainTex;
sampler2D _SecondAlbedo;
half _AlbedoLerp;

  现在我们需要在surf函数中添加适当的行。我们基本上是在处理我们的着色器输入(纹理、数值等),然后再将信息发送到照明函数。因此,我们需要查找第二个纹理,并使用相同的UVs,然后将这两个纹理的对比结果分配给反照率 输出(见清单7-7)。

清单7-7. 对第二个反照率纹理进行采样,并在两个反照率纹理之间进行Lerp

void surf (Input IN, inout SurfaceOutputStandard o) {
	fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
	fixed4 secondAlbedo = tex2D (_SecondAlbedo, IN.uv_MainTex);
	o.Albedo = lerp(c, secondAlbedo, _AlbedoLerp) * _Color;
	// Metallic and smoothness come from slider variables
	o.Metallic = _Metallic;
	o.Smoothness = _Glossiness;
	o.Alpha = c.a;
}

  这就是我们需要的一切。让我们看看最后的结果;第二个纹理是第一个纹理的不同版本,它充满了噪音。在它们之间进行Lerp,其结果如图7-2所示。
unity shader development[7]_第5张图片
  你可以看到来自第一个纹理的轻微黄色色调,以及来自第二个纹理的噪音。纹理产生的噪声,在最后的结果中显示出来。清单7-8显示了完整的着色器以方便你使用。

清单7-8。在两个反照率纹理之间切换的表面着色器

unity shader development[7]_第6张图片
  如果你需要更精细的控制,你可以用一个纹理来代替滑块,这样做的效果非常相似。你需要进行第三次纹理查询,然后从遮罩纹理的纹理通道中提取一个值,然后用它作为控制lerp的值。

添加一个法线图

  另一个非常常见的任务是处理法线贴图。让我们为本章开始时的默认着色器添加一个法线贴图。首先,像往常一样,让我们为法线图添加一个属性,如清单7-9所示。

清单7-9。添加法线图属性

Properties {
	_Color ("Color", Color) = (1,1,1,1)
	_MainTex ("Albedo (RGB)", 2D) = "white" {}
	_NormalMap("Normal Map", 2D) = "bump" {}
	_Glossiness ("Smoothness", Range(0,1)) = 0.5
	_Metallic ("Metallic", Range(0,1)) = 0.0
}

  然后,再次声明这个变量,并在surf函数中添加适当的处理。在这种情况下,我们需要对纹理进行取样(同样,主纹理的UV也可以),并对其使用UnpackNormal函数。然后我们需要将结果分配给表面输出数据结构中的Normal成员,如清单7-10所示

清单 7-10. 声明法线图的变量并解压法线图

sampler2D _NormalMap;
void surf (Input IN, inout SurfaceOutputStandard o) {
	fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
	o.Normal = UnpackNormal (tex2D (_NormalMap, IN.uv_MainTex));
	o.Albedo = c.rgb;
	// Metallic and smoothness come from slider variables
	o.Metallic = _Metallic;
	o.Smoothness = _Glossiness;
	o.Alpha = c.a;
}

  如果我们在Surface着色器中使用UnpackNormal函数,就需要额外的两个顶点着色器输出成员(世界空间中的binormal和tangent),并在片段函数中增加几行,如果我们在Unlit着色器中这样做的话。所以使用Surface shader来处理法线贴图,可以节省一些精力。支持法线贴图的结果可以在图7-3中看到。

unity shader development[7]_第7张图片
为方便起见,完整的着色器显示在清单7-11中。

unity shader development[7]_第8张图片
unity shader development[7]_第9张图片

确保阴影的作用

  你可能已经注意到,所有的着色器都有一个我们还没有提到的fallback值。fallback是一个不同的着色器的名字,它将被用来渲染使用该着色器的网格的阴影。如果你的后备着色器丢失或损坏,那么网格的阴影也会被破坏。

使用不同的内置照明模式

  我们一直在使用 "标准 "照明模式,但如果我们想的话,我们可以很容易地切换到不同的模式。让我们使用BlinnPhong的模型。要做到这一点,首先把surf pragma改成这样。

#pragma surface surf BlinnPhong fullforwardshadows

  BlinnPhong的光函数采用SurfaceOutput数据结构,而不是SurfaceOutputStandard,所以我们把surf函数的签名改成这样。

void surf (Input IN, inout SurfaceOutput o) {

  BlinnPhong没有光泽度和金属性的概念,所以我们应该把它们从属性、变量声明和surf函数中删除。然后,我们需要把BlinnPhong照明模型函数使用的光泽度和镜面值作为属性添加进去,如清单7-12所示。

清单 7-12. 内置的BlinnPhong照明模型功能

inline fixed4 UnityPhongLight (SurfaceOutput s, half3 viewDir, UnityLight light)
{
	half3 h = normalize (light.dir + viewDir);
	fixed diff = max (0, dot (s.Normal, light.dir));
	float nh = max (0, dot (s.Normal, h));
	float spec = pow (nh, s.Specular*128.0) * s.Gloss;
	fixed4 c;
	c.rgb = s.Albedo * light.color * diff + light.color * _SpecColor.rgb * spec;
	c.a = s.Alpha;
	return c;
} 

  BlinnPhong的实现中使用了Specular、SpecColor和Gloss。传统上,在Unity中 反照率纹理的alpha被用来提供光泽度,而镜面被声明为 作为属性中的光泽度。SpecColor也需要被添加到属性中,但不是添加到 声明,因为那是默认的。清单7-13显示了最终的BlinnPhong表面着色器

Shader "Custom/SurfaceShaderBlinnPhong" {
	Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_SpecColor ("Specular Material Color", Color) = (1,1,1,1)
		_Shininess ("Shininess", Range (0.03, 1)) = 0.078125
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		CGPROGRAM
		#pragma surface surf BlinnPhong fullforwardshadows
		#pragma target 3.0
		sampler2D _MainTex;
		float _Shininess;
		struct Input {
			float2 uv_MainTex;
		};
		fixed4 _Color;
		UNITY_INSTANCING_CBUFFER_START(Props)
		// put more per-instance properties here
		UNITY_INSTANCING_CBUFFER_END
		void surf (Input IN, inout SurfaceOutput o) {
			fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			o.Specular = _Shininess;
			o.Gloss = c.a;
			o.Alpha = 1.0f;
		}
		ENDCG
	}
	FallBack "Diffuse"
}

图7-4显示了将最终的BlinnPhong表面着色器应用于我们的鸭子场景的结果。

unity shader development[7]_第10张图片

编写一个自定义的照明模型

  编写照明函数将是我们从现在开始的主要重点,所以让我们开始做这个。Unity缺乏一个内置的Phong照明模型,因为它更喜欢使用BlinnPhong。我们可以利用我们已经掌握的关于Phong的知识来实现它作为一个自定义的照明模型。

照明模型函数签名

  如前所述,自定义照明模型函数必须符合几个可能的签名中的一个。它们有四个。其中两个用于Forward渲染器,一个只用于漫反射,还有一个是与视角相关的,所以它可以用于镜面。这里是漫反射的签名。

half4 Lighting<Name> (SurfaceOutput s, UnityGI gi); 

这里是取决于视角的:

half4 Lighting<Name> (SurfaceOutput s, half3 viewDir, UnityGI gi); 

  另外两个是针对当前的延迟渲染器和传统的延迟渲染器的。我们不打算在本书中介绍延迟渲染,因为使用延迟渲染器限制了我们在着色器中可以使用的数据,而且更有趣的光照模型需要额外的数据,而这些数据可能无法获得。无论如何,你所学习的所有原理都会对延迟渲染器起作用。

  以下是延迟渲染器使用的两个函数签名。

half4 Lighting<Name>_Deferred (SurfaceOutput s, UnityGI gi, out half4 outDiffuseOcclusion,
out half4 outSpecSmoothness, out half4 outNormal);
half4 Lighting<Name>_PrePass (SurfaceOutput s, half4 light); 

  我们不打算使用它们,但能够识别它们是好事。

SurfaceOutput数据结构

  所以,继续说,Phong肯定是依赖于视角的,因此我们需要使用第二个类型的签名。我们还需要知道SurfaceOutput有哪些成员,如清单7-14所示。

清单7-14. SurfaceOutput数据结构

struct SurfaceOutput {
	fixed3 Albedo;
	fixed3 Normal;
	fixed3 Emission;
	half Specular;
	fixed Gloss;
	fixed Alpha;
}; 

  我们不需要发射(Emission)(只在物体应该发射光的时候使用)。我们从这个数据结构中得到的Normal已经在世界空间中了,我们从函数签名中得到的光线方向和viewDir也是如此。

表面函数

  表面函数非常简单,因为我们只需要把反照率和alpha传给它(见清单7-15)。

清单7-15. 我们的Phong自定义表面着色器的表面函数

void surf (Input IN, inout SurfaceOutput o) {
	fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
	o.Albedo = c.rgb;
	o.Alpha = 1.0f;
}
属性块

  你可能还记得,Phong使用了一个SpecColor和一个Shininess值,所以我们需要将它们添加到属性中,并将Shininess声明为一个变量。同样,SpecColor不需要被声明为一个变量(见清单7-16)。

清单7-16. 我们的Phong自定义表面着色器的属性块

Properties {
	_Color ("Color", Color) = (1,1,1,1)
	_MainTex ("Albedo (RGB)", 2D) = "white" {}
	_SpecColor ("Specular Material Color", Color) = (1,1,1,1)
	_Shininess ("Shininess", Range (0.03, 128)) = 0.078125
}
自定义光照函数

  现在,这就是这个着色器的核心。我们正在改编几章前的Phong实现。请记住,所有传入照明函数的方向都已经在世界空间中了。你需要注意使用正确的源,因为传入的数据结构、传入的普通值和直接使用的属性中的变量是混合的。

  我们要去掉环境,因为从Surface着色器中,更容易访问Unity的全局照明功能。

清单7-17显示了最终的照明模型函数

inline fixed4 LightingPhong (SurfaceOutput s, half3 viewDir, UnityGI gi)
{
	UnityLight light = gi.light;
	float nl = max(0.0f, dot(s.Normal, light.dir));
	float3 diffuseTerm = nl * s.Albedo.rgb * light.color;
	float3 reflectionDirection = reflect(-light.dir, s.Normal);
	float3 specularDot = max(0.0, dot(viewDir, reflectionDirection)); //no more ambient
	float3 specular = pow(specularDot, _Shininess);
	float3 specularTerm = specular * _SpecColor.rgb * light.color.rgb;
	float3 finalColor = diffuseTerm.rgb + specularTerm;
	fixed4 c;
	c.rgb = finalColor;
	c.a = s.Alpha;
	#ifdef UNITY_LIGHT_FUNCTION_APPLY_INDIRECT
	c.rgb += s.Albedo * gi.indirect.diffuse;
	#endif
	return c;
} 

  注意最后的那个块,它是将全局光照值添加到最终的颜色中。为了使其发挥作用,我们需要提供一个额外的函数,叫做LightingPhong_GI。目前,让我们使用与BlinnPhong相同的GI函数(见清单7-18)。

清单 7-18. 为我们的自定义Phong照明模型提供必要的全局照明功能

inline void LightingPhong_GI (SurfaceOutput s, UnityGIInput data, inout UnityGI gi)
{
	gi = UnityGlobalIllumination (data, 1.0, s.Normal);
}

  最后一点是更新surf pragma以使用我们新的照明功能。

#pragma surface surf Phong fullforwardshadows

  全局照度数据必须比我们的环境强,所以结果是有点太亮了,如图所示

unity shader development[7]_第11张图片
  如果我们去掉全局照明那一行,再加回一个环境,我们可以看到结果和Phong Unlit着色器是一样的(见图7-6)。
unity shader development[7]_第12张图片
  现在的情况是,通过使用全局照明,我们将非基于物理的部分与基于物理的部分混合在一起,这是个麻烦的秘诀。接下来,我们将讨论什么是基于物理的意味着什么,以及如何坚持它。为了方便起见,清单7-19显示了最终的着色器,包括全局照明。

  清单7-19. 包括Phong自定义照明模型功能的完整着色器
unity shader development[7]_第13张图片
unity shader development[7]_第14张图片

总结

  本章剖析了Surface shaders,并讨论了它们的用处。你看到了几个有用的例子,说明你如何能够定制它们。最重要的是,本章解释了什么是自定义光照模型函数,并展示了如何将Phong的实现从Unlit着色器移植到Surface着色器中的自定义光照函数。

  我们将深入研究基于物理的原则,详细解释这些原则,并开始将其付诸实践。

你可能感兴趣的:(Shader,Unity,unity,shader)