PBR与Blinnphong解读

article/2025/9/17 0:26:20

我们做光栅化模式的渲染都了解有两种比较常用的渲染方式,一个是blinnphong的渲染,一个是pbr的渲染。

blinnphong:

blinnphong的渲染模式更多的是一种经验值模拟光照对物体的效果。所以他不是一个正确的能量守恒的渲染方式。

blinnphong的渲染公式

L=Cdiff * N\cdot L + Cspec (N\cdot H)^{n}

其中Cdiff是漫反射的颜色值,也就是我们的贴图乘颜色。Cspec是高光图乘高光颜色

N\cdot L是顶点法线与光线方向的点积,主要要得到光线与法线的角度,决定漫反射的颜色。

N\cdot H是顶点法线与半角向量的点积,主要得到光线照射到视角范围的角度。

原理:

为什么需要H呢?

按照现实的光照显示,应该是光线射到地面上每个分子然后进行反射或散射后吸收热量然后最终反射出来的光线。但是如果按照这个方式去判断究竟显示的是什么内容的话,那计算量会非常大。

而我们其实只关心的是射入我们眼球范围内的光照,那么我们就需要寻找跟我们眼球反射有关的物体反射信息,所以用到了半角向量,半角向量是通过入射光线到点、射线方向和点相加后得到的向量,实际上他反映了是与视线和灯光相关的法线信息。

那么我们要判断指定部位的光照强度就可以用半角向量与顶点的法线向量做点乘后得出N\cdot H。点乘值越大说明法线与半角向量越重合,那么高光肯定越高。

那么这里可能会考虑另一个问题,正常来说法线向量和半角向量重叠才能说明光线L射入了视线V中。为什么用N\cdot H来表示呢?因为点乘可能会再-1到1之间。

是因为我们真实世界是每个分子都能做该运算,然后照到瞳孔接收光线范围内就让我们看到了物体的颜色。但再光栅化下,每个像素可能对应的是多个分子,我们要做的只能是得出该像素下分子反射的平均值,如果我们要L和V的法线完全与H重叠的话,会导致一些像素上完全没有高光,看起来是不自然的。所以我们再光栅化下接受了点乘再-1到1之间的浮点数值来表示他再该像素上高光的比值。

 

说完参数原理,我们可以开始带入具体的参数来实现我们的效果了。

L=(\frac{Cdiff}{\pi } + \frac{(m + 8)}{8\pi } * Cspec * (N\cdot H)^{m})* N\cdot L

这里跟\pi有关系的原因是,我们知道圆面积是\pi r^{2},已经单位圆的半径r=1,所以单位圆面积就是\pi

我们做的颜色控制都是再对单元圆做的,所以需要用颜色除单位圆面积得到具体面积内的颜色。

然后需要说下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,如下图。

渲染公式

L_{o}= L_{e}+\int _{\Omega }f_{_{r}}\ast L_{i}\ast (\omega _{i}\cdot n)\ast d\omega _{i}

Le是自发光,一些物体可能会有自发光的信息或者有自发光的图片,通过这个可以得到本身发光物体的颜色信息。

fr是入射与出射之间的反射比例,一般有BRDF,BTDF,BSDF(就是BRDF+BTDF),BSSRDF等。

Li是入射光亮度

(\omega _{i}\cdot n)是法线与反射的点乘,得出的值可以表示入射光的衰减

\int _{\Omega }.... d\omega _{i}是入射方向的半球积分,可以立即理解为累计的数据值。

 

近似解

通过蒙特卡洛积分的方式,通过N项f_{_{r}}\ast L_{i}\ast (\omega _{i}\cdot n)相加可以求出积分的近似解。

\frac{1}{N}\sum_{k=1}^{N}f_{_{r}}\ast L_{i}\ast (\omega _{i}\cdot n)=\frac{1}{N}\sum_{k=1}^{N}L_{i}*\frac{1}{N}\sum_{k=1}^{N}f_{_{r}}\ast (\omega _{i}\cdot n)

环境光照

其中\frac{1}{N}\sum_{k=1}^{N}L_{i}是环境光照,我们可以用cubemap 来代替=Cubemap.sample(r.mip)

BRDF

然后我们可以根据高光的BRDF来算自身颜色

\int_{brdf}=Kdiff * \frac{Cdiff}{pi} + Kspec VDF

其中需要说明的是VDF

D是法线分布函数,相当于是确定再一个像素内所有分子的法线分布相关信息的均值。体现得更好得法线分布函数会有更好得高光长尾。

F是Fresnel的简写,是一个反射与折射的关系式,我们现实环境中看物体,比如水面,垂直看下来会比较容易看到水底下的物体,是因为折射大于反射。越接近平面的看水面会看到反射的内容越多,这是因为反射大于折射。

V是一个结合函数(其实应该是G(Gemetry Function))这里描述的是被自身遮挡的信息。

三种情况会导致遮挡,一是光照照射不到的地方,二是视图方向看不到的物体信息,三是反射后进入视线的信息。

 

公式分别是:

\int D=\tfrac{Roughness^{2}}{\pi * (\cos \Theta _{h} * (Roughness^{2} - 1) + 1)^{2}}

\int F=F_{0} + (1 - F_{0})(1 - \cos \Theta _{h})^{5}其中F0是反射的值,分为导体和电解质,导体一般会有rgb三个不同的颜色,而电解质一般rgb三个颜色是一样的值。默认的电解质为(0.4,0.4,0.4),但也有可能一个像素内出现金属和非金属,所以我们需要用一个公式来近似这个解(金属比值加上非金属比值):(Metallic是当前像素的金属度)

F_{0}=Metallic* Albedo + (1 - Metallic) * (0.4,0.4,0.4)

\int V=\frac{GGX(\Theta _{l})*GGX(\Theta _{v})}{4\cos \Theta _{l}\cos \Theta _{v}},这里是对光照和对视线方向都做了GGX运算结合起来得到视线与光照方向的遮蔽信息。下面除于4\cos \Theta _{l}\cos \Theta _{v}是对运算的值做光照方向和视线方向的纠正的值

GGX(\Theta )公式为:\int GGX=\frac{\cos (\Theta )}{K + (1-K) \cos \Theta },其中k为K=\frac{(Roughness + 1)^{2}}{8 }

 

最后为了能量守恒,应该要把能量消耗的损失加上:

也就是前面公式\int_{brdf}=Kdiff * \frac{Cdiff}{pi} + Kspec VDF下的Kdiff和Kspec。

Kspec描述的是有多少光的能灵被高光反射,也就是前面的F0。

Kdiff则是高光吸收后剩下的光线消耗:

Kdiff=(1-F_{0}) * (1-metallic)

代码:

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}}
}

参考资料

https://mp.weixin.qq.com/s?__biz=MzA4MDc5OTg5MA==&mid=2650610133&idx=1&sn=4ab4e5bd554f842e8b18e4aaf204cf74&chksm=87970828b0e0813e5ae7d3ff4fae72d848b9cde9d16fb5a992a3f4f39f35a2a6dff61f2784e4&mpshare=1&scene=1&srcid=0102fNRbQaGB8DIIVdfb3778&sharer_sharetime=1578021589935&sharer_shareid=8df5fbc7718f1db8a2e1763ea8b1a99f&rd2werd=1#wechat_redirect

https://zhuanlan.zhihu.com/p/53086060?utm_source=ZHShareTargetIDMore&utm_medium=social&utm_oi=827612570348310528

 


http://chatgpt.dhexx.cn/article/7HnNQR0X.shtml

相关文章

【基于物理的渲染(PBR)白皮书】(一) 开篇:PBR核心知识体系总结与概览

本文由浅墨_毛星云 出品,首发于知乎专栏,转载请注明出处 文章链接: https://zhuanlan.zhihu.com/p/53086060 先放出PBR知识体系的架构图: 图很大,建议下载到本地放大查看。原图下载地址: https://raw.gi…

什么是PBR?

一、什么是PBR? 基于物理渲染以前的渲染是在模仿灯光的外观现在是在模仿光的实际行为试图形看起来更真实 二、PBR组成部分 灯光属性:直接照明、间接照明、直接高光、间接高光、阴影、环境光闭塞表面属性:基础色、法线、高光、粗糙度、金属度…

PBR流程介绍和模型规范

(1)基本流程: (1)制作中模: 基础模型: 指的是中模!! 中模导出为.obj 格式(跟.fbx格式相比:不会出现模型大小的缩放) 高模&#xff1a…

PBR材质:基本原理和简单制作

概要:介绍PBR材质的基本原理以及制作一个简单的PBR材质 参考资料:BASIC THEORY OF PHYSICALLY-BASED RENDERING 如有问题,多多指正。 侵删。 1.PBR是什么,光线的基本原理。 PBR即Physically-based rendering,基于物理…

什么是PBR?pbr入门基础干货

(1)什么是PBR? 基于物理的渲染过程。 PBR是一种着色和渲染技术,用于更精确的描述光如何与物体表面互动。 PBR的优势: (1)方法论和算法基于精确的计算公式,免除创作表面的猜想过程…

PBR——概述、基于物理的材质

PBR概述 PBR,即Physically Based Rendering,主要分为基于物理的材质、基于物理的光照和基于物理的相机三个部分,目前来说对大家最为所熟知的是基于物理的材质部分。本文围绕基于物理的材质进行相关介绍。 什么是PBR 其实最早听说PBR这玩意…

PBR基础理论通俗解释

PBR基础理论通俗解释 今天给大家介绍PBR的基础理论, 不会涉及比较深的具体算法, 算是一篇扫盲的文章, 尽量尝试说人话, 让大家能够对PBR有基本的了解. 什么是PBR? PBR是基于物理的渲染(Physically Based Rendering), 也就是说通过模拟物理世界的方式来渲染. 既然有基于物理…

基于物理的渲染PBR(一):pbr的基础理论和推导

初始PBR 最近刚接触pbr不久,我搜寻了许多文章进行阅读并了解后发现,pbr涉及到的知识点繁琐且不容易理解,所以想在博客上给自己记录并总结一下,方便以后回顾并加深记忆。 这里首先借用知乎上的大佬毛星云关于pbr所涉及到的知识要…

策略路由(PBR)

1、基本概念 PBR (Policy-Based Routing,策略路由): PBR使得网络设备不仅能够基于报文的目的IP地址进行数据转发,更能基于其他元素进行数据转发,例如源IP地址、源MAc地址、目的MAc地址、源端口号、目的端口号、VLAN-ID等等。 用户还可以使用A…

技术美术知识学习_04:PBR的个人理解

一、什么是PBR? PBR(Physically Based Rendering),中文翻译为基于物理的渲染。 PBR是一种渲染方式,是使用基于物理原理和微平面理论的光照模型,以及使用从现实中测量的表面参数来准确表示真实世界材质的渲…

图灵直播|《第一行代码》作者郭霖在线Coding,今晚八点,给你留位!

图源来自Pexels “我们为什么需要 Kotlin?答:消失的 Getter 和 Setter、又见空指针、Smart Cast、打日志、再见Utils、晚安ButterKnife……” 相信很多人初识Kotlin,都是基于谷歌技术大牛 Steve Yegge的一篇文章《为什么说 Kotlin 比你们用的…

撸了郭霖大神写的Framework源码笔记,offer拿到手软

前不久听我一个字节的朋友说了一个神转折的故事。 一名大专生,异常执着地向他们公司投简历,屡战屡败,屡败屡战,前前后后向字节跳动投了九次简历。 你猜后面怎么着?还真让他成功了,第九次居然拿到了offer! 看到这里,不过是一个普普通通的励志故事吧,一个菜鸡凭借自己…

(郭霖)Android图片加载框架最全解析(一),Glide的基本用法

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/53759439 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每天都有文章更新。 现在Android上的图片加载框架非常成熟,从最早…

图灵专访:郭霖的成长之路

各位小伙伴们大家早上好,最近这段时间我真的是快要忙疯了,好多件事情同时在做,实在抽不出时间写原创文章。 正巧想到前段时间和图灵做了一次专访,记录了一些我的成长经历。这篇专访姑且也可以算是一篇原创吧,因为里面的…

第一行代码-第二版(郭霖著)笔记十一(Material Design)

目录 一、什么是Material Design 二、Toolbar 三、滑动菜单 1.DrawerLayout 2.NavigationView 四、悬浮按钮和可交互提示 1.FloatingActionButton:悬浮按钮 2.Snackbar:提示工具 3.CoordinatorLayout:加强版FrameLayout 五、卡片式…

第一行代码-第二版(郭霖著)笔记二(Activity)

目录 一、Activity的用法 1.Activity 2.Toast 3.菜单 4.销毁一个活动 二、Intent 1.使用显示Intent 2.使用隐式Intent 3.更多隐式Intent的用法 4.向下一个活动传递数据 5.返回数据给上一个活动 三、活动的生命周期 1.返回栈 2.活动的四种状态 3.活动的生存期 4…

android动态权限依赖库,动态申请app权限:郭霖大神的PermissionX库带你告别原生

引言 为什么那么多人想要自定义Android的权限申请PermissonX?因为PermissionX默认的权限提醒弹出实在是太丑了!而且,需要在你需要提醒用户弹出Dialog时,显得捉襟见肘,你可能就在想有没有一款能封装进去Dialog提醒用户,具有超棒的用户体验,还能看起来美观大气的Permissio…

跟随郭霖学Volley

volley 下载导入volleyjar 学习地址: https://blog.csdn.net/guolin_blog/article/details/17482095 2013在Google I/O大会提出 github地址: https://github.com/google/volley 下载volley导入到as 具体的操作是: project模式下 具体看图: 之后的操作是打开lib 选择jar 右…

android 6.0权限 郭霖,Permission——郭霖认为最优的运行时权限方案

Android6.0发布这么久,对运行时权限也看了很多资料,对比过几个流行的库。但是个人还是喜欢在项目里用自己动手封装的东西,哪怕照抄也好。。。不知道是什么原因。 前天无意听郭神的直播。讲解的是运行时权限的封装,收益颇多。依样画…

郭霖LitePal

由于项目需要开始学习sqlite 一开始先学习使用的是 android ormlite 操作 从最基本的建表增删改查一路走来 磕磕碰碰很多 都是在内存中操作sqlite 只能通过sqlitestudio工具进行查看 不能导出 并且应用卸载数据表就丢失 最终考虑在sd卡中操作sqlite 但是ormlite 并没有这方面…