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

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

原创
作者头像
WendyGrandOrder
发布于 2018-10-19 12:30:23
发布于 2018-10-19 12:30:23
8.3K00
代码可运行
举报
文章被收录于专栏:RESTART POiNTERRESTART POiNTER
运行总次数:0
代码可运行

视频滤镜,顾名思义,是在视频素材上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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
varying vec2 vUv;
void main()
{
	vUv = uv;
	vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
	gl_Position = projectionMatrix * mvPosition;
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
gl_FragColor = texture2D( texture, vUv );

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

灰阶:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
//取到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 删除。

评论
登录后参与评论
暂无评论
推荐阅读
linux中如何添加用户并赋予root权限
passwd: all authentication tokens updated successfully.
用户8965210
2021/10/14
9.7K0
linux下添加用户并赋予root权限
passwd: all authentication tokens updated successfully.
用户1685462
2021/07/27
9.7K0
使用了腾讯云Ubuntu系统,但是没有root权限怎么办?
(2)、设置成功后,终端会提示password updated successfully,此时输入以下指令,回车,进入SSH配置页面,如下图所示
用户6948990
2025/04/11
2140
使用了腾讯云Ubuntu系统,但是没有root权限怎么办?
Ubuntu系统获取和禁用root权限教程
sudo passwd root Enter new UNIX password: (在这输入你的密码) Retype new UNIX password: (确定你输入的密码) passwd: password updated successfully
会长君
2023/04/25
1.6K0
为ubuntu操作系统增加root用户
该文介绍了如何为ubuntu操作系统增加root用户。首先需要开启root账户,然后设置root密码,最后测试root账户。具体步骤包括打开终端,使用sudo gedit命令打开配置文件,在文件中添加greeter-show-manual-login=true,保存并关闭文件。之后需要重启计算机以使更改生效。最后,需要重新登录时使用root账户。","author":"赵阳好", "content":"该文介绍了如何为ubuntu操作系统增加root用户。首先需要开启root账户,然后设置root密码,最后测试root账户。具体步骤包括打开终端,使用sudo gedit命令打开配置文件,在文件中添加greeter-show-manual-login=true,保存并关闭文件。之后需要重启计算机以使更改生效。最后,需要重新登录时使用root账户。", "title":"为ubuntu操作系统增加root用户
别先生
2017/12/29
2.6K0
为ubuntu操作系统增加root用户
ubuntu root默认密码(初始密码)
ubuntu安装好后,root初始密码(默认密码)不知道,需要设置。 1、先用安装时候的用户登录进入系统 2、输入:sudo passwd  按回车 3、输入新密码,重复输入密码,最后提示passwd:password updated sucessfully 此时已完成root密码的设置 4、输入:su root 切换用户到root试试.......
似水的流年
2019/12/08
11.3K0
Ubuntu安装完后设置root密码
安装完Ubuntu 14.04后默认是没有主动设置root密码的,也就无法进入根用户。
用户8705036
2021/06/08
2.6K0
Linux 修改用户密码「建议收藏」
Linux修改密码用 passwd 命令,用root用户运行passwd ,passwd user_name可以设置或修改任何用户的密码,普通用户运行passwd只能修改它自己的密码。
全栈程序员站长
2022/09/07
5.8K0
远程Ubuntu系统时获取Root权限
在日常使用云服务器时,经常会遇到服务器无法获取root权限,特别是Ubuntu系统,系统在开始时,会让你采用你自定义的一个名称,类似windows10让你自己创建一个账号而并不是使用Administrator账号一个道理,但是往往自己创建的账号并没有什么用,特别是开发者在开发项目的时候,往往会导致权限不够,无法进行编辑,接下来我就给大家解决这个问题吧!
Meng小羽
2019/12/23
5.5K6
启用某些Linux发行版的root帐号
跟了我 5 年多的本本已步入花甲,CPU 严重老化,运行 Windows 异常吃力,于是考虑换成 Linux 试试。忙活了一天,测试了 2 个“家用”Linux 发行版,一个是深度的 Linux Deepin 2013,另一个是雨林木风的 StartOS 5.1。在测试过程中也遇到一些有用的经验,现在就一一记录一下。 这些发行版和 ubuntu 一样,root 帐号都是停用的,在我安装完后,发现进行一些操作时,提示权限不足。其实我知道可以使用 sudo 来临时获取 root 权限,但是毕竟想一劳永逸,于是就
张戈
2018/03/23
2.9K0
启用某些Linux发行版的root帐号
ubantu下su命令Authentication failure失败的解决方式
Ubuntu安装后,root用户默认是被锁定了的,不允许登录,也不允许 su 到 root 。 可以使用: sudo passwd 来重新设置root密码,后即可登陆root。 ortonwu@ubuntu:/etc/vim$ sudo passwd Enter new UNIX password: Retype new UNIX password: passwd: password updated successfully
Tencent JCoder
2018/07/02
1.4K0
如何给Ubuntu设置root账户?
昨天我们讲解了Java的构造函数重载以及和普通函数的一些区别, 那么今天来玩点别的,比如最新的Ubuntu系统在进入系统后并没有给我们设置root账号,那么今天就让小编来带大家演示一下。
小Bob来啦
2021/11/04
2.2K0
如何给Ubuntu设置root账户?
ubuntu中root和普通用户切换方法
ubuntu登录后,默认是普通用户权限,那么普通用户权限和root权限如何切换呢,下面总结下它们之间如何切换。
一个会写诗的程序员
2022/09/30
8.3K0
第二章,ubuntu系统的查看防火墙,切换root用户,设置固定ip、系统时间等
第一次接触ubuntu系统,之前用的都是centos系统,因此也需要知道ubuntu的基本操作,跟centos的差别还是很大的。
全栈程序员站长
2022/08/05
2.1K0
第二章,ubuntu系统的查看防火墙,切换root用户,设置固定ip、系统时间等
[Linux]Ubuntu设置root密码并解决xShell连接问题
在终端输入命令 sudo passwd,然后输入当前用户的密码,需要确认两次。 也可以输入命令sudo passwd root 进行设置。
祥知道
2020/03/10
2.5K0
centos 7系统下安装laravel运行环境的步骤详解
前言 因为最近在学习linux,而最好的学习就是实践,/【一个开发人员,能懂服务器量好,反之一个服务器维护人员,也应该懂开发】/学习linux同时安装LAMP的环境搭配,跑了度娘都没找到我想要的文章。那我就简单的写写我在centos7下安装laravel的安装过程。 网络设置 ping 114.114.114.144 网络连接失败,将虚拟机的网络适配器改成桥接模式(自动),然后设置开启启动
用户2323866
2021/07/01
1.7K0
CentOS普通用户添加管理员权限 原
1、添加用户,首先用adduser命令添加一个普通用户,命令如下: #adduser keaising//添加一个名为keaising的用户 #passwd ljl //修改密码 Changing password for user keaising. New UNIX password: //在这里输入新密码 Retype new UNIX password: //再次输入新密码 passwd: all authentication tokens updated successfully. 2、赋予root权限 方法一: 修改 /etc/sudoers 文件,找到下面一行,把前面的注释(#)去掉,最终结果为: ## Allows people in group wheel to run all commands %wheel ALL=(ALL) ALL 然后修改用户,使其属于root组(wheel),命令如下: #usermod -g root keaising 修改完毕,现在可以用keaising帐号登录,然后用命令 su – ,即可获得root权限进行操作。
拓荒者
2019/03/11
4K0
linux普通用户获取管理员权限
passwd: all authentication tokens updated successfully.
用户8826052
2022/03/02
4.2K0
【linux命令讲解大全】211.Linux系统命令之passwd的用法详解
passwd命令用于设置用户的认证信息,包括用户密码、密码过期时间等。系统管理者则能用它管理系统用户的密码。只有管理者可以指定用户名称,一般用户只能变更自己的密码。
全栈若城
2024/03/02
4660
每天学一个 Linux 命令(10):passwd
https://github.com/mingongge/Learn-a-Linux-command-every-day
民工哥
2021/01/12
1.1K0
每天学一个 Linux 命令(10):passwd
推荐阅读
相关推荐
linux中如何添加用户并赋予root权限
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档