PlayCanvasのレイキャストについて知ってみる

# 前座
レイキャストと聞いて、
その言葉を知っている人はおそらくUnityや3D関連のコンテンツを作ったことがある人だと思います。

そんな私はWebの平面しか作っていなかったので、その言葉の意味はわからなかったです。
じゃあ、レイキャストってなんだ？って人のためにどんなものかわかる範囲で話をばします


# レイキャストとは
多分、もともとはレイキャスティングというレンダリング方法のことだと思う。
> 視点と物体との位置を計算し、その値をつかってオブジェクトを描画する3Dグラフィックス全般を指す

とのことですが、レンダリング方法の話から掘り下げていくと以下のような感じになる。

> ある視点から発射された光線を追跡し、どれかの物体との衝突を検出。
・視点からスクリーンの各方向へ視線を追跡し、物体との交差を調べる。
→交差していれば、その物体は見えている。
・物体表面の点から光線を逆方向に追跡し、他の物体との交差を調べる。
→交差していれば、その点は影に入っている。

上記はレイキャスティングというレンダリングの話なのですが、
ゲーム分野でのレイキャストは一つ目が当てはまるかと。

> ある視点から発射された線が何か物体にぶつかったかを取得する

これが簡単にいうレイキャストです。

ちなみに、私個人的に解釈したレイキャストは以下です。

> 特定の地点から特定の方向へ直線の線を引き、その線上のどこかでぶつかるものがあるか検知するものです。

だいたい一緒かな


# PlayCanvasでレイキャスト処理
これをPlayCanvasでレイキャスト処理を行う話ですが、主に
「マウスがクリックされた時、カメラから見て、3Dオブジェクトとそのマウスカーソルが重なっているか」
を取得するような処理がほとんどなんじゃないかと思っています。

じゃあどうやって取得していくのか、実際にコードを書きつつ見ていこうと思います。

## サンプルその1
まずはサンプルがあるのか探してみます。

https://developer.playcanvas.com/ja/user-manual/physics/ray-casting/

<iframe src="https://playcanv.as/p/J02HZ0PC/"></iframe>

https://playcanv.as/p/J02HZ0PC/

Entityをクリックすることで、カメラがフォーカスし、`console.log`にクリックしたEntityの名前を表示する処理です。

```js
var Raycast = pc.createScript('raycast');

// initialize code called once per entity
Raycast.prototype.initialize = function() {
    if (!this.entity.camera) {
        console.error('This script must be applied to an entity with a camera component.');
        return;
    }

    // Add a mousedown event handler
    this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.mouseDown, this);

    // Add touch event only if touch is available
    if (this.app.touch) {
        this.app.touch.on(pc.EVENT_TOUCHSTART, this.touchStart, this);
    }
};

Raycast.prototype.mouseDown = function (e) {
    this.doRaycast(e.x, e.y);
};

Raycast.prototype.touchStart = function (e) {
    // Only perform the raycast if there is one finger on the screen
    if (e.touches.length === 1) {
        this.doRaycast(e.touches[0].x, e.touches[0].y);
    }
    e.event.preventDefault();
};

Raycast.prototype.doRaycast = function (screenX, screenY) {
    // The pc.Vec3 to raycast from (the position of the camera)
    var from = this.entity.getPosition();

    // The pc.Vec3 to raycast to (the click position projected onto the camera's far clip plane)
    var to = this.entity.camera.screenToWorld(screenX, screenY, this.entity.camera.farClip);

    // Raycast between the two points and return the closest hit result
    var result = this.app.systems.rigidbody.raycastFirst(from, to);

    // If there was a hit, store the entity
    if (result) {
        var hitEntity = result.entity;
        console.log('You selected ' + hitEntity.name);
    }    
};
```

ここの `doRayCast` 関数の中でレイキャスト処理が行われています。
`from`がEntityの座標
`to`がカメラ側から見たカーソルの座標
を指しています。
`this.entity.camera.screenToWorld()`という処理がカーソルなどの2次元座標を3次元座標に変化してくれるようです。

その`from`と`to`がぶつかるかどうか`result`で結果を出します。
`this.app.systems.rigidbody.raycastFirst(from, to)`で最初にぶつかったかboolean判別してくれます。

主にここの処理を使うことでレイキャスト処理はできます。


## サンプルその2
レイキャスト処理のやり方は色々ありそうです。

PlayCanvasのチュートリアルのサンプル集に「Information hotspots」というのがあります。

https://developer.playcanvas.com/ja/tutorials/information-hotspots/

当たり判定を作って、それをクリックすることでリクエストを返すことができます。

このプロジェクトのエディターから`hotspot.js`を見てみます。
表示非表示しているのは、カメラとEntityの3次元座標を減算し、その値とEntityのz軸ベクトルを内積演算した結果が0以下の場合は非表示、0以上は表示しているようです。
正直どう言う計算をしているのかわからないかもしれませんが、フローだけ説明すると…
`sub2()`でカメラとEntityの3次元座標を減算しているのは、カメラの方向を求めています。
こうして求めたカメラの方向をベクトルに変換するために`normalize()`を使って正規化します。
ベクトルの正規化については、ググると色々出てきますので3Dの勉強がてら調べるといいかもしれません。
そして正規化したカメラのベクトルとEntityのz軸ベクトルを`dot()`を使って内積を求めていきます。

表示非表示はこんなもので、クリックなどの当たり判定はどうなっているのか。
当たり判定は`BoundingSphere()`で作っています。
ここでは

```js
this.hitArea = new pc.BoundingSphere(this.entity.getPosition(), this.radius);
```

このhitAreaはのちにレイキャストのレイがぶつかったかを判定してくれます。
以下の`intersectsRay()`を使います。

```js
this.hitArea.intersectsRay(this.ray)
```

ここで出てきた`this.ray`はPlayCanvasで用意された光線です。
作り方は以下のような感じ。詳しく知りたい方は [リファレンスへ](https://developer.playcanvas.com/en/api/pc.Ray.html)

```js
this.ray = new pc.Ray();
```

このレイをまた以下のようにベクトルを正規化して求めていきます。
最終的に先ほども説明した`intersectsRay`を使って判定をとります。

```js
this.cameraEntity.camera.screenToWorld(screenPosition.x, screenPosition.y, this.cameraEntity.camera.farClip, this.ray.direction); 
this.ray.origin.copy(this.cameraEntity.getPosition());
this.ray.direction.sub(this.ray.origin).normalize();

if (this.hitArea.intersectsRay(this.ray)) {
    this.entity.fire("pulse:start");
}
```

これがレイキャストの処理になります。

その1で紹介したやり方よりも少しコードの量が増えて大変そうですね。

---

# 終わりに

私もレイキャストの言葉を知らない人間でしたが、言っていることはわかるのでニュアンスで実装なんかを行なっていました。
意外とこのレイキャストの処理ってゲームでもWebでも使う要素なので覚えておいて損はなさそうです。

PlayCanvasでの実装についても、意外と簡単に実装ができました。
`raycastFirst()`のように用意されているのは助かりますね。
raycastFirst() はカメラから見てクリックした直線上にある最初の Entity だけを参照しますが、その後ろにもEntityがありそれを参照したい場合には、 raycastAll() を使用します。
raycastAll()なら直線上の Entity を全て参照してくれます。

他にも当たり判定の作成なども簡単にできましたし、そこまで苦労せずにコンテンツのインターフェースを作れそうですね。

PlayCanvasは英語のドキュメントが豊富ですが、日本語はそこまで数がないので調べようと思ってもなかなか見つからないのが悩みの種ですね…
こういった技術記事が少しずつでも増えていけばいいなと思います。