ジャンル雑多なゲーム・ゲーム制作関連の色々な情報を取り扱っているブログ。最近はBlenderについてが中心。
[simplenoise] 自然な乱数-パーリンノイズをphina.jsと連携して使ってみる

[simplenoise] 自然な乱数-パーリンノイズをphina.jsと連携して使ってみる

[simplenoise] 自然な乱数-パーリンノイズの使い方をphina.jsと連携して使ってみる

今回の記事は、 パーリンノイズについて。ただの乱数では表現しきれない、自然物等を描くのに適した乱数です。

以前少しProcessingを触っていた時に、noise()メソッドでこのパーリンノイズを知り、何これ便利!凄い!と、感動したもので、ゲームで何か作るときに使いたいな!と目論んでいたりしました。

少し前に梶井基次郎氏追悼として制作したノベルゲーム『櫻の樹の下には』で、(使いこなせているかどうかはともかく) 『simplenoise』というJSライブラリを使わせていただきつつ、一応ゲーム内で一度使えたので、せっかくなので自分の備忘録も兼ねて、使い方と使用例を書いておきたいと思います。

準備

ライブラリ

まずは、下のページからライブラリをダウンロードしてください。

GitHub – josephg/noisejs: Javascript 2D Perlin & Simplex noise functions

その中のperlin.jsというファイルをscriptタグから読み込んでください。

htmlファイル

<script src='perlin.js'></script>

これでひとまず、パーリンノイズを使う準備はできました。canvasで描画する場合は、このままやればいいのですが、私がいつも使っているphina.jsと一緒に使わせてください。

<script src='https://cdn.jsdelivr.net/gh/phi-jp/phina.js@0.2.2/build/phina.js'></script>

もし、phina.jsをご存じでない方がいらっしゃいましたら、よろしければこちらの記事もご覧ください。

JSファイル

// グローバル領域に展開
phina.globalize();

/*
 * メインシーン
 */
phina.define("MainScene",{
  // 継承
  superClass:"DisplayScene",
  // コンストラクタ
  init: function(){
    // 親クラスの初期化
    this.superInit();
    // ここに処理を書く
  },
  // 更新
  update: function(){
  
  },
});

/*
 * メイン処理
 */
phina.main(() => {
   // アプリケーションを生成
   const app = GameApp({
     // MainSceneから開始
     startLabel: "main"
   });
   // fps表示
   //app.enableStats();
   // 実行
   app.run();
});

パーリンノイズとは

そもそも、パーリンノイズとは、


パーリンノイズは手続き的なコンテンツ生成によく使われる、非常に強力なアルゴリズムです。ゲームや、映画などの視覚媒体に特に有用です。パーリンノイズの開発者であるKen Perlinは、この最初の実装でアカデミー賞を受賞しました。彼が2002年に発表した改良パーリンノイズについて、私はこの記事で掘り下げていきます。パーリンノイズは、ゲーム開発においては、波形の類や、起伏のある素材、テクスチャなどに有用です。例えば手続き型の地形(Minecraftのような地形はパーリンノイズで生成できます)、炎のエフェクト、水、雲などにも使えます。これらのエフェクトのほとんどが2次元、3次元で使用するパーリンノイズを代表するものですが、このノイズはもちろん4次元にも使用可能です。さらに、パーリンノイズは(テラリアStarboundなどに使われている)横スクロールの地形や、手書き線のような画像描写にも使用できます。


引用元:パーリンノイズを理解する | POSTD

ようは、ある程度規則性を持った自然な乱数ってことで、理解しておけばいいと思います。

※パーリンノイズそのものの、もっと詳しい解説は、専門の他ページ様に丸投げします。

simplenoiseのメソッド

どのメソッドも、noise.〇〇〇の形で使います。

simplex2(x, y)
パーリンノイズではない普通の乱数が返されます。戻り値は、-1~1の浮動小数点であり、パーリンノイズよりも起伏が激しいものになります。
[引数]
x: 数値を入れます。この数値が小さいほど、乱数の変化のスパンが長くなります。
y: 数値を入れます。この数値が小さいほど、乱数の変化のスパンが長くなります。
simplex3(x, y, z)
パーリンノイズではない普通の乱数が返されます。戻り値は、-1~1の浮動小数点であり、パーリンノイズよりも起伏が激しいものになります。
[引数]
x: 数値を入れます。この数値が小さいほど、乱数の変化のスパンが長くなります。
y: 数値を入れます。この数値が小さいほど、乱数の変化のスパンが長くなります。
z: 数値を入れます。この数値が小さいほど、乱数の変化のスパンが長くなります。
perlin2(x, y)
パーリンノイズが返されます。戻り値は、-1~1の浮動小数点であり、普通の乱数よりもなだらかなものになります。
[引数]…上に同じなので省略
perlin3(x, y, z)
パーリンノイズが返されます。戻り値は、-1~1の浮動小数点であり、普通の乱数よりもなだらかなものになります。
[引数]…上に同じなので省略
seed(val)
乱数のシードを設定します。
[引数]
val: 0~1の浮動小数点数か、1~65536の整数を指定します。デフォルト値は0。

ただphina.jsと連携しただけのサンプル

クリックしたら、乱数のシードをリセットし、普通の乱数とパーリンノイズのシードを切り替えます。

※ダウンロードしたsimplenoiseのライブラリのフォルダに一緒に入っているdemo3d.htmlのコードを割と書き直しただけだったりするので、ここで密かにjosephg氏にソースコードをお借りした感謝(と謝罪)を申し上げさせていただきます。

ソースコード

phina.globalize();

const W = 640; // スクリーン横幅の半分
const H = 960; // スクリーン高さの半分

/*
* メインシーン
*/
phina.define("MainScene", {
    // 継承
    superClass: "DisplayScene",
    // コンストラクタ
    init: function () {
        // 親クラスの初期化
        this.superInit();

        // 背景色
        this.backgroundColor = "#000";
        // キャンバス用のRectangleShape
        this.c = RectangleShape({
            width: W,
            height: H,
            fill: null,
            stroke: null
        }).addChildTo(this).setPosition(W / 2 + 8, H / 2 + 8);
        // 普通の乱数か、パーリンノイズによる乱数か
        this.mode = 'perlin3';
        // 描画
        this.resetCanvas();

        // クリックされた場合
        this.onpointstart = () => {
            // 普通の乱数かパーリンノイズか切り替え
            this.mode = this.mode === 'perlin3' ? 'simplex3' : 'perlin3';
            // パーリンノイズのシード設定
            noise.seed(Math.random());
            // 再描画
            this.resetCanvas();
        };
    },
    resetCanvas: function () {
        let ctx = this.c.canvas.context;
        // イメージデータオブジェクトをRectangleShapeの大きさ分生成
        let image = ctx.createImageData(W, H);
        // ラスタデータ参照用
        let data = image.data;
        let f = (this.mode === 'perlin3') ? noise.perlin3 : noise.simplex3;
        for (let x = 0; x < W; x++) {
            for (let y = 0; y < H; y++) {
                let value = f(x / 100, y / 100, 0);

                value = (1 + value) * 1.1 * 128;
                // 各RGBのセルに同じ値を格納し、白黒にする
                let cell = (x + y * W) * 4;
                data[cell] = data[cell + 1] = data[cell + 2] = value;
                // アルファ値を格納する
                data[cell + 3] = 255;
            }
        }
        // イメージデータオブジェクトをRectangleShapeに格納する
        ctx.putImageData(image, 0, 0);
    }
});

/*
 * メイン処理
 */
phina.main(function () {
    // アプリケーションを生成
    const app = GameApp({
        // MainSceneから開始
        startLabel: "main"
    });
    // fps表示
    //app.enableStats();
    // 実行
    app.run();
});

カラフルなサンプル

こちらも、クリックしたら、乱数のシードをリセットし、普通の乱数とパーリンノイズのシードを切り替えます。

ソースコード

phina.globalize();

const W = 640; // スクリーン横幅の半分
const H = 960; // スクリーン高さの半分

/*
* メインシーン
*/
phina.define("MainScene", {
    // 継承
    superClass: "DisplayScene",
    // コンストラクタ
    init: function () {
        // 親クラスの初期化
        this.superInit();

        // 背景色
        this.backgroundColor = "#000";
        // キャンバス用のRectangleShape
        this.c = RectangleShape({
            width: W,
            height: H,
            fill: null,
            stroke: null
        }).addChildTo(this).setPosition(W / 2 + 8, H / 2 + 8);
        // 普通の乱数か、パーリンノイズによる乱数か
        this.mode = 'perlin3';
        // 描画
        this.resetCanvas();

        // クリックされた場合
        this.onpointstart = () => {
            // 普通の乱数かパーリンノイズか切り替え
            this.mode = this.mode === 'perlin3' ? 'simplex3' : 'perlin3';
            // パーリンノイズ設定
            noise.seed(Math.random());
            // 再描画
            this.resetCanvas();
        };
    },
    resetCanvas: function () {
        let ctx = this.c.canvas.context;
        // イメージデータオブジェクトをRectangleShapeの大きさ分生成
        let image = ctx.createImageData(W, H);
        // ラスタデータ参照用
        let data = image.data;
        let f = (this.mode === 'perlin3') ? noise.perlin3 : noise.simplex3;
        for (let x = 0; x < W; x++) {
            for (let y = 0; y < H; y++) {
                let value = f(x * 0.006, y * 0.006, 0);
                // 色相
                value = (1 + value) * 180;
                // 各RGBのセルに同じ値を格納し、白黒にする
                let cell = (x + y * W) * 4;
                // HSLをRGBへ変換
                let rgb = this.hsl2rgb(value, 80, 70);
                // red
                data[cell] = rgb.r;
                // green
                data[cell + 1] = rgb.g;
                // blue
                data[cell + 2] = rgb.b;
                // アルファ値を格納する
                data[cell + 3] = 255;
            }
        }
        // イメージデータオブジェクトをRectangleShapeに格納する
        ctx.putImageData(image, 0, 0);
    },
    // HSL色空間からRGB色空間へ変換する
    //  h(hue)       : 色相/色合い       0-360度の値
    //  s(saturation): 彩度/鮮やかさ     0-100%の値
    //  l(lightness) : 明度/明るさ       0-100%の値 
    hsl2rgb: function (h, s, l) {
        var max, min;
        var rgb = { 'r': 0, 'g': 0, 'b': 0 };

        if (h === 360) {
            h = 0;
        }

        if (l <= 49) {
            max = 2.55 * (l + l * (s / 100));
            min = 2.55 * (l - l * (s / 100));
        } else {
            max = 2.55 * (l + (100 - l) * (s / 100));
            min = 2.55 * (l - (100 - l) * (s / 100));
        }

        if (h < 60) {
            rgb.r = max;
            rgb.g = min + (max - min) * (h / 60);
            rgb.b = min;
        } else if (h >= 60 && h < 120) {
            rgb.r = min + (max - min) * ((120 - h) / 60);
            rgb.g = max;
            rgb.b = min;
        } else if (h >= 120 && h < 180) {
            rgb.r = min;
            rgb.g = max;
            rgb.b = min + (max - min) * ((h - 120) / 60);
        } else if (h >= 180 && h < 240) {
            rgb.r = min;
            rgb.g = min + (max - min) * ((240 - h) / 60);
            rgb.b = max;
        } else if (h >= 240 && h < 300) {
            rgb.r = min + (max - min) * ((h - 240) / 60);
            rgb.g = min;
            rgb.b = max;
        } else if (h >= 300 && h < 360) {
            rgb.r = max;
            rgb.g = min;
            rgb.b = min + (max - min) * ((360 - h) / 60);
        }

        rgb.r = Math.round(rgb.r);
        rgb.g = Math.round(rgb.g);
        rgb.b = Math.round(rgb.b);
        return rgb;
    }
});

/*
 * メイン処理
 */
phina.main(function () {
    // アプリケーションを生成
    const app = GameApp({
        // MainSceneから開始
        startLabel: "main"
    });
    // fps表示
    //app.enableStats();
    // 実行
    app.run();
});

※hsl2rgbのコードは、下記のページ様からコードをほぼ丸パクらせていただきましたので、深く(お詫び)感謝いたしますm(_ _)m

RGBとHSLの相互変換[色見本/サンプル付き]

参考にさせていただいたサイト・ページ一覧

ご意見・ご感想、また、ここ間違ってるよとか、もっといい方法あるよといったご指摘などございましたら、お手数ですがコメント欄やtwitterアカウントほろほろり(@_horo_horori)へお願いしますm(_ _)m

Pocket

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください