Vico Bill< 刘 利 波 > 的个人网站

记录关于学习、工作中的技术点滴

C,C++,Rust,Ruby爱好者;热衷于游戏开发、任务自动化与跨平台;沉迷于游戏引擎与图形表现;深信'简单、多元'哲学的力量。


访问主页

【DailyGfx】2-Shader

在现代渲染管线中,硬件GPU常常由显卡驱动程序驱动。显卡驱动常常适配至操作系统,以使操作系统能操纵显卡显示出图形。操作系统主要职能就是控制计算机所有的硬件资源,并提供接口给程序员使用。显卡也是硬件资源之一,当然也不例外。所以,我们常说的图形API OpenGL,Vulkan,DirectX等,都是由标准制定机构进行制定,与显卡驱动提供商进行约定,制定出标准。接着操作系统、显卡驱动商协商一致之后,提供给程序员用的接口,即是图形API。

程序员通过图形API,可以让显卡供我们工作。

那Shader是什么呢?Shader并非是GPU直接处理的资源,而是由图形API自行解释,并能在渲染管线对应的Shader中进行使用的语言。Shader最大的作用,是将渲染任务最大限度地交给了程序员,程序员可以定制渲染的几乎每个过程(Rasterization除外),这极大赋予了程序员自由。

Shader常常由图形API组织制定。例如,DirectX图形API制定了对应的HLSL着色器语言;OpenGL图形API制定了GLSL着色器语言;NVIDIA卡图形驱动提供者并显卡提供者,提供CG着色器语言等等不一而足。种种Shader语言虽然不同,但最终目的一致——即赋予程序员自由。

所以Shader在某种意义上说,是中间语言,立于程序员和显卡间。既然是中间语言,那么Shader程序必须经过编译,编译成GPU指令集,就如同C++程序一样,如果运行于CPU,则必须是编译成CPU指令集。无论是CPU还是CPU,它们只认它们范围域中的指令集,不认识什么C++或者Shader。因此,图形API必须提供了编译Shader的接口,如OpenGL中有glCompileShader,DirectX中有D3DXCompileShader。Vulkan则提供了一种称为SPIR-V的东西,这是字节代码,如同C#编译之后形成的中间字节代码一样。字节代码有其好处,即只需要提供虚拟机即可跨平台运行。Vulkan通常会提供glslangValidator程序,将GLSL转换成SPIR-V字节码。SPIR-V可避免实时编译,这提高了着色器的效率。

着色器不可避免地需要编译,但每次都编译则会很耗时间,所以最常见的优化是预编译。在CryEngine中,会创建ShaderCache,缓存编译后的Shader;有些则是通过外部程序,如UE4在游戏之前会编译所有的Shader,然后再使用。Vulkan的SPIR-V也是类似的操作,它提供外部程序编译Shader,然后再使用。也有些是通过Shader Thread来编译,但其本质上来说,仍然是预编译。

在实际运行中,不同的Shader有自己的入口函数,就如同C语言一样。Vertex Shader、Fragment Shader、Geometry Shader都有其自身的入口函数。Shader语言中,通常会有许多内置的变量可供使用,这些内置变量往往是当前Shader能处理的数据。例如,Vertex Shader需要得到顶点的位置数据,在OpenGL中存在gl_Position这样的变量。同样Shader中也可以自定义变量进行处理;应用程序也可以将数据传到Shader中,Shader也可把数据传出来。这个过程就如同C语言与Lua语言交互一样,Lua 语言里有一些内置的变量,它们有自己的用途,而程序员可通过C访问Lua里的变量,也可通过Lua访问C里的变量。在理解Shader过程中,可以将其当作特定于图形的Lua语言,有助于理解。

所以,当有大量数据要处理时,可交给Shader,由GPU来处理Shader任务。这样则能把消耗大的任务,交给硬件处理。也就是GPGPU,常见应用即是“挖矿”。“挖矿”常常借助显卡来实现并行处理。

在不同的游戏引擎中,很多又有自身的Shader程序编写方式。如Unity有ShaderLab,UE4有Blueprint。这是为了跨平台采取的措施,如Source引擎会有HLSL to GLSL的程序,应对不同图形API时的Shader应用。在跨平台引擎中,自定义Shader语言犹为常见。跨平台引擎直接面对的是不同的硬件,程序员不该为各个硬件编写不同的Shader语言,如在Linux下编写HLSL,在Windows下编写GLSL等等,所以跨平台引擎抽象出Shader语言,即使应对不同平台不同硬件时,也能自由处理。所以,可以得出一种结论,只要引擎是跨平台的,那么必定会有跨图形API的Shader语言存在。

那么,说了这么多,Shader到底怎么用呢?给个示例伪代码

vec3    in_position;
vec4    out_color;


void vs_main(){
    in_position.x += 10;
    out_color = vec4(.5,.5,.5,.5);
}
vec4    out_color;

void fs_main() {
    out_color = awesome_process()
}
加载Shader文件
vs = LoadVertexShaderSource();
fs = LoadFragmentShaderSource();

编译Shader文件
vsprog = glCompileShader(vs);
fsprog = glCompileShader(fs);

使用Shader处理任务
glUseProgram(vsprog);

Shader互通变量
color = glGetShaderVar("out_color");
glSetShaderVar("out_color",COLOR_WHITE);

注意,以上代码是伪代码,不要直接复制粘贴!它们存在的意义只是阐述其使用方式。

图形API的Shader语言通常脱胎于C语言(HLSL,GLSL,CG皆是),原因之一是c语言具有很好的跨平台,常用于嵌入式设备中,显卡也不例外,当然是图形驱动开发者最熟悉的语言;其次,c语言很简单,图形API的Shader语言则是简化版的c语言,其编译器很容易实现。

通常使用游戏引擎,那么最好是学习游戏引擎的Shader为好,无需再接触GLSL或HLSL或SPIR-V等,因为它们雨女无瓜;)。但一定要放轻松,Shader就是简化的中间语言/脚本语言,比起新学一门编程语言,Shader语言要简单多了!SO,TAKE IT EASY!

最近的文章

【DailyGfx】3-Mesh

在图形渲染管线最前面,是模型数据,也称为网格(Mesh)。网格是多边形列表,在图形学中,网格多采用三角形列表,也就是说,网格实质上是由若干三角形组合而成。如何让若干三角形正确表示一个模型呢?那么必不可少的必定是顶点。三角形由3个顶点构成。如果只是简单地列出这些顶点或这些三角形,不足以正确描述此模型。那么还缺的是如何互相组合顶点。网格的三角形,必定会有很多是共用顶点、共用边的,只存储三角形列表,会浪费许多空间。所以在图形学中,往往使用两个数组:顶点表和索引表,相互协作来达到描述网格三角形组的...…

继续阅读
更早的文章

【DailyGfx】1-渲染管线

渲染管线是整个渲染中的核心部分。渲染管线的主要目的是将3D描述的世界绘制成2D屏幕上显示的像素。可想而知其中的复杂度。渲染管线在早期通常实现在CPU端,称之为软渲染,即整个渲染管线的处理都交给CPU处理。软渲染可行,CPU不仅要处理游戏逻辑,还要处理渲染逻辑,其负重极大,影响FPS(Frame Per Second),对于游戏而言,帧率非常重要,需要达到30-60FPS才能让玩家感受到画面的流畅度。在VR上,则要达到90FPS。如今很少有直接使用软渲染(Software Renderer)...…

继续阅读