一、走样

1.1 走样原因

说到走样,首先要说的就是采样。这也算是很多图形学专著中提到反走样相关技术时的一个惯例。许多专著中都会提到奈奎斯特采样定理。一个直观理解:傅里叶变换告诉我们,任何一个函数都可以通过不同周期的正余弦函数线性组合而成,一个函数经过傅里叶变换后的函数的定义域表示分解后的正余弦函数的频率范围,值则表示了这个线性组合的系数(当然对于连续的傅里叶变换而言线性组合系数这个解释不准确)。总之,如果一个函数它的傅里叶变换函数只在一个有限的区间内值不为0,那就可以称之为有限带宽函数。假设它的最大频率不超过B,那么我们通过2B的采样频率,就可以无损地恢复这个原始函数。因为最大频率的波可以通过一个周期内的两个点唯一确定。

所谓渲染,实际上就是对一个连续函数在空间内进行离散的采样(这个函数应该包含的场景的几何覆盖关系,着色参数和着色方程等)。而这个函数不是有限带宽函数,这意味着我们不论以多大的采样频率(反映在图形上,就是图像分辨率)去采样这个函数,都不可能完美地恢复原始信号,也就都会造成走样(aliasing),反应在图像上就是锯齿或者噪点。总结来说,就是在图形渲染中,走样是不可避免的,我们能够做的,仅仅是利用各种技术去减轻这种现象。

1.2 走样类型

主要分为三种类型(可参考APPLYING SAMPLING THEORY TO REAL-TIME GRAPHICS):

  • 第一种是几何走样:它是由于对几何图形的可见性函数采样不足导致。对几何覆盖函数的采样不足,也就是我们最常看到的边缘锯齿或者更学术化地称之为几何走样(Geometry Aliasing),一般发生在光栅化阶段。
  • 第二种是着色走样:它是由于着色阶段对连续的着色函数采样不足导致的。对渲染方程的采样不足,因为渲染方程也是一个连续函数,对某些部分(比如法线,高光等)在空间变化较快(高频部分)采样不足也会造成走样,反映在视觉上一般是图像闪烁或者噪点,这类称之为着色走样(Shading Aliasing),一般发生在着色阶段。
  • 第三种是时间走样:它由于渲染帧率的限制使其对运动过程的采样不足导致的走样。比如车轮效应。可以采用动态模糊解决。

二、几何反走样算法

2.1 基于超采样的方法

2.1.1 SSAA(Supersampling Anti-Aliasing)

SSAA可以说是图形学中最简单粗暴的反走样方法,但同时也最有效,它唯一也是致命的缺点是性能太差。开篇已经说过,任何类型的走样归根结底都是因为欠采样,那么我们只需要增加采样数,就可以减轻走样现象。这就是SSAA,所以SSAA简单的来说可以分三步:

  1. 在一个像素内取若干个子采样点;
  2. 对子像素点进行颜色计算(采样);
  3. 根据子像素的颜色和位置,利用一个称之为resolve的合成阶段,计算当前像素的最终颜色输出.

基本的SSAA框架如下:

不同SSAA方式在子采样位置的选取和最终resolve使用的滤波器上有所不同,可以使用不同的采样模板(规则采样,旋转采样,随机采样,抖动采样等)或者不同的滤波函数(方波滤波器或者高斯滤波器)。

SSAA同时是几何反走样和着色反走样方法,因为它不但增加了当前几何覆盖函数(Coverage)的采样率,也对渲染方程进行了更高频率的采样(单独计算每个子像素的颜色)。

2.1.2 MSAA(Multisample Anti-Aliasing)

SSAA中每个像素需要多次计算着色,这对实时渲染的开销是巨大的(想想4K和1080P的性能差异),我们开始也说过,实际上人眼对几何走样更敏感,能否解耦几何覆盖函数的采样率和着色方程的采样率呢?答案是肯定的。MSAA的原理很简单,它仍然把一个像素划分为若干个子采样点,但是相较于SSAA,每个子采样点的颜色值完全依赖于对应像素的颜色值进行简单的复制(该子采样点位于当前像素光栅化结果的覆盖范围内),不进行单独计算。此外它的做法和SSAA相同,每个子像素会在光栅化阶段分别计算自身的Z值和模板值,有完整的Z-Test和Stencil-Test并单独保存在Z-Buffer和Stencil-Buffer里,就是我们需要的几何覆盖信息。类似于SSAA,MSAA也需要一个resolve的过程,在早期DX9这个过程是显卡的一个固有单元在执行,执行的方式一般也就是简单的Box Filter,随着可编程管线的功能逐渐强大,现在可以通过Pixel Shader来访问相应的MSAA Texture,并且定制resolve的算法(具体可参照:EXPERIMENTING WITH RECONSTRUCTION FILTERS FOR MSAA RESOLVE)。由于MSAA拥有硬件支持,相对开销比较小,又能很好地解决几何走样问题,在游戏中应用非常广泛(我们在游戏画质选项中常看到的4x/8x/16x抗锯齿一般说的就是MSAA的子采样点数量分别为4/8/16个)。

2.1.3 CSAA(Coverage Sampling Anti-Aliasing)

MSAA相对于SSAA的好处是不用多次计算着色,但对每个子采样点仍然需要单独存储其Z值和stencil值,并且实际上每个子采样点还需要单独存储颜色(只是该颜色不是通过单独计算得来),这仍然会造成非常巨大的存储及读写开销。能否把子像素的Z/Stencil/Color值和Coverage的值进一步解耦开呢?这就是CSAA[5]的思路:在MSAA已有的子像素的存储结构基础上,给每个像素再增加一个Coverage Info,也就是在光栅化阶段进一步提高每个像素的几何覆盖函数的采样率,这个采样结果用一个N比特的数表示(每一个比特表示一个子采样点的覆盖信息),可以这么理解:MSAA是一个像素有M个子像素,每个子像素有一份Z/Color/Stencil,而CSAA则是说,每个像素有M个子像素(每个子像素有一份Z/Color/Stencil),还有一个额外的N Bit的Coverage信息。 乍一看似乎CSAA的开销比MSAA更大,但实际上,CSAA可以使用少量的子像素加上更大的Coverage采样率,来实现MSAA需要更多子像素才能达到的同等效果。例如可以通过16x CSAA(4个子像素,每个像素16Bit的Coverage)达到8x或者16x的MSAA的效果。当然缺点是CSAA的resolve过程不可控。但是可以通过在Shader里输出一个Custom Coverage的结果,然后将光栅器算出的Geometry Coverage和Custom Coverage通过位与(AND)操作合成为最终用于resolve的Coverage。


注:CSAA是NVIDIA的叫法,AMD应该管这个技术叫EQAA(Enhanced Quality Anti-Aliasing)。

2.2 基于形态学的方法

前面提到的若干种基于超采样的反走样方法有一个基本特性:需要特定的硬件支持(当然SSAA除外),同时它的存储和性能开销也相当大,尤其是对一些性能瓶颈是带宽的渲染架构(没错,说的就是延迟渲染)来说负担更明显甚至无法支持(当然MSAA可以应用于延迟渲染的架构,感兴趣可见这里Deferred MSAA、Antialiased Deferred Rendering、CryEngine3 GRAPHICS GEMS这三篇文章,这里不再展开)。现如今不支持延迟渲染你都不好意思说自己自己的引擎是次时代的,自然反走样就需要更适合延迟渲染的方法。我们知道,延迟渲染框架带来的一大便利是丰富的全屏后处理效果。那么如果能在全屏后处理框架下完成反走样无疑是最快最合适的方案。

形态学反走样属于Screen Space AA的一类,它的基本思路是:假设同一物体在某些信息上存在连续性,那么可以通过检测像素在这些信息(颜色,深度,法线)上的不连续找出一些边缘,同时这些边缘根据局部形状不同会形成一些形态模式(pattern),我们通过总结出一些固有模式,然后通过这些模式反推(拟合)出采样前几何边界的解析形式(直线方程),最后通过这些方程再来计算每个像素的覆盖率,利用覆盖率的结果重新混合原始颜色(也就是resolve过程),最终达到反走样的目的。

2.2.1 MLAA(Morphological Antialiasing)→SMAA(Subpixel Morphological Antialiasing)

MLAA和SMAA在算法思路上并无区别,只是MLAA算法最初提出来是基于CPU的算法,而SMAA则结合GPU的特点进行了工程上的优化。 实际上在Siggraph2011中有一个非常棒的course[Filtering Approaches for Real-Time Anti-Aliasing]专门阐述了SMAA算法,从原理到工程细节一清二楚,非常推荐仔细阅读,这里有关SMAA的算法概述基本也来自于这个course。

简单来说SMAA(以及MLAA)包含三个步骤:

  1. 找到图像中不连续的像素(即边缘像素);
  2. 从每个不连续像素出发,找到经过它的直线的两个端点,记录端点的距离及整个线条的形状(这个形状模式最多有16种),并估算当前像素被这条线段切分后的两个部分的面积;
  3. 每个像素最多被四个不同方向的线条切分,则该像素最多有四个面积权重,根据该权重,取周围像素和当前像素进行颜色混合。


在第一个步骤里,我们确定哪些像素可能是边缘,这些区域会被认为是“可能出现走样的像素集合”。这和我们一般意义上的边缘检测区别不大,关键在于你如何定义“不连续”,可选的依据包括颜色(亮度),深度,法线,PrimitiveID等信息,颜色作为连续性的参考依据的优点是易于获得(谁还没个色彩信息),同时能一定程度上实现着色反走样(因为着色走样反映出来就是色彩信息不连续)。但它也有可能造成不必要的模糊。而深度,法线,PrimitiveID这些信息在前向渲染的框架内往往不易获取,但对延迟渲染来说天生就有(G-Buffer),使用它们能够较为准确地找到可能出现几何走样的像素集,缺点当然是边缘检测要更慢一些。此外,相较于由于CPU的版本可以一次完成上述三个步骤,而GPU必须分步执行,因此在第一步执行完毕后,我们可以使用Stencil Buffer把非边缘像素mask掉来进一步提高之后的算法效率。

第二个步骤需要从当前像素出发,向两端查找对应线段的端点,这个搜索过程单步来说很简单:即沿某个方向检查前一个像素和后一个像素是否都不为0(假设0为非边缘,1为边缘)。但是对于GPU来说,为了找到线段终点,每个像素需要执行大量的贴图读取操作。SMAA利用GPU固有的双线性插值特性来将两次贴图读取合并成一次,读取位置位于两个像素的中点,这样,我们只需要判断经过双线性插值得到的值是否为1即可,为1表示当前位置不是线段终点,否则即是。

找到终点实际上只是找到了线段的长度,线段是什么形态的还需要确定端点的位置,SMAA进一步用双线性插值加上一个小偏移量的做法,使得shader能够用一次贴图采样即判断出一个端点的位置。

此外,由于线段根据端点位置的不同可以有16中不同的形态,如果在shader里一一判断并计算,将造成大量的分支开销(有关图形渲染开销的话题或许会在我今后的专栏文章里详细解释)。为了防止这种情况,SMAA把线段形态的确定以及进一步的面积覆盖率计算全部预计算到了一张贴图上,然后根据找到的端点位置和长度作为索引去查找这张4D贴图。

2.2.2 FXAA(Fast Approximate Anti-Aliasing)

FXAA和也是一种形态学反走样方法,但相比于SMAA,它进一步简化了整个算法步骤,将我们描述的三个步骤整合在了一个后处理的pass里,当然它的基本算法也遵循以上步骤。实际算法的说明里拆解出了更多的步骤,这里还是对照SMAA来简单解释FXAA的步骤:

  1. 边缘像素集筛选,为了更好的通用性,FXAA使用sRGB空间的颜色作为输入,并根据局部的亮度对比度来确定一个像素是否是边缘像素。这也就是表示FXAA一般应该发生在Tone Mapping之后,或者也可以把Tone Mapping和FXAA整合成一个pass(下图中:红色像素表示找到的边界像素,黄色表示水平的边界,蓝色的表示垂直的边界)。

  2. 线段的搜索,不同于SMAA,FXAA只查找一个方向的线段,这个线段我们认为是主方向,它要么是横向要么是纵向。在通过局部差分得到主方向后,FXAA沿着主方向搜索两个端点到该点的距离(不同于SMAA,FXAA不需要确定两个端点的位置,只确定距离即可),假设是横向,则找到 Dr 和Dl 。

  3. 最后,FXAA根据 Dr 和Dl 确定当前当前像素在找到的直线上的Coverage,进一步得出垂直于主方向的另一个方向上的偏移量,基于该偏移量用双线性插值重采样,得到的最终颜色就相当于根据Coverage进行了线性混合后的最终结果。



从当前像素的两个方向查找长线段的端点

根据两端线段及总长度的比值,得出当前像素的中心沿垂直方向的偏移量。

更详细的算法说明可以看NV的文档[FXAA WhitePaper],另外这篇文章[Implementing FXAA]对FXAA算法也解释的很清晰。FXAA主要的优势体现在易于整合进现有架构,并且性能开销极低,但比起之前的算法,效果当然也要差一些。

2.3 基于时间的方法

近年来游戏引擎中最常用的反走样方法是基于时间的反走样方法,它的假设是:整个场景很少发生大幅度的镜头/物体运动,帧与帧之间具有比较明显的连续性,上一帧某个物体的微小表面在下几帧中仍会出现(只是位置发生了较小移动)。在文章开始时我们曾说过,走样是因为采样不足,前面介绍的方法是把采样点散布在二维空间里,这些可以统称为空间反走样方法(Spatial Anti-Aliasing),而基于时间的反走样则是把采样点散布在帧序列(时间)里,这样单帧渲染的压力就明显减小。理论上基于时间的反走样在场景运动不大的情况下效果和性能都显著好于上述各类方案。

2.3.1 TAA(Temporal Anti-Aliasing)

严格来说TAA并不能算一个具体的算法,而是更像一个统一的算法框架。和SSAA一样,TAA也能够同时减轻几何走样和着色走样的问题。它的基本框架大概是这样(具体文章可参考:):

总体来说TAA也分为**采样(sampling)和合成(resolve)**两个过程,不同的TAA的具体实现也是围绕这两个部分有所变化。

2.3.1.1 采样(sampling)

采样的部分主要涉及两个方面,一个是样本的位置分布,另一个是历史样本的获取。

因为每个像素需要的样本被分摊在了时间轴上,因此实际上每帧我们都只需渲染一个新的样本,然后将它和其他历史样本混合即可。考虑这样的情况:当整个场景完全不动的时候,每次我们获取的子样本位置都一样,那不论经过多少次混合,最终混合后的像素仍然是走样的,为此,我们需要在光栅化G-Buffer的阶段,在Projection Matrix之后再加上一个Jittered Matrix。这个Jitttered Matrix会根据一个样本分布的pattern对当前采样位置进行一个微小的偏移,保证每帧样本分布的位置都略微有所不同。这样经过混合即可产生反走样的效果。通常来说规则的采样点pattern效果不会太好,UE4使用的是Halton Sequence[High Quality Temporal Supersampling][Halton sequence - Wikipedia]。关于抖动采样的一个具体实现,这里有一篇介绍[Temporal Anti-Aliasing - Mali GPU and Vulkan]。

为了获取历史样本,我们需要一个称为reproject的过程,这个过程从原理上是比较简单的:首先根据当前像素的uv和depth以及当前相机的View Projection Matrix去反推出当像素的世界坐标,然后根据上一帧的View Projection Matrix计算出当前像素点在上一帧图像上的uv作为采样坐标。但只有这些还不够,原因是这里我们只考虑了相机的移动。如果物体本身也发生了移动呢?这里就需要另一个G-Buffer的辅助信息:Motion Vector Buffer。这里Motion Vector Buffer的不做描述,它是一般是一张RG16F的贴图,通常用于提供运动模糊计算需要的信息。综上所述,对于静态物体,我们使用反投影的方法找到它的历史像素采样坐标,对于动态物体,我们使用反投影结合Motion Vector Buffer来获取历史像素坐标。

在实际的计算中,我们往往不会使用当前像素点位置对应的Motion Vector的值,而是在取该位置的领域中运动最剧烈的向量作为实际的Motion Vector(比如3×3的领域里的最大Motion Vector)。这样做的原因是,如果只考虑当前像素自身的运动,那么在运动物体(前景)和不运动物体(背景)交界处的背景像素就会因为没有运动而无法产生较好的反走样效果。

Motion Vector的选取,注意电线杆(前景)和天空(背景)的边缘处发生的变化

2.3.1.2 合成(resolve)

合成部分相对来说比较简单,主要解决的问题是样本的合成方法以及历史样本的合法性验证。

最直观的样本合成方法是把所有历史像素和当前像素进行加权平均。

这个方法显著的缺点是需要大量的存储空间(每个历史像素单独存储),并且难于跟踪验证每一个历史样本的有效性。所以一般用我们称之为Exponential Move Average(EMA)的方法,名字看起来很唬人,但是看一眼公式你就会发现非常熟悉:

实际上如果你实现过基于Ping-Pong Buffer的Motion Blur就会发现,两者用的公式是一样,这里α一般设置成0.1。这个合成方法的优点是,不论我们有所少个采样样本,我们都只存一个像素的历史颜色。

之前我们说过,TAA假设场景里某一个微面元在连续的帧里都能找到对应的像素样本,但是有时候这样的假设是错误的,比如某些时候因为镜头或者物体的运动导致一些原本可见的像素变得不可见(或者相反),又或者场景某些物体的光照情况发生了剧烈的变化。这些都会导致我们在时间轴上累计的历史样本失效。如果将这些失效的像素混合进当前颜色里,就会产生所谓的鬼影(Ghosting)。

验证像素有效性的方法都基于一个假设:当前像素样本附近的颜色和它的颜色接近,并且它们的取值范围形成一个凸包,我们认为位于这个凸包内的历史样本的色彩取值都是有效的,可以采用,而这个凸包外的色彩取值则无效,需要通过进一步的处理才能够采用。

首先是凸包的构造,尽管我们能够在某个色彩空间内(比如RGB空间)根据像素领域内的每个像素颜色精确构造一个凸包并判断某个点是否位于凸包内,但这样做计算成本很高,所以一般来说我们使用每个分量的最大/最小值构造出一个AABB,利用这个AABB去做历史样本的验证。一般来说,基于YCoCg色彩空间的AABB的验证效果会优于RGB空间(毕竟亮度的变化是比较敏感的,所以YCoCg构造出的更像是RGB空间里的一个OBB包围盒,它更能精确反应当前区域的色彩分布)[14]。

另一种凸包的构造方案是基于当前区域的统计信息,我们称之为Variance Clipping。顾名思义,它使用当前像素领域内的所有像素颜色的一阶矩和二阶矩作为AABB的center和extent。

当历史像素不在合法范围内时,有两种处理方法,分别是clamp和clip,clamp就是直接逐分量地把每个颜色值截取到合法范围内,clip则更复杂一些,它将历史样本和当前AABB的中心值连线,并将连线和AABB的交点作为修正后的值来使用。

YCoCg空间下的clamp和clip的区别

可以看出来,TAA从原理以及算法上并不复杂,但由于它将样本分布在时间上这样一个特点,所以它的实现贯穿了整个引擎的渲染流水线(比如样本生成是在G-Buffer的绘制阶段和Lighting阶段,resolve一般发生在后处理阶段。相比之下全屏范走样的方案则往往只发生在后处理阶段)。所以它在引擎上实现的工程难度较大,需要针对具体引擎进行较为深度的架构改造。此外,理想的情况是TAA结合MSAA一起使用,当然这样会造成更大的开销,因此很少真的有引擎这样做。

实际上,这种将样本从空间分布到时间上的策略,对于一些需要随机采样点的图形学算法(比如SSAO和SSR)来说,也能起到很好的效果(大幅增加了可用的采样点)。HPG2017上的基于1SPP的光线追踪反走样方法[Spatiotemporal Variance-Guided Filter, 向实时光线追踪迈进]也利用了Temporal AA作为基本的思路。

图形_反走样技术总结相关推荐

  1. 图形学(7)反走样技术

    本模块内容绝大部分是在慕课上看中国农业大学网客时的笔记,因此算作转载,在此鸣谢赵明.李振波两位老师,感谢他们录制该门课程供大家学习! 其实,在之前绘制直线算法中,画出来的直线经放大会有明显的" ...

  2. 计算机图形学在卫星的应用,计算机图形学课程设计教程-反走样卫星

    计算机图形学课程设计教程-反走样卫星 实 验 报 告 2016 年 4 月 28 日 第 3 节 综合 楼 426 号室 课程名称 计算机图形学课程设计 学生姓名 学号 专业与年级 2013级数字媒体 ...

  3. 图形学-反走样/抗锯齿

    1.反走样 1.1 什么是走样 在上一篇文章中,我们通过采样的方式把一个三角形变成离散的点显示在屏幕上.在采样过程中,我们会产生很多锯齿,这些锯齿的学名就叫做走样 1.2 反走样 如何消除锯齿(走样) ...

  4. QT QPainter::antialiasing QPainter::textAntialiasing 反走样、抗锯齿探究

    QT中使用QPainter 进行自行绘图的时候,为了防止"锯齿"的出现,我们会经常使用抗锯齿属性,也叫反走样, 既: QPainter::Antialiasing //绘图抗锯齿 ...

  5. 计算机图形学-走样与反走样

    本专栏内容整理了GAMES101的计算机图形学课程的主要内容,作为我学习计算机图形学的一份复习备份或叫做笔记.内容中如有错误,或有其他建议,欢迎大家指出. 附上GAMES101计算机图形学课程:GAM ...

  6. opengl 反走样 混合 多重采样 blend multisample

    1. 反走样         在光栅图形显示器上绘制非水平且非垂直的直线或多边形边界时,或多或少会呈现锯齿状或台阶状外观.这是因为直线.多边形.色彩边界等是连续的,而光栅则是由离散的点组成,在光栅显示 ...

  7. 【转】OpenGL反走样

    反走样:        在光栅图形显示器上绘制非水平且非垂直的直线或多边形边界时,或多或少会呈现锯齿状或台阶状外观.这是因为直线.多边形.色彩边界等是连续的,而光栅则是由离散的点组成,在光栅显示设备上 ...

  8. 反走样和OpenGL多重采样

    1. 反走样 在计算机图形学中,在屏幕上显示对象时,可能会出现许多的"锯齿",这些锯齿是由顶点数据像素化之后成为片段的方式所引起的,由于将数学意义上的坐标转换到物理的显示器硬件上进 ...

  9. 计算机图形学【GAMES-101】2、光栅化(反走样、傅里叶变换、卷积)

    快速跳转: 1.矩阵变换原理Transform(旋转.位移.缩放.正交投影.透视投影) 2.光栅化(反走样.傅里叶变换.卷积) 3.着色计算(深度缓存.着色模型.着色频率) 4.纹理映射(重心坐标插值 ...

  10. 计算机图形学有很多应用,计算机图形学的论文_计算机图形学有很多应用_计算机科学与技术的论文...

    本人数学系,想考计算机图形学的研究生,请问我毕业论文改选下面哪项(无... 图形学里用到的数学知识主要是微积分(必备基础),线性代数(模型变换的基础),最优化理论与方法(写论文做研究的基础),微分几何 ...

最新文章

  1. 【效率】如何有效提问
  2. 网页设计精粹:网页中那些迷人的按钮设计
  3. concurrenthashmap 1.7/1.8
  4. Mysql 的 Explain性能分析
  5. QT之Win10安装(五)
  6. php无法写入json,php json解析不出来怎么办
  7. c语言 dirent,DIR和dirent结构体
  8. 全国高校计算机能力挑战赛Java试题(一)
  9. C# 读取xls格式的文件
  10. 微型计算机主机的主要部件,微型机主机的主要部件
  11. 【新学期新FLAG】一名计科新生の大一学习计划
  12. 如何投稿iMeta期刊?ScholarOne投审稿系统作者使用教程
  13. puppeteer 生成pdf,绝对解决你的需求
  14. 大学计算机基础知识判断题,大学计算机基础知识考试试题及答案
  15. 急刹车或停车时应该先踩离合还是先踩刹车?
  16. 唯美伤感个性日志推荐:有一种美因距离而产生
  17. mysql验证索引正确性_mysql索引测试
  18. Vue 组件封装之 Content 列表(处理多行输入框 textarea)
  19. 陀螺年度好文回顾|Staking 时代两大流派,屌丝和贵族谁将胜出?
  20. input 禁止手机唤起软键盘,并且光标存在

热门文章

  1. 数据挖掘 任务一:预测贷款是否逾期
  2. GHOST XP SP2 遐想网络 专用加强版
  3. openwrt修改默认网关地址_修改宇视摄像机IP地址的方法
  4. 谈谈写程序与学英语 --宋劲杉
  5. Linux基础命令(2)
  6. Git初学--创建版本库
  7. 一个IT技术人员的回忆“痛并快乐着”
  8. OkHttp缓存与连接
  9. 李诞是怎么把吐槽变成一门生意的?
  10. A - Faulty Odometer