基于物理的渲染算法 有关 Unity3D 的概括研究
来说说基于物理的渲染。
基于物理的渲染(PBR)近几年非常火爆。
Unity 5, 虚幻引擎 4, 寒霜引擎, 甚至是 ThreeJS, 更多的游戏引擎在使用它。
很多 3D 模型工作室在根据 Marmoset Toolbag 和 Allegorithmic Substance Suite等人气工具转换成 “PBR Pipeline”。
现在没几个不熟悉管线的美术,但是了解管线在后台是怎么运行的,很少有引擎和美术知道。
我这个指南分解(分析) PBR 渲染 (shading),是想让初学者方便理解 PBR。
让我们开始吧。
PBR的物理特征
在过去的 3 ~ 40 年,我们对周围世界的理解和科学上 / 数学上它是怎么运作的都有很多进步。
而且这种理解在渲染技术领域也有了很大的发展。
多位灵敏的研究者对光,视野,表面正常形态,然后这三个是怎么互相作用的有着深刻的结论。
这种发展大部分是围绕 BRDF (双向反射率分布函数)和固有能量保存的想法为中心形成的。
如果想理解光和观测点跟Surface是怎么相互作用的,首先要理解Surface本身。
光照射到完全光滑的表面几乎可以在表面形成完整的反射。
光和我们叫做粗糙表面的东西相互作用的时候,他的反射不是类似的,这可以用微表面 (microfacets)的存在说明。
我们看物件的时候,要假设他的表面不是完全光滑的,而是用很小的面组成的。每个物件都是完整的 Specular reflection 。
这个微表面(microfacets)通过光滑表面的法线,具有分布的法线。
微表面法线的不同程度是根据表面粗糙度定的。
表面越粗糙,高光损失的可能性就越大。
所以粗糙的表面有更大更模糊的斑点。
光滑的表面上,光的反射比之前更完整时可以压缩反射高光。
再回到 BRDF…
双向反射率分布函数 (Bridirectional Reflectance Distribution Function, BRDF)是说明表面反射率的函数。
有各种 BRDF 模型 / 算法,其中大部分不是基于物理的算法。
基于物理的 BRDF需要能量守恒。
'能量守恒 (Energy Conservation)'是说从地表反射的总光量,比地表收到的总量少。
表面反射的光不能比之前讨论的所有微表面相互作用前更强烈。
BRDF 算法的特点是比其他算法更复杂的Shading模型。
这个Shading模型技术上以三个单一部分组成 : 正规分布函数、几何阴影函数和菲涅尔函数。
一起使用这个算法就能理解了。
要理解 BRDF,重点是理解构成 BRDF 的三种功能。
依次实现并做对我们有帮助的阴影模型。
编写 PBR Shader : nut,bolt 和光滑的表面
PBR Shader 属性
大部分 PBR Shading 模型上通常是用某种形态影响几种相同属性。
现代 PBR 思路上最重要的两种属性是光滑 (Smoothness) 和金属性 (Metallic)。
这两个值都是在 0…1 之间的时候最好起效。
编写 PBR Shader 的方法有多个,其中一部分为了更多类似迪士尼 PBR 管线的效果可以使用 BRDF 模型。各个效果由特定属性引出。
如果在 Unity 里没有细读 Writing Shaders相关我的页面,这可以成为你可以集中细读的时间。
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 里定义 public 变数。那些以后会添加。
属性下方有Shader初始化结构。
以后添加更多功能的时候会参考 #pragma 指令。
Vertex Program
Vertex 程序跟 Unity 里的编写 Shade r相关自学书上生成的类似。
我们需要的核心因素是有关 vertex 的 normal, tangent 和 bitangent 信息。
也就是说这些一定要包含在 Vertex Program里。
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;
}
Fragment Program
fragment 程序里,在算法上要定义之后可以使用的变量Set:
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 Shader 指南的说明,将要用 Unity 提供的数据编译的变量。
这个变量移动到 BRDF 内部,会用在整体 Shader 上。
Roughness
下面的方法上会重新处理粗糙度。 我做这个的理由是个人喜好。
发现下方重新处理的粗糙度在视觉上的结果更分明。
float roughness = 1- (_Glossiness * _Glossiness); // 1 - smoothness*smoothness
roughness = roughness * roughness;
Metallic
PBR shader里使用 Metallic的时候要小心的很多。
可以知道任何算法都不说明那个算法。所以我们用其他形式完全包含了。
金属材料是电介质 (非金属,也就是金属 = 0) 或者是决定是不是金属 (금속 = 1) 材料的控制值。
也就是说,为了我们的金属度值用正确的方式影响到Shader,我们要用扩散的颜色连接这些,诱导出我们的反射颜色。
金属不会显示扩散反射(Diffuse),所以有完全反射的 Albedo,实际的反射颜色会反映物体的表面去变更。
参考以下 :
float3 diffuseColor = _Color.rgb * (1-_Metallic) ;
float3 specColor = lerp(_SpecularColor.rgb, _Color.rgb, _Metallic * 0.5);
我们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代码制作白色个体。
通过属性Shun, 变量section的变量, Helper 函数section的 Helper 函数,算法section的算法和 Fragments section的Shader代码实现扩张shader。