Treasure Island VR with A-Frame

Michael McAnally
19 min readOct 25, 2020
Treasure Island VR

This article is now depricated. Meaning you should read this article instead . . .

==============================================================

Aye, there be treasure here, mateys!

Pirate Translation: Treasure is hidden in this simulation, my friend.

Our treasure chest full of gold coins

This is really an article on how to create a compelling VR environment and experience inside a browser with A-Frame.

To be honest, I thought about having a sunken pirate ship with treasure, but that would be too many polygons and vertices.

So I opted for an office model instead, since after this article I will be writing something exploring multi-user VR access. Possibly with networked-aframe. The office would then be very useful as a meetup place.

Important Code Update:

Experience The Island

For now let’s take a look at our working island example first. Some explanation is in order. This example takes a while to load because of all the heavy 3D graphic models and assets. In comparison, think about how long it takes a complete game to download and install the first time, and now imagine doing that for the first time while loading a web page. Thank goodness for browser caching!

Check it out on a desktop or laptop; don’t even try with your cell phone:

INSTRUCTIONS TO USE: If you don’t have a VR headset, WASD keys move you around (or arrow keys), dragging with the left mouse button will change your orientation direction. ESC gets you out of that.

If you have a headset, select the VR button in the bottom right corner of your screen. Thumbstick or trackpad moves you around, right controller triggers laser selection (only for playing video in this example, if enabled in your browser).

Working example link:

The source code:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Treasure Island VR (Will take some time to load)</title>
<meta name="description" content="Treasure Island VR">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="gray-translucent" />
<script src="aframe-master/dist/aframe-v1.0.4.min.js"></script>
<script src="aframe-environment-component-master/dist/aframe-environment-component.min.js"></script>
<script src="aframe-extras-master/dist/aframe-extras.min.js"></script>
<script src="superframe-master/components/thumb-controls/dist/aframe-thumb-controls-component.min.js"></script>
<script src="superframe-master/components/text-geometry/dist/aframe-text-geometry-component.min.js"></script>
<script src="aframe-lensflare-component-master/dist/aframe-lensflare-component.min.js"></script>
<!-- Video Player Styling -->
<style type="text/css">
#video-permission {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 10000;
display: none;
}
#video-permission-button {
position: fixed;
top: calc(50% - 1em);
left: calc(50% - 60px);
width: 120px;
height: 2em;
}
</style>
<script type="text/javascript">function playSound() {//alert("TEST Sound playing functional!!!");
}
// Solves Google mute of audio problem https://stackoverflow.com/questions/47921013/play-sound-on-click-in-a-frame?answertab=active#tab-top
AFRAME.registerComponent('audiohandler', {
init:function() {
let playing = false;
let audio = document.querySelector("#playAudio");
this.el.addEventListener('click', () => {
if(!playing) {
audio.play();
} else {
audio.pause();
audio.currentTime = 0;
}
playing = !playing;
});
}
})
</script>

</head>
<body>
<div id="video-permission">
<button id="video-permission-button">Allow VR Video</button>
</div>
<button id="playButton" type="button">Play Music</button>
<audio id="playAudio" autoplay loop>
<!-- <source src="https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/Romanzeandante.mp3" type="audio/mpeg"> -->
</audio>
<a-scene background="color: #FAFAFA">
<a-assets>
<!-- Our font -->
<a-asset-item id="optimerBoldFont" src="assets/fonts/optimer_bold.typeface.json"></a-asset-item>
<img crossorigin="anonymous" id="flare-asset" src="assets/img/adjustflare.jpg">
<a-asset-item id="Building" src="assets/gltf/New_Building_final1.glb" nav-agent="speed: 1.5; active: true"></a-asset-item>
<a-asset-item id="Building_skeleton" src="assets/gltf/New_Building_final_skeleton1.glb" nav-agent="speed: 1.5; active: true"></a-asset-item>
<a-asset-item crossorigin="anonymous" id="Island" src="assets/gltf/Islands.glb"></a-asset-item>
<a-asset-item crossorigin="anonymous" id="Palm" src="assets/gltf/PalmMinimal.glb"></a-asset-item>
<img crossorigin="anonymous" src="assets/img/play2.png" id="play" >
<img crossorigin="anonymous" src="assets/img/pause.png" id="pause" >
<img crossorigin="anonymous" src="assets/img/volume-normal.png" id="volume-normal" >
<img crossorigin="anonymous" src="assets/img/volume-mute.png" id="volume-mute" >
<img crossorigin="anonymous" src="assets/img/seek-back.png" id="seek-back" >
<video crossorigin="anonymous" id="video-src" src="assets/video/MP_beach.mp4"></video>
<img crossorigin="anonymous" src="assets/img/coins.jpg" id="coins">
<img crossorigin="anonymous" src="assets/img/TreasureCard.png" id="treasurecard">
</a-assets>
<!-- nav-mesh: protecting us from running thru walls -->
<a-entity id="navmesh-Island" gltf-model="url(assets/gltf/navmeshTI.gltf)" scale="" visible="false" nav-mesh=""></a-entity>
<!-- Basic movement and teleportation -->
<a-entity id="cameraRig" movement-controls="constrainToNavMesh: true;" navigator="cameraRig: #cameraRig; cameraHead: #head; collisionEntities: .collision; ignoreEntities: .clickable" position="0 0 0" rotation="0 180 0">
<!-- camera -->
<a-entity id="head" camera="active: true" position="0 1.6 0" look-controls="pointerLockEnabled: true; reverseMouseDrag: true" ></a-entity>
<!-- Left Controller -->
<a-entity class="leftController" hand-controls="hand: left; handModelStyle: lowPoly; color: #15ACCF" tracked-controls vive-controls="hand: left" oculus-touch-controls="hand: left" windows-motion-controls="hand: left" visible="true"></a-entity>
<!-- Right Controller -->
<a-entity class="rightController" hand-controls="hand: right; handModelStyle: lowPoly; color: #15ACCF" tracked-controls vive-controls="hand: right" oculus-touch-controls="hand: right" windows-motion-controls="hand: right" laser-controls raycaster="showLine: true; far: 10; interval: 0; objects: .clickable, a-link;" line="color: lawngreen; opacity: 0.5" visible="true"></a-entity>
</a-entity>
<a-entity id="Meeting_Structure" gltf-model="#Building" position="-10 -0.07 12" rotation="0 0 0" scale="0.019 0.019 0.019" shadow=""></a-entity><!-- Sky -->
<a-sky color="#55D6F6"></a-sky>
<!-- Place the Sun -->
<a-sphere id="flare" radius="0.05" color="yellow" lensflare="createLight:false; relative: true; src: #flare-asset; position:-109.093 10.428 -46.857; lightColor:yellow; intensity: 5; lightDecay: 500" position="-109.093 26.20844 -14.03708"></a-sphere>
<!-- Island terrain -->
<a-entity id="The_Island" gltf-model="#Island" position="-29.71322 -5.19691 23.18321" scale="75 75 75" visible="true" shadow="cast: false; receive: true"></a-entity>
<!-- Ocean -->
<a-ocean position="-28.98488 -1.76439 22.54954" scale="3 3 3" width="50" depth="50" opacity=".75" ocean="density: 45; amplitude: -0.1; speed: 0.75; speedVariance: 0.1"></a-ocean>
<a-entity light="type: ambient; intensity: 0.89"></a-entity><!-- Shadow Camera -->
<a-entity light="intensity: .5; castShadow: true; shadowCameraLeft: -50; shadowCameraBottom: -50; shadowCameraRight: 50; shadowCameraTop: 50; shadowCameraVisible: false" position="-52.39331 48.67142 -2.25727"></a-entity>
<!-- Palm Trees These are a little heavy, but they really look nice... -->
<a-entity id="Palm_Tree1" gltf-model="#Palm" position="-60.19575 -0.18477 -20.35598" rotation="0 0 0" scale=".002 .003 .002" shadow></a-entity>
<a-entity id="Palm_Tree2" gltf-model="#Palm" position="-14.97217 -0.76341 -16.69816" rotation="-3.741 111.923 9.209" scale=".002 .003 .002" shadow></a-entity>
<a-entity id="Palm_Tree3" gltf-model="#Palm" position="-22.06368 -0.1212 -35.6895" rotation="-3.285 61.159 11.798" scale=".002 .002 .002" shadow></a-entity>
<a-entity id="Palm_Tree4" gltf-model="#Palm" position="-29.31117 -0.40163 0.262" rotation="0 78.26 0" scale=".002 .003 .002" shadow></a-entity>
<!-- Small Terrain Map On Wall -->
<a-entity id="Islands_small_map" gltf-model="#Island" position="0.64705 1.66753 3.31628" rotation="-89.87473270201606 125.90518364881788 -125.19586189844593" scale="0.806 1.0 0.806" shadow visible=""></a-entity>
<a-entity position="1.25239 2.34349 3.31628" rotation="0 180 0" scale="0.2 0.2 0.2" text-geometry="value: Treasure Island VR" material="color: #FFFFFF"></a-entity><a-entity id="Meeting_Structure_skeleton" gltf-model="#Building_skeleton" position="0.84773 1.5403 3.2393" rotation="-89.87473270201606 125.90518364881788 -125.19586189844593" scale="0.00019 0.00019 0.00019" shadow=""></a-entity><!-- 13 Bridges allowing us to move around the island on multiple levels. -->
<a-box id="bridge-1" position="-51.02139 -0.46314 12.85726" rotation="-90 -52.796 0" scale="1 2.532 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-2" position="-48.48011 -0.84417 -23.91551" rotation="-90 0 0" scale="4.304 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-3" position="-15.68861 4.7949 25.79727" rotation="-75.752 112.335 -91.715" scale="5.080 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-4" position="-12.01506 -0.52461 63.6735" rotation="-90 6.825 0" scale="5.31906 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-5" position="-47.94579 -0.10505 68.83707" rotation="-90 0 0" scale="1 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-6" position="-12.30953 -0.34999 35.37341" rotation="-90 0 0" scale="1 1.854 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-7" position="-20.82265 -0.29084 -0.3377" rotation="-90 0 0" scale="4.504 0.868 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-8" position="16.2233 -0.53012 11.56199" rotation="-90 0 0" scale="1 1.854 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-9" position="-3.59464 3.156 16.24416" rotation="-69.518 1.707 -91.206" scale="3.066 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-10" position="-8.59872 1.16195 11.92651" rotation="-78.18295593457648 -168.72461151012482 93.4591566683545" scale="3.8152 0.4676 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-11" position="23.36806 -2.96524 -12.2864" rotation="-77.23757557261062 116.60092201368845 -155.9797383152348" scale="1 6.4158 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-12" position="-64.53757 -0.36763 36.50537" rotation="-90 -52.796 0" scale="1 2.532 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-13" position="12.81913 -0.34999 51.0507" rotation="-90 0 0" scale="1 1.854 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<!-- Our Treasure Chest in glTF format Link to license: https://creativecommons.org/licenses/by/4.0/ Location: https://sketchfab.com/3d-models/old-chest-efc357374ecf4d17a36e85f1237e624c -->
<a-entity id="Treasure" gltf-model="url(assets/gltf/testChest2.glb)" position="-30.54377 -5.15361 41.89098" rotation="0 -48.819441891916924 0" scale=".01 .01 .01" visible="true"></a-entity>
<a-plane id="fillWithGold" position="-30.53062 -4.68798 41.89179" rotation="-89.67878113608133 66.30955154608043 -113.66164852466733" width="4" height="4" color="#EBE3B6" material="src: #coins" visible="true" scale="0.261 0.117 0.407"></a-plane><!-- Treasure Card -->
<a-plane id="tellAboutCard" position="-1.18813 1.66753 3.31628" rotation="0 180 0" scale="0.367 0.22404 1" width="4" height="4" color="#EBE3B6" material="src: #treasurecard" shadow="" visible="true" scale="1 1 1"></a-plane>
<!-- Controls for display of video screen, seems to work nicely, javascript below </scene> tag below -->

<!-- MEDIAS HOLDER -->
<a-sound id="alert-sound" src="src: url(assets/wav/action.wav)" autoplay="false" position="0 0 0"></a-sound>
<a-video id="video-screen" src="#video-src" position="-0.02132 1.89195 -3.56798" rotation="0 0 0" scale="0.50761 0.49394 1" width="8" height="4" rotation="0 0 0" visible="true"></a-video>
<!-- END MEDIAS HOLDER -->
<!-- CONTROLS -->
<a-image class="clickable" id="control-back" width="0.4" height="0.4" src="#seek-back" position="-0.42425 0.565 -3.47602" rotation="0 1.2570694025170261 0" visible="false" scale="0.85 0.85 0.85"></a-image>
<a-image class="clickable" id="control-play" width="0.4" height="0.4" src="#play" position="-0.00457 0.565 -3.47602" rotation="0 1.2570694025170261 0"></a-image>
<a-image class="clickable" id="control-volume" width="0.4" height="0.4" src="#volume-mute" position="0.38786 0.565 -3.47602" rotation="0 1.2570694025170261 0" visible="false" scale="0.75 0.75 0.75"></a-image>
<!-- END CONTROLS -->
<!-- PROGRESSBAR -->
<a-entity id="progress-bar" geometry="primitive:plane;height:0.1;width:4" material="opacity:0;transparent:true;visible:false" position="-0.00457 0.835 -3.47602" rotation="0 1.2570694025170261 0" >
<a-plane id="progress-bar-track" width="4" height="0.1" color="gray" position="" opacity="0.2" material="" visible="false" geometry=""></a-plane>
<a-plane id="progress-bar-fill" width="2.496982785433659" height="0.1" color="#7198e5" position="-0.7515086072831705 0 0" geometry="" visible="false" material=""></a-plane>
<!-- END PROGRESSBAR -->
</a-scene>
<script>//Google Code for un-audio mute
// Existing code unchanged.
window.onload = function() {
var context = new AudioContext();
// Setup all nodes
}// One-liner to resume playback when user interacted with the page.
document.querySelector('button').addEventListener('click', function() {
context.resume().then(() => {
console.log('Playback resumed successfully');
});
});

var AVideoPlayer = function() {
// Vals
this.duration = 0;
this.current_progress = 0;
this.progressWidth = 4;
this.paused = true;
// Elems
this.elProgressBar = null;
this.elProgressTrack = null;
this.elProgressFill = null;
this.elAlertSound = null;
this.elVideo = null;
this.elVideoScreen = null;
this.elControlBack = null;
this.elControlPlay = null;
this.elControlVolume = null;
this._initElements = function() {
this.elProgressBar = document.getElementById('progress-bar');
this.elProgressTrack = document.getElementById('progress-bar-track');
this.elProgressFill = document.getElementById('progress-bar-fill');
this.elAlertSound = document.getElementById('alert-sound');
this.elVideo = document.getElementById('video-src');
this.elVideoScreen = document.getElementById('video-screen');
this.elControlBack = document.getElementById('control-back');
this.elControlPlay = document.getElementById('control-play');
this.elControlVolume = document.getElementById('control-volume');
}
/**
* PROGRESS
*/
this.setProgress = function(progress) {
var new_progress = this.progressWidth*progress;
this._setProgressWidth(new_progress);
var progress_coord = this._getProgressCoord();
if (progress_coord != undefined) {
progress_coord.x = -(this.progressWidth-new_progress)/2;
this._setProgressCoord(progress_coord);
}
}
this._getProgressCoord = function() {
return AFRAME.utils.coordinates.parse(this.elProgressFill.getAttribute("position"))
}
this._getProgressWidth = function() {
return parseFloat(this.elProgressFill.getAttribute("width"));
}
this._setProgressCoord = function(coord) {
this.elProgressFill.setAttribute("position", coord);
}
this._setProgressWidth = function(width) {
this.elProgressFill.setAttribute("width", width);
}
/*
* UI SETTERS
*/
this.isProgressBarVisible = function(isVisible) {
this.elProgressTrack.setAttribute("visible", isVisible);
this.elProgressFill.setAttribute("visible", isVisible);
}
this.isControlVisible = function(isVisible) {
this.elControlBack.setAttribute("visible", isVisible);
this.elControlVolume.setAttribute("visible", isVisible);
this.elVideoScreen.setAttribute("visible", isVisible);
}
/*
* EVENTS
*/
this._addPlayerEvents = function() {
var that = this;
this.elVideo.pause();
this.elVideo.onplay = function() {
that.duration = this.duration;
}
this.elVideo.ontimeupdate = function() {
if (that.duration > 0) {
that.current_progress = this.currentTime/that.duration;
}
that.setProgress(that.current_progress);
}
}
this._addControlsEvent = function() {
var that = this;
this.elControlPlay.addEventListener('click', function () {
that._playAudioAlert();
if (that.elVideo.paused) {
this.setAttribute('src', '#pause');
that.elVideo.play();
that.paused = false;
that.isProgressBarVisible(true);
that.isControlVisible(true);
} else {
this.setAttribute('src', '#play');
that.elVideo.pause();
that.paused = true;
that.isProgressBarVisible(false);
that.isControlVisible(false);
}
});
this.elControlVolume.addEventListener('click', function () {
that._playAudioAlert();
if (that.elVideo.muted) {
that.elVideo.muted = false;
this.setAttribute('src', '#volume-normal');
} else {
that.elVideo.muted = true;
this.setAttribute('src', '#volume-mute');
}
});
this.elControlBack.addEventListener('click', function () {
that._playAudioAlert();
that.elVideo.currentTime = 0;
});
}
this._addProgressEvent = function() {
var that = this;
this.elProgressBar.addEventListener('click', function (e) {
if (e.detail == undefined || e.detail.intersection == undefined || that.duration === 0) {
return;
}
let seekedPosition = (e.detail.intersection.point.x+(that.progressWidth/2))/that.progressWidth;
try {
let seekedTime = seekedPosition*that.duration;
that.elVideo.currentTime = seekedTime;
} catch (e) {
}
});
}
this._playAudioAlert = function() {
if (this.elAlertSound.components !== undefined && this.elAlertSound.components.sound !== undefined) {
this.elAlertSound.components.sound.playSound();
}
}
/**
* MOBILE PATCH TO PLAY VIDEO
*/
this._mobileFriendly = function() {
if (AFRAME.utils.device.isMobile()) {
var that = this;
let video_permission = document.getElementById('video-permission');
let video_permission_button = document.getElementById('video-permission-button');
video_permission.style.display = 'block';
video_permission_button.addEventListener("click", function() {
video_permission.style.display = 'none';
that.elVideo.play();
that.elVideo.pause();
}, false);
}
}
this.init = function() {
this._initElements();
//this._determinateProgressWidth();
this.setProgress(this.current_progress);
this._addPlayerEvents();
this._addControlsEvent();
this._addProgressEvent();
this._mobileFriendly();
}
this.init();
}
let scene = document.querySelector('a-scene');
var cursor = document.querySelector('a-cursor');
/**
* CINEMA MODE
*/
scene.lightOff = function() {
scene.islightOn = true;
scene.removeAttribute('animation__fogback');
scene.setAttribute('animation__fog', "property: fog.color; to: #0c192a; dur: 800; easing: easeInQuad;");
}
scene.lightOn = function() {
scene.islightOn = false;
scene.removeAttribute('animation__fog');
scene.setAttribute('animation__fogback', "property: fog.color; to: #dbdedb; dur: 800");
}
/**
* AVideoPlayer
*/
var videoPlayer = new AVideoPlayer();
document.querySelector('#control-play').addEventListener('click', function () {
if (videoPlayer.paused) {
scene.lightOn()
} else {
scene.lightOff();
}
});
</script>
</body>
</html>

Now before getting into the island graphics and the code, please note that I have written a number of articles for newbies explaining the basics.

You might want to check those out:

Creating The Island

So how did I create the island? Much of the environment of the simulation is graphic in nature. Many years ago I explored Second Life and then Opensimulator, and while doing that I created something called a Heightmap.

Heightmap of island

Using my heightmap and Photoshop’s Gradient Map tool, I created a colormap.

Colormap of island

Then, using Blender (a free downloadable program) I created a Terrain Map.

Now I’m not an expert 3D modeler, or even close, but using a free student version of 3DS Max, I applied my color map to the terrain map and exported a glTF file of the island terrain model. Here is another way to do it.

You won’t have to go through all this. I will provide my model for free for this and any future exercises in later articles. You may also share, copy, and modify it freely.

glTF 3D terrain model of island in windows object viewer

The important thing to note here is that the model should have as few polygons (or vertices) as possible, but a sufficient number to make it look like a real landscape.

The reason for this is that traversing the landscape requires a lot of calculations for the GPU (especially with shadows and the rendering of other 3D model objects within the same scene). As a result, there can be visual stutters and lags. Not very appealing for the VR headset wearer; which can even result in a feeling of motion sickness.

So my rule of thumb: even if you don’t create your own 3D models, select your models to be as simple and low poly as possible.

Terrain map of island with sun in a-frame inspector

Now the unique and interesting thing about this terrain map is that it has an area of the ocean, and it is a mini archipelago.

<!-- Island terrain -->
<a-entity id="The_Island" gltf-model="#Island" position="-29.71322 -5.19691 23.18321" scale="75 75 75" visible="true" shadow="cast: false; receive: true"></a-entity>

We are going to position that island ocean area below our x-axis in the negative y-axis values and use the <a-ocean> feature to give us a cool looking moving ocean. In addition we will put a sun flare in the blue sky, and drop a number of palm trees around to give us an island like feel.

<!-- Ocean -->
<a-ocean position="-28.98488 -1.76439 22.54954" scale="3 3 3" width="50" depth="50" opacity=".75" ocean="density: 45; amplitude: -0.1; speed: 0.75; speedVariance: 0.1"></a-ocean>
<!-- Sky -->
<a-sky color="#55D6F6"></a-sky>
<!-- Place the Sun -->
<a-sphere id="flare" radius="0.05" color="yellow" lensflare="createLight:false; relative: true; src: #flare-asset; position:-109.093 10.428 -46.857; lightColor:yellow; intensity: 5; lightDecay: 500" position="-109.093 26.20844 -14.03708"></a-sphere>
<!-- Palm Trees These are a little heavy, but they really look nice...        -->
<a-entity id="Palm_Tree1" gltf-model="#Palm" position="-60.19575 -0.18477 -20.35598" rotation="0 0 0" scale=".002 .003 .002" shadow></a-entity>
<a-entity id="Palm_Tree2" gltf-model="#Palm" position="-14.97217 -0.76341 -16.69816" rotation="-3.741 111.923 9.209" scale=".002 .003 .002" shadow></a-entity>
<a-entity id="Palm_Tree3" gltf-model="#Palm" position="-22.06368 -0.1212 -35.6895" rotation="-3.285 61.159 11.798" scale=".002 .002 .002" shadow></a-entity>
<a-entity id="Palm_Tree4" gltf-model="#Palm" position="-29.31117 -0.40163 0.262" rotation="0 78.26 0" scale=".002 .003 .002" shadow></a-entity>

Office Building

After that we will add an office building, which I built using the 3D modeling SketchUp software on for a free 30 day trial period.

Inside the office building we will use the terrain map again (since it was already loaded in our assets) resized into a topological map and placing it on the wall. We will add a smaller model of our office, #Building_skeleton, with the roof removed and place that on our map. I chose not to include the palm trees, because they are high poly and will most likely slow down movement in the area around the map.

<a-entity id="Meeting_Structure" gltf-model="#Building" position="-10 -0.07 12" rotation="0 0 0" scale="0.019 0.019 0.019" shadow=""></a-entity>
Inside our office building
<!-- Small Terrain Map On Wall    -->
<a-entity id="Islands_small_map" gltf-model="#Island" position="0.64705 1.66753 3.31628" rotation="-89.87473270201606 125.90518364881788 -125.19586189844593" scale="0.806 1.0 0.806" shadow visible=""></a-entity>
<a-entity position="1.25239 2.34349 3.31628" rotation="0 180 0" scale="0.2 0.2 0.2" text-geometry="value: Treasure Island VR" material="color: #FFFFFF"></a-entity><a-entity id="Meeting_Structure_skeleton" gltf-model="#Building_skeleton" position="0.84773 1.5403 3.2393" rotation="-89.87473270201606 125.90518364881788 -125.19586189844593" scale="0.00019 0.00019 0.00019" shadow=""></a-entity>

Video Player

On the opposite wall I placed a video player screen and controls. It will play when the play button is selected in VR (only) with our controller hands laser pointer. In this case it shows a fairly high definition video which I took on the coastline in San Francisco.

This video has nice ocean wave crashing sounds and completes our audio effects nicely for this island environment.

Handy .MP4 video player code for whatever we want to show or say

You can replace the video with one of your own making or choosing; explaining anything you want your VR visitors to know.

This video can be of lower data bandwidth than mine, therefore making it more responsive while being played inside this “VR experience”. The actual video player code can be found between the <Script> tags after our </a-scene> is closed.

<a-assets><video crossorigin="anonymous" id="video-src" src="assets/video/MP_beach.mp4"></video>

At this point moving around the island environment is limited because not all the islands are connected together. In addition, it’s problematic when we move around, because we can move through walls, and even inside mountains.

This just won’t do!

The solution to this problem is twofold. First, we need ramps connecting the different parts of the islands at multiple levels.

Ramps for traversing island terrain

Second, we need something called a navigation mesh. We can create one with the aframe-inspector-plugin-recast (below). To do this we will need a reduced and modified version of our source file as well (below).

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Treasure Island VR</title>
<meta name="description" content="Treasure Island VR">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="gray-translucent" />
<script src="aframe-master/dist/aframe-v1.0.4.min.js"></script>
<script src="https://recast-api.donmccurdy.com/aframe-inspector-plugin-recast.js"></script></head>
<body>
<a-scene inspector-plugin-recast><a-assets><a-asset-item id="Building" src="assets/gltf/New_Building_final1.glb" nav-agent="speed: 1.5; active: true"></a-asset-item><a-asset-item crossorigin="anonymous" id="Island" src="assets/gltf/Islands.glb"></a-asset-item></a-assets><a-entity id="Meeting_Structure" gltf-model="#Building" position="-10 -0.07 12" rotation="0 0 0" scale="0.019 0.019 0.019" shadow=""></a-entity><!-- Island terrain -->
<a-entity id="The_Island" gltf-model="#Island" position="-29.71322 -5.19691 23.18321" scale="75 75 75" visible="true" shadow="cast: false; receive: true"></a-entity>
<a-entity light="type: ambient; intensity: 0.89"></a-entity><!-- Bridges allowing us to move around the island. -->
<a-box id="bridge-1" position="-51.02139 -0.46314 12.85726" rotation="-90 -52.796 0" scale="1 2.532 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-2" position="-48.48011 -0.84417 -23.91551" rotation="-90 0 0" scale="4.304 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-3" position="-15.68861 4.7949 25.79727" rotation="-75.752 112.335 -91.715" scale="5.080 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-4" position="-12.01506 -0.52461 63.6735" rotation="-90 6.825 0" scale="5.31906 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-5" position="-47.94579 -0.10505 68.83707" rotation="-90 0 0" scale="1 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-6" position="-12.30953 -0.34999 35.37341" rotation="-90 0 0" scale="1 1.854 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-7" position="-20.82265 -0.29084 -0.3377" rotation="-90 0 0" scale="4.504 0.868 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-8" position="16.2233 -0.53012 11.56199" rotation="-90 0 0" scale="1 1.854 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-9" position="-3.59464 3.156 16.24416" rotation="-69.518 1.707 -91.206" scale="3.066 1 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-10" position="-8.59872 1.16195 11.92651" rotation="-78.18295593457648 -168.72461151012482 93.4591566683545" scale="3.8152 0.4676 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-11" position="23.36806 -2.96524 -12.2864" rotation="-77.23757557261062 116.60092201368845 -155.9797383152348" scale="1 6.4158 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-12" position="-64.53757 -0.36763 36.50537" rotation="-90 -52.796 0" scale="1 2.532 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
<a-box id="bridge-13" position="12.81913 -0.34999 51.0507" rotation="-90 0 0" scale="1 1.854 0.143" width="4" height="4" color="#FFFFFF" shadow="receive: true"></a-box>
</a-scene></body>
</html>

Creating A Navmesh

You’ll notice that <a-scene inspector-plugin-recast> has been added to the <a-scene> tag as well as the script <script src=”https://recast-api.donmccurdy.com/aframe-inspector-plugin-recast.js"></script>

Most of our other assets and code such as the sky and the ocean have been removed. This makes sure that our navmesh creation is very clean and doesn’t cause any errors, which it can. See the documentation for further details.

Navigation Mesh Being Created

For an example, I have taken screen shots above and below of the process. In the end you will have a clean navmesh you can use in the original code to navigate the island.

<!-- nav-mesh: protecting us from running thru walls  -->
<a-entity id="navmesh-Island" gltf-model="url(assets/gltf/navmeshTI.gltf)" scale="" visible="false" nav-mesh=""></a-entity>
Light blue visible navmesh created
<a-entity id=”cameraRig” movement-controls=”constrainToNavMesh: true;”

This means the navmesh constrains you, and you will only be able to move where it allows. You will NOT be able to go through walls or into the mountains. Our navigational problems are solved.

Treasure Chest Of Gold

Finally, in order to give the island a theme, and something fun for our visitors to search for and find, we will name the island “Treasure Island VR” and hide a treasure chest of gold!

Free treasure chest model

After searching the internet for a low poly free treasure chest filled with treasure, the best I was able to do was find this animated chest on Sketchfab:

https://sketchfab.com/3d-models/old-chest-efc357374ecf4d17a36e85f1237e624c

So two more issues arise. The animation part, while being very cool, slowed the whole VR experience way down while moving around the island. Its performance was too clunky, resulting in delays while turning my head around, especially if all the assets had not completed loading.

Also, there was no treasure, only an empty chest!

Gold coins image used to fill our treasure chest
<a-plane id="fillWithGold" position="-30.53062 -4.68798 41.89179" rotation="-89.67878113608133 66.30955154608043 -113.66164852466733" width="4" height="4" color="#EBE3B6" material="src: #coins" visible="true" scale="0.261 0.117 0.407"></a-plane>

So the treasure part was easily solved by finding some free images of gold coins on the internet, duplicating them and making a good size texture of them in Photoshop. I then applied that texture to a plane with <a-plane> and put it inside the chest.

Model viewer showing our empty chest

Removing the animation and fixing the lid of the chest open involved using Blender again. This time importing the model, removing (deleting) any unneeded part (clasp). Propping (rotating) the lid open.

Model imported into Blender

Remove the animation part while exporting the glTF file by unchecking the Animations checkbox. This will reduce our file size considerably. This animation was just affecting too many polygons and taking cycles away from our rendering ticks.

Output our model with the animations box unchecked

The Assets

The last thing I did was put up another sign for the user notifying them in VR that there was treasure to be found.

Treasure Card to inform visitor

There are other things that can be done to decrease the load time of graphics, but with trade-offs.

You can also control some loading of the assets.

I did not put any “actions” on the treasure chest when you click on it, or the gold. I’ll leave that up to you, extra credit. This article is long enough!

The real prize, here are the assets in a zip file:

https://rocketvirtual.com/A-Frame_WebXR/assetsTreasureIslandVR.zip

Please have fun finding, and getting to the treasure. Which actually might be harder than finding it! I made it somewhat difficult on purpose, again by creating the navmesh to make it hard, or impossible to squeeze through very narrow spaces.

If you enjoyed this article, please clap me up, and visit my VR Blog.

Spoiler Alert: How to find the hidden treasure chest of gold coins video . . .

Treasure Reveal Video

--

--

Michael McAnally

Temporary gathering of sentient stardust. Free thinker. Evolving human. Writer, coder, and artist.