镜面的实现
在上一章中,我们解释了用于渲染的光照的基本理论,并从头开始在无光照度中实现了一个漫反射着色器。在这一章中,你将学习如何为该着色器添加一个Specular项。
基本照明的计算(第二部分)
在上一章中,我们学习了漫反射近似的理论;现在轮到Specular 近似的时候了。
镜面
只有当你的视角恰好与镜面的方向一致时,你才会看到镜面。这使得它依赖于视角。你可以在标准光照图中烘烤漫反射光照,但镜面光需要你在烘烤中使用一些技巧或实时计算。在烘焙中使用一些技巧,或者实时计算。
我们可以使用的最简单的specular公式之一被称为Phong。
这就是反射方向。它可以通过将法线和光线方向的点积乘以2和法线方向,然后减去光线方向而得到。在许多着色器语言中都有一个函数用于此,一般称为反射
清单 6-1. Phong的实现
float3 reflectionVector = reflect (-lightDir, normal);
float specDot = max(dot(reflectionVector, eyeDir), 0.0);
float spec = pow(specDot, specExponent);
为了实现Phong(见清单6-1),你需要首先计算出镜子的反射方向。然后计算反射方向与视图方向的点积–正如我们所说的,镜面反射是与视图有关。然后你把这个值提升到你在着色器属性中选择的指数的幂。属性中选择的指数。这就控制了镜面的强度。你将在未来的一章中看到这种方法是如何,虽然 松散地基于物理现实,但实际上却违反了许多基于物理的着色规则。当你修正它以符合 PBS 原则时,即使是一个简单的 Phong 也会感觉更真实。
现在我们已经介绍了Specular项的理论和实现,让我们将其付诸实践把它添加到漫反射着色器中。
你的第一个照明Unity着色器(第二部分)
在这一节中,你将使用DiffuseShader并为其添加一个specular项。
实现Specular
让我们创建一个新的材质,叫做SpecularMaterial。复制DiffuseShader,并调用复制的 SpecularShader。记得将着色器路径改为Custom/SpecularShader,否则你会有两个重叠的着色器路径名称。
着色器的结构是不变的,所以不用担心这个问题。首先,在属性块中添加两个新的属性块。_SpecColor和_Shininess。_SpecColor是镜面的颜色,使用白色作为默认值。默认值。Shininess是镜面的强度,它是一个数字。
Properties
{
_DiffuseTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,0,0,1)
_Ambient ("Ambient", Range (0, 1)) = 0.25
_SpecColor ("Specular Material Color", Color) = (1,1,1,1)
_Shininess ("Shininess", Float) = 10
}
接下来,向 v2f 结构添加一个成员。 您需要在顶点着色器中计算一个额外的值,然后通过 v2f 将它们传递给片段着色器。 这个额外的值是世界空间顶点位置,我们将其称为 vertexWorld:
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertexClip : SV_POSITION;
float4 vertexWorld : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
};
这样做是因为您需要在片段着色器中计算光的方向。 你可以在顶点着色器中做到这一点,这被称为被顶点光照。 但不出所料,如果您在片段着色器中进行光照计算,结果看起来会更好。
现在,在顶点着色器中填充该值:
v2f vert (appdata v)
{
v2f o;
o.vertexClip = UnityObjectToClipPos(v.vertex);
o.vertexWorld = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _DiffuseTex);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldNormal = worldNormal;
return o;
}
在这一行中,我们使用矩阵乘法将局部空间顶点位置转换为世界空间顶点位置。 unity_ObjectToWorld 是您进行此转换所需的矩阵; 它包含在标准库中
现在您已准备好开始将所需的行添加到片段函数中。 有几个值需要计算,例如归一化的世界空间法线、归一化的视图方向和归一化的光方向(参见清单 6-2)。
清单 6-2。 镜面反射计算所需的值
float3 normalDirection = normalize(i.worldNormal);
float3 viewDirection = normalize(UnityWorldSpaceViewDir(i.vertexWorld));
float3 lightDirection = normalize(UnityWorldSpaceLightDir(i.vertexWorld));
为了达到最佳效果,所有的向量都需要在转换后进行归一化处理。根据不同的情况,你可能会避免这样做,但你有可能在你的照明中出现不良的伪影。请注意,所有这些值都在 相同的坐标空间,世界空间。
在这里,我们使用了各种实用函数。 您可以使用 mul 和适当的矩阵,但 Unity 2017.x 无论如何都会用实用程序函数替换它们。 例如,如果您在 Unity 中保存和加载后使用 mul(UNITY_MATRIX_MVP, v.vertex),您会发现它被转换为 UnityObjectToClipPos(v.vertex) 并且此消息添加到文件的顶部:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
因此,一开始就不使用这些实用函数是很没有意义的。继续,在漫反射实现之后,你需要将Specular解释中的那些伪代码行翻译成有效的Unity着色器代码(见清单6-3)。
清单 6-3. 镜面计算
float3 reflectionDirection = reflect(-lightDirection, normalDirection);
float3 specularDot = max(0.0, dot(viewDirection, reflectionDirection));
float3 specular = pow(specularDot, _Shininess);
首先,你用反射函数找到反射方向。你需要lightDirection的负数,所以它从物体到光线。然后你计算viewDirection和reflectionDirection之间的点积,这与你在漫反射项中用来计算从表面反射多少光线的操作是一样的。
在漫反射中,它在法线和光线方向之间。在这里,它是在镜面反射方向和视角方向之间,因为镜面项是与视角有关的。请注意,同样,点积值不能为负。点积值不能是负数。你不可能有负的光。
然后你需要在最终的输出中加入镜面。对于漫反射,你要把它乘以表面的颜色。表面的颜色。对于镜面来说,这相当于乘以镜面的颜色。
float4 specularTerm = float4(specular, 1) * _SpecColor * _LightColor0;
你可能已经注意到了,我们并没有像你在做 "漫反射 "时那样,将 "镜面 "乘以 "表面 "的颜色。漫反射。那基本上会使它消失。这里面有一些物理原理,我们将在介绍基于物理的着色时解释。我们将在介绍基于物理的着色时解释。
最后,整个着色器显示在清单6-5中,以方便你使用。
这就结束了对非基于物理的Phong specular的介绍。到现在为止,你只在场景中支持一个灯光。场景中只支持一个灯光,但是通过添加一个ForwardAdd pass,你可以在一个场景中支持任意数量的 场景中的任何数量的灯光。让我们为这个着色器添加一个ForwardAdd通道。
支持不止一盏灯
你需要注意的一件事是确保ForwardAdd是一个单独的通道,因为你不希望 因为你不希望做像添加环境这样的事情超过一次。复制镜面着色器,并将着色器的路径改为 Custom/SpecularShaderForwardAdd。
你需要把目前停留在sub-shader中的标签和其他信息移到Pass中。并复制和粘贴当前的pass。将#pragma multi_compile_fwdbase加在ForwardBase pass的其他pragma之后(见清单6-6)。ForwardBase传递中的其他pragma之后(见清单6-6)。
然后我们应该把第二个Pass的Tag改成ForwardAdd,在标签后面加上Blend One One一行,并在标签,并添加pragma #pragma multi_compile_fwdadd(见清单6-7)。
ForwardAdd告诉编译器它应该在第一道光之后使用这道光,而Blend One One则设置了混合模式。混合模式基本上类似于Photoshop中的图层模式。我们有不同的图层,由不同的通道渲染,我们希望以一种合理的方式将它们混合在一起。请记住,混合模式比Photoshop中的模式要简单得多。计算公式是Blend SrcFactor DstFactor;我们对两个因素都使用了1,这意味着颜色是加法混合的。
这些谚语是为了利用自动多编译系统的优势,该系统会编译所有的着色器的所有变体,这些变体需要特定的通道来工作。
清单6-7。为ForwardAdd通道进行设置
这已经足够了,有了灯光后,首先影响的是结果。最后的修饰是将 _Ambient作为ForwardAdd通道中的最小值,这意味着我们不会再添加两次或更多的环境。(见清单6-8)。
清单6-8。避免将环境术语重新添加到ForwardAdd Pass中
//Diffuse implementation (Lambert)
float nl = max(0.0, dot(normalDirection, lightDirection));
让我们尝试一下,在项目中添加一个不同的灯光,也许用两种不同的颜色来区分这两个灯光。然后将应用于鸭子的着色器改为图6-2中所示的新着色器。
为了方便起见,清单6-9显示了生成的完整着色器,你可以看到它相当长 因为代码是重复的。
总结
在本章中,你实现了最简单的Specular版本,Phong。然后你在着色器中加入了对 然后你在着色器中加入了对多个灯光的支持,由于代码的重复,这导致了一个很长的着色器。
下一章将介绍表面着色器,它可以让你省去一些 的工作,并且在支持多个灯光时可以节省大量的代码行。