- 前5章主要是基础3D数学和Shader基本语法后面抽空再补上
第6章 光照模型
- 本章主要内容光照模型:Lambert、Half-Lambert、Phong、Blinn-Phong等
Lambert光照模型
物体的颜色是由射进眼睛里的光决定的,有反射光和透视光
- 漫反射满足Lambert定律:
反射光的强度与表面法线和光源方向夹角成正比
,公式如下
Cdiffuse=(Clight⋅Mdiffuse)saturate(n⋅l)C_{diffuse}=(C_{light}·M_{diffuse})saturate(n·l)Cdiffuse=(Clight⋅Mdiffuse)saturate(n⋅l)
ClightC_{light}Clight为入射光线颜色,MdiffuseM_{diffuse}Mdiffuse为材质漫反射颜色,lll为从物体指向光的方向,根据点乘可以知道当nnn和lll夹角越小漫反射强度就越大,但是当Cos夹角大于90为负,所以使用saturate函数限定至(0,1)
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float3 n = UnityObjectToWorldNormal(v.normal);
n = normalize(n);
// _WorldSpaceLightPos0和_LightColor0包含在UnityLightingCommon.cginc文件中
// l处在世界空间中,所以法向量需要转换
fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
fixed ndotl = dot(n,l);
// Lambert公式
o.diff = _LightColor0 * _MainColor * saturate(ndotl);
return o;
}
Half-Lambert光照模型
- 可以法线背对着光源,直接就是黑色的,所以提出了Half-Lambert,直接看下面公式
Cdiffuse=(Clight⋅Mdiffuse)[0.5(n⋅l)+0.5]C_{diffuse}=(C_{light}·M_{diffuse})[0.5(n·l)+0.5]Cdiffuse=(Clight⋅Mdiffuse)[0.5(n⋅l)+0.5]
这么看(n⋅l)∈[−1,1](n·l)∈[-1,1](n⋅l)∈[−1,1],[0.5(n⋅l)+0.5]∈[0,1][0.5(n⋅l)+0.5]∈[0,1][0.5(n⋅l)+0.5]∈[0,1]结果好像没啥区别,因为saturate也是[0,1]区间,但是仔细看一下saturate直接把(n⋅l)(n·l)(n⋅l)小于0的部分全部给抹掉了,这意味小于90度漫反射强度在[0,1],大于90度的全黑,而Half-Lambert公式并不像Lambert这么暴力
具体看下面两张图
Phong光照模型
- Lambert可以很好的模拟粗糙表面,但是现实生活中还有很多光滑的表面如金属、陶瓷等
CsurfaceColor=CAmbient+CDiffuse+CSpecularC_{surfaceColor}=C_{Ambient}+C_{Diffuse}+C_{Specular}CsurfaceColor=CAmbient+CDiffuse+CSpecular
CAmbientC_{Ambient}CAmbient为环境光、CSpecularC_{Specular}CSpecular为镜面反射,因为光照射到光滑表面还会反射一部分光
CSpecular=(Clight⋅Mspecular)saturate(v⋅r)MshininessC_{Specular}=(C_{light}·M_{specular})saturate(v·r)^{M_{shininess}}CSpecular=(Clight⋅Mspecular)saturate(v⋅r)Mshininess
MspecularM_{specular}Mspecular物体材质的镜面反射颜色、vvv是视角方向(顶点指向摄像机)、rrr是光线反射的方向、MshininessM_{shininess}Mshininess材质的光泽度 可以从UnityShaderVariables.cginc得到环境光变量
,具体代码如下
Shader "Unlit/Phong"
{
Properties
{
_MainColor("MainColor", Color) = (1,1,1,1)
// 材质镜面反射颜色
_SpecularColor("SpecularColor", Color) = (1,1,1,1)
// 材质光泽度
_Shininess("Shininess", Range(1,100)) = 1
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos : SV_POSITION;
fixed4 color:COLOR0;
};
fixed4 _MainColor;
fixed4 _SpecularColor;
half _Shininess;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float3 n = UnityObjectToWorldNormal(v.normal);
n = normalize(n);
fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
fixed ndotl = saturate(dot(n,l));
// 漫反射
fixed4 diff = _LightColor0 * _MainColor * ndotl;
// 计算光的反射方向,l计算从顶点到光源的方向
// 计算反射reflect需要的入射光线所以为-l
float3 r = reflect(-l, n);
r = normalize(r);
// 顶点指向摄像机
fixed3 view = WorldSpaceViewDir(v.vertex);
view = normalize(view);
fixed vdotr = saturate(dot(view, r));
// 镜面反射公式
fixed4 spec = _LightColor0 * _SpecularColor * pow(vdotr, _Shininess);
// phong公式
o.color = unity_AmbientSky + diff + spec;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return i.color;
}
ENDCG
}
}
}
逐像素光照
- 在上面Phong的材质转动视角会发现一个问题,镜面反射光并不光滑会出现棱角,如下图
这是因为上面代码是逐顶点光照而不是逐像素光照,逐顶点光照就是对每个顶点计算颜色,然后根据顶点在多边形占据的面积进行线性插值,这在三角面多的模型中没什么大问题,在三角面不是很多的模型上就会出现棱角,就像上面Unity默认球体模型
- 那什么又是逐像素呢?就是在片元着色器中计算,颜色的计算不再基于顶点,而是基于像素,最终屏幕越大计算量越大
具体可以看下图,上面是逐片元,下面是逐顶点可以很明显看出不同,代码和逐顶点其实没有什么太大不同
下面是逐像素光照
Shader "Unlit/FragPhong"
{
Properties
{
_MainColor("MainColor", Color) = (1,1,1,1)
// 材质镜面反射颜色
_SpecularColor("SpecularColor", Color) = (1,1,1,1)
// 材质光泽度
_Shininess("Shininess", Range(1,100)) = 1
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float3 normal:TEXCOORD0;
// 用来计算view所以需要将模型空间顶点坐标传给片元着色器
float4 vertex:TEXCOORD1;
};
fixed4 _MainColor;
fixed4 _SpecularColor;
half _Shininess;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal = v.normal;
o.vertex = v.vertex;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 n = UnityObjectToWorldNormal(i.normal);
n = normalize(n);
fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
fixed ndotl = saturate(dot(n,l));
// 漫反射
fixed4 diff = _LightColor0 * _MainColor * ndotl;
// 计算光的反射方向,l计算从顶点到光源的方向
// 计算反射reflect需要的入射光线所以为-l
float3 r = reflect(-l, n);
r = normalize(r);
// 顶点指向摄像机
fixed3 view = WorldSpaceViewDir(i.vertex);
view = normalize(view);
fixed vdotr = saturate(dot(view, r));
// 镜面反射公式
fixed4 spec = _LightColor0 * _SpecularColor * pow(vdotr, _Shininess);
// phong公式
return unity_AmbientSky + diff + spec;
}
ENDCG
}
}
}
Blinn-Phong光照模式
CSpecular=(Clight⋅Mspecular)saturate(n⋅h)MshininessC_{Specular}=(C_{light}·M_{specular})saturate(n·h)^{M_{shininess}}CSpecular=(Clight⋅Mspecular)saturate(n⋅h)Mshininess
Blinn-Phong不再依靠光反射向量rrr,而是依靠视角方向和灯光方向的角平分向量hhh计算,然后与法向量点乘,性能方面当观察者和灯光离物体很远时,Blinn可将h视为常量,而普通Phong需要根据表面曲率逐一去计算反射向量,Blinn计算效率远高于普通Phong
代码片段如下
fixed4 frag (v2f i) : SV_Target
{
float3 n = UnityObjectToWorldNormal(i.normal);
n = normalize(n);
fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
fixed ndotl = saturate(dot(n,l));
// 漫反射
fixed4 diff = _LightColor0 * _MainColor * ndotl;
// 顶点指向摄像机
fixed3 view = WorldSpaceViewDir(i.vertex);
view = normalize(view);
// Blinn通过计算视角方向和光方向的角平分线向量
float3 h = normalize(l + view);
fixed ndoth = saturate(dot(n, h));
// 镜面反射公式
fixed4 spec = _LightColor0 * _SpecularColor * pow(ndoth, _Shininess);
// phong公式
return unity_AmbientSky + diff + spec;
}
渲染路径
-
当显卡无法使用选定的渲染路径时,Unity将会选择较低精度的渲染路径,例如选择延迟渲染无法执行,将会选择前向渲染
-
延迟渲染路径:
– 灯光Pass基于G-Buffer(屏幕空间缓存)和深度计算光照,与场景复杂度无关,可以避免计算因未通过深度测试而被丢弃的片段,所有灯光都可以使用阴影和Cookie
– 延迟渲染不支持真正的抗锯齿、处理半透明物体,使用延迟渲染的模型不支持接收阴影选项、Culling Mask只能再限定条件下使用,并且只能有4个
– 延迟渲染只能再MRT(多重渲染目标)、Shader Model3或以上并支持深度渲染贴图的显卡运行,移动端可以在OpenGL ES3.0或以上设备运行,并且延迟渲染不支持正交投影模式摄像机 -
前向渲染路径:
– 前向渲染支持Unity所有图形功能,一部分最亮的灯光以完全逐像素渲染,4个点光源以逐顶点方式渲染,其余灯光使用更加快速的SH(球谐)光照渲染
–灯光的渲染方式具体以下决定
–Not Important总是以逐顶点或SH
– Important总是以逐像素
– 最亮的平行光总是以逐像素
– 如果逐像素灯光少于项目质量设置中Pixel Light数量,其余比较亮的灯光以逐像素渲染 -
Unity官方例子,如下图
-
基础Pass包含一个逐像素的平行光和所有逐顶点或SH灯光,并且包含所有来自Shader光照贴图、环境光、自发光,平行光能投射阴影,但灯光贴图不能接受SH灯光照明
-
其他逐像素灯光,每个灯光会产生额外的Pass,前线渲染只支持一个投射阴影的平行光,额外Pass中灯光默认不会投射阴影, 需要支持的话需要添加变体编译
实现阴影效果
Shader "Unlit/ShadowLambert"
{
Properties
{
_MainColor("MainColor", Color) = (1,1,1,1)
}
SubShader
{
// 基础Pass 平行光产生阴影
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 在当前pass渲染的每个灯光编译出不同Shader变体
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct v2f
{
float4 pos:SV_POSITION;
float3 normal:TEXCOORD0;
float4 vertex:TEXCOORD1;
//使用预定义宏保存阴影坐标 2代表TEXCOORD2 这个宏在AutoLight.cginc
SHADOW_COORDS(2)
};
fixed4 _MainColor;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal = v.normal;
o.vertex = v.vertex;
// 这个宏也定义在AutoLight.cginc,变换阴影贴图的纹理坐标存入结构体o中
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 n = UnityObjectToWorldNormal(i.normal);
n = normalize(n);
// 原来这样计算光源方向fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
// 使用_WorldSpaceLightPos0时,会导致点光源和聚光灯位置对光照效果不起作用
// i.vertex是float4
float3 l = WorldSpaceLightDir(i.vertex);
l = normalize(l);
float4 worldPos = mul(unity_ObjectToWorld, i.vertex);
// lambert
fixed ndotl = saturate(dot(n, l));
fixed4 c = _LightColor0 * _MainColor * ndotl;
// 加4个点光源光照
c.rgb += Shade4PointLights(
//4个点光源坐标、颜色、衰减、世界坐标、世界法线
unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,
unity_LightColor[0].rgb,unity_LightColor[1].rgb,
unity_LightColor[2].rgb,unity_LightColor[3].rgb,
unity_4LightAtten0, worldPos.rgb, n) * _MainColor;
// 环境光
c += unity_AmbientSky;
// 计算阴影系数
UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb);
c.rgb += shadowmask;
return c;
}
ENDCG
}
// 为除主光外其他逐像素灯光产生投影
Pass
{
Tags{"LightMode" = "ForwardAdd"}
// 使绘制的图像与上面的Pass完全混合
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd_fullshadows
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct v2f{
float4 pos :SV_POSITION;
float3 normal :TEXCOORD0;
float4 vertex : TEXCOORD1;
SHADOW_COORDS(2)
};
fixed4 _MainColor;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal = v.normal;
o.vertex = v.vertex;
// 这个宏也定义在AutoLight.cginc,变换阴影贴图的纹理坐标存入结构体o中
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 n = UnityObjectToWorldNormal(i.normal);
n = normalize(n);
// 原来这样计算光源方向fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
// 使用_WorldSpaceLightPos0时,会导致点光源和聚光灯位置对光照效果不起作用
// i.vertex是float4
float3 l = WorldSpaceLightDir(i.vertex);
l = normalize(l);
float4 worldPos = mul(unity_ObjectToWorld, i.vertex);
// lambert
fixed ndotl = saturate(dot(n, l));
fixed4 c = _LightColor0 * _MainColor * ndotl;
// 加4个点光源光照
c.rgb += Shade4PointLights(
//4个点光源坐标、颜色、衰减、世界坐标、世界法线
unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,
unity_LightColor[0].rgb,unity_LightColor[1].rgb,
unity_LightColor[2].rgb,unity_LightColor[3].rgb,
unity_4LightAtten0, worldPos.rgb, n) * _MainColor;
// 环境光
c += unity_AmbientSky;
// 计算阴影系数
UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb);
c.rgb += shadowmask;
return c;
}
ENDCG
}
}
// 阴影投射Pass
FallBack "Diffuse"
}