着色器使用指南

着色器通常用于在游戏中创建漂亮的图形效果。它们也是GameMaker提供的最高级功能之一,因此在开始使用它们之前,您必须对编程以及GameMaker的工作原理有基本的了解。

那么,什么是着色器?最初,它们被创建为照明提供阴影(因此得名),但现在它们被用来产生各种各样的效果。着色器代码类似于常规代码,但它(几乎总是)由GPU执行,而不是CPU。这种差异有它自己的一套规则和限制,但我们将在后面介绍。

每个着色器由两个单独的组件组成:顶点着色器片段着色器(也称为像素着色器)。让我们从顶点着色器开始。每个精灵都由一个矩形组成,但计算机喜欢绘制三角形,因此这些矩形被分成两个三角形(有时称为quad)。这使得我们每个精灵有六个顶点(角),但其中两个是相同的,所以我们只需要担心四个。现在,假设我们有一个for循环,它遍历每个顶点,并为每个顶点执行顶点着色器中的代码。这允许我们在传递给片段着色器之前改变顶点的位置和颜色,因为顶点着色器是在前面执行的。

这看起来是这样的:

Shader Vertices在片段着色器中,你可以想象和以前一样的循环,但这次它会遍历你的子画面中的每一个像素,给你诸如像素的位置和颜色等信息。在片段着色器代码中,可以执行操作和计算来确定该像素的颜色,以获得所需的效果。例如,如果您希望着色器使您的精灵是黑色和白色,那么您需要计算每个像素需要的灰色阴影来创建效果。

它看起来像这样:

Shader Fragments着色器代码通常由GPU执行的原因是它在这方面更有效。现代CPU通常具有两到八个内核。每个内核一次可以执行一个任务,因此通过利用多个内核,我们可以同时执行多个任务。相比之下,现代GPU可以同时执行数千甚至数万个任务。这对着色器很有帮助,因为我们可以同时执行数千个像素的着色器代码。限制是我们只能访问sprite的初始状态,因此我们不知道对其他像素的任何修改,因为我们不能确定代码是否已经在它们上运行。

注意 GameMaker允许用户使用GLSL(OpenGL着色器语言)、HLSL(高级着色器语言,在使用DirectX时使用)和GLSL ES(移动的设备中常见的GLSL子集)编写着色器。这里我们使用GLSL ES作为着色器语言,因为它是跨目标平台提供最佳兼容性的语言。一般来说,这是一个你总是想使用,除非你有非常具体的需求,并了解其他着色器语言的限制。然而,保存这里和那里的一些语法差异之外,所有三种语言之间的数学和技术应该是相似的。

顶点着色器首先执行,正如我们上面解释的,它处理顶点。它用于计算位置、法线和纹理坐标。这些着色器在2D中不是特别有用,因为每个精灵通常都是正方形,但它可以用来做一些倾斜,缩放等。它在3D中对于照明计算和网格变形变得更加有用。片段着色器更有趣,也是这里主要讨论的内容,因为片段着色器是我们获取纹理信息的地方,可以调整图像中每个像素的最终颜色。

 

着色器变量

如果您在GameMaker中创建了着色器,您可能已经注意到默认直通着色器中的以下关键字。这些关键字有助于着色器了解每个变量的用途和范围:

您还将看到使用vec作为关键字。这是用来标识着色器中的向量变量,你很快就会看到,向量是非常重要的,当使用着色器。这就是为什么它们在GLSL中被实现为基类型的原因。如果您不熟悉它们,它们是一个数学术语,表示为只有一列的矩阵。在编程中,我们通常将它们表示为一个数组,其中组件的数量与维度相对应。二维和三维向量通常用于位置、纹理坐标或没有Alpha通道的颜色,而四维向量用于具有Alpha通道的颜色。我们还可以指定它们是否保存布尔值、整数值或浮点值。声明vector的语法如下:

vec2 firstVec;  // Two-dimensional vector of floats
vec3 secondVec; // Three-dimensional vector of floats
vec4 thirdVec;  // Four-dimensional vector of floats

bvec3 boolVec;  // Three-dimensional vector of booleans
ivec4 intVec;   // Four-dimensional vector of integers

要初始化它们,我们可以使用构造函数来创建向量。您需要提供与向量长度相同数量的值,但您可以混合和匹配标量和较小的向量以达到目标长度。以下是一些例子:

// Simple 2D vector with 2 scalar values
vec2 firstVec  = vec2(2.0, 1.0);
// A 4D vector using 2 scalars and a vec2 create the 4 values
vec4 secondVec = vec4(1.0, firstVec, 0.0);
// A 3D vector using 1 component of a vec4 plus a vec2 to create the 3 values
vec3 thirdVec  = vec3(secondVec.x, firstVec);

我们也可以给它们分配另一个相同长度的向量(或者swizzle向量,直到它有合适的长度,但我们稍后会解释):

vec3 firstVec;
vec3 secondVec = firstVec;
vec4 thirdVec  = secondVec.xyz;
vec2 fourthVec = thirdVec.zx;

在GLSL中访问矢量分量时,我们有几个选项。最基本的方法是将向量视为数组,并使用方括号访问组件,如下所示:

vec4 myVec;
myVec[0] = 1.0;
myVec[1] = 0.0;
myVec[2] = 2.0;
myVec[3] = 1.0;

但是,还有另一种使用以下语法访问组件的方法:

vec4 myVec;
myVec.x = 1.0;
myVec.y = 2.0;

这将使用向量中的组件名称来访问它们。可以使用x、y、z或w分别获取第一、第二、第三或第四个分量。我们将此方法称为swizzling,因为以下语法也是有效的:

vec4 firstVec;
vec3 secondVec = firstVec.xyz;
vec2 thirdVec  = secondVec.zy;
vec4 fourthVec = thirdVec.yxxy;

正如你所看到的,我们可以使用最多四个字母的任何组合来创建一个长度相同的向量。我们不能尝试访问超出边界的组件(例如,尝试访问secondVecthirdVec中的w,因为它们没有第四个组件)。此外,我们可以重复字母并以任何顺序使用它们,只要它被分配给的向量变量与所使用的字母数量相同。

由于显而易见的原因,在使用swizzle设置组件值时,不能两次使用同一个组件。例如,下面的无效,因为您试图将同一组件设置为两个不同的值:

myVec.xx = vec2(2.0, 3.0);

最后,我们一直使用xyzw作为我们的混合掩码,这通常是处理头寸时的情况。还有两组面具可以用途:rgba(用于颜色)或stpq(用于纹理坐标)。在内部,这些掩码之间没有区别,我们只是使用它们来使代码更清楚地了解向量在该实例中表示的内容。此外,我们不能在同一个操作中联合收割机swizzle掩码,因此这是无效的:

myVec = otherVec.ybp;

这些是很多定义和信息,但了解这些东西对于理解着色器本身是必要的。

 

创建着色器

GameMaker中创建着色器时,它将为您打开两个文件:顶点着色器(.vsh)和片段着色器(.fsh)。这是您可以制作的最基本的着色器,它获取精灵,读取纹理,并使用该颜色为每个像素着色。如果在绘制时指定顶点颜色,则这些颜色将与纹理混合。

让我们从顶点着色器开始,浏览新创建的着色器资源的代码并对其进行分析。

// Passthrough Vertex Shader
attribute vec3 in_Position;                  // (x,y,z)
//attribute vec3 in_Normal;                  // (x,y,z)     unused in this shader.
attribute vec4 in_Colour;                    // (r,g,b,a)
attribute vec2 in_TextureCoord;              // (u,v)

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
    vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
    
    v_vColour = in_Colour;
    v_vTexcoord = in_TextureCoord;
}

在main函数之外,我们看到一些变量声明和它们的限定符。这些属性是由GameMaker提供给我们的。变化的着色器由用户创建以将该信息传递到片段着色器。在main函数中,我们有计算来找到顶点的屏幕位置:

如果您不打算使用顶点位置,则应单独使用此着色器,并且它不会在下面给出的任何示例中使用,因为显示的所有效果都将使用片段着色器创建。

现在让我们快速查看片段着色器:

// Passthrough Fragment Shader
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
}

如前所述,片段着色器背后的想法是返回当前像素的颜色。这通过将最终颜色值分配给变量gl_FragColor来完成。texture 2D函数接受一个纹理和一个vec 2,其中包含要在该纹理中检查的UV坐标,该函数返回一个vec4颜色。在穿透着色器中,我们所做的就是获取该像素坐标中的纹理颜色,并将其乘以与该像素关联的顶点的颜色。

现在我们有了第一个着色器,我们要做的就是创建一个对象并为其分配一个精灵,然后在对象的Draw Event中设置着色器如下:

// Draw Event
shader_set(shdrColorOverlay);
draw_self();
shader_reset();

我们在shader_set()shader_reset()之间进行的每个绘制调用都将应用着色器。在这里,我们使用我们的穿透着色器绘制对象精灵:

Drawing Sprite Using Passthough Shader当然你可能已经猜到了,这并不会在视觉上改变任何东西,因为这是一个简单的传递着色器。然而,下面的部分概述了一些简单的步骤,你可以采取修改这一点,并改变精灵的方式将绘制。每个部分都显示了一个不同的着色器,您可以在项目中创建和使用,解释了创建它们所需的步骤以及我们为什么要这样做。

 

颜色叠加着色器

黑白着色器

彩虹着色器

 

这是这篇简短的指南的结尾,现在你应该对着色器的工作原理和它们的一些用途有了更好的理解。你应该花时间来玩你按照这篇指南创建的着色器,并尝试用它们做其他事情-创建一个模糊着色器,或者一个着色器,使gameboy风格的单色屏幕怎么样?- 因为着色器是一个非常强大的工具,可以为游戏添加视觉复杂性和风格。

我们要感谢Alejandro HittiAmazon允许我们复制本指南。您可以在Amazon Developer Blog上找到原始版本。