Unity常用优化技术漫谈(1)

在渲染过程中,着色器的编译是一个将高级着色器语言(如HLSL、GLSL或者Unity的ShaderLab)转换成可以在图形硬件上运行的低级机器代码的过程。这个过程通常涉及以下几个步骤:

  1. 着色器编写和预处理
    开发者使用高级着色器语言编写着色器代码,这些代码可以包含预处理器指令、条件编译块等。预处理器指令在实际编译之前执行,用于条件编译和代码组织。

  2. 着色器编译
    Unity在编辑器中或者在游戏运行时将着色器代码编译成中间字节码。这个过程由Unity内部的编译器或者外部的编译器(如Microsoft的fxc编译器或者OpenGL的glslc编译器)完成。

  3. 平台特定的优化
    编译成字节码后,Unity或者图形API的驱动程序会根据目标平台进一步优化这些字节码。这些优化可能包括指令重排、寄存器分配、循环展开等,目的是为了提高着色器在特定硬件上的性能。

  4. 链接
    如果一个渲染过程涉及多个着色器阶段(如顶点着色器、片元着色器等),这些不同阶段的编译输出需要被链接在一起,形成一个可以在GPU上执行的完整程序。

  5. 加载和执行
    编译和链接完成的着色器程序被加载到GPU,并在渲染管线的相应阶段执行。这个过程是由图形API(如DirectX或OpenGL)和GPU驱动程序管理的。

具体细节
异步编译:为了避免运行时编译导致的卡顿,Unity可能会在一个单独的线程中异步编译着色器。
缓存:编译完成的着色器程序可以被缓存,这样在下次需要时可以直接加载,避免重复编译。
错误处理:如果编译过程中出现错误,Unity会提供错误信息,帮助开发者定位问题。
变体管理:Unity通过Shader Variant Collection管理预编译的着色器变体,减少运行时编译的需要。
性能考虑
着色器编译是一个耗时的过程,特别是对于复杂的着色器或者拥有大量变体的着色器。因此,开发者需要仔细管理着色器变体,避免在游戏运行时进行大量的编译工作。通过使用Unity的性能分析工具,开发者可以监控着色器编译对游戏性能的影响,并做出相应的优化。

在渲染过程中,着色器确实需要编译,但这通常不是在每一帧渲染时发生的。着色器编译通常发生在以下几个时刻:

开发阶段:当开发者在游戏引擎中创建或修改着色器时,着色器会被编译成中间字节码。例如,在Unity中,当你写一个新的Shader或者导入一个包含Shader的资产时,Unity会自动编译这些Shader。

游戏打包:在游戏打包的过程中,所有的着色器通常会被预编译成目标平台的机器码。这样做的目的是为了减少游戏运行时的编译开销。

首次使用:当一个着色器在游戏运行时首次被用于渲染时,它会被编译成最终的机器码。这是因为着色器可能依赖于特定的渲染上下文或硬件特性,这些只有在运行时才能确定。

着色器变体:如果着色器支持多种变体(例如,不同的光照模型或特殊效果),那么每个变体可能在首次使用时编译。

为了避免运行时编译导致的性能问题(如卡顿或延迟),开发者可以采取以下措施:

预编译着色器:在游戏打包时预编译所有可能的着色器变体。这可以通过在游戏引擎中设置来完成,例如Unity的Shader Variant Collection功能。

延迟编译:在游戏加载阶段或在背景线程中编译着色器,以避免在游戏的关键时刻进行编译。

缓存编译结果:一些平台和API允许将编译后的着色器缓存起来,这样在下次运行游戏时就不需要重新编译。

减少变体数量:通过优化着色器代码和减少编译指令,减少着色器变体的数量,从而减少编译时间。

使用更高级的图形API:比如Vulkan或DirectX 12,这些API允许更细粒度的控制,包括着色器的编译和管理。

通过这些方法,开发者可以确保着色器编译不会对游戏性能产能。

unity中预编译着色器的细节涉及到如何处理和优化着色器,以减少运行时的性能开销。以下是Unity预编译着色器的一些关键细节:

着色器变体(Shader Variants)
Unity着色器通常包含多个变体(variants),这些变体是由不同的着色器特性(比如光照模型、阴影类型、纹理选项等)组合而成的。每个变体对应一组特定的编译指令。

预编译过程
Shader Variant Collection:Unity允许开发者创建一个Shader Variant Collection资产,这个资产可以包含一个项目中所有可能用到的着色器变体。开发者可以手动选择要包含的变体,也可以通过在游戏中运行并收集数据来自动填充。

构建时编译:在游戏构建过程中,Unity会编译Shader Variant Collection中包含的所有着色器变体。这意味着这些变体会被编译成GPU可以直接执行的机器码,并随游戏一起打包。

剔除不必要的变体:为了减少构建大小和提高性能,Unity提供了工具和选项来剔除不常用的着色器变体。例如,可以使用Shader Stripping来移除那些在游戏中实际上不会用到的变体。

编译选项
Multi-Compile & Shader Feature:Unity中的#pragma multi_compile和#pragma shader_feature指令用于声明着色器变体。multi_compile会为每个可能的组合生成变体,而shader_feature只为实际在材质中使用的组合生成变体。

关键字限制:Unity有一个关键字限制,超过这个限制会导致编译错误。因此,开发者需要谨慎使用multi_compile指令,以避免生成过多的变体。

运行时加载
当游戏运行时,如果需要渲染的材质使用了预编译的着色器变体,Unity会从已编译的着色器库中加载对应的变体。这个过程比运行时编译要快得多,因为它避免了编译延迟。

性能和存储考虑
构建大小:预编译着色器变体会增加游戏的构建大小,因为所有这些变体都需要被包含在游戏数据中。
启动时间:加载所有预编译的着色器变体可能会增加游戏的启动时间,尤其是在移动设备上。
内存使用:预编译的着色器变体会占用更多的内存。
为了平衡性能、存储和内存使用,Unity开发者需要仔细管理着色器变体,确保只包含必要的变体,并利用Unity提供的工具来优化着色器的使用。通过这种方式,可以在提供丰富视觉效果的同时,保持游戏的流畅运行。

在Unity中,预编译着色器是一个重要的步骤,可以帮助避免游戏运行时出现由于着色器编译引起的性能问题。Unity提供了几种机制来管理和优化着色器的编译过程。

Shader Variant Collection
Unity使用一种称为“Shader Variant”的系统,其中每个着色器都可以有多个变体(variant),这些变体对应于着色器中不同的预处理器指令组合。例如,一个着色器可能有针对不同光照模型、阴影类型或特殊效果的变体。

为了减少运行时编译的负担,Unity允许开发者创建一个Shader Variant Collection。这是一个资源,你可以在编辑器中创建和编辑它,用来收集和包含游戏中将会用到的所有着色器变体。这样做的好处是:

预编译:在游戏构建过程中,Unity会预编译集合中的所有着色器变体。
减少延迟:因为这些变体已经被预编译,所以在游戏运行时就不需要再编译它们,这有助于减少延迟和卡顿。
控制包大小:通过仅包含必要的变体,可以减少最终游戏包的大小。
编辑器中的编译
在Unity编辑器中,当你创建或修改着色器时,编辑器会自动编译它们。这包括:

即时编译:编辑器会即时编译着色器代码,让你可以立即看到更改效果。
错误检查:如果着色器代码有错误,编辑器会提供反馈,帮助你快速定位和修复问题。
构建过程中的编译
当你构建游戏时,Unity会对所有包含在构建中的着色器进行编译。这个过程包括:

跨平台编译:Unity会根据目标平台编译着色器,确保它们在不同的硬件和驱动程序上能够正确运行。
优化:Unity还会尝试优化着色器代码,以提高运行时性能。

预编译着色器的目的是为了避免运行时编译,因为运行时编译可能会导致性能问题,比如在游戏中出现卡顿或者长时间的加载。预编译着色器意味着在游戏打包的时候,Unity会提前编译好所有可能用到的着色器变体。这样,当游戏运行时,就可以直接使用这些预编译好的着色器代码,而不需要再进行编译。

预编译着色器运行时的编译细节如下:

  1. 着色器变体的确定
    在游戏开发过程中,Unity允许开发者指定哪些着色器变体是必需的。这通常通过在Unity编辑器中手动选择或者使用脚本自动收集游戏运行时实际使用的着色器变体来完成。

  2. 打包时的编译
    当游戏被打包时,Unity会编译Shader Variant Collection中的所有着色器变体。这个过程包括将着色器代码从高级语言转换为低级语言,并进行平台特定的优化。

  3. 编译后的存储
    编译后的着色器代码被存储在游戏的数据包中。Unity使用一种特定的格式来存储这些编译后的代码,以便它们可以被快速加载和执行。

  4. 运行时加载
    当游戏运行时,Unity会根据需要加载预编译的着色器变体。如果一个材质使用了某个预编译的变体,Unity会从存储中检索并加载它,而不是在运行时进行编译。

  5. 缓存机制
    Unity还会使用缓存机制来存储和重用已经加载的着色器变体,以减少重复加载的开销。

  6. 延迟编译(续)
    如果游戏运行时请求了一个未预编译的着色器变体,Unity会在运行时编译这个变体。这种情况可能发生在开发者没有预见到所有可能的变体组合,或者在游戏更新后引入了新的材质或特性时。延迟编译会导致性能开销,因为它需要在游戏运行时进行,可能会引起卡顿或延迟。

  7. 异步编译
    为了减少延迟编译对游戏性能的影响,Unity可以选择异步编译着色器。这意味着着色器的编译将在一个单独的线程中进行,而不会阻塞主游戏线程。这样可以减少玩家感受到的卡顿,但是在着色器编译完成之前,可能需要使用一个简单的占位着色器。

  8. 编译错误处理
    如果在运行时编译过程中出现错误,Unity会提供错误信息。这通常发生在着色器代码中存在语法错误或者不兼容的代码时。开发者需要在开发过程中解决这些问题,以确保所有着色器变体都能正确编译。

  9. 性能优化
    预编译着色器的一个重要方面是性能优化。Unity会尝试优化编译的着色器代码,以减少运行时的性能开销。这包括减少着色器的复杂度、优化内存使用和提高执行效率。

  10. 跨平台兼容性
    预编译着色器需要考虑这些差异,确保着色器在不同平台上都能正常工作。Unity通常会为每个支持的平台生成特定的着色器代码。这意味着同一个着色器可能会有多个预编译版本,每个版本都针对特定的硬件和图形API进行了优化。

  11. 着色器级别的兼容性
    Unity中的着色器可以针对不同的图形API(如DirectX, OpenGL, Metal, Vulkan等)编写。预编译过程需要确保着色器代码与目标平台的图形API兼容。这可能涉及到使用特定的预处理器指令或者API特有的语言特性。

  12. 着色器特性的选择
    预编译着色器时,Unity会根据项目设置和目标平台的能力来决定哪些着色器特性会被包含。例如,对于不支持高级图形特性的平台,Unity可能会剔除一些高级光照模型或者后处理效果的着色器变体。

  13. 着色器预热
    在某些情况下,Unity允许开发者在游戏开始时“预热”着色器,这是一个预先加载和编译着色器的过程,目的是减少游戏运行时的性能开销。这通常在加载屏幕期间完成,可以帮助避免游戏早期的性能问题。

  14. 着色器数据的压缩
    为了减少预编译着色器对游戏包大小的影响,Unity可能会对着色器数据进行压缩。这需要在加载时解压缩,但通常这个过程的性能开销远小于运行时编译。

  15. 着色器的更新和维护
    预编译着色器在游戏发布后可能需要更新,特别是当游戏引擎或图形API更新时。Unity提供了工具和流程来帮助开发者更新和重新编译着色器,以确保它们与新的环境兼容。

  16. 资源管理
    预编译着色器变体可能会占用大量的磁盘空间和内存。Unity开发者需要仔细管理这些资源,确保游戏的性能和加载时间不会因为过多的着色器变体而受到负面影响。

预编译着色器是Unity中一个重要的性能优化工具,但它需要开发者在易用性、性能和资源使用之间做出平衡。正确使用预编译着色器可以显著提高游戏的运行效率,减少卡顿,提供更流畅的玩家体验。

Unity引擎支持运行时编译着色器主要是通过两种方式:预编译的着色器变体和按需编译着色器变体。

预编译的着色器变体
Unity允许开发者在编辑器中预编译着色器变体,这些变体在游戏运行时会被加载而不需要实时编译。这是通过使用Shader Variant Collection来实现的,它可以包含游戏中可能用到的所有着色器变体。这个集合可以在游戏构建过程中被编译,并且包含在游戏数据中。

当游戏运行时,如果需要某个已经预编译的变体,Unity会从集合中加载它,而不是在运行时编译。这大大减少了运行时的性能开销,因为编译过程通常是资源密集型的,并且可能导致卡顿。

按需编译着色器变体
尽管预编译可以覆盖许多情况,但有时游戏可能会遇到未预编译的着色器变体。这可能是因为:

游戏动态生成了新的材质或着色器设置。
开发者没有将所有可能的变体添加到Shader Variant Collection中。
游戏中使用了某些用户自定义的着色器,这些着色器在构建时未知。
在这些情况下,Unity需要在运行时编译这些新的着色器变体。Unity使用的是OpenGL或DirectX等图形API的编译器来编译着色器代码。这个过程是自动的,当Unity尝试渲染一个使用了未编译变体的材质时,它会触发编译过程。

为了减少按需编译带来的性能影响,Unity提供了一些策略:

异步编译:Unity可以在一个单独的线程中异步编译着色器,这样可以避免阻断主线程,减少对游戏帧率的影响。这种方式可以让游戏继续运行,而编译过程在后台完成。

着色器预热(Shader Warm-up):在游戏的加载阶段或者在场景加载时,可以预先渲染一些不会显示给玩家看的场景,这样可以提前触发着色器的编译。这种方法可以将编译过程放在玩家不太可能注意到卡顿的时候。

缓存编译好的着色器:一旦着色器变体被编译,Unity可以将编译好的着色器缓存起来,这样在下次需要相同变体时就可以直接使用,而不需要重新编译。

减少着色器复杂性:通过优化着色器代码,减少不必要的变体,可以减少运行时需要编译的着色器数量。

使用较少的关键字:Unity着色器中的关键字(Keywords)会增加变体的数量。通过有策略地使用关键字,可以减少变体的数量,从而减少编译负担。

反馈和调试:Unity提供了工具来分析哪些着色器变体被编译,以及它们的编译时间。这可以帮助开发者识别性能瓶颈,并作出相应的优化。

运行时编译着色器是一个复杂的过程,Unity通过这些机制和工具来帮助开发者管理着色器编译,以确保游戏有最佳的性能和用户体验。开发者需要在开发过程中不断测试和优化,以确保在玩家的设备上运行时,着色器编译不会成为问题。

在渲染过程中,着色器的编译是一个将高级着色器语言(如HLSL、GLSL或者Unity的ShaderLab)转换成可以在图形硬件上运行的低级机器代码的过程。这个过程通常涉及以下几个步骤:

  1. 着色器编写和预处理
    开发者使用高级着色器语言编写着色器代码,这些代码可以包含预处理器指令、条件编译块等。预处理器指令在实际编译之前执行,用于条件编译和代码组织。

  2. 着色器编译
    Unity在编辑器中或者在游戏运行时将着色器代码编译成中间字节码。这个过程由Unity内部的编译器或者外部的编译器(如Microsoft的fxc编译器或者OpenGL的glslc编译器)完成。

  3. 平台特定的优化
    编译成字节码后,Unity或者图形API的驱动程序会根据目标平台进一步优化这些字节码。这些优化可能包括指令重排、寄存器分配、循环展开等,目的是为了提高着色器在特定硬件上的性能。

  4. 链接
    如果一个渲染过程涉及多个着色器阶段(如顶点着色器、片元着色器等),这些不同阶段的编译输出需要被链接在一起,形成一个可以在GPU上执行的完整程序。

  5. 加载和执行
    编译和链接完成的着色器程序被加载到GPU,并在渲染管线的相应阶段执行。这个过程是由图形API(如DirectX或OpenGL)和GPU驱动程序管理的。

具体细节
异步编译:为了避免运行时编译导致的卡顿,Unity可能会在一个单独的线程中异步编译着色器。
缓存:编译完成的着色器程序可以被缓存,这样在下次需要时可以直接加载,避免重复编译。
错误处理:如果编译过程中出现错误,Unity会提供错误信息,帮助开发者定位问题。
变体管理:Unity通过Shader Variant Collection管理预编译的着色器变体,减少运行时编译的需要。
性能考虑
着色器编译是一个耗时的过程,特别是对于复杂的着色器或者拥有大量变体的着色器。因此,开发者需要仔细管理着色器变体,避免在游戏运行时进行大量的编译工作。通过使用Unity的性能分析工具,开发者可以监控着色器编译对游戏性能的影响,并做出相应的优化。

着色器在运行时需要编译的情况通常发生在以下几种情况:

首次使用:当着色器代码第一次被加载到游戏或应用程序中时,它需要被编译成GPU可以理解的机器码。

动态生成的着色器:一些应用程序可能会根据特定的需求动态生成着色器代码,例如基于用户输入或特定的游戏逻辑。这些着色器在生成后需要即时编译。

材质或效果变更:当游戏中的材质属性或视觉效果发生变化,可能需要重新编译着色器以适应新的参数或定义。

平台或设备切换:当游戏或应用程序从一个平台切换到另一个平台,或者在不同的GPU之间切换时,着色器可能需要重新编译以适应不同的硬件特性。

热重载:在游戏开发过程中,为了快速迭代,开发者可能会实现着色器的热重载功能,即在游戏运行时更新和重新编译着色器代码。

预编译着色器失效:如果预编译的着色器缓存失效或不兼容(例如,由于驱动程序更新),则需要在运行时重新编译着色器。

条件编译:着色器代码中可能包含预处理器指令,根据不同的条件编译不同的代码路径。这些条件在运行时评估后可能导致需要重新编译着色器。

为了避免运行时编译带来的性能开销,开发者通常会采取以下措施:

预编译着色器:在游戏打包时预先编译着色器,减少运行时编译的需要。
着色器缓存:将编译好的着色器缓存起来,当再次需要相同的着色器时直接从缓存中加载。
异步编译:在后台线程中编译着色器,避免阻塞主线程和影响游戏性能。
限制动态生成:尽量减少运行时动态生成着色器的情况,预先定义好可能需要的着色器变体。
分离着色器对象:使用OpenGL的分离着色器对象(Separable Shader Objects)或Vulkan的管线缓存,可以允许开发者只重新编译或链接改变的部分,而不是整个着色器程序。

延迟编译:将着色器编译延迟到实际需要渲染的时刻,这样可以避免编译那些最终并未使用的着色器。

优化着色器代码:优化着色器代码可以减少编译时间,例如减少复杂的分支、循环和不必要的计算。

使用高级着色器语言:如HLSL或GLSL的高级特性,比如函数、宏和模板,可以在不牺牲性能的情况下提高代码的可重用性和可维护性。

着色器剖析和性能分析:使用专门的工具来剖析着色器的性能,找出编译时间长的原因,并进行相应的优化。

减少着色器变体:通过减少着色器的变体数量,可以减少编译的总次数。这可以通过使用更通用的着色器代码或者在运行时动态调整着色器参数来实现。

使用跨平台着色器编译器:如Spir-V,可以将着色器编译为中间表示形式,然后在目标平台上进行最终编译,这样可以减少平台之间的编译差异。

合理安排资源加载:在游戏加载阶段或场景切换时预先编译着色器,避免在游戏进行中进行大量编译操作。

通过上述措施,开发者可以最小化运行时编译着色器带来的性能影响,确保游戏运行流畅,提升玩家体验。

已经发布到外网的游戏中,着色器程序的编译通常是由图形API(如DirectX, OpenGL, Vulkan等)提供的编译器来完成的。具体的编译过程取决于游戏使用的图形API和目标平台。以下是一些常见的图形API和它们的编译器:

DirectX:对于使用DirectX的游戏,着色器通常是用HLSL(High-Level Shader Language)编写的。编译过程由DirectX的编译器(如D3DCompile或fxc)负责,它会将HLSL代码编译成字节码(通常是DXBC或DXIL格式)。

OpenGL:OpenGL游戏中的着色器通常使用GLSL(OpenGL Shading Language)。OpenGL驱动程序包含了一个GLSL编译器,它会在运行时将GLSL代码编译成适合特定GPU的机器码。

Vulkan:Vulkan API允许使用GLSL或者SPIR-V(Standard Portable Intermediate Representation - Vulkan)。SPIR-V是一种中间语言,可以由开发者预先编译,然后在运行时由Vulkan驱动程序进一步编译成GPU可以执行的代码。

Metal:对于苹果的Metal API,着色器通常使用Metal Shading Language(MSL)。开发者可以使用Metal的编译工具将MSL代码编译成Metal的二进制格式。

WebGL:WebGL作为OpenGL ES的一个子集,使用GLSL ES(OpenGL ES Shading Language)。浏览器的WebGL实现包含了GLSL ES的编译器,它会在运行时编译着色器代码。

在游戏运行时,当需要编译着色器时,游戏会通过图形API提供的接口提交着色器源代码或预编译的字节码给图形驱动程序。然后,驱动程序会调用内置的编译器来编译着色器,生成可以在GPU上执行的机器码。

值得注意的是,着色器编译过程可能会受到玩家设备上安装的图形驱动程序的影响。不同的驱动程序版本可能会有不同的编译器行为,这也是为什么游戏开发者需要在多种硬件和驱动程序配置上测试他们的游戏,以确保着色器能够正确编译和执行。

  • 24
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

牛掰是怎么形成的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值