Ark's Blog

引っ越しました→ https://blog.arkark.dev/

ようこそ

Kotlin+Processing+GLSLでマンデルブロ集合の描画

この記事はQiitaで書いた記事をはてなブログに移動させたものです→ Internet Archive

手軽にシェーダで遊べる環境が欲しくて色々と調べてたら Kotlin + Processing + GLSL という環境に行き着きました。そこで、ざっくりとした使用方法を備忘録的に記事にまとめました。

(ちなみにピクセルシェーダだけとかであれば Shadertoy がおすすめです!)

内容

  • Kotlin + Processingでシェーダを使う方法を紹介します。Processingで使うシェーダ言語はGLSLです。
  • シェーダで何を描画するか悩んだのですが、シェーダと相性が良いマンデルブロ集合を描画させます。
  • せっかくなので「マウスホイールで拡大縮小」や「マウスドラッグで平行移動」の機能も追加させます。
  • IDEIntellij IDEAを用います。

準備

1. プロジェクトの新規作成

普通にKotlin(JVM)のプロジェクトを新規に作ります。

2. Processingのライブラリの追加

メニューバーのFile > Project Structure... > ProjectSettings > LibrariesでProcessingのjarファイルを追加します。

  1. 上の"+"(New Project Library) を押し、"Java"を選択
  2. Processingの"core.jar"や"gluegen-rt-natives-macosx-universal.jar"などが入っているディレクトリ"library"を選択して"OK"をクリック。
  3. 追加したProject LibraryのNameはProcessingとかに変更しておくといいかもしれません。

最終的に↓のようになっていれば大丈夫です。(パスはProcessingをダウンロードした場所によって異なります)

f:id:ark4rk:20200326075224p:plain

  • ちなみに今回はJOGLを用いるため、core.jarだけを追加するだけではダメです。

3. GLSL Supportのインストール

pluginのGLSL Supportをインストールしておくと、GLSLのコーディングが楽になります。

  • File > Settings > Plugins でBrowse repositories...ボタンを押して検索したらそれがあるので、インストールします。

4. 各ソースファイルの作成

とりあえずファイル構造は下のような感じに

f:id:ark4rk:20200326075227p:plain

main.ktにKotlinのコードを、MandelbrotSet.fragにGLSLのコードを書いていきます。

マンデルブロ集合の描画

画面を複素平面上に見立ててマンデルブロ集合を描画していきます。

main.kt

import processing.core.PApplet
import processing.core.PVector
import processing.opengl.PShader

class MainApp: PApplet() {
    private lateinit var sd: PShader

    override fun settings() {
        size(600, 400, P3D)
    }

    override fun setup() {
        sd = loadShader("MandelbrotSet.frag")
        sd.set("resolution", width.toFloat(), height.toFloat())
    }

    override fun draw() {
        shader(sd)

        beginShape(QUADS)
        PVector(width.toFloat(), height.toFloat()).let {
            vertex(0f, it.y, 0f)
            vertex(it.x, it.y, 0f)
            vertex(it.x, 0f, 0f)
            vertex(0f, 0f, 0f)
        }
        endShape()
    }

}

fun main(args: Array<String>) = PApplet.main(MainApp().javaClass.name)

MandelbrotSet.frag

uniform vec2 resolution;

vec2 mult(vec2 c1, vec2 c2) {
    return vec2(c1.x*c2.x - c1.y*c2.y, c1.x*c2.y + c1.y*c2.x);
}

float calc(vec2 c) {
    const int MAX = 256;
    vec2 z = vec2(0);
    int i;
    for(i=0; i<MAX; i++) {
        z = mult(z, z) + c;
        if (length(z) > 512.0) break;
    }
    if (i == MAX) {
        return 0.0;
    } else {
        return mod(float(i), 16.0) / 16.0;
    }
}

void main() {
    vec2 uv = (gl_FragCoord.xy*2.0 - resolution) / min(resolution.x, resolution.y);
    uv *= 1.5;

    gl_FragColor = vec4(vec3(calc(uv)), 1.0);
}

これをコピペしたら↓のようなマンデルブロ集合が描画されるはず。

f:id:ark4rk:20200326075230p:plain

コードの説明

import processing.core.PApplet
import processing.core.PVector
import processing.opengl.PShader

ProcessingのPAppletとPShaderを使うので最初にimportします。面倒だったらimport processing.core.*みたいにしてもいいです。


private lateinit var sd: PShader

ProcessingにはPShaderというシェーダのクラスがあります。この変数sdがProcessingとシェーダの橋渡しのような役割を果たしてくれます。


sd = loadShader("MandelbrotSet.frag")

シェーダプログラムのロードです。引数にプログラムを書いたファイルのパスの文字列を指定します。今回はピクセルシェーダだけなので、引数は一つだけです。頂点シェーダも使いたい場合は

sd = loadShader("MandelbrotSet.frag", MandelbrotSet.vert")

のように1つ目の引数にピクセルシェーダ、2つ目の引数に頂点シェーダのファイルを指定します。


sd.set("resolution", width.toFloat(), height.toFloat())

vec2(width.toFloat(), height.toFloat())の値をユニフォーム変数resolutionに流し込みます。この関数setは、第1引数には変数名の文字列、第2引数以降には流しこむ値を指定します。

今回は、変数の型がvec2だったので流しこむ値はfloat型の変数2つでしたが、変数の型によっては行列やテクスチャのパスを指定することもあります。詳しくはここを参考にして下さい。


shader(sd)

sdのシェーダを描画に適用させます。


beginShape(QUADS)
PVector(width.toFloat(), height.toFloat()).let {
    vertex(0f, it.y, 0f)
    vertex(it.x, it.y, 0f)
    vertex(it.x, 0f, 0f)
    vertex(0f, 0f, 0f)
}
endShape()

頂点情報を指定しています。今回は画面全体に2つの三角ポリゴンを敷き詰めています。


シェーダを使う流れは大体こんな感じです。

マウス操作に合わせて動かす

「マウスホイールで拡大縮小」や「マウスドラッグで平行移動」の機能の追加をします。

main.kt

import processing.core.PApplet
import processing.core.PVector
import processing.event.MouseEvent //
import processing.opengl.PShader

class MainApp: PApplet() {
    private lateinit var sd: PShader
    private var zoomPower: Float = 0f // 拡大縮小の倍率の指数部分
    private var preMousePressed: Boolean = false // 1フレーム前にマウスが押されているか?
    private var mouseDiff: PVector = PVector(0f, 0f) // 平行移動のためのベクトル

    override fun settings() {
        size(600, 400, P3D)
    }

    override fun setup() {
        sd = loadShader("MandelbrotSet.frag")
        sd.set("resolution", width.toFloat(), height.toFloat())
    }

    override fun draw() {
        updateMouseDiff() //
        updateUniformVariables() //

        shader(sd)

        beginShape(QUADS)
        PVector(width.toFloat(), height.toFloat()).let {
            vertex(0f, it.y, 0f)
            vertex(it.x, it.y, 0f)
            vertex(it.x, 0f, 0f)
            vertex(0f, 0f, 0f)
        }
        endShape()

        preMousePressed = mousePressed //
    }

    // マウスドラッグによる差分ベクトルの更新
    fun updateMouseDiff() { //
        if (mousePressed && preMousePressed) { //
            PVector((mouseX-pmouseX)/width.toFloat()*2f, (mouseY-pmouseY)/height.toFloat()*2f).let { //
                mouseDiff = mouseDiff.add(it.mult(exp(zoomPower))) //
            } //
        } //
    } //

    // ユニフォーム変数の更新
    fun updateUniformVariables() { //
        sd.set("zoom", exp(zoomPower)) //
        sd.set("diff", mouseDiff.x, -mouseDiff.y) //
    } //

    // マウスホイールの変化量を蓄積
    override fun mouseWheel(e: MouseEvent) { //
        zoomPower += e.count.toFloat() / 10f //
    } //
}

fun main(args: Array<String>) = PApplet.main(MainApp().javaClass.name)

MandelbrotSet.frag

uniform vec2 resolution;
uniform float zoom; // 拡大縮小の倍率
uniform vec2 diff;  // 平行移動の差分のベクトル

vec2 mult(vec2 c1, vec2 c2) {
    return vec2(c1.x*c2.x - c1.y*c2.y, c1.x*c2.y + c1.y*c2.x);
}

float calc(vec2 c) {
    const int MAX = 256;
    vec2 z = vec2(0);
    int i;
    for(i=0; i<MAX; i++) {
        z = mult(z, z) + c;
        if (length(z) > 512.0) break;
    }
    if (i == MAX) {
        return 0.0;
    } else {
        return mod(float(i), 16.0) / 16.0;
    }
}

void main() {
    vec2 uv = (gl_FragCoord.xy*2.0 - resolution) / min(resolution.x, resolution.y);
    uv *= 1.5;

    uv *= zoom; // 拡大縮小
    uv -= diff; // 平行移動

    gl_FragColor = vec4(vec3(calc(uv)), 1.0);
}

//がついてる行が追加した行です。

f:id:ark4rk:20200326075233g:plain

おわりに

もっと詳しくProcessingにおけるShaderの使い方について知りたい場合は、公式のここにとても詳しく書かれているので参考になります。

公式のドキュメントやサンプルが充実しているのは、Processingの強みの一つだと思う。

おまけ

  • このままだと、マンデルブロ集合の境界付近の描画があまりきれいではないので、distance estimationでグラデーションをつけるときれいになります。興味があれば、、、 参考記事: distance rendering for fractals

参考記事