Help us understand the problem. What is going on with this article?

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

前座

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

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

レイキャストとは

多分、もともとはレイキャスティングというレンダリング方法のことだと思う。

視点と物体との位置を計算し、その値をつかってオブジェクトを描画する3Dグラフィックス全般を指す

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

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

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

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

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

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

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

だいたい一緒かな

PlayCanvasでレイキャスト処理

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

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

サンプルその1

まずはサンプルがあるのか探してみます。
だいたい、「PlayCanvas レイキャスト」とかググると思いますが、多分これっていうのが出てきません。
「PlayCanvas Raycast」だと一例が出てきます。
https://developer.playcanvas.com/en/user-manual/physics/ray-casting/

Entityをクリックすることでconsole.logにクリックしたEntityの名前を表示する処理です。

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次元座標に変化してくれるようです。

そのfromtoがぶつかるかどうか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()で作っています。
ここでは

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

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

this.hitArea.intersectsRay(this.ray)

ここで出てきたthis.rayはPlayCanvasで用意された光線です。
作り方は以下のような感じ。詳しく知りたい方は リファレンスへ

this.ray = new pc.Ray();

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

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()のように用意されているのは助かりますね。
他にも当たり判定の作成なども簡単にできましたし、そこまで苦労せずにコンテンツのインターフェースを作れそう。

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

playcanvas
"PlayCanvasは、ブラウザ向けに作られたWebGL/HTML5ゲームエンジンです。PlayCanvas運営事務局は日本国内でのPlayCanvasの普及を目的に活動しています"
https://playcanvas.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away