As I finish up this blog in 2021 and the last articles, I sure hope you enjoy it as much as I did writing, and coding it. Be sure to check out a long technically detailed article.
Welcome VR Coders and Creators,
You've come to the right place! I really put the polish on this blog. It represents my four years work learning and coding the latest in browser based VR, now WebXR compatible. However, many things are changing very fast in this space, and I can't guarantee everything will keep working because of all the changes in WebXR specifications, the browsers, A-Frame, Three.js, hardware, new features, etc. It's harder to hit than a moving target . . .
I've included plenty of helpful links to external content; all I can really hope for is that it helps you. After you've digested this content, please be sure to checkout my VR sandbox server, and follow me on twitter and Medium.
Over the next decade I intuit massive changes in the VR and AR landscape, hardware and its intersection with the internet global community. Remember that "open", and open source really is always better for everyone. Please help to build the metaverse! Even though some don't believe this is a goal for the WebXR spec, I personally believe WebXR can be used to start building it.
UPDATES: I'd like to emphasize some recent articles I've written about NFTs and a Metaverse Server for your VR headset using the home network with a Raspberry Pi Model 4 8GB near the very end of this blog. I've arranged things chronologically, as my skills and understanding of WebXR has evolved, all while writing about it.
The Pi is a great development environment. It has a very established “Maker Community” with a large following, and support. Also, Network-Aframe, which is a multi-user version of VR talk with WebXR, and WebRTC, enabled in a browser, allows you to communicate inside a VR headset. So setting that up with the Pi on a home network provides for “no additional cost hosting” (since you already pay for your internet connection anyway), and no latency response over wi-fi (say with an Oculus Quest 2) locally. But still, all accessible over the internet too. Providing for an alternative method of development and testing to Glitch or Cloud. More like Edge, as in "Edge Computing". Or bleeding edge, Hahah!
Still, I believe it to be an elegant affordable solution for someone on a budget, or who just likes to tinker, as well as a great learning tool for those wanting to code VR for the metaverse.
Ultimately, this little metaverse server on a Pi has the potential of opening up and combining VR with IoT for the home, and possibly even AI, if paired with something like a Google’s Coral, or integrate with Nvidia’s Jetson or Xavier in future articles.
It is now the end of 2020 and VR for the web in a browser is quickly maturing. First, there was WebVR and now there is WebXR. These specifications allow for coding of VR and AR applications inside an internet browser, which are then viewed with a connected VR headset.
Open source repositories like A-Frame provides for easy VR coding in HTML5 and JavaScript. This is a very exciting time, and I encourage you all to explore and try coding VR for the web, in whatever tools or platforms you choose!
Unfortunately aside from serious gamers, engineers, scientists and some computer enthusiasts like myself, most people at this time don't own a VR Headset! So this blog may or may not be very useful to you unless you buy one or can get access to one.
Tethered Oculus Rift, HTC Vive and Windows Mixed Reality VR headsets should all work with code on this website as well as the standalone Oculus Quest 2. If you are developing VR, I still recommend a powerful VR compatible computer with a discrete graphics card; in fact, having a variety of different types of vendor VR headsets to test on, tethered and standalone, is highly recommended to insure cross vender product compatibility and a consistent user experience, as well as doing thorough cross browser testing.
Given some time (probably in the mid-to-late 2020's) I truly believe VR headsets and AR ones will merge and become as indespensible as the mouse is for 2D screens. No more sticking your silly smart phones into cardboard, that time has come and gone.
Using A Browser To Access Virtual Reality
Make sure you allow virtual reality inside your browser. Audio too, etc. The interface will be different for each browser vendor (Chrome desktop shown).
HOW TO VISIT VR FROM THIS WEBSITE ADDRESS ( https://rocketvirtual.com ):
1. Click on the images below to visit each VR example.
2. Please wait patiently for them to load.
3. Then click the VR button in the bottom right corner of the screen when it appears.
4. Finally put on your VR headset and enjoy. For those who are already wearing a VR Headset and viewing this page from inside a WebXR enabled browser, you can skip this step.
For use of the (2) 6DoF controllers while in VR: Right controller trigger selects, Left controller trigger teleports and one of the Thumbsticks or Pad (depending on your controllers vendor) moves you around.
Coded by
Michael McAnally
on August 13, 2018HOW TO PLAY: VR hand controller required. The right controller trigger shoots objects, making them disappear. Music button plays a fun tune. Local button changes the environment.
A Space Carousel ride experience in WebXR populated with flying objects. The theme of the game experience is a merry-go-round or carousel. An ancient contraption enjoyed by children of all ages, with looping movement up and down. I instead reversed things so that the player goes up and down and things go around and around them. I believe it is a simple and good example using mostly Vanilla JavaScript.
Source Code For Space Carousel: 600 lines, commented.
<!DOCTYPE>
<html>
<head>
<meta charset="utf-8">
<title>Space Carousel (loading . . . may make u dizzy)</title>
<meta name="description" content="A Yo-yo Space Carousel ride experience in WebXR populated with flying objects. Coded By Michael McAnally, August 13, 2018 using A-Frame, updated to A-Frame 1.0.4 master on August 9, 2020."></meta>
<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" />
<!-- A-frame component libraries, look them up on Github -->
<script src="aframe-master/dist/aframe-v1.0.4.min.js"></script>
<script src="aframe-extras-master/dist/aframe-extras.min.js"></script>
<script src="aframe-environment-component-master/dist/aframe-environment-component.min.js"></script>
<script src="superframe-master/components/thumb-controls/dist/aframe-thumb-controls-component.min.js"></script>
<!-- Removed teleport for now <script src="aframe-teleport-controls-master/dist/aframe-teleport-controls.js"></script> -->
<script src="superframe-master/components/text-geometry/dist/aframe-text-geometry-component.min.js"></script>
<script src="aframe-alongpath-component-master/dist/aframe-alongpath-component.min.js" ></script>
<script src="aframe-curve-component-master/dist/aframe-curve-component.min.js"></script>
<!-- Removed lensflare for now <script src="aframe-lensflare-component-master/dist/aframe-lensflare-component.min.js"></script> -->
<!-- Add rotating advertisement -->
<script src="aframe-multisrc-component-master/dist/aframe-multisrc-component.js"></script>
<script type="text/javascript">
// Yes, I'm using some global variables here.
var numberOfArrayDimension = 9; //IMPORTANT: set to max number of first index of array dimension when adding a new array
var planets = ["forest", "yavapai", "contact", "egypt", "checkerboard", "goaland", "goldmine", "threetowers", "poison", "arches", "tron", "dream", "starry", "osiris"];
var m = 1; //counter to visit another planet
var k = 15; //counter to reset 18 objects visible again
var indexElement = 0; //reset to array index
var counttx = 0, countup = true;
var nIntervId;
// Sound effects for objects loaded
var squawk = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/duck.mp3');
var brea = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/horse.mp3');
var thrust = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/rocket.mp3');
var zap = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/raygun.mp3');
var flyscr = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/saucer.mp3');
var troar = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/trex.mp3');
var croar = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/ceratops.mp3');
var keysnd = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/keys.mp3');
var heartsnd = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/magical_sparkle.mp3');
// originally was part of a point system, now changed to informational notes when objects are shot.
function startPoints() {
//nIntervId = setInterval(fadePoints, 600);
}
function fadePoints()
{
if (countup)
{
counttx = counttx + .10;
if (counttx >= 1)
countup = false;
}
else
{
counttx = counttx - .10;
if (counttx <= 0)
countup = true;
}
document.getElementById('Points').setAttribute('material', 'opacity: ' + counttx.toString());
}
function stopPoints() {
//document.getElementById('Points').setAttribute('material', 'opacity: 0');
//clearInterval(nIntervId);
}
function changePlanet() {
m = m + 1;
if (m > planets.length) {
m = 0; //reset to array beginning
}
// change our environment, may pause play for a moment and redraw
document.getElementById('onplanet').setAttribute('environment', 'preset: ' + planets[m]);
}
// royalty free sounds
function shoot_raygun(carousel_object) {
if (indexElement > 5) {
indexElement = 1; //reset to array index
}
switch(carousel_object) {
case "horse":
//startPoints();
brea.play();
//document.getElementById('Points').setAttribute('text-geometry', 'value: ' + funFacts[0][indexElement]);
break;
case "duck":
//startPoints();
squawk.play();
//document.getElementById('Points').setAttribute('text-geometry', 'value: ' + funFacts[1][indexElement]);
break;
case "rocket":
//startPoints();
thrust.play();
//document.getElementById('Points').setAttribute('text-geometry', 'value: ' + funFacts[2][indexElement]);
break;
case "raygun":
startPoints();
zap.play();
//document.getElementById('Points').setAttribute('text-geometry', 'value: ' + funFacts[3][indexElement]);
break;
case "saucer":
//startPoints();
flyscr.play();
//document.getElementById('Points').setAttribute('text-geometry', 'value: ' + funFacts[4][indexElement]);
break;
case "trex":
//startPoints();
troar.play();
//document.getElementById('Points').setAttribute('text-geometry', 'value: ' + funFacts[5][indexElement]);
break;
case "ceratops":
//startPoints();
croar.play();
//document.getElementById('Points').setAttribute('text-geometry', 'value: ' + funFacts[6][indexElement]);
break;
case "heart":
//startPoints();
heartsnd.play();
//document.getElementById('Points').setAttribute('text-geometry', 'value: ' + funFacts[7][indexElement]);
break;
case "key":
//startPoints();
keysnd.play();
//document.getElementById('Points').setAttribute('text-geometry', 'value: ' + funFacts[8][indexElement]);
break;
default:
}
indexElement = indexElement + 1;
}
// Creates a multi-dimensional array
function createArray(length) {
var arr = new Array(length || 0),
i = length;
if (arguments.length > 1) {
var args = Array.prototype.slice.call(arguments, 1);
while(i--) arr[length-1 - i] = createArray.apply(this, args);
}
return arr;
}
// create a large 2 dimensional global array
funFacts = createArray((numberOfArrayDimension + 1), 45); // Note: first index in createArray is +1 more than the numberOfArrayDimension
// load array values
// Note: the 0 element is the object name
// horse
funFacts[0][0] = "Equus caballus: common name is\n Horse.";
funFacts[0][1] = "Horses can sleep both lying\n down and standing up.";
funFacts[0][2] = "Horses can run shortly after\n birth.";
funFacts[0][3] = "Horses have been domesticated\n for over 5000 years.";
funFacts[0][4] = "Horses have bigger eyes than\n any other mammal on land.";
funFacts[0][5] = "Horses have evolved from much\n smaller ancestors.";
// duck
funFacts[1][0] = "Anas platyrhynchos: common name\n is Duck.";
funFacts[1][1] = "A yellow rubber duck has achieved\n iconic status in western pop culture.";
funFacts[1][2] = "The rubber duck is symbolically\n linked with bathing.";
funFacts[1][3] = " ";
funFacts[1][4] = " ";
funFacts[1][5] = " ";
// rocket
funFacts[2][0] = "Rocket";
funFacts[2][1] = "The first rocket was invented by\n the Chinese around year 1200.";
funFacts[2][2] = "Rockets were originally used for\n fireworks and for rescuing people at sea.";
funFacts[2][3] = "Robert Goddard is considered the father\n of modern rocketry. He built the first\n liquid-fueled rocket in 1926.";
funFacts[2][4] = "A typical rocket produces more than a\n million pounds of thrust.";
funFacts[2][5] = "Launch rockets are actually several\n rockets linked together.";
// raygun
funFacts[3][0] = "Raygun";
funFacts[3][1] = "A raygun is a science fiction particle-beam\n weapon that fires what is usually destructive energy.";
funFacts[3][2] = "They have various alternate names:\n ray gun, death ray, beam gun, blaster,\n laser gun, laser pistol, phaser, zap gun, etc.";
funFacts[3][3] = "A very early example of a raygun is\n the Heat-Ray featured in H. G. Wells'\n novel The War of the Worlds (1898).";
funFacts[3][4] = "Science fiction during the 1920s\n described death rays.";
funFacts[3][5] = " ";
// saucer
funFacts[4][0] = "Flying Saucer";
funFacts[4][1] = "The term UFO for Unidentified Flying\n Object was coined by US Air Force officer\n Edward Ruppelt in 1952.";
funFacts[4][2] = "Between 1947 and 1969, 12,618 UFO\n sightings were reported to Project\n Blue Book.";
funFacts[4][3] = "In Roswell, New Mexico on July 8th of 1947,\n an information office publicized the recovery\n of a crashed flying disc.";
funFacts[4][4] = "The book Flying Saucers from Outer Space,\n written by author Donald E. Keyhoe in\n 1953, used the term UFO.";
funFacts[4][5] = " ";
// trex
funFacts[5][0] = "Tyrannosaurus";
funFacts[5][1] = "Tyrannosaurus commonly refered to\n as T-Rex";
funFacts[5][2] = "A person who studies dinosaurs\n is called a paleontologist.";
funFacts[5][3] = "Dinosaurs lived on earth until\n 65 million years ago.";
funFacts[5][4] = " ";
funFacts[5][5] = " ";
// ceratops
funFacts[6][0] = "Triceratops";
funFacts[6][1] = "Modern birds descended from a group\n of two-legged dinosaurs known as theropods.";
funFacts[6][2] = "Scientist believe dinosaurs became\n extinct after a large metior impacted\n the earth.";
funFacts[6][3] = " ";
funFacts[6][4] = " ";
funFacts[6][5] = " ";
// Heart
funFacts[7][0] = "heart";
funFacts[7][1] = " ";
funFacts[7][2] = " ";
funFacts[7][3] = " ";
funFacts[7][4] = " ";
funFacts[7][5] = " ";
// key
funFacts[8][0] = "key";
funFacts[8][1] = " ";
funFacts[8][2] = " ";
funFacts[8][3] = " ";
funFacts[8][4] = " ";
funFacts[8][5] = " ";
// Component to do on click.
AFRAME.registerComponent('cursor-listener', {
init: function () {
this.el.addEventListener('click', function (evt) {
//count down the opbjects
k = k - 1;
if (k < 1) {
k = 15; //reset to beginning
//return all 15 objects to view
document.getElementById('sittingDuckA').setAttribute('visible', true);
document.getElementById('sittingDuckB').setAttribute('visible', true);
document.getElementById('sittingDuckC').setAttribute('visible', true);
document.getElementById('sittingDuckD').setAttribute('visible', true);
document.getElementById('sittingDuckE').setAttribute('visible', true);
document.getElementById('sittingDuckF').setAttribute('visible', true);
document.getElementById('sittingDuckG').setAttribute('visible', true);
document.getElementById('sittingDuckH').setAttribute('visible', true);
document.getElementById('sittingDuckI').setAttribute('visible', true);
// spinning hearts
document.getElementById('sittingDuckK').setAttribute('visible', true);
document.getElementById('sittingDuckL').setAttribute('visible', true);
document.getElementById('sittingDuckM').setAttribute('visible', true);
document.getElementById('sittingDuckO').setAttribute('visible', true);
document.getElementById('sittingDuckP').setAttribute('visible', true);
document.getElementById('sittingDuckQ').setAttribute('visible', true);
//visit another planet
m = m + 1;
if (m > planets.length) {
m = 0; //reset to array beginning
}
// change our environment, may pause play for a moment and redraw
document.getElementById('onplanet').setAttribute('environment', 'preset: ' + planets[m]);
} else {
// remove clicked object from view
this.setAttribute('visible', false);
}
});
}
});
// Solves Google mute of audio problem (and it is...) 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;
});
}
})
function playSound() {
//alert("TEST Sound playing functional!!!");
}
</script>
</head>
<body>
<button id="playButton" type="button">Play Music</button>
<!-- I like the sound of this fun, joyful Carnival Banna tune for game: this "Royalty Purchase" for 10,000 downloads -->
<audio id="playAudio" autoplay loop>
<source src="https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/Carnival_Banana.mp3" type="audio/mpeg">
</audio>
<!-- to embed uncomment this and style a-scene above <a-scene embedded> -->
<a-scene cursor="rayOrigin: mouse; fuse: false" raycaster="objects: .raycastable, .clickable, a-link">
<a-assets>
<img crossorigin="anonymous" id="music-image_on" src="assets/img/music.png">
<img crossorigin="anonymous" id="planet_cntl-image" src="assets/img/planets.png">
<img crossorigin="anonymous" id="game_logo-image" src="assets/img/game_logo2.png">
<a-asset-item id="horse" src="assets/gltf/Horse_Free.glb" shadow="receive: false"></a-asset-item>
<a-asset-item id="rocket" src="assets/gltf/rocket4.glb" shadow="receive: false"></a-asset-item>
<a-asset-item id="raygun" src="assets/gltf/raygun4/raygun.glb" shadow="receive: false"></a-asset-item>
<a-asset-item id="flyingsaucer" src="assets/gltf/FlyingSaucer/fsaucer.glb" shadow="receive: false"></a-asset-item>
<a-asset-item id="trex" src="assets/gltf/Trex_base_mesh.glb" shadow="receive: false"></a-asset-item>
<a-asset-item id="triceratops" src="assets/gltf/Triceratops_base_mesh.glb" shadow="receive: false"></a-asset-item>
<a-asset-item id="duck" src="assets/gltf/Duck.glb" shadow="receive: false"></a-asset-item>
<a-asset-item id="key" src="assets/gltf/Key.glb" shadow="receive: false"></a-asset-item>
<a-asset-item id="heart" src="assets/obj/heart_lp.obj/heart_lp.obj" shadow="receive: false"></a-asset-item>
<img crossorigin="anonymous" id="right" src="assets/img/box-logo-right.png">
<img crossorigin="anonymous" id="left" src="assets/img/box-logo-left.png">
<img crossorigin="anonymous" id="top" src="assets/img/box-logo-top.png">
<img crossorigin="anonymous" id="bottom" src="assets/img/box-logo-bottom.png">
<img crossorigin="anonymous" id="front" src="assets/img/box-logo3-front.png">
<img crossorigin="anonymous" id="back" src="assets/img/box-logo-back.png">
<img crossorigin="anonymous" id="HelloIsland" src="assets/img/GrayNexus.png">
<img crossorigin="anonymous" id="blue" src="assets/img/fontcolor.png" />
<!-- Our Font -->
<a-asset-item id="optimer_bold" src="assets/fonts/optimer_bold.typeface.json"></a-asset-item>
</a-assets>
<!-- Zoom and move the camera in for dramatic effects in browser window -->
<a-entity id="camera_yoyo" animation__second="property: position;
dir: alternate;
dur: 12000;
easing: easeInSine;
loop: true;
from: 0 1.6 0;
to: 0 30 0" begin="20000" >
<a-camera look-controls wasd-controls mouse-cursor id="camera_zoom" position="0 1.6 0" user-height="1.6"
animation="property: position;
dur: 15000;
from: 0 0 50;
to: 0 1.6 0;
easing: easeInOutSine;
loop: false;
direction: normal" >
<!-- This is where we output text -->
<a-entity id="Points" text-geometry="value: ; font: #optimer_bold; bevelEnabled: false; curveSegments: 12; size: .75; height: 0;" material="color:blue; metalness:1; roughness: 0; transparent: false; opacity: 1" position="-.4 -.1 -.4" scale=".03 .03 .03" ></a-entity>
</a-camera>
<a-entity class="leftController" hand-controls="hand: left; handModelStyle: lowPoly; color: #15ACCF" visible="true"></a-entity>
<a-entity class="rightController" hand-controls="hand: right; handModelStyle: lowPoly; color: #15ACCF" laser-controls raycaster="showLine: true; far: 40; interval: 0; objects: .clickable, a-link;" line="color: lawngreen; opacity: 0.5" visible="true"></a-entity>
</a-entity>
<a-box id="planetButton" class="clickable" position = "0 26 -14" material="src: #planet_cntl-image" scale="1.25 1.25 .25" onclick="changePlanet();"
animation="property: position;
dir: alternate;
dur: 20000;
easing: easeInSine;
loop: true;
to: 0 5 -17" begin="8000"></a-box>
<a-box id="playButton" class="clickable" position = "-2 26 -14" material="src: #music-image_on" scale="1.25 1.25 .25" onclick="playSound();" audiohandler ></a-box>
<a-box id="gameLogo" position="0 29.5 -18" scale="5.48 4.76 5.36" multisrc="srcs:#right,#left,#top,#bottom,#front,#back" animation="property: rotation; dur: 50000; fill: forward; to: 0 360 0; repeat: indefinite" opacity="1"></a-box>
<!-- Setup tracs A-I for the carousel. Not a true circle, but it works... Varying the y-axis for height in air of object following trac. Could be more complex tracks in the future. -->
<a-curve id="tracA" >
<a-curve-point position="0 1 8" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="5 1 6" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="7 1 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="5 1 -5" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="0 1 -7" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-6 1 -5" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-8 1 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-6 1 6" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="0 1 8" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
</a-curve>
<a-curve id="tracB" curve="">
<a-curve-point position="0 7 2" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-6 7 4" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-8 7 10" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-6 7 15" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="0 7 17" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="6 7 14" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="7 7 10" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="6 7 5" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="0 7 2" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
</a-curve>
<a-curve id="tracC" curve="">
<a-curve-point position="12 5 11" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="14 5 6" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="10 5 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="6 5 -1" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="0 5 2" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-2 5 7" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="1 5 12" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="6 5 14" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="12 5 11" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
</a-curve>
<a-curve id="tracD" curve="">
<a-curve-point position="9 20 -7" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="3 20 -5" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="1 20 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="5 20 6" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="9 20 8" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="14 20 6" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="16 20 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="14 20 -5" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="9 20 -7" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
</a-curve>
<a-curve id="tracE" curve="">
<a-curve-point position="0 1 -12" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-2 1 -7" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="0 1 -3" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="6 1 1" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="9 1 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="13 1 -4" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="13 1 -9" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="8 1 -14" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="0 1 -12" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
</a-curve>
<a-curve id="tracF" curve="">
<a-curve-point position="7 14 -9" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="5 14 -15" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="0 14 -17" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-6 14 -15" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-8 14 -9" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-4 14 -3" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="3 14 -3" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="7 14 -9" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
</a-curve>
<a-curve id="tracG" curve="">
<a-curve-point position="1 24 -7" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-3 24 -13" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-11 24 -13" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-14 24 -9" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-13 24 -2" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-7 24 1" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-3 24 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="0 24 -3" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="1 24 -7" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
</a-curve>
<a-curve id="tracH" curve="">
<a-curve-point position="-2 10 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-4 10 -5" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-10 10 -7" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-15 10 -5" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-17 10 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-15 10 6" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-10 10 8" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-4 10 6" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-2 10 0" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
</a-curve>
<a-curve id="tracI" curve="">
<a-curve-point position="-12 1 1" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-14 1 9" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-9 1 14" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-1 1 12" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="1 1 7" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-1 1 2" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-6 1 -1" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
<a-curve-point position="-12 1 1" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="false"></a-curve-point>
</a-curve>
<!-- objects following the trac. Letter A-I corresponds to trac curve above. Collada and glTF objects loaded here... Note: All objects are royalty free and/or created by me. -->
<a-entity id="sittingDuckA" class="clickable" gltf-model="#horse" alongpath="curve:#tracA;loop:true;dur:12000;rotate:true" scale="" position="-4.27 1 -6" shadow="receive:false" rotation="-24 -90 90" onclick="stopPoints();shoot_raygun('horse');" cursor-listener></a-entity>
<a-entity id="sittingDuckB" class="clickable" gltf-model="#duck" alongpath="curve:#tracB;loop:true;dur:14000;rotate:true" position="4.29 1 15.43" shadow="receive:false" rotation="0 261.96 0" scale="1 1 1" animation__rotate="property:rotation;dur:3000;easing:linear;loop:true;from:0 0 0;to:0 360 0" onclick="stopPoints();shoot_raygun('duck');" cursor-listener></a-entity>
<a-entity id="sittingDuckC" class="clickable" gltf-model="#key" alongpath="curve:#tracC;loop:true;dur:12000;rotate:true" position="-1.77 1 5.17" shadow="receive:false" rotation="-7.28 -89.07 82.71" scale="1.3 1.3 1.3" onclick="stopPoints();shoot_raygun('key');" cursor-listener></a-entity>
<a-entity id="sittingDuckD" class="clickable" gltf-model="#flyingsaucer" alongpath="curve:#tracD;loop:true;dur:12000;rotate:true" position="12.44 1 7.12" shadow="receive:false" rotation="0 261.96 0" scale="4.5 4.5 4.5" animation__rotate="property:rotation;dur:3000;easing:linear;loop:true;from:0 0 0;to:0 360 0" onclick="stopPoints();shoot_raygun('saucer');" cursor-listener></a-entity>
<a-entity id="sittingDuckE" class="clickable" gltf-model="#horse" scale="" alongpath="curve:#tracE;loop:true;dur:12000;rotate:true" position="11.90 1 -2.37" shadow="receive:false" rotation="0 138.07 0" onclick="stopPoints();shoot_raygun('horse');" cursor-listener></a-entity>
<a-entity id="sittingDuckF" class="clickable" gltf-model="#trex" alongpath="curve:#tracF;loop:true;dur:9000;rotate:true" position="-7.92 1 -8.56" shadow="receive:false" rotation="0 13.05 0" animation__rotate="property:rotation;dur:3000;easing:linear;loop:true;from:0 0 0;to:0 360 0" scale="1 1 1" onclick="stopPoints();shoot_raygun('trex');" cursor-listener></a-entity>
<a-entity id="sittingDuckG" class="clickable" gltf-model="#triceratops" alongpath="curve:#tracG;loop:true;dur:12000;rotate:true" position="-9.22 1 0.45" shadow="receive:false" rotation="0 70.51 0" scale="0.35 0.35 0.35" onclick="stopPoints();shoot_raygun('ceratops');" cursor-listener></a-entity>
<a-entity id="sittingDuckH" class="clickable" gltf-model="#rocket" alongpath="curve:#tracH;loop:true;dur:14000;rotate:true" position="-16.13 1 4.16" shadow="receive:false" rotation="0 23.64 0" scale="3.5 3.5 3.5" onclick="stopPoints();shoot_raygun('rocket');" cursor-listener></a-entity>
<a-entity id="sittingDuckI" class="clickable" gltf-model="#raygun" scale="" alongpath="curve:#tracI;loop:true;dur:12000;rotate:true" position="0.98 1 6.65" shadow="receive:false" rotation="0 -175.74 0" onclick="stopPoints();shoot_raygun('raygun');" cursor-listener></a-entity>
<!-- stationary hearts -->
<a-obj-model id="sittingDuckK" class="clickable" position="2.5 3.6 -8" scale="0.02 0.02 0.02" src="#heart" material="color: #F585D3"
animation__rotate="property: rotation; dur: 5000; easing: linear; loop: true; to: 0 360 0" shadow="receive:false" onclick="stopPoints();shoot_raygun('heart');" cursor-listener></a-obj-model>
<a-obj-model id="sittingDuckL" class="clickable" position="1.5 3.6 -8" scale="0.03 0.03 0.03" src="#heart" material="color: #B876F6"
animation__rotate="property: rotation; dur: 5000; easing: linear; loop: true; to: 0 360 0" shadow="receive:false" onclick="stopPoints();shoot_raygun('heart');" cursor-listener></a-obj-model>
<a-obj-model id="sittingDuckM" class="clickable" position="0.1 3.6 -8" scale="0.04 0.04 0.04" src="#heart" material="color: #E9EDF8"
animation__rotate="property: rotation; dur: 5000; easing: linear; loop: true; to: 0 360 0" shadow="receive:false" onclick="stopPoints();shoot_raygun('heart');" cursor-listener></a-obj-model>
<a-obj-model id="sittingDuckO" class="clickable" position="2.5 8.6 -6" scale="0.02 0.02 0.02" src="#heart" material="color: #F44268"
animation__rotate="property: rotation; dur: 5000; easing: linear; loop: true; to: 0 360 0" shadow="receive:false" onclick="stopPoints();shoot_raygun('heart');" cursor-listener></a-obj-model>
<a-obj-model id="sittingDuckP" class="clickable" position="1.5 8.6 -6" scale="0.03 0.03 0.03" src="#heart" material="color: #F44268"
animation__rotate="property: rotation; dur: 5000; easing: linear; loop: true; to: 0 360 0" shadow="receive:false" onclick="stopPoints();shoot_raygun('heart');" cursor-listener></a-obj-model>
<a-obj-model id="sittingDuckQ" class="clickable" position="0.1 8.6 -6" scale="0.04 0.04 0.04" src="#heart" material="color: #F44268"
animation__rotate="property: rotation; dur: 5000; easing: linear; loop: true; to: 0 360 0" shadow="receive:false" onclick="stopPoints();shoot_raygun('heart');" cursor-listener></a-obj-model>
<!-- Tune the world environment to the desired settings -->
<a-entity id="onplanet" environment="preset: forest; shadow: true; dressingAmount: 150;fog:0.65"></a-entity>
<a-entity class="environment" position="-1.2 2.94 3.03" light="intensity:0.5;castShadow:true;shadowCameraLeft:-20;shadowCameraBottom:-20;shadowCameraRight:20;shadowCameraTop:20" visible=""></a-entity>
<a-entity class="environmentDressing environment" visible="false"></a-entity>
</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');
});
});
</script>
</body>
</html>
Coded by
Michael McAnally
on January 29, 2019GAME PLAY: Arrow keys move around . . . VR hand controller recommended. The right controller trigger shoots hearts, making them disappear. Music button plays a nice love tune that accelerates as you shoot the hearts. The left hand controller gets a text love message. The environment changes after hearts are shot, then it resets the song speed.
Source Code For Valentine VR-Card Game: 463 lines, commented.
For those without access to VR Headsets click here to view on your desktop or laptop PC, not recommended for mobile. Drag with left mouse button to rotate view. Click spheres to teleport. WASD keys move menu around. ESC key to show your mouse cursor.
My avatar is a tripod!
360° Cameras are another way to experience VR immersively. Many newer high resolution models are coming to market. So I took it upon myself to develop some sample code for an immersive UI/UX.
Source Code For Mind Palace 360VR: 420 lines, commented.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width,shrink-to-fit=no,user-scalable=no,maximum-scale=1,minimum-scale=1">
<title>Mind Palace 360VR (Heavy graphic loading . . . please wait)</title>
<meta name="description" content="This is a 360 VR Mind Palace structure."></meta>
<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" />
<!-- A-frame component libraries, look them up on Github -->
<script src="aframe-master/dist/aframe-v1.1.0.min.js"></script>
<script src="aframe-rounded-master/dist/aframe-rounded-component.min.js"></script>
<script src="superframe-master/components/text-geometry/dist/aframe-text-geometry-component.min.js"></script>
<script src="aframe-extras-master/dist/aframe-extras.min.js"></script>
<script type="text/javascript">
var Speech = true;
var audio1 = new Audio('assets/wav/action.wav');
var audio2 = new Audio('assets/wav/swoosh.wav');
function speakInfo(narration) {
var audio_msg = new SpeechSynthesisUtterance(narration);
if (Speech === true) {
window.speechSynthesis.speak(audio_msg);
}
}
function changeOrb(orb_num) {
// change our orb sky
document.getElementById('orbSky').setAttribute('material', 'src: #orb' + orb_num.toString());
}
// audio 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;
});
}
})
function playSwoosh() {
audio2.play();
}
function playBlip() {
audio1.play();
}
function playSound() {
//alert("TEST Sound playing functional!!!");
}
</script>
</head>
<body>
<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 timeout="30000" >
<!-- mixin used to animate selected orbs -->
<a-mixin id="marble" scale=".30 .30 .30" material="color: white" animation__rotation="startEvents: mouseenter; pauseEvents: mouseleave; resumeEvents: mouseenter; property: rotation; to: 0 360 0; loop: true; dur: 10000" animation__mouseenter="startEvents: mouseenter; pauseEvents: mouseleave; resumeEvents: mouseenter; property: components.material.material.color; type: color; to: white; dur: 500; " animation__mouseleave="property: components.material.material.color; type: color; to: gray; startEvents: mouseleave; dur: 500;" shadow ></a-mixin>
<!-- Replace with eight 360 images of your choosing -->
<img crossorigin="anonymous" id="orb1" src="assets/360/image/theBeach.jpg">
<img crossorigin="anonymous" id="orb2" src="assets/360/image/SAM_101_0161.jpg">
<img crossorigin="anonymous" id="orb3" src="assets/360/image/SAM_101_0346.jpg">
<img crossorigin="anonymous" id="orb4" src="assets/360/image/SAM_101_0128.jpg">
<img crossorigin="anonymous" id="orb5" src="assets/360/image/SAM_101_0152.jpg">
<img crossorigin="anonymous" id="orb6" src="assets/360/image/SAM_101_0370.jpg">
<img crossorigin="anonymous" id="orb7" src="assets/360/image/360_0425.jpg">
<img crossorigin="anonymous" id="orb8" src="assets/360/image/SAM_101_0109.jpg">
<!-- Replace with eight thumbnails corresponding to the 360 images above to be wrapped around orbs -->
<img crossorigin="anonymous" id="orbthumb1" src="assets/360/image/thumb/theBeach_thumb.jpg">
<img crossorigin="anonymous" id="orbthumb2" src="assets/360/image/thumb/SAM_101_0161_thumb.jpg">
<img crossorigin="anonymous" id="orbthumb3" src="assets/360/image/thumb/SAM_101_0346_thumb.jpg">
<img crossorigin="anonymous" id="orbthumb4" src="assets/360/image/thumb/SAM_101_0128_thumb.jpg">
<img crossorigin="anonymous" id="orbthumb5" src="assets/360/image/thumb/SAM_101_0152_thumb.jpg">
<img crossorigin="anonymous" id="orbthumb6" src="assets/360/image/thumb/SAM_101_0370_thumb.jpg">
<img crossorigin="anonymous" id="orbthumb7" src="assets/360/image/thumb/360_0425_thumb.jpg">
<img crossorigin="anonymous" id="orbthumb8" src="assets/360/image/thumb/SAM_101_0109_thumb.jpg">
<!-- Replace MP4 video with your own -->
<video crossorigin="anonymous" id="video-src" src="assets/video/MP_beach.mp4"></video>
<!-- Our font -->
<a-asset-item id="optimer_bold" src="assets/fonts/optimer_bold.typeface.json"></a-asset-item>
<!-- Controls for the video player -->
<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" >
<img crossorigin="anonymous" id="music-image_on" src="assets/img/music.png">
</a-assets>
<a-entity id="mouseCursor" cursor="rayOrigin: mouse"></a-entity>
<a-sky id="orbSky" material="src: #orb1" rotation="0 -90 0" ></a-sky>
<!-- Title of the Mind Palace -->
<a-entity id="NodeName" position="-1.04751 -1.79712 -2.84702" rotation="-39.4 0 0" text-geometry="value: Mind Palace 360VR; opacity: .5; size: .175; font: #optimer_bold" material="color: #F4A460"></a-entity>
<!-- Basic movement and selection -->
<a-entity id="cameraRig" movement-controls="" position="0 0 5" rotation="0 0 0">
<!-- camera -->
<a-entity id="head" camera="active: true" look-controls="" position="0 1.6 0" ></a-entity>
<a-entity class="leftController" hand-controls="hand: left; handModelStyle: lowPoly; color: #15ACCF" visible="true"></a-entity>
<a-entity class="rightController" hand-controls="hand: right; handModelStyle: lowPoly; color: #15ACCF" laser-controls raycaster="showLine: true; far: 10; interval: 0; objects: .clickable, a-link;" line="color: #7cfc00; opacity: 0.5" visible="true"></a-entity>
</a-entity>
<!-- This lets us play music if browser allows it, enable audio -->
<a-box id="playButton" class="clickable" position="2.22428 -1.6294 -4.10106" rotation="-27.121 0 0" material="src: #music-image_on" scale="0.25 0.25 0.25" onclick="playBlip();" audiohandler shadow ></a-box>
<!-- Video Label -->
<a-entity id="VideoLabel" class="clickable" position="-0.40113 -0.598 -4.5" rotation="-24.697 0 0" text-geometry="value: Coastline Video; opacity: .5; size: 0.09; font: #optimer_bold" onclick="playBlip();speakInfo('To Play Video Select The Green Play Button. Other Video Controls Will Appear.');" material="color: #F4A460"></a-entity>
<!-- Replace labels and speak info for the orbs if you want them -->
<a-entity id="OrbName_place1" class="clickable" position="-2.25805 -0.598 -4.5" rotation="-24.697 0 0" text-geometry="value: Beach; size: 0.09; font: #optimer_bold" material="color: #F4A460" onclick="playBlip();speakInfo('Beach.');"></a-entity>
<a-entity id="OrbName_place2" class="clickable" position="-1.252 -0.598 -4.5" rotation="-24.697 0 0" text-geometry="value: Waterfront; size: 0.09; font: #optimer_bold" material="color: #F4A460" onclick="playBlip();speakInfo('Waterfront and Bay Bridge. San Francisco, California');"></a-entity>
<a-entity id="OrbName_place3" class="clickable" position="0.723 -0.598 -4.5" rotation="-24.697 0 0" text-geometry="value: Castro; size: 0.09; font: #optimer_bold" material="color: #F4A460" onclick="playBlip();speakInfo('The Castro.');"></a-entity>
<a-entity id="OrbName_place4" class="clickable" position="1.687 -0.598 -4.5" rotation="-24.697 0 0" text-geometry="value: Downtown; size: 0.09; font: #optimer_bold" material="color: #F4A460" onclick="playBlip();speakInfo('Downtown San Francisco.');"></a-entity>
<a-entity id="OrbName_place5" material="color: #F4A460" class="clickable" position="-1.775 -1.110 -4" rotation="-24.697 0 0" text-geometry="value: Pier; size: 0.09; font: #optimer_bold" onclick="playBlip();speakInfo('A Pier in San Francisco Bay.');"></a-entity>
<a-entity id="OrbName_place6" material="color: #F4A460" class="clickable" position="-0.775 -1.110 -4" rotation="-24.697 0 0" text-geometry="value: Salesforce; size: 0.09; font: #optimer_bold" onclick="playBlip();speakInfo('Street level and Salesforce Tower.');"></a-entity>
<a-entity id="OrbName_place7" material="color: #F4A460" class="clickable" position="0.225 -1.110 -4" rotation="-24.697 0 0" text-geometry="value: Academy; size: 0.09; font: #optimer_bold" onclick="playBlip();speakInfo('Academy of Science.');"></a-entity>
<a-entity id="OrbName_place8" material="color: #F4A460" class="clickable" position="1.192 -1.110 -4" rotation="-24.697 0 0" text-geometry="value: Lands End; size: 0.09; font: #optimer_bold" onclick="playBlip();speakInfo('Lands End and San Francisco Golden Gate Bridge.');"></a-entity>
<!-- Actual orbs and their placement -->
<a-sphere id="orb_place1" class="clickable" mixin="marble" position="-2 -1 -4.5" rotation="0 -125 0" material="src: #orbthumb1" onclick="playSwoosh();changeOrb(1);" ></a-sphere>
<a-sphere id="orb_place2" class="clickable" mixin="marble" position="-1 -1 -4.5" rotation="0 85 0" material="src: #orbthumb2" onclick="playSwoosh();changeOrb(2);" ></a-sphere>
<a-sphere id="orb_place4" class="clickable" mixin="marble" position="1 -1 -4.5" rotation="0 -75 0" material="src: #orbthumb3" onclick="playSwoosh();changeOrb(3);" ></a-sphere>
<a-sphere id="orb_place5" class="clickable" mixin="marble" position="2 -1 -4.5" rotation="0 -90 0" material="src: #orbthumb4" onclick="playSwoosh();changeOrb(4);" ></a-sphere>
<a-sphere id="orb_place6" class="clickable" mixin="marble" position="-1.5 -1.5 -4" rotation="0 -75 0" material="src: #orbthumb5" onclick="playSwoosh();changeOrb(5);" ></a-sphere>
<a-sphere id="orb_place7" class="clickable" mixin="marble" position="-.5 -1.5 -4" rotation="0 -75 0" material="src: #orbthumb6" onclick="playSwoosh();changeOrb(6);" ></a-sphere>
<a-sphere id="orb_place8" class="clickable" mixin="marble" position=".5 -1.5 -4" rotation="0 45 0" material="src: #orbthumb7" onclick="playSwoosh();changeOrb(7);" ></a-sphere>
<a-sphere id="orb_place9" class="clickable" mixin="marble" position="1.5 -1.5 -4" rotation="0 -85 0" material="src: #orbthumb8" onclick="playSwoosh();changeOrb(8);" ></a-sphere>
<!-- Translucent base for orbs -->
<a-rounded radius="0.1" top-left-radius="0.6" top-right-radius="0.6" bottom-left-radius="0.6" bottom-right-radius="0.6" position="-2.667 -2.20165 -4.13398" scale="0.661 0.218 0.00001" rotation="-50 0 0" width="8" height="8" color="#657383" opacity=".35" shadow="" rounded=""></a-rounded>
<!-- 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.00193 1.02935 -5.4166" rotation="0 0 0" scale="0.564 0.697 1" width="8" height="4" rotation="0 0 0" visible="false"></a-video>
<!-- END MEDIAS HOLDER -->
<!-- CONTROLS -->
<a-image class="clickable" id="control-back" width="0.4" height="0.4" src="#seek-back" position="-0.37171 -0.92581 -4.49178" rotation="0 0 0" visible="true" 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.03166 -0.92581 -4.49178" rotation="0 0 0"></a-image>
<a-image class="clickable" id="control-volume" width="0.4" height="0.4" src="#volume-mute" position="0.42174 -0.92581 -4.49178" rotation="0 0 0" visible="true" 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.03516 -0.5536 -5.48963" rotation="0 0 0">
<a-plane id="progress-bar-track" width="4" height="0.1" color="gray" position="" opacity="0.2" visible="false" material="" geometry=""></a-plane>
<a-plane id="progress-bar-fill" width="3.0772968174269693" height="0.1" color="#7198e5" position="-0.4613515912865154 0 0.01438" geometry="" visible="false" material=""></a-plane>
</a-entity>
<!-- END PROGRESSBAR -->
</a-scene>
<!-- A Video Player Script (still works, when permissions enabled) -->
<script type="text/javascript">
//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>
Here are links to the A-Frame GitHub code I use most. I've standardized off of A-frame version 1.0.4 which still works with aframe-extras and the navmesh and movement-controls (later versions break this functionality somewhat, of course unless it has been fixed recently - posted July 2021) . . .
In my examples I have copied over the framework component libraries above to my server. For those who don't host their own servers, CDNs are the answer and available for those who want to use something like Glitch to just initially learn A-Frame.
I would be remiss if I didn't mention the new Oculus Quest 2. So, since I write this blog, here are my personal opinions.
Can you say Walled Garden? I have found this video above to be basically an honest and fair review.
My personal VR Headset is the Samsung HMD Odyssey tethered to a PC with a minimum spec NVIDIA GeForce GTX 1060 graphics card (affordable at this time) on a Windows Mixed Reality Windows 10 PC. It works for me, and the tethered cable gives me much higher performance than my previous Samsung Gear VR, which was compatible with the Oculus Go, way back when. However, with the tethered cable attached the Oculus Quest 2 should perform well for VR games and for developers alike.
If you are looking to monetize on the Horizon platform, perhaps this is reason enough to join. And the recent Workrooms is interesting for enterprise VR play. Recent Update: I have since purchased an Oculus Quest 2 and retuned many of my apps for use with it. If I could afford it, I'd like a Valve Index on the SteamVR platform.
Multi-user access to the same web page in virtual reality is possible with and without a headset (VR hardware). See my Medium Article for details on setting up your own server with NAF.