1. 程式人生 > >手繪與碼繪————讓畫面動起來!

手繪與碼繪————讓畫面動起來!

簡介

和上次使用靜態畫來比較手繪和碼繪的區別不同,本次我們要用“動態”的影象來更深的體會藝術的不同表現方法和形式。
第一次實驗我們僅僅通過對手繪和碼繪的靜態作品進行了比較,從感官上來看似乎區別不大。在這一篇博文中,我將以一個非常有名的圖形作為線索,圍繞它來展開論述。

創作靈感

創作的原始靈感來自於一個數學公式:
ρ=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
手繪與碼繪————靜態對比