Zoals ik afgelopen week al schreef, wilde ik de panorama-viewer verder uitbreiden om meer foto’s toe te voegen. Interacties in de derde dimensie werken echter nét anders dan op het platte scherm. Daarnaast zijn allerlei conventies nog helemaal niet uitgewerkt, dus iedereen doet maar wat en dat is heel verfrissend.

In de 20e eeuw werden de nieuwe grafische interfaces aangeleerd door het gebruik van bedieningselementen die waren ontworpen naar het voorbeeld van een fysieke tegenhanger, een skeuomorfisme zoals dat heet. Mappen, schuifknoppen en zandlopers; allemaal elementen uit de echte wereld om ons te laten begrijpen hoe de interface werkt in de platte digitale wereld.


Meta Quest: Zwevende 2D windows met een 3D sausje

Digitale 3D werelden kunnen werken op een heel andere manier, maar de interface-ontwerpers maken het zich er makkelijk vanaf en plakken gewoon een 2D interface in de lucht, want die kennen we al. Ze willen het ons nu nog niet te lastig maken, maar de échte revolutie moet nog komen, denk ik.

Gemiste kans
Apple visionOS: Nog niet veel beter

Simpel

Hoe dan ook, ik wilde iets simpels maken om mee te beginnen. Met A-Frame doe je dat met Custom Elements in HTML. Hieronder zie je het specifieke deel om de scene te definiëren.

<a-scene>
    <a-assets>
        <img id="image-0"   src="image-0.jpg">
        <img id="city"      src="https://cdn.aframe.io/a-painter/images/sky.jpg">
        <img id="floor"     src="https://cdn.aframe.io/a-painter/images/floor.jpg">
    </a-assets>
    <a-sky src="#city"></a-sky>
    <a-curvedimage id="panorama"    src="#image-0"
                   position="0 2 0" height="2.5" radius="2"
                   theta-start="60" theta-length="240"></a-curvedimage>
    <a-cylinder id="ground" src="#floor" radius="32" height="0.1"></a-cylinder>
</a-scene>

Eerst definieer je een aantal assets, in dit geval afbeeldingen, waar je vanaf andere plekken naar kan verwijzen. Om te voorkomen dat je in het luchtledige lijkt te zweven is het verstandig een a-sky toe te voegen met een 360º afbeelding. Daarnaast heb ik een a-cylinder toegevoegd die als een soort vloer dient.

De a-curvedimage bevat het echte panorama-plaatje en is eigenlijk een boogsegment van de wand van een cilinder. Het is een beetje zoeken naar de juiste waardes, maar de meeste van mijn panorama’s zijn ongeveer 240º. Als het niet helemaal klopt, is dat nog niet zo’n ramp, want dat zie je amper, behalve als je met veel rechte lijnen werkt en het allemaal precies zou moet passen.

Nu voeg ik nog twee a-image toe onder de panorama om zometeen vooruit en achteruit te kunnen en voila, de scène is zo’n beetje klaar:

Overzicht van de scene
Overzicht van de scene

Met A-Frame hoef je je om de rendering verder geen zorgen meer te maken. Je headset schakelt om naar VR-modus en je wordt omringd door de scène die je hebt gebouwd. De interactie daarentegen zul je wel zelf moeten opbouwen, want anders dan op het platte web kan je hier niet gewoon ergens een <a href=...> wegzetten om een link te maken.

Interactie

In 2D interfaces zijn we gewend dat we één aanwijzer hebben die we eventueel met meerdere apparaten kunnen bedienen ( muis, touchpad, tekenpen, …). In de 3D wereld zijn al die apparaten opeens onafhankelijke bedieningen geworden, vergelijkbaar met hoe een smartphone elke vinger apart detecteert en “multitouch gestures” herkent. A-Frame ondersteunt hoofd- en controller-tracking, dus je kijkrichting en wat je met je controllers doet kan als invoer dienen. Omdat je hoofd (waarschijnlijk) geen knoppen heeft, werkt een interactie daarmee anders dan met een hand-controller.

Een controller declareer je door een a-entity te maken met een aantal attributen, bijvoorbeeld zo:

<a-entity oculus-touch-controls="hand: right" 
          laser-controls 
          b-button-listener></a-entity>

Met oculus-touch-controls="hand: right" zeg je dat je de Meta Quest controller voor de rechter hand wil configureren. De laser-controls zorgt ervoor dat de controller een laserstraal afschiet in de wijsrichting, en dat een aantal events worden afgevuurd als die laserstraal een object raakt, waarover later meer.

Uit de VR-modus gaan

Ten slotte koppel ik met b-button-listener mijn eigen code aan de controller hardware:

AFRAME.registerComponent('b-button-listener', {
    init: function () {
        this.el.addEventListener('bbuttondown', function (e) {
            const sceneEl = document.querySelector('a-scene');
            if (sceneEl.is('vr-mode')) {
                sceneEl.exitVR();
            }
        });
    }
});

Dit zorgt ervoor dat als je op de B knop op de controller drukt wanneer je in VR-mode bent, dat je uit VR-mode gaat.

Een foto vooruit of achteruit gaan

De twee kleine fotootjes onderaan op het screenshot had ik nog niet aan de scène toegevoegd, dus dat doen we nog even:

<a-image id="prev-image"        src="#image-2"
         position="-1 0.5 -1.5" rotation="-45 30 0"
         width="1"              height="0.33"
         cursor-listener="direction: prev"></a-image>
<a-image id="next-image"        src="#image-1"
         position="1 0.5 -1.5"  rotation="-45 -30 0"
         width="1"              height="0.33"
         cursor-listener="direction: next"></a-image>

Hier zie je de cursor-listener component. Deze code is straks verantwoordelijk voor het naar het click signaal luisteren dat de laser-controls component produceert wanneer je op een van de a-images “klikt”. Je ziet dat je extra informatie kan meegeven, net als bij de oculus-touch-controls, om de component te configureren.

AFRAME.registerComponent('cursor-listener', {
    schema: {
        direction: {type: 'string', default: 'next'}
    },

    init: function () {
        this.el.addEventListener('click', (e) => {
            switch (this.data.direction) {
                case 'prev':
                    nextImage(-1);
                    break;
                default:
                    nextImage(1);
                    break;
            }
        });
    }
});

In de code hierboven wordt aan this.el, dat is de a-image waar het op staat, een event listener toegevoegd voor het click signaal. Afhankelijk van de waarde van direction zal hij nu de volgende of de vorige afbeelding inladen.

De logica voor het wisselen van de afbeelding is niet A-Frame of WebXR specifiek en kan je vinden in de source van de panorama pagina (net als de rest van deze blog trouwens).

En dat is het! Het resultaat is een wereld waarin je kan schakelen tussen zes panorama’s met behulp van je controllers! Voor degenen zonder headset, een filmpje: