目录
一、标准光照模型(Phong光照模型)
1、环境光
2、自发光
3、漫反射
4、高光反射
(1)Phong模型
(2)Blinn模型
5、光照模型实现方法——逐顶点和逐像素
二、Unity Shader 漫反射光照模型的实现
1、实践:逐顶点
2、实践:逐像素
3、半兰伯特模型
4、漫反射光照模型效果展示
三、Unity Shader 高光反射光照模型的实现
1、实践:逐顶点
2、实践:逐像素
3、Blinn-Phong 光照模型
4、高光反射光照模型效果展示
四、Unity 的内置函数
在标准光照模型中,使用环境光来近似模拟间接光照。间接光照就是指,光线通常会在多个物体之间反射,最后进入摄像机,例如光线通过墙壁、镜面、地板等将光源反射后的一种照明效果,而不是直接将光源投像到被照物。
环境光的计算很简单,通常是一个全局变量,也就是场景中的所有物体都使用这个环境光。计算如下:
光线可以直接由光源发射进入摄像机,不需要经过任何物体反射。自发光的计算就是直接使用了该材质的自发光颜色;计算如下:
通常在实时渲染的时候,自发光不会被当成一个光源,也就不会对周围物体造成影响。后续会实现可以对周围环境影响的效果。
漫反射光照是用于对那些被物体表面随机散射到各个方向的辐射度进行建模的。漫反射中,视角的位置不重要,因为反射是完全随机的。入射光线角度很重要。
漫反射符合兰伯特定律:反射光线强度与表面法线和光源方向之间的夹角的余弦值成正比。 具体计算如下:
n是表面法线,I是指向光源的单位矢量,是材质的漫反射颜色,是光源的颜色。我们要防止法线和光源方向的点乘为负值,所以用取最大值函数截取到0,可以防止物体被从后面来的光源照亮。
这里的高光反射是一种经验模型——不完全符合真实世界中的高光反射现象。用于计算沿着完全镜面反射方向的光线,让物体看起来更有光泽。
计算高光反射我们需要知道:表面法线、视角方向、光源方向、反射方向。我们只要知道前三个矢量(都进行了归一化),反射方向可以通过计算得到,计算公式如:
r是反射方向、n是法线方向、I是光源方向。
高光反射部分计算公式如下:
是材质的光泽度,也叫反光度,用于控制高光区域的“亮点”有多大,越大,亮点越小。是材质的高光反射颜色,用于控制材质对于高光反射的强度、颜色。是光源的颜色和强度。同样这里也要防止v*r的结果为负值。
Blinn模型在上面的Phong模型上进行简单的修改来得到类似的效果。他的思想是,避免计算反射方向 r 。为此Blinn引入了一个新的矢量 h ,他是通过对 v 和 I 的取平均后再归一化得到的:
所以Blinn模型公式如下:
这两种模型各有优劣。如果摄像机和光源距离模型足够远的话,Blinn会快于Phong,因为这个时候 v 和 I 都可以看成定值,h 将是一个常量。反之Phong可能更快。
实现这些基本光照模型有两种方式,一种是在片元着色器中计算,被称为逐像素光照(per-pixel lighting);另一种是在顶点着色器中计算,被称为逐顶点光照(per-vertex lighting).
逐像素光照,以每个像素为基础,得到它的法线(可以是对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型计算,这种在面片之间对顶点法线进行插值的技术称为Phong着色,也被称为Phong插值或法线插值技术,这不同于Phong光照模型。
逐顶点光照,也称为高洛德着色(Gouraud shading)。在逐顶点光照中,我们在每个顶点上计算光照,然后再渲染图元内部进行线性插值,最后输出成像素颜色。由于顶点数目一般远小于像素的数目,所以逐顶点光照的计算量一般小于逐像素光照。但是因为逐顶点光照依赖于线性插值得到像素光照,所以,如果光照模型有非线性计算时(如计算高光反射时),就会出现问题。此外,逐顶点光照会再渲染图元内部对顶点颜色线性插值,导致渲染图元内部的颜色总是暗与顶点处的最高颜色,在某些情况下回产生明显的棱角现象。
实现代码如下,备注也写在代码注释里面了。
Shader "MyShader/6_DiffuseVertex.Leve!Mat"
{
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//头文件,可以使用unity内置的一些变量
#include "Lighting.cginc"
//fixed精度值-2到2,color颜色属性范围0-1
fixed4 _Diffuse;
//顶点着色器输入结构体
struct a2v {
float4 vertex : POSITION;
float3 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;
/* 把法线方向变换到世界坐标
使用顶点变换矩阵的逆转置矩阵,unity_WorldToObject可以得到模型空间到世界空间的
变换矩阵的逆矩阵。 调换在mul函数中的位置,得到和转置矩阵相同的矩阵乘法,相当于乘了逆转置矩阵。
法线是一个三维矢量,所以只要截取3*3*/
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// 获得世界坐标下的光照方向
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);//默认场景中只有一个平行光源,多个光源就不能只用这个
// 漫反射计算公式 = 入射光的颜色和强度 * 材质的漫反射系数 * max(0,表面法线*光源方向)
//表面法线*光源方向,两者要在同一个坐标系才有意义,这里选择的世界坐标系,所有有了上面的worldNormal和worldLight--转换为世界坐标空间
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));//saturate(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 "MyShader/DiffusePixe"
{
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//头文件,可以使用unity内置的一些变量
#include "Lighting.cginc"
//fixed精度值-2到2,color颜色属性范围0-1
fixed4 _Diffuse;
//顶点着色器输入结构体
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;//访问顶点的法线
};
//顶点着色器输出结构体
struct v2f {
float4 pos : SV_POSITION;
fixed3 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"
}
上面两个模型(也叫兰伯特光照模型)有个问题就是,光照没有照射到的地方是全黑的,没有明暗变化,导致背面就像是有个平面一样。为此 有一种改善技术被提出来,这就是半兰伯特 (Half Lambert) 光照模型。
广义半兰伯特光照模型公式如下:
与原来的兰伯特光照模型相比,半兰伯特光照模型没有使用max操作来防止n*I的点积为负值,而是进行了一个α倍的缩放和β大小的偏移。一般情况下这两个值都为0.5,即:
这样就可以把[-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);
}
Shader "MyShader/6_SpecularVertex"
{
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"} //LightMode是Pass标签的一种
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 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 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// 计算漫反射
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
//reflect(i,n)函数用于计算反射方向,i是入射方向,n是法线方向。对i的要求是由光源指向交点,所以下面worldLightDir加了负号
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// 获得世界坐标空间下的视角方向
//_WorldSpaceCameraPos获得相机位置,mul(unity_ObjectToWorld, v.vertex).xyz把顶点位置从模型空间转换到世界空间。
//上述两项相减得到世界空间下的视角方向。
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
// 计算高光反射
//基本光照模型,高光反射部分的基本公式:Cspecular = (Clight*Mspecular)max(0,v*r)^Mgloss
//Clight入射光线的颜色和强度、Mspecular材质的高光反射系数、v视角方向、r反射方向、Mgloss控制高光区域大小
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
o.color = ambient + diffuse + specular;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
Shader "Unity Shaders Book/Chapter 6/Specular Pixel-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"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
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 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// 计算反射方向
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);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
在前面以及介绍了这个Blinn高光反射的光照模型,Blinn模型没有计算反射方向,而是引用了一个新的矢量h,计算如下:
所以Blinn模型公式如下:
实现Blinn模型,只要修改前面的逐像素的高光反射模型代码的片元着色器
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 * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// 计算h向量
fixed3 halfDir = normalize(worldLightDir + viewDir);
// 计算高光反射
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
函数名 | 描述 |
---|---|
float3 WorldSpaceViewDir(float4 objectPos) | 输入模型空间的顶点位置,返回该点到摄像机的观察方向 |
float3 UnityWorldSpaceViewDir(float4 worldPos) | 输入世界空间顶点位置,返回世界空间该点到摄像机的观察方向 |
float3 ObjectSpaceViewDir(float4 objectPos) | 输入模型空间下的顶点位置,返回模型空间下的视角方向(观察方向) |
float3 WorldSpaceLightDir(float4 objectPos) | (仅可用于前向渲染中)即 Pass的Tags{"LightModel" = "ForwardBase"} 输入模型空间下的顶点坐标,返回世界空间下的光源方向(从该点到光源的向量) |
float3 UnityWorldSpaceLightDir(float4 worldPos) | (仅可用于前向渲染中)输入世界空间下的顶点坐标,返回世界空间下的光源方向 |
float3 ObjectSpaceLightDir(float4 objectPos) | (仅可用于前向渲染中)输入模型空间下的顶点坐标,返回模型空间下的光源方向 |
float3 UnityObjectToWorldNormal(float3 objectNormal) | 输入模型空间下的法线,返回世界空间下的法线 |
float3 UnityObjectToWorldDir(float3 objectDir) | 输入一个模型空间下的三维矢量,返回世界空间下的三维矢量 |
float3 UnityWorldToObjectDir(float3 worldDir) | 输入一个世界空间下的三维矢量,返回模型空间下的三维矢量 |