Мне нужен пиксельно-идеальный алгоритм заполнения треугольника, чтобы избежать сглаживания артефактов

Я помогаю кому-то с кодом пользовательского интерфейса визуализировать математический анализ изображений. Во время этого процесса мы будем сегментировать часть 2D-фигуры на треугольники и заполнять некоторые из этих треугольников в пользовательском интерфейсе.

мы ищем алгоритм заливки, который гарантирует, что если два треугольника имеют общее ребро (в частности, если любые две вершины треугольников идентичны), то независимо от порядка рисования и сглаживания не будет пустых, развернутых пикселей на линия между ними. (Это нормально, если некоторые пиксели рисуются дважды.) Результат должен выглядеть нормально при произвольном масштабировании. Некоторые треугольники могут быть очень тонкими щепками в местах, до 1 пикселя в ширину.

В идеале это также должен быть разумно эффективный алгоритм заполнения!

сглаживание не будет использоваться в рендеринге треугольника, так как конечное изображение должно быть 1-битной глубины.

контекст является приложением распознавания изображений, поэтому все координаты вершин будут с точностью до одного пикселя.

5 ответов


учитывая требования, похоже, что есть простое решение.

во-первых, растеризация треугольников. Вы можете использовать алгоритм рисования линии Bresenham для этого (как в коде ниже) или что-нибудь, что работает. Затем заполните область между ними. Это будет работать со сколь угодно тонкими треугольниками.

чтобы убедиться, что нет пробелов независимо от порядка, в котором рисуются треугольники, и независимо от порядка вершин, поставляемых в треугольник-код рисования вы хотите растеризировать общие ребра таким же образом в треугольниках, разделяющих ребро. одинаково означает одни и те же пиксели каждый раз.

чтобы гарантировать, что каждый раз, когда вы получаете одни и те же пиксели из одних и тех же пар координат вершин, вы в основном хотите установить фиксированный порядок, то есть установить правило, которое всегда будет выбирать одну и ту же вершину из двух заданных независимо от порядка, в котором они заданы.

один простой способ применения этого порядка-рассматривать вашу линию (край треугольника) как 2-d вектор и переворачивать ее направление, если она указывает в направлении отрицательных y или параллельна оси x и указывает в направлении отрицательных X. Время для некоторых ASCII искусства! :)

      3   2   1
       \  |  /
        \ | /
         \|/
4 --------+--------- 0
         /|\
        / | \
       /  |  \
      5   6   7

        4 -> 0
        5 -> 1
        6 -> 2
        7 -> 3

см., здесь сегмент линии, скажем, 1 и сегмент линии 5-это действительно одна и та же вещь, единственное отличие-направление от конечной точки в начале координат до другой конечной точки. Поэтому мы сокращаем эти случаи вдвое, поворачивая сегменты с 4 по 7 в сегменты с 0 по 3 и избавиться от неоднозначности направления. IOW, мы выбираем идти в направлении увеличения y или, если y одинаковы на краю, в направлении увеличения X.

вот как вы могли бы сделать это в коде:

#include <stddef.h>
#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#define SCREEN_HEIGHT 22
#define SCREEN_WIDTH  78

// Simulated frame buffer
char Screen[SCREEN_HEIGHT][SCREEN_WIDTH];

void SetPixel(long x, long y, char color)
{
  if ((x < 0) || (x >= SCREEN_WIDTH) ||
      (y < 0) || (y >= SCREEN_HEIGHT))
  {
    return;
  }

  if (Screen[y][x] == ' ')
    Screen[y][x] = color;
  else
    Screen[y][x] = '*';
}

void Visualize(void)
{
  long x, y;

  for (y = 0; y < SCREEN_HEIGHT; y++)
  {
    for (x = 0; x < SCREEN_WIDTH; x++)
    {
      printf("%c", Screen[y][x]);
    }

    printf("\n");
  }
}

typedef struct
{
  long x, y;
  unsigned char color;
} Point2D;


// min X and max X for every horizontal line within the triangle
long ContourX[SCREEN_HEIGHT][2];

#define ABS(x) ((x >= 0) ? x : -x)

// Scans a side of a triangle setting min X and max X in ContourX[][]
// (using the Bresenham's line drawing algorithm).
void ScanLine(long x1, long y1, long x2, long y2)
{
  long sx, sy, dx1, dy1, dx2, dy2, x, y, m, n, k, cnt;

  sx = x2 - x1;
  sy = y2 - y1;

/*
      3   2   1
       \  |  /
        \ | /
         \|/
4 --------+--------- 0
         /|\
        / | \
       /  |  \
      5   6   7

        4 -> 0
        5 -> 1
        6 -> 2
        7 -> 3
*/
  if (sy < 0 || sy == 0 && sx < 0)
  {
    k = x1; x1 = x2; x2 = k;
    k = y1; y1 = y2; y2 = k;
    sx = -sx;
    sy = -sy;
  }

  if (sx > 0) dx1 = 1;
  else if (sx < 0) dx1 = -1;
  else dx1 = 0;

  if (sy > 0) dy1 = 1;
  else if (sy < 0) dy1 = -1;
  else dy1 = 0;

  m = ABS(sx);
  n = ABS(sy);
  dx2 = dx1;
  dy2 = 0;

  if (m < n)
  {
    m = ABS(sy);
    n = ABS(sx);
    dx2 = 0;
    dy2 = dy1;
  }

  x = x1; y = y1;
  cnt = m + 1;
  k = n / 2;

  while (cnt--)
  {
    if ((y >= 0) && (y < SCREEN_HEIGHT))
    {
      if (x < ContourX[y][0]) ContourX[y][0] = x;
      if (x > ContourX[y][1]) ContourX[y][1] = x;
    }

    k += n;
    if (k < m)
    {
      x += dx2;
      y += dy2;
    }
    else
    {
      k -= m;
      x += dx1;
      y += dy1;
    }
  }
}

void DrawTriangle(Point2D p0, Point2D p1, Point2D p2)
{
  long y;

  for (y = 0; y < SCREEN_HEIGHT; y++)
  {
    ContourX[y][0] = LONG_MAX; // min X
    ContourX[y][1] = LONG_MIN; // max X
  }

  ScanLine(p0.x, p0.y, p1.x, p1.y);
  ScanLine(p1.x, p1.y, p2.x, p2.y);
  ScanLine(p2.x, p2.y, p0.x, p0.y);

  for (y = 0; y < SCREEN_HEIGHT; y++)
  {
    if (ContourX[y][1] >= ContourX[y][0])
    {
      long x = ContourX[y][0];
      long len = 1 + ContourX[y][1] - ContourX[y][0];

      // Can draw a horizontal line instead of individual pixels here
      while (len--)
      {
        SetPixel(x++, y, p0.color);
      }
    }
  }
}

int main(void)
{
  Point2D p0, p1, p2, p3;

  // clear the screen
  memset(Screen, ' ', sizeof(Screen));

  // generate random triangle coordinates

  srand((unsigned)time(NULL));

  // p0 - p1 is going to be the shared edge,
  // make sure the triangles don't intersect
  for (;;)
  {
    p0.x = rand() % SCREEN_WIDTH;
    p0.y = rand() % SCREEN_HEIGHT;

    p1.x = rand() % SCREEN_WIDTH;
    p1.y = rand() % SCREEN_HEIGHT;

    p2.x = rand() % SCREEN_WIDTH;
    p2.y = rand() % SCREEN_HEIGHT;

    p3.x = rand() % SCREEN_WIDTH;
    p3.y = rand() % SCREEN_HEIGHT;

    {
      long vsx = p0.x - p1.x;
      long vsy = p0.y - p1.y;
      long v1x = p0.x - p2.x;
      long v1y = p0.y - p2.y;
      long v2x = p0.x - p3.x;
      long v2y = p0.y - p3.y;
      long z1 = vsx * v1y - v1x * vsy;
      long z2 = vsx * v2y - v2x * vsy;
      // break if p2 and p3 are on the opposite sides of p0-p1
      if (z1 * z2 < 0) break;
    }
  }

  printf("%ld:%ld %ld:%ld %ld:%ld %ld:%ld\n\n",
         p0.x, p0.y,
         p1.x, p1.y,
         p2.x, p2.y,
         p3.x, p3.y);

  // draw the triangles

  p0.color = '-';
  DrawTriangle(p0, p3, p1);
  p1.color = '+';
  DrawTriangle(p1, p2, p0);

  Visualize();

  return 0;
}

пример вывода:

30:10 5:16 16:6 59:17







                +++
               ++++++++
              ++++++++++++
             +++++++++++++++++
            +++++++++++++++****---
          +++++++++++++****-----------
         ++++++++++****-------------------
        ++++++*****----------------------------
       +++****-------------------------------------
      ****---------------------------------------------
     *-----------------------------------------------------
                                                           -

легенда:

  • "+" - пиксели треугольника 1
  • "-" - пиксели треугольника 2
  • "* " - пиксели общего края между треугольниками 1 и 2

остерегайтесь, что даже если не будет незаполненных пробелов (пикселей), треугольник, пиксели которого (на общем краю) перезаписываются (из-за другого треугольника, нарисованного поверх него), может отображаться как непересекающийся или неуклюже сформированный, если он слишком тонкий. Пример:

2:20 12:8 59:15 4:17









            *++++++
           *+++++++++++++
          *+++++++++++++++++++++
         -*++++++++++++++++++++++++++++
        -*++++++++++++++++++++++++++++++++++++
        *+++++++++++++++++++++++++++++++++++++++++++
       *+++++++++++++++++++++++++++++++++++++++++++++++++++
      *+++++++++++++++++++++++++++++++++++++++++++++++++++++
     *+++++++++++++++++++++++++++++++++++++++++++
    -*+++++++++++++++++++++++++++++++
   -*+++++++++++++++++++++
   *++++++++++
  *

ваше беспокойство о соседних треугольниках является действительным. Если два треугольника имеют общее ребро, вы хотите быть уверены, что каждый пиксель вдоль этого ребра "принадлежит" исключительно одному треугольнику или другому. Если один из этих пикселей не принадлежит ни одному из треугольников, у вас есть пробел. Если он принадлежит обоим треугольникам, у вас есть overdraw (что неэффективно), и цвет может зависеть от порядка отображения треугольников (который не может быть детерминированным).

так как вы не используете сглаживание, это на самом деле не слишком сложно. Это не столько умный алгоритм, который вам нужен, сколько тщательная реализация.

типичный способ растеризации треугольника-вычислить горизонтальные сегменты, которые являются частью треугольника сверху вниз. Вы делаете это, отслеживая текущий левый и правый края и, по существу, делая расчет X-перехвата для каждого края на каждой линии развертки. Это также можно сделать с помощью двух алгоритмов рисования линий в стиле Bresenhem вместе. Фактически растеризация сводится к нескольким вызовам функции, которая рисует сегмент горизонтальной линии на некотором scanline y из какой-то левой координаты x0 к некоторой правой координате x1.

void DrawHLine(int y, int x0, int x1);

обычно это делается для того, чтобы убедиться, что растризатор округляет x-перехваты согласованным образом, так что X-координаты вычисляются последовательно независимо от того, являются ли они частью правого края одного треугольника или левого края соседний треугольник. Это гарантирует, что каждый пиксель вдоль общего края будет принадлежать и треугольников.

мы разрешаем двойное владение, настраивая DrawHLine так что он заполняет пиксели из x0 включительно до x1 эксклюзивные. Таким образом, все эти дважды принадлежащие пиксели на общем краю определяются как принадлежащие треугольнику справа от общего края.


то, что вы ищете-это .

вот.

еще одна ссылка.

вы можете google "floodfill-алгоритм"для большего.

[edit]

может быть этот сайт[шейдерный каркасный чертеж] может предложить еще несколько идей.


Это не самый эффективный, но вы можете обойти квадрат, содержащий треугольник, и проверить, находится ли каждый пиксель в треугольнике.

псевдокод:

for(x : minX -> maxX)
    for(y : minY -> maxY)
        if(triangle.contains(x,y))
            drawPixel(x,y);

где minX-минимальная координата X между тремя вершинами и аналогично для maxX, minY и maxY.

для более быстрого алгоритма вы можете сначала выполнить быстрое и грязное заполнение (например, заполнение потока slashmais), а затем сделать это для пикселей по краям.

в тест "точка в треугольнике" описан здесь.


Это хорошо изученная проблема. Узнайте об алгоритме рисования линий bresenham.

http://en.wikipedia.org/wiki/Bresenham s_line_algorithm