ホーム > お知らせ > コラム

CANDERA開発者コラム
第13回 VBAテクスチャ描画システムを拡張する(4)
~アルファブレンド(後編)~

 

はじめに


それでは、これまで数回にわたりアルファブレンドの基本について考えてきました。今回はその実装回となります。前回お話しましたとおり、今回は、Src over Dstのブレンドモードを実装します。また、計算の簡略化のために、描画面と使用する絵素材は、乗算済みアルファのものとします。さらに、Rectの描画については、今回はアルファブレンドは考慮しないこととします。それでは順番に実装を進めていきましょう。

 

今回の拡張で必要な機能


Src over Dstで、乗算済みの描画面、絵素材を前提とした上で今回は、以下の内容を実装していきます。

  • 半透明の画像をコンバートする仕組み
  • Excel上で対象の画像が半透明かどうかを判別させる仕組み
  • 半透明の描画処理

 

半透明画像をコンバートする仕組み

さて、まずは半透明画像をコンバートする仕組みを整えましょう。改めてコンバート環境を見直すと、これまで実装した画像コンバータは、ストレートアルファの画像を出力するようになっています。また、アルファチャネル付きかそうでないかで、画像の種別も判別できるようになっています。これは、かつて実装した画像のコンバートプログラムの定義からも読み取れます。

リスト1. コンバートツール側の構造体定義( ソースコードを見る )
#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;
リスト1. コンバートツール側の構造体定義

 

このツールでは、アルファ付きテクスチャは、ストレートアルファの画像になるわけですが、今回は画像を乗算済みアルファとして取り扱うと決めました。このツール自身に修正を加えることもできるのですが、今回は、画像データを展開した際に、乗算済みアルファに変換することとしましょう。つまり、VBA上で、アルファ付きテクスチャの場合は、各チャネルにアルファ値を乗算することになります。

ところで、現在Excel側にはアルファチャネルを取り扱うための構造がありません。
後の描画処理の方でも必要になりますので、ひとまずここでは、アルファチャネルの値をVBA側で処理できるような構造を作ります。ColorRGBをColorRGBAに拡張し、関係個所を全てColorRGBAに変更すると良いでしょう。ただし、Aについてはここではあくまで値を格納するための場所を準備すると考えておきましょう。

リスト2. VBA側の機能拡張( ソースコードを見る )
' -----------------------------------
' Types(抜粋)'
' 変更前.
Public Type ColorRGB
    R As Integer
    G As Integer
    B As Integer
End Type

color As ColorRGB

' 変更後.
Public Type ColorRGBA
	R As Integer
	G As Integer
	B As Integer
	A As Integer
End Type

color As ColorRGBA

' -----------------------------------
' RenderingSystem(抜粋)'
' 変更前.
Private m_color As ColorRGB
Function nGetColor() As ColorRGB
Dim sColor As ColorRGB

' 変更後.
Private m_color As ColorRGBA
Function nGetColor() As ColorRGBA
Dim sColor As ColorRGBA

' -----------------------------------
' ImageCache(抜粋)'
' 変更前.
Dim sColor As ColorRGB

' 変更後.
Dim sColor As ColorRGBA
リスト2. VBA側の機能拡張

 

Excel上で対象の画像が半透明かどうかを判別させる仕組み

次に、これから描画しようとしているものに対してアルファブレンドを適用させるかさせないかをどう判断させるか考えてみましょう。半透明の画像を読み込んだならば全てをアルファブレンドすべきなのでしょうか?確かにそう判定すれば機械的な判定ができるかもしれません。しかし、アルファ付きの画像ではあるものの、実は描画面との合成をさせたくない場合ももしかしたらあるかもしれません。どうしたいかはユーザ側に委ねるというのが良い選択ではないでしょうか。今回は、画像の描画においてのみアルファブレンドを行うかどうかのオプションを追加することとします。「imagedraw」シートに、表1のように新たな列を追加します。0ばブレンドを行わず、1はブレンドを行うとします。

表1. Blend列の追加

ID ImageID PosX PosY Blend
0 0 0 0 0
1 1 64 64 0
2 2 128 0 0
3 3 192 64 0

 

 

次にその設定を解釈できるように構造体の定義と設定の読み込み部分を拡張します。まずは、SWImageDraw構造体に、blendTypeというInteger形のメンバを追加します(リスト3)。続いて、ImagedrawシートのBlend列の内容を読み込めるように処理を追加します(リスト4)。ここまで実装が完了したら、EntryPointのSetupImageDrawの末尾などでBreakPointを配置して、aImageDrawに新たに追加したプロパティが反映されているかどうかを確認しましょう。

リスト3. SWImageDraw構造体の拡張( ソースコードを見る )
Public Type SWImageDraw
    id As Integer
    imageID As Integer
    pos As Position
    blendType As Integer ' このプロパティを追加.
End Type
リスト3. SWImageDraw構造体の拡張

 

リスト4. SetupImageDrawの拡張( ソースコードを見る )
Sub SetupImageDraw(ByRef aImageDraw() As SWImageDraw)
    ' まずはimagedrawシートの有効な行の数を調べる.
    Dim nRowCnt As Integer
    Dim nElementCnt As Integer
    
    Dim nRows As Integer
    With Worksheets("imagedraw")
        nRows = .Cells(.Rows.Count, 1).End(xlUp).Row - 1
    End With
    
    ReDim aImageDraw(nRows - 1)
    
    For nRowCnt = 2 To 2 + nRows - 1
        With Worksheets("imagedraw")
            ' 1列目は管理番号.
            aImageDraw(nElementCnt).id = .Cells(nRowCnt, 1).Value
            
            ' 2列目はImageID.
            aImageDraw(nElementCnt).imageID = .Cells(nRowCnt, 2).Value
            
            ' 3列目からは座標.
            aImageDraw(nElementCnt).pos.x = .Cells(nRowCnt, 3).Value
            aImageDraw(nElementCnt).pos.y = .Cells(nRowCnt, 4).Value

            ' 5列目はBlend設定.
            ' ここを追加!.
            aImageDraw(nElementCnt).blendType = .Cells(nRowCnt, 5).Value
            
            nElementCnt = nElementCnt + 1
        End With
    Next nRowCnt

End Sub
リスト4. SetupImageDrawの拡張

 

半透明の描画処理

では、いよいよ実際のブレンドの処理を実装していきましょう。まずは、画像の読み込み部分です。現在は画像データをそのまま読み込むような仕組みになっていますが、前述のとおり、アルファチャネル付きの画像データについては、乗算済みに変換する必要があります。このため、ファイルから配列の値に各ピクセルの値を格納するための構造体、SWImageStructureのdataメンバにおけるアルファ値以外については、アルファ値を乗算する必要があります。こうすることで、乗算済みアルファの色味でVRAM側にもキャッシュされることになります(リスト5)。

リスト5. LoadImageの拡張( ソースコードを見る )
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

	' 乗算済み変換に必要な変数.
    Dim nRowCnt As Long: nRowCnt = 0
    Dim nColCnt As Long: nColCnt = 0
        
    Dim nPixelPosition As Long: nPixelPosition = 0
    
    Dim nBytesPerPixel As Integer: nBytesPerPixel = 4
        
    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

    If IMAGE_FORMAT_TYPE_RGB = rImageStructure.format Then
        nBytesPerPixel = 3
    End If
    
    ' 次の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)

	' 乗算済みに変換する.
	' ここを追加!.
	' アルファチャネルが存在している場合.
    If 4 = nBytesPerPixel Then
        For nRowCnt = 0 To rImageStructure.height - 1
            For nColCnt = 0 To rImageStructure.width - 1
                nPixelPosition = nBytesPerPixel * (nColCnt + nRowCnt * rImageStructure.width)

                ' Alpha.
                Dim nAlpha As Integer: nAlpha = rImageStructure.data( nPixelPosition + 3 )
                Dim nWork As Long: nWork = 0

                ' R.
                nWork = CLng(CLng(rImageStructure.data(nPixelPosition)) * nAlpha / 255)
                rImageStructure.data(nPixelPosition) = CInt(nWork)
                ' G.
                nWork = CLng(CLng(rImageStructure.data(nPixelPosition + 1)) * nAlpha / 255)
                rImageStructure.data(nPixelPosition + 1) = CInt(nWork)
                ' B.
                nWork = CLng(CLng(rImageStructure.data(nPixelPosition + 2)) * nAlpha / 255)
                rImageStructure.data(nPixelPosition + 2) = CInt(nWork)

            Next nColCnt
        Next nRowCnt
    End If
    
    ' 読みたての場合はCacheは存在しない.
    rImageStructure.posUV.x = -1
    rImageStructure.posUV.y = -1
    
    ' 対象のファイルを閉じる.
    Close #fd
    
End Sub
リスト5. LoadImageの拡張

 

描画面側には特に対応は必要ありません。これは、乗算済み同士での混色を行う場合、前回の式(1)及び(2)の内容からも分かるとおり、描画面側のアルファ値は考慮する必要はなく、これから描画しようとしているもののアルファ値を考慮すれば良いためです。

$$ C_{Result\_PP} = ( A_{Src} + C_{Src} + ( 1.0 - A_{Src} ) * A_{Dst} * C_{Dst} ) )\tag{1} $$
$$ 
\begin{align}
    C_{Src\_PP} &= A_{Src} * C_{Src} \\
    C_{Dst\_PP} &= A_{Dst} * C_{Dst} 
\end{align}
$$
$$ C_{Result\_PP} = 1.0 * C_{Src\_PP} + ( 1.0 - A_{Src} ) * C_{Dst\_PP}\tag{2} $$


次にアルファブレンドの処理を実装しましょう。アルファブレンドはピクセル単位で行う必要がありますので、1セル1ピクセルのこの環境では、セルごとにループを回してブレンド処理を行うことになります。そこで、高速化前に使っていたピクセルごとにコピーを行う処理を改良し、ブレンド処理を実装します。今回の描画面はアルファチャネルは常に1であるとみなせますので、アルファチャネルの計算は不要となります(リスト6)。

リスト6. DrawImageの拡張
Sub DrawImage(ByRef pos As Position, ByRef rImageStructure As SWImageStructure)
    ' rImageStructure.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 ColorRGBA
    
    ' ピクセル辺り何Bytesか( デフォルトは4 ).
    Dim nBytesPerPixel As Integer: nBytesPerPixel = 4

    ' Alpha値を格納するための変数.
    Dim nAlpha As Integer: nAlpha = 255

    ' 描画面の色を格納しておくための変数.
    Dim nFBColor As Long: nFBColor = 0
    Dim sFBColor As ColorRGBA
    
    ' ただし、画像フォーマットが0番の場合は3.
    If IMAGE_FORMAT_TYPE_RGB = rImageStructure.format Then
        nBytesPerPixel = 3
    End If
    
    nRowStart = pos.y + 1
    nColStart = pos.x + 1
    
    With Worksheets("framebuffer")
        ' 幅( 列 )を塗りきったら改行.
        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)

                
                ' この形式では色はRGB(A)の順番で格納されている.
                If 4 = nBytesPerPixel Then
                    ' アルファチャネルが格納されている場合はその値を取得.
                    nAlpha = rImageStructure.data(nPixelPosition + 3)

                    ' RGBチャネルについては乗算済み.
                    ' 係数は、ONE, ONE - SrcA.
                    ' 現在のピクセルの値を取得.
                    ' 描画面のAlphaは1なのでAlpha値の計算は不要.
                    nFBColor = .Cells(nRowStart + nRowCnt, nColStart + nColCnt).Interior.color
                    sFBColor.R = nFBColor Mod 256
                    sFBColor.G = CInt(Int(nFBColor / 256) Mod 256)
                    sFBColor.B = CInt(Int(nFBColor / 65536))

                    sColor.R = CInt(CLng(sColor.R) + CLng(sFBColor.R) * (255 - nAlpha) / 255)
                    sColor.G = CInt(CLng(sColor.G) + CLng(sFBColor.G) * (255 - nAlpha) / 255)
                    sColor.B = CInt(CLng(sColor.B) + CLng(sFBColor.B) * (255 - nAlpha) / 255)
                End If

                .Cells(nRowStart + nRowCnt, nColStart + nColCnt).Interior.color = RGB(sColor.R, sColor.G, sColor.B)
            Next nColCnt
        Next nRowCnt
    End With
End Sub
リスト6. DrawImageの拡張

 

そして、アルファブレンドを行うかどうかを先ほど定義した、SWImageDrawのblendTypeで判定します。ただし、SWImageDrawのblendTypeが1だったとしても、SWImageStructureのformatがIMAGE_FORMAT_TYPE_RGBの場合、DrawImageサブルーチンに入ってくると、画像データをピクセル単位でコピーする処理に流れてしまいます。この場合は、DrawCachedImage関数を使用した方がパフォーマンスがよくなります。今回はこのような判定処理をMainサブルーチンに入れます(リスト7)。

リスト7. ピクセル単位の評価の処理に流す分岐( ソースコードを見る )
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)

        Dim bBlend As Boolean: bBlend = False

        ' まずBlend設定を見る.
        If 1 = aImageDraw(nCnt).blendType Then
            ' 一旦はBlendすると仮定.
            bBlend = True
        End If

        ' 次に対象の画像の状態を見る.
        If bBlend Then
            If IMAGE_FORMAT_TYPE_RGB = aImage(aImageDraw(nCnt).imageID).image.format Then
                ' Blendしたいと言っているものの、3チャネルしかない画像はBlendしようとしても意味がない.
                bBlend = False
            End If
        End If

        If bBlend Then
            Call cRenderSystem.DrawImage(aImageDraw(nCnt).pos, aImage(aImageDraw(nCnt).imageID).image)
        Else
            Call cRenderSystem.DrawCachedImage(aImageDraw(nCnt).pos, aImage(aImageDraw(nCnt).imageID).image)
        End If
        
    Next
    
    ' 終了処理をコール.
    Call Terminate(cRenderSystem, cVramSystem)
End Sub
リスト7. ピクセル単位の評価の処理に流す分岐

 

動作確認

それでは、実際に動作確認をしてみましょう。前回用意した半透明のテクスチャを使ってみましょう。第11回で使用した画像を使い表2のような読み込み設定を行い、表3のように描画設定を行いましょう。

 

表2. imageシートに読み込み設定を追加

ImageID ImageName
0 tex0.bin
1 tex1.bin
2 tex2.bin
3 tex3.bin
4 Mask_BlueCircle.bin

 

表3. imagedrawシートに描画設定を追加

ID ImageID PosX PosY Blend
0 0 0 0 0
1 1 64 64 0
2 2 128 0 0
3 3 192 64 0
4 4 100 60 1

 

プログラムを実行し、図1のように半透明が無事に描画されれば実装完了です。

 

図1. 実行結果

 

この半透明の処理が完了するまではそれなりに時間がかかったと思います。半透明の処理は過去に描画されたものとの合成になりますので、ピクセルごとの処理になります。このため、描画を完了させるには面積分のループが必要となり、計算回数が増加することになります。これは、不透明の描画を1ピクセルずつ処理していた数回前の状態とほぼ同じ状況です。不透明の描画は省力化できますが、半透明はそうもいかないという現実があります。また、不透明のテクスチャだったはずなのに、なぜか半透明の処理が走ってしまうというケースもあると思います。それは、これまで実装してきた判定の抜け穴をついた絵素材になります。それは、「アルファチャネル付きではあるものの、その値が全て255である画像に対してBlend設定を有効にした場合」です。上記のBlend設定を行うかどうかの判定基準は、imagedrawのBlendが1になっており、なおかつテクスチャがアルファ付きであることです。残念ながら当該画像はこのケースを満たしてしまいます。このため意図的に不透明描画を行いたいという場合はBlendを0にした方が高速に処理できます。このように、少しパフォーマンス調整の余地を残しておいたのも今回の実装のポイントとなります。

おわりに


いかがでしたでしょうか?アルファブレンドを有効にしてしまうと、これまで高速化してきた処理が無駄になってしまいました。つまり、それだけアルファ値を使って半透明のテクスチャを描画するということは処理コストが高くなるということになります。これは、並列処理が得意なGPUを使用したときも同様で、PCなどでは気にならないコストかもしれませんが、組み込み機器などで高パフォーマンスを目指す場合、描画面積と半透明の描画に対しては常に気にしなければならない内容となります。私たちが普段目にしている様々なUIも、その大半はアルファチャネルによるブレンド処理が行われています。例えば完全な矩形ではなく、四角形の角を落としたようなものの表現は、画像で用意する場合は不要な部分のアルファ値を0にしたりグラデーションをかけたりします。このように、半透明ではないと思っているもので案外半透明の描画処理を行っているケースもあります。またアルファブレンドを行うよりも、頂点を作ってしまった方が良いというケースもあります。先ほどの例ですと、矩形をテクスチャのアルファで削るのではなく、角を落とした図形を作成し、そこに不透明のテクスチャを描画するという方法です。このように使用する環境によって何が得意か、何が苦手かが異なることがあり、そこに合わせた表現を選択していくというのがポイントになると思います。
これまで、数回にわたり、アルファブレンドの基本的な考え方の共有と、その実装をしてきました。これを通して、アルファブレンドとは処理負荷の高いもので、使いどころは考えた方が良いものだというのは体感できたと思います。表現力、ユーザ体験というのは製品の魅力や使いやすさに直結しますが、HMIを作成していく上においては、技術的な限界、制約についてもある程度目を向けて、良い落としどころを作っていくというのが鍵になるかもしれません。

◆CANDERA(カンデラ)とは?

加賀FEI株式会社では、CGI StudioとUI Conductorという2つの組み込み向けHMIオーサリングツールを開発~販売しており、車載向けメータークラスター、ヘッドアップディスプレイ、ナビゲーション、周辺監視システム、車載以外にもプリンター、デジタルカメラ、建設機械など、多くの機器で広くご採用頂いております。

 

◆バックナンバー

第1回  身の回りにあるHMI
第2回  OpenGL ESを用いた簡単な図形の描画
第3回  OpenGL ESを用いた簡単な図形の描画 実践編(前編)
第4回  OpenGL ESを用いた簡単な図形の描画 実践編(後編)
第5回  OpenVGを用いた簡単な図形の描画
第6回  ソフトウェアレンダリングを実装してみる(設計編)
第7回  ソフトウェアレンダリングを実装してみる(実装編)
第8回  VBAでテクスチャを描画してみる(前編)
第9回  VBAでテクスチャを描画してみる(後編)
第10回  VBAのテクスチャ描画システムを拡張する(1)
第11回  VBAのテクスチャ描画システムを拡張する(2) ~アルファブレンド(前編)~ 
第12回  VBAのテクスチャ描画システムを拡張する(3) ~アルファブレンド(中編)~