漫反射效果
漫反射
是指投射在粗糙表面上的光向各个方向反射的现象。当一束平行的入射光线射到粗糙的表面时,表面会把光线向着四面八方反射,所以入射线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同的方向无规则地反射,这种反射称之为“漫反射”或“漫射”。这种反射的光称为漫射光。很多物体,如植物、墙壁、衣服等,其表面粗看起来似乎是平滑,但用放大镜仔细观察,就会看到其表面是凹凸不平的,所以本来是平行的太阳光被这些表面反射后,弥漫地射向不同方向。
实现原理
漫反射光照是用于对那些被物体表面随机散射到各个方向的辐射度进行建模的。在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。但是,入射光线的角度很重要。漫反射光照符合兰伯特定律(Lambert's law): 反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。因此,漫反射部分的计算如下:
Cdiffuse=(Clight · mdiffuse)max(0, n · I )其中, n 是表面法线, I 是指向光源的单位矢量, mdiffuse 是材质的漫反射颜色, clight 是光源颜色。需要注意的是,我们需要防止法线和光源方向点乘的结果为负值,为此,我们使用取最大值的函数来将其截取到 0, 这可以防止物体被从后面来的光源照亮。
逐顶点计算着色shader
我们在shader中需要计算输出的颜色,逐顶点着色也就是说我们的计算主要放在了vertex shader中,根据顶点来计算,每个顶点中计算出了该点的颜色,直接作为vertex shader的输出,pixel(fragment) shader的输入,当到达pixel阶段时,直接输出顶点shader的结果。比如一个三角形面片,在vertex阶段,分别计算了每个顶点的颜色值,在pixel阶段时,这个面片经过投影,最终显示在屏幕上的像素,会根据该像素周围的顶点来插值计算像素的最终颜色,这种着色方式也叫做 高洛德着色 。下面看一下unity shader实现的逐顶点着色:
Shader "Diffuse/Diffuse Vertex-Level" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1) // 控制材质的漫反射颜色
}
SubShader {
Pass {
// LightMode 标签是Pass 标签中的一种,它用于定义该Pass 在Unity 的光照流水线中的角色
//只有定义了正确的LightMode,我们才能得到一些Unity 的内置光照变量,例如下面的_LightColor0
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//为了使用Unity 内置的一些变量,如后面要讲到的_LightColor0,还需要包含进Unity 的内置文件Lighting.cginc
#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;
//将顶点从模型空间转换到投影空间
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 把法线从模型空间转化到世界空间
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
// 得到世界空间下的光照方向
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 根据兰伯特模型计算顶点的光照信息
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);
}
ENDCG
}
}
FallBack "Diffuse"
}
以上代码详细解释:
1:我们已经重复过很多次, 顶点着色器最基本的任务就是把顶点位置从模型空间转换到裁剪空间中, 因此我们需要使用Unity 内置的 模型*世界*投影矩阵UNITY_MATRIX_MVP 来完成这样的坐标变换。
2:接下来,我们通过Unity 的内置变量 UNITY_LIGHTMODEL_AMBIENT 得到了环境光部分。
3:然后, 就是真正计算漫反射光照的部分。为了计算漫反射光照我们需要知道4 个参数。在前面的步骤中, 我们已经知道了材质的漫反射颜色Diffuse 以及顶点法线v.normal。我们还需要知道光源的颜色和强度信息以及光源方向。
4:Unity 提供给我们一个内置变量 _LightColor0 来访问该Pass 处理的光源的颜色和强度信息(注意,想要得到正确的值需要定义合适的LightMode标签〉,而光源方向可以由 _WorldSpaceLightPos0 来得到。需要注意的是,这里对光源方向的计算并不具有通用性。在本节中, 我们假设场景中只有一个光源且该光源的类型是平行光。但如果场景中有多个光源并且类型可能是点光源等其他类型, 直接使用 _WorldSpaceLightPos0 就不能得到正确的结果。
5:在计算法线和光源方向之间的点积时,我们需要选择它们所在的坐标系, 只有两者处于同一坐标空间下,它们的点积才有意义。在这里, 我们选择了世界坐标空间。而由a2v 得到的顶点法线是位于模型空间下的, 因此我们首先需要把法线转换到世界空间中。我们已经知道可以使用顶点变换矩阵的逆转置矩阵对法线进行相同的变换,因此我们首先得到模型空间到世界空间的变换矩阵的逆矩阵 _World20bject,然后通过调换它在mul 函数中的位置,得到和转置矩阵相同的矩阵乘法。由于法线是一个三维矢量,因此我们只需要截取 _World2Object 的前三行前三列即可。
6:在得到了世界空间中的法线和光源方向后,我们需要对它们进行归一化操作。在得到它们点积的结果后,我们需要防止这个结果为负值。为此,我们使用了saturate 函数。saturate 函数是CG提供的一种函数,它的作用是可以把参数截取到[0, 1]的范围内。最后,再与光源的颜色和强度以及材质的漫反射颜色相乘即可得到最终的漫反射光照部分。
7:最后,我们对环境光和漫反射光部分相加,得到最终的光照结果。
逐像素计算着色shader
Shader "Diffuse/Diffuse Pixel-Level" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1) // 控制材质的漫反射颜色
}
SubShader {
Pass {
// LightMode 标签是Pass 标签中的一种,它用于定义该Pass 在Unity 的光照流水线中的角色
//只有定义了正确的LightMode,我们才能得到一些Unity 的内置光照变量,例如下面的_LightColor0
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//为了使用Unity 内置的一些变量,如后面要讲到的_LightColor0,还需要包含进Unity 的内置文件Lighting.cginc
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
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);
}
ENDCG
}
}
FallBack "Diffuse"
}
从vertex阶段到fragment阶段发生了什么
半兰伯特光照模型
广义的半兰伯特光照模型的公式如下:
Cdiffuse=(Clight · mdiffuse)(α (n· I)+ β)
可以看出, 与原兰伯特模型相比,半兰伯特光照模型没有使用max操作来防止n 和 I 的点积为负值,而是对其结果进行了一个α倍的缩放再加上一个β大小的偏移。绝大多数情况下, α和β 的值均为0.5 ,即公式为:
通过这样的方式,我们可以把n·I 的结果范围从[-1, 1 ]映射到[0, 1 ]范围内。也就是说,对于模型的背光面,在原兰伯特光照模型中点积结果将映射到同一个值,即0 值处;而在半兰伯特模型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。
需要注意的是,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。
Shader "Diffuse/Half Lambert" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1) // 控制材质的漫反射颜色
}
SubShader {
Pass {
// LightMode 标签是Pass 标签中的一种,它用于定义该Pass 在Unity 的光照流水线中的角色
//只有定义了正确的LightMode,我们才能得到一些Unity 的内置光照变量,例如下面的_LightColor0
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//为了使用Unity 内置的一些变量,如后面要讲到的_LightColor0,还需要包含进Unity 的内置文件Lighting.cginc
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
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
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);
}
ENDCG
}
}
FallBack "Diffuse"
}
我们用一个人物模型,分别使用两种shader,进行一下对比,左侧的shader主要计算在vertex,右侧的shader主要计算放在pixel: