Nanashi-softプログラマ専用AndroidでOpenGL ES 2.0プログラミング


◇AndroidでOpenGL ES 2.0プログラミング -ポリゴン表示-

前回作成した雛形を元に,ポリゴン表示の為の仕組みを追加していきます
ただし,現状はヘルパー関数も存在せず,かなり難易度が高いです
際物好きの私としては,楽しいのですけれどね(*'-')

まず,OpenGL ES 2.0は,DirectX11のようにプログラマブルシェーダーです
GLSL ES 1.0(OpenGL Shading Language)にて記述を行います
シェーダーを記述しなければ描画することはできません

GLSL1.0は,頂点シェーダーとフラグメントシェーダーの組み合わせで処理をおこないます
頂点シェーダーでポリゴン描画をおこない,フラグメントシェーダーで色を付けると考えて問題ないでしょう

プログラムは文字列でソースに埋め込みます
//頂点シェーダーGLSL
const char *GLSL_VS=
"attribute vec4 pos;\n"
"uniform mat4 World;\n"
"uniform mat4 View;\n"
"uniform mat4 Projection;\n"
"void main(){\n"
" gl_Position=World*pos;\n"
" gl_Position=View*gl_Position;\n"
" gl_Position=Projection*gl_Position;\n"
"}\n";

//フラグメントシェーダーGLSL
const char *GLSL_FS=
"void main(){\n"
" gl_FragColor=vec4(0.0, 1.0, 1.0, 1.0);\n"
"}\n";
何書いているのか,ちゃんとブラウザで表示されるのかわからない感じですが(^^;

頂点シェーダーのattributeで定義された変数に,頂点データを流し込みます
uniformで定義された変数に,メインプログラムから値を放り込みます
この違いは後でとても重要になります(ココでハマりましたから)

関数名はmainに決まっているようです
フラグメントシェーダーに送る計算後の頂点データは,gl_Position変数に入れる事が決まっているようです

フラグメントシェーダーは,とりあえずシアンで塗りつぶす単純なものにしてあります

シェーダープログラムは,実行時にコンパイルします
その前に関数の外に↓こういうのを定義しておきます
float debug_r=0.0f;
float debug_g=0.0f;
float debug_b=0.0f;
シェーダーコンパイルは起動時に一度だけ行えばよいので,onSurfaceCreatedに記述します
C言語で記述する関数は↓ココね
void Java_jp_testndk_testRenderer_NDKonSurfaceCreated(JNIEnv* env)

//頂点シェーダー生成
GLuint vertexShader=glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &GLSL_VS, NULL);
glCompileShader(vertexShader);
//エラー時には緑にしてみる
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &retflg);
if( ! retflg){
debug_g=1.0f;
}
頂点シェーダーである事を示すGL_VERTEX_SHADERを引数にして,シェーダーオブジェクト(?)を生成します
ソースを格納した文字列変数を指定して,コンパイルします
	//フラグメントシェーダー生成(ピクセルシェーダー)
GLuint fragmentShader=glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &GLSL_FS, NULL);
glCompileShader(fragmentShader);
//エラー時には赤にしてみる
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &retflg);
if( ! retflg){
debug_r=1.0f;
}
同様に,フラグメントシェーダーもコンパイルします
	//プログラムシェーダー生成(2つをくっつける)
shaderProgram=glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);
//エラー時には青にしてみる
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &retflg);
if( ! retflg){
debug_b=1.0f;
}
そうして,2つを1まとめにします
この点が DirectX11と大きな違いではないでしょうか?
今後フラグメントシェーダーは頻繁に切り替える必要があるので,どうなるか心配なところです

で,エラー時に,RGBそれぞれのデバッグ用変数に値を入れておいたので,
描画時に,そのカラーパレットを反映します
void Java_jp_testndk_testRenderer_NDKonDrawFrame(JNIEnv* env)
{
glClearColor(debug_r, debug_g, debug_b, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
これで,エラー時には画面の色が変わって知らせてくれます('-'*)
シェーダーエラー時には,クリアしか動作しないところを利用したシグナルです

モデルデータの頂点データとインデックスデータを入れます
グローバル変数に定義します
//頂点の配列データ
const int TYOUTEN=8;
float hVectorData[]={
-1.00000, 1.00000,-1.00000,
1.00000, 1.00000,-1.00000,
1.00000,-1.00000,-1.00000,
-1.00000,-1.00000,-1.00000,
1.00000, 1.00000, 1.00000,
1.00000,-1.00000, 1.00000,
-1.00000, 1.00000, 1.00000,
-1.00000,-1.00000, 1.00000
};

//頂点番号を知らせる
const int INDEXSU=36;
GLuint hIndexData[]={
0,1,2,
0,2,3,
1,4,5,
1,5,2,
4,6,7,
4,7,5,
6,0,3,
6,3,7,
6,4,1,
6,1,0,
3,2,5,
3,5,7
};
このデータは 8コの頂点と 36コの三角ポリゴンからなる,正立方体(サイコロ形)です
~ココにプログラムシェーダー生成~
//頂点データを頂点シェーダーに設定
GLuint vs_pos=glGetAttribLocation(shaderProgram, "pos");
glEnableVertexAttribArray(vs_pos);
//5番目の値は,1データのサイズで,その単位でシェーダーで読み取る
glVertexAttribPointer(vs_pos, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, hVectorData);
頂点シェーダーで定義してた attribute vec4 pos変数に,頂点データを流し込みます

glVertexAttribPointerの引数は,今後シェーダーにデータを送る上で法則を熟知しておく方が良いです
glVertexAttribPointer(
    glEnableVertexAttribArrayした変数,
    1度に送る個数。次に指定する型単位の数,
    送る変数の型,
    転置するかどうか?(普通はしない),
    構造体のバイト数。1回に送るデータ量のこと,
    構造体の先頭からのバイト数
);

Directx11の頂点レイアウト D3D11_INPUT_ELEMENT_DESCと同じ考え方のものです

次に,頂点シェーダーで使用する変換用行列をセットします
	//ワールド変換用行列を生成
nsMATRIX hWorld;
nsMatrixIdentity(&hWorld); //初期化

//ワールド変換行列を頂点シェーダーに設定
GLuint vs_World=glGetUniformLocation(shaderProgram, "World");
glUniformMatrix4fv(vs_World, 1, GL_FALSE, (float*)&hWorld);
ワールド変換の為のマトリックスデータを生成して,頂点シェーダーで定義した uniform mat4 World変数にセットします


//ビュー変換用行列を生成
nsMATRIX hView;
nsMATRIX hEye;
nsMATRIX hAt;
nsMATRIX hUp;
hEye.m[0][0]=0.0f; hEye.m[0][1]=0.0f; hEye.m[0][2]=-5.0f; hEye.m[0][3]=0.0f; //カメラの位置
hAt.m[0][0]=0.0f; hAt.m[0][1]=0.0f; hAt.m[0][2]=0.0f; hAt.m[0][3]=0.0f; //焦点の位置
hUp.m[0][0]=0.0f; hUp.m[0][1]=1.0f; hUp.m[0][2]=0.0f; hUp.m[0][3]=0.0f;
nsMatrixLookAtLH(&hView, hEye, hAt, hUp);

//ビュー変換行列を頂点シェーダーに設定
GLuint vs_View=glGetUniformLocation(shaderProgram, "View");
glUniformMatrix4fv(vs_View, 1, GL_FALSE, (float*)&hView);
同様に,ビュー変換用の行列を,頂点シェーダーで定義した uniform mat4 View変数にセットします
	//透視射影変換行列を生成
nsMATRIX hProjection;
nsMatrixPerspectiveFovLH(&hProjection, ToRadian(60.0f), 1.0f, 0.1f, 1000.0f);

//透視射影変換を頂点シェーダーに設定
GLuint vs_Projection=glGetUniformLocation(shaderProgram, "Projection");
glUniformMatrix4fv(vs_Projection, 1, GL_FALSE, (float*)&hProjection);
最後に,透視射影変換用の行列を,頂点シェーダーで定義した uniform mat4 Projection変数にセットします

……で,nsMATRIXとはなんぞや? と言うと,自作ヘルパー関数です(ぉ
ヘルパー関数が見当たらないのですよ……
どうせまた際物機器でプログラミングしていくと,同じ問題に直面するだろう(*'-')
というわけで,作ってみました(ぁ
//自作ヘルパー関数
#define PI ((float)3.141592654f)
#define ToRadian(var) ((var) * (PI / 180.0f))
#define cot(var) (1 / tan(var)) //cot(x)は1/tan(x)らしい

#pragma pack(push, 1)
//行列変数
typedef struct nstMATRIX{
float m[4][4];
}nsMATRIX;
#pragma pack(pop)

//行列の引き算(p1=h1-h2)
void nsMatrixMinus(nsMATRIX *p1, nsMATRIX h1, nsMATRIX h2){
int i,j;
for(j=0; j < 4; j++){
for(i=0; i < 4; i++){
p1->m[j][i]=h1.m[j][i] - h2.m[j][i];
}
}
}

//正規化
void nsMatrixNormalized(nsMATRIX *p){
float veclen=sqrt(p->m[0][0] * p->m[0][0] + p->m[0][1] * p->m[0][1] + p->m[0][2] * p->m[0][2]);

p->m[0][0]=p->m[0][0] / veclen;
p->m[0][1]=p->m[0][1] / veclen;
p->m[0][2]=p->m[0][2] / veclen;
}

//外積
void nsMatrixCross(nsMATRIX *p1, nsMATRIX h1, nsMATRIX h2){
p1->m[0][0]=h1.m[0][1] * h2.m[0][2] - h1.m[0][2] * h2.m[0][1];
p1->m[0][1]=-h1.m[0][0] * h2.m[0][2] + h1.m[0][2] * h2.m[0][0];
p1->m[0][2]=h1.m[0][0] * h2.m[0][1] + h1.m[0][1] * h2.m[0][0];
}

//内積
void nsMatrixDot(float *p1, nsMATRIX h1, nsMATRIX h2){
*p1=h1.m[0][0] * h2.m[0][0] + h1.m[0][1] * h2.m[0][1] + h1.m[0][2] * h2.m[0][2];
}

//行列の初期化
void nsMatrixIdentity(nsMATRIX *p){
//対角線に1.0fを入れる
p->m[0][0] = 1.0f; p->m[0][1] = 0.0f; p->m[0][2] = 0.0f; p->m[0][3] = 0.0f;
p->m[1][0] = 0.0f; p->m[1][1] = 1.0f; p->m[1][2] = 0.0f; p->m[1][3] = 0.0f;
p->m[2][0] = 0.0f; p->m[2][1] = 0.0f; p->m[2][2] = 1.0f; p->m[2][3] = 0.0f;
p->m[3][0] = 0.0f; p->m[3][1] = 0.0f; p->m[3][2] = 0.0f; p->m[3][3] = 1.0f;
}

//ビュー変換行列生成
void nsMatrixLookAtLH(nsMATRIX *p, nsMATRIX eye, nsMATRIX at, nsMATRIX up){
nsMATRIX z;
nsMatrixMinus(&z, at, eye); //w=at - eye
nsMatrixNormalized(&z); //zを正規化

nsMATRIX x;
nsMatrixCross(&x, up, z); //up×z
nsMatrixNormalized(&x); //zを正規化

nsMATRIX y;
nsMatrixCross(&y, z, x); //z×x

float wx;
nsMatrixDot(&wx, x, eye); //x・eye

float wy;
nsMatrixDot(&wy, y, eye); //y・eye

float wz;
nsMatrixDot(&wz, z, eye); //z・eye

p->m[0][0]=x.m[0][0]; p->m[0][1]=y.m[0][0]; p->m[0][2]=z.m[0][0]; p->m[0][3]=0;
p->m[1][0]=x.m[0][1]; p->m[1][1]=y.m[0][1]; p->m[1][2]=z.m[0][1]; p->m[1][3]=0;
p->m[2][0]=x.m[0][2]; p->m[2][1]=y.m[0][2]; p->m[2][2]=z.m[0][2]; p->m[2][3]=0;
p->m[3][0]=-wx; p->m[3][1]=-wy; p->m[3][2]=-wz; p->m[3][3]=1;
}

//透視射影変換行列生成
void nsMatrixPerspectiveFovLH(nsMATRIX *p, float radian, float aspect, float zmin, float zmax){
float ys=cot(radian / 2);
float xs=ys / aspect;

float w22=zmax / (zmax - zmin);
float w32=-zmin * zmax / (zmax - zmin);

p->m[0][0] = xs; p->m[0][1] = 0.0f; p->m[0][2] = 0.0f; p->m[0][3] = 0.0f;
p->m[1][0] = 0.0f; p->m[1][1] = ys; p->m[1][2] = 0.0f; p->m[1][3] = 0.0f;
p->m[2][0] = 0.0f; p->m[2][1] = 0.0f; p->m[2][2] = w22; p->m[2][3] = 1.0f;
p->m[3][0] = 0.0f; p->m[3][1] = 0.0f; p->m[3][2] = w32; p->m[3][3] = 0.0f;
}
こんな感じ?('-'*)?

描画を行う時のモードを指定します
	glEnable(GL_CULL_FACE); //カリングを有効に
glCullFace(GL_BACK); //両面描画

glFrontFace(GL_CCW); //左手座標系にする
テスト中は,両面描画にすることで,何も表示されない原因の1つを減らす事ができます
OpenGLは右手座標系なのですが,私の場合は DirectX11とソース互換を持たせる為に,左手座標系に変更しました

ここまで準備してようやく 3D描画が可能になります
void Java_jp_testndk_testRenderer_NDKonDrawFrame(JNIEnv* env)
{
glClearColor(debug_r, debug_g, debug_b, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//インデックス描画
glDrawElements(GL_TRIANGLES, INDEXSU, GL_UNSIGNED_INT, hIndexData);
}
三角ポリゴンなので GL_TRIANGLESを指定します
後は,送信するインデックスデータに合わせて設定します

これを実行すると,中央に立方体が表示されます
平面にしか見えませんが,ちゃんと 3Dに表示されていますよ(汗


TOPプログラマ専用AndroidでOpenGL ES 2.0プログラミング

メイナの実験場~素朴なツインテ娘+たくし上げ~