写给设计师的 OF 编程指南- 打造 Photoshop笔刷系统
从这节开始,内容会变得有趣一些。不再用大篇幅去阐述基础概念,而是更多地探寻图像世界背后的构筑原理。
下面将从数字绘画的角度切入,剖析 Photoshop 上的笔刷系统,并最终在 Openframeworks 上进行模拟。
在此之前,我们在 Openframeworks 上绘图更多是使用默认的函数,这些绘图函数往往是边缘锐利,造型工整的点线面,计算机生成的痕迹非常重。若掌握以下内容,就能把数字绘画的经验复用到程序上。作为一种补充,可以创造各种肌理,模拟各类手绘效果,让画面更有呼吸感。
纸上绘画与数字绘画
在对笔刷进行剖析之前,必须要了解数字绘画到底是什么。对于很多人来说,电脑画图工具的基本印象,最早来源于 Windows 画板。
这画板麻雀虽小,但里面已经包含绘画工具的许多基本要素:橡皮,可调节大小的画笔,色板,吸管等等。在久远的年代,使用这类“朴素”工具创作的数字绘画作品,风格往往是这样的。
在此之后,伴随各种工具的进步和普及。数字绘画的发展十分迅速。现在已经能用电脑绘制出这样的作品。
(作者:Craig Mullins)
数字绘画风格之多,就不再一一列举。这里只选了概念设计领域的 Craig Mullins 作为代表。他本身在传统绘画上就有很深的造诣,并且和数字绘画结合得非常好。它在笔刷方面的研究和应用,影响了一批后来者。
当你看过足够多的数字绘画作品之后,会发现只要有时间和技巧,几乎没有用电脑画不出的画面。这种从画风到精致度的重大转变,不是由于创作者们的整体绘画水平提高了。而是源于以下两个方面。
硬件的进步
有了手绘板数位屏等工具,使得运笔可以更精确,有了压感可以捕捉下笔力度。同时显示器,CPU,GPU 的更新迭代,可以显示色彩更艳丽,分辨率更高的图片。
(Wacom手写板)
(微软的 SurfaceStudio)
软件的进步
设计理念的改变,最重大的变化我认为是强化了笔刷系统,可以更灵活地模拟各种肌理材质。
(Photoshop)
(Sai)
(Painter)
这两方面综合起来,使得数字绘画的创作效率与质量都得到了空前提高。
Photoshop 中的笔刷使用方法
这节的重心是要理解笔刷的工作原理,并最终用代码去实现。我们得先从老祖宗 Photoshop 上去取经。
Photoshop 不仅仅是设计师用来“P图”的。得益于它强大的笔刷库,对原画师,插画师而言,几乎已经成为首选的数字绘画工具。不论你之前是否有接触和使用,下面都会从零开始,介绍它的基本使用方法。
1.安装并打开 Photoshop。在左侧的工具栏选择笔刷工具(可按快捷键 B)。
2.在选择笔刷工具之后,按右键会弹出笔刷面板。点击下方的列表就能够选择不同类型的笔刷。
3.点击右上角的“小齿轮”图标,会弹出一个选项卡。上方的选项可以切换显示模式。下方选项则可以切换不同的笔刷库。PS 中自带好几套笔刷,如“基本画笔”,“混合画笔”等等。
4.选中笔刷后。就可以按住左键在画布上拖动,就能开始绘制了。下面是使用Photoshop自带笔刷中的基本画笔进行的测试。分别为“硬边机械”和“柔边机械”。硬边机械顾名思义,边缘是硬的。而柔边机械有点像喷枪,可以做一些柔和的过渡。
从外部加载笔刷
PS 内置笔刷的绘制效果其实还是有一定局限。很多人在数字绘画上进行了很多摸索,积累了大量经验。为了方便创作,他们会自行定制各种笔刷来提高创作效率。这里推荐几位笔刷作者 CraigMullins,Andrewjones,杨雪果。初学者可以从杨雪果老师制作的 blur‘s good brush 笔刷库入门,它分类细致,比较推荐。
在网上下载完笔刷后,可以通过“小齿轮”图标弹出的选项卡中,选“载入画笔”或“替换画笔”。
下面是使用外部笔刷进行的一些测试
寥寥几笔,就能产生复杂的肌理效果。
在 Photoshop 中制作笔刷
要真正地理解笔刷,仅会使用是远远不够的。要透彻理解更好的方式是自己动手制作一个。
方法非常简单。先在 PS 中新建一个图层,绘制一个基本形状。在绘制过程可以使用任何颜色,但程序最终都会进行灰度化处理。颜色越浅,笔刷的透明度就越低。当绘制的颜色是黑色,黑色部分则是不透明的。
这里尝试绘制了几个圆点,再用选区工具框选。这个区域,就会作为笔刷的基本型。
接着在菜单栏选择-编辑-自定义画笔预设。就可以开始给笔刷取名。
按确定后,自制的笔刷就会自动收录到现有笔刷库的末尾。
可以选中它进行试画
在绘制过程中,PS 会将笔刷的形状作为基本的图形单元进行重复。现在简单的一笔,就能产生多根线条。除此之外,我们还可以调出控制面板进行修改。选择菜单栏上的“窗口”-“画笔”(快捷键为 F5)。以下是面板的默认设置。
面板非常重要,所有笔刷都是根据图片素材,再结合调节面板上的参数制作而来的。
例如,在“画笔笔尖形状” 上调节间距,就会改变笔刷元素之间的距离
从中可以发现。原来我们一直以为是连续的画笔,本质是由一个个点串联而成的。当间距比较小的时,看起来就是连续的。如果调大,就会成为一个个独立散布的点。
(间距为 30 时)
除了从“画笔笔尖形状”可以通过调节间距来影响笔刷效果。下方的“形状动态”也有同样的功能。勾选它以后,便会增加更多调控参数
从面板上可以看到很多个“抖动”参数。所谓的抖动其实就是随机。大小抖动是指笔刷在绘制过程中会随机变化,角度抖动是指笔刷会随机旋转,圆度抖动是指笔刷的形状的随机拉伸。其中数值的大小会影响随机的范围。
下面是使用角度抖动后的效果
请试着自己创建一个笔刷,折腾各种参数去熟悉用法
编程前的准备工作-制作笔刷图片
相信经过上面的实验。你已经对笔刷的工作原理有基本的了解。在程序中模拟笔刷,必须要从外部载入 png 图片素材。
由于 Openframeworks 中图片的着色机制,是根据各个通道色光的放出比例来显示图片的。所以若想笔刷可以自如地设置各种颜色,必须先进行“白化处理”。通过快捷键 Ctrl + U,把明度调到最大。
图片保存时,请记得带上 alpha 通道并保存为 png 图片。
除了用手动的方式去画,你也可以“站在巨人的肩膀上”,直接提取 PS 中现有的笔刷形状。通过使用第三方程序,比如 abrViewver。就可以批量导出笔刷。
但是由于导出图片都是黑色,无法直接使用,使用前依然要在 PS 中进行白化处理
动手创建笔刷系统
基本笔刷
有了笔刷图片之后,就可以开始在 Openframeworks 中大显身手。一开始很容易就能想到,只要载入笔刷图片,再在 mouseDragged 中把绘图函数写进去,拖动鼠标就能在窗口上留下笔刷形状。
代码示例(01):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofImage brush;
float r;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(700,400);
ofBackground(255);
ofSetBackgroundAuto(false);
r = 50;
brush.load("brush.png");
brush.setAnchorPercent(0.5,0.5);
}
void ofApp::update(){
}
void ofApp::draw(){
}
void ofApp::mouseDragged(int x, int y, int button){
ofSetColor(0,150);
brush.draw(mouseX,mouseY,r * 2,r * 2);
}
运行效果:
但这样有一个非常严重的问题。当绘制速率不稳定,笔刷间的疏密就会很不均匀。为了解决这个问题,需要设定一个参数,让每个笔刷元素之间的间距成为一个定值。
间距控制
代码示例(02):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofImage brush;
float r,brushDist;
float lastX, lastY;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(700,400);
ofBackground(255);
ofSetBackgroundAuto(false);
r = 50;
brushDist = 10;
brush.load("brush.png");
brush.setAnchorPercent(0.5,0.5);
}
void ofApp::update(){
}
void ofApp::draw(){
}
void ofApp::mouseDragged(int x, int y, int button){
int brushNum;
float dist = ofDist(mouseX, mouseY, lastX, lastY);
float angle = atan2(mouseY - lastY,mouseX - lastX);
if (dist > brushDist) {
brushNum = int(dist/brushDist);
float newX,newY;
for (int i = 1; i <= brushNum; i++) {
float length = i * brushDist;
newX = lastX + cos(angle) * length;
newY = lastY + sin(angle) * length;
ofSetColor(0,150);
brush.draw(newX,newY,r * 2,r * 2);
}
lastX = newX;
lastY = newY;
}
}
void ofApp::mousePressed(int x, int y, int button){
if (lastX == 0 && lastY == 0) {
lastX = mouseX;
lastY = mouseY;
}
}
void ofApp::mouseReleased(int x, int y, int button){
lastX = 0;
lastY = 0;
}
void ofApp::keyPressed(int key){
if(key == ' '){
ofBackground(255);
}
}
运行效果:
(brushDist 为 10 时)
(brushDist 设为 20 时)
代码说明:
现在无论速度怎么变化,都会非常均匀。变量 brushDist 用于设置笔刷的间距。有了这个数值作为参照,就能决定绘制的时机。变量 lastX,lastY 用于保存上个笔刷的坐标位置。而 dist 代表上个笔刷坐标到当前鼠标坐标的距离。当 dist 小于笔刷间距 brushDist,就说明距离太靠近,不应该绘制下一个笔刷。这可以避免绘制过慢时导致笔刷过密。而当 dist 大于等于 brushDist,才会开始绘制。
变量 brushNum 代表在一帧里面绘制多少个笔刷图形,它可以避免速度过快而导致中途“丢笔刷”。做一个极端的假设,假定笔刷间距为 20。程序上一帧的笔刷绘制坐标在(0,0),下一帧鼠标坐标却瞬间跳到(0,400)。那在 mouseDragged 函数里,一帧的时间内只画一个笔刷显然是不行的。它需要在这一帧内,填补 20 个笔刷图形才可能使得前后整个笔刷造型是连贯的。具体的填补位置,通过计算运笔的角度以及结合三角函数就能得出
若是不添加 mousePressed 和 mouseReleased 函数中的语句,那么绘制一笔后,再画第二笔时。程序就会自动把第一笔提笔的地方,和第二笔下笔的地方相连。为了解决这个问题,可以在提笔时将lastX,lastY 的坐标设成 0,将它作为结束的标志。而当新的一笔正式下笔,就重新把数值替换为 mouseX,mouseY,就能避免这个情况发生
模拟角度抖动,大小抖动,圆度抖动
要实现 PS 笔刷中的角度抖动,大小抖动,圆度抖动非常简单。在上段代码中,只需要修改 mouseDragged 函数的一小段即可
角度抖动模拟
代码示例(03):
if (dist > brushDist) {
brushNum = int(dist/brushDist);
float newX,newY;
for (int i = 1; i <= brushNum; i++) {
float length = i * brushDist;
newX = lastX + cos(angle) * length;
newY = lastY + sin(angle) * length;
ofSetColor(0,150);
ofPushMatrix();
ofTranslate(newX,newY);
ofRotate(ofRandom(360));
brush.draw(0,0,r * 2,r * 2);
ofPopMatrix();
}
lastX = newX;
lastY = newY;
}
运行效果:
(角度抖动 360 度)
(角度抖动 20 度)
代码说明:
为了要让图片绕中心旋转,需要用到 ofTranslate 函数
大小抖动模拟
代码示例(04):
if (dist > brushDist) {
brushNum = int(dist/brushDist);
float newX,newY;
for (int i = 1; i <= brushNum; i++) {
float length = i * brushDist;
newX = lastX + cos(angle) * length;
newY = lastY + sin(angle) * length;
ofSetColor(0,150);
ofPushMatrix();
ofTranslate(newX,newY);
float randomSize = ofRandom(0,1);
brush.draw(0,0,r * 2 * randomSize,r * 2 * randomSize);
ofPopMatrix();
}
lastX = newX;
lastY = newY;
}
运行效果:
圆度抖动模拟
代码示例(05):
if (dist > brushDist) {
brushNum = int(dist/brushDist);
float newX,newY;
for (int i = 1; i <= brushNum; i++) {
float length = i * brushDist;
newX = lastX + cos(angle) * length;
newY = lastY + sin(angle) * length;
ofSetColor(0,150);
ofPushMatrix();
ofTranslate(newX,newY);
float randomSize = ofRandom(0.2,1);
brush.draw(0,0,r * 2,r * 2 * randomSize);
ofPopMatrix();
}
lastX = newX;
lastY = newY;
}
运行效果:
特殊笔刷-运笔方向控制角度
当我们想制作一些特殊笔刷,比如的旋转角度会跟随方向而变化。
(笔刷的基本形状)
若不希望角度不变
而是想这样
就可以用以下代码实现
代码示例(06):
if (dist > brushDist) {
brushNum = int(dist/brushDist);
float newX,newY;
for (int i = 1; i <= brushNum; i++) {
float length = i * brushDist;
newX = lastX + cos(angle) * length;
newY = lastY + sin(angle) * length;
ofSetColor(0,150);
ofPushMatrix();
ofTranslate(newX,newY);
ofRotate(ofRadToDeg(angle) + 90);
brush.draw(0,0,r * 2,r * 2 * brush.getHeight()/(float)brush.getWidth());
ofPopMatrix();
}
lastX = newX;
lastY = newY;
}
代码说明:
由于不是所有笔刷图片都为规则的正方形,通过 brushPic.height/(float)brushPic.width 可以保证图片使用原比例显示。
辉光画笔
PS 中的某些笔刷是需要在特殊的图层模式下才能发挥最佳效果。例如描绘火焰,星光的笔刷。在 Openframeworks中,也有和 PS 相对应的图层叠加模式。我们可以通过 ofEnableBlendMode 来进行设置。
代码示例(07):
if (dist > brushDist) {
brushNum = int(dist/brushDist);
float newX,newY;
for (int i = 1; i <= brushNum; i++) {
float length = i * brushDist;
newX = lastX + cos(angle) * length;
newY = lastY + sin(angle) * length;
ofEnableBlendMode(OF_BLENDMODE_ADD);
ofSetColor(ofColor::fromHsb(ofRandom(255),255,255,ofRandom(30,100)));
ofPushMatrix();
ofTranslate(newX,newY);
ofRotate(ofRandom(255));
float randomSize = ofRandom(0,1);
brush.draw(0,0,r * 2 * randomSize,r * 2 * brush.getHeight()/(float)brush.getWidth() * randomSize);
ofPopMatrix();
}
lastX = newX;
lastY = newY;
}
运行效果:
绘制前先改成深色背景,辉光的显示效果会更佳。其余部分与前面保持不变
End
原本看似复杂的笔刷,其实底层原理都非常简单。以上的实例都是使用鼠标绘制的,没有压感,而且也只用到一个笔刷形状。尽管如此,已经有足够多的变化。
如果你对效果仍不满足,Openframeworks 本身还有相关的类库支持,可以让程序支持手绘板,把压感数据传到程序中。到时,你会有更多的参数去控制画笔的深浅力度,甚至可做出一个比 PS 更加功能强劲的画笔系统。
(矢量画板实验)
本文虽然只涉及到 Photoshop 画笔面板的几个核心功能,但已经能实现 80 % 的画笔效果。这里重新用代码来模拟,不是为了重复造轮子。而是通过理解底层规则,就能创造更多新的可能性。请用好奇心去肢解一切,在图形世界中必然能有所斩获。
源文件下载链接
网盘:https://pan.baidu.com/s/1nuCICWh
Github: https://github.com/Wenzy--/OpenFrameWorksTest
Openframeworks 系列文章
资源索引