Psychtoolbox(PTB)は、Matlabで心理物理学実験を行うためのツールボックスです。公式サイトはこちら。ちなみに私はPsychtoolboxをがんばるの管理人ですが、解説記事を書くのにQiitaのほうが使い勝手がよいので、これからはこちらで情報を共有していこうかと考え中です。
どうぞよろしくお願いいたします。
アルファブレンドをマスターしたい
アルファブレンド(アルファブレンディング)は2つの色を混ぜ合わせるときの方法を指定したもので、基本的な考え方は難しくはありませんが、いろいろな組み合わせがあるため分かりにくいです。私も毎度混乱するので、備忘録として残しておきます。
内容に誤りがあればご指摘ください。なお、PTBでのアルファブレンドはOpenGLのそれとほぼ同じです。
#PTBでの色表現
まずはPTBでの色表現から。基本的には次の3通りです。
% スカラーを指定
black = 0;
grey = 0.5;
white = 1;
% RGBチャンネルを指定
red = [1 0 0];
green = [0 1 0];
blue = [0 0 1];
% RGBに加えて、アルファチャンネル(透明度)を指定
red_with_transparency = [1 0 0 0.2]; % alpha = 0.2
green_with_transparency = [0 1 0 0.4]; % alpha = 0.4
PTBでは、Pattern1や2のように透明度(アルファ、A)を明記しない場合は、透明度=1.0となります。この場合、描画先を完全に不透明な色で描画(上書き)します。描画先の色が、描画元の色で入れ替わると思ってもよいです。
従来は0から255の数値を使って色を指定する方法が主流でしたが、最近は0から1に正規化された数値を使うことが推奨されています。PsychDefaultSetup(2);
を呼び出すことで、0から1の数値表現が有効になります。
#アルファブレンドとは?
模式図を示します。
画面になにかを描画しようとしたとき、そこには描画元の色情報と描画先の色情報があります。
ここでは便宜的に、描画元の色情報を (R1, G1, B1, A1)、描画先の色情報を (R2, G2, B2, A2)とします。RGBは赤緑青を表し、Aは透明度(アルファ)を表します。RGBAはチャンネルとも呼ばれます。
英語のマニュアルを読むときの参考までに、描画元はソース(SOURCE, SRC)、描画先はデスティネーション(DESTINATION, DST, あるいはTARGET)と表されることが多いです。上の模式図でソースアルファと書いていますが、それは描画元のアルファを意味します。
画面に表示される最終的な色は、次の式で計算されます。
最終的な色 = 描画元の色 x a1 + 描画先の色 x a2
つまり、描画元の色を何倍かして、同様に描画先の色も何倍かして、それらを足し合わせるだけです。
ここで分かりにくいのは、a1=A1(描画元のアルファ)、あるいは a2=A2(描画先のアルファ)とは限らないという点です。 a1やa2の組み合わせがいろいろあるので分かりにくいんです。
しかし逆に言えば、アルファブレンドとは、a1とa2をどのように指定するか、ということに過ぎません。a1とa2の組み合わせによって、画面に提示される画像の見た目が劇的に変化します。
よく使われるアルファブレンド
よく使われる、一番人気のブレンディングは次の通りです。
最終的な色 = 描画元の色 x A1 + 描画先の色 x (1 - A1)
# A1は描画元のアルファ
PTBでこれを実現するには、
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
です。引数の3番目(GL_SRC_ALPHA)が描画元に適用される係数(上の説明でのa1)で、引数の4番目(GL_ONE_MINUS_SRC_ALPHA)が描画先に適用される係数(a2)です。
例えば描画元のアルファが0.3であれば、描画元のRGBのそれぞれに0.3をかけて、描画先のRGBのそれぞれに0.7(=1-0.3)をかけて足し合わせます。
このとき、描画先のアルファ(A2)はまったく使われない点に注意をしてください。つまりA2=0.5だろうが、A2=1.0だろうが、BlendFunctionが上のように設定されているかぎり、ブレンディングの結果は変わりません。
では、具体的な数値で見てみましょう。
描画元の色情報 RGBA = [1 1 0 0.5]
描画先の色情報 RGBA = [0.5 0.2 0.4 1]
とします。最終的な色は、
最終的な R = 1 x 0.5 + 0.5 x (1 - 0.5) = 0.75
最終的な G = 1 x 0.5 + 0.2 x (1 - 0.5) = 0.6
最終的な B = 0 x 0.5 + 0.4 x (1 - 0.5) = 0.2
最終的な A = 0.5 x 0.5 + 1 x (1 - 0.5) = 0.75
RGBだけでなく、A(アルファ、透明度)にも計算式が適用される点に注意してください。なお、この最終的な色情報は、描画先の新しい色情報と言い換えることもできます。
実際に確認
チュートリアルで公開されているコードを使って説明します。
次のコードは、画面の左上に白い正方形を提示します。ブレンディングの機能はまだ使っていません。グレーの背景に、白い正方形を提示するだけです。
% Clear the workspace and the screen
sca;
close all;
clearvars;
% Here we call some default settings for setting up Psychtoolbox
PsychDefaultSetup(2);
% Get the screen numbers. This gives us a number for each of the screens
% attached to our computer.
screens = Screen('Screens');
% To draw we select the maximum of these numbers. So in a situation where we
% have two screens attached to our monitor we will draw to the external
% screen.
screenNumber = max(screens);
% Define black and white (white will be 1 and black 0). This is because
% in general luminace values are defined between 0 and 1 with 255 steps in
% between. All values in Psychtoolbox are defined between 0 and 1
white = WhiteIndex(screenNumber);
black = BlackIndex(screenNumber);
% Do a simply calculation to calculate the luminance value for grey. This
% will be half the luminace values for white
grey = white / 2;
% Open an on screen window using PsychImaging and color it grey.
[windowPtr, windowRect] = PsychImaging('OpenWindow', screenNumber, grey, [10 10 800 800]);
imageMatrix = ones(80); % 輝度の強度のみ。80 x 80のすべてのピクセルに1が入っているイメージです。
textureIndex=Screen('MakeTexture', windowPtr, imageMatrix); % 描画元となるテクスチャーを作成
dstRect = Screen('Rect', textureIndex); % テクスチャーの四角領域
% 正方形1
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 100, 150));
% ここにコードを加えていく
Screen('Flip', windowPtr); % 画面を更新
% Now we have drawn to the screen we wait for a keyboard button press (any
% key) to terminate the demo.
KbStrokeWait;
% Clear the screen.
sca;
描画元と描画先は以下の図のようになります。
補足説明です。
描画元について
imageMatrix = ones(80);
上述の Pattern1のように、80x80のすべてのピクセルのRGBAチャンネルに1(白色)が与えられていると思ってください。
textureIndex=Screen('MakeTexture', windowPtr, imageMatrix);
imageMatrixからテクスチャーを作成します。多少語弊がありますが、数値の情報から画像(視覚刺激)を作成する作業です。このテクスチャーはアルファブレンドされない限り白色で、以下の例で何度も(正方形10まで)使い回されます。
描画先について
描画先となる背景色はgrey = 0.5
です。RGBA = [0.5 0.5 0.5 1.0]
と同じです。透明度アルファは特に指定していないのでデフォルトの1.0です。
DrawTextureを使っていますが、このヘルプは、Matlabのコマンドウィンドウで、
Screen DrawTexture?
とすると表示されます。こちらの説明も役に立つかもしれません。
CenterRectOnPoint(dstRect, 100, 150)
で正方形を描画する位置を指定しています。座標(100, 150) を中心として正方形が描画されます。正方形の幅と高さは80ピクセルです。CenterRectOnPointの詳細は、Matlabのコマンドウィンドウでhelp CenterRectOnPoint
とすると確認できます。
% ここにコードを加えていく
という箇所があるかと思いますが、以下で説明することはすべてここに書き加えていってください。
アルファブレンドをしてみよう
% 正方形2
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 200, 150));
この2行を加えてみてください。ブレンドの方法は一番人気の、
最終的な色 = 描画元の色 x A1 + 描画先の色 x (1 - A1)
ですね。
BlendFunctionを指定したので、ブレンディングされるかな・・・と思いきや、白い正方形がふたつ並んで表示されると思います。(左側の正方形1はアルファブレンドを指定していない正方形。右側の正方形2はアルファブレンドを指定した正方形)
ただし、ブレンドされていないわけではありません。
正方形2については、描画元の色情報に描画元のアルファ(ソースアルファ)をかける設定(GL_SRC_ALPHA)になっていますが、描画元のアルファは具体的にいくつでしょう?
答えは1.0です。
さらに描画先の係数は、GL_ONE_MINUS_SRC_ALPHAなので、1から1を引いてゼロ。つまり描画先の色情報にゼロがかけられます。
最終的な色 = 描画元の色 x A1 + 描画先の色 x (1 - A1)
で、A1=1.0ということですから、
最終的な色 = 描画元の色
となりますね。その結果、ブレンドされていないように見えるのです。
globalAlpha
ブレンドの結果が見えるように正方形3を表示してみましょう。
imageMatrixに適切なアルファチャンネルを指定してもよいのですが、ここでは、DrawTextureのglobalAlphaを使ってみます。すでに設定されているアルファ値を、globalAlphaで更新することができます。globalAlphaはDrawTextureの8番目の引数です。
% 正方形3 globalAlphaを使って、ソースアルファを0.5にしてみる。
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 300, 150), [], [], 0.5);
上のコードを加えると3つ目の正方形が表示され、その色が明るいグレーになっていると思います。これは描画元の白色と描画先(背景色)のgreyがブレンドされた結果です。どのようにブレンドされるかというと、
描画元(白色 = 1.0) x 0.5 + 描画先(グレー = 0.5) x 0.5 = 0.75
です。アルファチャンネルもブレンドされることに注意してください。RGBA = [0.75 0.75 0.75 0.75]
です。やっとブレンド結果を見ることができました!
modulateColor
globalAlphaではなく、modulateColorを使う方法もあります。modulateColorはDrawTextureの9番目の引数です。ちなみに、globalAlphaとmodulateColorの両方を指定すると、globalAlphaが無視されます。
modulateColorを指定すると、描画元のRGBAチャンネルの数値をmodulate(変調)、分かりやすく言えば拡大・縮小することができます。例えば、
SRC_RGBA = [0.8 0.6 0.4 1]; % 描画元の色情報
modulateColor = [1 0.5 0.25 0.5];
の場合、
R = 0.8 x 1 = 0.8
G = 0.6 x 0.5 = 0.3
B = 0.4 x 0.25 = 0.1
A = 1 x 0.5 = 0.5
となります。またこれらの数値はあくまで描画元の色情報であり、この色情報に対してさらにアルファブレンドがほどこされます。
また、以下の説明ではScreen('BlendFunction')
を記述していませんが、記述していないときは直前のブレンド設定が引き継がれます。いまのところ、
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
の設定が引き継がれます。
modulateColorの動作確認をしてみましょう。次のコードを追加して正方形4を表示します。
% 正方形4
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 400, 150), [], [], [], [1 1 1 0.5]);
正方形4の見た目は正方形3と同じになるはずです。上の例では、
modulateColor = [1 1 1 0.5]
となっています。描画元の色情報はRGBA = [1 1 1 1]です。これに対してmodulateColorが適用されます。RGBチャンネルに変化はありません。アルファだけが0.5倍されて、1 x 0.5 = 0.5
になります。
では、次のコードはどんな正方形を描画するでしょうか。
% 正方形5
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 500, 150), [], [], [], [1 0 0 1]);
modulateColor = [1 0 0 1]
となっています。RとAについては描画元の情報をそのまま使いますが、GとBについてはゼロになります。
最終的なRとA = 描画元の色 x A1 + 描画先の色 x (1 - A1) = 1 x 1 + 0.5 x 0 = 1
最終的なGとB = 描画先の色 x (1 - A1) = 0
正方形5は真っ赤な正方形になります。
正方形6は正方形5とほぼ同じですが、ソースアルファ(描画元のアルファ)が0.5倍されます。描画元のGとBについてはmodulateColorによってゼロになります。
% 正方形6
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 600, 150), [], [], [], [1 0 0 0.5]);
最終的なR = 描画元(1) x 0.5 + 描画先(0.5) x (1 - 0.5) = 0.75
最終的なG = 描画先(0.5) x (1 - 0.5) = 0.25
最終的なB = 描画先(0.5) x (1 - 0.5) = 0.25
最終的なA = 描画元(0.5) x 0.5 + 描画先(1) x (1 - 0.5) = 0.75
となり、ややくすんだ感じの赤い正方形が表示されます。
GL_ONE と GL_ZERO
ブレンドの方法を変えてみましょう。
% 正方形7
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 100, 300), [], [], [], [1 0 0 0.5]);
まずは
描画元の色情報 RGBA = [1 1 1 1]
であることを思い出してください。
modulateColor = [1 0 0 0.5]
ですから、描画元の色情報が次のようにmodulateされます。
描画元の色情報 RGBA = [1 0 0 0.5]
さらに、描画元の係数がGL_SRC_ALPHA、描画先の係数がGL_ONE
になっていることから、次のような結果になります。
最終的なR = 描画元(1.0) x 0.5 + 描画先(0.5) x GL_ONE(1.0) = 1.0
最終的なG = 描画先(0.5) x GL_ONE(1.0) = 0.5
最終的なG = 描画先(0.5) x GL_ONE(1.0) = 0.5
最終的なA = 描画元(0.5) x 0.5 + 描画先(1.0) x GL_ONE(1.0) = 1.0(1.0以上にはならない)
次に、GL_ZEROを使ってみます。
% 正方形8
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ZERO);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 200, 300), [], [], [], [1 0 0 0.5]);
正方形7とほぼ同じですが、描画先の係数がGL_ZERO
であるため、描画先の色情報はすべて失われます。その結果、
最終的なR = 描画元(1.0) x ソースアルファ(0.5) = 0.5
最終的なG = 0
最終的なG = 0
最終的なA = 描画元(0.5) x ソースアルファ(0.5) = 0.25
となります。
colorMaskNew
Screen('BlendFunction')
の5番目の引数で、colorMaskNewを指定することができます。詳しい説明はこちら
簡単に言えば、RGBAのそれぞれにおいて、ブレンドを有効にしたり無効にしたりできます。例えば、
colorMaskNew = [1 0 0 1]
の場合は、RとAチャンネルについてのみブレンドを実行します。基本的に0または1を指定します。0.5などを指定しても意味がありません。0が指定されたときは、描画先の色情報がそのまま使われることを意味します。
例を見てみましょう。
% 正方形9 Blueチャンネルだけをブレンドする。その他のチャンネルは描画先のものを維持
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ZERO, [0 0 1 0]);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 300, 300));
colorMaskNew = [0 0 1 0]
なので、B以外のRGAの3つのチャンネルについては描画先の色情報(グレー)がそのまま使われます。
Bについてのみ、次のようなブレンドがなされます。
最終的なB = 描画元 x GL_SRC_ALPHA + 描画先 x GL_ZERO = 1.0
描画元 = 1.0、GL_SRC_ALPHA = 1.0であることに注意してください。
またこれも大切な点ですが、colorMaskNewはいったん設定すると、その後もずっと引き継がれます。
例えば、正方形9の下に以下のコードを加えてみます。
% 正方形10 colorMaskNewが引き継がれることに注意
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ZERO);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 400, 300), [], [], [], [0, 0, 1, 0.7]);
まず、modulateColor = [0, 0, 1, 0.7]
によって描画元の色情報が次のようになります。
描画元のRGBA = [0 0 1 0.7]
そして、colorMaskNewの設定が引き継がれているため、Bチャンネルのみにブレンドが適用されます。
最終的なB = 描画元(1.0) x GL_SRC_ALPHA(0.7) + 描画先 x GL_ZERO = 0.7
となります。RGAについては描画先の色情報がそのまま使われます。
アルファチャンネルを設定する
いままでの例では、透明度(アルファ)はglobalAlphaやmodulateColorを使って指定していました。この方法以外にも、単純にアルファチャンネルを設定する方法があります。
imageMatrix = ones(80, 80, 3); % RGBチャンネル
imageMatrix(:,:,4) = 0.5; % A(alpha)チャンネル
textureIndex=Screen('MakeTexture', windowPtr, imageMatrix);
このように設定して、次の正方形11は何色になるでしょうか?
% 正方形11
Screen('BlendFunction', windowPtr, GL_ONE, GL_ZERO, [1 1 1 1]);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 100, 450));
これはちょっと引っかけ問題みたいになっていますが、白色の正方形が表示されます。ブレンドされていないように見えますが、
Screen('BlendFunction', windowPtr, GL_ONE, GL_ZERO, [1 1 1 1]);
となっていて、係数にアルファがまったく含まれていないですね。描画元の係数にGL_ONE、描画先の係数にGL_ZEROを指定すると、描画先の色情報が描画元の色情報と単純に入れ替わります。
気を取り直して、次のようにしてみましょう。
% 正方形12
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 200, 450));
globalAlphaもmodulateColorも指定していませんが、ブレンドされた正方形が表示されたと思います。
正方形の色は、RGBAのいずれも次のようになります。
RGBA = 描画元(1.0)x0.5 + 描画先(0.5)x0.5 = 0.75
GL_DST_ALPHA
ここまでの例では描画先(デスティネーション, DST)のアルファをまったく使っていませんでした。
最後は描画先のアルファを使ってみましょう。正方形13と14のふたつの正方形を描画しますが、14は13の右下に描画され、重複する部分が存在します。
% 正方形13
imageMatrix = ones(80, 80, 3); % RGBチャンネル
imageMatrix(:,:,4) = 0.4; % A(alpha)チャンネル
textureIndex=Screen('MakeTexture', windowPtr, imageMatrix);
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ZERO, [1 0 0 1]);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 300, 450));
% 正方形14
Screen('BlendFunction', windowPtr, GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA, [1 1 1 1]);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 320, 470));
やや複雑な例ですが、順番に考えてみましょう。
正方形13の描画元のRGBA = [1 1 1 0.4]
正方形13の描画先のRGBA = [0.5 0.5 0.5 1]
正方形13については、colorMaskNew = [1 0 0 1]
としているため、RとAチャンネルのみがブレンドされます。描画元の係数はGL_SRC_ALPHAで、描画先の係数はGL_ZEROです。
グレーの背景とブレンドされた正方形13・・・①
R = 描画元(1) x 0.4 = 0.4
G(ブレンドされない) = 0.5
B(ブレンドされない) = 0.5
A = 描画元(0.4) x 0.4 = 0.16
続いて正方形14についてですが、14の一部は13の上に描画されます。
まずは重複しない部分について考えてみましょう。
正方形14の描画元のRGBA = [1 1 1 0.4]
重複しない描画先のRGBA = [0.5 0.5 0.5 1]
正方形14については、colorMaskNew = [1 1 1 1]
としているため、すべてのチャンネルがブレンドされます。描画元の係数はGL_DST_ALPHAで、描画先の係数はGL_ONE_MINUS_DST_ALPHAです。いずれも初めての係数ですね。でも考え方は同じです。描画先のアルファが1であることに注意して計算します。
重複していない部分について
正方形14のRGB = 描画元(1) x 1 + 0.5 x (1 - 1) = 1
正方形14のA = 描画元(0.4) x 1 + 0.5 x (1 - 1) = 0.4
つまり、重複していない部分は白色になります。
そして重複部分については、描画先が背景とブレンドされた後の正方形13であることに留意します。
正方形14の描画元のRGBA = [1 1 1 0.4]
重複している描画先のRGBA = [0.4 0.5 0.5 0.16]・・・上の①
GL_DST_ALPHA = 0.16ですから、
最終的なR = 描画元(1) x 0.16 + 0.4 x (1 - 0.16) = 0.496
最終的なG = 描画元(1) x 0.16 + 0.5 x (1 - 0.16) = 0.58
最終的なB = 描画元(1) x 0.16 + 0.5 x (1 - 0.16) = 0.58
最終的なA = 描画元(0.4) x 0.16 + 0.16 x (1 - 0.16) = 0.1984
まとめ
- 描画元の色情報と描画先の色情報を確認
- 描画元にも、描画先にも、色情報のRGBと透明度アルファAが存在
- 色情報にかけられる係数はなにか(描画元のアルファ? 描画先のアルファ? あるいは1からアルファを引いたもの?)
- 描画元の色情報と描画先の色情報のそれぞれに係数をかけて、足し合わせたものが最終的な色