光源
我们可以使用光源方向L和表面法线n 之间的夹角的余弦值来得到。需要注意的是,这里默认方向矢量的模都为1 。图6.1 显示了使用余弦值来计算的原因。
因为辐照度是和照射到物体表面时光线之间的距离d/cosθ反比的, 因此辐照度就和cosθ成正比。cosθ 可以使用光源方向L 和表面法线n 的点积来得到。这就是使用点积来计算辐照度的由来。
光照吸收和散射 (就是漫反射和高光反射)
在计算高光或者漫反射时 有个一值出射度(exitance) 来描述散射强度 有公式可以套用。
着色
着色( shading )指的是,根据材质属性(如漫反射属性等〉、光源信息(如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度的过程。我们也把这个等式称为光照模型(Lighting Model)。
BRDF (Bidirectional Reflectance Distribution Function,双面反射分布函数) 光照模型, 看起来对它就是对的
标准光照模型
标准光照模型只关心直接光照(direct light),也就是那些直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光线。
- 自发光( emissive ) 部分,本书使用Cemissive 来表示。这个部分用于描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。需要注意的是,如果没有使用全局光照( global illumination)技术,这些自发光的表面并不会真的照亮周围的物体,而是它本身看起来更亮了而己。
- 高光反射(specular ) 部分,本书使用Cspecuar 来表示。这个部分用于描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。
- 漫反射(diffuse ) 部分,本书使用 Cdiffuse来表示。 这个部分用于描述, 当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量。
- 环境光( ambient ) 部分,本书使用Cambient 来表示。它用于描述其他所有的间接光照。
环境光
在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。环境光的计算非常简单,它通常是一个全局变量,即场景中的所有物体都使用这个环境光。下面的等式给出了计算环境光的部分:
cambient= gambient
自发光
光线也可以直接由光源发射进入摄像机,而不需要经过任何物体的反射。标准光照模型使用自发光来计算这个部分的贡献度。它的计算也很简单,就是直接使用了该材质的自发光颜色:
Cemissive=memissive
通常在实时渲染中,自发光的表面往往并不会照亮周围的表面,也就是说,这个物体并不会被当成一个光源。Unity 5 引入的全新的全局光照系统则可以模拟这类自发光物体对周围物体的影响,我们会在第18 章中看到。
漫反射
在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。但是,入射光线的角度很重要。
漫反射光照符合兰伯特定律(Lambert’s law ): 反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。因此,漫反射部分的计算如下:
其中,** n 是表面法线, I 是指向光源的单位矢量, mdiffuse 是材质的漫反射颜色, clight是光源颜色。需要注意的是,我们需要防止法线和光源方向点乘的结果为负值,为此,我们使用取最大值的函数来将其截取到0 ,** 这可以防止物体被从后面来的光源照亮。
高光反射
这里的高光反射是一种经验模型,也就是说,它并不完全符合真实世界中的高光反射现象。
它可用于计算那些沿着完全镜面反射方向被反射的光线,这可以让物体看起来是有光泽的,例如金属材质。
计算高光反射需要知道的信息比较多,如表面法线、视角方向、光源方向、反射方向等。在本节中,我们假设这些矢量都是单位矢量。图6.3 给出了这些方向矢量。
我们实际上只需要知道其中3个矢量即可,而第四个矢量一一反射方向可以通过其他信息计算得到:
其中, mgloss 是材质的光泽度( gloss ),也被称为反光度( shininess )。它用于控制高光区域的“亮点”有多宽, mgloss 越大,亮点就越小。mspecuar 是材质的高光反射颜色, 它用于控制该材质对于高光反射的强度和颜色。Clight 则是光源的颜色和强度。同样,这里也需要防止v·r 的结果为负数。
在硬件实现时, 如果摄像机和光源距离模型足够远的话, Blinn 模型会快于Phong 模型,这是因为, 此时可以认为v 和 I 都是定值, 因此h 将是一个常量。但是,当v 或者I 不是定值时, Phong 模型可能反而更快一些。需要注意的是,这两种光照模型都是经验模型,也就是说,我们不应该认为Blinn 模型是对“正确的" Phong 模型的近似。实际上,在一些情况下, Blinn 模型更符合实验结果
逐像素还是逐顶点
在片元着色器中计算,也被称为逐像素光照( per-pixel lighting ): 在顶点着色器中计算,也被称为逐顶点光照( per-vertex Iighting ) 。
在逐像素光照中,我们会以每个像素为基础,得到它的法线(可以是对顶点法线插值得到的,也可以是从法线纹理中采样得到的〉,然后进行光照模型的计算。这种在面片之间对顶点法线进行插值的技术被称为Phong 着色(Phong shading ), 也被称为Phong 插值或法线插值着色技术。这不同于我们之前讲到的Phong 光照模型。
与之相对的是逐顶点光照,也被称为高洛德着色( Gouraud shading )。在逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值, 最后输出成像素颜色。由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。但是,由于逐顶点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性的计算(例如计算高光反射时〉时,逐顶点光照就会出问题。在后面的章节中,我们将会看到这种情况。而且,由于逐顶点光照会在渲染图元内部对顶点颜色进行插值, 这会导致渲染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象
Unity中的环境光和自发光
在标准光照模型中,环境光和自发光的计算是最简单的。
在Unity 中,场景中的环境光可以在Window->Lighting-> Ambient Source/Ambient Color/Ambient Intensity 中控制,如图6.5 所示。在Shader 中,我们只需要通过Unity 的内置变量UNITY_LIGHTMODEL AMBIENT 就可以得到环境光的颜色和强度信息。
而大多数物体是没有自发光特性的,因此在本书绝大部分的Shader 中都没有计算自发光部分。如果要计算自发光也非常简单,我们只需要在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色上即可。
代码实现漫反射 核心部分
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, 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)_World2Object));
// 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;
}
第一行是定义 返回值,也便于后面对他进行赋值。
第二行 : Unity 内置的 模型*世界*投影矩阵 UNITY_MATRIX_MVP 来完成这样的坐标变换 顶点自身的模型坐标 转换到世界坐标系
第三行: 用内置变量 UNITY_LIGHTMODEL_AMBIENT 获得环境光部分
漫反射 计算公式 (核心算法)漫反射颜色Diffuse 、顶点法线V.normal、还要知道光源颜色 和强度信息 这些都是需要正确的LightMode标签。
第四行: 顶点法线转换到 世界坐标系 坐标转换
在计算法线和光源方向之间的点积时,我们需要选择它们所在的坐标系, 只有两者处于同一坐标空间下,它们的点积才有意义。在这里, 我们选择了世界坐标空间。而由a2v 得到的顶点法线是位于模型空间下的, 因此我们首先需要把法线转换到世界空间中。在4.7 节中,我们已经知道可以使用顶点变换矩阵的逆转置矩阵对法线进行相同的变换,因此我们首先得到模型空间到世界空间的变换矩阵的逆矩阵 _World2Object,然后通过调换它在mul 函数中的位置,得到和转置矩阵相同的矩阵乘法。由于法线是一个三维矢量,因此我们只需要截取 _World2Object 的前三行前三列即可。
在得到了世界空间中的法线和光源方向后,我们需要对它们进行归一化操作
第五行:光源方向可以用_WorldSpaceLightPos0得到
六: 漫反射 计算公式
在得到它们点积的结果后,我们需要防止这个结果为负值。为此,我们使用了saturate 函数。saturate 函数是CG提供的一种函数,它的作用是可以把参数截取到[0, 1]的范围内。
七: 环境光和 漫反射 叠加
八:返回 顶点颜色计算结果
由于所有的计算在顶点着色器中都已经完成了,因此片元着色器的代码很简单,我们只需要直接把顶点颜色输出即可 :
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
逐顶点光照、已经可以得到比较好的光照效果了。但对于一些细分程度较低的模型,逐顶点光照就会出现一些视觉问题,例如我们可以在图6.6 中看到在胶囊体的背光面与向光面交界处有一些锯齿。为了解决这些问题,我们可以使用逐像素的漫反射光照。
逐像素光照
对上面的代码进行修改
修改顶点着色器的输出结构v2f
struct v2f{
float4 pos:SV_POSITION;
float3worldNormal:TEXCOORD0;
}
顶点着色器不用再计算光照模型,只要把法线传递给片元着色就可以了。所以改上面的结构。
v2f vert(a2v v){
v2f o;
//模型的位置 从 模型空间 转换到 投影空间坐标系(摄像机的坐标系,不是世界)深度裁剪等用投影
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//把法线信息 从模型坐标系 转换到 世界坐标系 这里世界是因为计算光照等都是在世界中计算
o.worldNormal = mul(v.normal,(float3*3)_world2Object);
}
片元着色器就散漫反射光照模型
这里有一个记忆错误,一直以为片元是插值计算,计算量小于顶点,其实这个是像素级计算,效果好计算量大。其实是插值不过是 插值得到了其中的像素信息然后再计算
fixed4 frag(v2f i): SV_Target{
//获得环境光照信息 用unity内置变量
fixed3 ambient =UNITY_LIGHTMODEL_AMBIENT.xyz;
//获得世界坐标系的 当前像素的法线信息 归一化操作
fixed3 worldNormal = normalize(i.worldNormal);
//获得世界空间内的 光照方向 对坐标归一操作就是 方法
fixed3 worldLightDir = normalize(_worldSpaceLightPos0.xyz);
//套用公式进行 光照模型计算
fixed3 diffuse =_LightColor0.rgb * _Diffuse.rgb* saturate(dot(worldNormal,worldLightDir));
//光照颜色的融合,环境光直接叠加就好
fixed3 color = ambient +diffuse;
return fixed4(color, 1.0);
}
但是,即便使用了逐像素漫反射光照,有一个问题仍然存在。在光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模型的背光区域看起来就像一个平面一样,失去了模型细节表现。实际上我们可以通过添加环境光来得到非全黑的效果,但即便这样仍然无法解决背光面明暗一样的缺点。为此, 有一种改善技术被提出来,这就是半兰伯特( Half Lambert)光照模型。
半兰伯特模型
来自《半条命》(童年啊)的一种技术,对兰伯特光照模型的修改
广义的半兰伯特光照模型 公式
与原兰伯特模型相比,半兰伯特光照模型没有使用max 操作来防止n 和 I 的点积为负值,而是对其结果进行了一个α倍的缩放再加上一个β大小的偏移。绝大多数情况下, α和β的值均为0.5 ,即公式为:
我的理解就是 原来算法 有些地方不是准确我直接 加减模拟一下就看起来对了 原来的做法背面会归到0值,这里【-1,1】都有值,这样背面也会有一定的差异计算,视觉就会有一定差异。 看起来不是一个样子的。这算是一种经验公式
片元着色器代码实现,其他部分和上面的像素计算一样
fixed4 frag(v2f i) : SV_Target {
...
// Compute diffuse term
// 漫反射公式替换掉
fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}
高光反射的光照模型
感觉公式和漫反射差不多
其中的r就是入射光线的正反射角度 有cg函数可以用
CG 提供了计算反射方向的函数reflect。
函数: reflect(i, n)
参数: i , 入射方向; n ,法线方向。可以是float、float2 、float3等类型。
描述: 当给定入射方向i 和法线方向n 时, reflect 函数可以返回反射方向。图6.9 给出了参数和返回值之间的关系。
逐顶点光照
代码实现
Shader "shader/name"{
Properties{
_Diffuse ("Diffuse", Color) = (1,1,1,1)
_Specular ("Specular",Color) =(1,1,1,1) //高光反射的颜色
_Gloss("Gloss",Range(8.0,256)) =20 //高光区域大小
}
SubShader{
Pass{
//定义正确的lightMode 才能得到一些内置的光照变量
Tags{ "LightMode" = "ForwardBase"}
CGPROGRAM
// 定义 着色器名称
#pragma vertex vert
#pragma fragment frag
//引入 Unity 内置文件
#include "Lighting.cginc"
//定义 属性块中的变量 将外部输入pass 可以使用
fixed4 _Diffuse;
fixed4 _Specular; //这些存的值都是 0 到1 之间
float _Gloss; //float 是因为存储的数据变化范围大
//顶点着色器的输入结构 一般都是顶点的位置和法线信息 这里也就用这些信息计算
Struct a2v{
float4 vertex :POSITION;
float4 normal: NORMAL ;
}
//顶点着色器的输出结构 也是根据需要调整
// 不计算颜色什么是 只要位置和法线, 有些计算放到顶点 就会带颜色值
struct v2f{
float4 pos: SV_POSITION;
fixed3 color :COLOR;
}
v2f vert(a2v v){
v2f o; //定义输出结构 声明变量
//坐标从 模型坐标系 换到 投影坐标系 --摄像机坐标系
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//获得环境光照
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//法线信息从 模型转到世界空间坐标系,法线一般计算需要归一 不然没用 _World2Object 内置转换矩阵
fixed3 worldNormal = normalize(mul(v.normal, (float3*3)_World2Object));
// 光源方向 在世界坐标系中,一般方向都是需要归一 normal 才可以计算
fixed3 worldLIghtDir = normalize(_WorldSpaceLightPos0.xyz);
// 计算 散射 光照模型
fixed3 diffuse = _LightColor0.rgb* _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
//计算获得 世界坐标系的 光线完全反射的方向
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNoram));
// 获得 实现方向在空间坐标系的方向
fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - mul(_Object2World, v.vertex).xyz) ;
// Compute specular term 高光光照模型
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
o.color = ambient + diffuse + specular;
return 0;
}
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
FallBack "Specular"
}
}
}
首先计算了入射光线方向关于表面法线的反射方向reflectDir。由于CG 的reflect 函数的入射方向要求是由光源指向交点处的,因此我们需要对worldLightDir取反后再传给reflect 函数。 也就是说 worldLightDir这个方向是从顶点到光源的方向
我们通过 _WorldSpaceCameraPos 得到了世界空间中的摄像机位置,再把顶点位置从模型空间变换到世界空间下,再通过和 _WorldSpaceCameraPos 相减即可得到世界空间下的视角方向。
逐像素实现 高光反射
修改 顶点着色器输出,计算挪到片元中。 里面的计算方法都是一样类似的
struct v2f {
float4 pos :SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos: TEXCOORD1;
};
v2f vert(a2v v){
v2f o;
//模型空间到透视空间,有个特点 位置都是直接到透视,光照什么的都是在世界 方便计算
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// 顶点法线 转换到 世界坐标系
o.worldNormal = mul(v.normal, (float3*3)_World2Object);
// Use the build- in function to compute the normal in world space 内置函数算法
//o.worldNormal = UnityObjectToWorldNormal(v.normal);
// 为了在片元中计算方便 也保存一份 世界坐标的位置信息
o.worldPos = mul(_object2World, v.vertex).xyz
return o;
}
//片元 计算光照模型
fixed4 frag(v2f i): SV_Target{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; //h环境光
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normailze(_WorldSpaceLightPos0.xyz); //内置变量,空间光方向
//计算 散射 模型
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
//反射方向
fixed3 reflectDir = normalize(_WorldspaceCameraPos.xyz - i.worldPos.xyz);
//获得视角方向
fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)),_Gloss); //高光模型
return fixed4(ambient + diffuse + specular, 1.0); //最终颜色的融合,环境 ,漫反射,高光
}
Blinn-Phong 光照模型
核心算法都是类似 就是最后 高光的地址 增加了变量
fixed4 frag(v2f i) : SV_Target {
……
// 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);
return fixed4(ambient + diffuse + specular, 1.0);
}
Blinn-Phong 光照模型的高光反射部分看起来更大、更亮一些。在实际渲染中,绝大多数情况我们都会选择Blinn-Phong 光照模型。需要再次提醒的是,这两种光照模型都是经验模型,也就是说,我们不应该认为Blinn-Phong 模型是对“正确的” Phong 模型的近似。实际上,在一些情况下(详见第18 章·基于物理的渲染), Blinn-Phong 模型更符合实验结果。