Common Lispでシャレオツなアートを描いてみる

はじめに

この記事は、ジェネラティブアートと呼ばれる、なんかコンピュータで生成したっぽいアーティスティックでカッコイイ画像を生成するために悪戦苦闘した、一人のプログラマの記録である。

ジェネラティブアートとは

ジェネラティブ(generative, 生成的)なアート(art, 美術作品)である。Wikipediaの当該項目から引くと、以下のようである:

コンピュータソフトウェアのアルゴリズムや数学的/機械的/無作為的自律過程によってアルゴリズム的に生成・合成・構築される芸術作品を指す。

ふむん。

さらに以下のような特徴を持っているようだ:

コンピュータの計算の自由度と計算速度を活かし、自然科学で得られた理論を実行することで、人工と自然の中間のような、統一感を持った有機的な表現を行わせる作品が多い。

わからぬ。これだけではどんなものかわからぬので、Googleの画像検索してみると、どうやらこのようなアートであるらしい:

f:id:t-sin:20171207210402p:plain

要するに、イカしたアートということだ。これは、やってみたい。

然らばやるべし。

ところで、筆者はLISPerである。このようなアートはProcessingでやるのが常套らしいが、Javaふう(というかALGOLふう)の言語とかやっていられなくて挫折した。LISPでやりたい。だから、LISPでジェネラティブなアートをキめてカッ飛ぼうという所存で臨む。

そういう記事である。

Common Lispのジェネラティブアートライブラリ: sketch

ProcessingのLispラッパーといえば、ClojureのQuilがある。Processing自体はJavaで実装されており、そのため同じJVMで動く言語であるClojureは、その機能をフルに利用できるというわけである。

ところで、筆者はCommon Lisp使いである。Clojureが嫌いというわけではない。手慣れた環境であるところのCommon Lispで書けると幸いであり、とてもハッピーであり、脳汁ドバドバなのである。と、いうことで、我が愛するCommon Lispで、Processingっぽい、Quilっぽいことをやってみるのである。

これは意地だ。ただの意地だ。

意地になってそのようなライブラリを探すと、それが案外見つかるもので、正直筆者もビビった。それがvydd氏によるsketchである。READMEはこう書かれてあり:

Sketch is a Common Lisp environment for the creation of electronic art, visual design, game prototyping, game making, computer graphics, exploration of human-computer interaction and more. It is inspired by Processing Language and shares some of the API.

求めていたものまさにこれ感が半端ではない。ぜひこいつを使わせていただこうと思う。

Sketchの導入

以降ではCommon Lispについての基本的な知識はあるものとする。ない読者については、手前味噌ながらこちらの記事「いまから始めるCommon Lisp」を読んでまずCommon Lispに入門してきてほしい。いい言語だよ。

Sketchの導入方法は以下である。また、ジェネラティブアートには欠かせない、パーリンノイズ(後述)のライブラリも併せて導入しておく。

CL-USER> (ql:quickload '(:sketch :black-tie))

Sketchのいろは

Sketchはdefsketchマクロでスケッチを定義する。そのスケッチの定義時にクラスが生成されるので、そのインスタンスを生成することで、描画プロセスがスタートする。とりあえず四角をいっこ表示するスケッチは以下のコードになる:

CL-USER> (sketch:defsketch first-sketch
             ((sketch:title "first your sketch")
              (sketch:width 600)
              (sketch:height 400))
           (sketch:rect 100 100 200 200))

このdefsketchのbody部分が、毎フレーム毎に呼ばれる描画関数となっている。このbodyを何度呼んでも結果が同じであれば同じ画像が表示され、乱数等の影響によりbodyを呼ぶ毎に数値が変わるとアニメーションになる、という感じである。

ちなみにこのコード、SBCLのREPLに突っ込むとたくさん警告が出るが、無視してほしい。titleとかwidthとかheightとかの未使用について怒られるのだ。ちゃんと(declare (ignorable ...))してほしいものである。

こいつを実際に表示するには、以下のようにする:

CL-USER> (make-instance 'first-sketch)

すると、こういうウィンドウが表示される。

f:id:t-sin:20171207210437p:plain

なにをやっているかは、コードを見てだいたい察せることと思う。(x, y)座標が(100, 100)を始点として、幅と高さが200の四角を描いているだけである。

パーリンノイズを可視化する

ジェネラティブアートでは、人工的な部分と自然な部分の中間を狙うものであるらしい。そこで、ここではランダムなんだけど自然な感じを表現するための、パーリンノイズを可視化してみようと思う。

まず、ただの乱数を点の輝度としたものを見てほしい:

CL-USER> (flet ((noise (x y)
                  (random 1.0)))
           (sketch:defsketch first-sketch
               ((sketch:title "first your sketch")
                (sketch:width 300)
                (sketch:height 300))
             (dotimes (x 300)
               (dotimes (y 300)
                 (sketch:with-pen (sketch:make-pen :fill nil
                                                   :stroke (sketch:hsb 0 0 (noise x y)))
                   (sketch:point x y))))))

f:id:t-sin:20171207210450p:plain

なんというか、砂嵐。ランダムすぎてガチのノイズであって、カオス以外の何者でもない。つらい。

一方で、ケン・パーリンが開発し伝説のディズニー映画『TRON』で使用したというこのノイズ関数は、だいぶ自然である、らしい。どんなノイズなのかを可視化すると、こんな感じ:

CL-USER> (flet ((noise (x y)
                  (normalize (black-tie:perlin-noise (* x 0.1) (* y 0.1) 0) -1 1)))
           (sketch:defsketch first-sketch
               ((sketch:title "first your sketch")
                (sketch:width 300)
                (sketch:height 300))
             (dotimes (x 300)
               (dotimes (y 300)
                 (sketch:with-pen (sketch:make-pen :fill nil
                                                   :stroke (sketch:hsb 0 0 (noise x y)))
                   (sketch:point x y))))))

f:id:t-sin:20171207210501p:plain

まだ自然っぽく見えないけど、ランダムだけどなだらかであるので、これをテクスチャとかに利用したりすると、自然なものができあがるっぽい。

これを使ってさっそくジェネラティブアートしてみる。簡単には、このノイズを拡大して、円の半径として可視化してみると、それっぽいことがわかった:

CL-USER> (sketch:defsketch mysketch
             ((sketch:title "perlin circle")
              (sketch:width 600)
              (sketch:height 400))
           (sketch:with-pen (sketch:make-pen :fill (sketch:rgb 0 0.1 0.1))
             (sketch:rect 0 0 600 400))
           (let ((interval 17)
                 (noise-factor 0.2))
             (dotimes (x (ceiling (/ 600 interval)))
               (dotimes (y (ceiling (/ 400 interval)))
                 (sketch:with-pen (sketch:make-pen :fill nil :stroke (sketch:rgb 0.2 0.6 0.9))
                   (sketch:circle (* x interval) (* y interval)
                           (+ (/ interval 4)
                              (* 10 (black-tie:perlin-noise (* x noise-factor) (* y noise-factor) 0)))))))))

f:id:t-sin:20171207210513p:plain

なんかシャレオツっぽい。アニメーション(円の半径が変わるとか)しておいてスクリーンセーバーにすると、なんかよさげな気がする。そうするのは読者への課題とする。

もっとジェネラティブっぽさを求めて

もっとノイズを使うといいって本に書いてあった([普及版]ジェネラティブ・アート―Processingによる実践ガイド調べ)ので、もっとランダムやノイズを取り込んでいこうと思う。

たとえば線を引く行為にランダムやノイズを導入して、さらにベジエ曲線にしてみるというのはどうだろう。始点と終点が与えられたとき、その間に制御点を設け、それらをランダマイズして描画するのだ。どうせなら、それを複数回してみるとそれっぽいのでは。

CL-USER> (defun yvalue (sx sy ex ey x)
           (let ((delta (/ (- ey sy) (- ex sx)))
                 (y0 (/ (- (* sx ey) (* sy ex)) (- sx ex))))
             (+ (* x delta) y0)))

CL-USER> (defun make-control-points (sx sy ex ey)
           (let* ((xlis (let (nums)
                    (dotimes (n 2)
                      (setf nums (cons (- ex sx) nums)))
                      (append (list sx)
                         (sort nums #'<)
                         (list ex))))
                  (ylis (loop
                          :for x :in xlis
                          :collect (+ (yvalue sx sy ex ey x) (- (random 150) 75)))))
             (loop
               :for x :in xlis
               :for y :in ylis
               :nconc (list x y))))

CL-USER> (let* ((+width+ 600)
                (+height+ 400)
                (sx (* +width+ 0))
                (sy (* +height+ 0.7))
                (ex (* +width+ 1.2))
                (ey (* +height+ 0.4))
                (rs (make-random-state)))
           (sketch:defsketch mysketch
               ((sketch:title "flowline")
                (sketch:width +width+)
                (sketch:height +height+)
                (sketch:copy-pixels t))
             (sketch:with-pen (sketch:make-pen :fill (sketch:hsb 0.6 0.9 0.15))
               (sketch:rect 0 0 +width+ +height+))
             (sketch:with-pen (sketch:make-pen :fill nil :stroke (sketch:hsb 0.374 0.4 0.8 0.04))
               (let ((*random-state* (make-random-state rs)))
                 (loop
                    :for n :from 0 :upto 1000
                    :do (apply #'sketch:bezier (make-control-points sx sy ex ey)))))))

f:id:t-sin:20171207210524p:plain

納豆の糸みたいで、それっぽい雰囲気がある。線の数を1000本にして、それぞれのアルファ値を0.15と少なめにしたため、ちょっとランダマイズしただけの線の集合にジェネラティブアートっぽい雰囲気が出ている。どうも本を見るに、ジェネラティブアートとは、多数のオブジェクトの相互作用っぽさがあれば、それっぽくなるものであるらしい。

Sketchの問題点

しかしながら、ここいらですでになんか問題を感じつつあるのである。

生成した画像を保存できない

ここまででわかる通り、sketchの生成する画像を保存するのには、OSのスクリーンショット機能を利用している。ほかに方法が用意されていないからだ。なので、例えば4000×4000の画像を生成したとして、それを保存するには4000x4000以上の解像度を持つディスプレイがなければならない。

一方でProcessingには画像を保存する関数があるので、そのような問題はない。

色の合成方法を指定できない

色の合成モードには色々なものがあるが、そのうちアルファブレンディングのみをsketchでは利用することができる。たとえば、発光した感じを表現するのにしばしば用いられる加算合成を利用することができない。

一方でProcessingには合成モードを指定する機能があるので、そのような点で困ることはない。

おわりに

この記事では、Common Lispでもジェネラティブアートをさくっと作ることができることを示した。

Common Lispでもsketchというライブラリを使えば、点や線を描画したり、それらにインタラクションしたりするプログラムを書くことができる。ただ、生成画像の保存ができないことや色のブレンドモードを指定できないことなど、問題もある

これらの機能が欲しいとなったときは、ClojureのQuilであるなり本家Processingであるなりを利用したほうがよいように思えた。あるいは、自分でProcessingライクなライブラリを実装するとか……。