窓を壁の自由な位置に設置できるようにする

窓を壁の自由な位置に設置できるようにする
【執筆者紹介】
井上 翔一朗さん
ポジション:エンジニア部クライアントチーム
入社時期:2021年2月頃
業界歴:約3年

はじめに

メタバース事業本部エンジニア部クライアントチームの井上 翔一朗です。
ホロアースの開発において、主に施設や建築部分の実装を担当しています。

近年ではゲーム内にハウジング要素を持つゲームが多くなっており、そこには窓も欠かさず登場します。ですが窓は多くのゲームの場合、先に窓用の穴が開いた壁を置いてからそこに嵌めるなど決まった位置にしかつけられません。チーム内からも壁に対して自由な位置に窓を付けたい!という要望があり、工夫して解決したので紹介します。

まずは実装結果の動画からご覧ください。

動画のように一枚の壁の自由な位置に窓を設置できるようになりました!
窓を置いた部分の壁の見た目が消えることで外が見えるようになっています。

また、コライダーも見た目に合わせて穴が開けることが可能です。そうすることで将来的に、設置した窓越しに弓矢を用いた戦闘などを行えるようになります。
どのように実現したのか解説していきます。

解決策の概要

前提

窓の設置をする前に壁を設置する必要があります。窓は壁と重なるようにしか設置できないようになっていて、回転も壁に対して正しい向きに自動で調整されます。この部分の説明は省きます。

窓は以下の情報をPrefabなどに設定して持っておく必要があります。
コライダー(どの壁に設置されているかを衝突で検知するため)
窓の幅 / 高さ

 

処理全体の流れ

窓はコライダーを持っており、自分が設置されている壁を取得します。
壁に衝突するとその壁に位置とサイズ情報を渡しつつ穴をあけるように依頼します
依頼を受けた壁は、シェーダーの値変更とコライダー管理スクリプトを実行し穴をあけます
また、窓が破壊された場合は壁に穴を元に戻すよう依頼します。

見た目に穴をあけるシェーダーと、コライダーに穴をあけるスクリプトについてはこの後それぞれ解説していきます。

今回の実装の範囲では以下の限界があります。

  • 窓の淵が壁からはみ出すことには対応していません。そのため、窓の4つの角すべてが1枚の壁の上に載っていることをRayCastで確かめています。
  • 一つの壁に複数の窓を開けることには対応していません。二つ目の窓を付けた際は古い窓を自動的に破壊する仕組みになっています。

シェーダーにより見た目に穴をあける

基本方針は、今描画している座標が窓に重なる座標なら描画しない。というものになります。

モデルの側面が何もない空間になりメッシュの裏側が見えてしまいますが、そこは窓側が持っている窓枠の3Dモデルで隠されて見えなくなるので問題ありません。
(左:シェーダーで窓と重なる壁を削除した状態。右:そこに窓枠を置いた状態)

描画点の座標が取得できるようにSurfaceシェーダーで作ります。
窓のスクリプトから窓の中心点の座標と、窓の大きさ(幅・高さ)を受け取っています。

判定方法

以下の二段階に分けて考えます。

①水平平面について描画座標が窓の中にいるかどうか
②垂直方向(高さ)について描画座標が窓の中にいるかどうか

①水平平面xz平面についての直線の式 z=kx+b を用いて考えます。
真上から見たときの窓に垂直な線の傾きを k とします。そして、窓の手前と奥の縁を通る直線をそれぞれ考えると b が二つ存在します。ここではそれを b1, b2 とします。
z=kx+b1 と z=kx+b2 の二本の直線で囲まれた黄色い領域の中を描画しなければよいことになります。

つまり、今描画しようとしている座標を(x1, y1, z1)とすると
k*x1+b1 < z1 < k*x1+b2
であれば水平方向においては窓の中であると言えます。

※k は窓の y軸回転量 = rotY から求めることができます。
※タンジェントの0は未定義ですので、tan関数の引数に0を指定しないように分岐処理を設ける必要があります。
k = tan( rotY/180f * π + 0.5π )

※b1, b2は窓の両端の座標から求めることができます。
b = z / kx

②垂直方向次にY軸について考えます。本ゲームでは窓はY軸でのみ回転するので式はシンプルで済みます。
_windowBottomY < position.y < _windowTopY
であれば垂直方向において窓の中であることが分かります。

※_windowBottomYと_windowTopYは窓の中心点Y座標に窓の幅を±すれば求めることができます。

①②を踏まえるとコードはこのようになります。

bool isInsideWindow(Vector3 pos) // pos:今描画しようとしている点のワールド座標
{
  // xz平面で窓の中
  var isInsideXZ = _k * pos.x + _b1 < pos.z && pos.z < _k * pos.x + _b2;

  // 高さが窓の中
  var isInsideY = _windowBottomY < pos.y && pos.y < _windowTopY;

  return isInsideXZ && isInsideY;
}

※実際のシェーダーは同チームの回凱によって製作されました。こちらでは分かりやすく改変して掲載しています。
※処理負荷軽減のためにも、_k, _b1, _b2, _windowTopY,_windowBottomY は窓を設置した時点で計算しておきます。

今後の課題

現状では長方形以外の形には対応していません。

他の形にも対応させたい場合は、その形の領域を計算する式を書く必要があります。例えば円形の窓であれば、xy平面に描画座標を投影した後で、描画点と中心点の距離が半径より小さいことを確かめれば実現できます。

コライダーに穴をあける

次は、見た目ではなく当たり判定に穴をあけていきます。

壁のコライダーは一つのBoxコライダーでできています。元のコライダーを窓部分をよけるように分割した4つのBoxコライダーに置き換えることで、穴が開いていることを表現します。

方法はシンプルで、最初のコライダーを削除した後で新しく四つのBoxコライダーをアタッチしています。そのためには、それぞれのコライダーの中心座標とサイズを計算で求める必要があります。

以下の変数が登場します。
壁の大きさ Vector3 wallSize
壁に対する窓の相対座標 Vector3 windowCenter
窓の一片の長さ Vector2 windowSize

計算式は以下になります
※コライダーはローカル座標系なので回転の考慮は不要です
※壁の原点は底面の中央、窓の原点は縦横の中央にあります

▼上側のコライダー

var sizeX = wallSize.x;
var sizeY = wallSize.y – (windowCenter.y + windowSize.y);
var colSize = new Vector3(sizeX, sizeY, windowSize.z);

var posX = 0f;
var posY = wallSize.y – sizeY/2f;
var colCenter = new Vector3(posX , posY, windowSize.z/2f);

▼下側のコライダー

var sizeX = wallSize.x;
var sizeY = windowCenter.y – windowSize.y/2f;
var colSize = new Vector3(sizeX, sizeY, windowSize.z);

var posX = 0f;
var posY = sizeY /2f;
var colCenter = new Vector3(posX , posY, windowSize.z/2f);

▼左側のコライダー
(x軸は壁の中心を0とするため、壁の左端の座標は – wallSize.x /2f となります)

var sizeX = – wallSize.x/2f + windowCenter.x – windowSize.x/2f;
var sizeY = windowSize.y;
var colSize = new Vector3(sizeX, sizeY, windowSize.z);

var posX = -windowSize.x/2f + sizeX/2f;
var posY = windowCenter.y;
var colCenter = new Vector3(posX , posY, windowSize.z/2f);

▼右側のコライダー

var sizeX = wallSize.x/2f – (wallCenter.x + wallSize.x/2f);
var sizeY = windowSize.y;
var colSize = new Vector3(sizeX, sizeY, windowSize.z);

var posX = wallSize.x/2f – sizeX/2f;
var posY = windowCenter.y;
var colCenter = new Vector3(posX , posY, windowSize.z/2f);

これらの値を設定することで、窓の部分に被らないようにコライダーを配置します。

今後の課題

今回紹介した方法では四角形以外の穴を表現することは難しいです。
現状では四角形以外の形の窓は登場しませんが、以下のような対処法も考えられます。

  • 積分のようにBoxコライダーの数を増やして並べることで疑似的に円形や三角形なども表現できるかもしれません。欠点としては、精度を上げようとするとBoxコライダーの数がかなり増えてしまいます。
  • あらかじめ三角や円形の穴が開いたコライダーを用意しておくのもいいかもしれません。四角形に穴をあけた後にその中に用意しておいたコライダーを配置します。特殊な形状のコライダの用意が手間ですが、コライダー数を少なくできる可能性があります

終わりに

シェーダーとコライダー管理の組み合わせによって、窓を壁の自由な位置に配置できる体験を実現することができました!今後も魅力的なハウジング機能を作っていきたいと思います!

興味を持って頂けましたら、ぜひホロアースを遊んでみて下さい!!