使用JavaScript和Google Cardboard过滤现实

在移动浏览器中运行虚拟现实的能力令人振奋并且令人兴奋。 Google Cardboard和其他类似的VR设备非常简单,只需将手机放入支架即可使用! 之前,我讨论了使用Google Cardboard和Three.js将VR投放到Web的问题 ,其中讨论了构建引入 Web数据的VR环境的基础。 人们真的很喜欢那篇文章(我也非常喜欢构建该演示),所以我想我会以不同的想法对其进行扩展。 与其引入Web 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场景。 这种效果使我们能够为每只眼睛提供显示,从而使事物在VR中呈现3D效果。 但是,我们没有删除上一篇文章中的浮动粒子,而是删除了大多数元素,并在播放我们的相机供稿的相机前面放置了一个简单的Three.js网格。

我们的守则解释

查看我们的变量声明,这里的大多数变量对于上一个示例的读者来说都是熟悉的。 用于准备Three.js场景的变量,相机,渲染器,用于画布输出的元素,用于放置该元素的容器以及用于存储立体效果的变量都是相同的。

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

与我们的摄像机供稿相关的三个新变量是videocanvascontext

video,
      canvas,
      context,
  • video –我们实际的HTML5 <video>元素。 那将在其中播放我们的相机提要。
  • canvas –一个虚拟的canvas元素,将包含我们video元素的内容。 我们将从该画布中读取视频数据,然后将主题过滤器添加回其上,然后再将其内容放入Three.js场景中。
  • context -我们的canvas '2D背景的,我们用它来对其执行的大部分功能。

在与我们的过滤器功能相关的变量下,我们还有其他一些变量。

themes = ['blackandwhite', 'sepia', 'arcade', 'inverse'],
      currentTheme = 0,
      lookingAtGround = false;
  • themes -我们的过滤器名称的数组。
  • currentTheme –当前正在themes数组中查看的索引。
  • lookingAtGround –是否查看地面(很快就会知道)。

我们从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事件实现任何相机移动功能。 与VR体验相比,在此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和1之间。如果是,则它们正在查看地面。 这是地面上的一个精确点,如果发现它太小且难以触发,则可以将范围增加到-1.5至1.5…等。

当他们在此范围内查找并且lookingAtGroundfalse ,我们将运行主题切换器代码。 这会将currentTheme调整为themes数组的下一个索引号。 我们将lookingAtGround设置为true并在4秒后将其设置回。 这样可以确保我们最多每四秒更换一次过滤器。

检索我们的主要相机供稿

为了过滤我们周围的世界,我们需要访问智能手机上面向“环境”的摄像头。 我们首先创建一个<video>元素,将autoplay设置为true(因为我们希望摄像机立即播放),然后为流设置选项。 在选项中,我们将facingMode设置为"environment" ,如果可用,它将使用该相机。 如果没有,它将改用自拍相机。 当您在没有环境摄像头的笔记本电脑上进行测试时,这很有用! (请注意,您的笔记本电脑可能会不断切换过滤器,如果这样,您需要在测试之前将其关闭!)

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

下一步是使用这些选项实际拉入我们的相机供稿。 为此,我们使用MediaStream API 。 这是一组JavaScript API,使我们能够从本地音频和视频流中提取数据-非常适合获取手机的摄像头流。 特别是,我们将使用getUserMedia函数。 MediaStream API仍在“ W3C编辑器的草稿”中,并且在浏览器之间实现的方式略有不同。 该演示主要针对移动版Google Chrome,但出于将来的兼容性考虑,我们获得了一个与用户当前浏览器兼容的演示并将其分配给navigator.getUserMedia

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

然后,只要我们的浏览器能够从MediaStream API理解MediaStreamTrack并成功在我们的浏览器中找到兼容的getUserMedia函数,我们就将开始搜索摄像机数据。

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" 。 然后,我们查看找到的视频是否具有等于"environment"facing属性,如果是,则这是我们希望使用的摄像机。 我们在API中检索其ID,然后从较早版本更新我们的options对象,以将该源ID也包括为首选的视频流。

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()场景中。 首先,将video元素添加到DOM,将其内容设置为返回的视频流,并使其成为窗口的整个宽度和高度(因为我们希望将高分辨率读入画布)。

function streamFound(stream) {
    document.body.appendChild(video);
    video.src = URL.createObjectURL(stream);
    video.style.width = '100%';
    video.style.height = '100%';
    video.play();

在页面中播放完摄像机流之后,我们在JavaScript中创建一个canvas元素,用于对视频数据进行操作。 canvas元素本身永远不会添加到页面本身中,仅保留在我们的JavaScript中。

我们将画布设置为与视频相同的宽度和高度,四舍五入到最接近的2的幂。 这样做的原因是Three.js纹理以2的幂次方最有效。如果传入不符合此要求的其他宽度和高度,那完全没问题,但是必须使用特定的minFiltermagFilter选项。 我更喜欢将其调整为2的幂,以使这里的事情保持简单。

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))); 
  }

接下来,我们创建Three.js纹理,其中将包含我们的流式视频素材,并将canvas元素传递到其中。 我们将context变量设置为创建的canvas元素的上下文,并将纹理的上下文分配给canva的上下文。 保持所有同步。

context = canvas.getContext('2d');
    texture = new THREE.Texture(canvas);
    texture.context = context;

然后,我们使用THREE.PlaneGeometry创建Three.js平面,并将我们的提要放到THREE.PlaneGeometry 。 我将其设置为1920×1280作为视频的基本尺寸。

var cameraPlane = new THREE.PlaneGeometry(1920, 1280);

然后,我们使用平面和视频馈送使用纹理创建一个THREE.Mesh对象。 我们将其在-z轴上放置-600 ,将其从视野中移开,并将其添加到Three.js场景中。 如果您使用其他尺寸的视频供稿,则可能需要调整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);
      }

看起来像这样:

我们的黑白现实滤镜在行动

下一个主题反转我们的像素,因此白色是黑色,依此类推。 它为图像提供X射线样式:

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);
      }

看起来像这样:

我们的棕褐色现实过滤器在行动

最后,我最喜欢的所有效果! “街机”风格将图像像素化,使其看起来像复古世界。 为了达到这种效果,我调整了David DeSandro和John Schulz的Close Pixelate插件。 插件的原始版本将转换嵌入式图像并将其替换为像素化画布版本。 我的版本取画布数据并将其放回相同的画布和上下文中,因此我们可以将其用于实时视频。 我调整后的版本仍然接受与其插件页面上相同的所有参数。 它比上面的其他过滤器慢一点,如果我有时间研究它,可能会对其进行优化。 现在,我会有点滞后,这会让它显得更复古! 给任何希望在过滤器中应用新选项的人(例如,将世界变成钻石)的注释–它可能使它滞后甚至更多!

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

看起来像这样:

我们行动中的像素化现实滤镜

最后,我们设置纹理以在Three.js的下一帧更新(因为我们已经以某种方式对其进行了更改),然后在下一个requestAnimationFrame()上再次运行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隧道。 我们在SitePoint的“ 从任何地方访问本地主机”中都有指南,可以帮助您入门。

行动中

为了能够访问网络摄像头以及所有网络摄像头,您似乎需要将其托管在服务器上,而不是在本地运行。 为了进行测试,我使用ngrok在手机上的Mac上进行测试。 如果您不熟悉设置ngrok之类的服务的想法,请参阅SitePoint上有关如何从任何地方访问Localhost的文章。 否则,将您的东西通过FTP传输到某处的Web服务器上并进行测试!

在您的Google Cardboard或其他VR头戴式耳机中运行它,您应该使用我们的黑白滤镜开始观察周围的环境。 如果您低头看地面,则应切换滤波器。 太有趣了! 这是一个小的动画gif,可以将其显示出来(在耳机外部,因此您可以看到它的显示内容):

我们的现实过滤器正在行动!

结论

结合使用Google Cardboard,HTML5,JavaScript和Three.js的强大功能,可以带来一些真正巧妙的可能性,而不仅限于虚拟现实。 使用摄像机输入,您也可以将周围的世界带入场景! 这个最初的想法还有很多其他方面可以发展。 还可以使用着色器通过Three.js本身对图像进行过滤,并可以将增强现实对象添加到您的场景中-我将在以后的文章中介绍两个想法。

如果您基于此演示进行了一些非常整洁的AR体验,在注释中留下笔记或在Twitter( @thatpatrickguy )上与我联系,我总是非常乐意看一下!

From: https://www.sitepoint.com/filtering-reality-with-javascript-google-cardboard/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值