クリスマスだしp5.jsでフラクタル雪片を降らせてみたよ2018


2018年12月24日
With
クリスマスだしp5.jsでフラクタル雪片を降らせてみたよ2018 はコメントを受け付けていません

前エントリから実に5カ月半ぶりとなります。ごぶさたしておりました。

それというのも、9月にPS Storeで値引き販売された「Diablo III: Eternal Collection」にハマってしまい、この数カ月、食事と睡眠以外の時間はひたすらPS4のコントローラを握っていたせい…おかげさまで秋以降、本業のほうが忙しくなってしまったことに加え、今夏以降に作ろうとしたスケッチに、ちょっと新しい知識が必要になりそうといったあたりで、なかなかまとまった形にできていないというのが正直なところであります。

とはいえ、この間全くp5.jsでスケッチしていなかったのかいえば、そんなこともなく。たまーに思いつきで作ったやつをちょいちょいOpenProcessingあたりに上げたりはしておりましたので、もしご興味がございましたらご覧いただければ。

●Katsumi Shibata – OpenProcessing

さて、そうこうしているうちに今年もあとわずか。本日はクリスマスイブでございます。そういえば、昨年の同日には、こんなスケッチを描いていました。

●クリスマスだしp5.jsでコッホ雪片を降らせてみたよ

当時、p5.jsでフラクタル図形を描くというのにいくつかチャレンジしており、「たくさんのコッホ雪片をひらひら降らせてみたら、なんかクリスマスっぽくなんじゃね?」と思いついて作りはじめたものでした。

ただ、再帰のアルゴリズムで雪片に見えるくらいのコッホ島を複数描きながら動かすというのは結構重い処理になることに気付き、解決策が思いつかぬまま、結局、あらかじめ画像として出力しておいた雪片をアニメーションさせるだけのスケッチに仕上げ、当時はそれで満足してしまったのでした。

今年は「Chaos Game」でやってみる

…時は流れ、つい最近のことなのですが、YouTubeの「The Coding Train」で、シフマン先生が「Chaos Game」というフラクタル描画のアルゴリズムを紹介しておられました。

●Coding Challenge #123.1: Chaos Game Part 1
●Coding Challenge #123.2: Chaos Game Part 2

前後編合わせても30分弱のビデオなので、興味がある方にはぜひ全編見ていただきたいのですが、これ、ものすごく簡単な反復描画のアルゴリズムでフラクタル図形を浮かび上がらせることができます。

僕も実際にビデオを見ながら、いくつかのパラメータをいじりつつ探索していたのですが、その中でこんな図形に出くわします。

…これ、めっちゃ雪片ぽくないですか? もしかして、このアルゴリズムなら、サイズを小さめにして、反復回数もできるだけ減らすことで、昨年諦めてしまったリアルタイムで雪片を描いて降らせるアニメーションが作れるかも…。

そう思いながら、若干の試行錯誤をしつつ仕上げたスケッチがこちらになります。軽めに作ったつもりではあるのですが、できればミッドレンジ以上のCPU/GPUを載せたデバイスで見てもらえると、よりそれっぽく見えるかと思います。

●クリスマスだしp5.jsでフラクタル雪片を降らせてみたよ2018(クリックで別タブが開きます)

…雪片が回転しながら降っているように見えるでしょうか。このスケッチ、実は1フレームごとを切り取って見てしまうと、1つの雪片あたりのドット数が少ないため、こんな感じでシルエットのはっきりしない、かなりボンヤリしたものになっています。

ただ、次のフレームでは、範囲内の別の位置に再度ランダムにドットが打たれるため、アニメーションをさせた結果として、見る側の人間には、各雪片のシルエットが実際の描画よりもくっきりと認知されるようになっています。若干苦しい副次的な効果として、フレームごとのドット位置の違いで、雪片がキラキラ明滅するようにも見えると思うのですが…どうでしょう。

今年のスケッチは、各雪片をその都度描画しているので、パラメータをイジってやることで、雪片の形を変えることが可能です。たとえば、13行目にある「flakes[i].n = 12;」の数値を変えてやることで、雪片の頂点数を変えられます。

たとえば「10」にすれば、星型になりますし、

「8」にすれば、グラディウスに出てくる敵キャラ「ザブ」の亜種みたいになります(古)。

さらに「6」だと、シェルピンスキーのギャスケットっぽいのが降ってきます。

この「Chaos Game」というアルゴリズムですが、数学の世界では「反復関数系(IFS)」と呼ばれているのだそうで。…って、あれ? どっかで聞いたことあるな? …あぁ! そうだ。「バーンズリーのシダ」! あれを描画するときに使ったのが「反復関数系」でした。

Chaos Gameでは、雪片だけでなくいろんなタイプのフラクタル図形が描画できます。ひとくちに「フラクタル」といっても、同じような図形を描画するのにいろんなアプローチがあるというのが、ものすごく面白いですね。

今回のスケッチを作るにあたり「IFSでパラメータがどんな値をとった時にどんな描画になるか」を(多少)手軽に試せるスケッチを別に作っています。そちらについても近々ご紹介しようかと。

(2018/12/27追記:↑のツールを「Chaos Game Explorer」として公開しました。結構楽しいと思うので、ぜひ遊んでみてください)

それではみなさま、良いクリスマスを。

const count = 17;
let flakes = [];
let bgCol;

function setup() {
    createCanvas(windowWidth, windowHeight);
    stroke(255);
    strokeWeight(1);
    bgCol = color(20, 0, 10, 255);

    for (let i = 0; i < count; i++) {
        flakes[i] = {};
        flakes[i].n = 12;
        initFlake(i);
        flakes[i].pos.x = random(width);
        flakes[i].pos.y = random(height);
    }
}

function draw() {
    background(bgCol);
    for (let i = 0; i < flakes.length; i++) {
        push();
        translate(flakes[i].pos.x, flakes[i].pos.y);
        rotate(frameCount / flakes[i].rotspeed);
        for (let j = 0; j < 100 + 100 * (flakes[i].r / 2 - 5); j++) {
            let next = random(flakes[i].points);
            if (flakes[i].allowPp == true || next !== flakes[i].previous) {
                flakes[i].current.x = lerp(flakes[i].current.x, next.x, flakes[i].percent);
                flakes[i].current.y = lerp(flakes[i].current.y, next.y, flakes[i].percent);
                point(flakes[i].current.x, flakes[i].current.y);
            }
            flakes[i].previous = next;
        }
        pop();
        flakes[i].pos.y += flakes[i].speed;
        flakes[i].pos.x += noise(flakes[i].noiseSd) * 3 - 1.4;
        flakes[i].noiseSd += 0.005;
        if (flakes[i].pos.y > height + flakes[i].r * flakes[i].multVal) {
            initFlake(i);
        }
    }
}

function initFlake(i) {
    flakes[i].r = random(10, 25);
    flakes[i].percent = random(0.68, 0.84);
    flakes[i].multVal = random(1.1, 2.2);
    flakes[i].points = initPoints(flakes[i].n, flakes[i].r, flakes[i].multVal);;
    flakes[i].pos = createVector(random(width / 3, width / 3 * 2), -flakes[i].r * flakes[i].multVal);
    flakes[i].current = createVector();
    flakes[i].previous = createVector();
    flakes[i].allowPp = getBool(.5);
    flakes[i].speed = random(.5, 1.5);
    flakes[i].rotspeed = random(20, 150);
    flakes[i].noiseSd = random(1, 1000);
    flakes[i].points = initPoints(flakes[i].n, flakes[i].r, flakes[i].multVal);
    if (getBool(.5)) {
        flakes[i].rotspeed = -(flakes[i].rotspeed);
    }
    if (getBool(.5)) {
        flakes[i].percent = 2 - (flakes[i].percent);
    }
}

function initPoints(n, r, m) {
    let tmpArr = [];
    for (let i = 0; i < n; i++) {
        let angle = i * TWO_PI / n;
        let v = p5.Vector.fromAngle(angle);
        if (i % 2 !== 0) {
            v.mult(r);
        } else {
            v.mult(r * m);
        }
        tmpArr.push(v);
    }
    return tmpArr;
}

function getBool(p) {
    if (random() < p) {
        return true;
    } else {
        return false;
    }
}