1

Three.js系列: 在元宇宙看电影,享受 VR 视觉盛宴 - 蓝色的秋风

 2 years ago
source link: https://www.cnblogs.com/qiufenghua/p/16466045.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

效果如下:

4f4e4697a7ba4d2680d27767089b8528~tplv-k3u1fbpfcp-zoom-1.image

然后紧接着加入我们的视频内容,想要把视频放入到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);
26398c5949044b879de155eb8a91f4f1~tplv-k3u1fbpfcp-zoom-1.image

我们看到皮神明显被拉伸了,这里就出现了一个问题就是纹理的拉伸。这也很好理解,我们的屏幕是 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

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

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

459a60a378f1455289a8a714daecf5b4~tplv-k3u1fbpfcp-zoom-1.image
da41db8888684fb2aa09ce84fd976f23~tplv-k3u1fbpfcp-zoom-1.image

我们先来捋一捋,假设我们的图片的映射是按照 图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);
}
`
46ae765e7b7c4a70a3ea6a05adde4b3b~tplv-k3u1fbpfcp-zoom-1.image

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

移动到中央的思路和上面差不多,我们只需要注重边界点,假设边界点 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);
}
`

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

8d4a0bedf107435a8576ed45fa94a89e~tplv-k3u1fbpfcp-zoom-1.image

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

784d215a8cc54e8cb0cc774c99c090ad~tplv-k3u1fbpfcp-zoom-1.image

通过查看源码(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),添加以上步骤后,就会如下显示:

d0d9657ac9ac433ea784ad9dad00fa27~tplv-k3u1fbpfcp-zoom-1.image

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

fedcdeb2f2164c6b8762c782acbe626f~tplv-k3u1fbpfcp-zoom-1.image

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

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

d99773786c044850958808117db789ec~tplv-k3u1fbpfcp-zoom-1.image

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

aabf39f0beb1403ebc2eb226c7504129~tplv-k3u1fbpfcp-zoom-1.image

系列其他文章:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK