2011年06月12日

球の描画1

球の描画は、主に2パターンの方法があると思う。
1. 地球儀のような、緯度と経度で割って描画する方法。
2. 正二十面体を、さらに割って描画する方法。

1 の方法は赤道付近でポリゴンが荒く、北極・南極に近づくと細かくなっていき効率が悪い。
ただ、プログラムで球を描くケースはゲーム画面内ではほとんどない。
単にデバッグ時に当たり描画用に出す程度ではないかと思う。

とりあえず、今回は地球儀のような方法で描画してみる。
以下ソースはJava ( JOGL 2.0 ) 。

まずトライアングルストリップで描ける中間部分を描画し、後で上下端をトライアングルファンで描画している。
三角関数がわかればそれほど難しくないと思う。
加法定理を使っているのは、何度も sin/cos を計算するコストを減らすため。
上下端を別に分けているが、頂点のリストを作って後で描画するのなら、まとめてやってしまった方が加法定理の計算部分うかせられる。
そもそも毎回計算して描かなくても、頂点のリストを1度作って、マトリックスで拡大縮小すれば済む話でもあるんだけど。

public void draw( Sphere shape, GL2 gl ) {
    gl.glTranslatef( shape.center.x, shape.center.y, shape.center.z ); // 原点を中心座標に移動

    final int div_h = 40;      // 水平方向分割数
    final int div_v = div_h/2; // 垂直方向分割数
    final float theta = (float) (Math.PI * 2.0 / div_h);
    final float sinTheta = (float) Math.sin( theta );
    final float cosTheta = (float) Math.cos( theta );
    float radius = shape.radius; // 球の半径

    // 中間の帯の部分はトライアングルストリップで描ける
    float accu_v_s = sinTheta; // 垂直方向 sin
    float accu_v_c = cosTheta; // 垂直方向 cos
    float ru = radius * accu_v_s; // 現theta値で水平スライスした時の半径
    float yu = radius * accu_v_c; // 垂直方向Y軸値
    for( int y = 0; y < (div_v-2); y++ ) {

        // 加法定理によって、垂直方向のsin/cos値を更新する
        float tmp = cosTheta * accu_v_c - sinTheta * accu_v_s;
        accu_v_s = sinTheta * accu_v_c + cosTheta * accu_v_s;
        accu_v_c = tmp;

        float rd = radius * accu_v_s; // 1個後の半径
        float yd = radius * accu_v_c; // 1個後の垂直方向Y軸値

        float accu_s = sinTheta;    // 水平方向sin
        float accu_c = cosTheta;    // 水平方向cos
        gl.glBegin( GL.GL_TRIANGLE_STRIP );
        for( int x = 0; x <= div_h; x++ ) {
            gl.glVertex3f( rd * accu_c, yd, rd * accu_s );
            gl.glVertex3f( ru * accu_c, yu, ru * accu_s );

            // 加法定理によって、水平方向のsin/cos値を更新する
            tmp = cosTheta * accu_c - sinTheta * accu_s;
            accu_s = sinTheta * accu_c + cosTheta * accu_s;
            accu_c = tmp;
        }
        gl.glEnd();
        ru = rd;
        yu = yd;
    }

    // 上半球の描画
    gl.glBegin( GL.GL_TRIANGLE_FAN );
    gl.glVertex3f( 0.0f, radius, 0.0f );
    final float cap_r = radius * sinTheta; // 半径
    final float cap_y_u = radius * cosTheta; // 上半球Y軸値
    float accu_s = sinTheta;    // 水平方向sin
    float accu_c = cosTheta;    // 水平方向cos
    for( int i = 0; i <= div_h; i++ ) {
        gl.glVertex3f( cap_r * accu_c, cap_y_u, cap_r * accu_s );

        // 加法定理によって、水平方向のsin/cos値を更新する
        float tmp = cosTheta * accu_c - sinTheta * accu_s;
        accu_s = sinTheta * accu_c + cosTheta * accu_s;
        accu_c = tmp;
    }
    gl.glEnd();

    // 下半球の描画
    gl.glBegin( GL.GL_TRIANGLE_FAN );
    gl.glVertex3f( 0.0f, -radius, 0.0f );
    final float cap_y_d = -cap_y_u; // 下半球Y軸値
    for( int i = 0; i <= div_h; i++ ) {
        gl.glVertex3f( cap_r * accu_c, cap_y_d, cap_r * accu_s );

        // 加法定理によって、水平方向のsin/cos値を更新する ( 逆回転 )
        float tmp = accu_c * cosTheta + accu_s * sinTheta;
        accu_s = accu_s * cosTheta - accu_c * sinTheta;
        accu_c = tmp;
    }
    gl.glEnd();
}

で、上記ソースをコンパイルしてアプレットにしたものをここに置く
分割数が多いのでそれなりに綺麗。

投稿者 Takenori : 01:49 | トラックバック

2011年06月17日

球の描画2

球の描画1に続き球の描画。
正二十面体を使う方法。
各頂点座標は以下で求める。

a = 1.0f / sqrt(5.0f) = 0.447213595
b = (1.0f - a) / 2.0f = 0.276393202
c = (1.0f + a) / 2.0f = 0.723606797
d = sqrt(b) = 0.525731112
e = sqrt(c) = 0.850650808

頂点座標は以下のようになる。
(0,1,0)(0,a,2a)(e,a,b)(d,a,-c)(-d,a,-c)(-e,a,b)(d,-a,c)(e,-a,-b)(0,-a,-2a)(-e,-a,-b)(-d,-a,c)(0,-1,0)

参考ページ
正二十面体(正20面体、Icosahedron)の頂点情報

これがわかれば二十面体を描画するのは簡単。
以下、前回に続き Java のソース。

public void draw( Sphere shape, GL2 gl ) {
    final float IA = 0.447213595f;
    final float IB = 0.276393202f;
    final float IC = 0.723606797f;
    final float ID = 0.525731112f;
    final float IE = 0.850650808f;
    final float VTX[][] = {
        { 0.0f, 1.0f,    0.0f },    // 0
        { 0.0f,   IA, 2.0f*IA },
        {   IE,   IA,      IB },
        {   ID,   IA,     -IC },
        {  -ID,   IA,     -IC },
        {  -IE,   IA,      IB },    // 5
        {   ID,  -IA,      IC },    // 6
        {   IE,  -IA,     -IB },
        { 0.0f,  -IA,-2.0f*IA },
        {  -IE,  -IA,     -IB },
        {  -ID,  -IA,      IC },    // 10
        { 0.0f,-1.0f,    0.0f }
    };

    // { 0, 1, 2 }, { 0, 2, 3 }, { 0, 3, 4 }, { 0, 4, 5 }, { 0, 5, 1 }, // ファン, 上側
    // {11, 6, 7 }, {11, 7, 8 }, {11, 8, 9 }, {11, 9,10 }, {11,10, 6 }, // ファン, 下側
    // トライアングルファン 7頂点 * 2
    final int FAN_IDX[] = {
         0, 1, 2, 3, 4, 5, 1,
        11, 6,10, 9, 8, 7, 6
    };
    // ストリップは以下のようにつながる
    // 1 2 3 4 5 1
    //  6 7 8 9 0 6 (0=10)
    final int STRIP_IDX[] = {
         1, 6, 2, 7, 3, 8, 4, 9, 5,10, 1, 6
    };
    final float radius = shape.radius;
    Vector3f vtx[] = new Vector3f[12];
    for( int i = 0; i < 12; i++ ) {
        vtx[i] = new Vector3f( VTX[i][0], VTX[i][1], VTX[i][2] );
        vtx[i].multiply( radius );
        vtx[i].add( shape.center );
    }
    final int FAN_VERTEX_CNT = 7;
    gl.glBegin( GL.GL_TRIANGLE_FAN );
    for( int i = 0; i < FAN_VERTEX_CNT; i++ ) {
        gl.glVertex3f( vtx[FAN_IDX[i]].x, vtx[FAN_IDX[i]].y, vtx[FAN_IDX[i]].z );
    }
    gl.glEnd();
    gl.glBegin( GL.GL_TRIANGLE_FAN );
    for( int i = 0; i < FAN_VERTEX_CNT; i++ ) {
        gl.glVertex3f( vtx[FAN_IDX[7+i]].x, vtx[FAN_IDX[7+i]].y, vtx[FAN_IDX[7+i]].z );
    }
    gl.glEnd();

    // ストリップで真ん中の帯を描く
    final int STRIP_VERTEX_CNT = 12;
    gl.glBegin( GL.GL_TRIANGLE_STRIP );
    for( int i = 0; i < STRIP_VERTEX_CNT; i++ ) {
        gl.glVertex3f( vtx[STRIP_IDX[i]].x, vtx[STRIP_IDX[i]].y, vtx[STRIP_IDX[i]].z );
    }
    gl.glEnd();
}

同じようにアプレットをここに。
制止していたら話からづらいのでカメラをくるくる回すようにした。
見てわかるけど、これでは球とははほど遠い。
球に近づけるには、各三角形のそれぞれの辺の中点を求め、三角形を4分割し、中点の頂点を正規化する。
これを必要なだけ繰り返し球を作る。
特に必要なかったので、このソースは書いていないが、頂点のインデックスを持った辺 ( Edge ) とトライアングルのクラスを作ってやれば、それほど難しくないと思う。
ただ、実際にゲーム等で表示するのなら、モデリングソフトで簡単に球が作れるので、そこからデータ取ってきてそれを表示した方が楽だし早い。
だから、あんまり使うことはないと思う。

投稿者 Takenori : 21:08 | トラックバック

カメラの回転1

中心位置のオブジェクトの周りをくるくる回る簡単なカメラ
簡単。

// 以下をメンバ変数に
float mRotate= 0.0f;
static final float ROT_VAL = (float) (Math.PI/20.0); // 1フレームの回転量
static final float ROT_RADIUS = 100.0f; // カメラの距離

// 三角関数で毎回計算する
float x = (float) (Math.cos(mRotate) * ROT_RADIUS);
float z = (float) (Math.sin(mRotate) * ROT_RADIUS);
mRotate += ROT_VAL;
glu.gluLookAt(x, 0.0, z, 0.0,0.0,0.0, 0.0,1.0,0.0);


// 加法定理を使うのなら以下のようになる
// 以下をメンバ変数に
float mAccSin= 0.0f;
float mAccCos= 0.0f;
float mSumSin= 0.0f;
float mSumCos= 0.0f;
static final float ROT_VAL = (float) (Math.PI/20.0); // 1フレームの回転量
static final float ROT_RADIUS = 100.0f; // カメラの距離

// 以下の処理をフレームごとに実行する。
float tmp = mAccCos * mSumCos - mAccSin * mSumSin;
mSumSin = mAccSin * mSumCos + mAccCos * mSumSin;
mSumCos = tmp;
float x = (float) (mSumCos * ROT_RADIUS);
float z = (float) (mSumSin * ROT_RADIUS);
glu.gluLookAt(x, 0.0, z, 0.0,0.0,0.0, 0.0,1.0,0.0);

ゲームなどで上下にカメラを振る場合はこれではダメで、上下に振るカメラ位置の横軸を中心に回転させる必要があるので、クォータニオンで回転させるのが手軽。

投稿者 Takenori : 21:24 | トラックバック

 
Total : Today : Yesterday :