1. 项目概述:为什么我们需要关注BakeShader?
在Unity项目开发的后期,尤其是涉及到复杂光照、阴影和全局光照(GI)的场景,性能优化往往是一个绕不开的坎。很多开发者都遇到过这样的场景:在编辑器里跑得挺流畅,一打包到移动端或者主机平台,帧率就直线下降。这时候,你打开Profiler,大概率会发现GPU的瓶颈卡在了实时渲染那些复杂的光照计算上。对于静态或半静态的场景元素,比如建筑、地形、大部分的道具,它们的光照信息其实在运行时是基本不变的。那么,一个很自然的想法就是:能不能把这些昂贵的计算提前做好,在运行时直接使用结果?这就是“烘焙”(Baking)的核心价值。
传统的Unity光照烘焙(Lightmapping)大家应该都不陌生,它生成的是存储光照颜色信息的贴图(Lightmap)。这对于漫反射光照效果很好,但对于一些需要高光、法线细节或者特殊着色效果的表面,仅靠Lightmap就显得力不从心了。这时,我们就需要更灵活、更强大的烘焙方案。BakeShader,顾名思义,就是一套专注于“烘焙”的Shader工具或框架。它允许你将任何自定义Shader的复杂渲染结果(不仅仅是光照,可以是任何计算)预先渲染到纹理上,从而在运行时用极其廉价的采样操作替代复杂的实时计算。这不仅仅是优化,更是一种设计范式的转变,让你能把美术效果和运行性能更好地解耦。
简单来说,BakeShader解决的核心痛点是: 在保证甚至提升视觉质量的前提下,将GPU从繁重的每帧计算中解放出来,特别适用于对性能有严苛要求的移动平台、VR/AR应用以及大型开放世界场景 。无论是想实现电影级的静态场景细节,还是为低端设备做深度优化,深入理解并应用BakeShader都是一项极具价值的技术。
2. BakeShader的核心原理与工作流程拆解
要理解BakeShader,我们得先抛开“Shader就是用来实时渲染的”这个固有观念。在这里,Shader扮演的是一个“离线计算器”的角色。其核心原理可以概括为: 利用渲染管线,将场景中特定物体在其最终显示状态下,通过你指定的Shader程序所计算出的像素颜色(或其他数据,如法线、深度),一次性渲染到一张或多张纹理(即烘焙贴图)上 。
2.1 与传统光照烘焙的异同
很多人会把BakeShader和Unity的Lightmapping搞混,它们有交集,但目标和能力范围不同。
-
Unity Lightmapping (光照贴图) :
- 目标 :主要烘焙静态物体的 间接光照 (全局光照)和 直接光照的阴影 。
- 输出 :通常是RGB颜色贴图(存储光照颜色)和方向贴图(存储主要光照方向,用于细节法线)。
- 局限 :烘焙流程相对固定,主要针对标准渲染管线或URP/HDRP的内置光照模型。对于自定义的高光、边缘光、程序化纹理等复杂着色效果,它无法直接烘焙。
-
BakeShader (着色器烘焙) :
- 目标 :烘焙 任意Shader的完整输出结果 。这可以是一个复杂的PBR材质(包含漫反射、法线、高光、粗糙度、金属度等所有信息的合成颜色),也可以是一个风格化的卡通着色效果,甚至是一张存储了世界位置、顶点颜色等数据的“数据贴图”。
- 输出 :完全由你定义的Shader决定。可以是一张RGBA的Albedo贴图,一张存储了法线和高度的RGBA贴图,或者多张不同用途的贴图集合。
- 优势 : 极度灵活 。你可以为美术资产定制最华丽的Shader来追求极致效果,然后通过烘焙将其“固化”为贴图,运行时使用一个极其简单的、只做纹理采样的Unlit Shader来显示。性能开销降至最低。
2.2 通用工作流程解析
一个典型的BakeShader工作流包含以下几个关键步骤,理解每一步的意图比记住具体操作更重要:
-
准备源模型与目标模型 :
- 源模型 (High-poly / Source Mesh) :这是拥有高细节、复杂材质的模型。它的UV可以不是最优的,但需要包含所有我们想烘焙的视觉细节。
- 目标模型 (Low-poly / Target Mesh) :这是最终在游戏中使用的、面数较低的模型。它需要有一套干净、舒展且不重叠的UV(通常称为“光照贴图UV”或第二套UV),用于承载烘焙出来的贴图。 烘焙的本质,就是将高模的细节“投影”或“渲染”到低模的UV空间上。
-
创建烘焙用Shader :
- 这不是一个运行时Shader,而是一个专门用于烘焙过程的Shader。它通常会接收高模的各类输入(顶点色、多套UV、切线空间等),并按照你希望最终呈现的效果进行计算。
- 例如,如果你想烘焙一个包含法线贴图细节的完整颜色,你的烘焙Shader就需要采样法线贴图,并在Shader中进行光照计算(使用一个固定的灯光方向),最后输出颜色。
-
配置烘焙渲染器与相机 :
- 在代码中,你需要创建一个临时的相机和渲染纹理(RenderTexture)。相机的渲染目标就是这个RenderTexture。
- 关键操作是: 将目标模型的每个UV坐标,映射为相机渲染空间中的一个像素 。这通常通过将目标模型的顶点,按照其UV坐标直接变换到NDC(标准化设备坐标)空间来实现,使得模型恰好填满整个渲染视口。然后,用这个相机、使用烘焙Shader去渲染源模型。这样,渲染纹理上的每一个像素,就精确对应了目标模型UV图上的一个点,并且其颜色是源模型在该处被烘焙Shader渲染的结果。
-
执行烘焙与后处理 :
- 触发渲染。将渲染纹理中的像素数据读取出来,保存为常见的图片格式(如PNG、TGA)。
- 可能需要进行后处理,如抗锯齿(通过超级采样)、色彩空间转换(线性到sRGB),或者将多张渲染纹理(如颜色、法线、金属度)打包成一张贴图集。
-
在运行时使用烘焙结果 :
- 为你的目标模型创建一个新的、极其简单的材质,使用一个Unlit Shader或者标准Shader(关闭实时光照)。
- 将烘焙好的贴图赋予这个材质。运行时,GPU只需要进行一次纹理采样,就能呈现出原本需要复杂计算才能得到的效果。
注意 :市面上也有一些成熟的第三方工具或插件(如TextureBaker、Bakery等)封装了上述流程。但理解底层原理,能帮助你在工具出错时进行排查,也能让你有能力定制特殊的烘焙需求。
3. 手动实现一个基础的BakeShader工具
理解了原理,我们不妨动手写一个简化版的烘焙工具核心代码。这将帮助你彻底吃透这个过程。我们将实现:将一个带有法线贴图的高模细节,烘焙到低模的漫反射贴图上。
3.1 创建烘焙Shader
首先,我们需要一个用于烘焙的Shader。这个Shader在烘焙时运行,模拟一次光照计算。
// BakeShader.shader
Shader "Hidden/BakeDiffuseWithNormal"
{
Properties
{
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_BakeLightDir ("Bake Light Direction", Vector) = (0.577, 0.577, 0.577, 0) // 归一化的光照方向
_BakeLightColor ("Bake Light Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldTangent : TEXCOORD2;
float3 worldBitangent : TEXCOORD3;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float3 _BakeLightDir;
float4 _BakeLightColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// 计算世界空间的TBN矩阵,用于法线贴图变换
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBitangent = cross(worldNormal, worldTangent) * v.tangent.w;
o.worldNormal = worldNormal;
o.worldTangent = worldTangent;
o.worldBitangent = worldBitangent;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 采样基础颜色
fixed4 albedo = tex2D(_MainTex, i.uv);
// 采样法线贴图,并解包到切线空间
float3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
// 构建TBN矩阵,将切线空间法线转换到世界空间
float3x3 TBN = float3x3(normalize(i.worldTangent), normalize(i.worldBitangent), normalize(i.worldNormal));
float3 worldNormal = mul(TBN, tangentNormal);
worldNormal = normalize(worldNormal);
// 简单的兰伯特漫反射光照计算(使用烘焙时设定的固定光源)
float NdotL = max(0, dot(worldNormal, _BakeLightDir));
fixed3 diffuse = albedo.rgb * _BakeLightColor.rgb * NdotL;
// 输出最终颜色(可以加上环境光等,这里简化)
return fixed4(diffuse, albedo.a);
}
ENDCG
}
}
}
这个Shader的关键在于,它不依赖于场景中的实时灯光,而是使用
_BakeLightDir
和
_BakeLightColor
这两个属性来定义一个固定的光源方向进行光照计算。烘焙时,我们会用这个Shader去渲染高模。
3.2 编写C#烘焙脚本
接下来是驱动烘焙过程的C#脚本。这个脚本的核心是创建一个临时的相机和RenderTexture,并按照目标模型的UV进行渲染。
// SimpleTextureBaker.cs
using UnityEngine;
using System.Collections;
using System.IO;
public class SimpleTextureBaker : MonoBehaviour
{
public GameObject sourceMeshObject; // 拥有高模和复杂材质的物体
public GameObject targetMeshObject; // 最终使用的低模物体
public string bakeShaderName = "Hidden/BakeDiffuseWithNormal";
public int textureSize = 1024; // 烘焙贴图尺寸
public string savePath = "Assets/BakedTextures/"; // 保存路径
public string textureName = "BakedDiffuse"; // 贴图名称
[Header("Bake Light Settings")]
public Vector3 lightDirection = new Vector3(0.577f, 0.577f, 0.577f).normalized;
public Color lightColor = Color.white;
private Shader bakeShader;
private Material bakeMaterial;
private Camera bakeCamera;
private RenderTexture renderTexture;
[ContextMenu("Bake Texture")]
public void Bake()
{
if (sourceMeshObject == null || targetMeshObject == null)
{
Debug.LogError("Source or Target Mesh Object is not assigned!");
return;
}
// 1. 加载烘焙Shader并创建临时材质
bakeShader = Shader.Find(bakeShaderName);
if (bakeShader == null)
{
Debug.LogError($"Bake Shader '{bakeShaderName}' not found!");
return;
}
bakeMaterial = new Material(bakeShader);
// 2. 设置烘焙光源参数到材质
bakeMaterial.SetVector("_BakeLightDir", lightDirection.normalized);
bakeMaterial.SetColor("_BakeLightColor", lightColor);
// 3. 将源物体的材质属性复制到烘焙材质(这里简化,假设只有一个材质)
Renderer sourceRenderer = sourceMeshObject.GetComponent<Renderer>();
if (sourceRenderer != null && sourceRenderer.sharedMaterial != null)
{
Material sourceMat = sourceRenderer.sharedMaterial;
if (sourceMat.HasProperty("_MainTex"))
bakeMaterial.SetTexture("_MainTex", sourceMat.GetTexture("_MainTex"));
if (sourceMat.HasProperty("_BumpMap"))
bakeMaterial.SetTexture("_BumpMap", sourceMat.GetTexture("_BumpMap"));
// 复制其他需要的属性...
}
// 4. 创建临时相机和RenderTexture
GameObject cameraGo = new GameObject("BakeCamera");
bakeCamera = cameraGo.AddComponent<Camera>();
bakeCamera.enabled = false; // 我们不希望它自动渲染
bakeCamera.clearFlags = CameraClearFlags.SolidColor;
bakeCamera.backgroundColor = Color.black; // 背景设为黑色或透明色
renderTexture = new RenderTexture(textureSize, textureSize, 24, RenderTextureFormat.ARGB32);
renderTexture.Create();
bakeCamera.targetTexture = renderTexture;
// 5. 关键步骤:设置相机投影矩阵,使其渲染与目标UV 1:1对应
// 思路:将目标模型的UV坐标(0,1)范围映射到相机的NDC(-1,1)范围。
// 一种常见方法是使用正交相机,并使其渲染范围恰好覆盖UV的(0,0)到(1,1)。
bakeCamera.orthographic = true;
bakeCamera.orthographicSize = 0.5f; // 正交大小设为0.5,这样从中心到上下边界是0.5,总高度是1。
bakeCamera.aspect = 1.0f; // 宽高比设为1,因为UV是正方形的。
// 设置相机位置和看向方向,使其正对渲染平面(这里简化处理,实际可能需要根据目标模型包围盒调整)
cameraGo.transform.position = new Vector3(0.5f, 0.5f, -1); // 看向UV空间中心点(0.5,0.5)
cameraGo.transform.rotation = Quaternion.LookRotation(Vector3.forward);
// 6. 创建一个临时物体来渲染源模型(使用烘焙材质)
GameObject tempRenderGo = Instantiate(sourceMeshObject);
Renderer tempRenderer = tempRenderGo.GetComponent<Renderer>();
if (tempRenderer != null)
{
tempRenderer.material = bakeMaterial; // 临时替换为烘焙材质
}
// 7. 执行渲染
bakeCamera.Render();
// 8. 从RenderTexture读取数据并保存为PNG
SaveRenderTextureToPNG(renderTexture);
// 9. 清理临时对象
DestroyImmediate(tempRenderGo);
DestroyImmediate(cameraGo);
if (Application.isEditor)
{
DestroyImmediate(bakeMaterial);
}
else
{
Destroy(bakeMaterial);
}
renderTexture.Release();
Debug.Log("Baking Complete! Texture saved to: " + savePath + textureName + ".png");
}
void SaveRenderTextureToPNG(RenderTexture rt)
{
// 确保目录存在
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
}
RenderTexture.active = rt;
Texture2D tex2D = new Texture2D(rt.width, rt.height, TextureFormat.ARGB32, false);
tex2D.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
tex2D.Apply();
RenderTexture.active = null;
byte[] bytes = tex2D.EncodeToPNG();
string fullPath = Path.Combine(savePath, textureName + ".png");
File.WriteAllBytes(fullPath, bytes);
// 在Unity编辑器中刷新资源数据库
#if UNITY_EDITOR
UnityEditor.AssetDatabase.Refresh();
#endif
DestroyImmediate(tex2D);
}
}
3.3 使用步骤与参数解析
-
场景准备
:在场景中放置你的高模(
sourceMeshObject)和低模(targetMeshObject)。低模需要有良好的第二套UV。 -
脚本挂载
:将
SimpleTextureBaker脚本挂载到一个空物体上。 -
参数配置
:
-
Source Mesh Object:拖入你的高模游戏物体。 -
Target Mesh Object:拖入你的低模游戏物体。 -
Texture Size:烘焙贴图的分辨率。根据低模的UV面积和所需精度来定,通常为1024或2048。 -
Bake Light Settings:这里设置一个固定的光源方向和颜色,用于烘焙时的光照计算。(0.577, 0.577, 0.577)是一个来自左上方的经典光源方向。
-
-
执行烘焙
:在Inspector窗口点击脚本上的
Bake Texture按钮([ContextMenu]特性生成)。脚本会自动执行上述流程,并在Assets/BakedTextures/文件夹下生成一张PNG贴图。 - 应用烘焙结果 :将生成的贴图赋给低模模型的一个新材质(使用Standard或Unlit Shader)。在运行时,这个低模就能呈现出接近高模(带有法线光照细节)的视觉效果,而性能消耗极低。
实操心得 :这个简易版本为了清晰省略了很多细节,比如处理多子网格(SubMesh)、多材质球、抗锯齿、处理透明通道、更精确的UV到相机的映射等。在实际项目中,你可能需要处理目标模型的UV包围盒,动态计算正交相机的位置和大小,以确保UV的每一个角落都被正确渲染。此外,对于复杂的模型,一次烘焙可能不够,需要分块(Chunk)烘焙或使用更专业的UV展开工具。
4. 进阶应用场景与方案选型
掌握了基础原理后,BakeShader的用武之地就非常广阔了。它不仅仅是为了优化,更能开启一些独特的视觉效果实现方式。
4.1 场景一:复杂材质效果的移动端优化
这是最经典的应用。你的美术制作了一个拥有多层混合、视差遮挡、动态苔藓等效果的顶级材质,在PC上跑都吃力,更别说手机了。
-
方案
:在PC或开发机上,使用一个复刻了所有复杂效果的烘焙Shader,为这个模型烘焙出一套贴图。这套贴图可能包括:
-
AlbedoBaked:包含所有颜色、纹理混合、光照信息的最终基础色。 -
EmissiveBaked:烘焙的自发光部分。 -
(可选)
PackedMap:将粗糙度、金属度、环境光遮蔽(AO)等单通道信息打包到一张贴图的RGBA通道中。
-
- 运行时 :移动端Shader只需采样1-2张贴图,进行最简单的计算。视觉损失极小,性能提升巨大。
4.2 场景二:程序化生成内容的预计算
你在做一个无限地形系统,地形材质由多种噪声图混合而成,并随着高度、坡度变化。每帧实时混合这些噪声图开销很大。
- 方案 :为地形区块(Chunk)预计算一张“材质索引图”或直接烘焙出最终的颜色/法线贴图。烘焙Shader的输入是世界的XZ坐标(或基于此计算的噪声),输出是混合后的材质权重或颜色。
- 运行时 :地形Shader只需采样这张预烘焙的贴图,无需进行复杂的噪声计算和混合。这对于开放世界游戏的地形渲染优化至关重要。
4.3 场景三:风格化渲染的“固化”
卡通渲染(Cel-shading)通常需要实时计算光照的阶跃、描边等。在低端设备或大量角色同屏时,这可能成为瓶颈。
- 方案 :为角色模型烘焙多张“色阶贴图”。例如,固定3个主光源方向(前、左、后),分别烘焙出亮部、中间调、暗部三张颜色贴图。
- 运行时 :使用一个简化版的卡通Shader,根据实时光源方向在这三张贴图之间进行插值或选择,来代替实时的光照计算和阶跃判断。描边信息也可以烘焙到一张贴图的Alpha通道中。
4.4 场景四:光照探针与Lightmap的补充
Unity的光照探针(Light Probes)为动态物体提供了间接光照,但它是基于球谐函数的低频近似,缺乏高频细节。对于需要精确镜面高光的动态物体,效果不佳。
- 方案 :将动态物体视为“静态”,为其烘焙一张包含精确高光信息的“反射探针贴图”或“高光遮罩贴图”。烘焙时,Shader可以采样场景的反射探针(Reflection Probe)和光照贴图,计算出精确的高光颜色和强度。
- 运行时 :动态物体的Shader采样这张烘焙贴图,再结合实时直接光,就能获得既有动态性又有高质量静态光照细节的效果。
4.5 工具选型:手动编码 vs. 第三方插件
-
手动编码(如上面的示例) :
- 优点 :完全可控,深度定制,理解底层,零成本。
- 缺点 :开发周期长,容易出错,功能不完善(如抗锯齿、UV展开、批量处理等)。
- 适用 :有特殊定制需求、项目规模较小或作为学习研究。
-
第三方插件(如TextureBaker, Bakery, Mesh Baker) :
- 优点 :功能强大且全面(支持多模型合并、UV自动展开、抗锯齿、AO烘焙等),有图形界面(GUI),工作流成熟,节省大量开发时间。
- 缺点 :需要付费,可能对项目管线有侵入性,遇到极限情况可能受限于插件功能。
- 适用 :中大型商业项目,追求稳定高效的生产流水线。
我的建议是 :对于严肃的商业项目,尤其是美术资源量大的项目, 强烈建议使用成熟的第三方插件 。它们解决了很多工程上的“脏活累活”,稳定性更高。手动实现更适合作为技术储备,用于解决插件无法覆盖的特定难题。
5. 常见问题、性能陷阱与排查技巧
在实际应用BakeShader的过程中,你会遇到各种各样的问题。下面是一些典型问题及其解决思路,很多都是我在项目中踩过的坑。
5.1 烘焙贴图出现接缝、错位或拉伸
这是最常见的问题,根源几乎都出在 UV 上。
-
问题诊断 :
- 检查目标模型的UV :确保用于烘焙的第二套UV(通常叫UV1或Lightmap UV)是完全展开、没有重叠、且边界留有足够padding(通常2-4个像素)的。UV islands之间不要靠得太近。
- 检查UV是否在0-1范围内 :烘焙相机通常只渲染UV(0,0)到(1,1)这个区域。如果模型的UV坐标超出了这个范围,超出的部分就不会被渲染,或者会导致重复平铺(如果Wrap Mode是Repeat)。
-
检查烘焙相机的投影设置
:在手动实现的工具中,确保相机正交投影的
orthographicSize和aspect设置正确,并且相机对准了UV空间的中心(0.5, 0.5)。一个快速的调试方法是,用一个纯色的Shader烘焙,看生成的贴图是否均匀地覆盖了整个UV空间。
-
解决方案 :
- 使用专业的3D软件(如Maya, 3ds Max, Blender)或Unity的模型导入设置中的“Generate Lightmap UVs”功能,重新生成干净的第二套UV。
-
在烘焙工具的代码中,根据目标模型UV的实际边界(
mesh.uv.bounds)来动态计算相机的投影矩阵和位置,而不是假设UV总是(0,0)到(1,1)。 -
增加烘焙贴图的分辨率,并在Shader中使用
tex2D时加上微小的偏移(如半个纹素),有时可以缓解因精度问题导致的接缝。
5.2 烘焙结果与实时渲染效果不一致
这通常是因为烘焙环境和运行时环境不一致。
- 光照不一致 :烘焙Shader中使用的光源方向、颜色、强度是否与游戏场景中的主光源匹配?环境光(Ambient)和天空盒(Skybox)是否考虑进去了?确保烘焙时模拟了游戏中的光照条件。
- Shader不一致 :你的烘焙Shader是否完美复刻了运行时复杂Shader的所有计算?包括纹理混合模式、Alpha测试/混合、顶点动画(如果烘焙的是静态帧)等。任何细微差别都会导致结果不同。
- 材质属性不一致 :检查所有材质属性(纹理、颜色、浮点数参数)是否都正确地从源材质传递到了烘焙材质。在脚本中遍历并复制所有属性是一个好习惯。
5.3 烘焙过程耗时过长或内存占用高
烘焙,尤其是高分辨率、多物体的烘焙,是一个资源密集型操作。
-
优化策略
:
- 分块烘焙 :不要试图一次性烘焙整个巨大场景。将场景按区域或按物体类型分块,分批烘焙。
- 降低烘焙分辨率 :对于远景或小物体,使用较低的贴图分辨率。可以制定一个分级标准(如:主角2048,主要道具1024,环境装饰512)。
- 使用渐进式烘焙或差值烘焙 :对于变化不大的部分,可以只烘焙变化区域(Dirty Region)。一些高级插件支持此功能。
-
管理临时资源
:在烘焙脚本中,确保及时销毁临时创建的Camera、RenderTexture、Material和GameObject。使用
DestroyImmediate(编辑器下)或Destroy,并调用RenderTexture.Release()。 - 异步操作 :对于需要烘焙大量资产的情况,考虑将烘焙操作放在后台线程或协程中分帧进行,避免主线程卡死。
5.4 在移动设备上,使用烘焙贴图后出现性能回退
这听起来反直觉,但确实可能发生。
-
原因分析 :
- 贴图尺寸过大 :一张4096x4096的贴图,即使在内存中,其采样带宽和缓存压力也可能超过进行一些简单实时计算的开销。特别是对于很多小物体,每个都用大贴图是极大的浪费。
- 过度绘制(Overdraw) :如果烘焙贴图包含透明通道,且渲染顺序设置不当,可能导致严重的过度绘制。
- Shader复杂度转移 :虽然片段着色器简化了,但顶点着色器或后处理可能引入了新的开销。或者,为了使用烘焙贴图,你启用了之前关闭的功能(如雾效、动态合批打断等)。
-
解决方案 :
- 严格管理贴图尺寸和格式 :使用ASTC、ETC2等移动端压缩格式。使用纹理图集(Texture Atlas)将多个小物体的烘焙贴图合并到一张大图上,减少Draw Call和纹理切换。
- 性能分析 :使用Unity Profiler和Frame Debugger。对比使用烘焙贴图前后的GPU耗时、内存占用、Draw Call和SetPass Call数量。找到真正的瓶颈。
- 简化运行时Shader :确保用于显示烘焙贴图的Shader是真正的“轻量级”。避免不必要的计算、纹理采样和分支。
5.5 烘焙贴图在特定角度或光照下“发平”或“变暗”
这通常是因为烘焙的光照信息是“固化”的,无法响应运行时光照的变化。
- 问题本质 :你烘焙的是“光照结果”,而不是“材质属性”。例如,你烘焙了一个在左上角光源下的漫反射颜色。当游戏中的光源移动到右侧时,模型本该变亮的部分(右侧)却因为烘焙了暗部的颜色而显得暗。
-
解决方案
:
- 烘焙“光照无关”的信息 :不要烘焙包含直接光照漫反射的颜色。改为烘焙 Albedo(反照率) 、 法线 、 粗糙度 、 金属度 、 环境光遮蔽(AO) 等材质属性。然后在运行时,使用标准PBR光照模型,结合这些属性和实时灯光进行计算。这样模型就能正确响应动态光照了。这才是更现代、更灵活的用法。
- 使用光照贴图(Lightmap)处理静态间接光 :将动态直接光和静态间接光分离。静态间接光(包括天空光、反射光、全局光照)用Unity的Lightmap烘焙。动态物体则使用实时直接光+烘焙的材质属性+Light Probe。这样既能保证动态性,又能有丰富的间接光照细节。
BakeShader是一个强大的“时间换空间”策略。它要求我们在开发管线中投入更多的前期准备和烘焙时间,来换取运行时极致的性能红利。它不是一个“银弹”,需要根据项目具体需求(目标平台、美术风格、场景复杂度)来谨慎设计和实施。当你被实时渲染的性能压得喘不过气时,回头看看BakeShader,它很可能就是那把帮你打开瓶颈的钥匙。从我个人的经验来看,在移动端项目的中后期,系统地引入BakeShader对静态和半静态资产进行优化,往往是性能提升最显著、性价比最高的手段之一。关键在于,要建立一套可靠、自动化的烘焙流水线,并让美术同学从一开始就在资源制作规范中考虑烘焙的需求,比如准备好低模和干净的UV。




被折叠的 条评论
为什么被折叠?



