动态化视频解决方案

| 导语 在特定场景下,我们需要在视频中嵌入动态的信息,比如用户头像,用户昵称等,这些动态信息又会跟随视频内容来改变位置和大小,有什么好的方案可以将视频内容与动态信息相结合呢?

在之前的文章《精准光影动效解决之道-透明视频》里面有提到,可以通过透明视频的方案来实现复杂的动画效果。之前的方案里面,视频都是固定内容的,没法跟用户信息结合在一起。

在直播送礼场景下,如果每个用户送的礼物都是一样的,会显得比较乏味,并且用户和主播之间的互动感会弱很多。如果礼物里面能包含用户和主播之间的互动信息和动画,给用户的代入感会更强。比如下面的这个礼物,希望可以把用户头像和主播头像都放到对应位置,并且跟随视频来动。

1529030299_69_w1100_h1440

标题提到的动态化视频,也就是跟上面提到的类似,固定的视频内容结合动态信息生成的动态化视频。

0x1 思路

一般有两种方案,第一:头像的动画是预设好的动画,比如从右到左,然后视频在对应的位置留出头像的空间。第二:头像的位置和大小由视频内容决定,每一帧视频都标识出头像的位置和大小。

第一种方案会有两个问题,1、很多动画并不是规则的动画,比如前面提到的案例。2、头像动画和视频动画是分离的,很容易造成头像动画和视频动画不统一的情况。

所以第二种方案相比会更适合,结合gl来渲染视频,并同时把头像贴到对应的位置。整体思路大概是这样的:

1529053085_96_w988_h1158

整个思路上,最关键的有两个,1、如何从视频中提取头像信息;2、如果通过webGL将头像和视频结合在一起。

0x2 头像信息提取

前面提到,头像的信息应该是跟随视频内容变化的,所以应该是跟视频关联,而且是由设计师编辑后提供。这里的方案是设计师在制作视频的时候,用特定颜色标识出头像位置,为了避免视频其他内容的影响,可以参照透明视频的方案,将头像信息与视频内容分离,然后合并在同个视频上,如下图:

1529062936_66_w1508_h870

把头像信息与视频内容放到同个视频里面最大的好处是可以保证同步,但是也带来一些问题,比如资源体积增大了,但最重要的问题是,如果在客户端渲染的过程中去识别头像区域,因为是像素扫描,而且没法用webgl来识别(目前没有找到合适的方法),所以客户端开销会很大,很容易造成掉帧,最终还是在视频制作的时候提前先把每一帧的头像信息先识别出来,然后生成json数据给到客户端直接使用,这种方法会略微有一些不同步的情况,但综合性能来看,效果还可以接受。

接下来就是如何通过颜色识别来识别出头像的位置和大小,也就是颜色的轮廓识别,有一些现有的库可以使用,这里选用的是 trackingjs(只支持web端,需手动适配node端),其中的ColorTracker功能正好可以满足要求:

1529065568_2_w1372_h988

使用trackingjs以后,问题就简单了,核心代码如下:

// 识别红色区域
tracking.ColorTracker.registerColor('head1'function(r, g, b{
    if (r > 240 && g < 10 && b < 10) {
    return true;
    }
    return false;
});
// 识别绿色区域
tracking.ColorTracker.registerColor('head2'function(r, g, b{
    if (r < 10 && g > 240 && b < 10) {
        return true;
    }
    return false;
});
let tracker = new tracking.ColorTracker(['head1''head2']);
tracker.on('track',(event)=> {
    event.data.forEach(function(rect
        if(rect.color=='head1'){
            // 红色区域位置和大小
        }
        if(rect.color=='head2'){
            // 绿色区域位置和大小
        }
    });
});

最后将取到的每一帧的头像信息进行组合生成JSON数据,这些数据可以直接在webgl里面使用,这就省去了客户端分析图片的过程。数据格式如下:

[
    //第1帧数据
    '1':{
        'head1':[
            0.2//x轴起始
            0.5//x轴终止
            0.2//y轴起始
            0.5//y轴终止
            -1   //终止标识
        ],
        'head2':[
            // ...
        ]
    }
    // ...
]

0x3 webGL多纹理渲染

有了上面的头像信息和视频信息以后,就可以通过webGL来绘制了,js代码的流程基本就是webGL的标准流程,不过需要注意的是,必须等纹理都加载完成以后才能进行贴图和上屏,不然纹理会加载失败,具体流程如下:

1529069975_25_w816_h1934

由于是视频作为纹理贴图,所以这里需要循环取视频内容,然后再渲染,这就要求每个循环里面尽可能的少做一些事情,把纹理初始化和shader初始化都提取到循环外面,因为这些都只需要初始化一次就可以了,并且头像纹理其实是固定不变的,可以在循环前先贴好图,而循环体内只需要给着色器赋予当前帧视频贴图和当前帧头像信息就可以了。

上面只是把纹理和头像信息传给了着色器,接下来就是着色器的代码逻辑了,着色器的多纹理渲染跟单纹理有一些差别,其中顶点着色器不需要特别处理,重点是片元着色器需要通过判断坐标来确认使用哪个纹理来渲染。具体流程如下:

1529071349_77_w804_h1764

两个比较关键的问题,第一、头像本来是方形的,而最终应用的效果需要是圆形的,webGL没有类似css的border-radius来实现,但可以通过纹理遮罩的方式,根据遮罩图片来决定纹理的透明度。

1529071557_71_w1194_h1192

第二、因为顶点着色器传过来的坐标是属于视频纹理的坐标,不能直接应用于头像纹理,所以需要做转换,把整个头像渲染出来,具体核心代码如下:

precision lowp float;
varying vec2 v_texcoord;
// 视频纹理
uniform sampler2D video_sampler;
// 遮罩纹理
uniform sampler2D mask_sampler;
// 头像纹理
uniform sampler2D head1;
uniform sampler2D head2;
// 头像信息。由于glsl不支持不定数组,所以指定了数组数量为40,每四项定义头像的一个位置,最多10个位置。
uniform float arr_head1[40];
uniform float arr_head2[40];
void main(void) {
    bool isImg=false;
    vec4 color;
    vec4 imgColor;
    // 先获取当前坐标的视频纹理颜色信息。这里因为要实现透明视频,所以使用了右侧视频作为rgb,左侧视频作为alpha
    vec4 bgColor = vec4(texture2D(video_sampler, vec2(v_texcoord.s/2.0+0.5,v_texcoord.t)).rgb, texture2D(video_sampler,vec2(v_texcoord.s/2.0,v_texcoord.t)).r);

    // head1处理
    for(int i=0;i<40;i+=4){
        // 因为头像信息并不一定有40,所以可以在头像信息里面设置一个结束标识来跳出循环,这里使用的是-1
        if(arr_head1[i]<0.0){
            break;
        }
        // 颜色的起始x、终止x、起始y、终止y
        float l1=arr_head1[i];
        float l2=arr_head1[i+1];
        float t1=arr_head1[i+2];
        float t2=arr_head1[i+3];
        // 判断当前坐标是否命中当前头像信息,这里t1和t2颠倒主要是因为webgl有做y轴翻转。
        if (v_texcoord.s>l1 && v_texcoord.s<l2 && v_texcoord.t>(1.0-t2) && v_texcoord.t<(1.0-t1)) {
            // 进行纹理转换,把原本的整个画布纹理坐标映射修改为头像区域的纹理映射。
            imgColor = texture2D(head1, vec2((v_texcoord.s-l1)/(l2-l1),(v_texcoord.t-1.0+t2)/(t2-t1)));
            // 同样把这个纹理映射应用于遮罩纹理,并且取出遮罩纹理的rgb作为头像纹理的alpha,这样头像就可以做到透明了。
            imgColor.a = 1.0-texture2D(mask_sampler, vec2((v_texcoord.s-l1)/(l2-l1),(v_texcoord.t-1.0+t2)/(t2-t1))).r;
            // 降低下头像的整体透明度,可以让头像融入到背景
            imgColor.a=imgColor.a-0.4;
            if(imgColor.a<0.0){
                imgColor.a=0.0;
            }
            // 将头像与视频颜色进行透明度混合。
            color = imgColor.a*imgColor + (1.0-imgColor.a)*bgColor;
            isImg=true;
        }
    }
    // ...
    // 这里可以参考head1的方式,处理head2...headN的头像纹理。


    if(isImg!=true){
        color=bgColor;
    }
    gl_FragColor = color;
}

0x4 总结

经过以上处理,动态化视频基本已经完成,后面只需要设计师提供素材就可以了,不需要针对不同视频单独做头像的动画。下面是简单的演示demo (由于头像位置是粗略编辑的,所以位置可能会有一些偏差):

1529072552_100_w710_h712

这种方案其实不单可以应用于直播礼物场景,任何视频需要跟动态信息结合的场景都适用,甚至一些动态信息的复杂动画也可以使用,比如某些场景下,我们希望用户的头像进行一些不规则的运动,如果通过代码来实现成本很大,这个方法也是不错的选择。这其实是跳出css、js的限制,使用设计师素材和GPU相结合的一种解决方案,可以让复杂度降低,并且效果的想象空间更大。

发表评论