Home Forums GAMES在线课程(现代计算机图形学入门)讨论区 【总结】MSAA中黑线问题的出现原因以及解决方案

Tagged: 

Viewing 8 reply threads
  • Author
    Posts
    • #3512 Score: 1
      戴皓天
      Participant
      8 pts

      具体看下面的回帖吧,希望能帮到各位
      因为是我在大佬的指点下自己摸索的,所以可能会有错误和疏漏,希望各位理解指正
      ps:其实昨天就发了个整合帖子了,但是一直说在审核中,不知道咋了所以今天再发一个,因为怕再卡审核所以就分批发到回帖里

      This post has received 1 vote up.
    • #3519 Score: 2
      戴皓天
      Participant
      8 pts

      因为我在这个问题上跌了很多跟头,浪费了太多太多时间,所以在这里整理一下我自己的错误以及分析一下对问题的理解,希望能够帮到还不理解的同学

      先看下错误示例:
      见附件第一张图
      (如果图挂了,可以按顺序查看附件的图片)

      产生原因
      “黑线”其实并不是完全的“黑色”,我们先来看几个图
      见附件第二、三、四张图

      通过RGB值的测量,我们可以发现,所谓的“黑线”的黑色,只不过是颜色对比的产物而已
      实际上,根据第三张图,经过RGB值的测量,其实A点,也就是绿色三角形的边缘颜色其实和“黑线”颜色一样
      那么问题就显而易见了:
      是因为在边缘处只渲染了绿色三角形,而没渲染蓝色三角形
      那么产生的原因实际上是深度检测的精度不够,
      假设我们渲染是从近到远渲染,且绿色三角形近,蓝色三角形远
      则此时是先渲染绿色三角形再渲染蓝色三角形
      因为在使用MSAA但并没有维护每一个子采样点的深度时,深度检测是以像素为单位的,而不是以子采样点为单位,所以此时即使在两三角形相交处那些来自蓝色三角形的像素的子采样点是处在三角形内部,那么由于深度检测的过滤,也会因为相交处蓝色三角形的像素深度小于相交处已经渲染完成的绿色三角形的像素而被放弃渲染,留下的就只是绿色三角形原本就渲染完成的边缘,也就是绿色和黑色的“混合”
      所以看起来会是有一条“黑线”

      This post has received 2 votes up.
      Attachments:
      You must be logged in to view attached files.
    • #3524 Score: 2
      戴皓天
      Participant
      8 pts

      解决方案
      一开始我没搞明白所谓的维护每个子采样点的深度是什么意思,后来经过大佬们的指点,我稍微理解一点
      具体做法:

      第一步:扩大深度缓存
      因为是2X2的MSAA,所以子采样点的个数是像素个数X4,为了能存下子采样点的深度信息,深度缓存就要扩大4倍

      rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
      {
          frame_buf.resize(w * h);
          w=w*4+100;//***这里改动***
          depth_buf.resize(w * h );
      }

      +100是为了防爆

      第二步:对每个子采样点进行三角形内部检测
      这里没什么好说的,就是给insideTriangle函数传一个大小为4的数组,分别对应每个像素的4个子采样点,分别对4个子采样点进行向量叉乘判断是否在三角形内部,然后对数组写入对应的值并返回即可

      第三步:对每个子采样点进行深度检测和深度写入
      我在这里浪费了很多时间,因为一开始我只做了对每个子采样点进行深度检测,并没有对每个子采样点进行深度写入

      for(对每个子采样点)
      {
          if(如果当前子采样点在三角形内部)
          {
              if(深度缓存中当前子采样点位置的深度 < 当前三角形的深度)
              {
                  深度缓存中当前子采样点位置的深度 = 当前三角形的深度;
              }
          }
      }

      *这里要注意的是深度检测之前要先判断这个子采样点是不是在三角形内部,如果不做这个判断就会出错。因为边缘处的子采样点,只有在三角形内部才有被深度检测的意义

      至此,问题就大致解决了,
      我们来看下最终效果
      见附件图1

      或许不同的情况会有不同的渲染顺序,结果可能不一样,但问题思路应该是差不多的
      这个问题浪费了三天时间,但好在最后总算有了个尾声
      希望能够帮到各位,谢谢

      This post has received 2 votes up.
      • This reply was modified 2 years, 3 months ago by 戴皓天.
      Attachments:
      You must be logged in to view attached files.
      • #3571 Score: 0
        戴皓天
        Participant
        8 pts

        经大佬提醒发现这里有误,需要再设置一个4倍大的color buffer用于暂存每个子采样点的颜色信息,这样可以彻底消除黑线,具体见下面更新

    • #3529 Score: 0
      Angus
      Participant
      23 pts

      感谢总结。还想问一下同学你的颜色设定方法,以及最后的边缘是(蓝+绿)混合还是(蓝+绿+黑)混合?

      • #3531 Score: 0
        Angus
        Participant
        23 pts

        我这句话的意思是,因为在这次实验中只有两个三角形,且背景色为黑色(0 ,0, 0),所以在对两个三角形边缘进行颜色混合时,混入首先渲染的三角形边缘的(0 ,0, 0)并不会对结果造成影响。可以试试如果背景色为红色(1, 0, 0),边缘会不会混入背景的红色,来验证一下自己算法的正确性。

        • #3552 Score: 0
          戴皓天
          Participant
          8 pts

          你这么一说我发现了我之前颜色写入的一个bug,我之前是直接frame_buffer+=color/通过深度和三角形测试的子采样点个数所以效果才会这么好,边界处显得很白,看起来像是(蓝+绿),实际上并不是,只不过是巧合而已
          如果把两个三角形深度对换一下就能发现问题了

          实际上应该是frame_buffer = frame_buffer/(4-通过深度和三角形测试的子采样点个数)+color/通过深度和三角形测试的子采样点个数这样才对

          不过这样的话解决了渲染顺序的问题,不论是哪个三角形在前都能有效果,但是边界处效果就不是那么好了,比如我先渲染近的绿三角形再渲染远的蓝三角形,最后边界处的颜色是(蓝+绿+黑)
          这样看起来还是有“黑线”,但是不是那么黑,只是通过对比看起来感觉有一点偏黑,不知道该怎么解决了,但是我这样是基本按照老师的意思做了。。。有点头疼,这种淡淡的黑线应该是没法解决的吧

          ps:因为先写进颜色缓存的是近的三角形,在边界处就是(绿+黑),然后再渲染蓝色,那肯定就是(绿+黑+蓝)了,根据生活经验,交界处应该是(绿+蓝)才对,不应该混入黑色,我是不知道该怎么解决了。这个现象跟渲染顺序有关,我这里的话如果先渲染远的再渲染近的就不会有这个混合的问题,当然这个也可以用我这里的说法解释,因为先渲染远的再渲染近的的话,交界处本来就没有黑色混入,也就是渲染远处时本来就就是(蓝色),(蓝+黑)的部分都被覆盖掉了,所以不会有淡淡的“黑线”

          • This reply was modified 2 years, 3 months ago by 戴皓天.
          • #3559 Score: 0
            Angus
            Participant
            23 pts

            其实按照老师第八节课中(35:14处)的说法,除了要为每个子样本维护一个depth buffer外,也要维护一个color buffer,这样就不会有黑线。背景色换为红色后,也不会有红线。

            不为子样本维护color buffer的话,无论如何都会把前一个渲染的三角形的边缘颜色引入。

            这里只是因为场景非常简单,只有两个三角形,没有把问题凸显出来,且如果幸运的话,渲染顺序还会隐藏这个问题。

            • This reply was modified 2 years, 3 months ago by Angus.
            • #3563 Score: 0
              戴皓天
              Participant
              8 pts

              我想了很久,之前就看到有同学也用了四倍的color buffer,但我一直觉得四倍color buffer跟我这样做本质上是一样,现在再想了想,四倍color buffer确实能够避免黑色的混入,谢谢提醒!我马上去改一下

    • #3575 Score: 2
      戴皓天
      Participant
      8 pts

      2020.03.08 17点35分 更新:
      ·这次更新完全消除了黑线,是最终方案

      解决方案
      一开始我没搞明白所谓的维护每个子采样点的深度是什么意思,后来经过大佬们的指点,我稍微理解一点
      具体做法:

      第一步:扩大深度缓存
      因为是2X2的MSAA,所以子采样点的个数是像素个数X4,为了能存下子采样点的深度信息,深度缓存就要扩大4倍

      rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
      {
          frame_buf.resize(w * h);
          w=w*4+100;//***这里改动***
          depth_buf.resize(w * h );
      }

      +100是为了防爆

      第二步:新建一个color buffer*****这一步是“2020.03.08 17点35分 更新”新加的******
      *color_buf用于存放子采样点的颜色,对每个子采样点进行深度检测时,若通过了深度检测,则在写入深度信息的同时也要往这个color_buf里写各个子采样点的颜色信息,而不是直接写进最终的颜色缓存frame_buf中
      在经过四个子采样点的深度检测之后再对四个子采样点的颜色做一个平均,然后传入frame_buf
      首先在rasterizer.hpp头文件里定义color_buf(用于持久保持防止回收)

      std::vector<Eigen::Vector3f> frame_buf;
      
      std::vector<Eigen::Vector3f> color_buf;//***这里
      
      std::vector<float> depth_buf;

      然后在rasterizer.cpp里设置初始化:

      void rst::rasterizer::clear(rst::Buffers buff)
      {
          if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
          {
              std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
              std::fill(color_buf.begin(), color_buf.end(), Eigen::Vector3f{0, 0, 0});//***这里
          }
          
          if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
          {
              //std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
              std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity()*(-1.0f));
          }
      }

      接着扩大到w*h的四倍

      rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
      {
          frame_buf.resize(w * h);
          w=w*4+100;
          color_buf.resize(w * h);//***这里
          depth_buf.resize(w * h );//*这是上一步改的
      }

      第三步:对每个子采样点进行三角形内部检测
      这里没什么好说的,就是给insideTriangle函数传一个大小为4的数组,分别对应每个像素的4个子采样点,分别对4个子采样点进行向量叉乘判断是否在三角形内部,然后对数组写入对应的值并返回即可

      第四步:对每个子采样点进行深度检测和深度写入
      我在这里浪费了很多时间,因为一开始我只做了对每个子采样点进行深度检测,并没有对每个子采样点进行深度写入

      for(对每个子采样点)
      {
          if(如果当前子采样点在三角形内部)
          {
              if(depth_buf中当前子采样点位置的深度 < 当前三角形的深度)
              {
                  depth_buf中当前子采样点位置的深度 = 当前三角形的深度;
                  color_buf中当前子采样点位置的颜色 = 当前三角形的颜色;//也就是等于t.getColor()
              }
          }
      }
      for(对每个子采样点)
      {
          最终颜色 += color_buf中当前子采样点位置的颜色/4.0f;
      }

      *这里要注意的是深度检测之前要先判断这个子采样点是不是在三角形内部,如果不做这个判断就会出错。因为边缘处的子采样点,只有在三角形内部才有被深度检测的意义

      至此,问题就大致解决了,
      我们来看下最终效果
      见附件,附件已更新,是最新的效果

      或许不同的情况会有不同的渲染顺序,结果可能不一样,但问题思路应该是差不多的
      这个问题浪费了三天时间,但好在最后总算有了个尾声
      希望能够帮到各位,谢谢

      This post has received 2 votes up.
      • This reply was modified 2 years, 3 months ago by 戴皓天.
      • This reply was modified 2 years, 3 months ago by 戴皓天.
      Attachments:
      You must be logged in to view attached files.
    • #3588 Score: 0
      maajor
      Participant
      3 pts

      诶测试下 我刚才的回复去哪了。。。

      • #3590 Score: 0
        maajor
        Participant
        3 pts

        怎么吞我回复 是不是不能贴图片引用
        —–
        谢谢楼主总结,我也查了些资料,这里做个补充

        贴一张RTR4的配图

        个人理解硬件实现上,是不会把color buffer/depth buffer x4的,但确实per pixel多存了一些样本。需要注意,这些样本可能是相同的。比如配图中的例子,0/2/3处的红色的color/z是相同的。per pixel上并不需要计算sub-pixel的颜色和深度,而仅仅是在通过相交测试的sub-pixel中,保存了同样的color/z。

        这也就是MSAA与超采样的区别,MSAA里,每个像素只有一次深度和颜色采样(或说光照计算),而超采样需要per sub-pixel都做颜色和深度采样。体现在代码中,应该是per pixel只有一次getColor和compute_z。

        也因此MSAAx2的时间应该小于没有AA的四倍,笔者的例子中,MSAAx2是1.4s,没有AA是0.55s,两倍多一点。
        这里可以注意,sub-pixel的样本不需要全屏存,其实只有边缘处是需要的。如果某个pixel的sub-pixel样本全部通过深度测试,那最后resolve阶段我就不用去平均这里sub-pixel的颜色,直接拿color buffer就行。

        这也就引申出右侧EQAA,既然样本可能一样,那我维护个表和索引就行。图中这个case,就少存了两个样本。一些当代硬件就是这么实现的,比如Apple的GPU Enhanced MSAA

        Attachments:
        You must be logged in to view attached files.
        • #3597 Score: 0
          sublimation
          Participant
          3 pts

          多谢你的资料,我觉得很有道理,MSAA的确可以这样优化,而且优化力度很大。从算法的角度来看,这可以说是,将静态空间O(H*W*4),进行了优化,改为动态的空间维护。

          不过关于“这也就是MSAA与超采样的区别,MSAA里,每个像素只有一次深度和颜色采样(或说光照计算),而超采样需要per sub-pixel都做颜色和深度采样。体现在代码中,应该是per pixel只有一次getColor和compute_z。”
          这里我觉得还是需要进行每个亚像素的采样的,我的采样表示需要计算深度和颜色,否则如何处理融合的颜色呢?在你的配图中,也是计算了每个亚像素的颜色和深度的。

          或者,我们只需要沿着每个图形的边界扫一边,然后维护一下。其余地方,计算单个像素即可。

          • #3606 Score: 0
            maajor
            Participant
            3 pts

            谢谢回复,是的,这样动态维护可以降低显存空间。

            我的做法是亚像素直接使用像素上的颜色和深度

            伪码大概是:
            对于某个像素:
            z = 计算像素深度
            foreach 亚像素
            if 相交测试通过:
            if 亚像素深度测试通过:
            存储当前亚像素深度(z)
            if 存在亚像素通过深度测试:
            c = 计算像素颜色
            foreach 通过深度测试的亚像素
            存储当前亚像素颜色(c)

            resolve阶段:
            foreach 需要处理的像素:
            计算亚像素平均颜色,赋进framebuffer

            请大佬们指正

            • #3609 Score: 0
              maajor
              Participant
              3 pts

              把我缩进吃了。。。

              
              对于某个像素:
                  z = 计算像素深度
                  foreach 亚像素
                      if 相交测试通过:
                          if 亚像素深度测试通过:
                              存储当前亚像素深度(z)
                  if 存在亚像素通过深度测试:
                      c = 计算像素颜色
                      foreach 通过深度测试的亚像素
                          存储当前亚像素颜色(c)
              
        • #3600 Score: 1
          Angus
          Participant
          23 pts

          感谢同学补充,不过你所说的恰巧是我一直纠结的点。

          我个人认为老师在PPT和作业中提到的「super-sampling 处理 Anti-aliasing」不是MSAA,而是SSAA。

          你的资料也显示了,MSAA有着与老师在PPT中提到的算法流程有着些许不同。

          所以本次作业我按照老师PPT中的算法流程和作业中的要求实现的是SSAA,需要4xdepth buffer和4xcolor buffer,且每个像素做4次着色操作(getcolor())。

          如果要实现的是MSAA的话,我觉得那就得另开一贴再议了。

          MSAA确实如同学所说,只需要做一次着色操作,但是我对您所说的不需要为子样本提供单独的颜色缓存和深度缓存这一点存疑,这与我之前的认知相悖。您可以继续展开说一下是如何实现不把背景色(比如说红色)引入两个三角形的相交边缘的吗?

          This post has received 1 vote up.
          • #3608 Score: 1
            maajor
            Participant
            3 pts

            谢谢补充,老师讲的像是SSAA。
            MSAA的问题我工作几年了一直没想清楚过:(,还是写代码重读RTR才想明白一些

            我的理解是,子样本是可以单独给一个x4的缓存的,我不确定硬件里是不是这么做的,可能需要老师解答一下。但framebufferx4这样太耗空间了,一种优化方式是:有需要的像素维护一个子样本array就行。微信群里有个大佬提到过,感觉有点像OIT维护一个链表的深度样本。

            肯定还是需要存储子样本的颜色,在resolve阶段做个平均的。只是怎么存储我有点异议。

            This post has received 1 vote up.
        • #3626 Score: 0
          戴皓天
          Participant
          8 pts

          谢谢补充!非常感谢!我在一开始做的时候就觉得作业2的pdf里写的虽然是MSAA但下面的要求看起来却像SSAA。可能是因为SSAA实现起来比较暴力,比较适合小白实现吧,MSAA的话还得判断是不是边缘,大佬上面补充的EQAA看起来是很不错的做法,至少省了很大的显存空间。

    • #3671 Score: 0
      Keneyr
      Participant
      6 pts

      大佬,为什么设置一个4倍的color_buf就能消除黑边啊??我还是没有搞懂。。

      我是对四个采样点分别进行“是否在三角形内部”、“深度是否小于缓冲区深度”判断以后,若该采样点符合条件,就更新采样点对应的深度缓冲区和颜色缓冲区。

      最后判断四个采样点中,若至少有一个符合上述两个条件,就取颜色平均值,然后setpixel(x,y,average_color)。

      最后结果如图,但是我还是没搞懂为什么这能消除黑边。

      因为,如果我程序中判断至少有两个采样点符合条件,才取颜色平均值,就会有黑边。

      这说明,我只是求颜色平均值时,绿色占比多,把黑色给抹杀了,实际上并没有做到绿色+蓝色。

      Attachments:
      You must be logged in to view attached files.
      • #3673 Score: 0
        Keneyr
        Participant
        6 pts

        我糊涂了。。如果按照判定条件,四个采样点,至少有一个符合条件就取颜色平均值,相当于最糟糕的情况是 green/4+black/4+black/4+black/4,不应该更黑吗??怎么反而没有黑边了呢。。。

        • #3676 Score: 0
          戴皓天
          Participant
          8 pts

          1/4绿+3/4黑只可能发生在除了两三角形相交处之外的其他绿三角形边缘,或者可能发生在渲染蓝三角形之前的相交处

      • #3674 Score: 0
        戴皓天
        Participant
        8 pts

        你是没有用四倍的深度缓存和颜色缓存吗?只是用了一个暂时的2X2大小的缓存用来暂存颜色,然后最后通过判断经过平均再写入总的颜色缓存中?
        如果是这样的话你的子采样点的颜色是怎么取的呢,我不知道你具体是怎么实现的,但就我看来如果是取自原大小的颜色缓存的话就会出现黑边,因为假设绿色三角形先被渲染,蓝色后被渲染,那么在绿色三角形被渲染时,绿色三角形的边界就被写入了(绿色+黑色)的颜色,然后渲染蓝色三角形时,在两个三角形的相交处,子采样点首先肯定要取颜色,那么此时就取了绿三角形边界的颜色,也就是前面提到的(绿色+黑色)的颜色,此时这个相交处肯定至少有一个蓝三角形的子采样点满足深度测试的,而三角形内部测试的话相交处很明显是在蓝色三角形内部,所以也会通过,所以在渲染蓝色三角形的情况下,在渲染相交处的像素时,会写入(绿色+黑色)+(蓝色)也就是(绿色+黑色+蓝色)的颜色,而不是(绿色+蓝色)

        如果用和最终颜色缓存独立的4倍大的子采样点颜色缓存的话,子采样点中都是没被平均过的颜色,注意,这个缓存存的是每个子采样点自己的颜色,不需要平均,也就是黑色就是黑色,绿色就是绿色,显而易见,在这个颜色缓存中,在两个三角形相交处要么是绿色(说明该子采样点在绿三角形内),要么是蓝色(说明该子采样点在蓝色三角形内)。因此在渲染蓝三角形时,相交处取到的肯定是绿色和蓝色而没有黑色,所以最终写入正真的像素的颜色缓存时用的就是(绿色+蓝色),此时不用平均(注意,平均是拿子采样点的颜色缓存中的颜色进行平均,然后直接写入最终的像素颜色缓存,不需要取出最终的像素颜色缓存里的颜色,这也就避免了先写入了黑色,后来就没法消除黑色的弊端)

        像我最开始也没用四倍大的子采样点的颜色缓存,每次都是子采样点判断后直接平均颜色写入颜色缓存,这样的话黑色只要写入了颜色缓存,后面因为怎么样都是在拿它做平均,所以一直会有黑色混入,最终颜色也就显然不正确了

        你这个图或许只是巧合,你可以交换两个三角形的深度,看下会不会出错。我一开始没用四倍大的子采样点颜色缓存也能得到“看似”正确的结果,但是经过大佬提醒,我交换了下深度发现出错了,然后才改正的。
        因为我不知道你具体是怎么实现的,所以如果你怎么变都是对的,那就坚持你自己的做法和想法吧,因为你那样肯定是对的

        • #3748 Score: 0
          SHERLOCK
          Participant
          -1 pt

          感谢~~解释的很清楚

        • #3944 Score: 0
          Keneyr
          Participant
          6 pts

          我交换了一下三角形,感觉效果也很好,没有黑边。我用的是独立的4倍缓存,总体而言,如果看起来是正确的,那我就认为它是正确的吧···虽然有时候我又觉得它work的不对。。感觉你讲的很到位!可能是我代码哪里还有有缺陷,谢谢!辛苦啦

    • #10982 Score: 0
      stdiohero
      Participant

      感谢楼主和楼上大佬们的讨论,虽然我自己也鼓捣出来了这个提高题,但是还是一知半解的。看了帖子中的分享和讨论后,很受启发!

    • #10993 Score: 0
      Daedra
      Participant

      这里我一开始也出现了黑边,不过我是把颜色混合方式想错了,关键是2点:
      1 使用4倍大小的buffer储存采样点的深度
      2 混合颜色 = 该像素当前颜色 + inside_count / 4 * t.getColor(),这里我一开始想成了 该像素当前颜色 + inside_count / 4 * (t.getColor() – 该像素当前颜色), 导致边缘重叠部分颜色更暗,从而看起来有黑边

Viewing 8 reply threads
  • You must be logged in to reply to this topic.