Статьи

Фильтрация реальности с помощью JavaScript и Google Cardboard

Возможность запуска виртуальной реальности в мобильном браузере расширяет возможности и делает ее интересной. Google Cardboard и другие подобные устройства VR делают это невероятно простым, просто поместите свой телефон в держатель и начинайте! Ранее я освещал « Перенос виртуальной реальности в Интернет» с помощью Google Cardboard и Three.js , где обсуждал основы построения среды виртуальной реальности, которая использует веб-данные. Людям действительно понравилась эта статья (и мне очень понравилось создавать эту демонстрацию), поэтому я решил расширить ее с другой идеей. Вместо того, чтобы вводить веб-API, почему бы не включить камеру вашего телефона и превратить это в опыт дополненной реальности?

В этой статье я собираюсь изучить, как мы можем получить данные камеры, отфильтровать их и отобразить обратно, используя HTML5 и JavaScript. Мы сделаем это с помощью эффекта стереоскопического зрения, чтобы создать опыт дополненной реальности для Google Cardboard и других устройств VR. Мы применим несколько различных фильтров к потоку с нашей камеры: мультяшный фильтр в оттенках серого, фильтр в стиле сепии, фильтр с пикселями (мой любимый) и фильтр с инверсией цвета.

Если вы новичок в фильтрации изображений с помощью HTML5, тега canvas и JavaScript, у меня есть целый курс на тему Learnable, который называется JavaScript in Motion ! Я подойду к этой статье с предположением, что вы понимаете тэги canvas и video, а также как транслировать видео в тэг canvas. Или с предположением, что вы достаточно уверены, чтобы решить это на ходу!

Демо-код

Если вы заинтересованы в том, чтобы прямо в код и попробовать его, вы можете найти его здесь, на GitHub .

Хотите попробовать это в действии? У меня есть работающая версия, размещенная здесь: Reality Filter .

Примечание: недавнее изменение в том, как Chrome обрабатывает ввод с камеры, требует, чтобы страница работала по HTTPS, чтобы это работало!

Как это будет работать

Мы будем использовать ту же начальную настройку из предыдущей статьи Google Cardboard — сцену Three.js, которую мы отображаем с помощью стереоскопического эффекта. Благодаря этому эффекту у нас есть дисплей для каждого глаза, благодаря чему все выглядит великолепно в 3D в виртуальной реальности. Однако вместо плавающих частиц и тому подобного из предыдущей статьи мы удаляем большинство элементов и помещаем одну простую сетку Three.js перед камерой, которая воспроизводит канал нашей камеры.

Наш код объяснил

Глядя на наши объявления переменных, большинство переменных здесь будут знакомы тем, кто прошел предыдущую демонстрацию. Переменные для подготовки нашей сцены Three.js, камера, средство визуализации, элемент для вывода на холст, контейнер для размещения этого элемента и переменная для хранения нашего стереоскопического эффекта одинаковы.

var scene,
      camera, 
      renderer,
      element,
      container,
      effect,

Наши три новые переменные, связанные с нашей камерой, — это videocanvascontext

 video,
      canvas,
      context,
  • video<video> Это будет иметь нашу камеру, воспроизводящую в нем.
  • canvascanvas Мы прочтем видеоданные с этого холста, а затем добавим на него наши фильтры тем перед размещением их содержимого в нашей сцене Three.js.
  • videocontext

У нас есть несколько других переменных, которые относятся к функциональности нашего фильтра.

 canvas
  • themes = ['blackandwhite', 'sepia', 'arcade', 'inverse'],
    currentTheme = 0,
    lookingAtGround = false;
  • themescurrentTheme
  • themeslookingAtGround

Мы начинаем с нашей функции init()

 init();

  function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.001, 700);
    camera.position.set(0, 15, 0);
    scene.add(camera);

    renderer = new THREE.WebGLRenderer();
    element = renderer.domElement;
    container = document.getElementById('webglviewer');
    container.appendChild(element);

    effect = new THREE.StereoEffect(renderer);

    element.addEventListener('click', fullscreen, false);

На этот раз у нас нет никакой функции перемещения камеры через событие DeviceOrientation По сравнению с виртуальной реальностью нам не нужно менять фактическое положение камеры в этой сцене Three.js. Мы держим сцену в одном месте — камера движется, когда пользователь смотрит вокруг.

Один из слушателей, который мы оставили в предыдущем примере, — это прослушиватель событий, который переходит в полноэкранный режим, если мы касаемся сцены. Это удаляет адресную строку Chrome из нашего представления.

Другое использование для DeviceOrientationEvent

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

 if (window.DeviceOrientationEvent) {
    window.addEventListener('deviceorientation', function(evt) {
      if (evt.gamma > -1 && evt.gamma < 1 && !lookingAtGround) {
        lookingAtGround = true;
        currentTheme = (themes.length > currentTheme+1) ? currentTheme+1 : 0;

        setTimeout(function() {
          lookingAtGround = false;
        }, 4000);
      }
    }.bind(this));
  }

В этом коде мы наблюдаем, находится ли evt.gamma Это довольно точное место на земле, если вам кажется, что оно слишком маленькое и его трудно активировать, вы можете увеличить диапазон до -1,5-1,5 … и т. Д.

Когда они смотрят в этом диапазоне и когда lookingAtGroundfalse Это корректирует currentThemethemes Мы устанавливаем lookingAtGroundtrue Это гарантирует, что мы меняем фильтр не чаще, чем раз в четыре секунды.

Получение нашего первичного канала камеры

Чтобы отфильтровать мир вокруг нас, нам нужен доступ к «окружающей» камере на нашем смартфоне. Мы начинаем с создания элемента <video> В настройках мы устанавливаем для facingMode"environment" Если нет, то вместо этого будет использоваться камера в стиле селфи. Это полезно, когда вы тестируете на ноутбуке без камеры! (Обратите внимание, что ваш ноутбук может постоянно переключать фильтры, если это так, вам нужно отключить это перед тестированием!)

 video = document.createElement('video');
  video.setAttribute('autoplay', true);
  
  var options = {
    video: {
      optional: [{facingMode: "environment"}]
    }
  };

Наш следующий шаг заключается в том, чтобы на самом деле включить нашу камеру с помощью этих опций Для этого мы используем MediaStream API . Это набор API-интерфейсов JavaScript, которые позволяют извлекать данные из локальных аудио- и видеопотоков — идеально подходят для получения потока с камеры нашего телефона. В частности, мы будем использовать функцию getUserMedia MediaStream API все еще находится в «Черновике W3C-редактора» и реализован несколько иначе, чем браузер. Эта демонстрация ориентирована в основном на Google Chrome для мобильных устройств, но для будущей совместимости мы получаем тот, который работает с текущим браузером нашего пользователя, и назначаем его на navigator.getUserMedia

 navigator.getUserMedia = navigator.getUserMedia ||
  navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

Затем, пока наш браузер MediaStreamTrackgetUserMedia

 if (typeof MediaStreamTrack === 'undefined' && navigator.getUserMedia) {
    alert('This browser doesn\'t support this demo :(');
  } else {
    // Get our camera data!

В MediaStream API у нас есть функция в MediaStreamTrack.getSources() Он может извлекать данные с каждого микрофона, подключенного к вашему устройству, а также видеоданные с каждой камеры.

Возвращенные значения из этой функции доступны нам в массиве с именем sources Мы перебираем каждый источник и ищем тех, чей kind"video" Каждый источник будет иметь kind"audio""video" Затем мы видим, имеет ли найденное видео свойство facing"environment" Мы извлекаем его идентификатор в API, а затем обновляем наш объект options

 MediaStreamTrack.getSources(function(sources) {
      for (var i = 0; i !== sources.length; ++i) {
        var source = sources[i];
        if (source.kind === 'video') {
          if (source.facing && source.facing == "environment") {
            options.video.optional.push({'sourceId': source.id});
          }
        }
      }

Объект options

 {
    video: {
      optional: [{facingMode: "environment"}, {sourceId: "thatSourceIDWeRetrieved"}]
    }
  }

Наконец, мы передаем эти параметры нашей функции navigator.getUserMedia Это сделает извлечение наших видео данных.

 navigator.getUserMedia(options, streamFound, streamError);
    });
  }

Помещение нашей камеры в нашу сцену

Получив наш видеопоток, мы помещаем его в нашу сцену в нашем streamFound() Мы начнем с добавления нашего видеоэлемента в DOM, установки его содержимого в нашем возвращаемом видеопотоке и установки его на всю ширину и высоту окна (так как мы хотим, чтобы высокое разрешение считывалось на нашем холсте).

 video

После того, как на странице воспроизводится поток нашей камеры, мы создаем элемент canvas в JavaScript, который мы используем для манипулирования нашими видеоданными. Сам элемент canvas никогда не добавляется на саму страницу, он остается только в нашем JavaScript.

Мы устанавливаем наш холст на ту же ширину и высоту, что и видео, округляя до ближайшей степени двойки. Причина этого в том, что текстуры Three.js лучше всего работают как степени 2. Если вы передаете другие значения ширины и высоты, которые не соответствуют этому, это совершенно нормально, но вы должны использовать определенные function streamFound(stream) {
document.body.appendChild(video);
video.src = URL.createObjectURL(stream);
video.style.width = '100%';
video.style.height = '100%';
video.play();
minFilter
Я предпочел настроить его на степень двух, чтобы все было просто.

 magFilter

Затем мы создаем нашу текстуру Three.js, которая будет содержать потоковое видео, передавая в нее наш элемент canvas = document.createElement('canvas');
canvas.width = video.clientWidth;
canvas.height = video.clientHeight;
canvas.width = nextPowerOf2(canvas.width);
canvas.height = nextPowerOf2(canvas.height);

function nextPowerOf2(x) {
return Math.pow(2, Math.ceil(Math.log(x) / Math.log(2)));
}
Мы устанавливаем нашу переменную canvascontext Держать все это в синхронизации.

 canvas

Затем мы создаем плоскость Three.js, на которую будем помещать наш канал, используя context = canvas.getContext('2d');
texture = new THREE.Texture(canvas);
texture.context = context;
Я установил его на 1920 × 1280 в качестве базового размера для нашего видео.

 THREE.PlaneGeometry

Затем мы создаем объект var cameraPlane = new THREE.PlaneGeometry(1920, 1280); Мы THREE.Mesh-600 Если у вас есть видео канал другого размера, вам может потребоваться отрегулировать положение z, чтобы форма заполняла область просмотра.

 cameraMesh = new THREE.Mesh(cameraPlane, new THREE.MeshBasicMaterial({
      color: 0xffffff, opacity: 1, map: texture
    }));
    cameraMesh.position.z = -600;

    scene.add(cameraMesh);
  }

После этого у нас есть функция обратного вызова с ошибкой, которая запустит console.log

 function streamError(error) {
    console.log('Stream error: ', error);
  }

В конце нашей функции init()animate() Вот где мы будем обрабатывать видеоизображение:

 animate();

Применение фильтров

Наша функция animate()context.drawImage()

 function animate() {
    if (context) {
      context.drawImage(video, 0, 0, canvas.width, canvas.height);

Оттуда мы можем прочитать наш холст, используя context.getImageData() Приведенный ниже код начинается с настроек нашего черно-белого фильтра, который считывает наши данные, получает общую яркость каждого пикселя изображения и затем фильтрует каждый пиксель как черный, серый или белый в зависимости от масштаба яркости, который он содержит. , Это дает ощущение мультипликации / старого стиля газеты к изображению.

 if (themes[currentTheme] == 'blackandwhite') {
        var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
        var data = imageData.data;

        for (var i = 0; i < data.length; i+=4) {
          var red = data[i],
              green = data[i+1],
              blue = data[i+2],
              luminance = ((red * 299) + (green * 587) + (blue * 114)) / 1000; // Gives a value from 0 - 255
          if (luminance > 175) {
            red = 255;
            green = 255;
            blue = 255;
          } else if (luminance >= 100 && luminance <= 175) {
            red = 190;
            green = 190;
            blue = 190;
          } else if (luminance < 100) {
            red = 0;
            green = 0;
            blue = 0;
          }

          data[i] = red;
          data[i+1] = green;
          data[i+2] = blue;
        }

        imageData.data = data;

        context.putImageData(imageData, 0, 0);
      }

Это выглядит так:

Наш черно-белый фильтр реальности в действии

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

 else if (themes[currentTheme] == 'inverse') {
        var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
        var data = imageData.data;

        for (var i = 0; i < data.length; i+=4) {
          var red = 255 - data[i],
              green = 255 - data[i+1],
              blue = 255 - data[i+2];

          data[i] = red;
          data[i+1] = green;
          data[i+2] = blue;
        }

        imageData.data = data;

        context.putImageData(imageData, 0, 0);
      }

Это выглядит так:

Наш обратный фильтр реальности в действии

Наша тема сепии использует формулу, которую я видел в различных местах в Интернете, чтобы придать изображению эффект сепии, цвета старой школы. Я также добавляю шум к изображению, добавляя случайный уровень красного, зеленого и синего к каждому пикселю. Если пиксели через сепию будут иметь более высокий уровень цвета, чем 255, я ограничу его 255.

 else if (themes[currentTheme] == 'sepia') {
        var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
        var data = imageData.data;

        for (var i = 0; i < data.length; i+=4) {
          var red = data[i],
              green = data[i+1],
              blue = data[i+2];
              
          var sepiaRed = (red * 0.393) + (green * 0.769) + (blue * 0.189);
          var sepiaGreen = (red * 0.349) + (green * 0.686) + (blue * 0.168);
          var sepiaBlue = (red * 0.272) + (green * 0.534) + (blue * 0.131);

          var randomNoise = Math.random() * 50;

          sepiaRed += randomNoise;
          sepiaGreen += randomNoise;
          sepiaBlue += randomNoise;

          sepiaRed = sepiaRed > 255 ? 255 : sepiaRed;
          sepiaGreen = sepiaGreen > 255 ? 255 : sepiaGreen;
          sepiaBlue = sepiaBlue > 255 ? 255 : sepiaBlue;

          data[i] = sepiaRed;
          data[i+1] = sepiaGreen;
          data[i+2] = sepiaBlue;
        }

        imageData.data = data;

        context.putImageData(imageData, 0, 0);
      }

Это выглядит так:

Наш фильтр реальности сепии в действии

Наконец, мой любимый из всех эффектов! Стиль «аркада», который пикселирует изображение, чтобы оно выглядело как ретро мир. Чтобы добиться этого эффекта, я настроил плагин Close Pixelate от David DeSandro и John Schulz. Исходная версия плагина преобразует встроенное изображение и заменяет его пиксельной версией холста. Моя версия вместо этого берет данные холста и помещает их обратно в тот же холст и контекст, чтобы мы могли использовать их для живого видео. Моя настроенная версия по-прежнему принимает все те же параметры, что и на их странице плагина . Он немного медленнее, чем другие фильтры выше, и его можно было бы оптимизировать, если бы у меня было время изучить его. На данный момент, я в порядке с небольшим отставанием, заставляет это чувствовать себя более ретро! Примечание для тех, кто хочет применить новые параметры в этом фильтре (например, вместо этого превращая мир в алмазы) — он может заставить его отставать еще больше!

 else if (themes[currentTheme] == 'arcade') {
        ClosePixelation(canvas, context, [
          {
            resolution: 6
          }
        ]);
      }

Это выглядит так:

Наш пиксельный фильтр реальности в действии

Наконец, мы устанавливаем текстуру для обновления в следующем кадре для Three.js (как мы определенно изменили ее каким-то образом) и снова запускаем animate()requestAnimationFrame() Мы также запускаем код для обновления и перерисовки нашей сцены Three.js.

 if (video.readyState === video.HAVE_ENOUGH_DATA) {
        texture.needsUpdate = true;
      }
    }

    requestAnimationFrame(animate);

    update();
    render();
  }

Время HTTPS

Обновление от конца 2015 года — я возвращаюсь к этой статье, чтобы добавить новую довольно важную информацию — Chrome теперь требует, чтобы веб-страницы, использующие камеру, обслуживались по HTTPS. Поэтому, прежде чем пытаться выполнить это, вам нужно найти способ запустить свой сервис через HTTPS. Один из методов, которые я использовал для тестирования, — это ngrok, который может предоставить HTTPS-туннель вашему локальному хосту. У нас есть руководство по доступу к Localhost From Anywhere здесь, на SitePoint, которое поможет вам начать работу.

В бою

Чтобы получить доступ к веб-камере и всем остальным, вам нужно разместить ее на сервере, а не запускать ее локально. Для тестирования я использовал ngrok для тестирования с моего Mac на моем телефоне. Если вы новичок в идее настройки службы, такой как ngrok, у меня есть статья на SitePoint о том, как это сделать, в Accessing Localhost From Anywhere . В противном случае, отправьте ваши вещи на веб-сервер и протестируйте их!

Запустите его в своем Google Cardboard или другой гарнитуре VR, и вы должны увидеть окружающую среду с нашим черно-белым фильтром для начала. Если вы посмотрите на землю, она должна поменять фильтры. Это очень весело! Вот небольшой анимированный GIF-файл, чтобы показать его в действии (за пределами гарнитуры, чтобы вы могли видеть, что он отображает):

Наш фильтр реальности в действии!

Вывод

Объединяя возможности Google Cardboard, HTML5, JavaScript и Three.js, вы получаете действительно замечательные возможности, которые не ограничиваются только виртуальной реальностью. Используя вход камеры, вы также можете перенести мир вокруг себя на сцену! Есть много других областей, в которых эта первоначальная идея может быть развита. Также возможно фильтровать изображение через сам Three.js с помощью шейдеров и добавлять объекты дополненной реальности на вашу сцену — две идеи, о которых я расскажу в следующих статьях.

Если вы сделаете действительно хороший AR опыт, основываясь на этой демонстрации, оставьте заметку в комментариях или свяжитесь со мной в Twitter ( @thatpatrickguy ), я всегда очень хочу посмотреть!