什么是PBR?为什么它改变了游戏图形?
基于物理的渲染(Physically Based Rendering,简称PBR)是现代计算机图形学中的一种渲染方法,它通过模拟真实世界中的光线物理行为来创建更加逼真的视觉效果。与传统渲染方法相比,PBR不是基于艺术家的直觉和经验,而是基于物理定律。
PBR的核心优势:
- 物理准确性:在不同光照条件下保持一致的视觉表现
- 艺术工作流简化:材质参数直观,易于理解和调整
- 跨平台一致性:在不同设备和光照环境下表现稳定
- 真实感增强:更好地模拟金属、粗糙度、菲涅尔效应等物理现象
PBR理论基石:理解关键概念
1. 能量守恒定律
在PBR中,出射光的总能量不能超过入射光能量。这意味着材质不能自发地产生光能,反射的光越强,折射/吸收的光就越少。
2. 双向反射分布函数(BRDF)
BRDF描述了光线如何从特定方向入射并从另一方向反射。PBR中常用的BRDF模型包括:
- Cook-Torrance BRDF:最常用的微表面模型
- Disney BRDF:用于电影制作的实用模型
3. 微表面理论
表面由许多微小的镜面组成,这些微观结构的分布决定了材质的宏观外观:
- 粗糙度:微表面法线的随机程度
- 法线分布函数:描述微表面法线分布的函数
4. 菲涅尔效应
光线以不同角度照射表面时,反射率会发生变化。掠射角(接近90度)时反射率接近100%。
PBR材质的关键参数
1. 反照率/基础色(Albedo)
- 非金属:RGB颜色值(sRGB空间)
- 金属:反射的RGB颜色值
- 不应包含光照信息(阴影、高光等)
2. 金属度(Metallic)
- 0.0(非金属)到1.0(纯金属)
- 非金属:漫反射+镜面反射
- 金属:只有镜面反射,无漫反射
3. 粗糙度(Roughness)
- 0.0(完全光滑)到1.0(完全粗糙)
- 控制高光的锐利程度和扩散范围
4. 法线贴图(Normal Map)
- 增加表面细节而不增加几何复杂度
- 存储切线空间法线信息
5. 环境光遮蔽(Ambient Occlusion)
- 模拟表面裂隙和凹陷处的阴影效果
在Unity中实现PBR:逐步指南
步骤1:设置PBR材质
csharp
using UnityEngine;
[CreateAssetMenu(fileName = "NewPBRMaterial", menuName = "Materials/PBR Material")]
public class PBRMaterialData : ScriptableObject
{
[Header("Albedo")]
public Color albedoColor = Color.white;
public Texture2D albedoMap;
[Header("Metallic & Roughness")]
[Range(0, 1)] public float metallic = 0.0f;
[Range(0, 1)] public float roughness = 0.5f;
public Texture2D metallicRoughnessMap;
[Header("Normal Map")]
public Texture2D normalMap;
[Range(0, 1)] public float normalStrength = 1.0f;
[Header("Ambient Occlusion")]
public Texture2D aoMap;
[Range(0, 1)] public float aoStrength = 1.0f;
[Header("Emission")]
public Color emissionColor = Color.black;
public Texture2D emissionMap;
[Range(0, 10)] public float emissionIntensity = 1.0f;
}
步骤2:创建自定义PBR着色器
shader
Shader "Custom/PBRShader"
{
Properties
{
// 基础属性
_Color ("Albedo", Color) = (1,1,1,1)
_MainTex ("Albedo Map", 2D) = "white" {}
// 金属度和粗糙度
_Metallic ("Metallic", Range(0,1)) = 0.0
_Roughness ("Roughness", Range(0,1)) = 0.5
_MetallicRoughnessMap ("Metallic Roughness Map", 2D) = "white" {}
// 法线贴图
[Normal] _BumpMap ("Normal Map", 2D) = "bump" {}
_BumpScale ("Normal Scale", Float) = 1.0
// 环境光遮蔽
_OcclusionMap ("Occlusion Map", 2D) = "white" {}
_OcclusionStrength ("Occlusion Strength", Range(0.0, 1.0)) = 1.0
// 自发光
_EmissionColor ("Emission Color", Color) = (0,0,0)
_EmissionMap ("Emission Map", 2D) = "white" {}
// 高级参数
[Toggle(_HEIGHTMAP)] _UseHeightMap ("Use Height Map", Float) = 0
_Parallax ("Parallax Strength", Range(0, 0.1)) = 0.01
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
#include "UnityPBSLighting.cginc"
sampler2D _MainTex;
sampler2D _MetallicRoughnessMap;
sampler2D _BumpMap;
sampler2D _OcclusionMap;
sampler2D _EmissionMap;
fixed4 _Color;
half _Metallic;
half _Roughness;
half _BumpScale;
half _OcclusionStrength;
fixed4 _EmissionColor;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
float3 viewDir;
INTERNAL_DATA
};
void surf (Input IN, inout SurfaceOutputStandard o)
{
// 反照率
fixed4 albedo = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = albedo.rgb;
o.Alpha = albedo.a;
// 金属度和粗糙度
fixed4 metallicRoughness = tex2D(_MetallicRoughnessMap, IN.uv_MainTex);
o.Metallic = _Metallic * metallicRoughness.b;
o.Smoothness = 1.0 - (_Roughness * metallicRoughness.g);
// 法线贴图
o.Normal = UnpackScaleNormal(tex2D(_BumpMap, IN.uv_BumpMap), _BumpScale);
// 环境光遮蔽
fixed occlusion = tex2D(_OcclusionMap, IN.uv_MainTex).r;
o.Occlusion = lerp(1.0, occlusion, _OcclusionStrength);
// 自发光
fixed3 emission = tex2D(_EmissionMap, IN.uv_MainTex).rgb * _EmissionColor.rgb;
o.Emission = emission;
}
ENDCG
}
FallBack "Diffuse"
}
步骤3:创建PBR材质管理器
csharp
using UnityEngine;
using System.Collections.Generic;
public class PBRMaterialManager : MonoBehaviour
{
public PBRMaterialData materialData;
[Header("Material Instances")]
public Material pbrMaterial;
[Header("Runtime Controls")]
[Range(0, 1)] public float metallicOverride = -1;
[Range(0, 1)] public float roughnessOverride = -1;
private MaterialPropertyBlock propertyBlock;
private Renderer objectRenderer;
void Start()
{
objectRenderer = GetComponent<Renderer>();
propertyBlock = new MaterialPropertyBlock();
ApplyMaterialData();
}
void Update()
{
if (Application.isPlaying)
{
UpdateMaterialProperties();
}
}
void ApplyMaterialData()
{
if (materialData == null || objectRenderer == null) return;
objectRenderer.GetPropertyBlock(propertyBlock);
// 设置纹理
if (materialData.albedoMap)
propertyBlock.SetTexture("_MainTex", materialData.albedoMap);
propertyBlock.SetColor("_Color", materialData.albedoColor);
if (materialData.metallicRoughnessMap)
propertyBlock.SetTexture("_MetallicRoughnessMap", materialData.metallicRoughnessMap);
propertyBlock.SetFloat("_Metallic", materialData.metallic);
propertyBlock.SetFloat("_Roughness", materialData.roughness);
if (materialData.normalMap)
{
propertyBlock.SetTexture("_BumpMap", materialData.normalMap);
propertyBlock.SetFloat("_BumpScale", materialData.normalStrength);
}
if (materialData.aoMap)
{
propertyBlock.SetTexture("_OcclusionMap", materialData.aoMap);
propertyBlock.SetFloat("_OcclusionStrength", materialData.aoStrength);
}
if (materialData.emissionMap)
propertyBlock.SetTexture("_EmissionMap", materialData.emissionMap);
propertyBlock.SetColor("_EmissionColor",
materialData.emissionColor * materialData.emissionIntensity);
objectRenderer.SetPropertyBlock(propertyBlock);
}
void UpdateMaterialProperties()
{
objectRenderer.GetPropertyBlock(propertyBlock);
if (metallicOverride >= 0)
propertyBlock.SetFloat("_Metallic", metallicOverride);
if (roughnessOverride >= 0)
propertyBlock.SetFloat("_Roughness", roughnessOverride);
objectRenderer.SetPropertyBlock(propertyBlock);
}
// 运行时修改材质属性
public void SetMetallic(float value)
{
metallicOverride = Mathf.Clamp01(value);
}
public void SetRoughness(float value)
{
roughnessOverride = Mathf.Clamp01(value);
}
public void SetAlbedoColor(Color color)
{
materialData.albedoColor = color;
ApplyMaterialData();
}
}
步骤4:实现IBL(基于图像的照明)
csharp
using UnityEngine;
[ExecuteInEditMode]
public class IBLManager : MonoBehaviour
{
[Header("IBL Settings")]
public Cubemap environmentCube;
public Texture2D brdfLUT;
[Range(0, 1)] public float reflectionIntensity = 1.0f;
[Range(0, 1)] public float diffuseIntensity = 1.0f;
[Header("Dynamic Updates")]
public bool updateInRealtime = false;
public float updateInterval = 0.1f;
private float lastUpdateTime;
void OnEnable()
{
UpdateIBL();
}
void Update()
{
if (updateInRealtime && Time.time - lastUpdateTime > updateInterval)
{
UpdateIBL();
lastUpdateTime = Time.time;
}
}
void UpdateIBL()
{
if (environmentCube)
{
Shader.SetGlobalTexture("_EnvCube", environmentCube);
Shader.SetGlobalFloat("_ReflectionIntensity", reflectionIntensity);
}
if (brdfLUT)
{
Shader.SetGlobalTexture("_BRDFLUT", brdfLUT);
}
Shader.SetGlobalFloat("_DiffuseIntensity", diffuseIntensity);
}
// 生成简单的环境贴图
public void CaptureEnvironment()
{
if (environmentCube == null)
{
environmentCube = new Cubemap(512, TextureFormat.RGBA32, false);
}
Camera cam = GetComponent<Camera>();
if (cam == null)
{
GameObject go = new GameObject("IBL Capture Camera");
cam = go.AddComponent<Camera>();
cam.enabled = false;
}
Vector3[] directions = {
Vector3.forward, Vector3.back,
Vector3.right, Vector3.left,
Vector3.up, Vector3.down
};
Vector3[] ups = {
Vector3.up, Vector3.up,
Vector3.up, Vector3.up,
Vector3.forward, Vector3.back
};
for (int i = 0; i < 6; i++)
{
cam.transform.position = transform.position;
cam.transform.rotation = Quaternion.LookRotation(directions[i], ups[i]);
cam.RenderToCubemap(environmentCube, 1 << i);
}
environmentCube.Apply();
UpdateIBL();
}
}
高级PBR功能实现
1. 视差映射(Parallax Mapping)
csharp
// 在着色器中添加视差映射功能
Shader "Custom/PBRParallax"
{
// ... 基础属性 ...
Properties
{
// 视差映射
_ParallaxMap ("Height Map", 2D) = "white" {}
_Parallax ("Parallax Scale", Range(0, 0.1)) = 0.01
_MinLayers ("Min Layers", Float) = 8
_MaxLayers ("Max Layers", Float) = 32
}
SubShader
{
// ... 其他代码 ...
void surf (Input IN, inout SurfaceOutputStandard o)
{
// 视差偏移计算
float3 viewDir = normalize(IN.viewDir);
float2 parallaxOffset = ParallaxOffset(_Parallax, _Parallax, IN.viewDir);
float2 uv = IN.uv_MainTex + parallaxOffset;
// 使用偏移后的UV坐标采样纹理
fixed4 albedo = tex2D(_MainTex, uv) * _Color;
// ... 其他采样 ...
}
}
}
2. 次表面散射模拟(SSS)
csharp
Shader "Custom/PBRSubsurface"
{
Properties
{
// ... 基础PBR属性 ...
// 次表面散射
_SubsurfaceColor ("Subsurface Color", Color) = (1,1,1,1)
_SubsurfacePower ("Subsurface Power", Range(0, 10)) = 5.0
_SubsurfaceDistortion ("Distortion", Range(0, 1)) = 0.0
_SubsurfaceScale ("Scale", Range(0, 10)) = 1.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf StandardSubsurface
#pragma target 3.0
struct SurfaceOutputStandardSubsurface
{
fixed3 Albedo;
fixed3 Normal;
fixed3 Emission;
fixed Metallic;
fixed Smoothness;
fixed Occlusion;
fixed Alpha;
fixed3 SubsurfaceColor;
};
// 自定义光照模型
half4 LightingStandardSubsurface(
SurfaceOutputStandardSubsurface s,
half3 lightDir,
half3 viewDir,
half atten)
{
// 标准PBR光照计算
half3 halfVec = normalize(lightDir + viewDir);
half NdotL = dot(s.Normal, lightDir);
half NdotV = abs(dot(s.Normal, viewDir));
half NdotH = dot(s.Normal, halfVec);
// 添加次表面散射
half3 subsurface = s.SubsurfaceColor *
pow(saturate(NdotL * 0.5 + 0.5), s._SubsurfacePower) *
saturate(1.0 - NdotV);
// 组合结果
half4 c;
c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten + subsurface;
c.a = s.Alpha;
return c;
}
// ... surf函数 ...
}
}
PBR最佳实践和优化技巧
1. 纹理优化
csharp
public class PBRTextureOptimizer : MonoBehaviour
{
public enum TextureResolution
{
Low = 512,
Medium = 1024,
High = 2048,
Ultra = 4096
}
public TextureResolution albedoResolution = TextureResolution.Medium;
public TextureResolution normalResolution = TextureResolution.High;
public TextureResolution otherResolution = TextureResolution.Medium;
public bool generateMipMaps = true;
public FilterMode filterMode = FilterMode.Trilinear;
public TextureWrapMode wrapMode = TextureWrapMode.Repeat;
public Texture2D OptimizeTexture(Texture2D source, TextureResolution resolution)
{
int targetSize = (int)resolution;
RenderTexture rt = RenderTexture.GetTemporary(
targetSize, targetSize, 0,
RenderTextureFormat.Default, RenderTextureReadWrite.sRGB);
Graphics.Blit(source, rt);
Texture2D result = new Texture2D(targetSize, targetSize,
TextureFormat.RGBA32, generateMipMaps);
RenderTexture.active = rt;
result.ReadPixels(new Rect(0, 0, targetSize, targetSize), 0, 0);
result.Apply(generateMipMaps);
RenderTexture.ReleaseTemporary(rt);
result.filterMode = filterMode;
result.wrapMode = wrapMode;
return result;
}
}
2. LOD(细节层次)系统
csharp
public class PBRMaterialLOD : MonoBehaviour
{
[System.Serializable]
public class LODLevel
{
public float distance;
public Texture2D albedoMap;
public Texture2D normalMap;
public Texture2D metallicRoughnessMap;
public float textureScale = 1.0f;
}
public LODLevel[] lodLevels;
public Transform cameraTransform;
public float updateInterval = 0.5f;
private MaterialPropertyBlock propertyBlock;
private Renderer objectRenderer;
private float lastUpdateTime;
private int currentLOD = 0;
void Start()
{
objectRenderer = GetComponent<Renderer>();
propertyBlock = new MaterialPropertyBlock();
if (cameraTransform == null)
cameraTransform = Camera.main.transform;
}
void Update()
{
if (Time.time - lastUpdateTime > updateInterval)
{
UpdateLOD();
lastUpdateTime = Time.time;
}
}
void UpdateLOD()
{
float distance = Vector3.Distance(
transform.position, cameraTransform.position);
int newLOD = 0;
for (int i = 0; i < lodLevels.Length; i++)
{
if (distance >= lodLevels[i].distance)
{
newLOD = i;
}
}
if (newLOD != currentLOD)
{
ApplyLOD(newLOD);
currentLOD = newLOD;
}
}
void ApplyLOD(int lodIndex)
{
if (lodIndex < 0 || lodIndex >= lodLevels.Length) return;
LODLevel level = lodLevels[lodIndex];
objectRenderer.GetPropertyBlock(propertyBlock);
if (level.albedoMap)
propertyBlock.SetTexture("_MainTex", level.albedoMap);
if (level.normalMap)
propertyBlock.SetTexture("_BumpMap", level.normalMap);
if (level.metallicRoughnessMap)
propertyBlock.SetTexture("_MetallicRoughnessMap", level.metallicRoughnessMap);
propertyBlock.SetFloat("_TextureScale", level.textureScale);
objectRenderer.SetPropertyBlock(propertyBlock);
}
}
调试和可视化工具
csharp
using UnityEngine;
public class PBRDebugVisualizer : MonoBehaviour
{
public enum DebugMode
{
None,
Albedo,
Normal,
Metallic,
Roughness,
AO,
Specular,
Final
}
public DebugMode debugMode = DebugMode.None;
public bool showWireframe = false;
public bool showUVs = false;
private Material originalMaterial;
private Material debugMaterial;
private Renderer objectRenderer;
void Start()
{
objectRenderer = GetComponent<Renderer>();
originalMaterial = objectRenderer.sharedMaterial;
CreateDebugMaterial();
}
void Update()
{
UpdateDebugView();
}
void CreateDebugMaterial()
{
debugMaterial = new Material(Shader.Find("Hidden/PBRDebug"));
}
void UpdateDebugView()
{
if (debugMode == DebugMode.None)
{
objectRenderer.sharedMaterial = originalMaterial;
}
else
{
objectRenderer.sharedMaterial = debugMaterial;
debugMaterial.SetInt("_DebugMode", (int)debugMode);
debugMaterial.SetInt("_ShowWireframe", showWireframe ? 1 : 0);
debugMaterial.SetInt("_ShowUVs", showUVs ? 1 : 0);
}
}
void OnDestroy()
{
if (debugMaterial != null)
Destroy(debugMaterial);
}
// 调试着色器
[ExecuteInEditMode]
public class PBRDebugShader : MonoBehaviour
{
void OnRenderObject()
{
// 这里可以添加自定义的调试渲染代码
}
}
}
性能优化建议
- 纹理压缩:使用适当的纹理压缩格式(BC7 for RGB,BC5 for normals)
- 着色器变体管理:减少不必要的着色器变体
- 动态批处理:对使用相同材质的物体进行批处理
- GPU实例化:对大量相同物体使用GPU实例化
- LOD系统:实现材质和几何体的LOD
结论
PBR是现代游戏开发中不可或缺的技术,它通过物理准确的方法大大提升了渲染的真实感。在Unity中实现PBR需要理解其核心概念,并合理使用Unity提供的工具和自定义着色器。通过本文介绍的方法,你可以创建出既美观又高效的PBR材质系统。
记住,PBR不仅仅是技术实现,更是艺术工作流的革命。正确使用PBR管道,可以让美术师更加专注于艺术创作,而不用过多担心技术限制。
进一步学习资源:
- Unity官方文档:Universal RP和HDRP的PBR实现
- 《Real-Time Rendering》第四版
- Disney的PBR原理论文
- Marmoset Toolbag的PBR教程
通过不断实践和优化,你将能够掌握PBR的精髓,为你的游戏或应用创造出令人惊叹的视觉效果。