标签 webgl 下的文章

0、介绍

regl 是一个简化 WebGL 编程的库,可以更轻松地编写 WebGL 程序。GL Transitions (github: gl-transitions) 是收集写好的 WebGL GLSL 程序的网站,用来实现转场效果。

有的视频剪辑软件,比如剪映,已经在浏览器中实现了视频转场特效的预览,其原理就是利用 WebGL 运行 GLSL 来实现的。GLSL 是一种运行在 GPU 中的语言,虽然不是很难,但想要在短时间内掌握也是不太现实的。

利用开头提到的两个工具,我们也可以实现一样的效果。目前 GL Transitions 网站上有六十多个转场效果,基本上能够满足常见需求。网站上还提供了在线编辑器,可以实现你自己想要的效果。

下面我们就一步一步实现视频转场。

1、转场流程

视频转场与图片转场不同。GL Transitions 网站上的视频转场例子,实际上也是把视频当做图片来转场了。在实际运用中,我们需要考虑到,视频需要完整播放完毕才能切换到下一个视频,不能像图片转场一样,每张图片都展示相同的时间。视频的转场效果需要运用在上一个视频播放即将结束、下一个视频即将开始播放的时候。

这张图大概描述了其流程:

1.png

假设转场效果持续时间为 duration,在上一个视频还差 duration 结束播放的时候,开始转场效果。

转场效果需要两个视频参与,以淡入淡出为例,上一个视频开始淡出的时候,下一个视频开始淡入。

转场的时间具体怎么安排,可以根据你的需要。比如,如果在第一个视频完全淡出之后,第二个视频才开始淡出,那么转场可能看起来是这样:第一个视频慢慢模糊,同时画面越来越黑,直到画面完全变黑;之后从黑到亮慢慢出现第二个视频的画面。这是因为视频变得越来越“透明”时,没有内容补充到黑色背景上去。如果这正是你想要的效果,那么就可以这样做——也就是根据你的需要。

2、准备视频资源

转场肯定是在多个视频之间进行的。使用 Promise.all 可以同时下载多个视频并确保全部下载完毕之后再使用。
首先准备一个容器,用来放 WebGL 生成的 canvas

<div id="canvas-box"></div>

之后需要一个创建 video 的函数:

const createVedio = src => {
    if (!src) return Promise.resolve(null);

    return new Promise((resolve, reject) => {
        try {
            let video = document.createElement('video');
            video.src = src;
            video.preload = true;
            video.autoplay = false;
            video.muted = true;
            video.controls = false;
            video.loop = false;
            video.crossOrigin = 'anonymous';
            video.oncanplaythrough = () => {
                resolve(video);
            };
        } catch (err) {
            reject(err);
        }
    });
};

在上面的函数中,如果视频资源是跨域的,即使服务器允许跨域,也需要为 video 设置 crossOrigin = 'anonymous',以允许 canvas 使用跨域资源。

需要注意的是,即使在上面的代码中设置了视频自动播放,在将视频作为纹理时,也可能只会显示黑屏,因为视频可能在等待其他资源下载时就已经播放完了。必须在用到视频时,调用视频的 play 方法。

假如我们有两个视频地址:

const url1 = 'https://xxx.xxx.xxx/xxx1.mp4';
const url2 = 'https://xxx.xxx.xxx/xxx2.mp4';

接下来就加载它们:

const videos = [];
Promise.all([createVedio(url1), createVedio(url2)]).then(r => {
  videos = r;
})

3、使用 regl 和 regl-transition 进行转场

首先需要导入相关库:

  import createREGL from 'regl';
  import GLTransitions from 'gl-transitions';
  import createREGLTransition from 'regl-transition';

如果没有安装以上三个库,请到 npm 官网搜索安装。接下来创建一个转场对象:

  const regl = createREGL({
    container: document.querySelector('#canvas-box')
  });
  //在 GLTransitions 库中搜索名称为 fade 的转场效果
  //这里固定使用这个效果,如果你想切换不同效果,请自行实现
  const fade = GLTransitions.find(t => t.name === 'fade');
  const transition = createREGLTransition(regl, fade);

transition 接受 3 个参数:progressfromto,分别表示当前的进度、前一个纹理,后一个纹理。其中 progress 的取值为 01,如果不需要应用转场效果,可以提供 0。

声明转场持续时长:

const transitionDuration = 1;

由于转场的开始和结束依赖视频的播放时间,所以,我们给 videos 进行如下处理:

  const slides = videos.map((video, index) => {
    return { video };
  });
  slides.forEach((s, i) => {
    const duration = i === 0 ? s.video.duration : s.video.duration - transitionDuration;
      if (i === 0) {
        s.timeline = duration;
      } else {
        s.timeline = duration + slides[i - 1].timeline;
      }
  });

上面的代码中,使用 timeline 保存了了每个视频结束的时间点。
从下图中可以看出,由于播放时间重合,因此除了第一个视频之外,后面的每个视频都会提前 transitionDuration 开始播放,而且提前量是累加的,比如第二个视频,比原来提前 1 秒播放,第三个视频比原来提前 2 秒播放,以此类推。

2.jpg

之后就是使用 regl.frame 方法播放视频,并在特定的时间进行转场。

regl.frame 方法的回调函数提供了一个对象,其中 time 表示从开始运行 regl.frame 到当前帧一共过去了多长时间。

const len = slides.length;
const endTimeline = slides[len - 1].timeline;

//初始化纹理
//纹理必须重用,不能在下面的 frame 中重复声明,否则会造成内存泄漏,WegGL 崩溃
let fromTexture = regl.texture();
let toTexture = regl.texture();

let progress = 0;

tick = regl.frame(({ time }) => {
  // 播放到最后时停止
  if (time >= endTimeline) {
    return tick.cancel();
  }

  // 当前播放视频的索引
  const index = slides.findIndex(ele => ele.timeline >= time);

  const curr = slides[index];
  //确保当前视频正在播放
  curr.video.paussed && curr.video.play();

  //第一个视频,还没播放到开始转场的时间
  //此时不需要 transition 的 to 参数,下同
  if (index === 0 && time < curr.timeline - transitionDuration) {
    progress = 0;
    //重新初始化纹理,用来捕捉当前视频画面,下同
    fromTexture(curr.video);
  } 
  //当 index 到最后一个视频时,最后一个视频已经参与了转场,后面都不会再转场了
  else if (index === len - 1) {
    progress = 0;
    fromTexture(curr.video);
  } else {
    //这里是正在转场的阶段
    if (time >= curr.timeline - transitionDuration && time <= curr.timeline) {
      //progress = (当前时间 - 转场开始时间) / 转场持续时间
      progress = (time - (curr.timeline - transitionDuration)) / transitionDuration;

      播放下一个视频
      const next = index === len - 1 ? curr : slides[index + 1];
      next.video.paused && next.video.play();

      fromTexture(curr.video);
      toTexture(next.video);
    }
    //以第二个视频为例,这里代表视频处于上一个转场完毕、下一个转场未开始的播放阶段
    else {
      progress = 0;
      fromTexture(curr.video);
    }
  }

  transition({ progress, from: fromTexture, to: toTexture });
});

4、总结

本文的难点在于确定转场的开始和结束时间。另外,还需要注意视频的播放状态,视频只有处于播放状态,才能够获取到它的内容作为纹理。另外还需要注意纹理的复用,一定不要在 regl.frame 回调中重复声明纹理,否则会造成严重的性能问题,甚至电脑黑屏。


End