我们做光栅化模式的渲染都了解有两种比较常用的渲染方式,一个是blinnphong的渲染,一个是pbr的渲染。
blinnphong:
blinnphong的渲染模式更多的是一种经验值模拟光照对物体的效果。所以他不是一个正确的能量守恒的渲染方式。
blinnphong的渲染公式
其中是漫反射的颜色值,也就是我们的贴图乘颜色。是高光图乘高光颜色
是顶点法线与光线方向的点积,主要要得到光线与法线的角度,决定漫反射的颜色。
是顶点法线与半角向量的点积,主要得到光线照射到视角范围的角度。
原理:
为什么需要H呢?
按照现实的光照显示,应该是光线射到地面上每个分子然后进行反射或散射后吸收热量然后最终反射出来的光线。但是如果按照这个方式去判断究竟显示的是什么内容的话,那计算量会非常大。
而我们其实只关心的是射入我们眼球范围内的光照,那么我们就需要寻找跟我们眼球反射有关的物体反射信息,所以用到了半角向量,半角向量是通过入射光线到点、射线方向和点相加后得到的向量,实际上他反映了是与视线和灯光相关的法线信息。
那么我们要判断指定部位的光照强度就可以用半角向量与顶点的法线向量做点乘后得出。点乘值越大说明法线与半角向量越重合,那么高光肯定越高。
那么这里可能会考虑另一个问题,正常来说法线向量和半角向量重叠才能说明光线L射入了视线V中。为什么用来表示呢?因为点乘可能会再-1到1之间。
是因为我们真实世界是每个分子都能做该运算,然后照到瞳孔接收光线范围内就让我们看到了物体的颜色。但再光栅化下,每个像素可能对应的是多个分子,我们要做的只能是得出该像素下分子反射的平均值,如果我们要L和V的法线完全与H重叠的话,会导致一些像素上完全没有高光,看起来是不自然的。所以我们再光栅化下接受了点乘再-1到1之间的浮点数值来表示他再该像素上高光的比值。
说完参数原理,我们可以开始带入具体的参数来实现我们的效果了。
这里跟有关系的原因是,我们知道圆面积是,已经单位圆的半径r=1,所以单位圆面积就是。
我们做的颜色控制都是再对单元圆做的,所以需要用颜色除单位圆面积得到具体面积内的颜色。
然后需要说下m,m是一个高光聚焦强度的值,m越大越聚焦,反之越小。同样需要跟单位圆做运算。可以试着把值带进去就知道效果了。
之后的Cspec就是高光的颜色信息,没什么可说的了。
代码:
Shader "Custom/BlinnPhong"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_SpecTex("SpecTex", 2D) = "white" {}
_SpecColor("SpecColor", Color) = (1,1,1,1)
_SpecM("Spec M", Range(0.0, 10.0)) = 0.5
}
SubShader{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent"}
Pass
{
Tags{ "LightMode" = "ForwardBase"}//设置光照类型
Blend SrcAlpha OneMinusSrcAlpha //开启颜色混合模式
CGPROGRAM
#pragma vertex vert //vextex着色器阶段
#pragma fragment frag //fragment着色器阶段
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
sampler2D _SpecTex;
float4 _SpecTex_ST;
fixed4 _SpecColor;
fixed _SpecM;
//定义输入顶点着色器阶段的数据结构
struct Input
{
float4 vertex : POSITION; //顶点位置
float4 texcoord : TEXCOORD0; //纹理座标
float4 normal : NORMAL;
float2 uv : TEXCOORD1;
};
//定义顶点着色器阶段输出的数据结构
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 normalWorld : TEXCOORD1;
float4 posWorld : TEXCOORD2;
};
//输出v2f到下一渲染阶段
v2f vert(Input v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float4 posWorld = mul(unity_ObjectToWorld, v.vertex);
float3 normalWorld = UnityObjectToWorldNormal(v.normal);
o.uv = v.uv;
o.normalWorld = normalWorld;
o.posWorld = posWorld;
return o;
}
fixed4 frag(v2f i) :SV_TARGET
{
float3 lightDirWorld = normalize(_WorldSpaceLightPos0.xyz - i.posWorld.xyz);
half3 viewDirWorld = normalize(_WorldSpaceCameraPos - i.posWorld.xyz);
half NdotL = saturate(dot(i.normalWorld, lightDirWorld));
half3 halfDir = normalize(lightDirWorld + viewDirWorld);
half NdotH = saturate(dot(i.normalWorld, halfDir));
float4 mainTex = tex2D(_MainTex, i.uv) * _Color;
float4 specTex = tex2D(_SpecTex, i.uv) * _SpecColor;
half pi = 3.1415926;
float3 color = (mainTex.rgb / pi + (_SpecM + 8) / (8 * pi) * specTex.rgb * (NdotH * NdotH * NdotH * NdotH * NdotH)) * NdotL;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
总结:
blinnphong是一个前人做出来的经验值算法,这个算法因为简单,运算低,所以不需要真实渲染或要求比较低的话可以用这个公式代替渲染方程。而且因为不需要真实渲染,这里也可以只用用GAMMA空间就足够了,不需要用到线性空间。整体运算量还是比较小的。
PBR:
因为BlinnPhong是一些渲染的经验模型,所以他的适用范围就比较受限,那么考虑到一个更通用,更合理的运算公式。一个基于物理平衡的渲染公式自然就被研究出来了。
原理:
PBR运算的是一个像素内光照反射和折射后的信息比值。
既然是基于物理平衡的真实渲染,那么pbr自然就有自己的一套理念(借用一下别人的总结):
- 微平面理论(Microfacet Theory)。微平面理论是将物体表面建模成做无数微观尺度上有随机朝向的理想镜面反射的小平面(microfacet)的理论。在实际的PBR 工作流中,这种物体表面的不规则性用粗糙度贴图或者高光度贴图来表示。
- 能量守恒(Energy Conservation)。出射光线的能量永远不能超过入射光线的能量。随着粗糙度的上升镜面反射区域的面积会增加,作为平衡,镜面反射区域的平均亮度则会下降。
- 菲涅尔反射(Fresnel Reflectance)。光线以不同角度入射会有不同的反射率。相同的入射角度,不同的物质也会有不同的反射率。万物皆有菲涅尔反射。F0是即 0 度角入射的菲涅尔反射值。大多数非金属的F0范围是0.02~0.04,大多数金属的F0范围是0.7~1.0。
- 线性空间(Linear Space)。光照计算必须在线性空间完成,shader 中输入的gamma空间的贴图比如漫反射贴图需要被转成线性空间,在具体操作时需要根据不同引擎和渲染器的不同做不同的操作。而描述物体表面属性的贴图如粗糙度,高光贴图,金属贴图等必须保证是线性空间。
- 色调映射(Tone Mapping)。也称色调复制(tone reproduction),是将宽范围的照明级别拟合到屏幕有限色域内的过程。因为基于HDR渲染出来的亮度值会超过显示器能够显示最大亮度,所以需要使用色调映射,将光照结果从HDR转换为显示器能够正常显示的LDR。
- 物质的光学特性(Substance Optical Properties)。现实世界中有不同类型的物质可分为三大类:绝缘体(Insulators),半导体(semi-conductors)和导体(conductors)。在渲染和游戏领域,我们一般只对其中的两个感兴趣:导体(金属)和绝缘体(电解质,非金属)。其中非金属具有单色/灰色镜面反射颜色。而金属具有彩色的镜面反射颜色。即非金属的F0是一个float。而金属的F0是一个float3,如下图。
渲染公式
Le是自发光,一些物体可能会有自发光的信息或者有自发光的图片,通过这个可以得到本身发光物体的颜色信息。
fr是入射与出射之间的反射比例,一般有BRDF,BTDF,BSDF(就是BRDF+BTDF),BSSRDF等。
Li是入射光亮度
是法线与反射的点乘,得出的值可以表示入射光的衰减
是入射方向的半球积分,可以立即理解为累计的数据值。
近似解
通过蒙特卡洛积分的方式,通过N项相加可以求出积分的近似解。
=*
环境光照
其中是环境光照,我们可以用cubemap 来代替=Cubemap.sample(r.mip)
BRDF
然后我们可以根据高光的BRDF来算自身颜色
其中需要说明的是VDF
D是法线分布函数,相当于是确定再一个像素内所有分子的法线分布相关信息的均值。体现得更好得法线分布函数会有更好得高光长尾。
F是Fresnel的简写,是一个反射与折射的关系式,我们现实环境中看物体,比如水面,垂直看下来会比较容易看到水底下的物体,是因为折射大于反射。越接近平面的看水面会看到反射的内容越多,这是因为反射大于折射。
V是一个结合函数(其实应该是G(Gemetry Function))这里描述的是被自身遮挡的信息。
三种情况会导致遮挡,一是光照照射不到的地方,二是视图方向看不到的物体信息,三是反射后进入视线的信息。
公式分别是:
其中F0是反射的值,分为导体和电解质,导体一般会有rgb三个不同的颜色,而电解质一般rgb三个颜色是一样的值。默认的电解质为(0.4,0.4,0.4),但也有可能一个像素内出现金属和非金属,所以我们需要用一个公式来近似这个解(金属比值加上非金属比值):(Metallic是当前像素的金属度)
,这里是对光照和对视线方向都做了GGX运算结合起来得到视线与光照方向的遮蔽信息。下面除于是对运算的值做光照方向和视线方向的纠正的值
公式为:,其中k为
最后为了能量守恒,应该要把能量消耗的损失加上:
也就是前面公式下的Kdiff和Kspec。
Kspec描述的是有多少光的能灵被高光反射,也就是前面的F0。
Kdiff则是高光吸收后剩下的光线消耗:
代码:
Shader "Custom/BRDF"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_SpecTex("SpecTex (RGB)", 2D) = "white" {}
_SpecColor("SpecColor", Color) = (1,1,1,1)
_Roughness("Roughness", Range(0.0, 10.0)) = 1
_Albedo("Albedo (RGB)", 2D) = "white" {}
_Metallic("Metallic", Range(0.0, 10.0)) = 1
_GGX_V_Transition("GGX V Transition", Range(0.0001, 10.0)) = 0.0001
}
SubShader{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent"}
Pass
{
Tags{ "LightMode" = "ForwardBase"}//设置光照类型
Blend SrcAlpha OneMinusSrcAlpha //开启颜色混合模式
CGPROGRAM
#pragma vertex vert //vextex着色器阶段
#pragma fragment frag //fragment着色器阶段
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
sampler2D _SpecTex;
sampler2D _Albedo;
fixed _Metallic;
fixed4 _SpecColor;
fixed _Roughness;
half _GGX_V_Transition;
//定义输入顶点着色器阶段的数据结构
struct Input
{
float4 vertex : POSITION; //顶点位置
float4 texcoord : TEXCOORD0; //纹理座标
float4 normal : NORMAL;
float2 uv : TEXCOORD1;
};
//定义顶点着色器阶段输出的数据结构
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 normalWorld : NORMAL;
float4 posWorld : TEXCOORD1;
};
//输出v2f到下一渲染阶段
v2f vert(Input v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float4 posWorld = mul(unity_ObjectToWorld, v.vertex);
float3 normalWorld = normalize(UnityObjectToWorldNormal(v.normal));
o.uv = v.uv;
o.normalWorld = normalWorld;
o.posWorld = posWorld;
return o;
}
half doubleNumber(half num)
{
return num * num;
}
half fiveNumber(half num)
{
return num * num * num * num * num;
}
half GGX(half dotX)
{
half k = doubleNumber(_Roughness + 1) / 8;
return dotX / (k + (1 - k) * dotX);
}
fixed4 frag(v2f i) :SV_TARGET
{
half3 lightDirWorld = normalize(_WorldSpaceLightPos0.xyz - i.posWorld.xyz);
half3 viewDirWorld = normalize(_WorldSpaceCameraPos - i.posWorld.xyz);
half3 halfDir = normalize(lightDirWorld + viewDirWorld);
half NdotL = saturate(dot(i.normalWorld, lightDirWorld));
half NdotH = saturate(dot(i.normalWorld, halfDir));
half NdotV = saturate(dot(i.normalWorld, viewDirWorld));
float4 mainTex = tex2D(_MainTex, i.uv) * _Color;
float4 specTex = tex2D(_SpecTex, i.uv) * _SpecColor;
float3 metalliTex = tex2D(_Albedo, i.uv).rgb;
half pi = 3.1415926;
half D = doubleNumber(_Roughness) / (pi * doubleNumber(doubleNumber(NdotH) * (doubleNumber(_Roughness) - 1) + 1));
half3 F0 = metalliTex * _Metallic + (1 - _Metallic) * half3(0.04, 0.04, 0.04);
half F = F0 + (1 - F0) * (1 - fiveNumber(NdotH));
half lambertNL = NdotL * 0.5 + 0.5;
half V = (GGX(NdotL) * GGX(NdotV)) / (4 * lambertNL * NdotV);
half kdiff = F0;
half kSpec = (1 - F0) * (1 - _Metallic);
float3 brdf = (kdiff * mainTex.rgb / pi + kSpec * V * F * D);
return fixed4(brdf.rgb, 1);
}
ENDCG
}
}
}
参考资料