Advanced Hello World for A-Frame

Michael McAnally
18 min readOct 2, 2020

Important Code Update:

I wrote a previous article in which I described where to begin with VR in a browser. This took the popular A-Frame Hello World and extended it a bit, with environment and the ability to move around in that environment with the VR controllers, teleport, etc.

Now this article will go much further. We are going to include JavaScript which can select and activate actions on objects in our VR scene. This will include some cool tracked animation with a quacking duck! A 3D model of the duck will loop on a track around us. When that duck gets too annoying you can shoot it with your laser pointer. No real animals will be harmed in this article.

Setup

Let’s begin . . . In order to make this article easier to read and for me to write, I’m going to give you a lot of code upfront. It may be a little confusing at first, but I will explain that code and you can modify it to see what happens.

Ok, so here is the Advance Hello World 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>Advanced Hello World A-Frame WebXR</title>
<meta name="description" content="Advanced Hello World, WebXR! A-frame provides a hello world that is really remarkable, however you usually need to have controller selection and environment with movement. With this example you get what works across the most headsets and controllers.">
<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" />
<!-- *** CHANGE THESE TO COMPONENTS ON YOUR SERVER *** -->
<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="aframe-teleport-controls-master/dist/aframe-teleport-controls.js"></script>
<script src="superframe-master/components/thumb-controls/dist/aframe-thumb-controls-component.min.js"></script>
<!-- These are added to provide for 3D text and tracked movement of the duck 3D model -->
<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>
<!-- for creating Navmesh... Also uncomment a-scene inspector-plugin-recast
<script src="https://recast-api.donmccurdy.com/aframe-inspector-plugin-recast.js"></script>
-->
<!-- The JavaScript below provides for selection and action on objects, environment and audio -->
<script type="text/javascript">
// Audio squawk for the duck
var squawk = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/duck.mp3');
// Function to execute onclick
function fire_laser() {
// Make Duck Quack sound
squawk.play();
//Disappear Duck
document.getElementById('movingDuck').setAttribute('visible', 'false');
}
function doCylinder() {

// Bring back visibility of Duck and Cube, turn Sphere red again
document.getElementById('movingDuck').setAttribute('visible', true);
document.getElementById('cube').setAttribute('visible', true);
document.getElementById('sphere').setAttribute('color', 'red');
}
function doCube() {

// Make Sphere green, disappear Cube for now
document.getElementById('sphere').setAttribute('color', 'green');
document.getElementById('cube').setAttribute('visible', false);
}
// Component to do on click.
AFRAME.registerComponent('click-listener', {
init: function () {
this.el.addEventListener('click', function (evt) {// remove clicked object from view
//this.setAttribute('visible', false);

});
}
});
// Solves Google Chrome mute of 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;
});
}
})
</script></head>
<body>
<button id="playButton" type="button">Play Music</button>
<!-- The music sound of the following open source classical Mozart .mp3 file. If you replace, make sure it's open source or you have your own rights. -->
<audio id="playAudio" autoplay loop>
<source src="https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/MozartALittleNightMusic.mp3" type="audio/mpeg">
</audio>
<!-- used for creating Navmesh... see javascript component above <a-scene inspector-plugin-recast> https://github.com/donmccurdy/aframe-inspector-plugin-recast--><a-scene background="color: #FAFAFA"><a-assets>
<!-- Our font -->
<a-asset-item id="optimerBoldFont" src="assets/fonts/optimer_bold.typeface.json"></a-asset-item>
<!-- Duck 3D GltF model -->
<a-asset-item id="duck" src="assets/gltf/Duck.glb"></a-asset-item>
</a-assets>
<!-- nav-mesh: protecting us from running thru sphere, cube and cylinder -->
<a-entity id="navmesh-Hello" gltf-model="assets/gltf/AdvHelloWorldnavmesh.gltf" 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 0 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" teleport-controls="cameraRig: #cameraRig; teleportOrigin: #head; button: trigger; type: line; curveShootingSpeed: 18; collisionEntities: #navmesh-Hello; landingMaxAngle: 60" 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>
<!-- Normal Hello World objects modified to respond to onclick and audio -->
<a-box id="cube" class="clickable" position="-1 0.66921 -3" rotation="0 45 0" color="#4CC3D9" visible="true" shadow onclick="doCube();" click-listener></a-box>
<a-sphere id="sphere" class="clickable" position="0 1.44508 -5" radius="1.25" color="#EF2D5E" shadow audiohandler></a-sphere>
<a-cylinder id="cylinder" class="clickable" position="1 0.8993 -3" radius="0.5" height="1.5" color="#FFC65D" shadow onclick="doCylinder();" click-listener></a-cylinder>
<a-plane position="0 0.08958 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow="recieve: true" ></a-plane >
<!-- Some 3D Text -->
<a-entity position="-3.44 2.801 -5.127" text-geometry="value: Advanced" material="color: #EC4DF4"></a-entity>
<a-entity position="-0.05 2.793 -5.090" text-geometry="value: Hello World; font: #optimerBoldFont" material="color: #F94DA2"></a-entity>
<a-entity id="movingDuck" class="clickable" gltf-model="#duck" alongpath="curve:#track;loop:true;dur:14000;rotate:true" position="0 1.6 -5" shadow="receive:false" scale="1 1 1" animation__rotate="property: rotation; dur: 2000; easing: linear; loop: true; to: 0 360 0" shadow onclick="fire_laser();" cursor-listener></a-entity><!-- A track for our Duck to follow -->
<a-curve id="track" >
<a-curve-point position="0 1 8" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="true"></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="true"></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="true"></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="true"></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="true"></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="true"></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="true"></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="true"></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="true"></a-curve-point>
</a-curve>
<!-- The track line in red -->
<a-draw-curve curveref="#track" material="shader: line; color: red;"></a-draw-curve>
<!-- Our Environment -->
<a-entity environment="preset: forest; dressing: mushrooms; dressingColor: #6e1d8b;" shadow="recieve: true"></a-entity>
<!-- A light in the scene casting shadows -->
<a-entity light="intensity: 0.6; castShadow: true; shadowCameraLeft: -10; shadowCameraBottom: -10; shadowCameraRight: 10; shadowCameraTop: 10; shadowCameraVisible: true" position="9.9649 17.32329 13.93447"></a-entity>
</a-scene>
</body>
</html>

You can try that code out here on my server by opening the link in a new tab below:

For those with a laptop or desktop, you can move around in the scene with the WASD keys on the keyboard and turn around by dragging in the center of the screen with your left mouse button.

If you enter the scene with your VR headset by clicking on the VR button in the bottom right corner of the screen, you will be immersed inside the 360 degree environment of VR! Either way, you’ll see something like this:

To make this easy, I’m going to use line numbers to describe parts of the code. So copy and past the above code into an editor of your choice with line numbers turned on OR download a free one off the internet called Sublime here.

Either way, you should end up with the code looking something like this (all 131 lines of it):

131 lines of numbered code in a editor

Lines 13–17 and 19–21 are the A-Frame JavaScript component libraries used to achieve various aspects of VR in the browser. If these don’t load, things just don’t work! Now, if you have access to a server on the internet, with https:// (required), you will have copied these files from GitHub onto your server and referenced them accordingly.

OR

Since this is an advanced article, if you don’t have access to a server, but can install on a local machine Node.js, then follow these instructions to setup your localhost:2000 system here. Also, make sure you use the code below instead of the code above, because it has client/ in places where it needs to be, to simulate a server locally with a localhost.

<!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>Advanced Hello World A-Frame WebXR</title>
<meta name="description" content="Advanced Hello World, WebXR! A-frame provides a hello world that is really remarkable, however you usually need to have controller selection and environment with movement. With this example you get what works across the most headsets and controllers.">
<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" />
<!-- *** CHANGE THESE TO COMPONENTS ON YOUR SERVER *** -->
<script src="client/aframe-master/dist/aframe-v1.0.4.min.js"></script>
<script src="client/aframe-environment-component-master/dist/aframe-environment-component.min.js"></script>
<script src="client/aframe-extras-master/dist/aframe-extras.min.js"></script>
<script src="client/aframe-teleport-controls-master/dist/aframe-teleport-controls.js"></script>
<script src="client/superframe-master/components/thumb-controls/dist/aframe-thumb-controls-component.min.js"></script>
<!-- These are added to provide for 3D text and tracked movement of the duck 3D model -->
<script src="client/superframe-master/components/text-geometry/dist/aframe-text-geometry-component.min.js"></script>
<script src="client/aframe-alongpath-component-master/dist/aframe-alongpath-component.min.js" ></script>
<script src="client/aframe-curve-component-master/dist/aframe-curve-component.min.js"></script>
<!-- for creating Navmesh... Also uncomment a-scene inspector-plugin-recast
<script src="https://recast-api.donmccurdy.com/aframe-inspector-plugin-recast.js"></script>
-->
<!-- The JavaScript below provides for selection and action on objects, environment and audio -->
<script type="text/javascript">
// Audio squawk for the duck
var squawk = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/duck.mp3');
// Function to execute onclick
function fire_laser() {
// Make Duck Quack sound
squawk.play();
//Disappear Duck
document.getElementById('movingDuck').setAttribute('visible', 'false');
}
function doCylinder() {

// Bring back visibility of Duck and Cube, turn Sphere red again
document.getElementById('movingDuck').setAttribute('visible', true);
document.getElementById('cube').setAttribute('visible', true);
document.getElementById('sphere').setAttribute('color', 'red');
}
function doCube() {

// Make Sphere green, disappear Cube for now
document.getElementById('sphere').setAttribute('color', 'green');
document.getElementById('cube').setAttribute('visible', false);
}
// Component to do on click.
AFRAME.registerComponent('click-listener', {
init: function () {
this.el.addEventListener('click', function (evt) {
// remove clicked object from view
//this.setAttribute('visible', false);

});
}
});
// Solves Google Chrome mute of 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;
});
}
})
</script>
</head>
<body>
<button id="playButton" type="button">Play Music</button>
<!-- The music sound of the following open source classical Mozart .mp3 file. If you replace, make sure it's open source or you have your own rights. -->
<audio id="playAudio" autoplay loop>
<source src="https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/MozartALittleNightMusic.mp3" type="audio/mpeg">
</audio>
<!-- used for creating Navmesh... see javascript component above <a-scene inspector-plugin-recast> https://github.com/donmccurdy/aframe-inspector-plugin-recast-->
<a-scene background="color: #FAFAFA">
<a-assets>
<!-- Our font -->
<a-asset-item id="optimerBoldFont" src="client/assets/fonts/optimer_bold.typeface.json"></a-asset-item>
<!-- Duck 3D GltF model -->
<a-asset-item id="duck" src="client/assets/gltf/Duck.glb"></a-asset-item>
</a-assets>
<!-- nav-mesh: protecting us from running thru sphere, cube and cylinder -->
<a-entity id="navmesh-Hello" gltf-model="client/assets/gltf/AdvHelloWorldnavmesh.gltf" 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 0 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" teleport-controls="cameraRig: #cameraRig; teleportOrigin: #head; button: trigger; type: line; curveShootingSpeed: 18; collisionEntities: #navmesh-Hello; landingMaxAngle: 60" 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>
<!-- Normal Hello World objects modified to respond to onclick and audio -->
<a-box id="cube" class="clickable" position="-1 0.66921 -3" rotation="0 45 0" color="#4CC3D9" visible="true" shadow onclick="doCube();" click-listener></a-box>
<a-sphere id="sphere" class="clickable" position="0 1.44508 -5" radius="1.25" color="#EF2D5E" shadow audiohandler></a-sphere>
<a-cylinder id="cylinder" class="clickable" position="1 0.8993 -3" radius="0.5" height="1.5" color="#FFC65D" shadow onclick="doCylinder();" click-listener></a-cylinder>
<a-plane position="0 0.08958 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow="recieve: true" ></a-plane >
<!-- Some 3D Text -->
<a-entity position="-3.44 2.801 -5.127" text-geometry="value: Advanced" material="color: #EC4DF4"></a-entity>
<a-entity position="-0.05 2.793 -5.090" text-geometry="value: Hello World; font: #optimerBoldFont" material="color: #F94DA2"></a-entity>
<a-entity id="movingDuck" class="clickable" gltf-model="#duck" alongpath="curve:#track;loop:true;dur:14000;rotate:true" position="0 1.6 -5" shadow="receive:false" scale="1 1 1" animation__rotate="property: rotation; dur: 2000; easing: linear; loop: true; to: 0 360 0" shadow onclick="fire_laser();" cursor-listener></a-entity>
<!-- A track for our Duck to follow -->
<a-curve id="track" >
<a-curve-point position="0 1 8" geometry="height:0.1;width:0.1;depth:0.1" material="color:#ff0000" curve-point="" visible="true"></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="true"></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="true"></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="true"></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="true"></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="true"></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="true"></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="true"></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="true"></a-curve-point>
</a-curve>
<!-- The track line in red -->
<a-draw-curve curveref="#track" material="shader: line; color: red;"></a-draw-curve>
<!-- Our Environment -->
<a-entity environment="preset: forest; dressing: mushrooms; dressingColor: #6e1d8b;" shadow="recieve: true"></a-entity>
<!-- A light in the scene casting shadows -->
<a-entity light="intensity: 0.6; castShadow: true; shadowCameraLeft: -10; shadowCameraBottom: -10; shadowCameraRight: 10; shadowCameraTop: 10; shadowCameraVisible: true" position="9.9649 17.32329 13.93447"></a-entity>
</a-scene>
</body>
</html>

The only differences for the Node.js version are lines: 13–17, 19–21, 87, 89 and 92 which all have “client/”.

Required A-Frame JavaScript Component Libraries

Get the A-Frame component libraries from these GitHub locations and copy them onto your server or in your local machines Node.js client directory:

I know that was a lot of setup, but believe me it will be worth it and you will end up with a development environment for VR!

So, let’s continue.

Now, let me provide you with a few assets. The duck squawk (quack) audio, line 28, the duck 3D model (glTF format .glb file), line 89, a 3D font, line 87, some open source music, line 81, and the navmesh, line 92, which we will get into later. They are all in a zip file below, which has an assets directory structure, which matches our example code.

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

The files should be put on your server or in your Node.js client/ directory. Modify lines 28, 81, 87, 89 and 92 to match the correct path for your server or Node setup. I should tell you that if you don’t want to include these files and modify the paths in the code, they should still come down from my server fine. However, they won’t be as fast as locally hosted, or they may get hit really hard, if this article becomes very popular. Which I hope it does!

Now we are finally done with all the setup we need and can move on to more code explanations.

Duck following a track

How about we talk about the duck track first?

In some simulations or games, things follow a looping or repeating track. The aframe-alongpath-component allows entities to follow predefined paths.

Lines 112–122 define a curve along a path by their x, y, z position values. There are nine curve points on the id=”track”. Which is the name of the track on line 112. Notice also that each curve-point has a visible=”true”. Let’s make them all visible=”false”. That will make the curve points disappear.

Also notice on line 124 we have a red curve line being drawn along the curveref=”#track”. If we were to remove this line of code the red line curved track that the duck follows would also disappear, but the duck would still follow the invisible track because it is defined by code on line 110.

Notice on line 110 the attribute:

alongpath=”curve:#track;loop:true;dur:14000;rotate:true”

This means the duck gltf-model=”#duck” will follow the track looping indefinitely while rotating. The rest of the rotation is defined in the animation__rotate= attribute. The duck goes around and around sort of like a teacup ride at an amusement park, except there the positions of the whole tracks are probably rotating as well. Animation can be a complicated subject, so we are just introducing a simple example in this code.

Finally notice that the duck was loaded as an asset between the <a-assets> and </a-assets> tags on line 89:

<a-asset-item id=”duck” src=”client/assets/gltf/Duck.glb”></a-asset-item>

The duck is a special type of 3D model in a glTF format. Again another subject outside our article scope.

Selecting Things In VR

Back to line 110. Notice the following attributes:

<a-entity id=”movingDuck” class=”clickable” gltf-model=”#duck” alongpath=”curve:#track;loop:true;dur:14000;rotate:true” position=”0 1.6 -5" shadow=”receive:false” scale=”1 1 1" animation__rotate=”property: rotation; dur: 2000; easing: linear; loop: true; to: 0 360 0" shadow onclick=”fire_laser();” cursor-listener></a-entity>

class=”clickable” -means the duck can be clicked with your laser pointer.

shadow=”receive:false” -means the duck can’t receive any shadow cast onto itself, this can reduce graphic processing overhead, but probably not in our little example.

shadow -means the duck can cast a shadow, say onto the ground, which is only natural.

onclick=”fire_laser();” -means when the duck is selected this JavaScript function on line 30 will be executed.

cursor-listener -means that the duck is waiting, listening for a selection. This is part of an AFRAME.registerComponent(‘click-listener’, . . . Registered components are “event”-ually where we want to be, but more complicated than we need to be for now.

Well that pretty much explains the duck and the track it follows. However what about selecting the duck, what happens when we do? fire_laser() gets executed. Lines 30–35.

function fire_laser() {
// Make Duck Quack sound
squawk.play();
//Disappear Duck
document.getElementById('movingDuck').setAttribute('visible', 'false');
}

We play the quack sound loaded into squawk on line 28. Then we do something interesting, we make the duck disappear by setting the visible attribute on line 110 again to false.

The invisible duck keeps circling the track, but we just can’t see it anymore, the graphics processor doesn’t have to draw it. I’m not sure whether raycaster selection is even executed, if we accidentally click on it? I suspect not.

Who knows if alongpath even keeps moving the duck? And I don’t care, because we are going to make it visible again on line 105, when you click on the cylinder which executes doCylinder() function, lines 36–42.

<a-cylinder id="cylinder" class="clickable" position="1 0.8993 -3" radius="0.5" height="1.5" color="#FFC65D" shadow onclick="doCylinder();" click-listener></a-cylinder>

Again the trick here is class=”clickable”, click-listener and onclick. These work together to launch the function.

function doCylinder() {

// Bring back visibility of Duck and Cube, turn Sphere red again
document.getElementById('movingDuck').setAttribute('visible', true);
document.getElementById('cube').setAttribute('visible', true);
document.getElementById('sphere').setAttribute('color', 'red');
}

Using the DOM to manipulate the attributes is technically less performance efficient than in a A-FRAME registered component. I do it here because this is an example which draws on what we might already know as web developers, and performance is not an issue.

Lights and Shadows

Let’s talk for a moment about the Shadow Camera which we manipulate in line 128.

<!-- A light in the scene casting shadows -->
<a-entity light="intensity: 0.6; castShadow: true; shadowCameraLeft: -10; shadowCameraBottom: -10; shadowCameraRight: 10; shadowCameraTop: 10; shadowCameraVisible: true" position="9.9649 17.32329 13.93447"></a-entity>

Notice the shadowCameraVisible: true. If we made that false, the orange lines of the camera would disappear from view. Good, because I set it on, visible to us; just to prove it exists and how it can be manipulated to control where you want shadows to appear in the scene. Have you noticed that none of the purple mushrooms cast shadows?

Shadow Camera doesn’t cover all of the mushrooms in the landscape

To fix that we changed the environment to have shadows and the shadow camera to be a little wider to cover more than just the center of the landscape. What we get is mushrooms with shadows!

<!-- Our Environment -->
<a-entity environment="preset: forest; dressing: mushrooms; dressingColor: #6e1d8b; shadow: true" shadow="recieve: true"></a-entity>
<!-- A light in the scene casting shadows -->
<a-entity light="intensity: 0.6; castShadow: true; shadowCameraLeft: -50; shadowCameraBottom: -50; shadowCameraRight: 50; shadowCameraTop: 50; shadowCameraVisible: false" position="9.9649 17.32329 13.93447"></a-entity>
Everything has shadows now, even the mushrooms cast shadows on other mushrooms

I see one shadow error when the duck moves around the track. There are other shadow cameras somewhere in the scene, which must be casting double shadows from other lights. We are not going to worry about that, because we must move on.

A navmesh created inside the Inspector with aframe-inspector-plugin-recast

Navigation Mesh

A navigation mesh helps navigate inside VR. It is one way of constraining movement controls and teleportation within a playable area. Without it you could walk right through into the middle of the Sphere, Cube or Cylinder; something you can’t do in real life.

Creating one is a little tricky. You’ll need to add, change and remove some things.

<!-- for creating Navmesh... Also uncomment a-scene inspector-plugin-recast     -->
<script src="https://recast-api.donmccurdy.com/aframe-inspector-plugin-recast.js"></script>
<a-scene background="color: #FAFAFA" aframe-inspector-plugin-recast>

Follow the instructions on Don McCurdy’s Inspector plug-in. Enter the Inspector with the keys ctrl-alt-i, select <a-scene> in the upper left. You should see the plug-in ready for your parameters. I removed a lot of code that wasn’t an object we wanted in our generated navmesh, like the 3D text on lines 108 and 109. I changed the cellSize to 0.11 and reduce the dressingAmount to 1, under the ENVIRONMENT to get it to work.

Once you have a navmesh.gltf file (which I renamed AdvHelloWorldnavmesh.gltf) you can use it in the original code. See line 92.

<!-- nav-mesh: protecting us from running thru sphere, cube and cylinder  -->
<a-entity id="navmesh-Hello" gltf-model="assets/gltf/AdvHelloWorldnavmesh.gltf" 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 0 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" teleport-controls="cameraRig: #cameraRig; teleportOrigin: #head; button: trigger; type: line; curveShootingSpeed: 18; collisionEntities: #navmesh-Hello; landingMaxAngle: 60" visible="true"></a-entity

I have provided one already called AdvHelloWorldnavmesh.gltf with an id=”navmesh-Hello”. On line 94 you will see the attribute movement-controls=”constrainToNavMesh: true;”. That enables the navmesh. Finally you need to add collisionEntities: #navmesh-Hello; to the teleport-controls on line 98.

That should do it; you won’t be able to walk through the sphere, cube and cylinder. Try it.

However, you may notice that once you disappear the cube by selecting it, it is gone, invisible, but you still can’t move through that space or teleport into it. Well, as they say, nothing’s perfect. You would have to swap out different navmeshes to fix that.

This concludes our Advanced Hello World for A-Frame. I hope you enjoyed it and the simple VR power of A-Frame. If you are interested in more code examples please visit my VR blog and subscribe to my newsletter.

Happy VR coding!

UPDATE: An article on animation can be be found here:

A Parade Of Planets In VR

--

--

Michael McAnally

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