В прошлой статье я описал, с чего начинать разработку VR в браузере. Для этого мы взяли популярный пример “Hello World” для A-Frame и немного его расширили, добавив окружение и возможность перемещаться в этом окружении с помощью VR-контроллеров, а также телепортироваться и т.д.

В текущей же статье мы пойдем гораздо дальше. Здесь мы добавим JS-код, который позволит выбирать объекты VR-сцены и активировать для них действия. Весь этот процесс будет также включать в себя крутую анимацию с крякающей уткой. 3D модель этой утки будет вращаться перед нами по заданной траектории. Когда кряканье этой птицы будет становиться слишком раздражающим, вы сможете подстрелить ее лазерным указателем. Реальные животные в этой статье не пострадают. 

Настройка

Что ж, начнем. Чтобы облегчить вам чтение этой статьи, а мне ее написание, я собираюсь дать вам кучу кода сразу наперед. Сперва он может вас несколько запутать, но далее я этот код объясню, и вы сможете его изменять, чтобы видеть, как это влияет на результат.

Итак, вот этот код:

<!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="Продвинутый пример "Hello World", WebXR! A-frame предлагает действительно стоящий пример "Hello World", но обычно вам также требуется возможность выбора контроллера и окружение, позволяющее перемещение. Используя этот пример, вы получите вариант, который будет работать для большинства гарнитур и контроллеров".>
    <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" />

<!-- *** ИЗМЕНИТЕ ЭТИ КОМПОНЕНТЫ НА СВОЕМ СЕРВЕРЕ *** -->
    <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>

<!-- для создания Navmesh (сетки навигации)... Также раскомментируйте a-scene inspector-plugin-recast
    <script src="https://recast-api.donmccurdy.com/aframe-inspector-plugin-recast.js"></script>
    -->

<!-- JavaScript-код ниже обеспечивает выбор и возможность выполнения действий с объектами, средой и звуком -->
    <script type="text/javascript">

// Аудио для утки
      var squawk = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/duck.mp3');

// Функция, срабатывающая по клику
      function fire_laser() {

// Утка издает звук
        squawk.play();

// Утка исчезает
        document.getElementById('movingDuck').setAttribute('visible', 'false');
      }function doCylinder() {
        
        // Возвращаем видимость утки и куба, снова делаем сферу красной
        document.getElementById('movingDuck').setAttribute('visible',
true);
        document.getElementById('cube').setAttribute('visible', true);
        document.getElementById('sphere').setAttribute('color', 'red');
      }function doCube() {
        
        // Делаем сферу зеленой, временно убираем куб
        document.getElementById('sphere').setAttribute('color', 'green');
        document.getElementById('cube').setAttribute('visible', false);      
      }
// Компонент, срабатывающий по клику
      AFRAME.registerComponent('click-listener', {
        init: function () {this.el.addEventListener('click', function (evt) {
// Удаляем из сцены объект, по которому кликнули
                //this.setAttribute('visible', false);
      
            });
        }
      });
//   Убираем блокировку звука в Google Chrome 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>
    <!-- Музыка из .mp3 файла с открытым для использования произведением Моцарта.  Если решите ее заменить, убедитесь, что используете либо открытое произведение, либо свое. -->
    <audio id="playAudio" autoplay loop>
        <source src="https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/MozartALittleNightMusic.mp3" type="audio/mpeg">
    </audio>

<!-- используется для создания Navmesh (сетки навигации)... смотрите javascript-компонент над <a-scene inspector-plugin-recast>  https://github.com/donmccurdy/aframe-inspector-plugin-recast-->

<a-scene background="color: #FAFAFA">

<a-assets>
            <!-- Шрифт -->
            <a-asset-item id="optimerBoldFont" src="assets/fonts/optimer_bold.typeface.json"></a-asset-item>
<!-- 3D GltF модель утки -->
            <a-asset-item id="duck" src="assets/gltf/Duck.glb"></a-asset-item>
      </a-assets>
<!-- nav-mesh: не дает нам проходить сквозь сферу, куб и цилиндр -->
      <a-entity id="navmesh-Hello" gltf-model="assets/gltf/AdvHelloWorldnavmesh.gltf" visible="false" nav-mesh=""></a-entity>
<!-- Базовое передвижение и телепортация  -->
      <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">
        <!-- камера-->
        <a-entity id="head" camera="active: true" position="0 1.6 0" look-controls="pointerLockEnabled: true; reverseMouseDrag: true" ></a-entity>
              <!-- Левый контроллер  -->
              <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>
              <!-- Правый контроллер  -->
              <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>
<!-- Стандартные объекты Hello World, измененные для реагирования на клики и аудио -->
      <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 >

<!-- 3D текст -->
      <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-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>

<!-- Делаем линию траектории красной --> 
      <a-draw-curve curveref="#track" material="shader: line; color: red;"></a-draw-curve>

<!-- Окружение -->
      <a-entity environment="preset: forest; dressing: mushrooms; dressingColor: #6e1d8b;" shadow="recieve: true"></a-entity>

<!-- Освещение сцены, отбрасывающее тени -->
      <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>

Вы можете опробовать этот код на моем сервере, открыв в новой вкладке следующую ссылку:Advanced Hello World A-Frame WebXR
Advanced Hello World, WebXR! A-frame provides a hello world that is really remarkable, however you usually need to have…rocketvirtual.com

Для тех, кто использует ноутбук или настольный ПК: вы можете перемещаться по сцене при помощи клавиш WASD и разворачиваться перетаскиванием центра экрана при зажатой левой кнопке мыши.

Если же вы зайдете в VR-гарнитуре, нажав кнопку VR в правом нижнем углу экрана, то погрузитесь в полноценную VR-среду, где вам будет доступен обзор на все 360 градусов. В том или ином случае вы увидите примерно следующее:

Чтобы упростить этот процесс, я буду описывать код, ссылаясь на соответствующие его строки. Поэтому скопируйте и вставьте приведенный выше код в свой редактор, активировав нумерацию строк. При желании вы также можете загрузить бесплатный редактор Sublime.

В том или ином случае полученный код должен выглядеть примерно так (все 131 строки):

131 строчка пронумерованного кода в редакторе

Строки с 13 по 17 и с 19 по 21 являются JS-библиотеками компонентов A-Frame, которые используются для реализации ряда особенностей VR в браузере. Если не загрузить их, то код просто не заработает. Итак, если у вас есть доступ к интернет-серверу через https:// (это необходимо), эти файлы скопируются с GitHub к вам на сервер и будут иметь соответствующие ссылки.

ЛИБО

Если у вас нет доступа к серверу, но при этом вы можете установить на локальную машину Node.js, тогда следуйте перечисленным здесь инструкциям для настройки системы на порт localhost:2000. Помимо этого, убедитесь, что используете код, приведенный ниже, а не тот, что был выше, потому что в этом инструкция client/ расположена в нужных местах, что позволит симулировать сервер локально при помощи 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="Продвинутый пример "Hello World", WebXR! A-frame предлагает действительно стоящий пример "Hello World", но обычно вам также требуется возможность выбора контроллера и окружение, позволяющее перемещение. Используя этот пример, вы получите вариант, который будет работать для большинства гарнитур и контроллеров.">
    <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" />
<!-- *** ИЗМЕНИТЕ ЭТИ КОМПОНЕНТЫ НА СВОЕМ СЕРВЕРЕ *** -->
    <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>
<!-- для создания Navmesh... Также раскомментируйте a-scene inspector-plugin-recast
    <script src="https://recast-api.donmccurdy.com/aframe-inspector-plugin-recast.js"></script>
    -->
<!-- JavaScript-код ниже обеспечивает возможность выбирать объекты, окружение и аудио, а также производить с ними действия -->
    <script type="text/javascript">
// Озвучивание утки
      var squawk = new Audio('https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/duck.mp3');
// Функция для выполнения при клике
      function fire_laser() {
// Утка издает квакающий звук
        squawk.play();
// Утка исчезает
       document.getElementById('movingDuck').setAttribute('visible', 'false');
      }
function doCylinder() {
        
        // Возвращаем видимость Duck и Cube, снова делаем Sphere красной
        document.getElementById('movingDuck').setAttribute('visible', true);
        document.getElementById('cube').setAttribute('visible', true);
        document.getElementById('sphere').setAttribute('color', 'red');
      }
function doCube() {
        
        // Делаем сферу зеленой, временно убираем куб
        document.getElementById('sphere').setAttribute('color', 'green');
        document.getElementById('cube').setAttribute('visible', false);      
      }
// Компонент, выполняющийся по клику
      AFRAME.registerComponent('click-listener', {
        init: function () {
this.el.addEventListener('click', function (evt) {
// Удаляем из сцены объект, по которому кликнули 
                //this.setAttribute('visible', false);
      
            });
        }
      });
//   Устраняет блокировку звука в Google Chrome 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>
    
<!-- Музыка из .mp3 файла с открытым для использования произведением Моцарта.  Если решите ее заменить, убедитесь, что используете либо открытое произведение, либо свое. -->
    <audio id="playAudio" autoplay loop>
        <source src="https://rocketvirtual.com/A-Frame_WebXR/assets/mp3/MozartALittleNightMusic.mp3" type="audio/mpeg">
    </audio>
<!-- используется для создания Navmesh... смотрите javascript-компонент над <a-scene inspector-plugin-recast>  https://github.com/donmccurdy/aframe-inspector-plugin-recast-->
<a-scene background="color: #FAFAFA">
<a-assets>
            <!-- Шрифт -->
            <a-asset-item id="optimerBoldFont" src="client/assets/fonts/optimer_bold.typeface.json"></a-asset-item>
<!-- 3D GltF модель утки -->
            <a-asset-item id="duck" src="client/assets/gltf/Duck.glb"></a-asset-item>
      </a-assets>
<!-- nav-mesh: не дает нам проходить сквозь сферу, куб и цилиндр -->
      <a-entity id="navmesh-Hello" gltf-model="client/assets/gltf/AdvHelloWorldnavmesh.gltf" visible="false" nav-mesh=""></a-entity>
<!-- Базовое перемещение и телепортация -->
      <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">
        <!-- камера-->
        <a-entity id="head" camera="active: true" position="0 1.6 0" look-controls="pointerLockEnabled: true; reverseMouseDrag: true" ></a-entity>
              <!-- Левый контроллер  -->
              <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>
              <!-- Правый контроллер  -->
              <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>
<!-- Стандартные объекты Hello World, измененные для реагирования на клик и аудио -->
      <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 >
<!-- 3D текст -->
      <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-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>

<!-- Красная линия трека --> 
      <a-draw-curve curveref="#track" material="shader: line; color: red;"></a-draw-curve>

<!-- Окружение -->
      <a-entity environment="preset: forest; dressing: mushrooms; dressingColor: #6e1d8b;" shadow="recieve: true"></a-entity>

<!-- Освещение сцены, отбрасывающее тени -->
      <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>

Эта версия для Node.js отличается только строчками 13–17, 19–21, 87, 89 и 92, где расположена инструкция “/client”.

Необходимые библиотеки A-Frame компонентов

Скопируйте следующие библиотеки компонентов A-Frame с указанных ресурсов GitHub на свой сервер или в директорию Node.js-клиента локальной машины:

aframevr/aframe
a: web framework for building virtual reality experiences. — aframevr/aframegithub.com

supermedium/aframe-environment-component
A simple way of setting up a whole basic environment for your A-Frame VR scene. Make sure you are using A-Frame 0.6.0…github.com

n5ro/aframe-extras
Add-ons and helpers for A-Frame VR.github.com

fernandojsg/aframe-teleport-controls
Teleport component Browser Installation Install and use by directly including the browser files: There are two ways to…github.com

supermedium/superframe
A super collection of A-Frame components. See documentation for individual components: aabb-collider — An axis-aligned…github.com

protyze/aframe-alongpath-component
A component for A-Frame that allows entities to follow predefined paths. New in Version 1.0.0: The alongpath component…github.com

protyze/aframe-curve-component
A Curve component to draw curves in A-Frame. The component consists of multiple components: curve: Draws a certain type…github.com

Я понимаю, что с настройками пришлось повозиться, но поверьте мне, оно того стоило, и в итоге вы получите среду разработки для VR.

Что ж, давайте продолжим.

Я подготовил для вас несколько заготовок: звук кряканья утки (строка 28), 3d модель утки (.glb файл в формате glTF  —  строка 89), 3D шрифт (строка 87), разрешенную для использования музыку (строка 81) и сетку навигации (строка 92), c которой мы познакомимся чуть позже. Все эти заготовки находятся в указанном ниже zip-файле со структурой директории assets, соответствующей нашему примеру кода.

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

Файлы следует разместить на сервере или в Node.js-директории /client. Измените строки 28, 81, 87, 89 и 92, указав правильный путь к серверу или локальному клиенту Node. Считаю нужным сообщить, что если вы не хотите включать эти файлы и изменять пути в коде, то они все равно должны работать с моего сервера. Однако их скорость будет уже не той, как если бы они размещались локально. Кроме того, они могут стать труднодоступными, если статья вдруг станет популярной.

Теперь мы завершили настройку и можем переходить к более подробному описанию кода.

Утка движется по треку

Траектория движения утки

В некоторых играх или симуляторах элементы могут следовать замкнутому или повторяющемуся треку. aframe-alongpath-component позволяет сущностям следовать предопределенным путям. 

Строки 112–122 определяют кривую пути при помощи пространственных значений x, y, z. В id=”track”, представляющий имя трека, прописаны девять точек кривой. Обратите также внимание, что у каждой из этих точек есть значение visible=true. Давайте изменим все эти значения на visible=”false”, в следствии чего точки исчезнут.

Заметьте, что на строке 124 мы нарисовали красную кривую линию вдоль curveref=”#track”. Если мы вдруг решим удалить эту строчку кода, то красная линия трека, по которой следует утка, исчезнет, но при этом утка продолжит следовать невидимой траектории, потому что определена она в строке 110.

Обратите внимание на атрибут строки 110:

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

Это означает, что утка gltf-model=”#duck” будет бесконечно повторять траекторию во время вращения. Остальная часть вращения определена в animation__rotate= attribute. Создание анимации может представлять определенную сложность, поэтому в коде мы используем ее простой пример.

Ну и наконец обратите внимание, что утка загружается в виде заготовки между тегами <a-sssets> и </a-assets> на строке 89:

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

Она представляет собой особый тип 3D модели в формате glTF. Рассмотрение этой темы также выходит за рамки текущей статьи.

Выбор элементов в режиме VR

Возвращаясь к строке 110, обратите внимание на следующие атрибуты:

<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” означает, что по утке можно кликнуть лазерным указателем.

shadow=”receive:false” означает, что на утку не должны отбрасываться тени. Это должно снизить графическую нагрузку, но в нашем скромном примере мы этого не заметим.

shadow означает, что утка может отбрасывать тень, скажем, на землю, что вполне естественно.

onclick=”fire_laser();” означает, что при выборе утки будет выполнена JS-функция на строке 30.

cursor-listener означает, что утка ожидает, прослушивая событие своего выбора. Это часть AFRAME.registerComponent(‘click-listener’,…. Зарегистрированные компоненты в итоге находятся там, где надо, но сами при этом несколько сложнее, чем хотелось бы на данном этапе.

Итак, мы достаточно подробно рассмотрели утку и ее движение по треку. Но что же все-таки происходит, когда мы по ней кликаем? Выполняется функция fire_laser(), расположенная на строках 30–35.

function fire_laser() {
// Утка издает крякающий звук
        squawk.play();
// Утка исчезает
        document.getElementById('movingDuck').setAttribute('visible', 'false');
      }

Мы проигрываем звук утки, загруженный в squawk на строке 28, после чего делаем кое-что интересное, а именно заставляем утку исчезнуть, установив атрибут visible в строке 110 на false.

Утка продолжает вращаться по треку, но для нас она теперь невидима, и графическому процессору не нужно ее прорисовывать. Я не уверен, сработает ли выбор raycaster, если мы все-таки случайно по ней кликнем. Думаю, что нет.

Продолжает ли вообще alongpath перемещать утку? Да это и не важно, ведь на строке 105 мы снова сделаем ее видимой. Для этого нужно будет кликнуть по цилиндру, который выполняет функцию doCylinder(), прописанную на строках 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>

И сновав дело вступают class=”clickable”, click-listener и onclick, которые, работая в тандеме, запускают функцию.

function doCylinder() {

        // Возвращаем видимость утке и кубу, снова делаем сферу красной
        document.getElementById('movingDuck').setAttribute('visible', true);
        document.getElementById('cube').setAttribute('visible', true);
        document.getElementById('sphere').setAttribute('color', 'red');
      }

Управлять атрибутами через DOM технически менее производительно, чем при помощи зарегистрированного компонента A-FRAME. Здесь же я делаю так, потому что этот пример опирается на уже известные нам как веб-разработчикам понятия, и производительность не является проблемой.

Освещение и тени

Давайте теперь рассмотрим Shadow Camera (англ.), которой мы управляем на строке 128.

<!-- Освещение сцены, отбрасывающее тени -->
      <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>

Обратите внимание, что shadowCameraVisible: true. Если изменить значение на false, то оранжевые линии камеры исчезнут. Я сделал их видимыми, чтобы продемонстрировать, что они существуют, и как можно с их помощью контролировать расположение теней на сцене. Вы заметили, что ни один из фиолетовых грибов не отбрасывает тень?

Камера теней не охватывает все грибы на ландшафте

Чтобы это исправить, мы добавили тени в окружение и расширили область охвата камеры. Теперь грибы начали отбрасывать тени.

<!-- Окружение -->
      <a-entity environment="preset: forest; dressing: mushrooms; dressingColor: #6e1d8b; shadow: true" shadow="recieve: true"></a-entity>
      <!-- Освещение сцены, отбрасывающее тени -->
      <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>
Теперь у всех сущностей есть тени, даже грибы отбрасывают их друг на друга.

Я вижу одну ошибку теней при перемещении утки по треку. Где-то в сцене есть и другие камеры теней, которые отбрасывают двойную тень от других источников освещения. Мы не будем заострять на этом внимание, потому что нужно двигаться дальше.

navmesh, созданная внутри Inspector с помощью aframe-inspector-plugin-recast

Сетка навигации (navmesh)

Эта сетка помогает перемещаться в VR-режиме. Она представляет один из способов ограничения управления движением и телепортации в рамках игровой зоны. Без нее вы могли бы проходить прямо сквозь сферу, куб или цилиндр, что в реальной жизни, конечно же, невозможно.

Создать эту сетку не так-то просто. потребуется кое-что добавить, изменить и даже удалить.

<!-- для создания навигационной сетки... Также раскомментируйте 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>

donmccurdy/aframe-inspector-plugin-recast
A plugin for the A-Frame Inspector, allowing creation of a navigation mesh while from an existing A-Frame scene. This…github.com

Следуйте инструкциям Дона МакКурди по использованию плагина Inspector. Войдите в Inspector, нажав Ctrl-Alt-I и выберите в верхнем левом углу <a-scene>. Вы должны увидеть плагин, подготовленный под ваши параметры. Я удалил большую часть кода, который был лишним для нашей сетки навигации, например 3D текст на строках 108 и 109. Кроме этого, в разделе ENVIRONMENT я изменил cellSize на 0.11 и уменьшил dressingAmount до 1, чтобы в итоге все заработало.

Когда у вас будет готов файл navmesh.gltf (переименованный мной в AdvHelloWorldnavmesh.gltf), вы сможете использовать его в исходном коде. Смотрите строку 92.

<!-- nav-mesh: не дает нам проходить сквозь сферу, куб и цилиндр  -->
      <a-entity id="navmesh-Hello" gltf-model="assets/gltf/AdvHelloWorldnavmesh.gltf" visible="false" nav-mesh=""></a-entity>

<!-- Базовое перемещение и телепортация  -->
      <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">
        <!-- Камера-->
        <a-entity id="head" camera="active: true" position="0 1.6 0" look-controls="pointerLockEnabled: true; reverseMouseDrag: true" ></a-entity>
              <!-- Левый контроллер  -->
              <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

Я подготовил такой файл под названием AdvHelloWorldnavmesh.gltf с id=”navmesh-Hello”. На строке 94 вы увидите атрибут movement-controls=”constrainToNavMesh: true;”. Он активирует навигационную сетку. В завершении нужно добавить в управление телепортом на строке 98 collisionEntities: #navmesh-Hello;.

Теперь все должно заработать и у вас не получится проходить сквозь сферу, куб и цилиндр. Можете попробовать.

Однако вы можете заметить, что после того, как вы выбираете куб, и он исчезает, вы все равно не можете проходить через занимаемое им пространство и телепортироваться в него. Что ж, как говорится, ничто не идеально. Чтобы это исправить, понадобиться перебрать разные сетки.

На этом наш продвинутый пример Hello World для A-Frame завершен. Надеюсь, что вам понравилась как сама статья, так и простота богатых VR-возможностей, предлагаемых A-Frame. 

Удачи в VR-программировании!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Michael McAnally: Advanced Hello World for A-Frame

Предыдущая статья5 признаков того, что вы тратите свой потенциал разработчика впустую
Следующая статьяКак развернуть пакет Cython в PyPI