UnityShader精要笔记五 基础光照(漫反射+高光反射)

本文继续对《UnityShader入门精要》——冯乐乐 第六章 基础光照 进行学习

虽然书名有入门俩字,但是本章涉及的内容却点到为止,可以参考闫令琪Games101课程来对照理解。

一、前置知识:主要参考闫令琪Games101课程
1.辐照度(irradiance)

参考
https://www.bilibili.com/video/BV1X7411F744?p=15
图形学笔记十 光追二 划分加速 辐射度量学
这里转一下部分结论来回顾一下:

  • energy:辐射能量,单位焦耳
  • flux(power) :单位时间内辐射的能量,单位lm流明
  • 立体角:三维尺度上角的大小。一个立体角张得越大,它在球面上的投影面积越大。单位sr球面度。
  • intensity: 翻译为光强度,单位时间内,单位立体角上辐射的能量。Power/4π。单位candela坎德拉。
  • Irradiance:对比理解,intensity是单位立体角上辐射的能量,Irradiance则是单位面积辐射的能量。
    单位是流明除以平方米,lm/m^2=lux(勒克斯)。
    注意这个面积必须和光线垂直,不垂直需要先投影。
    翻译为光照度。
  • Radiance:缝合怪,综合了intensity和Irradiance,即单位立体角和单位面积上辐射的能量。单位nit尼特。
    翻译为光亮度。
2.BRDF BSDF

参考
https://www.bilibili.com/video/BV1X7411F744?p=15
https://www.bilibili.com/video/BV1X7411F744?p=17

折射也需要对应一种 “BRDF”,但是我们知道BRDF的R是反射(Reflectance),所以折射应该叫BTDF,T为折射(Transmittance)。把这两个统称起来我们可以叫做BSDF,S表示散射(Scattering)。不管反射折射我们都认为是一种散射,BSDF(散射) = BRDF(反射) + BTDF(折射)

3.Lambert+ Phong+ Blinn-Phong
4.flat shading+Gouraud shading+Shade each pixel(Phong shading)

参考
https://www.bilibili.com/video/BV1X7411F744?p=7
https://www.bilibili.com/video/BV1X7411F744?p=8
https://www.bilibili.com/video/BV1X7411F744?p=9

图形学笔记六 Shading 渲染管线

5.冯乐乐6.2.6节

虽然标准光照模型仅仅是一个经验模型,也就是说,它并不完全符合真实世界中的光照现象。但由于它的易用性、计算速度和得到的效果都比较好,因此让被广泛使用。而也是由于它的广泛使用性,这种标准光照模型有很多不同的叫法。例如,一些资料中称它为Phong光照模型,因为裴祥风(Bui Tuong Phong)首先提出了使用漫反射和高光反射的和来对反射光照进行建模的基本思想,并且提出了基于经验的计算高光反射的方法(用于计算漫反射光照的兰伯特模型在那时已经被提出了)。而后,由于Blinn的方法简化了计算而且在某些情况下计算更快,我们把这种模型称为Blinn-Phong光照模型。

但这种模型有很多局限性。首先,有很多重要的物理现象多无法用Blinn-Phong模型表现出来,例如菲涅尔反射(Fresnel reflection)。其次,Blinn-Phong模型时各项同性的(isotropic)的。也就是说,当我们固定视角和光源方向旋转这个表面时,反射不会发生任何改变。但有些表面是具有各向异性(anisotropic)反射性质的,例如拉丝金属、毛发等。在后面我们将学习基于物理的光照模型,这些光照模型更加复杂,同时也可以更加真实的反映光和物体的交互。

关于菲涅尔反射,各项同性,各向异性,可以参考闫令琪https://www.bilibili.com/video/BV1X7411F744?p=17

二、Unity中的环境光和自发光

在Unity中,场景中的环境光可以在Window->Rendering->Lighting Settings->Ambient Source/Ambient Intensity中控制。在Shader中,我们只需要通过Unity的内置变量UNITY_LIGHTMODEL_AMBIENT就可以得到环境光的颜色和强度信息。

而大多数物体是没有发光特性的,因此在本文中的大部分Shader中都没有计算自发光部分。如果要就算自发光也很简单,我们只需要在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色即可。

三、在UnityShader中实现漫反射光照模型

参考图形学笔记六 Shading 渲染管线,先来回顾一下闫令琪课程中给出的公式:

image.png

再对比一下冯乐乐给出的公式,除了光衰减部分,其它是一样的:
入射光线的颜色和强度Clight,材质的漫反射系数mdiffuse,表面法线n以及光源方向I

四、Gouraud shading 逐顶点光照

原书操作步骤很详细,部分步骤和第五章的示例一样,这里重点介绍不一样的地方。
Chapter6-DiffuseVertexLevel.shader部分代码参考:

Tags { "LightMode"="ForwardBase" }

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

fixed4 _Diffuse;

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

struct v2f {
    float4 pos : SV_POSITION;
    fixed3 color : COLOR;
};

v2f vert(a2v v) {
    v2f o;
    // Transform the vertex from object space to projection space
    o.pos = UnityObjectToClipPos(v.vertex);
    
    // Get ambient term
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    
    // Transform the normal from object space to world space
    fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
    // Get the light direction in world space
    fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
    // Compute diffuse term
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
    
    o.color = ambient + diffuse;
    
    return o;
}

fixed4 frag(v2f i) : SV_Target {
    return fixed4(i.color, 1.0);
}
1.saturate

为防止点积的结果出现负值,我们需要使用max操作,而Cg提供了这样的函数。在本例中使用Cg的另一个函数可以达到同样的目的,即saturate函数。

  • 函数:saturate(x)
  • 参数:x:为用于操作的标量或矢量,可以是float、float2、float3等类型。
  • 描述:把x截取在[0,1]的范围内,如果x是一个矢量,那么会对它的每一个分量进行这样的操作。
2._Diffuse ("Diffuse", Color) = (1, 1, 1, 1)

使用Properties语义声明一个Color类型的属性,来控制材质的漫反射颜色。

3.Tags{"LightMode"="ForwardBase"}

LightMode标签是Pass标签中的一种,它用于定义该Pass在Unity的光照流水线中的角色,在后面我们会更加详细的解释它。在这里我们只需要知道,只有定义了正确的LightMode,我们才能得到一些Unity内置光照变量,例如下面讲到的_LightColor0。

4.#include "Lighting.cginc"

为了使用Unity内置的一些变量,如后面讲到的_LightColor0,还需要包含进Unity的内置文件Lighting.cginc

5.计算漫反射

在前面的步骤中,我们已经知道了材质的漫反射颜色_Diffuse以及顶点法线v.normal。我们还需要知道光源的颜色和强度信息以及光源方向。

Unity为我们提供了一个内置变量_LightColor0来访问该Pass处理的光源的颜色和强度信息(注意,想要得到正确的值需要定义合适的LightModel标签),而光源方向可以由_WorldSpaceLightPos0来得到。需要注意的是,这里对光源方向的计算并不具有通用性。在本节中,我们假设场景只有一个光源且该光源的类型是平行光。但如果场景中有多个光源并且类型可能是点光源等其它类型,直接使用_WorldSpaceLightPos0就不能得到正确的结果,我们将在6.6节学习如何使用内置函数来处理更复杂的光源类型。

在计算法线和光源方向之间的点积时,我们需要选择它们所在的坐标系,只有两者处于同一坐标空间下,它们的点积才有意义。在这里,我们选择了世界坐标空间。而由a2v得到的顶点法线是位于模型空间下的,因此我们首先需要把法线转换到世界空间中。

在以前,我们已经知道可以使用顶点变换矩阵的逆转置矩阵对法线进行相同的变换,因此我们首先得到模型空间到世界空间的变换矩阵的逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法。由于法线是一个三维矢量,因此我们只需要截取_World2Object的前三行前三列即可。

在得到了世界空间中的法线和光源方向后,我们需要对它们进行归一化操作。

在得到它们的点积结果后,我们要防止这个结果为负值。为此,我们使用了saturate函数。saturate函数是Cg提供的一种函数,它的作用是可以把函数截取到[0,1]的范围。

最后再与光源的颜色和强度以及材质的漫反射颜色相乘即可得到最终的漫反射光照部分。

最后,我们对环境光和漫反射光部分相加,得到最终的光照结果。

6.问题

基于顶点的着色(Gouraud shading)处理时,并没有看见重心插值得到片元颜色这部分逻辑的代码呀。其实,在第二章中已经讲过,在到达片元着色器之前,插值在三角形遍历那个过程中就会处理:


图2.6 GPU的渲染流水线实现。颜色表示了不同阶段的可配置性或可编程性:绿色表示该流水 线阶段是完全可编程控制的,黄色表示该流水线阶段可以配置但不是可编程的,蓝色表示该流 水线阶段是由GPU固定实现的,开发者没有任何控制权。实线表示该shader必须由开发者编 程实现,虚线表示该Shader是可选的

三角形遍历(Triangle Traversal)阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)。

三角形遍历会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。

结论:Barycentric interpolation重心插值,就在三角形遍历 (Triangle Traversal) 这一步用到,得到了每一个片元的信息

7.mul

在第五章第3节,曾经讲过mul函数,有一个我不懂怎么推导但我记住了结论的写法:mul(M,v) == mul(v,tranpose(M))

大部分情况下,都是把内置的变换矩阵比如UNITY_MATRIX_MVP放mul参数的左边,比如o.pos = mul(UNITY_MATRIX_MVP, v.vertex);。但是如果有逆矩阵,也可以当作转置矩阵用的时候,就可以放mul参数的右边。比如上面就是这样写的:fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

其实就是把法线从模型空间转到世界空间呀,如果能直接找到这个内置的矩阵变换,就可以直接放mul参数的左边了。参考如何在Unity中分别实现Flat Shading(平面着色)、Gouraud Shading(高洛德着色)、Phong Shading(冯氏着色),在UnityShaderVariables.cginc找到了这个内置矩阵:

#define UNITY_MATRIX_M unity_ObjectToWorld
    float4x4 unity_ObjectToWorld;
    float4x4 unity_WorldToObject;

参考Gouraud shading与Phong shading的区别(原理概念+shader代码),在UnityCg.cginc中找到了一个帮助函数

// Transforms normal from object to world space
inline float3 UnityObjectToWorldNormal( in float3 norm )
{
#ifdef UNITY_ASSUME_UNIFORM_SCALING
    return UnityObjectToWorldDir(norm);
#else
    // mul(IT_M, norm) => mul(norm, I_M) => {dot(norm, I_M.col0), dot(norm, I_M.col1), dot(norm, I_M.col2)}
    return normalize(mul(norm, (float3x3)unity_WorldToObject));
#endif
}

经过测试,这样写都是可以的:

// Transform the normal from object space to world space
//fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
//fixed3 worldNormal = mul((float3x3)unity_ObjectToWorld, v.normal);
//fixed3 worldNormal = normalize(mul((float3x3)UNITY_MATRIX_M, v.normal));
//fixed3 worldNormal = normalize(mul(UNITY_MATRIX_M, v.normal));
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);

这里把float3x3去掉,居然也是对的。那mul左边参数传入的是float4x4类型,右边的float3 normal : NORMAL;居然没报错,推测是mul内部做了重载。

8.saturate和max的区别

还有个问题是,在上面参考两个链接中,用的并不是saturate,而是max,比如:

float lightPor = max(0, dot(worldNor, lightDir));

然后我又搜索了一下,saturate和max的区别,参考【Unity3D Shader编程】之六 暗黑城堡篇: 表面着色器(Surface Shader)的写法(一)
saturate代码实现大致如下:

float saturate(float x)
{
    return max(0,min(1, x));
}
五、Phong Shading 逐像素着色

对于细分程度较高的模型,逐顶点光照已经可以得到比较好的光照效果了。但对于一些细分程度较低的模型,逐顶点光照就会出现一些细节问题,就如上面的图片我们看到胶囊体的背光面与向光面交界处有一些锯齿。为了解决这些问题,我们可以使用逐像素的漫反射光照。

根据我对第二章的理解,Gouraud Shading在顶点着色器计算颜色,然后GPU在三角形遍历时做重心插值得到片元中的颜色;Phong Shading在顶点着色器计算顶点法线,然后GPU在三角形遍历时对顶点法线做插值,我们在片元着色器中使用插值后的顶点法线,重新计算颜色。

Chapter6-DiffusePixelLevel.shader部分代码参考,和上面很类似的逻辑,不再解释。

v2f vert(a2v v) {
    v2f o;
    // Transform the vertex from object space to projection space
    o.pos = UnityObjectToClipPos(v.vertex);

    // Transform the normal from object space to world space
    o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);

    return o;
}

fixed4 frag(v2f i) : SV_Target {
    // Get ambient term
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    
    // Get the normal in world space
    fixed3 worldNormal = normalize(i.worldNormal);
    // Get the light direction in world space
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    
    // Compute diffuse term
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
    
    fixed3 color = ambient + diffuse;
    
    return fixed4(color, 1.0);
}
六、半兰伯特(Half Lambert)光照模型

逐像素光照可以得到更加平滑的光照效果。但是即便使用了逐像素,漫反射光照,有一个问题仍然存在。在光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模型背光区域看起来就像一个平面一样,失去了模型细节表现。实际上我们可以通过添加环境光来得到非全黑的效果,但即便这样让然无法解决背光面明暗一样的缺点。为此,有一种改善技术被提了出来,这就是半兰伯特(Half Lambert)光照模型。

哎,这个Half Lambert模型,闫令琪的课,好像没讲过呀。看冯乐乐讲的吧

在2.1小结中,我们使用的漫反射光照模型也被称为兰伯特光照模型,因为它符合兰伯特定律——在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比。为了改变上小结中提出的问题,Valve公司在开发游戏《半条命》时提出了一种技术,由于该技术是在原兰伯特光照模型的基础上进行了一个简单的修改,因此被称为半兰伯特光照模型。

广义的半兰伯特光照模型的公式

可以看出,与原兰伯特模型相比,半兰伯特光照模型没有使用max操作来防止n和l的点积为负值,而是对其结果进行了一个α倍的缩放再加上一个β大小的偏移。绝大多数情况下,α和β的值均为0.5。

通过这样的方式,我们可以把n·l的结果范围从[-1,1]映射到[0,1]的范围内。也就是说,对于模型的背光面,在原版兰伯特光照模型中点积结果将映射到同一个值,即0值处;而在半兰伯特模型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。


转到背光面,就能看出区别了

需要注意的是,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。

代码改动很小:

// Compute diffuse term
fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
七、在Unity Shader中实现高光反射光照模型
1.公式回顾

参考图形学笔记六 Shading 渲染管线,Blinn-Phong模型做了一个很聪明的事情,观察方向和镜面反射方向足够接近时,意味着法线方向和半程向量接近。所谓半程向量,就是l和v的角平分线方向。这个向量很好求出,直接向量加法,再归一化即可。如下图:

image.png

现在计算就变得很简单了,要判断n和h足够接近,只要用点乘即可。

  • 公式中的ks是镜面反射系数,这里没有像漫反射那样考虑能量吸收系数,这是因为这个模型将其简化掉了。
  • 为什么要用半程向量判断,而不是直接判断R和v的角度呢。其实这样做就是Phong模型。Blinn-Phong模型相当于是一种改进,计算半程向量比计算反射向量R要容易许多。
  • 公式右上角有个小写字母p,是做什么的?向量夹角的余弦确实能体现两个向量是否足够接近,但是容忍度太高了。如果直接使用的话,就会生成一个范围超级大的高光,这样就不太合理。实现生活中看到的高光,应该是非常亮,集中在一个很小的区域。所以应该是两个向量离得稍微远一点,就算它们离开高光点了。所以就用了上图最右边的曲线,加了64次方。在实际应用中,p的值一般在100到200之间,大概就是3到5度之外,就看不到高光了。

冯乐乐在6.2.4中也给出了公式,我们可以对照闫令琪讲的知识,互相理解。


image.png

如图,我把冯乐乐给出的图,标出了闫令琪给出的公式中的α,也就是使用向量夹角的余弦来判断观察方向和镜面反射方向是否足够接近。


image.png
  • 其中mgloss是材质的光泽度(gloss),也被称为反光度(shininess)。它用于控制高光区域的“亮点”有多宽,mgloss越大,亮点就越小。我们可以理解为这就是闫令琪给出的公式中的右上角小写字母p。
  • mspecular是材质的高光反射颜色,它用于控制该材质对于高光反射的强度和颜色,可以理解为闫令琪给出公式中的Ks
  • Clight则是光源的颜色和强度
  • 同样这里也需要防止v·r为负数。

当然这里的r可以通过平形四边形的向量加法计算出来,即l+r=2n,这个n的方向就是法线方向,长度需要l投影过来,就是点乘:


image.png

然后Blinn Phone就用半程向量了:


image.png

image.png

这部分的公式和闫令琪给出的公式完全一致。

在硬件实现时,如果摄像机和光源距离模型足够远的话,Blinn模型就会快于Phong模型,这是因为此时可以认为v和l都是定值,因此h将是一个常量,但是,当v或者l不是定值时,Phong模型可能反而更快一些。需要注意的是,这两种模型都是经验模型,也就是说,我们不应该认为Blinn模型是对“正确的”Phong模型的近似。实际上,一些情况下,Blinn模型更符合实验结果。

2.CG的reflect函数计算反射方向r

函数:reflect(i,n)
参数:i,入射方向;n,法线方向。可以是float、float2、float3等类型。
描述:当给定入射方向i和法线方向n时,reflect函数可以返回反射方向。下图给出了参数和返回值之间的关系。


image.png

注意这个函数用的向量方向和上面公式中有差异

八、高光反射 逐顶点

源代码在Scene_6_5场景中,和之前步骤一样的就不说了,主要看Chapter6-SpecularVertexLevel.shader。

1.Properties
    Properties {
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
        _Specular ("Specular", Color) = (1, 1, 1, 1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
  • _Specular用于控制材质的高光反射颜色
  • _Gloss用于控制高光区域的大小,这里按照闫令琪说的,实际一般是100到200,可以自己调调看
2.高光部分代码
// Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);

// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);

o.color = ambient + diffuse + specular;

首先就是上面讲的reflect用的入射光向量方向,需要取反。

然后我们通过_WorldSpaceCameraPos得到了世界空间中的摄像机的位置,再把顶点位置从模型空间变换到世界空间下,再通过和_WorldSpaceCameraPos相减即可得到世界空间下的视角方向

3.总结

使用逐顶点的方法得到的高光效果有比较大问题,我们可以在上图中看出高光部分明显不平滑。这主要是因为,高光反射部分的计算是非线性的,而在顶点着色器中计算光照再进行插值的过程是线性的,破坏了原计算的非线性关系,就会出现较大的视觉问题。因此,我们就需要使用逐像素的方法来计算高光反射。

九、高光反射 逐像素

参考Chapter6-SpecularPixelLevel.shader。

这个和之前漫反射从顶点改到片元的例子很类似,差别在于高光反射需要知道viewDir 。在世界坐标系中,viewDir是用摄像机位置减当前位置,上面的高光反射逐顶点就是这么计算的:

fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);

但是现在逐像素的话,每个像素的位置是从顶点插值过来的。所以一个简单的思路就是,在顶点着色器中,先把顶点坐标转化到世界坐标系中,这样在片元着色器拿到的插值坐标就是世界坐标系的了,然后就能直接计算viewDir。所以输出结构体v2f除了法线,还要加一个世界坐标系的顶点坐标:

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };
            
            v2f vert(a2v v) {
                v2f o;
                // Transform the vertex from object space to projection space
                o.pos = UnityObjectToClipPos(v.vertex);
                
                // Transform the normal from object space to world space
                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                // Transform the vertex from object spacet to world space
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                
                return o;
            }

因为多了一个输出坐标,所以第一次用到TEXCOORD1语义。这个之前介绍过,再简单回顾一下:

https://qxsoftware.github.io/Unity-Shader-Semantics.html
Vertex Shader Output Semantics / Fragment Shader Input Semantics
TEXCOORD0~N 高精度(float4) 用来保存高精度数据,例如纹理坐标,位置坐标等

十、高光反射 Blinn-Phong光照模型

算半程向量的方法上面已经说了,具体代码参考Chapter6-BlinnPhong.shader

// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// Get the half direction in world space
fixed3 halfDir = normalize(worldLightDir + viewDir);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
十一、对比效果
本图使用的_Gloss全是默认值20

可以看出,Blinn-Phong光照模型的高光反射部分看起来更大、更亮一些。在实际渲染中,绝大多数情况我们都会选择Blinn-Phong光照模型。需要再次提醒的是,这两种光照模型都是经验模型,也就是说我们不应该认为Blinn-Phong模型是对“正确的”Phong模型的近似。实际上,在一些情况下,Blinn-Phong模型更符合实验结果。

十二、召唤神龙:使用Unity内置的函数

其实在上面参考其它链接时,已经使用过UnityObjectToWorldNormal,在这一节冯乐乐介绍了关于光源的内置函数。

读者可以发现,计算光照模型的时候,我们往往需要得到光源方向、视角方向这两个基本信息。在上面的例子中,我们都是自行在代码里面计算的,例如使用normalize(_WorldSpaceLightPos0.xyz)来得到光源方向(这种方法只适用于平行光),使用normalize(_WorldSpaceCameraPos.xyz-i.worldPosition.xyz)来得到视角方向。但如果要处理更复杂的光照类型,如点光源和聚光灯,我们计算光源的方法就是错误的。这需要我们在代码中先判断光源类型,在计算它的光源信息。具体方法会在9.2节讲到。

手动计算这些光源信息的过程相对比较麻烦(但并不意味着你不需要了解它们的原理)。幸运的是,Unity提供了一些内置函数帮我们计算这些信息。

例如WorldSpaceViewDir函数实现如下:

//Compute world space view direction ,from object space position
inline float3 UnityWorldSpaceViewDir(in float3 worldPos)
{
return _WorldSpaceCamearPos.xyz-worldpos;
}

可以看出,这与之前计算视角的方向一致。需要注意的是,这些函数都没有保证得到的方向矢量是单位矢量,因此,我们需要在使用前把它们归一化。

1.常用帮助函数
  • float3 WorldSpaceViewDir(float4 v)         输入一个模型顶点坐标,得到世界空间中从该点到摄像机的观察方向
  • float3 ObjSpaceViewDir(float4 v)          输入一个模型顶点坐标,得到模型空间中从该点到摄像机的观察方向
  • float3 WorldSpaceLightDir(float4 v)        输入一个模型顶点坐标,得到世界空间中从该点到光源的光照方向(方向没有归一化,且只可用于前向渲染)
  • float3 ObjSpaceLightDir(float4 v)          输入一个模型顶点坐标,得到模型空间中从该点到光源的光照方向(方向没有归一化,且只可用于前向渲染)
  • float3 UnityObjectToWorldNormal(float3 norm)  将法线从模型空间转换到世界空间
  • float3 UnityObjectToWorldDir(in float3 dir)     把方向矢量从模型空间转换到世界空间
  • float3 UnityWorldToObjectDir(float3 dir)      把方向矢量从世界空间转换到模型空间

WorldSpaceLightDir、UnityWorldSpaceLightDir和ObjSpaceLightDir,稍微复杂一些,这是因为Unity帮我们处理了不同种类光源的情况。需要注意的是,这三个函数仅可用于前向渲染(关于什么是前向渲染会在9.1节讲到)。这是因为只有在前向渲染时,这三个函数里使用的内置变量_WorldSpaceLightPos0才会被正确赋值。关于哪些内置变量只会在前向渲染中被正确赋值,可以参见9.1.1节。

2.实例

参考Chapter6-BlinnPhongUseBuildInFunctions.shader

fixed4 frag(v2f i) : SV_Target {
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    
    fixed3 worldNormal = normalize(i.worldNormal);
    //  Use the build-in funtion to compute the light direction in world space
    // Remember to normalize the result
    //fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
    
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
    
    // Use the build-in funtion to compute the view direction in world space
    // Remember to normalize the result
    //fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
    fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
    fixed3 halfDir = normalize(worldLightDir + viewDir);
    fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
    
    return fixed4(ambient + diffuse + specular, 1.0);
}

你可能感兴趣的:(UnityShader精要笔记五 基础光照(漫反射+高光反射))