抗锯齿_【第1980期】CSS 幻术 抗锯齿

前言

奇怪的知识点可以Get了。今日早读文章由@仿生狮子授权分享。

@仿生狮子,平时喜欢捣鼓一些好玩儿东西的前端。现在正在深入学习前端各种知识,期待成为技术专家的那天早日到来。工作之余喜欢弹琵琶吉他,欢迎朋友们来B站找仿生狮子玩哦~

正文从这开始~~

传统网页的呈现是基于像素单位的,所以图片不能和 SVG 一样进行任意尺寸缩放后还保持边缘平整。也就是说,放大像素逻辑的图片,必然导致可视质量下降(信息失真)。所以我们往往会使用技术手段去规避失真,如:

  • 使用 SVG 替换位图

  • 使用矢量字体(如 TrueType 字体)替换位图字体

如果不得已,被迫进行像素操作,我们也有多种手段用来矫正失真:

  • 使用 CSS Image-Rendering 属性调整图像缩放时的采样算法

  • 使用 CSS Font-Smoothing 属性平滑字体渲染

  • 绘图时使用 Canvas 的抗锯齿 API

  • 将元素尺寸放大,然后再使用 Transform 将布局尺寸还原

  • 某些特殊情况下,可以使用浏览器硬件加速来平滑锯齿

  • 将图片模糊处理迫使用户开启钛合金脑放

这篇文章将会简单的提及以上几点,并介绍一种通过 CSS BackgroundImage 抗锯齿的全新思路(我称之为 Pixel-Offset Anti-Aliasing)。要提前说明的是,当下手机的屏幕分辨率已经相当高,同时处理器性能却十分薄弱,这直接导致我们没有在手机端浏览器讨论抗锯齿的必要。本文所述几乎都局限于桌面端的大显示器(我祈祷你不是在用 8K 分辨率的显示器看这篇博客)。

抗锯齿及相关技术
抗锯齿的形成

信息失真(Aliasing)和图像锯齿不是一码事儿,但是对于游戏玩家来说,几乎可以把两者划上等号。要使用 CSS 抗锯齿,我们不得不先提及锯齿的形成。

为什么会有锯齿?

我们的眼睛能对物体的形状进行感知,意识到到一条实际上并不存在的“线条”。见下图,我们能感受到线条,虽然看起来不太平整:

e63cb4e2fd5a62c5c1dbc5d09c81c0b3.png

下面这幅图中,带箭头的线代表我们感知的线段,其余线段相交的网格代表像素网格。从上图可以发现,只要是带箭头的线经过的地方,就会被黄颜色填充。不过理想中的线段是完美的,它完全平滑的。把不定方向的平滑线段,映射到像素排列的低 DPI 的屏幕上,就会出现信息丢失的情况。像素颗粒越大,信息丢失情况就越严重(以下就简称为锯齿)。

03076e97a2bf6e024527e33a467f00f5.png

怎么样看起来才没有锯齿?

这里我画了一张图,可以先仔细观察,然后再站在离显示器稍微远一些的地方眯起眼睛看:

ebeb68d8a964e456a6f2cda36a24a4f5.png

在像素周围,我用黄色涂鸦将丢失的信息稍加补充。图中黄色涂鸦的大小代表了像素透明度。这里有一张抗锯齿的成品图片,可以看处图形的边缘被填充了有透明度的像素:

51bfe9e134395d8c1b76bbc01a11d312.png

常见抗锯齿技术

在音频领域,我们可以通过高质量的播放器和无损音频减少传入耳朵的信息失真。但在游戏领域,普通玩家不可能在家里准备了8K显示器。伴随显示器分辨率从 720p 到 1080p 发展的,是几种同样跟随游戏业界发展而成长起来的抗锯齿技术。

SSAA(Super Sampling Anti-Aliasing)

超级采样抗锯齿,它会把当前画面渲染的分辨率成倍提高,比如 1024×768 的图形开启2倍 SSAA 后,显卡实际运算就变成了 2048×1536,这之后,再降采样,将多个像素融合,映射回显示器的单个像素。像素融合能使颜色过渡更自然,看起来没有明显的毛刺。不过,因为硬件的运算增加(指数级),可以想象它会消耗极高的性能。

MSAA(Multi Sampling Anti-Aliasing)

多重采样抗锯齿,它针对特定缓存区域的数据进行多重采样——可以简单理解为对多边形的边缘进行多重采样。性能消耗较高,但效果也不错。

FXAA(Fast Approximate Anti-Aliasing)

快速近似抗锯齿,它找到画面中所有图形的边缘并进行平滑处理。尽管很多图形边缘并不对应游戏实际建模的边缘(如材质和纹理),但 FXAA 性能消耗小,性价比高,不失为一种抗锯齿的常用选择。

DLSS(Deep Learning Super Sampling)

深度学习超级采样,它通过硬件加速的深度学习算法,根据几何、着色、时域多个方面的数据(说人话就是根据过往帧、形状、像素动量等数据)对实时渲染的低分辨率图像重建多倍超级采样结果。相对于传统渲染,不仅能极大提高画质,还能极大提高帧率。

CSS 抗锯齿技术

以下,我们提及几种常见的抗锯齿技术。

CSS Font-Smoothing

字体平滑属性属于早期的 CSS 规范,后来因为种种原因又被移除了。不过现在仍可以通过前缀属性兼容(如 -webkit-font-smoothing)。一般来说,字体平滑有三个值可选,none、subpixel-antialiased、antialiased。值的作用正如其名,分别是无抗锯齿,亚像素级抗锯齿和(全像素)抗锯齿。

一般来说,屏幕上的每一个像素点,都是由三原色条纹(可能如红、绿、蓝三个发光点)组合而成。亚像素级抗锯齿,意味着字体渲染时,将以亚像素(如红光)为单位。不发光的像素显示黑色,其余像素在抗锯齿处理时则会显示暗色,见下图:

0ef1e43d5aeba243b6659450997b67d7.png

全像素抗锯齿,则以整颗像素(包含红蓝绿三个条纹)为单位渲染字体。抗锯齿处理时,字体若超出了一个像素的单位,会以一颗与之相邻的透明暗色像素作平滑,见下图:

3b12659fa71670c011577bd2d90df338.png

“后浪”的“后”字,中间那一横,实际的宽度要小于一个像素,所以也用透明暗色渲染。除了单字,在 @MAXVOLTAR 这篇博客,有英文排版的示例图片,以下直接引用了:

none

153149d11a5e048c9a205007d41e7a1c.png

subpixel-antialiased

c5cc26673d7199b4defd2b60df0b90b4.png

antiliasing

6822d28e479f8893fa7f5869cf8fc61f.png

那三种值应该如何选择呢?

我的建议是,仅仅了解渲染机制和呈现方式就行。像素抗锯齿会使字体呈现稍细,而亚像素级抗锯齿则使字体呈现过粗。黑色背景下则反之。倒不必因为知道它就必须使用上——这三种方式有各自的优点和缺陷。一般来说,扔掉这个属性,让浏览器自行判断字体渲染的方式就可以了。如果你引入了特殊字体(比如印刷字体)进行平滑处理。(我相信中文的网页版面下,能自由发挥的范围应该很有限。)

附,感兴趣的话,文末我留了相关链接,可以再查阅。

CSS Image-Rendering

Image-Rendering 属性用于设置图像缩放算法,这个属性有几种常见的值。见下组件:

可以发现,Pixelated 值设置之后,浏览器不会对边缘进行平滑处理,而 Auto 则对整幅图像进行柔和处理。也就是说,使用 Transform Scale 放大图片,浏览器会应用默认的平滑缩放算法(可能是双线性插值之类的)。

那可不可以对图片先放大数倍,再缩小还原为实际尺寸呢?以下是试验结果:

6d96f7afc3588301c182ff19d1dda4f1.gif

不知道是浏览器对多个 Scale 串联进行了优化,还是使用了某种不损失图像信息的采样算法,总之不改变图片尺寸又想使用平滑图片是行不通的。

硬件加速抗锯齿

关于使用浏览器的硬件加速抗锯齿功能,是我在试验 PXAA 时的偶得(不过已经有博客介绍过了)。当元素通过 Transform:Rotate 旋转之后,如果此元素是被 GPU 渲染的,那么会应用浏览器对应 GPU 的抗锯齿属性——比方说你用 GTX 1060ti 运行浏览器,那么相关配置就能在英伟达控制面板中找到(不过这有相当程度是我的猜测,待验证)。听起来好像有点复杂,看下面例子就一目了然了:

09e77f3cacf6b31b870ddbf46d5e861b.gif

当元素旋转,并应用硬件加速(TranslateZ)之后,渲染出来的边缘会被平滑处理。但是如果仅仅启用硬件加速或是单使用旋转,不能达到效果。经过我的测试,在 Windows 端 Chrome 内核的浏览器,这种抗锯齿方式能得到一些体验——你甚至可以通过仅旋转 0.1° 来柔和边缘(虽然不明显)。CSS 相关的抗锯齿技术就到此为止,下一节开始是新的思路。

Pixel-Offset Anti-Aliasing

像素偏移抗锯齿(下简称 POAA),这是一种很神奇的方法,貌似网上还没人分享过,不过效果确实挺惊艳的。我不知道具体原理是什么,但是它就是有效(It works!)。这里有两副使用 BackgroundImage 属性绘制的图像,我先展示一下应用 POAA 后的结果吧:

效果展示

4e89819fa6cf5d82a0c9c075c697bc92.gif

原理

常见的游戏抗锯齿技术是建立在游戏渲染前或后,从模型到光照多个步骤产生的数据的基础上的,所以我们可以根据帧历史的内容、像素动量、提高采样等方法中进行筛选信息并重建画面。但浏览器给用户展现的内容,可以说就是渲染后的东西。我们能够参与浏览器内部渲染的方式貌似几乎没有(以后可能会有 CSS Houdini)。比方说使用 BackgroundImage 绘图展现在你的屏幕上的这些像素,你无法参与渲染改变它们,你也没有办法用预渲染数据告诉浏览器“你应该这样做”。不过好消息是,程序员都是坚信任何问题都能被解决的人,这里我们换种思路。

我想你应该记得开篇我们提及过 FXAA。FXAA 可以简单概括为边缘寻找->重建边缘这两个步骤(并不专业,也许还会有矫正之类的我不清楚)。在Implementing FXAA这篇博客中,解释了 FXAA 具体是如何运作的。对于一个已经被找到的图形边缘,经过 FXAA 处理后会变成这样,见下两幅图:

720b325e719e76143a9ee1672620bc79.png

480e7728f4f66a41377236d55d932c11.png

给 FXAA 输入源图像,就能通过颜色或对比度确认物体的边缘,并通过改变像素周围的点的透明度,让整体看起来得到平滑。仔细想想,使用 BackgroundImage 绘图时,其实我们已经知道边缘在哪儿了。边缘不藏在国王的帽子里,它就在我们写的代码中。比方,上一小节那个圆形渐变图形的源码是这样的:

841094d6ead03f09b48fd879667875ff.png

.circle-con {

$c1: #cd3f4f;

$c2: #e6a964;

position: relative;

height: 300px;

background-image: repeating-radial-gradient(

circle at 0% 50%,

$c1 0,

$c2 50px

);

}

我们可以轻易找到找到边缘——对,就是那些渐变的颜色改变的地方——0px(50px)。现在我们有了边缘信息,接着就要重建边缘。重建边缘也许可以再拆分,分为以下几个步骤:

  • 需要通过某种方法得到透明度的点

  • 这些点需要能够组成线段

  • 线段完全吻合我们的 BackgroundImage

  • 使线段覆盖在 BackgroundImage 的上一层以应用我们的修改

这就是大体思路,我们并没有参与浏览器的渲染,而是通过像 FXAA 一样的后处理的方法。在已渲染的图像上做文章。不过将上述步骤仔细考虑后,会发现问题的难点在于如何生成抗锯齿条纹。

ebeb68d8a964e456a6f2cda36a24a4f5.png

总之,我们需要继续改良思路。

在 BackgroundImage 中,像素是基本单位不能再分,点的透明度显然不能通过点的大小来模拟。这里有两种解决方法:

  • Opacity,使用 CSS Opacity ,或者 CSS RGBA 函数、SCSS 函数。

  • 两种颜色相融合模拟像素透明度,如果不想扯上 JS,SCSS 也能解决。

至于线段,也可以用 BackgroundImage 模拟,比如针对上面那段 CSS 代码,可以通过改写成以下方式:

.circle-con {

$c1: #cd3f4f;

$c2: #e6a964;

$line-width: 1px;

position: relative;

height: 300px;

background-image: repeating-radial-gradient(

circle at 0% 50%,

$c1 0,

transparent calc($line-width),

transparent calc(50px - $line-width),

$c2 50px

);

}

取得线段之后,将容器偏移几个单位像素,放到浏览器测试结果:

6041b3a2bfed3fbb86030f8b43602383.png

可以发现,会自然而然得到颜色混合透明度组成的线段。只不过透明度的方向并不是我们想要的。我希望能够得到透明线条反过来的图样:

551e38e00e74428b15520cc8fc544092.png

经过试验,我发现只需简单调换颜色的顺序就行。比方说这是在容器 50% 的位置绘制的一条线段:

e0bc026d5f1c344a2f92c262bf920825.png

.old {

background: linear-gradient(

var(--deg),

transparent,

transparent

calc(50% - var(--line-width)),

yellow 50%,

red 50%,

transparent calc(50% + var(--line-width)),

transparent

);

}

如果将线段颜色调换,就会变成:

10ecbe2703e77c667d0b0bf3f340f5f8.png

.new {

background: linear-gradient(

var(--deg),

transparent,

transparent

calc(50% - var(--line-width)),

red 50%,

yellow 50%,

transparent calc(50% + var(--line-width)),

transparent

);

}

得到了我们想要的线段虚化的效果!这之后要做的事儿是吻合线条。

接下来是见证奇迹的时刻:

92441a927bfe5725f93c9ab22bda8640.gif

Well done!

来一张成品 GIF,稍微离屏幕远一些看效果最好:

75e289d95b620976e1625ad8b59a7ed9.gif

成品在吻合线条的基础上还增加了一些内容及调整了相关参数:

  • 暗色和亮色混合的透明度的值不同

  • X轴和Y轴的偏移不同

  • 调整了拟合线段的粗细

成品的代码如下:

.repeat-con {

--c1: #cd3f4f;

--c2: #e6a964;

--c3: #5996cc;

position: relative;

height: 300px;

background-image: repeating-linear-gradient(

var(--deg),

var(--c1),

var(--c1) 10px,

var(--c2) 10px,

var(--c2) 40px,

var(--c1) 40px,

var(--c1) 50px,

var(--c3) 50px,

var(--c3) 80px

);

&.antialiasing {

&:after {

--offsetX: 0.4px;

--offsetY: -0.1px;

--dark-alpha: 0.3;

--light-alpha: 0.6;

--line-width: 0.6px;

content: '';

position: absolute;

top: var(--offsetY);

left: var(--offsetX);

width: 100%;

height: 100%;

opacity: 0.5;

background-image: repeating-linear-gradient(

var(--deg),

var(--c3),

transparent calc(0px + var(--line-width)),

transparent calc(10px - var(--line-width)),

var(--c2) 10px,

var(--c1) 10px,

transparent calc(10px + var(--line-width)),

transparent calc(40px - var(--line-width)),

var(--c1) 40px,

var(--c2) 40px,

transparent calc(40px + var(--line-width)),

transparent calc(50px - var(--line-width)),

var(--c3) 50px,

var(--c1) 50px,

transparent calc(50px + var(--line-width)),

transparent calc(80px - var(--line-width)),

var(--c1) 80px

);

}

}

}

理论上,通过 SCSS 函数,能自动判断代码中线段的位置并生成填充抗锯齿的像素。无论是 LinearGradient、ConicGradient 还是 RadialGradient,都可以抗锯齿。不过我只是当试验品来写,所以没有写相应的工具函数。欢迎各位补充到 Github。

关于本文 作者:@仿生狮子 原文:http://www.lionad.art/articles/CSSAA.html

16ff26c0bc4d182a49afb9fd503f991d.png

为你推荐

【第1923期】如何用CSS绘制饼图

【第1854期】CSS 故障艺术

欢迎自荐投稿,前端早读课等你来

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值