【Unity】Bitmapを直接読み込んでTextureにする

# はじめに

今回は、Bitmap(bmp)ファイルのメモリイメージを直接読み込んで、
そこからTexture2Dを生成する方法をご紹介します。

Unityでは画像ファイルをインポートしてテクスチャにする方法が用意されています。([参考](https://docs.unity3d.com/ja/2021.3/Manual/ImportingTextures.html))
また、PNGやJPGファイルであれば[LoadImage](https://docs.unity3d.com/ja/2020.3/ScriptReference/ImageConversion.LoadImage.html)を使うことでテクスチャに変換ができます。

しかし、どうしてもBitmap(bmp)を扱わないといけない事情がありましたので自作することにしました。
わざわざこの方法を用いるケースは少数と思われますが、誰かの役に立てればと置いておきます。

:::note warn
今回のコードは主に24bitカラーのBitmapを対象にしたものです。
また、簡便のためにエラー・例外処理を省いている箇所があります。
:::

# Bitmapの構造体定義

まずはBitmapのヘッダ構造体を定義します。
Bitmapのヘッダはファイルヘッダとインフォヘッダの2つの構造体があります。

```cs
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BITMAPFILEHEADER
{
	public UInt16 bfType;
	public UInt32 bfSize;
	public UInt16 bfReserved1;
	public UInt16 bfReserved2;
	public UInt32 bfOffBits;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BITMAPINFOHEADER
{
    public UInt32 biSize;
    public Int32 biWidth;
    public Int32 biHeight;
    public UInt16 biPlanes;
    public UInt16 biBitCount;
    public UInt32 biCompression;
    public UInt32 biSizeImage;
    public Int32 biXPelsPerMeter;
    public Int32 biYPelsPerMeter;
    public UInt32 biClrUsed;
    public UInt32 biClrImportant;
}
```
これらはWindowsAPIの構造体をそのまま写したものなのでわかりやすいでしょう。
Packを1に設定して、不要な隙間が挿入されないようにする必要があります。

詳しくはこちらを参照：

https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/ns-wingdi-bitmapfileheader

https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader

# ファイルを読み込んでメモリイメージにする

ファイルを読み込む手は色々ありますが、今回はFileStreamで読むことにします。

```cs
FileStream fs = new FileStream( @"file.bmp", FileMode.Open, FileAccess.Read );

byte[] srcData = new byte[(int)fs.Length];
fs.Read( srcData, 0, (int)fs.Length );
```

# ヘッダを取り出す

ファイルが読み込めたら、先頭からヘッダ構造体を取り出します。
C#では直接バイナリを扱えませんので、今回はMarshalを使って取り出す方法にしました。

```cs
int sizeBfh = Marshal.SizeOf( typeof( BITMAPFILEHEADER ) );
int sizeBih = Marshal.SizeOf( typeof( BITMAPINFOHEADER ) );
IntPtr ptrBih = Marshal.AllocHGlobal( sizeBih );
Marshal.Copy( srcData, sizeBfh, ptrBih, sizeBih );

BITMAPINFOHEADER bih = ( BITMAPINFOHEADER )Marshal.PtrToStructure( 
                        ptrBih, typeof( BITMAPINFOHEADER ) );
Marshal.FreeHGlobal(ptrBih);
```

Bitmapファイルは先頭からファイルヘッダ→インフォヘッダの順に並んでいます。
今回必要なのはインフォヘッダの方なので「**ファイルの先頭からファイルヘッダのサイズ**」分オフセットしたところからコピーして、PtrToStructureで構造体に変換します。
Marshalで確保したポインタは解放するのを忘れないようにしましょう。

# サイズの取得ともろもろの計算をする
インフォヘッダからは画像のサイズやビット深度を取ることができます。
他にも必要な要素があるので取り出して計算します。

```cs
int pad = 0;
if( ( ( bih.biBitCount * bih.biWidth ) % 32 ) != 0 )
{
	pad = 32 - ( ( bih.biBitCount * bih.biWidth ) % 32 );
}
int lineBytes = ( bih.biBitCount * bih.biWidth + pad ) / 8;
int byteCount = ( bih.biBitCount / 8 );
```
Bitmapのピクセルデータは、1行ごとに32bit単位になるようにパディングデータが詰められますので、あらかじめ計算しておきます。
そしてパディングを加味した状態での、1行に必要なデータサイズを割り出します。

byteCountはbitcountをバイト単位に換算したものです（例：24bit→3byte）

# ピクセルデータを取り出す

サイズ類が確定したら、ピクセルデータを取り出していきます。
取り出す先の領域はパディングが必要ないので、(**幅x高さxバイトカウント**)のサイズで確保しておきます。

バイト配列への書き込みの方法は色々ありますが、ここではMemoryStreamを使うことにします。

```cs
byte[] dstImage = new byte[ bih.biWidth * bih.biHeight * byteCount ];
MemoryStream dstStream = new MemoryStream( dstImage );

int readOffset = 0;
for( int y = 0; y < bih.biHeight; y++ )
{
	readOffset = (sizeBfh + sizeBih) + (bih.biHeight-y-1) * lineBytes;
	for( int x = 0; x < bih.biWidth; x++ )
	{
		dstStream.WriteByte( srcData[ readOffset + (x * byteCount) + 2 ] );
		dstStream.WriteByte( srcData[ readOffset + (x * byteCount) + 1 ] );
		dstStream.WriteByte( srcData[ readOffset + (x * byteCount) + 0 ] );
	}
}
```
読み込み元の領域から順繰りにピクセルデータを取り出していきます。

- 読み込み元のデータは、ファイル先頭から(**ファイルヘッダのサイズ＋インフォヘッダのサイズ**)分進んだ場所にあります。
今回は扱いませんが、カラーパレットがある場合(インデックスカラー)は、その分だけ位置をずらす必要があります。

- Bitmapは通常、ピクセルデータは下から上の順に格納されています。
そのため、オフセット計算に上下を逆転する式(**bih.biHeight-y-1**)を挿入しています。
上から下の順に格納されているファイルを扱うのであれば、
bih.biHeightが負数になっているかどうかで判定して処理を分岐してください。

- ピクセルごとの色値はリトルエンディアンで格納されているため、BGRの順になっています。
今回はRGB順に並べたいため、元の色を取り出す際にオフセットを2, 1, 0の順に参照しています。

- このコードは24bitカラーを想定したもので書いています。
32bit(アルファチャンネルあり)のファイルを扱うのであれば、
データはBGRAの順に格納されているため、次のようになります（RGBA32を使用する場合）

```cs
    dstStream.WriteByte( srcData[ readOffset + (x * byteCount) + 2 ] );
    dstStream.WriteByte( srcData[ readOffset + (x * byteCount) + 1 ] );
    dstStream.WriteByte( srcData[ readOffset + (x * byteCount) + 0 ] );
    dstStream.WriteByte( srcData[ readOffset + (x * byteCount) + 3 ] );
```

# テクスチャを生成する

ピクセルデータが取り出せたので、Texture2Dを作ります。

```cs
Texture2D texture = new Texture2D( bih.biWidth, bih.biHeigh, TextureFormat.RGB24, false );
texture.LoadRawTextureData( dstImage );
texture.Apply();
```
LoadRawTextureDataを使うと、バイナリの生データからテクスチャを作ることができます。

https://docs.unity3d.com/ScriptReference/Texture2D.LoadRawTextureData.html

32bit(アルファチャンネルあり)の場合、TextureFormatはRGBA32にしてください。