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

标签: webgl

已有 6 条评论

  1. 2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
    新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
    新车首发,新的一年,只带想赚米的人coinsrore.com
    新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
    做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
    新车上路,只带前10个人coinsrore.com
    新盘首开 新盘首开 征召客户!!!coinsrore.com
    新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
    新车即将上线 真正的项目,期待你的参与coinsrore.com
    新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
    新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

  2. 新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

  3. 大佬 我想问下 可以对转场的视频设置背景颜色、背景图、高斯模糊背景这些功能么

  4. 大佬,如果我设置一个容器来装这个canvas,360*640的一个容器,然后如果视频是横屏的视频,那么就会出现视频显示不完全的问题,我希望它这个横屏视频能够像竖屏的视频一样播放,但是他生成的这个canvas好像没法处理这个啊

  5. 使用你这套逻辑后,发现2个问题 :
    1 time 值一会大一会小,不是持续单调递增
    2 视频渲染卡顿

    1. time值正常应该是随着时间增加的,如果你发现time值变小了,看看是不是你的代码重新运行了regl.frame或者调用了其他影响渲染的方法?渲染卡顿可能是你在每一帧之间的计算量太大了,我的实际体验还是挺流畅的。如果你发现了具体的问题点,欢迎指正~

添加新评论