1. 3Dグラフィクスの基本
この節では3Dグラフィクスの基本的な考え方について,Cinderellaを動かしながら説明します。
座標系と透視投影
空間内の点は、x,y,zの3つの座標で表します。このとき、各軸の方向の取り方が2通りあります。親指をx軸、人さし指をy軸、中指をz軸としてそれぞれが直交するようしたとき、親指と人さし指ででできる面(xy平面)に対して中指の向いている方向が、右手と左手では異なります。
左手系では、x軸が奥から手前に向かっているとするとz軸は下向きになり、z軸を上向きにするとx軸は手前から奥の方に向かうことになります。3Dコンピュータグラフィクスでは、視点を原点において透視投影したときに左手系が便利ということで左手系がよく使われるようですが、ここでは学校の教科書に合わせて右手系で考えることにします。Cinderellaの作者の一人であるゲバート氏がMatheVitalに載せている3Dの作品も右手系になっています。
さて、軸の取り方が決まったところで、次の問題は、Cinderella.2の描画面と軸の関係です。Cinderella.2は平面幾何のソフトですので、描画面はxy平面です。作図機能を使って点をとり、CindyScriptでその座標を取得すると、それはxy座標です。
一方、右手系の空間を考えると、通常は上図のようにとりますので、見ている人にとっては描画面がyz平面に見えます。平面なら描画面がxy平面なのに、空間座標にしたら描画面はyz平面であるという、この違いをどうすればよいかがクリアしなければならない一つの問題です。
次に、空間内の点を2次元の紙の上(コンピュータのディスプレイ上)にどのように置くか、という問題があります。わかりやすい例として、学校の廊下を考えましょう。
レオナルド・ダ・ヴィンチの最後の晩餐の絵でも結構です。廊下や窓・天井などの線が、画面の中央に集まっていきます。いわゆる遠近法ですが、遠いものは小さく、原点付近に描かれることになります。
上図の右手系で考えると、x座標の負の方が遠くにあり、正の方が手前にありますので、y,z座標が同じでもx座標が負なら原点近くに描かれることになります。
高校の数学の教科書で扱う空間座標は、遠近法を取り入れないで書いてある場合がほとんどです。これを平行投影といいますが、写真と同じように遠近法で描く(これを透視投影といいます)方が自然に見えるでしょう。たとえば、下の図の透視投影では、視点が立方体OABC-EFGHの上面より少し下にあると思ってください。一番近い点はGなので、それより少し遠いBはその分原点の方に寄るので、辺GBはz軸と平行ではなくなります。
視点の位置
透視投影では、視点をどこに置くか(どこから見ているか)が問題になります。上の図のように、視点を斜め上のどこかにあると考えると計算がややこしくなります。そこで、視点はx軸上の手前のどこかにあることにしましょう。ただし、Cinderellaでは描画面がxy平面でしたので、軸を変えて、視点はz軸の正の部分のどこかということにします。すると上の立方体は次のように見えることになるでしょう。
どうなっているかわかりますか? 上の透視投影の図で、z軸方向すなわち上方から見ていると考えてください。
まず、z軸は点にしか見えません。原点と重なっています。
点Bと点Gの座標はB(2,2,0)、G(2,2,2) でx座標とy座標は同じですが、上から見ているのでBの方が遠くにあります。したがってBが原点の方に少し寄ります。同じ理由で、点Aは点Fよりも、点Cは点Hよりも原点に近くなります。
でも、これでは立方体に見えませんね。立方体であることがわかる上図のように、斜めから見た図を描くにはどうすればよいでしょう。
さいころを手に持っていると想像してください。このさいころの3つの面を同時に見ようと思ったらどうしますか? 顔を動かしますか? 立方体の建物だったら適当な場所まで歩いたりはしごに登ったりするかもしれませんが、さいころだったら手の上のさいころを動かすでしょう。つまり、視点は動かさずに、対象物を動かせばいいのです。
では、どのように動かしますか? 頭の上の方に持ってきたりする? もちろんそれもあるでしょうが、手の上で転がす、つまり回転すればいいですね。
以上で、やるべきことは決まりました。
(i) 透視投影するために、z座標を見て、遠ければ(負の方にあれば) その割合に応じて原点に近いところに表示するようにx,y座標を変換する。
(ii) Cinderellaの描画面xy平面が、右手系空間座標のxy平面である床面になるように対象物の軸を回転する。
(iii) 斜め上から見た図になるように、対象物そのものをさらに少し回転する。
では、これらの処理をするCindyScriptのコードを考えていきましょう。
透視投影における座標変換
点 (a,b,c) を描画面に投影します。投影面はxy平面とします。z座標をみて、遠くにあればx,y座標を原点近くに移動します。視点がz軸上の(0,0,p)という点にあるとしたら、p−c で対象物と視点の距離が求められますので、これでx,y座標を割れば、遠い点(cが負の数で絶対値が大きい)ならばx,yは小さな値(0に近い値)になります。視点のz座標pの値をどのくらいにすればよいかは、これから実験してみましょう。
次のスクリプトをInitializationスロットに書きます。Initializationスロットに置く理由は、最初に1度だけ実行すればいいからです。
--- Initialization スロット --------------------------------------
viewpoint=8.001;
zoom=15;
map3d(p):=(
regional(pz,mp);
pz=p_3;
mp=p/(viewpoint-pz);
[mp.x,mp.y]*zoom;
);
-----------------------------------------------------------------------------
はじめの regional(pz,mp) はこの関数内だけで使う局所変数の宣言です。
viewpoint が視点のz座標です。変数名を単なるpではなく、言葉にしています。あとからプログラムを読むとき、そのほうがわかりやすいからです。整数でなく8.001と一見変な数にしているのは、viewpoint-pz が0になることへの備えです。もちろん、これでもviewpoint-pzが0になることはあり得ますが、その確率はかなり低くなります。
zoom は計算後に拡大する率です。透視変換により、x,y座標を小さくしますので、拡大しないと、図が小さくなってしまいます。
map3d(p) が点pを透視変換をする関数です。引数のpは、空間座標すなわち要素が3つのベクトルとします。
pzがpのz座標。右辺の _3 でz座標を取得します。アンダーバーに続く数が、ベクトルの何番目の要素かを示しています。すなわち、 p_3 はベクトルpの3番目の要素、すなわちz座標ということになります。
mpが変換した座標です。
最後の式は代入の式になっていないので、意味がわかりにくいかも知れません。でも、プログラム上、とても大切なことなので正しく理解してください。
「関数」とは、ある機能を持ったものです。今考えている map3d(p) という関数は、透視変換をする関数です。引数として3次元座標 p=(a,b,c) を渡すと、 mp=p/(viewpoint-pz) という式によって、座標を変換します。では、その結果はどうなるのでしょうか。その結果を取り出さなければなりません。これを「戻り値」といいます。関数を定義するときには、戻り値を何にするかを考えておく必要があります。CindyScriptでは、関数の処理の中で最後に行われた結果が戻り値として返されることになっています。 [mp.x,mp.y]*zoom; によって、透視変換されたmpのx座標とy座標にzoomをかけて大きさを戻した[x,y]座標を作っています。これが最後に行われた処理なので、戻り値はこの[x,y] 座標となります。
なお、CindyScriptでは、点の座標もベクトルも「リスト」という考え方で処理しますので、(mp.x,mp.y) ではなく [mp.x,mp.y]と四角い括弧を使っています。
もうひとつ,最後の行が mp*zoom; ではない理由を説明しておきます。
Cinderellaで扱う点の表現は,実際には「同次座標」です。この「同次座標」は3つの要素からなるベクトルですが、これは空間座標とは異なるものです。透視変換によって必要なのは、(x,y) 座標です。ですから戻り値は [mp.x,mp.y]*zoom とし、次のようにして受け取ります。
p=[1,2,3];
A.xy=map3d(p);
map3d(p) によって、透視変換された座標(x,y)が戻されますので、それを点Aのxy座標に代入しています。
では、上の関数の働きを確かめてみましょう。
Drawスロットに、次のスクリプトを書きます。
A.xy=map3d([2,2,2]);
B.xy=map3d([2,2,0]);
実行する前に、「グリッドにスナップする」 をクリックして、座標軸と方眼を表示しておき、「点を加える」ツールボタン を選んで適当なところに点AとBを取りましょう。それから、CindyScriptの実行アイコン(歯車アイコン)をクリックしますと、点Aは(2,2,2)を、点Bは(2,2,0)を透視変換した点になります。
軸のまわりの回転
Cinderellaの描画面はxy平面です。右手系空間座標のxy平面は床面のイメージですので、そうなるように対象物を回転します。描画面そのものを回転するのではなく、対象物の座標系を回転させるのです。
次の図で、黒の軸はCinderellaの画面です。ここで、z軸が画面の奥から手前に来ている3次元の座標系を考えます。z軸は原点と重なって見えていると考えます。赤の軸がこの座標系です。この赤の座標系を回転して右図のようにします。ただし、Cinderellaの座標系は変化しません。
まず、xy平面における回転を表す行列の作り方について復習しましょう。点Pをθだけ回転してQに移動します。座標をsinとcosを使って表します。cos(θ+α)とsin(θ+α)を加法定理で展開すれば、(X,Y)と(x,y)の関係が求められます。
この結果を行列で表すと次のようになります。
これを3Dに拡張します。xy平面で原点まわりに回転するということは、3Dでしたら、z軸まわりに回転することになります。このとき、z座標は変化しませんので、Z=z です。これを行列で表すと、次のようになります。x軸まわりの回転とy軸まわりの回転も同じように考えます。
CindyScriptで、それぞれの行列を用意します。z軸まわりは rtz(θ) (rotation of z) というように名前を付けましょう。これはInitializationスロットに書きます。
--- Initialization スロット ---------------------------------------------------------------
rtx(θ):=[[1,0,0],[0,cos(θ),-sin(θ)],[0,sin(θ),cos(θ)]];
rty(θ):=[[cos(θ),0,sin(θ)],[0,1,0],[-sin(θ),0,cos(θ)]];
rtz(θ):=[[cos(θ),-sin(θ),0],[sin(θ),cos(θ),0],[0,0,1]];
viewpoint=8.001;
zoom=15;
map3d(p):=(
pz=p_3;
mp=p/(viewpoint-pz);
[mp.x,mp.y]*zoom;
);
------------------------------------------------------------------------------------------------------
4行目からあとは、(3) で作った透視変換をするスクリプトです。
これを使って、対象物のx,y,z軸を回転してみましょう。まず、Cinderellaの描画面に3本の矢印を描きます。前に描いた点などがあれば全部消して描き直しましょう。また,Drawスロットに書いたスクリプトは消しておきます。
まず、「グリッドにスナップする」をクリックして、座標軸と方眼を表示しておきます。「線分を追加する」ツールボタンを選んで適当な場所に3つの線分を描きます。軸に合わせなくても構いません。つぎに、「要素を動かす」ツールボタンを選んで、線分をクリックして選択します。これを矢印に変えます。編集メニューからインスペクタを開き、「特別な表示の設定」を選びます。この中に「矢印の設定」があります。「矢印の種類」を「終点に矢印」にします。これで3本の矢印ができます。
これをx,y,z軸に乗せるために、次のように設定します。
線分ABはx軸として、点A,Bの座標を A(-2,0,0) , B(2,0,0) とする。
線分CDはy軸として、点C,Dの座標を C(0,-2,0) , D(0,2,0) とする。
線分EFはz軸として、点A,Bの座標を E(0,0,-2) , F(0,0,2) とする。
CindyScriptのDrawスロットに次のように書いて、実行ボタン(歯車アイコン)を押してみましょう。
--- Draw スロット -------------------------------------------------------------------------
A.xy=map3d([-2,0,0]);
B.xy=map3d([2,0,0]);
C.xy=map3d([0,-2,0]);
D.xy=map3d([0,2,0]);
E.xy=map3d([0,0,-2]);
F.xy=map3d([0,0,2]);
------------------------------------------------------------------------------------------------------
ABがx軸、CDがy軸上に乗ります。EFはZ軸上ですが原点と重なって見えます。
では、これを回転します。先ほど定義した回転の関数を使って、回転の行列を作ります。これをmat (matrix の意)としましょう。
点の座標を3次元ベクトルとして、この行列に掛けます。Drawスロットのスクリプトを次のように修正します。
--- Draw スロット -------------------------------------------------------------------------
mat=rtz(pi/3);
A.xy=map3d(mat*[-2,0,0]);
B.xy=map3d(mat*[2,0,0]);
C.xy=map3d(mat*[0,-2,0]);
D.xy=map3d(mat*[0,2,0]);
E.xy=map3d(mat*[0,0,-2]);
F.xy=map3d(mat*[0,0,2]);
------------------------------------------------------------------------------------------------------
1行目の pi は、CindyScriptで予約語として使うもので、円周率πを表します。
rtzですので、z軸まわりにpi/3 (60°) 回転します。
rtzではなく、rtxにすればx軸まわりの回転になります。
y軸とz軸が少しずれて重なって見えます。
この2つの回転を連続して行なうには、回転の行列を掛けます。先に行なう方を右側に書きます。
どうなったのかわかりますか? まずz軸まわりに回転したものを、x軸まわりに回転しました。透視変換をしているので、点Aは点Bより奥にあるため原点寄りになっています。
マウスでぐりぐりと3次元的に動かす
3Dで描いた図は、手の上でさいころを転がすようにリアルタイムで回転できると大変わかりやすくなります。Mathematica や GeoGebra にはマウスでぐりぐりと回転できる機能が備わっています。これをCindyScriptを使って実現します。
マウスをドラッグしたときにどのように動かすかを決めておきましょう。左右にドラッグすれば横に回転(z軸まわりの回転)、上下にドラッグすれば縦に回転(y軸まわりの回転)、とするのが直感的でよいと思います。
次に、マウスの動きをどう検出して何をするのかを考えましょう。
・マウスボタンが押されたら、ドラッグかクリックかのどちらか。どちらにしても、その時のマウスカーソルの位置を検出して記憶する。
・マウスがドラッグされている間、マウスカーソルの位置を検出して回転のための行列を作成する。
ここで,図形の回転の目的ではなく、点の移動(スライダを動かすときなど)のためのドラッグかも知れないので、その判断が必要ですが,実際には空間内にある点を平面上で移動するのはあまり直感的ではないので(できないわけではありません)ここではそこはパスすることにします。
CindyScriptの便利なところは、「どんなときに実行したいかによって、スロットを使い分ける」ということです。マウスボタンが押されたときに処理したい内容は Mouse Down スロットに、マウスがドラッグされている間に処理したい内容は Mouse Drag スロットに書けばよいのです。
マウスの位置は、mouse()で取得できますので,これを利用します。
-- Mouse Down -----------------------------------------------------
startx=mouse().x;
starty=mouse().y;
------------------------------------------------------------------------
startx と starty は、マウスボタンが押されたときのマウスカーソルの位置になります。
-- Mouse Drag -----------------------------------------------------
movingx=mouse().x-startx;
movingy=mouse().y-starty;
------------------------------------------------------------------------
mouse().x と mouse().y が、現在のマウスカーソルの位置ですので、これと startx , starty の差が、マウスがドラッグされたときに動いた距離です。この値によって、回転量を決めます。
Draw スロットの mat を次のように変えます。
mat=rtx(movingy/10+pi/3)*rty(movingx/10+pi/3);
それぞれ10で割っているのは,そのままだと動きすぎるためです。10以外の数で試してみるとわかります。
なお,この movingx と movingy は,Initialization スロットで 0 に設定(初期化)しておきます。
ではこれで実行してみましょう。次の二つの図は、マウスを横方向にドラッグしたときと、縦方向にドラッグしたときの様子です。各点の足跡を表示しています。
この節の最後に、ちょっとした調整をします。マウスをドラッグするとき、薄い長方形が表示されませんか。「要素を動かす」モードになっていると現れます。これは、要素を複数選択するときの長方形です。
この長方形が現れないようにしておきましょう。編集メニューからインスペクタを開き、「全体の設定」のなかの「ドラッグで複数選択」のチェックを外します。これで長方形は現れません。そのかわり、複数の要素をまとめて選択することはできなくなりますが。
以上で,基本的な設定はできました。
正四面体を描く
例として正四面体を描いてみましょう。
まず,はじめの画面でx軸がこちらを向くようにしましょう。いろいろ試行錯誤して,適当な回転角を決めます。
点は4つなので,前の画面が残っていたら,D,Eは削除しましょう。
スクリプトは次のようにします。
mat=rtx(movingy+pi/12)*rty(movingx-2*pi/3)*rtx(-pi/2);
A.xy=map3d(mat*[2*sqrt(3)/3,0,0]);
B.xy=map3d(mat*[-sqrt(3)/3,1,0]);
C.xy=map3d(mat*[-sqrt(3)/3,-1,0]);
D.xy=map3d(mat*[0,0,2*sqrt(6)/3]);
これだけだと頂点が配置されるだけなので,作図ツールで頂点を結び,さらに,多角形ツールで面を塗ります。ここで,インスペクタを使って,透明度を適当に設定するのがポイントです。
マウスドラッグで回転してみましょう。