游戏着色器(Shader)基础介绍-游戏着色器质量是影响帧数吗为什么

2023-08-18 06:30:47

 

谈到游戏画面表现,不可避免会使用到着色器(Shader),在游戏里为了能实时显示极为复杂的场景,呈现及逼真的效果,许多运算工作都是交给着色器处理。着色器能完成精确的实时光照计算效果,处理大量的三维数据处理。

这篇文章希望帮助读者了解着色器,分为两部分,第一部分首先谈到着色器基础,介绍着色器语言和编译流程;第二部分接着介绍游戏引擎管理着色器方式,有什么样的流程和功能帮助开发者使用着色器。

一、着色器基础

图1:虚幻5引擎的效果图片,使用着色器能实时对极为复杂的三维场景计算真实光照。

为什么着色器能更高效的完成复杂场景或逼真效果等这些工作呢?因为着色器通常是运行在GPU上的程序,而GPU硬件相比传统CPU硬件更适合处理数据量庞大的工作。

普通计算机就像这个管道一样运行,一个接一个地处理这些任务,但是图形计算例如现在常见分辨率 1980x1080屏幕每秒运行60帧,需要每秒处理128,304,000个像素,这个时候,并行处理就是最好的解决方案。比起用四个或八个强大的微处理器来处理这些信息,用一大堆小的微处理器来并行计算,能够更高效的完成这些工作。

图2:CPU和GPU处理方式示意图,把微处理器模拟为管道,GPU能同时处理更多任务。

1.1 着色器语言

实时渲染上着色器的语言有许多种,软件或硬件开发商也可能自己定义着色器语言,这里举出一些图形API使用的着色器语言,通常一个图形API对应一个着色器语言,而在不同平台或操作系统会支持不同图形程序接口,以下举出一些不同平台上主流图形API和对应的着色器语言:

图3:不同平台对应的一些主流图形API。

如Windows平台上主流使用微软公司的DirectX图形程序接口和HLSL着色器语言,在同一个语言下着色器也分为不同的种类,例如HLSL除了传统的光栅化实时渲染,为了让GPU硬件做不同类型的工作,也有其他种类着色器。

在HLSL就分为四种渲染管线,这些管线由单个或多个不同的着色器所构成,每个着色器会有不同的处理对象, 例如在普通图形渲染,为光栅化渲染管线主要由两种着色器构成vertex shader就是处理顶点,pixel shader是处理像素,在Compute Shader渲染管线则只有compute shader一个种类,利用GPU做较通用的数据处理,其他较新推出的管线有用做光线追踪和更适应现代GPU硬件架构光栅化渲染的网格管线。

图4:HLSL着色器渲染管线,绿色为必要着色器,灰色为可选着色器。

1.2 自定义着色器语言

除了这些图形API的着色器语言,许多游戏引擎也定义了自己的着色语言,例如Unity引擎的ShaderLab,可看作是这些图形着色器的扩展,除了支持原本图形API语言,也增加了许多自定义着色器语言,这样做能提供一些额外好处。

由于在实际使用上,图形API的着色器基本语法无法满足许多需求,例如为了编辑和开发便利性,输入需要特定默认值或数据输入的范围,或是给输入变量更容易理解的名称,另外一个例子是着色器为了呈现特定效果,需要去设置额外渲染状态例如半透明着色器,他的渲染状态是AlphaBlend,这种设置写在着色器内能进一步扩展着色器功能,让最终呈现效果能在着色器决定。

例如以下一个Unity 着色器代码例子,只有struct appdata开始的18到38行属于原本HLSL语法,其他部分不属于原本图形API着色器语法,额外语法用来描述渲染类型和输入数据:

Shader "Unlit/NewUnlitShader 1" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = v.vertex; return o; } float4 frag (v2f i) : SV_Target { return 1; } ENDHLSL } } }

1.3 着色器语言自动转换

游戏开发会需要在不同平台运行的需求,这时同个着色器需要在不同平台上运行,如果对每个平台都编写对应平台着色器,维护工作量大并且容易出错。尤其是当两个着色器语言语法差异大的情况下,很难用简单的方式做转换,例如下面HLSL极为简单的pixel shader读取贴图的例子:

struct VSInput { float2 uv : TEXCOORD0; }; Texture2D _BaseMap; SamplerState sampler_BaseMap; float4 frag(VSInput IN) : SV_Target { return _BaseMap.Sample(sampler_BaseMap, IN.uv); }

GLSL着色器语言版本,使用了不同的一些输入内容定义语法:

#version 300 es layout(location = 0) uniform mediump sampler2D _BaseMap; in highp vec2 vs_TEXCOORD0; layout(location = 0) out highp vec4 SV_Target0; void main() { SV_Target0 = texture(_BaseMap, vs_TEXCOORD0.xy); return; }

Metal 着色器语言版本,输入内容需在函数进入位置:

struct Mtl_FragmentIn { float2 TEXCOORD0 [[ user(TEXCOORD0) ]] ; }; struct Mtl_FragmentOut { float4 SV_Target0 [[ color(0)]]; }; fragment Mtl_FragmentOut xlatMtlMain( sampler sampler_BaseMap [[ sampler (0) ]], texture2d<float, access::sample > _BaseMap [[ texture(0) ]] , Mtl_FragmentIn input [[ stage_in ]]) { Mtl_FragmentOut output; output.SV_Target0 = _BaseMap.sample(sampler_BaseMap, input.TEXCOORD0.xy); return output; }

可以看到不同着色器语法和结构上有明显区别,因此需要有个着色器语言转换流程,把着色器从一种语言转换成另一种,这种功能叫跨平台着色编译。

这里以Unity的跨平台编译HLSLcc为例子:由上节知道,Unity定义了自己一套ShaderLab语言,这种语言有许多扩展语法但核心着色器语言通常最终转成HLSL,然后做一套复杂的转换流程,首先会把HLSL由微软的着色器编译程序编译成DirectX Bytecode,这是种类似于汇编语言的格式,有着固定的的计算指令集,从编译中也获取了一系列着色器信息例如着色器的输入和输出,加上一些引擎和ShaderLab里提供的额外编译指引数据最后输入到名为HLSLcc工程,一个文本着色器转译功能,对这个Bytecode以单个指令和变量为单位转为GLSL和Metal着色器语言写法,这样的流程就完成着色器语言不同平台的转换。

图5:ShaderLab编译成Metal和GLSL流程,绿色方块代表着色器数据,蓝色为处理流程,橘色是其他数据。

二、引擎对着色器管理与使用

在游戏开发中我们会使用大量的着色器,完成游戏各种效果或是运行于GPU计算,这时要有一套系统管理和开发大量的着色器,我们这节介绍几个引擎较通用的着色器管理与数据处理方式。

着色器语言由于有自己独特的语法和编译程序,通常会把代码放在独立的文件上,这些着色器代码加载后还需经过编译才能使用,因此可视为需要管理着许多程序文本文件,引擎工具会具备一个文件管理系统去加载这些着色器文件,以Unity引擎下的着色器文件陈列窗口为例子:

图6:Unity引擎着色器数据陈列窗口。

2.1 着色器数据更新与加载

着色器涉及到美术表现,会需要反复测试各种方法或参数,来实验出满意的效果,因此系统要支持对着色器反复修改。这时需支持动态的数据更新机制,能够动态判断着色器数据变化,需要去追踪着色器是否做了修改,并有一套流程去重新编译着色器并能把编译结果直观反馈出来。

图7:Unity引擎着色器编译问题提示。

另外需要有一套关联数据更新机制,由于着色器修改涉及到了输入和输出端口的变化,而着色器又和其他美术数据有关,系统要能侦测到一系列的变化并做相应处理,防止着色器输入或输出变化,造成程序运行错误,例如增加新的着色器贴图输入端口后,系统设置默认贴图资源,避免输入未定值造成运行问题。

2.2 着色器编辑辅助功能

着色器影响到最终美术表现,提供着色器更直观的编辑功能,可帮助开发者更容易实现许多效果,并且降低开发着色器的门坎。其中一个例子就是Unity Shader Graph,这个功能提供节点化的着色器编辑器,把代码指令替换成许多节点,数据的依赖与使用替换成节点间的链结,让使用者可以不写着色器代码下完成着色器开发。

图8:Unity Shader Graph工具窗口。

2.3 材质数据

游戏引擎有着跟着色器密切相关的数据名为材质,这种资源让着色器有更好的使用性。材质是一种数据结构引纪录了着色器和输入着色器的真实数据,这种结构的好处是让同个着色器能根据表现不同替换输入参数和输入资源,把着色器当成可重复利用的模板。

材质文件记录着对应的着色器文件和输入着色器实际用的参数和资源,例如以下是Unity材质文件内容:

图9:材质编辑面板。
图10:真实存的文本数据。

2.4 着色器变种

当着色器功能越来越复杂,又为了保持着色器高性能的渲染,针对不同效果和设备需要做不同程度的着色器代码简化,不能让程序有额外的变量或计算。为了同时满足复杂表现和良好的渲染性能,在一般C++开发中会使用条件判断式,但着色器条件判断会大大影响性能尤其在移动平台设备,这时增加着色器数量是一种常见的优化手段。

这引出了着色器变种的概念,针对不同的条件判断生成不同的着色器,由于着色器语言接近于C语言,常用的手段是用宏产生不同功能的着色器。有了多样的着色器后,就能针对不同的状况动态去择与切换当前使用哪个着色器变种。

例如Unity提供复杂的变种功能,着色器代码内能定义数量庞大的变种变种,以keyowrd对应一个着色器代码的宏,在程序中就能用这些keyword动态控制使用哪个着色器变种:

图11:着色器代码使用keyword控制表现效果。

2.5 着色器数据处理工具链

引擎编辑状态和实际运行时着色器使用上通常有较大的区别,在开发时可以忍受着色器花一段时间编译,对占用的内存或大小也不会太重视。但在游戏真正运行时需要保证游戏流畅度,因此要让着色器能快速完成编译,保持最小的系统资源使用量,尤其在游戏使用了数量庞大的着色器的情况。因此还需对着色器数据做额外处理,保证运行时是以最优化的方式使用着色器。可从主要两个方面考虑,不影响游戏的流畅度和数据量减少,例如下面举出一种着色器数据处理流程:

绿色为数据,橘色为数据减少流程,蓝色为加快运行时速度流程。以下是各流程的处理内容:

1、删减着色器数据:此阶段可把运行时不会用到的数据删去,例如去掉只有开发和查错时会用到的着色器。

2、预先编译着色器:特定图形API能在运行前为目标平台编译着色器,避免运行时编译降低游戏流畅度。

3、着色器数据压缩:使用数据压缩算法减少着色器空间占用同时减少运行时加载内容。

三、总结

从以上内容我们能对着色器有更进一步的了解,对不同平台的着色器有基础认识,并了解在这基础上所衍伸的进阶着色器功能。认知到游戏引擎通常用什么手段管理着色器,实际使用着色会经过怎样的流程和处理。

另外着色器随着时间又越来越多的新功能、新着色器编译方式或管理和产生庞大着色器变种做法,这些功能随着图形API版本更迭不断变化,仍有许多值得更深入研究与开发的东西,以下是一些衍伸阅读内容链接。

着色器语法比较:https://alain.xyz/blog/a-review-of-shader-languages着色器变种的管理做法比较:https://therealmjp.github.io/posts/shader-permutations-part2/HLSL新版本的变化:https://devblogs.microsoft.com/directx/跨平台编译:https://zhuanlan.zhihu.com/p/69397055

如果你对游戏开发技术或计算机图形学等热门话题感兴趣,欢迎给“网易游戏雷火事业群”点个关注喔( ゚∀゚) ノ♡)


以上就是关于《游戏着色器(Shader)基础介绍-游戏着色器质量是影响帧数吗为什么》的全部内容,本文网址:https://www.7ca.cn/baike/68649.shtml,如对您有帮助可以分享给好友,谢谢。
标签:
声明

排行榜