東方STGノーマル大会を少し分析してみた
今日の話は大会参加者向けだけどまぁいいか。
まず、参加者のクリア最高難易度別分布図を示しておきましょう。
ってなんじゃこれ。なんでこんなにLunatic クリア経験者が多いんですか。一応ノーマル大会なんだけどなぁ。。。
次はノーマル以上でのNB達成経験者の割合。
こちらはおおよそ半々。ん〜なんかえらくレベルが高いぞこれ。
さて、結果に関する分布はこんな感じ。
まずは被弾数の分布から。
おぃおぃ。永夜抄がおかしい。NM半分ってなんですかこれは(笑)
次にNB達成者割合
妖々夢は半分くらいか。永夜抄はみんなNM狙いで行ったようだな。NBは少なかった。
最後に取得スペカ枚数の分布
ん〜もう少し細かくしたほうが良かったか。でも半分以上取得者がかなりいたね。
それにしてもレベルの高い大会でした。真ん中くらいに入れたのは大検討だったか(笑)
AndroidでOpenGL ES2を使ってたくさん描画する場合の注意点
かな〜り前にAndroidのOpenGL ES2を使ってテクスチャを表示する方法を書いた。今回は弾幕シューティングのように大量のオブジェクトを描画しないといけない場合の注意点を書いておこう。
描画命令は1回のみにする
描画命令(前回書いたサンプルではGLES20.glDrawArrays()を使ってる)はとても重い処理になるため、極力減らすべきである。できれば、1回で全部描画するくらいにしたい。
1回で描画するにはどうするか?予め座標データをたくさん送っておけばよいだけだ。
テクスチャは1つだけ用意し、作り直しはキリの良いタイミングで。
テクスチャ作成や、テクスチャ切り替えも重い処理に入る。そのため、できれば、1枚の大きめなテクスチャを用意し、そこから切り出して使うようにしたい。
データの転送は1回で
OpenGL ES2では、描画させるオブジェクトの頂点データをGPU側へ送る必要があるが、これを小分けにして送るとオーバーヘッドが増える。なので、1回で全部送るようにしたい。
データの転送量を極力減らす
これは当たり前の話。そのためには、floatの代わりにshortやbyteデータを送るようにするとか、工夫する。
ByteBufferには一括でデータを入れる
これはAndroid特有の話になるのだが、AndroidではGPU側にデータを渡すためにByteBuffer系のクラスを使用する必要がある。例えば、オブジェクトの頂点配列をshort で管理しているとする。GPU側へ渡すには short → ShortBuffer → 転送 の手順が必要になる。このとき、ShortBufferへshort[]のデータを突っ込むのだが、1オブジェクトずつShortBufferへ格納していると結構時間が掛かる。なるべく多くのデータを一括で格納したほうが、速くなる。
こんなところかな。あとは、最近の端末は複数コアなので、弾道計算をマルチスレッド化すると、相当数のオブジェクトが処理できる。ちなみに Nexus7でやってみたところ、6000個ぐらい処理させても60FPS出るというおっかない結果が出た。最近のタブレットすげ〜
具体的なコードは次回にする。
NVIDIA はやっぱりすごいかも
グラフィックボードを NVIDIA GT630に変えてみた。スペック的にはおそらくAMD Radion HD 5670のほうがいいのではないかと思うが、うるさいのが難点。
いやー、交換するまで、PC内臓のファンが五月蝿いのかと思ってたが、グラボだったようだわ。交換したら静かになった。
さて、なぜ変えたか・・・というと、AMDのドライバが最近ひどいから。理由これだけ。
どう酷いかは・・・うまく言えないが、更新すると悪くなるのはかんべんして欲しい。で新しいのでも治ってない。手なわけで見切りをつけたのだ。
で、なぜNVIDIAがすごいと思ったかというと、以前最近のグラボの問題 で書いたんだけど、AMDもintelも2Dのシューティングをやるとカクつき現象が起こるのだが、このボードはナント起きないのだ。いやー快適ですわ。静かだし。
詳細に調べてないからなんとも言えないが、GPUクロックの変動が早いのだろうか、うまくソフト的に処理してるのかわからんが、いずれにしろ、カクつきが起きないのは良いことだ。
OpenGL ES2 でテクスチャ 〜Android版〜
AndroidでOpenGL ES2に関しては、探すと結構いいのが見つかるので残さなくても良いかな?とも思ったけど、まぁ、メモ程度に残しておくことにした。iOS版と手順はほぼ同じ。ただ、eclipseではテンプレート作ってくれないので(作ってくれるのあると思うんだけど)、ぜーんぶ自分で作る必要があるところが違うかな。という訳で今回は長そうだ。
あ、importするものは、eclipseの補完機能で全部解決できると思うので省略する。
まず、Activityから。あ、プロジェクト名は適当で。とりあえずgltestなんて名前にしてみた。
public class GltestActivity extends Activity { GLSurfaceView glView; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); glView = new GLSurfaceView(this); glView.setEGLContextClientVersion(2); glView.setRenderer(new GLRenderer(glView)); setContentView(glView); } }
GLSurfaceViewを作成し、Rendererをセットします。このとき、セットするRendererは自前で作成するクラスにします。あとは、OpenGL ES2を使用することをGLSurfaceViewにsetEGLContextClientVersion()で設定します。
次にRendererを作っていきます。
import android.opengl.GLSurfaceView.Renderer; public class GLRenderer implements Renderer { ・・・略・・・ }
中身は後で書くとして、GLRendererクラスはandroid.opengl.GLSurfaceView.Rendererを継承して作成するので、以下の3つは必ず実装します。
public void onDrawFrame(GL10 arg0) public void onSurfaceChanged(GL10 gl, int width, int height) public void onSurfaceCreated(GL10 gl, EGLConfig config)
まずは、onSurfaceChanged()から・・・といきたいところだけども、テクスチャ作成にviewが要るので、コンストラクタで渡してもらったGLSurfaceViewをストックしておきます。ついでに必要なものも作成しておきます。
private GLSurfaceView mView; private FloatBuffer mVertexBuffer; private FloatBuffer mTexcoordBuffer; private final float vertexs[] = { -1.0f, -1.0f, 0.0f, //left top -1.0f, 1.0f, 0.0f, //left bottom 1.0f, -1.0f, 0.0f, //right top 1.0f, 1.0f, 0.0f, //right bottom }; private final float texcoords[] = { 0.0f, 0.0f, //left top 0.0f, 1.0f, //left bottom 1.0f, 0.0f, //right top 1.0f, 1.0f, //right bottom }; public GLRenderer(GLSurfaceView view) { mView = view; mVertexBuffer = ByteBuffer.allocateDirect(vertexs.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mVertexBuffer.put(vertexs).position(0); mTexcoordBuffer = ByteBuffer.allocateDirect(texcoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mTexcoordBuffer.put(texcoords).position(0); }
OpenGLのAPIでvetexとtexcoordの配列を渡したいのだけれど、FloatBufferでないと渡せないので、これを作成しておきます。
onSurfaceChanged()を実装しましょう。やることは、シェーダの作成、テクスチャ作成、OpenGLの各設定です。
private int program; private int mTexture; private int shPosition; private int shTexcoord; private int shTexture; public void onSurfaceCreated(GL10 gl, EGLConfig config) { mTexture = loadTexture(R.drawable.back); int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); program = GLES20.glCreateProgram(); GLES20.glAttachShader(program, vertexShader); GLES20.glAttachShader(program, fragmentShader); GLES20.glLinkProgram(program); GLES20.glUseProgram(program); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glEnable(GLES20.GL_BLEND); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); shPosition = GLES20.glGetAttribLocation(program, "position"); shTexcoord = GLES20.glGetAttribLocation(program, "texcoord"); shTexture = GLES20.glGetUniformLocation(program, "texture"); GLES20.glEnableVertexAttribArray(shPosition); GLES20.glEnableVertexAttribArray(shTexcoord); }
シェーダのコンパイルはこんな感じ
private int compileShader(int type, String code) { int shader = GLES20.glCreateShader(type); GLES20.glShaderSource(shader, code); GLES20.glCompileShader(shader); //↓ここの部分はあってもなくても良い。コンパイルエラーのときのログが見られる。 int logLength[] = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_INFO_LOG_LENGTH, logLength, 0); if (logLength[0] > 0) { String log = GLES20.glGetShaderInfoLog(shader); Log.e("GLRenderer", "Shader compile log:" + log); } //↑ここまで int status[] = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0); if (status[0] == 0) { Log.e("GLRender", "Couldn't compile shader"); GLES20.glDeleteShader(shader); return -1; } return shader; }
glCompileShader()でシェーダをコンパイルして、glGetShaderiv()でステータスを確認しています。
テクスチャ作成はこんな感じ
private int loadTexture(int res) { int texture[] = new int[1]; Resources r = mView.getResources(); Bitmap bmp; bmp = BitmapFactory.decodeResource(r, res); GLES20.glGenTextures(1, texture, 0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]); GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bmp, 0); bmp.recycle(); return texture[0]; }
テクスチャはビットマップデータから作成するので、BitmapFactoryを使ってビットマップを作成して、テクスチャを用意します。
最後に、描画。
public void onDrawFrame(GL10 arg0) { GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexture); GLES20.glUniform1i(shTexture, 0); GLES20.glVertexAttribPointer(shTexcoord, 2, GLES20.GL_FLOAT, false, 0, mTexcoordBuffer); GLES20.glVertexAttribPointer(shPosition, 3, GLES20.GL_FLOAT, false, 0, mVertexBuffer); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); }
この辺はiOS版と同じなので。
今回はここまで。
PNG画像からテクスチャ作成
mTexture = [self loadTexture:@"back.png"];
の中身振れてなかったんで、今回はテクスチャの作り方について。
画像を読み込んで、CGContextを使ってビットマップデータを作成して、テクスチャにします。
- (GLuint) loadTexture:(NSString*)fileName { GLuint texture; CGImageRef img = [UIImage imageNamed:fileName].CGImage; int w, h; w = CGImageGetWidth(img); h = CGImageGetHeight(img); GLubyte* data = (GLubyte*)malloc(w * h * 4); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef ctx = CGBitmapContextCreate(data, w, h, 8, w * 4, colorSpace, kCGImageAlphaPremultipliedLast); CGContextClearRect(ctx,CGRectMake(0, 0, w, h)); CGContextDrawImage(ctx, CGRectMake(0, 0, w, h), img); glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); free(data); return texture; }
こんな感じ。ただ、これは、画像サイズでテクスチャ作ってるので、1つのテクスチャに複数の画像を貼る場合は、そういう画像をあらかじめ用意するか、このloadTexture関数を工夫してくださいな。