Android的(2D)画布绘制流程中的各个部分是如何组合在一起的?

65
我希望更好地了解Android(2D)画布绘制管道的组件如何配合使用。
例如,XferModeShaderMaskFilterColorFilter如何交互?这些类的参考文档非常简洁,而CanvasPaint的文档并没有提供任何有用的解释。
对于具有内在颜色的绘图操作(例如:drawBitmap与“向量”基元如drawRect),它们如何适应所有这些内容还不是很清楚 - 它们总是忽略Paint的颜色并使用它们的内在颜色吗?
我也对以下事实感到惊讶:
Paint eraser = new Paint();
eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawOval(rectF, eraser);

这将擦除一个椭圆形。在我注意到这一点之前,我的心理模型是,绘制到画布(概念上)会绘制到单独的“层”,然后使用Paint的传输模式将该层与Canvas的位图组合。如果事情就那么简单,那么上面的代码将擦除整个位图(在剪辑区域内),因为CLEAR始终将颜色(和alpha)设置为0,而不管源的alpha如何。因此,这意味着还有另一种额外的掩蔽来限制擦除为椭圆形。

我确实找到了API演示,但每个演示都是“真空”中工作的,并没有展示它关注的东西(例如:XferModes)如何与其他内容(例如:ColorFilters)交互。

如果有足够的时间和精力,我可以通过经验法则弄清楚这些部分之间的关系或解密源代码,但我希望有人已经解决了这个问题,或者更好的是,我错过了某些实际的管道/绘图模型的文档。

这个问题的灵感来自于看到这个对另一个SO问题的答案中的代码。

更新

在寻找一些文档时,我想到由于我感兴趣的大部分内容似乎都是skia上覆盖的一个薄薄的表层,也许有一些skia文档会很有帮助。我能找到的最好的东西是SkPaint的文档,其中写道:

有6种类型的效果可以分配给画笔:

  • SkPathEffect-在生成 alpha mask 之前对几何图形(路径)进行修改(例如虚线)
  • SkRasterizer - 组合自定义遮罩层(例如阴影)
  • SkMaskFilter - 在对 alpha mask 进行着色和绘制之前对其进行修改(例如模糊,浮雕)
  • SkShader-例如渐变(线性,径向,扫描),位图模式(clamp,repeat,mirror)
  • SkColorFilter-在应用xfermode之前修改源颜色
  • SkXfermode-例如porter-duff传输模式,混合模式

虽然没有明确说明,但我猜这里的效果顺序是它们出现在管道中的顺序。


它们最好被理解为数学函数,从1或2个源图像中获取(alpha,color)元组,并输出一个元组。http://hi-android.info/doc/android/graphics/PorterDuff.Mode.html(我从未使用过Skia,只是基于网络搜索结果) - rwong
@rwong:这是对PorterDuff.Mode的很好解释,但它并没有真正解释整个流程。例如,请参阅我关于PorterDuff.Mode.CLEAR和drawOval的问题部分。CLEAR被定义为始终将0输出到alpha和颜色通道,而不考虑输入,因此应用CLEAR应该擦除整个目标,除非有进一步的遮罩限制数学函数clear(src, dst) = [0,0]的应用。 - Laurence Gonsalves
为什么你假设有额外的遮罩?椭圆本身就是遮罩。软件渲染器不会费心混合绘制形状之外的像素。你的解释只适用于位图。 - Romain Guy
3个回答

51

正如Romain Guy所说,“在StackOverflow上回答这个问题很困难”。实际上并没有完整的文档,而且要包含完整的文档可能会很大。

于是我开始阅读源代码并进行了一系列的实验。一路上我做了笔记,最后把它们整理成了一个文档,你可以在这里看到:

以及这张图表:

enter image description here

这显然是“非官方”的,所以需要注意一些常规性的警告。

基于上述内容,下面是对一些“子问题”的答案:

我还不太确定具有内在颜色的绘制操作(例如:`drawBitmap`与`drawRect`等“向量”原语)如何适配到其中--它们是否总是忽略`Paint`的颜色并改用它们自己的内在颜色?

“源颜色”来自于`Shader`。在使用非`ALPHA_8`位图时,`drawBitmap`会临时替换`Shader`为一个`BitmapShader`。在其他情况下,如果没有指定`Shader`,则使用一个只生成纯色的`Shader`,即`Paint`的颜色。

我还惊讶于这样的事实:

Paint eraser = new Paint();
eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawOval(rectF, eraser);
这会擦除一个椭圆形。在我意识到这点之前,我的心理模型认为绘制到画布上(在概念上)是绘制到一个单独的“层”,然后将该层与 Canvas 的 Bitmap 使用 Paint 的 transfer mode 组合。如果事情就这么简单,那么上面的代码将会擦除整个 Bitmap (在剪切区域内),因为 CLEAR 总是将颜色(和 alpha 值)设置为 0,无论源的 alpha 值是多少。所以这意味着还有一种额外的掩码操作限制了椭圆形的擦除。
XferMode 应用于“源颜色”(来自 Shader)和“目标颜色”(来自 Canvas 的 Bitmap)。然后使用在光栅化中计算出的遮罩与目标进行混合。有关更多详细信息,请参见上文中的传输阶段。

@Aleadam 哎呀!谢谢提醒。现在应该已经修复了。 - Laurence Gonsalves
@Laurence 做得好!我需要很长时间才能浏览完所有的信息。 - Aleadam
你图表中的着色部分有点令人困惑。似乎着色器(Shader)会生成一张图像,然后通过颜色过滤器(ColorFilter)进行处理,最后再发送到 Xfermode。但是当你的 Shader 是线性渐变时,实际上并不会生成“图像”。你网站上的解释更加清晰(一个 Shader 是一个 f(x, y) 返回颜色的函数)。 - Romain Guy
您在网站上提到了两个可能的错误。第一个不是错误,这是一种保证行为。第二个确实很有趣,我认为它是一个错误。我需要与Skia的原始作者核实。 - Romain Guy
@Romain 是的,我知道着色器在一般情况下并不实际生成图像。着色函数会被调用多次,这有点破坏了管道的隐喻,除非你说着色阶段的结果是一个函数。我认为一等函数可能对高级描述(和图表)来说有点过于复杂,所以我把(x,y) → (α,r,g,b)函数称为图像(虽然是“虚拟”的)。我对这部分的写法并不完全满意,所以我可能会回去重写它,并且一定会考虑你的反馈意见。 - Laurence Gonsalves
显示剩余2条评论

12

这个问题在StackOverflow上很难回答。在开始回答之前,请注意,形状(例如drawRect())没有固有的颜色。颜色信息始终来自于Paint对象。

这段代码清除了一个椭圆形。在我意识到这一点之前,我的心理模型是绘制到画布(从概念上讲)会绘制到一个单独的“层”,然后该图层使用Paint的转移模式与Canvas的位图组合。如果情况真的如此简单,那么上面的代码将擦除整个位图(在剪切区域内),因为CLEAR始终将颜色(和alpha)设置为0,而不考虑源的alpha。因此,这表明还有另一种额外的遮罩作用于限制擦除范围为椭圆形。

你的模型有点不对。椭圆形并没有被绘制到单独的层(除非您调用了Canvas.saveLayer()),它直接绘制到Canvas的背景位图上。每个由原语绘制的像素都应用了Paint的转移模式。在这种情况下,只有椭圆形的光栅化结果会影响位图。没有特殊的遮罩操作,椭圆形本身就是遮罩。

无论如何,这里是管道的简化视图:

  1. 原语(矩形、椭圆形、路径等)
  2. PathEffect
  3. 光栅化
  4. MaskFilter
  5. 颜色/着色器/颜色过滤器
  6. Xfermode

(我刚看到你的更新,是的,你找到的描述了管道的阶段顺序。)

当使用图层(Canvas.saveLayer())时,管道会稍微复杂一些,因为它会加倍。首先通过管道渲染原语以在屏幕外的位图(即图层)内部,然后再通过管道将屏幕外的位图应用于Canvas。


1
然后,Android 3中的2D和3D管道之间存在关系。 - Ed Burnette
谢谢。我认为你漏掉了“对比”这个词。我的意思是drawBitmap有固有的颜色,并将其与“矢量”原语(如drawRect)进行对比,后者没有颜色。可以这样说,Paint的颜色(或设置了Shader的情况下)用于没有颜色的基元,但对于位图,颜色来自位图本身。在这三种情况下都应用ColorFilters吗(颜色、Shader和Bitmap)? - Laurence Gonsalves
@Ed 没关系,硬件加速的管道与软件管道相同,只是使用OpenGL进行光栅化/渲染,而不是使用软件。着色器、颜色滤镜等应用方式保持不变。 - Romain Guy
你所说的“生成像素集”是我所说的“掩码”。它们是同构的。我不确定你所说的“在光栅化器生成的每个像素的 alpha 通道上应用变换”的意思是什么。这是否意味着从光栅化器出来的像素集与从 MaskFilter 出来的像素集相同,但当它们从 MaskFilter 出来时,alpha 值不同,或者 MaskFilter 实际上可以改变生成的像素集? - Laurence Gonsalves
当我谈论掩码和光栅化器时,我并不是指Skia的C++类,也从未这么说过。你的困惑可能来自于这个假设。 - Romain Guy
显示剩余6条评论

0

SkPathEffect - 对几何图形(路径)进行修改,然后生成alpha遮罩(例如虚线)。 SkRasterizer - 组合自定义遮罩层(例如阴影)。 SkMaskFilter - 修改alpha遮罩以便着色和绘制之前(例如模糊)。 SkShader - 例如渐变(线性、放射状、扫描),位图模式(夹紧、重复、镜像)。 SkColorFilter - 在应用xfermode(例如色彩矩阵)之前修改源颜色。 SkXfermode - 例如porter-duff转换模式、混合模式。

http://imgur.com/0X5Yqod


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接