Objective-C フラグメントシェーダー事始め
PROGRAM
blockquote.samplecode
{
border: medium #A71DA7 double;
}
table.testcase th
{
white-space: nowrap;
}
table.testcase td
{
white-space: nowrap;
}
div.block-type
{
width: 360px !important;
overflow: auto !important;
}
div.block-type th
{
vertical-align: middle !important;
}
div.block-type td
{
width: 200px !important;
overflow: auto !important;
vertical-align: middle !important;
}
div.block-operation
{
width: 230px !important;
}
画像をモノトーンに変換したい
iPhone アプリで使うボタン画像を 1 色だけ用意してシーンに合わせて色を変えて使えたなら、矢印みたいな単純な画像を 1 枚用意するだけでいろんなプロジェクトでも使えるし、ユーザーに背景色を選んでもらってそれに合わせてボタンの色を変更したりできて便利そうです。
そう思って調べてみたところ、CIFilter で画像をモノトーンに変換する 方法が見つかりました。
たしかにこれでも色を単調化できたのですけど、単調化で使う色をベースにして、黒ほど黒に、白ほど白に、表現されるため、色があるほど全体的に指定した色よりも暗く仕上がるところが気になりました。
特に、黒は黒として表現されるため、写真の変換にはいいのですけど、ボタンのような画像には少し勝手が悪いように感じます。
既存のフィルタが合わないとなると、自分で画像を調整するしかなさそうですけど、用意された画像を 1 ピクセルずつ処理して調整するには、負荷がかかりそうで、こういったボタンの色を変更するような何気ない処理には向かないような気もします。
そんな中、iphone_dev_jp 東京勉強会で @kamiyan さんが発表されていた OpenGL ES 2.0 のフラグメントシェーダーという高速なレンダリング技術を思い出しました。
これなら、もしかすると理想に近いモノトーン変換処理が実用的なレベルで作成できるかもしれません。
そう思ってさっそく試してみたところ、イメージ通りに近いモノトーン変換を実現することができました。
この例では、指定した色を黒に適用して、そこから明るくなるにつれて白に近づけて行く方法を採ってフラグメントシェーダーを作成してみました。
これなら、CIFilter の CIColorMonochrome のような指定した色を基調に黒は黒、白は白に表現する方法とは違って、真っ黒で作成した記号的な画像であっても、指定した色で単調化することができます。
もっとも、自分でどういう風に画像を出力するかをフラグメントシェーダーで指定している訳なので、理想と違えば自分で調整できるところが何より最大の魅力です。
そんなフラグメントシェーダーですけど、さすが OpenGL という 3D レンダリングの技術だけあって、その方面の知識が何もなかった自分には、取っ掛かりも判らなければ、サンプルコードに書かれている意味も解らず、そもそも解らないところの調べ方さえ判りませんでした。
それでも順を追って少しずつ調べて行くうちに、ようやくプログラムを動かすことができるレベルまでたどり着くことができました。
まだまだ理解できていないところも多くて曖昧な記述になってしまいますけど、今回はモノトーン変換フィルタを作るまでに得られた知識を中心に、iOS で初めてフラグメントシェーダーを使うための、取っ掛かりを紹介する記事として記してみたいと思います。
OpenGL ES 2.0 フラグメントシェーダー予備知識
フラグメントシェーダー
OpenGL ES 2.0 フラグメントシェーダーとは、iOS 3.0 以上で利用可能な、画像をピクセルレベルで高速に処理できる仕組みです。
そもそもの OpenGL ES (OpenGL for Embedded Systems) というのは、iPhone や Android などの組み込みシステムでも利用できる 2D, 3D グラフィックス API です。ハードウェアの性能を活かして高速な画像処理が行えるのが特徴です。
フラグメントシェーダーでは、"シェーディング言語 (OpenGL Shading Language, GLSL)" というものを使ってシェーダープログラムを作成し、OpenGL ES 2.0 の機能を使ってそれをコンパイルして実行するという流れになります。
OpenGL ES 2.0 のシェーダーには、次の 2 つのシェーダーがあるようです。
- フラグメントシェーダー
- 頂点シェーダー(バーテックスシェーダー)
どちらかを使うというわけではなくて、ポリゴンの頂点座標やテクスチャ位置などを元データとして、頂点シェーダーではポリゴンデータをクリッピング座標系に変換し、それを元に得られた画素データをフラグメントシェーダーが受け取って加工するという流れになるようです。
頂点シェーダーとフラグメントシェーダーとの間では、OpenGL によってビューポートに合わせてラスタライズ(ビットマップ画像化)が行われ、そのビットマップをフラグメントシェーダーで加工して、最終的な画像を作り出します。
なお、頂点シェーダーでポリゴンデータをクリッピング座標に変換するときに、"平行投影変換" や "透視投影変換" という写像(行列)が必要になってくるようでした。
バッファーの種類
フラグメントシェーダーを使って画像を描画する場合、そのデータのやりくりをするバッファーが 2 つ必要になるようです。
- フレームバッファー
- レンダーバッファー
フレームバッファーは、頂点やアフィン行列というものなどを描画する上で必要なデータを扱うバッファーだそうです。
レンダーバッファーというのは、さらに 3 種類に分けられています。
カラーバッファー | 画像の色データを扱うレンダーバッファー | GL_COLOR_ATTACHMENT0 |
---|---|---|
Z バッファー | 3D グラフィックの印面消去で使うレンダーバッファー | GL_DEPTH_ATTACHMENT |
ステンシルバッファー | 3D グラフィックの印面消去で使うレンダーバッファー | GL_DEPTH_ATTACHMENT |
これらのレンダーバッファーを実際に使うときは、レンダーバッファーのハンドルを作成した上で、用途 (GL_COLOR_ATTACHMENT0 など) を指定して OpenGL ES のコンテキストに連結します。
フラグメントシェーダーで画像ファイルを加工するような場合であれば、2D グラフィックスの範囲で済むので、カラーレンダーバッファーだけを使うことになると思います。
ポリゴン
ポリゴンというのは、OpenGL ES で描画するときの、図形を構成する基本要素です。
ポリゴンの種類には、点、線、三角形、システムによってはそれ以上の多角形もあるようですけど、今回はその中でも、基礎的な三角形のポリゴンを知っておきたいところです。
まず、三角形なので 3 つの頂点の座標で描くことができます。
OpenGL ES 2.0 では glDrawArrays 関数に、描画する頂点の情報をまとめて渡すことで、複数のポリゴンを一度に描けるようになっています。
このとき、点の指定は順番と描き方の指定も重要になるようで、右回りに頂点を描くと表向き、左回りに頂点を描くと裏向きになるそうでした。
また、glDrawArrays で描く頂点は、描き方を指定することで、連続するポリゴンを 3 頂点全てを指定しなくても描けるようになっているようです。
GL_TRIANGLES | ポリゴン 1 つずつの 3 頂点を、省略せずに全て指定します。 | (A, B, C, B, C, D, C, D, E) と渡すことで、三角形 ABC, BCD, CDE を描画します。 |
---|---|---|
GL_TRIANGLE_STRIP | 最初のポリゴンを 3 頂点で指定したら、それ以降のポリゴンは 1 頂点を指定すれば、直前の 2 頂点を再利用して描画されます。 | (A, B, C, D, E) と渡すことで、三角形 ABC, BCD, CDE を描画します。 |
GL_TRIANGLE_FAN | 最初のポリゴンを 3 頂点で指定したら、それ以降のポリゴンは 1 頂点を指定すれば、いちばん最初の 1 頂点と、最後の 1 頂点を再利用して描画されます。 | (A, B, C, D, E) と渡すことで、三角形 ABC, ACD, ADE を描画します。 |
描く向きによってポリゴンの裏表があるという話をしましたが、こうやって連続してポリゴンを描く場合はどうなるのでしょう。
TRIANGLE_STRIP で試してみた感じだと、二つ目に描かれたポリゴンは左回りになりそうなものでしたけど、裏面を表示しない設定にしてみても、最初のポリゴンも二つ目のも、描画されるようでした。
カリング
特定の面を表示させないようにすることを、カリングというのだそうです。
カリングを有効にしたい場合には、glEnable(GL_CULL_FACE) 関数を実行して、たとえば裏向きを表示させないようにしたい場合は glCullFace(GL_BACK) を実行します。表向きを表示させない場合は GL_FRONT を指定します。
目に映らないポリゴンを描かないことで高速化が図れるそうですが、今回のように平面画像として正方形を 1 つだけ描くような場合は、隠れる面というものは出来てこないと思うので、今回は特に気にする必要はなさそうです。
アルファブレンド
描画する色に透明度が設定されていた場合に、その透明度に応じて後ろの色を透過させて描画する方法です。
アルファブレンドを有効にしたい場合は、glEnable(GL_BLEND) 関数を実行した上で、glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 関数を実行します。
こうすることで、透明度を操作するだけで、最終的な画像はそれを加味した描画になります。
また iOS の CAEAGLLayer を使っている場合は、こちらの opaque プロパティも併せて NO にしておく必要があるようでした。
テクスチャとミップマップ
テクスチャというのは、ポリゴンにはる画像です。
テクスチャは平面画像を (0.0, 0.0) - (1.0, 1.0) の空間に用意して、ポリゴンの任意の場所に貼ることでテクスチャを彩ります。
ポリゴンにテクスチャを貼るときには、必要に応じてテクスチャの拡大や縮小が行われますが、その時に補完方法として、双線形補完 (GL_LINER) や最近傍補完 (GL_NEAREST) が選択できるようになっています。
テクスチャの縮小時には、あらかじめ小さなミップマップをいくつか用意しておくことで、より綺麗にポリゴンを仕上げられるようにもなっています。
ミップマップは glTexImage2D 関数を使って割り当てられるようになっていて、オリジナルのテクスチャの詳細レベルを 0 として、ミップマップはより詳細(縮小版)なほど大きい詳細レベル値を指定して、割り当てて行くことになるようです。
または、glGenerateMipmap 関数を使って、アクティブに設定されているテクスチャのミップマップを自動生成することもできそうでした。
なお、ミップマップを作成するのには制約があって、テクスチャの画像サイズが 2 の累乗の場合に限られます。
ただし、OpenGL が "GL_ARB_texture_non_power_of_two" 拡張をサポートしている場合には、それに限らずミップマップを作成できるそうでした。
この拡張をサポートしているかは glutExtensionSupported("GL_ARB_texture_non_power_of_two") で確認できるらしいのですが、そもそも iOS 6.0.2 の OpenGLES 2.0 ではこの関数自体が用意されていないようでした。
シェーディング言語で使える機能
フラグメントシェーダーでも頂点シェーダーでも、シェーディング言語を使って処理を記述します。
データ型
シェーディング言語で使う変数はデータ型を指定する必要があり、例えば次のものを使って画像データを処理して行きます。
これらを使って構造体 (struct) を定義することもできるようです。
float | 単精度浮動小数点数 |
---|---|
int | 整数 |
bool | 真偽値 (true, false) |
単純な 1 つの値を表現するデータ型です。
double や uint といったデータ型は、iOS 6.0.2 の OpenGL ES 2.0 では使用できない様子でした。
float 型 | int 型 | bool 型 | |
---|---|---|---|
vec2 | ivec2 | bvec2 | 2 要素で構成される値 |
vec3 | ivec3 | bvec3 | 3 要素で構成される値 |
vec4 | ivec4 | bvec4 | 4 要素で構成される値 |
複数の要素で構成されるデータ型です。
値は value[0] のように配列で扱う方法の他にも、{ x, y, z, w } や { r, g, b, a } や {s, t, r, g } という名前を使って、それぞれの要素にアクセスできます。例えば value.y のように指定できます。
また、vec4 データでも value.rgb とすれば vec3 型のデータとして扱ったり、value.aaa というような取り出し方もできます。
float 型 | |
---|---|
mat2 | 2×2 の要素で構成される行列 |
mat3 | 3×3 の要素で構成される行列 |
mat4 | 4×4 の要素で構成される行列 |
複数の要素で構成されるデータ型です。
値は value[0][0] のように行と列とをインデックス番号で指定して扱います。また、各行の列の値はベクトル型の要素と同じように扱えます。例えば value[0].rgb といった感じに読み込めたり、value[1] = value.rgb というように代入できたりします。
他にも mat2x4 などの行列もシェーディング言語には定義されているようでしたが、iOS 6.0.2 の OpenGL ES 2.0 で試してみると、コンパイルエラーで使用できない様子でした。
sampler2D | 2D テクスチャのハンドル |
---|
他にも sampler3D や sampler2DRect, sampler2DArray などいろいろあるようです。
たとえば sampler2D 型のハンドルであれば、texture2D(handle, xy) 関数を使って、テクスチャのある座標上の色を取得したりといったことができます。
変数の精度
各変数には、データ型と合わせて精度を指定できるようです。
lowp | 低精度 |
---|---|
mediump | 中精度 |
highp | 高精度 |
精度の詳細はシェーダー言語や GPU に依存するらしいのと、詳しいことは解りませんが、シェーダー言語が演算する際にこの精度が影響してくるらしいですが、必要になったら気にしだせば大丈夫なような気がします。
0.0 から 1.0 で表す色のような情報であれば lowp で十分らしいですが、同じ 0.0 から 1.0 でも精度が必要になってくると mediump みたいな選択をするというお話もありました。
ちなみに、データ型に精度を指定したい場合には、変数宣言のところでこれらを指定します。
lowp vec3 value3;
データ型に既定の精度を設定したい場合には main 関数よりも前のコード先頭で、次のように指定します。
precision mediump float;
たとえばこのようにすることで、float 型を lowp の精度で扱うことを宣言できるそうです。
なお、iOS 6.0.2 の OpenGL ES 2.0 で試した感じでは、フラグメントシェーダーのプログラムではこの precision を使って float 型の精度を指定しないと、コンパイルエラーになってしまう様子でした。
値の組換えと型の変換
シェーダー言語では、スカラーやベクトルや配列を、簡単に相互に変換できるようになっていました。
float 型の value、vec2 型の value2、vec3 型の value3、vec4 型の value4 があったとして、たとえば次のようにして、値の組換えや型の変換が行えました。
rval = value4.a | vec4 型 { r, g, b, a } の a に該当するスカラー値を取得しています。 |
---|---|
rval3 = value4.rgb | vec4 型 { r, g, b, a } の rgb に該当する vec3 値を取得しています。 |
rval4 = vec4(value4.rgb, value) | vec4 型 { r, g, b, a } の rgb に該当する値と float 型の値を使って新しい vec4 値を取得しています。 |
rval3 = value3.xxx | vec3 型 { x, y, z } の x に該当する値だけを使って新しい vec3 値を取得しています。 |
rval3 = vec3(value4) | vec4 型 の値を vec3 型にキャストすることでも、先頭 3 要素を取り出した vec3 値を取得できます。 |
rval3 = vec3(1.0, 0.9, 0.8) | 要素の値を明示して vec3 型の値を取得しています。 |
rval4 = vec4(1.0) | 全部の要素が同じ値の場合は、値を 1 つ書くだけで、すべての値がそれになった vec4 を取得できます。 |
演算
シェーダー言語では、スカラー型だけでなく、ベクトル型や行列もそのまま演算できます。
たとえば、それぞれ vec4 型の変数をつかって次のように演算できます。
c4 = a4 + b4
こうすることで、それぞれの要素を足し合わせるられます。
mat4 なども同じように計算できて、演算のされ方はきっと数学で定義されている通りなのでしょう。
使える演算は C 言語とそっくりで、たとえば次のものがあるようです。
+ | 加算 |
---|---|
- | 減算 |
* | 乗算 |
/ | 除算 |
&& | 両方が true の場合に true |
---|---|
|| | どちらかまたは両方が true の場合に true |
! | true の場合に false |
<< | 左シフト |
---|---|
>> | 右シフト |
& | AND 演算 |
| | OR 演算 |
^ | XOR 演算 |
~ | NOT 演算 |
< | 左が右より小さい |
---|---|
<= | 左が右と同じか小さい |
> | 左が右より大きい |
>= | 左が右と同じか大きい |
== | 左と右が同じ |
!= | 左と右が異なる |
C 言語でおなじみの ++ 演算や += 演算などの代入演算子や、 ( ? : ) による条件演算子も使えます。
剰余演算 "%" も仕様にあるようでしたが、iOS 6.0.2 の OpenGL ES で試してみたところ、型が不適切というようなコンパイルエラーで上手く使えませんでした。単なる使い方の間違いかもしれません。
変数の種類
シェーダープログラムは main 関数から始まりますが、その時に外部から値を受け取ったりする変数を定義するときに、その変数の扱われ方をキーワードで指定するようになっています。
attribute | 頂点シェーダーで、プログラムから受け取る変数に指定します。 |
---|---|
uniform | 頂点シェーダーやフラグメントシェーダーで、プログラムから受け取る変数に指定します。 |
varying | 頂点シェーダーで代入して、フラグメントシェーダーでそれを受け取る変数に指定します。 |
これらの違いによって、変数の値をプログラムから渡さないといけなかったり、渡す方法が少しだけ違ったりするので、意識して使い分けるようにします。
定義済みの変数
シェーダープログラムでは、あらかじめ目的が決まっている変数があります。
gl_Position | 頂点シェーダーで、クリッピング座標系に変換した頂点の座標を設定する変数だそうです。これを元にポリゴンがクリッピングされ、ビューポート変換され、ラスタライズされて、ピクセルデータに変換されるそうです。 |
---|---|
gl_FragColor | フラグメントシェーダーで、ピクセルの色を決定する変数です。ここにセットされた色が、そのピクセルの色になります。この値の設定状況にかかわらず discard 命令を実行すると、そのピクセルは描かないことにもできるようでした。 |
シェーダー言語では、このような変数を使って最終的な画像を決定して行くことになります。
あらかじめ決められた変数は、これらの他にも、頂点シェーダーでは「gl_PointSize(点の大きさ)」「gl_ClipVertex(クリッピング座標)」という変数に設定できたり、フラグメントシェーダーでは「gl_FragDepth(奥行)」という変数を設定したり「gl_FrontFacing(ポリゴンの表に位置するか)を参照したりできるらしいですが、今の知識ではよく判りませんでした。
シェーダープログラムの雰囲気
main 関数
シェーダープログラムは、頂点シェーダーとフラグメントシェーダーのそれぞれが main 関数で始まります。
シェーディング言語の決まり事を踏まえて、シェーダープログラムを作成すると、頂点シェーダーのプログラムは次のような雰囲気になります。
attribute vec2 a_position;
attribute vec2 a_texCoord;
uniform mat4 u_matrix;
varying vec2 v_texCoord;
void main(void)
{
gl_Position = u_matrix * vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
頂点シェーダーがポリゴンを描くためにプログラムから値を受け取る attribute 変数とか、汎用的な値を受け取る uniform 変数とか、フラグメントシェーダーへの出力に使う varying 変数とかを、最初に宣言します。
そして main 関数を書き始め、その中で計算をして、最終的には gl_ で始まるあらかじめ定められた変数や、頂点シェーダーの場合はフラグメントシェーダーに渡すための varying 変数に値を設定します。
フラグメントシェーダーのプログラムも、頂点シェーダーのプログラムと雰囲気はほとんど同じです。
precision mediump float;
uniform lowp vec4 u_color;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
void main(void)
{
vec4 texcolor;
texcolor = texture2D(u_texture, v_texCoord.xy);
gl_FragColor = texcolor * u_color;
}
ただ、先頭で precision を使って float の精度を宣言しておかないと、コンパイルでエラーになってしまうようでした。
他は、書き方自体は特に難しいところはなくて、プログラムから受け取った uniform 変数と、頂点シェーダーから受け取った varying 変数を計算して、目的のピクセルの色を gl_FragColor に設定します。
制御構文
シェーディング言語では、C 言語によく似た条件分岐や繰り返し処理を使うことができます。
if (条件)
{
}
else if (条件)
{
}
else
{
}
条件に最初に合致するブロックだけが実行されます。else if や else ブロックは省略できます。
for (初期化; 終了条件; 変化式)
{
}
「初期化」では "vec4 i" のような型指定を伴う変数宣言も行えるようです。
繰り返しの度に「変化式」が実行され、「終了条件」を満たしたところでループを抜けます。
while (条件)
{
}
条件が満たされるまで、ブロック内を実行します。
do-while 文とは違い、最初にまず「条件」を判定します。
do
{
}
while (条件)
条件が満たされるまで、ブロック内を実行します。
while 文とは違い、最初にブロックを実行してから「条件」を判定します。
変数 = (条件 ? 真値 : 偽値)
「条件」が true の場合に「真値」が、false の場合に「偽値」が選択されます。
C 言語の制御構文と言えば他にも switch 文がありますが、iOS 6.0.2 の OpenGL ES 2.0 で試す限りでは、switch は予約語という理由でコンパイルエラーになってしまいました。
定数
シェーディング言語では定数を定義することもできます。
const vec4 cval4 = vec4(1.0, 1.0, 1.0, 1.0);
このように、変数宣言と合わせて const を指定して、併せて初期値を指定します。
宣言する場所は、関数内でも、最初の uniform 変数などを宣言する場所でも、どちらでも大丈夫そうです。
シェーダープログラムを iOS で実行する
シェーディング言語の大まかな様子が掴めたので、これからは順を追って、色を単調化するフラグメントシェーダーを iOS で実行する方法について見て行きたいと思います。
OpenGL ES を利用できるようにする
iOS プログラムで OpenGL ES 2.0 を使用するために、次のファイルをインポートします。
#import <GLKit/GLKit.h>
#import <QuartzCore/QuartzCore.h>
また、次の 2 つのフレームワークをリンクします。
- OpenGLES.framework
- QuartzCore.framework
そして、今回は UIView から派生したクラスで、画像を単調して表示するビューを作成しようと思います。
この時、UIView のレイヤーにフラグメントシェーダーで描画できるように、UIView から派生したクラスの layerClass メソッドをオーバーライドして、CAEAGLLayer クラスを返すようにします。
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
これで、下地的な準備が整ったことになるようでした。
検出用のマクロを定義する
OpenGL ES 2.0 では、関数ひとつがエラーになっても、glGetError 関数で確認しないと判りません。
慣れていないせいもあって、どこでエラーになりそうかを判断するのも難しいので、今回は OpenGL ES 2.0 の関数実行後に、エラーだった場合にはプログラムを中断するマクロを用意しておくことにしました。
#define EzOpenGLESAssert \
{ \
int glGetErrorResult = glGetError(); \
NSAssert1(glGetErrorResult == GL_NO_ERROR, @"glGetError() = 0x%X", glGetErrorResult); \
}
OpenGL ES 2.0 関数を実行した後に、続けてこの EzOpenGLESAssert マクロを実行すれば、エラーがあった場合は直ちにそこでプログラムが停止してくれます。
ちなみにこの glGetError 関数は、エラー番号を正しく取得できるとエラー番号がリセットされるようなので、判定の後にその番号を表示したい場合には、上記のように取得時に、変数にエラー番号を保存しておく必要があります。
また、シェーダープログラムをコンパイルした時にエラーになった場合に、エラーになった行番号や理由を簡単に表示できるようにする C 関数もひとつ用意しておくことにします。
void EzOpenGLESShaderCompileAssert(GLuint shader)
{
GLint infoLogLength = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);
if (infoLogLength > 0)
{
char* infoLog = (char*)malloc(sizeof(char)*infoLogLength);
glGetShaderInfoLog(shader, infoLogLength, NULL, infoLog);
NSLog(@"Compile error: %s", infoLog);
free(infoLog);
}
else
{
NSLog(@"Unknown compile error.");
}
assert(false);
}
この関数の引数にシェーダーのハンドルを渡すことで、そこからログメッセージを取得してデバッグコンソールに出力して停止します。
使用する変数を定義する
今回 UIView を派生して、そこでフラグメントシェーダーを使って描画を行うに当たって、そのクラスのクラス変数として、次のものを定義しておくことにしました。
@implementation EzMonoImage
{
// OpenGL ES のコンテキストです。
EAGLContext* mpGLContext;
// フレームバッファーのハンドルです。
GLuint mFrameBuffer;
// レンダーバッファーのハンドルです。
GLuint mColorBuffer;
// レンダーバッファーの幅と高さを保存します。
GLint bufferWidth;
GLint bufferHeight;
// シェーダープログラムのハンドルです。
GLuint program;
// シェーダーで使う uniform 変数のハンドルです。
GLint u_texture;
GLint u_color;
GLint u_matrix;
// テクスチャのハンドルです。
GLuint texture;
// テクスチャ画像とその情報を保持します。
UIImage* image;
CGImageRef imageRef;
size_t imageWidth;
size_t imageHeight;
// 単調化で使用する色の各要素です。
CGFloat red;
CGFloat green;
CGFloat blue;
CGFloat alpha;
}
コンテキストとバッファーの初期化
iOS で OpenGL ES 2.0 のフラグメントシェーダーを使用するには、EAGLContext を用意する必要があります。
併せて、そこで使用するフレームバッファーとレンダーバッファーも用意する必要があるので、今回は次の OpenGL ES 関連の初期化メソッドを作成しました。
そしてこれを Interface Builder からインスタンス化されたときに実行される initWithCoder: メソッド内から呼び出します。
- (void)glInit
{
// このビューに設定されたレイヤを取得します。
CAEAGLLayer* pGLLayer = (CAEAGLLayer*)self.layer;
// 不透明にすることで処理速度が上がるそうです。透過させたい場合は NO とします。
pGLLayer.opaque = NO;
// 描画の設定を行います。[値, キー] の順でプロパティを指定します。
pGLLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
@NO, kEAGLDrawablePropertyRetainedBacking, // 描画後にレンダバッファの内容を保持しない。
kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, // レンダバッファーの 1 ピクセルあたり RGBA を 8bit ずつ保持する。
nil ];
// OpenGL ES 2.0 のコンテキストを生成します。
mpGLContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
NSAssert(mpGLContext != nil, @"Invalid context.");
// 現在のコンテキストにレンダリングコンテキストを設定します。
[EAGLContext setCurrentContext:mpGLContext];
// フレームバッファとレンダーバッファを 1 つずつ作成します。
glGenFramebuffers(1, &mFrameBuffer); EzOpenGLESAssert;
glGenRenderbuffers(1, &mColorBuffer); EzOpenGLESAssert;
// 作成したバッファーをバインドします。
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffer); EzOpenGLESAssert;
glBindRenderbuffer(GL_RENDERBUFFER, mColorBuffer); EzOpenGLESAssert;
// フレームバッファとレンダーバッファを関連付けます。レンダーバッファーはカラーレンダーバッファー (GL_COLOR_ATTACHMENT0) として割り当てています。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, mColorBuffer); EzOpenGLESAssert;
}
上記のプログラムについて、部分部分は次のような実装になっています。
描画条件の整備
最初に、後で描画先として使う、自分自身のレイヤーの状態を整えています。
CAEAGLLayer* pGLLayer = (CAEAGLLayer*)self.layer;
pGLLayer.opaque = NO;
pGLLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
@NO, kEAGLDrawablePropertyRetainedBacking, // 描画後にレンダバッファの内容を保持しない。
kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, // レンダバッファーの 1 ピクセルあたり RGBA を 8bit ずつ保持する。
nil ];
今回は透明色でブレンドしたいので、レイヤの opaque を NO に設定しています。また、drawableProperties で、レンダーバッファーの扱いに関する情報を設定しています。
OpenGL ES 2.0 コンテキストとバッファーの準備
続いて、それとは別に EAGLContext を OpenGL ES 2.0 用として準備しています。
mpGLContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
NSAssert(mpGLContext != nil, @"Invalid context.");
コンテキストの準備ができたら、それを setCurrentContext: を使って既定のコンテキストに設定して、その既定のコンテキストに対して、フレームバッファーとレンダーバッファーを作成してバインド(割り当て)しています。
glGenFramebuffers(1, &mFrameBuffer);
glGenRenderbuffers(1, &mColorBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, mColorBuffer);
フレームバッファーとレンダーバッファーの準備ができたら、glFramebufferRenderbuffer 関数を使って、既定のコンテキストにそれらのバッファーを関連付けます。
このとき、レンダーバッファーは「カラーレンダーバッファー」(GL_COLOR_ATTACHMENT0) として関連付けています。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, mColorBuffer);
これで、コンテキストとバッファーの初期化がひとまず完了しました。
コードの補足
なお、ここで glGen で始まる関数 (glGenFramebuffers, glGenRenderbuffers) で取得したバッファーのハンドルは、使い終わったら削除 (glDeleteFramebuffers, glDeleteRenderbuffers) する必要があります。
また、今回の例では OpenGL ES 関連の gl で始まる関数を実行した直後に EzOpenGLESAssert マクロを毎回実行しています。
このマクロでは、glGetError 関数を呼び出してエラーが検出されたらプログラムの処理を中断するようになっているので、これによって、gl 系の関数でエラーが発生した直後にプログラムが停止されて、間違ったところを判りやすくするのが狙いです。
描画領域の準備
OpenGL ES 2.0 の基本的な準備が終わったら、続いてモノトーン画像を描画するための領域の準備に取り掛かります。
そのために次のメソッドを用意して、今回は UIView のレイアウトができる段階になったときに呼び出される layoutSubviews から呼び出すことにします。
- (void)glPrepare
{
// 複数のコンテキストが存在するときは、別のところでコンテキストを変更されると正しく動作しなくなるので、冒頭でコンテキストを選択しておきます。
[EAGLContext setCurrentContext:mpGLContext];
// 画像の情報を取得します。
image = self.sourceImageView.image;
imageRef = image.CGImage;
imageWidth = CGImageGetWidth(imageRef);
imageHeight = CGImageGetHeight(imageRef);
// Retina に対応するために、画像のイメージスケールを自分自身 UIView のスケールに設定します。
self.contentScaleFactor = image.scale;
// モノトーンで塗る色を準備しています。
[self.sourceMonochromeView.backgroundColor getRed:&red green:&green blue:&blue alpha:&alpha];
// カラーレンダバッファの描画メモリとしてレイヤーを割り当てます。
[mpGLContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];
// ここまできたら、フレームバッファが正しく設定されたかチェックします。
NSAssert1((glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE), @"Invalid framebuffer. (status=%x)", glCheckFramebufferStatus(GL_FRAMEBUFFER));
// カラーレンダーバッファーの幅と高さを取得します。
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &bufferWidth); EzOpenGLESAssert;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &bufferHeight); EzOpenGLESAssert;
// レンダリングした画像を表示する領域を、左下が原点の座標系で指定します。今回は画像サイズと同じにしています。ビューのサイズにすると、ビュー全体に引き延ばされて描画されます。
glViewport(0.0, 0.0, imageWidth, imageHeight); EzOpenGLESAssert;
}
上記のコードの詳細は、次のようになっています。
カレントコンテキストの設定
このメソッドでも、最初に EAGLContext の setCurrentContext: メソッドを呼び出しています。
[EAGLContext setCurrentContext:mpGLContext];
複数のコンテキストを扱う場合、今回のように最初に setCurrentContext: を呼び出したメソッドとは違うメソッドに処理が分かれると、このメソッドを呼び出したとき、目的のコンテキストとは別のものがカレントコンテキストになっている場合があります。
OpenGL ES の操作で呼び出す gl 系の関数は、カレントコンテキストに対して命令を出すようなので、カレントコンテキストが違っていると、期待通りの動作をしてくれなくなります。
そのため、使っているコンテキストをインスタンス変数に入れておいて、メソッドが呼ばれたときにそれをカレントコンテキストに設定するようにしています。
描画する画像の情報を取得
フラグメントシェーダーで処理する画像は、今回は sourceImageView プロパティに設定されているものとして、それを取得しています。
image = self.sourceImageView.image;
imageRef = image.CGImage;
imageWidth = CGImageGetWidth(imageRef);
imageHeight = CGImageGetHeight(imageRef);
画像情報を取得するには CGImage の方が扱いやすいので、取得した情報は CIImage に変換しています。そしてさっそく、取得した画像の幅と高さを取得して、インスタンス変数に格納しています。
self.contentScaleFactor = image.scale;
また、Retina ディスプレイの場合は、取得した画像のスケールが 2.0 になっているので、フラグメントシェーダーで描画したときにもそのスケールで表示されるように、自分自身のスケールを画像と同じスケールに設定しています。
単調化で使用する色の準備
次では、今回はフラグメントシェーダーで画像をモノトーンに単調化するフィルタを作ろうと思うので、単調化のときに使用する色の要素を取得しています。
[self.sourceMonochromeView.backgroundColor getRed:&red green:&green blue:&blue alpha:&alpha];
単調化に使用する色は RGBA 形式で sourceMonochromeView の背景色として設定されているものとして、iOS 5 から使えるようになった UIColor の getRed:green:blue:alpha メソッドを使って、各要素の情報をインスタンス変数に取得しています。
カラーレンダーバッファーの割り当て
そして、自分自身のレイヤーが持っている画像領域を、EAGLContext の renderbufferStorage:fromDrawable: メソッドを使って OpenGL ES のカラーレンダーバッファーの描画先として指定しています。
[mpGLContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];
バッファーの状態チェック
ここまでできたら、バッファーが正しく準備できたかを glCheckFramebufferStatus 関数を使って調べることができるようです。
NSAssert1((glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE), @"Invalid framebuffer. (status=%x)", glCheckFramebufferStatus(GL_FRAMEBUFFER));- (void)glPrepare
この関数が GL_FRAMEBUFFER_COMPLETE を返せば、フレームバッファーが正しく準備できているかが判ります。
また、glGetRenderbufferParameteriv 関数に、引数として GL_RENDERBUFFER_WIDTH や GL_RENDERBUFFER_HEIGHT を渡すことで、用意されたレンダーバッファーの幅や高さを取得することもできます。
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &bufferWidth); EzOpenGLESAssert;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &bufferHeight); EzOpenGLESAssert;
今回は UIView のレイヤーを領域として渡したからか、幅と高さは UIView の bounds サイズと同じになっているようでした。
画像の映し先を指定
これまでで用意したバッファーに描画されたデータは、最後にビューポートに映し出されることになるようです。
その映し先は glViewport 関数を使って (x, y, width, height) の形で指定します。このとき、原点 (0, 0) は、普段のグラフィックスの座標系とは違って左下が (0, 0) になることに注意します。
glViewport(0.0, 0.0, imageWidth, imageHeight); EzOpenGLESAssert;
今回は、画像サイズと同じ大きさをビューポートに指定しています。
これを例えば UIView のサイズと同じにすると、フラグメントシェーダーで画像が描画されたとき、それが画面いっぱいに引き延ばされて表示されることになるようでした。
ただし、Retina 環境対応のために contentScaleFactor に 2.0 を指定している場合は、glViewport で UIView と同じサイズを指定しても、縦横ともに半分のサイズで描画されます。
そのため、Retina 環境でも期待通りのサイズで描画にするためには、UIView のサイズに画像のスケールを掛け算しておく必要がありました。
逆に glViewpoint で画像サイズと同じに設定する場合、contentScaleFactor を設定しておかないと、画像が倍の大きさで表示されてしまう様子です。
こちらは逆に、Retina 用の倍のサイズの画像を扱うことに因るもので、この場合は逆に画像の scale でサイズを割ってあげることになるのでしょうか。
もっとも、こちらの場合はそんな複雑なことをしないで、contentScaleFactor を画像サイズと同じに設定するのが適切なように思います。
シェーダーのコンパイルと uniform 変数のハンドル取得
描画のための準備が終わったので、描画で使用するシェーダーの準備を行います。
先ほどの描画領域の準備プログラムに続いて、次のシェーダーのコンパイルプログラムを実行します。
このとき、フラグメントシェーダーのソースコードは "fragment.txt" ファイルに、頂点シェーダーのソースコードは "vertex.txt" に記載されているものとします。これらのファイルは後で用意します。
- (void)glBuild
{
// 複数のコンテキストを使う場合、違うコンテキストが選択されている場合があるので、目的のコンテキストを設定し直します。
[EAGLContext setCurrentContext:mpGLContext];
// フラグメントシェーダーのコードを準備します。
NSString* fCodePath = [[NSBundle mainBundle] pathForResource:@"fragment" ofType:@"txt"];
NSString* fCodeSource = [[NSString alloc] initWithContentsOfFile:fCodePath encoding:NSUTF8StringEncoding error:nil];
const char* fCode = fCodeSource.UTF8String;
// 頂点シェーダーも用意します。フラグメントシェーダーにテクスチャの値を渡すために必要です。
NSString* vCodePath = [[NSBundle mainBundle] pathForResource:@"vertex" ofType:@"txt"];
NSString* vCodeSource = [[NSString alloc] initWithContentsOfFile:vCodePath encoding:NSUTF8StringEncoding error:nil];
const char* vCode = vCodeSource.UTF8String;
GLint compiled;
// 頂点シェーダーを生成します。
GLuint vShader = glCreateShader(GL_VERTEX_SHADER); EzOpenGLESAssert;
NSAssert(vShader != GL_FALSE, @"Failed to create a vertex shader.");
glShaderSource(vShader, 1, &vCode, NULL); EzOpenGLESAssert;
glCompileShader(vShader); EzOpenGLESAssert;
glGetShaderiv(vShader, GL_COMPILE_STATUS, &compiled);
if (!compiled)
{
EzOpenGLESShaderCompileAssert(vShader);
}
// フラグメントシェーダーを生成します。
GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER); EzOpenGLESAssert;
NSAssert(fShader != GL_FALSE, @"Failed to create a fragment shader.");
glShaderSource(fShader, 1, &fCode, NULL); EzOpenGLESAssert;
glCompileShader(fShader); EzOpenGLESAssert;
glGetShaderiv(fShader, GL_COMPILE_STATUS, &compiled);
if (!compiled)
{
EzOpenGLESShaderCompileAssert(fShader);
}
// プログラムを構築します。
program = glCreateProgram();
// プログラムにシェーダーを関連づけます。
glAttachShader(program, fShader); EzOpenGLESAssert;
glAttachShader(program, vShader); EzOpenGLESAssert;
// 頂点シェーダーの attribute 番号に変数名を割り当てます。
glBindAttribLocation(program, 0, "a_position"); EzOpenGLESAssert;
glBindAttribLocation(program, 1, "a_texCoord"); EzOpenGLESAssert;
// 関連づけたシェーダーをリンクします。
glLinkProgram(program); EzOpenGLESAssert;
// リンクに成功したかを調べます。
GLint linked;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked)
{
GLint infoLogLength=0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
if (infoLogLength > 0)
{
char* infoLog = (char*)malloc(sizeof(char)*infoLogLength);
glGetProgramInfoLog(program, infoLogLength, NULL, infoLog);
NSAssert1(false, @"Link error: %s", infoLog);
free(infoLog);
}
else
{
NSAssert(false, @"Unknown link error.");
}
}
// リンクに成功したら、シェーダーは不要になるようなので、デタッチして削除します。
glDetachShader(program, fShader); EzOpenGLESAssert;
glDeleteShader(fShader); EzOpenGLESAssert;
glDetachShader(program, vShader); EzOpenGLESAssert;
glDeleteShader(vShader); EzOpenGLESAssert;
// シェーダーで宣言したユニフォーム変数の値を格納するための ID を取得します。
u_texture = glGetUniformLocation(program, "u_texture");
NSAssert(u_texture != -1, @"Uniform variable 'u_texture' was not found.");
u_color = glGetUniformLocation(program, "u_color");
NSAssert(u_color != -1, @"Uniform variable 'u_color' was not found.");
u_matrix = glGetUniformLocation(program, "u_matrix");
NSAssert(u_matrix != -1, @"Uniform variable 'u_matrix' was not found.");
}
上記のコードを、部分ごとに見て行きます。
カレントコンテキストの設定
このメソッドでも、最初に EAGLContext の setCurrentContext: メソッドを呼び出しています。
[EAGLContext setCurrentContext:mpGLContext];
複数のコンテキストを扱う場合、今回のように最初に setCurrentContext: を呼び出したメソッドとは違うメソッドに処理が分かれると、このメソッドを呼び出したとき、目的のコンテキストとは別のものがカレントコンテキストになっている場合があります。
ソースコードの読み込み
今回は、頂点シェーダーのソースコードを "vertex.txt" ファイルに、フラグメントシェーダーのソースコードを "fragment.txt" に保存しておくことにしたので、それを NSString の initWithContentsOfFile:encoding:error: で読み込んでいます。
NSString* fCodePath = [[NSBundle mainBundle] pathForResource:@"fragment" ofType:@"txt"];
NSString* fCodeSource = [[NSString alloc] initWithContentsOfFile:fCodePath encoding:NSUTF8StringEncoding error:nil];
const char* fCode = fCodeSource.UTF8String;
glShaderSource 関数では、ソースコードを C 言語文字列 (char*) のポインタで渡す必要があるので、読み込んだ NSString から UTF8String を取得しています。
ソースコードのコンパイル
シェーダーのソースコードが準備できたら、glCreateShader でシェーダーハンドルを作成して、glShaderSource 関数を使ってシェーダーハンドルにソースコードを関連付けます。
GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
NSAssert(vShader != GL_FALSE, @"Failed to create a vertex shader.");
glShaderSource(vShader, 1, &vCode, NULL);
そして glCompileShader 関数を実行して、ソースコードをコンパイルします。
glCompileShader(vShader);
glGetShaderiv(vShader, GL_COMPILE_STATUS, &compiled);
if (!compiled)
{
EzOpenGLESShaderCompileAssert(vShader);
}
コンパイルが正しく実行できたかどうかは、glGetShaderiv 関数で GL_COMPILE_STATUS を取得することで確認できます。
コンパイルに失敗した場合は、今回は EzOpenGLESShaderCompileAssert 関数を呼び出していますが、これは、最初の方で作成した、シェーダーハンドルからログメッセージを取得して表示・中断する関数です。
これらを、頂点シェーダーとフラグメントシェーダーのそれぞれで行います。
なお、ここで glCreateShader を使って作成したシェーダーハンドルは、使い終わった後で glDeleteShader 関数を読んで削除する必要があります。
シェーダープログラムの構築と、使用済みシェーダーの解放
使用するシェーダーの準備ができたら、それらをひとつのプログラムにまとめます。
program = glCreateProgram();
glAttachShader(program, fShader);
glAttachShader(program, vShader);
まず、シェーダープログラムのハンドルを glCreateProgram 関数を使って作成します。このハンドルは、描画が終わったら glDeleteProgram 関数を使って削除する必要があります。
シェーダープログラムを作成したら glAttachShader 関数を使って、使用するシェーダーハンドルを必要な数だけ関連付けます。
シェーダーハンドルを関連付けたら、続いて、頂点シェーダーの attribute 変数として宣言した変数名に、変数番号を割り当てます。
glBindAttribLocation(program, 0, "a_position");
glBindAttribLocation(program, 1, "a_texCoord");
たとえば glBindAttribLocation(program, 0, "a_position") とすることで、頂点シェーダーの attribute 変数 "a_position" という変数を 0 番の変数として割り当てられます。
こうすることで、後で glVertexAttribPointer 関数を使用して、0 番の変数に値を設定する、ということができるようになります。
後は、関連付けたシェーダーをリンクして、シェーダープログラムを完成させます。
glLinkProgram(program); EzOpenGLESAssert;
リンクには glLinkProgram 関数を使用します。
実行したら glGetProgramiv 関数に GL_LINK_STATUS を渡すことで、リンクが成功したかどうかを確認できます。
GLint linked;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked)
{
GLint infoLogLength=0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
if (infoLogLength > 0)
{
char* infoLog = (char*)malloc(sizeof(char)*infoLogLength);
glGetProgramInfoLog(program, infoLogLength, NULL, infoLog);
NSAssert1(false, @"Link error: %s", infoLog);
free(infoLog);
}
else
{
NSAssert(false, @"Unknown link error.");
}
}
リンクに失敗した場合は、今回は glGetProgramInfoLog 関数でエラーメッセージを取得して、それを表示し、プログラムを中断しています。
リンクに成功すれば、シェーダープラグラムに関連付けたシェーダーハンドルは解放して大丈夫なようです。
glDetachShader(program, fShader);
glDeleteShader(fShader);
glDetachShader(program, vShader);
glDeleteShader(vShader);
頂点シェーダーとフラグメントシェーダーのそれぞれについて、glDetachShader でプログラムから関連付けを解消してから、glDeleteShader 関数を使ってシェーダーハンドルを解放します。
uniform 変数のハンドル取得
シェーダープログラムの準備も整ったところで、uniform 変数に値を渡すためのハンドルもここで取得しておくことにします。
u_texture = glGetUniformLocation(program, "u_texture");
NSAssert(u_texture != -1, @"Uniform variable 'u_texture' was not found.");
頂点シェーダーの attribute 変数とは違って、uniform 変数の場合は glGetUniformLocation 関数を使って、変数名からその変数を操作するためのハンドルが取得できるようになっています。
このハンドルは、後で uniform 変数を設定するときに使用します。
描画データーの設定とフラグメントシェーダーでの描画
後は、描画に必要なデータをそろえてシェーダープログラムの引数に渡して、レイヤーへ画像を描画します。
先ほどのシェーダーのコンパイルプログラムの実行に続いて、次の描画メソッドを実行します。
- (void)glDraw
{
// 複数のコンテキストを使う場合、違うコンテキストが選択されている場合があるので、目的のコンテキストを設定し直します。
[EAGLContext setCurrentContext:mpGLContext];
// レンダーバッファーを透明で初期化します。iOS シミュレーターだとノイズが入るようでした。
glClearColor(0.0, 0.0, 0.0, 0.0); EzOpenGLESAssert;
glClear(GL_COLOR_BUFFER_BIT); EzOpenGLESAssert;
// 扱うサイズは画像サイズにすることにします。
CGFloat width = imageWidth;
CGFloat height = imageHeight;ight;
// 頂点シェーダーの 4 頂点を準備します。今回は画像と同じサイズの正方形を 2 つのポリゴンで描きます。
GLKVector2 positions[4] =
{
{ 0.0, 0.0 },
{ 0.0, height },
{ width, 0.0 },
{ width, height }
};
// テクスチャを設定する座標は全体にぴったり貼るという指定になっているでしょうか。
GLKVector2 texCoords[4] =
{
{ 0.0, 0.0 },
{ 0.0, 1.0 },
{ 1.0, 0.0 },
{ 1.0, 1.0 }
};
// テクスチャの画像データを準備します。
size_t imageBytesPerRow = CGImageGetBytesPerRow(imageRef);
size_t imageBitsPerComponent = CGImageGetBitsPerComponent(imageRef);Ref);
CGColorSpaceRef imageColorSpace = CGImageGetColorSpace(imageRef);
size_t imageTotalBytes = imageBytesPerRow * imageHeight;
Byte* imageData = (Byte*)malloc(imageTotalBytes);
// バッファを初期化しないと、読み込んだ画像の透明部分にノイズが入る様子なので初期化しています。iOS シミュレーターだけかもしれません。
memset(imageData, 0, imageTotalBytes);
// テクスチャ画像と同じサイズのビットマップコンテキストを構築します。
CGContextRef memContext = CGBitmapContextCreate(imageData, imageWidth, imageHeight, imageBitsPerComponent, imageBytesPerRow, imageColorSpace, kCGImageAlphaPremultipliedLast);
// ビットマップコンテキストに画像を描画すると、コンテキスト構築時に指定したデータバッファーに描画されます。
CGContextDrawImage(memContext, CGRectMake(0.0f, 0.0f, (CGFloat)imageWidth, (CGFloat)imageHeight), imageRef);
// データバッファーに描画できたら、コンテキストは不要になります。
CGContextRelease(memContext);
// テクスチャ 0 を有効化します。
glActiveTexture(GL_TEXTURE0); EzOpenGLESAssert;
// テクスチャを 1 つ生成してバインドします。
glGenTextures(1, &texture); EzOpenGLESAssert;
glBindTexture(GL_TEXTURE_2D, texture); EzOpenGLESAssert;
// メモリを参照するときのアドレス境界の数を 1, 2, 4, 8 で指定します。今回は 1 を指定していますが、RGBA の 4 バイト構成なら 4 を指定すると最適になるそうです。
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); EzOpenGLESAssert;
// テクスチャにテクスチャ画像を割り当てます。
// param1: テクスチャの種類です。必ず GL_TEXTURE_2D になるようです。
// param2: ミップマップを行う場合のテクスチャの解像度レベルだそうです。MIPMAP を使用しない場合は 0 を指定します。
// param3: 内部で保持するテクスチャの形式を指定します。
// param4: テクスチャの幅です。
// param5: テクスチャの高さです。
// param6: テクスチャの境界線の太さを指定するのだそうです。
// param7: 画像データ (imageData) の画像形式を指定します。
// param8: 画像データ (imageData) のデータ型を指定します。
// param9: テクスチャに割り当てる画像データです。
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData); EzOpenGLESAssert;
// テクスチャサイズが 2 の累乗であればミップマップを作成できます。ミップマップを使うときれいになるようです。GL_TEXTURE_MIN_FILTER で GL_LINEAR_MIPMAP_LINEAR するためには必要です。
// glGenerateMipmap(GL_TEXTURE_2D); EzOpenGLESAssert;
// テクスチャを準備できたら、画像データは不要になります。
free(imageData);
// シェーダーを使用するために、プログラムを使います。
glUseProgram(program); EzOpenGLESAssert;
// アルファブレンドを有効にして、透明色を透過させるようにします。
glEnable(GL_BLEND); EzOpenGLESAssert;
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); EzOpenGLESAssert;
// テクスチャの横 (GL_TEXTURE_WRAP_S) と縦 (GL_TEXTURE_WRAP_T) のリピート方法を指定します。
// GL_REPEAT は繰り返し適用で、2 の累乗のテクスチャサイズのときに使えるらしいです。淵を延々と延ばす場合は GL_CLAMP_TO_EDGE を指定するそうです。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); EzOpenGLESAssert;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); EzOpenGLESAssert;
// 拡大 (GL_TEXTURE_MAG_FILTER) 縮小 (GL_TEXTURE_MIN_FILTER) 時の補完指定を行います。指定しないと正しく表示されないか荒くなるようです。
// GL_NEAREST=最近傍法, GL_LINEAR=双線形補完
// 縮小の場合で、ミップマップが有効なときは次のものも選べます。
// GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); EzOpenGLESAssert;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); EzOpenGLESAssert;
// アトリビュート 0 番の値を設定します。 (glBindAttribLocation で "a_position" を 0 に割り当てています)
glEnableVertexAttribArray(0); EzOpenGLESAssert;
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, positions); EzOpenGLESAssert;
// アトリビュート 1 番の値を設定します。 (glBindAttribLocation で "a_texCoord" を 0 に割り当てています)
glEnableVertexAttribArray(1); EzOpenGLESAssert;
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, texCoords); EzOpenGLESAssert;
// ユニフォーム変数 u_texture に 0 番のテクスチャを設定します。
glUniform1i(u_texture, 0); EzOpenGLESAssert;
// ユニフォーム変数 u_color に、今回はモノトーン変換で使用する色情報を渡します。
glUniform4f(u_color, red, green, blue, alpha); EzOpenGLESAssert;
// ユニフォーム変数 u_matrix に、平行投影変換の写像を渡します。
// 視野空間の中心が原点になるように、-width / 2.0 平行移動して、大きさを 2 / width 倍します。つまり -1.0 から 1.0 の座標系にします。y, z についても同様です。
//
// 2/w, 0, 0, 0
// 0, 2/-h, 0, 0 0, 0
// 0, 0, -2/d, 0
// 0, 0, 0, 1
//
// ×
//
// 1, 0, 0, -(right+left)/2
// 0, 1, 0, -(top+bottom)/2
// 0, 0, 1, (far+near)/2
// 0, 0, 0, 1
//
// これを縦横を入れ変えた float 配列にします。
GLfloat matrix[16] =
{
2.0f / width, 0.0f, 0.0f, 0.0f,
0.0f, -2.0f / height, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f
};
glUniformMatrix4fv(u_matrix, 1, GL_FALSE, matrix); EzOpenGLESAssert;
// glVertexPointer で用意した頂点 4 つをレンダーバッファー描画します。
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); EzOpenGLESAssert;
// これまでの描画を全て実行することを命令します。なくても大丈夫そうです。
glFlush(); EzOpenGLESAssert;
// レンダーバッファーへの描画が終わったら、ブレンド設定などはリセットしてもよくなります。
glDisable(GL_BLEND); EzOpenGLESAssert;
// attribute 設定の関連付けも無効化できます。
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
// レンダーバッファーに描画した内容を画面に描画します。
[mpGLContext presentRenderbuffer:GL_RENDERBUFFER];
}
上記のコードを、部分ごとに順を追って見て行きます。
カレントコンテキストの設定
このメソッドでも、最初に EAGLContext の setCurrentContext: メソッドを呼び出しています。
[EAGLContext setCurrentContext:mpGLContext];
複数のコンテキストを扱う場合、今回のように最初に setCurrentContext: を呼び出したメソッドとは違うメソッドに処理が分かれると、このメソッドを呼び出したとき、目的のコンテキストとは別のものがカレントコンテキストになっている場合があります。
描画領域のクリア
カラーレンダーバッファーを今回は RGBA = (0, 0, 0, 0) で初期化しています。
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
透明度だけ 0.0 にしても何故か上手く初期化できなかったため、ここでは全てをゼロで初期化しています。
この初期化は iPhone 5 や iPod touch 4th 実機で試した感じでは無くても大丈夫そうだったのですけど、iOS シミュレーターで実行すると、画像の透明部分のところにランダムなノイズが頻繁に出るようだったので、入れておくのが安心そうです。
描画点とテクスチャ位置の準備
頂点シェーダーで正方形を書くために 4 つの点の座標を準備します。
GLKVector2 positions[4] =
{
{ 0.0, 0.0 },
{ 0.0, height },
{ width, 0.0 },
{ width, height }
};
幅と高さは、今回は画像の幅と高さを指定しています。
今回は後で TRIANGLE_STRIP でポリゴンを描くことにするので、最初のポリゴンの 3 頂点と、その最後 2 頂点ともう 1 頂点を使って四角形を描いています。
このとき、座標系は左下を原点 (0, 0) とした座標で指定することになるようなので、上下を間違えないように注意します。
また、ポリゴンは右回りで描き始めるかによって表と裏とがありますけど、今回は平面 1 枚の四角形を描くだけなので、裏表はとくに気にしなくても大丈夫そうです。
そして、このポリゴンのどこにテクスチャを張り付けるかの座標も用意します。
GLKVector2 texCoords[4] =
{
{ 0.0, 0.0 },
{ 0.0, 1.0 },
{ 1.0, 0.0 },
{ 1.0, 1.0 }
};
指定方法が理解できていないのですけど、ポリゴンを描くときに指定した各 4 頂点と、平面四角のポリゴンの頂点とが、ぴったり合わさるように貼っている感じになるのでしょうか。
テクスチャ画像の作成
次に、ポリゴンに貼るテクスチャ画像を作成します。
まずは、これまでに用意しておいたポリゴン用の CGImage から、必要な画像情報を揃えておくことにします。
size_t imageBytesPerRow = CGImageGetBytesPerRow(imageRef);
size_t imageBitsPerComponent = CGImageGetBitsPerComponent(imageRef);Ref);
CGColorSpaceRef imageColorSpace = CGImageGetColorSpace(imageRef);
size_t imageTotalBytes = imageBytesPerRow * imageHeight;
CGImage 関係の関数で画像を構成する要素の大きさなどを取得したら、それらを使って、画像情報を格納するために必要なメモリ容量を計算します。
そして、そのメモリ容量を納められるメモリ領域を確保します。
Byte* imageData = (Byte*)malloc(imageTotalBytes);
memset(imageData, 0, imageTotalBytes);
ここで memset 関数を使ってメモリーデータをリセットしていますが、このようにしないと読み込んだ画像の透明部分にノイズがはいることがあったため、このようにしています。
iPhone 5 や iPod touch 4th 実機で試すときにはノイズに出会ったことがないため、もしかすると iOS シミュレーターの場合だけかもしれませんが、安全のため初期化しておくことにします。
そして、用意したメモリ上に、テクスチャデータを CGBitmapContext を使って書き込みます。
CGContextRef memContext = CGBitmapContextCreate(imageData, imageWidth, imageHeight, imageBitsPerComponent, imageBytesPerRow, imageColorSpace, kCGImageAlphaPremultipliedLast);
CGContextDrawImage(memContext, CGRectMake(0.0f, 0.0f, (CGFloat)imageWidth, (CGFloat)imageHeight), imageRef);
CGContextRelease(memContext);
CGBitmapContextCreate では、ビットマップデータを生成するためのコンテキストを作成しています。
このとき、第一引数で、データを書き込むメモリ領域として先ほど確保したメモリ領域 (imageData) を指定しているので、個のコンテキストに対して CGContextDrawImage 関数を呼び出した時には、その画像がこのメモリ領域に書き込まれるようになります。
それ以降の引数では、作成するビットマップのサイズや画素情報の扱い、透明要素の置き場所を指定しています。
今回はそのほとんどを、テクスチャ用に用意した画像の情報をそのまま設定しています。テクスチャデータが上手く読み込めない場合には、これらの値を適切に調整する必要もあるかもしれません。
CGContextDrawImage 関数では、書き込む領域を指定したビットマップコンテキストと、画像サイズ、CGImage 画像を渡すことで、コンテキストに画像を描画することができます。
これが終わると、画像データはメモリ上に書き込み終わった状態になるので、ビットマップコンテキストは解放して大丈夫です。
テクスチャの割り当て
テクスチャ画像のデータが用意出来たら、それを OpenGL ES のテクスチャハンドルを 1 つ作成して、そこに関連付けます。
このとき、関連付けるテクスチャをあらかじめ glActiveTexture 関数を使って有効化する必要があるようでした。
glActiveTexture(GL_TEXTURE0);
今回は 0 番目のテクスチャ (GL_TEXTURE0) を有効にしています。
こうした上で、glGenTextures 関数を使ってテクスチャハンドルを 1 つ作成して、glBindTexture 関数を使って 0 番目のテクスチャに関連付けます。
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
そして、メモリを参照するときのアドレス境界を glPixelStorei 関数の GL_UNPACK_ALIGNMENT で設定して、glTexImage2D 関数を使ってテクスチャハンドルに、先ほど用意したテクスチャデータを設定します。
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
アドレス境界というのは、1 つの画素データを読み込むときに何バイト単位で読み込むかを指定するもののようです。
この値は 1, 2, 4, 8 のいずれかを指定する必要があるそうで、8bit RGBA 構成の画像であれば 4 を指定すると最適になるようです。8bit RGB 構成の場合は 3 を選ぶことができないので 1 を指定することになるそうです。
8bit RGBA 構成であっても 1 を指定して問題なさそうだったので、今回はそのようにしています。
そして glTexImage2D 関数を使って、画像データを現在アクティブなテクスチャに描画しているようです。
引数でテクスチャデータを OpenGL ES に受け渡すための詳細情報を指定します。
param 1 | テクスチャの種類です。必ず GL_TEXTURE_2D になるようです。 |
---|---|
param 2 | ミップマップを行う場合のテクスチャの解像度レベルだそうです。MIPMAP を使用しない場合は 0 を指定します。 |
param 3 | 内部で保持するテクスチャの形式を指定します。 |
param 4 | テクスチャの幅です。 |
param 5 | テクスチャの高さです。 |
param 6 | テクスチャの境界線の太さを指定するのだそうです。 |
param 7 | 画像データ (imageData) の画像形式を指定します。 |
param 8 | 画像データ (imageData) のデータ型を指定します。 |
param 9 | テクスチャに割り当てる画像データです。 |
テクスチャのデータを割り当てられたら、用意したテクスチャデータは不要になるので解放します。
free(imageData);
ミップマップの作成(可能であれば)
テクスチャの準備ができたら、可能であればここでミップマップを作成しておくと、テクスチャがよりきれいに表示されるようでした。
glGenerateMipmap(GL_TEXTURE_2D);
または glTexImage2D 関数を使って、第二引数に渡す詳細レベルを 0 で登録したオリジナルテクスチャよりも、小さいサイズの画像をミップマップレベルを詳細レベル 1 として渡すことでも、ミップマップを登録することができるそうでした。
ただし、ミップマップを作成するためには、テクスチャサイズが 2 のべき乗である必要があるようです。
また、ミップマップを作成した場合は、テクスチャ描画のオプションで glTexParameteri 関数の GL_TEXTURE_MIN_FILTER の値として GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR を指定する必要があるようです。
使用するシェーダープログラムの指定
テクスチャの準備もできて、これでシェーダープログラムのひと通りの準備が整いました。
準備したシェーダーを使用するために、ここで glUseProgram 関数を実行します。
glUseProgram(program);
そして、描画に向けての最終的な調整を行います。
ブレンド指定
今回は透明部分を透過させたいので、アルファブレンドを有効にします。
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
これで、テクスチャの透明部分が透過して表示されるようになるようでした。
テクスチャ描画オプションの設定
テクスチャの横方向(S 方向)と縦方向(T 方向)の、一枚で覆い切れなかった部分の描画方法を設定します。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
ここでは次のものが指定できるようになっていました。
GL_CLAMP_TO_EDGE | 端部分を延々と引き延ばすようにテクスチャを描画します。 |
---|---|
GL_REPEAT | テクスチャを最初から繰り返して描画します。 |
今回はテクスチャをぴったり貼るので何でもよさそうです。
そして、テクスチャを拡大したり縮小したりするときの補完方法を指定します。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
拡大時の補完設定は GL_TEXTURE_MAG_FILTER で行い、パラメータには次のものが指定できるようでした。
GL_NEAREST | 最近傍法 | 最も近い点の値を利用するため高速に動作するそうです。 |
---|---|---|
GL_LINEAR | 双線形補完 | 近隣の点から近い色を加重平均で合成するそうです。ハードウェアがサポートしていない場合は処理速度に大きく影響することもあるらしいです。 |
縮小時の補完設定は GL_TEXTURE_MIN_FILTER で行い、拡大時の補完指定と合わせてミップマップを使った補完指定もできるようになっています。
GL_NEAREST | 最近傍補完 | 最も近い点の値を利用するため高速に動作するそうです。 |
---|---|---|
GL_LINEAR | 双線形補完 | 近隣の点から近い色を加重平均で合成するそうです。ハードウェアがサポートしていない場合は処理速度に大きく影響することもあるらしいです。 |
GL_NEAREST_MIPMAP_NEAREST | テクスチャもミップマップも、最近傍法を使って補完します。 | |
GL_NEAREST_MIPMAP_LINEAR | テクスチャは最近傍法で、ミップマップは双線形法で補完します。 | |
GL_LINEAR_MIPMAP_NEAREST | テクスチャは双線形法で、ミップマップは最近傍法で補完します。 | |
GL_LINEAR_MIPMAP_LINEAR | テクスチャもミップマップも、双線形法を使って補完します。この補完方法がいちばん滑らかに描かれるそうです。 |
これらを指定しないと、正しく描画されなかったり、描画が荒くなったりするようでした。
また、ミップマップを使う場合は、GL_LINEAR_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR の中から補完方法を選ぶようにします。
attribute 変数の設定
頂点シェーダーに渡す attribute 変数の値を設定します。
glEnableVertexAttribArray 関数に attribute 変数番号を指定して、その attribute 変数を利用可能にしたら、glVertexAttribPointer 関数を使ってその番号の変数に、頂点毎の値を配列で設定します。
ちなみに attribute 変数番号は、頂点シェーダーを作成した時に glBindAttribLocation 関数で、変数名に関連付けた番号です。
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, positions);
glVertexAttribPointer 関数では、次の引数を取ります。
param1 | attribute 変数番号です。 |
---|---|
param2 | データとして渡す配列の 1 つ当たりの構成要素数です。例えば vec2 型用に GLKVector2 型の変数を使用している場合は 2 です。1, 2, 3, 4 で指定します。 |
param3 | データとして渡す配列の要素のデータ型です。GL_BYTE, GL_INT, GL_FLOAT などで指定します。 |
param4 | データの値を正規化するかどうかを GL_TRUE か GL_FALSE で指定します。正規化すると、値は [-1, 1] の範囲になるように調整されるそうです。 |
param5 | データとして渡す配列の、頂点毎のデータのサイズが何バイトであるかを指定します。これを 0 に指定すると、データとして渡した配列の値のデータサイズを指定したのと同じになるようです。 |
param6 | attribute 変数の値として渡すデータを、頂点毎に格納した配列の先頭要素で渡します。頂点毎のデータは GLKVector2 などの構造体の配列で渡すと便利なように思います。 |
たとえば、わざわざ難しくする必要はないですけど、
第六引数に渡すデータ配列を "GLKVector3 vec3[4]" などで定義しておいて、そのうちの先頭 2 要素 (vec3 の x と y だけ) を頂点データとして使いたい場合には、第二引数(構成要素数)を 2 にして、第五引数(データの刻み幅)を "sizeof(float) * 3" にするということもできるようでした。
このようにして、頂点シェーダーで使用する attribute 変数の分だけ、その値を設定します。
uniform 変数の設定
頂点シェーダーやフラグメントシェーダーに渡す uniform 変数も設定します。
uniform 変数の場合は、あらかじめ glGetUniformLocation 関数で取得しておいたハンドルと、データ型毎に用意された値設定関数を使って設定します。
たとえば sampler2D 型の uniform 変数にテクスチャを設定する場合は、次のように glUniform1i 関数を使って、uniform 変数の配列にテクスチャ番号を設定します。
glUniform1i(u_texture, 0);
ここで指定するテクスチャ番号は、テクスチャをバインドするときにアクティブ化していたテクスチャの番号になるようです。
たとえば、glActiveTexture(GL_TEXTURE0) として glBindTexture でテクスチャハンドルを関連付けていた場合、そのテクスチャを uniform 変数に渡すときには、テクスチャ番号 0 を指定します。
vec4 型の uniform 変数に値を設定する場合は、glUniform4f 関数を使います。
glUniform4f(u_color, red, green, blue, alpha);
このようにして、float 型の 4 要素を、第一引数のハンドルで指定した uniform 変数に設定できます。
mat4 型の行列であれば、glUniformMatrix4fv 関数を使用します。
glUniformMatrix4fv(u_matrix, 1, GL_FALSE, matrix);
第一引数で uniform 変数のハンドルを指定して、第二引数で設定する行列の数を指定します。uniform 変数が配列で宣言されていなければ、行列の数は 1 になります。
第三引数では、第四引数に指定した値を転置行列にして uniform 変数に格納する場合に GL_TRUE とするらしいのですが、iOS 6.0.2 の OpenGL ES 2.0 で試してみるとエラー番号 0x501 で失敗してしまいました。
第四引数には、第二引数で指定した行列の数を満たせるだけの、設定する値を配列で指定します。
このとき値は、引数に渡した配列の先頭から順に、uniform 変数の列から順番に格納されるようなので注意が必要です。
つまり、先頭から順に縦方向に、最初の行の列1、列2、と埋められて、その列が埋め終わったら、右隣の行の上の列から下の列へと埋められて行きます。
頂点シェーダーで使用する写像
ところで今回は、頂点シェーダーの "u_matrix" という uniform 変数 に設定する行列として、次の行列を渡しています。
GLfloat matrix[16] =
{
2.0f / width, 0.0f, 0.0f, 0.0f,
0.0f, -2.0f / height, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f
};
どうやらこの行列は、平行投影変換という写像を表しているようです。
このあたりは理解がなかなか及ばないところですけど、"床井研究室 第 5 回 座標変換" というサイトで詳しく解説されていました。平行投影変換の他にも、透視投影変換という写像もあるようです。
頂点シェーダーでは、頂点データを平面のビューポートに映し出すために "クリッピング空間" という座標系に変換(写像)する必要があるらしいため、その写像を変換する方式として、こういった写像を使う必要があるようです。
シェーダープログラムの実行
シェーダープログラムを設定して、attribute 変数や uniform 変数の指定も終わり、これでシェーダーで描画する準備が整いました。
シェーダープログラムを走らせるには、これまでに設定した頂点データのうち、何番目からのいくつを描画するかを glDrawArrays 関数で指示します。
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
このとき、第一引数で、頂点データをどのように使って描画するかを指定します。
指定できる描画方法は GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN で、それぞれの描かれ方は ポリゴン の項に記してあります。
第二引数では、attribute 変数に glVertexAttribPointer 関数を使って設定した頂点データの、何番目のデータから使い始めるかを 0 から始まるインデックス番号で指定します。続く第三引数では、そこから何個の頂点データを使用するかを、個数で指定します。
この関数を呼び出すことで、頂点シェーダーからフラグメントシェーダーまでのすべての描画が行われます。
ただし、この段階ではまだレンダーバッファーに画像データが出力されただけで、画面上にはその映像が表示されていないようでした。
シェーダーで生成した画像を映し出す
glDrawArrays 関数でレンダーバッファーに描画した画像データは、EAGLContext の presentRenderbuffer: メソッドを使って画面に描画します。
ただし、このとき環境によってはシステムが描画タイミングを選ぶ場合があり、そのような場合は glDrawArrays 関数を呼んだ直ぐには描画が完了しないこともあるそうです。
そのため、表示に先立って、レンダーバッファーへの描画を確実に完了させるために glFlush 関数を呼び出しておきます。
glFlush();
その上で EAGLContext の presentRenderbuffer: メソッドを呼び出します。
[mpGLContext presentRenderbuffer:GL_RENDERBUFFER];
これで、レンダーバッファーの内容が、EAGLContext の renderbufferStorage:fromDrawable: で指定したレイヤーの画面上に映し出されました。
描画後の後始末
glDrawArrays 関数と glFlush 関数を使ってレンダーバッファーへ描画が終わったら、そのタイミングで描画に関する情報をリセットすることができます。
ブレンド設定を無効化したい場合は、glDisable 関数に GL_BLEND を渡します。。
glDisable(GL_BLEND);
attribute 変数を無効化する場合は、glDisableVertexAttribArray 関数に attribute 変数番号を添えて呼び出します。
glDisableVertexAttribArray(0);
シェーダープログラムの削除は、glDeleteProgram 関数で行います。
glDeleteProgram(program);
テクスチャの削除には glDeleteTextures 関数に、テクスチャハンドルと、そのハンドルを作成する際に指定したテクスチャの数を渡します。
glDeleteTextures(1, &texture);
フレームバッファーとレンダーバッファーも、それぞれ glDeleteFramebuffers 関数や glDeleteRenderbuffers 関数に、バッファーハンドルと、そのハンドルを作成する際に指定したバッファー数を渡します。
glDeleteFramebuffers(1, &mFrameBuffer);
glDeleteRenderbuffers(1, &mColorBuffer);
このような感じで、シェーダーを使う上で呼び出した glCreate 系の関数や glGen 系の関数で取得したハンドルは、最終的には全て解放するようにします。
EAGLContext については、解放する前に、EAGLContext のクラスメソッド setCurrentContext: を使って使用中のコンテキストを取り除きます。
[EAGLContext setCurrentContext:nil];
このように、カレントコンテキストとして nil を指定することで、これまでカレントコンテキストとして設定していたコンテキストが取り除かれ、使われることがなくなります。
モノトーン化のシェーダープログラム
今回は、画像を指定した色で単調化するのが目的で、それを実現するためのシェーダーソースとして次のものを作成してみました。
頂点シェーダーのソースコード
attribute vec2 a_position;
attribute vec2 a_texCoord;
uniform mat4 u_matrix;
varying vec2 v_texCoord;
void main(void)
{
gl_Position = u_matrix * vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
attribute 変数で、各頂点の座標 (a_position) とテクスチャの貼り付け位置 (a_texCoord) を受け取ります。
uniform 変数では、"u_matrix" に、平面に適切に映し出すための平行投影変換の行列を受け取っています。
これらを元にクリッピング空間の座標系を取得して、それを既定の変数 gl_Position に設定しています。
また、フラグメントシェーダーに渡す値として varying 変数を定義して、attribute 変数で受け取ったテクスチャの貼り付け位置をそのまま設定しています。
なんて、頂点シェーダーの動きは理解しきれていないのですけど、きっとこういう感じになっていると思います。
フラグメントシェーダーのソースコード
precision mediump float;
uniform lowp vec4 u_color;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
void main()
{
vec4 texcolor;
vec4 monocolor;
float texcolor_brightness;
vec3 coefficient;
monocolor = u_color;
texcolor = texture2D(u_texture, v_texCoord.xy);
texcolor_brightness = max(texcolor.r, max(texcolor.g, texcolor.b));
coefficient = vec3(1.0) - monocolor.rgb;
gl_FragColor = vec4(monocolor.rgb + vec3(pow(texcolor_brightness, 3.0)) * coefficient, monocolor.a * texcolor.a);
}
uniform 変数から、単調化する画像のテクスチャを u_texture で、単調化で使う色を u_color で受け取っています。
頂点シェーダーからは varying 変数を通して、テクスチャの貼り付け位置に関する情報を v_texCoord 変数で受け取っています。
テクスチャから現在位置のピクセルの色を取り出して、その明るさを計算して、ローカル変数 texcolor_brightness に保存しています。
また、単調化で使用する色が白色になるのに必要な差分値を、透明を除く各色の要素のから計算して、それをローカル変数 coefficient に格納しています。
そして、それらの値を使って、ピクセルの色を計算し、組み込みの gl_FragColor 変数に、最終的な色として指定しています。
今回の色の計算では、明るい色ほど白色に近づきやすく、暗い色ほど単調化で使う色から離れないようにしています。
今回作成したシェーダーとサンプルコード
こんな感じで、今回は OpenGL ES 2.0 フラグメントシェーダーを使って画像のモノトーン変換をしてみました。
最低でも指定された色に留まるようにすることで、CIFilter で画像をモノトーンに変換する の時のように、黒色の画像でも色が変化するようにしています。
ボタンの画像とかに使用するような画像は、CIFilter を使うよりも理想的に仕上がっているように思います。
逆に写真データなどは CIFilter の CIColorMonochrome フィルタを使った方が自然に仕上がる印象でした。
もっと工夫すればより仕上がるかもしれないですけど、もしそうやって改良したくなったときでも、フラグメントシェーダーであれば自力で調整できるので心強いです。
今回の記事を書く上で作成したソースコードは GitHub にアップしてあります。
Xcode 4.5.2 + iPhone シミュレーター 6.0 で作成しているので、そんな環境であれば動作すると思います。同じ色で CIFilter の CIColorMonochrome フィルターを使って単調化した場合も表示するようになっています。