unity shader development[5]

56 篇文章 2 订阅

你的第一个 Unity 光照着色器

  在前面的章节中,我们介绍了着色器编辑工作流程,从最简单的着色器开始,并解释了图形管道和坐标空间变换在其中的作用。

  本章从本书的主要主题开始:照明。您可以编写无限多种着色器,而无需担心照明。但本书的核心是光照计算以及它们如何帮助您的游戏看起来很棒且与众不同。首先,我们将采用 MonochromeShader 着色器并使用一些非常近似的光照计算对其进行扩展。

  这种类型的近似照明在基于物理的渲染出现之前使用。它更便宜,更简单,但也更丑。您可能会认为它曾经是典型的“视频游戏外观”。也就是说,它将通过简短而简单的着色器向您介绍照明,还将帮助您了解着色是如何随时间演变的。

照明着色器

  光照着色器包括模拟光击中表面所需的所有计算。虽然大多数光照着色器至少是松散地基于渲染方程,但几年前,GPU 中的计算能力不足以做更多的事情,而不仅仅是对它的非常松散的近似。直到大约 2010 年,当基于物理的模型开始从电影渲染社区中渗透出来时,这些是我们用来指代照明计算的不同部分的术语:

  • 漫反射是具有不规则微平面的表面的子集,可在许多不同方向反射光
  • 镜面反射是具有对齐微平面的表面子集,并在几个相似的方向反射光
  • 环境反射是场景中的最小光照强度,所以直射光照不到的地方最终不会只有黑色。

  这些解释都是以microfacet理论为基础的,这是我们现在可以做到的,因为基于物理的渲染在实时着色中引入了它。但在这之前,漫反射和镜面反射是很难精确确定的。这是因为在物理现实中,镜面和漫反射之间并没有如此明确的划分。一个表面的微观面的方向变化控制着这个表面的光滑或粗糙程度。粗糙的表面看起来更像漫反射;光滑的表面看起来更像镜面。漫反射和镜面反射是一个连续的过程,不是一个二元选择。

什么是近似值

  近似值是一个接近但不完全正确的数值或数量。在我们的例子中,近似值意味着计算渲染方程某些部分的另一种方法。近似值的计算量可能比较小,也可能比较大。到目前为止,还不可能实时计算渲染方程。由于其中存在一个积分。
在这里插入图片描述
  意味着,在这组计算中,在F后面的一组计算将对表面上名为x的点的每个方向w进行重复计算。正如你可能怀疑的那样,每个方向意味着很多的方向。到目前为止,我们还没有找到实时解决这个积分的方法,尽管我们已经开始看到诸如在GPU上实现某种程度的实时光线追踪。

  使用更便宜的近似值可能意味着你的游戏可以以60帧的速度运行,但这也将意味着
你失去了一些潜在的渲染保真度。并非所有的游戏都注重保真度。大多数时候,图形需要
只需要足够好。但在本书中,我们关注的是保真度,因为这是基于物理的渲染结果的标准。

漫反射近似

  让我们试着用更实际的术语来定义漫反射。想象一下光击中一个表面,然后被反射到任何可能的方向(见图 5-1)。每个反射方向的可能性都相同。
在这里插入图片描述
  这种近似只与光的方向、颜色和强度有关。它没有在微观层面考虑表面的性质。目标是确定有多少光线照射到渲染模型的每个像素并被反射,而无需深入研究更详细的模拟。把它想象成一个函数——你向它传递光线的方向、颜色和强度,以及表面的未照亮颜色,它会给你在那个点上最终图像的颜色。

  可以在顶点着色器中实现它,这在计算上比较便宜,因为通常顶点比像素少。这样做的原因是光栅器会对数值进行插值,但作为一种副作用,会产生相当明显的伪影。

镜面近似

  想想看,一束光打在一个表面上,只在几个方向上被反射(见图5-2)。在这个近似水平上,镜面光常常表现为一个几乎是白色的小圆圈。同样,这个近似值只涉及到光的方向、颜色和强度。

在这里插入图片描述
  光仅在几个方向上反射这一事实使镜面反射依赖于镜面,这意味着如果您的视点移动,镜面反射项将发生变化。在光照贴图中模拟镜面反射比较棘手,因为您还需要在光照贴图中烘焙方向和其他信息。

漫反射和镜面反射相结合

  大多数情况下,漫反射和镜面反射项将出现在同一表面上。金属具有较低的漫反射分量和高得多的镜面反射分量,但它们仍然具有漫反射分量。当您将它们组合起来时,您将获得通常用于表示照明计算的典型图形,如图 5-3 所示。

在这里插入图片描述

计算基本照明

  现在我们已经定义了镜面反射和漫反射近似值,让我们尝试实现它们。

Diffuse

  如果您还记得,在第 1 章中,我们讨论过光线射到表面的角度是多么重要。这个角度称为入射角,它越大,表面接收到的光线就越少。对于大于 90 度的角度,它根本不会接收到任何光线(见图 5-4)。
在这里插入图片描述
  要计算表面将接收的亮度/光量,我们可以使用入射角的余弦。

  为了在我们的着色器中计算照明,我们需要计算表面法线方向和光线方向的那个角度。这两个是向量,这意味着它们是由一个以上的数值组成的组。着色器语言中的向量通常可以包含两个、三个或四个元素。我们在这里需要的操作被称为点积。余弦和点积都是每个着色器语言中包含的函数。 翻译成代码,这个概念在清单5-1中显示。

  清单5-1. 计算亮度的两种方法(Normal和LightDir的幅度为1)。

float brightness = cos( angle_of_incidence ) // brightness from the angle of incidence
float brightness = dot( normal, lightDir )  //  brightness calculated from the Normal and Light directions
float3 pixelColor = brightness * lightColor * surfaceColor // final value of the surface color

  这个操作的结果,乘以光线的颜色和表面的颜色,就可以得到粗略但有效的照明近似值。给你一个粗略但有效的照明近似值。这种基本的漫反射也被称为兰伯特,或 兰伯特反射率(Lambertian Reflectance)。

在这里插入图片描述
  意思是说,光的方向与法线方向的点积,乘以光的颜色和强度,就可以得到该点的像素的颜色。

  现在我们已经介绍了漫反射项的理论和实现,我们可以通过将它们添加到我们的立方体场景来将其付诸实践。

你的第一个照明Unity着色器

  让我们用漫反射项扩展单色着色器。

实现一个扩散项

  让我们创建一个新的材质,叫做DiffuseMaterial。复制MonochromeShader,并调用复制的DiffuseShader。记得将着色器路径改为Custom/DiffuseShader,否则你会有两个重叠的着色器路径名称。

  我们需要对旧着色器进行大量更改。首先,我们需要把标签部分改成这样

Tags { "LightMode" = "ForwardBase" }

  这意味着该通道将用于前向渲染器的第一个光通道。如果我们只有一个 ForwardBase pass,第一个 pass 之后的任何灯都不会影响最终结果。如果我们希望他们这样做,我们需要添加另一个传递并将其标签设置为以下内容:

Tags { "LightMode" = "ForwardAdd" }

  我们暂时不会担心第二次Pass。继续,我们需要在文件中的适当位置添加另一个include:

#include "UnityLightingCommon.cginc"

  UnityLightingCommon.cginc是一个包含许多有用的变量和函数的文件,可以在照明着色器中使用。有了这些,杂事就结束了,所以现在我们要进入实现的实质。

  首先,请记住法线和光方向需要在第 4 章中介绍的坐标空间之一中。考虑一下,我们不应该使用对象空间,因为光在我们正在渲染的模型之外。用于这些光照计算的合适空间是世界空间。

  首先,我们需要从渲染器中获取 Normal 信息;因此,我们需要为该法线添加一个槽到 appdata,该数据结构包含我们从渲染器请求的信息。这是 appdata 的样子:

struct appdata
{
	float4 vertex : POSITION;
	float3 normal : NORMAL;
};

  请注意,我们还通过在声明中添加 NORMAL 语义来告诉它我们想要一个法线; 否则,渲染器将无法理解我们想要什么。

  这个顶点函数需要计算世界空间中的法线方向。 幸运的是,有一个方便的函数,叫做 UnityObjectToWorldNormal(在第 4 章中提到过),它将我们刚刚通过 appdata 传递给顶点着色器的 Object Space Normal 方向转换为 World Space

v2f vert (appdata v)
{
	v2f o;
	o.vertex = UnityObjectToClipPos(v.vertex);
	float3 worldNormal = UnityObjectToWorldNormal(v.normal); //calculate world normal
	o.worldNormal = worldNormal; //assign to output data structure
	return o;
}

  然后我们需要将它分配给输出结构。 为此,我们需要添加该插槽,使用 TEXCOORD0 语义告诉它使用适合三个或四个值的向量的插槽

struct v2f
{
	float4 vertex : SV_POSITION;
	float3 worldNormal : TEXCOORD0;
};

  现在我们可以使用该信息来计算我们的兰伯特漫反射。 我们可以从变量 _LightColor0 中获取灯光颜色,该变量来自额外的包含文件,以及场景中第一个灯光的世界空间灯光位置来自变量 _WorldSpaceLightPos0

  当你想访问一个向量的子集时,你可以在一个点之后添加 r、g、b、a 或 x、y、z、w。 这意味着直接访问向量包含的数字,有点像您使用 [0]、[1] 等访问 ac 数组索引处的值。这称为 swizzle 运算符,它可以做比数组更多的事情 索引,比如通过改变字母的顺序来重新排列值本身。

  在片段着色器中,我们需要先对worldNormal 进行归一化,因为变换的结果可能不是一个幅度为1 的向量。然后我们计算法线和光线方向的点积,注意不要让它变成负数。 这就是 max 函数的作用。

float4 frag (v2f i) : SV_Target
{
	float3 normalDirection = normalize(i.worldNormal);
	float nl = max(0.0, dot(normalDirection, _WorldSpaceLightPos0.xyz));
	float4 diffuseTerm = nl * _Color * tex * _LightColor0;
	return diffuseTerm;
}

  最后,我们将该点积与表面颜色和光的颜色相乘。 这就是结局; 你的着色器应该是完整的。 为方便起见,清单 5-2 显示了该着色器的所有代码。

//Listing 5-2. Our Diffuse Shader So Far

Shader "Custom/DiffuseShader"
{
	Properties
	{
		_Color ("Color", Color) = (1,0,0,1)
	}
	SubShader
	{
		Tags { "LightMode" = "ForwardBase" }
		LOD 100
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			#include "UnityLightingCommon.cginc"
			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};
			struct v2f
			{
				float4 vertex : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
			};
			float4 _Color;
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				float3 worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldNormal = worldNormal;
				return o;
			}
			float4 frag (v2f i) : SV_Target
			{
				float3 normalDirection = normalize(i.worldNormal);
				float nl = max(0.0, dot(normalDirection, _WorldSpaceLightPos0.xyz));
				float4 diffuseTerm = nl * _Color * _LightColor0;
				return diffuseTerm;
			}
			ENDCG
		}
	}
}

  要检查您的工作,请将此着色器应用于场景中的立方体。 将此着色器分配给它的材质,然后将材质分配给您的立方体(见图 5-5)。

在这里插入图片描述
  为了更好地判断结果,您可能需要一个更复杂的模型。 让我们看看如何导入模型。 在本书随附的源代码中,您会找到一个名为duck.fbx 的模型。 将一个名为 Models 的文件夹添加到您的项目中,然后将鸭子模型拖入其中。 这很可能会创建一个包含虚假材料的文件夹; 删除它,因为我们需要完全控制我们的材料,并且最好保持场景整洁。 Unity 尝试为每个导入的模型创建一个材质,但是这种自动转换的结果往往不是很好。

  拖动场景中的鸭子模型,然后将 DiffuseMaterial 拖动到生成的 GameObject 上,您应该可以欣赏到 Lambert 阴影鸭子的视图(见图 5-6)。

在这里插入图片描述

添加纹理属性

  为了让鸭子看起来更好看,我们可以添加一个纹理属性。 让我们添加到属性:

_DiffuseTex ("Texture", 2D) = "white" {}

  然后,您需要在 appdata 中为纹理坐标添加一个槽:

float2 uv : TEXCOORD0;

  在 v2f 中,由于我们已经将 TEXCOORD0 语义用于世界法线,我们需要将其更改为 TEXCOORD1,然后添加:

float2 uv : TEXCOORD0;

  发生这种情况是因为我们要求 GPU 在该数据结构中为我们提供内插纹理 UV。 我们可以访问的纹理插值器数量是有限的,这取决于您机器中的 GPU。 如果您使用移动 GPU 并尝试在数据结构中传递太多向量,您可能会遇到编译器错误。

  让我们为纹理添加变量:

sampler2D _DiffuseTex;
float4 _DiffuseTex_ST;

  在顶点函数中,我们将添加:

o.uv = TRANSFORM_TEX(v.uv, _MainTex);

  这是缩放和偏移纹理坐标的宏。 这样,关于比例和偏移的材料属性的任何更改都将在此处应用。 这也是我们声明为 _DiffuseTex_ST的原因,因为 TRANSFORM_TEX 需要它。 现在我们要改变片段函数。 我们需要添加行来对纹理进行采样,然后我们需要将此纹理与漫反射计算和已经存在的 _Color 属性一起使用。

float4 frag (v2f i) : SV_Target
{
	float3 normalDirection = normalize(i.worldNormal);
	float4 tex = tex2D(_DiffuseTex, i.uv);
	float nl = max(_0.0, dot(normalDirection, _WorldSpaceLightPos0.xyz));
	float4 diffuseTerm = nl * _Color * tex * _LightColor0;
	return diffuseTerm;
}

  我们通过将_Color与纹理样本的颜色相乘,以及法线和光线方向的点积来获得最终的颜色。法线和光线方向的点积。现在你应该建立一个名为Textures的新目录,拖入源代码中的Duck_DIFF. tga纹理,并通过检查器将其分配给我们刚刚添加的材质属性。Et voila, 你的鸭子现在看起来会更像一只鸭子(见图5-7)。
在这里插入图片描述

添加环境值

  如前所述,环境基本上是一个截止值,在这个值之下我们不应该让我们的漫反射值下降。目前,它在片段着色器中被硬编码为0。

float nl = max(0.0, dot(normalDirection, _WorldSpaceLightPos0.xyz));

  然而,我们也许应该为它添加一个属性,这样我们就可以在不改变代码的情况下改变它。这也向你介绍了另一种类型的属性,范围。

_Name ("Description", Range (min, max)) = number

  所以我们称这个属性为 _Ambient,范围为 0 到 1,默认值为 0.25。 然后让我们像往常一样为它创建一个变量,称为 float _Ambient。 我们将把这个变量代替 0 作为 max 函数的第一个参数,在那里我们计算我们的 nl 变量。

  现在你可以玩玩这个属性的滑块,实时看看改变它是如何影响到 最终效果(见图5-8)。

在这里插入图片描述
清单5-3显示了该着色器的完整代码

在这里插入图片描述
在这里插入图片描述

总结

  本章介绍了照明的基础知识,解释了一些照明公式,并通过实施漫反射照明扩展了我们对着色器编写和 ShaderLab 属性的了解。 我们还介绍了如何处理资产,这一主题将在后面的章节中介绍。

  在下一章,我们将为这个漫反射着色器添加一个镜面组件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值