第6章 次世代场景构建实战:《暗黑王朝》光影环境的全流程制作
第6章 次世代场景构建实战:《暗黑王朝》光影环境的全流程制作
在《暗黑王朝》这样一款写实风格的射击手游中,场景的视觉品质直接决定了游戏的沉浸感。玩家踏入游戏世界的第一眼,看到的不应该是生硬的几何体和简陋的贴图,而应该是一个有温度、有氛围、光影交错的真实空间。要实现这样的效果,仅仅依靠模型和贴图是远远不够的,必须引入完整的光影烘焙技术。本章将从最基础的模型制作规范开始,逐步深入到Unity的光照贴图烘焙、天空盒制作、摄影机特效等完整流程,为读者呈现一套完整的次世代场景构建方案。
6.1 移动端场景模型的制作规范与优化策略
6.1.1 手机游戏场景的建模思维转变
在PC或主机平台上,开发者可以相对自由地使用高面数模型和复杂的着色器,因为玩家拥有强大的GPU和充足的内存。但在移动平台上,一切都必须精打细算。一个在PC上运行流畅的场景,放到手机上可能连10帧都跑不到。
移动端场景建模的第一个核心原则是:用最少的面数传达最多的信息。这不是简单地减少模型细节,而是通过巧妙的资产设计、纹理运用和LOD技术,让玩家在视觉上感受到丰富的细节,而GPU实际处理的数据量却很小。
根据行业实践,单个角色的面数建议控制在5000-15000面之间,单个场景的总体面数不宜超过10万面。对于中低端设备,这个数字还需要进一步压缩。在《暗黑王朝》的开发中,我们为不同重要性的物体制定了严格的面数预算:
- 主角模型:8000-10000面(包括武器)
- 精英敌人:5000-6000面
- 普通敌人:2500-3000面
- 主要建筑:1500-2000面
- 装饰物(路灯、箱子等):200-500面
- 远景建筑物:500-800面(配合LOD可降至200面)
第二个核心原则是:纹理尺寸不是越大越好。2K纹理在PC上可能刚刚好,但在手机上会导致严重的内存压力。对于移动设备,主纹理建议使用1024x1024或2048x2048,次要物体使用512x512,远处的物体甚至可以降到256x256。同时必须开启Mipmap,让远处的物体自动使用低分辨率纹理,既节省性能又避免闪烁。
6.1.2 从参考图到建筑模型的完整流程
在《暗黑王朝》的第一个关卡中,玩家需要穿越一座被遗弃的研究站。这座建筑的设计融合了工业风格和科幻元素,需要从零开始构建。
建模的第一步是收集参考图。我们搜集了真实的工业建筑照片、科幻电影的场景截图、甚至是一些废弃工厂的实拍素材。将这些图片导入到三维软件(如Blender或3ds Max)的参考图视口中,开始进行基础建模。
以下是一个典型建筑的建模步骤:
-
主体结构:从一个立方体开始,通过挤出(Extrude)和倒角(Bevel)操作建立建筑的主体框架。主体面数控制在300面以内。
-
细节添加:在主体上添加窗户、门框、管道、通风口等细节。这些细节不能直接建模在主体上,而应该作为单独的部件,以便后续的纹理映射和LOD处理。
-
模块化设计:将建筑分解为可复用的模块——墙段、柱子、窗户组件、门组件等。这种模块化设计不仅提高了建模效率,也便于在Unity中进行组合和布局。
-
UV展开:为模型展开UV,确保纹理空间得到充分利用。对于建筑模型,通常采用投影映射的方式,将不同方向的面对应到纹理的不同区域。
-
AO烘焙:在三维软件中烘焙环境光遮蔽贴图,作为后续PBR材质的辅助。
在建模过程中,需要特别注意面片的走向。移动端的GPU对三角面的处理效率很高,但三角面之间的排列方式会影响渲染效果。尽量避免出现狭长的三角面,这种面在光照计算中容易产生异常。
6.1.3 多个建筑之间的视觉关联与布局
单个建筑制作完成后,需要将它们组合成一个完整的场景。在这个过程中,需要考虑建筑之间的视觉关联——它们不应该像是随意摆放在地面上的独立物体,而应该构成一个有逻辑、有故事的空间。
在《暗黑王朝》的研究站场景中,我们采用了"中心辐射式"的布局:
- 核心建筑:中央主楼是场景的视觉中心,体量最大,细节最丰富
- 附属建筑:围绕主楼分布着仓库、宿舍、岗亭等小型建筑
- 连接元素:通过走廊、管道、围栏等元素将各个建筑连接起来
- 环境叙事:通过倒塌的围墙、废弃的车辆、散落的箱子等元素,暗示这里曾经发生过什么
这种布局不仅让场景看起来更加真实,也为玩家提供了清晰的视觉引导。在游戏中,玩家会自然地朝着视觉中心移动,这为关卡设计提供了天然的路径。
在Unity中摆放建筑时,可以利用网格对齐功能,确保建筑之间保持合理的间距和角度。同时,要避免建筑之间出现穿模或过于拥挤的情况。
6.1.4 场景构架的元素构成
一个完整的游戏场景不仅仅包含建筑,还需要多种元素的配合:
地形:地面是场景的基础。对于移动平台,地形应该使用网格而非Unity的地形系统,因为后者会产生大量的顶点和绘制调用。可以使用一个简单的平面,在上面放置道路、草地、碎石等平面装饰物。
植被:树木和草丛是营造氛围的重要元素,但也是最消耗性能的部分。对于移动平台,建议使用面片交叉(Cross Plane)的方式制作树木和草丛,每个植物由两个互相垂直的面片组成,既节省面数又能保持较好的视觉效果。
道具:散布在场景中的小道具——箱子、油桶、路牌、路灯等——能够增加场景的丰富度。这些道具应该设计为可复用的预制体,通过不同的摆放组合产生变化。
特效:粒子特效可以增加场景的生动感,例如飘落的树叶、飞扬的尘土、闪烁的灯光等。但粒子特效对CPU的消耗较大,需要严格控制数量和范围。
在《暗黑王朝》中,我们将场景元素按照重要性划分为三个层级:
- 核心元素:玩家必然经过的区域、主要建筑——高面数、高分辨率纹理
- 次要元素:玩家可能看到但不会近距离接触的区域——中等面数、中等纹理
- 背景元素:远景、天空盒等——低面数、低分辨率纹理,或仅使用2D贴图
6.1.5 面数分配与贴图大小的平衡艺术
面数和贴图大小是移动端优化的两大核心指标,它们之间存在微妙的平衡关系。高面数低纹理可以得到清晰的几何轮廓,但纹理细节不足;低面数高纹理可以获得丰富的表面细节,但几何轮廓会显得生硬。
在《暗黑王朝》中,我们遵循以下原则:
-
近景物体重纹理,远景物体重轮廓:玩家近距离观察的物体,使用较低的精度加上高分辨率纹理;远距离看到的物体,使用简化的轮廓和低分辨率纹理。
-
法线贴图替代几何细节:在建筑表面使用法线贴图模拟铆钉、焊缝、砖缝等细节,而不是实际建模这些细节。一个法线贴图只需要几个像素的代价,却能产生相当于数万面的视觉效果。
-
共用纹理集:将多个相关物体的纹理合并到一张大纹理中,减少材质数量和绘制调用。例如,所有金属材质的物体共用一张金属纹理集,所有木头材质的物体共用一张木纹纹理集。
-
按距离分层LOD:为每个重要物体创建2-3个LOD级别,当物体距离变远时,自动切换为更低面数的版本。LOD0(最近)使用完整面数,LOD1(中等距离)删除40%的面数,LOD2(远距离)删除70%的面数。
以下是一个LOD组的配置示例:
using UnityEngine;
namespace DarkOrder.Optimization
{
/// <summary>
/// LOD组自动配置工具
/// </summary>
[RequireComponent(typeof(LODGroup))]
public class LODAutoConfig : MonoBehaviour
{
[Header("LOD设置")]
[SerializeField]
private float[] m_LODPercentages = { 0.5f, 0.3f, 0.15f }; // LOD级别占原始面数的百分比
[SerializeField]
private float[] m_LODScreenRelativeTransitionHeights = { 0.3f, 0.15f, 0.05f };
[SerializeField]
private Renderer[] m_Renderers;
private void Start()
{
ConfigureLODs();
}
[ContextMenu("配置LOD")]
private void ConfigureLODs()
{
if (m_Renderers == null || m_Renderers.Length == 0)
{
m_Renderers = GetComponentsInChildren<Renderer>();
}
// 创建LOD组
LODGroup lodGroup = GetComponent<LODGroup>();
// 创建LOD数组
LOD[] lods = new LOD[m_LODPercentages.Length + 1]; // +1 表示剔除(Culled)
// 配置每个LOD级别
for (int i = 0; i < m_LODPercentages.Length; i++)
{
// 这里应该根据百分比实际简化模型,但运行时无法简化
// 实际项目中需要在编辑器预处理阶段生成简化模型
lods[i] = new LOD(
m_LODScreenRelativeTransitionHeights[i],
m_Renderers
);
}
// 最后一个LOD级别为剔除
lods[lods.Length - 1] = new LOD(0f, new Renderer[0]);
lodGroup.SetLODs(lods);
lodGroup.RecalculateBounds();
Debug.Log($"LOD已配置,共 {lods.Length} 个级别");
}
}
}
6.2 模型导入Unity的规范化流程
6.2.1 模型的分类管理与命名规范
当所有模型在三维软件中制作完成后,需要导入到Unity中进行组装。在这个过程中,建立清晰的分类管理和命名规范至关重要。
《暗黑王朝》的项目结构采用以下组织方式:
Assets/
├── Art/
│ ├── Characters/
│ │ ├── Player/
│ │ │ ├── Models/
│ │ │ ├── Textures/
│ │ │ ├── Materials/
│ │ │ └── Animations/
│ │ └── Enemies/
│ │ ├── EnemyTypeA/
│ │ └── EnemyTypeB/
│ ├── Environment/
│ │ ├── Buildings/
│ │ ├── Props/
│ │ ├── Terrain/
│ │ └── Skybox/
│ └── Effects/
├── Scripts/
├── Prefabs/
├── Scenes/
└── Audio/
命名规范采用"类型_名称_用途"的格式:
- 模型文件:
mdl_ResearchStation_Main.fbx - 纹理文件:
tex_ResearchStation_Base_Albedo.png - 材质文件:
mat_ResearchStation_Main - 预制体:
pre_ResearchStation_Complete
6.2.2 动画模型与静态模型的导入设置差异
在导入模型时,需要根据模型类型选择不同的导入设置。Unity的模型导入器提供了丰富的选项,合理的设置可以显著优化运行时性能。
动画模型的导入设置:
// 此代码仅为说明,实际需要在导入器中手动设置或使用AssetPostProcessor
- Rig选项卡:Animation Type设置为Humanoid(角色)或Generic(非人型),Avatar Definition设置为Create From This Model
- Animation选项卡:根据需要导入的动画剪辑,设置循环时间、循环姿势等
- Mesh压缩:设置为Medium或High,减少内存占用
静态模型的导入设置:
- Model选项卡:取消选择Import Animation,如果不需要导入灯光和摄像机也一并取消
- Mesh压缩:可以设置为High,因为静态模型对顶点精度的要求相对较低
- Generate Colliders:根据需要选择,如果模型需要物理碰撞则开启
- Lightmap UVs:必须开启,因为后续需要烘焙光照贴图
以下是一个模型导入后处理器的示例,用于自动设置不同类型模型的导入选项:
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
namespace DarkOrder.EditorTools
{
/// <summary>
/// 模型导入后处理器 - 自动设置导入选项
/// </summary>
public class ModelImportProcessor : AssetPostprocessor
{
private void OnPreprocessModel()
{
ModelImporter importer = (ModelImporter)assetImporter;
// 根据文件路径判断模型类型
if (assetPath.Contains("Characters"))
{
// 角色模型设置
ConfigureCharacterModel(importer);
}
else if (assetPath.Contains("Environment"))
{
// 环境模型设置
ConfigureEnvironmentModel(importer);
}
else if (assetPath.Contains("Props"))
{
// 道具模型设置
ConfigurePropModel(importer);
}
}
private void ConfigureCharacterModel(ModelImporter importer)
{
importer.importAnimation = true;
importer.animationCompression = ModelImporterAnimationCompression.KeyframeReduction;
importer.importBlendShapes = true;
importer.meshCompression = ModelImporterMeshCompression.Medium;
importer.generateSecondaryUV = true; // 生成光照贴图UV
importer.animationRotationError = 0.5f;
importer.animationPositionError = 0.5f;
Debug.Log($"角色模型导入设置完成: {assetPath}");
}
private void ConfigureEnvironmentModel(ModelImporter importer)
{
importer.importAnimation = false;
importer.importCameras = false;
importer.importLights = false;
importer.meshCompression = ModelImporterMeshCompression.High;
importer.generateSecondaryUV = true;
Debug.Log($"环境模型导入设置完成: {assetPath}");
}
private void ConfigurePropModel(ModelImporter importer)
{
importer.importAnimation = false;
importer.meshCompression = ModelImporterMeshCompression.Medium;
importer.generateSecondaryUV = true;
Debug.Log($"道具模型导入设置完成: {assetPath}");
}
private void OnPostprocessModel(GameObject root)
{
// 导入后处理:添加必要的组件
if (assetPath.Contains("Environment") || assetPath.Contains("Props"))
{
// 为静态模型添加Static标志
SetStaticRecursive(root.transform, true);
}
}
private void SetStaticRecursive(Transform transform, bool isStatic)
{
transform.gameObject.isStatic = isStatic;
foreach (Transform child in transform)
{
SetStaticRecursive(child, isStatic);
}
}
}
}
#endif
6.2.3 动画专用模型的合并策略
对于带有动画的角色模型,通常会将模型和动画分开处理:模型作为一个FBX导入,动画作为独立的FBX文件或同一FBX中的多个动作。这种分离式管理有助于减少资源冗余。
在《暗黑王朝》中,每个角色类型都有一个基础模型FBX,包含角色的网格和骨骼。动画剪辑则存储在以动作命名的独立FBX文件中,或者使用Unity的动画系统直接导入。
合并动画模型的原则是:相同骨骼结构的模型可以共用动画。这意味着我们可以创建一个基础骨骼结构,所有同类角色都使用这个骨骼,然后为每个角色创建不同的外观模型。这样,一套动画就可以复用于多个角色,大大减少动画文件的数量。
6.2.4 固定模型的合并优化
对于不会移动的静态模型(建筑、地形、大型道具),可以合并它们以减少绘制调用。Unity的静态批处理功能可以自动合并标记为Static且使用相同材质的物体。
以下是静态批处理的最佳实践:
-
合并材质:尽量让多个静态物体使用同一张材质。可以通过纹理集(Texture Atlas)技术,将多个物体的纹理合并到一张大纹理上。
-
标记Static:在Inspector中勾选物体的Static复选框,或在代码中设置
gameObject.isStatic = true。 -
按材质分组:如果无法避免使用多种材质,至少确保相同材质的物体可以被批处理。
-
避免动态物体穿插:静态批处理的物体不能移动,如果它们和动态物体穿插,可能会导致渲染顺序问题。
6.2.5 光照贴图UV通道的生成
光照贴图烘焙需要模型拥有第二套UV通道,即UV2。这可以在三维建模软件中手动展开,也可以在Unity导入时自动生成。
自动生成光照贴图UV的设置如下:
- 在模型导入器的Model选项卡中,勾选Generate Lightmap UVs
- 设置以下参数:
- Hard Angle:硬边角度,超过这个角度的面会被分开
- Pack Margin:UV岛之间的间距
- Angle Error、Area Error:控制UV展开的质量
以下是一个通过代码检查模型是否有有效UV2的脚本:
using UnityEngine;
using UnityEditor;
namespace DarkOrder.EditorTools
{
/// <summary>
/// UV2检查器
/// </summary>
public class UV2Checker : EditorWindow
{
[MenuItem("Tools/检查UV2通道")]
public static void ShowWindow()
{
GetWindow<UV2Checker>("UV2检查器");
}
private Vector2 m_ScrollPosition;
private void OnGUI()
{
GUILayout.Label("检查选中模型的UV2通道", EditorStyles.boldLabel);
m_ScrollPosition = EditorGUILayout.BeginScrollView(m_ScrollPosition);
GameObject[] selectedObjects = Selection.gameObjects;
foreach (GameObject obj in selectedObjects)
{
CheckGameObjectUV2(obj);
}
EditorGUILayout.EndScrollView();
if (GUILayout.Button("修复选中模型的UV2"))
{
FixUV2ForSelected();
}
}
private void CheckGameObjectUV2(GameObject obj)
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField(obj.name, EditorStyles.boldLabel);
MeshFilter[] meshFilters = obj.GetComponentsInChildren<MeshFilter>();
SkinnedMeshRenderer[] skinnedRenderers = obj.GetComponentsInChildren<SkinnedMeshRenderer>();
EditorGUI.indentLevel++;
foreach (MeshFilter mf in meshFilters)
{
CheckMeshUV2(mf.sharedMesh, mf.gameObject.name);
}
foreach (SkinnedMeshRenderer smr in skinnedRenderers)
{
CheckMeshUV2(smr.sharedMesh, smr.gameObject.name);
}
EditorGUI.indentLevel--;
EditorGUILayout.EndVertical();
}
private void CheckMeshUV2(Mesh mesh, string objectName)
{
if (mesh == null) return;
bool hasUV2 = mesh.uv2 != null && mesh.uv2.Length > 0;
GUI.color = hasUV2 ? Color.green : Color.red;
EditorGUILayout.LabelField($"{objectName}: {(hasUV2 ? "✓ 有UV2" : "✗ 无UV2")}");
GUI.color = Color.white;
}
private void FixUV2ForSelected()
{
foreach (GameObject obj in Selection.gameObjects)
{
string assetPath = AssetDatabase.GetAssetPath(obj);
if (string.IsNullOrEmpty(assetPath))
{
Debug.LogWarning($"跳过 {obj.name}: 不是资源文件");
continue;
}
ModelImporter importer = AssetImporter.GetAtPath(assetPath) as ModelImporter;
if (importer != null)
{
importer.generateSecondaryUV = true;
importer.secondaryUVHardAngle = 80f;
importer.secondaryUVPackMargin = 4f;
importer.SaveAndReimport();
Debug.Log($"已为 {assetPath} 生成UV2");
}
}
AssetDatabase.Refresh();
}
}
}
6.2.6 模型的统一导出规范
从三维软件导出模型时,需要遵循统一的规范,以确保在Unity中的一致性:
- 轴方向:Unity使用Y轴向上,因此导出时应确保模型朝向正确。如果使用3ds Max,需要设置Z轴向上并旋转-90度,或直接在Unity导入器中调整。
- 单位:统一使用米作为单位,确保不同模型导入后比例一致。
- 缩放:导出时应用所有变换,确保缩放值为1,1,1。
- 材质:选择嵌入材质或导出材质文件,但最终在Unity中都会重新指定材质,因此可以简化处理。
6.3 Unity中的光影环境构建
6.3.1 模型的导入与场景组装
当所有模型都准备好并导出为FBX后,接下来需要在Unity中进行场景组装。
步骤1:导入模型 - 将FBX文件拖入Project窗口的对应文件夹。Unity会自动生成模型文件和对应的材质球(如果有嵌入材质)。
步骤2:创建预制体 - 将模型从Project窗口拖入场景,调整位置、旋转、缩放,然后拖回Project窗口创建预制体。这样可以保留模型的变换信息,方便后续复用。
步骤3:布置场景 - 使用预制体在场景中进行布局。可以利用Unity的网格对齐功能(按住Ctrl移动)确保物体之间的精确对齐。
步骤4:添加碰撞体 - 为玩家可以站立、碰撞的物体添加碰撞体。对于复杂模型,可以使用多个基础碰撞体组合,而不是使用模型的网格碰撞体,后者性能较差。
6.3.2 基础灯光系统的搭建策略
在《暗黑王朝》中,我们采用主光源+辅助光源+环境光的组合方式来营造场景氛围。
主光源:使用一盏平行光(Directional Light)作为主光源,模拟太阳或月亮。这盏光源负责产生主要的明暗关系和阴影。对于移动平台,主光源应该是唯一开启阴影的光源。
辅助光源:使用点光源(Point Light)或聚光灯(Spot Light)补充局部光照。这些光源通常关闭阴影,仅用于照亮特定区域。
环境光:通过Lighting窗口中的Environment Lighting设置环境光照。可以使用天空盒的颜色作为环境光,或自定义环境光颜色和强度。
在Lighting窗口中进行基础设置:
- Skybox Material:指定天空盒材质
- Sun Source:指定主光源
- Environment Lighting:设置环境光来源和强度
- Environment Reflections:设置反射探针的来源
以下是一个自动设置场景光照的脚本:
using UnityEngine;
namespace DarkOrder.Lighting
{
/// <summary>
/// 场景光照初始化器
/// </summary>
[ExecuteAlways]
public class SceneLightingInitializer : MonoBehaviour
{
[Header("主光源设置")]
[SerializeField]
private Light m_MainLight;
[SerializeField]
private Color m_LightColor = Color.white;
[SerializeField]
private float m_Intensity = 1f;
[SerializeField]
private float m_Longitude = -30f; // 经度
[SerializeField]
private float m_Latitude = 60f; // 纬度
[Header("环境设置")]
[SerializeField]
private Material m_SkyboxMaterial;
[SerializeField]
private Color m_AmbientColor = new Color(0.2f, 0.2f, 0.2f);
[SerializeField]
private float m_AmbientIntensity = 1f;
private void Awake()
{
InitializeLighting();
}
[ContextMenu("初始化光照")]
public void InitializeLighting()
{
// 创建或获取主光源
if (m_MainLight == null)
{
m_MainLight = FindObjectOfType<Light>();
if (m_MainLight == null)
{
GameObject lightObj = new GameObject("Main Directional Light");
lightObj.transform.SetParent(transform);
m_MainLight = lightObj.AddComponent<Light>();
m_MainLight.type = LightType.Directional;
}
}
// 设置主光源
m_MainLight.color = m_LightColor;
m_MainLight.intensity = m_Intensity;
// 计算平行光方向(基于经纬度)
float radianLat = m_Latitude * Mathf.Deg2Rad;
float radianLon = m_Longitude * Mathf.Deg2Rad;
Vector3 direction = new Vector3(
Mathf.Cos(radianLat) * Mathf.Sin(radianLon),
Mathf.Sin(radianLat),
Mathf.Cos(radianLat) * Mathf.Cos(radianLon)
);
m_MainLight.transform.rotation = Quaternion.LookRotation(-direction);
// 设置阴影
m_MainLight.shadows = LightShadows.Soft;
m_MainLight.shadowStrength = 0.8f;
m_MainLight.shadowResolution = LightShadowResolution.Medium;
// 设置天空盒
if (m_SkyboxMaterial != null)
{
RenderSettings.skybox = m_SkyboxMaterial;
}
// 设置环境光
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
RenderSettings.ambientLight = m_AmbientColor * m_AmbientIntensity;
Debug.Log("场景光照初始化完成");
}
}
}
6.4 光照贴图烘焙的完整流程
6.4.1 烘焙前的准备工作
光照贴图烘焙(Lightmapping)是将静态光照信息预先计算并存储在纹理中的过程。烘焙后的场景不再需要实时光照计算,可以大幅提高运行效率。
在开始烘焙前,需要完成以下准备工作:
-
标记静态物体:所有参与烘焙的物体必须标记为Contribute GI(在Static下拉菜单中选择)。这些物体会接收烘焙光照,也会影响其他物体的光照。
-
检查UV2:确保所有参与烘焙的物体都有正确的UV2通道。UV2应该没有重叠,且充分利用纹理空间。
-
设置光照贴图参数:在每个物体的Mesh Renderer组件中,可以设置:
- Scale In Lightmap:控制该物体在光照贴图中的分辨率倍数
- Lightmap Parameters:高级参数,控制采样率等
-
布置光照探针:对于动态物体,需要放置光照探针(Light Probes),让动态物体能够接收静态环境的光照信息。
6.4.2 烘焙参数的精细设置
Unity提供了两种光照贴图烘焙器:渐进式光照贴图(Progressive Lightmapper)和Enlighten。在2021.3版本中,渐进式光照贴图是默认选项,它基于路径追踪算法,能够产生更高质量的间接光照。
在Lighting窗口的Lightmapping Settings中,关键参数包括:
Lightmapper:选择Progressive GPU(如果显卡支持)可以获得最快的烘焙速度。
Direct Samples / Indirect Samples:控制直接光照和间接光照的采样数。较高的采样数可以降低噪点,但会显著增加烘焙时间。在测试阶段可以使用较低的采样数(如32/128),最终烘焙时再提高(如128/512)。
Bounces:光线反弹次数。对于室外场景,2-3次通常足够;对于室内场景,可能需要3-4次。
Lightmap Resolution:光照贴图的分辨率,单位是"texels per unit"。这个参数控制每个世界单位对应多少纹素。对于主要场景,通常设置为20-40;对于细节区域,可以提高到50-60。
Lightmap Padding:不同物体的光照贴图之间的间距。过小可能导致漏光,过大会浪费纹理空间。
Compress Lightmaps:移动平台必须开启,可以减少内存占用。
以下是一个通过脚本批量设置光照贴图参数的工具:
using UnityEngine;
using UnityEditor;
namespace DarkOrder.EditorTools
{
/// <summary>
/// 光照贴图批量设置工具
/// </summary>
public class LightmapBatchSetter : EditorWindow
{
private float m_ScaleInLightmap = 1f;
private float m_Priority = 1f;
[MenuItem("Tools/批量设置光照贴图参数")]
public static void ShowWindow()
{
GetWindow<LightmapBatchSetter>("光照贴图参数");
}
private void OnGUI()
{
GUILayout.Label("批量设置选中物体的光照贴图参数", EditorStyles.boldLabel);
m_ScaleInLightmap = EditorGUILayout.FloatField("Scale In Lightmap", m_ScaleInLightmap);
m_Priority = EditorGUILayout.FloatField("Priority", m_Priority);
if (GUILayout.Button("应用到选中物体"))
{
ApplyToSelected();
}
if (GUILayout.Button("按类型自动计算Scale"))
{
AutoCalculateScale();
}
}
private void ApplyToSelected()
{
GameObject[] selectedObjects = Selection.gameObjects;
int count = 0;
foreach (GameObject obj in selectedObjects)
{
MeshRenderer[] renderers = obj.GetComponentsInChildren<MeshRenderer>();
foreach (MeshRenderer renderer in renderers)
{
renderer.scaleInLightmap = m_ScaleInLightmap;
renderer.receiveGI = ReceiveGI.Lightmaps;
count++;
}
}
Debug.Log($"已更新 {count} 个MeshRenderer的光照贴图参数");
}
private void AutoCalculateScale()
{
GameObject[] selectedObjects = Selection.gameObjects;
int count = 0;
foreach (GameObject obj in selectedObjects)
{
MeshRenderer[] renderers = obj.GetComponentsInChildren<MeshRenderer>();
foreach (MeshRenderer renderer in renderers)
{
// 根据物体在屏幕上的大小自动计算Scale
// 大物体使用较高的Scale,小物体使用较低的Scale
Bounds bounds = renderer.bounds;
float size = bounds.size.magnitude;
float scale = Mathf.Clamp(size * 0.5f, 0.2f, 2f);
renderer.scaleInLightmap = scale;
count++;
}
}
Debug.Log($"已自动计算 {count} 个物体的Scale");
}
}
}
6.4.3 开始烘焙与问题排查
当所有参数设置完成后,点击Lighting窗口底部的Generate Lighting按钮开始烘焙。烘焙时间取决于场景复杂度和参数设置,可能从几分钟到几小时不等。
常见问题及解决方案:
漏光:UV岛之间的间距不够,导致光照信息串扰。可以增加UV Pack Margin或提高Lightmap Padding。
锯齿阴影:光照贴图分辨率不足。可以提高Lightmap Resolution,或为重要物体单独增加Scale In Lightmap。
间接光照噪点:采样数不足。可以增加Indirect Samples,或开启Denoising选项。
烘焙时间过长:场景过于复杂,或采样数过高。可以先用低采样数测试,或者分区域烘焙。
6.4.4 效果灯光的补充
完全静态的场景虽然性能好,但会显得缺乏生气。在《暗黑王朝》中,我们在烘焙基础上添加了一些效果灯光:
- 动态闪烁灯光:如破损的荧光灯、火灾的光照等
- 武器开火闪光:短暂的点光源,照亮周围环境
- 手电筒:玩家可控的聚光灯
这些动态灯光数量很少(通常1-2盏),但能显著增强场景的生动感。
以下是一个闪烁灯光效果的实现:
using UnityEngine;
namespace DarkOrder.Lighting
{
/// <summary>
/// 闪烁灯光效果
/// </summary>
[RequireComponent(typeof(Light))]
public class FlickeringLight : MonoBehaviour
{
[Header("闪烁参数")]
[SerializeField]
private float m_MinIntensity = 0.5f;
[SerializeField]
private float m_MaxIntensity = 1.5f;
[SerializeField]
private float m_FlickerSpeed = 1f;
[Header("随机性")]
[SerializeField]
private float m_Randomness = 0.3f;
[SerializeField]
private float m_ChangeSpeed = 5f;
private Light m_Light;
private float m_BaseIntensity;
private float m_TargetIntensity;
private float m_NoiseOffset;
private void Awake()
{
m_Light = GetComponent<Light>();
m_BaseIntensity = m_Light.intensity;
m_TargetIntensity = m_BaseIntensity;
m_NoiseOffset = Random.Range(0f, 100f);
}
private void Update()
{
// 使用Perlin噪声生成平滑的随机值
float noise = Mathf.PerlinNoise(m_NoiseOffset + Time.time * m_FlickerSpeed, 0.5f);
// 映射到强度范围
float targetIntensity = Mathf.Lerp(m_MinIntensity, m_MaxIntensity, noise);
// 添加随机性
targetIntensity += Random.Range(-m_Randomness, m_Randomness);
// 平滑过渡
m_Light.intensity = Mathf.Lerp(
m_Light.intensity,
targetIntensity,
Time.deltaTime * m_ChangeSpeed
);
}
}
}
6.4.5 Unity显示等级的调节
在移动平台上,需要根据设备性能动态调整画质设置。Unity的Quality Settings中可以定义多个质量等级,并在运行时切换。
在《暗黑王朝》中,我们定义了三个质量等级:
- 低端设备:关闭阴影,关闭抗锯齿,纹理质量减半,关闭后处理
- 中端设备:开启烘焙阴影,2倍抗锯齿,开启基础后处理
- 高端设备:开启高质量烘焙阴影,4倍抗锯齿,开启所有后处理
以下是一个运行时根据设备性能自动切换质量等级的脚本:
using UnityEngine;
namespace DarkOrder.Optimization
{
/// <summary>
/// 自动画质适配器
/// </summary>
public class QualityAutoAdapter : MonoBehaviour
{
[Header("性能阈值")]
[SerializeField]
private int m_LowEndMemoryMB = 2048;
[SerializeField]
private int m_MidEndMemoryMB = 4096;
[Header("质量等级索引")]
[SerializeField]
private int m_LowQualityIndex = 0;
[SerializeField]
private int m_MidQualityIndex = 1;
[SerializeField]
private int m_HighQualityIndex = 2;
private void Start()
{
int systemMemory = SystemInfo.systemMemorySize;
if (systemMemory <= m_LowEndMemoryMB)
{
SetQualityLevel(m_LowQualityIndex, "低端");
}
else if (systemMemory <= m_MidEndMemoryMB)
{
SetQualityLevel(m_MidQualityIndex, "中端");
}
else
{
SetQualityLevel(m_HighQualityIndex, "高端");
}
// 同时根据GPU性能调整
AdjustForGPU();
}
private void SetQualityLevel(int index, string levelName)
{
QualitySettings.SetQualityLevel(index, true);
Debug.Log($"画质已设置为: {levelName} - {QualitySettings.names[index]}");
// 保存选择
PlayerPrefs.SetInt("UserQuality", index);
}
private void AdjustForGPU()
{
string gpuName = SystemInfo.graphicsDeviceName.ToLower();
// 某些特定GPU可能需要额外优化
if (gpuName.Contains("mali") && SystemInfo.graphicsDeviceVersion.StartsWith("OpenGL ES 2"))
{
// 老旧Mali GPU,强制使用低端设置
QualitySettings.SetQualityLevel(m_LowQualityIndex, true);
Debug.Log("检测到老旧Mali GPU,已强制使用低端画质");
}
// 设置帧率目标
Application.targetFrameRate = 30; // 移动平台30帧足够流畅,可以省电
}
}
}
6.5 天空盒的制作与应用
6.5.1 天空盒的类型与选择
天空盒(Skybox)是包围整个场景的视觉效果,给玩家一种远处有广阔世界的错觉。在Unity中,天空盒本质上是一个材质,通过特殊的着色器渲染。
Unity支持几种类型的天空盒:
6-Sided天空盒:由六张纹理组成,分别对应前后左右上下六个方向。这种类型最常用,适合大多数室外场景。
Cubemap天空盒:使用立方体贴图,可以看作是6-Sided的变体,但存储在特殊的纹理格式中。
Panoramic天空盒:使用单张2:1比例的等距柱状图(equirectangular)纹理,适合360度全景照片。
Procedural天空盒:由Unity的着色器程序化生成,可以动态调整颜色、云层等参数。
在《暗黑王朝》中,我们为不同关卡设计了不同的天空盒:
- 第一关(黄昏研究站):使用6-Sided天空盒,橙红色的晚霞
- 第二关(夜晚废墟):使用程序化天空盒,深蓝色背景配星星
- 第三关(地下设施):没有天空盒,而是使用封闭的洞穴环境
6.5.2 天空盒材质的创建与配置
创建6-Sided天空盒的步骤:
- 准备六张纹理,确保它们无缝连接
- 在Project窗口右键,Create > Material
- 将Shader设置为 Skybox > 6 Sided
- 将六张纹理拖入对应的纹理槽中
- 调整Tint Color、Exposure、Rotation等参数
程序化天空盒的设置更为灵活:
- 创建新材质,Shader选择 Skybox > Procedural
- 设置Sun Size、Atmosphere Thickness、Sky Tint等参数
- 可以通过代码动态调整这些参数,实现昼夜变化
以下是一个动态控制程序化天空盒的脚本:
using UnityEngine;
namespace DarkOrder.Environment
{
/// <summary>
/// 动态天空盒控制器
/// </summary>
public class DynamicSkyboxController : MonoBehaviour
{
[Header("天空盒材质")]
[SerializeField]
private Material m_SkyboxMaterial;
[Header("昼夜循环")]
[SerializeField]
private bool m_EnableDayNightCycle = true;
[SerializeField]
private float m_CycleDuration = 120f; // 完整周期秒数
[SerializeField]
private Gradient m_SkyTintGradient;
[SerializeField]
private Gradient m_GroundTintGradient;
[Header("时间控制")]
[SerializeField]
[Range(0, 24)]
private float m_CurrentHour = 12f;
private float m_TimeOfDay; // 0-1
private void Start()
{
if (m_SkyboxMaterial == null)
{
m_SkyboxMaterial = RenderSettings.skybox;
}
if (m_SkyboxMaterial == null || !m_SkyboxMaterial.shader.name.Contains("Procedural"))
{
Debug.LogWarning("需要程序化天空盒材质");
enabled = false;
}
}
private void Update()
{
if (m_EnableDayNightCycle)
{
// 更新一天中的时间
m_TimeOfDay += Time.deltaTime / m_CycleDuration;
if (m_TimeOfDay > 1f)
{
m_TimeOfDay -= 1f;
}
m_CurrentHour = m_TimeOfDay * 24f;
}
UpdateSkyboxParameters();
}
private void UpdateSkyboxParameters()
{
if (m_SkyboxMaterial == null) return;
// 计算太阳高度(0 = 午夜,0.5 = 正午,1 = 午夜)
float sunHeight = Mathf.Sin((m_TimeOfDay - 0.25f) * Mathf.PI * 2);
// 调整曝光和大气厚度
float exposure = Mathf.Lerp(0.5f, 1.3f, Mathf.Max(0, sunHeight));
m_SkyboxMaterial.SetFloat("_Exposure", exposure);
float atmosphere = Mathf.Lerp(1.8f, 1.2f, Mathf.Max(0, sunHeight));
m_SkyboxMaterial.SetFloat("_AtmosphereThickness", atmosphere);
// 调整颜色
Color skyTint = m_SkyTintGradient.Evaluate(m_TimeOfDay);
m_SkyboxMaterial.SetColor("_SkyTint", skyTint);
Color groundTint = m_GroundTintGradient.Evaluate(m_TimeOfDay);
m_SkyboxMaterial.SetColor("_GroundColor", groundTint);
// 调整太阳大小和强度
float sunSize = Mathf.Lerp(0.02f, 0.05f, Mathf.Max(0, sunHeight));
m_SkyboxMaterial.SetFloat("_SunSize", sunSize);
float sunStrength = Mathf.Lerp(0.5f, 1.2f, Mathf.Max(0, sunHeight));
m_SkyboxMaterial.SetFloat("_SunStrength", sunStrength);
}
/// <summary>
/// 设置具体时间(小时)
/// </summary>
public void SetTimeOfDay(float hour)
{
m_CurrentHour = Mathf.Clamp(hour, 0, 24);
m_TimeOfDay = m_CurrentHour / 24f;
m_EnableDayNightCycle = false;
UpdateSkyboxParameters();
}
}
}
6.5.3 天空盒与场景的融合
天空盒虽然是背景,但它应该与场景的光照、雾效等元素协调一致,共同营造统一的氛围。
雾效配合:将雾的颜色设置为与天空盒的地平线颜色相近,可以实现场景与天空盒的无缝过渡。在Lighting窗口中可以设置雾的颜色、密度、模式等。
环境光配合:天空盒的颜色会影响环境光的颜色。在Lighting窗口的Environment Lighting中,可以选择Source为Skybox,让环境光自动从天空盒采样。
反射配合:场景中的金属物体应该反射天空盒的颜色。通过放置反射探针(Reflection Probe),可以捕获天空盒信息并用于PBR材质的反射计算。
以下是一个雾效设置脚本:
using UnityEngine;
namespace DarkOrder.Environment
{
/// <summary>
/// 动态雾效控制器
/// </summary>
[ExecuteAlways]
public class FogController : MonoBehaviour
{
[Header("雾效设置")]
[SerializeField]
private bool m_EnableFog = true;
[SerializeField]
private FogMode m_FogMode = FogMode.ExponentialSquared;
[SerializeField]
private Color m_FogColor = Color.gray;
[SerializeField]
private float m_FogDensity = 0.01f;
[SerializeField]
private float m_FogStartDistance = 10f;
[SerializeField]
private float m_FogEndDistance = 50f;
[Header("与天空盒联动")]
[SerializeField]
private bool m_MatchSkyboxColor = false;
[SerializeField]
private Material m_SkyboxMaterial;
private void Update()
{
RenderSettings.fog = m_EnableFog;
RenderSettings.fogMode = m_FogMode;
RenderSettings.fogDensity = m_FogDensity;
RenderSettings.fogStartDistance = m_FogStartDistance;
RenderSettings.fogEndDistance = m_FogEndDistance;
if (m_MatchSkyboxColor && m_SkyboxMaterial != null)
{
// 从天空盒采样地平线颜色
if (m_SkyboxMaterial.HasProperty("_SkyTint"))
{
Color skyTint = m_SkyboxMaterial.GetColor("_SkyTint");
RenderSettings.fogColor = Color.Lerp(skyTint, Color.gray, 0.3f);
}
}
else
{
RenderSettings.fogColor = m_FogColor;
}
}
}
}
6.6 灯光与摄影机特效
6.6.1 太阳特效的模拟
在室外场景中,太阳是最重要的光源。除了提供照明外,太阳本身的视觉效果也值得精心设计。
在《暗黑王朝》中,我们使用以下几种方法来增强太阳效果:
方法一:镜头光晕(Lens Flare) - 在主光源方向放置Lens Flare组件,模拟镜头对着强光时产生的光晕效果。需要导入Lens Flare预设,并调整其颜色、亮度、位置。
方法二:体积光(Volumetric Lighting) - 使用点光源或聚光灯配合体积雾,模拟光线穿过空气的效果。在移动平台上需要谨慎使用,因为体积光对性能消耗较大。
方法三:Shader特效 - 编写简单的屏幕后处理Shader,在太阳方向添加光晕。
以下是一个简化的镜头光晕控制器:
using UnityEngine;
namespace DarkOrder.Effects
{
/// <summary>
/// 镜头光晕控制器
/// </summary>
[RequireComponent(typeof(LensFlare))]
public class LensFlareController : MonoBehaviour
{
[Header("跟随目标")]
[SerializeField]
private Transform m_TargetLight; // 主光源
[Header("可见性控制")]
[SerializeField]
private Camera m_MainCamera;
[SerializeField]
private float m_MinDotProduct = 0.3f; // 相机方向和光方向的最小点积
[SerializeField]
private LayerMask m_OcclusionLayer; // 遮挡检测层
private LensFlare m_LensFlare;
private void Awake()
{
m_LensFlare = GetComponent<LensFlare>();
if (m_MainCamera == null)
{
m_MainCamera = Camera.main;
}
if (m_TargetLight == null)
{
Light mainLight = FindObjectOfType<Light>();
if (mainLight != null && mainLight.type == LightType.Directional)
{
m_TargetLight = mainLight.transform;
}
}
}
private void Update()
{
if (m_TargetLight == null || m_MainCamera == null) return;
// 计算相机到光源的方向
Vector3 lightDirection = -m_TargetLight.forward;
Vector3 cameraForward = m_MainCamera.transform.forward;
// 计算点积,判断光源是否在相机前方
float dot = Vector3.Dot(cameraForward, lightDirection);
if (dot > m_MinDotProduct)
{
// 检测是否有遮挡
RaycastHit hit;
Vector3 cameraPos = m_MainCamera.transform.position;
if (Physics.Raycast(cameraPos, lightDirection, out hit, 1000f, m_OcclusionLayer))
{
// 有遮挡,减弱光晕
m_LensFlare.brightness = Mathf.Lerp(m_LensFlare.brightness, 0f, Time.deltaTime * 5f);
}
else
{
// 无遮挡,根据点积调整亮度
float brightness = Mathf.InverseLerp(m_MinDotProduct, 1f, dot);
m_LensFlare.brightness = Mathf.Lerp(m_LensFlare.brightness, brightness, Time.deltaTime * 5f);
}
}
else
{
// 光源不在视野内
m_LensFlare.brightness = Mathf.Lerp(m_LensFlare.brightness, 0f, Time.deltaTime * 5f);
}
// 使光晕始终面向相机
transform.position = m_TargetLight.position;
transform.LookAt(m_MainCamera.transform);
}
}
}
6.6.2 摄影机特效的合理使用
Unity的Post Processing Stack提供了丰富的后期特效,能够显著提升画面品质。但在移动平台上,必须谨慎选择特效组合,避免性能开销过大。
在《暗黑王朝》中,我们根据不同设备性能采用不同的后处理方案:
基础方案(所有设备):
- 色调映射(Tonemapping):使用ACES或中性模式,提升画面色彩表现
- 抗锯齿(Anti-aliasing):使用FXAA或TAA,消耗较低
进阶方案(中高端设备):
- 环境光遮蔽(Ambient Occlusion):使用SSAO或Scalable AO,增强物体之间的接触阴影
- 泛光(Bloom):为亮部添加光晕效果
- 景深(Depth of Field):在过场动画中使用
高级方案(高端设备):
- 体积光照(Volumetric Lighting)
- 屏幕空间反射(Screen Space Reflections)
- 动态模糊(Motion Blur)
以下是一个根据质量等级动态配置后处理的脚本:
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace DarkOrder.Effects
{
/// <summary>
/// 后处理配置器
/// </summary>
public class PostProcessingConfigurator : MonoBehaviour
{
[Header("后处理体积")]
[SerializeField]
private Volume m_Volume;
[Header("特效组件")]
private Tonemapping m_Tonemapping;
private Bloom m_Bloom;
private AmbientOcclusion m_AmbientOcclusion;
private DepthOfField m_DepthOfField;
private Vignette m_Vignette;
private ColorAdjustments m_ColorAdjustments;
private void Start()
{
if (m_Volume == null)
{
m_Volume = GetComponent<Volume>();
}
if (m_Volume == null) return;
// 获取或添加特效组件
m_Volume.profile.TryGet(out m_Tonemapping);
m_Volume.profile.TryGet(out m_Bloom);
m_Volume.profile.TryGet(out m_AmbientOcclusion);
m_Volume.profile.TryGet(out m_DepthOfField);
m_Volume.profile.TryGet(out m_Vignette);
m_Volume.profile.TryGet(out m_ColorAdjustments);
// 根据质量等级配置
ConfigureForQualityLevel();
}
private void ConfigureForQualityLevel()
{
int qualityLevel = QualitySettings.GetQualityLevel();
// 所有等级都启用的基本特效
if (m_Tonemapping != null)
{
m_Tonemapping.active = true;
m_Tonemapping.mode.value = TonemappingMode.Neutral;
}
if (m_ColorAdjustments != null)
{
m_ColorAdjustments.active = true;
}
// 根据等级配置
switch (qualityLevel)
{
case 0: // 低端
ConfigureLowEnd();
break;
case 1: // 中端
ConfigureMidEnd();
break;
case 2: // 高端
ConfigureHighEnd();
break;
}
}
private void ConfigureLowEnd()
{
if (m_Bloom != null) m_Bloom.active = false;
if (m_AmbientOcclusion != null) m_AmbientOcclusion.active = false;
if (m_DepthOfField != null) m_DepthOfField.active = false;
if (m_Vignette != null) m_Vignette.active = true; // 暗角消耗低,可以开启
// 降低色调映射质量
if (m_Tonemapping != null)
{
m_Tonemapping.mode.value = TonemappingMode.Neutral;
}
Debug.Log("低端后处理配置完成");
}
private void ConfigureMidEnd()
{
if (m_Bloom != null)
{
m_Bloom.active = true;
m_Bloom.intensity.value = 0.8f;
m_Bloom.threshold.value = 1.1f;
}
if (m_AmbientOcclusion != null)
{
m_AmbientOcclusion.active = true;
m_AmbientOcclusion.quality.value = AmbientOcclusionQuality.Medium;
}
if (m_DepthOfField != null) m_DepthOfField.active = false;
if (m_Vignette != null) m_Vignette.active = true;
Debug.Log("中端后处理配置完成");
}
private void ConfigureHighEnd()
{
if (m_Bloom != null)
{
m_Bloom.active = true;
m_Bloom.intensity.value = 1.2f;
m_Bloom.threshold.value = 0.9f;
}
if (m_AmbientOcclusion != null)
{
m_AmbientOcclusion.active = true;
m_AmbientOcclusion.quality.value = AmbientOcclusionQuality.High;
}
if (m_DepthOfField != null)
{
m_DepthOfField.active = true;
m_DepthOfField.quality.value = DepthOfFieldQuality.Medium;
}
if (m_Vignette != null) m_Vignette.active = true;
Debug.Log("高端后处理配置完成");
}
}
}
6.7 移动端场景制作的核心原则
6.7.1 模型面数的终极控制
经过《暗黑王朝》的实战开发,我们总结出移动端模型面数控制的几条铁律:
铁律一:能用的面才是有意义的面。那些隐藏在墙壁后面的、玩家永远看不到的几何体,应该被彻底删除。在建模时就要考虑相机的视野,只有玩家可能看到的部分才需要建模。
铁律二:用纹理替代几何。一个法线贴图可以替代数千个三角面。在制作建筑时,先思考能否用法线贴图表达细节,只有必须的轮廓才用几何建模。
铁律三:LOD不是选项,是必需。每个重要物体都必须有至少两个LOD级别。LOD0用于近距离,LOD1用于中距离,LOD2用于远距离,超过LOD2的距离直接剔除。
铁律四:共用资源。相同的部件(窗户、柱子、箱子)应该使用同一个模型多次实例化,而不是复制出多个独立模型。利用Unity的预制体系统和GPU Instancing技术。
以下是一个运行时监控面数的工具:
using UnityEngine;
using UnityEngine.UI;
namespace DarkOrder.Diagnostics
{
/// <summary>
/// 面数监控器
/// </summary>
public class TriangleCounter : MonoBehaviour
{
[SerializeField]
private Text m_DisplayText;
[SerializeField]
private float m_UpdateInterval = 0.5f;
private int m_TotalTriangles;
private float m_Timer;
private void Update()
{
m_Timer += Time.deltaTime;
if (m_Timer >= m_UpdateInterval)
{
CountTriangles();
m_Timer = 0f;
}
}
private void CountTriangles()
{
m_TotalTriangles = 0;
MeshFilter[] meshFilters = FindObjectsOfType<MeshFilter>();
foreach (MeshFilter mf in meshFilters)
{
if (mf.sharedMesh != null && mf.gameObject.activeInHierarchy)
{
m_TotalTriangles += mf.sharedMesh.triangles.Length / 3;
}
}
SkinnedMeshRenderer[] skinnedRenderers = FindObjectsOfType<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer smr in skinnedRenderers)
{
if (smr.sharedMesh != null && smr.gameObject.activeInHierarchy)
{
m_TotalTriangles += smr.sharedMesh.triangles.Length / 3;
}
}
if (m_DisplayText != null)
{
m_DisplayText.text = $"三角面: {m_TotalTriangles}";
// 根据面数显示警告颜色
if (m_TotalTriangles > 100000)
{
m_DisplayText.color = Color.red;
}
else if (m_TotalTriangles > 80000)
{
m_DisplayText.color = Color.yellow;
}
else
{
m_DisplayText.color = Color.green;
}
}
}
}
}
6.7.2 贴图大小与利用率的最大化
贴图是移动平台上最大的内存消耗源之一。优化贴图使用是内存管理的关键。
原则一:选择合适的压缩格式。Android平台应使用ETC2或ASTC格式,iOS平台应使用PVRTC或ASTC格式。对于不支持ETC2的旧设备,需要准备回退方案。
原则二:纹理尺寸是2的幂。纹理尺寸必须是2的幂(如256、512、1024),否则无法生成Mipmap,也无法进行纹理压缩。
原则三:善用纹理集。将多个小纹理合并到一张大纹理中,可以显著减少材质数量和绘制调用。纹理集的布局需要合理规划,确保不同物体能够正确映射到对应的区域。
原则四:Mipmap必须开启。Mipmap让远处物体使用低分辨率纹理,既节省性能又避免闪烁。虽然会增加约33%的纹理内存,但带来的性能提升远超这个代价。
以下是一个纹理分析工具:
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
namespace DarkOrder.EditorTools
{
/// <summary>
/// 纹理分析器
/// </summary>
public class TextureAnalyzer : EditorWindow
{
[MenuItem("Tools/纹理分析器")]
public static void ShowWindow()
{
GetWindow<TextureAnalyzer>("纹理分析器");
}
private Vector2 m_ScrollPosition;
private void OnGUI()
{
GUILayout.Label("项目纹理分析", EditorStyles.boldLabel);
if (GUILayout.Button("分析所有纹理"))
{
AnalyzeAllTextures();
}
m_ScrollPosition = EditorGUILayout.BeginScrollView(m_ScrollPosition);
// 显示分析结果
if (m_Results != null)
{
foreach (string result in m_Results)
{
EditorGUILayout.LabelField(result);
}
}
EditorGUILayout.EndScrollView();
}
private List<string> m_Results = new List<string>();
private void AnalyzeAllTextures()
{
m_Results.Clear();
string[] guids = AssetDatabase.FindAssets("t:texture");
int totalMemory = 0;
int nonPowerOfTwoCount = 0;
int noMipmapCount = 0;
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
if (importer == null || texture == null) continue;
// 检查尺寸是否为2的幂
bool isPowerOfTwo = (texture.width & (texture.width - 1)) == 0 &&
(texture.height & (texture.height - 1)) == 0;
if (!isPowerOfTwo)
{
m_Results.Add($"[警告] 非2的幂纹理: {path} ({texture.width}x{texture.height})");
nonPowerOfTwoCount++;
}
// 检查是否开启Mipmap
if (!importer.mipmapEnabled && texture.width > 64 && texture.height > 64)
{
m_Results.Add($"[建议] 未开启Mipmap: {path}");
noMipmapCount++;
}
// 估算内存占用
int memory = texture.width * texture.height * 4 / 1024 / 1024;
totalMemory += memory;
}
m_Results.Insert(0, $"总纹理内存估计: {totalMemory} MB");
m_Results.Insert(1, $"非2的幂纹理数量: {nonPowerOfTwoCount}");
m_Results.Insert(2, $"未开启Mipmap纹理数量: {noMipmapCount}");
AssetDatabase.Refresh();
}
}
}
#endif
6.8 本章小结
本章围绕《暗黑王朝》的场景构建流程,系统讲解了从模型制作、导入优化、光影烘焙到后处理特效的完整知识体系。
在模型制作阶段,我们强调了移动平台的特殊性:面数控制、纹理大小优化、模块化设计。通过合理的面数分配和纹理使用,可以在有限的硬件资源内创造出丰富的视觉效果。
在模型导入阶段,分类管理和规范化设置是提高效率的关键。通过自定义导入后处理器,可以自动为不同类型的模型应用合适的设置,避免手动操作的遗漏和错误。
光照贴图烘焙是移动场景的核心技术。通过将静态光照预计算并存储在纹理中,可以在运行时获得高品质的光照效果,同时避免实时光照的性能开销。本章详细讲解了烘焙前的准备工作、参数设置、常见问题排查等内容。
天空盒和摄影机特效是提升画面品质的锦上添花。通过动态天空盒控制、雾效配合、后处理特效的合理使用,可以让场景呈现出丰富的情感和氛围。
最后,我们总结了移动端场景制作的几条铁律:面数控制、纹理优化、LOD使用、资源共用。这些原则来自于《暗黑王朝》的实际开发经验,也是任何移动游戏开发都必须遵循的基本准则。
通过本章的学习,读者应该能够建立起移动端场景构建的完整知识体系,理解从模型到最终场景的每一个环节的技术要点,并能够在实际项目中灵活应用这些技术,创造出既美观又流畅的游戏世界。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)