Краткое содержание
Шести художникам было предложено рисовать, проектировать и лепить в виртуальной реальности. Это процесс того, как мы записывали их сеансы, преобразовывали данные и представляли их в режиме реального времени с помощью веб-браузеров.
https://g.co/VirtualArtSessions
Какое время быть живым! С появлением виртуальной реальности в качестве потребительского продукта открываются новые и неизведанные возможности. Tilt Brush, продукт Google, доступный в HTC Vive, позволяет рисовать в трехмерном пространстве. Когда мы впервые попробовали Tilt Brush, ощущение рисования с помощью контроллеров, отслеживающих движение, в сочетании с ощущением присутствия «в комнате со сверхспособностями» остается с вами; на самом деле нет ничего лучше, чем возможность нарисовать пустое пространство вокруг себя.
Перед командой Data Arts в Google стояла задача продемонстрировать этот опыт тем, у кого нет гарнитуры VR, в Интернете, где Tilt Brush еще не работает. С этой целью команда привлекла скульптора, иллюстратора, концепт-дизайнера, художника-модельера, художника-инсталлятора и уличных художников, чтобы они создавали произведения искусства в своем собственном стиле в этой новой среде.
Запись рисунков в виртуальной реальности
Программное обеспечение Tilt Brush, созданное на базе Unity, представляет собой настольное приложение, которое использует виртуальную реальность в масштабе комнаты для отслеживания положения вашей головы (головного дисплея или HMD) и контроллеров в каждой из ваших рук. Изображение, созданное с помощью Tilt Brush, по умолчанию экспортируется как файл .tilt
. Чтобы перенести этот опыт в Интернет, мы поняли, что нам нужно нечто большее, чем просто данные об изображениях. Мы тесно сотрудничали с командой Tilt Brush, чтобы изменить Tilt Brush, чтобы она экспортировала действия отмены/удаления, а также положение головы и рук художника 90 раз в секунду.
При рисовании Tilt Brush учитывает положение и угол вашего контроллера и с течением времени преобразует несколько точек в «обводку». Вы можете увидеть пример здесь . Мы написали плагины, которые извлекали эти штрихи и выводили их в виде необработанного JSON.
{
"metadata": {
"BrushIndex": [
"d229d335-c334-495a-a801-660ac8a87360"
]
},
"actions": [
{
"type": "STROKE",
"time": 12854,
"data": {
"id": 0,
"brush": 0,
"b_size": 0.081906750798225,
"color": [
0.69848710298538,
0.39136275649071,
0.211316883564
],
"points": [
[
{
"t": 12854,
"p": 0.25791856646538,
"pos": [
[
1.9832634925842,
17.915264129639,
8.6014995574951
],
[
-0.32014992833138,
0.82291424274445,
-0.41208130121231,
-0.22473378479481
]
]
}, ...many more points
]
]
}
}, ... many more actions
]
}
В приведенном выше фрагменте описывается формат эскиза JSON.
Здесь каждый штрих сохраняется как действие с типом «ХОД». В дополнение к действиям с штрихами мы хотели показать, как художник совершает ошибки и меняет свое решение в середине эскиза, поэтому было крайне важно сохранить действия «УДАЛИТЬ», которые служат действиями стирания или отмены для всего штриха.
Основная информация для каждого мазка сохраняется, поэтому собираются все данные о типе кисти, размере кисти, цвете RGB.
Наконец, каждая вершина штриха сохраняется, включая положение, угол, время, а также силу давления триггера контроллера (обозначается буквой p
внутри каждой точки).
Обратите внимание, что вращение представляет собой 4-компонентный кватернион. Это важно позже, когда мы будем визуализировать штрихи, чтобы избежать блокировки подвеса.
Воспроизведение эскизов с помощью WebGL
Чтобы отобразить эскизы в веб-браузере, мы использовали THREE.js и написали код генерации геометрии, имитирующий работу Tilt Brush.
Хотя Tilt Brush создает треугольные полосы в режиме реального времени на основе движения руки пользователя, весь эскиз уже «завершен» к тому моменту, когда мы показываем его в Интернете. Это позволяет нам обойти большую часть вычислений в реальном времени и запекать геометрию под нагрузкой.
Каждая пара вершин в штрихе создает вектор направления (синие линии, соединяющие каждую точку, как показано выше, moveVector
в фрагменте кода ниже). Каждая точка также содержит ориентацию, кватернион, который представляет текущий угол контроллера. Чтобы создать треугольную полосу, мы перебираем каждую из этих точек, создавая нормали, перпендикулярные направлению и ориентации контроллера.
Процесс расчета треугольной полосы для каждого штриха практически идентичен коду, используемому в Tilt Brush:
const V_UP = new THREE.Vector3( 0, 1, 0 );
const V_FORWARD = new THREE.Vector3( 0, 0, 1 );
function computeSurfaceFrame( previousRight, moveVector, orientation ){
const pointerF = V_FORWARD.clone().applyQuaternion( orientation );
const pointerU = V_UP.clone().applyQuaternion( orientation );
const crossF = pointerF.clone().cross( moveVector );
const crossU = pointerU.clone().cross( moveVector );
const right1 = inDirectionOf( previousRight, crossF );
const right2 = inDirectionOf( previousRight, crossU );
right2.multiplyScalar( Math.abs( pointerF.dot( moveVector ) ) );
const newRight = ( right1.clone().add( right2 ) ).normalize();
const normal = moveVector.clone().cross( newRight );
return { newRight, normal };
}
function inDirectionOf( desired, v ){
return v.dot( desired ) >= 0 ? v.clone() : v.clone().multiplyScalar(-1);
}
Комбинирование направления и ориентации штриха само по себе дает математически неоднозначные результаты; может быть получено несколько нормалей, что часто приводит к «искажению» геометрии.
При переборе точек обводки мы сохраняем «предпочтительный правый» вектор и передаем его в функцию computeSurfaceFrame()
. Эта функция дает нам нормаль, из которой мы можем получить квадрат в полосе квадратов, основываясь на направлении штриха (от последней точки до текущей точки) и ориентации контроллера (кватерниона). Что еще более важно, он также возвращает новый вектор «предпочтительного права» для следующего набора вычислений.
После создания четырехугольников на основе контрольных точек каждого штриха мы объединяем четырехугольники, интерполируя их углы, от одного четырехугольника к другому.
function fuseQuads( lastVerts, nextVerts) {
const vTopPos = lastVerts[1].clone().add( nextVerts[0] ).multiplyScalar( 0.5
);
const vBottomPos = lastVerts[5].clone().add( nextVerts[2] ).multiplyScalar(
0.5 );
lastVerts[1].copy( vTopPos );
lastVerts[4].copy( vTopPos );
lastVerts[5].copy( vBottomPos );
nextVerts[0].copy( vTopPos );
nextVerts[2].copy( vBottomPos );
nextVerts[3].copy( vBottomPos );
}
Каждый квадрат также содержит UV-развертки, которые генерируются на следующем этапе. Некоторые кисти содержат различные рисунки мазков, чтобы создать впечатление, что каждый мазок ощущается как отдельный мазок кисти. Это достигается с помощью _текстурного атласа, _где каждая текстура кисти содержит все возможные варианты. Правильная текстура выбирается путем изменения значений UV обводки.
function updateUVsForSegment( quadVerts, quadUVs, quadLengths, useAtlas,
atlasIndex ) {
let fYStart = 0.0;
let fYEnd = 1.0;
if( useAtlas ){
const fYWidth = 1.0 / TEXTURES_IN_ATLAS;
fYStart = fYWidth * atlasIndex;
fYEnd = fYWidth * (atlasIndex + 1.0);
}
//get length of current segment
const totalLength = quadLengths.reduce( function( total, length ){
return total + length;
}, 0 );
//then, run back through the last segment and update our UVs
let currentLength = 0.0;
quadUVs.forEach( function( uvs, index ){
const segmentLength = quadLengths[ index ];
const fXStart = currentLength / totalLength;
const fXEnd = ( currentLength + segmentLength ) / totalLength;
currentLength += segmentLength;
uvs[ 0 ].set( fXStart, fYStart );
uvs[ 1 ].set( fXEnd, fYStart );
uvs[ 2 ].set( fXStart, fYEnd );
uvs[ 3 ].set( fXStart, fYEnd );
uvs[ 4 ].set( fXEnd, fYStart );
uvs[ 5 ].set( fXEnd, fYEnd );
});
}
Поскольку каждый эскиз имеет неограниченное количество штрихов и их не нужно изменять во время выполнения, мы заранее вычисляем геометрию штрихов и объединяем их в одну сетку. Несмотря на то, что каждый новый тип кисти должен иметь собственный материал, это все равно сокращает количество вызовов отрисовки до одного на кисть.
Для стресс-тестирования системы мы создали эскиз, который занял 20 минут, заполняя пространство как можно большим количеством вершин. Полученный скетч по-прежнему воспроизводился со скоростью 60 кадров в секунду в WebGL.
Поскольку каждая из исходных вершин штриха также содержала время, мы можем легко воспроизвести данные. Пересчет количества штрихов для каждого кадра будет очень медленным, поэтому вместо этого мы предварительно просчитали весь эскиз при загрузке и просто открыли каждый четырехугольник, когда пришло время это сделать.
Скрытие четырехугольника просто означало свертывание его вершин до точки 0,0,0. Когда время дошло до точки, в которой четырехугольник должен быть раскрыт, мы возвращаем вершины на место.
Область для улучшения — это полностью манипулирование вершинами на графическом процессоре с помощью шейдеров. Текущая реализация размещает их, просматривая массив вершин из текущей временной метки, проверяя, какие вершины необходимо раскрыть, а затем обновляя геометрию. Это создает большую нагрузку на процессор, что приводит к вращению вентилятора и расходу заряда батареи.
Запись артистов
Мы чувствовали, что самих эскизов будет недостаточно. Мы хотели показать художников внутри их эскизов, прорисовывая каждый мазок кисти.
Чтобы запечатлеть артистов, мы использовали камеры Microsoft Kinect для записи данных о глубине тела артистов в космосе. Это дает нам возможность показывать их трехмерные фигуры в том же пространстве, что и рисунки.
Поскольку тело художника закрывалось, не позволяя нам видеть то, что находится за ним, мы использовали двойную систему Kinect, расположенную на противоположных сторонах комнаты и направленную в центр.
Помимо информации о глубине, мы также захватили информацию о цвете сцены с помощью стандартных зеркальных камер. Мы использовали превосходное программное обеспечение DepthKit для калибровки и объединения материалов с камеры глубины и цветных камер. Kinect способен записывать цвет, но мы решили использовать зеркальные камеры, потому что мы могли контролировать настройки экспозиции, использовать красивые высококачественные объективы и записывать в высоком разрешении.
Для записи отснятого материала мы построили специальную комнату для HTC Vive, художника и камеры. Все поверхности были покрыты материалом, поглощающим инфракрасный свет, чтобы обеспечить более чистое облако точек (пуветин на стенах, ребристый резиновый коврик на полу). На случай, если материал появится в кадрах облака точек, мы выберем черный материал, чтобы он не отвлекал внимание так же, как белый материал.
Полученные видеозаписи дали нам достаточно информации, чтобы спроектировать систему частиц. Мы написали в openFrameworks несколько дополнительных инструментов для дальнейшей очистки отснятого материала, в частности удаления полов, стен и потолка.
Помимо демонстрации художников, мы хотели также визуализировать HMD и контроллеры в 3D. Это было важно не только для четкого отображения HMD в конечном результате (отражающие линзы HTC Vive искажали ИК-показания Kinect), но и давало нам точки соприкосновения для отладки вывода частиц и выравнивания видео со скетчем.
Это было сделано путем написания специального плагина для Tilt Brush, который извлекал позиции HMD и контроллеров в каждом кадре. Поскольку Tilt Brush работает со скоростью 90 кадров в секунду, потоком передавались тонны данных, а размер входных данных эскиза составлял более 20 МБ в несжатом виде. Мы также использовали эту технику для захвата событий, которые не записываются в обычном файле сохранения Tilt Brush, например, когда художник выбирает параметр на панели инструментов и положение виджета зеркала.
При обработке собранных нами 4 ТБ данных одной из самых больших проблем было согласование всех различных источников визуальных данных и данных. Каждое видео с камеры DSLR необходимо совместить с соответствующим Kinect, чтобы пиксели были выровнены как в пространстве, так и во времени. Затем кадры с этих двух камер нужно было соединить друг с другом, чтобы сформировать единого художника. Затем нам нужно было совместить нашего 3D-художника с данными, полученными из его рисунка. Уф! Мы написали инструменты для браузера, которые помогут справиться с большинством этих задач, и вы можете попробовать их самостоятельно здесь.
После того, как данные были выровнены, мы использовали несколько сценариев, написанных на NodeJS, для их обработки и вывода видеофайла и серии файлов JSON, обрезанных и синхронизированных. Чтобы уменьшить размер файла, мы сделали три вещи. Во-первых, мы снизили точность каждого числа с плавающей запятой, чтобы они имели точность не более 3 десятичных знаков. Во-вторых, мы сократили количество точек на треть до 30 кадров в секунду и интерполировали позиции на стороне клиента. Наконец, мы сериализовали данные, поэтому вместо использования простого JSON с парами ключ/значение создается порядок значений для положения и вращения шлема виртуальной реальности и контроллеров. Это сократило размер файла до 3 МБ, что было приемлемо для передачи по проводу.
Поскольку само видео представляет собой видеоэлемент HTML5, который считывается текстурой WebGL и становится частицей, само видео должно воспроизводиться скрыто в фоновом режиме. Шейдер преобразует цвета изображения глубины в положения в трехмерном пространстве. Джеймс Джордж поделился отличным примером того, как можно работать с отснятым материалом прямо из DepthKit.
iOS имеет ограничения на воспроизведение встроенного видео, которые, как мы полагаем, предназначены для того, чтобы пользователи не были докучены веб-видеорекламой, которая воспроизводится автоматически. Мы использовали метод, аналогичный другим обходным путям в Интернете , который заключается в копировании видеокадра на холст и ручном обновлении времени поиска видео каждые 1/30 секунды.
videoElement.addEventListener( 'timeupdate', function(){
videoCanvas.paintFrame( videoElement );
});
function loopCanvas(){
if( videoElement.readyState === videoElement.HAVE\_ENOUGH\_DATA ){
const time = Date.now();
const elapsed = ( time - lastTime ) / 1000;
if( videoState.playing && elapsed >= ( 1 / 30 ) ){
videoElement.currentTime = videoElement.currentTime + elapsed;
lastTime = time;
}
}
}
frameLoop.add( loopCanvas );
Наш подход имел неприятный побочный эффект в виде значительного снижения частоты кадров iOS, поскольку копирование буфера пикселей из видео на холст очень сильно нагружает процессор. Чтобы обойти эту проблему, мы просто предоставили версии тех же видеороликов меньшего размера, которые позволяют поддерживать скорость не менее 30 кадров в секунду на iPhone 6.
Заключение
Общий консенсус в отношении разработки программного обеспечения для виртуальной реальности с 2016 года заключается в том, чтобы сохранять геометрию и шейдеры простыми, чтобы вы могли работать со скоростью 90+ кадров в секунду в HMD. Это оказалось действительно отличной целью для демонстраций WebGL, поскольку методы, используемые в Tilt Brush, очень хорошо соответствуют WebGL.
Хотя веб-браузеры, отображающие сложные 3D-сетки, сами по себе не интересны, это было доказательством концепции, согласно которой перекрестное опыление работы виртуальной реальности и Интернета вполне возможно.