Введение
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 так.
- Создаем новый проект Xcode, в качестве платформы выбираем iOS.
- Во вкладке General («Общие») настроек проекта выставляем версию с поддержкой Metal — iOS 8 или новее.
- Во вкладке Build Settings («Параметры сборки») находим Metal и задаем параметру MetalCaptureEnabled значение Yes: включаем API-интерфейсы Metal для проекта.
- В коде импортируем фреймворк Metal, добавляя вверху файла эту строку:
import Metal
. - Чтобы использовать 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 и его использования в приложениях.
Читайте также:
- Реализация масштабируемого и гибкого пользовательского экрана с несколькими переключателями на Swift
- Реализация цифрового конверта в iOS
- Реализуем функцию управления взглядом с помощью SwiftUI, ARKit и SceneKit
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jimmy Sanghani: How to get started with Metal APIs — with UIView and SwiftUI