Unity渲染优化实战指南:从Draw Call合批到性能瓶颈定位
1. 项目概述:为什么Unity渲染优化是每个开发者的必修课
做Unity开发这些年,我越来越觉得,渲染优化不是一个“高级”话题,而是项目从原型走向产品、从能跑到跑得流畅的必经之路。无论是独立开发者还是团队协作,当你的场景里角色超过十个,特效开始叠加,或者想在移动设备上保持60帧的流畅体验时,优化就成了绕不开的坎。很多人觉得优化很玄学,是“大神”们才玩的,其实不然。它更像是一套系统的工程方法,有明确的指标、可循的路径和大量经过验证的最佳实践。今天,我就结合自己踩过的无数坑和总结的经验,把这套方法掰开揉碎了讲清楚。我们不止要讲“怎么做”,更要深挖“为什么这么做”,以及“在什么情况下选择哪种做法”。无论你是在纠结Unity和Three.js哪个更适合你的Web项目,还是在为Unity Shader的复杂性和性能头疼,亦或是被Unity ECS这种新架构搞得云里雾里,希望这篇长文都能给你带来实实在在的帮助。
2. 核心思路:建立性能分析与瓶颈定位的系统观
优化不是盲目地试错,第一步永远是“看清现状”。你需要知道你的游戏卡在哪里,是CPU忙不过来,还是GPU压力太大,或者是内存、带宽出了问题。Unity提供了一套强大的工具链来帮助我们完成这个诊断过程。
2.1 诊断工具三板斧:Profiler, Frame Debugger, Stats窗口
Unity Profiler是你的第一双眼睛,也是最重要的工具。我习惯在开发中期就常开Profiler窗口,而不是等到卡顿发生才去救火。看Profiler,首先要分清主次:
- CPU Usage:这里能看到每一帧所有CPU活动的耗时。你需要重点关注的是
Rendering、Scripts、Physics这几项。如果Rendering耗时很高,那瓶颈很可能在GPU(但需要结合GPU数据看)或者是CPU向GPU提交指令(Draw Call)太多。如果Scripts耗时异常,就要点开具体查看是哪个MonoBehaviour的Update或协程占用了大量时间。 - GPU Usage:在Profiler的
Rendering区域可以查看GPU耗时。如果这里接近或超过你的目标帧时间(比如16.6ms对应60FPS),那瓶颈就在GPU。你需要结合Frame Debugger来进一步分析。 - Memory:关注
Texture、Mesh、Material和GameObject的数量和内存占用。一个常见的问题是大量重复但微小的资源,或者资源没有及时卸载导致的内存泄漏。
注意:移动平台(Android/iOS)上分析,务必使用Development Build并勾选Autoconnect Profiler和Deep Profiling,然后在编辑器端连接真机进行 profiling。模拟器或编辑器的数据与真机差异可能很大。
Frame Debugger是你的显微镜。它可以让你“暂停”在某一帧,并逐步查看这一帧中每一个Draw Call是如何被提交的。这是理解合批(Batching)是否生效、为什么材质球(Material)实例化过多、以及渲染顺序问题的终极利器。当你发现Draw Call数量异常高时,打开Frame Debugger,逐条查看,很快就能定位到是哪个物体、用了哪个材质破坏了批处理。
Game视图Stats窗口提供了一个快速的性能仪表盘。其中几个关键指标:
- FPS:帧率,最直观的感受。
- Batches:批处理次数。这是经过引擎优化(如动态批处理、静态批处理、GPU Instancing)后的Draw Call数量。你的优化目标就是尽可能地降低这个数字。
- SetPass calls:设置渲染状态的次数。通常,使用不同材质的物体就会产生一次SetPass Call。这个数越接近Batches,说明合批效果越好;如果远小于Batches,说明有很多物体无法合批。
- Tris和Verts:三角形和顶点数。这是GPU负载的另一个重要指标,尤其在移动端或低端PC上需要严格控制。
2.2 建立性能预算意识
在项目初期,团队就应该对关键指标设立“预算”。例如:
- 每帧CPU时间:目标帧率60FPS,则每帧预算约16ms。可以进一步细分:渲染<8ms,逻辑<5ms,物理<2ms,留出余量给GC等。
- Draw Call (Batches):移动端中低端机,建议场景内控制在100-150以下;PC端可以放宽,但复杂场景也应控制在500以内。
- 面数:移动端主角角色建议在1.5万三角面以内,场景总面数视复杂度控制在10-30万。PC端可以更高,但也要避免无节制的滥用。
- 纹理内存:注意纹理尺寸和压缩格式。一张2048x2048的RGBA32纹理就占用16MB!大量使用会导致内存暴涨和带宽压力。
有了这些预算和诊断方法,你就能像医生一样,对项目的性能“健康状况”心中有数,接下来的优化就是对症下药。
3. 核心优化策略:从Draw Call与合批入手
渲染优化的核心矛盾之一,就是减少CPU向GPU发送指令的开销,而Draw Call是其中最主要的部分。每一次Draw Call都有固定的CPU开销,即使只画一个三角形。因此,合批(Batching)是我们的首要武器。
3.1 静态批处理 (Static Batching)
原理:将标记为Static(且参与批处理)的物体的网格数据在运行前(或运行时)合并成一个大网格。渲染时,只需一次Draw Call就能绘制所有合并的物体。怎么做:在Inspector窗口右上角,将物体的Static复选框勾选。通常用于场景中永远不会移动、旋转、缩放的环境物体,如建筑、道路、岩石。优点:合批效果好,CPU开销极低。缺点:
- 增加内存和存储:因为合并了网格数据。如果大量静态物体共享同一个网格(如1000个相同的石头),合并后会存储1000份顶点数据,造成浪费。这时可以考虑使用GPU Instancing。
- 无法处理移动物体:这是“静态”二字的定义决定的。
实操心得:不要无脑全选场景物体设为Static。只对确实不会动的、且不是大量重复的物体使用。对于大量重复的静态物体(如草地、树叶),优先考虑GPU Instancing。
3.2 动态批处理 (Dynamic Batching)
原理:Unity在运行时,每帧自动将满足条件的小型动态物体的网格数据合并,再一次性提交。条件非常苛刻:
- 网格顶点属性规模小于900(通常指顶点数*(位置+法线+UV等))。
- 使用相同的材质球实例。
- 缩放一致(非镜像缩放)。
- 不接受实时阴影等。优点:对小型动态物体(如子弹、金币)自动生效,无需额外操作。缺点:条件苛刻,适用范围窄,且合并本身有CPU开销。对于顶点数稍多的物体(比如一个300个顶点的小道具)就无效了。建议:不要依赖动态批处理作为主要优化手段。了解其限制,对于确实符合条件的小物体,它能起到作用。但对于稍复杂的角色或道具,应寻求其他方案。
3.3 GPU Instancing
原理:这是目前处理大量相同网格、不同位置/旋转/缩放(可能还有少量自定义属性如颜色)物体的最优解。它不合并网格数据,而是向GPU传递一个包含所有实例变换信息的缓冲区。GPU通过一次Draw Call,读取同一个网格和同一个材质,但根据缓冲区数据为每个实例进行不同的变换和渲染。怎么做:
- 对于Unity标准着色器(Standard/Standard Specular),只需在材质的Inspector上勾选
Enable GPU Instancing。 - 对于自定义Shader,需要在Shader代码中添加
#pragma multi_compile_instancing,并使用UNITY_INSTANCING_BUFFER_START等宏来处理实例化属性。优点:
- 能高效渲染成千上万个相同物体(如草地、树木、人群),Draw Call仅为1。
- 相比静态批处理,不额外增加网格内存。
- 物体可以动态移动(通过修改每实例的变换数据)。缺点:
- 要求网格和材质完全相同。如果实例间需要不同的纹理,需要使用纹理数组(Texture2DArray)等技术,复杂度上升。
- 对Shader有一定要求,自定义Shader需要支持。
避坑指南:GPU Instancing在移动平台上的支持度需要检查。虽然现代OpenGL ES 3.0+和Metal都支持,但驱动和硬件实现可能有差异,务必在目标设备上充分测试。另外,如果实例数量巨大(数万),更新变换数据缓冲区的CPU开销也需要关注。
3.4 材质球(Material)与着色器变体(Shader Variants)管理
即使网格合批了,如果材质不同,依然会产生新的SetPass Call。因此,减少材质种类是另一个关键。
- 纹理图集(Texture Atlas):将多个小物体的纹理合并到一张大图上,这样它们就可以共享同一个材质球。这是UI系统(如UGUI)和2D游戏的常规操作,在3D中也常用于环境贴花、道具等。
- 材质属性块(MaterialPropertyBlock):当你需要渲染大量相同网格但有些许参数不同(如颜色、浮点参数)时,使用
MaterialPropertyBlock来修改这些属性,而不是创建新的材质实例。这不会打断GPU Instancing(如果Shader支持),是一种非常高效的动态修改材质参数的方式。 - 警惕Shader变体爆炸:Unity的Shader会在编译时根据不同的关键字(如
_NORMALMAP,_EMISSION)生成多个变体。如果你的项目使用了多个不同的Shader功能组合,或者像URP/HDRP那样有很多渲染管线特性,可能会导致构建时Shader变体数量激增,从而极大地增加构建时间、包体大小和运行时内存占用。在Graphics Settings中查看并管理Shader变体,移除不需要的组合。
4. 层级优化:LOD、遮挡剔除与渲染顺序
当物体离相机很远时,用高精度模型渲染是一种浪费。当物体被其他物体完全挡住时,渲染它更是毫无意义。这就需要更智能的渲染管理。
4.1 多层次细节(LOD)
原理:为同一个物体准备多个不同面数的模型(LOD0高模,LOD1中模,LOD2低模,甚至LOD3一个Billboard面片)。根据物体与相机的距离,自动切换不同的模型进行渲染。Unity实现:使用LOD Group组件。你需要手动拖入不同层级的Renderer。关键参数:
- LOD百分比:该层级在屏幕高度中所占的百分比。这是一个相对值,需要根据场景和相机FOV调整。通常LOD0(100%-60%), LOD1(60%-30%), LOD2(30%-15%), LOD3(15%-0%)。
- Fade Mode:交叉淡入淡出模式,可以避免层级切换时的“跳变”。优点:显著减少远处物体的渲染顶点和像素开销,对GPU压力大的场景效果立竿见影。缺点:需要制作多个模型,增加美术工作量。管理多个LOD组会增加一定的CPU开销(距离计算)。
经验之谈:LOD切换的距离阈值需要精心调整,最好在目标设备上实际观察。切换过早(高模还没看清就切到中模)会影响画质,切换过晚则优化效果打折扣。对于移动端,可以更激进一些。
4.2 遮挡剔除(Occlusion Culling)
原理:在运行时判断哪些物体被其他物体(遮挡物)完全挡住,从而避免提交这些被遮挡的物体进行渲染。Unity实现:
- 静态遮挡剔除:针对静态场景。需要在
Window -> Rendering -> Occlusion Culling中打开面板,先烘焙(Bake)。烘焙会生成数据,记录从不同视角看,哪些静态物体会被遮挡。 - 动态遮挡剔除:对于动态物体,Unity使用其包围盒(Bounds)参与遮挡测试。如果动态物体的包围盒被静态遮挡体完全挡住,它也不会被渲染。重要设置:
- 物体需要勾选
Occluder Static(作为遮挡物)和/或Occludee Static(作为被遮挡物)。 - 调整
Smallest Occluder和Smallest Hole参数,控制烘焙的精细度。值越小,结果越精确,但烘焙时间越长,数据量也越大。适用场景:室内场景、城市峡谷等遮挡关系复杂的场景效果极佳。对于一望无际的平原或天空盒,则基本无效。常见问题:烘焙后物体在相机移动时“闪烁”出现。这通常是因为Smallest Hole设置过大,或者相机的近裁剪平面(Near Clip Plane)太远,导致系统认为物体可以从“小孔”中被看到。需要调整参数或检查相机设置。
4.3 渲染顺序与Overdraw
Overdraw(过度绘制)指同一个像素被多次绘制。这纯粹是GPU的浪费。优化渲染顺序可以减少Overdraw。
- 不透明物体:Unity默认使用从前往后(Front-to-Back)的深度排序。GPU的深度测试(ZTest)会丢弃那些被挡住的像素的片元着色器计算,从而节省性能。因此,保持这个顺序即可。
- 半透明物体:由于需要混合,必须使用从后往前(Back-to-Front)排序。这无法进行深度测试,每个半透明像素都会被计算多次,开销很大。因此,尽量减少半透明物体的数量和面积是根本。对于粒子系统,可以使用
Alpha Test(如Cutout材质)来代替Alpha Blend,因为Alpha Test的物体仍可进行深度测试和写入。 - Shader中的深度写入(ZWrite):对于复杂的半透明效果(如毛玻璃),有时可以尝试开启深度写入并进行特殊混合,但这属于高级技巧,需要谨慎处理。
5. 资源与内存优化:纹理、网格与光照
优化的很大一部分工作是管理好你的资源,它们直接占用内存和带宽。
5.1 纹理优化
纹理是内存和带宽的大户。
- 尺寸与Mipmap:永远不要使用超过必要尺寸的纹理。一个在屏幕上只有100x100像素显示的物体,用1024x1024的纹理就是浪费。利用Mipmap可以在物体变远时自动使用更小的纹理级别,节省带宽。在移动端,关闭不必要的纹理类型的Mipmap(如UI纹理)可以节省内存。
- 压缩格式:
- PC:DXT1/3/5 (BC1/2/3)。
- Android:ETC2 (OpenGL ES 3.0以上支持透明通道), ETC1 (不支持透明,需拆分Alpha通道), ASTC(压缩质量高,但需要硬件支持,按块大小如4x4, 8x8选择)。
- iOS:PVRTC 或 ASTC。
- 在
Import Settings中根据平台选择正确的格式。ASTC是移动端目前的首选,在支持的设备上质量和性能俱佳。
- 纹理图集:如前所述,合并小纹理。
- 纹理流式加载(Texture Streaming):对于开放大世界,启用
Texture Streaming功能。它只会将当前所需精度的纹理数据留在内存中,其他纹理以更低分辨率存在或从磁盘按需加载,能极大降低内存峰值。
5.2 网格优化
- 减少顶点数:在建模阶段就做好优化,移除看不见的面、减少分段数。使用Unity的
Mesh Compression(在模型导入设置中)可以进一步减少网格数据大小。 - 优化顶点属性:检查网格是否包含不必要的顶点数据,如切线、顶点色、多套UV。如果Shader用不到,就在导入设置中取消勾选,可以减少顶点缓冲区大小。
- 网格合并(Mesh Combining):对于静态场景,除了引擎的静态批处理,也可以使用工具(如Unity的
Mesh.CombineMeshesAPI或Asset Store插件)在编辑期手动合并,以获得更彻底的控制。但要注意Draw Call和材质数量的权衡。
5.3 光照与阴影优化
实时光照和阴影是性能杀手。
- 烘焙光照(Baked GI):对于静态物体和静态光源,使用光照贴图(Lightmapping)将光照信息烘焙到纹理上。运行时几乎零开销,效果稳定。这是提升场景视觉质量和性能的最有效手段之一。
- 混合光照(Mixed Lighting):对于静态场景中的动态物体,或需要部分动态效果的光源。Unity会为动态物体提供光照探针(Light Probes)来获取烘焙的间接光信息。合理设置光照模式是关键。
- 实时光照精简:
- 严格控制每帧活动的实时光源数量。移动端建议不超过1-3个。
- 使用光源层级(Light Layers)和裁剪遮罩(Culling Mask),让光源只影响必要的物体。
- 减少光源的阴影分辨率和距离。软阴影比硬阴影开销大得多。
- 考虑用烘焙光照+实时反射探针(Reflection Probes)来模拟一些动态光影效果,而不是完全依赖实时光。
- 阴影优化:
- 阴影距离(Shadow Distance):在Quality Settings中调低。远处的阴影细节看不到,完全可以关闭。
- 级联阴影映射(Cascaded Shadow Maps, CSM):用于方向光(如太阳)。增加级联数可以改善近处阴影质量,但会增加开销。通常2-4级足够。调整每一级的覆盖范围,使其更匹配相机视锥体。
- 阴影贴图分辨率:不是越高越好,找到质量和性能的平衡点。
6. 高级与管线特定优化
当基础优化都做完后,可以关注一些更深入的领域。
6.1 渲染管线选择:Built-in, URP, HDRP
- 内置渲染管线(Built-in):灵活但需要手动配置很多优化。适合需要深度定制或维护老项目。
- 通用渲染管线(URP):Unity主推的现代管线,默认包含了很多针对移动端和中级PC的优化,如更高效的SRP Batcher。SRP Batcher是一个强大的功能,它能大幅降低使用不同材质但相同Shader变体的物体的渲染状态设置开销。启用URP并让Shader兼容SRP Batcher规则,能带来显著的CPU渲染线程性能提升。对于新项目,尤其是面向移动端或跨平台,URP是首选。
- 高清渲染管线(HDRP):为PC/主机高端硬件设计,追求电影级画质。性能开销大,除非你的项目目标平台是高端设备,否则不要轻易选择。关于“Unity和Three.js哪个好”:这完全取决于你的目标。Three.js是基于WebGL的网页端3D库,轻量、易集成到Web。Unity是一个完整的游戏引擎,功能强大,可发布到几乎所有平台(包括WebGL)。如果你的核心产品是复杂的、需要大量逻辑和资源的3D应用或游戏,Unity是更成熟的选择。如果只是一个展示型的、轻量级的3D网页效果,Three.js可能更快捷。
6.2 Shader优化
复杂的Shader是GPU的负担。
- 简化计算:在片元着色器(Fragment Shader)中进行的计算远多于顶点着色器。尽可能将计算移到顶点着色器,或者通过纹理查找(Lookup Texture)来替代复杂的实时计算。
- 减少纹理采样:纹理采样是昂贵的操作。合并纹理(如将金属度、光滑度、AO打包到一张纹理的RGB通道),或者重用采样结果。
- 避免分支(if/else):GPU是并行处理器,分支会导致性能波动。尽量使用
step()、lerp()等函数来替代条件判断。 - 使用半精度浮点数:在支持的平台(如移动端)上,在Shader中使用
half或fixed来代替float,可以提升计算速度和降低功耗。 - 利用Shader LOD:类似于网格LOD,可以为Shader设置不同的LOD级别。当摄像机远离时,自动切换到更简化的Shader变体。
6.3 代码与架构优化
渲染优化不光是美术和TA的事,程序代码也至关重要。
- 避免在Update中做昂贵操作:如
FindGameObjectsWithTag、GetComponent、物理查询(Raycast, OverlapSphere)等。这些操作的结果应该缓存起来。 - 使用对象池(Object Pooling):对于频繁创建和销毁的对象(如子弹、特效),使用对象池复用,避免频繁的实例化和垃圾回收(GC)。
- 控制Update频率:对于不需要每帧更新的逻辑,使用
InvokeRepeating、协程(WaitForSeconds)或自己维护一个计时器来降低更新频率。 - 减少SendMessage和BroadcastMessage:使用基于接口或委托的事件系统来代替,效率更高。
- ECS与Job System:对于拥有海量实体(如成千上万的单位、粒子)的模拟,传统的GameObject+MonoBehaviour模式可能成为瓶颈。Unity的ECS(实体组件系统)架构配合Burst编译器和Job System,可以利用多核CPU进行高效的数据并行处理,极大提升模拟性能。但这套架构学习曲线陡峭,会彻底改变你的编程模式,适用于性能要求极端苛刻的特定场景(如大规模策略游戏、模拟仿真),不适合一般项目盲目引入。
7. 平台特定优化与发布实践
不同平台有各自的特性,优化策略也需微调。
7.1 移动平台(Android/iOS)
- 功耗与发热:是移动端优化的核心目标之一。除了帧率,还要关注耗电量。过高的GPU/CPU负载会迅速导致设备发热降频,反而使帧率更低。保持稳定的30帧或60帧比波动的帧率更省电。
- 分辨率与帧率:考虑支持动态分辨率渲染或固定一个稍低于屏幕物理分辨率的分辨率(如1080p设备渲染900p),对性能提升非常明显。也可以提供30/60帧的选项让用户选择。
- 纹理优化:如前所述,ASTC/PVRTC压缩格式至关重要。同时注意纹理上传(Texture Upload)到GPU的带宽,避免同一帧加载大量新纹理。
- 图元(三角形)数量:移动端GPU的三角形吞吐量有限,需严格控制。
- 使用移动端优化的着色器:URP提供了
Universal Render Pipeline/Lit等针对移动端优化的Shader。避免使用过于复杂的自定义Shader。
7.2 WebGL平台
- 内存限制:WebGL运行在浏览器沙盒中,可用内存通常远小于原生应用。纹理、音频和AssetBundle的大小需要格外小心。
- 代码大小:由于所有代码(包括引擎)都要下载,构建后的wasm/js文件大小直接影响加载时间。使用代码裁剪(Code Stripping)和压缩。
- 多线程限制:WebGL 1.0不支持多线程,WebGL 2.0/WebAssembly有一定支持但有限。Job System和ECS在WebGL上的收益可能不如原生平台。
- 数据加载:使用
UnityWebRequest异步加载资源,避免阻塞主线程。
7.3 发布设置检查清单
在点击Build之前,检查以下关键设置:
- Player Settings -> Resolution and Presentation:设置默认分辨率、是否全屏、是否允许横竖屏切换。
- Player Settings -> Other Settings:
- Color Space:移动端和性能优先项目用Linear,但注意兼容性。
- Auto Graphics API:移除不需要的图形API(如iOS只留Metal,Android只留Vulkan或OpenGL ES 3)。
- Multithreaded Rendering:如果目标平台支持,务必开启。
- Static Batching和GPU Skinning:根据项目需求勾选。
- Strip Engine Code:开启,以减小包体。
- Quality Settings:为不同等级(Fastest, Fast, Good, Beautiful等)设置不同的渲染参数(如像素光照数量、阴影距离、纹理质量等)。在运行时可以根据设备性能动态切换质量等级。
8. 常见问题排查与实战技巧
最后,分享一些实战中高频出现的问题和解决思路。
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 游戏突然卡顿,帧率周期性下降 | 垃圾回收(GC) | 1. 在Profiler的CPU图表中观察,卡顿帧是否伴随一个GC.Collect的峰值。2. 使用Deep Profiler或Memory Profiler工具,定位哪些对象在频繁分配和释放(尤其是字符串拼接、LINQ查询、未缓存的新建对象)。 3. 优化代码,使用对象池,缓存组件引用,避免在循环或Update中分配新对象。 |
| Draw Call (Batches) 数量异常高 | 材质实例过多或合批失败 | 1. 打开Frame Debugger,查看每一帧的Draw Call列表。 2. 检查是否有很多使用相同Shader但不同参数的材质球。尝试使用纹理图集或MaterialPropertyBlock。 3. 检查动态物体是否因为缩放不一致、包含实时阴影等导致动态批处理失败。 4. 对于大量相同静态物体,检查是否错误地没有标记为Static,或应使用GPU Instancing。 |
| GPU耗时很高,但三角形数量不多 | Overdraw(过度绘制)或复杂Shader | 1. 在Scene视图的渲染模式下拉菜单中选择Overdraw,观察屏幕上的过度绘制情况(越亮表示绘制次数越多)。 2. 优化半透明物体的渲染顺序和数量。 3. 使用Shader分析器(如Unity Frame Debugger的Shader查看或第三方工具)分析热点Shader,简化片元着色器计算,减少纹理采样。 |
| 移动设备发热严重,帧率不稳 | GPU或CPU持续满载 | 1. 使用电池分析工具或平台专属的Profiler(如Xcode的Instruments, Android GPU Profiler)。 2. 降低分辨率或渲染质量。 3. 检查是否有WaitForTargetFPS或垂直同步(Vsync)导致的CPU空转,适当调整目标帧率或关闭Vsync测试。 4. 检查后台是否有不必要的协程或Update在空跑。 |
| 构建后包体巨大 | 资源未压缩或包含多余数据 | 1. 检查纹理、音频的导入设置,是否使用了未压缩格式。 2. 检查是否在Player Settings中勾选了Create Visual Studio Solution等开发选项。 3. 使用Asset Bundle Analyzer工具分析包内资源占比,移除未使用的资源。 |
最后的个人体会:渲染优化是一个贯穿项目始终的、平衡艺术与技术的过程。没有银弹,最好的优化往往是项目初期正确的设计和规范。养成常开Profiler的习惯,像关注游戏逻辑一样关注性能数据。每次添加新功能或资源时,都问自己一句:“这对性能的影响是什么?” 多测试,尤其是在你的目标低端设备上测试。优化之路永无止境,但掌握这些系统性的方法和工具,能让你在遇到性能问题时不再迷茫,而是有条不紊地分析、定位并解决它。记住,优化的目标不是让画面变得简陋,而是在有限的资源下,让体验变得尽可能流畅和美妙。