To complete this workshop you will need:

Hardware:

Languages used:

This workshop will show you how to:

Create a new index.html in VSCode. AR.Js is built on the top AFrame, and like AFrame, its Hello World scene requires just few lines of HTML to run.

Since version 3.0.0, AR.js has two different sources: one using NFT Image tracking, and a second one using Marker Based Tracking. The latter one is used in this workshop.

<script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
<script src="https://raw.githack.com/AR-js-org/AR.js/master/aframe/build/aframe-ar.js"></script>

Add to the the components:

Add a camera entity

<a-entity camera> </a-entity>

and the

<a-marker preset="hiro"> </a-marker>

AR.js has two embedded markers: Hiro, the image can be download here, and Kanji, the image can be download here.

When AR.js detects the marker, the entities within are set to visible.

<a-marker preset="hiro">
          <a-box position='0 0 0' material='color: blue;'></a-box>
</a-marker>

Here the complete index.html for the Hello WebAR scene

<!DOCTYPE html>
<html>
  <head>
    <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
    <script src="https://raw.githack.com/AR-js-org/AR.js/master/aframe/build/aframe-ar.js"></script>
  </head>
  <body style='margin : 0px; overflow: hidden;'>
    <a-scene 
      embedded
      arjs='debugUIEnabled: false; sourceType: webcam; trackingMethod: best;'
      vr-mode-ui="enabled: false;"
      renderer="
          logarithmicDepthBuffer: true;
          physicallyCorrectLights: true;"
      >
      
      <a-marker preset="hiro">
          <a-box position='0 0.5 0' material='color: blue;'></a-box>
      </a-marker>
      
      <a-entity camera></a-entity>
    
    </a-scene>
  </body>
</html>

The renderer component in is used to control the final appearance of the AR cube: e.g. without physically correct lighting... Hello AFrame

...and with physically correct lighting Hello WebAR

The online Marker Training tool is used to easily create a custom marker for AR.js.

Hello WebAR

From:

<a-marker preset="hiro">
  <a-box position='0 0.5 0' material='color: blue;'></a-box>
</a-marker>

To:

<a-marker id="marker1" type="pattern" url="resources/pattern/pattern-1.patt">
  <a-box position='0 0 0' material='color: blue;'></a-box>
</a-marker>

<a-marker id="marker2" type="pattern" url="resources/pattern/pattern-2.patt">
  <a-box position='0 0 0' material='color: red;'></a-box>
</a-marker>
<a-scene 
      embedded
      arjs='debugUIEnabled: false; sourceType: webcam; trackingMethod: best; patternRatio: 0.85;'
      vr-mode-ui="enabled: false;"
      renderer="
          logarithmicDepthBuffer: true;
          physicallyCorrectLights: true;"
      >

Multi markers

The markers can be used to display any kind of AFrame entity, as well as external assets.

  <assets>
      <a-asset-item
          id="windbody"
          src="resources/models/windmeterBody.glb"
          response-type="arraybuffer" >
      </a-asset-item>
      
      <a-asset-item
          id="windneedle"
          src="resources/models/windmeterNeedle.glb"
          response-type="arraybuffer" >
      </a-asset-item>
    </assets>
      
      <a-marker id="marker1" type="pattern" url="resources/pattern/pattern-1.patt">
          <a-entity id="windMeter" scale="5 5 5" position="0 0 0" rotation="0 0 0">
              <a-entity
                    id="WBody"
                    gltf-model="#windbody"
                    scale="1 1 1"
                    position="0 0 0"
                    rotation="0 0 0">
              </a-entity>
              <a-entity id="WNeedleContainer" position="0 0.1875 0">
                  <a-entity
                        id="WNeedle"
                        gltf-model="#windneedle"
                        scale="1 1 1"
                        position="0 -0.1875 0"
                        rotation="0 0 0">
                  </a-entity>
              </a-entity>
            </a-entity>
      </a-marker>

Display assets

Using standard touch events it is possible to interact with the AR digital model. Hammer.js is a library that can be used to add gestures on any WebApp.

<script src="resources/js/hammer.min.js"></script>

A custom AFrame component is used to listen to pan and pinch events.

AFRAME.registerComponent("rotatescale",{
    init:function() {    } )
      var element = document.querySelector('body');
      var hammerEl = new Hammer(element);

the component is applied to the to control the in it.

  this.marker = document.querySelector('a-marker')
  var model = this.marker.querySelector('a-entity');
var pinch = new Hammer.Pinch(); // Pinch is not default
hammerEl.add(pinch); // add it to the Manager instance
hammerEl.on('pan', (ev) => {
        let rotation = model.getAttribute("rotation")
        switch(ev.direction) {
          case 2: //DIRECTION_LEFT from https://hammerjs.github.io/api/
            rotation.y = rotation.y - 4
            break;
          case 4: //DIRECTION_RIGHT from https://hammerjs.github.io/api/
            rotation.y = rotation.y + 4
            break;
          case 8: //DIRECTION_UP from https://hammerjs.github.io/api/
            rotation.x = rotation.x - 4
            break;
          case 16: //DIRECTION_DOWN from https://hammerjs.github.io/api/
            rotation.x = rotation.x + 4
            break;
          default:
            break;
        }
        model.setAttribute("rotation", rotation)
      });
hammerEl.on("pinch", (ev) => {
        let scale = {x:ev.scale/1, y:ev.scale/1, z:ev.scale/1}
        model.setAttribute("scale", scale);
    });

the complete rotatescale.js looks like

AFRAME.registerComponent("rotatescale",{
    init:function() {
      var element = document.querySelector('body');
      var hammerEl = new Hammer(element);
      
      this.marker = document.querySelector('a-marker') //the component rotatescale need to be apply on the a-marker tag
      var model = this.marker.querySelector('a-entity');
      
      var pinch = new Hammer.Pinch(); // Pinch is not by default in the recognisers
      hammerEl.add(pinch); // add it to the Manager instance

      hammerEl.on('pan', (ev) => {
        let rotation = model.getAttribute("rotation")
        switch(ev.direction) {
          case 2: //DIRECTION_LEFT from https://hammerjs.github.io/api/
            rotation.y = rotation.y - 2
            break;
          case 4: //DIRECTION_RIGHT from https://hammerjs.github.io/api/
            rotation.y = rotation.y + 2
            break;
          case 8: //DIRECTION_UP from https://hammerjs.github.io/api/
            rotation.x = rotation.x - 2
            break;
          case 16: //DIRECTION_DOWN from https://hammerjs.github.io/api/
            rotation.x = rotation.x + 2
            break;
          default:
            break;
        }
        model.setAttribute("rotation", rotation)
      });

     hammerEl.on("pinch", (ev) => {
        let scale = {x:ev.scale/1, y:ev.scale/1, z:ev.scale/1}
        model.setAttribute("scale", scale);
    });
  }
});
<script src="resources/js/rotatescale.js"></script>
<a-marker rotatescale id="marker1" type="pattern" url="resources/pattern/pattern-1.patt">

The Paho JavaScript Client is used to receive the data from the MQTT Broker.

<script src="resources/js/paho-mqtt-min.js"></script>

A custom AFrame component is used to read the data from the MQTT Broker

AFRAME.registerComponent('mqttreader', {

  init:function (){
  		console.log('MQTT Reader');

In this example the MQTT data is used to control, in real time, the pointer of the digital wind meter.

      const sceneEl = document.querySelector('a-scene');
      const windNeedle=sceneEl.querySelector('#WNeedleContainer');
      let mqtt;
      let reconnectedTimeout=5000;
      let hostname="ADDRESS.OF.THE.MQTT.BROKER";
      let port=PORT;
      function onConnect()
      {
        console.log("Connected...");
        
        mqtt.subscribe("TOPIC/TO/SUBSCRIBE");
        
      }

      function MQTTconnect()
      {
        console.log("Connected to "+hostname+": "+ port);

        //works but in ws not wss
        mqtt=new Paho.MQTT.Client(hostname,port,"clientjs");

        let options={
          timeout:3,
          onSuccess:onConnect,
          onFailure:onFailure,
        };

        mqtt.onMessageArrived=onMessageArrived;

        mqtt.connect(options);
      }

      function onFailure(message)
      {
      console.log("Connection to "+hostname+" failed");
      setTimeout(MQTTconnect,reconnectedTimeout);
      }

the function onMessageArrived is used to control the rotation of the pointer on the body of the wind meter. In this case the playloadString is the MPH value that need to be transform to a rotation value

      function onMessageArrived(msg)
      {
        out_msg="msg is "+msg.destinationName+"  --  "+msg.payloadString+"<br>";
        console.log(out_msg);
        let valueWRot=(((msg.payloadString*6)*Math.PI)/180);
          console.log(valueWRot);
        windNeedle.object3D.rotation.z = -valueWRot;

      }
MQTTconnect();
}
})
<a-entity mqttreader
                    id="WBody"
                    gltf-model="#windbody"
                    scale="1 1 1"
                    position="0 0 0"
                    rotation="0 0 0">
              </a-entity>