3D座標変換

解説


ここからが3Dの本番となります。 今回は3Dの中で一番基本であり、数ええられないくらいの 3D 初心者を消してきた「座標変換」について説明します。 今までの中で間違いなく強敵で、数学もどんどん出てきます。 それでも 3D ゲームを作るに当たってこの行列変換は必ず必要になってきます。 気合を入れましょう。

座標変換って何?


そもそも座標変換とは何なのでしょうか? 座標については知ってますよね。 変換といえば、文字を変えていくあの変換ですよね。 座標変換とは文字通り「座標を変えていきます。」

3Dの座標変換は数式で演算することもできますが、通常は行列を用いて変換していきます。 さて、ここでプリミティブのことを思い出してください。 ポリゴンを形成するプリミティブは全て頂点から形成されています。 ということは頂点を操作すれば図形が変わるということです。 そこで、この性質を用いてこの頂点に対して行列を演算して、求めたい座標にします。 このように行列を用いて図形を変化させます。

まずは拡大・縮小行列から見ていきましょうか。 2次元の拡大縮小行列は以下のようになっています。

2ji_kake.png


この行列を以下の四角形のポリゴンに適用します。

zahyou_mini.png


適用の仕方は簡単です。 ただ単に座標と行列をかければいいだけです。以下は(1,1)座標の点を(2,1)倍したときの計算です。

kaku_kei.png


このように一つずつ頂点に行列を乗算します。 そして先ほどの図の頂点すべてに行列を掛けた場合は以下のようになります。

zahyou_big.png


これでうまく拡大されていますよね。 このように行列を掛けることによって拡大を簡単に表せることができます。 次に回転です。回転の行列は以下のようになっています。

rot_siki.png


さて今度は(1,1)の座標を90度回転してみましょう。

rot_keisan.png


上のように(1,1)の点は(-1,1)に置き換えられました。 以下全ての頂点に対して適用したときの画像です。

rot_shutu.png


最後にややっこしいとされる平行移動行列があります。 平行移動の数式は以下の式で表せるとおもいます。

X' = X + Tx (Xはもとの点、Txは移動した量、X'は移動した後の点)
Y' = Y + Ty (Yはもとの点、Tyは移動した量、Y'は移動した後の点)


このとき今まで掛け算で表していたのに、平行移動だけ足し算で実装と言うのはなんかいやです。 どうにかして掛け算の行列で表したいです。 そこで先ほどの式を以下のように変形させてみます。

X' = X * 1 + Y * 0 + 1 * Tx 
Y' = X * 0 + Y * 1 + 1 * Ty


この時の掛けている右側の係数を行列化させたものが平行移動行列です。

hei_gyou.png


このように平行移動行列は次元が一つ増えて 3 * 3 行列になります。 このときの次元が増えた座標を同時座標といいます。 同時座標を使うと拡大・縮小が簡単に表せますが、今回は説明を省略します。

それでは先ほどの行列でうまくいくか例を出します。 座標(1,1)を(0,1)分移動させたいとします。

trans.png


このとき第三番目の座標は気にしないでください。 第一番目の座標X、第二番目の座標Yの値をみてください。 うまく移動できて、(1,1)から(1,2)に変わってますよね。 それでは最後にすべての頂点を適用した場合の図を見ましょうか。

trans_keka.png


今までの行列演算でおかしいと思った方がいたらすばらしいです。 実はDirectXでは通常の数学とは行列の演算順序を逆にしてるんですよね。 数学では座標 * 行列の演算順序であるのに、DirectXでは行列 * 座標の演算順序で表しています。

迷惑な話ですが、このようなことで配列を効率よく使うことができ、メモリ効率が上がるらしいです。 ただ、これだと行列の演算順序が変わると結果が変わってしまいます。 そこで、DirectXでは行列を転置させて結果を変えないようにしています。 ややっこしい。

ジオメトリ(ビューイング)パイプライン

。 3Dと2Dというのは全く違ってるように見えて、実は一緒の次元で考えられて作られています。 以下の図は2Dと3Dは関係を表しています。

3d_2d_pro.png


3Dの座標というのは座標変換を通して全て2Dに変換されて表示されているのです。 モニタは2Dですから、当たり前といえば当たり前なんですよね。

しかし、適当に変換されてるだけでは3Dを2Dに変換することができません。 そこで出てくるのがビューイングパイプラインです。 ビューイングパイプラインは以下のような決められた行列を掛けていくことで3Dを2Dにする処理のことを言います。

afin_nagare.png


もうわけわかりませんね。 まぁ、順に説明していきましょう。 とりあえず、3Dから2Dにするためにはこのビューイングパイプラインが必要だと覚えといてください。

ワールド(もしくは世界)座標変換


ワールド座標変換を説明する前に座標系について知らないといけません。 座標系には左手座標系と右手座標系があります。 以下の図はその座標系を表した図です。

zahyoukei.png


この座標系の違いは Z 座標の正の方向が違うという点です。 左手座標系では Z 座標の正の方向は奥に、右手座標系では Z 座標の正の方向は手前に伸びています。 方向が逆に向いているということは左手座標系では正だったのに、右手では負という位置関係になってしまうこともあります。

DirectXではどちらを使わなければならないということはありません。 ですが、サンプルでは左手座標系を使ってることが多いので今回は左手座標系で話していきます。

ここで本題のワールド座標変換について説明します。 ワールド座標変換とはローカル座標からワールド座標に変換することを表します。

ローカル座標、ワールド座標とはなんなのでしょうか? 例を表して説明してみます。

すきやきの家の 100 M先にコンビニがあります。 これはすきやきの家を中心としたローカルな座標ですよね。 これがローカル座標です。

ですが、すきやきの家といってもアメリカ人に言ってもわかるはずないですよね。 そこで、経度〜度、緯度〜度と言えばアメリカ人でもわかるでしょう。 これがワールド座標です。 以下青い四角のローカル座標からワールド座標への変換です。

world_keka.png


まず、ポリゴンを初めローカル座標で組み立てます。 それをワールド変換行列でローカル座標からワールド座標に変換します。 このときワールド変換行列は先ほど話した平行移動行列、回転行列、スケーリング行列を使っていきます。

ここで問題になってくるのが拡大もしたくて、回転もしたいといったときです。 そこで、行列の演算しても最終的な結果は同じといった性質を使います。 拡大の後、回転をしたい場合は以下のようにします。

求めたい行列 = 拡大行列 * 回転行列


このように行列同士の演算で求めることができます。 ここで注意しないといけないのは平行移動行列の演算です。

たとえば自分を中心に 90 度回転した後に原点から右に10M移動すると位置は当たり前ですが、位置は右に10Mのところですよね。 ですが、原点から右に10M 移動した後に、原点を中心に 90 度回った場合は角度だけでなく位置も変わってしまいますよね。 他の行列は変わらないのですが、平行移動行列は演算順序によって変わってきます。

draw_junjo.png


それで、対策ですが、以下のように行列の演算順序を守ってください。。

求めたい行列 = 拡大行列 * 回転行列 * 平行移動行列


このような順序でかけていきますときちんと求めたい行列が求められます。 行列の順序を変えますとまったく違った演算結果が返ってきます。 これは平行行列が交換法則が成り立たないからです。 試しに行列の演算順序を変えてみてください。 百聞は一見に如かずともいいますからね。

理論はここまでにして、プログラムで表してみましょうか。 今までは二次元の話でしたので三次元で表してみます。 ですが、実際のDirect Graphicsでは三次元ですよね 三次元といっても先ほどまでの二次元と同じように同じように行列を掛けていくだけです。

問題はこの行列に値をSetすると言うことです。 いちいちこの行列の中に値をSetしては大変です。 そこでDirectXでは以下のような関数で3次元行列を簡単に取得することができます。

種類行列DirectXの関数
スケーリング行列(拡大縮小)
scale_3d.png
D3DXMatrixScaling?
(D3DXMATRIX*, //出力行列
float, //X座標の拡大値
float, //Y座標の拡大値
float //Z座標の拡大値
);
回転行列(X軸)
rotx_3d.png
D3DXMatrixRotationX
(
D3DXMATRIX*,//出力行列
float //X 軸回転の回転角度(弧度法)
);
回転行列(Y軸)
roty_3d.png
D3DXMatrixRotationY
(
D3DXMATRIX*,//出力行列
float //Y 軸回転の回転角度(弧度法)
);
回転行列(Z軸)
rotz_3d.png
D3DXMatrixRotationZ
(
D3DXMATRIX*, //出力行列
float //Z 軸回転の回転角度(弧度法)
);
回転行列(Y軸 X軸 Z軸)
rotz_3d.png
D3DXMatrixRotationYawPitchRoll?
(
D3DXMATRIX*,//出力行列
float, //&color(#FF0000){Y 軸中心回転の回転角度(弧度法)
float, //&color(#FF0000){X 軸中心回転の回転角度(弧度法)
float //Z 軸中心回転の回転角度(弧度法)
);
平行移動行列
trans_3d.png
D3DXMatrixTranslation?
(
D3DXMATRIX*, //出力行列
float, //X座標移動量
float, //Y座標移動量
float //Z座標移動量
);


3次元の場合ですと、平行移動行列のことを考えて 4 * 4にしなくてはいけませんね。 注意しないといけないのはD3DXMatrixRotationYawPitchRoll?関数はY軸、X軸、Z軸中心回転となります。 X軸,Y軸,Z軸の順序ではないですよ。

後は上の変換行列を頂点に演算すればワールド座標にすることができます。
ところで、このワールド変換行列の前と後の図形ってほとんど同じ形ですよね。 このように変換によっても図形の形がかわらないことをアフィン変換といいます。

ビュー(もしくはカメラ)座標変換


一つ目の変換行列であるワールド座標変換が済みました。 ですが、このままだとどこから物体を見ているのかがわかりません。 我々はGPSのようなワールド座標系で物事を見ているのではなく、各人の視点から見てますよね。

そこで出てくるのが今回のビュー座標変換です。 ビュー座標変換とはワールド座標をビュー座標にすることを言います。 ビュー座標ではカメラの位置を定義することによって物体をビュー座標にすることができます。

カメラの位置の定義としてはカメラの位置、カメラの対象物(焦点)の位置、そしてカメラの上などがあります。

camera_zu.png


なぜカメラの上を定義するのかと言いますと、テレビを寝転んでみるのと正座してみるのとでは違いますよね。 カメラも同様で上がはっきりしないと全く違った出力になります。

実装についてですが、このビュー座標変換はワールド座標変換と同じことをします。 ワールド座標変換は定義された位置まで平行移動しました。

ビュー座標変換は逆に、カメラの座標を定義された位置から原点まで移動します。 そしてその負の移動座標を全てのオブジェクトに適用します。 そうするとでカメラを中心とした座標ができます。 以下の図はビュー座標変換をしてカメラが原点に戻った様子を表します。

view_trans.png


さてカメラを移動した後は軸の調整をしなくてはなりません。 カメラの軸は以下のようにして求まります。

zaxis = normal(At - Eye)        //z軸にカメラをあわせる カメラが向いてる方向がz軸だよね
xaxis = normal(cross(Up, zaxis))//x軸にカメラを合わせる 向いてる方向と上の法線だとx軸が出るよね。
yaxis = cross(zaxis, xaxis)     //y軸にカメラを合わせる 同じ理由でy軸が出るよね。


この求めた軸に合わせて全てのオブジェクトを回転させます。 以上を踏まえるとビュー座標変換行列は以下のようになります。

xaxis.x           yaxis.x           zaxis.x          0
xaxis.y           yaxis.y           zaxis.y          0
xaxis.z           yaxis.z           zaxis.z          0
-dot(xaxis, eye)  -dot(yaxis, eye)  -dot(zaxis, eye)  1


このビュー行列を作成するプログラムををわざわざ自分たちで作る必要はありません。 以下の関数でビュー行列を簡単に作ることができます。

変換行列の種類変換行列
ビュー変換行列
(左手座標)
D3DXMATRIX *D3DXMatrixLookAtLH
(
D3DXMATRIX *pOut, //出力される行列
CONST D3DXVECTOR3 *pEye,//カメラの座標 CONST D3DXVECTOR3 *pAt, //カメラの注目している点
CONST D3DXVECTOR3 *pUp //カメラの上
);
ビュー変換行列
(右手座標)
D3DXMATRIX *D3DXMatrixLookAtRH
(
D3DXMATRIX *pOut, //出力される右手座標行列
CONST D3DXVECTOR3 *pEye,//カメラの座標
CONST D3DXVECTOR3 *pAt, //カメラの注目している点
CONST D3DXVECTOR3 *pUp //カメラの上
);

プロジェクション(もしくは射影)座標変換


先ほどはカメラの位置などを求めました。 ですが、実際の世界はズームなどの機能もありますね。 同様にプログラムの世界もカメラの設定をしなくてはなりません。 このカメラの設定をするのがこのプロジェクション座標変換行列です。

プロジェクション座標変換行列には二通りあります。 一つは平行投影、もう一つは透視投影です。

平行投影は視点の手前側にあるのと視点の奥行きにあるものの大きさは同じ大きさになります。 以下の図のような視点となっているためです。

heikou.gif


それに対してDirectXなどで使われる手法は透視投影といわれるものです。 実際にカメラに覗いてるように物体が視点に近いと大きくなり、遠いと小さくなります。

tousi.gif


今回はDirectXに使われる透視投影について説明します。 以下の図を見てください。

sisuidai.png


図は視錘台と呼ばれるものです。 この視錘台の中にあるオブジェクトがカメラが見えるものです。 このときの視錘台を定義するための要素が前方クリップ面、後方クリップ面、アスペクト比、視覚です。

前方クリップ面、後方クリップ面はこの二つのクリップ面の間が見えます。 視覚はカメラの角度です。ズームインやズームアウトといったことができます。 アスペクト比は横と縦の比率を決めます。

それでは今回もまた変換行列を生成してみますか。 以下の流れはプロジェクション変換行列を生成する関数です。

   float    h, w, Q;

   w = (float)1/tan(fov_horiz*0.5);  // 1/tan(x) == cot(x)
   h = (float)1/tan(fov_vert*0.5);   // 1/tan(x) == cot(x)
   Q = far_plane/(far_plane - near_plane);

   D3DXMATRIX ret;
   ZeroMemory(&ret, sizeof(ret));

   ret(0, 0) = w;
   ret(1, 1) = h;
   ret(2, 2) = Q;
   ret(3, 2) = -Q*near_plane;
   ret(2, 3) = 1;
   ・
   ・
   ・


こんなのわかるはずもないですよね。 このプロジェクション変換行列はジオメトリパイプラインの中で一番難しいところです。 まぁ、やってることは以下の図のようなことなんですよね。。

shaei.png


要は3D物体の頂点を2Dのスクリーンに投影するんですよね。 プログラムは難しいといってもビュー変換行列と同じく以下の関数に渡すだけで簡単に行列を生成できます。

変換行列の種類変換行列
プロジェクション変換行列
(左手座標)
D3DXMATRIX *D3DXMatrixPerspectiveFovLH
(
D3DXMATRIX *pOut,//出力される行列 左手座標
FLOAT fovY, //視野角 PIで入力 大抵は60 * PI
FLOAT Aspect, //アスペクト比 大抵は1.0
FLOAT zn, //最近点
FLOAT zf //最遠点
);
プロジェクション変換行列<br>(右手座標)D3DXMATRIX *D3DXMatrixPerspectiveFovRH
(
D3DXMATRIX *pOut,//出力される行列 右手座標
FLOAT fovY, //視野角 PIで入力 大抵は60 * PI
FLOAT Aspect, //アスペクト比 大抵は1.0
FLOAT zn, //最近点
FLOAT zf //最遠点
);


さて、この変換行列を適用した結果,X座標が -1.0f から 1.0f,Y座標が-1.0f から 1.0f ,Z軸が0.0f から 1.0f の間のオブジェクトが表示されます。 以下の図は適用された後の描画空間です。 この中にある物体が描画されます。

proj_kuu.png

スクリーン(もしくはビューポート)座標変換


先ほどのプロジェクション変換行列によってとうとう3Dの物体を2Dにすることができました。 しかし、値が-1.0から1.0では使い物になりません。 普通のウィンドウの大きさ(640,480)に合わせる必要があります。 そこで出てくるのがスクリーン変換行列です。

スクリーン変換行列はプロジェクション座標をスクリーン座標に変換することができます。 やり方はいたって簡単で、以下のようにしてスクリーン座標にすることができます。

screen_katei.png


  1. の時にY軸を反転させます。
  2. の時にX軸、Y軸を(-1,-1)平行移動します。(オブジェクトは(1,1)移動する。)
  3. の時にX、Yの大きさを(480,640)に調整。


これを式で表すと以下のようになります。

X' = 320 * (X + 1)
Y' = 240 * (1 - Y)


以上のことを踏まえてスクリーン変換を行列で表します。

width / 2            0           0              0
       0   height / 2           0              0
       0            0           1              0
width / 2   height / 2           0              1   


この行列を生成する関数といったものは用意されていません。 この行列がほしければ自分で作る必要があります。

また、このスクリーン座標変換行列は他の座標変換行列とは違い構造体によって操ります。 詳しくは多人数ゲームを参照してください。 今回はあまり重要ではないので省略します。

実はこのスクリーン座標変換についてはほとんど操作することがないんですよね。 勝手にDirectXがスクリーン座標変換をせっていしてくれちゃいます。

プログラムの実装


いやぁ長かったですね。 半分魂が抜けかかった方もいるのではないでしょうか? 一回見ただけじゃこの 3D 変換を理解するのはまず無理でしょう。 いろいろな本やサイトを見て勉強してください。 最後にDirectXの設定法のサンプルを載せたいと思います。 サンプルは<a href="../sam/DrawVertexBuffer.zip">こちらから</a>

尚行列の設定は以下のようにしています。

   
   //=============================================================
   //変換行列の設定
   //=============================================================

    
   //=================================================================
   //ワールド変換行列の作成
   //今回は大きさを 3 倍にして 上に0.12f動かして  Z軸中心回転をします
   //==================================================================
   D3DXMATRIX matWorld,matTrans,matRot,matScale;
   D3DXVECTOR3 vec3Trans(0.0f,0.12f,0.0f);                    //平行移動
   D3DXVECTOR3 vec3Scale(3.0f,3.0f,3.0f);                    //拡大、縮小値
   D3DXVECTOR3 vec3Rot(0.0f,0.0f,60 * 3.141692f / 180.0f); //X軸、Y軸,Z軸回転
   D3DXMatrixScaling(&matScale,vec3Scale.x,vec3Scale.y,vec3Scale.z);//拡大縮小
   D3DXMatrixRotationYawPitchRoll(&matRot,vec3Rot.y,vec3Rot.x,vec3Rot.z);//Yaw=Y軸,Pitch=X軸,Roll=Z軸回転
   D3DXMatrixTranslation(&matTrans,vec3Trans.x,vec3Trans.y,vec3Trans.x); //平行移動
  
  //==========================================
  //行列の演算順序に注意  
  //ためしにいろいろ変えてみてください。
  //楽しいことになります。
  //==========================================
  matWorld = matScale * matRot * matTrans; 
  デバイスのポインタ->SetTransform(D3DTS_WORLD,&matWorld);//最後にワールド行列をSet

    
   //=================================================================
   //  ビュー行列の作成
   //  今回はZ軸正の方向から原点に向かってオブジェクトを見ます。
   //=================================================================
   D3DXMATRIX matView;
   D3DXVECTOR3 vec3From(0.0f,0.0f,1.0f);                   //カメラの位置をSet
   D3DXVECTOR3 vec3At(0.0f,0.0f,0.0f);                     //カメラの対象をSet
   D3DXVECTOR3 vec3Up(0.0f,1.0f,0.0f);//カメラの上をSet普通はこの値                   
 
   D3DXMatrixLookAtLH(&matView,&vec3From,&vec3At,&vec3Up); //ビュー行列を作成
   デバイスのポインタ->SetTransform(D3DTS_VIEW,&matView);   //ビュー行列をSet
   

   
   //=================================================================
   //プロジェクション行列の作成
   //=================================================================
   D3DXMATRIX matProj; 
   float fAngle = 30 * 3.141592f  / 180.0f;                       //視角
   float fAspect = 1.0f;                                          //アスペクト比   普通は1.0f
   float fNear = 0.01f;                                           //最近点
   float fFar = 100.0f;                                           //最遠点
   D3DXMatrixPerspectiveFovLH(&matProj,fAngle,fAspect,fNear,fFar);//プロジェクション行列の作成
   デバイスのポインタ->SetTransform(D3DTS_PROJECTION,&matProj);    //プロジェクション行列のSet


DirectXではいちいち行列の演算を指定して頂点を求めなくても、勝手に演算してくれます。 これが、便利でもあり不便でもあるんですけど。

それと、今回のプログラムはスクリーン行列を作成してません。 この場合はDirectXのデフォルトを意味して、画面の大きさに合わせて描画されるようになります。

速度・メモリ効率向上


さて今まで変換行列をただ単に実装してみました。 ここで問題が出るのがワールド変換行列です。

前に話した通り回転行列、拡大行列、平行移動行列の複数の行列によってワールド変換行列が成り立っています。 もしこれらを実装させたい場合は回転行列、拡大牛列、平行移動行列の全ての行列を保持しとかなくてはいけません。 回転、拡大、平行移動行列をオブジェクトが保持した場合以下のメモリサイズが必要です。

float型 * 列の個数 * 行の個数 * 行列の個数 = 4 * 4 * 4 * 3 = 192バイト


上のように一つのオブジェクトに192バイトものメモリが使われてしまいます。 一つだけならまだしもゲームの世界ではオブジェクトの数は100個を超えます。 そうしますととてつもないメモリの無駄になってしまいます。

さらに高速化で話したとおり、オペレータ・オーバーロードは無駄な演算です。 それだけは避けなければなりません。

そこで行列を作る前の回転、拡大、平行移動の情報を確保します。 そうすることでメモリサイズは小さくなります。

float型 * ベクトルのメンバの数 * ベクトルの個数 = 4 * 3 * 3 = 36バイト


これでメモリは約 1/ 6に軽減されます。 たしかにメモリ効率は上がるのですが、問題は実行速度です。 一回一回行列を作らなければなりません。 そこで以下のようにすれば一つの行列を作成するだけでワールド行列を表せます。

void UpData()
{
   //============================================================
   //拡大、回転をSet
   //============================================================
   D3DXMatrixRotationYawPitchRoll(&matWorld,回転.y,回転.x,回転.z);
   matWorld._11 = vec3Scale.x * matWorld._11;
   matWorld._12 = vec3Scale.x * matWorld._12; 
   matWorld._13 = vec3Scale.x * matWorld._13; 
        
   matWorld._21 = vec3Scale.y * matWorld._21; 
   matWorld._22 = vec3Scale.y * matWorld._22;
   matWorld._23 = vec3Scale.y * matWorld._23;

   matWorld._31 = vec3Scale.z * matWorld._31; 
   matWorld._32 = vec3Scale.z * matWorld._32;
   matWorld._33 = vec3Scale.z * matWorld._33;

   //===========================================================
   //位置座標をSet
   //===========================================================
   matWorld._41 = 位置.x;
   matWorld._42 = 位置.y;  
   matWorld._43 = 位置.z;
}


このように行列をメンバー変数に保持しないで、回転、拡大、位置のベクトルを保持すればメモリの節約になります。 さらにオペレータ・オーバーロードを使用していないので実行速度が上がるはずです。

サンプルについてですが、次のトピックのビルボードと一緒のサンプルです。 fileBillStatic.zipダウンロードはこちらから


添付ファイル: fileworld_keka.png 2042件 [詳細] filezahyou_big.png 2135件 [詳細] filescale_3d.png 2061件 [詳細] fileheikou.gif 2041件 [詳細] fileroty_3d.png 2220件 [詳細] filehei_gyou.png 1948件 [詳細] filecamera_zu.png 2059件 [詳細] filekaku_kei.png 2069件 [詳細] filesisuidai.png 2125件 [詳細] filezahyou_mini.png 2331件 [詳細] filerot_keisan.png 2379件 [詳細] filetrans_keka.png 1969件 [詳細] filerot_shutu.png 2128件 [詳細] filezahyoukei.png 2179件 [詳細] filescreen_katei.png 2206件 [詳細] filetrans_3d.png 2047件 [詳細] filedraw_junjo.png 2407件 [詳細] file2ji_kake.png 2106件 [詳細] filerotx_3d.png 2277件 [詳細] fileview_trans.png 2051件 [詳細] fileproj_kuu.png 2005件 [詳細] fileBillStatic.zip 2792件 [詳細] file3d_2d_pro.png 2143件 [詳細] fileshaei.png 1972件 [詳細] filerot_siki.png 2043件 [詳細] fileafin_nagare.png 2696件 [詳細] filerotz_3d.png 2330件 [詳細] filetrans.png 2142件 [詳細] filetousi.gif 2015件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2008-08-20 (水) 22:13:35 (2866d)