Slang – универсальный шейдерный язык

Что выглядит читабельнее? Это:

struct VertexOutput
{
    @builtin(position) position: vec4<f32>,
    @location(0) fragmentPosition: vec4<f32>
};

@vertex
fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput
{
    let x = f32(i32(vertexIndex) - 1);
    let y = f32(i32(vertexIndex & 1u) * 2 - 1);
    var output: VertexOutput;
    output.position = vec4<f32>(x * 0.5, y * 0.5, 0.0, 1.0);
    output.fragmentPosition = 0.5 * (vec4<f32>(x, y, 0.0, 1.0) + 1.0);
    return output;
}

…или это:

struct VertexOutput
{
    float4 position: SV_Position;
    float4 fragmentPosition;
};

[shader("vertex")]
VertexOutput vertexMain(uint vertexIndex: SV_VertexID)
{
    let x = float(int(vertexIndex) - 1);
    let y = float(int(vertexIndex & 1u) * 2 - 1);
    VertexOutput output;
    output.position = float4(x * 0.5, y * 0.5, 0.0, 1.0);
    output.fragmentPosition = 0.5 * (float4(x, y, 0.0, 1.0) + 1.0);
    return output;
}

Второй листинг, на мой взгляд, выигрывает сравнение. Это код на Slang, HLSL-подобном языке, который позиционируется в качестве платформонезависимой основы для написания одних и тех же шейдеров под любые графические API, включая D3D12, Vulkan, Metal, OpenGL, а с недавних пор и WebGPU. Идея, конечно, не новая – если вы писали шейдеры в нулевые, то, наверное, помните Cg от NVIDIA – но с со времен попыток написать универсальный компилятор в низкоуровневые шейдерные языки утекло немало воды и сменилось несколько поколений графического железа. Сейчас, в эпоху Vulkan и SPIR-V, кросс-компиляция шейдеров актуальна как никогда: старичок GLSL уже сдает позиции, для новых стандартов создаются новые языки – индустрия переживает очередной этап фрагментации.

Я уже писал о своих впечатлениях от WGSL, встроенного шейдерного языка WebGPU, и многие его конструкции мне до сих кажутся спорными и неудобными. Особенно бесят типы вида vec4<f32>. Не так давно поддержка WGSL была добавлена в компилятор Slang, в связи с чем я теперь всерьез рассматриваю этот язык как основной для создания графического движка на WebGPU.

Главная киллер-фича Slang – это, конечно, модули. Причем, что интересно, есть и препроцессор а ля C с директивами #include, #ifdef и др. От HLSL и GLSL язык выгодно отличается большим набором фич, присущих современным высокоуровневым языкам: поддержкой вывода типов, пространств имен, функций-членов структур (с неизменяемым по умолчанию неявным this – по-моему, отличная идея!), а также конструкторов, геттеров/сеттеров и даже перегрузки операторов. Есть дженерики, интерфейсы, кортежи. Интересен тип Optional, который дополняет любой другой тип поддержкой значения none – чтобы можно было указать отсутствие какого-либо значения. Для SPIR-V и CUDA в языке есть ограниченная поддержка указателей. Очень полезный инструмент – декоратор ForceInline, который заменяет вызов функции подстановкой ее кода. Наконец, в языке есть автоматическое дифференцирование, которое используется в задачах машинного обучения.

Старую как мир проблему некоммутативности умножения матриц и векторов в Slang решили следующим образом: оператор * всегда означает произведение двух матриц, как принято в математике. Чтобы трансформировать вектор матрицей, нужно вместо m * v писать mul(v, m). Для такого старого ветерана OpenGL, как я, слегка непривычно, но жить можно 🙂

Репозиторий

Документация по языку

Dagon 0.9.0 и dlib 0.15.0

На днях вышли новые версии Dagon и dlib – 0.9.0 и 0.15.0 соответственно. Релиз Dagon – самый крупный со времени портирования движка на современный OpenGL: он содержит 190 коммитов, практически весь рендерер был переписан заново. Вот краткий список изменений:

  • Состоялся переход с прямого рендеринга (forward) на отложенный (deferred). Это должно было рано или поздно произойти, все современные движки в той или иной мере используют отложенные эффекты. В Dagon этот рефакторинг серьезно улучшил производительность и позволил полноценно реализовать SSAO. Количество динамических источников света теперь ограничено только fillrate’ом видеокарты. Ценой стало повышенное потребление видеопамяти, также при отложенном рендеринге невозможно эффективно реализовать прозрачность, поэтому все прозрачные объекты рендерятся в прямом fallback-режиме, и для них не учитываются точечные источники света.
  • Система шейдеров была переписана с нуля. Старая система с бэкендами материалов была заменена на шейдеры, все избыточные классы материалов были объединены в один, создавать новые материалы и передавать шейдерам параметры стало значительно проще. Все шейдеры используют GLSL 4.0 Core, ветвление в шейдерах было заменено на uniform-подпрограммы, что повысило производительность и сделало код более читаемым. Материалы с пользовательскими шейдерами также рендерятся в прямом режиме.
  • Была серьезно улучшена система частиц: появилась поддержка мягких частиц, освещения, отбрасывания теней. Также теперь можно создавать несколько эмиттеров в каждой системе. Опционально в качестве частиц можно рендерить любые объекты вместо биллбордов.
  • Добавлена поддержка Screen space ambient occlusion (SSAO).
  • Добавлены шейдер воды по методу Себастьяна Лагарда и модель неба по Рэлею (экспериментально).
  • Dagon теперь использует BindBC вместо Derelict.
  • Добавлена поддержка автоматического деплоя: при каждой сборке Dub копирует в проект все необходимые файлы для запуска, включая DLL’ки под Windows.

Полный список изменений смотрите на странице релиза. Также было обновлено и демонстрационное приложение.

Изменения в dlib по большей части носят исправляющий и косметический характер: я решил постепенно избавиться от устаревших компонентов, в связи с чем следующие модули пометил как deprecated:

  • dlib.image.parallel
  • dlib.math.fixed
  • dlib.functional.quantifiers
  • функции map, filter, reduce в dlib.functional.range.

Следующие модули были удалены:

  • dlib.container.aarray
  • dlib.math.affine
  • dlib.core.fiber (временно перенесен в отдельную ветку до завершения windows-порта)

Нововведения включают:

  • dlib.text.unmanagedstring – альтернативная реализация строк, не использующая сборщик мусора
  • Улучшенные декодеры текстовых кодировок, модуль dlib.text.encodings.
  • Также dlib теперь может быть собран компилятором GDC (за исключением модуля dlib.math.sse, который в этом случае не будет доступен).

Полный список изменений – на странице релиза.

Интерполяция на основе сигмоиды

Для одного из шейдеров на GLSL мне потребовалась “умная” интерполяция цветов с возможностью изменять резкость перехода от одного значения к другому – от полностью плавного (линейного) до дискретного. В итоге получилась вот такая функция, которую я вывел на основе рациональной сигмоиды – может быть, кому-то пригодится:

float sigmoid(float x, float k)
{
    return (x + x * k - k * 0.5 - 0.5) / 
        (abs(x * k * 4.0 - k * 2.0) - k + 1.0) + 0.5;
}

При k = 0 функция обращается в линейную, при k = 1 – разрывается в точке 0,5. Вы можете увидеть, как это работает, при помощи интерактивного графика на Desmos: https://www.desmos.com/calculator/s0cwcrtzvs.

Результат этой функции передается в привычный mix – то есть, вместо mix(c1, c2, t) пишем mix(c1, c2, sigmoid(t, k)). Получится, например, такое:

(градиенты гамма-скорректированы)

Тени на шейдерах

Наконец-то переписал тени с использованием FBO и GLSL:

Следующим шагом будет variance shadow mapping (VSM).

Bump mapping в Atrium

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

Сборка для Windows (2,68 МБ)
Сборка для Linux (3,1 МБ)

Шейдеры написаны на GLSL и требуют наличия расширения ARB_shading_language_100 (или OpenGL 2.0), но это не является критической необходимостью – игра будет работать и со старыми видеокартами без поддержки шейдеров.