7、视频滤镜与特效:LUT滤镜、分屏特效、时间特效(慢动作/快进)、转场动画
各位同学,欢迎来到第七章。这一章我们聊点好玩的——滤镜和特效。
说实话,做视频处理最让人兴奋的就是这部分。你想想看,一段平平无奇的视频素材,加上合适的滤镜、配上酷炫的转场,瞬间就有了电影感。我在做短视频SDK的时候,经常被产品经理追着要「那个抖音同款滤镜」,嗯,其实就是LUT。
7.1 LUT滤镜:让色彩一键变高级
LUT的全称是Look-Up Table,查色表。说白了,就是把输入的颜色值映射到另一个颜色值。你给一个像素的RGB,LUT告诉你输出什么RGB。
我习惯用3D LUT,也就是64x64x64的立方体。为什么是64?因为精度够用,文件又不会太大。每个维度64个采样点,总共262144个颜色映射关系。够精细了。
核心原理:LUT本质上是一个三维数组。输入(R,G,B),查表得到输出(R',G',B')。如果输入值不在采样点上,就用三线性插值。
来看一个加载LUT的代码片段。这是我在项目中实际用过的方案:
// 加载.cube格式的LUT文件
public class LUTLoader {
public static float[][][] loadCubeLUT(String path) {
// 解析.cube文件头,获取size
// 读取RGB映射数据
// 构建3D LUT数组
float[][][] lut = new float[size][size][size * 3];
// 注意:cube文件的数据排列是BGR顺序
// 我曾经在这里踩过坑,颜色完全不对
return lut;
}
}
我的经验:LUT文件格式有很多种,.cube、.3dl、.png。我个人最推荐.cube,因为它纯文本,容易调试。有一次客户给的LUT颜色偏紫,我打开.cube文件一看,发现蓝色通道的映射曲线有问题。
在OpenGL ES里应用LUT,核心是片段着色器。你需要把纹理坐标映射到LUT的3D纹理坐标上:
// 片段着色器核心代码
uniform sampler2D inputTexture;
uniform sampler3D lutTexture;
void main() {
vec4 color = texture2D(inputTexture, vTexCoord);
// 把颜色映射到LUT空间
vec3 lutCoord = color.rgb * (63.0/64.0) + (0.5/64.0);
vec4 lutColor = texture3D(lutTexture, lutCoord);
gl_FragColor = lutColor;
}
为什么是63/64和0.5/64?因为LUT的采样点范围是0到63,我们需要把[0,1]的颜色值映射到采样点的中心位置。这个细节,很多教程都不讲。
7.2 分屏特效:一个画面,多种视角
分屏特效在短视频里很常见,比如左右对比、上下对比、九宫格。实现思路其实很简单——把输出画面分成几个区域,每个区域独立渲染。
我做过一个双屏对比功能,左边是原图,右边是滤镜效果。用户滑动中间的分割线,可以实时调整左右比例。嗯,这里要注意滑动时的平滑过渡。
分屏的核心是修改顶点坐标和纹理坐标的映射关系。比如左右分屏:
// 左半屏:纹理坐标不变,顶点坐标x范围[-1, 0]
// 右半屏:纹理坐标不变,顶点坐标x范围[0, 1]
// 更灵活的做法:在片段着色器里判断
void main() {
if (vTexCoord.x < 0.5) {
// 左半屏:显示原图
gl_FragColor = texture2D(originalTexture, vTexCoord * 2.0);
} else {
// 右半屏:显示滤镜效果
vec2 coord = vec2((vTexCoord.x - 0.5) * 2.0, vTexCoord.y);
gl_FragColor = applyLUT(texture2D(originalTexture, coord));
}
}
注意:分屏时纹理坐标要重新计算。如果你直接使用原始坐标,画面会被拉伸。我曾经犯过这个错,结果左右两边的画面都变形了。
分屏特效的变种很多:
- 画中画:小窗口叠加在大画面上,可以拖动位置
- 镜像分屏:左右对称,或者上下对称
- 不规则分屏:圆形、心形裁剪区域
7.3 时间特效:让时间变慢或变快
时间特效,说白了就是改变视频的播放速度。慢动作是放慢,快进是加快。但实现起来,远不止「跳帧」这么简单。
我刚开始做慢动作时,直接跳帧播放,结果画面卡顿得不行。后来才明白,慢动作需要做帧插值。
7.3.1 慢动作(慢放)
慢动作的核心是:在原始帧之间插入新的帧。比如原始帧率30fps,你想放慢到10fps,就需要在每两帧之间插入2个新帧。
最简单的插值方法是帧重复:直接复制前一帧。但这样画面会跳,不流畅。更好的方法是线性插值:
// 假设原始帧A和B,时间点t在[0,1]之间
// 插值帧 = A * (1 - t) + B * t
// 对于像素级别的插值,需要在GPU上做
// 更高级的做法:光流法插值
// 计算A到B的运动向量,然后沿着运动方向插值
// 效果最好,但计算量大
我的建议:移动端做慢动作,用线性插值就够了。光流法虽然效果好,但手机发热严重。我曾经在某个项目里试过,手机烫得能煎鸡蛋。
7.3.2 快进(快放)
快进相对简单,就是跳帧播放。比如你想快进2倍,就每隔一帧取一帧。但要注意音频同步问题。
音频也要跟着变速。Android里可以用MediaCodec的变速参数,或者用SoundPool的速率控制。我习惯的做法是:视频跳帧,音频用重采样。
// 快进2倍:视频帧索引跳2
int targetFrameIndex = (int)(currentTime * frameRate * speed);
// speed = 2.0 表示2倍速
// 音频重采样:用AudioTrack的setPlaybackRate
// 或者用MediaCodec的变速参数
7.4 转场动画:让切换更丝滑
转场动画是视频剪辑的「调味料」。两个镜头之间,直接切换太生硬,加个转场就自然多了。
常见的转场类型:
- 淡入淡出:前一个画面逐渐消失,后一个逐渐出现
- 滑动:前一个画面滑出,后一个滑入
- 缩放:前一个缩小消失,后一个放大出现
- 擦除:像擦黑板一样,从左到右擦除前一个画面
转场的核心是混合两个画面。在片段着色器里,根据时间t(0到1),计算两个画面的混合比例:
// 淡入淡出转场
uniform sampler2D frameA;
uniform sampler2D frameB;
uniform float progress; // 0.0 到 1.0
void main() {
vec4 colorA = texture2D(frameA, vTexCoord);
vec4 colorB = texture2D(frameB, vTexCoord);
gl_FragColor = mix(colorA, colorB, progress);
// progress=0时全显示A,progress=1时全显示B
}
滑动转场稍微复杂一点,需要根据progress修改纹理坐标:
// 从左到右滑动
void main() {
vec2 coordA = vTexCoord;
vec2 coordB = vTexCoord;
// A向左滑出
coordA.x = vTexCoord.x + progress;
// B从右侧滑入
coordB.x = vTexCoord.x - (1.0 - progress);
vec4 colorA = texture2D(frameA, coordA);
vec4 colorB = texture2D(frameB, coordB);
// 根据坐标是否在[0,1]范围内决定显示哪个
if (coordA.x < 0.0 || coordA.x > 1.0) {
gl_FragColor = colorB;
} else {
gl_FragColor = colorA;
}
}
避坑指南:转场动画的progress要平滑变化。我曾经用线性变化,结果转场看起来很机械。后来改用缓动函数(ease-in-out),效果就好多了。推荐用三次贝塞尔曲线控制progress的变化速度。
转场动画的变种非常多,你可以组合使用:
- 淡入 + 缩放
- 滑动 + 模糊
- 擦除 + 颜色渐变
嗯,这一章的内容就到这里。滤镜、分屏、时间特效、转场动画,这四个技能组合起来,你已经能做出很酷的视频效果了。记住,实践出真知,多写代码多调试,你会发现这些特效其实没那么神秘。