着色器使用指南

着色器通常用于在游戏中创建漂亮的图形效果。它们也是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当然你可能已经猜到了,这并不会在视觉上改变任何东西,因为这是一个简单的传递着色器。然而,下面的部分概述了一些简单的步骤,你可以采取修改这一点,并改变精灵的方式将绘制。每个部分都显示了一个不同的着色器,您可以在项目中创建和使用,解释了创建它们所需的步骤以及我们为什么要这样做。

 

颜色叠加着色器颜色叠加着色器

我们现在可以编辑基础着色器来做一些不同的事情。我们不会触及顶点着色器部分,而只是编辑片段着色器,首先我们将做一个非常简单的操作,即让着色器使用红色绘制精灵。我们将通过简单地将gl_FragColor更改为红色来实现这一点。像这样:

// Color Overlay Fragment Shader
void main()
{
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

这将给我们给予以下结果:

Initial Colour Overlay Block不完全符合我们的预期! 我们需要记住的是,每个精灵最终都是一个矩形,因此除非我们考虑透明度(我们没有考虑),否则这就是我们将得到的结果。

注意在上图中,矩形的大小发生了变化,因为当 GameMaker 将其放置在纹理页面上时,基础精灵已自动裁剪了其周围的“空白”空间,因此每个动画都将三角形框起来 组成它的尺寸不同,以适应框架的裁剪尺寸。 如果禁用此选项,那么屏幕上只会出现一个静止的红色方块。

上面我们提到了 texture2D 函数,我们将使用它来获取正在处理的像素处的颜色并从中获取透明度。 texture2D 的返回值是一个 vec4,其中组件按顺序为红色、绿色、蓝色和 Alpha。 我们可以通过在变量名称后添加一个句点并后跟 aw 来访问 Alpha 通道。 这分别对应于 RGBA 和 XYZW。

下面是更新后的代码:

// Color Overlay Fragment Shader
varying vec2 v_vTexcoord;

void main()
{
    vec4 texColor = texture2D(gm_BaseTexture, v_vTexcoord);
    gl_FragColor = vec4(1.0, 0.0, 0.0, texColor.a);
}

我们现在为 gl_FragColor 分配一个新的 vec4,其中红色通道已最大化,绿色和蓝色通道为零,并且 Alpha 通道与原始纹理相同。 输出如下所示:

Overlay Shader Making Sprite Red这就是我们所追求的! 我们将每个像素的颜色替换为红色,但保持 Alpha 通道完好无损。

每次我们想要使用不同的颜色时都必须更改着色器并不是一个好主意,特别是因为我们需要为我们想要的每种颜色都有一个单独的着色器。 相反,我们将使用 uniform 将颜色信息传递给着色器。 为此,我们首先需要获取指向制服的指针。 我们将在具有精灵的对象的 创建事件 中执行此操作,方法是添加:

// Create Event
_uniColor = shader_get_uniform(shdrColorOverlay, "u_colour");
_color    = [1.0, 1.0, 0.0, 1.0];

我们需要做的就是调用 shader_get_uniform() 来获取指向 uniform 的指针。 我们需要传递的参数是着色器资源名称(不带引号,因为我们要传递 GameMaker 为我们生成的 ID)和着色器内部的统一变量的名称(这次是字符串)。 该名称需要与着色器代码中的名称完全匹配才能正常工作。 我们还添加了一个颜色变量,以便我们可以在运行时更改它并让它记住我们的更改。

现在,我们的draw事件中的代码将稍微改变以传递uniform变量。

// Draw Event
shader_set(shdrColorOverlay);
shader_set_uniform_f_array(_uniColor, _color);
draw_self();
shader_reset();

这段代码和前面的代码一样,但是在我们绘制任何东西之前,我们需要将所有的uniform值传递给shader。在这个例子中,我们将颜色作为一个浮点数组传递。至于shader,我们将改变它以包含uniform并使用它,所以它变成:

// Color Overlay Fragment Shader
varying vec2 v_vTexcoord;
uniform vec4 u_colour;
void main()
{
    vec4 texColor = texture2D(gm_BaseTexture, v_vTexcoord);
    gl_FragColor = vec4(u_colour.rgb, texColor.a);
}

我们声明一个与创建着色器 (u_colour) 中同名的变量,并利用调配功能将其作为 gl_FragColor 向量的前三个分量进行传递。 如果我们再次编译,我们应该看到:

Overlay Shader Making Sprite Yellow现在,着色器更加有用且可重用。 如果您需要在运行时设置颜色(使用变量 _color),您可以添加更多功能。

 

黑白着色器黑白着色器

制作一个黑白着色器是了解着色器工作原理的好方法,很多初学者都是从尝试这样做开始的,因为从概念上讲,它非常简单:获取每个像素并为其分配灰色阴影。但它简单吗?不太简单。

当使用RGB颜色时,如果所有三个分量都是相同的值,那么我们就会得到一个灰色色调。使用这种想法创建着色器的简单方法是将所有三个颜色通道(红色,绿色和蓝色)相加,然后将其除以3。之后,您将值分配给所有三个通道,从而创建一个灰色色调。下面是片段着色器的外观:

// Black and white fragment shader
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
    vec4 texColor = texture2D(gm_BaseTexture, v_vTexcoord);
    float gray = (texColor.r + texColor.g + texColor.b) / 3.0;
    gl_FragColor = v_vColour * vec4(gray, gray, gray, texColor.a);
}

您可能已经注意到的一件事是,在 gl_FragColor 代码中,我们将 vec4 与名为 v_vColour 的值相乘。 这是顶点着色器传递的变量,它告诉我们与该像素关联的顶点的颜色。 将最终计算的颜色与顶点颜色相乘总是一个好主意。 在大多数情况下,它不会执行任何操作,但如果您更改了 GML 中的顶点颜色,这将反映出来(通过使用 draw_sprite_ext()draw_sprite_general() 等函数) 更改 image_blend)。

至于绘制事件,非常简单,因为我们没有要传入的uniform :

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

让我们编译一下,看看我们得到了什么。

Black And White Shader这看起来已经很棒了,对吧?嗯,是也不是..

// Black and white fragment shader
varying vec2 v_vTexcoord;

void main()
{
    vec4 texColor = texture2D(gm_BaseTexture, v_vTexcoord);
    float gray = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
    gl_FragColor = vec4(gray, gray, gray, texColor.a);
}

我们使用点积作为简写,将texColor的每个分量乘以正确的权重,然后将它们加在一起。如果您不熟悉点积,则基本上是这样的:

float gray = (texColor.r * 0.299) + (texColor.g * 0.587) + (texColor.b * 0.114);

最后,它看起来非常相似,但在技术上更正确。

Corrected Black And White Shader

 

彩虹着色器彩虹着色器

我们最后一个着色器示例是一个有趣的示例,可以用来为文本、按钮和其他东西添加生命力。我们将从简单的开始,逐渐添加功能,因为这个着色器是高度可定制的。这个着色器有很多内容要讲,所以如果你觉得有点困惑或困惑,请回去重新阅读上面的一些部分。

我们要做的第一件事是根据像素的水平位置,用每种色调给像素上色,方法是将x位置设为色调,然后从HSV转换(色调、饱和度、亮度)格式转换为RGB(红、绿色和蓝)格式。为此,我们需要在片段着色器中编写一个辅助函数,它接受HSV值并返回RGB向量。我们将使用一个函数来完成此操作,而不需要任何if语句,因为在着色器代码中使用条件会使着色器非常慢,应该避免。

以下是着色器在此阶段的外观:

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

vec3 hsv2rgb(vec3 c) 
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

void main()
{
    vec3 col = vec3(v_vTexcoord.x, 1.0, 1.0);
    float alpha = texture2D(gm_BaseTexture, v_vTexcoord).a;
    gl_FragColor = v_vColour * vec4(hsv2rgb(col), alpha);
}

这里比前面的例子多了一点,但大部分对你来说应该是相当明显的。首先,有我们的hsv2rgb函数,它使用HSV颜色获取vec3,并使用RGB转换返回另一个vec3。在main函数中,我们首先创建HSV颜色,其中色调是x位置,我们现在将饱和度和亮度设为1.0。然后,我们从纹理中获取alpha,这样它只会给精灵角色着色,而不是整个精灵矩形(正如我们在上面的颜色叠加示例中所做的那样)。最后,我们将片段颜色设置为使用alpha转换为RGB的HSV颜色,乘以顶点颜色(最好始终这样做)。

至于我们的draw代码,目前它是微不足道的:

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

让我们看看我们得到了什么:

Initial Rainbow Shader我们已经接近我们想要的结果,但有一个问题:我们并没有在动画的每一帧中同时看到所有颜色,而且颜色似乎是随机变化的。 原因是我们假设 v_vTexcoord 为我们提供了精灵的坐标,从左上角 (0,0) 开始,到右下角 (1,1) 结束,这是着色器中的标准坐标 。 不过,为了优化,GameMaker 将尽可能多的纹理填充到所谓的纹理页面中,因此,这就是我们的纹理的实际外观:

如上所述,v_vTexcoord 为我们提供了精灵在整个纹理页面内的绝对坐标,但我们想要的是一个仅覆盖当前精灵的 0.0 到 1.0 之间的值。 此过程称为标准化(获取值并将其转换为 0 到 1 的范围)。 为了标准化我们的水平值,我们需要知道上图中 x0 和 x1 的值。 幸运的是,GameMaker 有一个函数可以为我们提供纹理页面中精灵中每个角的位置。 首先,我们需要转到创建事件并创建一个制服以将此数据传递给着色器:

// Create Event
_uniUV = shader_get_uniform(shdrRainbow, "u_uv");

我们修改绘制事件以获取值,然后将它们传递给着色器:

// Draw Event
shader_set(shdrRainbow);
var uv = sprite_get_uvs(sprite_index, image_index);
shader_set_uniform_f(_uniUV, uv[0], uv[2]);
draw_self();
shader_reset();

函数 sprite_get_uvs() 接受一个精灵和一个索引,它返回一个包含大量信息的数组,例如每个角的坐标、裁剪了多少像素来优化等等。我们 对其中两个值感兴趣:精灵的左坐标和右坐标,它们分别存储在 uv[0]uv[2] 中。 在片段着色器中,我们现在将使用这些值来计算标准化水平位置,如下所示:

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

uniform vec2 u_uv;

vec3 hsv2rgb(vec3 c) 
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

void main()
{
    float pos = (v_vTexcoord.x - u_uv[0]) / (u_uv[1] - u_uv[0]);
    vec3 col = vec3(pos, 1.0, 1.0);
    float alpha = texture2D(gm_BaseTexture, v_vTexcoord).a;
    gl_FragColor = v_vColour * vec4(hsv2rgb(col), alpha);
}

这里我们在文件的顶部添加了一个uniform变量,其名称与我们在创建事件中使用的名称相同。接下来,我们通过将当前的x坐标转换到原点(v_vTexcoord.x - u_uv[0])来计算归一化的水平位置,然后将其除以精灵的宽度,得到从0到1的范围(u_uv[1] - u_uv[0])。

其结果是:

Rainbow Overlay Shader Improved好了!这正是我们想要的。我们可以看到精灵内部光谱的每一种颜色。

你可能会很高兴,但我们可以用这个着色器来玩得更开心。如果我们根据时间为颜色添加偏移量来产生移动会怎么样?要做到这一点,我们需要两个额外的变量,分别用于速度时间。我们还需要两个制服,每个新变量一个,所以创建事件变成:

// Create Event
_uniUV    = shader_get_uniform(shdrRainbow, "u_uv");
_uniTime  = shader_get_uniform(shdrRainbow, "u_time");
_uniSpeed = shader_get_uniform(shdrRainbow, "u_speed");
_time  = 0;
_speed = 1.0;

我们还需要增加每帧的时间,因此在步事件中添加:

// Step Event
_time += 1 / game_get_speed(gamespeed_fps);

现在让我们转到绘制事件,将这些 uniforms 发送到着色器:

// Draw Event
shader_set(shdrRainbow);
var uv = sprite_get_uvs(sprite_index, image_index);
shader_set_uniform_f(_uniUV, uv[0], uv[2]);
shader_set_uniform_f(_uniSpeed, _speed);
shader_set_uniform_f(_uniTime, _time);
draw_self();
shader_reset();

最后,我们将回到我们的着色器,现在实际使用这些变量。我们要做的是将速度乘以时间,然后将其添加到位置,如下所示:

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

uniform vec2 u_uv;
uniform float u_speed;
uniform float u_time;

vec3 hsv2rgb(vec3 c) 
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

void main()
{
    float pos = (v_vTexcoord.x - u_uv[0]) / (u_uv[1] - u_uv[0]);
    vec3 col = vec3((u_time * u_speed) + pos, 1.0, 1.0);
    float alpha = texture2D(gm_BaseTexture, v_vTexcoord).a;
    gl_FragColor = v_vColour * vec4(hsv2rgb(col), alpha);
}

如果你做的一切都是正确的,你应该会看到这样的东西:

Rainbow Shader Overlay Moving With Time为了完成此着色器,我们将添加更多uniforms以进一步自定义它。 前两个是控制饱和度和亮度。 下一个我们将称为“部分”,它的功能是允许用户传递 0 到 1 之间的数字来确定我们一次看到的整个光谱的百分比。 最后,我们将添加一个名为“mix”的变量,它将指定我们想要将着色器颜色与原始纹理颜色混合的程度(1.0 是所有彩虹,0.0 是所有纹理)。 与往常一样,我们首先将变量添加到创建事件中:

// Create Event
_uniUV         = shader_get_uniform(shdrRainbow, "u_uv");
_uniTime       = shader_get_uniform(shdrRainbow, "u_time");
_uniSpeed      = shader_get_uniform(shdrRainbow, "u_speed");
_uniSection    = shader_get_uniform(shdrRainbow, "u_section");
_uniSaturation = shader_get_uniform(shdrRainbow, "u_saturation"); 
_uniBrightness = shader_get_uniform(shdrRainbow, "u_brightness");
_uniMix        = shader_get_uniform(shdrRainbow, "u_mix");

_time  = 0;
_speed = 1.0;
_section = 0.5;
_saturation = 0.7;
_brightness = 0.8;
_mix = 0.5;

我们的绘制事件发生了变化,包括这些 uniforms ,如下所示:

// Draw Event
shader_set(shdrRainbow);
var uv = sprite_get_uvs(sprite_index, image_index);
shader_set_uniform_f(_uniUV, uv[0], uv[2]);
shader_set_uniform_f(_uniSpeed, _speed);
shader_set_uniform_f(_uniTime, _time);
shader_set_uniform_f(_uniSaturation, _saturation);
shader_set_uniform_f(_uniBrightness, _brightness);
shader_set_uniform_f(_uniSection, _section);
shader_set_uniform_f(_uniMix, _mix);
draw_self();
shader_reset();

至于着色器,我们需要将饱和度和亮度传递给颜色,这将影响我们的辅助函数生成的颜色。 该部分需要乘以我们的位置以缩小范围。 我们还将获取整个纹理颜色,因此我们可以通过将纹理颜色与颜色的 RGB 转换混合来计算最终颜色。 mix 函数的最后一个参数决定了我们要添加多少第二种颜色。 这是我们最终的着色器代码:

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

uniform vec2 u_uv;
uniform float u_speed;
uniform float u_time;
uniform float u_saturation;
uniform float u_brightness;
uniform float u_section;
Uniform float u_mix;

vec3 hsv2rgb(vec3 c) 
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

void main()
{
    float pos = (v_vTexcoord.x - u_uv[0]) / (u_uv[1] - u_uv[0]);
    vec4 texColor = texture2D(gm_BaseTexture, v_vTexcoord);
    
    vec3 col = vec3(u_section * ((u_time * u_speed) + pos), u_saturation, u_brightness);
  vec4 finalCol = mix(texColor, vec4(hsv2rgb(col), texColor.a), u_mix);
    
    gl_FragColor = v_vColour * finalCol;
}

我们的最终结果是这样的!

Rainbow Shader Final

 

 

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

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