在Unity3D里理解实现PBR 算法【译1】

Physically Based Rendering Algorithms: A Comprehensive Study In Unity3D【翻译1】

关于PBR

基于物理的渲染(PBR)这几年非常热门,Unity5、虚幻4、寒霜甚至是ThreeJS,还有更多的游戏引擎正在使用它。许多3D模型软件将工作流迁移到了PBR的渲染管线,例如Marmoset Toolbag和Allegorithmic的Substance Suite。今天,几乎每一位艺术家都熟悉这一套管线,但是很难找到工程师或者技术美术能完全解释清楚这背后的工作原理。我希望能够通过这个教程来分解PBR渲染,让初学者也能更容易地入门。

怎么实现PBR?

​ 在过去的三四十年间,我们围绕着世界科学和数学的了解突飞猛进,这些也对渲染技术有着深远的影响。站在巨人的肩膀上,智者们总结出了一系列对于光、视野、表面法线的规律总结,以及这三者是如何互相影响的。这些之中最大的成就就是BRDF(双向反射分布函数)的思想,其中蕴含的就是能量守恒的思想。

​ 为了理解光以及你的视点与平面的交互,你必须首先了解平面本身,当光线照在一个绝对平滑的表面,它会以一个几乎完美的轨迹反射出去。当光射到粗糙表面,它将无法以相似的方式反射出去.这可以解释为存在微表面

​ 当我们看一个物体,我们必须假定这个物体表面并非绝对平滑,而是由无数的微平面组成,这些微平面每一个都可以被看成完美的镜面。这些微平面的法线分散在物体的表面上。这些微平面法线角度差异决定于这个物体的粗糙度。这个平面越粗糙,高光就越容易被打断。因此,粗糙的表面有更大更加黯淡的高光。光滑表面可以产生更小的高光。

image

​ 让我们回到BRDF函数。双向反射分布函数是一种描述表面反射率的函数。现在有很多种不同的BRDF模型以及算法,它们都必须遵循能量守恒原则。能量守恒原则表示了平面反射的光照必须少于平面接收到的总光照量。平面反射的光必须小于所有微平面光照的总和。
​ BRDF算法模型比其他的光照模型更加复杂也更加精确,这种光照模型由三个部分组成:

正态分布函数(Normal Distribution Function), 几何阴影函数(Geometric Shadowing Function), 菲涅尔函数(Fresnel Function)

他们三者组成了这个算法,我们在后面会将它们逐一分解。

image.png

想要了解BRDF,先了解这三个函数如何组成BRDF是非常重要的。让我们逐个击破,来看看它们是如何在光照模型中如何开始运作的。

实现一个PBR 着色器:螺母,螺栓和光滑的表面(Nuts, Bolts, and Smooth Surfaces)

PBR Shader的属性有哪些

​ 大部分的PBR光照模型中,我们可以看到几乎有着相同的属性来影响他们的渲染。现代PBR Shader中最常见的两种属性分类是光滑度(或者粗糙度)以及金属度。这两种属性都是在0~1之间进行取值。有很多方式去实现不同的PBR Shader,有些会使用BRDF产生更多的效果,例如[迪士尼的PBR管线],每一种效果都受特定的属性影响。
​ 让我们继续把这些属性构建出来,如果你还没有看过我之前写的[Writing Shaders in Unity],现在是时候去从头读一下了。

Shader "Physically-Based-Lighting" {
Properties {
  _Color ("Main Color", Color) = (1,1,1,1) //diffuse Color
  _SpecularColor ("Specular Color", Color) = (1,1,1,1) //Specular Color (Not Used)
  _Glossiness("Smoothness",Range(0,1)) = 1 //My Smoothness
  _Metallic("Metalness",Range(0,1)) = 0 //My Metal Value
 
}
SubShader {
  Tags {
  "RenderType"="Opaque" "Queue"="Geometry"
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#define UNITY_PASS_FORWARDBASE
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
 
float4 _Color;
float4 _SpecularColor;
float _Glossiness;
float _Metallic;

​ 在此我们在Unity Shader中定义了我们的开放属性。我们会在后面添加更多属性,但先开个好头。在属性的下方是我们Shader的基本结构。我们还会使用#pragma指令添加函数,加上更多的功能。

顶点着色器

我们的顶点着色器非常类似于我在[Writing Shaders in Unity]中进行编写的顶点着色器。在我们的顶点中我们会用到法线(normal)、切线(tangent)、以及副切线(bitangent)信息,所以我们在着色器中也会有所体现。

struct VertexInput {
  float4 vertex : POSITION; //local vertex position
  float3 normal : NORMAL; //normal direction
  float4 tangent : TANGENT; //tangent direction
  float2 texcoord0 : TEXCOORD0; //uv coordinates
  float2 texcoord1 : TEXCOORD1; //lightmap uv coordinates
};
 
struct VertexOutput {
  float4 pos : SV_POSITION; //screen clip space position and depth
  float2 uv0 : TEXCOORD0; //uv coordinates
  float2 uv1 : TEXCOORD1; //lightmap uv coordinates
 
  //below we create our own variables with the texcoord semantic.
  float3 normalDir : TEXCOORD3; //normal direction
  float3 posWorld : TEXCOORD4; //normal direction
  float3 tangentDir : TEXCOORD5;
  float3 bitangentDir : TEXCOORD6;
  LIGHTING_COORDS(7,8) //this initializes the unity lighting and shadow
  UNITY_FOG_COORDS(9) //this initializes the unity fog
};
 
VertexOutput vert (VertexInput v) {
  VertexOutput o = (VertexOutput)0;
  o.uv0 = v.texcoord0;
  o.uv1 = v.texcoord1;
  o.normalDir = UnityObjectToWorldNormal(v.normal);
  o.tangentDir = normalize( mul( _Object2World, float4( v.tangent.xyz, 0.0 ) ).xyz );
  o.bitangentDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
  o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
  o.posWorld = mul(_Object2World, v.vertex);
  UNITY_TRANSFER_FOG(o,o.pos);
  TRANSFER_VERTEX_TO_FRAGMENT(o)
  return o;
}

片段着色器

在我们的片段着色器中,我们会将我们之后用到算法当中的所有值都提前算出来:

float4 frag(VertexOutput i) : COLOR {
 
//normal direction calculations
float3 normalDirection = normalize(i.normalDir);
 
float3 lightDirection = normalize(lerp(_WorldSpaceLightPos0.xyz, _WorldSpaceLightPos0.xyz
- i.posWorld.xyz,_WorldSpaceLightPos0.w));
 
float3 lightReflectDirection = reflect( -lightDirection, normalDirection );
 
float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
 
float3 viewReflectDirection = normalize(reflect( -viewDirection, normalDirection ));
 
float3 halfDirection = normalize(viewDirection+lightDirection);
 
float NdotL = max(0.0, dot( normalDirection, lightDirection ));
 
float NdotH = max(0.0,dot( normalDirection, halfDirection));
 
float NdotV = max(0.0,dot( normalDirection, viewDirection));
 
float VdotH = max(0.0,dot( viewDirection, halfDirection));
 
float LdotH = max(0.0,dot(lightDirection, halfDirection));
 
float LdotV = max(0.0,dot(lightDirection, viewDirection));
 
float RdotV = max(0.0, dot( lightReflectDirection, viewDirection ));
 
float attenuation = LIGHT_ATTENUATION(i);
 
float3 attenColor = attenuation * _LightColor0.rgb;

​ 以上的这些变量我们会通过Unity给我们的数据算出来,Unity中数据的含义都在[Unity Shader Tutorial]中进行总结了。这些变量会重复的出现在我们的Shader中以帮助我们编写BRDF。

粗糙度(Roughness)

​ 在我的做法当中,我重新映射了粗糙度,我这么做的原因其实更多是个人喜好,因为我发现重定向之后的粗糙度在进行一下处理之后更符合物理规律。

float roughness = 1- (_Glossiness * _Glossiness); // 1 - smoothness*smoothness
roughness = roughness * roughness;

金属度(Metallic)

​ 在PBR Shader中使用金属度的时候有太多的东西需要关注了。你会发现没有一个算法是对其负责的,所以我们使用一个完全不同的方式来描述它。

​ 金属度是一个用于控制一个材质接近金属的程度(一个非金属:金属度为0,一个完全金属:金属度为1)因此,为了给我们的材质赋值一个正确的金属度,我们会让其决定我们的漫反射颜色以及高光颜色。所以一个完全的金属不会表现出任何的漫反射,它会显示一个完全黑色的漫反射,并且其高光颜色会改变为这个物体表面的颜色。代码如下:

float3 diffuseColor = _Color.rgb * (1-_Metallic) ;
float3 specColor = lerp(_SpecularColor.rgb, _Color.rgb, _Metallic * 0.5);

Shader的内部实现

​ 下面的内容是就是我们要编写的shader的基本材质格式。注意Shader中的注释,因为它们会帮助我们组织代码并且告诉我们在哪边插入我们的代码。

Shader "Physically-Based-Lighting" {
Properties {
_Color ("Main Color", Color) = (1,1,1,1) //diffuse Color
_SpecularColor ("Specular Color", Color) = (1,1,1,1) //Specular Color (Not Used)
_Glossiness("Smoothness",Range(0,1)) = 1 //My Smoothness
_Metallic("Metalness",Range(0,1)) = 0 //My Metal Value
// future shader properties will go here!! Will be referred to as Shader Property Section
}
 
SubShader {
Tags {
"RenderType"="Opaque" "Queue"="Geometry"
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#define UNITY_PASS_FORWARDBASE
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
 
float4 _Color;
float4 _SpecularColor;
float _Glossiness;
float _Metallic;
 
//future public variables will go here! Public Variables Section
 
struct VertexInput {
float4 vertex : POSITION; //local vertex position
float3 normal : NORMAL; //normal direction
float4 tangent : TANGENT; //tangent direction
float2 texcoord0 : TEXCOORD0; //uv coordinates
float2 texcoord1 : TEXCOORD1; //lightmap uv coordinates
};
 
struct VertexOutput {
float4 pos : SV_POSITION; //screen clip space position and depth
float2 uv0 : TEXCOORD0; //uv coordinates
float2 uv1 : TEXCOORD1; //lightmap uv coordinates
 
//below we create our own variables with the texcoord semantic.
float3 normalDir : TEXCOORD3; //normal direction
float3 posWorld : TEXCOORD4; //normal direction
float3 tangentDir : TEXCOORD5;
float3 bitangentDir : TEXCOORD6;
LIGHTING_COORDS(7,8) //this initializes the unity lighting and shadow
UNITY_FOG_COORDS(9) //this initializes the unity fog
};
 
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.uv0 = v.texcoord0;
o.uv1 = v.texcoord1;
o.normalDir = UnityObjectToWorldNormal(v.normal);
o.tangentDir = normalize( mul( _Object2World, float4( v.tangent.xyz, 0.0 ) ).xyz );
o.bitangentDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.posWorld = mul(_Object2World, v.vertex);
UNITY_TRANSFER_FOG(o,o.pos);
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}
 
//helper functions will go here!!! Helper Function Section
//algorithms we build will be placed here!!! Algorithm Section
 
float4 frag(VertexOutput i) : COLOR {
 
//normal direction calculations
float3 normalDirection = normalize(i.normalDir);
 
float3 lightDirection = normalize(lerp(_WorldSpaceLightPos0.xyz, _WorldSpaceLightPos0.xyz - i.posWorld.xyz,_WorldSpaceLightPos0.w));
 
float3 lightReflectDirection = reflect( -lightDirection, normalDirection );
 
float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
 
float3 viewReflectDirection = normalize(reflect( -viewDirection, normalDirection ));
 
float3 halfDirection = normalize(viewDirection+lightDirection);
 
float NdotL = max(0.0, dot( normalDirection, lightDirection ));
 
float NdotH = max(0.0,dot( normalDirection, halfDirection));
 
float NdotV = max(0.0,dot( normalDirection, viewDirection));
 
float VdotH = max(0.0,dot( viewDirection, halfDirection));
 
float LdotH = max(0.0,dot(lightDirection, halfDirection));
 
float LdotV = max(0.0,dot(lightDirection, viewDirection));
 
float RdotV = max(0.0, dot( lightReflectDirection, viewDirection ));
 
float attenuation = LIGHT_ATTENUATION(i);
 
float3 attenColor = attenuation * _LightColor0.rgb;
 
float roughness = 1- (_Glossiness * _Glossiness); // 1 - smoothness*smoothness
 
roughness = roughness * roughness;
 
float3 diffuseColor = _Color.rgb * (1-_Metallic) ;
 
float3 specColor = lerp(_SpecularColor.rgb, _Color.rgb, _Metallic * 0.5);
 
//future code will go here! Fragment Section
 
return float4(1,1,1,1);
}
ENDCG
}
}
FallBack "Legacy Shaders/Diffuse"
}

在Unity中创建物体并且附上材质,使用这个Shader,会显示一个白色的物体。我们会在shader中添加各种属性、变量、方法以及算法来扩充这个函数。

原文地址

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