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

栏目: CSS3 · 发布时间: 7年前

内容简介:视频滤镜,顾名思义,是在视频素材上duang特效的一种操作。 随着H5页面越做越炫酷的趋势,单一的视频播放已经不能满足我们的需求,视频滤镜在Web页面上的应用越来越广泛。如何实现视频滤镜呢?最容易想到的方案是使用CSS3内置的滤镜。CSS3为我们封装了一些常用的滤镜算法,如模糊,灰阶、饱和度等,使用filter属性来定义,详细参见

视频滤镜,顾名思义,是在视频素材上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,修改像素值。 具体的方式,在我的另一篇 介绍“视频吸色”的文章 中有详细描述。

概括地说,代码如下。

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纹理坐标,传递给片元着色器就可以了。

varying vec2 vUv;
void main()
{
	vUv = uv;
	vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
	gl_Position = projectionMatrix * mvPosition;
}

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

gl_FragColor = texture2D( texture, vUv );

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

灰阶:

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);

反向:

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);

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

雪花怀旧效果:

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:

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)

//取到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,我们还可以做更多的事情,比如曲面视频,球面视频等等,详细的应用场景,有待各位看官大神继续发掘。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Search User Interfaces

Search User Interfaces

Marti A. Hearst / Cambridge University Press / 2009-9-21 / USD 59.00

搜索引擎的本质是帮助用户更快、更方便、更有效地查找与获取所需信息。在不断改进搜索算法和提升性能(以技术为中心)的同时,关注用户的信息需求、搜寻行为、界面设计与交互模式是以用户为中心的一条并行发展思路。创新的搜索界面及其配套的交互机制对一项搜索服务的成功来说是至关重要的。Marti Hearst教授带来的这本新作《Search User Interfaces》即是后一条思路的研究成果,将信息检索与人......一起来看看 《Search User Interfaces》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具