【8th wall】World Tracking Portalのサンプルを理解してみる
このサンプルコードの解説です.
現実世界に,異世界への扉「ポータル」に入ると異世界へ行ける
こちらをタップして体験↓
裏側の仕組みの全体観
現実にいる時,実は外側には異世界のskyboxや異世界に置いてあるオブジェクトがありますが,自分の周りに壁があるので異世界は見えません.
ポータルの外(現実に見える)にいるとき
カメラの位置がポータルを潜ると,自分を囲っていた壁が消え,異世界が見えます.それと同時に,ポータルの円の部分にだけ,視界を隠す壁ができ,現実世界が見えます.
ポータルの中にいるとき
壁に色をつけてみるとわかりやすいです
コード読む
body.html
まず全体像から
portal用の部分だけ取り出すと↓
<!-- Hider walls -->
: 重なっている片方の世界を隠すための壁
<!-- Portal Contents -->
: Portalの中の世界
<!-- Portal -->
: Portal(入り口の輪っかの部分と,入り口のモヤモヤアニメーションと影)
<!-- Hider walls -->
<a-box scale="100 1 100" position="0 -1 49" xrextras-hider-material></a-box>
<a-box scale="100 100 1" position="0 50 75" xrextras-hider-material></a-box>
<a-box scale="100 1 100" position="0 100 49" xrextras-hider-material></a-box>
<a-box scale="1 100 100" position="-30 50 50" xrextras-hider-material></a-box>
<a-box scale="1 100 100" position="30 50 50" xrextras-hider-material></a-box>
<a-ring id="portalHiderRing" radius-inner="0.001" radius-outer="100" position="0 7.5 -0.2" xrextras-hider-material></a-ring>
自分の周りを囲む板を5枚と,ドーナツ状の平面でプレイヤーを取り囲む壁を作って,Portalの中の景色を隠している
<!-- Portal Contents -->
異世界の中身です.中の風景を書き換えたい時はここを書き換えればOK.
<!-- Portal Contents -->
<a-entity id="portal-contents">
<a-entity
gltf-model="#moon-model"
...>
</a-entity>
<a-entity
gltf-model="#platform-model"
...>
</a-entity>
<a-plane
material="src: #satellite-img; transparent: true; roughness: 0.8; metalness: 0"
...>
</a-plane>
<a-entity
gltf-model="#flag-model"
...>
</a-entity>
<a-entity
gltf-model="#rocks-model"
...>
</a-entity>
<a-sky src="#skybox-img" rotation="0 7 0" transparent="true"></a-sky>
</a-entity>
jsでは,"portal-contents"idで参照していじっているので,中身を書き換える分には挙動に影響ありません.
<!-- Portal -->
ポータル自体のモデルとか装飾
<!-- Portal -->
<a-entity
id="portalRim"
gltf-model="#portal-rim-model"
...>
</a-entity>
<a-entity
id="portalVideo"
auto-play-video="video: #portal-video"
...>
</a-entity>
<a-circle
id="portalShadow"
radius="0.5"
...>
</a-circle>
このauto-play-videoて何??調べても出て来ないんだけど!と思った結果
ここにありました
AFRAME.registerComponent('auto-play-video', {
schema: {
video: {type: 'string'},
},
init() {
const v = document.querySelector(this.data.video)
v.play()
},
})
やってることとしては,
<a-entity>でplaneに,video要素をmaterialとして貼ることで表示
auto-play-videoでソースのvideo要素自体を再生する
という仕組み.
portal-contents.js
- portalCameraComponent
カメラの移動に応じて,異世界を隠す/隠さないを制御 - tapToPlacePortalComponent
画面をタップしたらポータルを表示 - promptFlowComponent
"Tap to Place Moon Portal"を出したり消したり(説明しない) - spinComponent
ポータルの輪っかを回す(説明しない)
portalCameraComponent
const portalCameraComponent = {
schema: {
width: {default: 10},
height: {default: 10},
},
init() {
this.camera = this.el
this.contents = document.getElementById('portal-contents')
this.walls = document.getElementById('hider-walls')
this.portalWall = document.getElementById('portal-wall')
this.portalVideo = document.getElementById('portalVideo')
this.isInPortalSpace = false
this.wasOutside = true
},
// UnityでいうUpdate。描画のたびに呼ばれる
tick() {
const {position} = this.camera.object3D
const isOutside = position.z > 0
// ポータルに入ったかどうかを判別
const withinPortalBounds =
position.y < this.data.height && Math.abs(position.x) < this.data.width / 2
if (this.wasOutside !== isOutside && withinPortalBounds) {
this.isInPortalSpace = !isOutside
}
// isOutsideに応じて各オブジェクトの挙動を変更
this.contents.object3D.visible = this.isInPortalSpace || isOutside
this.walls.object3D.visible = !this.isInPortalSpace && isOutside
this.portalWall.object3D.visible = this.isInPortalSpace && !isOutside
this.portalVideo.object3D.visible = isOutside
this.wasOutside = isOutside
},
}
挙動は上の図に示したとおりです.照らし合わせながら見るとわかりますので再掲します.
ポータルの外(現実に見える)にいるとき
ポータルの中にいるとき
tapToPlacePortalComponent
主な挙動は
- handleClickEvent
センターボタンがクリックされたとき - firstPlaceEvent
初めて画面をクリックしたとき,ポータルを出す
の二つ
handleClickEvent
センターボタンがクリックされたとき,シーンにrecenterイベントをエミット(呼ばれる側がどこにあるか結局よく分からず)
一回のタップでイベントが重複して発火されないようにsetTimeoutで管理
const handleClickEvent = (e) => {
if (!e.touches || e.touches.length < 2) {
recenterBtn.classList.add('pulse-once')
sceneEl.emit('recenter')
setTimeout(() => {
recenterBtn.classList.remove('pulse-once')
}, 200)
}
}
firstPlaceEvent
シーンを1回目にタップされた時にPortalを開く
portalHiderRingのradius-innerが大きくなることによって,Hiderは薄い輪っかのようになり,空間に穴が開く
const firstPlaceEvent = (e) => {
// recenterなにが呼ばれている??
sceneEl.emit('recenter')
// "Tap to Place Portal"の文字を消す
sceneEl.emit('dismissPrompt')
// hider ringの内側の半径を広げることによって,異世界の目隠しをしていた前方の面に穴が開く
portalHiderRing.setAttribute('animation__1', {
...
})
// ポータルの縁を出現させる
portalRim.setAttribute('animation__2', {
...
})
// ポータルの縁のモヤモヤアニメーションを出現させる
portalVideo.setAttribute('animation__3', {
...
})
// ポータルのしたの影を出現させる
portalShadow.setAttribute('animation__4', {
...
})
// 画面をクリックしたらfirstPlaceEventを呼ぶためのイベントリスナーを解除
sceneEl.removeEventListener('click', firstPlaceEvent)
// recenterボタンにイベントリスナーを付ける
recenterBtn.addEventListener('click', handleClickEvent, true)
}
バグ?みっけ
今のサンプルだと,portal-cameraのtick()内で,
this.contents.object3D.visible = this.isInPortalSpace || isOutside
となっていることによって,ポータルが開いていなくても実はポータル内の世界に入れてしまう
portalCameraComponentを↓のように修正して,
const portalCameraComponent = {
schema: {
width: {default: 10},
height: {default: 10},
active: {default: false},
},
init() {
this.camera = this.el
this.contents = document.getElementById('portal-contents')
this.walls = document.getElementById('hider-walls')
this.portalWall = document.getElementById('portal-wall')
this.portalVideo = document.getElementById('portalVideo')
this.isInPortalSpace = false
this.wasOutside = true
},
tick() {
if (!this.data.active) return
const {position} = this.camera.object3D
const isOutside = position.z > 0
const withinPortalBounds =
position.y < this.data.height && Math.abs(position.x) < this.data.width / 2
if (this.wasOutside !== isOutside && withinPortalBounds) {
this.isInPortalSpace = !isOutside
}
this.contents.object3D.visible = this.isInPortalSpace || isOutside
this.walls.object3D.visible = !this.isInPortalSpace && isOutside
this.portalWall.object3D.visible = this.isInPortalSpace && !isOutside
this.portalVideo.object3D.visible = isOutside
this.wasOutside = isOutside
},
}
tapToPlacePortalComponentを
const tapToPlacePortalComponent = {
init() {
const {sceneEl} = this.el
const recenterBtn = document.getElementById('recenterButton')
this.camera = document.getElementById('camera')
this.contents = document.getElementById('portal-contents')
this.contents.object3D.visible = false
...
const handleClickEvent = (e) => {...}
const firstPlaceEvent = (e) => {
sceneEl.emit('recenter')
sceneEl.emit('dismissPrompt')
this.camera.setAttribute('portal-camera', 'active:true;')
....
sceneEl.removeEventListener('click', firstPlaceEvent)
recenterBtn.addEventListener('click', handleClickEvent, true)
}
sceneEl.addEventListener('click', firstPlaceEvent)
},
}
とすると直る
beforehttps://www.8thwall.com/8thwall/portal-aframe)
(projectページ:after
まとめ
の中身と全体観をまとめました.読めばわかりますが.
360°画像の表現方法としても面白いなと思いました!
Discussion