PlayCanvasのレイキャストをもっと見てみる

前回、[PlayCanvasのレイキャストの設定について知ってみる](https://qiita.com/sutobu000/items/3b105ca85f4152d59cb2)、でレイキャストについて話しました。

他のUnityやThree.jsでも通じるようなレイキャストの設定方法だったんじゃないかなと思います。

今回はPlayCanvasの事例の一つで[BMW i8のコンフィギュレーター](https://playcanv.as/p/RqJJ9oU9/)があるのですが、
ここのドアなどで使用されているレイキャストの四角形…
![スクリーンショット 2019-12-05 11.48.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/d7a636dc-5533-f17d-7508-c18622e26d61.png)


**これ、DOMじゃん**

前回紹介したレイキャストの例ではElementComponentとEntityを配置してその座標を参照してレイキャスト処理を行なっていました。
３Dオブジェクトの3次元座標とカーソルの2次元座標を演算してレイキャスト処理を行い実装していました。
そこでは、`screenToWorld()`を使っていました。


今回は、「3次元座標を参照して2次元座標に反映する」、的なことを行います。

試しに前回のレイキャストの記事で説明したものと今回のDOMを追従させる方法をデモってみました。
![スクリーンショット 2019-12-05 12.26.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/380dd0bd-ca0c-a4d7-29cf-7ec0d38431ed.png)


- 緑がDOM
- 黄がEntity
- 赤がElementComponent

になっていて、それぞれクリックでリアクションを返すようにしています。
https://playcanv.as/p/SYcc55E4/

基本的にやることはElementComponentと同じ方法を使っています。
このElementComponentをDOMに置き換えた感じですね。

コードは以下の感じですね。

```js
var DomHotspot = pc.createScript('domHotspot');

DomHotspot.attributes.add("cameraEntity", {type: "entity", title: "Camera Entity"});

DomHotspot.prototype.initialize = function() { // init
    this.directionToCamera = new pc.Vec3();
    this.defaultForwardDirection = this.entity.forward.clone();
    
    this.btn = document.createElement("div");
    document.body.appendChild(this.btn);
    this.btn.style.position = "fixed";
    this.btn.style.width = "30px";
    this.btn.style.height = "30px";
    this.btn.style.background = "#ffffff";
    this.btn.style.transition = "opacity .5s";
    this.btn.style.zIndex = 10;
    this.btn.addEventListener("mousedown",function(){ this.style.background = "#333333"; });
    this.btn.addEventListener("mouseup",function(){ this.style.background = "#ffffff"; });
};

DomHotspot.prototype.update = function(dt) { // update
    var worldPos = this.entity.getPosition();
    var screenPos = new pc.Vec3();
    
    this.cameraEntity.camera.worldToScreen(worldPos, screenPos);
    
    this.directionToCamera.sub2(this.cameraEntity.getPosition(), this.entity.getPosition());
    this.directionToCamera.normalize();
    var dot = this.directionToCamera.dot(this.defaultForwardDirection);

    if (dot < 0) {
        this.btn.style.opacity = 0;
    } else {
        this.btn.style.opacity = 1;
    }
    
    this.btn.style.transform = "translate(" + screenPos.x + "px," + screenPos.y + "px)";
};
```

initializeをみてわかりますが、DOMの設定は即興ものなので悪しからず…

sub2とかnormalizeとかdotとかで表示非表示を切り替えていますがその説明は[前回の記事](https://qiita.com/sutobu000/items/3b105ca85f4152d59cb2)から引用しますね。
> 表示非表示しているのは、カメラとEntityの3次元座標を減算し、その値とEntityのz軸ベクトルを内積演算した結果が0以下の場合は非表示、0以上は表示しているようです。
正直どう言う計算をしているのかわからないかもしれませんが、フローだけ説明すると…
sub2()でカメラとEntityの3次元座標を減算しているのは、カメラの方向を求めています。
こうして求めたカメラの方向をベクトルに変換するためにnormalize()を使って正規化します。
ベクトルの正規化については、ググると色々出てきますので3Dの勉強がてら調べるといいかもしれません。
そして正規化したカメラのベクトルとEntityのz軸ベクトルをdot()を使って内積を求めていきます。

このDOMのポジションを指定する上で一番重要なのが以下です。

```js
var worldPos = this.entity.getPosition();
var screenPos = new pc.Vec3();

this.cameraEntity.camera.worldToScreen(worldPos, screenPos);
```

この`worldToScreen()`、他で使用されている`screenToWorld()`の逆のことをやっています。
今までは2次元座標を3次元座標に変換していましたが、今回は3次元座標を2次元座標に変換します。

変換した値を何か処理することもできるのですが、BMW i8のプロジェクトのスクリプトを見てみると…
なんとこの値をそのまま`transfrom: translate()`に入れるだけで良いみたいです。

```js
this.btn.style.transform = "translate(" + screenPos.x + "px," + screenPos.y + "px)";
```

これだけでいいなんて……
なんて楽なんだ…


このscriptを空っぽのEntityなんかに当てて使います。
![スクリーンショット 2019-12-05 12.18.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/0b5dcae7-b872-f3e4-1df6-092a8a86b472.png)

なんとjsの行数は40未満で出来ていることに驚きました。
かつ、エディターからどこにDOMを配置させるのか視覚的に見れるのも良いですね。

[前回](https://qiita.com/sutobu000/items/3b105ca85f4152d59cb2)から通してレイキャストについて調べてみました。
なかなか難しく感じてしまっていましたが、こう実装してみる意外と簡単に出来てしまうんだなって思いました。

これで、Web３Dコンテンツをたくさん作れるようになれるかなー