• Three.js系列: 在元宇宙看电影,享受 VR 视觉盛宴


    本文 gihtub 地址: https://github.com/hua1995116/Fly-Three.js

    最近元宇宙的概念很火,并且受到疫情的影响,我们的出行总是受限,电影院也总是关门,但是在家里又没有看大片的氛围,这个时候我们就可以通过自己来造一个宇宙,并在 VR 设备(Oculus 、cardboard)中来观看。

    今天我打算用 Three.js 来实现个人 VR 电影展厅,整个过程非常的简单,哪怕不会编程都可以轻易掌握。

    想要顶级的视觉盛宴,最重要的肯定是得要一块大屏幕,首先我们就先来实现一块大屏幕。

    大屏幕的实现主要有两种几何体,一种是 PlaneGeometry 和 BoxGeometry,一个是平面,一个是六面体。为了使得屏幕更加有立体感,我选择了 BoxGeometry。

    老样子,在添加物体之前,我们先要初始化我们的相机、场景和灯光等一些基础的元件。

    const scene = new THREE.Scene();
    
    // 相机
    const camera = new THREE.PerspectiveCamera(
        75,
        sizes.width / sizes.height,
        0.1,
        1000
    )
    camera.position.x = -5
    camera.position.y = 5
    camera.position.z = 5
    scene.add(camera);
    
    // 添加光照
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
    scene.add(ambientLight)
    
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
    directionalLight.position.set(2, 2, -1)
    
    scene.add(directionalLight)
    
    // 控制器
    const controls = new OrbitControls(camera, canvas);
    scene.add(camera);
    

    然后来写我们的核心代码,创建一个 5 * 5 的超薄长方体

    const geometry = new THREE.BoxGeometry(5, 5, 0.2);
    const cubeMaterial = new THREE.MeshStandardMaterial({
        color: '#ff0000'
    });
    const cubeMesh = new THREE.Mesh(geometry, cubeMaterial);
    scene.add(cubeMesh);
    

    效果如下:

    然后紧接着加入我们的视频内容,想要把视频放入到3d场景中,需要用到两样东西,一个是 html 的 video 标签,另一个是 Three.js 中的视频纹理 VideoTexture

    第一步将视频标签放入到 html 中,并设置自定播放以及不让他显示在屏幕中。

    ...
    <canvas class="webgl"></canvas>
    <video 
      id="video"
      src="./pikachu.mp4"
      playsinline
      webkit-playsinline
      autoplay
      loop
      style="display:none"
      ></video>
    ...
    

    第二步,获取到 video 标签的内容将它传给 VideoTexture,并且纹理赋给我们的材质。

    +const video = document.getElementById( 'video' );
    +const texture = new THREE.VideoTexture( video );
    
    const geometry = new THREE.BoxGeometry(5, 5, 0.2);
    const cubeMaterial = new THREE.MeshStandardMaterial({
    -    color: '#ff0000'
    +    map: texture
    });
    const cubeMesh = new THREE.Mesh(geometry, cubeMaterial);
    scene.add(cubeMesh);
    

    我们看到皮神明显被拉伸了,这里就出现了一个问题就是纹理的拉伸。这也很好理解,我们的屏幕是 1 : 1 的,但是我们的视频却是 16:9 的。想要解决其实也很容易,要么就是让我们的屏幕大小更改,要么就是让我们的视频纹理渲染的时候更改比例。

    第一种方案很简单

    通过修改几何体的形状(也及时我们显示器的比例)

    const geometry = new THREE.BoxGeometry(8, 4.5, 0.2);
    const cubeMaterial = new THREE.MeshStandardMaterial({
        map: texture
    });
    const cubeMesh = new THREE.Mesh(geometry, cubeMaterial);
    scene.add(cubeMesh);
    

    第二种方案稍微有点复杂,需要知道一定的纹理贴图相关的知识

    Untitled

    图1-1

    首先我们先要知道纹理坐标是由 u 和 v 两个方向组成,并且取值都为 0 - 1。通过在 fragment shader 中,查询 uv 坐标来获取每个像素的像素值,从而渲染整个图。

    因此如果纹理图是一张16:9 的,想要映射到一个长方形的面中,那么纹理图必要会被拉伸,就像我们上面的视频一样,上面的图为了表现出电视机的厚度所以没有那么明显,可以看一下的图。(第一张比较暗是因为 Three.js 默认贴图计算了光照,先忽略这一点)

    我们先来捋一捋,假设我们的图片的映射是按照 图1-1,拉伸的情况下 (80,80,0) 映射的是 uv(1,1 ),但是其实我们期望的是点(80, 80 * 9/16, 0) 映射的是 uv(1,1),所以问题变成了像素点位 (80, 80 * 9/16, 0) 的uv值 如何变成 (80, 80, 0) 的uv 值,更加简单一些就是如何让 80 * 9 / 16 变成 80,答案显而易见,就是 让 80 * 9 / 16 像素点的 v 值 乘以 16 / 9,这样就能找到了 uv(1,1) 的像素值。然后我们就可以开始写 shader 了。

    // 在顶点着色器传递 uv
    const vshader = `
    varying vec2 vUv;
    
    void main() {
      vUv = uv;
    
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
    `
    
    // 核心逻辑就是 vec2 uv = vUv * acept; 
    const fshader = `
    varying vec2 vUv;
    
    uniform sampler2D u_tex;
    uniform vec2 acept;
    
    void main()
    {
      vec2 uv = vUv * acept;
      vec3 color = vec3(0.3);
      if (uv.x>=0.0 && uv.y>=0.0 && uv.x<1.0 && uv.y<1.0) color = texture2D(u_tex, uv).rgb;
      gl_FragColor = vec4(color, 1.0);
    }
    `
    

    然后我们看到我们画面已经正常了,但是在整体屏幕的下方,所以还差一点点我们需要将它移动到屏幕的中央。

    移动到中央的思路和上面差不多,我们只需要注重边界点,假设边界点 C 就是让 80 * ( 0.5 + 9/16 * 0.5 ) 变成 80 ,很快我们也可能得出算是 C * 16/9 - 16/9 * 0.5 + 0.5 = 80

    然后来修改 shader,顶点着色器不用改,我们只需要修改片段着色器。

    const fshader = `
    varying vec2 vUv;
    
    uniform sampler2D u_tex;
    uniform vec2 acept;
    
    void main()
    {
      vec2 uv = vec2(0.5) + vUv * acept - acept*0.5;
      vec3 color = vec3(0.0);
      if (uv.x>=0.0 && uv.y>=0.0 && uv.x<1.0 && uv.y<1.0) color = texture2D(u_tex, uv).rgb;
      gl_FragColor = vec4(color, 1.0);
    }
    `
    

    好了,到现在为止,我们的图像显示正常啦~

    那么 Three.js 中的 textureVideo 到底是如何实现视频的播放的呢?

    通过查看源码(https://github.com/mrdoob/three.js/blob/6e897f9a42d615403dfa812b45663149f2d2db3e/src/textures/VideoTexture.js)源码非常的少,VideoTexture 继承了 Texture ,最大的一点就是通过 requestVideoFrameCallback 这个方法,我们来看看它的定义,发现 mdn 没有相关的示例,我们来到了 w3c 规范中寻找 https://wicg.github.io/video-rvfc/

    这个属性主要是获取每一帧的图形,可以通过以下的小 demo 来进行理解

    <body>
      <video controls></video>
      <canvas width="640" height="360"></canvas>
      <span id="fps_text"/>
    </body>
    
    <script>
      function startDrawing() {
        var video = document.querySelector('video');
        var canvas = document.querySelector('canvas');
        var ctx = canvas.getContext('2d');
    
        var paint_count = 0;
        var start_time = 0.0;
    
        var updateCanvas = function(now) {
          if(start_time == 0.0)
            start_time = now;
    
          ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    
          var elapsed = (now - start_time) / 1000.0;
          var fps = (++paint_count / elapsed).toFixed(3);
          document.querySelector('#fps_text').innerText = 'video fps: ' + fps;
    
          video.requestVideoFrameCallback(updateCanvas);
        }
    
        video.requestVideoFrameCallback(updateCanvas);
    
        video.src = "http://example.com/foo.webm"
        video.play()
      }
    </script>
    
    折叠

    通过以上的理解,可以很容易抽象出整个过程,通过 requestVideoFrameCallback 获取视频每一帧的画面,然后用 Texture 去渲染到物体上。

    Untitled

    然后我们来加入 VR 代码, Three.js 默认给他们提供了建立 VR 的方法。

    // step1 引入 VRButton
    import { VRButton } from 'three/examples/jsm/webxr/VRButton.js';
    // step2 将 VRButton 创造的dom添加进body 
    document.body.appendChild( VRButton.createButton( renderer ) );
    // step3 设置开启 xr
    renderer.xr.enabled = true;
    // step4 修改更新函数
    renderer.setAnimationLoop( function () {
    	renderer.render( scene, camera );
    } );
    

    由于 iphone 太拉胯不支持 webXR ,特地借了台安卓机(安卓机需要下载 Google Play、Chrome 、Google VR),添加以上步骤后,就会如下显示:

    点击 ENTER XR 按钮后,即可进入 VR 场景。

    然后我们我们可以再花20块钱就可以买个谷歌眼镜 cardboard。体验地址如下:

    https://fly-three-js.vercel.app/lesson03/code/index4.html

    或者也可以像我一样买一个 Oculus 然后躺着看大片

    系列其他文章:

  • 相关阅读:
    Worthington肽合成应用丨胰凝乳蛋白酶方案
    LeetCode50天刷题计划第二季(Day 32 — 阶乘后的零(13.20-14.00)
    告警运维中心|构建高效精准的告警协同处理体系
    数据库的基本操作
    jvm 一之 类加载器
    mongodb的简单查询
    Server-Sent Events 一种轻量级的Push方式
    手写RPC——数据序列化工具protobuf
    linux 3.13版本nvme驱动阅读记录三
    ArcGIS API for JavaScript实现要素服务query接口的功能
  • 原文地址:https://www.cnblogs.com/qiufenghua/p/16466045.html