Как я могу улучшить производительность моего собственного поколения глубины текстуры OpenGL ES 2.0? - PullRequest
39 голосов
/ 19 мая 2011

У меня есть приложение с открытым исходным кодом для 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, но я не могу придумать лучшего способа сделать это.

Мойшейдер глубинных фрагментов для сфер (самый распространенный элемент отображения), похоже, лежит в основе этого узкого места (использование рендерера в инструментах привязано к 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 циклов графического процессора, в худшем - 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);
}

на iPad 1 занимает 18 - 35 мс, а на iPhone 4 - всего 1,7 - 2,4 мс. Расчетное количество циклов графического процессора для этого шейдера составляет 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?

Ответы [ 4 ]

19 голосов
/ 30 мая 2011

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

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

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

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

Layered spheres and opacity testing

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

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

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

Как ни странно, когда я пытался уточнить это далее, используя вставные и описанные восьмиугольники, которые должны охватывать ~ 17На меньшее количество пикселей при рисовании и более эффективная блокировка фрагментов, производительность была на самом деле хуже, чем при использовании простых квадратов для этого.Использование тайлера в худшем случае было все еще менее 60%, поэтому, возможно, большая геометрия приводила к большему количеству промахов в кеше.

РЕДАКТИРОВАТЬ (31.05.2011):

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

Rasterization optimizing octagons

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

РЕДАКТИРОВАТЬ 2 (31.05.2011):

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

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);
}

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

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

9 голосов
/ 19 мая 2011

На настольном компьютере многие ранние программируемые устройства имели дело с тем, что, хотя они могли обрабатывать 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);

}
8 голосов
/ 28 мая 2011

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

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

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

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

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

2 голосов
/ 20 мая 2011

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

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

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

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

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

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

...