本チュートリアルの動画をYouTubeから試聴しながらの学習をお奨めしております。
目次
- 概要
- Chapter01 - プロジェクトをフォークする
- Chapter02 - カメラが移動する処理を作る
- Chapter03 - 3Dオブジェクトでマウスのイベントを起こす
- Chapter04 - 3Dオブジェクトに応じたHTMLのページを表示させる
概要
ゲームエンジンでwebサイトを作れる?
PlayCanvasはWebGL/HTML5ゲームエンジンでありますが、ゲームだけでなくWebページを作成などの展開もできます。
このチュートリアルでは、PlayCanvasを使って3D空間のコンテンツを作成し、その上にhtmlの要素を配置して、LPページ(ランディングページ)やキャンペーンサイトのようなwebサイトを作成していきたいと思います。
できるページは以下のようなページができます。
https://playcanv.as/p/BiGmjSlN/
Project: https://playcanvas.com/project/854144/
3Dオブジェクトをクリックするとそれに応じたhtmlが表示されるサイト。
ドラッグするとカメラの位置も変わり、3Dオブジェクトを好きな位置に配置して探す、といったこともできます。
今回は上記のものを作成するべく、チュートリアルを始めましょう。
PlayCanvasアカウントをまだ作成していない場合は、3分でアカウント作成から3Dモデルビュワーまで体験ができるチュートリアルがございますので、まずはこちらから参照ください。
→ https://support.playcanvas.jp/hc/ja/articles/4404199480089
Chapter01 - プロジェクトをフォークする
チュートリアル用に準備したプロジェクトがあるので、こちらをフォークしてチュートリアルを始めましょう。
以下のURLからプロジェクトをフォークします。
https://playcanvas.com/project/854139/
プロジェクトページのForkボタンをクリックし、プロジェクトをフォークしましょう。
プロジェクト名は任意で設定します。
読み込みが完了するとフォーク先のプロジェクトのページに飛びます。
そしたら、プロジェクトのEDITORボタンからエディター画面に飛びます。
エディター画面の右上の矢印のボタンをクリックすると、Launch(実行)されます。
現状のシーンを実行し、実行画面を確認しながら開発することができます。
エディター画面とライブリンクが持続した状態となり、エディター画面でPositionやScaleの数値などを変更するとリアルタイムで実行画面にプレビューされます。
Chapter02 - カメラが移動する処理を作る
マウスドラッグで3D空間をカメラを移動させます。
事前に用意している scripts ディレクトリ内の cameraMove.js を開きます。
マウスイベントを取得
以下のコードを cameraMove.js に書き換えます。
var CameraMove = pc.createScript('cameraMove');
// カメラ移動などの処理
CameraMove.prototype.initialize = function() {
this.f_click = false;
this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this); // クリックが押された時
this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this); // クリックを押している状態から離れる時
this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this); // マウスカーソルが動いている時
};
CameraMove.prototype.update = function(dt) {};
CameraMove.prototype.onMouseDown = function (event) { // クリックダウンしたら
if (event.button === pc.MOUSEBUTTON_LEFT) { this.f_click = true; } // 左クリックのとき
if (event.button === pc.MOUSEBUTTON_MIDDLE) {} // 中クリック
if (event.button === pc.MOUSEBUTTON_RIGHT) {} // 右クリック
};
CameraMove.prototype.onMouseUp = function (event) { // クリックアップしたら
if (event.button === pc.MOUSEBUTTON_LEFT) { this.f_click = false; } // 左クリック
if (event.button === pc.MOUSEBUTTON_MIDDLE) {} // 中クリック
if (event.button === pc.MOUSEBUTTON_RIGHT) {} // 右クリック
};
CameraMove.prototype.onMouseMove = function (event) { // マウスカーソルが動いたら
if(this.f_click){
console.log(event);
}
};
initialize 内で実行したいイベント処理を追記し、onMouseDown、onMouseUp、onMouseMoveという関数をそれぞれ実行しています。
pc.MOUSEBUTTON_LEFT
で、マウスの左ボタンのイベントを取得し判別することができます。
CameraMove.prototype.onMouseDown = function (event) { // クリックダウンしたら
if (event.button === pc.MOUSEBUTTON_LEFT) {} // 左クリックのとき
if (event.button === pc.MOUSEBUTTON_MIDDLE) {} // 中クリック
if (event.button === pc.MOUSEBUTTON_RIGHT) {} // 右クリック
};
CameraMove.prototype.onMouseUp = function (event) { // クリックアップしたら
if (event.button === pc.MOUSEBUTTON_LEFT) {} // 左クリック
if (event.button === pc.MOUSEBUTTON_MIDDLE) {} // 中クリック
if (event.button === pc.MOUSEBUTTON_RIGHT) {} // 右クリック
};
CameraMove.prototype.onMouseMove = function (event) { // マウスカーソルが動いたら
console.log(event);
};
追加した関数をイベントで取得できるように、initialize に以下を記載しています。
this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this); // クリックが押された時
this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this); // クリックを押している状態から離れる時
this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this); // マウスカーソルが動いている時
クリックしている時にドラッグしたら実行するように、 initialize
にクリックしているか判定するフラッグを用意しています。
this.f_click = false;
これに合わせて、 onMouseDown と onMouseUp にも適当なフラッグを追加しています。
CameraMove.prototype.onMouseDown = function (event) { // クリックダウンしたら
if (event.button === pc.MOUSEBUTTON_LEFT) { this.f_click = true; } // 左クリックのとき
~~~
CameraMove.prototype.onMouseUp = function (event) { // クリックアップしたら
if (event.button === pc.MOUSEBUTTON_LEFT) { this.f_click = false; } // 左クリック
これでクリックしドラッグした時だけ console.log() が呼ばれるようになります。
スクリプトを用意できましたが、このままではこのスクリプトは呼び出されません。対象となるEntityに登録する必要があります。
ヒエラルキーから Camera を選択し、インスペクターの ADD COMPONENT から Script を選択。
SCRIPTS の設定の ADD SCRIPT から cameraMove を選択します。
cameraMove が設定されたらOKです。
スクリプト名の横にある Edit をクリックするとコードエディターを開きます。
その横の Parse をクリックすると設定した script を再読み込みします。
実行画面をリロードし、Console を確認すると onMouseMove の console.log がマウスを左クリックでドラッグするたびに呼び出されているのが確認できます。
このイベントの処理を使って、カメラを移動させてみましょう。
以下のコードを cameraMove.js に書き換えます。
var CameraMove = pc.createScript('cameraMove');
// カメラ移動などの処理
CameraMove.prototype.initialize = function() {
this.f_click = false;
this.cameraEntity = this.entity; // このjsを受けているEntityを取得(カメラであることを前提に)
this.pos = this.cameraEntity.getPosition(); // カメラの座標を取得
this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this); // クリックが押された時
this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this); // クリックを押している状態から離れる時
this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this); // マウスカーソルが動いている時
};
CameraMove.prototype.update = function(dt) {};
CameraMove.prototype.onMouseDown = function (event) { // クリックダウンしたら
if (event.button === pc.MOUSEBUTTON_LEFT) { this.f_click = true; } // 左クリックのとき
if (event.button === pc.MOUSEBUTTON_MIDDLE) {} // 中クリック
if (event.button === pc.MOUSEBUTTON_RIGHT) {} // 右クリック
};
CameraMove.prototype.onMouseUp = function (event) { // クリックアップしたら
if (event.button === pc.MOUSEBUTTON_LEFT) { this.f_click = false; } // 左クリック
if (event.button === pc.MOUSEBUTTON_MIDDLE) {} // 中クリック
if (event.button === pc.MOUSEBUTTON_RIGHT) {} // 右クリック
};
CameraMove.prototype.onMouseMove = function (event) { // マウスカーソルが動いたら
if(!this.f_click) return; // クリックが押されている時
var posDX = event.dx*0.005; // deltaX
var posDY = event.dy*0.005; // deltaY
var posX = this.pos.x - posDX; // ドラッグ量からカメラの座標を計算
var posZ = this.pos.z - posDY; // ドラッグ量からカメラの座標を計算
this.cameraEntity.setPosition(posX,this.pos.y,posZ); // カメラの座標をセット
};
initialize でカメラの Entity の情報を取得しています。ここではカメラの Position を取得します。
https://developer.playcanvas.com/en/api/pc.Entity.html
this.cameraEntity = this.entity; // このjsを受けているEntityを取得(カメラであることを前提に)
this.pos = this.cameraEntity.getPosition(); // カメラの座標を取得
onMouseMove の中身を以下に差し替えています。
if(!this.f_click) return; // クリックが押されている時
var posDX = event.dx*0.005; // x座標の変化量
var posDY = event.dy*0.005; // y座標の変化量
var posX = this.pos.x - posDX; // ドラッグ量からカメラの座標を計算
var posZ = this.pos.z - posDY; // ドラッグ量からカメラの座標を計算
this.cameraEntity.setPosition(posX,this.pos.y,posZ); // カメラの座標をセット
クリックされていない場合は return するように変更します。
event からベクトル量を取得し、その値を使ってカメラのポジションを再設定します。
var posDX = event.dx*0.005; // x座標の変化量
var posDY = event.dy*0.005; // y座標の変化量
ベクトル量は event から取得するままだと、変化量が大きすぎてしまうので、変化量を調整しています。Launchで確認して好みの移動量に変更してみてください。
Launchしてカメラが移動するか確認してみましょう。
これでカメラの移動ができるようになりました。
次は3Dオブジェクトをクリックしてイベントを取得する処理です。
Chapter03 - 3Dオブジェクトでマウスのイベントを起こす
3Dオブジェクトとマウスカーソルとの関係性を作ります。
ヒエラルキーから chara->Model を選択。
ADD COMPONENT から Script を選択し、 hotspot.js を登録します。
この時、Modelを一つずつ選択する動作は面倒ですが、複数選択して設定を行うこともできます。
複数選択は Shift や Control(Command)キーを押しながら左クリックすることで複数選択が可能です。
hotspot.js をコードエディターで開きます。
配置された3Dオブジェクトとマウスカーソルとの関係を持たせるにはレイキャストを使用します。
レイキャスト(Ray Cast)とは、特定の地点から特定の方向へ直線の線を引き、その線上のどこかでぶつかるものがあるか検知するものです。
ここでのレイキャストは、「マウスがクリックされた時、カメラから見て、3Dオブジェクトとマウスカーソルが重なっているか」を取得するような処理を作ります。
ここから以下の処理を追加していきます
- 3Dオブジェクトにレイキャストの線が当たるヒットエリアを作成
- 使用するカメラのEntityとヒットエリアの大きさをスクリプト属性(Attributes)でEditorから設定
- ヒットエリアにマウスオーバーしてイベントが取れる
以下のコードを hotspot.js に書き換えます。
var Hotspot = pc.createScript('hotspot');
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の境界エリアを作成
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); // マウスカーソルがオーバーした時
};
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");
}
};
attributes はスクリプト属性を設定ができ、PlayCanvasのエディター画面からデータを変更することができるようになります。
ここではカメラのEntityの選択と、対象の3Dオブジェクトのヒットエリアの大きさを指定できます。
Hotspot.attributes.add("cameraEntity", {type: "entity", title: "Camera Entity"}); // カメラのEntityを取得
Hotspot.attributes.add("radius", {type: "number", title: "Radius"}); // Entityのヒットエリアの範囲を指定
Editの横にあるParseをクリックすることでスクリプトが更新され、スクリプト属性がその下に表示されます。
hotspot.js で設定したスクリプト属性をそれぞれ設定します。
Camera Entity には、ヒエラルキーにある Camera を設定。
Radius には、 0.5 を設定します。
このとき、3つあるModelのエンティティを複数選択することで一括設定することができます。
複数選択は Shift や Control(Command)キーを押しながら左クリックすることで可能です。
initialize ではヒットエリア、レイキャストのレイの設定、マウス移動のイベントを取得しています。
Hotspot.prototype.initialize = function() {
this.hitArea = new pc.BoundingSphere(this.entity.getPosition(), this.radius); // ヒットエリアを作成。BoundingSphereがentityの境界エリアを作成
this.ray = new pc.Ray(); // 直進する線のデータを作成。(Rayは光線の意でstart pointからentityまでの距離を測ったりすることが可能)
this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseHover, this); // マウスカーソルがオーバーした時
};
マウス移動のイベントで3Dオブジェクトとカメラから見てマウスオーバーしているか onMouseHover で取得しています。
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.js を以下に書き換えます。
var Hotspot = pc.createScript('hotspot');
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)) { // ヒットエリアとレイが交差した場合
console.log("click!!");
}
};
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); // レイキャストを呼ぶ
}
};
initializeにマウスのクリックが押された時のイベントを追加しています。
クリックされたら onMouseDown を実行します。
this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this); // クリックが押された時
onMouseDown
マウスの左クリックのみを取得し、クリックされたら doRayCast を実行します。
Hotspot.prototype.onMouseDown = function(event) { // クリックが押されている時
if (event.button == pc.MOUSEBUTTON_LEFT) { // 左クリックが押された時
this.doRayCast(event); // レイキャストを呼ぶ
}
};
doRayCast
クリックされたマウスポインターの位置がヒットエリアと交わっていれば、 console.log を実行します。
Hotspot.prototype.doRayCast = function (screenPosition) { // レイキャスト処理(ある地点から特定方向に直線で線を引いて、その線上で物体があるか検知する処理)
if (this.hitArea.intersectsRay(this.ray)) { // ヒットエリアとレイが交差した場合
console.log("click!!");
}
};
Launch画面をリロードしてコンソールから確認ができます。
次は3Dオブジェクトに応じてHTMLのページを表示させます。
Chapter04 - 3Dオブジェクトに応じたHTMLのページを表示させる
マウスからイベントが取得できたので、クリックしたら HTML を表示させます。
予め Assets に markup というディレクトリ内に使用される html と css のサンプルがあります。
事前にサンプルを用意していますが、作る際には自分で作成する必要があるのでご注意ください。
今回は 3つの3Dオブジェクト と 3つのhtml の関係を持たせて、
「3Dオブジェクトがクリックされたら、それにリンクするhtmlが表示される」 までを作ります。
ここで使用する Script は addhtml.js ですが、これもエンティティに ADD SCRIPT する必要があります。
今回は chara というグループのエンティティに ADD SCRIPT します。
addhtml.js を ADD SCRIPT したら、コードエディターを開きます。
addhtml.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("scenes", {type: 'asset', assetType:'html', title: 'HTML Scenes', array:true }); // アセットのhtmlを配列で読み込み
スクリプト属性が追加されたので、早速ファイルを登録していきます。
下のアセットからmarkupというディレクトリに登録するファイルが用意してあります。
以下のようにスクリプト属性にファイルを登録します。
・CSS Style : master.css
・HTML Header : header
・HTML Scenes : 3
∟ scene01
∟ scene02
∟ scene03
addhtml.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("scenes", {type: 'asset', assetType:'html', title: 'HTML Scenes', array:true }); // アセットのhtmlを配列で読み込み
var container;
Addhtml.prototype.initialize = function() { // init
var htmlNameArr = this.scenes; // attributesの配列データを代入します
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); // attributesで追加したヘッダーを追加
style.append(this.css.resources[0]);
head.appendChild(style); // headの最後にstyleを追加
for(var i = 0; i < htmlNameArr.length; i++){ // htmlのアセットの配列を回す
container.insertAdjacentHTML("beforeend", htmlNameArr[i].resource); // アセットから取得したhtmlを追加
}
};
initialize に追加したコード群はスクリプト属性で追加したHTMLやCSSのファイルから中のコードを取り出して、<head></head> や <body></body> に追加する処理をおこなっています。
var head = document.getElementsByTagName("head")[0]; // headタグ取得
var body = document.getElementsByTagName("body")[0]; // bodyタグ取得
appendChild()やinnerHTML、insertAdjacentHTML()などでhtmlを追加するために、 <head></head> と <body></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); // attributesで追加したヘッダーを追加
style.append(this.css.resources[0]);
head.appendChild(style); // headの最後にstyleを追加
HTMLやCSSを追加するためのdiv要素やstyle要素を createElement() で作成します。
クラス名指定をしているのは今回用意しているCSSスタイルに使用するためです。
スクリプト属性の header を insertAdjacenthTML() で wrapper に追加し、styleタグにはCSSを追加しています
for(var i = 0; i < htmlNameArr.length; i++){ // htmlのアセットの配列を回す
container.insertAdjacentHTML("beforeend", htmlNameArr[i].resource); // アセットから取得したhtmlを追加
}
複数登録したSceneのHTMLはfor文を用いて一つずつ追加しています。
これでhtmlとcssの情報を追加できました。
headタグの中にcssのスタイルが入り、bodyタグの中にhtmlの要素が入っているのを確認できます
次に、オブジェクトをクリックしたらそれに連動したHTML要素を表示していきます。
エディターからヒエラルキーの chara → Model を選択し、インスペクターから Tags を登録します。
Tags の名前は任意のもので大丈夫ですが、3つあるエンティティを上から順に tag1, tag2, tag3... と接尾辞に数字をそれぞれに入れるのを忘れないようにしてください。この数字を参照してスクリプトが処理をします。
※フォークしたプロジェクトでは既にTagsが設定されていますので、この設定は不要になっています。
登録した Tags をクリックした時に取得できるか確認します。
hotspot.js の doRayCast にある console.log を書き換えることで Tags を確認します。
Hotspot.prototype.doRayCast = function (screenPosition) { // レイキャスト処理(ある地点から特定方向に直線で線を引いて、その線上で物体があるか検知する処理)
if (this.hitArea.intersectsRay(this.ray)) { // ヒットエリアとレイが交差した場合
console.log("click!! : ",this.entity.tags.list()[0]);
}
};
クリックした3Dオブジェクトから各々の Tags が取得できたと思います。
これを使って HTML とリンクさせます。
addhtml.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("scenes", {type: 'asset', assetType:'html', title: 'HTML Scenes', array:true }); // アセットのhtmlを配列で読み込み
globalPc = {}; // グローバルな変数(オブジェクト)
var container;
Addhtml.prototype.initialize = function() { // init
globalPc.scene = 0; // どのシーンページを開いているか保管
var htmlNameArr = this.scenes; // attributesの配列データを代入します
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のアセットの配列を回す
container.insertAdjacentHTML("beforeend", 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を削除
}
}
}
クリックした Tags の数字を HTMLの表示とリンクさせる方法として、ここではグローバル変数の globalPc で今開いているページを管理できるようにしています。
initializeには以下を追加してロードされた時の状態を設定しています。例えば、1 に設定していれば、 scene01 のhtmlページが表示された状態でロードされるようになります。
globalPc = {}; // グローバルな変数(オブジェクト)
var container;
Addhtml.prototype.initialize = function() { // init
globalPc.scene = 0; // どのシーンページを開いているか保管
追加した update ではglobalPC の値を監視しています。
これが0以上の場合には、いずれかのSceneのHTMLを表示する処理を記載しています。
ここでは、HTMLのクラス名を使って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の閉じるボタンにイベントをセット
}
}
};
表示されたSceneのHTMLを閉じるための処理を btnClose で実行しています。
これはPlayCanvasではなくWebのHTMLに適用するため、 function btnClose(e){ ~ } という書き方をしています。
閉じるボタンが押されたら、globalPcを0に戻して、クラス名の処理をおこなって非表示しています。
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を削除
}
}
}
あとは、先ほど Tags をクリックして確認した hotspot.js の doRayCast を書き換えてグローバル変数に数字を設定することで、HTMLが表示されます。
hotspot.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); // レイキャストを呼ぶ
}
};
先ほど console.log で Tags を確認していた箇所を以下のように書き換えました。クリックで取得した Tags の数字だけ取得して、globalPc に数値を渡しています。
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画面で確認してみましょう。
Launch画面でリロードして3Dオブジェクトをクリックすると、それぞれ該当したhtmlが表示されるようになります。
これで本チュートリアルは完成になります。
3Dモデルを変更したり、ステージもお好みに変更して自分だけのコンテンツを作ってみてください。
今回はマウスドラッグでカメラ視点を変更するコンテンツとなりましたが、クリックしてカメラを移動させるコンテンツをつくりたい場合はこちらのチュートリアルを参照ください!
チュートリアル - 箱庭・ミニスケープなコンテンツを作ってみる
---
蛇足
headerの処理
現状では、既存で用意したheaaderのリンクをクリックしても機能していません。
同じようにglobal変数を使って同じように実装していきます。
addhtml.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("scenes", {type: 'asset', assetType:'html', title: 'HTML Scenes', array:true }); // アセットのhtmlを配列で読み込み
globalPc = {}; // グローバルな変数(オブジェクト)
var container;
Addhtml.prototype.initialize = function() { // init
globalPc.scene = 0; // どのシーンページを開いているか保管
var htmlNameArr = this.scenes; // attributesの配列データを代入します
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で追加したヘッダーを追加
// ヘッダー内のgnaviを取得
var gnavLists = document.querySelector(".gnav_lists");
gnavLists.innerHTML = ""; // .gnav_lists内のhtmlを空っぽに
var gnavItem = document.createElement("div"); // gnavの各リンクのdivを作成
gnavItem.className = "gnav_item"; // gnavリンク要素のclass名を指定
var gnavItemCl = []; // gnav_itemを整理する
style.append(this.css._resources[0]);
head.appendChild(style); // headの最後にstyleを追加
for(var i = 0; i < htmlNameArr.length; i++){
container.insertAdjacentHTML("beforeend", htmlNameArr[i].resource); // アセットから取得したhtmlを追加
// 今回はgnavのリンクの数とhtmlのアセットの数が同じなのでどう処理を行う。
gnavItemCl[i] = gnavItem.cloneNode(true); // gnavItemのクローンを作成
gnavItemCl[i].innerHTML = '<label for="menu"><span>' + htmlNameArr[i].name + '</span></label>'; // innerHTMLでheaderのitemの名前を作成
gnavItemCl[i].setAttribute("data-scene",i+1); // scene1,scene2,scene3を切り替えるため数値をdata-sceneに追加
gnavLists.append(gnavItemCl[i]); // gnav_lists内に追加
gnavItemCl[i].addEventListener("click", gnavClick, false); // gnav_item各々にイベントをセット
}
};
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を削除
}
}
};
function gnavClick(e){ // gnav_itemをクリックしたら発火
if(globalPc.scene === 0) {
globalPc.scene = this.getAttribute("data-scene"); // クリックしたgnav_itemのdata-sceneを取得
}
};
initialize で header.html のDOMを取得してクラスや属性を当てています。
クリックイベントも適用し、グローバル変数の globalPC の数値も変更します。
// ヘッダー内のgnaviを取得
var gnavLists = document.querySelector(".gnav_lists");
gnavLists.innerHTML = ""; // .gnav_lists内のhtmlを空っぽに
var gnavItem = document.createElement("div"); // gnavの各リンクのdivを作成
gnavItem.className = "gnav_item"; // gnavリンク要素のclass名を指定
var gnavItemCl = []; // gnav_itemを整理する
style.append(this.css._resources[0]);
head.appendChild(style); // headの最後にstyleを追加
for(var i = 0; i < htmlNameArr.length; i++){
container.insertAdjacentHTML("beforeend", htmlNameArr[i].resource); // アセットから取得したhtmlを追加
// 今回はgnavのリンクの数とhtmlのアセットの数が同じなのでどう処理を行う。
gnavItemCl[i] = gnavItem.cloneNode(true); // gnavItemのクローンを作成
gnavItemCl[i].innerHTML = '<label for="menu"><span>' + htmlNameArr[i].name + '</span></label>'; // innerHTMLでheaderのitemの名前を作成
gnavItemCl[i].setAttribute("data-scene",i+1); // scene1,scene2,scene3を切り替えるため数値をdata-sceneに追加
gnavLists.append(gnavItemCl[i]); // gnav_lists内に追加
gnavItemCl[i].addEventListener("click", gnavClick, false); // gnav_item各々にイベントをセット
}
クリックイベントの処理も追加しています。
どのSceneに該当する要素をクリックしたのかを、data-scene の属性で参照して渡しています。
function gnavClick(e){ // gnav_itemをクリックしたら発火
if(globalPc.scene === 0) {
globalPc.scene = this.getAttribute("data-scene"); // クリックしたgnav_itemのdata-sceneを取得
}
};
スマホ対応
現時点ではスマホに対応していませんので、スマホに対応するためにスクリプトを以下に書き換えましょう。
cameraMove.js を以下に書き換える
var CameraMove = pc.createScript('cameraMove');
// カメラ移動などの処理
CameraMove.prototype.initialize = function() {
this.f_click = false;
this.cameraEntity = this.entity; // このjsを受けているEntityを取得(カメラであることを前提に)
this.pos = this.cameraEntity.getPosition(); // カメラの座標を取得
this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this); // クリックが押された時
this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this); // クリックを押している状態から離れる時
this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this); // マウスカーソルが動いている時
// SP対応
if(pc.platform.touch) {
if(pc.platform.mobile || pc.platform.android || pc.platform.ios) {
this.app.touch.on(pc.EVENT_TOUCHSTART, this.onMouseDown, this); // クリックが押された時
this.app.touch.on(pc.EVENT_TOUCHEND, this.onMouseUp, this); // クリックを押している状態から離れる時
this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onMouseMove, this); // マウスカーソルが動いている時
}
}
};
CameraMove.prototype.update = function(dt) {};
CameraMove.prototype.onMouseDown = function (event) { // クリックダウンしたら
// SP対応と処理分岐
if(!event.changedTouches) {
if (event.button === pc.MOUSEBUTTON_LEFT) { this.f_click = true; } // 左クリックのとき
if (event.button === pc.MOUSEBUTTON_MIDDLE) {} // 中クリック
if (event.button === pc.MOUSEBUTTON_RIGHT) {} // 右クリック
} else {
this.f_click = true;
this.firstTouch = event.changedTouches[0]
}
};
CameraMove.prototype.onMouseUp = function (event) { // クリックアップしたら
// SP対応と処理分岐
if(!event.changedTouches) {
if (event.button === pc.MOUSEBUTTON_LEFT) { this.f_click = false; } // 左クリック
if (event.button === pc.MOUSEBUTTON_MIDDLE) {} // 中クリック
if (event.button === pc.MOUSEBUTTON_RIGHT) {} // 右クリック
} else {
this.f_click = false;
}
};
CameraMove.prototype.onMouseMove = function (event) { // マウスカーソルが動いたら
if(!this.f_click) return; // クリックが押されている時
var posDX = 0;
var posDY = 0;
// SP対応と処理分岐
if(!event.changedTouches) {
posDX = event.dx/500; // deltaX
posDY = event.dy/500; // deltaY
} else {
posDX = ( event.changedTouches[0].x - this.firstTouch.x ) / 10000; // deltaX
posDY = ( event.changedTouches[0].y - this.firstTouch.y ) / 10000; // deltaY
}
var posX = this.pos.x - posDX; // ドラッグ量からカメラの座標を計算
var posZ = this.pos.z - posDY; // ドラッグ量からカメラの座標を計算
this.cameraEntity.setPosition(posX,this.pos.y,posZ); // カメラの座標をセット
};
hotspot.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); // クリックが押された時
// SP対応
if(pc.platform.touch) {
if(pc.platform.mobile || pc.platform.android || pc.platform.ios) {
this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onMouseHover, this); // マウスカーソルが動いている時
this.app.touch.on(pc.EVENT_TOUCHSTART, this.onMouseDown, this); // クリックが押された時
}
}
this.localScale = this.entity.getLocalScale().clone(); // 初期のscaleを保持
this.hoverScale = this.localScale.clone().scale(1.2); // ホバー時のentityのscaleを保持
};
Hotspot.prototype.update = function(dt) { // update
this.cameraPosition = this.cameraEntity.getPosition(); // cameraのポジション(座標)を取得
this.directionToCamera.sub2(this.cameraPosition, this.entity.getPosition()); // 2つの3次元ベクトルの値を互いに減算
this.directionToCamera.normalize(); // 3次元ベクトルを単位ベクトルに変換
};
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) { // マウスホバー時
// SP対応と処理分岐
if(!screenPosition.changedTouches) {
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次元ベクトルを他の場所から減算し、単位ベクトルに変換
} else {
this.cameraEntity.camera.screenToWorld(screenPosition.changedTouches[0].x, screenPosition.changedTouches[0].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) && globalPc.scene === 0) { // sceneが選択されていなくて、ヒットエリアとレイが交差した場合
this.entity.setLocalScale(this.hoverScale); // entityのスケールを大きく
}else{
this.entity.setLocalScale(this.localScale); // entityのスケールを小さく
}
};
Hotspot.prototype.onMouseDown = function(event) { // クリックが押されている時
if (event.button == pc.MOUSEBUTTON_LEFT || event.changedTouches[0]) { // 左クリックが押された時
this.doRayCast(event); // レイキャストを呼ぶ
}
};
それぞれスマホのタッチ操作に対応させるために、 pc.EVENT_TOUCHSTART や pc.EVENT_TOUCHEND などを追記しています。
event.button などもマウス操作特有のEvent処理をしているため、event.touches などタッチ操作に変更してあげる必要があります。
マウスは左クリックや右クリックがあるのに対して、タッチは指が1本、2本と判別するため、別々で処理を分ける必要があるので少し手間がかかりますが、ここではタッチした最初の指を判定するために event.changedTouches[0] と配列の一つ目を取得するようにしています。
スマホ対応については以下も参考ください
- https://developer.playcanvas.com/en/tutorials/basic-touch-input/
- https://developer.playcanvas.com/en/api/pc.Touch.html
コメント
0件のコメント
サインインしてコメントを残してください。