PBR渲染方程简单总结和实现

渲染方程的一些总结和简单实现

背景

看了很多PBR相关的文章了,虽然复杂,但似乎使用起来很简单的样子。
这儿写个简单版的PBR实现,留下点学习痕迹。

渲染方程

经典光照模型

1975年Phong提出Phong反射模型(Phong Reflection Model):
I_{Phong} = k_aI_a + k_d(n·l)I_d + k_s(r·v)^aI_s
r = 2(n·l)n-l
1977年Blinn对Phong模型做出修改,这就是后来广泛使用的Blinn-Phong反射模型:
I_{Blinn-Phong} = k_aI_a + k_d(n·l)I_d + k_s(n·h)^aI_s
h = \frac{l+v}{\lVert l+v \rVert} \text{, h is Half-Angle}
一个简单的基于物理的Blinn-Phong:
L_o(v) = (c_{diff} + \frac{a+2}{8}(n·h)^aF(c_{spec},l_c,h)) \otimes c_{light}(n·l_c)

辐射度量学

物理上辐射度量学的基本量及其关系(符号被我简化了,比如立体角和偏导):

名称 英文名 符号 单位 公式 补充描述
辐射能量 Radiant energy Q 焦耳(J) - 电磁辐射能量
辐射通量 Radiant flux \Phi 瓦(W) \Phi = \frac{dQ}{dt} 单位时间辐射的能量
辐==照==度 Irradiance E W/m^2 E = \frac{d\Phi}{dA} 到达单位面积的辐射通量
辐射度 Radiosity E? W/m^2 E = \frac{d\Phi}{dA} ==离开==单位面积的辐射通量
辐射强度 Radiant Intensity I W/sr I = \frac{d\Phi}{dw} 通过单位立体角的辐射通量
==辐射率== Radiance L W/m^2sr L = \frac{d^2\Phi}{dwdA^\bot} 不随距离变化
立体角 Solid Angle w sr w = \frac{S}{r^2} 角度的二维扩展,

辐射率被用来量化单一方向上发射来的光线的大小或者强度,不随距离变化,人眼看到的颜色强度的度量单位就是这个。

PBR渲染方程

PBR的渲染方程(反射方程)一般长这样:
L_o(v) = L_e(v) + \int\limits_{\Omega} f(l,v) L_i(l) cos\theta_i dw_i
v是观察方向,l是光照方向,cos\theta_i = n·l
有些文章使用f(l,v) \otimes L_i(l), 表示按向量的分量相乘,因为fL_i都包含RGB三个分量。
辐射率与辐照度按定义有这样的关系:
L_i(l) = \frac{d^2\Phi}{dw_idA^\bot} = \frac{d^2\Phi}{dw_idAcos\theta_i} = \frac{dE(l)}{dw_icos\theta_i}
双向反射分布函数BRDF的定义是:f(l,v) = \frac{dL_o(v)}{dE(l)}

积分方程一般用蒙特卡洛方法近似计算,即便如此,也是不能实时计算,一般都是先离线预处理。知道的几个方法

  1. 离线计算积分结果,保存成类似cubemap,然后shader里根据方向采样。
  2. 离线计算3阶左右的球谐系数,使用球谐光照来快速计算积分。

理想光源的渲染方程

对于理想光源(点光源、方向光等),L是没有意义的,比如平行光,只有一个方向有值,且值是无限大。
对于理想光源照射的物体来说,有意义的是辐照度E,渲染方程退化成:
L_o(v) = f(l,v) E_l cos\theta_i,\textbf{单光源} \\ L_o(v) = \sum_{k = 1}^{n} f(l,v) E_{l_k} cos\theta_{i_k},\textbf{多光源}
这个方程可以在shader里实时算。

BRDF

下面这个模型大概是叫Cook-Torrance BRDF模型吧。
首先BRDF分为漫反射和镜面反射两部分:f_r = k_d f_{lambert} + k_s f_{cook-torrance}
k_d,k_s系数用来控制能量守恒的。

漫反射BRDF:

f_{lambert} = \frac{c_{diff}}{\pi}

镜面反射BRDF:

f_{cook-torrance} = \frac{DFG}{4cos\theta_i cos\theta_o} = \frac{D(h)F(v,h)G(l,v)}{4(n·l)(n·v)}
n是表面法线
h就是前面提到的半角向量,在这儿它多了一个含义,微表面法线

法线分布函数,D(Normal Distribution Function,NDF):

D(h) = \frac{a^4}{\pi((n · h)^2 (a^4 - 1) + 1)^2}
α是表面的粗糙度(UE的文章定义a=roughness^2,导致有些文章写错了,这儿直接就是roughness),n是表面法线。这个方程是大佬特地研究出来的。

菲涅尔方程,F(Fresnel equation)

F(v,h) = F_0 + (1 - F_0) (1 - (h · v))^5
菲涅尔现象是掠角越大,镜面反射越强,表现出边缘高光现象。
F_0表示的是表面基础反射率,这个值区分了金属和非金属。金属大于0.5,非金属小于0.2,并且常见的非金属小于0.04。
这个值输入的方式一般是:金属的通过贴图输入,非金属的直接设置为0.04。

vec3 F0 = vec3(0.04);
F0 = lerp(F0, albedo, metallic);
漫反射系数

前面提到的系数k_d,k_sF有关系。
k_s=1,k_d=(1-F)*(1-metallic),就能达到能量守恒了:镜面反射+漫反射<=1,同时还减弱金属的漫反射。

几何遮挡函数,G(Geometry function)

k_{direct} = \frac{(a+1)^2}{8}
k_{IBL} = \frac{a^2}{2}
G_1(v) = \frac{n · v}{(n · v)(1-k)+k}
G(l,v) = G_1(l) G_1(v)

整合后的PBR渲染方程

L_o(w_o) = \int\limits_{\Omega} (k_d\frac{c}{\pi} + \frac{DFG}{4(w_o · n)(w_i · n)}) L_i(w_i) n · w_i dw_i
其中k_d,D,F,G有各种不同的方案,disney、unity、unreal的渲染方程就有差别。
这儿只是列一个可行的方案。

关于实现

分两种情况

  • 理想光源用累加方程实时算。
  • 积分方程,知道有两种方式
    • 采用预计算出纹理的方式,在shader里采样纹理计算。
    • 球谐光照,预计算去球谐系数,然后shader里算球谐向量积。

PBR的渲染需要综合间接光才能效果不错。

关于线性相加

PBR需要在线性空间里计算,不同光源是可以线性相加的。当结果>1时,有专门的算法归一。
tone = \frac{hdr}{hdr + 1}
tone = 1-e^{-hdr*exposure}
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"
}

Unity自带的一些系统的使用

  1. Shadow
    1. https://www.jianshu.com/p/7b56eeb5ba4c

    2. https://blog.csdn.net/A13155283231/article/details/95073757

  2. LightProbe
    1. https://www.jianshu.com/p/6dfe403f75f6
  3. SkyBox
    1. https://github.com/Arcob/UnityPbrRendering/blob/master/Assets/unity%20pbr/Height2.shader
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345