Введение

Metal  —  это мощный низкоуровневый фреймворк графического и вычислительного программирования на платформах Apple с максимальным задействованием производительности графического процессора для сложных вычислений и задач отрисовки.

Metal применяется в основном для обработки изображений и видео в реальном времени с высокой производительностью и низкими накладными расходами.

В этой статье, рисуя треугольник, мы погрузимся в мир Metal, рассмотрим основные понятия и терминологию, этапы настройки среды разработки, научимся делать с Metal простую 3D-графику для сложных проектов и в итоге создадим приложение, в котором этот треугольник отрисуется.


Содержание

· Настройка среды.
· Шейдеры Metal.
- создание и использование шейдеров в Metal;
- конвейеры и состояние.
· Применение Metal в ViewController.
1. Настройка исходного представления.
2. Настройка представления Metal.
3. Создание слоя Metal.
4. Создание буфера вершин.
5. Создание состояния конвейера отрисовки.
6. Отрисовка треугольника.
· Применение Metal в SwiftUI.
· Заключение.

Настройка Metal в проекте на iOS

API-интерфейсы Metal используются в проекте на iOS так.

  1. Создаем новый проект Xcode, в качестве платформы выбираем iOS.
  2. Во вкладке General («Общие») настроек проекта выставляем версию с поддержкой Metal  —  iOS 8 или новее.
  3. Во вкладке Build Settings («Параметры сборки») находим Metal и задаем параметру MetalCaptureEnabled значение Yes: включаем API-интерфейсы Metal для проекта.
  4. В коде импортируем фреймворк Metal, добавляя вверху файла эту строку: import Metal.
  5. Чтобы использовать Metal в коде, нужен объект MTLDevice  —  это устройство, обычно графический процессор, на котором запускается Metal.

Создаем этот объект:

let device = MTLCreateSystemDefaultDevice()

Затем создаются другие объекты Metal: MTLCommandQueue и MTLCommandBuffer  —  для выполнения операций в устройстве.

Рассмотрим основные понятия Metal.

Шейдеры Metal

Шейдер в Metal  —  это небольшая программа, запускаемая в графическом процессоре для выполнения конкретной задачи, например отрисовки 3D-модели и наложения фильтра на изображение.

В Metal имеется три типа шейдеров: вершинные, фрагментные и вычислительные.

1. Вершинные шейдеры

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

2. Фрагментные шейдеры

Ими определяется конечный цвет каждого пикселя на экране. Принимаются интерполированные данные вершин из вершинного шейдера, а возвращается конечный цвет каждого пикселя.

3. Вычислительные шейдеры

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

Создание и использование шейдеров в Metal

В Metal шейдеры пишут на языке шейдеров Metal Shading Language (MSL).

Новый MSL-файл в Xcode создается так: переходим в File («Файл») > New («Новый») > File («Файл») и в разделе Metal выбираем Metal Shading Language File («Файл языка шейдеров Metal»).

Написав шейдеры, загружаем их в проект Metal с помощью MTLLibrary:

// Создаем устройство Metal
let device = MTLCreateSystemDefaultDevice()

// Создаем библиотеку Metal из MSL-файла
let metalLibrary = device.makeDefaultLibrary()

// Загружаем из библиотеки функцию вершины
let vertexFunction = metalLibrary.makeFunction(name: "vertexShader")

// Загружаем из библиотеки функцию фрагмента
let fragmentFunction = metalLibrary.makeFunction(name: "fragmentShader")

Конвейеры и состояние

Привяжем эти функции к состоянию конвейера:

// Создаем дескриптор конвейера отрисовки
let pipelineDescriptor = MTLRenderPipelineDescriptor()

// Задаем функции вершин и фрагментов
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction

// Создаем состояние конвейера
let pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)

Что такое MTLRenderPipelineDescriptor?

Это объект со свойствами  —  функция вершины, функция фрагмента и состояние трафарета/глубины,  —  которыми настраивается объект состояния конвейера отрисовки.

makeRenderPipelineState(descriptor:)  —  это метод класса MTLDevice, которым из объекта MTLRenderPipelineDescriptor создается новый объект MTLRenderPipelineState: на входе в метод принимается MTLRenderPipelineDescriptor, а на выходе возвращается MTLRenderPipelineState для отрисовки 3D-моделей или изображений.


Применение Metal в контроллере представления

Посмотрим, как с Metal отрисовывается треугольник в UIView или NSView в приложении iOS или macOS соответственно.

Нас интересуют основные этапы настройки представления, создания слоя и отрисовки окружности шейдерами Metal.

1. Настройка исходного представления

Добавим простой UIView в main.storyboard и зададим желаемые высоту, ширину и положение.

Перенесем вывод этого представления в файл Swift и назовем его, например mainView.

Созданное представление добавится к этому, поэтому понадобится его вывод.

2. Настройка представления Metal

Для применения Metal нужно создать пользовательское представление, поддерживаемое слоем Metal.

Это делается созданием подкласса UIView и переопределением свойства layerClass для возвращения класса CAMetalLayer:

class MetalView: UIView {
override class var layerClass: AnyClass {
return CAMetalLayer.self
}
}

Добавляем его объект в главный класс контроллера представления:

let metalView = MetalView()

3. Создание слоя Metal

Настроив представление Metal, вызываем его свойство layer для доступа к опорному слою Metal.

Слой Metal нужен для управления ресурсами графического процессора и воспроизведения отображаемого содержимого:

let metalLayer = self.metalView.layer as? CAMetalLayer
metalLayer?.frame = .init(x: 0, y: 0, width: mainView.frame.width, height: mainView.frame.height)

Здесь задан его фрейм с родительским представлением, ведь нужно отрисовать представление в добавленном статическом представлении.

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

mainView.addSubview(metalView)
metalView.topAnchor.constraint(equalTo: mainView.topAnchor).isActive = true
metalView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor).isActive = true
metalView.leftAnchor.constraint(equalTo: mainView.leftAnchor).isActive = true
metalView.rightAnchor.constraint(equalTo: mainView.rightAnchor).isActive = true

Добавим все это в метод viewDidLoad(): нужно показать представление Metal с отрисовкой представления.

4. Создание буфера вершин

Чтобы отрисовать треугольник с помощью Metal, создадим буфер вершин с данными вершин треугольника и передадим эти данные в вершинный шейдер:

let vertexData: [Float] = [0.0, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 1.0,
0.5, -0.5, 0.0, 1.0]
// Создаем буфер вершин для окружности
let vertexBuffer = device.makeBuffer(bytes: vertexData, length: vertexData.count * MemoryLayout<Float>.size, options: [])

Добавили объект device после объявления объекта metalView:

let device = MTLCreateSystemDefaultDevice()!

Чтобы раскрасить окружность, используем фрагментный шейдер.

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

Вот реализация обеих функций:

vertex float4 vertexShader(constant float3 *vertexArray [[ buffer(0) ]],
uint vid [[ vertex_id ]]) {
float3 ver = vertexArray[vid];
return float4(ver, 1.0);
}

fragment float4 fragmentShader() {
return float4(1.0, 0.5, 0.5, 1.0);
}

Добавляем обе эти функции в .metal file проекта.

5. Создание состояния конвейера отрисовки

Чтобы отрисовать треугольник, создаем объект MTLRenderPipelineState, в котором содержатся шейдеры и другая информация о состоянии:

guard let drawable = metalLayer?.nextDrawable() else { return }

// Создаем дескриптор конвейера отрисовки
let pipelineDescriptor = MTLRenderPipelineDescriptor()

// Получаем определtнные функции Metal из файла Metal
let metalLibrary = device.makeDefaultLibrary()
let vertexFunction = metalLibrary?.makeFunction(name: "vertexShader")
let fragmentFunction = metalLibrary?.makeFunction(name: "fragmentShader")

// Задаем функцию вершин
pipelineDescriptor.vertexFunction = vertexFunction

// Задаем функцию фрагментов
pipelineDescriptor.fragmentFunction = fragmentFunction

// Задаем формат пикселей для конвейера
pipelineDescriptor.colorAttachments[0].pixelFormat = drawable.texture.pixelFormat

do {
// Создаем состояние конвейера отрисовки
let pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
} catch let error {
print("Error: \(error.localizedDescription)")
}

Но этого недостаточно.

6. Отрисовка треугольника

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

Добавим в блок do этот код:

// Создаем энкодер команд отрисовки
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear

// Создаем буфер команд
let commandQueue = device.makeCommandQueue()!
let commandBuffer = commandQueue.makeCommandBuffer()!

// Создаем энкодер команд отрисовки
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!

// Задаем состояние конвейера
renderEncoder.setRenderPipelineState(pipelineState)

// Задаем буфер вершин
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)

// Отрисовываем треугольник
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexData.count)

// Завершаем кодировку
renderEncoder.endEncoding()

// Представляем отрисовываемое
commandBuffer.present(drawable)

// Фиксируем буфер команд
commandBuffer.commit()

Запускаем приложение.

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

Посмотрим, как это сделать в SwiftUI.


Применение Metal в SwiftUI

Теперь с помощью Metal отрисуем треугольник в SwiftUI.

Единственное отличие от UIView  —  как получается drawable, используемое в Metal для renderEncoder.present.

Чтобы использовать Metal в SwiftUI, создаем MTKView с помощью фреймворка MetalKit, обертываем MTKView в структуру UIViewRepresentable и применяем в представлении SwiftUI:

struct TriangleView: UIViewRepresentable {
func makeUIView(context: Context) -> MTKView {
let view = MTKView()
view.device = MTLCreateSystemDefaultDevice()
view.delegate = context.coordinator
return view
}

func updateUIView(_ uiView: MTKView, context: Context) {}

func makeCoordinator() -> Coordinator {
Coordinator()
}

class Coordinator: NSObject, MTKViewDelegate {

func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}

func draw(in view: MTKView) {
.
.
let descriptor = view.currentRenderPassDescriptor
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)
guard let drawable = view.currentDrawable else { return }
.
.
}
}

В реализации SwiftUI все остальное то же, что у ViewController.

Здесь мы создаем drawable из MTKView, а не из CAMetalLayer.

Применим это представление в ContentView для отображения треугольника:

struct ContentView: View {

var body: some View {
VStack {
TriangleView()
.frame(width: 230, height: 200, alignment: .center)
Text("Hello, world!")
}
.padding()
}
}

Вот и все.

Заключение

Мы рассмотрели основы использования API-интерфейсов Metal для отрисовки треугольника в UIView и в SwiftUI, настроили представление, создали слой Metal и отрисовали окружность с помощью шейдеров Metal. Кроме того, теперь у вас есть фрагменты кода и рекомендации для применения Metal в собственном приложении iOS или macOS.

Хотя этот пример относительно простой, мощь и гибкость API-интерфейсов Metal им демонстрируется. С Metal можно создавать сложную и высокооптимизированную 3D-графику, обрабатывать изображения, выполнять другие задачи с ускорением графического процессора.

Для дальнейшего изучения Metal рекомендуем ознакомиться с официальным руководством по программированию Metal и справочником по фреймворку Metal от Apple.

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

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Jimmy Sanghani: How to get started with Metal APIs — with UIView and SwiftUI

Предыдущая статьяКакой метод глубокого клонирования в JavaScript наиболее эффективный  —  исследование
Следующая статья4 альтернативы Pandas: ускоренное выполнение анализа данных