本文作者:@梧忌  @云墨 
双促期间,手淘产出了大量的高质量全景视频,并且根据业务配置生成了大量的封面视频。封面视频在双促期间有了大量的曝光和引导观看。在本地化会场,封面视频承接的模块点击率提升了 2 倍
为什么全景视频如此吸引消费者?全景视频在双十一是怎么应用的?又为什么需要封面视频?前端在这方面做了哪些的工作?本文将从业务需求到技术方案进行层层拆解,深入浅出全景封面视频生成这一场景。
手淘上的全景封面视频

全景视频的应用

行业商品互动创新经过一年时间的沉淀,商品互动表达的核心交互形式基本确定,包含 3D 模型渲染,序列帧渲染方式,还有今年新推进的场景化互动:全景视频。
全景视频是一种用 3D 摄像机进行全方位 360 度拍摄的视频,用户在观看视频的时候可以随意调节视频上下左右动态地进行观看,用户有一种身临其境的感觉,不受空间和地域的限制。
全景视频目前在手淘中主要用于房产、家装等业务场景。在家装场景中,针对软装门店特色商品以全景视频的形式进行展示,用户可体验沉浸式导购,视频所见即门店所展示,规避图片展示的单一性,是一种全新的导购形式。
来看一个全景视频的线上示例:

全景视频的业务价值

全景视频在电商的应用,对于消费者、平台和商家的价值在于:
  • 全景视频提升了消费者的购物体验,场景化互动的形式支持 720 视角旋转,真实的视觉盛宴,增强用户体感,降低用户的决策成本;视频导购形式,用户可以转动视角更好的了解场景中的商品,有更深度的参与感。
  • 对于平台端,低拍摄成本+低制作成本+轻商品发布体系,将撬动家装行业线下海量优质高客单供给上行;新交互将带给平台更多的内容素材,丰富手淘内容生态
  • 在商家端,对比全景漫游不需要清场拍摄,拍摄更加灵活;以全景视频为媒介,在商品、内容、店铺(门店)等触点,全面赋能本地化商家提升公域流量获取能力和手段

为什么需要封面视频?

全景视频作为今年全新的互动形式,也需要从零开始建设其在手淘内的展现形式。在双 11 大促期间需要通过会场、主搜、轻店等列表页上对外进行透出
全景视频在列表页上进行透出,由于无法在平面上显示 3D 画面,以及区域有限无法进行交互,因此我们需要设计透出的方案。总结下来,有几种可行的方案:
方案形式优点缺点
视频封面图只显示一张图片生产简单转化吸引力不足
全景视频展示原视频中心点的画面不需要再加工无法显示有效的画面
封面视频*动态切换视角显示重点画面有一定的生产成本点击吸引力高
为了能够最大限度地提升全景视频的在列表页的曝光转化率,产品上最终选择了封面视频的方案。对于封面视频需要以普通平面视频的展示形式进行透出,且:
  • 消费体验需要越高越好,需体现出全景视频的特点,最好是动态切换视角始终显示重点画面;
  • 生成成本需要越低越好,最好是自动生成;
  • 视频时长不宜太长,消费时能够迅速抓住用户眼球,视频的存储成本也更低;
*全景封面视频,即全景视频的封面视频,以下简称为封面视频。
封面视频的生产流程

生成封面视频的方案

前面讲到过封面视频的需求的体验越高越好,成本越低越好,时长在可控的范围。对应的,有几个生成封面视频的方案:
方案描述优点缺点
依靠算法算法自动识别全景视频中讲解商品的时间段和画面位置,视频自动切换到该视角商家生产成本低技术开发成本高,视频质量不可控
依靠人工由商家在端上操作来选择视角和视频内容商家生产成本高技术开发成本一般,视频质量完全可控
基于规则基于一定的视角切换规则来生成商家生产成本一般技术开发成本一般,质量可控
当然还有可以采取前期依靠算法加以人工干预的方式,这里不一一展开。考虑到全景视频产品当下是初期状态,结合开发成本、商家生产成本,我们选择了基于规则,规则后台可配置,生产时需商家确认的方式
默认规则是:
  1. 截取整段视频的前 8s
  2. 视频画面定位到视频中间的位置
  3. 视频播放过程中,视角左右移动 20 度

全景视频的业务流程

全景视频的业务流程可以分为以下几个部分:
  1. 第一部分是全景视频的采集流程,通过 insta 360 设备进行拍摄,然后导出对应的全景视频文件;
  2. 第二部分是全景视频的素材生成,对应新零售工作台和小二工作台,通过这两个平台,完成商家对全景视频的上传及其业务关联,小二端全景视频的审核;
  3. 第三部分是审核通过的全景视频会在C 端 场景透出,区分具体的业务域和投放渠道

封面视频的业务流程

封面视频的生成和审核,在全景视频业务流程的第二部分,涉及到新零售工作台和小二工作台。
在新零售工作台上,为了能够让商家的操作成本最低,商家只需要配置在原有的流程上,点击自动生成封面视频,预览确认封面视频,即可完成整个流程:
  1. 第一步:生成
  1. 第二步:确认
基于这个业务流程,封面视频生产的系统时序图:
封面视频生成的技术调研
结合上面的产品交互,将生成全景视频封面视频的能力加入到整个流程中,主要有两个直接的技术调研方向:一个是在服务端对全景视频进行渲染处理,二是在前端对全景视频进行渲染处理。

服务端方案

服务端可以通过一些媒体处理库(例如 FFmpeg)将全景视频按照上诉规则来生成封面视频。其处理过程同下,差别在于需要针对每帧来进行视野移动:
1.将视频时长截取为 5s
$ ffmpeg -ss 00:00:00 -t 00:00:5 -i 全景视频.mp4 -vcodec copy -acodec copy output.mp4

2.将视频映射到球面或者立方体面
$ ffmpeg -i output.mp4  -vf v360=e:c3x2:cubic:out_pad=0.01 output2.mp4

3.选择视角进行截取

前端方案

前端可以将全景视频渲染出来,然后进行录制,基于规则参数自动操作视频的视角变化,生成普通视频文件。流程如下:
  • 在浏览器通过 WebGL 渲染全景视频;
  • 根据规则驱动全景视频的画面变化;
  • 通过 captureStream 获取 Canvas 上的媒体流数据;
  • 通过 MediaRecorder 将 Canvas 中的媒体流转化成视频文件;
  • 将视频文件上传到内容中台,从而进行下发透出。

方案对比

方案优点缺点
服务端方案质量高,计算速度快需要较大的计算资源
前端方案分布式,开发部署成本低性能一般
相比较而言,前端方案开发成本低,结合商家人工预览确认、调参的过程,做到视频质量可控,功能快速上线的目的。
基于前端技术的全景视频播放
根据上面的方案,要生成封面视频,我们需要在浏览器上打造一个播放器来渲染全景视频,并且提供 API 控制视野画面的能力。要渲染全景视频,我们需要了解全景视频是怎么拍摄的,生成的原始视频展现形式是怎样的,然后再看看如何将 3D 空间渲染到 2D 的平面上。

全景视频的拍摄

全景视频的摄影器材可以分面向普通消费者的消费级别;对发烧友或者专业团队初步探索的入门级别;拥有更复杂算法,更好的画面质量和 3D 效果的专业级;还有针对不同导演和客户调性设计的更高要求,使用和后期制作上更复杂的电影级。为了能够实现轻量级拍摄,平台建议采用消费级别的 insta 360 来进行拍摄。
家装场景下使用 insta 360 相机进行拍摄的场景如下:

全景视频的投影方式

全景是将多张图片按照一定的投影方式(projection)方式进行拼接,最终形成一个 360 度(球面)围绕观察者的图像。
那么全景视频的球面投影要解决哪些问题呢?由于球面全景视频与传统编解码方式不兼容,有投影的复杂度(算法效率)、投影后的图像失真程度(视频质量)等问题需要解决。全景视频在与传统视频具有同等清晰度的情况下,其像素总量往往是普通视频的 3-4 倍,因此对传输的带宽消耗巨大,如何减轻传输带宽压力也是全景视频研究中不得不面对的难点之一。
目前主流的投影方式有圆柱型投影立方体投影

圆柱体投影

圆柱体投影不借助中间的投影几何体而直接将球面投影在平面上。现在应用得最广泛的是等距圆柱体投影(Equi-Rectangular Projection, ERP)。它的实现过程如下:
  • 首先在平面宽长比为 1:2 的矩形区域内按照目标分辨率进行均匀的像素格划分,得到长为 w 等分宽为 h 等分的分割;
  • 然后按照矩形的长和宽在球面上进行均匀的经线和纬线采样,将经线 w等分,纬线 h 等分,获得球面网格。
这一方法的优势是简单且直观,便于直接播放和编辑。缺点也很明显,极低的投影复杂度带来的是投影均匀性的降低,两极处的像素采样密度大于赤道 —— 导致南北极部分内容会被严重拉伸且存在大量像素冗余,赤道部分则在还原时清晰度比较糟糕。
等距柱状投影格式的全景视频原始画面:

立方体投影

立方体投影(Cube Map Projection, CMP) 是通过将球面内容投影在立方体模型上后将各个面展开,然后拼接为矩形的一种投影方式。立方体投影通过透视的形式实现从球面到立方体面的映射,具体的操作其实就是简单的坐标比例缩放。由于立方体模型具有极好的对称性,所以在与球面进行相互投影的过程中可以大大降低计算复杂度,并且面与面之间的投影关系是一致的。
相比等距柱状投影,立方体贴图的扭曲更小。将等圆柱映射的 4K 全景视频(3840×1920)转换为相同的正方体映射的分辨率为 2880×1920,文件大小缩小了近 1/3,这也是立方体贴图的优点之一。
但是在球面映射到几何体表面的方法中,放射型投影由于模型每个面的中心位置到球心的距离不同,越靠近边角的地方离球体越远,所以投影的不均匀性无法避免。
在下图的立方体投影方式及其横截面示意图中可以看出,经过圆心和圆周上每一点的射线是以同样的角度向外发射的,但是在投影到正方形的边上时,越靠近中点对应的线段长度越短,越远离中点则对应线段越长,即圆上相同长度的弧映射到正方形上之后长度是不等的。因而球面上相同数量的像素点,投影到立方体边缘区域所分配到的采样像素数量会多于投影到中心区域时所分配的采样像素数量(即边缘区域稀疏,中心区域稠密):
立方体投影格式的全景视频原始画面(传统立方体投影会按照下图格式将六个面进行排布):

等角度立方体投影

等角度立方体投影(Equi-Angular Cubemap, EAC) 则是谷歌所提出的一种对立方体投影改进方法,通过调整球面像素点对应的立方体上采样像素点的位置来改善这种不均匀的分布。EAC 在 CMP 的结果之上,额外做一个映射,将原本长度不同的块映射为相同:
这样做的好处就是在相同的源视频分辨率下可以提高细节部分的清晰度:
等角度立方体投影格式的全景视频原始画面(文字方向都是基于旋转后天空在上、地面在下、画面方向正常的情况标注的):
观察截图可以发现,画面上半部分即为面向前方时的横向(左右)扫视图,下半部分逆时针旋转 90° 后即为面向后方时的纵向(上下)扫视图。
从投影质量、投影效率和带宽来进行对比,EAC 是三者中最优的。但由于历史和易于展示/编辑的原因,市面上摄像设备普遍产出的是 ERP 投影的全景视频。
在新零售工作台上,商家上传的是 ERP 投影的全景视频,上传后内容中台将转换为 EAC 投影来供手淘进行渲染播放。我们的封面视频生成环节是在商家的上传流程中,因此需要渲染的是 ERP 投影的全景视频。

3D 渲染基础知识

了解完投影的方式,接下来看如何实现投影。这部分涉及到一些 3D 渲染的基础知识。
想在屏幕上展示 3D 物体,大体上的思路是这样的:
  1. 创建一个三维空间,称之为场景(Scene)
  2. 确定一个观察点,并设置观察的方向和角度,称之为相机(Camera)
  3. 在场景中添加供观察的物体(Objects),物体有网格(Mesh), 线(Line), 点(Points)等
  4. 最后我们需要把所有的东西渲染到屏幕上
下面来具体看一看这些概念。

场景

场景(Scene)是所有物体的容器,也对应着我们创建的三维世界。

相机

相机(Camera)就相当于我们的眼睛,为了观察这个世界,我们需要描述某个物体的位置。描述物体位置需要用到坐标系。常用的坐标系有左手坐标系和右手坐标系。
常用的有两种相机,正投影相机(OrthographicCamera)和透视投影相机(PerspectiveCamera):
上面左图是正交投影,物体反射的光平行投射到屏幕上,其大小始终不变,所以远近的物体大小一样。在渲染一些 2D 效果和 UI 元素的时候会用到;右图是透视投影,符合我们平时看东西的感觉,近大远小,经常用在 3D 场景中。
要理解两者的不同,需要明白「视景体」这个概念。它是指成像景物所在空间的集合。简单点说,视景体是一个几何体,只有在视景体内的物体才会被我们看到,视景体之外的物体将被裁剪掉(所见即所得)。这是为了去除不必要的计算。通过变换视景体,我们就得到不同的相机。
正交投影相机
的视景体是一个长方体,它有几个属性:left, right, top, bottom, near, far 。把 Camera 看作一个点,left 则表示视景体左平面在左右方向上与 Camera 的距离,另外几个参数同理。于是六个参数分别定义了视景体六个面的位置。我们可以近似地认为,视景体里的物体平行投影到近平面上,然后近平面上的图像被渲染到屏幕上。

透视投影相机
的视景体是一个四棱台,它有几个属性:fov, aspect, near, far。fov(field of view)即视野,对应着下图图中的视角,是上下两面的夹角;aspect 是近平面的宽高比;再加上近平面距离 near,远平面距离 far,就可以唯一确定这个视景体了。

物体

物体(Objects)顾名思义,就是三维空间里的物体。有网格(Mesh), 线(Line), 点(Points)等。这里我们只看 Mesh
我们都知道,计算机的世界里,一条弧线是由有限个点构成的有限条线段连接得到的。线段很多时,看起来就是一条平滑的弧线了。计算机中的三维模型也是类似的,普遍的做法是用三角形组成的网格来描述,我们把这种模型称之为 Mesh 模型。
这是在 3D 图形处理中与图像处理领域的 Lena 图齐名的斯坦福兔子。随着三角形数量的增加,它的表面将会越来越平滑。
物体有两个基础的属性,一个是形状(Geometry),另一个是材质(Material)。通俗来说,Geometry 就好像是骨架,材质则类似于皮肤。
  1. 形状(Geometry) 在程序中是通过存储模型用到的点集和点间关系(哪些点构成一个三角形)来达到描述物体形状的目的。有立方体、平面、球体、圆形、圆柱、圆台等许多基本形状。也可以通过自己定义每个点的位置来构造形状;
  2. 材质(Material) 是物体表面除了形状以为所有可视属性的集合,例如色彩、纹理、光滑度、透明度、反射率、折射率、发光度等。

基于 Three.js 实现全景渲染

在 Web 上实现 3D 的渲染,通常使用到 WebGL 技术。WebGL 是一个JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D 和 2D 图形。
但 WebGL 门槛相对较高,需要相对较多的图形学和数学知识。而 Three.js 则对 WebGL 提供的接口进行了非常好的封装,掩盖了 3D 渲染的细节,大大降低了学习成本,并且几乎没有损失 WebGL 的灵活性。因此我们选择了 Three.js 来实现全景视频的渲染。

EAC 全景图片渲染

我们知道,视频是由一张张图像组成的动态画面。因此需要渲染视频,可以先从渲染一张全景照片开始。
前面讲到过,全景视频的投影方式有 ERP 和 EAC 两种,其中 EAC 的的渲染相对易于实现和理解。因此我们从 EAC 投影方式开始实现。EAC 投影的全景图片如下:
  1. 创建场景、相机和渲染器,将渲染器挂载到 DOM 上:
const
 scene = 
new
 THREE.Scene();


const
 _width = 
window
.innerWidth > 
640
 ? 
640
 : 
window
.innerWidth;

const
 _height = 
window
.innerHeight;

const
 camera = 
new
 THREE.PerspectiveCamera(
90
, _width / _height, 
0.1
100
); 
// PerspectiveCamera(fov, aspect, near, far);

const
 renderer = 
new
 THREE.WebGLRenderer();

renderer.setPixelRatio(
window
.devicePixelRatio); 
// 设置设备像素比,通常用于避免 HiDPI 设备上绘图模糊
renderer.setSize(_width, _height); 
// 将输出 canvas 的大小调整为 (width, height) 并考虑设备像素比

document
.getElementById(
'container'
).appendChild(renderer.domElement);

  1. 初始化一个立体几何,并上色:
const
 geometry = 
new
 THREE.BoxGeometry( 
1
1
1
 ); 
// BoxGeometry(width, height, depth)
const
 material = 
new
 THREE.MeshBasicMaterial({ 
color
0x156289
 });

const
 box = 
new
 THREE.Mesh(geometry, material);

scene.add( box ); 
// 将物体添加到场景
camera.position.z = 
5
// 设置相机的 z 轴位置为正,从外部观察物体

// 执行动画渲染
const
 animate = 
() =>
 {

  requestAnimationFrame(animate);

  renderer.render(scene, camera);

};

animate();

  1. 给立方体添加图片纹理:
- const material = new THREE.MeshBasicMaterial({ color: 0x156289 });
+ const texture = new THREE.TextureLoader().load('textures/demol.gif');
+ const material = new THREE.MeshBasicMaterial( { map: texture } );
  1. 将上面的纹理替换为 6 张全景图片,图片加载的顺序是 正X(px.jpg),负X(nx.jpg),正Y(py.jpg),负Y(ny.jpg),正Z(pz.jpg) 和 负Z(nz.jpg),将他们分别赋给 6 个材质的贴图,作为立方体 box 的材质。
const geometry = new THREE.BoxGeometry( 1, 1, 1 );

- const texture = new THREE.TextureLoader().load('textures/demol.gif');
+ const textures = [
+ 'https://img.alicdn.com/imgextra/i3/O1CN01LsO1Bk20QbKpFTUQr_!!6000000006844-0-tps-1500-1500.jpg',
+ 'https://img.alicdn.com/imgextra/i3/O1CN01uTWCLc1XOCOuA92H0_!!6000000002913-0-tps-1500-1500.jpg',
+ 'https://img.alicdn.com/imgextra/i4/O1CN016lU3YJ1JdrJuFTcWt_!!6000000001052-0-tps-1500-1500.jpg',
+ 'https://img.alicdn.com/imgextra/i2/O1CN01nYe2Mn1ohkmBVyKpp_!!6000000005257-0-tps-1500-1500.jpg',
+ 'https://img.alicdn.com/imgextra/i4/O1CN014TNffn1nlaTfA98Fg_!!6000000005130-0-tps-1500-1500.jpg',
+ 'https://img.alicdn.com/imgextra/i1/O1CN01sS5m781ya6JgLSaVk_!!6000000006594-0-tps-1500-1500.jpg',
+ ];
- const material = new THREE.MeshBasicMaterial( { map: texture } );
+ const materials = [];
+ for (let i = 0; i < textures.length; i ++ ) {
+  const textureLoader = new THREE.TextureLoader();
+   materials.push( new THREE.MeshBasicMaterial({ map: textureLoader.load(textures[i]) }));
+ }
const box = new THREE.Mesh(geometry, materials);

  1. 将相机放入 box 里面,同时将 box 的 X 轴或者 Z 轴的放大倍数变为负数,这样才能看到内部:
- camera.position.z = 5;
+ camera.position.z = 0.01; // 将相机放在里面
+ box.geometry.scale(1, 1, -1); // 相当于将 Z 轴正向的面移到 Z 轴负方向上
然后再添加并配置控制器,使得用户可以通过交互来进行观察:
+ const controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableZoom = false; // 禁用放大
+ controls.enablePan = false; // 禁用双指缩放
+ controls.enableDamping = true; // 开启阻尼效果
+ controls.rotateSpeed = -0.25; // 旋转方向取反,使内部拖拽旋转方向一致

const animate = () => {

  requestAnimationFrame(animate);

+  controls.update(); // 开启阻尼效果后必须更新
  renderer.render(scene, camera);

};

最终效果:

ERP 全景视频渲染

ERP 全景视频的渲染与 EAC 全景图片的渲染主要差别在于:
  • ERP 使用的是球形投影
  • 全景视频使用的是视频纹理贴图
  • 视频的渲染需要媒体流输出
根据这几点不同,只要将上面的代码稍作改造,就可以实现 ERP 全景视频的渲染。
我们要渲染的全景视频如下:
改造步骤:
  1. 使用球形替换立方体
- const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+ const geometry = new THREE.SphereGeometry(1, 32, 16);
  1. 使用视频纹理替换图片纹理
- const textures = [
- 'https://img.alicdn.com/imgextra/i3/O1CN01LsO1Bk20QbKpFTUQr_!!6000000006844-0-tps-1500-1500.jpg',
- 'https://img.alicdn.com/imgextra/i3/O1CN01uTWCLc1XOCOuA92H0_!!6000000002913-0-tps-1500-1500.jpg',
- 'https://img.alicdn.com/imgextra/i4/O1CN016lU3YJ1JdrJuFTcWt_!!6000000001052-0-tps-1500-1500.jpg',
- 'https://img.alicdn.com/imgextra/i2/O1CN01nYe2Mn1ohkmBVyKpp_!!6000000005257-0-tps-1500-1500.jpg',
- 'https://img.alicdn.com/imgextra/i4/O1CN014TNffn1nlaTfA98Fg_!!6000000005130-0-tps-1500-1500.jpg',
- 'https://img.alicdn.com/imgextra/i1/O1CN01sS5m781ya6JgLSaVk_!!6000000006594-0-tps-1500-1500.jpg',
- ];
+ const video = document.createElement('video');
+ video.muted = true;
+ video.src = 'https://example.com/panorama.mp4';
+ const texture = new THREE.VideoTexture(video);

- const materials = [];
- for (let i = 0; i < textures.length; i ++ ) {
-  const textureLoader = new THREE.TextureLoader();
-   materials.push( new THREE.MeshBasicMaterial({ map: textureLoader.load(textures[i]) }));
- }
+ const material = new THREE.MeshBasicMaterial( { map: texture } );
  1. 使用球形和视频纹理的材质初始化网格并添加入场景
- const box = new THREE.Mesh(geometry, materials);
+ const sphere = new THREE.Mesh(geometry, material);
- scene.add(box);
+ scene.add(sphere);
  1. 启动视频播放,Three.js 将捕获视频的媒体流并渲染
+ video.play();
最终效果:
基于前端技术的封面视频生成
接下来我们还需要实现「根据参数驱动视野变更」(模拟上面的点击拖动效果)和「将画面生成为新的视频」并上传到后台。

全景视频的控制

前面讲到,我们的封面视频,需要安装一定的规则来改变视野画面。该规则可以由小二在后台来配置,也允许商家进行调整。回顾一下我们的默认规则:视频画面定位到视频中间的位置,视频播放过程中,视角向左移动 20 度(2 秒),回到中间(2 秒),向右移动 20 度(2 秒),回到中间(2 秒)。总共生成时长为 8 秒的封面视频。

坐标系及视点

在前面的章节中,我们已经将全景视频的画面映射到了一个球体中,并且将相机(我们的眼镜,观察者)放入了这个球体内部中央位置。我们要完成「向左移动 20 度」的指定,但相机现在面向哪里,画面是怎样的?怎么度量「20 度」?
我们示例全景视频使用的是等距柱状投影方式,这种投影方式把球的经线映射成间距相等的垂直线,把球的纬线映射成间距相等的水平线,则可生成一幅横纵比为 2:1 的地图。
其中左侧中心点就是 0, 0 的坐标点:
进而回顾一下上面 3D 渲染基础中透视相机的知识 —— 我们只能看到视窗内的平面大小。这个视窗是由视点视野决定的:
结合上面的示例画面,可以确定初始视点坐标是 (90, 0)
因此我们可以提供一个方法去控制相机的视点和视野,就可以得出我们想要的画面。
locate(longitude, latitude, fov)
  • longitude:经度
  • latitude:维度
  • fov: 视野

坐标系的转换

Three.js 里面有没有这样的 API 呢?依据笔者所查,并没有。因为我们忽略了另一个概念:高度。在三维空间中,我们的相机定位「视点」,还需要 Z 轴信息。Three.js 中的 API 是 camera.lookAt(x, y, z),该 API 可以旋转相机使其在世界空间中面朝一个点。
把刚才的经纬度坐标映射为三维空间的某个点,需要应用到一些数学知识:经纬度坐标系与三维笛卡尔坐标系转换。
将经纬度坐标系转换为三维笛卡尔坐标系:
将三维笛卡尔坐标系转换为经纬度坐标系:
只要理解其数学公式和其推导过程,我们就能通过实现代码该转换算法。文章篇幅有限不再对数学部分展开,其转换算法的代码实现如下:
functionlocate(longitude = 0, latitude = 0, fov = 90
{

const
 modifiedLat = 
Math
.max(
-90
Math
.min(
90
, latitude));

const
 phi = THREE.MathUtils.degToRad(
90
 - modifiedLat);

const
 theta = THREE.MathUtils.degToRad(longitude + 
180
);


const
 distance = 
1
// 球的半径
const
 x = distance * 
Math
.sin(phi) * 
Math
.cos(theta);

const
 y = distance * 
Math
.cos(phi);

const
 z = distance * 
Math
.sin(phi) * 
Math
.sin(theta);

  camera.lookAt(x, y, z);

  camera.fov = fov;

}

向左移动 20 度:
locate(
0
0
90
); 
// 初始化视点和视野
setTimeout(
() =>
 locate(
-20
), 
2000
); 
// 两秒后视点向左移动 20 度
执行效果(观察两秒后视角的变化):

按规则执行动画

按照我们既定的规则「每 2 秒来回转动 20 度」,实现代码如下:
const
 duration = 
2000
;

setTimeout(
() =>
 locate(
-20
), duration);

setTimeout(
() =>
 locate(
0
), duration * 
2
);

setTimeout(
() =>
 locate(
20
), duration * 
3
);

setTimeout(
() =>
 locate(
0
), duration * 
4
);

执行效果:
可以看到移动的效果非常地生硬,不像是正常的人眼睛转动的感觉。因为我们的程序缺少了对执行过程的描述。可以引入动画来进行优化,让「 x 时间转动到 x 位置」的执行过程化:
import
 animejs 
from'animejs'
;


functionrunAnimate( update
{

const
 anim = {

x
0
,

  };

  animejs.timeline({

targets
: anim,

duration
2000
// 间隔 2 秒
easing
'linear'
// 线性执行
update
() =>
 update(anim),

  })

    .add({ 
x
-20
, })

    .add({ 
x
0
, })

    .add({ 
x
20
, })

    .add({ 
x
0
, });

}


// 视频播放即开始执行动画
video.addEventListener(
'play'
, () => {

  runAnimate(
({ x }) =>
 locate(x));

});

最终效果:
自此我们已经完成了全景视频控制 API 的提供,后面只需要规范一种数据格式来让程序生成上面的执行代码,即可完成根据配置驱动全景视频视野的目的。
在新零售工作台我们通过默认参数来生成封面视频,也提供了表单的方式允许商家重设这些参数:

媒体流的捕获和上传

最后将 Three.js 渲染用的 Canvas 通过 captureStream 捕获其媒体流(Stream),使用 MediaRecorder API 将其转换为 Blob 文件对象,即可用于上传。
捕获文件对象:
let
 recordedBlobs = [];

let
 mediaRecorder;

functionstartRecording() 
{

  mediaRecorder = 
new
 MediaRecorder(

    renderer.domElement.captureStream()

  );

  mediaRecorder.ondataavailable = 
function(event
{

if
 (event.data && event.data.size > 
0
) {

      recordedBlobs.push(event.data);

    }

  };

  mediaRecorder.start();

}

functionstopRecording() 
{

  mediaRecorder.stop();

}

上传文件对象:
import
 { createUploader } 
from'@ali/speedster-media-upload'
// 内容中台视频上传库

const
 file = 
new
 File(

new
 Blob(recordedBlobs),

'cover.webm'
,

);


const
 uploader = 
await
 createUploader();

const
 fileInfo = 
await
 uploader.startUpload(file);
线上效果
商家上传的全景视频原格式:
生成的封面视频:
未来展望
借助封面视频生成的场景,我们在 Web 端探索了全景视频这种新的视频形态的播放。随着视频制作成本的降低、生产和消费链路的完善,全景视频技术的发展将为家装行业的数字化升级提供新的推动力,加速其进程。在技术侧,现有的硬件条件下,仍需进一步升级客户端技术解决消费侧 App 的流量和性能的问题;升级前端技术解决生产侧审核平台的播放能力问题。例如:
  • 多种投影方式的支持:拍摄设备的多样性以及业界标准的不一导致产出的全景视频投影方式多种多样;在进行视频审核时播放器对主流投影方式的支持可以减少服务端转格式的计算成本,同时转码和审核的并行将能提高视频生产时效;
  • 音频播放的支持:封面视频生成的场景只需要渲染画面,但在审核场景下播放器需要对音频进行播放来让工作人员进行审核;
  • 交互能力的支持:封面视频生成的场景不需要人工进行交互,因此播放器并未提供控件。但在审核场景下需要有诸如进度条拖拽、全屏和倍速等交互能力。
参考资料
本文内容参考或引用了以下资料:
  • 常见的 360° 全景视频格式介绍及播放方式(https://www.bilibili.com/read/cv788511)
  • 谈谈全景视频投影方式(https://zhuanlan.zhihu.com/p/27296011)
  • 初识 Three.js(https://zhuanlan.zhihu.com/p/27296011)
  • 实现一个 360 全景的 N 种方案

关注「Alibaba F2E」微信公众号把握阿里巴巴前端新动向
继续阅读
阅读原文