Chelovek & Tvoidrug
Привет, а ты когда-нибудь пытался превратить сложный процедурный шейдер в что-то с минимальным кодом и высокой производительностью, но при этом чтобы выглядело круто? Думаю, мы сможем кое-что интересное придумать.
Конечно, можно облегчить шейдер, если сосредоточиться на главном. Убери лишние вычисления, замени сложные функции на приближения, предварительно рассчитай статические значения. Сохраняй цикл быстрым, используй унифицированные буферы для констант, и дай видеокарте поработать на износ. Так ты получишь нужный результат, используя меньше инструкций. Давай выстроим план основных шагов и посмотрим, где можно сэкономить.
Отлично. Сначала перечисли все вызовы функций, потом отметь те, что выполняются каждый кадр. Затем замени sin/cos на таблицы соответствия или простые приближения. Далее, сложи константные выражения в единый буфер. И, наконец, профилируй цикл и удали ненужные ветвления. Начнём с первого шага.
Вот список наиболее часто используемых функций в сложных процедурных шейдерах:
- `sin`, `cos`, `tan`
- `pow`, `exp`, `log`
- `smoothstep`, `step`, `mix`
- `texture`, `texelFetch`
- `fract`, `floor`, `ceil`
- `length`, `dot`, `cross`
- `normalize`, `reflect`, `refract`
- `abs`, `sign`, `clamp`
- `rand`, `hash`, `noise` (самописные)
- `getCameraPos`, `getViewDir` (если используете свои вспомогательные функции)
- `computeLight`, `shadowMapLookup` (самописные)
- `calculateNormal`, `displace` (самописные)
Это полный перечень, который тебе нужно будет просмотреть с учётом использования в каждом кадре.
Отличный список, давай разделим работу: сложные тригонометрические функции (sin, cos, tan), экспоненты (pow, exp, log), выборки текстур и вызовы кастомного шума/хеша – это обычно самые проблемные места. Всё, что можно предварительно вычислить или приближённо оценить, подходит для оптимизации. Проверь ещё, действительно ли `normalize`, `reflect` и `refract` нужны на каждый пиксель, или их можно запечь в карту нормалей. Начнём помечать каждую функцию, ставя метки “каждый кадр” или “постоянная”, тогда уже будем решать, что можно убрать или заменить. Погружаемся в первую партию.
Милая, смотри:
sin – каждый кадр
cos – каждый кадр
tan – каждый кадр
pow – каждый кадр
exp – каждый кадр
log – каждый кадр
smoothstep – каждый кадр
step – каждый кадр
mix – каждый кадр
texture – каждый кадр
texelFetch – каждый кадр
fract – каждый кадр
floor – каждый кадр
ceil – каждый кадр
length – каждый кадр
dot – каждый кадр
cross – каждый кадр
normalize – каждый кадр (если нормаль не запечена)
reflect – каждый кадр (если не запечено)
refract – каждый кадр (если не запечено)
abs – каждый кадр
sign – каждый кадр
clamp – каждый кадр
rand – каждый кадр (или на основе seed)
hash – каждый кадр
noise – каждый кадр (или кэшировано)
getCameraPos – каждый кадр (камера меняется)
getViewDir – каждый кадр
computeLight – каждый кадр (изменения освещения)
shadowMapLookup – каждый кадр (изменения тени)
calculateNormal – каждый кадр (если нормаль не запечена)
displace – каждый кадр (если вершины анимированы)
Всё, что помечено "per‑frame" – кандидат на аппроксимацию или предварительный расчёт; константы можно перенести в uniforms или запечь.
Отлично, разбор! Теперь можно переходить к замене дорогих синусов и косинусов на более простые приближения, выносить константы в общие параметры и закэшировать этот шум в текстуру 2D. Потом профилируем цикл и посмотрим, какие ветки все еще тормозят. Давай начнем с первых десяти функций.
Первые десять: синус, косинус, тангенс, возведение в степень, экспонента, логарифм, smoothstep, step, mix, текстура. Замени синус/косинус полиномиальной функцией от 0 до 1 или небольшой таблицей значений; тангенс можно аппроксимировать или избегать, если можно использовать обратную величину. Для возведения в степень, если показатель постоянный – используй предварительно рассчитанную таблицу; иначе аппроксимируй exp(log(x)*y). Попробуй заменить экспоненту/логарифм одной цепочкой exp(log). Smoothstep/step/mix – дешевы, но выполняются на каждом пикселе; если аргументы статичные – попробуй включить их в uniform. Самая большая проблема – текстурные сэмплы, используй mipmap с меньшим разрешением или одиночный канал, если не нужны все данные. После этих изменений запусти профайлер и удали все неиспользуемые ветвления.
Отлично, давай прорисуем таблицы для синуса/косинуса и вообще исключим тангенс. Для возведения в степень – если показатель степени фиксирован, заранее рассчитаем небольшую таблицу. Попробуем объединить exp/log в одну функцию exp(log), если получится, а потом оптимизируем привязку текстур к одному каналу и уровню детализации. Потом запустим профилировщик и вычистим все ненужные участки кода. Начинаем?
Хорошо, заготовим небольшую таблицу на 256 элементов для синуса и косинуса, будем менять их местами по ходу дела, убираем тангенс, пропекаем таблицу экспонент, объединяем экспоненту и логарифм, уменьшим текстуру до одного уровня детализации, а потом запустим профайлер и вычистим неиспользуемые участки кода. Приступаем.
Ладно, давай настроим таблицы, поменяем синус на косинус, убираем тангенс, вычисляем степень, упрощаем экспоненту с логарифмом, сжимаем текстуру до одного канала, запускаем профилировщик и отсекаем то, что не имеет значения. Занялась.