В своем предыдущем посте я поделился кратким обзором покраски модели и запекания ее обратно в текстуру. Одной из проблем с начальной реализацией была прорисовка модели. То есть при покраске одной стороны модели краска будет наноситься на другую сторону модели. Это часто нежелательно.

При запекании изменений в UV-пространстве шейдер будет оценивать, была ли закрашена та часть экрана с точки зрения камеры. У него нет никакого контекста, на котором полигон фактически был нарисован. Только то, что там краска. Давайте обсудим несколько способов предотвратить появление этой краски.

Стратегия 1: примитивные идентификаторы

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

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

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

bool isPainted(vec3 uv)
{
    highp int screenPrimitiveId = int(texture2D(drawTexture, uv.xy).a);
    return screenPrimitiveId == gl_PrimitiveID;
}
void main() {
    ...
    // only apply paint color if isPainted
    float paintIntensity = 0.0f;
    if (isPainted(paintUv)) {
        paintIntensity = texture2D(paintTexture, paintUv.xy).r;
    }
    ...

Эта стратегия очень проста и работает довольно хорошо, но имеет довольно большую проблему — границы между примитивами.

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

Стратегия 2: буфер глубины

Проход рендеринга модели уже сохраняет цвет и глубину модели во вложении цвета FBO. Шейдер запекания уже перепроецирует фрагмент из UV-пространства в пространство камеры для поиска краски. На самом деле мы можем провести сравнение глубины, чтобы увидеть, вероятно, это та же часть модели (а не что-то за ней).

Вот обновленная функция видимости:

bool isPainted(vec3 uv) 
{
    // between 0 and 1, depth of model in FBO
    float drawZ = texture2D(drawTexture, uv.xy).b;
    // between 0 and 1, depth of model from back projection
    float meshZ = uv.z;
    // if the depth from the FBO and the fragment projected there
    // are within an episolon, assume they're the same surface
    return abs(drawZ - meshZ) < 0.0001;
}

А вот и тест прокраски в действии:

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

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

Я могу придумать несколько вариантов улучшения сравнения. Один из них заключается в сохранении другого значения глубины в FBO, которое находится дальше от первого фрагмента в пикселе, например, среднего значения первой и второй поверхностей. Second-Depth Shadow Mapping (pdf), например, предлагает сохранить второе значение и изменить поиск, но есть много способов улучшить этот поиск.

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

Вывод

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

Что дальше

Теперь, когда прорисовка стала меньшей проблемой, мы можем сосредоточиться на фильтрации швов. Когда текстуры запекаются, значение UV используется для рисования полигонов на текстуре. Это прекрасно работает для области внутри полигона, но с фильтрацией текстур средство визуализации будет захватывать образцы вдоль «шва», которые не были окрашены, что вызывает артефакты визуализации.

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

Исходный код

Код рисования для приведенных выше анимаций доступен на Github. Он написан на C++, OpenGL и Qt5. В настоящее время ему не хватает документации, но его можно собрать в Qt Creator и assimp (для загрузки 3D-моделей).