This workshop will show you how to:

Hello AFrame

To complete this workshop you will need:

Languages used:

Setup a Visual Studio Code work space by adding your project's folder, and ensure that Live Server extension is installed, View -> Extensions or Ctrl+Shift+X.

The AFrame Hello World scene requires just few HTML lines to run. Behind the scene, it is Three.js doing all the heavy lifting

<!DOCTYPE html>
<html>
  <head>
  <!--AFrame version 1.2.0-->
  <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
  </head>
    <body>
    <a-scene>
      <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
      <a-entity geometry="primitive: box;  width: 1; height: 1; depth: 1"
          material="color: red; shader: standard;"
          rotation="0 45 0" position="-1 1.7 -3"></a-entity>
      <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
      <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
      <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
      <a-sky color="#ECECEC"></a-sky>
    </a-scene>
  </body>
</html>

In VSCode create a new file named index.html with the above HTML.
At the top of the file the AFrame library is loaded. In the body of the page, the HTML tag <a-scene> ... </a-scene> defines the AFrame scene. AFrame is based on a entity-component system: the scene contains a series of objects, called entities<a-entity></a-entity>. They could be generic or based on primitives (e.g. box, sphere, cylinder, plane and sky).
An <a-entity> by itself does not display anything. It is just a placeholder object. To define it we need to attach components that provides: appearance (e.g. geometry, position, rotation, colour); behaviour (e.g. animation, event listeners), and/or other functionality. For example, the primitive <a-box> can be written also as <a-entity geometry="primitive: box">.

In VSCode press Go Live of the Live Server (bottom-right corner) to see the Hello World AFrame scene in the default browser. During the development it is a good practice to regularly check the Inspect Console of your browser for any error (right-click Inspect or Ctrl+Shift+i).

Hello AFrame

HTML attributes (or components in AFrame) are used to extended AFrame functionalities. For example, by adding the inbuilt component stats to the <a-scene> it is possible to overlay some useful information about the scene, such as frames per second, number of textures and triangles and other performance's values. The component vr-mode-ui="enabled: false" is used to disable the VR button on the bottom right corner of the scene.

  <a-scene stats vr-mode-ui="enabled: false" >

Add stats to the scene and remove the VR button

An AFrame scene does not need to be full screen. It can be embedded in any web page using the component embedded. We just need to set the size of the a-scene in a <style> </style> tag in the <head> </head> of the page, or we can use an external CSS style sheet

<style media="screen">
    a-scene {
        height: 400px;
        width: 600px;
            }
    </style>

The embedded component need to be added to the <a-scene>. It is possible to wrap the entire <a-scene> </a-scene> in a DIV element if we need more control on the composition of the web page

<body>
  <div id="myEmbeddedScene">
    <a-scene embedded stats vr-mode-ui="enabled: false" >
      <!-- ... -->
    </a-scene>
  </div>
</body>

To control the FOV (field of view) and height of the point of view we need to define a <a-camera>.

<!-- ... -->
<a-camera fov="60" position="0 2 0"></a-camera>
<!-- ... -->

New FOV and embedded scene

An AFrame scene can be modified just using HTML. To add a blue sphere on the top of the existing one:

<a-scene embedded stats vr-mode-ui="enabled: false">
      <a-sphere
          position="0 3.5 -5"
          radius="1"
          color="blue">
      </a-sphere>
      <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>

A new blue sphere

More complex geometries can be added as GLTF model. Good practice is to add the models to the Asset Management System<a-assets> of the scene. The Asset Management System is used to preload 3D models, sounds, images, videos etc. that will be used in the scene.

In this example the asset is the 3D model of Russell Square in GLB format.

<a-scene  stats vr-mode-ui="enabled: false" >
  <a-assets>
    <a-asset-item
      id="russellsquare"
      src="resources/RussellSq_Qgis.glb"
      response-type="arraybuffer">
    </a-asset-item>
  </a-assets>
  
  <a-entity
      id="Rsquare"
      gltf-model="#russellsquare"
      scale="1 1 1"
      position="0 0 0"
      rotation="0 0 0">
  </a-entity>
  
  <a-sky color="lightblue"></a-sky>
  <a-camera fov="60" position="0 2 0"></a-camera>
</a-scene>

Russell Square from the ground

The current camera does not allow us to see the full extension of the model, to the existing <a-camera>:

  <a-scene  stats vr-mode-ui="enabled: false" >
  <a-assets>
       <a-asset-item id="russellsquare" src="resources/RussellSq_Qgis.glb" response-type="arraybuffer" ></a-asset-item>
  </a-assets>
  <a-entity
    id="Rsquare"
    gltf-model="#russellsquare"
    scale="1 1 1"
    position="0 0 0"
    rotation="0 0 0">
  </a-entity>
  <a-sky color="lightblue"></a-sky>
  <a-camera
    look-controls
    wasd-controls="acceleration: 500; fly: true;"
    fov="60" 
    position="0 10 0">
  </a-camera>
</a-scene>

The camera is located at coordinate (x:0, y:10, z:0). The rotation is controlled by pressing and holding the left mouse button and the movement using W A S D keys.

Fly over Russell Square

AFrame components are used to further extend the functionalities of the scene. The components are written in JavaScript.

Before its use, a component need to be registered using a specific syntax:

AFRAME.registerComponent('name-of-the-component', {
   init: function(){ myFunction } })  

This is an example of a basic ray-cast component.

AFRAME.registerComponent('raycast-mouse', {
  //The function run when the scene is loaded
  init: function () {
  var tempLine; //temporary edge geometry to highlights mesh edges

//Add an EventListener of type mouse 'click' to this entity (this.el)
  this.el.addEventListener('click', function (evt) {
//print the name of the object intersected by the ray
       console.log('I am: ', evt.detail.intersection.object);

  if(evt.detail.intersection.object.children.length>0)
  {
    //if the intersected 3d object has children[] >0 it means that there is already an EdgesGeometry and we can remove it
    evt.detail.intersection.object.children=[];
  }
  
  else
  {
        //Otherwise, create a edge geometry on the top of the current geometry
        var geometry=evt.detail.intersection.object.geometry;
        var edges = new THREE.EdgesGeometry( geometry);
        tempLine = new THREE.LineSegments( edges, new THREE.LineBasicMaterial( { color: 0xff0000 } ) );

        //Add the EdgesGeometry as child of the object.children[] when clicked, so we can check if the mesh has already the highlighted edges
        evt.detail.intersection.object.add(tempLine);
      }
      });
    }
  });

The this.el.addEventListener is used to listen an event fired by this component applied to the element el (e.g. if we apply the component to the <a-entity> that contain the gltf-model, this will be that <a-entity>). In this case the event is a mouse click event. The evt.detail.intersection.object return the intersected object.

The if-else is used to check if the intersected geometry already has a THREE.EdgesGeometry, created using a red THREE.LineSegments, if not, the THREE.EdgesGeometry is added as child of the intersected geometry.

<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>

<script src="js/raycast-mouse.js"></script>
<a-entity raycast-mouse class="collidable" id="Rsquare" gltf-model="#russellsquare" scale="1 1 1" position="0 0 0" rotation="0 0 0" ></a-entity>
<a-scene stats vr-mode-ui="enabled: false" cursor="rayOrigin: mouse" raycaster="objects: .collidable">

Highlight the geometry

The if-else statement of the raycast-mouse.js is used to add the <a-text> (or any other entity) at the position of the hit point. Create the new variables in the raycast-mouse.js component:

    var tempLine; //it stores a temporary edge geometry that highlights the borders of the mesh

    //textContainer is an empty <a-entity>, appended to the main <a-scene>, used to store the <a-text> entities
    var sceneEl = document.querySelector('a-scene');
    var textContainer = document.createElement('a-entity');
        textContainer.setAttribute('id','textContainer');
    
    sceneEl.appendChild(textContainer);

    var textEl; //the <a-text> used to display the name of the object
    var scaleModel=this.el.object3D.scale.x; //scale the text with the model

    //create a <a-text> with the name of the object selected

        var prevText = document.querySelector('#text'); //check if the <a-text> already exists...
    
        if(prevText===null){ //...if not
          textEl   = document.createElement('a-entity');
          textEl.setAttribute('id','text');
          textContainer.appendChild(textEl);
        }
          else
          {
            textEl=prevText;
          }
          //position of the <a-text> is the hit point of the ray cast, scaled on the Y to avoid to place the texEl inside the object3D
          textEl.object3D.position.set(evt.detail.intersection.point.x,evt.detail.intersection.point.y+(scaleModel*5),evt.detail.intersection.point.z);

          //textEl uses the scale of the object3D
          textEl.object3D.scale.set(scaleModel*1,scaleModel*1,scaleModel*1);
          
          // the text to visualise is the name of the object and it is applied as property `value` of the <a-text>
          var textToViz="";
          if(evt.detail.intersection.object.userData.properties!==undefined)
          {
          textToViz=evt.detail.intersection.object.userData.properties[17]; //the field properties has been created by QGis and contain the attribute table. The name of the column is lost but from QGis we know that _name_ is the column 17
          }
          if(textToViz==='NULL') //some buildings do not have name, instead of visualise NULL just left the text empty
            {
              textToViz="";
            }
          
          textEl.setAttribute('text',{color: 'red', align: 'center', value:textToViz, height:'50', width: '50'});

The evt.detail.intersection is used to set the position of the entity, using the node .point. The model used in this example has been exported using the QGis2Threejs plugin of QGis. The properties of each building have been automatically stored in the userData field in an array named properties. In this example the value name is the 17th field of the attribute table. The result is a 3D text floating over the point hit by the ray.

Add a name to the selected object

The text created has a fix rotation="0 0 0". The external component aframe-look-at-component, forces the text to always face the user's camera. The aframe-look-at-component is one of the many created by the AFrame community.

<script src="https://unpkg.com/aframe-look-at-component@1.0.0/dist/aframe-look-at-component.min.js"></script>
<a-entity 
    camera 
    id="user"
    look-controls 
    wasd-controls="acceleration:100; fly: true;"
    fov="60"
    position="0 10 0">
  </a-entity>
var textToViz="";
if (typeof evt.detail.intersection.object.userData.properties !== "undefined" && evt.detail.intersection.object.userData.properties[17] != "NULL") {
          textToViz = evt.detail.intersection.object.userData.properties[17];
        }
textEl.setAttribute('text',{color: 'red', align: 'center', value:textToViz, height:'50', width: '50'});

textEl.setAttribute('look-at',"#user");