Vulkan API — урок 24. Фиксированные функции (ч2) (+листинг)

Multisampling

Структура VkPipelineMultisampleStateCreateInfo настраивает multisampling – один из вариантов сграживания (anti-aliasing). Он работает по принципу объеденения результатов fragment shader для нескольких полигонов (многоугольников), растеризируемых в одни и те же пиксели. В основном это имеет место быть на гранях, где зачастую и вылазиют наиболее заметные артефакты при сглаживании. Благодаря чему не нужно вызывать fragment shader несколько раз, если только лишь полигоны влияют на пиксели, это получается значительно дешевле, чем рендер в более  высоком разрешении и затем его уменьшение. Потребует включения функции GPU.

VkPipelineMultisampleStateCreateInfo multisampling = {};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f; // Optional
multisampling.pSampleMask = nullptr; /// Optional
multisampling.alphaToCoverageEnable = VK_FALSE; // Optional
multisampling.alphaToOneEnable = VK_FALSE; // Optional

В основных уроках не будет использоваться multisampling, но в будущем еще к нему вернемся.

Глубина и проверка трафарета (Depth and stencil testing)

Если вы собираетесь использовать глубину и/или буфер трафарета, вам так же нужно настроить глубину и проверку трафарета используя VkPipelineDepthStencilStateCreateInfo. В текущей, описываеммой здесь задаче необходимость в этом отсутствует, потому значение структуры будет nullptr, а к теме вернемся после того, как выведем треугольник на экран.

Ну а т.к. начинающие кодеры наврядли сталкивались с понятием Stencil Buffer, то вот ссылочка для краткого введения.

Color blending

После того как fragment shader возвращает цвет, его нужно смешать с цветом, который уже имеется в фреймбуфере. Этот процесс так же известен как color blending и может выполняться твумя способами:

  • Смешать старый и новый цвета, для получения конечного цвета
  • Смешать значения старого и нового цвета используя побитовую операцию

Имеется два типа структур для настройки смешения цветов. Первая структура VkPipelineColorBlendAttachmentState, содержит конфигурацию, соответствующую прилагаемому буферу. Вторая структура, содержащая глобальные настройки смешения цветов. В нашем случае имеется только один фреймбуфер:

VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional

Эта структура для каждого фреймбуфера (per-framebuffer) позволяет настроить первый способ смешения цветов. Операции, которые будут выполняться лучше всего продемонстрированны в данной процедуре:

if (blendEnable) {
    finalColor.rgb = (srcColorBlendFactor * newColor.rgb) <colorBlendOp> (dstColorBlendFactor * oldColor.rgb);
    finalColor.a = (srcAlphaBlendFactor * newColor.a) <alphaBlendOp> (dstAlphaBlendFactor * oldColor.a);
} else {
    finalColor = newColor;
}

finalColor = finalColor & colorWriteMask;

Если blendEnable установлено в VK_FALSE, тогда новый цвет из fragment shader будет передан без изменений. Иначе будут выполнены две операции смешения для вычисления нового цвета. К результирующему цвету «добавляется» colorWriteMask для определения какие каналы будут использоваться.

Наиболее распространенный вариант смешения цветов – реализация альфа-смешивания, в которой новый цвет смешивается со старым на основе затенения (непрозрачности, opacity). В итоге finalColor рассчитывается таким способом:

finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;

Это достигается следующими командами:

colorBlendAttachment.blendEnable = VK_TRUE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;

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

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

VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f; // Optional
colorBlending.blendConstants[1] = 0.0f; // Optional
colorBlending.blendConstants[2] = 0.0f; // Optional
colorBlending.blendConstants[3] = 0.0f; // Optional

Если вы хотите использовать второй метод (побитовое сочетание), тогда вам впоставьте значение VK_TRUE для logicOpEnable. Побитовая операция может быть задана в поле logicOp. Обратите внимание, что это отключит мервый метод, как если установить VK_FALSE для blendEnable для каждого подключенного фреймбуфера. colorWriteMask  будет так же использоваться в данном режиме, что бы определить какие каналы в фреймбуфере будут затронуты. Кроме того, можно отключить оба режима, как пока и будет сделано, тогда цвета будут записаны в фреймбуфер неизменными.

Динамическое состояние (Dynamic state)

 Некоторое количество состояний, которые мы определили в предыдущих структурах, может быть изменено без пересоздания всего конвейера. Например – размер окна, ширина линии, постоянные смешения. Если вы хотите сделать это, тогда вам нужно заполнить структуру VkPipelineDynamicStateCreateInfo подобным образом:

VkDynamicState dynamicStates[] = {
    VK_DYNAMIC_STATE_VIEWPORT,
    VK_DYNAMIC_STATE_LINE_WIDTH
};

VkPipelineDynamicStateCreateInfo dynamicState = {};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = 2;
dynamicState.pDynamicStates = dynamicStates;

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

Инструменты конвейера (Pipeline layout)

Вы можете использовать значения uniform в шейдерах, которые глобальны, аналогично переменным dynamic state, и могут быть изменены во время отрисовки, и тем самым изменить поведение шейдеров без необходимости их пересоздания. Обычно они используются для передачи матрицы преобразований в vertex shader, или создания шаблонов текстур в fragment shader.

Эти значения должны быть определены в процессе создания конвейера в объекте VkPipelineLayout. Даже если мы пока что не будем использовать их, мы все равно должны создать пустой pipeline layout.

Создадим новый член класса, для хранения объекта и для возможности ссылаться на него из различных функций. Не забываем про VDeleter:

VDeleter<VkPipelineLayout> pipelineLayout{device, vkDestroyPipelineLayout};

Затем создаем объект в функции createGraphicsPipeline:

VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0; // Optional
pipelineLayoutInfo.pSetLayouts = nullptr; // Optional
pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional
pipelineLayoutInfo.pPushConstantRanges = 0; // Optional

if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr,
    pipelineLayout.replace()) != VK_SUCCESS) {
    throw std::runtime_error("failed to create pipeline layout!");
}

Структура так же определяет константы (push constants), которые являются еще одним способом передачи динамических значений для шейдеров.

Итоги

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

До создания конвейера осталось разобраться только лишь с проходом рендеринга, о чем в следующем уроке.

Так же хочу обратить внимание на все возрастающее количество терминов, дублирующихся своими английскими аналогами. Не смотря на поиски какого-то определенного и «правильного» перевода в интернетах, зачастую он не увенчивается успехом, и потому вставляю к себе наиболее распространенный/понравившийся вариант, что не есть гуд, но все же иного пути не нахожу. И самое главное – любому читающему данные уроки и желающему продолжить развиваться в данной сфере придется, к сожалению, со временем переходить сначала на спецификации на английском языке, а потом, возможно, и на литературу. В итоге привычка видеть термины в таком виде (все равно придется привыкать, ведь код не на русском языке пишется, а с 1С работал долгое время и никому особо не рекомендую) окажется полезной в один прекрасный день. Не смотря на несколько усложнившийся перевод доделать базовую часть и вывести треугольник пока все так же планирую на этой неделе.

Ну и листинг.

Main Admin

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *