Перевод статьи Брайанта Фрэнсиса — старшего редактора gamedeveloper.com

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

Различные шейдеры в игровом движке Unreal
Материалы созданные в Unreal

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

Развеиваем мифы

Технический специалист по связям с разработчиками Epic Games — Мэтт Озталай совсем недавно поднял эту мысль в своей презентации на Game Developer Talks под названием «Исследование и развеивание мифов о шейдерах… с помощью науки!».

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

«Нужно ли использовать LUT (таблицу преобразований) вместо полинома? Нужно ли всегда превращать плавающие значения в вектор, прежде чем выполнять над ними математические операции? Возможно! Но если вы можете перепроверить свои вычисления, прежде чем нажимать кнопку «commit», почему бы не сделать это?»

Вот несколько кратких уроков из выступления Озталая.

Является ли количество инструкций эквивалентом производительности?

В любом ПО количество инструкций — это общее число их выполнений, которое содержится в программе. Озталай признался, что это «трудный» миф, ведь в Unreal Engine от Epic Games… количество инструкций отображается в нескольких ключевых местах.

«К сожалению, Epic Games не рассказывают всей истории. HLSL-инструкции — это лишь часть большого пути к тому, что вы хотите увидеть на экране»

  • Обычно художник выстаивает последовательный граф и преобразует его в HLSL.
  • Он становится обобщенным кодом на Ассемблере.
  • Ну и в конце его прогоняют через конкретную видеокарту, трансформируя в аппаратно-специфический байт-код для выполнения на графическом процессоре.

Но, как сказал Озталай, GPU «не очень любят возвращаться назад», чтобы выполнить один набор инструкций. Поэтому не все инструкции HLSL компилируются в одинаковое количество операций или циклов байт-кода.

Озталай придумал специальную метафору, на случай, если ваш мозг немного сломался от всех этих терминов для программирования (мой то уж точно, я ведь писатель, а не программист или технический художник):

«Вы можете думать о шейдерах как о рецепте. Рецепте печенья на шести шагов, которое делается всего за час. Но затем вы берете другой рецепт какого-нибудь другого печенья, который также состоит из шести шагов, но занимает шесть часов. Если вы возьмите эти рецепты и разложите на составные части, то заметите, что каждый шаг не занимает эквивалентное количество времени. К примеру, мне требуется меньше времени, чтобы нарезать лук кубиками, чем маленькому ребенку, но наливать масло в голландскую печь мы будем с одинаковой скоростью»

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

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

Является ли умножение более производительным, чем деление?

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

Озталай каждый раз намеренно проверял самый «наихудший сценарий» в каждом из своих тестов. Так он смог лучше разобраться в работе собственного кода. В конце-концов эксперимент привел его к проверке веса умножения по сравнению с делением.

«Я всегда понимал, что деление в программе — более дорогая операция, чем умножение. Но я никогда не подвергал это заявление сомнению»

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

«Тригонометрические операции в системе материалов Unreal используют период, который равен единице. Это означает, что один радиан равен одному градусу, или обратному 360, или повторению .0027. Так как я всегда понимал, что деление дороже, чем умножение, я выбирал именно второй вариант — умножал значение Gries на это бессмысленное повторяющееся число .0027. А мог бы просто разделить его на 360, что было бы более читабельно и разборчиво»

В ходе тестирования образца кода, который Озталай показал нам во время презентации, получилось два результата: деление на 360 и умножение на .0027 с повторением. Оба значения в итоге были «довольно близки». Но почему так?

Сравнительные тесты в Unreal Engine
Результаты умножения / деления

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

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

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

Является ли стоимость узла pow экспоненциальной?

Мы завершим этот обзор выступления Озталая небольшим рассказом о том, как затраты на операцию pow — повышение X до степени Y — экспоненциально связаны со стоимостью Y.

«Это вроде как имеет смысл, верно? Если вам дана ограниченная серия операций байт-кода, то вполне логично, что для возведения значения в степень, вам нужно умножить его в определенное количество раз»

В примере Озталая, вы берете 2^8 — и зацикливаете фактическую математику, которая стоит за этим уравнением. Интересно, что результат получается «довольно странным». На графике видно, как значение росло по экспоненте, но неожиданно выровнялось на 64.

«Если посмотреть на время, когда это произошло, то становится немного интересно, потому что это одно и то же число. Настоящие экспоненциальные графики являются суммарными — они никогда не выравниваются, они выравниваются только на бесконечности»

Exponential Pow
Разбивка затрат на операцию Pow

Как вы можете видеть выше, результаты получились абсолютно плоскими. Так что же случилось?

Озталай вновь покопался в байт-коде и проверил результирующий ассемблерный код в шейдерном плейграунде. По итогу он обнаружил, что код доходил до 16-й степени и выполнял математический трюк экспоненцирования: вычислял логарифм базового значения, затем умножал логарифм базового значения на экспоненту, а затем увеличивал «e» до этой степени.

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

«Я поступал так всегда и видел, как так поступают другие. Поэтому даже не думал о ложности мифа»

Озталай продемонстрировал еще целую кучу математических выкладок и пришел к одному простому, но занятному выводу:

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

Иван Гвоздь