第11章 高级绘图(III)shader着色器

本章是高级绘图的最后一章,将介绍一个重磅炸弹:shader着色器。有了它,你几乎能实现你能想象到的任何绘图效果。当然,它并不是很容易用,无论在语言上还是数学思想上。

高级绘图(III)

什么是shader

shader在love中是一个工具,使用它可以直接的跟GPU进行交流,从而十分高效的绘制出各种像素级别的效果。
一般而言,你的游戏代码是运行在cpu上的,cpu最大的问题在于它是串行的,也就是它将按顺序一个一个命令运行。而对于数据吞吐比较大的运算,比如图片数据的实时修改,它的效率并不高。然而gpu(显卡)的特点是并行操作,也就是说,它可以并列的处理图片的每一个像素。由于架构上的差异,gpu特别适合并发的运算。所以,在写shader时的核心就是并行。
有了shader你可以做很多事情,比如ps中所有的图层效果:模糊,内外发光,艺术化,去色,描边,扭曲等等。还可以实现3d,实时光照,影子,反射和折射,等等光影效果。

什么是GLSL

GLSL或者OpenGL Shading Language;就是love绘图核心opengl使用的着色器语言,这是个类C的语言,习惯于lua很不相似,因此,如果你曾经没有接触过类C语言,比如C,C++,C#之类的话,可能还需要学一些类C的基本语法。love在boot启动时,会尝试对GLSL进行编译,如果编译失败将显示shader源码的行号和错误信息。如果编译成功,运行中它讲不会产生任何的除了图像显示外的输出。因此对glsl进行调试比较困难。

love的GLSL与原版的区别

float number
sampler2D Image
uniform extern
texture2D(tex, uv) Texel(tex, uv)
love几乎与原版glsl相同,只是因为与love使用的关键字以及lua关键字相协调,采用了一些关键字的修改,实际上及时你用原版的,一般情况下也不会报错。
love中使用shader代码是以string的形式传入到newShader中的。当然,你也可以单独写,然后用love.filesystem.read()来读取。目前版本也至此直接传入文件名即可。

1
shader = love.graphics.newShader( code )

love中的shader基本框架

love目前支持顶点着色器(vertex shader)和片元作色器(pixel shader),我们下面分别介绍。

顶点着色器

顶点着色器的作用是确定一个像素绘制的位置。
下面是一个标准顶点着色器代码

1
2
3
4
vec4 position(mat4 transform_projection, vec4 vertex_position)
{
return transform_projection * vertex_position;
}

参数 transform_projection是一个四维矩阵,它实际包含了mat4 TransformMatrix和mat4 ProjectionMatrix。transform指的是诸如love.graphics.transform等函数导致的坐标变换。projection指的是投影矩阵,2d引擎较为简单,是正交投影,而如果你需要3d视角的话,就要使用透视投影。但是,love2d的所有绘图相关的z值都没有接口让你去修改,因此无法进行3d化。而且没有3d所必须的深度检测和面剔除。当然你可以借助钩子来取得opengl的指针来进行其他操作。关于Mat4,它既可以表示一个状态,也可以表示一种变换。相关知识,请自行学习。
参数vertex_position指的是像素所在顶点的位置,它是个四维数,前两维是x,y,后面用不上。它是某个像素在屏幕上的绝对坐标。
返回值是经过矩阵变换后的像素屏幕绝对坐标。
使用顶点着色器能够改变图片的绘制位置,绘制结构等。

片元着色器(色彩着色器)

片元着色器的作用是确定某一个像素将被绘制为什么颜色。
下面是一个标准片元着色器代码

1
2
3
4
5
vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords )
{
vec4 texcolor = Texel(texture, texture_coords);
return texcolor * color;
}

参数color就是你所设置的颜色了,通过love.graphics.setColor
texture就是被shader绑定的drallcall所使用的贴图
texture_coords是当前像素在贴图上的相对位置,取值(0,1)
texture_coords该像素屏幕上的绝对坐标。
他的返回值是一个vec4颜色,分别是r,g,b,a.
这里有一个重要的函数Texel,它可以取一个texture贴图的任意一点颜色。所以标准的返回值就是从这个图像取对应位置的颜色。

多canvas片元着色器

对于位于多个canvas内部,(setCanvas的嵌套),可以使用下面的片元着色器来对每一层canvas进行操作。
void effects(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords)
{
love_Canvases[0] = color;
love_Canvases[1] = color + vec4(0.5);
}
这里love_Canvases是love引擎内建的一个队列,代表了每一层你所设置的canvas,与上面不同的是,它不以返回值作为颜色,而是直接对love_Canvases进行赋值。注意!你必须对每一个canvas片元颜色赋值,如果跳过了某些你设定的canvas会导致那个位置没有任何颜色,会出现系统错误。

内建变量

love为了方便shader的使用,内建了一些变量,这些变量不需要声明自动赋值,当然你可以对这些值进行更改。
mat4 TransformMatrix 变形矩阵
mat4 ProjectionMatrix 投射矩阵
mat4 TransformProjectionMatrix 变形投射矩阵
vec4 VaryingTexCoord 这个值作为片元着色器对应的贴图相对位置。
vec4 VaryingColor 这个值作为gamma矫正过的颜色传入到片元着色器
vec4 love_ScreenSize love窗体大小
vec4 VertexPosition 在顶点着色器对应参数
vec4 VertexTexCoord 片元着色器对应参数,可以在顶点着色器使用
vec4 VertexColor 片元着色器对应参数,可以在顶点着色器使用
vec4 ConstantColor 片元着色器对应参数,可以在顶点着色器使用
vec4 array love_Canvases[] canvas渲染队列

shader的外部变来的定义和赋值

shader可以从外部取得参数。需要在shader代码中使用extern关键字来声明。在love代码中,使用shader:send(name,value)的方式来赋值。name为shader代码中声明的变量名。value解释如下:
目前,love支持的可传入外部变量类型有:
整形 int 必须使用 sendInt来代替send,因为lua的数值都是浮点数。
向量 vector 包括vec2,vec3,vec4等,发送数据格式为形如{1,2,3,4};注意vector有很多代称。比如x,y,z,w;r,g,b,a等,他们和[1],[2],[3],[4]具有同等效力。
矩阵 matrix 包括mat2,mat3,mat4等,发送数据格式为

1
2
3
4
5
6
{
{1,2,3,4},
{1,2,3,4},
{1,2,3,4},
{1,2,3,4}
}

贴图 texture 可以是image对象或canvas对象,直接发送即可。
布尔值 boolean 这个就跟lua 一样了, true/false
如果你发送的是一个队列,那么就在参数里继续添加即可。如 shader:send(“arr4”,v1,v2,v3,v4)
你可以使用shader:getExternVariale来重新获取上面发送的数值。
但是,在shader代码内部,是不能更改extern关键字声明的变量的,只能作为右值使用。所以原版的extern叫uniform。^^

shader的代码过程

我们用一个案例来讲解shader的代码过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shader = love.graphics.newShader(code)
image = love.graphics.newImage("assets/res/logo.png")
timer =0
function love.update(dt)
timer = timer + dt
shader:send("time", timer)
end
function love.draw()
love.graphics.setShader(shader)
love.graphics.draw(image)
love.graphics.setShader()
print(shader:getExternVariable("time"))
end

首先定义shader,假设code代码为上面写的标准代码(无任何效果)。然后在绘制前,发送必要的变量。在draw的过程中用shader开关把draw(image)包括进去就可以了。
在shader内部,对于image的每一个像素,它都经历者下面的过程:

  1. 进入顶点着色器,根据当前的绘制矩阵来计算出绘制屏幕的位置。
  2. 进入片元着色器,根据当前像素对于image图片上的相对位置的颜色数据以及由setColor和gamma教程后的颜色合并计算,得出当前像素的颜色。
  3. 在屏幕或者绘制对象的位置用颜色进行填充。
    对于image的所有像素,这个过程(渲染管线)是并列的,没有先后顺序的,你可以理解他们同时开启,同时结束。因此,一个像素的颜色的改变不会对其他颜色改变进行任何干扰,所有的数值也都是固定的,方式也是固定的。

如果对上述讲解不理解的,没关系,因为你们尚未接触。上面的东西,你可以在看了一些例子以后回头再去理解。希望你们有时间百度GLSL相关内容,可以稍微看看shadertoy上的案例,但是那个网站的东西都比较复杂,没有必要深入。

代码时间

我们下面尝试利用shader来做出一些最简单的效果,以便让各位理解shader为何物。

设计阶段

  1. 用顶点着色器来模拟绘制位置。
  2. 使用片元着色器绘制一个矩形。
  3. 使用片元着色器加深logo图案的颜色。
  4. 使用片元着色器,使logo在屏幕的左侧正常,在屏幕的右侧为反色。
    由于上面内容在设计角度都比较简单,直接在代码中讲解。

程序阶段

绘制位置

1
2
3
4
5
6
7
8
9
10
11
local code1 = [[
extern vec2 pos;
#ifdef VERTEX
vec4 position( mat4 transform_projection, vec4 vertex_position )
{
vertex_position += pos;
return transform_projection * vertex_position;
}
#endif
]]
shader1:send("pos",{100,100})

很容易理解,但是这里注意的是要用叠加而非直接赋值,因为vertex_position并非直接对应屏幕坐标。我们导入了坐标100,100作为图片的位置,在绘制时0,0就相当于实际屏幕的100,100 因为有叠加。
绘制矩形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local code2 = [[
extern vec4 rect;
#ifdef PIXEL
vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords )
{
if (screen_coords.x < rect.x || screen_coords.x > rect.x + rect.z
|| screen_coords.y< rect.y || screen_coords.y > rect.y + rect.w) {
return vec4(0.0,0.0,0.0,0.0);
}
else
{
return vec4(1.0,1.0,0.0,1.0);
}
}
#endif
]]
shader2:send("rect",{300,100,100,100})

我们发送rect的数据为x,y,w,h,作为一个四元数,当然,也可以用number的array。在绘图阶段,我们可以看出,当屏幕坐标在矩形的范围内,我们绘制空颜色,否则绘制黄色。
色彩加深

1
2
3
4
5
6
7
8
9
#ifdef PIXEL
vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords )
{
vec4 texcolor = Texel(texture, texture_coords);
texcolor.rgb = texcolor.rgb/2;
return texcolor * color;
}
#endif
]]

实际上,就是把图片的所有r,g,b减半;就达到了颜色加深的目的了。其中texcolor.rgb的用法相当于取其中的前三位作为vec3进行计算。

区域变色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local code4 = [[
#ifdef PIXEL
vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords )
{
vec4 texcolor = Texel(texture, texture_coords);
if (screen_coords.x > 400) {
return vec4(1.0 - texcolor.r,1.0 - texcolor.g,1.0 - texcolor.b, texcolor.a );
}
else
{
return texcolor;
}
}
#endif
]]

取反色,直接用1减去相应的通道值即可,通过判断该像素是否在屏幕右侧,如果在,则取反色,否则不变。

作业

  1. 用着色器绘制一个圆环。
  2. 用这个圆环作为镂空,绘制一个图片。
  3. 简单模糊(一个像素的颜色是其周围4点颜色的平均值)
  4. 制作一个雪花屏幕(老电视那种无信号屏幕),使用rnd函数
  5. 设计一个简单的畸变,实现水波纹的效果。(就是实际点到圆心的距离和取色点偏移按正弦函数排布)

本章代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
local code1 = [[
extern vec2 pos;
#ifdef VERTEX
vec4 position( mat4 transform_projection, vec4 vertex_position )
{
vertex_position += pos;
return transform_projection * vertex_position;
}
#endif
]]
local code2 = [[
extern vec4 rect;
#ifdef PIXEL
vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords )
{
if (screen_coords.x < rect.x || screen_coords.x > rect.x + rect.z
|| screen_coords.y< rect.y || screen_coords.y > rect.y + rect.w) {
return vec4(1.0,0.0,0.0,0.0);
}
else
{
return vec4(1.0,1.0,0.0,1.0);
}
}
#endif
]]
local code3 = [[
#ifdef PIXEL
vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords )
{
vec4 texcolor = Texel(texture, texture_coords);
texcolor.rgb = texcolor.rgb/2;
return texcolor * color;
}
#endif
]]
local code4 = [[
#ifdef PIXEL
vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords )
{
vec4 texcolor = Texel(texture, texture_coords);
if (screen_coords.x > 400) {
return vec4(1.0 - texcolor.r,1.0 - texcolor.g,1.0 - texcolor.b, texcolor.a );
}
else
{
return texcolor;
}
}
#endif
]]
function love.load()
image = love.graphics.newImage("assets/res/logo.png")
shader1 = love.graphics.newShader(code1)
shader1:send("pos",{100,100})
shader2 = love.graphics.newShader(code2)
shader2:send("rect",{300,100,100,100})
shader3 = love.graphics.newShader(code3)
shader4 = love.graphics.newShader(code4)
love.graphics.setBackgroundColor(100, 100, 100, 255)
end
function love.draw()
love.graphics.setShader(shader1)
love.graphics.draw(image)
love.graphics.setShader()
love.graphics.setShader(shader2)
love.graphics.rectangle("fill", 0,0, love.graphics.getWidth(), love.graphics.getHeight())
love.graphics.setShader()
love.graphics.setShader(shader3)
love.graphics.draw(image,500,100)
love.graphics.setShader()
love.graphics.setShader(shader4)
love.graphics.draw(image,love.mouse.getX(),love.mouse.getY())
love.graphics.setShader()
end