使用Three.js 95行代码实现一个全景视频播放器原型

9,084 阅读2分钟

缘由

昨天晚上睡不着觉,突然从国外的视频网站VR视频里面得到灵感,花了40分钟写下了这个小小demo,和大家分享

效果图

xy-20210802-153030.gif

原理

首先我们使用一个球体

image.png

将视频的每一帧生成一张纹理单元按照球体的uv在片元着色器内进行展开

摄像机位置放在原点位置

使用rAf更新渲染器

准备物料

视频素材准备

首先我们从某个不知名的网站(国内国外都可以)科学手段下载一部全景视频。用vlc播放器打开,大概长这样:

image.png

代码实现

首先我们引入three和其自带的轨道控制器

import * as THREE from "three/build/three.module";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

在html里面加入如下DOM

 <div class="player">
    <div>
      <button @click="$refs.video.play()">播放</button>
    </div>
    <video
      preload
      ref="video"
      controls
      loop
      style="width: 100%; visibility: hidden; position: absolute"
      :src="src"
    ></video>

    <canvas
      style="width: 80%; height: 823px"
      width="1920"
      height="823"
      ref="canvas"
    ></canvas>
  </div>

初始化视频纹理

initVideoTexture() {
      this.videoTexture = new THREE.VideoTexture(this.$refs.video);
      this.videoTexture.needsUpdate = true;
      this.videoTexture.updateMatrix();
    }

这里我们使用Three.js 自带的视频纹理,当让也可以自己实现,创建一个离屏canvas,drawImage 把 video 传进去,通过 Texture 实例化也是可以的。记得要把 needUpdate 属性设为true

初始化场景

    initScene() {
      this.scene = new THREE.Scene();
    }

初始化相机

注意,透视摄像机的起始点要设为(0,0,0)

    initCamera() {
      this.camera = new THREE.PerspectiveCamera(45, 1024 / 768, 1, 1000);
      this.camera.position.z = 30;
      this.controls = new OrbitControls(this.camera, this.renderer.domElement);
      this.controls.maxDistance = 100;

      this.controls.update();
      // const helper = new THREE.CameraHelper(this.camera);
      // this.scene.add(helper);
      this.scene.add(this.camera);
    }

初始化网格

注意,这里定义材质我们要利用uniforms 对象来吧我们刚刚初始化的纹理对象传进去,这里我们使用 tex_0 作为我们使用的变量名称

    initMesh() {
      this.geometry = new THREE.SphereGeometry(100, 32, 16);
      this.material = new THREE.ShaderMaterial({
        wireframe: false,
        side: THREE.DoubleSide,
        map: this.videoTexture,
        uniforms: {
          tex_0: new THREE.Uniform(this.videoTexture),
        },
        vertexShader: require("@/components/v.glsl").default,
        fragmentShader: require("@/components/f.glsl").default,
      });
      this.mesh = new THREE.Mesh(this.geometry, this.material);
    }

tick函数

  update() {
      this.renderer.render(this.scene, this.camera);

      requestAnimationFrame(this.update);
    }

mounted函数

由于我使用的是vue SFC ,在mounted 钩子里面编写

 mounted() {
    this.initRenderer();
    this.initScene();
    this.initVideoTexture();
    this.initMesh();
    this.initCamera();
    this.addMeshToScene();
    this.update();
  }

着色器实现

顶点着色器

这里我们使用three.js 自带的MVP矩阵对传入的顶点坐标进行世界转换,然后把我们的uv通过varying 关键字定义的的变量 v_uv传入到片元着色器

precision highp float;
varying vec2 v_uv;
void main() {
    gl_Position = projectionMatrix *
        modelViewMatrix *
        vec4(position.xyz, 1.0);
    v_uv = uv;
}

片元着色器

注意,由于球体的默认法线是由内向外的,所以我们的uv纹理坐标的映射也是向外的,我们在设置双面渲染的时候,由于我们的摄像机是被球体包裹在内的,因此会发生球内纹理翻转的现象,这明显是不对的,因此我们需要把uv 坐标坐下翻转,很简单,用 1.0 - v_uv.x 即可

precision highp float;
varying vec2 v_uv;
uniform sampler2D tex_0;
void main() {
    vec4 texColor = texture2D(tex_0, vec2(1. - v_uv.x, v_uv.y));
    gl_FragColor = texColor;
}

这样我们的全景视频就实现了。demo我已经放上gitee了,各位大佬有什么想法或者分享也可以私信我哦~~