Как правильно сделать Z-заказ в программном обеспечении

Я рендеринг 3D-объектов на 2D-холсте, выполняя все необходимые вычисления в программном обеспечении. Я не использую ускорение графики.

первоначально все объекты были кубами одинакового размера, поэтому я мог сортировать их на основе их расстояния в Z от камеры, и это упорядочило бы их правильно. Но сейчас я пытаюсь нарисовать Кубы различных размеров. Это делает мой простой алгоритм Z-упорядочения неудачным в перспективной проекции.

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

есть простой трюк, чтобы сделать это в программном обеспечении? Какие-либо примеры из ранних дней 3D-графики, когда видеокарты были недоступны?

хотя это общий 3D графический вопрос, если это поможет, я делаю это поверх HTML5 Canvas 2D API.

3 ответов


как уже упоминалось @ybungalobill, z-буфер является самым простым алгоритмом для реализации. Когда вы заполняете треугольники / полигоны, которые составляют ваши Кубы, интерполируйте координату Z между ними и сохраните ее на пиксель. Если позже вы заполните другой многоугольник, который отображается по той же координате X, Y, проверьте, меньше ли его Z, чем Z, уже хранящийся в буфере. Не забудьте очистить буфер Z до бесконечности, до перекраски. Псевдокод:

foreach (scanline in polygon)  {
  int length = scanline.right.x - scanline.left.x;
  foreach (pixel in scanline)  {
    float t = (float)pixel.x / length;
    float z = (1 - t) * scanline.left.z + t * scanline.right.z;  // Interpolate the Z coordinate
    if (z < zbuffer[scanline.y][pixel.x])
      drawPixel(pixel.x, scanline.y, polygon.color);  // The pixel is closer, paint it
  }
}

пересмотренный подход буфера Z, который лучше работает на CPU, не рисуя пиксели, которые будут перезаписаны, называется буфер сегмента: http://www.gamedev.net/reference/articles/article668.asp

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

void warnock(Rectangle rect)
{
  float minZ = infinity;
  foreach (polygon in polygons)  {
    if (rect is inside polygon)  {
      float z = interpolateZ(polygon, rect.x + rect.width / 2, rect.y + rect.height / 2);  // Get Z coordinate at the centre of the rectangle
      if (z < minZ)  {  // If there are more polygons in this rectangle, make sure the topmost one gets drawn last
        fillRect(polygon.color);
        minZ = z;
      }
    } else {
      // Divide to 4 subrectangles
      warnock(Rectangle(rect.x, rect.y, rect.width / 2, rect.height / 2));  // Top left
      warnock(Rectangle(rect.x, rect.y + rect.height / 2, rect.width / 2, rect.height / 2));  // Bottom left
      warnock(Rectangle(rect.x + rect.width / 2, rect.y, rect.width / 2, rect.height / 2));  // Bottom right
      warnock(Rectangle(rect.x + rect.width / 2, rect.y + rect.height / 2, rect.width / 2, rect.height / 2));  // Top right
    }
  }
}

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

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

Raycasting это еще один способ определить видимость на пиксель. Это, однако, довольно интенсивный процессор. Основная идея состоит в том, чтобы проверить для каждого пикселя экрана, какой многоугольник пересекает его (какой многоугольник попадает в Луч, отлитый из текущий пиксель). Источником лучей является положение глаз. Псевдокод:

foreach (pixel in screen)  {
  float minZ = infinity;  // Can be zfar from the perspective projection
  Color pixelColor = backgroundColor;
  foreach (polygon in projectedPolygons)  {
    if (polygon contains Point(pixel.x, pixel.y))  {
      float z = interpolateZ(polygon, pixel.x, pixel.y);  // Get the current Z for (x, y) and this polygon using bilinear interpolation
      if (z < minZ)  {
        minZ = z;
        pixelColor = polygon.color;
      }
    }
  }
}

правильно, сегодня вы используете Z-буферизацию на GPU для сравнения глубины на пиксель. Вы можете сделать это и в программном обеспечении.

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

более общая техника, которая использовалась во многих ранних играх (например, Doom), - это деревья BSP. Они не будут работать с динамическими сценами, так как это дорого создать их. Однако они решают проблему упорядочения в целом.


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

enter image description here

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

enter image description here

теперь визуализируйте каждую ячейку сетки, если она "достаточно проста" (критерии Warnock). Если нет, примените Warnock.

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

тем не менее, я действительно не делаю это для целей отображения в реальном времени. Это может быть довольно сложно сделать достаточно эффективно на сложных сетках в реальном времени.

Я в основном делаю это, чтобы делать такие вещи, как шатер и лассо, выбирать вершины/ребра/полигоны в 3D-программном обеспечении на очень плотных сетках, где мы не хотим пропустить незамкнутые примитивы, аппроксимируя с фиксированным разрешением пикселей. В этом случае пользователь может увеличить далеко от сетки, и мы не хотим наши выборы лассо и шатра, чтобы пропустить целую кучу субпиксельных примитивов, поэтому привлекательность использования независимого от разрешения Warnock здесь заключается в том, что вы можете рекурсивно применять алгоритм так глубоко, как вам нужно, пока не получите этот "достаточно простой" результат, который может быть прямоугольником намного меньше пикселя. Это также может быть полезно для сглаживания с достаточно эффективной суб-выборкой (поскольку она не будет суб-выборкой, если пиксель имеет полный охват, например). Я никогда не использовал это для растеризации контексты.

Raytracing также весело из-за всех вариантов, которые он открывает, насколько делает косвенное освещение, каустики, DOF и т. д., Хотя это очень дорого, как указал Карел. Тем не менее, я нашел с хорошим BVH, что я могу делать в режиме реального времени отслеживание лучей в эти дни при довольно высоком разрешении, если я просто делаю в основном прямое освещение.

вот небольшой пример, который я выкопал из raytracing миллион треугольной сетки в режиме реального времени на CPU, который я взбил из некоторых лет назад. Это было на моем i3 и на 1600x1200 пикселях. Потребовался всего день, чтобы закодировать его. GIF действительно понизил качество и частоту кадров (первоначально было более ~120 кадров в секунду) , но, надеюсь, вы получите идею:

enter image description here

основной недостаток для меня с realtime raytracing на CPU (а также GPU) на самом деле не является частью растеризации. Хотя я мог довольно легко визуализировать основные материалы и освещение в реальном времени с помощью i3 (и это даже не был оптимизированный код, просто некоторые основные SIMD и параллельные петли в C), было бы намного сложнее, если бы этот миллион треугольной сетки деформировался каждый кадр. Тогда я должен был бы иметь возможность обновить BVH, хранящий более миллиона треугольников на более чем 100 FPS, которые я понятия не имею, как сделать достаточно быстро.

тем не менее, есть одно программное обеспечение, которое на самом деле растеризует миллионы полигонов, которые деформируют данные в реальном времени. Это называется ZBrush:http://i1.wp.com/www.cgmeetup.net/home/wp-content/uploads/2013/11/Zbrush-Character-Modeling-for-The-Last-of-Us-8.jpg

I хотя понятия не имею, как им это удается. Они могут использовать LOD или вокселизировать сетку супер быстро, пока пользователи деформируют ее кистью или чем-то еще; для меня это не имеет значения, поскольку они позволяют вам управлять вещами на уровне вершин, на уровне полигонов, позволяют вам видеть каркасы и позволяют вводить и выводить полигональные сетки при загрузке и сохранении. В любом случае он имеет эффект обработки миллионов полигонов данных (ему даже удалось растеризировать и деформировать сетки, охватывающие более 20 миллион полигонов 17 лет назад, что не имеет аналогов; люди даже не могут соответствовать этому сегодня 17 лет спустя) и позволяет пользователю лепить результаты и управлять вещами на уровне каждой вершины каким-то образом, сохраняя интерактивную частоту кадров без использования GPU для растеризации. Насколько я понимаю, у них там какое-то Вуду-программирование, хотя я мог бы обменять палец, чтобы узнать, как они это делают.