Знакомство с Apple’s Metal

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

Настраивать

Мы собираемся начать с приложения для macOS. Причина в том, что мы можем использовать графический процессор Mac в симуляторе. Если вы хотите сделать это для приложения iOS, вам придется запустить его на физическом устройстве, поскольку симуляторы iOS не поддерживают Metal.

Теперь мы добавим в наш подкласс NSView (по сути, UIView для Mac) под названием «MetalCircleView». Здесь мы будем делать самую тяжелую работу. Контроллер корневого представления приложения (называемый ViewController) будет просто отображать этот NSView.

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

Теперь мы покажем представление из нашего ViewController в окно, используя ограничения автоматического макета.

Теперь, если мы запустим его, мы должны получить пустое окно!

Настройка нашего представления MetalKit

  1. Импортируйте MetalKit в свой файл MetalCircleView.
  2. Объявите наш MTKView (Metal-Kit-View) как переменную экземпляра класса.
  3. Ограничьте это нашим взглядом.
  4. Установите себя в качестве его делегата, соответствующего MTKViewDelegate.

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

Указание MTKView, как и когда «обновлять»

Нам нужно указать нашему представлению, как и когда оно должно перерисовываться. У нас есть три варианта:

  1. Мы позволяем ему перерисовываться на основе его внутреннего таймера (непрерывно)
  2. Мы сообщаем ему, когда перерисовывать себя, используя сеттер, который будет происходить на основе его внутреннего таймера (инициированного нами)
  3. Мы прямо говорим ему рисовать, игнорируя его внутренний таймер (инициированный нами)

Мы выберем номер два, так как мы рисуем только один раз и будем полагаться на использование currentRenderPassDescriptor представления (подробнее об этом позже). Согласно документации, нам нужно приостановить его с помощьюmetalView.isPaused = true и включить отображение его набора с помощьюmetalView.enableSetNeedsDisplay = true. Это говорит ему, что он должен быть приостановлен и должен ждать, пока мы сообщим ему, когда ему нужно что-то отобразить.

Подключение к графическому процессору устройства

Наш MTKView необходимо подключить к устройству типа MTLDevice. По сути, вы можете думать об этом устройстве как о самом графическом процессоре.

Протокол MTLDevice определяет интерфейс для графического процессора.

Мы можем получить графический процессор во время выполнения, используя MTLCreateSystemDefaultDevice() в iOS или tvOS, а также в macOS. Существует еще один вариант, позволяющий выбрать конкретный графический процессор (полезно, если вы хотите использовать выделенный или встроенный графический процессор Mac), но это выходит за рамки данного руководства.

Мы хотим, чтобы это металлическое устройство было доступно глобально, поэтому мы объявляем его как переменную экземпляра класса, инициализируем его в нашей функции setupMetal() и устанавливаем как устройство нашего metalView.

metalDevice = MTLCreateSystemDefaultDevice()
metalView.device = metalDevice

Наш конечный продукт теперь выглядит так, и мы готовы приступить к настройке нашей функции рендеринга!

Настройка нашей функции рендеринга

Создание очереди команд

Первое, что нам нужно сделать, это создать очередь команд MTLCommandQueue. Эта очередь должна быть уникальной для нашего устройства (нашего интерфейса графического процессора); мы используем его для передачи инструкций нашему графическому процессору. Инструкции представлены символом MTLCommandBuffer и создаются для выполнения в очереди команд.

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

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

metalCommandQueue = metalDevice.makeCommandQueue()!

(На этом этапе вам должно быть интересно, почему я выполняю принудительное развертывание. Убедитесь, что вы правильно обрабатываете свои опции!) После настройки очереди команд. наш код должен выглядеть так.

Выдача нашей первой команды GPU!

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

Первое, что мы хотим сделать внутри draw, - это создать наш commandBuffer. Он будет содержать инструкции, необходимые для выполнения наших команд!

Создание конвейера

Наш командный буфер нуждается в конвейере. Пайплайну нужна внутренняя информация и информация об интерфейсе. Мы используем MTLRenderPassDescriptor для настройки информации об интерфейсе. Для этого руководства нам не нужно создавать собственное - мы можем получить значение по умолчанию из MTKView с помощью .currentRenderPassDescriptor.

Теперь, получая доступ к свойству массива colorAttachements дескриптора прохода рендеринга, мы можем установить для него значение (0-я запись) .clearColor, которое описывает данные цвета для текстуры, назначенной текущему отображаемому объекту представления. Проще говоря, это можно рассматривать как «цвет фона» для нашего вида металла.

Затем нам понадобится MTLRenderCommandEncoder для настройки внутренней части конвейера. Он компилируется из нашего commandBuffer с помощью renderDescriptor.

Отсюда мы можем начать вводить данные вершин и команды рисования, которые будут отображаться на GPU, или, что лучше думать, как команды «кодирования» для запуска GPU. На данный момент мы не готовы кодировать какие-либо настоящие команды рисования, поэтому оставим это на потом (я солгал вам в заголовке раздела: p). Мы хотим видеть этот красивый синий цвет фона в нашем MTKView!

Нам нужно сделать четыре вещи, чтобы завершить кодирование и запустить commandBuffer, который будет выполняться на графическом процессоре и отображаться в нашем представлении!

  1. Завершите кодирование.

renderEncoder.endEncoding()

2. Сообщите графическому процессору, куда отправить результат рендеринга.

commandBuffer.present(view.currentDrawable!)

Мы можем использовать currentDrawable MTKView, объект для рисования, представляющий текущий фрейм. MTLDrawable - это «отображаемый ресурс, который можно отображать или записывать».

3. Добавляем инструкцию в нашу metalCommandQueue

commandBuffer.commit()

4. Скажите нашему металлическому представлению нарисовать, который запускает метод рисования, мы добавим его в конец нашей setupMetal() функции, но вы можете вызывать его где угодно (после того, как вы настроили металлические компоненты, курс).

metalView.needsDisplay = true

Наша функция рисования теперь должна выглядеть так.

Если вы нажмете «Беги», вы увидите синий экран!

Примечание.

Ранее, когда мы выбирали способ обновления нашего MTKView, я упоминал, что мы установили его вручную, используя внутренний таймер представления, поскольку мы полагаемся на currentRenderPassDescriptor. Если бы мы выполнили команду draw() вручную, игнорируя ее таймер, нам пришлось бы вызывать ее дважды, поскольку в первый раз у представления не было бы currentRenderDescriptor.

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

Этапы трубопровода

Кодирование команд рисования / данных вершин:: данные, которые получает графический процессор и которые должны обрабатываться в конвейере.

Vertex Shader: преобразует местоположения 3D-вершин в 2D-координаты экрана. Он также передает данные вершин по конвейеру.

Тесселяция. Разделяет треугольники на треугольники для получения более качественных результатов.

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

Фрагментный шейдер. Учитывая интерполированные пиксельные данные из растеризатора, фрагментный шейдер определяет окончательный цвет каждого пикселя.

Шейдеры

Metal поддерживает три типа шейдерных функций: Vertex, Fragment и Compute (ядра). Они описывают части конвейера рендеринга.

Вершинные шейдеры: функция, используемая для управления точками вершин многоугольника. Он выполняется на каждой точке вершины, которую мы передаем. Здесь мы можем управлять положением точек вершины и другими свойствами, такими как цвет.

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

Фрагментные шейдеры: функция, используемая для управления тем, как выглядят пиксели между вершинами. Он выполняется на каждом пикселе между набором вершинных точек. Здесь мы можем вернуть информацию о цвете каждого пикселя.

Равномерные скаляры. На этом этапе вы можете спросить: а как насчет передачи скаляров? Скажем, постоянный тип Float для представления множителя позиции нашего объекта; изменяя эту константу, мы можем сделать наш многоугольник больше или меньше. Ну, это называется Равномерным, потому что это значение, которое единообразно применяется ко всем точкам, ИНАЧЕ, оно не меняется.

Примитивы

На самом низком уровне графические процессоры предназначены для отрисовки треугольников. Треугольники - это самый простой и универсальный объект, с которым можно работать, и именно на этом ориентировано современное оборудование (объяснение StackOverflow здесь). Это не значит, что мы можем приказать графическому процессору рисовать только треугольники. Например, если вы используете фреймворк, поддерживающий квадраты (прямоугольники), вы можете передать ему четыре точки и указать, чтобы он рисовал прямоугольник. Это упрощает работу программиста, но на самом деле графический процессор по-прежнему разбивает эту инструкцию на две инструкции треугольника.

Думайте об этом как о написании сложной строчки кода на языке высокого уровня. Когда код компилируется в сборку, эта «одна» инструкция разбивается на серию из нескольких инструкций, которые ЦП может фактически выполнить.

MTLPrimitiveType - Тип геометрического примитива для команд рисования.

  1. точка - растрирует точку в каждой вершине.
  2. линия - растрирует линию между каждой отдельной парой вершин (создает несвязанные линии)
  3. lineStrip - растрирует линию между каждой парой вершин (образует серию соединенных линий).
  4. треугольник: растрирует треугольник для каждой отдельной тройки точек.
  5. треугольникStrip: растрирует треугольник для каждых трех смежных троек точек.

Подводя итог, нам нужно три шага высокого уровня, чтобы образовать круг.

  1. Создайте точки вершин на ЦП.
  2. Отправьте вершинные точки в вершинный шейдер.
  3. Примените цвет во фрагментном шейдере.

Настройка нашего металлического файла

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

Первое, что нам нужно сделать, это создать металлический файл в папке нашего проекта. Это похоже на добавление нового файла, за исключением того, что мы выбираем «Металл» вместо «Класс какао» или «Swift».

Назовите его CircleShader.metal.

Открыв его, мы видим, что импортируем стандартную библиотеку Metal и используем пространство имен metal. Используемый здесь язык называется Metal Specification Language. Если вы когда-нибудь работали с C ++, вы заметите, что он уже выглядит похожим; это потому, что MSL (Metal Shading Language) основан на C ++.

Создание структуры данных для передачи наших вершин в GPU

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

Во-первых, давайте посмотрим, что нам нужно для представления точки вершины: нам нужна переменная положения, содержащая две координаты, и нам нужна переменная цвета, содержащая информацию о цвете для точки.

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

struct VertexIn {
    position : vector_float2 //<x,y>
    color : vector_float4 //<R,G,B,A>
}
var verticesForCircle = [VertexIn]() //array of VertexIn

Если мы хотим нести только один набор информации, тогда нам не нужна структура.

var verticesForCircle = [vector_float2]() //array of <x,y>

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

Вы также можете заметить, что приведенные выше примеры содержат vector_floats. В рамках Accelerate Apple использует библиотеку SIMD для векторных изображений. Он был создан для C ++ и также доступен в Swift, поэтому мы будем использовать его для представления наших ценностей.

Импортируем simd в наши файлы .metal и .swift:

simd C ++ (металл)

#include <simd/simd.h>

Объявление вектора:

vector_float2 varName;

simd Swift

import simd

Объявление вектора:

let varName : simd_float2

Используя библиотеку SIMD, мы обеспечиваем единообразное представление наших данных в памяти CPU и GPU.

Создание вершин для нашего круга

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

Здесь есть два варианта.

1.

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

2.

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

Здесь нет правильного или неправильного ответа, поэтому мы выберем более простой вариант (вариант 1).

Мы собираемся создать переменную экземпляра с именем circleVertices и функцию с именем createVertexPoints(). Внутри функции createVertexPoints() нам понадобится вспомогательная функция для вычисления градусов в радианы, поскольку мы будем использовать функции тригонометрии Swift.

Наш класс MetalCircleView теперь должен выглядеть так:

Поскольку в круге 360 градусов, мы можем создать n * 360 точек периметра (где n представляет ненулевые положительные целые числа) с (n * 360) / 2 исходными точками. По сути, чем больше n, тем больше треугольников мы визуализируем и тем более гладким будет круг. К счастью, n = 2 нам достаточно.

Я сэкономлю на уроке тригонометрии, но вот как мы получаем 720 точек периметра.

Фактически, это 721 точка периметра. Это потому, что мы хотим сделать полный круг (буквально). Мы начинаем с 0 * и хотим убедиться, что закончили на 360 *. Если бы мы пошли с 0… ‹720, то закончили бы на 395,5 *. В этом есть заметная разница, так как если бы мы оставили его таким образом, была бы незаполненная часть круга. Теперь между каждыми двумя точками периметра нам нужно сформировать треугольник с началом координат.

Стоит отметить, что точки, которые мы создаем, нормализованы к экрану. В примере Apple Hello Triangle он определяется как:

Функция вершины переводит произвольные координаты вершины в нормализованные координаты устройства, также известные как координаты пространства отсечения. Пространство обрезки - это двумерная система координат, которая отображает область окна просмотра в диапазон [-1.0, 1.0] по осям x и y.

Это означает, что область, в которой мы можем визуализировать точки, изменяется от -1,0 до 1,0 как по оси x, так и по оси y, и что эта система координат отображается на область окна просмотра. В нашем случае мы не касались области просмотра, поэтому область просмотра - это весь MTKView.

Теперь мы готовы отправить эти данные в GPU и создать шейдерные функции :)

Настройка функций шейдера

Указатели и память

В Спецификации языка Metal Shading, Глава 4

Аргументы для графики Metal и функций ядра, объявленных в программе, которые являются указателями, должны быть объявлены с помощью атрибута Metal device, threadgroup, threadgroup_imageblock или постоянного атрибута адресного пространства.

Они определяют, в каком адресном пространстве графического процессора должен храниться массив. Атрибут device определяет адресное пространство для чтения и записи, а constant определяет адресное пространство только для чтения.

Константы функции области действия программы

Переменные области действия программы, объявленные (или инициализированные) с помощью следующего атрибута, являются константами функции: [[function_constant (index)]]

Эти атрибуты обычно используются в параметрах, чтобы металл знал, куда передавать конкретные данные.

Сначала я покажу вам шаблон, а затем объясню, что происходит.

вершинная функция

const constant vector_float2 *vertexArray [[buffer(0)]]

Первый параметр - это то, что мы берем наш массив точек вершин, которые мы будем передавать. Разбив синтаксис, мы видим, что у нас есть указатель на массив векторных чисел с плавающей запятой. Данные вершин, как вы скоро увидите, необходимо передать как «данные буфера». [[buffer(0)]] указывает, что мы хотим, чтобы первые (и наши единственные) данные буфера были переданы в этот параметр. Атрибут constant сообщает металлу, что данные вершин нужно хранить в постоянной памяти.

unsigned int vid [[vertex_id]]

Второй параметр vid означает «идентификатор вектора». Это однозначно определяет, в какой вершине мы сейчас находимся; он будет использоваться как индекс для нашего vertexArray. Точно так же, как в нашем параметре vertexArray нам нужно было сообщить металлу, что он должен пройти, мы даем металлу знать, чтобы передать наш идентификатор вершины в параметр vid, используя [[vertex_id]].

VertexOut

Выходные данные имеют тип VertexOut, который содержит вектор положения и вектор цвета. Выходные данные сначала проходят тесселяцию / растеризацию, поэтому атрибут [[position]] указывает металлу использовать поле position структуры в качестве нормализованного положения экрана. Возможно, вы уже заметили, что это 4-мерное поле, а не 2-мерное, которое мы передаем для позиции. Третья / четвертая координаты представляют собой глубину и однородное пространство - нам не о чем беспокоиться. Затем эта структура VertexOut будет передана на вход нашей функции фрагмента, из которой мы захотим использовать поле color.

функция фрагмента

VertexOut interpolated [[stage_in]]

У нас есть только один входной параметр типа VertexOut с именем interpolated. Атрибут [[stage_in]] сообщает металлу, что переменная должна быть введена в интерполированный результат растеризатора.

На выходе получается просто цвет ‹R, G, B, A›, который мы получаем из структуры VertexOut, которая была передана из функции vertexShader.

Заполнение функций шейдера

  1. Мы получаем текущую вершину из буфера по идентификатору вершины.
  2. Инициализируем вывод типа VertexOut.
  3. Мы устанавливаем информацию о 4-мерном положении выходных данных только с 2-мерным положением от нашей точки currentVertex.
  4. Мы возвращаем результат для растрирования, а затем передаем его в наш фрагментный шейдер.
  5. В нашем фрагментном шейдере мы просто возвращаем цвет.

Несколько интересных заметок:

  • Если вы не включите атрибут [[position]] в структуру, вы получите ошибку компиляции, сообщающую, что VertexOut является недопустимым типом возвращаемого значения.
  • Если вы передаете только vector_float4 без структуры, metal автоматически сделает вывод, что это координаты.

Оптимизация здесь:

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

Настройка конвейера рендеринга

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

Вот на чем мы остановились в прошлый раз в нашей функции рисования в классе MetalCircelView.

Мы создали буфер команд, который будет добавлен в нашу commandQueue, которая была создана для интерфейса нашего графического процессора. Настраиваем вход и выход конвейера. Теперь осталось только связать renderEncoder (или «внутреннюю часть нашего конвейера») с нашими шейдерными функциями и передать его в наши точки вершин в качестве данных буфера!

Связывание наших металлических функций с нашим renderEncoder

Первый шаг - создать MTLRenderPipelineState.

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

Чтобы создать состояние конвейера, нам нужен MTLRenderPipelineDescriptor.

Аргумент опций, которые вы передаете устройству, чтобы получить объект состояния конвейера рендеринга.

Итак, мы собираемся создать новую переменную экземпляра класса для MTLRenderPipelineState и функцию для создания MTLRenderPipelineState, которую мы вызовем в функции setupMetal() прямо перед рисованием нашего представления.

Чтобы создать состояние конвейера, нам необходимо:

  1. Создайте дескриптор конвейера.
  2. Найдите наши металлические файлы с помощью интерфейса графического процессора.
  3. Сообщите дескриптору конвейера, как вызываются наши функции вершин и фрагментов.
  4. Сообщите дескриптору конвейера, в каком формате хранить данные пикселей.
  5. Создайте состояние конвейера из дескриптора конвейера.

Как обычно, убедитесь, что правильно обрабатываете выбросы и опции (делайте то, что я говорю, а не то, что я делаю).

Теперь, чтобы подключить его к нашему кодировщику рендеринга, все, что нам нужно сделать, это использовать его функцию setRenderPipelineState.

Теперь мы готовы рисовать примитивы из наших вершин!

Превращение точек вершины в данные буфера

Во-первых, нам нужно создать данные буфера типа MTLBuffer. Документацию по этому поводу стоит прочитать, чтобы понять, что происходит.

Объект MTLBuffer можно использовать только с MTLDevice, создавшим его. Не применяйте этот протокол самостоятельно; вместо этого используйте следующие методы MTLDevice для создания MTLBufferobjects:

  1. makeBuffer(length:options:)

создает объект MTLBuffer с новым распределением памяти.

2. makeBuffer(bytes:length:options:)

создает объект MTLBuffer, копируя данные из существующего распределения памяти в новое распределение.

3. makeBuffer(bytesNoCopy:length:options:deallocator:)

создает объект MTLBuffer, который повторно использует существующее выделение памяти и не выделяет новую память.

Мы хотим выбрать второй вариант, так как у нас уже есть данные, хранящиеся в нашем массиве circleVertexes.

Мы объявляем наш vertexBuffer вверху как переменную экземпляра:

private var vertexBuffer : MTLBuffer!

А затем заполните его внутри нашей функции setupMetal():

vertexBuffer = metalDevice.makeBuffer(bytes: circleVertices, length: circleVertices.count * MemoryLayout<simd_float2>.stride, options: [])!

Функция makeBuffer берет количество байтов length из нашего circleVertices и сохраняет его в доступной памяти GPU / CPU. Для длины мы получаем шаг (количество байтов от начала одного экземпляра T до начала следующего при хранении в непрерывной памяти или в Array<T>) из MemoryLayout типа данных (в нашем случае a simd_float2) и умножьте его на количество записей этого типа в массиве.

Связывая все это вместе, мы получаем следующее:

Рисуем наш первый примитив (круг!)

Мы почти там! У нас есть все необходимое для выполнения нашей команды рисования в кодировщике рендеринга. Здесь действительно важна документация для MTLRenderCommandEncoder. Есть два примечательных раздела:

1. Указание ресурсов для вершинной функции (данные буфера)

func setVertexBuffer(MTLBuffer?, offset: Int, index: Int)

Устанавливает буфер для вершинной функции.

Помните, как мы использовали атрибут [[buffer (some index)]] для нашего параметра vertexArray в нашей функции вершинного шейдера? Что ж, в нашей функции рисования мы можем установить vertexBuffer на определенный индекс, чтобы metal знал, в какой входной параметр его передать.

renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)

Здесь установка индекса на 0 соответствует атрибуту [[buffer (0)]]. Смещение указывает начальную точку наших данных буфера, которые мы хотим назначить этому индексу. Поскольку мы заботимся обо всех наших вершинах, мы устанавливаем смещение на 0.

2. Рисование геометрических примитивов

func drawPrimitives(type: MTLPrimitiveType, vertexStart: Int, vertexCount: Int)

Кодирует команду для рендеринга одного экземпляра примитивов с использованием данных вершин в смежных элементах массива.

Это то, что запускает нашу функцию vertexShader. Все, что мы сделали до сих пор, сделано на данный момент. Мы говорим нашему кодировщику рендеринга отрисовать конкретный примитив (помните, когда мы перебирали MTLPrimitiveTypes), с какой вершины начинать и vertexCount.

Вы можете спросить, зачем нам указывать точку vertexStart и точку vertexCount. Это необходимо, когда вы хотите создать разные типы примитивов в одном проходе рендеринга. Если ваши первые 1000 вершин предназначены для треугольников, а следующие 1000 - для линий, вам нужно указать, с какой вершины начинается следующий тип примитива.

renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 1081)

У нас есть 1081 вершинная точка, и мы хотим визуализировать треугольники с самой первой точки.

Наконец, наша функция рисования должна выглядеть так:

Все, что нам нужно сделать, это нажать на бег, и мы должны увидеть наш первый круг!

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

Давайте вернемся к вариантам примитивов треугольников - у нас их два:

  1. треугольник: растрирует треугольник для каждой отдельной тройки точек.
  2. треугольникStrip: растрирует треугольник для каждых трех смежных троек точек.

Что, если мы изменим тип примитива с треугольника на треугольник?

Круг замкнулся, ура! По сути, мы закрыли пробелы, нарисовав больше треугольников с созданными нами точками.

Чтобы связать все это воедино, наш класс MetalCircleView должен выглядеть так:

Полный исходный код на моем Github здесь.

Оставшийся вопрос: область просмотра

На этом этапе вам должно быть интересно, почему круг масштабируется и растягивается вместе с окном. Имейте в виду, что мы ограничили наш металлический вид нашим окном, поэтому изменение этого параметра приводит к растяжению нашего «нормализованного» 2D координатного пространства. Если вы помните, в разделе «Создание наших вершинных точек» мы видели, что нормализованное координатное пространство отображается на наш MTLViewPort.

Есть два способа справиться с этим:

  1. Ограничьте MTKView так, чтобы его ширина == высота (либо соотношение, либо жестко заданное значение).
  2. Установите область просмотра на renderEncoder в функции рисования.

Что отлично ведет к последнему разделу этого урока :)

Куда пойти отсюда

Мы только что создали наш первый круг из металла! Мы узнали, как использовать основы металла (настройка нашего конвейера рендеринга), использовать язык затенения (язык затенения металла), узнали, как рисует графический процессор, и нарисовали наши первые примитивы, чтобы образовать круг!

Я бы предложил следующие шаги:

  1. Передача большего количества полей в vertexArray в металлической функции. Вспомните, когда мы решили представить наши вершины, используя только одно поле. Попробуйте также передать вершины как структуру с цветовым полем.
  2. Передайте данные буфера функции фрагментного шейдера.
  3. Рисование большего количества фигур за один проход.
  4. Установка области просмотра на метод делегата drawableSizeWillChange MTKView путем перерисовки самого представления.

Надеюсь, вам понравилось это не очень краткое введение в металл :). Полный проект можно найти на моей странице GitHub здесь.

Также ознакомьтесь со следующим уроком из этой серии!