前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Web H5视频滤镜的“百搭”解决方案——WebGL着色器

Web H5视频滤镜的“百搭”解决方案——WebGL着色器

原创
作者头像
WendyGrandOrder
发布2018-10-19 20:30:23
7.7K1
发布2018-10-19 20:30:23
举报
文章被收录于专栏:RESTART POiNTERRESTART POiNTER

视频滤镜,顾名思义,是在视频素材上duang特效的一种操作。 随着H5页面越做越炫酷的趋势,单一的视频播放已经不能满足我们的需求,视频滤镜在Web页面上的应用越来越广泛。

问题概述

如何实现视频滤镜呢?最容易想到的方案是使用CSS3内置的滤镜。

CSS3为我们封装了一些常用的滤镜算法,如模糊,灰阶、饱和度等,使用filter属性来定义,详细参见 https://www.w3cplus.com/css3/ten-effects-with-css3-filter

除了作用于图片,该属性也可以作用于video标签,即视频滤镜。 同理,svg的filter也可以实现类似的效果,实现方式大同小异。

小伙伴的IceVideo组件便置入了基于CSS3 filter实现的视频滤镜,链接内有包括案例在内的详细说明,本文不再赘述。

本文主要讨论的是上述方案无法覆盖的场合。 对于一些特殊风格化、定制化的效果,我们很难通过现有的filter来做出,比如

上述的抠图效果、旧电视雪花效果,本身计算方式复杂,无法使用简单的规则来定义。 对于这类“很难归类”需求,难道就没有一种更加自由的,泛用的滤镜实现方式,可以满足复杂场景吗? 答案当然是有的。 本文便介绍一种“百搭”的解决办法——WebGL着色器。 使用WebGL提供的api,在像素操作级别,定制只属于你的一款滤镜。

先睹为快的示例

(示例中的视频均来自QQ-AR项目合作商的线上素材)

为了探索合适的方案,我们需要从问题的本质入手分析。

问题一、视频滤镜的本质是什么?

滤镜的本质是一种映射。即通过某种特定的算法,将图像中的像素点从一个值,映射成另一个。 对于视频,则是对每一个图像帧进行映射。 映射算法的设计,是图形图像处理的内容,目前已经有很多成熟的算法。

举几个简单的例子:

灰阶的映射算法。 new rgb = (0.2989*r + 0.5870*g + 0.1140*b)

反相(底片)的映射算法 new r=1.0 - r; new g=1.0 - g; new b=1.0 - b;

通过调节其中的计算参数,就可以控制效果的强弱。

在Web上,如何实现这些算法呢?

我们不能够直接操作video标签的内容,但我们能够做一个“中转”,把video绘制到canvas里,然后直接使用canvas提供的绘制api,修改像素值。 具体的方式,在我的另一篇介绍“视频吸色”的文章中有详细描述。

概括地说,代码如下。

代码语言:javascript
复制
function playCanvas() {
      var mycanvas = $(_this).find("#mycanvas")[0]
      var myvideo = $(_this).find("#myvideo")[0]
      var context = mycanvas.getContext("2d")
      context.drawImage(myvideo, 0, 0, opts.videoWidth, opts.videoHeight)
      colorData = getPixelColor(mycanvas, canvasMousePos.x, canvasMousePos.y)
      requestAnimationFrame(playCanvas)
}

将原始的video标签设为隐藏,然后使用requestAnimationFrame回调,不断地用video的内容来更新canvas。 使用canvas方案,我们有了处理单帧图片的方法,而且它的兼容性比CSS3 filter要好,只要支持canvas的浏览器都可以渲染。 这种方法对于图片来说是足够的,几乎没有时间延迟,但处理每秒24-60帧的视频,就会产生较大的延迟,引发严重的性能问题。

上图是使用canvas的像素操作实现灰阶滤镜时,在chrome console录制的资源消耗图 可以看到,cpu的主线程已经被占满,在电脑上有明显卡顿,在手机上几乎是无法使用的。

这种方案的问题在于,将所有的像素都输入给cpu,逐点串行,没有考虑并行化的可能。 那么视频滤镜操作能否并行呢?主要取决于滤镜的实现方式,即“像素是怎么映射的”。

问题二、能否并行?

笔者考察了图形图像处理中,常见的滤镜实现方式,将其归纳总结为以下三类。

1、单像素映射法 对单个像素的颜色值进行操作。 比如反相,灰阶,变亮变暗,饱和度效果。 乃至在笔者的需求中遇到的,更为复杂的绿幕视频抠图效果(后文会有详细叙述)。

2、区域卷积法 计算一个像素时,同时使用邻近n个像素的值。 可以描述为卷积操作,使用一个矩阵作为卷积核,遍历整个图像。 比如模糊,浮雕等效果,都是用这种方式做出的。

3、颜色查表法 对于一些高度风格化的处理,很难采用单一算法描述,此时可以将颜色保存在一个512x512的表里,通过查找和差值,推算出每个像素的映射结果。 这种算法叫做Color Lookup Table,简称Color LUT,最经典的实现来自于ios内置算法库GPUImage。 该算法库已开源,github地址 https://github.com/BradLarson/GPUImage

以上三种类别,虽然原理各异,但都是局限在图像局部的操作,空间复杂度是O(1)级别的。

那么,这些算法,一定是可以并行化的。

问题三、如何并行?

实际上,css3中的filter属性,和我们熟悉的transform一样,是强制使用强制使用GPU渲染的。 也就是说,如果我们给video标签设置一个filter,像素间的计算便已经并行化了。

如果不使用css3中定义的属性,而自定义计算方式,仅靠video或者canvas方案,都无法唤起cpu,前面说的“中转”方案也无法直接使用。 这时候,我们就需要用到前端的一个强大武器——WebGL。

WebGL是一套实现了OpenGL标准的Web API,这其中也包括像素级的并行计算API——着色器(Shader)。 着色器定义了一个三维空间中的点,如何渲染成为屏幕上的一个像素点。 可以理解为WebGL渲染管道的最后一个步骤。 分为顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)两个步骤,具体的工作原理有很多介绍OpenGL的教程都有提及,此处不再赘述。

利用WebGL提供的api,我们可以定义自己的Shader。 虽然是在Web上实现,但并不是使用Javascript语法,而是使用GLSL语法书写的。 关于具体的语法,这里也不再展开赘述。

在Web上使用自定义Shader进行渲染的过程,可以用下图来概括。

落实到具体实现过程,可以分为三步。

1、建立一个场景,并且把视频作为材质,贴到一个平面物体上。

2、对这个材质指定顶点着色器和片元着色器。

3、将物体置入场景,在屏幕中的canvas对象中渲染出来。

因为物体是简单的平面,所以我们的顶点着色器很简单,只要计算出每个像素的UV纹理坐标,传递给片元着色器就可以了。

代码语言:javascript
复制
varying vec2 vUv;
void main()
{
	vUv = uv;
	vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
	gl_Position = projectionMatrix * mvPosition;
}

在片元着色器里,我们通过下面的语句

代码语言:javascript
复制
gl_FragColor = texture2D( texture, vUv );

取到这个点的实际色值,然后开始真正的像素映射计算。

灰阶:

代码语言:javascript
复制
float gray = 0.2989*gl_FragColor.r+0.5870*gl_FragColor.g+0.1140*gl_FragColor.b;
gl_FragColor = vec4(gray,gray,gray , gl_FragColor.a);

反向:

代码语言:javascript
复制
float reverser=1.0 - gl_FragColor.r;
float reverseg=1.0 - gl_FragColor.g;
float reverseb=1.0 - gl_FragColor.b;
gl_FragColor = vec4(reverser,reverseg,reverseb,gl_FragColor.a);

下面是两个较为复杂的效果实现。

雪花怀旧效果:

代码语言:javascript
复制
float dx = fract(sin(dot(vUv ,vec2(12.9898,78.233))) * 43758.5453);
vec3 cResult = gl_FragColor.rgb + gl_FragColor.rgb * clamp( 0.1 + dx, 0.0, 1.0 );
vec2 sc = vec2( sin( vUv.y * 4096.0 ), cos( vUv.y * 4096.0 ) );
cResult += gl_FragColor.rgb * vec3( sc.x, sc.y, sc.x ) * 0.025;
cResult = gl_FragColor.rgb + clamp( 0.35, 0.0,1.0 ) * ( cResult - gl_FragColor.rgb );
if( false ) {
  cResult = vec3( cResult.r * 0.3 + cResult.g * 0.59 + cResult.b * 0.11 );
}
float oldr=0.393*cResult[0]+0.769*cResult[1]+0.189*cResult[2];
float oldg=0.349*cResult[0]+0.686*cResult[1]+0.168*cResult[2];
float oldb=0.272*cResult[0]+0.534*cResult[1]+0.131*cResult[2];
gl_FragColor =  vec4( oldr,oldg,oldb , gl_FragColor.a);

(参考了Threejs官方范例)

绿幕抠图Chroma Keying:

代码语言:javascript
复制
float rgb2cb(float r, float g, float b){
  return 0.5 + -0.168736*r - 0.331264*g + 0.5*b;
}
float rgb2cr(float r, float g, float b){
  return 0.5 + 0.5*r - 0.418688*g - 0.081312*b;
}
float smoothclip(float low, float high, float x){
  if (x <= low){
    return 0.0;
  }
  if(x >= high){
    return 1.0;
  }
  return (x-low)/(high-low);
}
vec4 greenscreen(vec4 colora, float Cb_key,float Cr_key, float tola,float tolb, float clipBlack, float clipWhite){
  float cb = rgb2cb(colora.r,colora.g,colora.b);
  float cr = rgb2cr(colora.r,colora.g,colora.b);
  float alpha = distance(vec2(cb, cr), vec2(Cb_key, Cr_key));
  alpha = smoothclip(tola, tolb, alpha);
  float r = max(gl_FragColor.r - (1.0-alpha)*color.r, 0.0);
  float g = max(gl_FragColor.g - (1.0-alpha)*color.g, 0.0);
  float b = max(gl_FragColor.b - (1.0-alpha)*color.b, 0.0);
  if(alpha < clipBlack){
    alpha = r = g = b = 0.0;
  }
  if(alpha > clipWhite){
    alpha = 1.0;
  }
  if(clipWhite < 1.0){
    alpha = alpha/max(clipWhite, 0.9);
  }
  return vec4(r,g,b, alpha);
}

float tola = 0.0;
float tolb = u_threshold/2.0;
float cb_key = rgb2cb(color.r, color.g, color.b);
float cr_key = rgb2cr(color.r, color.g, color.b);
gl_FragColor = greenscreen(gl_FragColor, cb_key, cr_key, tola, tolb, u_clipBlack, u_clipWhite);

(参考了github上的开源项目greenscreen)

以Chroma Keying算法为例,看起来代码比较长,我们可以分解一下它的核心原理,简要描述如下:

1、计算key色的红、蓝分量,组成向量A。 2、计算目标颜色的红蓝分量,组成向量B。 3、计算两个向量的距离(一个分量在另一个分量上的投影) 当AB向量接近,alpha趋于1 AB向量很远,alpha趋于0 4、以alpha作为过滤指标,滤掉目标颜色rgb值中的key色分量,计算出该点的rgb值 5、将1-alpha作为该点的透明度值(rgba中的a) 6、将该点像素值设置为新的rgba

提取分量A、B,计算alpha值,并设置新颜色的算法,可以用下图表示

通过这样的映射,我们可以很好地处理半透明边缘、模糊边缘

上图是应用在QQ-AR透明Webview项目中的案例

更多的滤镜算法,可以参考其他图形图像方面的资料。

虽然看似复杂,但上述所有算法,都是局部像素的浮点数计算。 我们把它们放进GPU中充分并行之后

得到是Chrome console资源消耗图

可以看出,计算重心转移到了GPU,cpu仍是相对空闲的。

我们对QQ-AR透明Webview中的示例进行帧率考察

可以看出,在使用gpu并行计算时,滤镜几乎不会引发掉帧。

除了定义Shader之外,我们在建立场景时,还要考虑如何完成从3D到2D的合理映射。 如何把视频作为材质渲染到场景中,并且刚好填满视口? 我们知道,一个三维场景是通过摄像机来映射到二维视口的。

传统的投影相机,有近大远小的问题。 实际上,我们很难通过视频素材本身的宽高,计算出最终视口的宽高。

这里要用到OrthographicCamera(正交相机)

正交相机没有投影变形,所以也就不存在近大远小准则。 在建立场景时,只要保证相机视口的尺寸和渲染物体的尺寸相同。 渲染物体尺寸又根据视频本身的长宽来取。 就可以建立一个视频同等大小的WebGL Canvas场景。

下面是核心代码

(使用了Three.js操作WebGL api)

代码语言:javascript
复制
//取到video标签
var video = document.getElementById(videoId);

//设置场景
var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer( { antialias: true,alpha: true } );
document.getElementById(container).appendChild(renderer.domElement);
renderer.setClearColor(0xffffff,0);
renderer.setSize( video.width, video.height );
//设置正交相机
var camera = new THREE.OrthographicCamera(-2, 2, 1.5, -1.5, 1, 10);
camera.position.set(0, 0, 1);
scene.add(camera);
//设置平面物体,并将视频作为材质
var movieMaterial = new ChromaKeyMaterial(videoId, video.width, video.height, 0x00ff05,0);
var movieGeometry = new THREE.PlaneGeometry(4, 3);
var movie = new THREE.Mesh(movieGeometry, movieMaterial);
movie.position.set(0, 0, 0);
movie.scale.set(1, 1, 1);
movie.visible=false;
scene.add(movie);
//开始动画
video.play();
animate();
function animate() {
  if( (video.currentTime>1) && movie.visible==false){
    movie.visible=true;
  }
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

说明1:ChromaKeyMaterial是继承了Three默认的ShaderMeterial实现的自定义材质类。

自定义类的代码较长,此处不再贴出,详细可以右键本文提供的案例代码。

说明2:animate函数里,通过video.currentTime来切换movie物体的显示隐藏,是为了预防平面物体在材质贴图完成前(视频还在载入时)的一段时间黑屏,实际项目中可以加入一些loading效果,以保证体验

问题四、兼容性如何?

不是所有的设备都兼容CSS3 filter(仅限Chrome内核) 也不是所有设备都支持WebGL标准(比如万恶的ie) 这是CANIUSE提供的WebGL兼容性结果。

这是腾讯大数据中心对移动设备兼容WebGL的统计结果。

实际上,由于x5内核的存在,在手机QQ中兼容WebGL的比例要比图上的16%更高一些。 下面则是我们使用上报的方式,对移动设备进行考察,得到的结果。

在移动端大部分设备都越来越先进的今天,为了duang出更好更酷炫的效果,在必要的场合使用WebGL方案是可取的。

总结

以上就是本文主要介绍的内容,在文章结尾,我们再重新看一遍开头的例子。

传送门

例子中,左边是一个普通视频,右边是使用Chroma Keying算法进行抠图的绿幕视频。 我对二者都应用了自定义的滤镜,并且开放了一部分参数由用户控制。

从例子中可以看出。 1、滤镜是可以叠加的(因为这些滤镜算法本质都是像素计算,只要把算法叠加起来就好了) 2、参数是可控的(因为算法的实现完全透明,所以我们对它有全权控制权,用起来足够灵活)

当然代价就是实现成本比较高,所以,对于简单的需求,我们仍推荐使用简单的方案(比如css3滤镜,svg滤镜)。 对于复杂的需求,再来使用本文提出的方案,定制个性化特效。

并且注意对于不兼容情况的降级处理(推荐降级成使用普通video标签来渲染,放弃滤镜)

WebGL的强大之处绝不仅于此,使用自定义Shader,我们还可以做更多的事情,比如曲面视频,球面视频等等,详细的应用场景,有待各位看官大神继续发掘。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题概述
  • 问题一、视频滤镜的本质是什么?
  • 问题二、能否并行?
  • 问题三、如何并行?
  • 问题四、兼容性如何?
  • 总结
相关产品与服务
GPU 云服务器
GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档