[WebGL]使用regl对视频使用gl-transitions中的转场效果
0、介绍
regl 是一个简化 WebGL 编程的库,可以更轻松地编写 WebGL 程序。GL Transitions (github: gl-transitions) 是收集写好的 WebGL GLSL 程序的网站,用来实现转场效果。
有的视频剪辑软件,比如剪映,已经在浏览器中实现了视频转场特效的预览,其原理就是利用 WebGL 运行 GLSL 来实现的。GLSL 是一种运行在 GPU 中的语言,虽然不是很难,但想要在短时间内掌握也是不太现实的。
利用开头提到的两个工具,我们也可以实现一样的效果。目前 GL Transitions 网站上有六十多个转场效果,基本上能够满足常见需求。网站上还提供了在线编辑器,可以实现你自己想要的效果。
下面我们就一步一步实现视频转场。
1、转场流程
视频转场与图片转场不同。GL Transitions 网站上的视频转场例子,实际上也是把视频当做图片来转场了。在实际运用中,我们需要考虑到,视频需要完整播放完毕才能切换到下一个视频,不能像图片转场一样,每张图片都展示相同的时间。视频的转场效果需要运用在上一个视频播放即将结束、下一个视频即将开始播放的时候。
这张图大概描述了其流程:
假设转场效果持续时间为 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 个参数:progress
、from
、to
,分别表示当前的进度、前一个纹理,后一个纹理。其中 progress
的取值为 0
到 1
,如果不需要应用转场效果,可以提供 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 秒播放,以此类推。
之后就是使用 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
使用你这套逻辑后,发现2个问题 :
1 time 值一会大一会小,不是持续单调递增
2 视频渲染卡顿