Phaser3 : Pipelineを使う

今回はカスタムパイプラインを使ってスプライトにシェーダを適応してみます。

カスタムパイプラインを宣言する

こちらのサンプルなどでカスタムパイプラインが使われていますが、typescriptではだいぶ書き方が違うようです。
まずはパイプライン用のオブジェクトを準備します。

public pipeline: Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline;

new Phaser.Renderer.WebGL.Pipelines.TextureTintPipelineでパイプラインオブジェクトを作成します。
gameにthis.game、rendererにthis.game.rendererをわたし、fragShaderにシェーダの実装を書きます。

//パイプラインを作成する
this.pipeline= new Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline({
    game: this.game,
    renderer: this.game.renderer,
    fragShader: [
        "precision mediump float;",

        "uniform float     time;",
        "uniform vec2      resolution;",
        "uniform sampler2D uMainSampler;",
        "varying vec2 outTexCoord;",

        "void main( void ) {",

        "vec2 uv = outTexCoord;",
        "//uv.y *= -1.0;",
        "uv.y += (sin((uv.x + (time * 0.5)) * 10.0) * 0.1) + (sin((uv.x + (time * 0.2)) * 32.0) * 0.01);",
        "vec4 texColor = texture2D(uMainSampler, uv);",
        "gl_FragColor = texColor;",

        "}"
    ].join('\n')
           
});


次にrendererにカスタムパイプラインを追加します。
rendererはPhaser.Renderer.Canvas.CanvasRendererとPhaser.Renderer.WebGL.WebGLRendererの共有型なのですが、Phaser.Renderer.Canvas.CanvasRendererにはaddpipeline関数がないようで、Phaser.Renderer.WebGL.WebGLRendererにキャストしなければなりませんでした。

setFloat2でパイプラインの初期値を設定し、スプライトにパイプラインを適応します。
スプライトはこちらのlayer9を使用しました。

edermunizz.itch.io

//rendererにカスタムパイプラインを登録する
(this.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer).addPipeline("custom", this.pipeline)

//シェーダの値を設定する
this.pipeline.setFloat2('uResolution', this.game.config.width as number, this.game.config.height as number);

let forest = this.add.sprite(this.sys.canvas.width / 2, 200, "forest");

//スプライトにパイプラインを設定する
forest.setPipeline("custom");

最後にupdateでパイプラインのtimeを変動させます。

update() {
    this.pipeline.setFloat1('time', this.t);

    this.t += 0.005
}

実行結果がこちらです。 f:id:Phaser_3:20190118152154g:plain 波打つようなエフェクトが適応されています。
今回のソースはこちらです。

/// <reference path="../app.ts" />

namespace MyGame {



    export class MyScene1 extends Phaser.Scene {

        public pipeline: Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline;
        public t: number = 0;
        constructor() {
            super({ key: 'MyScene1', active: false });
        }

        preload() {
            //ノーマルマップ
            this.load.image("rect", "assets/images/rect3.png");
            this.load.image("forest", "assets/images/Layer_0000_9.png");
        }

        create() {

            //パイプラインを作成する
            this.pipeline= new Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline({
                game: this.game,
                renderer: this.game.renderer,
                fragShader: [
                    "precision mediump float;",

                    "uniform float     time;",
                    "uniform vec2      resolution;",
                    "uniform sampler2D uMainSampler;",
                    "varying vec2 outTexCoord;",

                    "void main( void ) {",

                    "vec2 uv = outTexCoord;",
                    "//uv.y *= -1.0;",
                    "uv.y += (sin((uv.x + (time * 0.5)) * 10.0) * 0.1) + (sin((uv.x + (time * 0.2)) * 32.0) * 0.01);",
                    "vec4 texColor = texture2D(uMainSampler, uv);",
                    "gl_FragColor = texColor;",

                    "}"
                ].join('\n')
            

            });

            //シェーダの値を設定する
            this.pipeline.setFloat2('uResolution', this.game.config.width as number, this.game.config.height as number);
             
            //rendererにカスタムパイプラインを登録する
            (this.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer).addPipeline("custom", this.pipeline)

            let forest = this.add.sprite(this.sys.canvas.width / 2, 200, "forest");

            //スプライトにパイプラインを設定する
            forest.setPipeline("custom");

        }

        update() {
            this.pipeline.setFloat1('time', this.t);

            this.t += 0.005
        }
    }
}

Phaser3: Lightを使う 

今回はLightを使ってみようと思います。

スプライトでノーマルマッピング

Phaser3のスプライトはノーマルマッピングをサポートしており、lightを当てたときにスプライトの凹凸を表現できます。

GIMPでノーマルマップを作成する

まずはスプライト用のノーマルマップを作成します。

SpriteIlluminator - Normalmap Editor for 2d Dynamic Lightingが高機能で使いやすそうなのですが、フリー版ではノーマルマップの出力ができないのでGIMPのノーマルマッププラグインを使います。
こちらの画像をもとに、 f:id:Phaser_3:20190117134945p:plain こちらのノーマルマップを作成しました。 f:id:Phaser_3:20190117135011p:plain

Phaser3に取り込む

ノーマルマップ付きのスプライトを作成する

まずはノーマルマップ付きのスプライトを作成します。
[スプライト、ノーマルマップ]の形でload.imageを実行するとノーマルマップ付きのテクスチャデータが作成されます。

//ノーマルマップ
this.load.image("rect", ["assets/images/rect3.png", "assets/images/rect2_normal.png"]);


createでスプライトを作成し、setPipelineでlight2Dを指定します。
light2Dはphaserに組み込まれているシェーダのようです。

create() {

    let rect = this.add.sprite(this.sys.canvas.width / 2, this.sys.canvas.height / 2,  "rect");

    //light2dのパイプライン
    rect.setPipeline("Light2D")

}

次にlightを追加し、マウスに追従させるようにします。

        create() {

            let rect = this.add.sprite(this.sys.canvas.width / 2, this.sys.canvas.height / 2,  "rect");

            //light2dのパイプライン
            rect.setPipeline("Light2D")

            //ライトを追加
            let light = this.lights.addLight(0, 0, 200);

            this.lights.enable().setAmbientColor(0x555555);

            this.input.on('pointermove', function (pointer) {
                light.x = pointer.x;
                light.y = pointer.y;
            });
        }

実行結果がこちらです。 f:id:Phaser_3:20190117135733g:plain すこしわかりにくいので、灰色のスプライトにライティングを行ってみました。 f:id:Phaser_3:20190117140015p:plain f:id:Phaser_3:20190117140031g:plain

テキストの部分が浮かび上がっている様子がはっきりわかります。


今回のソースはこちらです。

/// <reference path="../app.ts" />

namespace MyGame {


    export class MyScene1 extends Phaser.Scene {

        constructor() {
            super({ key: 'MyScene1', active: false });
        }

        preload() {
            //ノーマルマップ
            this.load.image("rect", ["assets/images/rect3.png", "assets/images/rect2_normal.png"]);
        }

        create() {

            let rect = this.add.sprite(this.sys.canvas.width / 2, this.sys.canvas.height / 2,  "rect");

            //light2dのパイプライン
            rect.setPipeline("Light2D")

            //ライトを追加
            let light = this.lights.addLight(0, 0, 200);

            this.lights.enable().setAmbientColor(0x555555);

            this.input.on('pointermove', function (pointer) {
                light.x = pointer.x;
                light.y = pointer.y;
            });
        }
        update() {
        }
    }
}

Phaser 3 : depthで重なり順の制御

今回はDepthを使って重なり順を制御してみます。

Depth

Phaser2ではグループそのものとグループ内でのスプライトをソートすることによって重なり順を制御していましたが、Phaser3からはDepthを使って簡単に重なり順制御を行えるようになりました。


まずy座標とDepthを連動させたスプライトを作ります。

for (let i = 0; i < 10; i++) {
    let block= this.add.sprite(300, 100 + i * 32, "block");
    block.setTint(0x111111 * i);
    block.depth = block.y;
}

次にy座標に応じてDepthを変えるプレイヤーオブジェクトを作成します。

create() {
    this.player = this.add.graphics({ fillStyle: { color: 0xaa1111 } });
    this.player.fillRectShape(new Phaser.Geom.Rectangle(280, 0, 40, 40));
~~~
    this.input.keyboard.on('keydown_UP', ()=> {
        this.player.y -= 5;
    });

    this.input.keyboard.on('keydown_DOWN', ()=> {
        this.player.y += 5;

    });

}

update() {
    this.player.depth = this.player.y+20;
}

実行結果がこちらです。 f:id:Phaser_3:20190116140114g:plain

移動しながら重なり順が切り替わっているのがわかると思います。
Phaser2では重ね順に相当する変数を自分でつけてsortを実行する必要があったのですが、phaser3ではdepthを変更するだけで自動で重なり順を変えて描画してくれます。
今回のソースはこちらです。

/// <reference path="../app.ts" />

namespace MyGame {


    export class MyScene1 extends Phaser.Scene {

        //グラフィックオブジェクトを用意
        public player: Phaser.GameObjects.Graphics;

        constructor() {
            super({ key: 'MyScene1', active: false });
        }

        preload() {
            this.load.image("block", "assets/images/block1.png");
        }

        create() {
            this.player = this.add.graphics({ fillStyle: { color: 0xaa1111 } });
            this.player.fillRectShape(new Phaser.Geom.Rectangle(280, 0, 40, 40));

            this.cameras.main.setBackgroundColor(0xffffff);

            for (let i = 0; i < 10; i++) {
                let block= this.add.sprite(300, 100 + i * 32, "block");
                block.setTint(0x111111 * i);
                block.depth = block.y;
            }

            this.input.keyboard.on('keydown_UP', ()=> {
                this.player.y -= 5;
            });

            this.input.keyboard.on('keydown_DOWN', ()=> {
                this.player.y += 5;
            });
        }
        update() {
            this.player.depth = this.player.y+20;
        }
    }
}

Phaser3 :凹面の当たり判定を自動で生成してみる⑥ 物理ボディの生成

前回に引き続きメッシュの自動生成を行っていきます。

vertsを設定し物理ボディの作成

前回三角形分割を行いましたが、物理ボディ用の頂点は三角形分割の必要がないらしく、輪郭をそのまま結合して渡すと物理ボディが作成できました。

//取得した輪郭からverticesを作る
let verts = ""
for (let p of this.border) {
    verts += (p.x+x) + " " + (p.y+ y)+ " "
}

this.matter.world.setBounds();

var poly = this.add.polygon(400, 300, verts, 0x0000ff, 0.2);

this.matter.add.gameObject(poly, { shape: { type: 'fromVerts', verts: verts, flagInternal: false } });

いままでのコードをまとめて物理ボディ作成用のCreateVerts関数を作りました。

        public CreateVerts(x: number, y: number, texture: string) {
            //phaserのテクスチャマネージャからsourceimageを取得する
            let source = this.textures.get(texture).getSourceImage();;

            //canvasを作成しテクスチャを描画
            this.canvas = this.textures.createCanvas(texture+"_canvas", source.width, source.height);
            this.canvas.draw(0, 0, source as HTMLImageElement);

            //imagedataを取得
            this.imageData = this.canvas.imageData;

            this.pixels = new Array();
            this.pixelsCopy = new Array();
            //ピクセルを配列に入れる
            for (let i = 0; i < this.canvas.imageData.data.length; i += 4) {
                let p = [this.canvas.imageData.data[i], this.canvas.imageData.data[i + 1], this.canvas.imageData.data[i + 2], this.canvas.imageData.data[i + 3]]
                this.pixels.push(p);
            }

            //ピクセルを配列にいれなおす
            for (let i = 0; i < this.imageData.height; i++) {
                this.pixelsCopy[i] = new Array();
                for (let j = 0; j < this.imageData.width; j++) {
                    this.pixelsCopy[i].push(0)
                }
            }

            let xx = 0;
            let yy = 0;

            //アルファ値の取得
            for (let i = 0; i < this.pixels.length; i++) {

                this.pixelsCopy[yy][xx] = this.pixels[i][3]
                xx++;

                if (xx === source.width) {
                    xx = 0;
                    yy++;
                }
            }

            let origin = 0;
            let tmp = new Phaser.Math.Vector2(0);

            while (this.pixels[origin][3] === 0) {
                origin++;
            }

            let mov = new Phaser.Math.Vector2(0, 0);
            tmp = this.GetBoarder(new Phaser.Math.Vector2(origin, 0), mov);

            this.border.push(new Phaser.Math.Vector2(origin, 0))

            let prevmov = new Phaser.Math.Vector2(mov.x, mov.y);

            while (!(tmp.x === origin && tmp.y === 0)) {
                tmp = this.GetBoarder(tmp, mov);
                if (!(prevmov.x === mov.x && prevmov.y === mov.y)) {
                    this.border.push(new Phaser.Math.Vector2(tmp.x, tmp.y));
                }
                prevmov.set(mov.x, mov.y);
            }

            //取得した輪郭からverticesを作る
            let verts = ""
            for (let p of this.border) {
                verts += (p.x+x) + " " + (p.y+ y)+ " "
            }

            this.matter.world.setBounds();

            var poly = this.add.polygon(400, 300, verts, 0x0000ff, 0.2);

            this.matter.add.gameObject(poly, { shape: { type: 'fromVerts', verts: verts, flagInternal: false } });

        }

実行結果がこちらです。 f:id:Phaser_3:20190115145158g:plain f:id:Phaser_3:20190115145211g:plain f:id:Phaser_3:20190115145621g:plain f:id:Phaser_3:20190115145634g:plain

物理ボディを自動生成できているのがわかります。

今回のソースはこちらです。

/// <reference path="../app.ts" />

namespace MyGame {

    interface XY {
        x: number;
        y: number;
    }

    export class Vert {
        public point: Phaser.Math.Vector2;
        public next1: Phaser.Math.Vector2;
        public next2: Phaser.Math.Vector2;
        constructor(p: Phaser.Math.Vector2, n1: Phaser.Math.Vector2, n2: Phaser.Math.Vector2, ) {
            this.point = p;
            this.next1 = n1;
            this.next2 = n2;
        }
    }

    export class MyScene1 extends Phaser.Scene {

        //グラフィックオブジェクトを用意
        public player: Phaser.GameObjects.Image;
        public beamGroup: Phaser.GameObjects.Group;

        public graphics: Phaser.GameObjects.Graphics;
        public points: Phaser.Math.Vector2[] = [];
        public closepath: Phaser.Math.Vector2[] = [];
        public pixels: number[][];
        public imageData: ImageData;
        public pixelsCopy: number[][];

        public border: Phaser.Math.Vector2[] = [];
        public verts: { [key: string]: Phaser.Math.Vector2[] } = {};
        public canvas: Phaser.Textures.CanvasTexture;

        constructor() {
            super({ key: 'MyScene1', active: false });
        }

        preload() {
            this.load.image("bar", "assets/images/bar.png");
            this.load.image("block1", "assets/images/block1.png");
            this.load.image("block2", "assets/images/block2.png");
            this.load.image("block3", "assets/images/block3.png");
            this.load.image("rect", "assets/images/rect32.png");
        }

        public CreateVerts(x: number, y: number, texture: string) {
            this.points = [];
            this.closepath = [];
            this.border = [];

            //phaserのテクスチャマネージャからsourceimageを取得する
            let source = this.textures.get(texture).getSourceImage();;

            if (this.verts[texture] === undefined) {
                //canvasを作成しテクスチャを描画
                this.canvas = this.textures.createCanvas(texture + "_canvas", source.width, source.height);
                this.canvas.clear()
                this.canvas.draw(0, 0, source as HTMLImageElement);

                //imagedataを取得
                this.imageData = this.canvas.imageData;
                this.pixels = new Array();
                this.pixelsCopy = new Array();
                //ピクセルを配列に入れる
                for (let i = 0; i < this.canvas.imageData.data.length; i += 4) {
                    let p = [this.canvas.imageData.data[i], this.canvas.imageData.data[i + 1], this.canvas.imageData.data[i + 2], this.canvas.imageData.data[i + 3]]
                    this.pixels.push(p);
                }

                //ピクセルを配列にいれなおす
                for (let i = 0; i < this.imageData.height; i++) {
                    this.pixelsCopy[i] = new Array();
                    for (let j = 0; j < this.imageData.width; j++) {
                        this.pixelsCopy[i].push(0)
                    }
                }

                let xx = 0;
                let yy = 0;

                //アルファ値の取得
                for (let i = 0; i < this.pixels.length; i++) {

                    this.pixelsCopy[yy][xx] = this.pixels[i][3]
                    xx++;

                    if (xx === source.width) {
                        xx = 0;
                        yy++;
                    }
                }

                let origin = 0;
                let tmp = new Phaser.Math.Vector2(0);

                while (this.pixels[origin][3] === 0) {
                    origin++;
                }

                let mov = new Phaser.Math.Vector2(0, 0);
                tmp = this.GetBoarder(new Phaser.Math.Vector2(origin, 0), mov);

                this.border.push(new Phaser.Math.Vector2(origin, 0))

                let prevmov = new Phaser.Math.Vector2(mov.x, mov.y);

                while (!(tmp.x === origin && tmp.y === 0)) {
                    tmp = this.GetBoarder(tmp, mov);
                    if (!(prevmov.x === mov.x && prevmov.y === mov.y)) {
                        this.border.push(new Phaser.Math.Vector2(tmp.x, tmp.y));
                    }
                    prevmov.set(mov.x, mov.y);
                }

                //取得した輪郭からverticesを作る
                let verts = ""
                for (let p of this.border) {
                    verts += (p.x + x) + " " + (p.y + y) + " "
                }


                var poly = this.add.polygon(x, y, verts, 0x0000ff, 0.2);

                this.matter.add.gameObject(poly, { shape: { type: 'fromVerts', verts: verts, flagInternal: false } });
                this.verts[texture] = this.border;

            } else {
                let verts = ""
                for (let p of this.verts[texture]) {
                    verts += (p.x + x) + " " + (p.y + y) + " "
                }


                var poly = this.add.polygon(x, y, verts, 0x0000ff, 0.2);

                this.matter.add.gameObject(poly, { shape: { type: 'fromVerts', verts: verts, flagInternal: false } });

            }

        }

        create() {
            this.matter.world.setBounds();

            this.cameras.main.setBackgroundColor(0xffffff);

            this.graphics = this.add.graphics();
            this.graphics.fillStyle(0x000000);


        }

        update() {
            let rand = ["bar","block1","block2","block3","rect"]
            if (this.input.activePointer.isDown) {
                this.CreateVerts(Phaser.Math.Between(0, this.sys.canvas.width), Phaser.Math.Between(0, this.sys.canvas.height/2), rand[Phaser.Math.FloorTo(Phaser.Math.Between(0,rand.length))])

            }
        }
        public GetBoarder(v: Phaser.Math.Vector2, m: Phaser.Math.Vector2): Phaser.Math.Vector2 {
            let r: Phaser.Math.Vector2 = null;

            //左下のピクセルから調べていく
            let dir1 = () => {
                if (v.x > 0 && v.y < this.imageData.height - 1) {
                    if (this.pixelsCopy[v.y + 1][v.x - 1] !== 0) {
                        m.set(-1, 1);
                        return new Phaser.Math.Vector2(v.x - 1, v.y + 1);
                    }
                }
                return null;
            }

            //下
            let dir2 = () => {
                if (v.y < this.imageData.height - 1 && !(m.x == 0 && m.y == -1)) {
                    if (this.pixelsCopy[v.y + 1][v.x] !== 0) {
                        m.set(0, 1);
                        return new Phaser.Math.Vector2(v.x, v.y + 1);
                    }
                }
                return null;
            }

            //右下
            let dir3 = () => {
                if (v.x < this.imageData.width - 1 && v.y < this.imageData.height - 1) {
                    if (this.pixelsCopy[v.y + 1][v.x + 1] !== 0) {
                        m.set(1, 1);
                        return new Phaser.Math.Vector2(v.x + 1, v.y + 1);
                    }
                }
                return null;

            }

            //右
            let dir4 = () => {
                if (v.x < this.imageData.width - 1) {
                    if (this.pixelsCopy[v.y][v.x + 1] !== 0) {
                        m.set(1, 0);
                        return new Phaser.Math.Vector2(v.x + 1, v.y);
                    }
                }
                return null;
            }

            //右上
            let dir5 = () => {
                if (v.x < this.imageData.width - 1 && v.y > 0) {
                    if (this.pixelsCopy[v.y - 1][v.x + 1] !== 0) {
                        m.set(1, -1);
                        return new Phaser.Math.Vector2(v.x + 1, v.y - 1);
                    }
                }
                return null;
            }

            //上
            let dir6 = () => {
                if (v.y > 0) {
                    if (this.pixelsCopy[v.y - 1][v.x] !== 0) {
                        m.set(0, -1);
                        return new Phaser.Math.Vector2(v.x, v.y - 1);
                    }
                }
                return null;
            }


            //左上
            let dir7 = () => {
                if (v.x > 0 && v.y > 0) {
                    if (this.pixelsCopy[v.y - 1][v.x - 1] !== 0) {
                        m.set(-1, -1);
                        return new Phaser.Math.Vector2(v.x - 1, v.y - 1);
                    }
                }
                return null;
            }

            //左
            let dir8 = () => {
                if (v.x > 0) {
                    if (this.pixelsCopy[v.y][v.x - 1] !== 0) {
                        m.set(-1, 0);
                        return new Phaser.Math.Vector2(v.x - 1, v.y);
                    }
                }
                return null;
            }

            if (m.x === -1 && m.y === 1) {
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
            }

            if (m.x === 0 && m.y === 1) {
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 1 && m.y === 1) {
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 1 && m.y === 0) {
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 1 && m.y === -1) {
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 0 && m.y === -1) {
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === -1 && m.y === -1) {
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === -1 && m.y === 0) {
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 0 && m.y === 0) {
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
            }
            return null;
        }
    }
}

Phaser3 : 凹面の当たり判定を自動で生成してみる⑤ 簡単な三角形分割

前回に引き続きメッシュの自動生成を行っていきます。

ベクトルが変わる部分を保存する

前回で輪郭の抽出ができたので、今回は応用していきたいと思います。
探索するベクトルが変わる部分を保存すれば輪郭の頂点だけを取得できるはずです。

let prevmov =new Phaser.Math.Vector2(mov.x, mov.y);

while (!(tmp.x === origin && tmp.y === 0)) {
    tmp = this.GetBoarder(tmp, mov);
    if (!(prevmov.x === mov.x && prevmov.y === mov.y)) {
        this.border.push(new Phaser.Math.Vector2(tmp.x, tmp.y));
    }
    prevmov.set(mov.x, mov.y);
}
~~
for (let p of this.border) {

    let dx = posX + p.x;
    let dy = posY + p.y;

    let pic = this.add.graphics();
    pic.x = dx;
    pic.y = dy;
    pic.fillStyle(0xff0000, 1);
    pic.fillPointShape(new Phaser.Math.Vector2(0, 0), 1.5);
}

実行結果がこちらです。 f:id:Phaser_3:20190111150800p:plain うまく頂点を取得できたようです。

簡単な三角形分割を実装してみる

こんどはこちらの記事を参考に三角形分割を実装してみたいと思います。 Javaゲーム制作記 任意多角形の三角形分割 MapとFilterを使って最も遠い点を取得します。
border配列には反時計回りに頂点が入っていますので、border配列の番号続きをそのまま両隣りの頂点として扱えます。

while (this.border.length > 3) {
    //最も遠い頂点とその隣の頂点を取得する
    let max = Math.max.apply(null, this.border.map(function (o) { return o.x + o.y; }))
    let farPoint = this.border.filter(function (x) { return x.x + x.y === max })[0];
    let nextPoint1 = this.border[Math.abs(this.border.indexOf(farPoint) - 1 % this.border.length)]
    let nextPoint2 = this.border[Math.abs(this.border.indexOf(farPoint) + 1 % this.border.length)]

    let line = new Phaser.Geom.Line(nextPoint1.x + posX, nextPoint1.y + posY, nextPoint2.x + posX, nextPoint2.y + posY);
    lines.strokeLineShape(line);
    console.log(farPoint, nextPoint1, nextPoint2)
    this.border.splice(this.border.indexOf(farPoint),1)
}

実行結果がこちらです。 f:id:Phaser_3:20190111151434p:plain スプライトを変更してみても一応三角形分割できていることがわかります。 f:id:Phaser_3:20190111151516p:plain

Phaser3: 凹面の当たり判定を自動で生成してみる④ 輪郭追跡の続き

前回に引き続き輪郭追跡の続きを作成します。

前回の位置に基づいて追跡の始点を変える

始点を左下に固定するとある部分でループしてしまうことが判明したので、前回の移動ベクトルに基づいて追跡の始点を変えようと思います。
始点がずれると飛ばした方向の輪郭追跡ができないので、最低でも一周はループさせるようにしてみました。

        public GetBoarder(v: Phaser.Math.Vector2, m: Phaser.Math.Vector2): Phaser.Math.Vector2 {
            let loop = 0;
            let vec = -1;

            if (m.x === -1 && m.y === 1) {
                vec = 0;
            }

            if (m.x === 0 && m.y === 1) {
                vec = 1;
            }

            if (m.x === 1 && m.y === 1) {
                vec = 1;
            }

            if (m.x === 1 && m.y === 0) {
                vec = 2;
            }

            if (m.x === -1 && m.y === -1) {
                vec = 2;
            }

            if (m.x === 0 && m.y === -1) {
                vec = 3;
            }

            if (m.x === -1 && m.y === -1) {
                vec = 3;
            }

            if (m.x === -1 && m.y === 0) {
                vec = 0;
            }

            while (loop === 0) {


                //左下のピクセルから調べていく
                if (vec === 0 || vec === -1) {
                    if (v.x > 0 && v.y < this.imageData.height - 1 && !(m.x == 1 && m.y == -1)) {
                        if (this.pixelsCopy[v.y + 1][v.x - 1] !== 0) {
                            m.set(-1, 1);
                            loop = 1;
                            return new Phaser.Math.Vector2(v.x - 1, v.y + 1);
                        }
                    }
                }

                //下
                if (vec === 1 || vec === -1) {
                    if (v.y < this.imageData.height - 1 && !(m.x == 0 && m.y == -1)) {
                        if (this.pixelsCopy[v.y + 1][v.x] !== 0) {
                            m.set(0, 1);
                            loop = 1;
                            return new Phaser.Math.Vector2(v.x, v.y + 1);
                        }
                    }
                }
                //右下
                if (vec === 1 || vec === -1) {
                    if (v.x < this.imageData.width - 1 && v.y < this.imageData.height - 1 && !(m.x == -1 && m.y == -1)) {
                        if (this.pixelsCopy[v.y + 1][v.x + 1] !== 0) {
                            m.set(1, 1);
                            loop = 1;
                            return new Phaser.Math.Vector2(v.x + 1, v.y + 1);
                        }
                    }
                }
                //右
                if (vec === 2 || vec === -1) {

                    if (v.x < this.imageData.width - 1 && !(m.x == -1 && m.y == 0)) {
                        if (this.pixelsCopy[v.y][v.x + 1] !== 0) {
                            m.set(1, 0);
                            loop = 1;
                            return new Phaser.Math.Vector2(v.x + 1, v.y);
                        }
                    }
                }
                //右上
                if (vec === 2 || vec === -1) {

                    if (v.x < this.imageData.width - 1 && v.y > 0 && !(m.x == -1 && m.y == 1)) {
                        if (this.pixelsCopy[v.y - 1][v.x + 1] !== 0) {
                            m.set(1, -1);
                            loop = 1;
                            return new Phaser.Math.Vector2(v.x + 1, v.y - 1);
                        }
                    }
                }

                //上
                if (vec === 3 || vec === -1) {

                    if (v.y > 0 && !(m.x == 0 && m.y == 1)) {
                        if (this.pixelsCopy[v.y - 1][v.x] !== 0) {
                            m.set(0, -1);
                            loop = 1;
                            return new Phaser.Math.Vector2(v.x, v.y - 1);
                        }
                    }
                }
                //左上
                if (vec === 3 || vec === -1) {

                    if (v.x > 0 && v.y > 0 && !(m.x == 1 && m.y == 1)) {
                        if (this.pixelsCopy[v.y - 1][v.x - 1] !== 0) {
                            m.set(-1, -1);
                            loop = 1;
                            return new Phaser.Math.Vector2(v.x - 1, v.y - 1);
                        }
                    }
                }
                //左
                if (vec === 0 || vec === -1) {

                    if (v.x > 0 && !(m.x == 1 && m.y == 0)) {
                        if (this.pixelsCopy[v.y][v.x - 1] !== 0) {
                            m.set(-1.0);
                            loop = 1;
                            return new Phaser.Math.Vector2(v.x - 1, v.y);
                        }
                    }
                }

                vec = -1;
            }
            return null;
        }
    }

実行結果がこちらです。 f:id:Phaser_3:20190110192405p:plain 輪郭が左側に向かうところで左下に下がっていってしまいます。
ループの構造が適当だったので当たり前の結果です。

力技で解決する

うまい方法が思い浮かばず、各方向への探索関数を作って前回探索時のベクトルで場合分けする方法をためしてみました。

        public GetBoarder(v: Phaser.Math.Vector2, m: Phaser.Math.Vector2): Phaser.Math.Vector2 {
            let r: Phaser.Math.Vector2 = null;

            //左下のピクセルから調べていく
            let dir1 = () => {
                if (v.x > 0 && v.y < this.imageData.height - 1) {
                    if (this.pixelsCopy[v.y + 1][v.x - 1] !== 0) {
                        m.set(-1, 1);
                        return new Phaser.Math.Vector2(v.x - 1, v.y + 1);
                    }
                }
                return null;
            }

            //下
            let dir2 = () => {
                if (v.y < this.imageData.height - 1 && !(m.x == 0 && m.y == -1)) {
                    if (this.pixelsCopy[v.y + 1][v.x] !== 0) {
                        m.set(0, 1);
                        return new Phaser.Math.Vector2(v.x, v.y + 1);
                    }
                }
                return null;
            }

            //右下
            let dir3 = () => {
                console.log(v)
                if (v.x < this.imageData.width - 1 && v.y < this.imageData.height - 1) {
                    if (this.pixelsCopy[v.y + 1][v.x + 1] !== 0) {
                        m.set(1, 1);
                        return new Phaser.Math.Vector2(v.x + 1, v.y + 1);
                    }
                }
                return null;

            }

            //右
            let dir4 = () => {
                if (v.x < this.imageData.width - 1) {
                    if (this.pixelsCopy[v.y][v.x + 1] !== 0) {
                        m.set(1, 0);
                        return new Phaser.Math.Vector2(v.x + 1, v.y);
                    }
                }
                return null;
            }

            //右上
            let dir5 = () => {
                if (v.x < this.imageData.width - 1 && v.y > 0) {
                    if (this.pixelsCopy[v.y - 1][v.x + 1] !== 0) {
                        m.set(1, -1);
                        return new Phaser.Math.Vector2(v.x + 1, v.y - 1);
                    }
                }
                return null;
            }

            //上
            let dir6 = () => {
                if (v.y > 0) {
                    if (this.pixelsCopy[v.y - 1][v.x] !== 0) {
                        m.set(0, -1);
                        return new Phaser.Math.Vector2(v.x, v.y - 1);
                    }
                }
                return null;
            }


            //左上
            let dir7 = () => {
                if (v.x > 0 && v.y > 0) {
                    if (this.pixelsCopy[v.y - 1][v.x - 1] !== 0) {
                        m.set(-1, -1);
                        return new Phaser.Math.Vector2(v.x - 1, v.y - 1);
                    }
                }
                return null;
            }

            //左
            let dir8 = () => {
                if (v.x > 0) {
                    if (this.pixelsCopy[v.y][v.x - 1] !== 0) {
                        m.set(-1.0);
                        return new Phaser.Math.Vector2(v.x - 1, v.y);
                    }
                }
                return null;
            }

            if (m.x === -1 && m.y === 1) {
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
            }

            if (m.x === 0 && m.y === 1) {
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 1 && m.y === 1) {
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 1 && m.y === 0) {
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 1 && m.y === -1) {
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 0 && m.y === -1) {
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === -1 && m.y === -1) {
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === -1 && m.y === 0) {
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
            }

            if (m.x === 0 && m.y === 0) {
                r = dir1();
                if (r !== null) {
                    return r;
                }
                r = dir2();
                if (r !== null) {
                    return r;
                }
                r = dir3();
                if (r !== null) {
                    return r;
                }
                r = dir4();
                if (r !== null) {
                    return r;
                }
                r = dir5();
                if (r !== null) {
                    return r;
                }
                r = dir6();
                if (r !== null) {
                    return r;
                }
                r = dir7();
                if (r !== null) {
                    return r;
                }
                r = dir8();
                if (r !== null) {
                    return r;
                }
            }


            return null;
        }
    }

実行結果がこちらです。 f:id:Phaser_3:20190110192708p:plain なにはともあれ一応輪郭の取得はできているように思います。
別スプライトで実行した様子です。 f:id:Phaser_3:20190110192835p:plain

次回以降はソースを整理して輪郭追跡を実用にうつしてみたいと思います。

Phaser 3 : 凹面の当たり判定を自動で生成してみる③ 輪郭追跡

前回に引き続き、凹面メッシュの自動生成に挑戦します。

取得したアルファ値をもとに輪郭追跡をしてみる

輪郭追跡処理アルゴリズム 画像処理ソリューションの記事を参考に輪郭追跡し画像の境界線を取得してみたいと思います。
ピクセルをわかりやすく扱うためにimageDataからpixelCopy配列を作成します。

for (let i = 0; i < this.imageData.height; i++) {
    this.pixelsCopy[i] = new Array();
    for (let j = 0; j < this.imageData.width; j++) {
        this.pixelsCopy[i].push(0)
    }
}

次に輪郭追跡の原点(画像のピクセルの中でy値が最小のピクセルのうちx値が最小のもの)を探します。
記事のサンプルでは上から順にピクセルを走査していますが、今回の画像はアルファ値でトリミングしてあるのでy座標は0で決め打ちしていいはずです。

let origin = 0;

while (this.pixels[origin][3] === 0) {
    origin++;
}

次に左下からピクセルのアルファ値を調べるGetBorder関数を作ります。

public GetBoarder(v: Phaser.Math.Vector2): Phaser.Math.Vector2 {

    //左下のピクセルから調べていく
    if (v.x > 0 && v.y < this.imageData.height - 1 ) {
        if (this.pixelsCopy[v.y + 1][v.x - 1] !== 0) {
            return new Phaser.Math.Vector2(v.x - 1, v.y + 1);
        }
    }
    //下
    if (v.y < this.imageData.height - 1 ) {
        if (this.pixelsCopy[v.y + 1][v.x] !== 0) {
            return new Phaser.Math.Vector2(v.x, v.y + 1);
        }
    }

    //右下
    if (v.x < this.imageData.width - 1 && v.y < this.imageData.height - 1) {
        if (this.pixelsCopy[v.y + 1][v.x + 1] !== 0) {
            return new Phaser.Math.Vector2(v.x + 1, v.y + 1);
        }
    }

    //右
    if (v.x < this.imageData.width - 1 ) {
        if (this.pixelsCopy[v.y][v.x + 1] !== 0) {
            return new Phaser.Math.Vector2(v.x + 1, v.y);
        }
    }

    //右上
    if (v.x < this.imageData.width - 1 && v.y > 0 ) {
        if (this.pixelsCopy[v.y - 1][v.x + 1] !== 0) {
            return new Phaser.Math.Vector2(v.x + 1, v.y - 1);
        }
    }

    //上
    if (v.y > 0 ) {
        if (this.pixelsCopy[v.y - 1][v.x] !== 0) {
            return new Phaser.Math.Vector2(v.x, v.y - 1);
        }
    }

    //左上
    if (v.x > 0 && v.y > 0) {
        if (this.pixelsCopy[v.y - 1][v.x - 1] !== 0 ) {
            return new Phaser.Math.Vector2(v.x - 1, v.y - 1);
        }
    }

    //左
    if (v.x > 0 ) {
        if (this.pixelsCopy[v.y][v.x - 1] !== 0) {
            return new Phaser.Math.Vector2(v.x - 1, v.y);
        }
    }
    return null;
}
}

originの位置から一回GetBorderを実行し、そのあとwhileでループさせます。
originの位置に戻ってくるまでやりたいのですが、無限ループになるといけないので適当に1000回ほど実行してみます。

tmp = this.GetBoarder(new Phaser.Math.Vector2(origin, 0));
let calc = 0;
while (calc < 1000) {

    tmp = this.GetBoarder(tmp);

    let posX = this.sys.canvas.width / 2;
    let posY = this.sys.canvas.height / 2;

    let dx = posX + tmp.x;
    let dy = posY + tmp.y;


    let pic = this.add.graphics();
    pic.x = dx;
    pic.y = dy;
    pic.fillStyle(0x494949, 1);
    pic.fillPointShape(new Phaser.Math.Vector2(0, 0), 1);

    console.log(tmp.x,tmp.y,origin,mov)

    calc++
}

実行結果がこちらです。 f:id:Phaser_3:20190109164009p:plain 右下で途切れてしまっています。
原因を調べてみると、左下から追跡を開始しているので、左下が透明でないピクセルの位置でループしてしまっていました。
GetBorder関数を変更し、元の位置のピクセルには戻らないようにしてみました。

let mov = new Phaser.Math.Vector2(0, 0);
tmp = this.GetBoarder(new Phaser.Math.Vector2(origin, 0), mov);
let calc = 0;
while (calc < 1000) {

    tmp = this.GetBoarder(tmp,mov);

    let posX = this.sys.canvas.width / 2;
    let posY = this.sys.canvas.height / 2;

    let dx = posX + tmp.x;
    let dy = posY + tmp.y;


    let pic = this.add.graphics();
    pic.x = dx;
    pic.y = dy;
    pic.fillStyle(0x494949, 1);
    pic.fillPointShape(new Phaser.Math.Vector2(0, 0), 1);

    console.log(tmp.x,tmp.y,origin,mov)

    calc++
}
~~
public GetBoarder(v: Phaser.Math.Vector2, m: Phaser.Math.Vector2): Phaser.Math.Vector2 {

    //左下のピクセルから調べていく
    if (v.x > 0 && v.y < this.imageData.height - 1 && !(m.x == 1 && m.y == -1)) {
        if (this.pixelsCopy[v.y + 1][v.x - 1] !== 0) {
            m.set(-1, 1);
            return new Phaser.Math.Vector2(v.x - 1, v.y + 1);
        }
    }
    //下
    if (v.y < this.imageData.height - 1 && !(m.x == 0 && m.y == -1)) {
        if (this.pixelsCopy[v.y + 1][v.x] !== 0) {
            m.set(0,1);
            return new Phaser.Math.Vector2(v.x, v.y + 1);
        }
    }

    //右下
    if (v.x < this.imageData.width - 1 && v.y < this.imageData.height - 1 && !(m.x == -1 && m.y == -1)) {
        if (this.pixelsCopy[v.y + 1][v.x + 1] !== 0) {
            m.set(1,1);
            return new Phaser.Math.Vector2(v.x + 1, v.y + 1);
        }
    }

    //右
    if (v.x < this.imageData.width - 1 && !(m.x == -1 && m.y == 0)) {
        if (this.pixelsCopy[v.y][v.x + 1] !== 0) {
            m.set(1,0);
            return new Phaser.Math.Vector2(v.x + 1, v.y);
        }
    }

    //右上
    if (v.x < this.imageData.width - 1 && v.y > 0 && !(m.x == -1 && m.y == 1)) {
        if (this.pixelsCopy[v.y - 1][v.x + 1] !== 0) {
            m.set(1,-1);
            return new Phaser.Math.Vector2(v.x + 1, v.y - 1);
        }
    }

    //上
    if (v.y > 0 && !(m.x == 0 && m.y == 1)) {
        if (this.pixelsCopy[v.y - 1][v.x] !== 0) {
            m.set(0,-1);
            return new Phaser.Math.Vector2(v.x, v.y - 1);
        }
    }

    //左上
    if (v.x > 0 && v.y > 0 && !(m.x == 1 && m.y == 1)) {
        if (this.pixelsCopy[v.y - 1][v.x - 1] !== 0 ) {
            m.set(-1,-1);
            return new Phaser.Math.Vector2(v.x - 1, v.y - 1);
        }
    }

    //左
    if (v.x > 0 && !(m.x == 1 && m.y == 0)) {
        if (this.pixelsCopy[v.y][v.x - 1] !== 0) {
            m.set(-1.0);
            return new Phaser.Math.Vector2(v.x - 1, v.y);
        }
    }
    return null;
}

実行結果がこちらです。 f:id:Phaser_3:20190109164534p:plain 今度は線が上に向かうところで途切れてしまっています。
調べてみると、真上のピクセルに移動した直後に左下のピクセルを追跡してしまっているため、右下の輪郭近辺をぐるぐるとループしてしまっていました。
すべてのピクセルにおいて左下から検索してしまうと不都合があるようです。
引き続き輪郭追跡について実装していきます。