не будь такой квадратной...

Введение

Во второй части серии N мы рассмотрим, как визуализировать сетку с помощью печально известной графической библиотеки Metal. Обязательно ознакомьтесь с моим предыдущим руководством, потому что мы будем опираться на эти концепции.

Конвейер рендеринга

Чтобы отобразить изображения на экране, нам сначала нужно настроить конвейер рендеринга, чтобы описать, как отрисовываются изображения. В типичном средстве визуализации на основе Metal объект MTL::RenderPipeLineState используется, чтобы указать графическому процессору, как следует рисовать геометрические данные. Объект MTL::RenderPipeLineState определяет состояние графики и функции вершинного и фрагментного шейдера. Создание объекта является дорогостоящим процессом, поэтому рекомендуется создавать их перед выполнением основного цикла выполнения или всякий раз, когда выполняется операция загрузки. После того, как мы настроим все необходимые данные, которые нужны объекту MTL::RenderPipeLineState, и зафиксируем командный буфер, конвейер рендеринга запустится.

Конвейер рендеринга — это процесс операций, через которые проходят данные вершин, в ходе которых координаты преобразуются в разные координатные пространства. В Metal этапы обработки вершин и фрагментов являются единственными программируемыми этапами шейдера.

Извлечение вершин

Целью этапа Выборка вершин является чтение примитивных данных (треугольников, линий и точек) из металлических буферов и передача их в модуль Планировщик.

Обработка вершин

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

Примитивная сборка

На этапе Сборка примитива групповой блок вершин, отправленный из обработки вершин, собирается для соответствия назначенному примитиву. Metal API предоставляет 5 типов примитивов: заплатка, точка, линия, линейная полоса, треугольник и треугольная полоса.

Растеризация

На этом этапе растеризатор вычисляет наклон между любыми двумя точками вершин и строит группу наклонов для построения примитива. Затем растеризатор ищет, что видно, а что нет. Затем Z and Stencil Test удаляет невидимые фрагменты. Окончательно. Интерполятор берет оставшиеся видимые фрагменты и генерирует атрибуты фрагментов.

Обработка фрагментов

На этапе Обработка фрагментов все фрагменты интерполируются вместе, чтобы создать окончательный цвет пикселя в буфере кадра.

Кадровый буфер

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

Визуализатор, часть 2:Electric Boogaloo

Давайте взглянем на наш новый интерфейс рендерера.

В нашем классе Renderer есть две новые функции: BuildShaders() для компиляции и хранения наших шейдерных программ и BuildBuffers() для построения некоторых данных тестовых вершин. У нас также есть новый член данных типа RenderPass, который будет представлять один проход рендеринга. Концепцию прохода рендеринга можно представить как набор вложений (буферов, текстур и т. д.) с командами, описывающими, как они используются и какие работы по рендерингу необходимо выполнить.

Простой объект RenderPass может выглядеть так:

В классе RenderPass мы храним указатель на объект MTL::RenderPipelineState и объект Mesh, содержащий указатели на два MTL::Buffer. с. Идея наличия объекта RenderPass состоит в том, чтобы хранить различные состояния, в которых может оказаться конвейер средства визуализации. В конструкторе Renderer мы делаем вызовы BuildShaders() & BuildBuffers() для создания шейдерных программ и построения буферов вершин с данными.

Создание шейдеров, и я не говорю о солнцезащитных очках.

Metal использует язык шейдеров высокого уровня, известный как Metal Shading Language (MSL), для определения вершинных и фрагментных шейдеров. MSL основан на C++14, поэтому синтаксис должен быть похож на нас, программистов C++.

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

Похоже, здесь происходит много сумасшествия, давайте разберемся. Сначала мы включаем заголовок metal_stdlib, чтобы использовать функции и перечисления MSLlib. В строке 4 мы определяем структуру VertexOut, которая содержит два типа float4: положение и цвет. Объявление позиции содержит спецификатор атрибута[[position]], который указывает растеризатору использовать эту переменную в качестве нормализованной позиции в пространстве экрана для выполнения растеризации/интерполяции. В строке 9 мы указываем функцию вершинного шейдера. Тип возвращаемого значения этой функции — структура, которую мы создали ранее. После того, как мы определили возвращаемый тип, мы определяем тип шейдерной функции, в данном случае это «вершина». Далее следует имя функции «vertexMain». Функция принимает 3 аргумента. Первый аргумент определяет номер вершины, с которой мы работаем. Второй аргумент — это указатель устройства на адрес памяти графического процессора, содержащий буфер данных о положении. Третий аргумент — это еще один указатель устройства на адрес памяти графического процессора, содержащий буфер данных цвета. Внутри функции мы возвращаем данные о позиции и цвете, заполненные в объекте VertexOut из адресов буфера. В строке 19 мы создаем фрагментный шейдер, который принимает объект VertexOut и возвращает внутри него данные о цвете для интерполяции.

Теперь, когда мы получили исходный код шейдера, давайте взглянем на функцию BuildShader().

Давайте разберем, что здесь происходит. В строке 5 мы передаем путь к файлу шейдера, который хотим скомпилировать, и получаем строку, содержащую содержимое исходного кода. В строке 10 мы создаем объект MTL::Library, который содержит скомпилированный исходный код металла. Идея здесь в том, что мы превращаем скомпилированную программу в «библиотеку», имеющую функции, которые может вызывать графический процессор. . Первый аргумент принимает указатель на строку исходного кода шейдера; второй аргумент принимает указатель на объект MTL::CompileOptions, указывающий, как должен быть построен исходный код; третий аргумент принимает указатель на объект NS:Error, который содержит все ошибки, возникшие во время компиляции шейдера. В строках 17 и 18 мы создаем две MTL::Function, которые представляют функцию шейдера в объекте MTL::Library. Мы создаем эти объекты MTL::Function, передавая имя функции в файле шейдера в виде строки в первом аргументе; второй аргумент указывает формат строкового типа. В строке 21 мы создаем экземпляр MTL::RenderPipelineDescriptor, который указывает состояние рендеринга, используемое во время прохода рендеринга. Мы можем использовать объект RenderPipelineDescriptor, чтобы указать функции вершин и фрагментов в шейдерной программе, которые будут использоваться во время выполнения прохода рендеринга. В строках 23 и 25 мы устанавливаем ссылку на функции вершин и фрагментов текущей скомпилированной шейдерной программы. В строке 27 мы указываем формат данных фреймбуфера. В этом случае мы используем формат PixelFormatBGRA8Unorm_sRGB, который определяет четыре смежных 8-битных нормализованных целых числа без знака, представляющих значения Blue, Green, Red, Alpha, составляющие один пиксель. В строке 30 мы создаем MTL::RenderPipelineState, передавая объект MTL::RenderPipelineDescriptor и объект NS::Error; и сохраните его в нашем объекте RenderPass. Строка 31 — это просто проверка, создан ли объект RenderPipelineState. Как всегда, не забывайте освобождать объекты, которые вам больше не нужны!

Буферы сборки

Металлические буферы — это неструктурированные области памяти графического процессора. Mac может содержать несколько графических процессоров, каждый из которых имеет унифицированную или дискретную модель памяти. Унифицированная модель памяти имеет место, когда Mac не имеет дискретного графического чипа в материнской плате (пример: Macbook Air, Macbook Pro M1); поэтому GPU и CPU совместно используют системную память. В модели с дискретной памятью GPU имеет собственную память, называемую видеопамятью; системная память по-прежнему доступна для графического процессора, но видеопамять недоступна для ЦП. Мы можем настроить буферы GPU Metal в разных режимах, чтобы контролировать, как ЦП и ГП могут получить к ним доступ.

Давайте посмотрим на функцию BuildBuffer().

В строках 7 и 16 мы создаем два массива (можно также рассматривать как буферы для сохранения симметрии) типа simd::float3 (вектор из трех 32-битных элементов с плавающей запятой) размером 6. В этом случае мы агрегируем инициализацию этих буферов, предоставляя данные, необходимые для построения двух треугольников, и данные цвета для каждой вершины. Один массив будет содержать все данные о положении вершин, а другой массив будет содержать данные о цвете вершин. В строках 30 и 31 мы создаем буфер на графическом процессоре, передавая общий размер в качестве первого аргумента, а второй аргумент принимает перечисление, указывающее режим хранения. Режим хранения MTL::ResourceStorageModeManaged указывает, что буфер доступен ЦП только для инициализации, а затем синхронизируется с памятью графического процессора и, наконец, становится доступным для графического процессора. В строках 34 и 35 мы используем can use memcpy для копирования наших локальных буферов данных вершин в определенные местоположения MTL::Buffer. В строках 38 и 39 мы сообщаем GPU, что буфер был изменен, указав диапазон изменения. В строках с 42 по 45 мы получаем ссылку на объект Mesh в объекте RenderPass и обновляем указатели буфера и количество вершин.

Рендеринг квадрата

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

Давайте посмотрим на нашу функцию Draw().

В строке 13 мы передаем ранее созданный объект MTL::RenderPipelineState в функцию setRenderPipelineState(), чтобы сообщить графическому процессору, что мы собираемся использовать это состояние конвейера рендеринга для наших следующих команд рисования. В строках 16 и 18 мы кодируем буфер в таблице аргументов буфера с определенным индексом и смещением для начала. Таблица аргументов буфера указывает, в каком индексе содержится какой буфер. В строке 20 мы кодируем команду рисования для рендеринга экземпляра примитивов с использованием данных вершин, указав тип примитива для рисования, смещение начала и общее количество вершин.

Выход

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

Ознакомьтесь с репозиторием проекта здесь!

Заключение

Надеюсь, вы многому научились из моих небольших статей о Metal-cpp. Ищите часть 3, где мы углубимся в безумие……