AWT Graphics での描画処理を高速化してみる
ゲーム開発は引き続き土台整備をしています。LWJGL でテクスチャーを読み込んでいたら、サイズが 1024 * 1024 以上の画像が 3 枚くらいになってからロード時間の長さが気になるようになりました。
画像 1 枚読み込むたびに 1 秒くらいかかるとなると、さすがに見過ごせないなーということで、きっちりプロファイリングして原因を探ってみました。
結論から言うと、読み込んだ BufferedImage を LWJGL の glTexImage2D() に渡す ByteBuffer に変換する際に Java AWT の Graphics を使っていたのが問題でした。
glTexImage2D() に渡す画像のバイトデータには、ある形式の ColorModel・Raster を使っているのですが、読み込んだ画像の画像データ形式がこれと同じという保証はないため、正しいデータ形式の BufferedImage を作って、Graphics.drawImage() で描き直していました。が、この Grpahics を使った処理が思いの外重いのです。
AWT の Graphics を使わずに、BufferedImage のピクセルデータを直接 ByteBuffer に積み直すという処理に変えてみたところ、かなり高速化することができました。
改良前後の速度差はこちら。
画像の大きさとファイルサイズが違う画像をいくつか読み込んで処理時間を計測してみました。
PNG 800*400 32.6KB
212.79561 mili sec. → 36.39858 mili sec.
PNG 500*250 13.4KB
29.673136 mili sec. → 18.440008 mili sec.
JPG 2048*2048 347KB
640.6848 mili sec. → 79.925674 mili sec.
PNG 1024*1024 1.22MB
618.1265 mili sec. → 45.179134 mili sec.
PNG 960*480 240KB
104.69864 mili sec. → 14.152516 mili sec.
のきなみ速くなってます。212.79561 mili sec. → 36.39858 mili sec.
PNG 500*250 13.4KB
29.673136 mili sec. → 18.440008 mili sec.
JPG 2048*2048 347KB
640.6848 mili sec. → 79.925674 mili sec.
PNG 1024*1024 1.22MB
618.1265 mili sec. → 45.179134 mili sec.
PNG 960*480 240KB
104.69864 mili sec. → 14.152516 mili sec.
Graphics 使用時に数百ミリ秒かかっていたのが数十ミリ秒で変換できるとなると、まさに効果は抜群だ!という感じ。1 桁減るのは大きいですよねー。
……うーん、ゲーム用途では AWT Graphics は使っちゃだめだということですね。
今回は LWJGL 用ということで ByteBuffer に積み直してますが、AWT でゲームを作る際にも、Raster に対して直接ピクセルデータを読み書きするとか、Raster から DataBufferByte(等)→ byte 配列を取り出し、直接ビット演算して BufferedImage 再構築するとかした方が早くなりそう。とにかく Graphics を介しちゃうと重くなってしまいます。
改良前のコード
glTexImage2D() 用の BufferedImage を用意して、テクスチャーにしたい画像を Graphics.drawImage() で描くという手法です。その後で描き込んだ BufferedImage を ByteBuffer に変換します。// 任意の BufferedImage srcImage を LWJGL の glTexImage2D() に渡す ByteBuffer に変換する // OpenGL のテクスチャー生成用のカラーモデル、Raster を用意する // BufferedImage はこの画像データ形式に変換する必要がある WritableRaster raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, texWidth, texHeight, 4, null); BufferedImage texImage = new BufferedImage(glAlphaColorModel, raster, false, new Hashtable()); // 異なる画像データ形式から変換するため、Graphics を使って drawImage() する Graphics g = texImage.getGraphics(); g.setColor(new Color(0f, 0f, 0f, 0f)); g.fillRect(0, 0, texWidth, texHeight); // 画像は最初に透明色で塗りつぶす g.drawImage(srcImage, 0, 0, null); g.dispose(); // 用意した BufferedImage を ByteBuffer に変換する byte[] data = ((DataBufferByte) texImage.getRaster().getDataBuffer()).getData(); ByteBuffer imageBuffer = ByteBuffer.allocateDirect(data.length); imageBuffer.order(ByteOrder.nativeOrder()); imageBuffer.put(data, 0, data.length); imageBuffer.flip();BufferedImage 作成や ByteBuffer への変換処理も処理速度を量ってみましたが、こちらはほとんど誤差としてもいいレベルで、Graphics 作成から dispose() までの時間が圧倒的に大きくなっていました。
冒頭の計測結果はこの Graphics を使った一連の処理のものです。
改良後のコード
BufferedImage の Raster から直接ピクセルデータを取り出し、RGBA の各データに分解して直接 ByteBuffer に書き込んでやります。// 任意の BufferedImage srcImage を LWJGL の glTexImage2D() に渡す ByteBuffer に変換する // OpenGL のテクスチャー生成用のカラーモデル、Raster を用意する // ここは Graphics を使う場合と同じ WritableRaster raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, 4, null); BufferedImage texImage = new BufferedImage(glAlphaColorModel, raster, true, new Hashtable()); // BufferedImage の持つ Raster からピクセルのバイトデータを取り出し、 // 変換先の ByteBuffer に直接積んでいく DataBufferByte imageBuffer = (DataBufferByte)texImage.getRaster().getDataBuffer(); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(imageBuffer.getSize()); int[] bytes = new int[4]; byteBuffer.order(ByteOrder.nativeOrder()); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { srcImage.getRaster().getPixel(x, y, bytes); byteBuffer.put(bytes[0]); // Red byteBuffer.put(bytes[1]); // Green byteBuffer.put(bytes[2]); // Blue byteBuffer.put(bytes[3]); // Alpha } } byteBuffer.flip();BufferedImage.getRaster() が遅かったりすると目も当てられない状況でしたが、嬉しいことにそれは杞憂でした。
なお、このコードは BufferedImage の type が BufferedImage.TYPE_4BYTE_ABGR(透過ありの PNG)の場合です。
他のファイル形式に対応するためには、そのファイル形式にあわせたビット演算を実装する必要があります。
例えば JPG は画像形式が BufferedImage.TYPE_3BYTE_BGR ですが、Raster.getPixel() には int[3] を渡し、Alpha 値については(透過色がないので)常に (byte)255 を積んでいく感じになります。
LWJGL のテクスチャー生成に関する各処理の実行時間
補足資料として、LWJGL でテクスチャーを生成する際の処理時間の内訳を乗せておきます。テクスチャーのバインドが遅いとかテクスチャー生成が遅いとかいろいろ言われていますが、実際どこがボトルネックなのかを計ってみました。
対象ファイルは大きさが 2048*2048 ピクセルの PNG 画像で、ファイルサイズは 5.37 MB です。
処理全体: 556.4956 mili sec.
画像の読み込み: 289.9468 mili sec.
glBindTexture: 0.652796 mili sec.
ByteBuffer への変換: 259.98352 mili sec.
glTexImage2D: 5.358586 mili sec.
今回チューニングした ByteBuffer への変換と、画像の読み込み処理がボトルネックということでした。画像の読み込み: 289.9468 mili sec.
glBindTexture: 0.652796 mili sec.
ByteBuffer への変換: 259.98352 mili sec.
glTexImage2D: 5.358586 mili sec.
画像の読み込みには ImageIO.read(FileInputStream) を使っていますが、Graphics の例もあるので個人的には少し怪しい感じ。バイトデータを BufferedImage へ変換するとなるといろいろ大変そうではありますが、もしかしたらチューニングの余地があるのかもしれません。。
テクスチャーのバインド(glBindTexture)やテクスチャー生成(glTexImage2D)は、これに比べれば問題にならない速度でした。件の話は、テクスチャーの描画がめちゃくちゃ早いので、それに比べたら遅い方という文脈なんだと思います。
ゲーム製作 コメント (0) 2012/06/15 22:44:20