和上次使用静态画来比较手绘和码绘的区别不同,本次我们要用“动态”的图像来更深的体会艺术的不同表现方法和形式。
第一次实验我们仅仅通过对手绘和码绘的静态作品进行了比较,从感官上来看似乎区别不大。在这一篇博文中,我将以一个非常有名的图形作为线索,围绕它来展开论述。
创作的原始灵感来自于一个数学公式:
ρ=1-cosθ
what?你在逗我?
别急,来看看这个公式的曲线图案:
这个心形线是不是很眼熟,没错,就是那颗著名的“笛卡尔之心”。我觉得没什么比这个更能体现数学的和谐与美感了。
有了一颗心还不够,我们得让它“跳起来”!方法多种多样,这一次我们选择一个比较基础的,使用向量来实现。
我在此推荐3Blue1Brown的科普视频,原作者通对数学的可视化处理方法让我获益匪浅。我就是通过他的视频才认识到数学之美的,而他对于线性代数的讲解也是我灵感的重要来源。
关于向量概念的直观认识,我推荐如下视频
线性代数的本质 - 01 - 向量究竟是什么?
这是手绘的概念图,实现跳跃效果的思路是把这颗心分成有限个向量,让每个向量都从很小的一个点上往外扩张,这就形成了动的效果。
画的有点简陋抽象,别介意,知道个概念就好。
我使用Processing作为绘制的工具,其语言以java为基础,可以说是非常亲民了。
首先,我们用向量来绘制一个静态的笛卡尔之心。向量集的初始化函数如下:
void initHeartVectors(ArrayList<PVector> heart, int vectorNum, int r)
{
float x,y,a;
float crossOver;
crossOver = 1.6;
x=y=0;a=r;
pushMatrix();
translate(width/2,height/2);
//这里是核心,是笛卡尔之心在笛卡尔坐标系中的表现形式
for(float t = -PI;t < PI;t += 2*PI/vectorNum){
x = a*(crossOver*cos(t)-cos(2*t));
y = a*(crossOver*sin(t)-sin(2*t));
heart.add(new PVector(x,y));
}
popMatrix();
}
在这个函数中,我们向heart向量集中塞入了指定数量vectorNum的“点”充当心型线的向量。
将所有点连起来后的效果图如下:效果不错,接下里我们要让它跳起来。这里要引入一个概念,向量的线性插值。我们通过不断的在起点和终点之间插入中间值,让图形逐渐由一个形状变为另一个。这有点类似于动画中“关键帧”的概念。
在放上代码前,先看看跳动的效果图:
效果还不错,下面贴上实现代码(别在意函数的名字):
void drawExplosion(ArrayList<PVector> tar, ArrayList<PVector> ori, ArrayList<PVector> morph, int[] colorValue, int delayValue)
{
delay(delayValue);
float totalDistance = 0;
for (int i = 0; i < ori.size(); i++) {
PVector v1;
if (state) {
v1 = ori.get(i);
}
else {
v1 = tar.get(i);
}
//v1 = ori.get(i);
PVector v2 = morph.get(i);
//v2.lerp(v1, random(0,0.08));
v2.lerp(v1, 0.1);
totalDistance += PVector.dist(v1, v2);
}
if (totalDistance < 0.1) {
state = !state;
//background(51);
}
pushMatrix();
translate(width/2, height/2);
rotate(-PI/2);
strokeWeight(4);
beginShape();
noFill();
stroke(colorValue[0],colorValue[1],colorValue[2]);
for (PVector v : morph) {
vertex(v.x, v.y);
}
endShape(CLOSE);
popMatrix();
}
v2.lerp(v1, 0.1);这一句是实现这个效果的核心。
整个函数的作用就是让最终用于绘制的向量点集morph保存有ori向量集向tar向量集变化的中间线性插值,然后用特定颜色绘制。循环往复,笛卡尔之心就跳起来了!
你以为这样就结束了?那不行,一颗怎么够呢,我们再来6颗颜色不同的笛卡尔之心!
效果相当酷炫
接下就更有意思了,让我们现在来“折磨”一下这些向量。不仅要给他们加“噪声”,还要让它们互相冲突。所谓噪声就是把固定的线性插值改为随机的,如v2.lerp(v1, random(0,0.1)); 互相冲突在后面解释。
我们先来看看效果如何
“折磨”一颗心
“折磨”七颗心
看得出来,他们对我的做法十分不满,因此组成了一副看起来非常暴躁的图案。
说实话,这个效果是我在调试过程中偶然发现的,实现起来非常有意思,只需要这样:
void draw() {
pushMatrix();
translate(0,-60);
colorfulExplosion(heart,circle_1,morph,delayValue);
colorfulExplosion(circle_1,heart,morph,delayValue);
popMatrix();
}
解释一下,colorfulExplosion是绘制最终效果图,第一个参数是终点,第二个参数是起点。发现了吗,同时用两个函数并且第一二个参数掉位置,我们就能得到暴躁的向量图了。我推测可能是在做随机线性插值时因为一个向外扩张一个向内缩,两者都达不到到终点的判断条件,所以只能不开心的在两个图形的中间部位不停的乱跳。
(注释掉一行colorfulExplosion函数这些向量就老实了)
好啦,下面附上完整的代码,对此有兴趣的可以自己试着调一下各种参数看看有什么不同效果。
代码的效果是那张七彩的疯狂跳着的笛卡尔之心
//向量集
ArrayList<PVector> circle_1 = new ArrayList<PVector>(); //原点
ArrayList<PVector> heart = new ArrayList<PVector>(); //笛卡尔之心
ArrayList<PVector> morph = new ArrayList<PVector>(); //最终绘制用的向量集
//状态
boolean state = false;
void setup() {
size(1024, 768);
background(51);
//单个图案的向量数
int vecNum = 80;
//初始化向量集
initHeartVectors(heart,vecNum,140);
initCircleVectors(vecNum,20,circle_1);
addMorph(vecNum,morph);
}
//颜色
int[] cv1 = new int[]{255,0,0};
int[] cv2 = new int[]{255,255,0};
int[] cv3 = new int[]{0,255,0};
int[] cv4 = new int[]{0,255,255};
int[] cv5 = new int[]{0,0,255};
int[] cv6 = new int[]{255,0,255};
int[] cv7 = new int[]{255,255,255};
//延时,让效果更好看
int delayValue = 1;
float part = 0.1;
//最终效果
void colorfulExplosion(ArrayList<PVector> tar, ArrayList<PVector> ori, ArrayList<PVector> morph, int delayValue)
{
background(51);
drawExplosion(ori,tar,morph,cv1,delayValue);
drawExplosion(ori,tar,morph,cv2,delayValue);
drawExplosion(ori,tar,morph,cv3,delayValue);
drawExplosion(ori,tar,morph,cv4,delayValue);
drawExplosion(ori,tar,morph,cv5,delayValue);
drawExplosion(ori,tar,morph,cv6,delayValue);
drawExplosion(ori,tar,morph,cv7,delayValue);
}
//绘制!
void draw() {
pushMatrix();
translate(0,-60);
colorfulExplosion(heart,circle_1,morph,delayValue);
colorfulExplosion(circle_1,heart,morph,delayValue);
popMatrix();
}
//单个图案的变化效果
void drawExplosion(ArrayList<PVector> tar, ArrayList<PVector> ori, ArrayList<PVector> morph, int[] colorValue, int delayValue)
{
delay(delayValue);
float totalDistance = 0;
for (int i = 0; i < ori.size(); i++) {
PVector v1;
if (state) {
v1 = ori.get(i);
}
else {
v1 = tar.get(i);
}
//v1 = ori.get(i);
PVector v2 = morph.get(i);
v2.lerp(v1, random(0,0.1)); //核心
//v2.lerp(v1, 0.1);
totalDistance += PVector.dist(v1, v2);
}
if (totalDistance < 0.1) {
state = !state;
//background(51);
}
//真正的绘制部分
pushMatrix();
translate(width/2, height/2);
rotate(-PI/2);
strokeWeight(4);
beginShape();
noFill();
stroke(colorValue[0],colorValue[1],colorValue[2]);
for (PVector v : morph) {
vertex(v.x, v.y);
}
endShape(CLOSE);
popMatrix();
}
//初始化圆形向量集
void initCircleVectors(int vectorNum, int radius ,ArrayList<PVector> circle)
{
int count = 0;
float angleSector = 2*PI/vectorNum;
for (float angle = -PI; angle < PI; angle += angleSector) {
PVector v = PVector.fromAngle(angle);
v.mult(radius);
circle.add(v);
count++;
}
print("Circle"+count);
}
//初始化心形向量集
void initHeartVectors(ArrayList<PVector> heart, int vectorNum, int r)
{
float x,y,a;
float crossOver;
crossOver = 1.6;
x=y=0;a=r;
pushMatrix();
translate(width/2,height/2);
//rotate(-PI/2);
int count = 0;
for(float t = -PI;t < PI;t += 2*PI/vectorNum){
x = a*(crossOver*cos(t)-cos(2*t));
y = a*(crossOver*sin(t)-sin(2*t));
heart.add(new PVector(x,y));
count++;
}
print("heart"+count);
popMatrix();
}
//初始化绘制用向量集
void addMorph(int vectorNum, ArrayList<PVector> morph){
for (int i = 0; i < vectorNum; i++) {
morph.add(new PVector());
}
}
在这里提一句,不管起点图案和终点图案的形状是怎么样的,只要用于表现他们的向量集数目一致,都可以通过这样的方法来进行“跳跃”反复。
这是我另外实现的效果,大家就当有个概念吧。
对于手绘和码绘的区别,我们将从以下几个方面来探讨:载体、技法、理念、创作体验、呈现效果、局限性、应用。
载体——或者说是绘图的工具——可以是多种多样的,传统的绘画载体是纸和笔,正如我在手绘中用的素描本和铅笔;而码绘的工具就是前面提到的Processing。手绘和码绘在载体上已经有着天然的不同了。
之所以放在一起讲,是因为这俩是一个硬币的两面,无法分离。
手绘时,人的创作理念一般是感性的,非逻辑的,换句话说,就是想象力有多丰富,想要实现的画面就有多复杂,而手绘的技法——包含对各种材料和作图工具的运用以及对线条和色彩的掌控——正是用于实现这些想象的画面的。总结来讲,手绘非常自由,可以做到想到什么画什么。不过,这种自由对于动态的实现却遇到了困难——不是人想象力不够或者技法还不够扎实,而是实现动画所需要的巨量劳动实在是超出了单个人的承受极限。
码绘时,人的创作理念是被绘图软件的功能或者编程语言的语法所限制的,你的想象力被迫从跳跃式的转为逻辑式的。于是,码绘的技法就没有手绘这么“浪漫”了,你必须和一些叫做“函数”、“类”和“循环迭代”什么的看着就跟艺术没关系的玩意打交道。但是,这些不浪漫的东西是对现实世界运行逻辑的高度概括。换句话说,码绘是对已经抽象出来的事物进行再创造的方法,它更讲逻辑,而且因其高度抽象,各种各样的艺术类型都可以通过一定运行逻辑去创造。比如手绘难以实现的动态效果,码绘只需要一个循环语句就可实现。
想用手绘制作动态效果是非常繁琐的事情,说白了,就像制作手绘动画,你必须一帧帧的画下去,做一秒动画可能要画十几二十几张画,每幅画之间的区别都必须不大。这不折磨人吗?
用码绘制作动态效果非常容易,由于循环的存在,重复劳动被机器所代替。我们只要想清楚代码运作的内在逻辑就可以轻松画出不同的动态效果。
至于问我手绘码绘那个好,emmmm,我觉得手绘更像是一种作者直接的情感宣泄,就证据来说,我每次画完画后心情都会变得很愉快(* ̄︶ ̄);而敲代码,说实话敲多了会感到很枯燥,也有可能被不断地调试坏了心情,但是代码有代码的好,他能让不会画画的人有一个平台来展示自己的想法。
我有一个观点,那就是作为个体的人必须拥有一个能够展示自己的平台,不然他不是变得庸庸碌碌就是被自己无法释放的情感活活憋死。代码,正好为那些有搞艺术想法的脑洞巨大的却不会任何艺术创作技法的人提供一个平台。毕竟敲键盘是个人都会,拿起画笔画直线可不是人人都会了。
从手绘的概念图可以看到我加了不少箭头引导人们想象出“动”的效果,但是其本质上只是一副关键帧说明书而已,是静止的;但是,当我们在代码中用上循环,用上函数,用上随机数后,这些关键帧互相之间有了联系,这颗笛卡尔之心也就开始跳动了!
刚刚在技法和理念中也提到了一点,这里再做几点补充。
手绘依靠人的想象力具有无限的潜能,能够做出令人惊叹的各类绘图案,但是在这个“动”字上,手绘却显得有些乏力。
码绘依靠机器来高速处理重复劳动,因此可以轻松实现画面的动态效果,然而,想象一下,如果达芬奇先生来到现代,他能看着一个模特用代码敲出一副类似蒙娜丽莎的画吗?不管能不能,反正我认为码绘的操作太过看重逻辑了,以至于它更多的体现了作者当时的逻辑设计而非内心的真实情感。
刚刚在局限性中我提出了码绘不能很好的体现创作者情感的观点,但是我觉得码绘还是能给别人带来情感上的感动。比如我看3Blue1Brown就会感到轻松愉快,看到各种类型的分形动画或者体现无限的动画就会觉得“好厉害”。所以,不仅仅手绘能够拿来转达情感,码绘其实也可以,只不过这种情感不是人的,而是逻辑的和数学的。换言之,我认为码绘更多的给人带来的是逻辑世界和数学世界的美感。
像这一次做的动态笛卡尔之心,它就传达出了许多逻辑的和数学的美——笛卡尔之心本身,其循环往复的运动,或者其“暴躁”的跳跃。
这还是我第一次发表这么长的文章,可能会存在语言啰嗦,论证不严谨等问题,还请各位包含。我在此希望各位和我一样,能对码绘所蕴含的逻辑和数学之美所振奋,对手绘和码绘的特点有更深入的了解。
如果我的代码出现了什么问题,还清在下面留言告诉我。
0.1 用代码画画——搞艺术的学编程有啥用?
1.1 开始第一幅“码绘”——以编程作画的基本方法
以编程的思想来理解绘画—— (一)用”一笔画“表现“过程美”
3Blue1Brown 线性代数的本质
Processing Examples
手绘与码绘————静态对比