GoogleAdsence

画像弾幕制作ツール

BPMファイルの線画を元に弾幕を作る

さて、今回は今までみたいに計算して弾幕を作るのではなく元々用意された線画から弾幕を作ってみます。
以下の様な感じになります。

線画から弾幕を表示させるツールを作る

今回使う線画はこんな感じでBMP形式で保存されています。

初期化処理

それではまず画像弾幕変換ツールのプログラムから見ていきましょう。
まずInitializ関数でLoadGraphで上の線画をLoadDivGraphで弾を読み込みます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DxLibDLL;
using System.IO;
using static DrawTest.CInput;
using System.Windows.Forms;
using System.Runtime.Serialization.Formatters.Binary;

namespace DrawTest
{
    public class CMain
    {
        /* ビットマップ画像の横サイズ */
        const int BMP_YOKO = 400;
        /* ビットマップ画像の縦サイズ */
        const int BMP_TATE = 400;

        /* ビットマップのヘッダサイズ */
        const int HEAD = 54;
        /* トータルサイズ */
        const int TOTAL = (BMP_YOKO * BMP_TATE * 3 + HEAD);

        /* AAデータを作成する合計枚数 */
        const int BULLET_MAX = 4000;

        /* ビットマップの色情報を格納するための構造体 */
        public class img_t
        {
            public byte[] col = new byte[3];
        };

        //弾の座標(55)
        [Serializable()]
        public class Pt_t
        {
            public float x, y;
        };
        //置いた弾の構造体
        [Serializable()]
        public class CBl_t
        {
            public int num;//現在置いた弾数
            public Pt_t[] Pt = new Pt_t[BULLET_MAX];
        };
        public class Operate_t
        {
            public int state; //状態
            public double len; //間隔
        };
        CBl_t Bl = new CBl_t();
        Operate_t Operate = new Operate_t();
        int[] img_bullet = new int[4]; //(55)
        int img_back;

        /* TOTAL分データを用意 */
        byte[] data = new byte[TOTAL];
        /* イメージ画像のピクセル数だけ色格納用構造体を用意 */
        img_t[][] img = new img_t[BMP_TATE][];
        /* 2値化した情報を入れるための配列を用意 */
        byte[][] Pixel = new byte[BMP_TATE][];
        /* ビットマップを読み込みdataに格納する関数 */
        byte[] ReadBmp()
        {
            for (int i = 0; i < img.Length; ++i)
            {
                img[i] = new img_t[BMP_YOKO];
                for(int j = 0; j < img[i].Length; ++j)
                {
                    img[i][j] = new img_t();
                }
            }
            string filePath = "miina.bmp";
            using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                var buffer = new byte[fs.Length];
                fs.Read(buffer, 0, buffer.Length);
                return buffer;
            }
        }

        /* ビットマップの生データを各ピクセルの色格納用構造体に入れ直す */
        void ConvData()
        {
            int x, y, c, t;
            t = HEAD;
            for (y = BMP_TATE - 1; y >= 0; y--)
            {
                for (x = 0; x < BMP_YOKO; x++)
                {
                    for (c = 0; c < 3; c++)
                    {
                        if (data.Length <= t) break;
                        img[y][x].col[c] = data[t];
                        t++;
                    }
                }
            }
        }

        /* 2値化 */
        void Binarization()
        {
            int x, y, c;
            int sum;
            for (y = 0; y < BMP_TATE; y++)
            {
                Pixel[y] = new byte[BMP_YOKO];
                for (x = 0; x < BMP_YOKO; x++)
                {
                    sum = 0;
                    //色の輝度平均を計算
                    for (c = 0; c < 3; c++)
                    {
                        sum += img[y][x].col[c];
                    }
                    sum /= 3;
                    //0~255の輝度の平均が128以上なら(明るいなら)0、未満なら(暗いなら)1
                    if (sum >= 128)
                    {
                        Pixel[y][x] = 0;
                    }
                    else
                    {
                        Pixel[y][x] = 1;
                    }
                }
            }
        }
        /* 描画 */
        uint Black = DX.GetColor(0, 0, 0);
        void Graph()
        {
            int i;
            //状態によって描画パターンを変える
            if (Operate.state == 1 || Operate.state == 2)
            {
                DX.DrawGraph(0, 0, img_back, 0);
            }
            if (Operate.state == 0 || Operate.state == 1)
            {
                for (i = 0; i < Bl.num; i++)
                {
                    DX.DrawRotaGraphF(Bl.Pt[i].x, Bl.Pt[i].y, 1.0, 0.0, img_bullet[1], 1);
                }
            }
            DX.SetDrawBlendMode(DX.DX_BLENDMODE_ALPHA, 64);
            DX.DrawBox(0, 0, 100, 40, 0, 1);
            DX.SetDrawBlendMode(DX.DX_BLENDMODE_NOBLEND, 0);
            DX.DrawString(0, 0, $"間隔={Operate.len}",DX.GetColor(255,255,255));
            DX.DrawString(0, 20, $"弾数={Bl.num}", DX.GetColor(255, 255, 255));
        }
        /* 弾置き計算 */
        void CalcPut()
        {
            int i, x, y;
            double lx, ly, len;
            //現在登録されている弾を0個に
            Bl.num = 0;
            //ビットマップ画像サイズ分ループ
            for (y = 0; y < BMP_TATE; y++)
            {
                for (x = 0; x < BMP_YOKO; x++)
                {
                    //もう登録できないなら抜ける
                    if (Bl.num >= BULLET_MAX - 1)
                    {
                        break;
                    }
                    //その場が色つきなら
                    if (Pixel[y][x] == 1)
                    {
                        //今登録されている弾分ループ
                        for (i = 0; i < Bl.num; i++)
                        {
                            //その全ての弾と現在の場所との距離がOperate.len以上なら抜ける
                            lx = x - Bl.Pt[i].x;
                            ly = y - Bl.Pt[i].y;
                            if (lx * lx + ly * ly < Operate.len * Operate.len)
                            {
                                break;
                            }
                        }
                        //途中で抜けなかった=一つも近くに弾が無かったなら
                        if (i == Bl.num)
                        {
                            Bl.Pt[Bl.num] = new Pt_t();
                            //登録
                            Bl.Pt[Bl.num].x = x;
                            Bl.Pt[Bl.num].y = y;
                            Bl.num++;
                        }
                    }
                }
            }
        }
        void CalcOperate()
        {
            //スペースが押されるたびに状態変化
            if (GetPadKey(EINPUT_TYPE.Start) == 1)
            {
                Operate.state = (Operate.state + 1) % 3;
            }
            //左が押されると.lenを少なく
            if (GetPadKey(EINPUT_TYPE.Left) == 1 || GetPadKey(EINPUT_TYPE.Left) > 20)
            {
                if (Operate.len > 1)
                {
                    Operate.len -= 0.2;
                }
                CalcPut();
            }
            //右が押されると.lenを多く
            if (GetPadKey(EINPUT_TYPE.Right) == 1 || GetPadKey(EINPUT_TYPE.Right) > 20)
            {
                if (Operate.len < 50)
                {
                    Operate.len += 0.2;
                }
                CalcPut();
            }
        }
        public object LoadFromBinaryFile(string path)
        {
            FileStream fs = new FileStream(path,
                FileMode.Open,
                FileAccess.Read);
            BinaryFormatter f = new BinaryFormatter();
            //読み込んで逆シリアル化する
            object obj = f.Deserialize(fs);
            fs.Close();
            return obj;
        }
        //データを出力
        public void SaveToBinaryFile(object obj, string path)
        {
            FileStream fs = new FileStream(path,
                FileMode.Create,
                FileAccess.Write);
            BinaryFormatter bf = new BinaryFormatter();
            //シリアル化して書き込む
            bf.Serialize(fs, obj);
            fs.Close();
        }
        
        public void Initializ()
        {
            //CBl_t obj2 = (CBl_t)LoadFromBinaryFile("aisha.dat");

            Operate.len = 5;//弾の間隔を5にセット
            img_back = DX.LoadGraph("miina.bmp");
            if(img_back == -1)
            {
                MessageBox.Show("miina.bmpが読み込めません");
            }
            if(DX.LoadDivGraph("b14.png", 4, 4, 1, 6, 6, img_bullet) == -1)
            {
                MessageBox.Show("b14.pngが読み込めません");
            }

            data = ReadBmp();
            ConvData();
            Binarization();
            CalcPut();//弾を置く計算
        }

        public void MainLoop()
        {
            CalcOperate();
            Graph();
        }

        public void Destroy()
        {
            for (int i = 0; i < Bl.num; i++)
            {//座標データを-1~1に変換する
                Bl.Pt[i].x -= BMP_YOKO / 2;
                Bl.Pt[i].y -= BMP_TATE / 2;
                Bl.Pt[i].x /= BMP_YOKO / 2;
                Bl.Pt[i].y /= BMP_TATE / 2;
            }
            SaveToBinaryFile(Bl, "miina.dat");
        }
    }
}

シリアライズとデシリアライズする時の注意点

C#では、自作クラスを保存したり読み込んだりする時には、読み込む時にシリアライズ、書き込む時にデシリアライズをします。この時に保存するクラスに[Serializable()]というアトリビュート(属性)を付けないと読み書きすることができません。そしてこの時は必ず構造体でなくクラスを使う事をおすすめします。

構造体を使うべき場面

詳しく知りたい方はこちらをどうぞ

次にReadBmp関数が呼ばれます役割は

  • img_tクラスを使えるようにnew演算子でインスタンスを作成
  • FileStreamクラスを使ってバイナリファイルを読み込む
  • byte型の配列を返す

以上です。後はusing System.Runtime.Serialization.Formatters.Binary;を上に追加するのを忘れないようにしましょう。

次にConvData関数が呼ばれます。役割は、ReadBmpでdataに読み込んだ情報をピクセルの色格納用クラスに入れ直します。この時にHEAD分の最初の54バイトは読み飛ばしています。

次にBinarization関数がよばれます。役割は、 ConvDataで格納された色情報を利用して、2値化して、byte[][]Pixel(ジャグ配列)に入れ直すです。

以上が初期化処理です。

毎ループ行う処理

CalcOperate関数の役割

  • スペースが押されるたびに状態変化
  • 左が押されるとlen(間隔)を少なくする
  • 右が押されるとlen(間隔)を多くする

以上の処理が実行された後CalcPutが呼ばれ、感覚に応じた弾幕の座標がBlに登録されます。

そして、Graph関数でDrawGraphDrawRotaGraphSetDrawBlendModeを使って描画しています。

終了処理

Destroy関数がよばれ、座標の値を-1~1の間に調整し、SaveToBinaryFileを読んでシリアル化して書き込みます。流れは以上です。

実行結果