提供:Japanese Scratch-Wiki

このきじは ひらがなでよめません。ごめんなさい。編集者向け:作成する

レイキャスティングとは、視点と物体との位置を計算し、その値をつかってオブジェクトを描画する3Dグラフィックス全般を指す用語であるが、特に、ゲーム分野では、2Dマップに基づいて3D空間をレンダリングする手法を指すことが多い (レイキャスティングを使用したプロジェクトの例、こちら )。Scratch でレイキャスティングを行うときは、タイムラグを減らすために、シングルフレームにしたり、あえて低解像度にするなどの対策がとられる。

同じ3Dグラフィックスのレンダリング方法でも、レイキャスティングとレイトレーシングは別物なので、注意が必要である。レイトレーシングは、1方向 (1次元) で光線をトレースするレイキャスティングとは異なり、光の反射や屈折を扱いながら、2方向の光線 (2次元) をトレースする手法である。

レイキャスティングの基本

レイキャスティングのプロセスを図に表したもの。

レイキャスティングは、視点から光線 (レイ) を放射して (キャストして)、一番手前の物体までの距離を測定する手法である。

たとえば一人称視点の迷路ゲームでレイキャスティングを使うときは、プレイヤー位置から放射した光線が壁にぶつかるところで、視点からの距離を計算し、その距離にあわせて壁を描画する方法が取られる。通常は、壁が近ければ近いほど、壁の上下幅を大きく、色を明るく描画することで壁との距離を表現する。

1つの場面を描写するには、プレイヤーの視界をすべてカバーできるように、複数の光線を一定の角度ごとに放射する必要がある。右側に放射した光線はプレイヤーから右側に見える壁を、左側に放射した光線は、プレイヤーから左側に見える壁を描写するわけである。光線を何度ごとに放射するかによって、壁の描写密度が決まる。

スプライトベース vs リストベース

この記事では、Scratchでレイキャスティングを実現する2種類の方法を説明する。1つはスプライトを使ったレイキャスティング、もう1つはリストを使ったレイキャスティングである。 それぞれ次のような短所、長所がある。

スプライトベース リストベース
難しさ 簡単 難しい
フレームレイト 低い 高い
必要な知識 Scratchプログラミングの基本知識 スクラッチプログラミング、三角関数、二次元配列、ベクトルの知識
マップの保存場所 スプライト。各マップ/面はコスチュームに保存 配列 (数字の並びなどで表現)
必要なスプライトの数 1以上。方法による 最低1つ (レンダリングスプライトのみでもいける)
その他のメリット 曲線を含んだ世界を作れる ランダム生成が簡単

初心者には、比較的簡単に理解できる、スプライトベースのレイキャスティングからはじめることをおすすめする。プログラミングに慣れていてスプライトベースのレイキャスティングはすでに作ったことがある人は、少々複雑だが実行速度が速い、リストベースのレイキャスティングも試してほしい。

スプライトベースのレイキャスティング

スプライトをつかったレイキャスティングを行うには、次の3つの部品が必要である:

  • マップデータ
    • 作成するゲーム面のマップ。コスチュームとして指定
  • 距離を調べる部分
    • プレイヤーと壁の距離を計算する
  • 描画する部分
    • ↑で計算した距離をもとに壁を描画する

このサンプルでは、これらの機能とプレイヤー役で、あわせて4つのスプライトを作成する。途中で不明な点などがあれば、完成したサンプルを見て確認してほしい。完成したサンプル

マップ

マップの例

マップ保存用のスプライト作成する。コスチュームのサイズは480×360、床部分 (壁以外) は「透明」にしておくこと。

次に、このスプライトにスクリプトを追加する:

@greenFlag が押されたとき::events hat
x座標を (0)、y座標を (0) にする
[幽霊 v] の効果を (100) にする

プレイヤー

次に、地図上を動きまわるプレイヤー役のスプライトを作る。

このスプライトでは、コスチュームとして 1 × 1 の点を描く。必ず、コスチュームが画面の中心になるようにしてほしい (重要!)。

スクリプトを記述する前に、プレイヤーの動くスピードを決める「スピード」変数を作っておこう。 プレイヤースプライトのスクリプトは次のとおり。

@greenFlag が押されたとき::events hat
x座標を (0) 、y座標を (10) にする //  プレイヤーの初期位置。マップ上の好きな場所にする
[幽霊 v] の効果を (100) にする
[距離を調べる v] を送って待つ
ずっと
  もし <[右向き矢印 v] キーが押された> なら
    @turnRight (3) 度回す //  より回転が早くなるように調整。タイムラグが減少
    [距離を調べる v] を送って待つ

    もし <[左向き矢印 v] キーが押された> なら
      @turnLeft (3) 度回す //  より回転が早くなるように調整。タイムラグが減少
      [距離を調べる v] を送って待つ
    end
  end
  もし <[上向き矢印 v] キーが押された> なら
    動く (スピード) :: custom
    [距離を調べる v] を送って待つ

    もし <[下向き矢印 v] キーが押された> なら
      動く ((-1) * (スピード)) :: custom
      [距離を調べる v] を送って待つ
    end
  end
end
定義 動く (スピード)
(スピード) 歩動かす
もし <[マップ v] に触れた> なら
  ((-1) * (スピード)) 歩動かす
end //  壁を検知

これで、マップの中を歩きまわれるようになった。

距離を調べる

ここからが3Dレイキャスティングの開始である。

現実世界では、対象が遠くにあればあるほど、小さく見える。


ここでは、プレイヤーとその周りにある壁の距離を調べるセンサースクリプトをつくる。このスクリプトでは、1回「距離を調べる」メッセージが送られるたびに、異なる96個の角度で距離を調べる。

最初に「距離を調べる」スプライトのコスチュームとして、1 × 1 の点を描いておく。必ず、コスチュームが画面の中心になるようにすること。

次に、以下のスクリプトを追加する:

  @greenFlag が押されたとき::events hat
  [幽霊 v] の効果を (100) にする

  [距離を調べる v] を受け取ったとき //  プレイヤースプライトから送られる
  距離を調べる :: custom
  [描画 v] を送って待つ

  定義 距離を調べる
  . . . //  重要:このカスタムブロックでは、[画面を再描画せずに実行]をオンにすること!
  [距離リスト v]のすべてを削除する::list
  [距離 v] を [0] にする
  [角度のオフセット v] を [-48] にする
  (96) 回繰り返す
    [距離 v] を [0] にする
    [プレイヤー v] へ行く
    (([プレイヤー v]の[向き v]::sensing) + (角度のオフセット)) 度に向ける
    <<[マップ v] に触れた> または <(距離::variables) = [80]>> まで繰り返す
      (1) 歩動かす
      [距離 v] を (1) ずつ変える
    end
    (距離::variables) を [距離リスト v] に追加する //  壁を発見したので、距離をリストに記録しておく
    [角度のオフセット v] を (1) ずつ変える //  次に距離を測る角度をセット
  end

描画

いよいよ「距離を調べる」の結果を使って、3D画面を描画する。オブジェクトが遠くにあればあるほど、小さく見えることを思い出してほしい。

描画スプライトには次のスクリプトを追加する:

  [描画 v] を受け取ったとき
  描画する :: custom

  定義 描画する
  . . . //  重要:このカスタムブロックでは、[画面を再描画せずに実行]をオンにすること!
  x座標を (-237.5) 、y座標を (180) にする
  ペンの太さを (5) にする
  ペンの色を [#7AF] にする //  好きな色を指定する
  全部消す::pen //  一旦、画面をリセットして描画に備える
  ペンを上げる
  [壁ナンバー v] を (1) にする
  ([距離リスト v] の長さ :: list) 回繰り返す
    ペンの[明るさ v]を ((50) + (([距離リスト v]の(壁ナンバー)番目 :: list) * ((50) / (80)))) にする ::pen //  遠くにいくほど白くする。立体っぽさが出る。
    y座標を ((-1200) / ([距離リスト v]の(壁ナンバー)番目 :: list) ) にする //  -1200を距離で割って、距離が遠くなるほど壁の開始位置が上になるようにする
    ペンを下ろす
    y座標を ((1200) / ([距離リスト v]の(壁ナンバー)番目 :: list) ) にする //  1200を距離で割って、距離が遠くなるほど壁の終了位置が下になるようにする
    ペンを上げる
    x座標を (5) ずつ変える
    [壁ナンバー v] を (1) ずつ変える
  end

これで完成だ。ただし、残念ながらこのプロジェクトをScratchで実行すると、とても時間がかかる。

実行速度の最適化

実行速度を上げるにはいくつか方法がある:

  • 「person」スプライトの回転方法 を、「回転しない」にする
  • 「距離を調べる」スプライトで、距離をはかるときの動きを、1歩ずつではなく2歩ずつにする。
  • 壁の描画時に、96本も描画するのはやめて、描画の数を減らす(その場合、ペンの太さを変える必要がある)。「距離を調べる」スプライトが1歩ずつではなく2歩ずつ動く場合は、60程度がよいだろう。

スプライトベースのレイキャスティングの速度上のボトルネック (速度が遅くなる最大のポイント) は、「距離を調べる」スプライトが大量の処理を行うところにある。

リストベース レイキャスティング

リストベースのレイキャスティングでは、マップ情報を座標としてリスト (2次元配列) に保存して使用する。プレイヤーや光線などの座標情報も、すべて変数に入れる。すべての情報を数値として管理し、スプライトのコスチュームなどは一切使用しないので、ある意味、とても仮想的な方法といえる。

ペンで壁を描画するスプライトが1つあれば、それ以上のスプライトは不要である。リストベースのレイキャスティングサンプルとしては、こちらを参照してほしい。マップデータがほしいときは、こちらからダウンロードできる。

リストベースのレイキャスティングを使ったゲームの例。FPS (1秒あたりの描画枚数スピード) がScratchとしてはかなり速いことに注目してほしい。

このチュートリアルでは、、スクリプトが見やすいように処理をいくつかのカスタムブロックに分割しているが、これらのカスタムブロックでは必ず[画面を再描画せずに実行]をオンにしてほしい。

また、この記事の内容は、リストベースでレイキャスティングを行う最低限の方法を紹介するものである。個々のアルゴリズムについて、一からきちんと理解したい場合は、こちら のサイトなどを参考にしてほしい。

RunWithoutScreenRefreshPicForRaycaster.png

Warning
メモ:
リストベースのレイキャスティングは、スプライトベースのレイキャスティングを完全に理解してから、あるいは、レイキャスティングについての知識がすでに十分ある場合に試すと良いだろう。

変数を設定する

次に、変数とマップの初期設定を行うスクリプトを示す:

  定義 初期設定
  [X Position v] を [10] にする // プレイヤーのマップ上のX、Y位置
  [Y Position v] を [3] にする 
  [Direction X v] を [1] にする // プレイヤーの向きベクトルのX、Y要素
  [Direction Y v] を [0] にする 
  [Resolution v] を [4] にする // 解像度 (ここでは、壁を描画するペンの太さ)
  [Plane X v] を [0] にする // カメラ平面X、Yベクトル。このX-Y比を変えることで、画面の描画範囲 (描画する視野角度) が変わる。0.66はFPS向けの設定と言われている。
  [Plane Y v] を [0.66] にする
  [Height v] を [300] にする // 壁の高さの基準値
  [Move Speed v] を [0.1] にする // 前進・後退時の移動スピード
  [Rotation Speed v] を [4] にする // ターン時の回転スピード
  [World Map v]のすべてを削除する::list //マップの初期化
  [11111111111111111111] を [World Map v] に追加する
  [10000100000000000001] を [World Map v] に追加する
  [10001101111111010101] を [World Map v] に追加する
  [10000101000001010101] を [World Map v] に追加する
  [10000101000101010101] を [World Map v] に追加する
  [10000101000101000101] を [World Map v] に追加する
  [10000100000101000101] を [World Map v] に追加する
  [10111111111101010001] を [World Map v] に追加する
  [10000000000001010001] を [World Map v] に追加する
  [10000000111001010001] を [World Map v] に追加する
  [10000000001100000001] を [World Map v] に追加する
  [11111111111111111111] を [World Map v] に追加する

メインループ

次に、緑の旗クリックされたときにほかのスクリプトを実行するメインスクリプトを示す:

@greenFlag が押されたとき::events hat
初期設定 :: custom
ずっと // ペンサイズを1にして、ペンを上げておくと動作が多少早くなる。DadOfMrLogの発見。
  ペンを上げる
  ペンの太さを (1) にする
  レイキャスティング :: custom
  隠す
end

レイキャスティングスクリプト

次にレイキャスティング部分のスクリプトを示す。このブロックはその他のカスタムブロックの大半を制御している。必ず[画面を再描画せずに実行]をオンにしておいてほしい。

  定義 レイキャスティング
  全部消す
  [x v] を [-240] にする
  キー操作の制御 :: custom
  <(x) > [240]> まで繰り返す 
    ペンを上げる
    ペンの太さを (1) にする
    ペンの[明るさ v]を (0) にする::pen
    [Camera Direction v] を ((x) / (240)) にする // 画面の左右に対するカメラの相対位置 (-1 〜 1)。画面左端のとき-1、右端のとき1、0で中央
    [Ray X Position v] を (X Position::variables) にする // 光線の発射位置(プレイヤー位置)
    [Ray Y Position v] を (Y Position::variables) にする
    [Ray X Direction v] を ((Direction X) + ((Plane X) * (Camera Direction))) にする // 発射する光線の向きをベクトルで定義。プレイヤー向きを表すDirectionベクトルに対し、さらに、(Plane X) * (Camera Direction) でカメラの向きベクトルを適用する
    [Ray Y Direction v] を ((Direction Y) + ((Plane Y) * (Camera Direction))) にする
    [Map X v] を ([floor v] \( (Ray X Position) \)::operators) にする // カメラのマップグリッド上での位置
    [Map Y v] を ([floor v] \( (Ray Y Position) \)::operators) にする
    壁までの距離計算 :: custom
    壁の描画 :: custom
    [x v] を (Resolution) ずつ変える
  end

制御部分

レイキャスティングブロックに最初に登場するカスタムブロックは「キー操作の制御」である。このスクリプトの中身を見てみよう。

  定義 キー操作の制御
  もし <[上向き矢印 v] キーが押された> なら 
    [next X v] を ((X Position::variables) + ((Direction X) * (Move Speed))) にする
    [next Y v] を ((Y Position::variables) + ((Direction Y) * (Move Speed))) にする
    もし <(([World Map v]の((next X)の[切り捨て v]::operators)番目::list)の((next Y)の[切り捨て v]番目::operators)番目::operators) = [0]> なら 
      [X Position v] を ((Direction X) * (Move Speed)) ずつ変える
      [Y Position v] を ((Direction Y) * (Move Speed)) ずつ変える
    end
  end
  もし <[下向き矢印 v] キーが押された> なら 
    [next X v] を ((X Position::variables) + ((-1) * ((Direction X) * (Move Speed)))) にする
    [next Y v] を ((Y Position::variables) + ((-1) * ((Direction Y) * (Move Speed)))) にする
    もし <(([World Map v]の((next X)の[切り捨て v]::operators)番目::list)の((next Y)の[切り捨て v]番目::operators)番目::operators) = [0]>なら
      [X Position v] を ((-1) * ((Direction X) * (Move Speed))) ずつ変える
      [Y Position v] を ((-1) * ((Direction Y) * (Move Speed))) ずつ変える
    end
  end
  もし <[左向き矢印 v] キーが押された> なら 
    [temp v] を (Direction X) にする
    [Direction X v] を (((Direction X) * (((Rotation Speed) * (-1)) の[cos v]::operators)) - ((Direction Y) * ((Rotation Speed)の[sin v]::operators))) にする
    [Direction Y v] を (((temp) * (((Rotation Speed) * (-1))の[sin v]::operators)) + ((Direction Y) * (((Rotation Speed) * (-1))の[cos v]::operators))) にする
    [temp v] を (Plane X) にする
    [Plane X v] を (((Plane X) * (((Rotation Speed) * (-1)) の[cos v]::operators)) - ((Plane Y) * ((Rotation Speed)の[sin v]::operators))) にする
    [Plane Y v] を (((temp) * (((Rotation Speed) * (-1))の[sin v]::operators)) + ((Plane Y) * (((Rotation Speed) * (-1))の[cos v]::operators))) にする
  end
  もし <[右向き矢印 v] キーが押された> なら 
    [temp v] を (Direction X) にする
    [Direction X v] を (((Direction X) * (((Rotation Speed) * (1)) の[cos v]::operators)) - ((Direction Y) * ((Rotation Speed)の[sin v]::operators))) にする
    [Direction Y v] を (((temp) * (((Rotation Speed) * (1))の[sin v]::operators)) + ((Direction Y) * (((Rotation Speed) * (1))の[cos v]::operators))) にする
    [temp v] を (Plane X) にする
    [Plane X v] を (((Plane X) * (((Rotation Speed) * (1)) の[cos v]::operators)) - ((Plane Y) * ((Rotation Speed)の[sin v]::operators))) にする
    [Plane Y v] を (((temp) * (((Rotation Speed) * (1))の[sin v]::operators)) + ((Plane Y) * (((Rotation Speed) * (1))の[cos v]::operators))) にする
  end

壁の計算

この節では、壁との距離を計算するCalculate Wallsブロックについて説明する。

  定義 壁までの距離計算
  [Distance X Delta v] を ( ((1) + (((Ray Y Direction) * (Ray Y Direction)) / ((Ray X Direction) * (Ray X Direction)))) の[平方根 v]::operators) にする // マップグリッドをX方向に1進んだときの、光線の距離。Y方向も同様
  [Distance Y Delta v] を ( ((1) + (((Ray X Direction) * (Ray X Direction)) / ((Ray Y Direction) * (Ray Y Direction)))) の[平方根 v]::operators) にする
  [hit v] を [0] にする
  もし <(Ray X Direction) < [0]> なら // 光線が左右どちらに傾いているかによって処理を分ける
    [Step X v] を [-1] にする
    [Side X Distance v] を (((Ray X Position) - (Map X)) * (Distance X Delta)) にする
  でなければ 
    [Step X v] を [1] にする
    [Side X Distance v] を ((((Map X) + (1)) - (Ray X Position)) * (Distance X Delta)) にする //光線X方向ベクトルが左右どちら向きかによって、光線とマス目がぶつかる場所が1ずれるので、片側に1を足して調整している。Y方向も同様
  end
  もし <(Ray Y Direction) < [0]> なら 
    [Step Y v] を [-1] にする
    [Side Y Distance v] を (((Ray Y Position) - (Map Y)) * (Distance Y Delta)) にする
  でなければ 
    [Step Y v] を [1] にする
    [Side Y Distance v] を ((((Map Y) + (1)) - (Ray Y Position)) * (Distance Y Delta)) にする
  end
  <(hit) = [1]> まで繰り返す 
    もし <(Side X Distance) < (Side Y Distance)> なら // 次にぶつかるのが、縦向きのグリッド線(xが整数のときのグリッド)か、横向きのグリッド線(yが整数のときのグリッド)かを調べ、先にぶつかる場所まで光線を進める。
      [Side X Distance v] を (Distance X Delta) ずつ変える
      [Map X v] を (Step X) ずつ変える
      [side v] を [0] にする
    でなければ 
      [Side Y Distance v] を (Distance Y Delta) ずつ変える
      [Map Y v] を (Step Y) ずつ変える
      [side v] を [1] にする
    end
    もし <((Map Y) 番目\( ((Map X) 番目( [World Map v] ) :: list) \)の文字) = [1]> なら 
      [hit v] を [1] にする
    end
  end
  もし <(side) = [0]> なら 
    [Perpendicular Wall Distance v] を ((((Map X) - (Ray X Position)) + (((1) - (Step X)) / (2))) / (Ray X Direction)) にする // そのままの距離ではなく、カメラ平面に投影した距離を使用(そのままの距離を使うと、魚眼レンズのように壁が丸くなり酔ってしまう)
  でなければ // (1-Step X)/2部分 → 光線のX方向が左右どちら向きか、Y方向が上下どちら向きかによって、光線とマス目がぶつかる場所が1ずれるので、片側に1を足して調整している。
    [Perpendicular Wall Distance v] を ((((Map Y) - (Ray Y Position)) + (((1) - (Step Y)) / (2))) / (Ray Y Direction)) にする
  end
  [Line Height v] を ((Height) / (Perpendicular Wall Distance)) にする
  [DrawStart v] を ((0) - ((Line Height) / (2))) にする
  もし <(DrawStart) < [-180]> なら 
    [DrawStart v] を [-180] にする
  end
  [DrawEnd v] を ((Line Height) / (2)) にする
  もし <<(DrawEnd) > [180]> または <(DrawEnd) = [180]>> なら 
    [DrawEnd v] を [180] にする
  end

壁を描画する

最後に紹介するのは、壁を描画するペンスクリプトである。こちらは非常にシンプルだ。

  定義 壁の描画
  ペンの色を [#179fd7] にする
  もし <(side) = [1]> なら // Y方向の壁の色を少し暗くすると、より立体らしく見える
    ペンの[明るさ v]を (45) にする::pen
  でなければ 
    ペンの[明るさ v]を (50) にする::pen
  end
  x座標を (x) 、y座標を (DrawStart) にする
  ペンの太さを (Resolution) にする
  ペンを下ろす
  x座標を (x) 、y座標を ((2) * (DrawEnd)) にする
  ペンを上げる
  ペンの太さを (1) にする
  ペンの[明るさ v]を (0) にする::pen

以上で、リストベースのレイキャスティングに最低限必要なコードをすべて紹介した。自分でもいろいろと試してほしい。

外部リンク

Cookieは私達のサービスを提供するのに役立ちます。このサービスを使用することにより、お客様はCookieの使用に同意するものとします。