最近工作上在研讨将Android 12的窗口布景含糊移植到Android 11的体系上,便是下面这张图中的b作用所示:
计划是研讨出来了,可是考虑到潜在的法律风险要素,不能往外发,因而这篇文章就来对Android 11 上布景含糊的核心——BlurFilter的原理做一个简略的剖析吧。
1. 含糊原理
所谓含糊,能够了解成对图画上的每一个像素都取周边像素的均匀值,在图形上,就相当于产生含糊作用,中心点失掉细节。核算均匀值时,取值规模越大,含糊作用越激烈。
含糊的算法由很多种,咱们所熟知的“高斯含糊”仅仅很多含糊算法中名望最大的一个,常见的含糊算法有以下这些:
- 高斯含糊(Gaussian Blur)
- 方框含糊(Box Blur)
- Kawase含糊(Kawase Blur)
- 两层含糊(Dual Blur)
- 散景含糊(Bokeh Blur)
- 移轴含糊(Tilt Shift Blur)
- 光圈含糊(Iris Blur)
- 粒状含糊(Grainy Blur)
- 径向含糊(Radial Blur)
- 方向含糊(Directional Blur)
这些算法都有什么优缺点,哪个算法更好呢?在评价这些含糊算法的时分,一般从以下三个维度考虑:
(1)含糊质量(Quality):含糊质量的好坏是含糊算法是否优秀的首要目标。
(2)含糊安稳性(Stability):含糊的安稳性决议了在画面变化进程中,含糊是否安稳,不会出现跳变或许闪耀。
(3)功能(Performance):功能的好坏是含糊算法是否能被广泛运用的要害地点。
依照上述规范,它们的功能比照如下表所示:
含糊算法 | 含糊质量 | 安稳性 | 功能 |
---|---|---|---|
高斯含糊(Gaussian Blur) | 高 | 好 | 一般 |
方框含糊(Box Blur) | 高 | 好 | 一般 |
Kawase含糊(Kawase Blur) | 高 | 好 | 较好 |
两层含糊(Dual Blur) | 高 | 好 | 好 |
散景含糊(Bokeh Blur) | 高 | 好 | 差 |
移轴含糊(Tilt Shift Blur) | 高 | 好 | 差 |
光圈含糊(Iris Blur) | 高 | 好 | 差 |
粒状含糊(Grainy Blur) | 一般 | 好 | 好 |
径向含糊(Radial Blur) | 高 | 好 | 一般 |
方向含糊(Directional Blur) | 高 | 好 | 一般 |
Android 官方体系选用的含糊算法选用了Kawase Blur和Dual Blur相结合的算法。Kawase Blur其含糊作用与高斯含糊十分接近, 但在类似的含糊表现下,其功率比高斯含糊要快1.5到3倍。下文先介绍Kawase Blur,只要了解清除了这个才干讲解Dual Blur。
关于Kawase算法的原理,现在互联网上各材料都讲的不太透彻,看起来让人一头雾水。本文从头总结拆解了一下,尽量让它看起来容易了解一点。先从步长为1个单位(下文简称步长1)的状况开端看起。
上图是Kawase算法采样步长1的示意图。当处理由黑点表明的像素时,挑选由周围四个红点表明的采样点来求和均匀核算所得。采样时挑选的采样点(即红点)是红点四周的四个像素通过插值核算所得。在3*3像素矩阵的状况下,其最终核算作用等效于对黑点及黑点周围的8个像素依照上图右侧图示的权重进行加权求和,得到的即为黑点的采样像素。
那么问题来了,为什么不直接选用右侧的加权值,对像素进行核算呢?原因在于,选用左边的办法能够充分利用GPU的功能,让GPU来执行插值的操作,通过这种办法,读取像素纹路的次数已从9次减少到4次,充分利用GPU双线性插值采样开支低的优势完结了算法的优化。
上面这张图为步长2和步长3的状况。为了尽可能提高含糊作用,Kawase算法能够挑选对进行屡次含糊,并对更多像素进行采样。例如,将上述办法用作第一次含糊,能够通过添加采样点的偏移量来执行下一次含糊。如上图所示,假设图画含糊的1个单位步长代表2个像素,那么鄙人一次含糊时,黑点会挑选偏移量为2个步长(即4个像素)处的红点,用于核算自身的像素值。而且这种迭代采取了一种Ping-Pong Blit 的办法进行。所谓Ping-Pong Blit,指的是将上一次的含糊好的图片,鄙人一次迭代时以资料的办法输入,进行更进一步的含糊。
依照以上办法,进行屡次含糊后,就能够完结类似于高斯含糊的作用了。
解说完Kawase算法后,下面能够来了解一下Dual Blur了。严格来说,Dual Blur并不是一种算法,它仅仅一种优化思想。它能够搭配Kawase Blur运用,也能够搭配Gaussian Blur 运用。它的完结思路在于:在原有含糊办法的基础上,选用降采样、升采样的办法进行迭代,然后到达含糊的优化。
那么什么叫降采样?什么叫升采样呢?
图画的烘托是根据画布进行的,画布存在宽高的特点。假设画布宽高越来越小,则采样的速度越快,烘托速度也越快,相比之下画质也会变含糊,这个进程叫降采样。
相反,假设画布越来越大,采样的速度变慢,但画质烘托会更加精细,烘托速度变慢,这样进程就叫做升采样。
Dual Blur便是不断地在迭代进程中,反复利用降采样和升采样的办法,在降采样后含糊,然后再升采样,来到达优化含糊作用和功率的目的。
2. Android BlurFilter
在Android 11中添加含糊作用,有一点特别的好处便是,谷歌官方现已在体系中提供了一个根据opengl es3.1的含糊滤镜类,不需要咱们从头去开发。详细的类途径如下所示:
frameworks/native/libs/renderengine/gl/filters/BlurFilter.cpp
下面来详细分析一下Android 11 含糊计划的详细完结。先从GLSL开端看起,看看BlurFilter是如何控制GPU去烘托来完结含糊作用的。
首先是极点着色器,详细代码如下:
这段代码便是原封不动地将传入的极点坐标数据传递给GPU,能够看到代码中没有任何的改换修正,另外,界说了vUV变量保存传入的纹路坐标,用于传递给接下来的片段着色器中,对纹路施行改换操作。
BlurFilter中界说了两个片段着色器,片段着色器一是用来生成将纹路资料含糊后的图画的,代码如下:
注意到代码的第5行,in highp vec2 vUV这个vUV变量么,它的数据便是从前文说到的极点着色器中传入的纹路坐标了。从代码上看,android在做含糊采样时,并没有严格依照采样点四周的四个像素通过插值核算的结果来作为采样像素值,仅仅仅仅取样了指定偏移量下的四个方位的像素点的数据来直接代入核算。这种规划办法有两种考量,一是考虑到功率的要素,需要功率优先而舍弃必定的含糊质量。二是引入了Dual Blur,Dual Blur在纹路的缩放进程中就现已对纹路进行了线性插值,必定程度上弥补了算法规划上的不足。
片段着色器二是对含糊后的纹路与原始图画进行混色处理,代码如下:
上述代码有3个要害变量,uCompositionTexture、uTexture以及uMix。uCompositionTexture是含糊前的原始纹路,uTexture是含糊后的纹路,uMix是混合因子。两张纹路图片会通过指定的uMix值来进行混合插值。通过插值处理后的图画,便是最终的含糊作用图了。
在前文的含糊原理中,咱们从未说到过要和原图做一次混色的操作,这儿为什么需要做这样的行为呢?原因在于,为了更好的显现作用。
为了更进一步解说这个问题,首先得先重点介绍一下GLSL中的mix这个函数。
GLSL言语提供了Mix函数,能够用来混合不同的色彩。Mix函数承受三个参数,它们分别是两个需要混合的值和一个混合因子。混合因子是一个0-1之间的值,它表明第一个值填充的比例。假设混合因子为0,则返回第一个值;假设混合因子为1,则返回第二个值;假设混合因子为0.5,则返回两个值的均匀值。
因而,传入的uMix值决议了最终生成的含糊图画的呈现作用。uMix为0,表明输出了原图,最终呈现的图画没有任何含糊作用;uMix为1,表明呈现的图画是彻底含糊后的图画;假设传入的值为0.2呢?那可能的视觉作用是,图画全体细节明晰,但又带一点轻微含糊的感觉。
BlurFilter中详细设置的uMix值究竟是多少呢?咱们能够从它的render办法里的这一行代码中找到答案:
GLfloat mix = fmin(1.0, mRadius / kMaxCrossFadeRadius);
其间mRadius指的是含糊半径,kMaxCrossFadeRadius是预设的常量,表明开端彻底含糊的最小含糊半径,这儿界说的是30。也便是说,但指定含糊作用的含糊半径小于30时,GPU就会启用混色算法,将含糊的图画与原图依照特定的混合因子进行混合,以优化视觉作用。
前文还说到,BlurFilter在含糊时,还选用了Dual Blur么。这一点,咱们能够在prepare办法中找到相关的处理代码:
能够看到,这儿选用了两张画布来进行烘托,draw以及read。两张画布的巨细是不相同的,其间draw画布巨细是read画布的1/4。当纹路被烘托到draw画布后,对纹路进行Kawase含糊,并将含糊后的图画扩大到read画布上。扩大时GPU会对图画中的像素进行双线性差值,使图画的色彩过渡尽量显得滑润,不会出现断层现象。然后交换read和draw画布,将此次输出的图画作为下次迭代的输入,同时添加第二次迭代时的步长。如此反复,直到到达限定的passes次数。
那么,Kawase算法究竟要迭代多少次呢?代码中是这么规定的:
const auto radius = mRadius / 6.0f;
const auto passes = min(kMaxPasses, (uint32\_t)ceil(radius));
迭代次数与含糊半径有关,但最多不超越kMaxPasses次(这儿是4次)。也便是说,当含糊半径大于24时,它的迭代次数也最多只能是4次了。
3. GLES烘托引擎的处理
前面讲了含糊原理,接下来要讲一下GLES烘托引擎在含糊计划所做的一些处理了。咱们需要对窗口后方的屏幕做含糊作用,可是怎么样才干知道,详细是哪些层级的窗口需要含糊呢?
在烘托引擎中,咱们用Layer的概念来表明一块独立的图画缓冲区,这些独立的图画缓冲区保存着即将烘托到屏幕上的图画内容。为了便于了解,必定程度上,咱们能够把Layer与android 上层的Window划等号(不彻底是一一对应关系)。在WindowManager中,android 11提供了一个flag —— FLAG_BLUR_BEHIND,用于给Window的Attrs打上一个符号,这个符号意味着当时Window地点的Layer以下的所有图层,都需要进行含糊处理。
然而,在GLES烘托引擎中,你无法找到任何有关这个flags的标识。烘托引擎判别含糊的条件非常简略,假设当时layer的布景含糊半径backgroundBlurRadius值大于0,则表明当时Layer以下的图层都需要含糊。
那前文说到的FLAG_BLUR_BEHIND究竟有什么用,烘托引擎压根就不关注它?其实,这个参数是在framework层处理的,只要设置了这个flag,Window才干被设置含糊半径,否则它的值便是0。
Layer在烘托引擎里保存的容器是vector,且窗口层级越低的Layer,越早被压入vector中。请注意这一点次序,它关于特定层级下的内容含糊有着至关重要的作用。
烘托引擎对Layer的烘托办法因为牵涉到opengl的帧缓冲区对象(FBO)的相关知识,解说起来会非常复杂。为了能更好地方便了解,仍是用一个比喻来解说一下烘托流程。
咱们用建房子来类比烘托引擎对一帧图画的烘托进程,假设咱们要造7层楼,每制作一层楼,就好比于烘托引擎完结一个长度为7的Layer vector的烘托。
造房子要从第一层开端造起,烘托也是如此,要从最底层的Layer开端烘托。
今后每修一层,都要先拿制作图纸看一下,看制作图纸上在这一层上有没有特殊的需求,比方给下面所有现已修好的楼层外墙给贴上瓷砖。有的话,先贴好瓷砖,再开端建筑这一层。烘托逻辑也是如此,在layer开端烘托前,先判别当时layer有没有含糊的需求(即layer的backgroundBlurRadius特点值大于0),有的话,对之前已完结烘托的纹路添加含糊作用,然后再将当时Layer的纹路烘托上去。
如此反复,完结7层楼的建筑,然后交付房子。烘托引擎也是如此,完结悉数Layer的烘托后,将离屏烘托的内容输出到屏幕上,显现1帧的画面。