6.3Dタートルグラフィクス
1.コマンドの定義
3次元空間でのタートル グラフィクスを定義します。以下は、リンデンマイヤーの「The Algorithmic Beauty of Plants」に準拠しています。しかし、Cinderellaでシステムを作成する場合、少し困ったことがあります。CindyScriptだけで処理するのであれば問題はないのですが、画面上でコマンド列を入力できるようなシステムにすると、キー入力の問題があり、キーボードの種類によって(JISかそうでないかなど)入力できない文字があります。バックスラッシュなどです。そこで、別の文字でも代替できるようにしておきます。
平面では、亀の動きを指定する記号は原則4つでした
F:線を描きながら前へ進む
f :線を描かないで前へ進む
+:左を向く
−:右を向く
このうち、Fについては、置き換えのためにAやBにすることもありました。
これに対し、空間では「向き」の種類が増えます。次のようにその種類と記号を決めます。
+:z軸回りにθ回転 左を向く
−:z軸回りに−θ回転 右を向く
&またはd:y軸回りにθ回転 下を向く
^またはu:y軸回りに−θ回転 上を向く
\ または r:x軸回りにθ回転 体を右に回転させる
/ または l :x軸回りに−θ回転 体を左に回転させる
|または t:z軸回りに180度回転 180°反転する
例として立方体を描いてみましょう。立方体は一筆書きはできないので、どれかの辺は2度通ることになります。
たとえば、回転角θを90度にして、
F+F+F+FuF+F+F+f+fuFuFtf-lFdFtf+F
とすると描けます。さて、どの順番で描いたかわかりますか?
これをインタラクティブにマウスで動かすことのできるページがあります。やってみてください。
2.亀を1歩進める
Cinderellaの描画面は平面です。したがって、3Dグラフィクスを実現するためには、3次元空間を2次元の平面に投影したものを描画します。その方法は、シンデレラレクチャの「第23回 3Dグラフィクス」に書いてあります。また、3Dグラフィクスのための関数群をまとめたものが、「基本パッケージ」の中にあります。これをダウンロードして使いましょう。
さて、2次元のタートルグラフィクスでは、亀が進んだときは座標系そのものを平行移動したり回転したりする translate()関数、rotate()関数を使って亀の座標系をその都度変えてきました。しかし、これらの関数は3次元では使えません。そこで、亀の位置と向いている方向を表す変数を用意して、亀が動くたびにその値を変えていくことにします。
亀の位置 Turtlepos=[0,0,0]; ・・・ 初め、亀は原点 (0,0,0) にいます。
亀の向き Turtledirec=[1,0,0]; ・・・ 亀は常にx軸の方向を向いていますので、亀の座標系のx軸方向ベクトルと同じです。
では、亀を1歩だけ動かす関数を作りましょう。
亀を動かす人は、亀がおおもとの座標系でどこにいるかを知っていなければなりません。そこで
亀が次に移動する位置 Turtleposnext
という変数を用意します。現在の位置と向きから次に移動する位置Turtleposnextを計算し、2次元のときと同じ手順で1歩進んだら、亀が今いる位置 Turtleposの値をTurtleposnextに変えておきます。
ーーーーーーーーーーーーーーーーーーーーー
forward3d():=(
Turtleposnext=Turtlepos+step*Turtledirec;
draw(map3d(Turtlepos),map3d(Turtleposnext));
Turtlepos=Turtleposnext;
);
ーーーーーーーーーーーーーーーーーーーーー
この中のmap3d()という関数は、3次元座標 (a,b,c)を2次元座標(p,q)に変換する関数です。
3.亀を回転する
亀の位置を原点とする座標系で考えたとき、亀の向いている方をx軸、亀から見て左をy軸、上をz軸とします。そして、亀が向きを変えるというのは、それぞれの軸のまわりに回転することであると考えます。
2次元のタートルグラフィクスを思い出してください。亀は1歩進んだり向きを変えたりしますが、動いた直後、亀は自分が原点にいてx軸方向を向いているものと思っています。つまり、亀にとっての座標系は亀とともに移動・回転するのです。では、亀をコントロールする人にとってはどうでしょう。移動したあとの亀の位置は前節のとおりに把握できます。しかし、回転したときの亀の向きが少しややこしいのです。
ここでポイントは2つです。
(1) 亀はつねに自分がx軸方向を向いていると思っている
(2) 亀にとっての座標系は亀とともに回転する
これを、「回転命令によって亀の座標系が回転し、その後、亀はx軸方向を向く」と考えます。そこで、亀座標系のx軸、y軸、z軸を、回転命令によってそれぞれ回転することにします。それから、亀の向き Turtledirec をx軸方向にするのです。
そのために、「ある方向ベクトルの周りに回転するための行列」を用います。
ちょっと復習(シンデレラレクチャの「第23回 3Dグラフィクス」の復習)をします。
x軸、y軸、z軸まわりの回転は、それぞれ次の行列で表すことができます。
これに対し、あるベクトルを軸とした回転は、次の行列で表されます。
亀座標系の3つの軸をこの行列を使って回転し、その後、亀の向きをx軸にすればよいのです。次のスクリプトではこの行列を関数化して rtv としています。
// コマンド +,- による回転 : tz周り
rlz(θ):=(tx=rtv(tz,θ)*tx;ty=rtv(tz,θ)*ty;Turtledirec=tx);
// コマンド &,d, ^,u による回転 : ty周り
rly(θ):=(tx=rtv(ty,θ)*tx;tz=rtv(ty,θ)*tz;Turtledirec=tx);
// コマンド r,\, l,/ による回転 : tx周り
rlx(θ):=(ty=rtv(tx,θ)*ty;tz=rtv(tx,θ)*tz;Turtledirec=tx);
4.コマンドに従って亀を動かす
亀を動かすコマンドによって亀を1歩進めたり、向きを変えさせたりする関数を作ります。2次元では turtle() という関数でした。3次元ですので turtle3d()としましょう。
ーーーーーーーーーーーーーーーーーーーーー
turtle3d(command):=(
if(command=="A",forward3d());
if(command=="B",forward3d());
if(command=="F",forward3d());
if(command=="f",Turtlepos=Turtlepos+step*Turtledirec;);
if(command=="+",rlz(angle));
if(command=="-",rlz(-angle));
if(command=="&",rly(angle));
if(command=="d",rly(angle));
if(command=="^",rly(-angle));
if(command=="u",rly(-angle));
if(command=="\",rlx(angle));
if(command=="r",rlx(angle));
if(command=="/",rlx(-angle));
if(command=="l",rlx(-angle));
if(command=="|",rlz(180°));
if(command=="t",rlz(180°));
if(command=="[",push());
if(command=="]",pop());
);
ーーーーーーーーーーーーーーーーーーーーー
f は線を描かずにただ1歩進むだけです。
[ と ] は現在の状態を保存するのでした。ここでも2次元で使っていた gstore() , grestore() は使えませんので、Turtlestackというリストを用意して、ここに現在の状態を保存しています。これをスタックといいます。スタックに保存するのを push 、スタックから取り出すのを pop と呼ぶのが普通です。亀の位置、向きと亀座標の軸をまとめてリストとして処理します。
ーーーーーーーーーーーーーーーーーーーーー
push():=(Turtlestack=append(Turtlestack,[Turtlepos,Turtledirec,tx,ty,tz]));
pop():=(
nn=length(Turtlestack);
if(nn>0,
pp=Turtlestack_nn;
Turtlepos=pp_1;
Turtledirec=pp_2;
tx=pp_3;
ty=pp_4;
tz=pp_5;
Turtlestack=apply(1..(nn-1),Turtlestack_#);
);
);
ーーーーーーーーーーーーーーーーーーーーー
4.コマンド列にしたがって亀を動かす
2次元の tree() の3次元版です。ここは2次元の場合とほとんど同じです。
ーーーーーーーーーーーーーーーーーーーーー
tree3d():=(
s=initiator;
repeat(n,s=replace(s,generator););
repeat(length(s),turtle3d(s_#));
);
ーーーーーーーーーーーーーーーーーーーーー
3行目は、置き換えシステムによってコマンド列を作成するためのものです。
以上を Initialization スロットに書きますが、3D基本パッケージですでにInitializationスロットは使っていますね。そこに書き足してもよいのですが、同じスロットに別のスクリプトを書くことができるのでそのようにして書き分けましょう。initialization というフォルダをクリックすると「クリックしてスクリプトを書き始めてください」と出ます。これで新しいスクリプトを書けばよいのです。