Androidアプリ開発 OpenGL シェーディング

ライティングをすることで、3DCGらしい画像を生成することができるようになった。細かくポリゴンを作っていくことで、「リアルな物体」を画面に描くことができる。すごく細かくすれば、曲線もなめらかに再現できるはず。
しかしである。細かくするにも限界がある。メモリ容量とか、体力とか。
実は、少ないデータ量でなめらかな曲面となっているように見せる方法がある。「スムースシェーディング」「グローシェーディング」と呼ばれるものである。これに対して、今までやってきたのは、「フラットシェーディング」である。
フラットシェーディングでは、面の法線と光源、視線の関係から、面の色を「一様」に計算して表示していた。面に対して法線データはひとつだけとなっていた。
スムースシェーディングでは、頂点ごとに法線データが必要となる。各頂点の法線と光源、視線の関係から色を計算し、間を補完して描画する。
前置きはこれくらいにして、やってみるべし。
まずは、法線データを頂点ごとに作成しなければならない。これは、既に作成している頂点データのようにx,y,z座標が入ったバッファを作成することで準備できる。

public class Model {
private FloatBuffer buffer;			// 頂点用バッファ
private FloatBuffer normalBuffer;	// 法線用バッファ
public Model() {
float vertex[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f,  1.0f, 0.0f,
};
ByteBuffer vb = ByteBuffer.allocateDirect(vertex.length * 4);
vb.order(ByteOrder.nativeOrder());
buffer = vb.asFloatBuffer();
buffer.put(vertex);
buffer.position(0);
float normal[] = {
1.0f, 0.0f, 1.0f,
-1.0f, 0.0f, 1.0f,
0.0f, 1.0f, 1.0f,
};
ByteBuffer nb = ByteBuffer.allocateDirect(normal.length * 4);
nb.order(ByteOrder.nativeOrder());
normalBuffer = nb.asFloatBuffer();
normalBuffer.put(normal);
normalBuffer.position(0);
}

法線は単位ベクトルで指定しないといけないのだが、サンプルは計算するのがめんどうだったので、適当なベクトル。OpenGL側で単位ベクトルに計算し直してくれたようだ。
次に、描画時に法線データがバッファに入っているので、頂点データと同様に、OpenGLにバッファの場所を教えてやる。
glNormalPointerでバッファを指定するのだが、glVertexPointerと微妙に引数が異なるので注意。
ここで、引数の指定を誤ると、GPUに正しいバッファのアドレスが渡らず、Segmentation faultで落ちることになる。OpenGLのエンジン部分は、C/C++の世界であることに注意しなければならない。

public void draw(GL10 gl) {
gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, buffer);
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
gl.glNormalPointer(GL10.GL_FLOAT, 0, normalBuffer);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
}
}

次に、描画する際に、シェーディング方式を指定する。glShadeModelGL_SMOOTHを指定してスムースシェーディングを行う。
フラットシェーディングとの違いがわかるように、左側の三角形をスムースシェーディングしてみる。
シャイニネスの値も両方で80に統一している。シェーディング方法以外はどちらも同じ条件になっている。

@Override
public void onDrawFrame(GL10 gl) {
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
// ライティングをON
gl.glEnable(GL10.GL_LIGHTING);
// 光源を有効にして位置を設定
gl.glEnable(GL10.GL_LIGHT0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, lightpos, 0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, red, 0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, blue, 0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, yellow, 0);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();		// マトリックス記憶
gl.glTranslatef(1f, 0, -3f);
gl.glRotatef(angle, 0, 1, 0);
gl.glEnable(GL10.GL_CULL_FACE);
gl.glCullFace(GL10.GL_BACK);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, gray, 0);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, gray, 0);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, gray, 0);
gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 80f);
gl.glShadeModel(GL10.GL_FLAT);
model.draw(gl);
gl.glPopMatrix();		// マトリックスを戻す
// ふたつめの描画
gl.glPushMatrix();		// マトリックス記憶
gl.glTranslatef(-1f, 0, -3f);
gl.glRotatef(angle, 0, 1, 0);
gl.glCullFace(GL10.GL_BACK);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, gray, 0);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, gray, 0);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, gray, 0);
gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 80f);
// スムースシェーディング
gl.glShadeModel(GL10.GL_SMOOTH);
model.draw(gl);
gl.glPopMatrix();		// マトリックスを戻す
angle += 0.5;			// 回転角度は最後に計算
}


スムースシェーディングの方は、ハイライトが丸い感じになっていることがわかる。
スムースシェーディングは、丸いものを少ないポリゴン数で描画できる、といったメリットがある。ただし、なめらかに見えているのは「色だけ」。形状は定義したポリゴンのままなので、あまりに荒いと角ができてしまう。
また、サンプルのように平らなものでもスムースシェーディングにより、金属的な材質感を出すことができる。

投稿者プロフィール

asai
asai
システムエンジニア
喋れる言語:日本語、C言語、SQL、JavaScript