2011年2月6日日曜日

AndroidでOpenGLを使った文字列描画

最近Blenderの記事ばかりなので今回はAndroid。
ちなみにこんな感じ。


以前、AndroidでMMDを読み込んだときにキャプチャしたもの。
端末はIS01です。以下はその時のソース。
package net.npaka.androidopengl.graphics;

import net.npaka.androidopengl.graphics._2D.SpriteTexture;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;

/**
 * フォントクラス
 */
public class Font {
 final static Paint paint = new Paint(); // 描画情報
 static FontMetrics font = paint.getFontMetrics(); // フォントメトリクス

 /**
  * コンストラクタ
  * インスタンス化禁止
  */
 Font() {}

 /**
  * 描画
  * @param text
  * @param x
  * @param y
  */
 public static void draw(String text, int x, int y) {
  // 文字サイズの取得
  int width = getWidth(text);
  int height = getHeight();

  // キャンバスとビットマップを共有化
  Bitmap image = Bitmap.createBitmap(width, height, Config.ARGB_8888);
  Canvas canvas = new Canvas(image);

  // 文字列をキャンバスに描画
  paint.setAntiAlias(true);
  canvas.drawText(text, 0, Math.abs(font.ascent), paint);

  // キャンバスデータからスプライトを作成
  SpriteTexture sprite = SpriteTexture.create(image);

  // スプライトの描画
  sprite.draw(x, y);

  // 開放
  sprite.release();
  sprite = null;
  canvas = null;
 }

 /**
  * 文字列色の設定
  * @param color
  */
 public static void setColor(int color) {
  paint.setColor(color);
 }

 /**
  * 文字サイズの設定
  * @param size
  */
 public static void setSize(int size) {
  paint.setTextSize(size);
  font = paint.getFontMetrics(); // 文字サイズ更新後のメトリクスを取得
 }

 /**
  * 文字列の幅の取得
  * @param text
  * @return 文字列の幅
  */
 public static int getWidth(String text) {
  return (int) (paint.measureText(text) + 0.5f);
 }

 /**
  * 文字列の高さを取得
  * @return 文字列の高
  */
 public static int getHeight() {
  return (int) (Math.abs(font.top) + Math.abs(font.bottom) + 0.5f);
 }
}
簡単に説明すると、文字列をBitmap化してOpenGLで
テクスチャとして描画してます。SpriteTextureでは、

・GLUtils.texImage2D
・GL10.glBindTexture
・GL10.glGenTextures
・GL10.glDrawArrays
・GL10.glDeleteTextures 等をしてます。

で、これを前回手に入れたIS03でやってみたらこんな風になりました。 


文字のところに真っ白な長方形がでて、何も見えなくなってしまいました。

Canvasで色を変更しても反映されず、GL10.glColor4fで色を変えても
四角形全体の色が変わるだけで効果なし。

で、Canvas周りがおかしいのかと思い、こんなプログラムを作ってみました。
package test.android;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.context.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;

/**
文字列描画アクティビティクラス
**/
public class TextViewTest extends Activity {
 /**
  * 初期化イベント
 **/
 @Override
 public void onCreate(Bundle bundle) {
  super.onCreate(bundle);
  setContentView( new TextView(this) );
 }

 /**
  * 文字列表示Viewクラス
 **/
 class TextView extends View {
  /**
   * コンストラクタ
  **/
  public TextView(Context context) {
   super(context);
   setBackgroundColor( Color.rgb(0, 128, 255) );
  }

  /**
   * 描画イベント
  **/
  @Override
  public void onDraw(Canvas canvas) {
   Paint paint = new Paint();

   // パラメータ設定
   paint.setAntiAlias(true);
   paint.setColor(Color.BLACK);
   paint.setTextSize(20);

   // BitmapとCanvas作成
   Bitmap image = Bitmap.createBitmap(128, 128, Config.ARGB_8888);
   Canvas image_canvas = new Canvas(image);

   // Bitmapに描画
   image_canvas.drawText("test", 20, 20, paint);

   // 画面へ描画
   canvas.drawBitmap(0, 0, image, new Paint());
  }
 }
}
直接画面へ表示せず、一旦Bitmapを中継して画面へ描画させてます。
実行してみたところ、水色の背景に黒文字で「test」と画面に出てきました(キャプチャし忘れた)。

他に怪しいところがないか色々調べてみた結果、作成したBitmapの幅がおかしいことに気付き確認したところ、「41」とか奇数が入っていてあまりPCにやさしくない数字が入ってました。

で、描画の際の文字サイズの取得方法を以下のように変更してみました。
// 文字サイズの取得
int[] size = { getWidth(text), getHeight() };
  
// 2の階乗サイズに変更
int[] power_size ={ 2, 2 };
  
for(int i = 0; i < size.length; i++) {
 while(power_size[i] < size[i]) {
  power_size[i] <<= 1;
 }
}

// キャンバスとビットマップを共有化
Bitmap image = Bitmap.createBitmap(power_size[0], power_size[1], Config.ARGB_8888);
Canvas canvas = new Canvas(image);
画像の幅が収まる2の冪乗(べきじょう)サイズを算出するようにしました。
2, 4, 8, 16, 32, 64, 128, 256, 512, 1024といった具合です。

これで再度実行してみたところ・・・ドンピシャでした。

DirectXで画像の描画をやってた頃にも似たようなことがあったのですが、
画像幅は2の冪乗ではないとうまく表示できないみたいですね。
以前エミュレータやVirtualBoxで動かした時も似たようなことがありましたが、原因はコイツのようです。

最近BlenderばかりでPythonしか触ってなかったので久しぶりのJavaに
多少戸惑いましたが、とりあえず暫くはIS01、IS03とでAndroidをやってく予定です。

簡単なモデルビューワーのようなアプリでも作ろうかと思います。
↑気が向いたらね。色々忙しいのですよ。

追記 2011/9/14:
より詳細な記述をして欲しいとコメントを頂いたので、SpriteTextureクラスのソースコードと実際の使い方も書き加えました。
package net.npaka.androidopengl.graphics._2D;

import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

import net.npaka.androidopengl.graphics.ColorValue;
import net.npaka.androidopengl.graphics.Graphics;
import net.npaka.androidopengl.math.MathHelper;
import net.npaka.androidopengl.ndk.NDKLib;
import net.npaka.androidopengl.util.BufferEditer;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.graphics.RectF;
import android.opengl.GLUtils;

/**
 * スプライトクラス
 */
public class SpriteTexture extends Graphics {
 public final static int LOAD_ERROR = -1; // 読み込みエラーメッセージ
 
 final static float[] TEXCOORDS = { 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }; // UV座標

 final static FloatBuffer TEXCOORD_BUFFER = BufferEditer.getFloatBuffer(TEXCOORDS);   // UVバッファ
 
 public int index; // イメージバインドID
 Bitmap image;  // ビットマップイメージ
 int width, height; // 画像サイズ
 
 public ColorValue color; // 色
 public float scale;  // 拡大値
 public float angle;  // 回転値

 /**
  * コンストラクタ
  */
 public SpriteTexture() {
  index = LOAD_ERROR;
  image = null;
  color = new ColorValue();
  scale = 1;
  angle = 0;
 }
 
 /**
  * 開放処理
  */
 public void release() {
  int[] textures = {index};
  gl.glDeleteTextures(1, textures, 0);
  image = null;
 }
 
 /**
  * 画像の読み込み
  * @param resourceID
  * @return 作成されたスプライトデータ
  */
 public static SpriteTexture load(int resourceID) {
        // ビットマップイメージの読み込み
        Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceID);
       
        // スプライトの作成
        return create(bitmap);
 }
 
 /**
  * スプライトの作成
  * @param bitmap
  * @return 作成されたスプライトデータ
  */
 public static SpriteTexture create(Bitmap bitmap) {
  SpriteTexture sprite = new SpriteTexture();
  
  // テクスチャIDの設定
  sprite.index = setTextureID();
  
  // ビットマップイメージの設定
  GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
  sprite.image = bitmap;
  sprite.image.recycle();
  
  // 画像サイズの取得
  sprite.width = bitmap.getWidth();
  sprite.height = bitmap.getHeight();
        
  return sprite;
 }
 
 /**
  * テクスチャIDの設定
  * @return テクスチャID
  */
 static int setTextureID() {
  int[] ids = { LOAD_ERROR };
  
  // 使用テクスチャ数の設定
  gl.glGenTextures(1, ids, 0);
  
  // IDをバインド
  gl.glBindTexture(GL10.GL_TEXTURE_2D, ids[0]);
  
  // パラメータ設定
  gl.glTexParameterx( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT );
  gl.glTexParameterx( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT );
  gl.glTexParameterx( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR );
  gl.glTexParameterx( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR );
  
  // ポリゴン色とテクスチャ色の合成方法
  gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_MODULATE);
  
  return ids[0];
 }
 
 /**
  * 描画
  * @param x X座標
  * @param y Y座標
  */
 public void draw(int x, int y) {        
  // テクスチャをバインド
  gl.glBindTexture(GL10.GL_TEXTURE_2D, index);

  // UV座標
  setTexture();
  
  // 頂点カラー
  gl.glColor4f(color.r, color.g, color.b, color.a);
  
  // 頂点バッファ
  float[] vertices = getVertices(x, screen_height - y, getWidth(), getHeight());
  gl.glVertexPointer(3, GL10.GL_FLOAT, 0, BufferEditer.getFloatBuffer(vertices));
  
  // 描画  
  gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
 }
 
 /**
  * 分割描画
  * @param x X座標
  * @param y Y座標
  * @param u テクスチャX座標
  * @param v テクスチャY座標
  * @param w テクスチャ横幅
  * @param h テクスチャ縦幅
  */
 public void drawUVWH(int x, int y, float u, float v, float w, float h) {
  // UV座標
  float[] texcoords = {
   u, v + h,
   u + w, v + h,
   u, v,
   u + w, v
  };
  
  int width = (int) (this.width * w + 0.5f);
  int height = (int) (this.height * h + 0.5f);
  
  // テクスチャをバインド
  gl.glBindTexture(GL10.GL_TEXTURE_2D, index);  
  setTexture(texcoords);
  
  // 頂点カラー
  gl.glColor4f(color.r, color.g, color.b, color.a);
  
  // 頂点バッファ  
  float[] vertices = getVertices(x, screen_height - y, width, height);
  gl.glVertexPointer(3, GL10.GL_FLOAT, 0, BufferEditer.getFloatBuffer(vertices));
  
  // 描画  
  gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
 }
 
 /**
  * 分割描画
  * @param x X座標
  * @param y Y座標
  * @param rect テクスチャ領域
  */
 public void drawRectangle(int x, int y, RectF rect) {
  // UV座標
  float[] texcoords = {
   rect.left, rect.bottom,
   rect.right, rect.bottom,
   rect.left, rect.top,
   rect.right, rect.top
  };

  int width = (int) (this.width * (rect.right - rect.left) + 0.5f);
  int height = (int) (this.height * (rect.bottom - rect.top) + 0.5f);
  
  // テクスチャをバインド
  gl.glBindTexture(GL10.GL_TEXTURE_2D, index);  
  setTexture(texcoords);
  
  // 頂点カラー
  gl.glColor4f(color.r, color.g, color.b, color.a);
  
  // 頂点バッファ 
  float[] vertices = getVertices(x, screen_height - y, width, height); 
  gl.glVertexPointer(3, GL10.GL_FLOAT, 0, BufferEditer.getFloatBuffer(vertices));
  
  // 描画  
  gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
 }
 
 /**
  * UV座標の設定
  */
 public void setTexture() {
  gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, TEXCOORD_BUFFER);
 }
 
 /**
  * UV座標の設定
  * @param texcoords
  */
 public void setTexture(float[] texcoords) {
  gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, BufferEditer.getFloatBuffer( texcoords ));
 }
 
 /**
  * UV座標の設定
  * @param texcoords
  */
 public void setTexture(FloatBuffer texcoords) {
  gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texcoords);
 }
 
 /**
  * 座標変換頂点バッファの取得(画像幅指定)
  * @param x X座標
  * @param y Y座標
  * @param width 横幅
  * @param height 縦幅
  * @return 頂点バッファ
  */
 final float[] getVertices(int x, int y, int width, int height) {
  // 座標系変換用
  int px = width >> 1;
  int py = height >> 1;

  // 頂点バッファ
  float[] vertices = {
   x, y, 0,
   x + width, y, 0,
   x, y + height, 0,
   x + width, y + height, 0,
  };

  for(int i = 0; i < 12; i += 3) {
   vertices[i] -= x + px;
   vertices[i + 1] -= y + py;

   // 拡大
   vertices[i] *= scale;
   vertices[i + 1] *= scale;

   // 回転
   float tx = vertices[i];
   float ty = vertices[i + 1];
   vertices[i] = (float) (Math.cos(MathHelper.toRadian(angle)) * tx - Math.sin(MathHelper.toRadian(angle)) * ty);
   vertices[i + 1] = (float) (Math.sin(MathHelper.toRadian(angle)) * tx + Math.cos(MathHelper.toRadian(angle)) * ty);

   // 移動
   vertices[i] += x + px;
   vertices[i + 1] += y - py;
  }

  return vertices;
 }
 
 /**
  * 横幅取得
  * @return
  */
 public int getWidth() {
  return width;
 }
 
 /**
  * 縦幅取得
  * @return
  */
 public int getHeight() {
  return height;
 }
 
 /**
  * 範囲取得
  * @return
  */
 public Rect getRect() {
  return new Rect(0, 0, width, height);
 }
}
実際に文字を描画する際はこのように使います。以下はOpenGL ESのバージョン情報を表示するサンプルプログラムです。
//---------- 初期化(一度やればよい) ----------

// カラーとテクスチャー座標の補間精度
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);
  
// アルファチャンネル機能(色設定時の透明度)ON
gl.glEnable(GL10.GL_ALPHA_TEST);
  
// ブレンディング設定
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); // デフォルトはアルファ合成
  
// テクスチャ0を有効
gl.glActiveTexture(GL10.GL_TEXTURE0);


//---------- バージョン情報取得 ----------

String version = gl.glGetString(GL10.GL_VERSION);


//---------- スプライト描画準備 ----------

// カメラ行列を2Dに変換
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
// screen_width及びscreen_heightはデバイスの画面サイズ
GLU.gluOrtho2D(gl, 0, screen_width, 0, screen_height);
  
// モデル行列を無効化
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
  
// デプスバッファを無効化する
gl.glDisable(GL10.GL_DEPTH_TEST);
  
// 法線を無効化
gl.glDisable(GL10.GL_NORMALIZE);
gl.glDisable(GL10.GL_LIGHTING);
  
// テクスチャを有効化
gl.glEnable(GL10.GL_TEXTURE_2D);


//---------- スプライト描画 ----------

// 文字サイズ設定
Font.setSize(10);

// 文字色設定(黒)
Font.setColor(Color.argb(255, 0, 0, 0));

// X:0 Y:40に描画
Font.draw(version, 0, 40);


//---------- スプライト描画終了 ----------

// テクスチャを無効化
gl.glDisable(GL10.GL_TEXTURE_2D); 

// デプスバッファ有効
gl.glEnable(GL10.GL_DEPTH_TEST);
  
// 法線有効
gl.glEnable(GL10.GL_NORMALIZE);
gl.glEnable(GL10.GL_LIGHTING);

なお、実際にスプライト描画をやるのならAndroid NDKで実装することをオススメします。たくさん描画しすると結構性能の差が実感できますよ。

2011年2月2日水曜日

Blenderのメタセコイアスクリプトの更新【機能の追加&バグの修正】

今回はBlender2.49とBlender2.5のメタセコイアスクリプトを更新しました。
更新内容は以下の通りです。

---------------------------------------------------共通部分の更新内容---------------------------------------------------

・インポーターの読み込み方法の修正

ソースが見やすくなっただけなのでとくに説明はなし。

・バイナリ頂点群の読み込みに対応。

メタセコイアのBVertexチャンクの読み込みに対応しました。
前回コメントで報告のあったメタセコイアの標準添付mqoファイルの
「body.mqo」, 「cat.mqo」, 「nas.mqo」, 「Nihonbashi.mqo」等が読み込めるように
なりました。また、線データを読み込みの際に弾くようにしています。



・「表示/非表示」と「編集可能/編集禁止」の情報の入出力処理を追加。

メタセコイアのObjectチャンクの中の「visible」と「locking」の入出力に対応しました。lockingが掛かってる場合、Blenderではそのオブジェクトが選択できないようにしています。


・提供していただいたオブジェクトのグループ化機能を追加。

新たにEmptyオブジェクトを作成し、読み込んだオブジェクトデータをEmptyオブジェクトの入れ子にしてグループ化しました(図の右側のツリーを参照)。

ファイル名のEmptyオブジェクトが新たに作成されています。
body,fire,kami,kaoオブジェクトがwitch_mqoオブジェクトの入れ子になっています。

--------------------------------------------------Blender2.5の更新内容--------------------------------------------------

上記以外に、前回の記事でいただいたコメントを元に修正を行っています。
改良していただいたソースを参考にして修正を行いました(丸々使ってる部分もあります)。
また、オリジナルのオプション機能も追加させていただきました。

・スムージング化の機能を追加。
面をスムージング化します。

・頂点データからdouble値を取り除く機能を追加。
頂点からfloat値を超える値を取り除きます。

・出力するオブジェクトを選択する機能を追加。
選択中のオブジェクト、又は全てのオブジェクトを出力対象にするかを選択できます。

・ポリゴンが欠けるバグの修正
前回のコメントにありました、「ポリゴンのインデックスリストにナンバー"0"が含まれている場合、ポリゴンが欠ける場合がある」というものです。

今回コメントに寄せていただいた情報のおかげで、把握してなかったバグを取り除き、新たな機能を追加してより使い勝手の良いスクリプトを作ることが出来ました。

コメントをくださった方々、本当にありがとうございます。