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(双向反射分布函数)的思想,其中蕴含的就是能量守恒的思想。
为了理解光以及你的视点与平面的交互,你必须首先了解平面本身,当光线照在一个绝对平滑的表面,它会以一个几乎完美的轨迹反射出去。当光射到粗糙表面,它将无法以相似的方式反射出去.这可以解释为存在微表面。
当我们看一个物体,我们必须假定这个物体表面并非绝对平滑,而是由无数的微平面组成,这些微平面每一个都可以被看成完美的镜面。这些微平面的法线分散在物体的表面上。这些微平面法线角度差异决定于这个物体的粗糙度。这个平面越粗糙,高光就越容易被打断。因此,粗糙的表面有更大更加黯淡的高光。光滑表面可以产生更小的高光。
让我们回到BRDF函数。双向反射分布函数是一种描述表面反射率的函数。现在有很多种不同的BRDF模型以及算法,它们都必须遵循能量守恒原则。能量守恒原则表示了平面反射的光照必须少于平面接收到的总光照量。平面反射的光必须小于所有微平面光照的总和。
BRDF算法模型比其他的光照模型更加复杂也更加精确,这种光照模型由三个部分组成:
正态分布函数(Normal Distribution Function), 几何阴影函数(Geometric Shadowing Function), 菲涅尔函数(Fresnel Function)
他们三者组成了这个算法,我们在后面会将它们逐一分解。
想要了解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中添加各种属性、变量、方法以及算法来扩充这个函数。