CANDERA開発者コラム
|
はじめに
それでは、これまで数回にわたりアルファブレンドの基本について考えてきました。今回はその実装回となります。前回お話しましたとおり、今回は、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;
このツールでは、アルファ付きテクスチャは、ストレートアルファの画像になるわけですが、今回は画像を乗算済みアルファとして取り扱うと決めました。このツール自身に修正を加えることもできるのですが、今回は、画像データを展開した際に、乗算済みアルファに変換することとしましょう。つまり、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
Excel上で対象の画像が半透明かどうかを判別させる仕組み
次に、これから描画しようとしているものに対してアルファブレンドを適用させるかさせないかをどう判断させるか考えてみましょう。半透明の画像を読み込んだならば全てをアルファブレンドすべきなのでしょうか?確かにそう判定すれば機械的な判定ができるかもしれません。しかし、アルファ付きの画像ではあるものの、実は描画面との合成をさせたくない場合ももしかしたらあるかもしれません。どうしたいかはユーザ側に委ねるというのが良い選択ではないでしょうか。今回は、画像の描画においてのみアルファブレンドを行うかどうかのオプションを追加することとします。「imagedraw」シートに、表1のように新たな列を追加します。0ばブレンドを行わず、1はブレンドを行うとします。
| 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
リスト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
半透明の描画処理
では、いよいよ実際のブレンドの処理を実装していきましょう。まずは、画像の読み込み部分です。現在は画像データをそのまま読み込むような仕組みになっていますが、前述のとおり、アルファチャネル付きの画像データについては、乗算済みに変換する必要があります。このため、ファイルから配列の値に各ピクセルの値を格納するための構造体、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
描画面側には特に対応は必要ありません。これは、乗算済み同士での混色を行う場合、前回の式(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
そして、アルファブレンドを行うかどうかを先ほど定義した、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
動作確認
それでは、実際に動作確認をしてみましょう。前回用意した半透明のテクスチャを使ってみましょう。第11回で使用した画像を使い表2のような読み込み設定を行い、表3のように描画設定を行いましょう。
| ImageID | ImageName |
|---|---|
| 0 | tex0.bin |
| 1 | tex1.bin |
| 2 | tex2.bin |
| 3 | tex3.bin |
| 4 | Mask_BlueCircle.bin |
| 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) ~アルファブレンド(中編)~