Несмотря на то, что понятие «виртуальная реальность» уже не первый год мелькает перед глазами, оно до сих пор остается загадкой для большинства людей, а цены на аксессуары, связанные с этим развлечением, могут уходить в бесконечность. Но существует бюджетный вариант. Среднестатистический человек, интересующийся новыми технологиями, может позволить себе смартфон с гироскопом, встроенный в Google Cardboard или любой аналог этого нехитрого устройства и простой джойстик с парой кнопок. В наше время такой вариант знакомства с этой технологией наиболее распространенный. Но, как и многие другие технологичные новинки, вроде квадрокоптеров, интересная игрушка быстро превращается в пылящуюся на полке. Практическое применение сильно ограничено. Взрослые люди, купившие очки виртуальной реальности, первые дни играются в игрушки, смотрят различные видео с прекрасными барышнями, радуются, как дети. Это здорово. Новые впечатления всегда полезны. Но быстро приходит понимание, что графика в игрушках находится на уровне конца 90-х, видео наскучивает, а другого практического применения очкам для этих людей нет. Для отдельных разработчиков, дизайнеров и экспериментаторов очки становятся инструментом в работе, но обычный потребитель кладет их на полку и забрасывает. Было бы интересно применить эту технологию в интернете на обычных сайтах. В этой статье мы попробуем с помощью Javascript адаптировать привычную нам верстку под новые условия отображения. Информации по теме пока не много, статья носит характер эксперимента, так что всем заинтересованным людям, а также тем, кто имел подобный практический опыт, заранее предлагается присоединиться к обсуждению и поделиться своими мыслями и опытом в комментариях.
Введение
С течением времени в нашем мире появляются новые устройства ввода-вывода. Монитор, механическая клавиатура и мышка с шариком дополнились тачпадами, у мышки появилось колесико, у людей стали появляться устройства с плоскими экранами, в которые все тыкали старыми шариковыми ручками вместо стилуса, затем телефоны с touch-экранами стали чем-то повседневным и совершенно не примечательным. Мы перестали говорить «не тыкай пальцем в монитор» и стали задумываться об адаптации интерфейсов под новые реалии. Кнопки становятся больше, чтобы пальцем не промахиваться, интерактивные элементы поддерживают свайпы и другие движения разным количеством пальцев. С появлением очков виртуальной реальности (телефон с парой линз тоже будем считать таковым) нам стоит задуматься о том, как адаптировать то, что мы имеем в интернете на данный момент, к такому повороту событий. Да, пройдет еще несколько лет (а может несколько десятков лет) пока такие очки будут в каждом доме, но если мы не начнем что-то думать сейчас — мы просто еще сильнее отложим приход этого момента.
Что-то получаем, но и что-то теряем
VR очки дают нам возможность располагать информацию в трех измерениях (ну или по крайней мере создается такой эффект), в отличии от обычного монитора. У нас появляется возможность управлять происходящим с помощью поворота головы, что довольно интересно. Некоторые джойстики, если их можно так назвать, позволяют управлять тем, что мы видим, с помощью движения рук в пространстве. Но при этом мы теряем возможность использовать клавиатуру — только если вы действительно умеете печатать вслепую, вы сможете ее использовать. Причем для тех, кто много печатает, отсутствие привычного тактильного ощущения от нее и отсутствее стука клавиш может быть серьезным испытанием. Подобный эффект, хотя и не в такой степени, происходит при переходе со старой клавиатуры из начала 2000-х на новую, с малым ходом клавиш и почти бесшумную. Теряем и обычную мышку — крайне проблематично представить себе использование двухмерного контроллера в трехмерном пространстве.
Получается, что мы имеем совершенно новый набор устройств ввода-вывода с новым для нас поведением. У нас не просто эволюционирует одно из них, а заменяется весь стек. В такой ситуации неизбежны вопросы о том, насколько приспособлены все эти устройства для решения уже существующих задач.
Потребление контента. Все-таки плоскости?
Поскольку мы начали говорить о применении технологии с точки зрения потребителя, а не разработчика, продолжим смотреть с этой же стороны. Потребление информации последние десятилетия не претерпевало значительных изменений. Контент может быть представлен в виде текста, в виде звука, графического изображения или видео. Механические кнопки заменяются виртуальными, перелистывание страниц заменяется кликанием по ссылкам, но сам контент сильно не меняется. Запахи и тактильные ощущения из фильмов или игр широкому кругу потребителей пока не доступны.
С аудио все более-менее понятно. Наушники или несколько колонок могут дать определенное ощущение объема, хотя два наушника не позволяют «перемещать» источник звука вперед или назад. Слушать музыку мы можем также, как и раньше, а большинство интерфейсов работают беззвучно, так что вопрос звука будет волновать нас в последнюю очередь. Плееры для видео в 3d уже есть в разных вариантах. Можно считат, что вопрос потребления видео «на весь экран» более-менее решен.
А вот с текстом все интереснее. Первым делом, оказавшись в виртуальном пространстве, возникает мысль об отходе от идеи плоского монитора и расположении текста на кривой поверхности. Шарик? А пользователь в центре. Почему бы и нет. Но лучше все же сохранить вертикальные оси на повержности, а то и вырвать может от такого интерфейса. Раньше мы не задумывались о таком? А теперь придется. На некоторых игрушках делают предупреждения для эпилептиков, а теперь видимо будут для людей с проблемами с вестибулярным аппаратом. Цилиндрическая поверхность кажется хорошим выбором. Одинаковое «расстояние» до текста во всех точках должно снизить нагрузку на глаза (по сравнению с огромным монитором), эффективное использование пространства…
Но реальность бьет очень больно. Из-за искажений читать такой текст очень сложно, причем это усугубляется плохим качеством дисплеев у большинства смартфонов — буквы по краям области видимости превращаются в мыльно-пиксельное нечто. А постоянно крутить головой на каждой строчке текста — не вариант. Похоже, что в ближайшем будущем мы будем ограничены плоскостями в 3d. Придется склониться к призме — три вертикальных монитора мы можем встроить в наше пространство и при этом более-менее сохраним читаемость текста. Причем желательно разместить их так, чтобы при повороте головы к «монитору» взгляд попадал ровно в его центр и линия взгляда была перпендикулярна плоскости монитора. Таким образом искажения будут минимальными. Если вы пробовали сидеть под углом к монитору, то поймете о чем я. Почему три? Можно было бы сделать еще пару мониторов за спиной, но если предполагать длительную работу, то нужно будет иметь здоровье космонавта, чтобы вращаться на стуле целый день. Некоторых людей и без очков от пары оборотов хватит весьма неприятный приступ головокружения и помутнение в глазах. Так что имеет смысл ограничить угол обзора. Возможно стоит произвести эксперименты с частью додекаэдра, а не с призмой, но мне пока сложно представить интерфейс, построенный на пятиугольниках.
А что со вводом?
Трехмерный контроллер — вещь интересная. Но… Вы когда-нибудь видели обычного художника, который рисует на мольберте? Формально он не занимается физическим трудом — кисточка весит несколько граммов (ну может пару десятков), но после полного дня рисования рука у него просто отваливается. Да и спина устает, что бы там не говорили. Вот у трехмерных контроллеров та же проблема. Ослабленные от многих лет лежания на столе и печатания на клавиатуре руки не могут весь день находиться в подвешенном состоянии. Так что для игр — здорово, для отдельных этапов 3d-моделирования — может быть, но для постоянной работы такой вид контроллера плохо приспособлен. Остается вариант, когда рука лежит и нажимает несколько кнопок. Но и это уже хорошо. На контроллерах кнопок обычно больше, чем на стандартной мышке (для пользователей топовых моделей от Razer напомню, что их там две + колесико).
С клавиатурой — беда. Можно сделать подобие экранной клавиатуры, но скорость печати на таком чуде будет ниже плинтуса. Возможно, со временем, у нас будут распространены полупрозрачные очки, чтобы на уровне глаз у нас было изображение, а снизу мы могли видеть реальную клавиатуру, но пока о бюджетном варианте речи не идет. Плюс — у тех устройств, которые есть на рынке, имеется проблема с утомляемостью глах — читать полупрозрачный текст в течении длительного времени можно только если на фоне что-то более-менее однотонное и неподвижное. Уведомление посмотреть можно, но читать на постоянной основе — нет. Голосовой ввод пока тоже далек от совершенства. Похоже, что в ближайшее время в виртуальной реальности вводить большие объемы текста не получится.
На данный момент вариант со смартфоном не позволит нам совместить очки и штуки вроде WebGazer для того, чтобы использовать направление взгляда как заменитель мышки. Если будет интересно, я разберу отдельно идею построения интерфейса для веб приложения, которым можно будет управлять с помощью одного лишь взгляда (вполне вероятно, что у этого может быть практическое применение в медицине). Но в случае с очками у нас есть только поворот головы. Наиболее эффективным вариантом его использования в нашей ситуации кажется следующий: взять центр поля зрения, построить луч от пользователя и использовать его пересечение с плоскостями-мониторами для того, чтобы создавать подобие мышки на этих плоскостях.
Попробуем что-нибудь сделать.
Раз уж мы делаем несколько плоскостей-мониторов, создадим какую-нибудь разметку для них
(ссылка на GitHub с полными исходниками будет в конце статьи). Это обычная разметка, я в полных исходниках использовал кнопочки и аккордеон из одного из своих проектов.
<div class='vr-area -left'>
....
</div>
<div class='vr-area -center'>
....
</div>
<div class='vr-area -right'>
....
</div>
Вариантов использовать обычную разметку HTML + CSS в трехмерном пространстве не так уж и много. Но существуют пара рендереров для Three.js, которые сразу привлекают к себе внимание. Один из них, который называется CSS3DRenderer, позволяет взять нашу разметку и отрендерить ее на плоскости. Это вполне подходит под нашу задачу. Второй, CSS3DStereoRenderer, который мы и будем использовать, делает то же самое, но делит экран на две части, создает как бы два изображения, по одному для каждого глаза.
Инициализация Three.js уже не раз упоминалась в различных статьях, так что не будем сильно зацикливаться на ней.
var scene = null,
camera = null,
renderer = null,
controls = null;
init();
render();
animate();
function init() {
initScene();
initRenderer();
initCamera();
initControls();
initAreas();
initCursor();
initEvents();
}
function initScene() {
scene = new THREE.Scene();
}
function initRenderer() {
renderer = new THREE.CSS3DStereoRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = 0;
renderer.domElement.style.background = '#343335';
document.body.appendChild(renderer.domElement);
}
function initCamera() {
camera = new THREE.PerspectiveCamera(45,
window.innerWidth/window.innerHeight, 1, 1000);
camera.position.z = 1000;
scene.add(camera);
}
Второй компонент, который мы сразу добавим, — это DeviceOrientationController, который позволит использовать поворот устройства для ориентации в 3d-пространстве. Технология определения положения устройства является экспериментальной, и, мягко говоря, на сегодняшний день работает так себе. Все три устройства, на которых она проверялась, вели себя по-разному. Это еще усугубляется тем, что помимо гироскопа (а, возможно, и вместо него) браузером используется компас, который в окружении различного железа и проводов ведет себя сильно неадекватно. Мы будем использовать именно этот контроллер еще и для того, чтобы сохранить возможность использования обычной мышки при оценке результатов эксперимента.
function initControls() {
controls = new DeviceOrientationController(camera, renderer.domElement);
controls.connect();
controls.addEventListener('userinteractionstart', function () {
renderer.domElement.style.cursor = 'move';
});
controls.addEventListener('userinteractionend', function () {
renderer.domElement.style.cursor = 'default';
});
}
CSS3DRenderer, как мы уже сказали, дает возможность использовать обычную разметку в нашем 3d примере. Создание CSS объектов при этом похоже на создание обычных плоскостей.
function initAreas() {
var width = window.innerWidth / 2;
initArea('.vr-area.-left', [-width/2 -width/5.64,0,width/5.64], [0,Math.PI/4,0]);
initArea('.vr-area.-center', [0,0,0], [0,0,0]);
initArea('.vr-area.-right', [width/2 + width/5.64,0,width/5.64], [0,-Math.PI/4,0]);
}
function initArea(contentSelector, position, rotation) {
var element = document.querySelector(contentSelector),
area = new THREE.CSS3DObject(element);
area.position.x = position[0];
area.position.y = position[1];
area.position.z = position[2];
area.rotation.x = rotation[0];
area.rotation.y = rotation[1];
area.rotation.z = rotation[2];
scene.add(area);
}
Далее опять идут стандартные для почти всех демок Three.js функции для анимации и изменения окна браузера.
function initEvents() {
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
render();
}
function animate() {
controls.update();
render();
requestAnimationFrame(animate);
}
function render() {
renderer.render(scene, camera);
}
А вот дальше начинается интересное. CSS3DStereoRenderer по своей сути создает два идентичных дерева элементов, которые с помощью CSS-трансформаций выстраиваются в нужном месте. Иными словами, магии в этом рендерере нет. А то, что есть, создает проблему. Как можно взаимодействовать с интерактивными элементами, если они все продублировались? Мы не можем сфокусироваться на двух элементах одновременно, не можем сделать двойной :hover, вообщем маленькая засада. Вторая проблема — id элементов, которые могут быть только в единственном экземпляре на странице. Но если id можно и не использовать, то над взаимодействиями нужно плотно подумать.
Единственным способом, который позволит управлять внешним видом двух элементов одновременно — это добавление и удаление к ним каких-то CSS классов. Также мы можем имитировать click(). Кликать на несколько элементов мы можем, это не ведет к странному поведению браузера. Таким образом, мы можем сделать второй курсор в центре поля зрения (не всего экрана, а именно в центрах его половин, предназначенных для разных глаз). Для того, чтобы узнавать элемент, находящийся под курсором, мы можем использовать функцию document.elementFromPoint(x, y). В этот момент кажется очень полезным тот факт, что рендерер использует трансформации, но сами элементы остаются стандартными DOM-элементами. Нам не нужно придумывать геометрию с пересечением луча с плоскостью, на которой рендерится контент, поиском координат пересечения и определением элементов на плоскости по этим координатам. Все гораздо проще. Стандартная функция вполне справляется с этой задачей.
Правда возникает вопрос о том, к какому именно элементу добавлять и удалять классы. Я решил временно остановиться на простом переходе к родительским элементам до тех пор, пока не появится фокусируемый элемент. Вероятно есть более красивое решение, да и можно это вынести в отдельную функцию, но уж очень хотелось поскорее посмотреть, что же там получится.
function initCursor() {
var x1 = window.innerWidth * 0.25,
x2 = window.innerWidth * 0.75,
y = window.innerHeight * 0.50,
element1 = document.body,
element2 = document.body,
cursor = document.querySelector('.fake-cursor');
setInterval(function() {
if (element1 && element1.classList) {
element1.classList.remove('-focused');
}
if (element2 && element2.classList) {
element2.classList.remove('-focused');
}
element1 = document.elementFromPoint(x1, y);
element2 = document.elementFromPoint(x2, y);
if (element1 && element2) {
while (element1.tabIndex < 0 && element1.parentNode) {
element1 = element1.parentNode;
}
while (element2.tabIndex < 0 && element2.parentNode) {
element2 = element2.parentNode;
}
if (element1.tabIndex >= 0) {
element1.classList.add('-focused');
}
if (element2.tabIndex >= 0) {
element2.classList.add('-focused');
}
}
}, 100);
}
Также добавим на скорую руку простую имитацию клика по обоим элементам под нашим фейковым курсором. В случае с очками, тут должна быть обработка кнопки на контроллере, но не имея такового, остановимся на обычном enter на клавиатуре.
document.addEventListener('keydown', function(event) {
if (event.keyCode === 13) {
if (element1 && element2) {
element1.click();
element2.click();
cursor.classList.add('-active');
setTimeout(function() {
cursor.classList.remove('-active');
}, 100);
}
}
});
Что же там получилось?
Поскольку значительная часть того, что использовалось в статье, является экспериментальным, пока сложно утверждать о кроссбраузерности. В Chrome под линуксом с обычной мышкой все работает, в Chrome под андроидом — на одном устройстве все немного поворачивается в зависимости от положения в помещении (полагаю из-за компаса, на который влияют окружающие наводки), на другом — все разлетается в щи. По этой причине записал небольшое видео с экрана, на котором виден результат с десктопного браузера:
Мысли вместо заключения.
Эксперимент интересный. Да, до реального применения еще очень далеко. Браузеры пока еще очень плохо поддерживают определение положения устройства в пространстве, возможности для калибрации компаса можно сказать отсутствуют (формально есть событие compassneedscalibration, но на практике толку от него не очень много). Но в целом идея создания интерфейса в трехмерном пространстве вполне реализуема. По крайней мере на десктопе мы уже точно можем такое применять. Если заменить стерео-рендерер на обычный, а камеру поворачивать просто по движению мыши, то получим что-то похожее на обычную трехмерную игрушку, только с возможностью использовать bootstrap все любимые инструменты верстальщика.
Полные исходники примера доступны на GitHub
Автор: Ivan Bogachev