笔记都是照着“Unity Shader入门精要”摘抄的,为以后想要复习实现方便。
标准光照模型
上面是考虑光照时抽象出的背景情况,光照模型Lighting Model
就是用于根据材质属性
、光源信息
等得出在某个观察方向
的出射度
的过程。也就是着色(Shading)
过程。
标准光照模型使用自发光
、环境光
、高光反射
和漫反射
来叠加计算出摄像头看到的物体的光(颜色)
环境光和自发光
环境光是一个全局变量,在Unity中设置,
在Shader中可以通过UNITY_LIGHTMODEL_AMBIENT
获取。
自发光暂时不考虑:
考虑直接自发光很简单,直接在片元着色器最后的颜色输出中添加即可,
考虑间接自发光较难,因为需要计算其它物体对自发光物体光源
的反射。
漫反射光照
基本光照模型中,漫反射光照的计算公式:
入射光线的颜色和强度:
c
l
i
g
h
t
c_{light}
clight,
材质的漫反射系数:
m
d
i
f
f
u
s
e
m_{diffuse}
mdiffuse,
表面法线:
n
^
\hat{n}
n^,
光源方向:
I
I
I。
逐顶点光照
Shader "Custom/DiffuseVertex"
{
Properties{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader{
Pass{
Tags{"LightMode" = "ForwardBase"}//定义了正确的LightMode,才能得到Unity内置的光照变量
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //使用内置库文件
fixed4 _Diffuse; //得到材质的漫反射属性,需要使用Properties中声明的属性,要定义一个类型相通的同名变量
//顶点、片元着色器的输入输出结构体
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)_World2Object));//将模型法向量转化到世界坐标系下,使用右乘,避免逆
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);//得到世界光源的光线向量(假设只有一个光源且使用平行光)
//根据上述公式,计算漫反射。这里max函数用saturate函数代替,功能为将参数截取到[0,1]区间
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
}
}
}
效果是这样的:
因为是逐顶点计算,可见有比较明显的锯齿状的颜色过渡:
逐片元光照
代码和上面很相近,但是片元着色器承担了计算的职责。
虽然顶点传给片元的各种法向量等值没有变化,但是
n
^
∗
I
\hat{n}*I
n^∗I的会有细微变化。
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Custom/DiffusePixel"
{
Properties{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader{
Pass{
Tags{"LightMode" = "ForwardBase"}//定义了正确的LightMode,才能得到Unity内置的光照变量
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //使用内置库文件
fixed4 _Diffuse; //得到材质的漫反射属性,需要使用Properties中声明的属性,要定义一个类型相通的同名变量
//顶点、片元着色器的输入输出结构体
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f{
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));
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}
ENDCG
}
}
}
可以看到非常明显的进步
半兰伯特模型
但是可以看到,在背光面,由于saturate
函数,让点积为负的顶点/片元都取0值,
于是看起来完全就是一个同色的平面了:
一种解决方法就是使用半兰伯特模型
,本质上就是改进一下saturate
函数,让点积为负的也能映射到[0, 1]区间上——实际就是做了个缩放平移:
当然这里的系数0.5
可以换成别的。
只需要将片元着色器代码中计算diffuse的那一行改一下:
从
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
改到
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);
效果是这样的:
高光反射
基本光照模型中,高光反射公式:
入射光线的颜色和强度:
c
l
i
g
h
t
c_{light}
clight,
材质的高光反射系数——颜色:
m
s
p
e
c
u
l
a
r
m_{specular}
mspecular,
材质的高光反射系数——反射区域大小:
m
g
l
o
s
s
m_{gloss}
mgloss,
视角方向:
v
^
\hat{v}
v^,
反射方向:
r
r
r。
其中反射方向的计算公式为:
r
=
2
∗
(
n
^
⋅
I
^
)
∗
n
^
−
I
^
r = 2 * (\hat{n}·\hat{I}) * \hat{n} - \hat{I}
r=2∗(n^⋅I^)∗n^−I^
而且可以直接用CG中的reflect(i, n)
计算(i为入射方向,n为法线方向)。
逐片元光照
内含漫反射
Shader "Custom/DiffuseLambert"
{
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,才能得到Unity内置的光照变量
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;
float3 worldPos;
};
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);//将模型法向量转化到世界坐标系下
o.worldPos = mul(v.vertex, _World2Object).xyz;//将顶点坐标转化到世界坐标系下,为了得到视角方向
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 * (dot(worldNormal, worldLight) * 0.5 + 0.5);
//上面计算了漫反射,下面计算高光反射
fixed3 reflectDir = normalize(reflect(worldLight, worldNormal));//反射方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);//向量的差,得到模型到摄像机的方向向量
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color, 1.0);
}
ENDCG
}
}
}
效果,可以看到一个漂亮的光圈:
上面的代码整体就实现了一个完整地Phong
光照模型。
Bling-Phong模型
主要是计算高光反射使用Bling的算法:
其中
h
^
\hat{h}
h^计算方式是:
其它基本一致。
使用Unity内置的函数
上面基本上就是光照模型的底层原理。但是实在是理想的情况下得到的——只考虑单光源且平行光。
实际为了正确性,在计算光源和向量变换中,会使用Unity内置的函数。