第9章 高级绘图(I)——Scissor,Stencil,Blender

本章开始的接下来几章,将介绍一下love2d的一些高级绘图概念。所谓高级绘图,也就是给你更多的工具来让画面更好看,当然并不是说会了高级绘图你就能会画出更好的效果,还需要你有一颗审美的心0.0
本章将介绍scissor,stencil,blender,colorMask以及对于绘图对象的filter和wrapMode。

高级绘图(I)

很早之前,我就向各位传递了一个概念,就是love绘图实际上就是一个动态的Photoshop绘图,你用自动化的手段来控制photoshop的各种画笔和功能。当然,有的人可能photoshop还不太熟悉,个人建议很有必要学习一下,对于基础应用来讲,ps不难,而且是图形化界面而非love的各种api,所以更容易理解和记忆。而且,对于游戏人来讲,你拿到的素材很可能需要你用ps再次处理一下;甚至你以后给你女朋友P照片,不还得需要这技能。所以,不会的赶紧去学两下。

scissor 选区

虽然这个单词直译过来叫做剪切,但是从理解来讲,个人认为叫做选区比较合适。在love中,可以像ps里面那样进行框选出一片矩形区域,来作为选区,在设定选区的内部,任何绘图的范围都限定选区之中,不会影响到选区外面。而且这个选区是屏幕坐标系的,love的世界坐标系转换对它没有影响。

1
2
3
love.graphics.setScissor( x, y, width, height )
-- draw calls
love.graphics.setScissor()

具有影响范围的一类函数,他们的格式是上面这样的,当有参数时,他们开启这个功能,当无参数调用时,关闭这个功能。我们会在后文见到类似的用法。
还有一个函数是love.graphics.intersectScissor( x, y, width, height ),当它存在于一个选区之中时,进行的操作是交选(交集选区),而如果没有选区则是跟普通选区相同。
对了,还有一个要十分注意的,千万别忘了用完就把setScissor关掉,其他类似格式的操作也一样,如果不关掉会出现比较大的错误。

stencil 蒙版

用过ps的同学,应该对蒙版的感念比较清楚,当然,对于ps而言,蒙版也是个比较高级的用法,可能一般使用也不太能用得上。下面,我简单的介绍一下蒙版。
首先用一块不透明的胶片作为蒙版的基础放到屏幕前方,此时屏幕被完全遮挡。
然后,在胶片上画图形,注意,这个画笔实际上只有画与没画两种,没有颜色之分。如果设置多层蒙版,就多加几层胶片。每层胶片也可以设置权重分值,默认为1,比如某一层的权重为5。实际上,你也可以把某个蒙版分值设为负数。
之后,我们定义一下蒙版制作的规则,比如“权重分为1的”,“权重分大于等于3的”等。然后开始用剪刀把符合规则的区域剪下来。
然后,我们用这个蒙版绘图,你的笔触和图片仅仅限制到扣开的区域,而无法影响到没有扣开的区域。
最后,我们把蒙版扔掉,就不再受到蒙版影响了。
在love中定义一个蒙版也分为四个步骤。

  1. 制作蒙版胶片

    1
    2
    3
    local function myStencilFunction()
    love.graphics.rectangle("fill", 225, 200, 350, 300)
    end
  2. 指定蒙版胶片权重分数

    1
    love.graphics.stencil(myStencilFunction, "replace", 1)
  3. 指定蒙版规则,并剪切蒙版。

    1
    love.graphics.setStencilTest("greater", 0)
  4. 抛弃蒙版。

    1
    love.graphics.setStencilTest()

我们可以在步骤3和步骤4之间加入任何绘图代码来绘制受蒙版限制的图片。同时,你也可以制作多个第1,2步来建立多重蒙版,或者在绘图中多次绘制(drawcall),每绘制一次,就相当于按此规则叠加一次。
注意,这里有个隐含的“蒙版分数”,stencil的最后一个参数是,是是否保留最后一次蒙版值(默认为不保留)。如果想建立多重蒙版,就需要在除第一张胶片外的其他胶片设置保留蒙版值来继承上一次的分数,否则将被最后一次覆盖。目前版本暂时无法取得蒙版值,以及直接设置蒙版值(可以通过canvas来保留,以后再讲,未来可能出一个mask对象也说不定)。

Blender 混色

blender的意义是混色器,也就是不同绘图(drawcall)的颜色是如何相互影响的。从PS的角度讲,我们曾经说过,每个drawcall就相当于ps中的一个图层,而混色就相当于图层间的混色关系(ps中,右下角,图层透明度旁边的下拉菜单)。

颜色值

我们之前接触过颜色值,它是由四个0,255之间的数值组成的,即 r,g,b,a 分别表示红,绿,蓝,透明度(权重)。当然,因为lua中的数值都是浮点数,所以0,255(8位)这种并不能真正的表示它内存的具体存在,所以在0.11版本的love中,颜色数值回归了它数值的本源0,1之间的浮点数。颜色还有一种表示方法叫做,HSV,色调,饱和度,明度,关于他们之间的转换方法,请自行百度。HSV的好处之一在于很容易的表达颜色色调渐变(比如彩虹)。

混色方法

混色实际上是一种基于数学的方法,而其中一个重要的角色就是alpha。笼统的说,alpha叫做透明度,但是对于其他混色方法而言,他更多的叫做权重值。而数学方法涉及两个颜色,一个是画面已经绘制的颜色dst,另一个是当前设置的颜色src 。而结果为res,它将作为下一次判断的画面颜色。下面讲解的数值均取新版的0,1范围,而非0,255范围。
alpha模式,默认。

1
2
3
4
res.r = dst.r * (1 - src.a) + src.r * src.a
res.g = dst.g * (1 - src.a) + src.g * src.a
res.b = dst.b * (1 - src.a) + src.b * src.a
res.a = dst.a * (1 - src.a) + src.a

它是默认模式,从这个方法可以很容易理解,比如红色而言,结果颜色是设置颜色透明度,和原有颜色(1-透明度)之和。
它是我们的颜色默认用法,这种用法可以保证色彩正常的混合,由alpha的权重叠加。
add模式

1
2
3
4
res.r = dst.r + (src.r * src.a)
res.g = dst.g + (src.g * src.a)
res.b = dst.b + (src.b * src.a)
res.a = dst.a

就是图层模式中的“增加”,颜色将直接叠加,高于1的保留到1。比如你想让画面看起来更亮,你仅仅需要add一个比较淡的白色即可。而不是增加原来的颜色。这里注意的是,颜色的色相是r,g,b所占的比例,而增加是一个加法,因此,如果要是增加一个纯白,会导致色彩偏差。
subtract模式

1
2
3
4
res.r = dst.r - (src.r * src.a)
res.g = dst.g - (src.g * src.a)
res.b = dst.b - (src.b * src.a)
res.a = dst.a

也叫做”减少”模式,是add的反方法。
replace模式

1
2
3
4
res.r = src.r * src.a
res.g = src.g * src.a
res.b = src.b * src.a
res.a = src.a

替换模式,也就是无视之前的绘图,直接用当前颜色覆盖。
multiply 相乘
这个模式颜色会跟之前的相乘,一般而言它用作阴影处理。
darken 变暗
取屏幕颜色和设置颜色之间更暗的(数值更低)。
lighten 变亮
取屏幕颜色和设置颜色之间更亮的(数值更高)。
screen 屏幕

1
2
3
4
res.r = dst.r * (1 - src.r) + src.r
res.g = dst.g * (1 - src.g) + src.g
res.b = dst.b * (1 - src.b) + src.b
res.a = dst.a * (1 - src.a) + src.a

屏幕混色相当于对原色进行alpha处理,对设置色进行叠加处理。

预乘

对于混色而言,还有一个是否预乘的方法,所谓预乘就是将alpha值,先分别乘以自身的r,g,b值之后,再进入混色。关于预乘的概念,请自行百度。

gamma矫正

Love提供gamma矫正的功能,不过事先你需要再conf里进行设置。关于gamma矫正请自行百度。

colorMask

色彩通道,跟ps中的色彩通道类似,它将仅允许目标通道的颜色进入绘制。而其他颜色不发生改变。(相当于alpha模式下,该通道的数值变化为0,保持底色)
它也是个开关函数。通过无参数调用来关闭该功能。

绘图对象filter

绘图对象,比如image对象(不是imageData),在进行缩放时,用filter方法来指定缩放的模式。
linear模式(默认),它将采用线性插值的方法来补全由于放大或缩小造成的像素差异。它将让图像看起来更加平滑,但是也更加模糊。对于一般的背景图,地图等,如果我们不希望图片出现马赛克,就需要使用线性模式。
nearest模式,就近模式,它直接使用其相邻的像素的颜色补全。这种模式多用于低像素素材的放大。如果使用线性模式,由于放大的倍数较高,整个图片将模糊成一大片。

绘图对象wrapMode

对于绘图对象而言,还有一种叫做环绕模式或者填充模式,它的作用是,如果我们绘制目标(quad),大于你图片大小时,对于大的部分如何处理。
clamp 拉伸模式,最边缘的像素延伸到目标区域边缘。
repeat 重复模式,以区域中心为中心,重复的填充纹理,这种填充是最右边的像素接着图像最左边的像素。
clampzero 剪切模式,仅绘制在区域中心,其他地方以无alpha的黑色填充。
mirroredrepeat 镜像重复模式,头对头,尾对尾的镜像重复,这种对于左右非连续的纹理来讲,效果比顺序重复要好。
warpMode可以用纹理来填充一个形状等等。也可以用来做滚动字幕(像素化后)。

编程时间

由于本章的知识点比较散,因此分别对学习内容进行代码演示,而不是整体了。

设计阶段

  1. 做一个走马灯效果的字符框,文字可以滚动。
  2. 利用一个左右可以拼接的地图,做一个滚动的地图效果。
  3. 利用一个像素风格的动画,拉伸4倍,同时利用平行畸变来做一个影子。
  4. 在蒙版上绘制四个圆形相互交叠的图片,并且分别展示绘制次数为1,2,3的情形。
    走马灯效果,主要利用我们学到的scissor来减去框以外的文字来实现。这里是一种比较简单的情形。
    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
    local ScrollText = {}
    function ScrollText:new(text,width,rollSpeed,wrapMode,font)
    local new = {
    text = text,
    rollSpeed = rollSpeed or 100,
    wrapMode = wrapMode or "left",
    font = font or love.graphics.getFont(),
    current = 0
    }
    new.width = new.width or new.font:getWidth(new.text)*1.5
    new.height = new.height or new.font:getHeight(new.text)
    self.__index = self
    setmetatable(new, self)
    return new
    end
    function ScrollText:update(dt)
    self.current = self.current - self.rollSpeed*dt
    if self.current < - self.width then
    self.current = 0
    end
    end
    function ScrollText:draw(x,y)
    love.graphics.setScissor(x,y,self.width,self.height)
    love.graphics.print(self.text,x+self.current,y)
    love.graphics.print(self.text,x+self.current+self.width,y)
    love.graphics.setScissor()
    love.graphics.rectangle("line", x, y, self.width, self.height)
    end

当然,有很多东西还没有实现,这里仅展现原理。
无限滚动地图,利用的是我们学的,wrapmode的repeat实现的。

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
local ScrollMap = {}
function ScrollMap:new(path,w)
local image = love.graphics.newImage(path)
image:setWrap("repeat")
local sw , sh = image:getDimensions()
w = w or sw/ 2
local new = {
image = image,
rollSpeed = rollSpeed or 100,
wrapMode = wrapMode or "left",
current = 0,
sw = sw,
sh = sh,
w = w
}
new.quad = love.graphics.newQuad(new.current, 0, w, sh, sw, sh)
self.__index = self
setmetatable(new, self)
return new
end
function ScrollMap:update(dt)
self.current = self.current + self.rollSpeed*dt
-- 这里与文字是反向的,因为参考系不同
self.quad = love.graphics.newQuad(self.current, 0, self.w, self.sh, self.sw, self.sh)
end
function ScrollMap:draw(x,y)
love.graphics.draw(self.image, self.quad ,x, y)
end

内部实现,跟滚动文字很像。只是Update的时候,对quad进行更新。因为这里的current指的是原图的x起始点,因此方向是相反的。
动画这个实际很简单了,只是用上了我们之前没用过的平行畸变。

1
2
3
4
5
6
7
catTexture:setFilter("nearest","nearest")
love.graphics.setColor(0, 0, 0, 255)
catAnim:draw(catTexture,500,300,0,4,4,22,64,1,0) --影子
love.graphics.setColor(255, 255, 255, 255)
catAnim:draw(catTexture,500,300,0,4,4,32,64)
catTexture:setFilter("linear","linear")
catAnim:draw(catTexture,600,300,0,4,4,32,64)

这里特意加上了一个效果对比图,来说明filter对图片的影响。

最后利用stencil来绘图,比较有意思,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local function myStencilFunction()
love.graphics.circle("fill", 600, 500, 30)
love.graphics.circle("fill", 600, 500+20, 30)
love.graphics.circle("fill", 600+20, 500+20, 30)
love.graphics.circle("fill", 600+20, 500, 30)
end
for i = 1, 4 do
love.graphics.stencil(myStencilFunction, "increment")
love.graphics.setStencilTest("equal", i)
love.graphics.setColor(255, 50*i, 50*i, 255)
love.graphics.rectangle("fill", 500, 400, 200, 200)
love.graphics.setStencilTest()
end

这个我们可以看到,被绘制的次数越多,颜色越淡。由于是四个图形交叠,最多被绘制4次,最少1次。
通过上面图,应该可以清楚的理解到stencil的用法。

作业:

  1. 画一个安卓机器人(或者你喜欢的其他图形),并用love的logo填充到机器人中。
  2. 仅使用stencil来绘制生化危害(生化危机)的那个图标。利用不同的绘制次数来标记颜色。
  3. 在网上找一个世界地图,制作一个能够滚动的地图,利用鼠标拖动来滚动地图。
  4. 使用stencil+wrapMode来用任意地图材质填充多边形。(需要使用到绘制多边形以及love.math.trianglate三角形化。)

本章代码:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
local font = love.graphics.newFont(20)
local text = love.graphics.newText(font,"hello world")
local quad = love.graphics.newQuad(0, 0, 300, 300, text:getWidth(),text:getHeight())
local logo = love.graphics.newImage("assets/res/logo.png")
local ScrollText = {}
function ScrollText:new(text,width,rollSpeed,wrapMode,font)
local new = {
text = text,
rollSpeed = rollSpeed or 100,
wrapMode = wrapMode or "left",
font = font or love.graphics.getFont(),
current = 0
}
new.width = new.width or new.font:getWidth(new.text)*1.5
new.height = new.height or new.font:getHeight(new.text)
self.__index = self
setmetatable(new, self)
return new
end
function ScrollText:update(dt)
self.current = self.current - self.rollSpeed*dt
if self.current < - self.width then
self.current = 0
end
end
function ScrollText:draw(x,y)
love.graphics.setScissor(x,y,self.width,self.height)
love.graphics.print(self.text,x+self.current,y)
love.graphics.print(self.text,x+self.current+self.width,y)
love.graphics.setScissor()
love.graphics.rectangle("line", x, y, self.width, self.height)
end
local ScrollMap = {}
function ScrollMap:new(path,w)
local image = love.graphics.newImage(path)
image:setWrap("repeat")
local sw , sh = image:getDimensions()
w = w or sw/ 2
local new = {
image = image,
rollSpeed = rollSpeed or 100,
wrapMode = wrapMode or "left",
current = 0,
sw = sw,
sh = sh,
w = w
}
new.quad = love.graphics.newQuad(new.current, 0, w, sh, sw, sh)
self.__index = self
setmetatable(new, self)
return new
end
function ScrollMap:update(dt)
self.current = self.current + self.rollSpeed*dt
-- 这里与文字是反向的,因为参考系不同
self.quad = love.graphics.newQuad(self.current, 0, self.w, self.sh, self.sw, self.sh)
end
function ScrollMap:draw(x,y)
love.graphics.draw(self.image, self.quad ,x, y)
end
local st
local sm
local anim8 = require "assets/anim8"
local grid64 = anim8.newGrid(64,64,1024,1024,0,0,0)
local catTexture = love.graphics.newImage("assets/res/cat_basic.png")
catTexture:setFilter("nearest","nearest")
function love.load()
love.graphics.setBackgroundColor(100, 100, 100, 255)
st = ScrollText:new("hello world")
sm = ScrollMap:new("assets/res/terrain.png")
catAnim = anim8.newAnimation(grid64("1-4",1),0.1)
end
function love.update(dt)
st:update(dt)
sm:update(dt)
catAnim:update(dt)
end
local function myStencilFunction()
love.graphics.circle("fill", 600, 500, 30)
love.graphics.circle("fill", 600, 500+20, 30)
love.graphics.circle("fill", 600+20, 500+20, 30)
love.graphics.circle("fill", 600+20, 500, 30)
end
function love.draw()
love.graphics.setColor(255, 255, 255, 255)
love.graphics.draw(text)
love.graphics.setColor(255, 255, 255, 50)
love.graphics.draw(logo,300,100,0,1,1,32,64,1,0)
love.graphics.setColor(255, 255, 255, 255)
love.graphics.draw(logo,300,100,0,1,1,32,64)
st:draw(500,100)
sm:draw(100,300)
catTexture:setFilter("nearest","nearest")
love.graphics.setColor(0, 0, 0, 255)
catAnim:draw(catTexture,500,300,0,4,4,22,64,1,0) --影子
love.graphics.setColor(255, 255, 255, 255)
catAnim:draw(catTexture,500,300,0,4,4,32,64)
catTexture:setFilter("linear","linear")
catAnim:draw(catTexture,600,300,0,4,4,32,64)
for i = 1, 4 do
love.graphics.stencil(myStencilFunction, "increment")
love.graphics.setStencilTest("equal", i)
love.graphics.setColor(255, 50*i, 50*i, 255)
love.graphics.rectangle("fill", 500, 400, 200, 200)
love.graphics.setStencilTest()
end
end