チュートリアル - 3D空間を使った簡単webサイト 4/5

[前回](https://qiita.com/sutobu000/items/5c6b5e8bd8125d0ea985)

#3Dオブジェクトに応じたHTMLのページを表示させる
マウスからイベントが取得できたので、クリックしたらHTMLを表示するようにします。

予めAssetsにmarkupというディレクトリ内に使用されるhtmlとcssのサンプルがあります。
事前にサンプルを用意していますが、作る際には自分のローカルで作成する必要があるのでご注意ください。

今回は3つの3Dオブジェクトと3つのhtmlを関係を持たせて、
「3Dオブジェクトがクリックされたら、それにリンクするhtmlが表示される」 までを作ります。

ここで使用するscriptはaddhtml.jsですが、これもまたEntityにADD SCRIPTする必要があります。
使用するaddhtml.jsは特定のEntityにADD SCRIPTする必要はありません。
今回はcharaというグループのEntityにADD SCRIPTします。

![スクリーンショット 2019-07-10 20.13.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/278d6cea-7c75-b982-d6ac-630ed3f3fd31.png)



addhtml.jsをADD SCRIPTしたら、コードエディターを開きます。

以下のコードを追加します。

```js
Addhtml.attributes.add("css", {type: 'asset', assetType:'css', title: 'CSS Style'}); // アセットのcss読み込み
Addhtml.attributes.add("header", {type: 'asset', assetType:'html', title: 'HTML Header'}); // アセットのhtmlのheader読み込み
Addhtml.attributes.add("scene1", {type: 'asset', assetType:'html', title: 'HTML Scene1'}); // アセットのhtmlのscene1読み込み
Addhtml.attributes.add("scene2", {type: 'asset', assetType:'html', title: 'HTML Scene2'}); // アセットのhtmlのscene2読み込み
Addhtml.attributes.add("scene3", {type: 'asset', assetType:'html', title: 'HTML Scene3'}); // アセットのhtmlのscene3読み込み
```

早速、markupディレクトリのファイルを登録していきます。

下のアセットからmarkupというディレクトリに登録するファイルがあります。

![スクリーンショット 2019-07-10 20.16.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/1b4bfd99-bf41-6a74-bc75-55167c4074e0.png)

![スクリーンショット 2019-07-31 12.19.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/7a74594d-256b-4a0b-406c-1ad0c21b2400.png)



htmlとcssを登録していきます。

登録したコードを追加するために<head></head>と<body></body>のエレメントを取得する必要があります。
なぜ取得するのかというと、取得したheadやbodyに対して、appendChild()やinnerHTML、insertAdjacentHTML()などでhtmlを追加するからです。

以下のコードを追加していきます。

```js
var container;

Addhtml.prototype.initialize = function() { // init
    var htmlNameArr = []; // attributesのデータの名前を配列にします
    var arrIndex = Addhtml.attributes.index;
    for(var i = 0; i < Object.keys(arrIndex).length; i++){ // attributesのデータを取得してfor文で回す
        if(!Object.keys(arrIndex)[i].indexOf("scene")){ // 名前にhtmlが入っているデータをif文
            htmlNameArr.push(Object.keys(arrIndex)[i]); // htmlデータの名前だけ配列化する
        }
    }

    var head = document.getElementsByTagName("head")[0]; // headタグ取得
    var body = document.getElementsByTagName("body")[0]; // bodyタグ取得

    var wrapper = document.createElement("div"); // DOMを囲う要素を作成
    wrapper.className = "wrapper"; // クラス名指定
    body.appendChild(wrapper); // bodyタグの最後に要素を追加

    container = document.createElement("main"); // DOMを囲う要素を作成
    container.className = "container"; // クラス名指定
    wrapper.appendChild(container); // bodyタグの最後に要素を追加

		var style = document.createElement("style"); // cssのstyleタグ

    wrapper.insertAdjacentHTML("afterbegin", this.header.resource); // attrで追加したヘッダーを追加
    
    style.append(this.css._resources[0]);
    head.appendChild(style); // headの最後にstyleを追加

    for(var i = 0; i < htmlNameArr.length; i++){ // htmlのアセットを配列管理するためにfor文でattrの名前の配列を回す

        container.insertAdjacentHTML("beforeend", this[htmlNameArr[i]].resource); // アセットから取得したhtmlを追加

    }
};
```

これでhtmlとcssの情報を追加できました。
headの中にcssのスタイルが入り()、bodyの中にhtmlの要素が入っています。

![スクリーンショット 2019-06-20 18.05.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/9883c35f-e922-8958-341c-c93df191971b.png)

次にオブジェクトをクリックしたらそれに連動したhtmlを表示するやり方です。

attributesで登録したhtmlでscene1、scene2、scene3とありました。
この数字を3Dオブジェクトとの関係性に使います。

Editorから3DオブジェクトのModelを選択し、インスペクターからTagsを登録します。
tagの名前は任意のもので大丈夫です。数字をそれぞれに入れるのを忘れないように。

![スクリーンショット 2019-06-20 18.11.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/84961bf1-a6b6-9c0e-6e28-c114aa94ed04.png)

登録したtagをクリックした時に取得できるか確認します。
hotspot.jsのdoRayCastでtagを確認します。

```js
Hotspot.prototype.doRayCast = function (screenPosition) { // レイキャスト処理（ある地点から特定方向に直線で線を引いて、その線上で物体があるか検知する処理）
    if (this.hitArea.intersectsRay(this.ray)) { // ヒットエリアとレイが交差した場合
        console.log("click!! : ",this.entity.tags._list[0]);
    }
};
```

クリックした3Dオブジェクトから各々のtagが取得できたと思います。
これを使ってhtmlとリンクさせます。

addhtml.jsに以下を追加します。

```js
globalPc = {}; // グローバルな変数（オブジェクト）

Addhtml.prototype.update = function(dt) { // update
    if(Number(globalPc.scene) > 0) { // いずれかのsceneが選択されている場合
        if(!container.classList.contains("is-open")){ // 追加したアセットのhtmlにis-openのclassが追加されていない場合（どのhtmlも表示されていない）
            container.classList.add("is-open"); // is-openのclass名をcontainerに追加
            var sectionElements = document.getElementsByClassName("section"); // sectionを取得
            for(var i = 0; i < sectionElements.length; i++){ // sectionをループ処理
                if(sectionElements[i].classList.contains("is-current")){ // sectionにis-currentのclass名を持つかif処理
                    sectionElements[i].classList.remove("is-current"); // is-currentを削除
                }
            }
            sectionElements[globalPc.scene-1].classList.add("is-current"); // 選択されたsceneにis-currentのclass名を追加
            document.getElementsByClassName("section_close")[globalPc.scene-1].addEventListener("click", btnClose, false); // 選択されたsceneの閉じるボタンにイベントをセット
        }
    }

};

function btnClose(e){ // 閉じるボタンが押されたら発火
    e.preventDefault();
    globalPc.scene = 0; // 閉じるボタンなので選択されたsceneはnullにするので、0を代入
    container.classList.remove("is-open"); // containerのis-openのclass名を削除
    var sectionElements = document.getElementsByClassName("section"); // sectionを取得
    for(var i = 0; i < sectionElements.length; i++){ // sectionをループ処理
        if(sectionElements[i].classList.contains("is-current")){ // sectionにis-currentのclass名を持つかif処理
            sectionElements[i].classList.remove("is-current"); // is-currentを削除
        }
    }
}
```

リンクさせる方法として、global変数のglobalPcで今開いているページを管理できるようにします。

initializeには以下を追加します。

```js
globalPc.scene = 0; // どのシーンページを開いているか保管
```

先ほど確認したhotspot.jsのdoRayCastでもglobal変数に数字を与えます。
数字のみを代入するようにします。

```js
Hotspot.prototype.doRayCast = function (screenPosition) { // レイキャスト処理（ある地点から特定方向に直線で線を引いて、その線上で物体があるか検知する処理）
    if (this.hitArea.intersectsRay(this.ray)) { // ヒットエリアとレイが交差した場合
        globalPc.scene = this.entity.tags._list[0].replace(/[^0-9]/g, ''); // entityで設定したタグを取得し、scene1、scene2...の数字以外をreplaceで削除し数字のみ代入
    }
};
```

Launch画面でリロードして3Dオブジェクトをクリックすると、それぞれ該当したhtmlが表示されるようになります。

これで簡単なwebの大枠はできました。

[ hotspot.jsのコード ]

```js
var Hotspot = pc.createScript('hotspot');

// canvasのclickやhoverなどの処理を行う

Hotspot.attributes.add("cameraEntity", {type: "entity", title: "Camera Entity"}); // カメラのentityを取得
Hotspot.attributes.add("radius", {type: "number", title: "Radius"}); // entityのヒットエリアの範囲を指定

Hotspot.prototype.initialize = function() { // init
    this.hitArea = new pc.BoundingSphere(this.entity.getPosition(), this.radius); // ヒットエリアを作成。BoundingSphereがentityの境界エリアを作成(Photoshopでいうバウンディングボックス的な)
    this.ray = new pc.Ray(); // cameraからentityへ直進する線のデータを作成。（Rayは光線の意でstart pointからentityまでの距離を測ったりすることが可能）
    
    this.directionToCamera = new pc.Vec3(); // Vector座標の型を取得
    
    this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseHover, this); // マウスカーソルがホバーした時
    this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this); // クリックが押された時
};


Hotspot.prototype.doRayCast = function (screenPosition) { // レイキャスト処理（ある地点から特定方向に直線で線を引いて、その線上で物体があるか検知する処理）
    if (this.hitArea.intersectsRay(this.ray)) { // ヒットエリアとレイが交差した場合
        globalPc.scene = this.entity.tags._list[0].replace(/[^0-9]/g, ''); // entityで設定したタグを取得し、s1、s2...の数字以外をreplaceで削除し数字のみ代入
    }
};


Hotspot.prototype.onMouseHover = function(screenPosition) { // マウスホバー時
    this.cameraEntity.camera.screenToWorld(screenPosition.x, screenPosition.y, this.cameraEntity.camera.farClip, this.ray.direction); // ポジションを2Dスクリーンから3D空間へ変換
    this.ray.origin.copy(this.cameraEntity.getPosition()); // レイのオリジナルのポジションにカメラのポジションをコピー
    this.ray.direction.sub(this.ray.origin).normalize(); // 3次元ベクトルを他の場所から減算し、単位ベクトルに変換

    if (this.hitArea.intersectsRay(this.ray)) { // ヒットエリアとレイが交差した場合
        console.log("hover");
    }
};

Hotspot.prototype.onMouseDown = function(event) { // クリックが押されている時
    if (event.button == pc.MOUSEBUTTON_LEFT) { // 左クリックが押された時
        this.doRayCast(event); // レイキャストを呼ぶ
    }
};
```

[ addhtml.js のコード ]

```js
var Addhtml = pc.createScript('addhtml');

// htmlを追加などする処理を記入

Addhtml.attributes.add("css", {type: 'asset', assetType:'css', title: 'CSS Style'}); // アセットのcss読み込み
Addhtml.attributes.add("header", {type: 'asset', assetType:'html', title: 'HTML Header'}); // アセットのhtmlのheader読み込み
Addhtml.attributes.add("scene1", {type: 'asset', assetType:'html', title: 'HTML Scene1'}); // アセットのhtmlのscene1読み込み
Addhtml.attributes.add("scene2", {type: 'asset', assetType:'html', title: 'HTML Scene2'}); // アセットのhtmlのscene2読み込み
Addhtml.attributes.add("scene3", {type: 'asset', assetType:'html', title: 'HTML Scene3'}); // アセットのhtmlのscene3読み込み

globalPc = {}; // グローバルな変数（オブジェクト）
var container;

Addhtml.prototype.initialize = function() { // init
    globalPc.scene = 0; // どのシーンページを開いているか保管
    
    var htmlNameArr = []; // attributesのデータの名前を配列にします
    var arrIndex = Addhtml.attributes.index;
    for(var i = 0; i < Object.keys(arrIndex).length; i++){ // attributesのデータを取得してfor文で回す
        if(!Object.keys(arrIndex)[i].indexOf("scene")){ // 名前にhtmlが入っているデータをif文
            htmlNameArr.push(Object.keys(arrIndex)[i]); // htmlデータの名前だけ配列化する
        }
    }

    var head = document.getElementsByTagName("head")[0]; // headタグ取得
    var body = document.getElementsByTagName("body")[0]; // bodyタグ取得

    var wrapper = document.createElement("div"); // DOMを囲う要素を作成
    wrapper.className = "wrapper"; // クラス名指定
    body.appendChild(wrapper); // bodyタグの最後に要素を追加

    container = document.createElement("main"); // DOMを囲う要素を作成
    container.className = "container"; // クラス名指定
    wrapper.appendChild(container); // bodyタグの最後に要素を追加

	var style = document.createElement("style"); // cssのstyleタグ

    wrapper.insertAdjacentHTML("afterbegin", this.header.resource); // attrで追加したヘッダーを追加
    
    style.append(this.css._resources[0]);
    head.appendChild(style); // headの最後にstyleを追加

    for(var i = 0; i < htmlNameArr.length; i++){ // htmlのアセットを配列管理するためにfor文でattrの名前の配列を回す

        container.insertAdjacentHTML("beforeend", this[htmlNameArr[i]].resource); // アセットから取得したhtmlを追加

    }
};

Addhtml.prototype.update = function(dt) { // update
    if(Number(globalPc.scene) > 0) { // いずれかのsceneが選択されている場合
        if(!container.classList.contains("is-open")){ // 追加したアセットのhtmlにis-openのclassが追加されていない場合（どのhtmlも表示されていない）
            container.classList.add("is-open"); // is-openのclass名をcontainerに追加
            var sectionElements = document.getElementsByClassName("section"); // sectionを取得
            for(var i = 0; i < sectionElements.length; i++){ // sectionをループ処理
                if(sectionElements[i].classList.contains("is-current")){ // sectionにis-currentのclass名を持つかif処理
                    sectionElements[i].classList.remove("is-current"); // is-currentを削除
                }
            }
            sectionElements[globalPc.scene-1].classList.add("is-current"); // 選択されたsceneにis-currentのclass名を追加
            document.getElementsByClassName("section_close")[globalPc.scene-1].addEventListener("click", btnClose, false); // 選択されたsceneの閉じるボタンにイベントをセット
        }
    }

};

function btnClose(e){ // 閉じるボタンが押されたら発火
    e.preventDefault();
    globalPc.scene = 0; // 閉じるボタンなので選択されたsceneはnullにするので、0を代入
    container.classList.remove("is-open"); // containerのis-openのclass名を削除
    var sectionElements = document.getElementsByClassName("section"); // sectionを取得
    for(var i = 0; i < sectionElements.length; i++){ // sectionをループ処理
        if(sectionElements[i].classList.contains("is-current")){ // sectionにis-currentのclass名を持つかif処理
            sectionElements[i].classList.remove("is-current"); // is-currentを削除
        }
    }
}
```

次はここまで作成したSceneをPublishして公開します。



[次回](https://qiita.com/sutobu000/items/2406d003c146042052fc)