使用逐顶点光照在unityShader中实现漫反射效果
-
漫反射公式——兰伯特模型
首先需要了解基本光照模型中,漫反射(Diffuse)的
兰伯特模型
兰伯特模型 共有4个参数 -
入射光线的颜色和强度—— C light
-
材质的漫反射系数—— M diffuse
-
表面法线—— n
-
光源方向—— I
为了避免 n 和 I 的点积为负值,我们需要使用max来操作。但在Unity Shader中,我们可以使用 saturate(x)
函数达到相同的效果。
其中,x
可以是操作的标量或矢量,即float、float2、float3等类型
saturate
会把传进来的x
截取在[0,1]的范围内,如果x是一个矢量,那么就会对它的每一个分量进行截取操作。
在宏观上,渲染可以分为一下两个:
1、决定像素是否可见;
2、决定像素的光照计算。
在这一片博客,我们主要探讨第二点——光照的计算。
计算光照的公式有很多,我们使用的是兰伯特模型,但是如何理解其中的变量值?为什么会有阴影的部分?为什么有些部分的颜色与其他部分不同?
-
Q:我们探讨为什么有些光亮的颜色不同:
A:在Unity之中,假设一个平行光源带来的每条光线间隔为 d ,那么如果是正午,也就是平行光源正向下照射,那么我们可以轻易知道,此时找到物体表面的光线间隔其实也是为 d 的。但是如果光线不是正下直射,而是有点倾斜角度(清晨或黄昏时),那么如果平行光带来的光线间隔仍为 d 时,照到物体上的间距其实已经不是 d 了,因为在光线数量有限的Unity中,发生了角度的偏移。下图可以更直观的展示这个结论:
-
Q:兰伯特公式是如何在Shader中工作的?
A:在上图中,cosθ可以使用 光源方向 l 和表面法线 n 的点积
求得。而兰伯特模型中,尾部的部分,就是 光源方向 与 表面法线 点积。结合下图可以更轻易的了解:
(图片取自知乎@俊铭)
根据上图,可以分为右上角的三个情况。再结合兰伯特公式,最终可以作为权重来控制颜色的输出。当点积的值小于0时,直接颜色权重变为0,(因为要控制在[ 0 , 1]的区间内)最终导致颜色为0,阴影的产生。
代码开始:
1. 创建一个空场景(只有摄像机和平行光),删除该场景中的天空盒。
2. 新建一个材质,命名为Mat_Diffuse
。
3. 新建一个UnityShader,命名为Sha_DIffuse
并将该Shader赋值给Mat_Diffuse
。
4. 在场景中新建一个胶囊体,将Mat_Diffuse
赋予该物体。
5. 保存,开始编辑Shader代码:
①删除第3步新建的Shader里的默认代码。
②为了控制该Shader的颜色,我们需要在Properties中添加漫反射颜色
Properties{
_Diffuse ( "Diffuse", Color) = (1,1,1,1)
}
(这里更加类似于 C# 脚本的 Public Color _Diffuse;
)
③在SubShader之中定义Pass语义块。在Pass的一开始就指名该Pass的光照模式:
SubShader{
Pass{
Tags {
"LightMode"="ForwardBase"}
}
}
只有正确定义了LightMode,我们才可以在后续得到内置的光照变量。
④声明vertex和fragment着色器 并 包含Lightint.cginc
#pragma vertex vert
#pragma fragment frag
#include "Lightint.cginc"
⑤由于在Properties中声明了_Diffuse
属性,因此我们需要定义一个和该属性类型一致的变量,方便Shader控制
fixed4 _Diffuse;
(这里的就类似于 _Diffuse = new fixed4();
初始化)
由于Diffuse的取值范围是 [0,1] 因此使用fixed
类型。值得一提的是,这里的fixed
类型其实是很常见的(在UnityShader中)
类型 | 精度 (CG / HLSL) |
---|---|
float | 最高精度的浮点值。通常使用32位来存储。 |
half | 中等精度的浮点值。通常使用16位来存储。( -60,000 ~ 60,000) |
fixed | 最低精度的浮点值。通常使用11位来存储。( -2.0 ~ 2.0 ) |
Tip:对于PC端而言,三者区别不大,因为都会被视作float的类型来处理。但是对于移动端而言,三者使用的优先级为:fixed > half > float ,且尽量少出现float。
⑥定义顶点着色器和片元着色器的输入输出结构体(着色器的输出结构体与输入结构体一致)
struct a2v {
float4 vertex : POSITION;
float4 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
(SV_
是System Value
的意思,是DirectX 10 新增的系统数值语义
类型。SV_POSITION会返回一个裁剪后的顶点位置信息,并直接返回到屏幕之中。如果开发PS4平台,则必须使用SV_POSITION,否则会导致细分着色器无法工作。)
⑦完善流水线中的顶点着色器
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));
o.color = ambient + diffuse;
return o;
}
到了这一步,我们已经得到了漫反射颜色
_Diffuse 和 顶点法线
v.normal。
光的强度和颜色 以及 光的方向。光强和光色
使用_LightColor0来获取。这里是因为只有一个平行光,光源方向
使用_WorldSpaceLightPos0不会出错,如果在光照环境复杂的情况下可能无法得到正确的结果。
至此,漫反射公式的四个值都已经得到。在计算点积时,两个值必须实在同一个坐标空间下
,这里我们将表面法线和光源方向在世界空间下进行点积运算。
⑧实现流水线中的片元着色器
Shader代码:
fixed4 frag(v2f i) : SV_Target{
return fixed4(i.color, 1.0);
}
⑨最终结果:
Shader "LeonShader/shader_6_4_Diffuse"
{
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;
float4 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : 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));
o.color = ambient + diffuse;
return o;
}
fixed4 frag(v2f i) : SV_Target{
return fixed4(i.color , 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
逐像素光照
shader代码:
Shader "LeonShader/shader_6_4_Diffuse_Pixel"
{
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;
float4 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
v2f vert(a2v v) {
v2f o;
//将定点左边从本地空间转变投影空间
o.pos = UnityObjectToClipPos(v.vertex);
//得到环境信息
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//法线信息由物体空间转变为世界空间
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"
}
- 思考
根据文末的效果图(没有比较逐像素法,因为逐像素法的效果非常可观),其实可以很清楚的看到,逐顶点的影子是由明显的锯齿效果的,这当然不是我们想要的。于是我在Blender制作了一个经纬度均为64的Sphere,以此来与unity默认的Sphere做对比。
如下图可以看到,出现锯齿的主要原因其实就是因为物体的细分程度不够所导致,加上了我导入的(左上角的)Sphere在Blender中添加了平滑着色处理,显得非常自然。