第11回 VBAのテクスチャ描画システムを拡張する(2)
カンデラの開発者による連載コラムです。 第11回は、「VBAのテクスチャ描画システムを拡張する(2)」です。
描画の高速化
現状ですと、図2のように、画像がframebufferシートに表示されるまでかなり待たされます。一方で、Rectの描画は一瞬です。これは、指定領域を一気に塗りつぶす( 四角形 )処理と、1ピクセルずつ色を決定していく処理( 画像 )の差だと考えることができます。指定範囲を指定色で塗りつぶすのが高速なら、指定範囲を別の指定範囲で塗りつぶすのも高速ではないかと予想できます。イメージとしては、まさに図3そのものを実現したいということになります。
では、図3の状況を実現するにはどうすればよいでしょうか?描画面とは別の描画面のようなものがあれば実現可能となります。この実装は、システムにとって都合の良いメモリに予め画像データを展開しておくというものになります。OpenGL ESなどが動作するGPU付きの環境ではこのようなシステムにとって都合の良い領域がVRAMということになります。つまり、これからやろうとしていることは、このVBAの仕組みが画像を高速に読み込むことのできる領域、つまりVRAMを作成することになります。そこで、図4のような仕組みを実装しましょう。具体的には、新たにvramというシートを作成し、画像を読み込んだ際に仮の描画面として、vramシートに読み込んだByte列を描き込みます。次に、それを描画するデータがimagedrawシートに存在している場合、SWImageStructure構造体に含まれているvramシート上の位置から、同構造体の持つ幅高さの領域分の色をSWImageDrawが持つ描画位置を参照し、framebufferシートに描き込みます。これらの処理を実装していくことになります。
図4の仕組みが実現された場合、図2のような現象が発生しなくなるはずです。ですので、この仕組みを実装した後に、図2と同様の描画を試してみてframebufferシートへの描画が一瞬で終わることを確認しましょう。残念ながらvramシートへの描画については、1セルずつ色を乗せていくしかないため、ここは高速化することはできませんが、それでも描画面には一瞬で反映されるためこれらの処理を実装する価値は十分にあると考えられます。それでは実装していきましょう。まずは、vramシートを用意しframebufferシート同様、セルの大きさを調整しましょう( 図5 )。次にSWImageStructreを拡張します。vramシート上の開始位置( 左上 )を記録するためにPosition構造体をメンバに追加します( リスト1 )。
vramシート上のテクスチャ領域開始位置を記録する場所( ソースコードを見る )
Public Type SWImageStructure
format As Long
width As Long
height As Long
padding As Long
data() As Byte
' テクスチャ専用領域の左上を指す.
posUV As Position
End Type
さて、ここまで作成したところで、一つ大きな課題があります。vramシートに読み込まれた画像はどのように配置していけばよいでしょうか。複数の画像を読み込んだ際にどのように画像を並べていけばよいでしょうか。これは、決まった大きさの矩形の中にサイズの異なる矩形を隙間なく敷き詰めるという問題になるのですが、厳密にこの問題を解こうとするとかなりのコストがかかりますし、もしかしたら終了しない処理が出来上がる可能性があります。そこで、今回はあまり深く考えずに、とにかく左から右に画像を並べていきます。ある画像をvramシートに描画しようとしたときに、指定幅を超えそうまらば改行、その際の改行の高さはその行の中で最も大きな画像に合わせるという贅沢な使い方をします( 図6 )。一方で、今回はこのような割り切った仕様ですので、図7のように読み込む画像の大きさ次第では、非効率な配置になってしまう可能性があります。ですので複数の画像を扱う場合は、なるべく画像の大きさは揃えて効率よく配置することを運用面で意識していくことにしましょう。
では、図6の仕組みを実装していきます。今回は既存のLoadImage関数で読み込んだバイト列を、vramシートにキャッシュする機能(CacheImage)を実装しましょう。つまり、LoadImgeのあとにCacheImageをコールするような流れになります。まずは、新たにImageCacheクラスを準備します( リスト2 )。外部からはvramシートを初期化するためのSetup、LoadImageで読み込んだバイト列をvramシートに描画するためのRegisterの2つの処理をコールすることになります。次に、RenderSystemクラスにvramシートの内容をコピーする処理、DrawCachedImageを実装します( リスト3 )。
ImageCacheクラスの実装( ソースコードを見る )
Option Explicit
' vram領域の幅と高さ.
Private m_wh As WidthHeight
' 現在の行の最大高.
Private m_level As Integer
' 次の描き出し位置.
Private m_nowPos As Position
Private Sub ImageCache_Initialize()
m_wh.height = 0
m_wh.width = 0
m_level = 0
m_nowPos.x = 0
m_nowPos.y = 0
End Sub
Private Sub ImageCache_Terminate()
' 破壊ルーチンは今の所は何も行わない.
End Sub
Public Sub Setup(ByVal width As Integer, ByVal height As Integer)
' 初期化時に1度呼び出す.
' 幅と高さを設定する.
Call SetBufferWidthHeight(width, height)
' 指定領域をクリアしておく.
Call Clear
End Sub
Public Sub Register(ByRef rImageStructure As SWImageStructure)
' 渡された画像を初期化.
If 0 <= rImageStructure.posUV.x Then
' 既に画像が登録されているようならば何もしない.
Exit Sub
End If
If 0 <= rImageStructure.posUV.y Then
' 既に画像が登録されているようならば何もしない.
Exit Sub
End If
' 現在の位置から、幅高さ分を描画する.
If Not IsDrawable(rImageStructure) Then
' 現在位置から描画できない.
' 改行したら描画できるか.
If IsDrawableNewLine(rImageStructure) Then
Call MoveToNewLine
Call DrawImage(m_nowPos, rImageStructure)
Call UpdateCache(rImageStructure)
End If
Else
Call DrawImage(m_nowPos, rImageStructure)
Call UpdateCache(rImageStructure)
End If
End Sub
Private Sub MoveToNewLine()
' 現在のlevelの値を足しこみ、列を0に戻したものを新しい現在位置とする.
m_nowPos.y = m_nowPos.y + m_level
m_nowPos.x = 0
m_level = 0
End Sub
Private Function IsDrawable(ByRef rImageStructure As SWImageStructure) As Boolean
' 今の状態で描画できるか.
IsDrawable = True
If m_wh.width <= m_nowPos.x + rImageStructure.width Then
IsDrawable = False
End If
If m_wh.height <= m_nowPos.y + rImageStructure.height Then
IsDrawable = False
End If
End Function
Private Function IsDrawableNewLine(ByRef rImageStructure As SWImageStructure) As Boolean
' 改行したら描画できるか.
Dim posWork As Position
posWork.x = 0
posWork.y = m_nowPos.y + m_level
IsDrawableNewLine = True
If m_wh.width <= posWork.x + rImageStructure.width Then
IsDrawableNewLine = False
End If
If m_wh.height <= posWork.y + rImageStructure.height Then
IsDrawableNewLine = False
End If
End Function
Private Sub UpdateCache(ByRef rImageStructure As SWImageStructure)
' posUVを更新.
rImageStructure.posUV.x = m_nowPos.x
rImageStructure.posUV.y = m_nowPos.y
' 今までで一番高いテクスチャの場合はlevelを更新.
If m_level < rImageStructure.height Then
m_level = rImageStructure.height
End If
' 位置更新.
m_nowPos.x = m_nowPos.x + rImageStructure.width
End Sub
Private Sub DrawImage(ByRef pos As Position, ByRef rImageStructure As SWImageStructure)
' 実装の内容としては、RenderingSystem.DrawImageとほぼ同じ.
' ただしこの処理は外部からはコールできない.
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
Dim nBytesPerPixel As Integer: nBytesPerPixel = 4
If IMAGE_FORMAT_TYPE_RGB = rImageStructure.format Then
nBytesPerPixel = 3
End If
nRowStart = pos.y + 1
nColStart = pos.x + 1
With Worksheets("vram")
For nRowCnt = 0 To rImageStructure.height - 1
For nColCnt = 0 To rImageStructure.width - 1
nPixelPosition = nBytesPerPixel * (nColCnt + nRowCnt * rImageStructure.width)
sColor.R = rImageStructure.data(nPixelPosition)
sColor.G = rImageStructure.data(nPixelPosition + 1)
sColor.B = rImageStructure.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
Private Sub SetBufferWidthHeight(ByVal width As Integer, ByVal height As Integer)
Dim nPrevWidth As Integer: nPrevWidth = m_wh.width
Dim nPrevHeight As Integer: nPrevHeight = m_wh.height
If 0 < width Then
m_wh.width = width
End If
If 0 < height Then
m_wh.height = height
End If
' 指定された大きさが異なる場合は、その大きさでリサイズ.
If nPrevWidth <> m_wh.width Or nPrevHeight <> m_wh.height Then
Call Resize
End If
End Sub
Private Sub Resize()
' リサイズ時に実行する処理.
' 現在のところは何も行わない.
End Sub
Private Sub Clear()
' vramシートの当該範囲をクリアする.
With Worksheets("vram")
.Range(.Cells(1, 1), .Cells(m_wh.height, m_wh.width)).Interior.ColorIndex = 0
End With
End Sub
RenderingSystemクラスにDrawCachedImage( ソースコードを見る )
Public Function DrawCachedImage(ByRef pos As Position, ByRef rImageStructure As SWImageStructure)
DrawCachedImage = False
If 0 <= rImageStructure.posUV.x And 0 <= rImageStructure.posUV.y Then
' UV座標系が決まっているということはvramシートにCacheされているということ.
' vramシートの指定領域を、framebufferシートの指定領域にコピー.
Dim nRowStart As Integer: nRowStart = pos.y + 1
Dim nColStart As Integer: nColStart = pos.x + 1
Dim nRowEnd As Integer: nRowEnd = nRowStart + rImageStructure.height - 1
Dim nColEnd As Integer: nColEnd = nColStart + rImageStructure.width - 1
Dim nVramRowStart As Integer: nVramRowStart = rImageStructure.posUV.y + 1
Dim nVramColStart As Integer: nVramColStart = rImageStructure.posUV.x + 1
Dim nVramRowEnd As Integer: nVramRowEnd = nVramRowStart + rImageStructure.height - 1
Dim nVramColEnd As Integer: nVramColEnd = nVramColStart + rImageStructure.width - 1
Dim framebuffer As Worksheet: Set framebuffer = Worksheets("framebuffer")
Dim vram As Worksheet: Set vram = Worksheets("vram")
' .Interior.colorは範囲指定でコピーできないため、内容をそのままコピーする.
vram.Range(vram.Cells(nVramRowStart, nVramColStart), vram.Cells(nVramRowEnd, nVramColEnd)).Copy Destination:= _
framebuffer.Range(framebuffer.Cells(nRowStart, nColStart), framebuffer.Cells(nRowEnd, nColEnd))
DrawCachedImage = True
End If
End Function
次に、EntryPointモジュールにImageCacheクラスのインスタンスを持たせて、初期化する処理を追加します。また、この流れで、もともと存在していたInitializeルーチンを分割してImageCacheとRenderingSystemの初期化処理をコールするつくりにします( リスト4 )。
EntryPointモジュールの一部改修( ソースコードを見る )
Sub Initialize(ByRef rRenderingSystem As RenderingSystem, ByRef rVram As ImageCache)
Call InitializeVRAMSystem(rVram)
Call InitializeRenderingSystem(rRenderingSystem)
End Sub
Private Sub InitializeVRAMSystem(ByRef rVram As ImageCache)
' 画面の幅、高さ、クリアの色を格納しておく変数.
Dim nWidth As Integer, nHeight As Integer
' 対象シートの内容を読み込んでまずは必要な情報を取得する.
nWidth = Worksheets("configuration").Range("B3").Value
nHeight = Worksheets("configuration").Range("C3").Value
' 初期化.
Call rVram.Setup(nWidth, nHeight)
' vramシートの表示倍率を小さめにしておく.
With Worksheets("vram")
.Activate
.Cells(1, 1).Select
End With
ActiveWindow.Zoom = 10
End Sub
Private Sub InitializeRenderingSystem(ByRef rRenderingSystem As RenderingSystem)
' 画面の幅、高さ、クリアの色を格納しておく変数.
Dim nWidth As Integer, nHeight As Integer
Dim nR As Integer, nG As Integer, nB As Integer
' 対象シートの内容を読み込んでまずは必要な情報を取得する.
nWidth = Worksheets("configuration").Range("B1").Value
nHeight = Worksheets("configuration").Range("C1").Value
nR = Worksheets("configuration").Range("B2").Value
nG = Worksheets("configuration").Range("C2").Value
nB = Worksheets("configuration").Range("D2").Value
' Systemのクラスにその値をセット.
Call rRenderingSystem.SetBufferWidthHeight(nWidth, nHeight)
Call rRenderingSystem.SetColor(nR, nG, nB)
' framebufferをクリア.
Call rRenderingSystem.Clear
' framebufferのシートの表示倍率を小さめにしておく.
Worksheets("framebuffer").Activate
Worksheets("framebuffer").Cells(1, 1).Select
ActiveWindow.Zoom = 10
End Sub
続いて、このImageCacheクラスのインスタンスに読み込んだ画像を登録する処理を作成します。先程のImageCacheクラスのRegisterをコールする流れになります( リスト5 )。最後に、これらをMainルーチン内から呼び出す処理を実装します( リスト6 )。
ImageCacheクラスの処理をEntryPointモジュールでコール
Sub CacheImage(ByRef rVram As ImageCache, ByRef rImageStructure As SWImageStructure)
' ImageCacheに登録.
rVram.Register (rImageStructure)
End Sub
Sub SetupImageCache(ByRef rVramSystem As ImageCache, ByRef aImage() As SWImage)
Dim nCnt As Integer
For nCnt = LBound(aImage) To UBound(aImage)
Call rVramSystem.Register(aImage(nCnt).image)
Next
End Sub
Mainルーチン
Sub Main()
Dim cRenderSystem As RenderingSystem
Set cRenderSystem = New RenderingSystem
Dim cVramSystem As ImageCache
Set cVramSystem = New ImageCache
' Rectの格納場所を用意.
Dim aRect() As Rect
Dim nCnt As Integer
' imageの格納場所を用意.
Dim aImage() As SWImage
' imagedrawの格納場所を用意.
Dim aImageDraw() As SWImageDraw
' 初期化処理をコール.
Call Initialize(cRenderSystem, cVramSystem)
' rectシートから、Rectをセットアップ.
Call SetupRect(aRect)
' imageシートから、SWImageをセットアップ.
Call SetupImage(aImage)
' Cacheをセットアップ.
Call SetupImageCache(cVramSystem, aImage)
' imagedrawシートから、SWImageDrawをセットアップ.
Call SetupImageDraw(aImageDraw)
' aRectの要素数に合わせてRenderingSystemのDrawRectをコール.
For nCnt = LBound(aRect) To UBound(aRect)
Call cRenderSystem.DrawRect(aRect(nCnt))
Next
' aImageの要素数に合わせてRenderingSystemのDrawImageをコール.
For nCnt = LBound(aImageDraw) To UBound(aImageDraw)
' Call cRenderSystem.DrawImage(aImageDraw(nCnt).pos, aImage(aImageDraw(nCnt).imageID).image)
' 上記の代わりにDrawCachedImageをコールする.
Call cRenderSystem.DrawCachedImage(aImageDraw(nCnt).pos, aImage(aImageDraw(nCnt).imageID).image)
Next
' 終了処理をコール.
Call Terminate(cRenderSystem, cVramSystem)
End Sub
動作確認
それでは動作確認をしてみましょう。設定関連はこれまでの図に出てきたものをそのまま使います。実行ボタンを押してみて、画像が正しく表示されることを確認してください。今回の設定では、framebufferシートは図8のような状態に、vramシートは図9のような状態にそれぞれなっているはずです。
ここまで動作確認ができたら、次に複数の画像を読み込ませてみましょう。VRAMの大きさを、400×300にして、128×128のテクスチャを4枚読み込ませ( 図10 )、framebufferシートとvramシートの状態を確認してみます( 図11 )。
いかがでしたでしょうか?これで多少効率的なシステムになりました。まだまだ改良の余地はありますし、今後回転やスケールなどの処理を実装した場合はこれまでとは全く異なった視点でのロジックを設計・実装しなければなりませんが、現段階ではそれなりに効率の良いものが出来上がったと思います。次回はここに新たな描画を追加したいと思います。前回と今回で実装した改善がその新たな描画で役に立つのかどうかという側面でも考えていきたいと思います。