Подтвердить что ты не робот

Как я могу улучшить производительность своей пользовательской генерации текстур OpenGL ES 2.0?

У меня есть приложение с открытым исходным кодом для iOS, которое использует пользовательские шейдеры OpenGL ES 2.0 для отображения трехмерных представлений о молекулярных структурах. Он делает это, используя процедурно сгенерированные сферы и цилиндровые самозванцы, нарисованные над прямоугольниками, вместо этих же форм, построенных с использованием множества вершин. Недостатком этого подхода является то, что значения глубины для каждого фрагмента этих объектов-самозванов должны быть вычислены в фрагментаторном шейдере, который будет использоваться, когда объекты перекрываются.

К сожалению, OpenGL ES 2.0 не позволяет писать в gl_FragDepth, поэтому мне нужно было вывести эти значения в пользовательскую текстуру глубины. Я пропускаю свою сцену, используя объект фреймбуфера (FBO), только визуализируя цвет, соответствующий значению глубины, при этом результаты сохраняются в текстуре. Эта текстура затем загружается во вторую половину моего процесса рендеринга, где генерируется фактическое изображение на экране. Если фрагмент на этом этапе находится на уровне глубины, хранящемся в текстуре глубины для этой точки на экране, он отображается. Если нет, то это брошено. Подробнее о процессе, включая диаграммы, можно найти в моем сообщении здесь.

Генерация этой глубинной текстуры является узким местом в моем процессе рендеринга, и я ищу способ сделать это быстрее. Это кажется медленнее, чем должно быть, но я не могу понять, почему. Чтобы добиться правильной генерации этой текстуры глубины, GL_DEPTH_TEST отключен, GL_BLEND включен с glBlendFunc(GL_ONE, GL_ONE), а glBlendEquation() установлен на GL_MIN_EXT. Я знаю, что выход сцены таким образом не самый быстрый на отложенном рендерере на основе плитки, таком как серия PowerVR на устройствах iOS, но я не могу придумать лучшего способа сделать это.

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

precision mediump float;

varying mediump vec2 impostorSpaceCoordinate;
varying mediump float normalizedDepth;
varying mediump float adjustedSphereRadius;

const vec3 stepValues = vec3(2.0, 1.0, 0.0);
const float scaleDownFactor = 1.0 / 255.0;

void main()
{
    float distanceFromCenter = length(impostorSpaceCoordinate);
    if (distanceFromCenter > 1.0)
    {
        gl_FragColor = vec4(1.0);
    }
    else
    {
        float calculatedDepth = sqrt(1.0 - distanceFromCenter * distanceFromCenter);
        mediump float currentDepthValue = normalizedDepth - adjustedSphereRadius * calculatedDepth;

        // Inlined color encoding for the depth values
        float ceiledValue = ceil(currentDepthValue * 765.0);

        vec3 intDepthValue = (vec3(ceiledValue) * scaleDownFactor) - stepValues;

        gl_FragColor = vec4(intDepthValue, 1.0);
    }
}

На iPad 1 это занимает 35 - 68 мс, чтобы визуализировать кадр модели заполнения пробелов ДНК с помощью шейного шейдера для отображения (от 18 до 35 мс на iPhone 4). Согласно компилятору PowerVR PVRUniSCo (часть их SDK), этот шейдер использует в лучшем случае 11 циклов GPU, в худшем - 16 циклов. Я знаю, что вам не рекомендуется использовать ветвление в шейдере, но в этом случае это привело к лучшей производительности, чем в противном случае.

Когда я упрощу его

precision mediump float;

varying mediump vec2 impostorSpaceCoordinate;
varying mediump float normalizedDepth;
varying mediump float adjustedSphereRadius;

void main()
{
    gl_FragColor = vec4(adjustedSphereRadius * normalizedDepth * (impostorSpaceCoordinate + 1.0) / 2.0, normalizedDepth, 1.0);
}

он занимает 18-35 мс на iPad 1, но только 1,7 - 2,4 мс на iPhone 4. Оценочный счетчик GPU для этого шейдера составляет 8 циклов. Изменение времени рендеринга на основе количества циклов не кажется линейным.

Наконец, если я просто вывел постоянный цвет:

precision mediump float;

void main()
{
    gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);
}

время рендеринга падает до 1,1-2,3 мс на iPad 1 (1,3 мс на iPhone 4).

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

Если вы прочли этот вопрос, мой вопрос: на основе этой профилирующей информации, как я могу улучшить производительность рендеринга моего настраиваемого шейдера глубины на устройствах iOS?

4b9b3361

Ответ 1

Основываясь на рекомендациях Tommy, Pivot и rotoglup, я реализовал некоторые оптимизации, которые привели к удвоению скорости рендеринга для генерации текстуры глубины и общего конвейера рендеринга в приложении.

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

Что еще более важно, теперь я делаю проход перед тем, как визуализировать как текстуру глубины, так и конечные raytraced impostors, где я устанавливаю некоторую непрозрачную геометрию, чтобы блокировать пиксели, которые никогда не будут визуализированы. Для этого я включаю проверку глубины, а затем вычеркиваю квадраты, которые составляют объекты в моей сцене, усеянные sqrt (2)/2, с простым непрозрачным шейдером. Это создаст вложенные квадраты, которые, как известно, непрозрачны в представленной сфере.

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

Layered spheres and opacity testing

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

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

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

Как ни странно, когда я попытался уточнить это, используя вложенные и ограниченные восьмиугольники, которые должны покрывать на 17% меньше пикселей при рисовании и быть более эффективными с блокировкой фрагментов, производительность была на самом деле хуже, чем при использовании простых квадратов для этого. В худшем случае использование тиллеров по-прежнему составляло менее 60%, поэтому, возможно, большая геометрия привела к большему количеству промахов в кеше. Забастовкa >

EDIT (5/31/2011):

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

Rasterization optimizing octagons

Я смог уменьшить общее время рендеринга в среднем на 14% поверх описанных выше оптимизаций, переключившись на восьмиугольники из квадратов. Текстура глубины теперь генерируется в 19 мс, со случайными падениями до 2 мс и шипами до 35 мс.

ИЗМЕНИТЬ 2 (5/31/2011):

Я передумал идею Томми об использовании функции шага, теперь, когда у меня осталось меньше фрагментов, из-за восьмиугольников. Это, в сочетании с текстурой глубины для сферы, теперь приводит к среднему времени рендеринга 2 мс на iPad 1 для генерации текстуры глубины для моей тестовой модели. Я считаю, что это было так хорошо, как я мог надеяться в этом случае, и гигантское улучшение, с которого я начал. Для потомков здесь используется шейдер глубины, который я использую:

precision mediump float;

varying mediump vec2 impostorSpaceCoordinate;
varying mediump float normalizedDepth;
varying mediump float adjustedSphereRadius;
varying mediump vec2 depthLookupCoordinate;

uniform lowp sampler2D sphereDepthMap;

const lowp vec3 stepValues = vec3(2.0, 1.0, 0.0);

void main()
{
    lowp vec2 precalculatedDepthAndAlpha = texture2D(sphereDepthMap, depthLookupCoordinate).ra;

    float inCircleMultiplier = step(0.5, precalculatedDepthAndAlpha.g);

    float currentDepthValue = normalizedDepth + adjustedSphereRadius - adjustedSphereRadius * precalculatedDepthAndAlpha.r;

    // Inlined color encoding for the depth values
    currentDepthValue = currentDepthValue * 3.0;

    lowp vec3 intDepthValue = vec3(currentDepthValue) - stepValues;

    gl_FragColor = vec4(1.0 - inCircleMultiplier) + vec4(intDepthValue, inCircleMultiplier);
}

Я обновил образец тестирования здесь, если вы хотите увидеть этот новый подход в действии по сравнению с тем, что я делал изначально.

Я по-прежнему открыт для других предложений, но это огромный шаг вперед для этого приложения.

Ответ 2

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

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

Как первый тест, что произойдет, если вы попробуете следующее?

void main()
{
    float distanceFromCenter = length(impostorSpaceCoordinate);

    // the step function doesn't count as a conditional
    float inCircleMultiplier = step(distanceFromCenter, 1.0);

    float calculatedDepth = sqrt(1.0 - distanceFromCenter * distanceFromCenter * inCircleMultiplier);
    mediump float currentDepthValue = normalizedDepth - adjustedSphereRadius * calculatedDepth;

    // Inlined color encoding for the depth values
    float ceiledValue = ceil(currentDepthValue * 765.0) * inCircleMultiplier;

    vec3 intDepthValue = (vec3(ceiledValue) * scaleDownFactor) - (stepValues * inCircleMultiplier);

     // use the result of the step to combine results
    gl_FragColor = vec4(1.0 - inCircleMultiplier) + vec4(intDepthValue, inCircleMultiplier);

}

Ответ 3

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

  • Сам шейдер выполняет некоторую потенциально избыточную работу. Длина вектора, вероятно, будет рассчитываться как sqrt(dot(vector, vector)). Вы не нуждаетесь в sqrt, чтобы отклонять фрагменты вне круга, и youre squaring длину, чтобы вычислить глубину, во всяком случае. Кроме того, вы посмотрели, действительно ли требуется явное квантование значений глубины, или вы можете избавиться от использования преобразования аппаратных средств с плавающей точкой в ​​целое число для фреймбуфера (потенциально с дополнительным уклоном, чтобы убедиться, что ваш квази -depth тесты появляются позже)?

  • Многие фрагменты тривиально за пределами круга. Только π/4 области рисования квадов вы создаете полезные значения глубины. На данный момент я предполагаю, что ваше приложение сильно искажено в отношении обработки фрагментов, поэтому вы можете рассмотреть возможность увеличения количества вершин, которые вы рисуете, в обмен на уменьшение площади, которую вы должны затенять. Поскольку вы рисуете сферы через орфографическую проекцию, любой ограничивающий регулярный многоугольник будет делать, хотя вам может потребоваться немного дополнительного размера в зависимости от уровня масштабирования, чтобы убедиться, что вы растрируете достаточное количество пикселей.

  • Многие фрагменты тривиально закупорены другими фрагментами.. Как отмечали другие, вы не используете аппаратную проверку глубины и, следовательно, не в полной мере используете возможности TBDR, чтобы убивать работу затенения раньше, Если вы уже реализовали что-то для 2), все, что вам нужно сделать, это нарисовать вписанный правильный многоугольник на максимальной глубине, которую вы можете создать (плоскость через середину сферы), и нарисовать реальный многоугольник на минимальной глубине ( фронт сферы). Сообщения Tommys и rotoglups уже содержат специфику вектора состояния.

Обратите внимание, что 2) и 3) применимы и к вашим лучам трассировки лучей.

Ответ 4

Я вообще не специалист по мобильной платформе, но я думаю, что вас укусит, что:

  • Ваш шейдер глубины довольно дорог
  • перегрузите массивный переполненный проход глубины при отключении GL_DEPTH test

Не будет ли полезен дополнительный проход, сделанный до теста глубины?

Этот проход мог выполнить предварительную заливку GL_DEPTH, например, путем рисования каждой сферы, представленной в виде камеры с четырех сторон (или куба, который может быть проще настроить), и содержащегося в соответствующей сфере. Этот проход можно рисовать без цветовой маски или фрагментарного шейдера, только с помощью GL_DEPTH_TEST и glDepthMask. На настольных платформах эти виды пропускаются быстрее, чем цвета + глубины.

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

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