std140: статическая валидация структур

std140 – самый платформонезависимый лейаут uniform-блоков. Если вы передаете параметры в шейдер структурами, а не по одному, то лучше использовать именно его. Конечно, у переносимости есть своя цена: стандарт предусматривает несколько ограничений, самое известное из которых – выравнивание по 16 байтам. То есть, все поля структуры должны быть по размеру кратны 16 байтам. Например, если это вектор – то vec4, если матрица – то mat4. Некратные 16 байтам типы (float, int и др.) также можно использовать, но их необходимо выравнивать вручную – то есть, одиночный float пойдет все равно как vec4. Использование невыровненных данных приводит к тому, что в шейдере считываются неправильные значения, и это вызывает недоумение у начинающих.

Благодаря CTFE и статической интроспекции в D можно проверять структуры на соответствие этому правилу на этапе компиляции – все необходимое есть в std.traits:

import std.traits;

bool fieldsAligned(T, alias numBytes)()
{
    static if (is(T == struct))
    {
        alias FieldTypes = Fields!T;
        
        static foreach(FT; FieldTypes)
        {
            static if (FT.sizeof % numBytes != 0)
                return false;
        }
        
        return true;
    }
    else return false;
}

alias Std140Compliant(T) = fieldsAligned!(T, 16);

Теперь можно делать так:

import dlib.math.vector;
import dlib.math.matrix;

struct Uniforms
{
    Matrix4x4f modelViewMatrix;
    Matrix4x4f normalMatrix;
    Vector4f worldPosition;
    float someParameter; // Will not compile, use Vector4f instead
}

static assert(Std140Compliant!Uniforms,
    Uniforms.stringof ~ " does not conform to std140 layout");

Возникает вопрос, почему бы просто не делать align(16)? Можно!

struct AlignedUniforms
{
  align(16):
    float a;
    Vector3f b;
    Matrix4x4f c;
}

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

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, как я, слегка непривычно, но жить можно 🙂

Репозиторий

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

Первое знакомство с WGSL

У тех, кто работает с низкоуровневой графикой, сегодня на слуху WebGPU – новый кроссплатформенный API для доступа к возможностям современных видеокарт. WebGPU призван объединить Vulkan, Metal и D3D12 под унифицированным набором функций и станет не просто веб-стандартом, но и, в перспективе, неплохой заменой OpenGL: реализации этого API уже существуют в виде рабочих прототипов wgpu-native от Mozilla и Dawn от Google – любой может использовать их в своих собственных приложениях.

WebGPU имеет сравнительно простую архитектуру, доступную для понимания “простыми смертными” практически с первого прочтения заголовочного файла. Единственной проблемой до недавнего времени было отсутствие консенсуса по шейдерному языку – существующие реализации WebGPU использовали двоичное промежуточное представление SPIR-V от Khronos, а Apple настаивала на текстовом языке на основе WSL. Компромиссом стал WGSL (WebGPU Shading Language), высокоуровневый язык со строго определенной семантикой и буквальной трансляцией в/из SPIR-V. Многие разработчики оказались недовольны, так как SPIR-V уже успел стать привычным решением и оброс инструментами – сегодня можно компилировать в SPIR-V код на всех языках предыдущих поколений. Однако я вижу больше преимуществ, чем недостатков – перечислю некоторые из них.

  • Использование SPIR-V усложняет жизнь при создании игрового движка, требует внедрения дополнительной стадии компиляции шейдеров на стороне разработчика. Референсным компилятором шейдеров считатеся GLSLang от Khronos, но его довольно трудно встроить в приложение как библиотеку, особенно если вы не пишете на C++ – приходится использовать GLSLang как приложение, и это усложняет тулчейн разработки, если нужна кроссплатформенность. Встроенный в API высокоуровневый язык решает эту проблему.
  • WGSL разрабатывается как текстовый аналог SPIR-V – они имеют общий набор возможностей. Это значит, что не будет повторения ситуации с GLSL, когда язык по-разному обрабатывается в компиляторах от различных поставщиков. Сохраняется главное преимущество SPIR-V при высоком удобстве использования.
  • Vulkan-диалект GLSL 4.60, являющийся де-факто стандартным языком под SPIR-V, имеет множество костылей и архаизмов – у WGSL более продуманный синтаксис, лишенный неявности и многозначности.

Синтаксис WGSL имеет много общего с Rust, особенно заголовки функций:

fn someFunc(x: i32) -> i32 {
    //...
}

Типы объявляются через двоеточие после идентификатора, переменные – при помощи ключевого слова let, константность подразумевается по умолчанию. Для изменяемых переменных есть ключевое слово var. Система типов также пришла из вселенной Rust. Векторные типы имеют форму vec4<f32> (вместо простого vec4), что позволяет явным образом указать битовость используемых чисел. При этом можно объявить type vec4f = vec4<f32>; и писать коротко, если вам так привычнее.

Кстати, очень порадовало, что есть вывод типов – можно не указывать тип переменной, если она тут же инициализируется:

let a = vec4<f32>(0.0, 0.0, 0.0, 1.0);

Вместо ключевого слова layout – нотация с использованием двойных квадратных скобок, внутри которых записываются атрибуты location и др.:

[[location(0)]] position: vec4<f32>;

Встроенные переменные конвейера обозначаются атрибутом builtin, что весьма удобно при объявлении структур для хранения промежуточных результатов:

struct VertexOutput
{
    [[builtin(position)]] position: vec4<f32>;
    [[location(0)]] color: vec4<f32>;
};

Сравните это с GLSL, где для встроенных переменных используется зарезервированный префикс gl_.

Структуры, являющиеся uniform-блоками, помечаются атрибутом block:

[[block]] struct Uniforms {
    //...
};

Прямым аналогом вулкановских set и binding являются атрибуты group и binding.

Vulkan/GLSL:

layout(set=0, binding=0) uniform Uniforms uniforms;

WGSL:

[[group(0), binding(0)]] var uniforms: Uniforms;

Программы на WGSL можно не разделять на два текста – вершинный и фрагментный шейдеры можно хранить в одном файле и, таким образом, использовать общие объявления. Для этого используется атрибут stage. Названия самих входных точек могут быть произвольными, но чаще всего в примерах используют vs_main и fs_main.

[[stage(vertex)]]
fn vs_main() -> VertexOutput
{
    //...
}

[[stage(fragment)]]
fn fs_main(input: FragmentInput) -> [[location(0)]] vec4<f32>
{
    //...
}

Очень непривычно в WGSL записываются циклы:

var i = i32(0);
loop {
    break if (i == 5);
    //...
    continuing {
        i = i + 1;
    }
}

Впрочем, на момент написания статьи обсуждается возможность поддержки классического for.

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

Шейдер ландшафта и декали

Шейдер ландшафта и декали

Наконец-то добавил в Dagon специализированный материал для объекта Terrain – 4-канальный шейдер ландшафта, поддерживающий карты нормалей.

До этого также появилась поддержка отложенного рендеринга декалей (deferred decals) – текстур, проецируемых на статические объекты. При помощи декалей можно сделать на поверхностях различные следы, пятна, надписи, граффити, мелкий мусор и т.д. Для декалей поддерживаются карты нормалей, PBR и излучения света, так что они позволяют разнообразить сцену с высокой степенью реалистичности. Реализованы они путем блиттинга текстур в G-буфер – таким образом, декали могут быть отрисованы поверх уже отрендеренной геометрии с возможность смешивания цвета, нормалей и других атрибутов поверхности по альфа-маске. В демо-приложении Dagon декали используются для рендеринга следов игрока на земле:

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, который в этом случае не будет доступен).

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