JavaScript
ModPE

【第2回】画像をワールドにコピーするMod【ModPE Script】

最初に

どうもこんにちは。
いつも少し真面目な解説して全くModPE自体には触れられていない解説者、黐麟です。
今回は少しModPEでのModを紹介しながら話を進めます。

開発経緯

画像をワールドにコピーするModって実際は簡単なんじゃね…?
そんな疑問が事の発端です。
と言ってもバイナリデータ弄ってコピーするわけじゃありません。
逐次ブロックを置き換えます。

どんなものができたのか

まずはインプットとなる画像です。
ネットで拾って結構気に入ってるやつです。
誰なのかわかる人が居たら教えてほしいってのは置いておいて、こいつを読み込みます。
src.jpg

次はアウトプットですね。
結果としてはこのようになりました。
Screenshot_20190112-004741.png

元データが貧弱でショボいのは別の話として、コピーに成功しました。

どうやったんだ、本官に教えたまえ

以前も言いましたが、ModPEはJavaScriptです。
でもJavaScriptに画像をあたる関数は備わってましたっけ?
よく覚えていません。

じゃあできないじゃん!って言いたい人も居るでしょうが、肝心なことを忘れていませんか?
なんとModPEでは、JavaScriptの構文でJavaが扱えます。

はいそこ!
こいつ何言ってんだみたいな目で見ない!

じゃあ早速中身を見てみましょう

元データ

まずは元データです。
ベタ貼りはあれなので簡略化します。

script.js
(function(){
    "use strict";
    const COLOR_PALETTE=[
        {id: 41,data: 0,red: 249,green: 236,blue: 78},
    ];
})();

鉄ブロックのデータです。
IDとダメージ値、RGBの値です。
RGBの値に関してはテクスチャ内の色を足して割って出してます。
他に羊毛や粘土等も元データに登録しています。

発火させる関数

無条件に実行しても良いんですが、チャンク読み込み中に処理をすると正常に実行されない場合があります。
なのでブロックを叩いたときに実行するようにしておきましょう。

script.js
(function(){
    "use strict";
    const use=this.useItem;
    this.useItem=function(x,y,z,itemid,blockid,side,itemdata,blockdata){
        clientMessage("Start");
        main(x,y,z,"/games/src.jpg");
        clientMessage("Finish");
        if(typeof use=="function"){
            use.call(this,x,y,z,itemid,blockid,side,itemdata,blockdata);
        }
    };
})();

実行開始時にStartとメッセージを送って処理を開始、終了時にFinishとメッセージを送ります。
次は実際の処理のmain(x,y,z,"/games/src.jpg");のところを触れていきましょう。

実際の処理

渡した引数

今回定義したmainには、x,y,z,"/games/src.jpg"を引数として渡しています。
x,y,zは叩いたブロックの座標です。
ブロックを置き換えるときの基準になります。
"/games/src.jpg"はインプットとなる画像のパスです。

まずはファイルがあるのかを確認しよう

ファイルを読み込むわけですが、ファイルが無かったら実行しようがないです。
なので必ず確認しておきます。
Javaエンジニアの皆さんでしたら方法はご存知ですよね?

Main.java
import java.io.File;
public class Main{
    public static void main(String[] args){
        String path="src/src.jpg";
        File file=new File(path);
        if(!file.exists()){
            System.out.println("Can't find file: "+path);
            return;
        }
    }
}

でしたらこいつをどうJavaScriptで書くのか。
こうなります。

script.js
(function(){
    "use strict";
    const File=java.io.File;
    function main(x,y,z,path){
        const file=new File(storage(path));
        if(!file.exists()){
            print("Can't find file: "+path);
            return;
        }
    }
})();

まぁ大まかには一緒ですね。
3行目でクラスを変数に格納して、5行目でFileクラスのインスタンス作って、6行目で確認しています。
はい。
こんなに簡単なんです。
じゃああとはちゃっちゃか進めます。

こうなります

以下にJava+JavaScriptという変態的なModPEで作ったModを貼り付けます。

script.js
(function(){
    "use strict";
    const BitmapFactory=android.graphics.BitmapFactory;
    const Color=android.graphics.Color;
    const Environment=android.os.Environment;
    const File=java.io.File;
    const FileInputStream=java.io.FileInputStream;
    const COLOR_PALETTE=[
        {id: 41,data: 0,red: 249,green: 236,blue: 78}
    ];
    const use=this.useItem;
    function storage(path){
        return Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+path;
    }
    this.useItem=function(x,y,z,itemid,blockid,side,itemdata,blockdata){
        clientMessage("Start");
        main(x,y,z,"/games/src.jpg");
        clientMessage("Finish");
        if(typeof use=="function"){
            use.call(this,x,y,z,itemid,blockid,side,itemdata,blockdata);
        }
    };
    function main(x,y,z,path){
        const file=new File(storage(path));
        if(!file.exists()){
            print("Can't find file: "+path);
            return;
        }
        const fileInputStream=new FileInputStream(file);
        const bitmap=BitmapFactory.decodeStream(fileInputStream);
        const width=bitmap.getWidth();
        const height=bitmap.getHeight();
        for(let $x=0;
                $x<width;
                $x+=2){
            for(let $z=0;
                    $z<height;
                    $z+=2){
                let pixel=bitmap.getPixel($x,$z);
                let red=(pixel>>16)&0xff;
                let green=(pixel>>8)&0xff;
                let blue=(pixel>>0)&0xff;
                let value=null;
                let distance=null;
                COLOR_PALETTE.forEach(function(currentValue,index,array){
                    let rd=red-currentValue.red;
                    let gd=green-currentValue.green;
                    let bd=blue-currentValue.blue;
                    let d=Math.sqrt(rd*rd+gd*gd+bd*bd);
                    if(distance==null||distance>d){
                        value=currentValue;
                        distance=d;
                    }
                });
                setTile(x+((width/2)-$x)/2,y,z+((height/2)-$z)/2,value.id,value.data);
            }
        }
    }
})();

皆さんの推測通りです。
FileクラスのインスタンスからFileInputStreamのインスタンス作って、BitmapFactory.decodeStream(InputStream inputStream);を使ってBitmapに変換。
Bitmap#getWidth();Bitmap#getHeight();を使ってforループの回数を決め、その回数処理を行います。
Bitmap#getPixel(int x,int y);を使って指定座標のRGB値を取得、(pixel>>16)&0xff;(pixel>>8)&0xff;(pixel>>0)&0xff;で対応した値に変換して変数に格納します。
最後にCOLOR_PALETTEArray.prototype.forEach(function callback(currentValue[,index[,array]]){}[,thisArg]);で回します。
その中でRGBをXYZ軸とした3次元空間で距離を比較し、より近ければ変換を上書きしてあげます。
一通りループが終わったら対応する座標のブロックを置き換えます。

最後に

ModPE=Java+JavaScriptという、各方面のエンジニアには理解不能な仕様ですが、Javaで作ったプログラムをJavaScriptの構文に置き換えれば完了です。
ね?
簡単でしょ?(威圧)
実際、私も最初は抵抗がありました。
私が初めて触れた言語はJavaScriptで、その切っ掛けがModPEです。
その中でJavaを使い、自然とJavaがわかるようになってきました。
JavaScriptだからわかりやすい、おまけに他の言語もわかるようになる。
ModPEの魅力ですよ。