第9回 VBAでテクスチャを描画してみる(後編)
カンデラの開発者による連載コラムです。 第9回は、「第9回 VBAでテクスチャを描画してみる(後編)」です。
VBA側の実装
それでは、VBA側の実装を行っていきます。今回VBA側で読み込む画像のフォーマットは図1のような内容で、それをC言語風に表したのがリスト1の内容です。まずはシンプルな図2のような画像を使用して実装を行い、その後、もう少し大きい画像の読み込みを行ってみることにします。
C言語風データ構造( ソースコードを見る )
#define N_SWIMAGE_TYPE_RGB (0)
#define N_SWIMAGE_TYPE_RGBA (1)
typedef struct _SSWImage {
uint32_t unType;
uint32_t unWidth;
uint32_t unHeight;
uint32_t unPadding;
// 16Bytes.
uint8_t* pData;
} SSWImage;
仮実装
VBA側はリスト1の構造を読み込む実装をします。まずは動作テストを兼ねて、標準モジュール( ImageLoadTest )を追加します。次に、ImageLoadTestというシートを追加しておきます。ImageLoadTestモジュールは、リスト2のような実装をします。さらに、ImageLoadTestシートに通し番号と読み込む画像のパスを指定する列を設けます( 図3 )。最後に、TestLoadImageルーチンを、ImageLoadTestシートのボタンから呼び出せるようにし( 図4 )、図2の画像をコンバートしたものをloatTest.binという名称に変更して、このワークブックと同じディレクトリに配置します。この状態で、デバッガのLoadImageルーチンの最後にブレークポイントを配置し、LoadTestボタンを押してみて、デバッガを動かしてみます。コンバート画像が正しく読み込まれ、VBA側の構造体にも正しくデータが入っていることが確認できます( 図5 )。
ImageLoadTestモジュール( ソースコードを見る )
Public Const IMAGE_FORMAT_TYPE_RGB As Integer = 0
Public Const IMAGE_FORMAT_TYPE_RGBA As Integer = 1
Public Const IMAGE_STRUCTURE_HEADER_SIZE As Integer = 16
Public Type SWImageStructure
format As Long
width As Long
height As Long
padding As Long
data() As Byte
End Type
Public Type SWImage
id As Integer
image As SWImageStructure
End Type
Sub LoadImage(ByRef rImageStructure As SWImageStructure, ByRef szFileName As String)
Dim fd As Integer
Dim nSize As Long
Dim aBuff() As Byte
Dim nCount As Integer
nCount = 0
fd = FreeFile
' 対象のファイルを開く.
Open szFileName For Binary Access Read As #fd
' 配列の確保.
ReDim aBuff(IMAGE_STRUCTURE_HEADER_SIZE)
' データの読み込み.
aBuff = InputB(IMAGE_STRUCTURE_HEADER_SIZE, fd)
' 最初の4Bytesはフォーマット番号だが、0か1にしかなり得ないので先頭の1Byteのみで良い.
rImageStructure.format = aBuff(nCount)
nCount = nCount + 4
' 次の4Bytesが幅( uint32_t ).
rImageStructure.width = aBuff(nCount) + 255 * aBuff(nCount + 1) + 65535 * aBuff(nCount + 2) + 16711680 * aBuff(nCount + 3)
nCount = nCount + 4
' その次の4Bytesが高さ( uint32_t ).
rImageStructure.height = aBuff(nCount) + 255 * aBuff(nCount + 1) + 65535 * aBuff(nCount + 2) + 16711680 * aBuff(nCount + 3)
' その次はPaddingなので、気にしない.
rImageStructure.padding = 0
' データ部分のサイズを取得.
nSize = LOF(fd) - IMAGE_STRUCTURE_HEADER_SIZE
' データ部分のサイズに合わせて配列のサイズを変更.
ReDim rImageStructure.data(nSize)
' データの読み込み.
rImageStructure.data = InputB(nSize, fd)
' 対象のファイルを閉じる.
Close #fd
End Sub
Sub TestLoadImage()
' まずは有効なデータの数を調べる
Dim nRowCnt As Integer
Dim nElementCnt As Integer
' 画像の実体
Dim aImage() As SWImage
Dim nRows As Integer
With Worksheets("ImageLoadTest")
nRows = .Cells(.Rows.Count, 1).End(xlUp).Row - 1
End With
ReDim aImage(nRows - 1)
For nRowCnt = 2 To 2 + nRows - 1
With Worksheets("ImageLoadTest")
' 1列目が画像の番号.
aImage(nElementCnt).id = .Cells(nRowCnt, 1).Value
' 2列目が画像のファイルの場所.
Call LoadImage(aImage(nElementCnt).image, ThisWorkbook.Path & "\" & .Cells(nRowCnt, 2).Value)
nElementCnt = nElementCnt + 1
End With
Next
End Sub
本実装
これまでの実装で、コンバートした画像データをメモリに読み込むことができるのは確認しました。このメモリの内容を指定位置にコピーすることで描画の処理を実装します。まずはこれまで試験的に実装したコードを本番想定の位置に移動します。定数や構造体の定義はTypesモジュールに、LoadImageルーチンは、EntryPointモジュールに移しましょう。次に、imageシートを作成し、ImageLoadTestシートの列に加えて、描画面の座標系で、描き始めの左上の位置を指定できるようにします( 図6 )。次に、SWImage構造体の内容と図6で定義した列の内容を併せ持つSWImageDraw構造体を用意し( リスト3 )、imageシートの内容に従い、画像をロードする仕組みを実装します( リスト4 )。さらに、SWImageDraw構造体の内容を参照して、画像を描画する仕組みをRenderingSystemモジュールに実装し( リスト5 )、最後にそれらを呼び出す処理を実装します( リスト6 )。では、前回作成した実行ボタンからMainルーチンを実行してみましょう。ひと目では何も違いはないように見えるかもしれません。しかし、拡大してみると図7のようにコンバータで作成した画像が正しく読み込まれ描画されていることが確認できると思います。せっかくですので、適当な画像をコンバートしてみて、imageシートに追加して描画できるかどうか確認してみましょう。図8の例では、幅128、高さ128のpng画像をコンバートして表示しています。
Typesモジュールに追加( ソースコードを見る )
Public Type SWImageDraw
pos As Position
sSWImage As SWImage
End Type
EntryPointモジュールに追加( ソースコードを見る )
Sub SetupImage(ByRef aImage() As SWImageDraw)
' まずはimageシートの有効な行の数を調べる.
Dim nRowCnt As Integer
Dim nElementCnt As Integer
Dim nRows As Integer
With Worksheets("image")
nRows = .Cells(.Rows.Count, 1).End(xlUp).Row - 1
End With
ReDim aImage(nRows - 1)
For nRowCnt = 2 To 2 + nRows - 1
With Worksheets("image")
' 1列目が管理番号.
aImage(nElementCnt).sSWImage.id = .Cells(nRowCnt, 1).Value
' 2列目はファイル名なので、ここで画像の読み込み処理をコール.
Call LoadImage(aImage(nElementCnt).sSWImage.image, ThisWorkbook.Path & "\" & .Cells(nRowCnt, 2).Value)
' 3列目は位置.
aImage(nElementCnt).pos.x = .Cells(nRowCnt, 3).Value
aImage(nElementCnt).pos.y = .Cells(nRowCnt, 4).Value
nElementCnt = nElementCnt + 1
End With
Next
End Sub
RenderingSystemクラスモジュールに追加( ソースコードを見る )
Sub DrawImage(ByRef rImage As SWImageDraw)
' rImage.sSWImage.image.dataの内容をコピーする.
Dim nRowStart As Long
Dim nColStart As Long
' カウンタ変数を用意.
Dim nRowCnt As Long: nRowCnt = 0
Dim nColCnt As Long: nColCnt = 0
' 読み込んだ画像データ上のピクセルの開始位置.
Dim nPixelPosition As Long: nPixelPosition = 0
' 色を一時的に格納する変数.
Dim sColor As ColorRGB
' ピクセル辺り何Bytesか( デフォルトは4 ).
Dim nBytesPerPixel As Integer: nBytesPerPixel = 4
' ただし、画像フォーマットが0番の場合は3.
If IMAGE_FORMAT_TYPE_RGB = rImage.sSWImage.image.format Then
nBytesPerPixel = 3
End If
nRowStart = rImage.pos.y + 1
nColStart = rImage.pos.x + 1
With Worksheets("framebuffer")
' 幅( 列 )を塗りきったら改行.
For nRowCnt = 0 To rImage.sSWImage.image.height - 1
For nColCnt = 0 To rImage.sSWImage.image.width - 1
' 読み込んだ画像データ上の位置.
nPixelPosition = nBytesPerPixel * (nColCnt + nRowCnt * rImage.sSWImage.image.width)
' この形式では色はRGB(A)の順番で格納されている.
' まだアルファチャネルは考慮しない.
sColor.R = rImage.sSWImage.image.data(nPixelPosition)
sColor.G = rImage.sSWImage.image.data(nPixelPosition + 1)
sColor.B = rImage.sSWImage.image.data(nPixelPosition + 2)
.Cells(nRowStart + nRowCnt, nColStart + nColCnt).Interior.color = RGB(sColor.R, sColor.G, sColor.B)
Next nColCnt
Next nRowCnt
End With
End Sub
今回実装した処理を呼び出すMainルーチン( ソースコードを見る )
Sub Main()
Dim cRenderSystem As RenderingSystem
Set cRenderSystem = New RenderingSystem
' Rectの格納場所を用意.
Dim aRect() As Rect
Dim nCnt As Integer
' Imageの格納場所を用意.
Dim aImage() As SWImageDraw
' 初期化処理をコール.
Call Initialize(cRenderSystem)
' rectシートから、Rectをセットアップ.
Call SetupRect(aRect)
' imageシートから、SWImageDrawをセットアップ.
Call SetupImage(aImage)
' aRectの要素数に合わせてRenderingSystemのDrawRectをコール.
For nCnt = LBound(aRect) To UBound(aRect)
Call cRenderSystem.DrawRect(aRect(nCnt))
Next
' aImageの要素数に合わせてRenderingSystemのDrawImageをコール.
For nCnt = LBound(aImage) To UBound(aImage)
Call cRenderSystem.DrawImage(aImage(nCnt))
Next
' 終了処理をコール.
Call Terminate(cRenderSystem)
End Sub
いかがでしたでしょうか?これで殺風景だった描画システムも少しにぎやかになったのではないでしょうか。ソフトウェアレンダリングの描画とはメモリのコピーに過ぎないことが体感できたと思います。描画面と同一のフォーマットのメモリであれば、そのままコピーしてしまえば、簡単に描画できることになります。また、読み込ませるデータについては、独自のフォーマットを決めて、対応するコンバータと処理するロジックを実装してみました。これは、画像データに限らずターゲットのシステムにとって都合の良いデータを作成するという点で組み込み機器の分野にとっては重要な発想となります。 ところで、比較的大きな画像が表示されるまで環境次第ではかなり低速だったのではないでしょうか。この辺りは、リアルタイムレンダリングでは大きな課題となります。パフォーマンスチューニングのタイミングでこの部分の高速化の検討をすることにしましょう。また、アルファチャネルの取り扱いについては、特に何も触れずにこれまでやってきました。次回以降は少しその辺りにも触れてみたいと思います。