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) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年03月01日

プチコン3号 24日目 擬似3D(1) 遠近感の表現

今回からオブジェクト指向はいったんお休みにして、3次元グラフィックスを扱います。
といっても、まずは映画のCGのような本格的なものではなく、スプライトの拡大・縮小を使った疑似3Dについて考えます。

プチコン3号では、スプライトの拡大・縮小を自由に行うことができます。
スプライトの表示を、視点から遠くなるほど縮小し、近くでは拡大すれば、遠近感を表現できます。
また、動きを遠くのものほど小さく、近くのものでは大きくすることで、距離感を強調できます。

スプライト自体は2次元の絵なのですが、大きさを変化させることによって疑似的に3次元ぽく見せることができます。
さらに、プチコン3号はSPOFS命令でZ値を指定することで視差による奥行きもつけられますので、より3次元ぽさを強調することもできます。

今回は、まず「遠くのものほど小さく表示され動きも小さくなる」プログラムを作ります。
pc24-1.jpg

図のように、2000×2000×2000(X,Yがそれぞれ-1000〜1000、Zが0〜2000)の立方体の空間を、スクリーンから覗くことを考えます。
スクリーンは立方体の1つの面の中央に貼りついていて、大きさは400×240です。
座標軸は、スクリーンから見て右方向をX軸、下方向をY軸、奥行き方向をZ軸と考えましょう。
原点はスクリーンの中央とします。

そして、スクリーンに表示される範囲は、立方体のスクリーンと反対側の面がスクリーンの横幅に収まるようにします。
つまり、XY座標での表示範囲は

 Z=0のとき、  400×240、座標では(-200, 120)〜(200,-120)
 Z=2000のとき、2000×1200、座標では(-1000, 600)〜(1000,-600)

となります。
pc24-2.jpg

上(XZ平面)から見ると、図左の明るい灰色で示した台形の範囲が、スクリーンに表示されることになります。
横(YZ平面)から見ても同様に表示範囲は台形になります。
3次元で考えると、表示される空間は四角錘の先端を切り取ったような形になります。

では、この空間の中に200×200の正方形を表示することを考えてみましょう。
Z=0のときは、正方形のサイズは200×200です。
Z=2000のときは、スクリーン全体では2000×1200の領域を400×240に縮小して表示しています。
比率は、400/2000=1/5になりますので、200×200の正方形は40×40に縮小されることになります。

つまり、Zの値が0から2000まで変化するとき、

 ・スクリーンに表示される範囲はZに比例して400×240から2000×1200まで変化。
 ・正方形のサイズはZに反比例して200×200から40×40まで変化。

となります。

以上をプログラムにしてみると、以下のようになります。
Zの値を200ずつ変化させながら正方形を描画しました。
GCLS

LEFT = -100
TOP = -100
RIGHT = 100
BOTTOM= 100

FOR Z=0 TO 2000 STEP 200
W = 400+(2000-400)*(Z/2000)
SCALE = 400/W
X1 = LEFT * SCALE + 200
X2 = RIGHT* SCALE + 200
Y1 = TOP * SCALE + 120
Y2 =BOTTOM* SCALE + 120
GBOX X1,Y1,X2,Y2
NEXT

結果はこんな感じになります。
pc24-3.jpg

プログラム中のWは、スクリーンに表示される範囲を表し、下のグラフのようにZに応じて400から2000まで変化します。
この範囲が、スクリーン上では400ピクセルの範囲に縮小表示されます。
その縮小率がSCALEで、Zに反比例します。
pc24-4.jpg
pc24-5.jpg


さて、このプログラムで、Zの値を変化させるたびに画面をクリアしてから正方形を描画してみます。
すると、正方形が手前から奥へと飛んでいくようなアニメーションが作れます。
さらに、グラフィック画面のZの値を、正方形のZの値に合わせて設定すると、距離感が強調されます。

以上の変更を加えたものが以下のプログラムです。
アニメーションなのでZの変化幅をより小さくし、Zの最大値も5000にして、より遠くまで表現しています。
GCLS

LEFT = -100
TOP = -100
RIGHT = 100
BOTTOM= 100
@LOOP
FOR Z=0 TO 5000 STEP 50
GCLS
GPRIO Z/5

W=400+1600*(Z/2000)
SCALE=400/W
X1=LEFT*SCALE+200
X2=RIGHT*SCALE+200
Y1=TOP*SCALE+120
Y2=BOTTOM*SCALE+120
GBOX X1,Y1,X2,Y2
VSYNC 1
NEXT
GOTO @LOOP

さらに、この正方形に上下左右の動きを加えてみましょう。
スライドパッドで、正方形のXY座標を変化させてみます。
このとき「遠くほど小さく、近くほど大きく動かす」ことを考慮する必要はありません。
表示の際に行う座標変換で、移動量も変換されるからです。
GCLS

LEFT = -100
TOP = -100
RIGHT = 100
BOTTOM= 100
@LOOP
FOR Z=0 TO 5000 STEP 50
STICK OUT X,Y
LEFT = LEFT + X*10
RIGHT = RIGHT + X*10
TOP = TOP - Y*10
BOTTOM= BOTTOM- Y*10
GCLS
GPRIO Z/5
W=400+1600*(Z/2000)
SCALE=400/W
X1=LEFT*SCALE+200
X2=RIGHT*SCALE+200
Y1=TOP*SCALE+120
Y2=BOTTOM*SCALE+120
GBOX X1,Y1,X2,Y2
VSYNC 1
NEXT
GOTO @LOOP

正方形が手前から向こうへ飛び去っていくだけですが、スライドパッドで軌道を変更することができます。

では、最後にグラフィックスではなくスプライトで同じことをやってみましょう。
グラフィックスでは正方形の4つの座標値を指定して描画していましたが、スプライトでは中心座標とスケール値での指定になります。
プログラムは以下のようになります。
スプライトが遠くへ行くほど、色が暗くなるようにして、遠近感を強調しています。
ACLS
SPSET 0,3482
LEFT = -100
TOP = -100
@LOOP
FOR Z=0 TO 5000 STEP 50
STICK OUT X,Y
LEFT = LEFT + X*10
TOP = TOP - Y*10
W=400+1600*(Z/2000)
SCALE=400/W
X1=LEFT*SCALE+200
Y1=TOP*SCALE+120
SPSCALE 0,6*SCALE,6*SCALE
SPOFS 0,X1,Y1,Z/5-100
C=127+128*SCALE
SPCOLOR 0,RGB(255,C,C,C)
VSYNC 1
NEXT
GOTO @LOOP
posted by boochow at 16:50| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年02月07日

プチコン3号 23日目 シューティングゲームの当たり判定とマスク値設定

前回までに、自機と自機の弾、敵機と敵の弾をそれぞれ生成・更新できるようになりました。
今回は当たり判定を追加します。

当たり判定はSPHITSP命令で行えます。
この命令は以前にも11日目に迷路ゲームで使用しました。
このときは、2つのスプライトが衝突しているか否かの判定でした。

今回は、SPHITSP命令のもう一つの機能である「衝突しているスプライトを検索する」機能を使います。
この機能を使う場合は、SPHITSPのパラメータにはスプライト管理番号を1つだけ渡します。
そのスプライトに衝突している別のスプライトがあれば、その管理番号が返り値として得られます。
衝突していなければ、-1が返ります。


これだけだと、処理する必要がない衝突まで検索されてしまいます。
たとえば自機と自機の弾の衝突は検出する意味がありません。

無用な衝突検出を避けるには、スプライトの衝突判定条件に「マスク値」を追加します。
マスク値はSPCOL命令のパラメータで、スプライトごとに設定することができます。

SPHITSPで衝突したスプライトを検索する際に、2つのスプライトのマスク値のANDを取り、0でないスプライトだけが衝突検出の対象になります。
具体的なマスクの設定の例を下図に示します。
pc23-1.jpg

画面に登場するスプライトと、当たり判定の要不要を表の形で表しました。
自機は敵機または敵の弾と当たり判定を行いますが、自機の弾との判定は不要です。
自機の弾は、敵機とのみ当たり判定を行います。

マスク値は、ボタン入力の判定と同じように2進数で考える必要があります。
上の図では、マスク値を2進数で書いています。
2進数の01は10進数の1、2進数の10は10進数では2です。

敵機のマスク値が11で、自機のマスク値が01、自機の弾のマスク値が10なら、

11 AND 01 = 01
11 AND 10 = 10

ですから、敵機は自機とも自機の弾とも当たり判定されることになります。

マスク値の具体的な決め方は、以下のように考えると良いでしょう。

(1)衝突判定の対象が1番多いオブジェクトを選ぶ。(複数ある場合は、その中からどれか1つ選ぶ)
(2)(1)のオブジェクトと衝突しないオブジェクトで、衝突判定の対象が1番多いオブジェクトを選ぶ。
(3)(1)と(2)に衝突しないオブジェクトで、・・・(以下同じ)

今回はオブジェクトが4種類しかありませんが、上記の方法でやってみると

(1)の候補は自機、または敵機。ここでは自機としましょう。
(2)の候補は自機の弾のみ。
(3)もうありません。

そして、上記で選び出したオブジェクトに、それぞれ独立なマスク値を与えます。
マスク値は32ビットですので、以下のように独立なマスクが32個あります。

0000 0000 0000 0000 0000 0000 0000 0001
0000 0000 0000 0000 0000 0000 0000 0010
         :
0100 0000 0000 0000 0000 0000 0000 0000
1000 0000 0000 0000 0000 0000 0000 0000

どのオブジェクトをどのマスク値にするか制約はありません。
ここでは、自機が末尾01、自機の弾が末尾10としましょう。

上記で選ばれなかったオブジェクトは、独立なマスク値を持ちません。
それらのオブジェクトのマスク値は、

「当たり判定が必要な全てのオブジェクトのマスク値のOR」

で求めます。

たとえば敵機は自機、自機の弾のいずれとも当たり判定が必要ですので、

 01 OR 10 = 11

がマスク値になります。
敵の弾は、自機とのみ当たり判定しますので、マスク値は01になります。

なお、(1)のところで自機ではなく敵機を選ぶことも可能です。
その場合は(2)で敵機の弾が選ばれ、この2つに独立なマスク値を与えることになります。


前回までのプログラムに衝突判定の処理を追加すると、以下のようになります。
まず独立なマスク値を定義します。
VAR _MASK_MY:_MASK_MY     = &H01
VAR _MASK_SHOT:_MASK_SHOT = &H02

&Hは、「16進数」を表します。

次に、各オブジェクトについてSPCOL命令で衝突判定領域とマスク値の設定を追加します。
SPCOLのパラメータは

 管理番号, Xオフセット, Yオフセット, 幅, 高さ, スケール対応, マスク

です。
DEF MY_NEWSHIP
VAR SP
SP=SP_NEWOBJECT(_MY)
IF SP != _S_NULL THEN
SPCHR SP,3311'3299
SPOFS SP,_SCW>>1,200
SPCOL SP,-6,-6,12,12,TRUE,_MASK_MY
ENDIF
END
DEF NEW_SHOT X,Y,SPEED
VAR SP
SP=SP_NEWOBJECT(_SHOT)
IF SP != _S_NULL THEN
SPCHR SP,3353
SPOFS SP,X,Y
SPANIM SP,"XY+",-SPEED,0,-240,1
SPCOL SP,-1,-8,2,16,TRUE,_MASK_SHOT
ENDIF
END
DEF UPDATE_ENEMY_C SELF
VAR SP
IF MAINCNT MOD 20 != 0 THEN RETURN
SP=SP_NEWOBJECT(SELF)
IF SP != _S_NULL THEN
SPCHR SP,3251
SPOFS SP,50+RND(_SCW-100),-8
SPANIM SP,"XY+",-180,0,256,1
SPANIM SP,"I+",5,0,5,1,5,2,0
SPCOL SP,-6,-8,12,16,TRUE,_MASK_MY OR _MASK_SHOT
ENDIF
END
DEF NEW_ESHOT X,Y,DESTX,DESTY,TICK
VAR SP
SP=SP_NEWOBJECT(_ESHOT)
IF SP != _S_NULL THEN
SPCHR SP,3372
SPOFS SP,X,Y
SPANIM SP,"XY+",-TICK,DESTX,DESTY,1
SPANIM SP,"I+",5,0,5,1,5,2,5,3,0
SPCOL SP,-2,-2,4,4,TRUE,_MASK_MY
ENDIF
END

これで各スプライトの当たり判定が有効になりました。
当たり判定処理は、自機側で行うことも敵機側で行うこともできます。
どちらかと言えば、自機・自機の弾を中心に考えるほうがプログラムを書きやすいと思います。
以下の例では、自機および自機の弾のUPDATE処理内で行っています。

まず自機の弾の当たり判定処理です。
DEF UPDATE_SHOT SELF
VAR X,Y,HIT
SPOFS SELF OUT X,Y
IF Y < 0 THEN SP_FREE SELF:RETURN

HIT=SPHITSP(SELF)
IF HIT == -1 THEN RETURN
SP_FREE HIT
SP_FREE SELF
END

衝突する相手は敵機のみですので、衝突していたらその相手と弾自身を単に廃棄しています。
厳密には、他に「自機の弾同士の衝突」があり得ます。
連射速度が非常に速い場合などは、これを避けるために
IF SP_CLASS(HIT) == _SHOT THEN RETURN

という処理が必要になります。

次に自機の当たり判定です。
動作確認のために、とりあえず敵や敵弾に当たったらその都度、画面に「*」をプリントするようにしました。
実際にはこの部分に、自機がやられた場合の処理を入れます。
DEF UPDATE_MY SELF
VAR SX,SY,X,Y,B,HIT
STICK OUT SX,SY
SPOFS SELF OUT X,Y
X=MIN(_SCW-8,MAX(X+SX*8,8))
Y=MIN(_SCH-8,MAX(Y-SY*8,100))
SPOFS SELF,X,Y,1

B=BUTTON(1)
IF B AND 16 THEN
NEW_SHOT X,Y,60
ENDIF

HIT=SPHITSP(SELF)
IF HIT == -1 THEN RETURN
PRINT "*";

END

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