MCJE着色器教程:从开发入门到游戏崩溃(一)—— 了解着色器
单独讲解着色器时,其实和Minecraft并不会产生太大的关系,因为着色器是广泛适用于游戏的一个概念。在Minecraft Wiki中,着色器是这样定义的:
着色器(Shader)用于描述渲染游戏的过程。
这句话告诉我们,着色器和游戏渲染相关。这个描述其实就是控制的意思。也就是说,我们通过更改着色器,可以改变Minecraft的渲染方式。这个渲染包括什么呢,举个常见的例子,光影。光影能让你的游戏画面焕然一新,同时让你的显卡变得充满热情和激情。光影的作用就是修改了游戏中的渲染方式,让画面变得更加的精美了。光影的作用方式同样是基于着色器实现的,不同的是,我们今天说的是资源包中的着色器,它是基于原版的,而不是像光影那样基于外部的。同样,资源包中的着色器也能实现很多更改游戏画面的效果。在本教程中,我们将逐步了解Minecraft中的着色器工作原理,编写一个可以完整运行的着色器资源包,以及最后最令人激动的部分——着色器和命令之间的交互。
着色器
首先了解一下,一个游戏的画面是如何被绘制出来的。怎么绘制出来的呢?当然是一个像素点一个像素点绘制的啦。一个游戏画面的窗口中包含了大量的像素点。在每一帧,游戏程序都会计算出每一个像素点的颜色等等数据,并将其一个个显示出来。这个绘制的过程就是渲染。在Minecraft中是使用OpenGL进行绘制的,在OpenGL的教程中是这样描述渲染的:
你好,三角形 – LearnOpenGL CN (learnopengl-cn.github.io)
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线
(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
有些着色器可以由开发者配置,因为允许用自己写的着色器来代替默认的,所以能够更细致地控制图形渲染管线中的特定部分了。因为它们运行在GPU上,所以节省了宝贵的CPU时间。OpenGL着色器是用OpenGL着色器语言
(OpenGL Shading Language, GLSL)写成的,在下一节中我们再花更多时间研究它。
其中包含了三个信息:第一、着色器是运行在GPU上的用于处理渲染过程的小程序;第二、GLSL是专用于着色器的语言;第三、着色器能接受上一个着色器传来的画面(数据),并能在经过自己的处理后传递给下一个着色器。
在Minecraft中,我们只需要处理中间的着色器部分,而不用管最初的画面数据是怎样产生的,也不用管它是怎么样把这些画面呈现在屏幕上的。我们唯一要做的就是,把最初的画面拿过来,改一改,再交给输出画面的程序。一般情况下,没有特殊着色器被调用的时候,最初的画面是直接输出的,但是增加着色器后,就相当于在输出前插入了一个额外的处理过程。
下面用一个经典例子来说明。
在Minecraft中,我们用旁观者模式进入苦力怕的视角时,我们游戏的画面会发生模糊和变色。显然,在这个时候,着色器被调用了,它处理了最初的画面,即我们平时看到的画面,将它进行模糊和变色,并传给输出程序,于是我们看到的画面便变得模糊并且呈现绿色了。
这幅图展示了着色器具体的执行过程。我们可以发现,在苦力怕视角产生的过程中,实际上有两个着色器参与了渲染的过程。第一个着色器color_convolve(作用是更改颜色)将最初的画面转变为了名为swap的画面,我们把这个画面称作一个缓冲,意为暂时储存数据的地方,因为这个画面接下来就被第二个着色器bits(作用是让画面变糊)更改了。我们发现,最初的画面名为minecraft:main,最后的画面名字也叫minecraft:main,这是因为输出程序在输出画面的时候,正是读取minecraft:main画面进行输出。我们将原来的画面更改后,将其命名为minecraft:main进行”偷梁换柱”,于是程序就会输出我们更改后的画面了。值得注意的是,着色器不能直接将更改后的画面储存在原来的画面中,因此即使只有一个着色器,我们也需要在中间动用一个缓冲画面。此时的第二个着色器就相当于没有任何效果了,只是简单的进行一个数据传递。如果仔细查看Minecraft中的原版着色器,可以看到类似的例子。
画面的绘制
在前面我们已经提到了,着色器在Minecraft中起作用的大致过程,下面我们就说一说渲染画面的具体的工作流程,以及每个阶段的着色器都干了什么。
这段内容我直接摘录了OpenGL中的教程,因为它写的真的很好,也相当通俗,虽然我仍然删除和修改了一部分内容。事实上,我推荐诸位读者去稍微了解一下OpenGL方面的知识,将会对各位Minecraft着色器的编写相当有帮助。
你好,三角形 – LearnOpenGL CN (learnopengl-cn.github.io)
下面,你会看到一个图形渲染管线的每个阶段的抽象展示。要注意蓝色部分代表的是我们可以注入自定义的着色器的部分。
如你所见,图形渲染管线包含很多部分,每个部分都将在转换顶点数据到最终像素这一过程中处理各自特定的阶段。我们会概括性地解释一下渲染管线的每个部分,让你对图形渲染管线的工作方式有个大概了解。
首先,我们以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个3D坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,但是简单起见,我们还是假定每个顶点只由一个3D位置(译注1)和一些颜色值组成的吧。
译注1
当我们谈论一个“位置”的时候,它代表在一个“空间”中所处地点的这个特殊属性;同时“空间”代表着任何一种坐标系,比如x、y、z三维坐标系,x、y二维坐标系,或者一条直线上的x和y的线性关系,只不过二维坐标系是一个扁扁的平面空间,而一条直线是一个很瘦的长长的空间。
为了让OpenGL知道我们的坐标和颜色值构成的到底是什么,OpenGL需要你去指定这些数据所表示的渲染类型。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做图元
(Primitive),任何一个绘制指令的调用都将把图元传递给OpenGL,告诉它该怎么处理这些数据。
图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入,并把所有的点装配成指定图元的形状;本节例子中是一个三角形。
图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。
几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素(即你看不到的像素),用来提升执行效率。图中的每一个小方格就代表了一个像素。
OpenGL中的一个片段就是OpenGL渲染一个像素所需的所有数据。片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
可以看到,图形渲染管线非常复杂,它包含很多可配置的部分。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。
在Minecraft中,我们需要编写的正是顶点着色器和片段着色器。
在Minecraft中,着色器被另外分成了核心着色器和后处理着色器。核心着色器输出的就是在苦力怕视角例子中提到的最初画面,它将游戏的数据进行处理,并最后输出。这包含了顶点着色器和片段着色器的过程。在本教程中,我们着重讲解更加基础易懂的后处理着色器。对于后处理着色器来说,核心着色器已经输出的画面就是顶点数据。后处理着色器中的顶点着色器处理的便是游戏画面的四个角落的坐标,产生如下图所示的效果:
前面说过,顶点着色器的一个作用是将三维坐标转换为另一个三维坐标。在核心着色器输出的画面中,对角的两个坐标可能分别是(1920,1080,500)和(0,0,500)。为了方便程序最终的输出处理,顶点着色器会将这两个坐标变换到一个2*2*2的空间内。注意,前面的坐标是以像素为单位,这里的2*2*2的单位应当理解为和游戏窗口大小有关的比例坐标,其(0,0,0)坐标代表了屏幕的中央(不必在意最后的z轴坐标,在投影的时候整个画面会像被投影在一个面上,因此z轴基本没有作用)。比如顶点着色器将上述的两个点变换到了(1,1,0.2)和(-1,-1,0.2)的位置。
随后,这样被处理过的画面送到了片段着色器的位置。片段着色器将会成为本次教程的重点,因为这是我们在后处理着色器中最自由的地方之一。片段着色器会处理前面上的传来的画面中的每一个像素,并将处理后的图像输出。前面提到的苦力怕视角就是片段着色器作用的效果。
两个着色器的具体工作的代码原理将会在后续讲解,但是在讲解这个着色器之前,我想你或许还需要了解一些GLSL代码的知识。这个就是下一节要讲的啦,本节的内容就到这里。
2条评论