2015年03月29日

プチコン3号 29日目 擬似3D(6) 迷路の中を動く敵を隠面消去つきで描画する

pc29-1.jpg

前回は隠面消去のアルゴリズムを使って迷路の壁を表示しました。
しかし、表示できるものが壁だけでは寂しいですね。
今回は前回のプログラムを拡張して、3D迷路の中に敵を表示しました。
これで、11日目の迷路探検ゲームの3D版ができます。

今回のプログラムは長いので、全体は掲載しません。
動かしてみたい方はMiiverseからダウンロードしてください。
公開キーは【BKEYX39J】です。
操作はスライドパッドで移動、L/Rで方向転換です。
敵とぶつかると目が回ってしまいます。
ゴールの黄色い壁を目指して進んでください。


この記事では、3D迷路の中に敵を表示する場合の描画処理方法について解説します。
その前にまず、迷路の中をカメラが移動する場合の処理について、簡単に解説しておきます。
迷路の中を移動する場合の処理については、7日目に記事を書きました。

プチコン3号 7日目 ドラクエ風に迷路の中を歩き回る: 楽しくやろう。

今回のプログラムでも、基本的な処理は全く同じです。
ただ、迷路の部屋やマップに割り当てるピクセル数が違います。

7日目の記事では、迷路が32×32ピクセルの部屋をつなぎ合わせたもので、その中を16×16ピクセルのスプライトを動かしていました。
今回のプログラムはスプライトは使いませんが、その代わりに3DSのスクリーンが迷路の中を向きを変えながら動くと考えます。
スクリーンの幅は400ピクセルですので、スクリーンの向きが変わることも考慮すると、400×400ピクセルのオブジェクトが迷路の中を動き回っていることになります。

従って、迷路の通路の幅は少なくとも400ピクセル以上必要です。
また、カメラの座標はスクリーンの中心だとすると、その座標は迷路の壁から少なくとも200ピクセルは離さないと、下図に示すようにスクリーンに壁の向こう側が映ってしまいます。
pc29-2.jpg

こういったことを考慮して、今回のプログラムでは、大きさに関する数値を7日目のプログラムの16倍にしました。
下図のように、迷路の1部屋は1024×1024ピクセルになります。
カメラの中心点は常に壁から256ピクセル以上離れており、スクリーンに壁の向こうが写ることはありません。
pc29-3.jpg

処理内容自体は7日目と大きく変わるところはなく、1部屋がマップで4×4マスになる点も同じです。
ただし、スプライトでは座標値はスプライトの左上の点の座標値でしたが、今回のプログラムではカメラや敵の座標は中心の点の座標値になっています。


それでは、本題の敵の表示の話に戻ります。

グラフィックスで敵を描画する際、壁の向こうに居る敵は、壁で隠さなければなりません。

前回は壁だけの描画でしたので、遠くの壁を先に、近くの壁を後に描画することで隠面消去を実現していました。
敵の描画を行う場合も基本は同じです。
敵よりも遠い壁を描いた後に、敵を描き、敵より近い壁はその後に描いていくことになります。

このためには、敵と壁を区別することなく、Z座標の大きい順に並べ替える必要があります。

前回のプログラムでは、壁の遠近の判定において、壁の中心のZ座標で壁全体を表すことで計算を簡略化しました。
簡略化の条件として、迷路の壁が「碁盤目状に配置されている」という条件がありました。
これはより詳しく言うと、壁同士が交差しない(壁の前後関係が途中で変化しない)こと、壁の大きさが全て同一であること、という条件になります。
pc29-6.gif

敵と壁を同列に扱うために、敵のZ値も、敵の中心のZ座標で敵全体を現すことにします。

敵や壁のZ値を、中心の座標だけで管理するということは、ポリゴンの向きによらず中心の座標だけで、敵と壁の前後関係が判断できなければなりません。

次の図のような状態の、敵とその手前の壁について考えます。
これは迷路を上から見た状態で、敵の形状は円柱で近似しています。
Z軸は下から上、つまり下にあるものほど手前にあるものとします。

このとき、壁が手前になるためには「壁の中央のZ値」が「敵の中心のZ値」よりも常に小さくなければなりません。
ところが実際には、敵の位置によっては壁が敵よりも遠くにあると判定されてしまいます。
pc29-4.jpg

図では青の線が壁のZ値なので、緑の円柱は壁の向こうですが、赤の円柱は壁の手前にあるという判定になります。
その結果、図右下のように、間違った前後関係で描画されてしまいます。
壁の手前半分のZ値は、壁の中央のZ値よりも実際には小さい(手前にある)のに、それが考慮されていないためにこのようなことが起こります。

これを回避するには、敵のZ値が壁のZ値より小さい時に、上の図で黄色で示したエリアに敵のポリゴンが重ならないようにします。
黄色の領域は前後関係の判定に用いる壁のZ値よりも実際のZ値のほうが小さいために、遠近が正しく判定できない領域だからです。

この条件を満たすには敵のポリゴンの「位置」と「大きさ」の制限が必要です。
pc29-5.jpg

上の図にあるように、

・敵の中心が通路の中央であること
・敵の幅が通路の幅の1/2未満であること

の2つを両方満たしている場合(緑の円)、黄色の領域と重なることはありません。
一方、赤の円柱は2番目の条件よりもサイズが大きいため、間違った前後関係で描画されてしまいます。


というわけで、だいぶ長い解説になってしまいましたが、前回のプログラムに敵の描画を追加したプログラムを以下に示します。
まずはZバッファの処理の部分です。
FOR I=0 TO 399+NUM_E+1
ZBUF[I]=POW(2,31)
W_VISIBLE[I]=-9999
NEXT

FOR I=0 TO LEN(W_BGN)-1
(略)
NEXT

FOR I=0 TO NUM_E
ZBUF[I+400]=E_RW[I]
W_VISIBLE[I+400]=-1-I
NEXT

敵のデータは以下のようになっています。

 ・NUM_E ・・・敵の数-1
 ・E_RW ・・・敵のZ座標値(スクリーン中央を原点とした座標系に変換済み)

ZBUFとW_VISIBLEのサイズは、本来はスクリーンの横方向のピクセル数に等しいのですが、これを敵の数の分だけ大きくしています。
そして、敵については、壁で隠されているか否かに関わらず、すべて「見えている」(ただし壁は隠していない)ことにして、ZBUFとW_VISIBLEに追加しています。

W_VISIBLEには「壁の番号」を記録しますので、敵0が-1、敵1が-2、敵2が-3、、、というように、敵は負の数で表して壁と区別が付くようにしています。

続いて描画処理の部分です。
GCLS

RSORT ZBUF,W_VISIBLE
W_LAST=-9999
FOR I=0 TO LEN(W_VISIBLE)-1
IF W_VISIBLE[I]==W_LAST THEN CONTINUE
W_LAST=W_VISIBLE[I]
IF W_LAST==-9999 THEN CONTINUE
IF W_LAST<0 THEN DRAW_ENEMY W_LAST:CONTINUE
P1=W_BGN[W_LAST]
P2=W_END[W_LAST]
L[0]=R_X[P1]:L[1]=R_Z[P1]
L[2]=R_X[P2]:L[3]=R_Z[P2]
IF CLIPLINE(L,X_MIN,Z_MIN,X_MAX,Z_MAX) THEN
(以下略)

RSORTによって、壁も敵も区別なく、Z値によってソートされます。
そして、スクリーンから遠い壁または敵から順に描画していきます。
見て分かるように、W_VISIBLE[I]が負の数だったら、敵の描画を行うユーザ手続きを呼び出しているだけです。

このDRAW_ENEMY関数は、中身は省略しますが、下図のように上から見ると「半径256ピクセルの円の内部」に納まるように敵を描画しています。

また、上で述べた条件を満たすように、敵は通路の中央、図の青い線の上を移動します。
pc29-7.jpg


最後に、敵との当たり判定です。
これは2点間の距離の公式

 距離=√(dx^2 + dy^2)

を使います。dxはX座標の差分、dyはY座標の差分です。
FOR I=0 TO NUM_E
D=(E_U[I]-U)*(E_U[I]-U)+(E_W[I]-W)*(E_W[I]-W)
IF D < E_R*E_R THEN
SPANIM 0,"C",1,&H00000000,-60,&HFFFF0000,-45,&H00000000,-45,1
BEEP 107,-600,96
ATTACKED=TRUE
BREAK
ENDIF
NEXT

IF ATTACKED THEN
T=T+.3
IF SPCHK(0) == 0 THEN ATTACKED=FALSE
ENDIF

変数は、

 プレイヤー: X座標 U, Z座標 W, 向き T
 敵(配列): X座標 E_U, Z座標 E_W, 半径 E_R

です。
距離の2乗を計算し(変数D)、半径の2乗より小さければ、敵とぶつかったと判定して変数ATTACKEDをTRUEにします。

スプライト0はスクリーン全体を覆う白地の図形で、通常は透明にしています。
ぶつかった場合には、このスプライトを赤い色にフェードイン/フェードアウトさせます。

フェードイン・フェードアウトのアニメーションを実行中は、プレイヤーの向きTを変化させて視点を強制的に回転させています。


以上で3D版 迷路探検ゲームの解説は終わりにします。
プログラムを作るより解説を書くほうが大変でした・・・。
posted by boochow at 00:13| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年03月14日

プチコン3号 28日目 擬似3D(5) 隠面消去

pc28-1.jpg

前回、ワイヤーフレームで3D空間を表現しましたが、今回からは11日目で作成した迷路探検ゲームの3D版を作ってみます。

プチコン3号 11日目 当たり判定を追加して迷路探検ゲーム完成!: 楽しくやろう。

前回のプログラムでは、線画で碁盤目状の地面を表示しました。
迷路の3Dでの表示は、この碁盤目に沿って垂直に壁を立てれば実現できます。

具体的には、まず迷路を二次元で作ってから、前回同様に視点からの距離に応じて縮小してやると、地面に迷路を描くことができます。(下図の青のライン)
次に、画面の下半分を鏡に映すように画面上半分に反転して表示します。(下図の緑のライン)
最後に、下半分と上半分の対応する点同士を垂直線で結んでやれば出来上がりです。(下図の赤のライン)
pc28-6.jpg


もちろんこれだけでは不十分で、壁の向こう側が見えないように、迷路の壁は塗りつぶさなければなりません。
これには、プチコン3号の3.1.0へのバージョンアップで追加された、塗りつぶした三角形の描画命令GTRIが使えます。

ただしGTRI命令で描けるのは、二次元の三角形です。
三次元ポリゴンの描画では、ポリゴン同士の重なり具合が座標のZ値を正しく反映していなければなりません。
具体的には、ポリゴンが重なる時は、Z値が小さいもの(視点により近いもの)が上になる必要があります。

例えば立方体を描く場合、下図のように色々な見え方があります。
どの見え方になるかは、立方体の各面と視点との位置関係に依存しています。
視点の移動や回転によってZ値は変化しますので、重なる順序も時々刻々変化します。
pc28-2.jpg

上の図の下半分は、左側がいろいろな壁が立っている様子を上空から眺めた図で、スクリーンは赤い位置にあります。
右側が、スクリーンからの見え方です。
手前の壁が後の壁を隠しています。

このように、重なりなどの影響を考慮して、見えている部分だけを画面に描画する処理を「隠面消去」といいます。
陰面消去には様々な方法がありますが、遠方のものから順に描画する「Zソート法」もその一つです。
この方法では、全てのポリゴンをZ軸の値の大きさで並べ替えてから、Z値が大きい順に描画します。
上の図で言えば、

  緑→オレンジ→青→ピンク

の順にポリゴンを描けば、右のような画像が得られます。
ただ、この方法は面同士が交差する場合は利用できません。

一方、三次元グラフィックスのハードウェアで広く用いられているのが「Zバッファ法」です。
これは、スクリーンの各ピクセルについて、描画時のZ値をバッファに保持しておくものです。
新しい3次元のピクセルを描画する際に、Zバッファの対応する値と、描こうとするピクセルのZ値を比較します。
そして、Zバッファの値のほうが大きければ、スクリーンを新しいピクセルで、ZバッファをそのピクセルのZ値で、それぞれ上書きします。

例えば、下図の画面中央の赤いラインに対応するZバッファは、図の下のような値を取ります。
pc28-3.jpg


前置きが長くなりましたが、上で述べたのは(擬似でない)三次元グラフィックスの一般論です。
迷路を三次元で描くことに限定すると、一般論よりも手抜きができます。
迷路の壁のポリゴンには、以下のような非常に強い制約条件があるからです。

 ・XZ平面に対して垂直である
 ・全ての壁で、幅や高さが同一である
 ・規則正しく配置されており、交差はしない

これらの制約条件を前提にすると、
「全ての壁の重なり具合は
  ・ある特定のY(ただし0<Y<壁の高さ)の場合について
  ・各壁のZ値の最大値(あるいは最小値、平均値など1つの値)を調べれば十分」
であることが分かります。

壁が垂直、かつ全ての壁の高さが同じなので、Yの値が変わっても重なり具合は変わりません。
壁が碁盤目状に配置されているので、2つの壁のどちらが手前にあるかも代表点の座標比較だけで決まります。

ちなみに碁盤目状でない場合は、垂直な壁であっても、Z値だけではどちらの壁が手前か判定できません。
下図の2つのパターンは、どちらも各壁のZ値の最大値、最小値は同じですが、壁の重なり方は異なっています。
pc28-7.jpg

今回は、以下のようなアルゴリズムで隠面消去を行っています。

 (1)全ての壁を、高さ1ピクセル横400ピクセルのZバッファに描画する
    (ただしZ値はまじめに計算せず、壁の2つの端点の平均値を採用)
 (1’)描画時に、各ピクセルが表示している壁の番号を、長さ400の配列に記録する
 (2)壁の番号の配列を、Z値が大きい順に並べ替える
 (3)壁の番号の配列の順に壁を描く

このアルゴリズムは、表示すべきポリゴンの抽出を(1)でZバッファ法で行い、抽出したポリゴンの描画は(2)でZソート法で行っていることになります。

今回のアルゴリズムは、隠れている壁を描画しなくて済むので、描画処理を大幅に短縮できます。
このような処理をせずに、迷路の壁を普通に全てワイヤーフレームで描くと、例えば以下のようになります。
これをすべて遠い順にGTRI命令で描画すると、さすがにアニメーションさせるのは困難です。
pc28-4.jpg


上記のアルゴリズムを使って、「見える壁」だけを抽出すると、以下のようになります。
この場合は、壁の枚数は10枚ですので、GTRI命令20回で描画することができます。
この程度の回数であれば、プチコン3号でも、何とかアニメーションができる程度の時間で描画できます。
pc28-5.jpg

遠くの壁から順に描画すると、以下のような画像ができます。
pc28-1.jpg

プログラムですが、全体はMiiverseにアップロードしてあります。
公開キーは4R2EP4K3です。
サイズが大きいので、今回は上記の隠面処理の部分だけを抜き出して掲載します。

FOR I=0 TO 399
ZBUF[I]=POW(2,31)
W_VISIBLE[I]=-9999
NEXT


FOR I=0 TO LEN(W_BGN)-1
P1=W_BGN[I]
P2=W_END[I]
L[0]=R_X[P1]:L[1]=R_Z[P1]
L[2]=R_X[P2]:L[3]=R_Z[P2]
IF !CLIPLINE(L,X_MIN,Z_MIN,X_MAX,Z_MAX) THEN CONTINUE
X1=FLOOR(L[0]*SCALE_Z(L[1])+200)
X2=FLOOR(L[2]*SCALE_Z(L[3])+200)
IF X1>X2 THEN SWAP X1,X2
X1=MAX(X1,0)
X2=MIN(X2,399)
ZVAL=(R_Z[P1]+R_Z[P2])/2
FOR J=X1 TO X2
IF ZBUF[J]>ZVAL THEN
ZBUF[J]=ZVAL
W_VISIBLE[J]=I
ENDIF
NEXT
NEXT

pc28-8.jpg

リストで使われている変数の意味は上の図を参照してください。
壁は始点と終点で表します。始点と終点は、図の左にあるように、格子の交差点につけた番号です。
図では、0番の壁の始点・終点はp0とp1です。1番の壁の始点・終点はp1とp(w+1)です。
壁の始点と終点は図右の表のように、始点が配列W_BGN、終点が配列W_ENDに格納されています。
点pの座標は、配列R_XとR_Zに格納されています。

最初のFOR...NEXTループは、2つの配列を初期化しています。
配列ZBUFはZバッファで、視点からの距離(Z値)を格納します。
Zバッファの各要素の初期値は無限大…としたいところですが、実際は十分大きな値(2の31乗=約20億)にしています。
W_VISIBLE[I]は、ZBUF[I]に表示される壁の番号が入ります。
初期値-9999は、「壁が表示されていない」ことを示すために使います。

次のFOR...NEXTループで、I番目の壁の処理を行います。

壁の端点のXZ座標は(R_X[P1], R_Z[P1])と(R_X[P2], R_Z[P2])です。
これを配列Lに設定し、Z>0の空間でクリッピングします。
これによって、視点よりも後方の壁は消去されます。
(クリッピングについては前回の記事を参照してください。)
表示される部分が無ければ、次の壁の処理に移ります。

表示される部分があった場合は、配列Lにクリッピング処理後の座標が入っています。
L[1]とL[3]がZ値で、これを元に縮小率を求め、画面上で壁が表示される範囲のX座標X0とX1を求めます。

そして、ZBUF[X0]..ZBUF[X1]にこの壁のZ値を代入します。
ただし、元からZBUFに入っていた値のほうがZ値よりも小さい場合には何もしません。

Z値は実際にはL[1]〜L[3]まで変化しますが、先に書いたように壁が規則正しく並んでいるので、壁の中点(下図左上の緑色の点)のZ座標で代用します。
また、W_VISIBLE[X0]..W_VISIBLE[X1]には、この壁の番号Iを代入します。
pc28-9.jpg

次に、壁の描画処理です。

まず、作成したZバッファの値の大きい順に、W_VISIBLEをソート(並べ替え)します。
プチコン3号は、このための命令RSORTがあります。
RSORTは第一引数の配列を降順でソートしますが、第二引数以降の配列が与えられた場合は、それらの配列も同じ順序でソートします。

ソートした結果のW_VISIBLEは、表示されるピクセル数の分だけ、壁の番号が連続して並んでいます。
描画処理ではW_VISIBLEの先頭から順に、指定された番号の壁を描画していきます。
直前と同じ番号の壁はもう描画する必要が無いので、飛ばします。

緑色の部分が壁を1つ描くためのコードです。
壁は台形になるので、GTRI命令を2回使います。
また、壁の輪郭の縦線の部分をGLINEで描いています。
GCLS
RSORT ZBUF,W_VISIBLE
W_LAST=-9999
FOR I=0 TO LEN(W_VISIBLE)-1
IF W_VISIBLE[I]==W_LAST THEN CONTINUE
W_LAST=W_VISIBLE[I]
IF W_LAST==-9999 THEN CONTINUE
P1=W_BGN[W_LAST]
P2=W_END[W_LAST]
L[0]=R_X[P1]:L[1]=R_Z[P1]
L[2]=R_X[P2]:L[3]=R_Z[P2]
IF CLIPLINE(L,X_MIN,Z_MIN,X_MAX,Z_MAX) THEN
S1=SCALE_Z(L[1])
L[0]=L[0]*S1+200
L[1]=V*S1+120
S2=SCALE_Z(L[3])
L[2]=L[2]*S2+200
L[3]=V*S2+120
C=(S1+S2)/2*128+8
C=RGB(C,C,C)
GTRI L[0],L[1],L[2],L[3],L[2],-L[3]+240,C
GTRI L[0],L[1],L[0],-L[1]+240,L[2],-L[3]+240,C
C=MAX(S1,S2)*128+32
C=RGB(C,C,C)
GLINE L[0],L[1],L[0],-L[1]+240,C
GLINE L[2],L[3],L[2],-L[3]+240,C

ENDIF
NEXT


以上で迷路の3D表示は終わりです。

次回は、3D迷路の中に敵を追加してみたいと思います。

posted by boochow at 19:48| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年03月08日

プチコン3号 27日目 擬似3D(4) 背景をワイヤーフレームで描く

pc27-1.jpg

前回、前々回とスプライトを使った擬似3D表示のプログラムを作ってきました。
しかし2Dのゲームと比べると背景が簡素になってしまいます。

2DではBGやスプライトを使用することができますが、どちらも回転と拡大しか行えません。
擬似3D表示では近くのものは大きく、遠くのものは小さく表示します。
これをBGやスプライトで実現するには台形型の変換(射影変換、ホモグラフィと呼ばれます)が必要ですが、プチコン3号ではこの変換はサポートされていません。

今回は、比較的高速に表示できる方法として、ワイヤーフレームの背景をGLINE命令で描いてみます。
前々回のプログラムで、GPSET命令で点を描画させましたので、座標変換は既にできています。
基本的には、この点と点を結べばワイヤーフレームの描画ができます。
pc25-7.jpg


ただし、線分を描画する場合には、点の描画には無かった問題が起こります。
下図は、3次元の空間を横から見た状態(YZ平面)です。
中央近くの赤い縦線のところがスクリーンで、右方向がスクリーンから見た奥行き方向になります。
左方向はスクリーンよりも手前なので、表示させない領域です。
pc27-2.jpg

今、青い矢印のように線を描いたとします。
足元から、前方遠くに向かってまっすぐ線が延びているようなイメージです。
スクリーン上では、この線はスクリーンの下半分に「下から上に」伸びるように見えています。

この線がスクリーンよりも手前方向にも伸びていて、図左の赤い領域まで入っているとします。
すると、この線はスクリーン上では、スクリーンの上半分で「上から下に」延びているように見えます。
図の中央でスクリーン下端から延びる線とスクリーン上端から延びる線が交差していますが、この点より左側では縮小率が負になるため、上下が逆になってスクリーンへ投影されるのです。

この不要な表示を抑制するためには、「クリッピング」と呼ばれる処理が必要です。
点やスプライトを描画する場合には、Zが負の場合は単に描画しなければ済みました。
しかし、線や面のように広がりを持つものを描画する場合は、描画可能な部分だけを「切り取る」ことが必要です。
上の図で言えば、青い線分のうち、スクリーンよりも右側(Z>0)の領域を抽出することになります。

さらに、Z>0であっても、擬似3DのプログラムではGLINE命令に与えるXやYがスクリーンのサイズよりはるかに大きくなる場合があります。
ところがプチコン3号のGLINE命令は、現状(v3.1.0)ではやや不安定で、与えた座標値によってはプチコン3号自体がクラッシュすることがあります。


GCLIP命令で描画範囲を制約することができますが、この命令を使っても使わなくても不安定さには変わりありませんでした。
そのため、スクリーンに描画する前にもスクリーンサイズでのクリッピングが必須です。

クリッピングについては、既に様々なアルゴリズムが考案されています。
「線分を矩形でクリッピングする」アルゴリズムとしては、Cohen-Sutherlandのアルゴリズムや、Liang-Barskyのアルゴリズムが知られています。

今回は、後者のLiang-Barskyのアルゴリズムでクリッピングを実装しました。
このアルゴリズムは、下図のように線分をtというパラメータで表現します。
t=0が線分の一方の端点を表し、t=1が線分のもう一方の端点を表します。
クリッピングされた線分はt0とt1という2つのパラメータで表されます。
pc27-3.jpg

このパラメータの求め方は以下のようになります。

まず、線分全体の長さのX値をp、線分の端点と矩形領域の左端との差分のX値をqとします。
すると、線分が矩形領域の左端と交差する点のt=q/pが求まります。

次に、下図のように、同じことをY座標についても行います。
すると同様に、線分が矩形領域の下端と交差する点のt=q/pが求まります。
この2つのtのうち、より値が大きいほうをt0として採用します。
pc27-4.jpg


t1についても同様の処理を行います。
2つのt1のうち、より小さい値をt1として採用します。

アルゴリズムの基本的な部分は以上で、このほか、線分が水平または垂直であった場合や、線分が交差しない場合などの処理が必要になります。
以下のページにこのアルゴリズムの図解やソースコードがあります。
(ただし、ソースコードはp==0の判定より前にq/pを求めているというバグがあります。)

Skytopia : The Liang Barsky line clipping algorithm in a nutshell

このソースコードを参考に、プチコン3号で線分のクリッピングを行う関数を作ったのが以下のリストです。
DEF CLIPLINE(A_LINE,LEFT,TOP,RIGHT,BOTTOM)
VAR I,T0,T1,DX,DY,P,Q,R
DX=A_LINE[2]-A_LINE[0]
DY=A_LINE[3]-A_LINE[1]
T0=0:T1=1
FOR I=0 TO 3
IF I==0 THEN P=-DX:Q= A_LINE[0]-LEFT
IF I==1 THEN P= DX:Q=-A_LINE[0]+RIGHT
IF I==2 THEN P=-DY:Q= A_LINE[1]-TOP
IF I==3 THEN P= DY:Q=-A_LINE[1]+BOTTOM
IF P==0 THEN
IF Q<0 THEN RETURN FALSE
CONTINUE
ENDIF
R=Q/P
IF P<0 THEN
IF R>T1 THEN RETURN FALSE
T0=MAX(R,T0)
ELSEIF P>0 THEN
IF T0>R THEN RETURN FALSE
T1=MIN(R,T1)
ENDIF
NEXT
A_LINE[2]=A_LINE[0]+ROUND(T1*DX)
A_LINE[3]=A_LINE[1]+ROUND(T1*DY)
A_LINE[0]=A_LINE[0]+ROUND(T0*DX)
A_LINE[1]=A_LINE[1]+ROUND(T0*DY)
RETURN TRUE
END

最初のパラメータは長さ4の配列で、クリッピングしたい線分の座標をX0,Y0,X1,Y1の順に格納します。
続く4つのパラメータがクリッピングするための矩形です。
座標軸はX軸が右向き、Y軸が下向きと考えて、矩形の左上の座標が(LEFT,TOP)、右下の座標が(RIGHT,BOTTOM)です。

クリッピングした結果は第一引数の配列に格納されます。
描画するものが無い場合はFALSE、描画するものがある場合はTRUEが返り値になります。


これで、安心して擬似3DでGLINE命令を使えますので、ちょっとしたデモプログラムを作ってみました。
この記事の最初の画面写真がそれです。

床面に10×10の格子を描き、立方体を5つ配置しました。
さらに、スプライトを1つ、円周上に動かしています。
上から見ると以下のような感じです。円はスプライトの軌跡です。
pc27-5.jpg

まずプログラムのメイン部分です。
DEF ROTATE X,Y,T OUT X1,Y1
X1=COS(T)*X - SIN(T)*Y
Y1=SIN(T)*X + COS(T)*Y
END

ACLS
DIM X[100],Z[100]
FOR I=0 TO 99
X[I] = -1800 + 400*(I MOD 10)
Z[I] = -1800 + 400*FLOOR(I / 10)
NEXT

SPSET 0,3088
SPANIM 0,"I+",10,0,10,1,10,2,10,3,0
SPOFS 0,200,120
SP_T=0:SP_U=0:SP_V=0:SP_W=0

T=0:U=0:V=0:W=0
DIM R_X[100],R_Z[100]
PAGE=0
@LOOP
GPAGE PAGE,(PAGE+1) MOD 2
PAGE=(PAGE+1) MOD 2
B=BUTTON(0)
IF B AND 256 THEN T=T-0.05
IF B AND 512 THEN T=T+0.05
IF B AND 1 THEN V=V-10
IF B AND 2 THEN V=V+10
STICK OUT DX,DY
ROTATE DX,DY,-T OUT DX,DY
U=U + DX*20
W=W + DY*20
FOR I=0 TO 99
ROTATE X[I]-U,Z[I]-W,T OUT R_X[I],R_Z[I]
NEXT
GCLS
DRAW_SCENE R_X,R_Z

SP_T=SP_T+.1
ROTATE 500,0,SP_T OUT SP_U,SP_W
SP_V=-300+50*SIN(SP_T*5)
ROTATE SP_U-U,SP_W-W,T OUT SP_U,SP_W
IF SP_W >0 THEN
SCALE=SCALE_Z(SP_W)
SP_X=SP_U*SCALE+200
SP_Y=(SP_V-V)*SCALE+120
SPSCALE 0,SCALE*6,SCALE*6
SPOFS 0,SP_X,SP_Y,0
SPSHOW 0
ELSE
SPHIDE 0
ENDIF
VSYNC 1
GOTO @LOOP

赤い部分がスプライトのための処理です。
コウモリのキャラクタを設定し、アニメーションも付けています。
3次元空間の中でのスプライトの座標を計算するために変数(SP_U,SP_V,SP_W)を使います。

また、このスプライトは原点を中心とした円周上を飛ばします。
円周上の位置は角度SP_Tで表します。

ループに入る前後のところで
PAGE=0
@LOOP
GPAGE PAGE,(PAGE+1) MOD 2
PAGE=(PAGE+1) MOD 2

というコードがありますが、これは画面のダブルバッファの処理です。
今回は描画処理が重く、そのままではちらつくので、GPAGEで表示ページと描画ページに別のページを指定し、描画処理が終わったら2つのページを入れ替えるようにしています。

背景をGLINEで描く部分は、DRAW_SCENEとして別関数に分けています。
DRAW_SCENEのパラメータとして、視点の回転による座標変換を行った後のX座標とZ座標の配列を渡しています。
座標の個数はそれぞれ100個で、10×10の格子状に並んでいます。

その後が、スプライトを回転移動させるための処理です。
まずSP_Tを0.1ラジアン増やします。
そして、(500,0)をSP_Tだけ回転した座標をSP_U,SP_Wに代入します。

Y座標であるSP_Vは、-300を基準として、sin関数でゆらゆらと上下に動かしています。

以上でスプライトの(空間内での)座標が決まり、こんどはそれをスクリーンからの相対座標に変換します。
これは前回・前々回で行ったのと同じ処理です。

そして、変換後のZ座標が正なら、縮小率を計算してスクリーン上での表示位置を決定し、スプライトのスケールや位置を指定します。
Z座標が負ならスプライトを消します。

関数SCALE_Zは縮小率を求める関数です。

DEF SCALE_Z(Z)
RETURN 400/(400+1600*(Z/2000))
END


では、先ほど青字で示した、背景のワイヤーフレーム描画の処理です。

以下のGLINE3Dが、一番基本的な、3次元空間での線分描画を行います。
(X0,Y0,Z0)から(X1,Y1,Z1)まで、色Cで線分を描きます。

X,Z座標値はカメラの向きに応じて回転した後の座標値が必要です。
また、パラメータで与えていませんが、現在のスクリーンの位置に関する変数「V」をY座標の計算に使っています。
DEF GLINE3D X0,Y0,Z0,X1,Y1,Z1,C
VAR S0,S1,X2,Y2,X3,Y3
DIM L[4]
L[0]=X0
L[1]=Z0
L[2]=X1
L[3]=Z1
IF !CLIPLINE(L,-9999,-1,9999,19999) THEN RETURN
S0=SCALE_Z(L[1])
S1=SCALE_Z(L[3])
L[0]=L[0]*S0+200
L[2]=L[2]*S1+200
L[1]=(Y0-V)*S0+120
L[3]=(Y1-V)*S1+120
IF CLIPLINE(L,0,0,400,240) THEN
GLINE L[0],L[1],L[2],L[3],C
ENDIF
END

GLINE3Dは座標を全て与えなければなりませんので、X座標の配列とZ座標の配列を使って、XZ座標を番号で指定できるようにPGLINE3Dを定義しています。
DEF PGLINE3D P1,Y1,P2,Y2,C,X,Z
GLINE3D X[P1],Y1,Z[P1],X[P2],Y2,Z[P2],C
END

PGLINE3Dを使って直方体を描画する関数DRAW_CUBEです。
DEF DRAW_CUBE P1,P2,P3,P4,Y1,Y2,C,X,Z
PGLINE3D P1,Y1,P2,Y1,C,X,Z
PGLINE3D P2,Y1,P3,Y1,C,X,Z
PGLINE3D P3,Y1,P4,Y1,C,X,Z
PGLINE3D P4,Y1,P1,Y1,C,X,Z
PGLINE3D P1,Y2,P2,Y2,C,X,Z
PGLINE3D P2,Y2,P3,Y2,C,X,Z
PGLINE3D P3,Y2,P4,Y2,C,X,Z
PGLINE3D P4,Y2,P1,Y2,C,X,Z
PGLINE3D P1,Y1,P1,Y2,C,X,Z
PGLINE3D P2,Y1,P2,Y2,C,X,Z
PGLINE3D P3,Y1,P3,Y2,C,X,Z
PGLINE3D P4,Y1,P4,Y2,C,X,Z
END

以下がDRAW_SCENEの本体です。
GLINE3DとDRAW_CUBEを呼んでいるだけです。
DEF DRAW_SCENE X,Z
VAR C,I
C=&HFFFFFFFF
FOR I=0 TO 9
GLINE3D X[I],120,Z[I],X[90+I],120,Z[90+I],C
GLINE3D X[I*10],120,Z[I*10],X[I*10+9],120,Z[I*10+9],C
NEXT
GLINE3D X[99],120,Z[99],X[9],120,Z[9],C
GLINE3D X[90],120,Z[90],X[99],120,Z[99],C

C=&HFFFFFF00
DRAW_CUBE 7,8,18,17,120,-240,C,X,Z
DRAW_CUBE 92,93,83,82,-300,-560,C,X,Z
C=&HFFFF00FF
DRAW_CUBE 97,98,88,87,120,-240,C,X,Z
DRAW_CUBE 1,2,12,11,-400,-600,C,X,Z
C=&HFF00FFFF
DRAW_CUBE 12,14,24,22,120,-800,C,X,Z
END


以上で、擬似3Dの背景をグラフィックスで描くことができました。
とはいえ、ワイヤーフレームではやっぱりちょっと寂しい感じもします。

プチコン3号の先日のアップデートで、三角形を描く命令GTRIが新たに追加されましたので、次回はGTRIを使ってポリゴンで描く擬似3Dプログラムを作ります。
posted by boochow at 23:30| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年03月06日

プチコン3号 26日目 擬似3D(3) 一人称視点・三人称視点

pc26-1.jpg

今回は前回のプログラムのバリエーションとして、視点がスクリーンより手前にある場合、およびスクリーンより奥にある場合のプログラムを作ってみます。
前者はいわゆるFPS(First Person Shooting)ゲーム、後者はRPGなどキャラクタが画面中央に居るタイプのゲームなどでよく用いられます。

前回、三次元空間の中で見回す動作を実現するために、座標の回転を行いました。
このとき、回転の中心は下図のように、座標系の原点、すなわちスクリーンの中央としていました。
これは図右のように、カメラを三脚で固定してから水平方向に回転する(パンする)見え方になっています。
pc26-2.jpg

カメラを三脚ではなく、スクリーンを見る人自身に固定する方法もあります。
これは、人が見ている風景をそのままスクリーンに再現しますので、「一人称視点」と呼ばれます。

一人称視点では、回転の中心はスクリーンより手前、台形の2つの斜辺が交わる点になります。
前回のプログラムでは、Zが0から2000まで変化するとき、スクリーンの幅が400から2000まで変化していましたので、計算すると斜辺が交わる点=スクリーンの幅が0になる点=スクリーンの手前500の位置であることが分かります。
pc26-3.jpg

前回のプログラムを、一人称視点に変更すると以下のようになります。
赤い部分が変更点です。
DEF ROTATE X,Y,T OUT X1,Y1
X1=COS(T)*X - SIN(T)*Y
Y1=SIN(T)*X + COS(T)*Y
END

ACLS
DIM X[100],Z[100]
FOR I=0 TO 99
X[I] = -1800 + 400*(I MOD 10)
Z[I] = -1800 + 400*FLOOR(I / 10)
SPSET I,2067
NEXT

T=0:U=0:V=0:W=0
@LOOP
B=BUTTON(0)
IF B AND 256 THEN T=T-0.02
IF B AND 512 THEN T=T+0.02
IF B AND 1 THEN V=V-10
IF B AND 2 THEN V=V+10
STICK OUT DX,DY
ROTATE DX,DY,-T OUT DX,DY
U=U + DX*10
W=W + DY*10
FOR I=0 TO 99
ROTATE X[I]-U,Z[I]-W,T OUT X1,Z1
SCALE=500/Z1
IF Z1<244 THEN SPHIDE I:CONTINUE
SPSHOW I
IF Z1>500 THEN Z1=500+(Z1-500)/4
IF Z1>1024 THEN Z1=1024
C=127+128*SCALE
SPCOLOR I,RGB(255,C,C,C)
SPSCALE I,12*SCALE,12*SCALE
SPOFS I,X1*SCALE+200,(-V+120)*SCALE+120,Z1-500
NEXT
VSYNC 1
GOTO @LOOP

このプログラムでは、座標系の原点を視点と一致させています。
すると、遠近感を表現するための縮小率の計算は、

 SCALE = 500/Z

という非常に単純な式になります。
スクリーンはZ=500の位置にありますので、Z=500のとき縮小率が1.0になります。

また、スクリーンの位置がZ=500となったのを反映して、Zの値に関する定数がすべて500ずつ増えています。
ただしSPOFS命令ではスクリーンの位置がZ=0ですから、このときは逆にZの値を500減らしています。

なお実際に動かしてみると、左右に見回す動作が少し遅く感じられたので、回転角の変化量を2倍にしています。


一人称視点は、ゲームの空間の中に入り込んだら見えるはずのものがスクリーンに表示されるので、ゲームへの没入感が強調されます。
その一方、自分のキャラクタの周囲の状況を把握しづらいので、ゲームをデザインする上では制約になる場合もあります。

これに対し、三人称視点は、画面内にプレイヤーのキャラクタを表示します。
一人称視点に比べ没入感は減るものの、周囲の状況が把握しやすくなります。

三人称視点の表示では、下図のようにスクリーンよりも奥にプレイヤーのキャラクタを置き、そこが視点の回転軸になります。
回転軸とスクリーンとの距離をPとすると、X=0,Z=Pの位置が回転軸となります。
pc26-4.jpg


前回のプログラムを三人称視点にしたものが以下のプログラムです。
赤い箇所が前回からの変更点です。

この記事の最初に載せた画面は、このプログラムの画像です。
画面中央部に、後ろ向きのプレイヤーキャラクタが表示されています。
DEF ROTATE X,Y,T OUT X1,Y1
X1=COS(T)*X - SIN(T)*Y
Y1=SIN(T)*X + COS(T)*Y
END

ACLS
DIM X[100],Z[100]
FOR I=0 TO 99
X[I] = -1800 + 400*(I MOD 10)
Z[I] = -1800 + 400*FLOOR(I / 10)
SPSET I,2067
NEXT

T=0:U=0:V=0:W=0:P=200
SPSET 100,2556
SPANIM 100,"I+",10,0,10,1,10,2,10,3,0
SPOFS 100,200,206,P/4
SPSCALE 100,5,5
@LOOP
B=BUTTON(0)
IF B AND 256 THEN T=T-0.01
IF B AND 512 THEN T=T+0.01
IF B AND 1 THEN V=V-10
IF B AND 2 THEN V=V+10
STICK OUT DX,DY
ROTATE DX,DY,-T OUT DX,DY
U=U + DX*10
W=W + DY*10
FOR I=0 TO 99
ROTATE X[I]-U,Z[I]-W-P,T OUT X1,Z1
Z1=Z1+P
SCALE=400/(400+1600*(Z1/2000))
IF Z1<0 THEN SPHIDE I:CONTINUE
SPSHOW I
Z1=Z1/4
IF Z1>1024 THEN Z1=1024
C=127+128*SCALE
SPCOLOR I,RGB(255,C,C,C)
SPSCALE I,12*SCALE,12*SCALE
SPOFS I,X1*SCALE+200,(-V+120)*SCALE+120,Z1
NEXT
VSYNC 1
GOTO @LOOP

まず、回転軸の中心のZ座標をP=200と決めています。
その直後にプレイヤーのキャラクタを表示していますが、ここの説明は後回しにします。

ループの中の座標の計算ですが、ROTATEに与えるZ座標は、回転の中心から見た座標ですので、「Z[I]-W-P」となっています。
そして、回転が終わった後、「Z1=Z1+P」で元の座標系に戻しています。
基本的な計算はこれで終わりです。

今回はプレイヤーのキャラクタがスクリーンよりも奥にあります。
そのため、Zの座標値が-256〜Pの範囲にスプライトを表示すると、プレイヤーのキャラクタが隠れてしまいます。
これをなるべく減らすため、Z値が0より小さいスプライトは表示しないようにしています。
これでもZ値が0〜199の場合はプレイヤーを隠してしまいますが、この範囲に全くキャラクタが表示されないのもかえって不自然になります。

さて、先ほど省略したプレイヤーキャラクタの表示ですが、SPOFSで指定する座標を計算する必要があります。

キャラクタの位置は、スクリーン中央を原点とした座標系で、X=0、Y=120、Z=Pです。
これを遠近感を考慮したスクリーン上での座標(X',Y')に変換すると、X'=200、Y'は以下のように求められます。

 SCALE = 400 / (400 + 1600 * (P / 2000))
= 400 / (400 + 1600 * (200 / 2000))
= 400 / (400 + 160)
= 400 / 560

Y' = Y * SCALE + 120
= 120 * 40/56 + 120
= 205.7142857142857 …約 206

Z値は、他のスプライトの場合と整合させて、三次元空間内でのZ座標の1/4としています。

SPSCALEのパラメータは5倍としました。表示サイズは16×5=80ピクセルになります。
縦のサイズがスクリーンの1/3ですので、表示のバランスとしては、これくらいの大きさが上限ではないかと思います。
posted by boochow at 00:55| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年03月04日

プチコン3号 25日目 擬似3D(2) 視点の移動

pc25-1.jpg

今回は、3次元の空間の中を視点が動くプログラムを作ってみます。
上の画面が今回の最終形です。
視点をスライドパッドで前後左右に移動し、十字キー上下で上下に移動し、L・Rボタンで左右に回転します。
物体のほうは水平面上に格子状に等間隔に並べてあり、位置は固定です。

前回は、スクリーンは3次元の立方体の空間の1つの面に固定されていました。
今回はスクリーンが下図のように空間の中を移動します。
pc25-2.jpg

ただし、視点から見た画像をスクリーン上に構成するためには、最終的にはすべての物体の座標を「スクリーンの中央を原点とした座標系」の上で表現する必要があります。
これは、スクリーンがある方向に移動したということを、空間全体を反対方向に移動させて表現するということになります。
つまり「プチコン3号 4日目 背景をスクロールさせる」と同じです。

さらにこれに加えて、視点の回転を表現するために、座標をY軸周りに回転することが必要です。
回転による座標の変換は、三角関数を使います。
pc25-3.jpg

円周の上にある点が、円周上を角度θだけ回転するとき、

 ・座標(x,0)は(x cosθ, x sinθ)に
 ・座標(0,y)は(-y sinθ, y cosθ)に

それぞれ移ります。

座標(x, y)を角度θだけ回転すると、上の2つを重ね合わせた

 (x cosθ-y sinθ, x sinθ+y cosθ)

へ移ります。


以上を使って、まず「2次元平面の上をスプライトが方向転換しながら移動するプログラム」を作ってみました。
ちょうど、3次元の立方体の空間を真上から見ているようなイメージです。
座標系で言うと、XZ平面を表示していることになります。

といっても、実際には普通の2次元のプログラムです。
ただし操作がちょっと変わっています。
△のスプライトを画面内で動かすのですが、動きは「回転+前後左右への移動」で表現します。

L,Rのボタンは、その場でスプライトが左回り・右回りに回転します。
スライドパッドは、上方向が「前」、下方向が「後」への移動です。
つまり、スライドパッドを上に動かすと、そのときスプライトが向いている方向に進むわけです。
スライドパッドの左右は、そのときスプライトが向いている方向に対して、左右へ(向きを変えずに)移動します。
pc25-4.jpg

DEF ROTATE X,Y,T OUT X1,Y1
X1=COS(T)*X - SIN(T)*Y
Y1=SIN(T)*X + COS(T)*Y
END

ACLS
DIM X[100],Z[100]
FOR I=0 TO 99
X[I] = -90 + 20*(I MOD 10)
Z[I] = -90 + 20*FLOOR(I / 10)
NEXT
FOR I=0 TO 99
GPSET X[I]+200,-Z[I]+120
NEXT

SPSET 0,2353
T=0:U=0:W=0
@LOOP
B=BUTTON(0)
IF B AND 256 THEN T=T-0.1
IF B AND 512 THEN T=T+0.1
SPROT 0,DEG(T)
STICK OUT DX,DY
ROTATE DX,DY,-T OUT DX,DY
U=U + DX*5
W=W + DY*5
SPOFS 0,U+200,-W+120
VSYNC 1
GOTO @LOOP

手続きROTATE X,Y,Tは、座標(X,Y)を角度Tだけ回転し、その結果をX1,Y1へ返します。
中で使っている数式は、先ほど説明したとおりのものです。

点を100個用意し、(-90,-90)から(90,90)の範囲に10×10の格子状に並べています。
点の座標は、配列XとZに格納しています。
表示はグラフィック画面上に1度だけ行っています。
なお、座標はXZ平面ですので、原点がスクリーン中央、X軸は左から右、Z軸は下から上に向かいます。

ループの中で、スライドパッドとL・Rボタンでスプライトを操作しています。
スプライトのXZ平面上での座標は、(U, W)で、向き(回転角)がT(単位はラジアン)です。

3次元空間でのY軸は、画面の手前から奥に向かう方向になります。
このとき、Y軸周りの回転の角度は、画面上では時計回りが正となります。
従って、Lボタンを押したときはTを減らし、Rボタンを押したときTを増やしています。

そして、SPROTでスプライトを回転させます。
SPROTの角度指定はラジアンではなく度(DEGREE)なので、関数DEGで変換します。
なおSPROTの角度指定は、時計回りが正です。

スライドパッドから読み取った移動量も、スプライトの向きに合わせて回転させます。
この計算は最初に定義した関数ROTATEを使いますが、この関数では角度指定が反時計回りが正です。
そのため、回転角はTではなく-Tを指定しています。


さて、実際に上のプログラムを動作させてみると、実に操作しづらいです。
スライドパッドを動かす方向と、スプライトが動く方向とが一致しないのだから当たり前です。
このプログラムを、スプライトは位置も向きも固定して、格子点のほうを移動させたり回転させたりするように変更したのが、次のプログラムです。
pc25-5.jpg

DEF ROTATE X,Y,T OUT X1,Y1
X1=COS(T)*X - SIN(T)*Y
Y1=SIN(T)*X + COS(T)*Y
END

ACLS
DIM X[100],Z[100]
FOR I=0 TO 99
X[I] = -90 + 20*(I MOD 10)
Z[I] = -90 + 20*FLOOR(I / 10)
NEXT
SPSET 0,2353
SPOFS 0,200,120
T=0:U=0:W=0
@LOOP
B=BUTTON(0)
IF B AND 256 THEN T=T-0.1
IF B AND 512 THEN T=T+0.1
STICK OUT DX,DY
ROTATE DX,DY,-T OUT DX,DY
U=U + DX*5
W=W + DY*5
GCLS
FOR I=0 TO 99
ROTATE X[I]-U,Z[I]-W,T OUT X1,Z1
GPSET X1+200,-Z1+120
NEXT

VSYNC 1
GOTO @LOOP

格子点の描画をループの内側に移動し、座標はスプライトの位置(U,W)の周りに角度Tだけ回転しています。
回転方向は、スプライトを回転させる場合と逆方向にする必要がありますが、もともとTは時計回りが正であるのに対してROTATEは反時計回りが正ですので、Tをそのまま渡せば逆方向の回転になっています。


さて、以上で準備ができました。
次にこの100個の格子点の座標を、前回のようにZの値に応じて縮小し、遠近感を出してみましょう。

前回はX座標の縮小を図示しましたので、今回はY座標の縮小を図示してみました。
pc25-6.jpg

回転のことを考えなければ、スクリーンはZ=0の平面上にあり、縦方向のサイズは240ピクセルです。
Z=2000のとき、高さ1200ピクセル分がスクリーンの240ピクセルの中に納まることになります。

原点はスクリーン中央、Y軸は下向きが正です。
格子点は、青い点線のところにあるとしましょう。
これはY=120のXZ平面です。

Z=0のとき、この平面はスクリーンの最下部に表示されます。
Z=2000のときは、Y = -600〜+600 の範囲がスクリーンに表示されますので、Y=120の平面はスクリーン上では1/5に縮小され、Y=25となります。

当然のことながら、この縮小率はX座標の縮小率と同じ値ですので、Y座標のための特別な計算は不要です。


では、格子点の座標の縮小を行ったプログラムです。
このプログラムで、視点の移動や回転の処理は完成です。
pc25-7.jpg

DEF ROTATE X,Y,T OUT X1,Y1
X1=COS(T)*X - SIN(T)*Y
Y1=SIN(T)*X + COS(T)*Y
END

ACLS
DIM X[100],Z[100]
FOR I=0 TO 99
X[I] = -450 + 100*(I MOD 10)
Z[I] = -450 + 100*FLOOR(I / 10)
NEXT

T=0:U=0:V=0:W=0
@LOOP
B=BUTTON(0)
IF B AND 256 THEN T=T-0.01
IF B AND 512 THEN T=T+0.01
IF B AND 1 THEN V=V-10
IF B AND 2 THEN V=V+10
STICK OUT DX,DY
ROTATE DX,DY,-T OUT DX,DY
U=U + DX*5
W=W + DY*5
GCLS
FOR I=0 TO 99
ROTATE X[I]-U,Z[I]-W,T OUT X1,Z1
SCALE=400/(400+1600*(Z1/2000))
GPSET X1*SCALE+200,(-V+120)*SCALE+120

NEXT
VSYNC 1
GOTO @LOOP

赤い箇所が変更点です。
前回同様の座標変換処理と、Y軸方向への移動処理を追加しています。

格子点の間隔が、元のプログラムのままでは狭すぎるので、(-450,-450)から(450,450)の範囲に並ぶように変更しています。

視点=スクリーンの中央の点の3次元空間内での位置は、座標(U,V,W)で表されます。
スライドパッドと、上下ボタンでこの座標を移動させています。

前回の記事と同様に縮小率の計算を行い、格子点のX座標とY座標を縮小しています。
縮小前のY座標は、さきほど説明したとおり、Y=120で一定ですが、描画時の座標は視点からの相対座標になりますので、-V+120になります。


それでは、最後に格子点の場所にスプライトを置いて、本格的に擬似3Dにしてみましょう。
スプライトはそのままでは16×16ピクセルと小さいので、12倍に拡大し、Z=0のときに192×192ピクセルになるようにしてみました。
これがこの記事の冒頭の画面を生成したプログラムです。
お花畑の中を散歩してみてください。
DEF ROTATE X,Y,T OUT X1,Y1
X1=COS(T)*X - SIN(T)*Y
Y1=SIN(T)*X + COS(T)*Y
END

ACLS
DIM X[100],Z[100]
FOR I=0 TO 99
X[I] = -1800 + 400*(I MOD 10)
Z[I] = -1800 + 400*FLOOR(I / 10)
SPSET I,2067
NEXT

T=0:U=0:V=0:W=0
@LOOP
B=BUTTON(0)
IF B AND 256 THEN T=T-0.01
IF B AND 512 THEN T=T+0.01
IF B AND 1 THEN V=V-10
IF B AND 2 THEN V=V+10
STICK OUT DX,DY
ROTATE DX,DY,-T OUT DX,DY
U=U + DX*10
W=W + DY*10
FOR I=0 TO 99
ROTATE X[I]-U,Z[I]-W,T OUT X1,Z1
SCALE=400/(400+1600*(Z1/2000))
IF Z1<-256 THEN SPHIDE I:CONTINUE
SPSHOW I
Z1=Z1/4
IF Z1>1024 THEN Z1=1024
C=127+128*SCALE
SPCOLOR I,RGB(255,C,C,C)
SPSCALE I,12*SCALE,12*SCALE
SPOFS I,X1*SCALE+200,(-V+120)*SCALE+120,Z1
NEXT
VSYNC 1
GOTO @LOOP


スプライトが192ピクセルだと、格子点の間隔が200ピクセルでは狭すぎますので、400ピクセル間隔にして(-1800,-1800)から(1800,1800)の範囲に配置しました。
この格子点の1つ1つに、スプライトを1つ置きます。

また、これによってフィールドが広くなったので、視点の移動速度を2倍にしています。

スプライトは、SPOFSでZ座標を指定できますが、値は(-256〜1024)でなければなりません。
Zが-256より小さいものは、視点よりも後方にあるスプライトなので、SPHIDEで表示を抑制しています。

一方、Z値が1024より大きい場合ですが、全て上限値1024にしてしまうと、遠くにあるスプライトが沢山重なってしまい、違和感があります。
このため、SPOFSで指定するZ値は実際のZ値の1/4にしています。
格子点のエリア全体が3600×3600ですので、1/4にすれば全体を表示してもZ値は1024以下に収まります。

このほか、前回同様、遠くにあるスプライトほど色を暗くしています。
posted by boochow at 02:43| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする
人気記事