[Unity-Shader]标准光照模型-漫反射(diffuse)

学习资料来自《Shader入门精要》

漫反射

  • 漫反射光照是用于对那些被物体表面随机散射到各个方向的辐射度进行建模的。在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。但是入射光线的角度很重要。
    漫反射光照符合兰伯特定律:反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。因此,漫反射部分计算如下:

C d i f f u s e = C l i g h t ⋅ m d i f f u s e m a x ( 0 , n ⃗ ⋅ l ⃗ ) C_{diffuse}=C_{light}\cdot m_{diffuse}max(0,\vec n\cdot \vec l) Cdiffuse=Clightmdiffusemax(0,n l )

其中 n ⃗ \vec n n 是表面法线, l ⃗ \vec l l 是指向光源的单位矢量, m d i f f u s e m_{diffuse} mdiffuse是材质的漫反射颜色, C l i g h t C_{light} Clight是光源颜色。我们需要防止法线和光源方向点乘的结果为负值,为此,我们使用取最大值的函数来将其截取到0,这可以防止物体被从后面来的光源照亮。

逐像素还是逐顶点

在片元着色器中计算,也被称为逐像素光照;在顶点着色器中计算,也被称为逐顶点光照。

在逐像素光照中,我们会以每个像素为基础,得到它的法线(可以是对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型的计算。这种在面片之间对顶点法线进行插值的技术被称为Phong着色

与之相对的是逐顶点光照,也被称为高洛德着色。在逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。由于顶点数目往往小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。但是,由于逐顶点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性的计算(例如计算高光反射时)时,逐顶点光照就会出问题。而且,由于逐顶点光照会在渲染图元内部对顶点颜色进行插值,这会导致渲染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象。

Unity Shade中实现漫反射光照模型

逐顶点光照:
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter 6/DiffuseVertexLevel" {
	Properties {
		_Diffuse ("Diffuse", Color)=(1,1,1,1) //控制材质的漫反射颜色 定义一个Color类型的属性,并初始化为白色
	}
	SubShader {
		Pass{
			Tags{"LightMode"="ForwardBase"} //定义该Pass在Unity的光照流水线中的角色

			CGPROGRAM

			#pragma vertex vert  //定义顶点着色器名字
			#pragma fragment frag //定义片元着色器名字

			#include "Lighting.cginc"//引入Unity的内置文件

			fixed4 _Diffuse; //为了使用Properties语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量

			//定义顶点着色器的输入和输出结构体
			struct a2v {
				float4 vertex : POSITION; 
				float3 normal : NORMAL; //通过NORMAL语义来告诉Unity要把模型的顶点的法线信息存储到normal变量中。
			};
			struct v2f {
				float4 pos : SV_POSITION;
				fixed3 color : COLOR;//为了把顶点着色器中计算得到的光照颜色传递给片元着色器,需要在v2f中定义一个color变量
			};

			//逐顶点的漫反射光照
			v2f vert(a2v v){
				v2f o;
				//将顶点坐标从模型空间转换成裁剪空间
				o.pos=UnityObjectToClipPos(v.vertex);

				//获取环境光部分
				fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;

				//将法线坐标从模型空间转换成世界坐标
				fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
				//获取世界坐标的光照向量
				fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
				//计算漫反射
				fixed3 diffuse=_LightColor0.rgb *_Diffuse.rgb * saturate(dot(worldNormal,worldLight));//saturate(x)函数把x截取在[0,1]范围内

				o.color=ambient+diffuse;
				return o;

			}
			//片元着色器代码 输出顶点颜色
			fixed4 frag(v2f i): SV_Target{
				return fixed4(i.color,1.0);
			}
			ENDCG
		}
		
	}
	FallBack "Diffuse" //回调shader设置为内置的diffuse
}

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

逐像素光照
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter 6/DiffusePixelLevel"{
	Properties{
		_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
	}
	SubShader {
		Pass{
			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;
				float3 worldNormal:TEXCOORD0;  
			};

			v2f vert(a2v v){
				v2f o;

				o.pos=UnityObjectToClipPos(v.vertex);
				o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
				return o;
			}

			fixed4 frag(v2f i):SV_Target{
				//获取环境光部分
				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);
			}
			ENDCG
		}
	}
	FallBack "Diffuse"
}

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

半兰伯特模型

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

C d i f f u s e = C l i g h t ⋅ m d i f f u s e ( α ( n ⃗ ⋅ l ⃗ ) + β ) C_{diffuse}=C_{light}\cdot m_{diffuse}(\alpha(\vec n\cdot \vec l)+\beta) Cdiffuse=Clightmdiffuse(α(n l )+β)

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

C d i f f u s e = C l i g h t ⋅ m d i f f u s e ( 0.5 ( n ⃗ ⋅ l ⃗ ) + 0.5 ) C_{diffuse}=C_{light}\cdot m_{diffuse}(0.5(\vec n\cdot \vec l)+0.5) Cdiffuse=Clightmdiffuse(0.5(n l )+0.5)

通过这样的方式,我们可以把 n ⃗ \vec n n l ⃗ \vec l l 的结果范围从[-1,1]映射到[0,1]范围内。也就是说,对于模型的背光面,在原兰伯特光照模型中点积结果将映射到同一个值,即0值处;而在半兰伯特模型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。
需要注意的是,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。
代码只需要再刚刚的逐像素光照中修改片元着色器计算漫反射部分

fixed4 frag(v2f i):SV_Target{
				//获取环境光部分
				fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;

				//获取世界坐标下物体表面法线向量
				fixed3 worldNormal=normalize(i.worldNormal);
				//获取世界坐标下光源单位向量
				fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);

				//计算漫反射
				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);
			}

下图给出上述三者的最终呈现效果

(逐顶点漫反射光照、逐像素漫反射光照、半兰伯特光照)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值