Unity Shader入门精要读书笔记系列
文章目录
- 前言
- 一、什么是光照模型
- 1.BRDF光照模型
- 2.标准光照模型
- 二、Unity中实现光照模型
- 1.漫反射
- 兰伯特(Lambert)模型
- 半兰伯特(Half Lambert)模型
- 效果对比
- 2.高光反射
- Phone模型
- Blinn-Phong模型
- 效果对比
- 三、使用Unity内置的函数
前言
上一章我们学习了了Unity中的Shader基本语法,和Unity内置的一些结构体、函数和宏。
接下来我们继续学习Unity中基础光照模型。
一、什么是光照模型
辐照度:用于量化光,对于平行光 l。辐照度指的是垂直于 l的单位面积内单位时间穿过的光的能量。在计算光照模型时,我们需要知道一个物体表面的辐照度。
不难想象,光的入射方向反方向越垂直于物体表面(或者说与表面法向量夹角越小)辐照度越大。
漫反射:光照在粗糙的表面上,由于各点的法线方向不一致,反射光向不同的方向无规则的反射。
高光反射:物体表面都有相对光滑的镜面,面积越小越光滑,反射光线相对越集中,高光就越强。
根据材质属性(如漫反射属性)、光源信息(如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度。我们把这个等式称为光照模型(Lighting Model),这个过程叫做着色 (shading) 。
1.BRDF光照模型
BRDF (Bidirectional Reflectance Distribution Function) 双向反射分布函数。
对于某一点,当给出入射方向和辐照度后。可以通过BRDF计算出反射光在不同方向上的反射率。
对于人眼来说,即在不同方向看到这个点的颜色不同。
BDRF光照模型分为经验模型和基于物理的分析模型,本章主要是学习经验模型。
经验模型并不能真实地反映物体和光线之间的交互,只是看起来正确。
2.标准光照模型
color = 自发光 (emissive) + 环境光(ambient ) + 漫反射(diffuse ) + 高光反射(specular)
二、Unity中实现光照模型
1.漫反射
兰伯特(Lambert)模型
- 逐顶点光照
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Unity Shaders Book/Chapter 6/Diffuse VertexLevel"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100 //Level of detail 当LOD的值小于设定值时,相应的shader不会工作
Pass
{
// 只有定义了正确的 LightMode 才能得到一些 Unity 的内置光照变量
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "Lighting.cginc"
#include "UnityCG.cginc"
// 材质的漫反射属性
fixed4 _Diffuse;
struct a2v
{
// POSITION语义告诉Unity, 用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
// NORMAL语义告诉Unity, 用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
};
struct v2f
{
// SV_POSITION语义告诉Unity, pos里包含了顶点在裁剪空间中的位置信息
// 顶点着色器的输出结构中,必须包含 SV_POSITION
float4 pos : SV_POSITION;
// COLORO语义可以用于存储颜色信息
float3 color : COLOR0;
};
// 使用顶点着色器计算漫反射光照模型
v2f vert (a2v v)
{
// 声明输出变量
v2f o;
// 将顶点位置从模型空间转换到裁剪空间(使用MVP矩阵右乘v.vertex)
o.pos = UnityObjectToClipPos(v.vertex);
// 使用内置变量 UNITY_LIGHTMODEL_AMBIENT 获取环境光部分
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 将法线从模型空间转换到世界空间,使用normalize函数归一化
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// 使用内置变量 _WorldSpaceLightPos0 获取光源方向
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 兰伯特漫反射光照公式,使用内置变量 _LightColor0 获取光源颜色和强度信息
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"
}
- 逐像素光照
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Unity Shaders Book/Chapter 6/Diffuse PixelLevel"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100 //Level of detail 当LOD的值小于设定值时,相应的shader不会工作
Pass
{
// 只有定义了正确的 LightMode 才能得到一些 Unity 的内置光照变量
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "Lighting.cginc"
#include "UnityCG.cginc"
// 材质的漫反射颜色
fixed4 _Diffuse;
struct a2v
{
// POSITION语义告诉Unity, 用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
// NORMAL语义告诉Unity, 用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
};
struct v2f
{
// SV_POSITION语义告诉Unity, pos里包含了顶点在裁剪空间中的位置信息
// 顶点着色器的输出结构中,必须包含 SV_POSITION
float4 pos : SV_POSITION;
// 世界空间下的法线,自定义数据一般用 TEXCOORD0 语义
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"
}
半兰伯特(Half Lambert)模型
在兰伯特模型中,漫反射颜色在[-1,1]。通过saturate函数将负数全部改为0,即背光区域全部为黑色,效果不好。于是半兰伯特技术被提出。
大多数情况下α,β的值为0.5,他将漫反射颜色[-1,1]映射到[0,1]区间内。
这样即使是背光区域,也有了明暗变化,不会呈现完全死黑的现象。
// 在片元着色其中修改漫反射的计算公式
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (dot(worldNormal, worldLightDir) * 0.5 + 0.5);
效果对比
从左到右依次为逐顶点、逐像素、半兰伯特光照。
逐顶点光照(per-vertex lighting)
逐顶点光照也被称为高洛德着色(Gouraud shading)。在每个顶点上进行光照计算,然后在渲染图元内部进行线性插值,最后输出成像素颜色。
由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。
但是,由于逐顶点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性的计算(例如计算高光反射时)时,逐顶点光照就会出问题。
并且图元内部的颜色总是暗于顶点处的最高颜色值,某些情况下会产生明显的棱角现象。
逐像素光照(per-pixel lighting)
逐像素光照中以每个像素为基础,得到它的法线(对顶点法线插值得到或者从法线纹理中采样得到),然后进行光照计算。
这种着色技术称为Phong着色(Phong shading),或者Phong插值或法线插值着色技术。(不同于Phong光照模型)。
一般来说逐像素着色比逐顶点着色更加丝滑,同时计算量更大。
2.高光反射
Phone模型
max函数是为了防止视角方向v与反射方向r点乘出现负数。
- 逐顶点光照
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Unity Shaders Book/Chapter 6/SpecularVertexLevel"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
// 用于控制材质的高光反射颜色
_Specular ("Specular", Color) = (1, 1, 1, 1)
// 用于控制高光区域的大小
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
// 只有定义了正确的 LightMode 才能得到一些 Unity 的内置光照变量,例如 _LightColorO
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
{
// POSITION语义告诉Unity, 用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
// NORMAL语义告诉Unity, 用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
// COLORO语义可以用于存储颜色信息
float3 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);
// 漫反射部分 _LightColor0 光源颜色和强度信息
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// 使用 reflect 计算反射方向
fixed3 reflectDir = normalize(reflect(- worldLightDir, worldNormal));
// 视角方向,世界空间下的摄像机坐标减去顶点坐标
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
// 高光反射计算公式
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
}
}
}
- 逐像素光照
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Unity Shaders Book/Chapter 6/SpecularPixelLevel"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
// 用于控制材质的高光反射颜色
_Specular ("Specular", Color) = (1, 1, 1, 1)
// 用于控制高光区域的大小
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
// 只有定义了正确的 LightMode 才能得到一些 Unity 的内置光照变量,例如 _LightColorO
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
// _Gloss范围较大,使用float存储
float _Gloss;
struct a2v
{
// POSITION语义告诉Unity, 用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
// NORMAL语义告诉Unity, 用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
};
struct v2f
{
// 顶点着色器必须输出裁剪空间下的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 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
}
}
}
Blinn-Phong模型
修改片元着色器中高光反射的计算
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
可以想象,看到当视角与光反射方向越贴近时,视角与光方向的半角h与法线n夹角越小,高光就越亮。
效果对比
从左到右分别是逐顶点高光、逐像素高光、BlinnPhong高光。
由于逐顶点中使用了线性插值,可以看到效果不是很好。
三、使用Unity内置的函数
从上面的代码中可以看出,手动计算光源信息比较麻烦。Unity提供了一些内置函数来帮助我们计算这些信息。
使用内置函数改写Blinn-Phong
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Unity Shaders Book/Chapter 6/BlinnPhongUseBuildInFunction"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "Lighting.cginc"
#include "UnityCG.cginc"
float4 _Diffuse;
float4 _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 = UnityObjectToWorldNormal(v.normal); // 使用内置函数
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); //_WorldSpaceLightPos0只能用于平行光
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 使用内置函数,注意归一化
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
//fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); // 使用内置函数
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);
}
ENDCG
}
}
}