🔥实现一个抖音汽车定制礼物特效

发布于:2024-05-03 ⋅ 阅读:(21) ⋅ 点赞:(0)

🔔概述

动效作为产品与用户沟通的广泛形式,经历了从静态到动态,从不透明到透明,从单一到多元的进化。本文依托于业务需求,充分对比了常用的 Web 动效格式及其区别,调研优秀的特效播放方案,参考其原理经过改造并完成升级功能。

背景

礼物特效是直播间及语聊房中很常见的增值玩法,用户对玩法的多样性需求也在不断地增加。近期业务上期望能够参考抖音实现定制跑车特效。用户通过个性化的选配,最终送出形式各样的礼物,从而满足用户独一无二及炫耀的心理。

抖音效果:

录屏2024-01-19+17.19.12.gif

方案调研

看到这个效果的第一反应是通过游戏引擎实现。

方案一:制作3D模型,更换每个部件,类似换装。但有几个问题:

  1. 3D模型制作成本较高,周期长;
  2. 技术方案不可复用,下次仍需研发周期及成本;
  3. 业务属性决定礼物特效多平面的,非单一主体不适用游戏研发,视频播放最合适;

方案二:枚举合成组合好的视频,提前生成。同时这样也是有问题的:

  1. 存储成本和服务端压力,部件越多需提前合成的视频数量越多,排列组合;
  2. 需要运营配置和检查的成本较高,每一个视频都需要运营确认无误;
  3. 定好了就不能再加部件分类,不然所有的视频都需要重新合成,无法扩展;

由此,需要我们沉下心,从头开始寻找解决方案:

1 常用动效格式

​ Web 动效有很多种,不同文件格式之间各有利弊,从业务需求角度出发选取相对较优方案是我们的目的。常见的动效格式有 gif、apng、webp、lottie、svga、mp4 等[3]。

1.1 Gif

​ Gif(Graphics Interchange Format)是在1987年由 Compu Serve 为了填补跨平台图像格式的空白发展起来的位图动画格式,具有 GIF87aGIF89a 两个版本。

优点:

  • 支持平台多,兼容性性好
  • GIF89a 版本支持透明

缺点:

  • 仅支持 8 位 256 种颜色,对于更高的色彩范围会出现颜色失真、白边锯齿等情况
  • 不支持半透明
  • 体积较大,播放资源占用高
  • 只能循环播放,无法精准控制动画(可以通过 等第三方库进行播放和暂停)。

使用:

<img src="http:xxx.gif" />
img

1.2 Apng

​ APNG(Animated Portable Network Graphics)是一个基于PNG(Portable Network Graphics)的位图动画格式,2004 年由 Mozlilla 开发。Apng 也称为“动态 png”,后缀名依旧是 .png。

优点:

  • 支持平台多,兼容性性好
  • 保留对传统 PNG 向下兼容(不支持 APNG 的浏览器会以普通 PNG 静止显示第一帧)
  • 支持 24 位真彩色图片 8 位 Alpha 透明通道,支持半透明,色彩范围广,还原度高

缺点:

  • 只能设定播放次数或循环播放(可以通过 等第三方库转换为 canvas 播放和暂停动画)
  • 对于复杂动效,体积大且加载时间长

使用:

<img src="xxx.png" />
img

1.3 Webp

​ WebP(Web Picture),是一种由Google 于2010年推出的新一代图像格式。它的主要目标是提供更好的压缩效果,同时保持或提高图像质量。这种格式的设计是为了在网页加载时提供更快的加载速度和响应时间,从而提高用户体验。

优点:

  • 支持有损压缩和无损压缩两种形式
  • 压缩算法先进,同等质量下体积更小
  • 支持透明度

缺点:

  • iOS 设备兼容性差
  • 只能循环播放(可以通过 等第三方库转换为 canvas 播放和暂停动画)

使用:

<img src="xxx.webp" />
动图

1.4 Lottie

​ 是 Airbnb 于2017年开源的一套跨平台的完整的动画效果解决方案,设计师可以使用 设计出漂亮的动画之后,使用 Lottie 提供的 插件将设计好的动画导出成 JSON 格式,就可以直接运用在 iOSAndroidWebReact Native之上,无需其他额外操作。

优点:

  • JSON 文件可进行二次编辑
  • 可操控度高,可以设置是否自动播放、循环播放、播放速度等,可以监听播放事件
  • 可以无失真放大或缩小
  • 体积小
image-20231210215501413

缺点:

  • 对缓动曲线的解析占用内存高
  • 复杂粒子效果支持度差

使用:

使用 进行 lottie 动效的播放。

npm install lottie-web
import lottie from 'lottie-web'

lottie.loadAnimation({
  container: xxxElement,
  renderer: 'svg',
  loop: true,
  autoplay: true,
  path: 'xxx.json'
});

lottie-1702217124488

1.5 Svga

​ 是国内 YY 公司于 2016年开发的一款跨平台的开源动画格式,同时兼容 iOS / Android / Flutter / Web。动效设计及使用方式与 Lottie 非常相似。相对于Lottie 对缓动曲线解析差带来的性能问题和稳定性问题,Svga 表现会好很多。

优点:

  • 库体积及动画文件体积小
  • 支持序列帧动效

缺点:

  • 部分特效支持度不如 Lottie

  • 解码时间长,长时间播放内存占用高

使用:

使用 进行 Svga 动效的播放。

npm i svga
import { Parser, Player } from 'svga'

const parser = new Parser()
const svga = await parser.load('xxx.svga')

const player = new Player(document.getElementById('canvas'))
await player.mount(svga)

player.start()
svga-swuwu2wq1

1.6 MP4

​ MP4(MPEG-4 Part 14)是一种流行的多媒体文件格式,可以容纳音频、视频、文本和图像等多种媒体数据。其支持高质量的视频编码,包括 H.264(AVC)H.265(HEVC) 等,这些编码可以提供清晰的视频画面。

优点:

  • 兼容性高
  • 完美还原设计
  • 压缩率高,体积小
  • 支持边下边播,无需等待整个文件下载完成
  • 支持音频
  • 输出简单

image-20231211121055277

缺点:

  • 不支持透明度,大面积动效遮挡用户界面,视觉效果差

使用:

<video src='xxx.mp4' />
2023-12-11 01.28.27

2 视频融合

​ 由上述分析可以看出,MP4 格式具有设计输出简单,文件完美还原设计师 AE 动画效果,体积小等优点。而还原效果这一点,也是设计师和 Web 开发者共同的追求,好的效果不仅可以丰富用户的在线体验,也可以提高用户留存度,激发产品内内容创作者的创作热情,对用户和产品均产生积极的影响。

​ 但 MP4 有个致命的缺点:天生不支持透明度。那么,有没有什么方法可以使其带透明度播放吗?答案是肯定的。

​ 我们知道,可以将 MP4 看做是数帧图片快速连续播放而成的视频,而图片中的每一个像素,均为 RGBA 形式(如 (255, 255, 255, 255)),RGB 为红绿蓝三基色, A 就是透明通道 Alpha,它是一个8位灰度通道,由256级灰度来记录图像中的透明信息。

​ 原生的 MP4 相当于 A 通道所有值都是 255,均为不透明。而设计师可以通过某种方式告诉我们每一帧每一像素的透明度,Web 开发者对其颜色进行透明度替换,再呈现给用户,便解决了 MP4 不支持透明度的问题。设计如何将每一帧每一像素告诉我们,最简单且高效的方式,便是利用 AE 导出同步的 Alpha 视频,而为了能够在 Web 端同步播放,设计师可以将两块视频进行拼接,合 2 为1。这样,一个视频中便包含了完整的 RGB 和 Alpha 信息。

image-20231210233303257 image-20231210233325212 image-20231210233219072

​ Web 开发者拿到 MP4 文件播放时,从一侧提取像素点 RGB 信息,从另一侧提取 R通道(G/B 通道亦可)信息作为对应像素点的 Alpha 通道值,融合为新的 RGBA 值,根据 RGBA 值将每个像素点渲染到屏幕上,实现半透明效果。

img

​ 那么,Web 开发者如何获取视频每帧并对每帧进行解析及使用新的 RGBA 值进行屏幕重新渲染呢?

3 AlphaVideoPlayerJs

YY的: 轻量级 高性能 跨平台 MP4 动效播放器;

企鹅电竞的 : 企鹅电竞开发,用于播放酷炫动画的实现方案,可在动画中融入自定义属性(比如用户名称,头像);

anim2.gif

能够融入自定义属性核心原理都是 引入“遮罩”素材,利用遮罩与属性图片进行Porter-Duff操作,就能得到需要的形状,再将结果贴到视频对应坐标位置,就能实现最后的融合效果。

image.png

既然可以融合固定的内容,那么动态的内容(拆成多个视频部件)也是可以实现的,只需同时解码多个视频,一个基座视频,多个部件视频,每一帧绘制时取部件视频的部分贴到对应坐标位置即可。 参考以上两个优秀案例的实现原理,先完成AlphaVideoPlayerJs的基础功能,再此基础上完成多视频的叠加融合。

3.1 原理

代码主要由 video 与 render 两部分组成。

image-20231211112559143

3.1.1 video

private initAdaptAnimation () {
    const { videoFrame, fps } = this.config

    if ('requestVideoFrameCallback' in HTMLVideoElement.prototype && videoFrame) {
      return (cb: Function) => this.video.requestVideoFrameCallback(cb as VideoFrameRequestCallback)
    } else if (requestAnimationFrame) {
      let frame = -1
      return (cb: Function) => {
        frame++
        return requestAnimationFrame(() => {
          if (frame % (60 / fps) === 0) {
            return cb()
          } else {
            this.animationId = this.adaptAnimation(cb)
          }
        })
      }
    } else {
      return (cb: Function) => window.setTimeout(cb, 1000 / fps)
    }
  }

​ 获取到视频后,监听视频的每一帧,并调用 render 中相关方法进行帧解析。

3.1.2 canvas2d

    // 获取当前帧视频并进行缩放
    tempContext2d.drawImage(video, 0, 0, videoWidth, videoHeight, 0, 0, tempCanvasWidth, tempCanvasHeight)
    // 获取 RGB 通道视频图片数据
    const image = tempContext2d.getImageData(...imageCoords)
    const imageData = image.data
    // 获取 alpha 视频图片数据
    const alphaData = tempContext2d.getImageData(...alhpaImageCoords).data
    // 替换 alpha 通道
    for (let i = 3; i < imageData.length; i += 4) {
      imageData[i] = alphaData[i - 1]
    }
    // 将处理后的图片数据放回画布
    context2d.putImageData(image, 0, 0)

​ 一开始,AlphaVideoPlayerJs 使用 Canvas2D 对每帧图片进行遍历解析。然而,有上述代码可以看出,这种密集遍历像素点替换的方式,CPU计算频率极高,而浏览器的表现也印证了这一点。针对长时间运行的 Alpha MP4,CPU 暴涨甚至页面崩溃,完全无法上线使用。

image-20231211002804935 image-2023121100354193800

3.1.3 webgl

// vertex shader
precision lowp float;

uniform sampler2D u_Sampler;
varying vec2 v_Video_Texture_Position;
varying vec2 v_Alpha_Video_Texture_Position;

void main () {
  gl_FragColor = vec4(texture2D(u_Sampler, v_Video_Texture_Position).rgb, texture2D(u_Sampler, v_Alpha_Video_Texture_Position).r);
}
// fragment shader
attribute vec4 a_Position;
attribute vec2 a_Video_Texture_Position;
attribute vec2 a_Alpha_Video_Texture_Position;
varying vec2 v_Video_Texture_Position;
varying vec2 v_Alpha_Video_Texture_Position;

void main () {
  gl_Position = a_Position;
  v_Video_Texture_Position = a_Video_Texture_Position;
  v_Alpha_Video_Texture_Position = a_Alpha_Video_Texture_Position;
}

​ 既然使用 Canvas2D 占用 CPU 过高,那么可以试试性能更好的 WebGL。WebGL(Web Graphics Library)基于OpenGL,允许浏览器访问使用计算机上的 GPU 进行图形化渲染,为 canvas 绘制提供硬件 3D 加速。经过测试,动效播放时 CPU 使用率显著降低。

image-20231211003236385

3.2 优化

  • 高清渲染:使用 devicePixelRatio 恢复 canvas 尺寸,消除像素变粗及抗锯齿模糊等情况,保证渲染效果如原视频高清光滑。

  • 帧监听降级:使用 / / setTimeout 三级兼容性降级监听视频帧。

  • 渲染降级:canvas2d-render 与webgl-render 双渲染器,优先采用 webgl-render,兼容降级采用 canvas2d-render。

  • :支持横向视频-RGB 通道在前、横向视频-RGB 通道在后、纵向视频-RGB 通道在前、纵向视频-RGB 通道在后四类 Alpha MP4。

  • 丰富事件与场景支持:支持所有原生 video 事件,并可通过配置选择是否渲染第一帧及进行销毁释放内存等操作。

    • 对于直播间礼物打赏类场景,用户初始时是不需要看到礼物的,此时可设置初始时不渲染且播放结束后情况渲染,防止视频最后一帧残存页面。
    • 对于产品展示类(Alpha MP4 使用场景并不仅限于动效),属于常驻页面场景 。可设置用户初始化完成及渲染第一帧,且播放结束后无需自动销毁。

3.3 效果

遵循最小化配置、开箱即用的原则,默认只需配置渲染DOM和视频地址即可使用。

2023-12-11 01.33.06

多视频融合

多纹理渲染原理: 根据要渲染的video的个数,动态模板生成 片元着色器代码 一个video 对应一个 uniform, 再将 多个uniform 取到的颜色依次叠加混合,赋值给 gl_FragColor

uniform sampler2D u_sampler0;
uniform sampler2D u_sampler1;
uniform sampler2D u_sampler2;
void main(void) {
            // u_sampler0 取样器对0号纹理进行采样
            // 识别每一帧需要透明的区域,并设置为透明
            vec4 color0 = vec4(texture2D(u_sampler0, v_texCoord).rgb, texture2D(u_sampler0, v_texCoord+vec2(0.5, 0)).r);
            // u_sampler1 取样器对1号纹理进行采样
            vec4 color1 = vec4(texture2D(u_sampler1, v_texCoord).rgb, texture2D(u_sampler1, v_texCoord+vec2(0.5, 0)).r);
            // u_sampler2 取样器对2号纹理进行采样
            vec4 color2 = vec4(texture2D(u_sampler2, v_texCoord).rgb, texture2D(u_sampler2, v_texCoord+vec2(0.5, 0)).r);

            float blendFactor = color1.a;
  // 使用 mix函数 混合两个颜色得到新的颜色。3个参数分别是颜色1,颜色2,以及混合比例, 依次混合多个纹理, 得到最终要渲染的颜色。
  
            // 使用当前纹理的上一层问题 alpha值作为混合比例
            vec4 finalColor = mix(color0, color1, blendFactor);

            float blendFactor1 = color2.a;

            vec4 finalColor1 = mix(finalColor, color2, blendFactor1);


            gl_FragColor = finalColor1;
        }

最终效果:

录屏.gif

单部件视频:

oCLzQ6SQ.gif

性能简述

4个单独的mp4 播放器播放 CPU占用大约是 11% - 13 %

一个多纹理 视频播放器 播放4个mp4 视频 ,CPU占用 大约是 7%-9%,性能提升大约是 30%

总结

​ 虽然 Alpha MP4 相较于其他动效格式有很大的优势,但并不建议盲目无差别使用。由实现代码可以看出,不论是 Canvas2D 还是 WebGL 方式,都采用了逐帧重绘的形式。如果长时间或无间断重复性播放,必将长期占用 CPU 资源,带来高电量高、页面卡顿等一系列性能问题。动效实现方式有很多种,没有哪一种是绝对完美,分析各类动效的优缺点,选择适合自己业务需求的,才是最佳实践。

你有更好的想法么?欢迎评论区留言讨论。