現在作っているゲーム「陰都見聞録」で使用している3D描画方法について整理します。
1.基本となる考え方
スクリーンへの投影
3D描画の考え方は右図のイメージです。スクリーンに青丸を映すことで3D描画を行います。
青丸の描画位置は、まず視点の座標から見た相対的な座標を求め、その座標からウディタの角度機能を使って角度を求めます。
求めた角度について、視線の角度からの相対的な角度を求め、視野角の範囲に入っていれば描画します。
これを縦横でやることで3D座標を2D画面で見た際の描画位置が求まります。
ウディタ上での記載例[目標の角度を求める]
■変数操作: CSelf20[θ] = 角度[x10]←傾き X: CSelf0[X] Y: CSelf1[Y]
■変数操作: CSelf21[sinθ] = sin[x1000]←角度[x10] = CSelf20[θ] + 0
■変数操作: CSelf23[絶対値sinθ] 絶対値= CSelf21[sinθ] + 0
■条件分岐(変数): 【1】 CSelf23[絶対値sinθ] が 500 未満
-◇分岐: 【1】 [ CSelf23[絶対値sinθ] が 500 未満 ]の場合↓
|■変数操作: CSelf22[cosθ] = cos[x1000]←角度[x10] = CSelf20[θ] + 0
|■変数操作: CSelf24[XY距離r] = CSelf0[X] * 1000
|■変数操作: CSelf24[XY距離r] = CSelf24[XY距離r] / CSelf22[cosθ]
|■
-◇上記以外
|■変数操作: CSelf24[XY距離r] = CSelf1[Y] * 1000
|■変数操作: CSelf24[XY距離r] = CSelf24[XY距離r] / CSelf21[sinθ]
|■
◇分岐終了◇
■変数操作: CSelf25[φ] = 角度[x10]←傾き X: CSelf24[XY距離r] Y: CSelf2[Z]
留意点
・上記のコマンド文はXYZを入力し2D上のXY座標を出力するコモンの一部です。
・θは横方向の角度(方位角)です。φは縦方向の角度(仰角)です。
・一般的には横方向はX(横)とZ(奥行き)、縦方向でY(高さ)のようです。
・ここでのXYZ座標は対象のXYZ座標から視点のXYZ座標を引いたものです。
・Y/sinθまたはX/cosθで対象までの距離(r)を求められます。
・sinθが0に近づくとrが大きくなりすぎるのでX/cosθに切り替えるようにします。
・求めたrとzからφが求められます。
ウディタ上での記載例[目標の角度から描画位置を求める]
■変数操作: CSelf30[2D X] = CSelf20[θ] - V4[視点θ]
■条件分岐(変数): 【1】 CSelf30[2D X] が -1800 未満 【2】 CSelf30[2D X] が 1800 超
-◇分岐: 【1】 [ CSelf30[2D X] が -1800 未満 ]の場合↓
|■変数操作: CSelf30[2D X] = CSelf30[2D X] + 3600
|■
-◇分岐: 【2】 [ CSelf30[2D X] が 1800 超 ]の場合↓
|■変数操作: CSelf30[2D X] = CSelf30[2D X] - 3600
|■
◇分岐終了◇
■変数操作: CSelf30[2D X] = CSelf30[2D X] * V14[画面横幅]
■変数操作: CSelf30[2D X] = CSelf30[2D X] / V16[視点横幅]
■変数操作: V31[一時2D X] = CSelf30[2D X] + V18[画面横中央]
留意点
・θから視線のθをひき、視線から見て目標がどのくらい右または左にあるかを調べます。
・求めた角度にスクリーンの幅(1280ピクセル)をかけ、これを視野全体の横幅(6000ピクセル)で割り、最後に画面中央の座標(640ピクセル)を加えます。
・縦方向の描画位置もφと縦のスクリーン幅などを使って同じように求めます。求めた2D上のXとYを返してこのコモンは終わりです。
・なお、中央座標を加える前のX座標を2乗し適当な数値で除した数値は、Y座標に加えることで描画の歪みを抑える補正値として使えます。
2.タイル・対象の描画方法
3D空間上のある点の描画位置を求める方法を1.で解説しました。
ここでは、この方法を使いウディタで対象を描画する方法を考察します。
基本的な考え方:自由変形
ウディタのピクチャには自由変形という機能があります。この機能を使うと、ピクチャの四隅の描画座標を指定することで各描画座標を頂点とする四角形を描くことができます。
この4つの描画座標に前項で求める3次元座標の2次元上の描画座標を入れることで、立体的な構造物や空間を表現できます。
頂点の数だけ計算することになるため、変数や計算量が非常に多くなりますが、工夫次第で計算量を減らせます。
描画方法:四角形を一つずつ管理する
一番基本的かつ丁寧な方法は四角形(ポリゴン)を一つずつ管理する方法です。
四角形の四隅のXYZ座標+描画順計算等に使う中心の座標を可変DBに格納し、一つずつ描画します。
画像ごとに点滅させられたり、揺れを表現できたり、表現の幅が広いです。反面、一つ一つ座標を考えるのが極めて大変です。
下図のようにcsvに各座標や画像データの数値を入れていきますが、関数を駆使してもわりと苦行です。基本は中心座標+●/-●の組み合わせなのと、連続タイルは前のタイルの座標を半分引き継ぐことなどの性質があるので、これを利用してなるべくサクサク記入していくと良いです。
陰都見聞録では入口~陰都までのマップがこの形式で作られています。地底湖などのタイルを除くと300タイルくらいなので、案外無理ではないです。
補足:建物の描画ルール
建物の形状はそこまで種類が多いわけではないので、Excelで中心座標を入れると複数タイルの座標を入れるようなシートを作っておくとコピペが楽です。
おまけ部分の建物や道はこの形式で作っています。
ウディタ上の記載例
|-◇分岐: 【1】 [ CSelf10[頂点カウンター] が 1 と同じ ]の場合↓
| |■DB読込(可変): CSelf20[X] = 可変DB[ 0 : CSelf0[物体番号] : 3 ] (物体 : - : X-Y- X)
| |■DB読込(可変): CSelf21[Y] = 可変DB[ 0 : CSelf0[物体番号] : 4 ] (物体 : - : X-Y- Y)
| |■DB読込(可変): CSelf22[Z] = 可変DB[ 0 : CSelf0[物体番号] : 5 ] (物体 : - : X-Y- Z)
頂点カウンターを4回まわし、格納したXYZ座標を取得していきます。
| |■DB読込(可変): CSelf20[X] = 可変DB[ 0 : CSelf0[物体番号] : 3 ] (物体 : - : X-Y- X)
| |■DB読込(可変): CSelf21[Y] = 可変DB[ 0 : CSelf0[物体番号] : 4 ] (物体 : - : X-Y- Y)
| |■DB読込(可変): CSelf22[Z] = 可変DB[ 0 : CSelf0[物体番号] : 5 ] (物体 : - : X-Y- Z)
取得したXYZ座標から視点XYZをひき、2D座標変換コモンに入れていきます。描画対象とするかの除外処理をするタイミングの一つです。2D座標変換コモンから出力したXY座標は変数に全て格納し、カウンターを回し終わったら自由変形の各座標に入れ描画します。
描画方法:複数のタイルをまとめて描画する方法
オープニングの草むらとおまけの地面タイルがこの方法です。
画面を全てタイルで埋めれば3次元空間を表現できるので、主人公の周りにタイルを描画する方法です。タイルを管理する必要がないため手軽に3次元空間を表現できます。
主人公の周りを9のブロックに分割し、ブロック内でさらに9のタイルに分割します。
9タイルを1枚ずつ描画すると36回座標計算が必要になりますが、9タイルを一度に描画する場合は16回で済みます。
主人公の移動にあわせて遠くのタイルを消し、近くにタイルを移動していくイメージになります。
建物や地球のような複雑な構造物も複数のタイルをまとめて描画した方が計算量が少なくなります。
描画方法:複数のタイルをまとめて描画する方法
|■変数操作: CSelf11[カウンタ-] = 0 + 0
|■変数操作: CSelf15[カウンター②] = 0 + 0
|■条件分岐(変数): 【1】 V61[主キャラY] が 0 以上 【2】 V61[主キャラY] が 0 未満
|-◇分岐: 【1】 [ V61[主キャラY] が 0 以上 ]の場合↓
| |■変数操作: CSelf43[視点Y] = V61[主キャラY] / 300
| |■変数操作: CSelf43[視点Y] *= 300 + 0
| |■変数操作: CSelf43[視点Y] -= 300 + 0
| |■
|-◇分岐: 【2】 [ V61[主キャラY] が 0 未満 ]の場合↓
| |■変数操作: CSelf43[視点Y] = V61[主キャラY] / 300
| |■変数操作: CSelf43[視点Y] *= 300 + 0
| |■変数操作: CSelf43[視点Y] -= 600 + 0
| |■
|◇分岐終了◇
|■条件分岐(変数): 【1】 V60[主キャラX] が 0 以上 【2】 V60[主キャラX] が 0 未満
|-◇分岐: 【1】 [ V60[主キャラX] が 0 以上 ]の場合↓
| |■変数操作: CSelf42[視点X] = V60[主キャラX] / 300
| |■変数操作: CSelf42[視点X] *= 300 + 0
| |■変数操作: CSelf42[視点X] -= 300 + 0
| |■
|-◇分岐: 【2】 [ V60[主キャラX] が 0 未満 ]の場合↓
| |■変数操作: CSelf42[視点X] = V60[主キャラX] / 300
| |■変数操作: CSelf42[視点X] *= 300 + 0
| |■変数操作: CSelf42[視点X] -= 600 + 0
| |■
|◇分岐終了◇
|■回数付きループ [ 9 ]回
| |■イベントの挿入[名]: ["■■遠景描画"] <コモンEv 44> / CSelf11[カウンタ-] / CSelf42[視点X] / CSelf43[視点Y] / -200
| |■変数操作: CSelf11[カウンタ-] += 1 + 0
| |■変数操作: CSelf15[カウンター②] += 1 + 0
| |■変数操作: CSelf43[視点Y] += 300 + 0
| |■条件分岐(変数): 【1】 CSelf15[カウンター②] が 2 超
| |-◇分岐: 【1】 [ CSelf15[カウンター②] が 2 超 ]の場合↓
| | |■変数操作: CSelf15[カウンター②] = 0 + 0
| | |■変数操作: CSelf43[視点Y] -= 900 + 0
| | |■変数操作: CSelf42[視点X] += 300 + 0
| | |■
| |◇分岐終了◇
| |■
|◇ループここまで◇◇
留意点
・タイル位置把握後の描画方法は1つずつタイルを管理する方法と同じです。
3.描画順の計算
2.までの方法で描画したタイル・対象について描画順を決める必要があります。
ウディタはピクチャ番号が大きい方が手前に表示されるので、ソートが必要です。
基本的な考え方:ソートの対象をできる限り減らす
ソートの対象が増えると計算量が爆発的に増えるため、ソートの対象自体を減らすことが重要です。そのため、各タイルの性質を判定する変数を使い、特定の変数を持つタイルのみソートの対象にする等の工夫が大事です。具体的には、床や一部の壁はソートの対象にせず、最低の描画順にする等の処理があります。
また、陰都見聞録ではタイルの最大描画数が40くらいになるようにしています。
なお、ブロックを用いた描画順計算はブロックを用いる時点で描画量が多くなるという欠点があるため、一旦お蔵入りとしました。
ソート方法:コムソート
コムソートはバブルソートの改良版で、ソート対象が少ない場合にはクイックソートよりも早いらしいです。タイルの中心座標をそれぞれ2乗し合計した値(この平方根が距離ですが、ウディタには平方根の機能がありません)を比較し、値が大きければ描画順を落としていきます。
ウディタ上の記載例
■変数操作: CSelf10[カウンター上限] = V34[物体ピクチャ番号] - 1
■変数操作: CSelf30[比較範囲] = V34[物体ピクチャ番号] * 10
■変数操作: CSelf30[比較範囲] /= 13 + 0
■ループ開始
|■ループ開始
| |■変数操作: CSelf12[カウンター+n] = CSelf11[カウンター] + CSelf30[比較範囲]
| |■DB読込(可変): CSelf20[比較A] = 可変DB[ 2 : CSelf11[カウンター] : 1 ] (ソート用 : - : X^2+Y^2+Z^2)
| |■DB読込(可変): CSelf21[比較B] = 可変DB[ 2 : CSelf12[カウンター+n] : 1 ] (ソート用 : - : X^2+Y^2+Z^2)
| |■条件分岐(変数): 【1】 CSelf20[比較A] が CSelf21[比較B] 超
| |-◇分岐: 【1】 [ CSelf20[比較A] が CSelf21[比較B] 超 ]の場合↓
| | |■DB読込(可変): CSelf22[一時A] = 可変DB[ 2 : CSelf11[カウンター] : 0 ] (ソート用 : - : 物体番号)
| | |■DB読込(可変): CSelf23[一時B] = 可変DB[ 2 : CSelf12[カウンター+n] : 0 ] (ソート用 : - : 物体番号)
| | |■DB読込(可変): CSelf24[一時C] = 可変DB[ 2 : CSelf11[カウンター] : 2 ] (ソート用 : - : キャラフラグ)
| | |■DB読込(可変): CSelf25[一時D] = 可変DB[ 2 : CSelf12[カウンター+n] : 2 ] (ソート用 : - : キャラフラグ)
| | |■可変DB書込:DB[ 2 : CSelf11[カウンター] : 0 ] (ソート用 : - : 物体番号) = CSelf23[一時B]
| | |■可変DB書込:DB[ 2 : CSelf11[カウンター] : 1 ] (ソート用 : - : X^2+Y^2+Z^2) = CSelf21[比較B]
| | |■可変DB書込:DB[ 2 : CSelf11[カウンター] : 2 ] (ソート用 : - : キャラフラグ) = CSelf25[一時D]
| | |■可変DB書込:DB[ 2 : CSelf12[カウンター+n] : 0 ] (ソート用 : - : 物体番号) = CSelf22[一時A]
| | |■可変DB書込:DB[ 2 : CSelf12[カウンター+n] : 1 ] (ソート用 : - : X^2+Y^2+Z^2) = CSelf20[比較A]
| | |■可変DB書込:DB[ 2 : CSelf12[カウンター+n] : 2 ] (ソート用 : - : キャラフラグ) = CSelf24[一時C]
| | |■
| |◇分岐終了◇
| |■変数操作: CSelf11[カウンター] = CSelf11[カウンター] + 1
| |■条件分岐(変数): 【1】 CSelf12[カウンター+n] が V34[物体ピクチャ番号] 以上
| |-◇分岐: 【1】 [ CSelf12[カウンター+n] が V34[物体ピクチャ番号] 以上 ]の場合↓
| | |■変数操作: CSelf11[カウンター] = 0 + 0
| | |■条件分岐(変数): 【1】 CSelf30[比較範囲] が 1 以上 【2】 CSelf30[比較範囲] が 1 未満
| | |-◇分岐: 【1】 [ CSelf30[比較範囲] が 1 以上 ]の場合↓
| | | |■変数操作: CSelf30[比較範囲] *= 10 + 0
| | | |■変数操作: CSelf30[比較範囲] /= 13 + 0
| | | |■
| | |-◇分岐: 【2】 [ CSelf30[比較範囲] が 1 未満 ]の場合↓
| | | |■ループ中断
| | | |■
| | |◇分岐終了◇
| | |■
| |◇分岐終了◇
| |■
|◇ループここまで◇◇
|■変数操作: CSelf10[カウンター上限] = CSelf10[カウンター上限] - 1
|■条件分岐(変数): 【1】 CSelf10[カウンター上限] が 0 以下
|-◇分岐: 【1】 [ CSelf10[カウンター上限] が 0 以下 ]の場合↓
| |■ループ中断
| |■
|◇分岐終了◇
|■
◇ループここまで◇◇
留意点
・物体ピクチャ番号はソートするタイルの数が格納されています。
・このタイル数を1.3で割った数がコムソートで比較する際の比較範囲になります。
4.その他
初期設定
3D描画を機能させるためには様々な初期設定が必要です。最低限必要な初期設定項目は以下のとおりです。
★視点θと視点φの初期値
■変数操作: V4[視点θ] = 0 + 0
■変数操作: V5[視点φ] = 350 + 0
★スクリーン設定:スクリーンの中心値
■変数操作: V18[画面横中央] = 640 + 0
■変数操作: V19[画面縦中央] = 480 + 0
★スクリーン設定:スクリーンの大きさ・形状
■変数操作: CSelf20[画面横幅(半)] = 3000 + 0
■変数操作: CSelf21[画面縦幅(半)] = 3000 + 0
■変数操作: V10[画面左端] = V18[画面横中央] - CSelf20[画面横幅(半)]
■変数操作: V11[画面右端] = V18[画面横中央] + CSelf20[画面横幅(半)]
■変数操作: V12[画面上端] = V19[画面縦中央] - CSelf21[画面縦幅(半)]
■変数操作: V13[画面下端] = V19[画面縦中央] + CSelf21[画面縦幅(半)]
■変数操作: V14[画面横幅] = CSelf20[画面横幅(半)] * 2
■変数操作: V15[画面縦幅] = CSelf21[画面縦幅(半)] * 2
★スクリーン設定:視野の範囲(角度)
■変数操作: V16[視点横幅] = 1200 + 0
■変数操作: V17[視点縦幅] = 1200 + 0
軽量化
表示する画像のサイズを制限したことが一番効きました。
だいたい100座標あたり128ピクセルが解像度1280で違和感がない/重くないジャストサイズだと感じています。
次に効くのはピクチャの移動ですね。毎回表示する場合でも消さずに透明にしておくだけでだいぶ軽くなります。
当たり判定
地面に足がついているゲームを作る限りは2Dと同じです。
歩行時の高さ変動
坂を下りる時に主人公のZ座標も下げると3D感が強まります。
これは面の方程式を使います。主人公がいるタイルが坂であるとき、面の方程式を求め、主人公のXY座標を代入し、Zを求めます。
これは記載例を見ても意味不明だと思うので、面の方程式で調べてコモンを作ってみて、上手くいかなかったら比較する方がいいかもしれません。
ウディタ上の記載例
■DB読込(可変): CSelf20[AX] = 可変DB[ 0 : CSelf0[所在タイル] : 3 ] (物体 : - : X-Y- X)
■DB読込(可変): CSelf21[AY] = 可変DB[ 0 : CSelf0[所在タイル] : 4 ] (物体 : - : X-Y- Y)
■DB読込(可変): CSelf22[AZ] = 可変DB[ 0 : CSelf0[所在タイル] : 5 ] (物体 : - : X-Y- Z)
■DB読込(可変): CSelf23[BX] = 可変DB[ 0 : CSelf0[所在タイル] : 6 ] (物体 : - : X+Y- X)
■DB読込(可変): CSelf24[BY] = 可変DB[ 0 : CSelf0[所在タイル] : 7 ] (物体 : - : X+Y- Y)
■DB読込(可変): CSelf25[BZ] = 可変DB[ 0 : CSelf0[所在タイル] : 8 ] (物体 : - : X+Y- Z)
■DB読込(可変): CSelf26[CX] = 可変DB[ 0 : CSelf0[所在タイル] : 9 ] (物体 : - : X-Y+ X)
■DB読込(可変): CSelf27[CY] = 可変DB[ 0 : CSelf0[所在タイル] : 10 ] (物体 : - : X-Y+ Y)
■DB読込(可変): CSelf28[CZ] = 可変DB[ 0 : CSelf0[所在タイル] : 11 ] (物体 : - : X-Y+ Z)
■DB読込(可変): CSelf29[DZ] = 可変DB[ 0 : CSelf0[所在タイル] : 14 ] (物体 : - : X+Y+ Z)
■変数操作: CSelf23[BX] = CSelf23[BX] - CSelf20[AX]
■変数操作: CSelf24[BY] = CSelf24[BY] - CSelf21[AY]
■変数操作: CSelf25[BZ] = CSelf25[BZ] - CSelf22[AZ]
■変数操作: CSelf26[CX] = CSelf26[CX] - CSelf20[AX]
■変数操作: CSelf27[CY] = CSelf27[CY] - CSelf21[AY]
■変数操作: CSelf28[CZ] = CSelf28[CZ] - CSelf22[AZ]
■変数操作: CSelf30[BY×CZ] = CSelf24[BY] * CSelf28[CZ]
■変数操作: CSelf31[BZ×CY] = CSelf25[BZ] * CSelf27[CY]
■変数操作: CSelf32[BZ×CX] = CSelf25[BZ] * CSelf26[CX]
■変数操作: CSelf33[BX×CZ] = CSelf23[BX] * CSelf28[CZ]
■変数操作: CSelf34[BX×CY] = CSelf23[BX] * CSelf27[CY]
■変数操作: CSelf35[BY×CX] = CSelf24[BY] * CSelf26[CX]
■変数操作: CSelf36[BY×CZ-BZ×CY] = CSelf30[BY×CZ] - CSelf31[BZ×CY]
■変数操作: CSelf37[BZ×CX-BX×CZ] = CSelf32[BZ×CX] - CSelf33[BX×CZ]
■変数操作: CSelf38[BX×CY-BY×CX] = CSelf34[BX×CY] - CSelf35[BY×CX]
■変数操作: CSelf40[主X-AX] = CSelf1[X] - CSelf20[AX]
■変数操作: CSelf41[主Y-AY] = CSelf2[Y] - CSelf21[AY]
■変数操作: CSelf43[p(x-x0)] = CSelf36[BY×CZ-BZ×CY] * CSelf40[主X-AX]
■変数操作: CSelf44[q(y-y0)] = CSelf37[BZ×CX-BX×CZ] * CSelf41[主Y-AY]
■変数操作: CSelf45[-(p(x-x0)+q(y-y0))] = CSelf43[p(x-x0)] + CSelf44[q(y-y0)]
■変数操作: CSelf45[-(p(x-x0)+q(y-y0))] = CSelf45[-(p(x-x0)+q(y-y0))] * -1
■変数操作: CSelf45[-(p(x-x0)+q(y-y0))] = CSelf45[-(p(x-x0)+q(y-y0))] / CSelf38[BX×CY-BY×CX]
■変数操作: CSelf42[Z] = CSelf45[-(p(x-x0)+q(y-y0))] + CSelf22[AZ]
Sprite Sheet のキャラクターの動かし方
再UPはできないのでhttps://opengameart.org/content/golem-0 をご覧ください。
このゴーレムは縦8行がキャラの向きに対応し、横24列が動きに対応しています。
そのため、向き(0~7)*24+動き(1~24)とに分けてモーションを管理できます。
例えば、攻撃モードの時は14~17の動きをするので、14から17に少しずつ動かすことで攻撃モーションを取らせることができます。向きは別途管理すればいいため、容易に動かせます。
投影方法
これまで記載した方法は透視投影変換という描画方法です。奥行きが出せます。もう一つ、平行投影変換という描画方法があります。クオータービューみたいな感じになります。
画面中央からXYZそれぞれに-50と+50した座標の差分を全タイルの描画計算に当てはめると、平行投影変換になります。ただ自由変形を使う意味がないので、初めから平行四辺形のタイルを使った方が良いです。opengameartにはクオータービュー用の建物アセットなどもあります。
歪み
透視投影変換は角度で座標を求めるため、弧状に分布する対象を平面に描画することになり、描画が歪みます。端に行くほど歪むものの、この歪み幅は一定なので、端に行くほど(X^2が大きくなるほど)Yが下がるという補正を加えたところ、いい感じに補正されました。左図が補正前、右図が補正後です。
また、この処理を応用すると画面端が常に下がる、丘や惑星の上を歩いているような表現になります。
ソート順を維持する
これは入れたくてまだ入れられていない処理です。
建物が増えてくるとソートの計算量が馬鹿にならないので、キャラ以外のソート順がそうそう変わらないことを利用して一回計算して後はソート順をそのままにするというアイディアです。今のところはアイディアどまりです。
遠方タイルの不透明度を下げる
らたさんのこの表現が素敵だったので私も取り入れています。視点からの距離が遠いほど不透明度を下げる処理です。
https://twitter.com/10v3tm5/status/1367832063743692802?s=20&t=bhaQmYSHIH7JLuYa6zScwg
5.おわりに
ウディタ3Dは何やっても良いんだという自由さを感じられて作ってて楽しいです。
あとは三角関数の便利さにビビります。中高の時に知りたかった。。。
分からないことやアドバイスなどがあればhttps://twitter.com/Wolfrpg_hap
にお声がけください。
Comments