本文章用于帮助自己学习,因此只记录一些个人认为比较重要或者还不够熟悉的内容。
原作者:http://blog.csdn.net/candycat1992/article/
在实时渲染中,我们通常把光源当成一 个没有体积的点,用l来表示它的方向。
在光学里,我们使用辐照度(irradiance)来量化光。对于平行光来说, 它的辐照度可通过计算在垂直于I的单位面积上单位时间内穿过的能量来得到。
物体表面和l不垂直的情况,我们可以使用光源方向l和表面法线n之间的夹角的余弦值来得到,如图:
因为辐照度是和照射到物体表面时光线之间的距离d/cosθ 成反比的,因此辐照度就和cosθ 成正比。
cosθ 可以使用光源方向l和表面法线n的点积来得到。这就是使用点积来计算辐照度的由来。
光线由光源发射出来后,就会与一些物体相交。通常,相交的结果有两个:散射(scattering) 和吸收(absorption)。
散射只改变光线的方向,但不改变光线的密度和颜色。
而吸收只改变光线的密度和颜色,但 不改变光线的方向。
光线在物体表面经过散射后,有两种方向:
一种将会散射到物体内部,这种 现象被称为折射(refraction)或透射(transmission);
另一种将会散射到外部,这种现象被称为反射(renection)。
为了区分两种不同的散射方向,我们在光照模型中使用了不同的部分来计算它们:
高光反射(specular)部分表示物体表面是如何反射光线的。
漫反射(diffuse)部分则表示有多少光线会被折射、吸收和散射出表面。
根据入射光线的数量和方向,我们可以计算出射光线的数量和方向,我们通常使用出射度(exitance)来描述它。
辐照度和出射度之间是满足线性关系的,而它们之间的比值就是材质的漫反射和高光反射属性。
在1975年,著名学者裴祥风(Bui Tuong Phong)提出了标准光照模型背后的基本理念。
标准光照模型只关心直接光照(direct light),也就是那些直接从光源发射出来照射到物体表面后, 经过物体表面的一次反射直接进入摄像机的光线。
进入到摄像机内的光线分为4个部分:
间接光照指的是,光线通常会在多个物体之间反射,最后进入摄像机, 也就是说,在光线进入摄像机之前,经过了不止一次的物体反射。
在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。它通常是一个全局变量,即场景中的所有物体都使用这个环境光:
自发光直接由光源发射进入摄像机,而不需要经过任何物体的反射。它的计算也很简单,就是直接使用了该材质的自发光颜色:
漫反射光照是用于对那些被物体表面随机散射到各个方向的辐射度进行建模的。
在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一 样的。但是,入射光线的角度很重要。
漫反射光照符合兰伯特定律(Lambert’s law):反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。因此,漫反射部分的计算如下:
计算高光反射需要知道的信息比较多,如表面法线、视角方向、光源方向、反射方向等。在 本节中,我们假设这些矢量都是单位矢量。
在这四个矢量中,实际上只需要知道其中3个矢量即可,而第四个矢量——反射方向可以通过其他信息计算得到:
这样,我们就可以利用Phong模型来计算高光反射的部分:
其中m(gloss)是材质的光泽度,也被反称为反光度。它用于控制高光区域的“亮点”有多宽,m(gloss)越大,亮点就越小。m(spscular)是材质的高光反射颜色,它用于控制该材质对于高光反射的强度和颜色。c(light)则是光源的颜色和强度。
和上述的Phong模型相比,Blinn提出了一个简单的修改方法来得到类似的效果。它的基本思想是避免计算反射方向。为此,Blinn模型引入了一个新的矢量,如下:
然后,使用n和h之间的夹角进行计算,而非v和r之间的夹角,如下图所示:
总结一下,Blinn模型的公式如下:
在硬件实现时,如果摄像机和光源距离模型足够远的话,Blinn模型会快于Phong模型,但是,当v或者I不是定值时,Phong模型可能反而更快一些。
我们在哪里计算这些光照模型呢?通常有两种选择:
在片元着色器中计算,也被称为逐像素光照(per-pixel lighting);
在顶点着色器中计算,也被称为逐顶点光照(per-vertex lighting)。
在逐像素光照中,我们会以每个像素为基础,得到它的法线,然后进行光照模型的计算。
在逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。
由于逐顶点光照会在渲染图元内部对顶点颜色进行插值,这会导致渲染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象。
在Unity2020中,场景中的环境光可以在Window-> Rendering -> Lighting -> Environment-> Source / Ambient Color / Intensity 中控制,如图:
在Shader中,只需要通过Unity的内置变量UNITY_LIGHTM ODEL_AMBIENT就可以得到环境光的颜色和强度信息。
漫反射部分的计算公式:
从公式可以看出,要计算漫反射需要知道4个参数:入射光线的颜色和强度,材质的漫反射系数,表面法线以及光源方向。
为了防止点积结果为负值,还需要使用max操作,而CG提供了这样的函数。在本例中, 使用CG的另一个函数可以达到同样的目的,即saturate函数。
函数:saturate(x)
参数:X:为用于操作的标量或矢量,可以是float、float2、float3等类型。
描述:把x截取在[0,1]范围内,如果x是一个矢量,那么会对它的每一个分量进行这样的操作。
准备工作与之前类似,就不过多说,直接上代码:
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
}
SubShader
{
Pass
{
//指明该pass光照模式
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//定义和properties语义块中属性类型相匹配的变量
fixed4 _Diffuse;
struct a2v
{
float4 vertex : POSITION;
//定义变量,并通过语义告知unity将法线信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f
{
//SV_POSITION语义告诉Unity,pos 里包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
//COLOR0 语义可以用于存储颜色信息
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));//saturate防止点积为负
o.color = ambient + diffuse;//环境光和漫反射光相加
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return fixed4(i.color,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
对于细分程度较高的模型,逐顶点光照已经可以得到比较好的光照效果了。但对于一些细分程度较低的模型,逐顶点光照就会出现 一些视觉问题,例如我们可以在图中看到在胶囊体的背光面与向光面交界处有一些锯齿。为 了解决这些问题,我们可以使用逐像素的漫反射光照。
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//定义和properties语义块中属性类型相匹配的变量
fixed4 _Diffuse;
struct a2v
{
float4 vertex : POSITION;
//定义变量,并通过语义告知unity将发现信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f
{
//SV_POSITION语义告诉Unity,pos 里包含了顶点在裁剪空间中的位置信息
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 worldLight = normalize(_WorldSpaceLightPos0.xyz);//获得光源方向
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));//saturate防止点积为负
fixed3 color = ambient + diffuse;
return fixed4(color,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
效果如图:
可以看出,逐像素光照可以得到更加平滑的光照效果。但是,即便使用了逐像素漫反射光照,有一个问题仍然存在。在光照无法到达的区域,模型的外观通常是全黑的。为此,有一种改善技术被提出来,这就是半兰伯特(Half Lambert)光照模型。
Valve公司在开发游戏《半条命》时提出了一种技术,由于该技术是在原兰伯特光照模型的基础上进行了一个简单的修改,因此被称为半兰伯特光照模型。
广义的半兰伯特光照模型的公式如下:
通过这样的方式,可以把的结果范围从[-1, 1]映射到[0, 1]范围内。也就是说,对于模型的背光面,在原兰伯特光照模型中点积结果将映射到同一个值,即0值处;而在半兰伯特模型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//定义和properties语义块中属性类型相匹配的变量
fixed4 _Diffuse;
struct a2v
{
float4 vertex : POSITION;
//定义变量,并通过语义告知unity将法线信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f
{
//SV_POSITION语义告诉Unity,pos 里包含了顶点在裁剪空间中的位置信息
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 worldLight = normalize(_WorldSpaceLightPos0.xyz);//获得光源方向
fixed halfLambert = dot(worldNormal, worldLight) * 0.5 + 0.5;
//使用半兰伯特模型计算光照
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;//saturate防止点积为负
fixed3 color = ambient + diffuse;
return fixed4(color,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
高光反射部分的计算公式:
可以看出,要计算高光反射需要知道4个参数:入射光线的颜色和强度c(light),材质的高光反射系数m(specular),视角方向v以及反射方向r。
反射方向r可以由函数**reflect(i,n)**计算得,i是入射方向;n是法线方向。可以是float、float2> float3 等类型。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Unity Shaders Book/Chapter 6/Specular Vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20//控制高光区域大小
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//定义和properties语义块中属性类型相匹配的变量
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
//定义变量,并通过语义告知unity将法线信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f
{
//SV_POSITION语义告诉Unity,pos 里包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
//COLOR0 语义可以用于存储颜色信息
fixed3 color : COLOR;
};
v2f vert(a2v v)
{
v2f o;
//把顶点位置从模型空间转到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
//获得环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//法线转换到世界空间
fixed3 worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
//获得光源方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//计算漫反射
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));//saturate防止点积为负
//获得世界空间下的反射反向
//CG的reflect函数入射方向要求是光源指向交点,因此需要取反!
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
//获得世界空间下的视线反向
//_WorldSpaceCameraPos.xyz得到世界空间中摄像机位置,并与顶点位置相减
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
//计算高光反射
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)),_Gloss);//pow是次方
o.color = ambient + specular + diffuse;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return fixed4(i.color,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
Shader "Unity Shaders Book/Chapter 6/Specular Vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20//控制高光区域大小
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//定义和properties语义块中属性类型相匹配的变量
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
//定义变量,并通过语义告知unity将发现信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f
{
//SV_POSITION语义告诉Unity,pos 里包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos:TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
//把顶点位置从模型空间转到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
//法线转换到世界空间
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
//把顶点位置从模型空间转到世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
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 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rbg * saturate(dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
在6.2.4节中,我们还提到了另一种高光反射的实现方法 一Blinn光照模型。Blinn模型没有使用反射方向, 而是引入一个新的矢量h,它是通过对视角方向v和光照方向l相加后再归一化得到的。即:
Blinn模型计算高光反射的公式如下:
Shader "Unity Shaders Book/Chapter 6/Specular Vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20//控制高光区域大小
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//定义和properties语义块中属性类型相匹配的变量
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
//定义变量,并通过语义告知unity将发现信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f
{
//SV_POSITION语义告诉Unity,pos 里包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos:TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
//把顶点位置从模型空间转到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
//法线转换到世界空间
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
//把顶点位置从模型空间转到世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
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 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
//计算h向量,光源方向与视线方向相加
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
//blinn模型计算高光
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rbg * saturate(dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
需要注意的是,这些函数都没有保证得到的方向矢量是单位矢量,因此,我们需要在使用前把它们归一化。