渲染方程的一些总结和简单实现
背景
看了很多PBR相关的文章了,虽然复杂,但似乎使用起来很简单的样子。
这儿写个简单版的PBR实现,留下点学习痕迹。
渲染方程
经典光照模型
1975年Phong提出Phong反射模型(Phong Reflection Model):
1977年Blinn对Phong模型做出修改,这就是后来广泛使用的Blinn-Phong反射模型:
一个简单的基于物理的Blinn-Phong:
辐射度量学
物理上辐射度量学的基本量及其关系(符号被我简化了,比如立体角和偏导):
名称 | 英文名 | 符号 | 单位 | 公式 | 补充描述 |
---|---|---|---|---|---|
辐射能量 | Radiant energy | 焦耳() | - | 电磁辐射能量 | |
辐射通量 | Radiant flux | 瓦() | 单位时间辐射的能量 | ||
辐==照==度 | Irradiance | 到达单位面积的辐射通量 | |||
辐射度 | Radiosity | ==离开==单位面积的辐射通量 | |||
辐射强度 | Radiant Intensity | 通过单位立体角的辐射通量 | |||
==辐射率== | Radiance | 不随距离变化 | |||
立体角 | Solid Angle | 角度的二维扩展, |
辐射率被用来量化单一方向上发射来的光线的大小或者强度,不随距离变化,人眼看到的颜色强度的度量单位就是这个。
PBR渲染方程
PBR的渲染方程(反射方程)一般长这样:
v是观察方向,l是光照方向,。
有些文章使用, 表示按向量的分量相乘,因为和都包含RGB三个分量。
辐射率与辐照度按定义有这样的关系:
双向反射分布函数BRDF的定义是:。
积分方程一般用蒙特卡洛方法近似计算,即便如此,也是不能实时计算,一般都是先离线预处理。知道的几个方法
- 离线计算积分结果,保存成类似cubemap,然后shader里根据方向采样。
- 离线计算3阶左右的球谐系数,使用球谐光照来快速计算积分。
理想光源的渲染方程
对于理想光源(点光源、方向光等),是没有意义的,比如平行光,只有一个方向有值,且值是无限大。
对于理想光源照射的物体来说,有意义的是辐照度,渲染方程退化成:
这个方程可以在shader里实时算。
BRDF
下面这个模型大概是叫Cook-Torrance BRDF模型吧。
首先BRDF分为漫反射和镜面反射两部分:
系数用来控制能量守恒的。
漫反射BRDF:
镜面反射BRDF:
n是表面法线
h就是前面提到的半角向量,在这儿它多了一个含义,微表面法线
法线分布函数,D(Normal Distribution Function,NDF):
α是表面的粗糙度(UE的文章定义,导致有些文章写错了,这儿直接就是roughness),n是表面法线。这个方程是大佬特地研究出来的。
菲涅尔方程,F(Fresnel equation)
菲涅尔现象是掠角越大,镜面反射越强,表现出边缘高光现象。
表示的是表面基础反射率,这个值区分了金属和非金属。金属大于0.5,非金属小于0.2,并且常见的非金属小于0.04。
这个值输入的方式一般是:金属的通过贴图输入,非金属的直接设置为0.04。
vec3 F0 = vec3(0.04);
F0 = lerp(F0, albedo, metallic);
漫反射系数
前面提到的系数与有关系。
令,就能达到能量守恒了:镜面反射+漫反射<=1,同时还减弱金属的漫反射。
几何遮挡函数,G(Geometry function)
整合后的PBR渲染方程
其中有各种不同的方案,disney、unity、unreal的渲染方程就有差别。
这儿只是列一个可行的方案。
关于实现
分两种情况
- 理想光源用累加方程实时算。
- 积分方程,知道有两种方式
- 采用预计算出纹理的方式,在shader里采样纹理计算。
- 球谐光照,预计算去球谐系数,然后shader里算球谐向量积。
PBR的渲染需要综合间接光才能效果不错。
关于线性相加
PBR需要在线性空间里计算,不同光源是可以线性相加的。当结果>1时,有专门的算法归一。
exposure 默认是1,可以设置,也可以根据上一帧的图像平均亮度平滑过渡。
可以看LearnOpenGL的评论区。
代码
Unity SimplePBRShader
只实现了一个,只支持一个平行光。
额外利用了Unity内置的一些功能:Shadow,SH环境光,SkyBox。
标准胶囊体的渲染接近Standard了,有那么个感觉了。
获取环境间接光的部分真是~~~下次搞懂吧。
Shader "SimplePBRShader"
{
Properties
{
_Color("Color", Color) = (1,1,1,1)
_MainTex("Albedo (RGB)", 2D) = "white" {}
[Gamma] _Metallic("Metallic", Range(0,1)) = 0
_Glossiness("Smothness", Range(0, 1)) = 0.5 // 方便对比Standard
//_Roughness("Roughness", Range(0, 1)) = 0.5
}
SubShader
{
Tags { "RenderType" = "Opaque"}
LOD 200
Pass{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// shadow on 不加默认是关闭的
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Metallic;
float _Glossiness;
static const float PI = 3.14159265359;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
SHADOW_COORDS(3)
UNITY_FOG_COORDS(4)
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);// 法线特殊处理
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
UNITY_TRANSFER_FOG(o, o.pos);
return o;
}
float BRDF_D(float3 N, float3 H, float roughness)
{
const float PI = 3.14159265359;
float a2 = roughness * roughness;
float a4 = a2 * a2;
float NdotH = max(dot(N, H), 0);
float t1 = (NdotH * NdotH) * (a4 - 1) + 1;
float t2 = PI * t1 * t1;
return a4 / t2;
}
float BRDF_G1(float3 N, float3 V, float k)
{
float NdotV = max(dot(N, V), 0);
return NdotV / (NdotV * (1 - k) + k);
}
float BRDF_G(float3 N, float3 V, float3 L, float roughness)
{
float k = (roughness + 1) * (roughness + 1) / 8.0;// Kdirect
float g1 = BRDF_G1(N, V, k);
float g2 = BRDF_G1(N, L, k);
return g1 * g2;
}
float3 BRDF_F(float3 H, float3 V, float3 F0)
{
float HdotV = max(dot(H, V), 0);
return F0 + (1 - F0) * pow(1 - HdotV, 5);
}
float3 BRDF_All(float3 N, float3 V, float3 L, float3 albedo, float metallic, float roughness)
{
float3 H = normalize(V + L);
float NdotL = max(dot(N, L), 0);
float NdotV = max(dot(N, V), 0);
float3 F0 = lerp(0.04, albedo, metallic);
float D = BRDF_D(N, H, roughness);
float3 F = BRDF_F(H, V, F0);
float G = BRDF_G(N, V, L, roughness);
float3 kD = 1 - F;
kD *= 1 - metallic;
float3 DFG = D * F * G;
float3 specular = DFG / max(4 * NdotV * NdotL, 0.001);// avoid divide zero
float3 brdf = kD * albedo / PI + specular;
return brdf * NdotL;
};
float3 Get_EnvColor(float3 N, float3 V, float3 albedo, float metallic, float roughness)
{
// https://github.com/Arcob/UnityPbrRendering/blob/master/Assets/unity%20pbr/Height2.shader
// 又瞎调了一遍参数,效果勉强有些的样子,Orz。得找找关于天空盒子之类的文章。
// 发现竟然和Unity的Standard差不多,赞一个。
float3 F0 = lerp(0.04, albedo, metallic);
float NdotV = max(dot(N, V), 0);
float3 F = F0 + (max(1.0 - roughness, F0) - F0) * pow(1.0 - NdotV, 5.0);
// float3 F = F0 + (1 - F0) * pow(1.0 - NdotV, 5.0);
float3 reflectViewDir = reflect(-V, N);
float4 skyData = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectViewDir, roughness*10);
float3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
//float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
float3 ambient = ShadeSH9(float4(N, 1));
float3 kD = 1 - F;
kD *= 1 - metallic;
return skyColor * F + ambient * kD * albedo/* / PI*/;
}
float4 frag(v2f i) :SV_TARGET
{
float3 worldNormal = normalize(i.worldNormal);
float3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);// 平行光
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
float3 albedo = tex2D(_MainTex, i.uv) * _Color;
float _Roughness = 1 - _Glossiness*0.95;// 没有绝对光滑的物体,这样就和Unity更接近了
float3 brdf = BRDF_All(worldNormal, viewDir, worldLightDir, albedo, _Metallic, _Roughness);
float3 L0 = brdf * _LightColor0.rgb;
L0 *= PI;// Unity trick, Otherwise, the result is too dark.
float shadow_pass = SHADOW_ATTENUATION(i);
float3 envColor = Get_EnvColor(worldNormal, viewDir, albedo, _Metallic, _Roughness);
float3 color = envColor + shadow_pass * L0;
UNITY_APPLY_FOG(i.fogCoord, color);
return float4(color,1);
}
ENDCG
}
}
FallBack "Diffuse"
}