乐趣区

关于前端:Threejs系列-在元宇宙看电影享受-VR-视觉盛宴

1.Three.js 系列: 写一个第一 / 三人称视角小游戏
2.Three.js 系列: 造个陆地球池来学习物理引擎

本文 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);

第二种计划略微有点简单,须要晓得肯定的纹理贴图相干的常识

图 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 去渲染到物体上。

而后咱们来退出 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 而后躺着看大片

退出移动版