一、Diffuse Reflection
漫反射(Diffuse Reflection),又称Lambert反射,是投射在粗糙表面上的光向各个方向反射的现象。当一束平行的入射光线射到粗糙的表面时,表面会把光线向着四面八方反射,所以入射线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同的方向无规则地反射。
在生活中,我们看到的月球的光近乎完全是漫反射。粉笔或者磨砂纸也是漫反射。事实上,任何表面的漫反射看起来都是类似于磨砂表面的效果。
而在完美的漫反射的情况下,所观察到的反射光的强度取决于在表面法线矢量和入射光的光线之间的角度的余弦。如图所示,经过归一化的物体表面法线向量N与物体表面正交,光源照射到物体表面的光线方向为L。
也就是说,我们可以使用表面的法线矢量N和入射光方向矢量L,计算漫反射。
想要计算出眼睛观察到的漫反射光Idiffuse,需要计算归一化的表面法向量N和归一化的方向光源向量L之间夹角的余弦值,即点积运算N·L。因为任何两个向量a和b的点积运算可以表示为:
而对于已经经过归一化的向量,|a| 和|b|都是1。
如果点积运算N·L为负,那么光源方向就是在表面内部照射过来的,这是错误的。这种情况下,我们就将反射光设为0即可。这可以通过代码max(0, N·L)来实现,这样就确保了在点积结果为负时,得到的结果为0。此外,漫反射光还取决于入射光light Iincoming和材质的漫反射系数Kdiffuse。而对于一个黑色的表面,材质漫反射系数 为0,而对于白色的表面,材质的漫反射强度Kdiffuse就为1.
根据如上的表述,漫反射强度的方程如下:
ps:此公式适用于任何单一的颜色分量(如红、绿、蓝),也适用于颜色分量混合颜色的入射光。
二、实现
核心代码可以在顶点着色器中实现,也可以在片段着色函数中实现。
需在世界空间中进行实现,因为世界空间中Unity提供了光源方向。
关于如何获取参数,总结如下:
Kdiffuse通过属性properties中指定后传进来。
世界空间的光源方向由unity内置变量_WorldSpaceLightPos0给出。
光源颜色Iincoming由unity内置变量_LightColor0给出。
环境光颜色通过内置变量UNITY_LIGHTMODEL_AMBIENT给出。
用Tags {"LightMode" = "ForwardBase"}确保上述内置变量的值处于正确的状态。
世界空间下的物体表面的法线向量,可以通过输出参数语义NORMAL来获得物体空间下的表面的法线向量,然后将此向量从物体空间转到世界空间中获得。
2.1 单色可调的漫反射光照Shader
源码如下:
Shader "Shader/Diffuse(Lambert) Shader"
{
//------------------------------------【属性值】------------------------------------
Properties
{
//颜色值
_Color("Main Color", Color) = (1, 1, 1, 1)
}
//------------------------------------【唯一的子着色器】------------------------------------
SubShader
{
//渲染类型设置:不透明
Tags{ "RenderType" = "Opaque" }
//设置光照模式:ForwardBase
Tags{ "LightingMode" = "ForwardBase" }
//细节层次设为:200
LOD 200
//--------------------------------唯一的通道-------------------------------
Pass
{
//===========开启CG着色器语言编写模块===========
CGPROGRAM
//编译指令:告知编译器顶点和片段着色函数的名称
#pragma vertex vert
#pragma fragment frag
//包含头文件
#include "UnityCG.cginc"
//顶点着色器输入结构
struct appdata
{
float4 vertex : POSITION;//顶点位置
float3 normal : NORMAL;//法线向量坐标
};
//顶点着色器输出结构
struct v2f
{
float4 position : SV_POSITION;//像素位置
float3 normal : NORMAL;//法线向量坐标
};
//变量声明
float4 _LightColor0;
float4 _Color;
//--------------------------------【顶点着色函数】-----------------------------
// 输入:顶点输入结构体
// 输出:顶点输出结构体
//---------------------------------------------------------------------------------
v2f vert(appdata input)
{
//【1】声明一个输出结构对象
v2f output;
//【2】填充此输出结构
//输出的顶点位置为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
output.position = mul(UNITY_MATRIX_MVP, input.vertex);
//获取顶点在世界空间中的法线向量坐标
output.normal = mul(float4(input.normal, 0.0), _World2Object).xyz;
//【3】返回此输出结构对象
return output;
}
//--------------------------------【片段着色函数】-----------------------------
// 输入:顶点输出结构体
// 输出:float4型的像素颜色值
//---------------------------------------------------------------------------------
fixed4 frag(v2f input) : COLOR
{
//【1】先准备好需要的参数
//获取法线的方向
float3 normalDirection = normalize(input.normal);
//获取入射光线的值与方向
float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
//【2】计算出漫反射颜色值 Diffuse=LightColor * MainColor * max(0,dot(N,L))
float3 diffuse = _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection));
//【3】合并漫反射颜色值与环境光颜色值
float4 DiffuseAmbient = float4(diffuse, 1.0) + UNITY_LIGHTMODEL_AMBIENT;
//【4】将漫反射-环境光颜色值乘上纹理颜色,并返回
return DiffuseAmbient;
}
//===========结束CG着色器语言编写模块===========
ENDCG
}
}
}
2.2 可调颜色和自定义纹理的漫反射光照Shader
在Properties属性中加上一项纹理,然后在最终的漫反射颜色计算完成之后,乘上纹理即可。
源码如下:
Shader "Shader/Diffuse(Lambert) Shader with Texture"
{
//------------------------------------【属性值】------------------------------------
Properties
{
//主纹理
_MainTex("Texture", 2D) = "white"{}
//主颜色值
_Color("Main Color", Color) = (1, 1, 1, 1)
}
//------------------------------------【唯一的子着色器】------------------------------------
SubShader
{
//渲染类型设置:不透明
Tags{ "RenderType" = "Opaque" }
//设置光照模式:ForwardBase
Tags{ "LightingMode" = "ForwardBase" }
//细节层次设为:200
LOD 200
//--------------------------------唯一的通道-------------------------------
Pass
{
//===========开启CG着色器语言编写模块===========
CGPROGRAM
//编译指令:告知编译器顶点和片段着色函数的名称
#pragma vertex vert
#pragma fragment frag
//包含头文件
#include "UnityCG.cginc"
//顶点着色器输入结构
struct appdata
{
float4 vertex : POSITION;//顶点位置
float3 normal : NORMAL;//法线向量坐标
float2 texcoord : TEXCOORD0;//纹理坐标
};
//顶点着色器输出结构
struct v2f
{
float4 positon : SV_POSITION;//像素位置
float3 normal : NORMAL;//法线向量坐标
float2 texcoord : TEXCOORD0;//纹理坐标
};
//变量声明
float4 _LightColor0;
float4 _Color;
sampler2D _MainTex;
//--------------------------------【顶点着色函数】-----------------------------
// 输入:顶点输入结构体
// 输出:顶点输出结构体
//---------------------------------------------------------------------------------
v2f vert(appdata input)
{
//【1】声明一个输出结构对象
v2f output;
//【2】填充此输出结构
//输出的顶点位置为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
output.positon = mul(UNITY_MATRIX_MVP, input.vertex);
//获取顶点在世界空间中的法线向量坐标
output.normal = mul(float4(input.normal, 0.0), _World2Object).xyz;
//输出的纹理坐标也就是输入的纹理坐标
output.texcoord = input.texcoord;
//【3】返回此输出结构对象
return output;
}
//--------------------------------【片段着色函数】-----------------------------
// 输入:顶点输出结构体
// 输出:float4型的像素颜色值
//---------------------------------------------------------------------------------
fixed4 frag(v2f input) : COLOR
{
//【1】先准备好需要的参数
//获取纹理颜色
float4 texColor = tex2D(_MainTex, input.texcoord);
//获取法线的方向
float3 normalDirection = normalize(input.normal);
//获取入射光线的值与方向
float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
//【2】计算出漫反射颜色值 Diffuse=LightColor * MainColor * max(0,dot(N,L))
float3 diffuse = _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection));
//【3】合并漫反射颜色值与环境光颜色值
float4 DiffuseAmbient = float4(diffuse, 1.0) + UNITY_LIGHTMODEL_AMBIENT;
//【4】将漫反射-环境光颜色值乘上纹理颜色,并返回
return DiffuseAmbient* texColor;
}
//===========结束CG着色器语言编写模块===========
ENDCG
}
}
}