OpenGLによる描画 ― 2013/11/03
正確にいうと、OpenGL ES 2.0になりますか。
よくあるサンプルですが、三角形ポリゴンを表示してみることにします。
と、その前に
・描画全体の初期化など、共通の処理
・アプリ毎の処理
を区別するために、今のうちにソースを分けておきます。
jni
├ sys
│ ├ common.h いろいろ共通事項のヘッダ
│ ├ SysMain.cpp native側のメイン処理
│ ├ Renderer.h 描画管理クラスヘッダ
│ ├ Renderer.cpp 描画管理クラスソース
├ def.h アプリの定義
└ AppMain.cpp アプリのメイン処理
SysMain.cpp
native側のメイン処理、java側から呼ばれる initNativeや updateNativeは、ここに移しました。
見ての通り初期化とフレーム毎の稼働で、描画管理とアプリの処理をそれぞれ呼んでいるだけです。
Renderer.h
Renderer.cpp
描画全体の管理をするクラス sys::Rendererです。
初期化処理は以下の通り。
・シェーダの初期化
OpenGL ES 2.0はプログラマブルシェーダなので、シェーダの設定が必要です。
今回は、
・頂点シェーダは、行列projectionによる座標変換
・フラグメントシェーダは、カラーcolorを設定
という、ごく単純なものになっています。
・座標変換行列の設定
座標はそのままだと左上(-1.0, -1.0)-右下(1.0, 1.0)になりますが、2Dゲームでこの座標ではどうにも使いにくいです。
画面の解像度を決めて、座標を変換して使うようにします。画面解像度はアプリ毎に決めるので、def.hに定義しておきます。
行列mat_projectionによる変換で、画面上の座標は、
(-SCREEN_WIDTH/2, -SCREEN_HEIGHT/2) - (SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
になります。
2Dの画面で座標は、
(0, 0) - (SCREEN_WIDTH, -SCREEN_HEIGHT)
というのが多いとは思いますが、後に出てくるスプライト等も中心を原点にしたいということもあり、(0, 0)を中心とするような形にしました。
・ビューポートの設定
Androidの端末は画面解像度もバラバラなら、縦横の比率もバラバラです。
画面の比率をSCREEN_WIDTH、SCREEN_HEIGHTに合わせるようにglViewportでビューポートを設定しています。
次にフレーム毎の処理ですが、今回はとりあえずαブレンドだけしておきます。
アプリの処理
AppMain.cpp
初期化処理は、今回は単なる生ポリゴンなので何も無いです。
実際に三角形を描画する、フレーム毎の処理は以下の通りです。
・画面クリア
glClearで画面をクリアします。必ず必要な処理というわけではないので、sys::Rendererではなくアプリ側で行っています。デプスバッファでも使っていれば、共通処理でクリアしたほうが良いのでしょうが。
・シェーダプログラムの使用
今回のようにシェーダが一つしかないのであれば sys::Rendererで設定してしまっても良いのですが、後々シェーダの切り替えを行うことを考えてアプリ側で設定しています。
・三角形ポリゴンの描画
頂点座標・カラーを設定した後、glDrawArraysで GL_TRIANGLESを指定して三角形を描画します。
よくあるサンプルですが、三角形ポリゴンを表示してみることにします。
と、その前に
・描画全体の初期化など、共通の処理
・アプリ毎の処理
を区別するために、今のうちにソースを分けておきます。
jni
├ sys
│ ├ common.h いろいろ共通事項のヘッダ
│ ├ SysMain.cpp native側のメイン処理
│ ├ Renderer.h 描画管理クラスヘッダ
│ ├ Renderer.cpp 描画管理クラスソース
├ def.h アプリの定義
└ AppMain.cpp アプリのメイン処理
SysMain.cpp
#include "common.h" #include "Renderer.h" using namespace sys; extern "C" { JNIEXPORT void JNICALL Java_sys_BaseActivity_initNative(JNIEnv*, jobject, jint, jint); JNIEXPORT jboolean JNICALL Java_sys_BaseActivity_updateNative(JNIEnv*, jobject); } void init_app(void); // メイン初期化 Bool update_app(void); // メイン稼働 /************ 初期化 ************/ JNIEXPORT void JNICALL Java_sys_BaseActivity_initNative(JNIEnv* env, jobject, jint width, jint height) { LOGI("initNative (%d, %d)", width, height); Renderer::init(width, height); // 描画管理初期化 init_app(); // アプリメイン初期化 } /********** 稼働 **********/ JNIEXPORT jboolean JNICALL Java_sys_BaseActivity_updateNative(JNIEnv*, jobject) { Renderer::update(); // 描画管理稼働 if ( update_app() ) { // アプリメイン稼働 return JNI_TRUE; } return JNI_FALSE; // アプリ終了 } /**************** End of File *************************************************/
native側のメイン処理、java側から呼ばれる initNativeや updateNativeは、ここに移しました。
見ての通り初期化とフレーム毎の稼働で、描画管理とアプリの処理をそれぞれ呼んでいるだけです。
Renderer.h
#ifndef ___RENDERER_H___ #define ___RENDERER_H___ #include "common.h" #include <GLES2/gl2.h> #include <GLES2/gl2ext.h> namespace sys { /************************ シェーダプログラム ************************/ struct ShaderProgram { GLuint program; // プログラムオブジェクト GLint position; // 座標 GLint color; // カラー GLuint projection; // 透視変換 ShaderProgram(void) // コンストラクタ { program = 0; } ~ShaderProgram() // デストラクタ { quit(); } void init(GLuint, GLuint); // 初期化 void use(const GLfloat*); // 使用 void quit(void); // 終了 }; /************** 描画管理 **************/ class Renderer { private : static void initShader(void); // シェーダ初期化 static GLuint loadShader(GLenum, const char*); // シェーダ作成 public : static ShaderProgram* shader; // シェーダプログラム static float mat_projection[4*4]; // 透視変換行列 static void init(int, int); // 初期化 static void quit(void); // 終了 static void update(void); // 稼働 static ShaderProgram* use_shader(void); // シェーダ使用 }; } #endif /********************* End of File ********************************/
Renderer.cpp
/*************************** 描画管理 ***************************/ #include "Renderer.h" namespace sys { ShaderProgram* Renderer::shader; // シェーダプログラム float Renderer::mat_projection[4*4]; // 透視変換行列 /************************************************ 初期化 引数 width, height = 端末画面サイズ ************************************************/ void Renderer::init(int width, int height) { initShader(); // シェーダ初期化 for (int i = 0; i < 4*4; i++) { // 透視変換行列初期化 mat_projection[i] = 0.0f; } mat_projection[0] = 2.0f/SCREEN_WIDTH; mat_projection[5] = -2.0f/SCREEN_HEIGHT; mat_projection[10] = 1.0f; mat_projection[15] = 1.0f; int _w, _h; if ( width*SCREEN_HEIGHT < height*SCREEN_WIDTH ) { // 横長(上下カット) _w = width; _h = width*SCREEN_HEIGHT/SCREEN_WIDTH; } else { // 縦長(左右カット) _w = height*SCREEN_WIDTH/SCREEN_HEIGHT; _h = height; } glViewport((width - _w)/2, (height - _h)/2, _w, _h); // ビューポート設定 } /******************** シェーダ初期化 ********************/ void Renderer::initShader(void) { shader = new ShaderProgram(); GLuint vert_shader, frag_shader; static const char gVertexShader[] = // 頂点シェーダプログラム "attribute vec4 position;" "attribute vec4 color;" "varying vec4 vColor;" "uniform mat4 projection;" "void main() {" "gl_Position = projection*position;" "vColor = color;" "}"; static const char gFragmentShader[] = // フラグメントシェーダプログラム "precision mediump float;" "varying vec4 vColor;" "void main() {" "gl_FragColor = vColor;" "}"; vert_shader = loadShader(GL_VERTEX_SHADER, gVertexShader); // 頂点シェーダ frag_shader = loadShader(GL_FRAGMENT_SHADER, gFragmentShader); // フラグメントシェーダ shader->init(vert_shader, frag_shader); // シェーダプログラム作成 glDeleteShader(vert_shader); glDeleteShader(frag_shader); } /******************************************* シェーダ作成 引数 type = シェーダ種類 source = プログラムソース 戻り値 シェーダオブジェクト *******************************************/ GLuint Renderer::loadShader(GLenum type, const char* source) { GLuint _shader = glCreateShader(type); // シェーダオブジェクト作成 GLint _compiled = 0; assert(_shader != 0); glShaderSource(_shader, 1, &source, NULL); // プログラムソース設定 glCompileShader(_shader); // コンパイル glGetShaderiv(_shader, GL_COMPILE_STATUS, &_compiled); // コンパイル結果取得 if ( !_compiled ) { // コンパイル失敗 GLchar* _buf; GLint _len; glGetShaderiv(_shader, GL_INFO_LOG_LENGTH, &_len); if ( (_len > 0) && (_buf = new GLchar[_len]) ) { glGetShaderInfoLog(_shader, _len, NULL, _buf); // エラーログ取得 LOGE("Could not compile shader %d:\n%s\n", type, _buf); delete[] _buf; } glDeleteShader(_shader); return 0; } return _shader; } /************************************************* シェーダプログラム作成 引数 v_shader = 頂点シェーダ f_shader = フラグメントシェーダ *************************************************/ void ShaderProgram::init(GLuint v_shader, GLuint f_shader) { program = glCreateProgram(); // プログラムオブジェクト作成 assert(program != 0); glAttachShader(program, v_shader); // 頂点シェーダアタッチ glAttachShader(program, f_shader); // フラグメントシェーダアタッチ GLint _linked = GL_FALSE; glLinkProgram(program); // リンク glGetProgramiv(program, GL_LINK_STATUS, &_linked); // リンク結果取得 if ( !_linked ) { // リンク失敗 GLchar* _buf; GLint _len; glGetProgramiv(program, GL_INFO_LOG_LENGTH, &_len); if ( (_len > 0) && (_buf = new GLchar[_len]) ) { glGetProgramInfoLog(program, _len, NULL, _buf); // エラーログ取得 LOGE("Could not link program:\n%s\n", _buf); delete[] _buf; } glDeleteProgram(program); program = 0; return; } position = glGetAttribLocation(program, "position"); // 座標 color = glGetAttribLocation(program, "color"); // カラー projection = glGetUniformLocation(program, "projection"); // 透視変換 } /********** 終了 **********/ void Renderer::quit(void) { delete shader; // シェーダ削除 } void ShaderProgram::quit(void) { if ( program ) { glDeleteProgram(program); } } /********** 稼働 **********/ void Renderer::update(void) { glEnable(GL_BLEND); // αブレンド初期化 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } /************************** シェーダ使用 戻り値 シェーダ **************************/ ShaderProgram* Renderer::use_shader(void) { shader->use(mat_projection); return shader; } void ShaderProgram::use(const GLfloat* mat_projection) { glUseProgram(program); glUniformMatrix4fv(projection, 1, GL_FALSE, mat_projection); glEnableVertexAttribArray(position); glEnableVertexAttribArray(color); } } /**************** End of File ******************************************/
描画全体の管理をするクラス sys::Rendererです。
初期化処理は以下の通り。
・シェーダの初期化
OpenGL ES 2.0はプログラマブルシェーダなので、シェーダの設定が必要です。
今回は、
・頂点シェーダは、行列projectionによる座標変換
・フラグメントシェーダは、カラーcolorを設定
という、ごく単純なものになっています。
・座標変換行列の設定
座標はそのままだと左上(-1.0, -1.0)-右下(1.0, 1.0)になりますが、2Dゲームでこの座標ではどうにも使いにくいです。
画面の解像度を決めて、座標を変換して使うようにします。画面解像度はアプリ毎に決めるので、def.hに定義しておきます。
#ifndef ___DEF_H___ #define ___DEF_H___ static const int SCREEN_WIDTH = 640, // 画面サイズ SCREEN_HEIGHT = 960; #endif /********************** End of File ****************************************************/
行列mat_projectionによる変換で、画面上の座標は、
(-SCREEN_WIDTH/2, -SCREEN_HEIGHT/2) - (SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
になります。
2Dの画面で座標は、
(0, 0) - (SCREEN_WIDTH, -SCREEN_HEIGHT)
というのが多いとは思いますが、後に出てくるスプライト等も中心を原点にしたいということもあり、(0, 0)を中心とするような形にしました。
・ビューポートの設定
Androidの端末は画面解像度もバラバラなら、縦横の比率もバラバラです。
画面の比率をSCREEN_WIDTH、SCREEN_HEIGHTに合わせるようにglViewportでビューポートを設定しています。
次にフレーム毎の処理ですが、今回はとりあえずαブレンドだけしておきます。
アプリの処理
AppMain.cpp
#include "common.h" #include "Renderer.h" /************ 初期化 ************/ void init_app(void) { } /****************************** 稼働 戻り値 アプリ続行か ******************************/ Bool update_app(void) { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 画面クリア static const GLfloat vertex[] = { 0.0f, -480.0f, -320.0f, 480.0f, 320.0f, 480.0f }; static const GLubyte color[] = { 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, }; sys::ShaderProgram* _shader = sys::Renderer::use_shader(); // シェーダ使用 glVertexAttribPointer(_shader->color, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, color); // カラー glVertexAttribPointer(_shader->position, 2, GL_FLOAT, GL_FALSE, 0, vertex); // 頂点 glDrawArrays(GL_TRIANGLES, 0, 3); // 三角形描画 return TRUE; } /**************** End of File *************************************************/
初期化処理は、今回は単なる生ポリゴンなので何も無いです。
実際に三角形を描画する、フレーム毎の処理は以下の通りです。
・画面クリア
glClearで画面をクリアします。必ず必要な処理というわけではないので、sys::Rendererではなくアプリ側で行っています。デプスバッファでも使っていれば、共通処理でクリアしたほうが良いのでしょうが。
・シェーダプログラムの使用
今回のようにシェーダが一つしかないのであれば sys::Rendererで設定してしまっても良いのですが、後々シェーダの切り替えを行うことを考えてアプリ側で設定しています。
・三角形ポリゴンの描画
頂点座標・カラーを設定した後、glDrawArraysで GL_TRIANGLESを指定して三角形を描画します。
最近のコメント