Unityでクリックした場所にマーカーを置く|hit.pointとhit.normalを実際に使ってみよう

広告

前回は、RaycastHit の中に入っている情報として、

hit.collider
hit.point
hit.distance
hit.normal

を確認しました。

特に、

hit.point

は、

Rayが実際に当たった場所

を表し、

hit.normal

は、

Rayが当たった面の向き

を表していました。

今回は、この2つを実際に使って、

マウスでクリックした場所にマーカーを置き、そのマーカーを面の向きに合わせる

という処理を作ってみましょう。

今回作るもの

たとえばCubeをクリックしたとします。

すると、

          ↑ hit.normal
          │
     ┌────●────┐
     │   Marker│
     │  Cube   │
     └─────────┘
          ↑
       hit.point

というように、

クリックした場所にマーカーを表示

します。

さらに、床だけではなく、

床
壁
斜面
Cubeの側面

などをクリックしても、

その面の向きに沿ってマーカーが配置される

ようにします。

クリックした場所にマーカーを置く流れ:Rayを作り、Raycastで当たり判定をし、hitの情報でマーカーを配置する

まずは位置だけ考える

最初は向きのことを考えず、

hit.point

だけ使ってみましょう。

前回のRaycast処理は、次のようになっていました。

Ray ray =
    Camera.main.ScreenPointToRay(
        Mouse.current.position.value
    );

RaycastHit hit;

if (Physics.Raycast(ray, out hit))
{
}

ここで、

hit.point

には、Rayが当たった位置が入っています。

したがって、この場所にGameObjectを生成すれば、

クリックした場所にオブジェクトを置く

ことができます。

マーカーPrefabを用意する

今回は、マーカーとして小さなオブジェクトをPrefabにしておきます。

たとえば、

Marker

というGameObjectを作ります。

初心者向けの確認実習なら、

小さなSphere
小さなCube
薄い円形オブジェクト

などでも構いません。

重要なのは、

クリックした位置が目で確認できること

です。

作成したMarkerをProjectウィンドウへドラッグしてPrefabにしておきましょう。

Prefabをスクリプトから参照する

スクリプトに、

[SerializeField]
GameObject markerPrefab;

を追加します。

using UnityEngine;
using UnityEngine.InputSystem;

public class HitMarker : MonoBehaviour
{
    [SerializeField]
    GameObject markerPrefab;

    void Update()
    {
    }
}

Inspectorで、

Marker Prefab

の欄へ、先ほど作ったPrefabを設定します。

hit.pointにマーカーを生成する

Raycastが成功したところで、

Instantiate()

を使います。

まずは向きを考えず、

Instantiate(
    markerPrefab,
    hit.point,
    Quaternion.identity
);

としてみましょう。

全体では、

using UnityEngine;
using UnityEngine.InputSystem;

public class HitMarker : MonoBehaviour
{
    [SerializeField]
    GameObject markerPrefab;

    void Update()
    {
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            Ray ray =
                Camera.main.ScreenPointToRay(
                    Mouse.current.position.value
                );

            RaycastHit hit;

            if (Physics.Raycast(ray, out hit))
            {
                Instantiate(
                    markerPrefab,
                    hit.point,
                    Quaternion.identity
                );
            }
        }
    }
}

となります。

クリックした場所に置けた

ここで重要なのが、

hit.point

です。

Instantiate(
    markerPrefab,
    hit.point,
    Quaternion.identity
);

なので、

Markerの位置
      =
Rayが当たった位置

となっています。

つまり、

マウスクリック
↓
Ray
↓
Colliderに当たる
↓
hit.point
↓
そこへMarkerを生成

という流れです。

これで hit.point の意味が、かなり実感しやすくなります。

しかし向きがおかしい

床だけをクリックするなら、これでも問題がないように見えるかもしれません。

しかし、壁をクリックするとどうでしょう。

たとえば薄いマーカーを使っている場合、

壁
│
│  ─ Marker
│

のように、マーカーの向きが壁と合わない可能性があります。

なぜなら、

Quaternion.identity

としているからです。

これは、

回転させない

という指定です。

クリックした面がどちらを向いているかは考慮していません。

そこで、

hit.normal

を使います。

hit.normalをもう一度確認

hit.normal は、

Rayが当たった面から外側へ垂直に伸びる方向

です。

床なら、

      ↑ normal
      │
────────────

壁なら、

│
│────→ normal
│

斜面なら、

       ↗ normal
      /
─────/

のようになります。

つまり、

hit.normal

を使えば、

その面がどちらを向いているか

が分かります。

Quaternion.LookRotation()を使う

方向から回転を作りたいとき、

Quaternion.LookRotation()

を使うことができます。

たとえば、

Quaternion rotation =
    Quaternion.LookRotation(hit.normal);

とします。

Quaternion.LookRotation() は、指定した forward 方向へローカルZ軸を向ける回転を作ります。

つまり、

Quaternion.LookRotation(hit.normal)

なら、

MarkerのZ方向
      ↓
hit.normal

となります。

位置と向きを両方指定する

これで、

位置 → hit.point

向き → hit.normal

という2つの情報がそろいました。

そこで、

Quaternion rotation =
    Quaternion.LookRotation(hit.normal);

Instantiate(
    markerPrefab,
    hit.point,
    rotation
);

とします。

これで、

クリックした位置へ、面の方向に合わせてMarkerを生成

できます。

完成コード

using UnityEngine;
using UnityEngine.InputSystem;

public class HitMarker : MonoBehaviour
{
    [SerializeField]
    GameObject markerPrefab;

    void Update()
    {
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            Ray ray =
                Camera.main.ScreenPointToRay(
                    Mouse.current.position.value
                );

            RaycastHit hit;

            if (Physics.Raycast(ray, out hit))
            {
                Quaternion rotation =
                    Quaternion.LookRotation(
                        hit.normal
                    );

                Instantiate(
                    markerPrefab,
                    hit.point,
                    rotation
                );
            }
        }
    }
}

処理の流れを分解すると、

① マウスクリック

② ScreenPointToRay()
   Rayを作る

③ Physics.Raycast()
   Colliderに当たったか調べる

④ hit.point
   当たった位置を取得

⑤ hit.normal
   当たった面の向きを取得

⑥ Quaternion.LookRotation()
   normalから回転を作る

⑦ Instantiate()
   Markerを生成

となります。

床・壁・斜面をクリックしたとき、それぞれの面の向きに合わせてマーカーが配置される実行結果

hit.pointとhit.normalの役割がはっきりする

今回のコードで、2つの役割は明確です。

hit.point

は、

どこに置くか

です。

一方、

hit.normal

は、

どちらを向けるか

です。

つまり、

hit.point
   ↓
Position

hit.normal
   ↓
Rotation

と考えることができます。

面から少し浮かせた方がよい場合もある

マーカーを、

hit.point

ぴったりに置くと、マーカーと元の面がほぼ同じ位置になる場合があります。

たとえば、薄い板や画像を壁に貼るようなMarkerの場合、

壁の表面
Markerの表面

が重なってしまうことがあります。

すると、描画上ちらついて見えることがあります。

これは一般に、

Z-fighting

と呼ばれる現象です。

normal方向へ少しずらす

そこで、

hit.point + hit.normal * 0.01f

のようにして、

面から少し外側へずらすことができます。

Vector3 position =
    hit.point + hit.normal * 0.01f;

これは、

hit.point
    ↓
●────────→ hit.normal
 ↑
当たった位置

0.01だけ外側へ移動

という意味です。

修正版

Vector3 position =
    hit.point + hit.normal * 0.01f;

Quaternion rotation =
    Quaternion.LookRotation(hit.normal);

Instantiate(
    markerPrefab,
    position,
    rotation
);

全体では、

using UnityEngine;
using UnityEngine.InputSystem;

public class HitMarker : MonoBehaviour
{
    [SerializeField]
    GameObject markerPrefab;

    void Update()
    {
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            Ray ray =
                Camera.main.ScreenPointToRay(
                    Mouse.current.position.value
                );

            RaycastHit hit;

            if (Physics.Raycast(ray, out hit))
            {
                Vector3 position =
                    hit.point
                    + hit.normal * 0.01f;

                Quaternion rotation =
                    Quaternion.LookRotation(
                        hit.normal
                    );

                Instantiate(
                    markerPrefab,
                    position,
                    rotation
                );
            }
        }
    }
}

となります。

なぜnormalを足すと外側へ動くのか

ここも大切なところです。

hit.normal は、

面の外側を向く方向

でした。

そこで、

hit.normal * 0.01f

とすると、

その方向へ0.01

という移動量になります。

さらに、

hit.point + hit.normal * 0.01f

とすることで、

当たった場所
+
面の外側へほんの少し

という位置を作っています。

これは Vector3 の学習にもつながります。

床をクリックした場合

水平な床なら、normalはおおよそ、

(0, 1, 0)

です。

したがって、

hit.normal * 0.01f

は、

(0, 0.01f, 0)

になります。

つまり、

床から少し上へ

Markerを移動します。

壁をクリックした場合

たとえば壁のnormalが、

(1, 0, 0)

なら、

hit.normal * 0.01f

は、

(0.01f, 0, 0)

です。

つまり、

壁から少し外側へ

Markerを移動します。

コードを変えなくても、normalによって方向が自動的に変わります。

ここが hit.normal の便利なところです。

Prefabの向きには注意

ここは実習で非常に重要です。

Quaternion.LookRotation(hit.normal)

では、Markerの

ローカルZ軸

を hit.normal の方向へ向けます。

したがって、Marker Prefabそのものが、

Z方向が表側

になるように作られていると分かりやすくなります。

Prefabの作り方によっては、

X軸を面に向けたい

Y軸を面に向けたい

ということもあります。

その場合は、追加の回転が必要です。

なぜマーカーが90度ずれることがある?

コードが間違っていなくても、

Markerが横を向く

Markerが90度ずれる

ということがあります。

これは、

Prefabのどのローカル軸を「表」として作ったか

が関係しています。

たとえば、

Quaternion.LookRotation(hit.normal)

はZ軸をnormalに合わせます。

しかし、Prefab側が、

Y軸方向を表

として作られていれば、期待した向きにはなりません。

この場合、

コードだけでなくPrefabのローカル座標軸を見る

ことが大切です。

これはUnityを学ぶうえで重要な考え方です。

この実習で何が分かった?

これまで RaycastHit を、

Raycastの結果が入っている

と説明してきました。

今回実際に使ってみると、

hit.point

は、

オブジェクトを配置できる具体的な位置

であり、

hit.normal

は、

オブジェクトの向きを決められる具体的な方向

だということが分かります。

つまり、

Raycast
↓
RaycastHit
↓
3D空間の情報を取得
↓
その情報を使って別のGameObjectを配置

という流れです。

ここまでの記事をつなげてみる

これまでの流れを整理すると、

Camera.main
↓
メインカメラを取得

ScreenPointToRay()
↓
マウス位置からRayを作る

Physics.Raycast()
↓
RayがColliderに当たったか調べる

RaycastHit
↓
当たった情報を取得

hit.point
↓
当たった位置

hit.normal
↓
当たった面の方向

Instantiate()
↓
その場所にMarkerを作る

となりました。

最初は、

Camera.main.ScreenPointToRay(...)

という1行から始まりました。

しかし少しずつ分解すると、

画面上のマウス操作と3D空間のGameObjectがどのようにつながっているのか

が見えてきます。

まとめ

今回の一番重要なポイントは、

hit.point = どこ?

hit.normal = どっち向き?

です。

位置は、

Vector3 position =
    hit.point;

で取得できます。

面の向きは、

hit.normal

で取得できます。

そのnormalから、

Quaternion rotation =
    Quaternion.LookRotation(
        hit.normal
    );

と回転を作れます。

そして、

Instantiate(
    markerPrefab,
    position,
    rotation
);

とすることで、

クリックした場所に、その面の向きに合わせたMarkerを配置

できます。

さらに、

hit.point
+ hit.normal * 0.01f

とすれば、

面から少し浮かせることもできます。

今回の実習によって、

hit.point
hit.normal

は単に覚えるためのプロパティではなく、

3D空間で「位置」と「向き」を扱うための実用的な情報

だということが分かります。

訪問数 1 回, 今日の訪問数 1回

広告

Physics,Unity

Posted by hidepon