初级--单一光源光照

目录

[TOC]

写在前面

本文将简述Blinn-Phong光照模型以及PBS光照模型在UnityShader中的使用。本文只涉及单一光源的光照处理,多光源光照处理请见下一篇-初级–多光源光照。全篇基于Unity2018.3.2。

在PC前的各位读者,建议下载MD文件阅读,观感更好。若有图片无法显示,请开启网络。欢迎转载,只需注明转载来源地址即可。MD文件及工程Package下载地址(提取码:4c1s)

Unity中的Workflow

1.Metallic Workflow(金属工作流)

有”Metallic”值,表示材质是否为金属性。在使用金属性材质的情况下,反照率颜色(Albedo)将控制镜面反射的颜色,且大多数光线以镜面反射形势反射。非金属性材质将具有与入射光颜色相同的镜面反射,并且在正面观察表面时几乎不会反射。

2.SpecularWorkflow(镜面反射工作流)

有镜面反射颜色(Specular),用于控制材质的镜面反射颜色,镜面反射颜色可与漫反射颜色不同。

Blinn-Phong光照模型

反射(diffuse)

说明:符合兰伯特定律(Lambert’s law)反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比

计算公式: 其中:

半兰伯特光照模型(无物理依据,仅视觉加强):增强物体背面暗处的光照效果,将余弦值的[-1,1]映射到[0,1]。

计算公式:

其中:

高光反射(specular)

说明:经验模型,需要信息: 其中反射方向的计算:

方法一:(Phong 方法二:(Blinn)使用新矢量h(光源方向和视角方向的半角向量)代替反射方向矢量。当摄像机与光源距离模型足够远的时候,此方法计算更快。 计算公式:

PhongBlinn 其中:

实现步骤:

1.创建文件夹结构:

使用HTUtility工具(Shift+Alt+G)创建合适的文件夹结构,重命名Root文件夹为”单一光源光照”。在场景中创建胶囊体”Capsule”,Reset到原点位置。创建Material,命名为”SingleLightingMat”;创建Unlit Shader,命名为”SingleLighting”。选择”SingleLightingMat”的Shader为”SingleLighting”,并将材质赋值给场景中的胶囊体。

picture0

2.设置环境光:

在Unity菜单中点击“Window–Rendering–LightingSettings”。设置天空盒子和环境光(Environment Lighting)。

picture1

3.Shader编写:

先删除所有和雾相关的代码,在这里我们不需要雾。

属性块:

Properties
{
	_Tint("Tint",COLOR) = (1,1,1,1)//色调
	_MainTex ("Albedo", 2D) = "white" {}//反照率,diffuse颜色
	_Smoothness("Smoothness",Range(0,1)) = 0.5//物体表面平滑程度,越平滑越容易聚焦光线、光点越小
	_SpecularTint("Specular",Color) = (0.5,0.5,0.5,1)//镜面反射颜色
}

对应的在Pass中的属性:

fixed4 _Tint;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Smoothness;//物体表面平滑程度,越平滑越容易聚焦光线,光点越小
float4 _SpecularTint;//镜面反射颜色

由于本例只处理单一光源,所以我们只需要一个Pass,为其设置前向渲染的标签:

Pass
{
	Tags{"LightMode"="ForwardBase"}//前向渲染
	......
}

由于本例中需要用到Unity内置的DotClamped函数和EnergyConservationBetweenDiffuseAndSpecular能量守恒函数,所以需要分别引入”UnityStandardBRDF.cginc“库和UnityStandardUtils.cginc库。

库文件最好不要重复引入,虽然大多数情况下Unity会自动剔除重复引入的库,但重复引入库会导致变量重复命名的编译错误。库文件的层次结构:

picture2

picture3

在Pass块中的CGPROGRAM和ENDCG中引入库文件:

Pass
{
	Tags{"LightMode"="ForwardBase"}//前向渲染

	CGPROGRAM
	#pragma vertex vert
	#pragma fragment frag

	//定义了DotClamped,计算兰伯特的cos值--Clamp在[0,1]
	#include "UnityStandardBRDF.cginc"
	//已经包含UnityCG.cginc了

	//定义了能量守恒函数:EnergyConservationBetweenDiffuseAndSpecular
	#include "UnityStandardUtils.cginc"
	......
	ENDCG
}

顶点着色器的输入结构体:

struct appdata
{
	float4 pos : POSITION;
	float2 uv : TEXCOORD0;
	float3 normal : NORMAL;
};

顶点着色器的输出结构体/片元着色器的输入结构体:

struct v2f
{
	float4 vertex : SV_POSITION;
	float2 uv : TEXCOORD0;
	float3 normal : TEXCOORD1;
	float3 worldPos : TEXCOORD2;
};

顶点函数中需要做的事情:

1.将顶点坐标从模型空间转换到裁剪空间,可以直接使用Unity自带API:o.vertex = UnityObjectToClipPos(v.pos);

2.处理UV坐标:o.uv = TRANSFORM_TEX(v.uv, _MainTex);

3.获取世界空间下的法线:o.normal = UnityObjectToWorldNormal( v.normal);。通过此API获取的世界空间下的法线向量将不再是单位向量,会比单位向量小一点点。

4.获取世界空间下的顶点坐标:o.worldPos = mul(unity_ObjectToWorld , v.pos).xyz;

v2f vert (appdata v)
{
	v2f o;
	o.vertex = UnityObjectToClipPos(v.pos);
	o.uv = TRANSFORM_TEX(v.uv, _MainTex);
	o.normal = UnityObjectToWorldNormal( v.normal);
	o.worldPos = mul(unity_ObjectToWorld , v.pos).xyz;
	return o;
}

片元函数中要做的事情:

1.计算漫反射颜色(diffuse)。

需要光源颜色:fixed3 lightColor = _LightColor0.rgb;//该Pass处理的逐像素光源的颜色

反照率[^Albedo?]:先对主纹理贴图采样,顺带乘以颜色基调:float3 albedo = tex2D(_MainTex,i.uv).rgb * _Tint.rgb;;然后通过能量守恒公式调整反照率[^为什么需要使用能量守恒公式?]:albedo = EnergyConservationBetweenDiffuseAndSpecular(albedo , _SpecularTint.rgb , oneMinusReflectivity);;最后使用DotClamped计算光源方向向量和法线单位向量的点积:float3 diffuse = lightColor * albedo * DotClamped(lightDir,i.normal);

其中,在UnityShader库中的CGIncludes文件夹中可以搜索Unity自带函数:

  • (1).函数”EnergyConservationBetweenDiffuseAndSpecular“:
// Diffuse/Spec Energy conservation 能量守恒
inline half3 EnergyConservationBetweenDiffuseAndSpecular (half3 albedo, half3 specColor, out half oneMinusReflectivity)
{
    oneMinusReflectivity = 1 - SpecularStrength(specColor);
    #if !UNITY_CONSERVE_ENERGY
        return albedo;
    #elif UNITY_CONSERVE_ENERGY_MONOCHROME
        return albedo * oneMinusReflectivity;
    #else
        return albedo * (half3(1,1,1) - specColor);
    #endif
}

inline half OneMinusReflectivityFromMetallic(half metallic)
{
    // We'll need oneMinusReflectivity, so
    //   1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic)
    // store (1-dielectricSpec) in unity_ColorSpaceDielectricSpec.a, then
    //   1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) =
    //                  = alpha - metallic * alpha
    half oneMinusDielectricSpec = unity_ColorSpaceDielectricSpec.a;
    return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}
  • (2).函数”DotClamped“用于计算点积后再保证其为正值:
inline half DotClamped (half3 a, half3 b)
{
    #if (SHADER_TARGET < 30)
        return saturate(dot(a, b));
    #else
        return max(0.0h, dot(a, b));
    #endif
}

2.计算镜面反射颜色(specular)。

可以使用Phong模型或者Blinn模型计算。本文使用的Blinn模型。需要计算光源方向向量和视角方向向量之间的半角向量:float3 halfDir = normalize( lightDir + viewDir );;使用Blinn模型计算镜面反射颜色:float3 specular = lightColor * _SpecularTint.rgb * pow( DotClamped( i.normal , halfDir ) , _Smoothness * 100 );

3.计算法线单位向量的时候:法线在通过插值器传递的时候,会变短一点点,需要再次归一化。移动端可牺牲此步以提升性能。(常见优化手段)

fixed4 frag (v2f i) : SV_Target
{
	fixed3 lightColor = _LightColor0.rgb;//该Pass处理的逐像素光源的颜色
	//法线在通过插值器传递的时候,会变短一点点,需要再次归一化
	//移动端 可牺牲此步以提升性能。(常见优化手段)
	i.normal = normalize(i.normal);
	float3 lightDir = _WorldSpaceLightPos0.xyz;
	float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
	float3 halfDir = normalize( lightDir + viewDir );
	//物体的纹理颜色即为反照率
	float3 albedo = tex2D(_MainTex,i.uv).rgb * _Tint.rgb;
	//能量守恒:此步保证了漫反射加镜面反射小于1,即不会凭空制造光
	//albedo *= 1 - max(_SpecularTint.r , max(_SpecularTint.g , _SpecularTint.b));
	//1-反射率=1 - max(_SpecularTint.r , max(_SpecularTint.g , _SpecularTint.b))
	float oneMinusReflectivity;
	//能量守恒,会out出oneMinusReflectivity供其他地方使用
	albedo = EnergyConservationBetweenDiffuseAndSpecular(albedo , _SpecularTint.rgb , oneMinusReflectivity);
	//兰伯特:
	float3 diffuse = lightColor * albedo * DotClamped(lightDir,i.normal);
	//半兰伯特:
	//float3 diffuse = lightColor * albedo * (dot(lightDir,i.normal) * 0.5 + 0.5);
	float3 specular = lightColor * _SpecularTint.rgb * pow( DotClamped( i.normal , halfDir ) , _Smoothness * 100 );
	return float4( diffuse + specular , 1 );
}

导入Albedo纹理:

Brick_Diffuse

将Albedo赋值给Shader后,调整直接光方向到合适方向后,效果如下:

picture4

可见物体背面较黑,可用半兰伯特光照模型进行改善,改善后效果如下:

picture5

PBS(Physically-Based Shading)

本文不对PBS的数学部分进行解释,读者如果有兴趣可自行查阅相关资料。

实现步骤:

1.创建文件夹结构:

方法同上,创建胶囊体、Material、Shader

picture6

2.Shader编写:

属性块:

添加的金属值需要在属性前面加上[Gamma],告诉Unity该属性值也需要进行Gamma校正,以防在线性空间绘制时金属值不被Gamma校正。

Properties
{
	_Tint("Tint",COLOR) = (1,1,1,1)
	_MainTex ("Albedo", 2D) = "white" {}
	//告诉Unity,金属值也需要进行Gamma校正
	[Gamma]_Metallic("Metallic",Range(0,1)) = 0//金属值
	_Smoothness("Smoothness",Range(0,1)) = 0.1//物体表面平滑程度,越平滑越容易聚焦光线、光点越小
}

对应的在Pass中的属性:

fixed4 _Tint;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Smoothness;//物体表面平滑程度,越平滑越容易聚焦光线,光点越小
float _Metallic;//金属值

同样此Shader也只有一个Pass通道:Tags{"LightMode"="ForwardBase"}//前向渲染

要使用PBS必须指定目标平台在3.0及以上:#pragma target 3.0

引入库:#include "UnityPBSLighting.cginc",可以不再引入上一个Shader引入的那两个库了,因为此库已经包含有。

picture7

Pass
{
	Tags{"LightMode"="ForwardBase"}//前向渲染

	CGPROGRAM
	#pragma vertex vert
	#pragma fragment frag

	//使用PBS
	#pragma target 3.0

	//包含"UnityStandardBRDF.cginc"和"UnityStandardUtils.cginc"库
	#include "UnityPBSLighting.cginc"
	......
	ENDCG
}

顶点着色器的输入结构体:

struct appdata
{
	float4 pos : POSITION;
	float2 uv : TEXCOORD0;
	float3 normal : NORMAL;
};

顶点着色器的输出结构体/片元着色器的输入结构体:

struct v2f
{
	float4 vertex : SV_POSITION;
	float2 uv : TEXCOORD0;
	float3 normal : TEXCOORD1;
	float3 worldPos : TEXCOORD2;
};

顶点函数中需要做的事情:

1.将顶点坐标从模型空间转换到裁剪空间,可以直接使用Unity自带API:o.vertex = UnityObjectToClipPos(v.pos);

2.处理UV坐标:o.uv = TRANSFORM_TEX(v.uv, _MainTex);

3.获取世界空间下的法线:o.normal = UnityObjectToWorldNormal( v.normal);。通过此API获取的世界空间下的法线向量将不再是单位向量,会比单位向量小一点点。

4.获取世界空间下的顶点坐标:o.worldPos = mul(unity_ObjectToWorld , v.pos).xyz;

v2f vert (appdata v)
{
	v2f o;
	o.vertex = UnityObjectToClipPos(v.pos);
	o.uv = TRANSFORM_TEX(v.uv, _MainTex);
	o.normal = UnityObjectToWorldNormal( v.normal);
	o.worldPos = mul(unity_ObjectToWorld , v.pos).xyz;
	return o;
}

片元函数中要做的事情:

1.通过金属值计算反照率和镜面反射颜色:albedo = DiffuseAndSpecularFromMetallic(albedo , _Metallic , specColor , oneMinusReflectivity);;其中”oneMinusReflectivity“为”1-反射率“,反射率为镜面反射颜色RGB通道中的最大值。

其中,在UnityShader库中的CGIncludes文件夹中可以搜索Unity自带函数:

  • (1).函数”DiffuseAndSpecularFromMetallic“:会返回处理后的Albedo和oneMinusReflectivity值。

通过albedo * oneMinusReflectivity;可以保证能量不会增加。(数学推导比较简单,此处略)

inline half3 DiffuseAndSpecularFromMetallic (half3 albedo, half metallic, out half3 specColor, out half oneMinusReflectivity)
{
    specColor = lerp (unity_ColorSpaceDielectricSpec.rgb, albedo, metallic);
    oneMinusReflectivity = OneMinusReflectivityFromMetallic(metallic);
    return albedo * oneMinusReflectivity;
}
  • (2).(1)中函数中的函数”OneMinusReflectivityFromMetallic“:
inline half OneMinusReflectivityFromMetallic(half metallic)
{
    // We'll need oneMinusReflectivity, so
    //   1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic)
    // store (1-dielectricSpec) in unity_ColorSpaceDielectricSpec.a, then
    //   1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) =
    //                  = alpha - metallic * alpha
    half oneMinusDielectricSpec = unity_ColorSpaceDielectricSpec.a;
    return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}

2.使用Unity自带的双向反射分布函数UNITY_BRDF_PBS(...)计算最后的光照颜色:

//PBS需要使用的光数据
UnityLight light;
light.color = lightColor;
light.dir = lightDir;
light.ndotl = DotClamped(i.normal , lightDir);
//PBS需要使用的间接光数据
UnityIndirect indirectLight;
indirectLight.diffuse = 0;//漫反射代表环境光
indirectLight.specular = 0;//镜面反射代表环境反射

return UNITY_BRDF_PBS
(
	albedo , specColor , 
	oneMinusReflectivity ,_Smoothness , 
	i.normal ,viewDir , 
	light , indirectLight
);

片元函数:

fixed4 frag (v2f i) : SV_Target
{
	fixed3 lightColor = _LightColor0.rgb;//该Pass处理的逐像素光源的颜色
	//法线在通过插值器传递的时候,会变短一点点,需要再次归一化
	//移动端 可牺牲此步以提升性能。(常见优化手段)
	i.normal = normalize(i.normal);
	float3 lightDir = _WorldSpaceLightPos0.xyz;
	float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
	//物体的纹理颜色即为反照率
	float3 albedo = tex2D(_MainTex,i.uv).rgb * _Tint.rgb;
	//1-反射率=1 - max(_SpecularTint.r , max(_SpecularTint.g , _SpecularTint.b))
	float oneMinusReflectivity;
	float3 specColor;
	//通过金属值计算反照率和镜面反射颜色
	//返回albedo,out specColor和oneMinusReflectivity
	albedo = DiffuseAndSpecularFromMetallic(albedo , _Metallic , specColor , oneMinusReflectivity);

	//PBS需要使用的光数据
	UnityLight light;
	light.color = lightColor;
	light.dir = lightDir;
	light.ndotl = DotClamped(i.normal , lightDir);
	//PBS需要使用的间接光数据
	UnityIndirect indirectLight;
	indirectLight.diffuse = 0;//漫反射代表环境光
	indirectLight.specular = 0;//镜面反射代表环境反射

	return UNITY_BRDF_PBS
	(
		albedo , specColor , 
		oneMinusReflectivity ,_Smoothness , 
		i.normal ,viewDir , 
		light , indirectLight
	);
}

回到Unity中,调整直接光方向到合适方向后,效果如下:

(左边为SpecularWorkflow;右边为MetallicWorkflow)

picture8

其中”SpecularWorkflow”的设置:

picture9

“MetallicWorkflow”的设置:

picture10

写在最后

本文着重讲述了两种工作流在Unity中的简单实现,本文只处理了单一光源,对于多光源和阴影等支持将在后续文章中给出。

Reference

Rendering 4 The First Light

UnityShader入门精要