昔、流行った立方体のパズルを作ってみます。
ある軸を中心に回転すれば良いので、一次変換の回転を使ったら楽勝!と、思いきや、結構難しいことがわかりました。一次変換だと、世界全体に効果を与えてしまうのですが、立方体パズルで回転させたいのは一部分であって、全体ではありません。一次変換を部分的に適用する必要があります。
それに、ある部分を回転させようと思ったら、そこに位置している小さい立方体(以降小キューブ)の部品を特定する必要が出てきます。しかも、ひとたび回転させてしまうと、各々の小キューブの位置が変わってしまうので、小キューブが今どの位置にいるかを把握しながら処理する必要がありそうです。
何はともあれ、ランダムに一部分を回転する手順を作成します。
立方体パズルは、3×3×3=27個(外から見えない中央の小キューブを除くと26個)の小キューブでできています。一度に回転するのは、その内の3×3=9個とします。以下のように、ある回転軸を決めた時、回転できる3×3の部分は1〜3の3通り存在します。
さらに回転軸自体がx-y-z方向の3軸有りますので、回転できる部分は9通りになります。
実際には、2と示した真ん中の部分を回転するのは不自然なので、手順には含まないようにしようと思います。
回転する時、回転軸に対して、時計回りに回転する場合と、反時計回りに回転する場合があります。これに、後で小キューブの回転手順ファイルを作成する都合上、「回転しない」というケースを加えて、3通りのアクションを定義します。
データでは、回転軸をxyz、3×3の部分をlayer、回転のアクションをrdirという変数にしています。それぞれの意味を以下のように定義しました。
xyz:x軸方向=0/y軸方向=1/z軸方向=2
layer:回転軸方向の座標値で、絶対値の小さい方から順に0〜2
rdir:時計回り=0/回転しない=1/反時計回り=2
各パラメータをランダムに決定します。例えば、xyzは0〜2の3通りなので、rnd(3)という関数を使います。rnd(x)は、0〜xまでのランダムな実数(浮動小数)値を発生する関数です。
xyz=rnd (3);
それを整数に変換するint()関数を使って、整数にします。
xyz=int (rnd (3));
rnd(3)は、0〜3までの数値を発生するので、非常にレアなケースですが、xyz=3となってしまう場合があります。xyzは0〜2ですので、3は含みたくありません。そこで、3で剰余を取ることでxyz=3となった場合に、xyz=0と読み替えてしまいます。
xyz=int (rnd (3))%3;
手順として不適切な、真ん中の部分指定(layer=1)と、回転しない指定(rdir=1)を除くため、layer=1やrdir=1となった時は再度乱数を発生させています。
repeat
{
layer=int (rnd (3))%3;
} until (!(layer=1)); // 「真ん中」は手順に含まない
この後、これらのデータを使用しますので、配列変数を宣言しながら、全ての手順を保持します。
// シーケンスをNsステップ分作って、配列に保持する
var idx;
idx=0;
while (idx<Ns)
{
static : var xyz#(idx);
static : var layer#(idx);
static : var rdir#(idx);
xyz#(idx)=int (rnd (3))%3;
repeat
{
layer#(idx)=int (rnd (3))%3;
} until (!(layer#(idx)=1)); // 「真ん中」は手順に含まない
repeat
{
rdir#(idx)=int (rnd (3))%3;
} until (!(rdir#(idx)=1)); // 「回転しない」は手順に含まない
// 最初(最後)の一回は回転させない
if (idx=0)
{
rdir#(idx)=1;
}
idx=idx + 1;
}
Nsは作成する手順の数です。
一回動画を作成してみたところ、よく見ると「手戻し」がありました。一旦回転させているんだけど、「おっと、間違えた」という感じで、逆回転して元に戻している手順。手戻しがあると2手分無駄になってしまいますので、手戻し手順を禁止します。以下の条件が成立した場合に、乱数を作成しなおすようにしました。
idx>1の時で、
xyz#(idx - 1)=xyz#(idx)の時で、
layer#(idx - 1)=layer#(idx)の時で、
!(rdir#(idx - 1)=rdir#(idx))の時
実は、この条件だとまだ足りない。下の方に完成動画を貼り付けておきましたが、よーく見ると、手戻しがあります。
1)軸Aのレイヤ0を時計回し回転
2)軸Aのレイヤ2を時計回し回転
3)軸Aのレイヤ0を反時計回し回転
上のような感じで、同じ軸(A)の回転が続いた場合、1)と3)が手戻し手順になっていますが、間に2)の手順が入るため、前述の条件(「連続する2手順が各々逆回転の時は手戻し」)に引っかからず、すり抜けてしまいます。この条件の除外は、実装していません。興味があれば、挑戦してみてください。もし、2)がA以外の軸であれば、1)と3)は「手戻し」にはなりませんので、全て同じ軸であることが条件です。追い始めると、他にも条件が抜けているかも?です。キリが無いので、止めました。
立方体パズルの中には、3×3×3=27個の小キューブが存在することはお話ししました。前節で作った手順によって、ある部分(3×3個)だけが回転しますが、残りの3×3×2個については、動いてはいけません。即ち、ここから先、前節で作成した手順をある特定の小キューブについて読み替えていく必要があります。ここから作成する小キューブ毎の回転手順は、全てのアニメーションフレームを計算する度に同じものを使用しますので、噴水のサンプルで紹介したように、最初の実行時にファイルへ出力しておき、アニメーションの各フレームを計算する時にはファイルからデータを読み込むようにします。ファイルへ出力する手法に関しては、噴水のサンプルをご覧ください。
今回のファイルフォーマットは、以下のようにします。
<MODELA_FID>
<小キューブ番号0> <xyz#(0)> <layer#(0)> <rdir#(0)>
<小キューブ番号0> <xyz#(1)> <layer#(1)> <rdir#(1)>
:
<小キューブ番号0> <xyz#(Ns-1)> <layer#(Ns-1)> <rdir#(Ns-1)>
-1
<小キューブ番号1> <xyz#(0)> <layer#(0)> <rdir#(0)>
<小キューブ番号1> <xyz#(1)> <layer#(1)> <rdir#(1)>
:
<小キューブ番号1> <xyz#(Ns-1)> <layer#(Ns-1)> <rdir#(Ns-1)>
-1
:
<小キューブ番号Nc-1> <xyz#(Ns-1)> <layer#(Ns-1)> <rdir#(Ns-1)>
-1
<EOF>
Ncは小キューブの数で、Nc=3×3×3です。回転手順(xyz/layer/rdir)を小キューブ毎に作っていきます。実は、xyz#(n)とlayer#(n)は、全ての小キューブで同じ値ですが、当該小キューブがどの位置にいるか?で回転の対象かどうかが変わりますので、rdir#(n)は小キューブ毎に異なる値になります。
小キューブの位置を把握して続ける必要があるので、以下図のように座標値を決定します。
これから、(1,1,1)の小キューブを中心に回転させるので、座標値を0,1,2ではなく、−1,0,1にした方が良かったのですが、layerの数値と合わせて、0,1,2としました。layerも−1,0,1にして、揃えれば良かった??
手順に従って、回転させていきます。まず、0番目の手順(配列でxyz#(0),layer#(0),rdir#(0))で、今注目している1つの小キューブ(cx,cy,cz)が回転の対象になっているかどうか?どうすればわかるでしょうか?以下の条件が成立した時、回転の対象になっています。
xyz#(0)=0(x軸周り)で、layer#(0)=cxの時、または、
xyz#(0)=1(y軸周り)で、layer#(0)=cyの時、または、
xyz#(0)=2(z軸周り)で、layer#(0)=czの時
この条件が成立したら、rot命令を使って回転(90°)させます。
// 回転させる
unit;
static : if (xyz#(0)=0 & layer#(0)=cx)
{
rotx (rdir#(0) - 1)*90;
}
static : if (xyz#(0)=1 & layer#(0)=cy)
{
roty (rdir#(0) - 1)*90;
}
static : if (xyz#(0)=2 & layer#(0)=cz)
{
rotz (rdir#(0) - 1)*90;
}
通常、一次変換(回転)はブロック{}内でしか効果を持続できないため、このように条件分岐(if文)を使って一次変換の適用/不適用を切り替える場合、接頭子staticを付けて、効力をブロックの外まで拡張する必要があります。
ある座標(x,y,z)が現在の一次変換によって、どの座標に移動するか知るためには、linear命令を使います。
linear x,y,z;
と、実行すると、その時x,y,zという変数に入っていた数値を座標(x,y,z)と解釈して一次変換を実施し、移動後の座標を(x,y,z)に返します。小キューブ(1,1,1)を中心に回転させたいので、linear命令の前後で座標を変換する必要があります(回転の中心(1,1,1)を原点(0,0,0)に移動するため)。
cx=cx - 1;
cy=cy - 1;
cz=cz - 1;
linear cx,cy,cz;
cx=int (cx + 1.5);
cy=int (cy + 1.5);
cz=int (cz + 1.5);
linear命令の直前に(1,1,1)を引き算して、(1,1,1)が回転の中心(原点)になるようにしています。linear命令の後では、引き算した(1,1,1)を足し戻すと同時に、小数点以下を四捨五入(0.5を加えて整数化)しています。実行後、旧座標(cx,cy,cz)にあった小キューブは、一次変換(回転)によって新たな座標に移動しており、(cx,cy,cz)には移動後の新たな座標値が入っています。if文で回転(rotx/roty/rotz命令)が実行されなかった場合には、移動(回転)していませんので、linear命令実行前後で(cx,cy,cz)は変化しません。
以上の処理を1番目の手順、2番目、3番目、……と、最後のNs−1番目まで繰り返しながら、手順をファイルに書き出していきます。
// 回転シーケンスをスキャンする
idx=0;
while (idx<Ns)
{
// 回転させる
unit;
static : if (xyz#(idx)=0 & layer#(idx)=cx)
{
rotx (rdir#(idx) - 1)*90;
}
static : if (xyz#(idx)=1 & layer#(idx)=cy)
{
roty (rdir#(idx) - 1)*90;
}
static : if (xyz#(idx)=2 & layer#(idx)=cz)
{
rotz (rdir#(idx) - 1)*90;
}
cx=cx - 1;
cy=cy - 1;
cz=cz - 1;
linear cx,cy,cz;
cx=int (cx + 1.5);
cy=int (cy + 1.5);
cz=int (cz + 1.5);
// シーケンスを書き出す
if ((xyz#(idx)=0 & layer#(idx)=cx) |
(xyz#(idx)=1 & layer#(idx)=cy) |
(xyz#(idx)=2 & layer#(idx)=cz))
{
// 小キューブは回転の対象
fprint fout,"# # # #"$,, idx_c,xyz#(idx),layer#(idx),rdir#(idx);
}
else
{
// 小キューブは回転の非対象
fprint fout,"# # # #"$,, idx_c,xyz#(idx),layer#(idx),1;
}
idx=idx + 1;
}
fprint fout,"#"$,, -1;
小キューブが回転の非対象だった時は、rdirを書き出す部分で「1=回転なし」を書き出します。
小キューブはNc=3×3×3=27個あるので、全27個について前節の処理を実行します。小キューブの番号をidx_c=0〜Nc−1とすると、番号から座標値(cx,cy,cz)への変換は、以下の通りとなります。
// 小キューブの座標(初期値)
cx=idx_c%3;
cy=int (idx_c/3)%3;
cz=int (idx_c/9)%3;
前節の処理で、各小キューブ毎に回転手順を作成し、ファイルに出力してあります。ここからは、ファイルから手順を読み込んで実際に小キューブを定義していきます。一次変換命令の実行順番と、実際に変換のかかる順番は逆転しています(詳しくは、マニュアルに記載がありますので参照してください)ので、これに注意して、手順の逆順に命令を生成し、実行します。
// 小キューブ定義
idx=Ns - fn_seq;
static : while (idx>0)
{
idx=idx - 1;
cx=idx_c%3;
cy=int (idx_c/3)%3;
cz=int (idx_c/9)%3;
// 回転させる
static : if (xyz#(idx)=0)
{
rotx (rdir#(idx) - 1)*rot_deg;
}
static : if (xyz#(idx)=1)
{
roty (rdir#(idx) - 1)*rot_deg;
}
static : if (xyz#(idx)=2)
{
rotz (rdir#(idx) - 1)*rot_deg;
}
rot_deg=90;
}
scube (cx - 1)*2*R,(cy - 1)*2*R,(cz - 1)*2*R, 0.95*R, cx,cy,cz;
手順のステップ番号を示すidxを逆順(idx=idx−1)で参照しています。
完成したデータと動画をご覧ください。
Xi-TRACEデータ:cubic_puzzle.xitr
データとしては、六面の色が揃った状態で定義してから、手順に従ってランダムに回転させているので、最後には色がバラバラな状態になります。それを逆再生すると?バラバラな状態から、少しづつ色が揃っていき、最後に六面の色が揃うということです。
ちなみに、初めは、30FPS(Frames Per Second)で動画を作成したのですが、ちょっと変化が早すぎて見ている側がついていけなかったので、そのまま15FPSに落としてあります。なので、ちょっと回転が粗くてカクカクしてます。