OpenGL ESで2D描画してみよう
by K.I
2017/06/25〜
Index
- OpenGLは、GPUを使って3Dデータを高速に描画するグラフィックライブラリ
- 基本的な処理をGPUで行うので、メインのプログラムに影響を与えず、高速に描画することが出来る
- OpenGLを使うのは全く初めてなので、調べたことを基に自分なりに理解した事を纏めてみた
- 自分としては、とりあえずAndroid上での2D描画だけ出来れば良い
- まぁ、とにかく2D描画に限定して、出来るだけ簡単にOpenGLを使ってみようということで。
- OpenGL ESとは、OpenGL for Embedded System の略
- ESはエンベデッドシステム、つまり組込み用に機能を絞り込んだもの
- OpenGL ES1.0と、ES2.0は全く別物らしい
- ES1.0は、固定機能パイプラインという組込みのシェーダがあったが、
- ES2.0からは、シェーダを自分で記述してロードする必要がある
- その分、自由度があるらしいので、ES2.0の方が面白そうだ。
- 以降、この記事中では特に断りがない限り、
- OpenGLといえば、OpenGL ES2.0の事を表すものとする
[top]
- OpenGLでの描画を、簡単に表すと、こんな感じになる
- バーテックスシェーダ
- クリッピング座標系にポリゴンを配置する →頂点座標の変換
- Viewport変換
- クリッピング座標系のポリゴンのXY座標を、実際のスクリーン座標に変換
- ラスタライザ
- ポリゴンの頂点座標から、描画する表面のデータ(フラグメント)を生成する
- フラグメントシェーダ
- それで、バーテックスシェーダと、フラグメントシェーダを、
- プログラムしなきゃいけないという、ちょっと面倒な仕様になっている
- OpenGLのラスタライザは、この座標系のポリゴンしか描画できないらしい
- 結局、座標変換して、クリッピング座標にポリゴンを配置する必要がある
- この座標変換をバーテックスシェーダにやらせることになる
- 3Dだと、最終的に視点を設定して、透視投影とか平行投影する
- でも、とりあえず2Dしか扱わないので、座標変換については考えないで、
- クリッピング座標に直接配置することにしようと思う1
- クリッピング座標系のポリゴンのXY座標を、Viewport座標に変換する
- ポリゴンの頂点データから、描画する表面のデータ(フラグメント)を生成する
- 描画モードにより、頂点データをどのように描画するか決まる
- ポリゴンの表裏を判定して、ポリゴン表面のフラグメントを生成
- バーテックスシェーダからの頂点毎のデータを補間、フラグメント毎のデータにして、フラグメントシェーダに渡す
- ポリゴンが直線ならば頂点の間を線で繋ぐ、3角形ならば頂点間を面で埋める
- ポリゴンの裏表や、z軸を使った前後の関係から、画素の描画の可否判定も行うらしい
- まぁ、ラスタライザについては自分は全く分かっていないが、
- とにかく、いろいろ難しいことをして、フラグメントシェーダに渡す画素情報を作る
- とりあえず、2Dの描画しかしないので、今はこれぐらいの理解で良しとしよう
1つまり、座標変換が全て完了しているものと同義。
[top]
- シェーダプログラムは、GLSL (OpenGL Shading Language)という言語で記述され、
- これはGPU上で動作するので高速に描画することが出来る
- バーテックスシェーダは、
- プログラムが描画する頂点を、クリッピング座標に配置する
- フラグメントシェーダは、
- シェーダプログラムはテキストで定義して、実行時にコンパイル2される
- バーテックスシェーダの役割は、座標変換して、クリッピング座標に配置することになる
- プログラムから頂点情報を受け取り、クリッピング座標系に変換して、ラスタライザに渡す
- ローカル座標
- ワールド座標変換
- ビュー座標変換
- 視点となる座標、視点の先、上方向を指定して
- 透視投影、平行投影を行う
- クリッピング座標
- 視野空間をを立方体で切り取る
- そして切り取った範囲の中心を原点として、-1〜1の範囲に正規化する
- バーテックスシェーダは、最終的にgl_Positionに変換後の座標を出力する
- projectionMatrixは、ローカル座標からワールド座標変換、ビュー座標変換、透視・平行投影等の変換行列を纏めたもので、
- もし初めからxyzの座標が、例えば-1〜1の範囲ならば、座標変換行列は必要ないのでもっと簡単になる
- バーテックスシェーダでは、gl_Positionは、必ず設定しなければならない。
- 画素単位で色をgl_FragColorに指定する
- この場合、頂点毎に指定されたものを補間した色になる
- バーテックスシェーダ経由で渡された場合は、補間されるというのが特徴的と思う
- フラグメントシェーダでは、gl_FragColorを設定することで、画素が描画される
- OpenGLプログラムから、頂点毎にバーテックスシェーダに渡される変数
- glGetAttribLocationで、attribute変数のLocationを取得
- glVertexAttribPointerで、attribute変数に頂点情報を書き込む
int vposx = GLES20.glGetAttribLocation(shaderProg, "vpos"); // attribute変数vposのindex取得
GLES20.glEnableVertexAttribArray(vposx); // attribute変数のアクセスを有効に
GLES20.glVertexAttribPointer
(vposx, 2, GLES20.GL_FLOAT, false, 0, vpos); // 頂点情報のindexと頂点毎のデータ数を設定
// 描画
GLES20.glDisableVertexAttribArray(vposx); // attribute変数と頂点情報の対応付けを無効に
- attribute変数は、頂点毎のデータを渡すのに使われ、
- 使用前に、glEnableVertexAttribArrayで有効に、使用後にglDisableVertexAttribArrayで無効にする
- OpenGLプログラムから、描画するプリミティブ毎に、バーテックスシェーダ、フラグメントシェーダに渡される変数
- glGetUniformLocation関数で、uniform変数のLocationを取得して、
- uniform変数の書換え、例えば、float変数x4ならば、gluniform4f関数を使う
int fcolx = GLES20.glGetUniformLocation(shaderProg, "fcol"); // uniform変数fcolのindex取得
GLES20.glUniform4fv(fcolx,1,fcol); // 色情報の場所と書式を設定
- uniform変数は、プログラムで設定してシェーダで使われるグローバル変数のようなものだと思う
- バーテックスシェーダから、フラグメントシェーダに渡される変数
- バーテックスシェーダでは、頂点毎に設定されたデータなので、
- フラグメントシェーダには、補間された値が渡されることになる
- バーテックスシェーダとフラグメントシェーダの両方で、宣言しておく必要がある。
型 | 意味 |
void | 戻り値を持たない関数、引数リストが空を示す |
bool | 条件型(true/false) |
int | 符号付整数 |
float | 単精度浮動小数点スカラー |
vec2 | 2成分浮動小数点ベクトル |
vec3 | 3成分浮動小数点ベクトル |
vec4 | 4成分浮動小数点ベクトル |
bvec2 | 2成分boolベクトル |
bvec3 | 3成分boolベクトル |
bvec4 | 4成分boolベクトル |
ivec2 | 2成分整数ベクトル |
ivec3 | 3成分整数ベクトル |
ivec4 | 4成分整数ベクトル |
mat2 | 2×2浮動小数点行列 |
mat3 | 3×3浮動小数点行列 |
mat4 | 4×4浮動小数点行列 |
sampler2D | texture2D関数で使う2Dテクスチャ |
samplerCube | textureCube関数で使うテクスチャ |
- スウィズル演算子
- ベクトルの各要素に、ドットを使って xyzw、或いはrgba, stpq のようにアクセス可能
vec4 v1 = vec4(1.0, 2.0, 3.0, 4.0);
vec3 v2 = v1.yzw; // vec3(2.0, 3.0, 4.0)
vec2 v3 = v1.xy; // vec2(1.0, 2.0)
vec4 v4 = v1.xyxy; // vec4(1.0, 2.0, 1.0, 2.0)
vec4 v5 = vec4(v3, 0.0, 1.0); // vec4(1.0, 2.0, 0.0, 1.0)
- v5の例の様に、Verilogみたいに連結することも出来る
- 関数も一通り使える
- 数学関数:pow, exp, log, sqrt, abs, min, max
- 三角関数:sin, cos, tan, asin, acos, atan
- ベクトル関数:length, distance, dot, cross
2実行時にコンパイルするって、この仕様はちょっと驚いた。中間コード的なものにしても良かったんじゃないかな。
[top]
- とにかく、Androidで出来るだけ簡単にOpenGLを使ってみよう
- クリッピング座標系(xyzが-1〜1の範囲)に2Dの単純な図形を描くだけなら、
- バーテックスシェーダは座標を渡すだけ、フラグメントシェーダは色を設定する簡単なものになる
- OpenGLで、描くことの出来る図形は、基本的には、点と線、そして三角形だけらしい
- AndroidManifest.xmlに、以下を追加
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
- GooglePlayでのダウンロードを OpenGL ES 2.0使用の端末に限定する
- 描画対象のActivityにsetContentViewで、
- GLSurfaceViewへの描画は、GLRendererを通して行う
- RENDERMODE_CONTINUOUSLYでは、常に描き換えるが、
- RENDERMODE_WHEN_DIRTYにすると、requestRender()した時のみ描き換える
- JavaのByteBufferをネイティブのメモリ上にバッファを作成3、するユーティリティを用意する
- こちらの記事のBufferUtilを参考にして、可変長引数で直接定義するメソッドを追加した
- GLRendererを継承したクラスを作成して、
- ViewではonDrawで描画していたが、GLRendererはonDrawFrameで描画する
public class GLRenderer implements GLSurfaceView.Renderer {
private final Context context;
public GLRenderer(final Context cont){
context = context;
}
Triangle triangle;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// シェーダプログラムの準備
triangle = new Triangle();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// ビューポート設定
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 unused) {
// 背景色を青色で塗潰す
GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
// 三角形を赤色で
FloatBuffer vpos = BufferUtil.convertx(
0.0f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f );
FloatBuffer fcol = BufferUtil.convertx(1.0f, 0.0f, 0.0f, 1.0f);
// 描画する
triangle.draw(fcol, vpos);
}
}
- 背景色を青色にして、赤色で三角形を描画する
- triangle.drawが肝心の OpenGLの描画部分だが、長くなるので Triangleクラスに纏めた
- OpenGL ES2.0を使うので、GL10のパラメータは使わない
- 互換性のためだろうけど、使わないパラメータが残ってるのは気持ち悪いなぁ
3通常はJavaのバッファはヒープ上に作られるが、OpenGLのルーチンはネイティブのメモリ上のバッファを参照するため。バイトオーダーの問題もある。
[top]
- 三角形を描画するクラス Triangleを作成する
- コンストラクタで、シェーダプログラムを生成し、 drawメソッドで、三角形を描画する
- これはクリッピング座標系に、指定した色で描画するだけの、とても簡単なプログラムにした
- 以下は、Triangleクラスの中身を順に説明したもの
- バーテックスシェーダは、頂点毎に呼出されるので、
- 座標値は、xyの2次元のデータだが、
- gl_Positionは、xyzwの4次元のデータ4なので、z=0.0,w=1.0に設定する
- シェーダのコンパイルは共通なので、メソッドにしておく
private static final int INVALID = 0;
private static final int FIRST_INDEX = 0;
public static int ldShader(final int shaderType, final String source) {
int shader = GLES20.glCreateShader(shaderType);
if (shader != INVALID) {
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
final int[] compiled = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, DEFAULT_OFFSET);
if (compiled[FIRST_INDEX] == INVALID) {
Log.e(TAG, "ldShader:" + shaderType + ":" + GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = INVALID;
}
}
return shader;
}
- シェーダのエラーは、Javaのコンパイラでは分からないので、エラーチェックも入れておく
- シェーダのコンパイルを実行時に行うのは、確かに自由度はあると思うけど、
- Triangleクラスのコンストラクタで、シェーダプログラムをコンパイルして、準備しておく
private int shaderProg;
public Triangle(){
int vShader = ldShader(GLES20.GL_VERTEX_SHADER, vShaderCode); // バーテックスシェーダコンパイル
int fShader = ldShader(GLES20.GL_FRAGMENT_SHADER, fShaderCode); // フラグメントシェーダコンパイル
shaderProg = GLES20.glCreateProgram(); // シェーダプログラム生成
GLES20.glAttachShader(shaderProg, vShader); // バーテックスシェーダを追加
GLES20.glAttachShader(shaderProg, fShader); // フラグメントシェーダを追加
GLES20.glLinkProgram(shaderProg); // シェーダプログラムをリンク
GLES20.glDeleteShader(vShader); // シェーダオブジェクト開放
GLES20.glDeleteShader(fShader);
}
シェーダプログラムを生成したら、元のシェーダオブジェクトは破棄しても良いらしい
- Triangleクラスのdrawメソッドで、実際に三角形を描画する
public void draw(FloatBuffer fcol, FloatBuffer vpos){
GLES20.glUseProgram(shaderProg); // シェーダプログラム使用開始
int fcolx = GLES20.glGetUniformLocation(shaderProg, "fcol"); // uniform変数fcolのindex取得
GLES20.glUniform4fv(fcolx,1,fcol); // 色情報の場所と書式を設定
int vposx = GLES20.glGetAttribLocation(shaderProg, "vpos"); // attribute変数vposのindex取得
GLES20.glEnableVertexAttribArray(vposx); // attribute変数のアクセスを有効に
GLES20.glVertexAttribPointer
(vposx, 2, GLES20.GL_FLOAT, false, 0, vpos); // 頂点情報のindexと頂点毎のデータ数2を設定
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vpos.capacity()/2); // 開始位置と頂点数3を指定して描画
GLES20.glDisableVertexAttribArray(vposx); // attribute変数と頂点情報の対応付けを無効に
GLES20.glUseProgram(0); // シェーダプログラム使用終了
}
- シェーダプログラムを開始したら、
- 色情報と座標情報を受け取って描画
- 最後にシェーダプログラムを終了する
- glDrawArraysのGL_TRIANGLESを、GL_TRIANGLE_FANに変更すると、
- 正規化されたクリッピング座標系を、そのままスクリーンに合わせて表示しているだけなので、
- 縦向きと横向きでは、画面のアスペクト比の違いで当然、歪みが出ている
- 歪まないように座標変換すれば良いと思うが、
- とりあえず座標変換については、今は考えないことにする
4同次座標系、描画する次元の数+1で計算することで、座標計算がやりやすいらしい。
[top]
- OpenGLで描画すると、基本的に後から描画したもので上書きされるが、
- ブレンディングを有効にすると、元画像と描画する画像の混ぜ合わせ方を指定できるようになる
- これは、アルファブレンディングと呼ばれ、透明色の処理等に使用される
- OpenGLでは、このブレンドのモードをいろいろ柔軟に設定できる
- レンダラの生成時に、アルファブレンドの指定を入れてみる
- まず glEnableで、GL_BLENDを指定することで、アルファブレンディングを有効にする
- そしてglBlendFuncで、ブレンドする係数を指定する
- source →描画元:これから描画するもの
- destination →描画先:フレームバッファに描画済みのもの
- また、glBlendEquationで、合成方法を指定することが出来るらしい
- デフォルトは、GL_FUNC_ADD で単純に加算する
- 今の処、加算しか使わないので、これは特に指定しない
- まず、アルファブレンドの効果が分かり易いように、
- onDrawFrameで、3角形を2個描画するようにしておく
public void onDrawFrame(GL10 unused) {
// 背景色は黒で塗潰す
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
// 右半分を白色で描画
FloatBuffer vpos = BufferUtil.convertx(
0.0f, 0.8f, 0.0f, -0.8f, 0.8f, -0.8f, 0.8f, 0.8f );
FloatBuffer fcol = BufferUtil.convertx(1.0f, 1.0f, 1.0f, 1.0f);
triangle.draw(fcol, vpos, projectionMatrix);
int loop = 1;
float density = 1.0f;
for (int i=0; i<loop ;i++) {
// 四角形を青色で描画
vpos = BufferUtil.convertx(
-0.6f, 0.3f, -0.6f, -0.3f, 0.6f, -0.3f, 0.6f, 0.3f );
fcol = BufferUtil.convertx(0.0f, 0.0f, density, 0.3f);
triangle.draw(fcol, vpos, projectionMatrix);
// 三角形を赤色で描画
vpos = BufferUtil.convertx(
0.0f, 0.6f, -0.6f, -0.6f, 0.6f, -0.6f );
fcol = BufferUtil.convertx(density, 0.0f, 0.0f, 0.3f);
triangle.draw(fcol, vpos, projectionMatrix);
// 三角形を緑色で描画
vpos = BufferUtil.convertx(
0.0f, -0.6f, -0.6f, 0.6f, 0.6f, 0.6f );
fcol = BufferUtil.convertx(0.0f, density, 0.0f, 0.3f);
triangle.draw(fcol, vpos, projectionMatrix);
}
}
- Triangleクラスのdrawメソッドで、glDrawArraysにGL_TRIANGLE_FANを指定して多角形を描画できるようにしておく
- OpenGLのブレンドモード5による効果の違いを見てみよう
- ブレンディングなし(GL_ONE, GL_ZERO)
- 描画する色はそのまま(1)、元の色は上書き(0)なので、ブレンディングなしと同じ →S+0
- 描画する色はそのまま(1)、元の色もそのまま(1)で、加算するのでOR描画となる →S+D
- 加算+アルファ(GL_SRC_ALPHA, GL_ONE)
- 描画する色はアルファ値を掛け、元の色はそのままで加算する →Sα+D
- 乗算(GL_ZERO, GL_SRC_COLOR)
- 描画する色を0で、元の色に描画色を掛けるので、乗算になる →0+DS
- アルファブレンド(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
- 描画する色にアルファ値を掛け、元の色には1−アルファ値を掛ける →Sα+Dα
- 反転(GL_ONE_MINUS_DST_COLOR, GL_ZERO)
- スクリーン(GL_ONE_MINUS_DST_COLOR, GL_ONE)
- 描画する色は元の色成分以外で、元の色を加算する →SD+D
- XOR(GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_COLOR)
- 描画する色は元の色成分以外で、元の色は描画色以外として加算 →SD+DS
- 加算とスクリーンは全く同じ処理のように見えるが、これはRGBの値が100%だったからで、
- 今度はRGBの値を50%(density=0.5f)にしてみると違いが分かる
- 加算では、2回重ねると、50%+50%=100%なので、完全に白になり、それ以上重ねても同じ
- スクリーン(GL_ONE_MINUS_DST_COLOR, GL_ONE)
- スクリーンは、2回、3回と重ねると明るくなるが、完全に白にはならない
- 乗算(GL_ZERO, GL_SRC_COLOR)
- 乗算の場合も、2回、3回と重ねると暗くなるが、完全に黒にはならない6
- 加算は、OR合成の代わりに使えるかと思ったけど、RGB値が100%じゃない場合は色が変わってしまう
GLES20.glEnable(GL_COLOR_LOGIC_OP);
GLES20.glLogicOp(GL_OR);
- これでOR合成が出来ると思ったんだけど、OpenGL ES2.0では、glLogicOpはサポートされないらしい。。
- stackOverflowの 記事によると、どうやらダメっぽい。 GPUの制限なのかな?
- 折角、ES2.0を勉強したのに、論理演算を使うためだけに、ES1.0を使うのはちょっと悔しい
- ES1.0で論理演算できるといっても、GPUじゃなくてCPUで処理している可能性もあるし。。
- いろいろ調べてみると、glBlendFuncは、加算だけでなく減算も出来るらしい
パラメータ | 合成方法 |
GL_FUNC_ADD | 加算(S+D)→デフォルト |
GL_FUNC_SUBTRACT | 減算(S-D) |
GL_FUNC_REVERSE_SUBTRACT | 減算(D-S) |
GL_MIN | 最小 |
GL_MAX | 最大 |
- Source →描画元:これから描画するもの
- Destination →描画先:フレームバッファに描画済みのもの
- 但し、glBlendEquationが使えない端末もあるらしいので注意
- glBlendEquationで減算が出来るならば、減算・加算と続けて描画すればOR合成もどきになるんじゃないかな
- 減算(GL_ONE, GL_ONE) →D-S
- 加算(GL_ONE, GL_ONE) →(D-S)+S7
- 2回、3回と繰り返し描画しても、同色の場合は変化しないので、うまくいったらしい
- もうOR合成するなら、ES1.0を使うしかないかと思ったけど、
- 1回で描画出来ないのは残念だが、ES2.0でも何とかそれらしく出来そうだ8
5個人的には描画モードという感じがするが、OpenGLで描画モードというと、頂点情報を描画する順番を指定するものらしい。
6といっても、ほとんど黒だけど。
7これじゃ意味が無いみたいだけど、加算する成分が含まれない、或いは少ない場合は、ちゃんと加算される。
8自分としては、OR合成が出来るというだけで、OpenGLを使う意味があるかも。
[top]
// 背景色は黒で塗潰す
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
// 赤色で四角を描画
FloatBuffer vpos = BufferUtil.convertx(
-0.5f,-0.5f, 0.5f,-0.5f, 0.5f,0.5f, -0.5f,0.5f);
float[] fcol = BufferUtil.array(1.0f, 0.0f, 0.0f, 1.0f);
GLES20.glLineWidth(8f);
triangle.draw(vpos, fcol);
- Triangleクラスのシェーダと、glDrawArraysもちょっと弄ることになる
- バーテックスシェーダ9のgl_PointSizeで大きさを指定
gl_PointSize = 40.0;
プリミティブタイプに、GL_POINTSを指定して描画する
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, vpos.capacity()/2);
- 頂点のサイズの範囲(GL_ALIASED_POINT_SIZE_RANGE)を確認すると、自分の環境(Nexus5)では、1〜1023だった。
- 描画される頂点は四角だが、glEnable(GL_POINT_SMOOTH)で点を丸くすることが出来る
- と思ったんだけど、やはりこれもES1.0しかできない。。
- でも、フラグメントシェーダで、円形になるように制限することはできる
vec2 pt = gl_PointCoord - vec2(0.5);
if(abs(abs(pt.x)-abs(pt.y)) > 0.1) discard;
- 頂点を任意の図形にするには、Point Spriteを使うと良いらしい
- GLES20.glLineWidthで、線幅を指定
GLES20.glLineWidth(8f);
プリミティブタイプに、GL_LINES, GL_LINE_STRIP, GL_LINE_LOOPを指定して、描画する
GLES20.glDrawArrays(GLES20.GL_LINE_LOOP, 0, vpos.capacity()/2);
→最大のWidth=8でも、この程度の太さしかない
- 線幅の範囲(GL_ALIASED_LINE_WIDTH_RANGE)を確認すると、自分の環境(Nexus5)では、1〜8だった。
- これでは、実質的に幅のある線の描画は出来ないということか。。
- OpenGLで描画可能な線幅は、最低限のものだけらしい。
- 太い線幅のデータを表示したいなら、自前でアウトラインを生成しないとダメっぽい10
- ES1.0では、線種を変更することは、glLineStippleで簡単に出来るが、
- ES2.0は、線種を変更することは出来ないらしい。。
- バーテックスシェーダで、始点からの距離vdisを一緒に読み込む
- フラグメントシェーダで、破線を描く
- あとは、距離によって描画をON/OFFすれば良い
- LINE_LOOPだと、最後の線が足りないので
- 終点を加えて、LINE_STRIPで描いてみると、こんな感じになる
- これは縦横で破線のピッチが違ってしまうので改良が必要、速度的にもちょっと心配だけど、
- まぁ、とりあえずはシェーダでいろいろと細工は出来そうだ
9ES2.0では、バーテックスシェーダで指定するしかないらしい。
10これが自分的には、OpenGLで一番ガッカリだったことだったりする。
11これがフラグメントシェーダの面白い処だと思う。
[top]
- ES1.0では、glPolygonStippleっていうのがあるらしいが、
- フラグメントシェーダで、パターンによる塗潰しをしてみる
void main() {
vec3 c = vec3(1,0,0);
float f = パターンの式;
gl_FragColor = vec4(c*f, f);
}
- これは、図形自体にパターンが描かれているのではないので、
- これは、CAD等の図形表示で使われるパターン塗潰しに使えると思う
- WebGLと、AndroidでのOpenGL ESの描画は違いもあるが、
- 基本は同じなので、シェーダのシミュレーション的な確認として使える
- sinやcosで0〜1に正規化することで、パターンをいろいろと試してみた
f = cos(gl_FragCoord.x);
f = sin(gl_FragCoord.y);
f = sin(gl_FragCoord.x+gl_FragCoord.y);
f = cos(gl_FragCoord.x-gl_FragCoord.y);
f = 1.0-(1.0-sin(gl_FragCoord.x+gl_FragCoord.y))*(1.0-cos(gl_FragCoord.x-gl_FragCoord.y));
f = 1.0-(1.0-cos(gl_FragCoord.x))*(1.0-sin(gl_FragCoord.y));
f = sin((gl_FragCoord.x+gl_FragCoord.y)/2.)*cos((gl_FragCoord.x-gl_FragCoord.y)/2.);
f = cos(gl_FragCoord.x)*sin(gl_FragCoord.y);
- 三角関数は周期的な正規化に使っているだけなので、sinでもcosでも結果は大差ない
- パターンは見やすいが、中間色があるので、CAD等では使い辛いかも
- 格子の式は冗長になってしまった。もっと良い記述方法があるかも
- 中間色を無くしたかったので、2値のパターンを作る
- 周期的にするためにmod関数(剰余)、2値化にstep関数を使ってみた
- modの除数p=6.0、stepの閾値q=4.0に設定した
f = step(q,mod(gl_FragCoord.x,p));
f = step(q,mod(gl_FragCoord.y,p));
f = step(q,mod(gl_FragCoord.x+gl_FragCoord.y,p));
f = step(q,mod(gl_FragCoord.x-gl_FragCoord.y,p));
f = step(q,mod(gl_FragCoord.x+gl_FragCoord.y,p))+step(q,mod(gl_FragCoord.x-gl_FragCoord.y,p));
f = step(q,mod(gl_FragCoord.x,p))+step(q,mod(gl_FragCoord.y,p));
f = step(q,mod(gl_FragCoord.x,p))*step(q,mod(gl_FragCoord.y,p));
f = step(q,mod(gl_FragCoord.x+gl_FragCoord.y,p))*step(q,mod(gl_FragCoord.x-gl_FragCoord.y,p));
- 2値化の関数のお陰で、なんとか条件分岐なしで記述することが出来た
- 縦横は太く、斜めパターンは細くなるので、qの値を調整した方が良いかも
- パターンの描画方法は、まだまだ改良の余地があると思うが、
- フラグメントシェーダでパターン描画できることは確認できた
- Androidの実機でも、パターン描画を確認しておく
- Nexus5でパターンを作る場合は、細かくなり過ぎるので係数を掛ける必要があった
precision mediump float;
uniform lowp vec4 fcol;
void main() {
float x = gl_FragCoord.x*0.2;
float y = gl_FragCoord.y*0.2;
float f = sin(x-y); // 右斜線パターン
gl_FragColor = vec4(fcol.rgb*f, f);
}
- シェーダは分岐を作らない方が良いらしいので、パターン毎に別々のシェーダを作らなきゃいけないのかな?12
- glUseProgramでシェーダを切り替えて、右斜線と左斜線のパターンを重ねてみた
- とりあえず、期待通りにパターンの重なりを表現できるようだ
12ここらへんは、実際にプログラムして描画速度を確認しないと分かんないかも。
[top]
- OpenGLのポリゴンの表面に、貼り付ける画像をテクスチャという
- 画像は、読込み時に上下反転され、座標は左下原点で、0〜1に正規化される
- 結果として、元画像の左上原点からの0〜1の座標系になる
- X方向をU、Y方向をVの座標で表すので、UV座標系と呼ばれる13
- U座標 →Xピクセル座標/画像の幅
- V座標 →Yピクセル座標/画像の高さ
- テクスチャのピクセル情報は、テクセルと呼ばれるらしい
- まず、テクスチャを生成して、イメージを設定しておく
// Texture生成
int[] texture = new int[1];
GLES20.glGenTextures(1, texture, 0); // Textureを1個生成(複数生成も可能)
GLES20.glActiveTexture(GLES20.GL_TEXTURE0); // Textureユニットの切替
GLES20.glBindTexture(GL_TEXTURE_2D, texture[0]); // TextureユニットにTexture[0]をバインド
// 縮小拡大時の補間設定
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
// drawable/test_image.pngをTextureユニットに設定
Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.test_image);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bmp, 0);
- glActiveTextureは、デフォルトがTEXTUREユニット0なので、この場合はやらなくても良い。
- decodeResourceのリソース名は、拡張子なしで指定14する
- 四角形にテクスチャを貼り付けてみる
- 四角形の座標vposとテクスチャのUV座標v_uvを、attribute変数でバーテックスシェーダに
- テクスチャのユニット番号ftexを、uniform変数でフラグメントシェーダに渡して、
FloatBuffer vpos = BufferUtil.convertx(
-0.5f,-0.5f, 0.5f,-0.5f, 0.5f,0.5f, -0.5f,0.5f); // 四角形の座標
int vposx = GLES20.glGetAttribLocation(shaderProg, "vpos");
GLES20.glEnableVertexAttribArray(vposx);
GLES20.glVertexAttribPointer(vposx, 2, GLES20.GL_FLOAT, false, 0, vpos);
FloatBuffer v_uv = BufferUtil.convertx(
0.0f,1.0f , 1.0f,1.0f , 1.0f,0.0f, 0.0f,0.0f); // テクスチャのUV座標
int v_uvx = GLES20.glGetAttribLocation(shaderProg, "v_uv");
GLES20.glEnableVertexAttribArray(v_uvx);
GLES20.glVertexAttribPointer(v_uvx, 2, GLES20.GL_FLOAT, false, 0, v_uv);
int ftexx = GLES20.glGetUniformLocation(shaderProg, "ftex");
GLES20.glUniform1i(ftexx, 0); // テクスチャユニット0
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vpos.capacity()/2); // 四角形描画
- 最後に、glDrawArraysのGL_TRIANGLE_FANで四角形を描画する
- 四角形の座標値は、左下、右下、右上、左上の順だが、
- テクスチャのUV座標は、左上、右上、右下、左下の順になっている
- これは、UV座標が上下反転しているためだが、ちょっとややこしい。。
attribute vec2 vpos;
attribute vec2 v_uv;
varying vec2 f_uv;
void main() {
gl_Position = vec4(vpos, 0.0, 1.0);
f_uv = v_uv;
}
フラグメントシェーダ
precision mediump float;
varying vec2 f_uv;
uniform sampler2D ftex;
void main() {
gl_FragColor = texture2D(ftex, f_uv);
}
- 結局、バーテックスシェーダでUV値を設定しておいて、
- フラグメントシェーダで、補間された座標の色を、texture2Dで設定しているだけ
- 当然、UV座標で指定すれば、一部だけ貼り付けることも可能
FloatBuffer v_uv = BufferUtil.convertx(
0.0f,0.5f , 0.5f,0.5f , 0.5f,0.0f, 0.0f,0.0f);
- texImage2Dで読み込んだテクスチャを、texture2Dで描画するという感じだ
- といっても、OpenGLでは文字を描画する命令は無いらしい
- 普通にBitmapに文字を描画してから、Bitmapをテクスチャとして貼り付けることになる
- 結局、Canvasを用意して、Bitmap作るしかないんじゃないかな。
Bitmap bitmap = Bitmap.createBitmap(128,128, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(0xffffffff);
paint.setTextSize(32);
paint.setStyle(Paint.Style.FILL);
canvas.drawText("ABC",32,32,paint);
- 英数字だけなら、文字をポリゴンにして描画した方が簡単かもしれない。
- 単純に文字を表示したいだけなら、FrameLayoutでViewを重ねても良いんじゃないか15と思う。
13これはきっと、XYZWはポリゴン座標として使っているので、次はUVということになったんじゃないかなぁ。
14画像形式は、jpg, gif, png に対応している。
15あぁでもこれは速度的にはダメかも。
[top]
- フレームバッファオブジェクト、OpenGLの描画用のバッファ、
- ColorバッファとかDepthバッファ、ステンシルバッファ等の描画バッファを纏めて管理するものらしい
- 通常は、画面表示用のFBOに描画されているが、
- 自分でFBOを作って、それに切り替えて描画することが出来る
- オフスクリーン描画は、画面の重ね合わせ、表示画面と違うサイズの描画等、
- いろいろ使い道が多いので、概要を理解しておきたいところだ
- 必要なバッファを、レンダーバッファで生成してから、何に使うかを設定する
- Colorバッファは、レンダーバッファじゃなくて、テクスチャを設定する。
int[] args = new int[1];
// FBO生成
GLES20.glGenFramebuffers(args.length, args, 0);
fboid = args[0];
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboid);
// RBO(Depthバッファ)生成、FBOに関連付け
GLES20.glGenRenderbuffers(args.length, args, 0);
rboid = args[0];
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, rboid);
GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height);
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, rboid);
// Texture(Colorバッファ)生成、FBOに関連付け
GLES20.glGenTextures(args.length, args, 0);
texid = args[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texid);
GLES20Utils.setupSampler(GLES20.GL_TEXTURE_2D, GLES20.GL_LINEAR, GLES20.GL_NEAREST);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, texid, 0);
// FBOチェック
int stat = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
if (stat != GLES20.GL_FRAMEBUFFER_COMPLETE) fbo_error();
このようにレンダーバッファの代わりに、テクスチャをバッファとして設定することも可能
- テクスチャは、あとで貼り付ける時に自由度が大きいので、よく使われるんだと思う
- でも、シェーダでFBOのテクスチャに描画、出来たテクスチャをまた改めてシェーダで描画するのは、二度手間に感じる
- 本当はFBOのレンダーバッファに描画、画面のフレームバッファにシェーダを使わずに単純にコピーしたいんだけどね。
- GLSurfaceViewも、ダブルバッファになってるらしいので、やりようはあると思うけど。16
- まずシェーダでFBOのテクスチャに描画して、出来たテクスチャをまた改めてシェーダで描画する
// 作成したFBOに切り替えて
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboid);
GLES20.glViewport(0, 0, width, height);
// ここで、FBOへのオフスクリーン描画を行う
// 画面のフレームバッファに戻して
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
GLES20.glViewport(0, 0, width, height);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
// 上で生成したテクスチャを改めて描画
// FBO開放
GLES20.glDeleteTextures(1, new int[]{texid}, 0);
GLES20.glDeleteRenderbuffers(1, new int[]{rboid}, 0);
GLES20.glDeleteFramebuffers(1, new int[]{fboid}, 0);
- ということなので、少なくともシェーダは2種類必要になる
- いろいろ面倒なので、FBOのオフスクリーン描画はクラスに纏めた方が良いと思う。
16いろいろ調べてみたけど、やり方がわからん。まぁ、画面全体のテクスチャ貼り付けならシェーダは固定だから、最初に一度作ってしまえば後はそんなに手間は掛からないんだけど。
17FBOとシェーダの管理クラス、GLSurfaceView.Rendererを継承した、FBOのオフスクリーン描画クラスを作っているようだ。
[top]
- 2D描画に限定して、出来るだけ簡単に楽してOpenGLを使おうとしたんだけど、
- 一通りやりたいことを試しただけで、随分と手間取ってしまった
- 座標変換とか投影に関しては、バッサリと端折ってしまった18が、
- それは、OpenGLのプログラムというより、行列演算の範疇な気がするので、まぁ別に考えることにしよう
- OR描画もどきや、パターンの描画もシェーダを使うことで出来るようなので、
- とりあえず興味があったけど、今まで全然分からなかったOpenGLについて、
- ちょっとだけだが、使い方が分かってきたような気がする。
- でも、OpenGLでは出来ないこともいろいろあることが分かった
- 文字を描画できない
- 任意の凹多角形を描画できない
- 幅のある線を描画できない
- もちろん、これは描画可能なポリゴンに変換してから描画すれば良い訳なんだけど、
- 通常は当たり前に用意されているような描画ルーチンが無いので、
- これから、自分でいろいろ作らなければならない。。あぁ、面倒だ。。。
18それがOpenGLで高速化される肝心なところという気もするけど、そこらへんは詳しい記事がいろいろあるしね。
[top]
- OpenGLで描画できないものは、予めいろいろ手を加える必要がありそうなので、
[top]
- いろいろ覚え書き。ちゃんと確認していないことが多い
- gl_FragCoordは、画面サイズが width, height であるとき
- 0.5 < x < width-0.5, 0.5 < y < height-0.5 の範囲で表されるらしい
- 画素の中心座標を表しているらしい
- そうすると、gl_PointCoordとかも同様なのかもしれない。
- OpenGLのパラメータを確認する →参考
- パラメータが1つの時は、2個目は0になる
private void queryGlInfo(int pname, String msg) {
int[] params = new int[64];
GLES20.glGetIntegerv(pname, params, 0);
Log.i("GLInfo", String.format(msg + ": %d - %d", params[0], params[1]));
}
- 線幅と頂点の大きさの範囲、varying変数の最大数を確認する
queryGlInfo(GLES20.GL_ALIASED_LINE_WIDTH_RANGE,"GLES20.GL_ALIASED_LINE_WIDTH_RANGE");
queryGlInfo(GLES20.GL_ALIASED_POINT_SIZE_RANGE,"GLES20.GL_ALIASED_POINT_SIZE_RANGE");
queryGlInfo(GLES20.GL_MAX_VARYING_VECTORS,"GLES20.GL_MAX_VARYING_VECTORS");
Nexus5での結果
I/GLInfo: GLES20.GL_ALIASED_LINE_WIDTH_RANGE: 1 - 8
I/GLInfo: GLES20.GL_ALIASED_POINT_SIZE_RANGE: 1 - 1023
I/GLInfo: GLES20.GL_MAX_VARYING_VECTORS: 16 - 0
- VBO(Vertex Buffer Object)は、予めGPUのメモリに頂点情報を転送することで、描画効率を上げるらしい
- GL_ARRAY_BUFFERは、頂点データの座標や色を入れるバッファ
- GL_ELEMENT_ARRAY_BUFFERは、頂点インデックスを入れる
public static int makeVBO(Buffer buffer, int size, int target) {
int[] vboid = new int[1];
GLES20.glGenBuffers(1, vboid, 0);
GLES20.glBindBuffer(target, vboid[0]);
GLES20.glBufferData(target, buffer.capacity() * size, buffer, GLES20.GL_STATIC_DRAW);
return vboid[0];
}
VBOを使わない場合は
int vposx = GLES20.glGetAttribLocation(shaderProg, "vpos"); // attribute変数vposのindex取得
GLES20.glEnableVertexAttribArray(vposx); // attribute変数のアクセスを有効に
GLES20.glVertexAttribPointer
(vposx, 2, GLES20.GL_FLOAT, false, 0, vpos); // 頂点情報のindexと頂点毎のデータ数2を設定
VBOを使う場合(ちょっと自信なし)
int vposx = GLES20.glGetAttribLocation(shaderProg, "vpos");
GLES20.glEnableVertexAttribArray(vposx);
int vposid = makeVBO(vpos, 4, GLES20.GL_ARRAY_BUFFER);
// GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vposid);
GLES20.glVertexAttribPointer
(vposx, 2, GLES20.GL_FLOAT, false, 0, 0);
- VBOに関しては、これで良いのかちゃんと確認していないので、ここまでで保留
[top]
[プログラムの部屋に戻る]