Форум программистов, компьютерный форум, киберфорум
alecss131
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

Рисуем треугольник с помощью Metal

Запись от alecss131 размещена 24.06.2024 в 16:33
Показов 824 Комментарии 0

Рисуем треугольник с помощью Metal

Введение

В этой статье мы будем рисовать треугольник с помощью графического API от Apple, под названием Metal. В качестве языка я буду использовать Swift. В сети статей по-данному апи используя swift нет на русском языке (или я плохо искал). В итоге получим такой результат:

Нажмите на изображение для увеличения
Название: 00-result.png
Просмотров: 91
Размер:	88.4 Кб
ID:	8781

За основу взяты уроки и книги на английском языке и как можно более свежие. Но я буду все писать на старом ПО (то которое в наличии). Вот что имеем для старта:
  • MacBook Pro mid2012 (Intel Core i5-3210m, с интегрированным видео Intel HD4000)
  • macOS 10.15
  • Xcode 12.4
  • Swift 5.3
  • Metal 2.2

Графикой я увлекаюсь уже длительное время, но начинал с OpenGL на Windows используя С++. И только недавно увлекся языком Swift и программированием под устройства Apple и решил на нем тоже заняться 3д графикой. Использовать OpenGL и С++ не хотелось, да и на маке OpenGL ограничен версией 4.1, так что все привычные мне вкусности не доступны, а так же создать окно на C++ без сторонних библиотек или написания прослоек на Objective-C++ не представляется возможным.

OpenGL довольно простой и понятный апи, в отличие от тех же Vulkan и DirectX 12, которые слишком низкоуровневые, сложные и многословные для простых задач. Про metal до недавнего времени я был такого же мнения, пока не начал его изучать. Оказывается у него есть "упрощенная версия", представляющая собой MetalKit, то есть набор служебных классов, который позволяет создавать приложения metal быстрее и проще. В итоге использование данного апи становится не сложнее шейдерного OpenGL. От Вас потребуется только знание языка Swift.

Создание проекта и окна

В качестве gui framework я буду использовать Storyboard + AppDelegate, вместо актуального на данный момент SwiftUI. Я хочу охватить наибольшее количество устройств, а у SwiftUI (с жизненным циклом SwiftUI) требования macOS 11 и iOS 13 (телефон у меня на 12, так что тестировать негде будет) и код будет сложнее. Потом как-нибудь перепишу на SwiftUI + AppDelegate. Под iOS (используя UIkit, тот же Storyboard) данная статья должна подходить с минимумом правок.

Откроем Xcode и создадим проект (Create a new Xcode project)

Нажмите на изображение для увеличения
Название: 01-launch-xcode.png
Просмотров: 134
Размер:	132.9 Кб
ID:	8782

Теперь выберем шаблон macOS и App, нажмем Next

Нажмите на изображение для увеличения
Название: 02-select-type.png
Просмотров: 98
Размер:	196.2 Кб
ID:	8783

Настроим проект, напишем имя продукта hello-triangle, идентификатор организации ru.learn-metal, интерфейс Storyboard, жизненный цикл Appkit App Delegate, язык Swift и нажмем Next. Потом выберем и сохраним проект на диске.

Нажмите на изображение для увеличения
Название: 03-configure.png
Просмотров: 112
Размер:	189.6 Кб
ID:	8784

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

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
import Cocoa
 
@main
class AppDelegate: NSObject, NSApplicationDelegate {
 
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
    }
 
    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
}
В файле ViewController.swift оставим только один метод viewDidLoad

Swift
1
2
3
4
5
6
7
import Cocoa
 
class ViewController: NSViewController {    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}
Теперь настроим визуальную часть. Начнем с размера окна. Для этого будем использовать Interface Builder, в списке файлов проекта есть файл Main.storyboard. Тыкнем по нему 2 раза левой кнопкой мыши, откроется графический редактор интерфейсов. Для того чтобы задать размер окна надо изменить размер не окна, а View, которое находится на этом окне, окно само подстраивается под размер элементов, которые на нем находятся. Для этого откроем View Controller Scene -> View Controller и выберем View и в свойства в правой менюшке (со значком треугольника) заполним поля Width и Height значениями 800 и 600.

Нажмите на изображение для увеличения
Название: 04-set-window-size.png
Просмотров: 94
Размер:	129.4 Кб
ID:	8785

Теперь изменим название окна. Выберем на этот раз Window Controller Scene -> Window Controller -> Window и в том же меню справа, но в разделе со значком в виде ползунков изменим значение поля Title на Hello Triangle. Имя компонента в иерархии тоже поменяется. (на скриншоте имя уже изменено)

Нажмите на изображение для увеличения
Название: 05-set-window-title.png
Просмотров: 105
Размер:	482.8 Кб
ID:	8786

Теперь можно будет запустить наше приложение и увидеть пустое окно заданного размера и с нужным названием

Нажмите на изображение для увеличения
Название: 06-hello-window.png
Просмотров: 105
Размер:	86.0 Кб
ID:	8787

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

Начнем с создания поля класса, откроем файл ViewController.swift и добавим в него строчку @IBOutlet var mtkView: MTKView! перед методом viewDidLoad. И импортируем MetalKit в начале файла строчкой import MetalKit. Можно добавить и после, но сначала поля, а потом методы выглядит более эстетично и практично. В итоге файл будет выглядеть так:

Swift
1
2
3
4
5
6
7
8
9
10
import Cocoa
import MetalKit
 
class ViewController: NSViewController {
    @IBOutlet var mtkView: MTKView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}
Вернемся к редактированию Main.storyboard. Для начала поменяем тип нашего View с NSView на MTKView. Выберем наш View (View Controller Scene -> View Controller -> View). Потом в меню справа, значок похожий на газету, список Class в котором поменяем NSView на MTKView (пишем прямо руками и жмем Enter, так как в предложенных вариантах не будет нужного значения).

Нажмите на изображение для увеличения
Название: 07-change-view-class.png
Просмотров: 108
Размер:	469.2 Кб
ID:	8788

Теперь осталось связать наш интерфейс с кодом. В центральной области окна перетащим наш View вниз чтобы открылся синий круг с белым квадратом (при изменении размера элементы наехали друг на друга и получилась куча). Далее в правом меню со значком круг с точкой посередине из круга справа от Referencing Outlets -> New Referencing Outlet, левой кнопкой мыши (зажав и не отпуская) тянем линию к синему кругу с белым квадратом в центральной части экрана, после того как он выделится синим квадратом отпускаем мышку.

Нажмите на изображение для увеличения
Название: 08-connect.png
Просмотров: 111
Размер:	466.6 Кб
ID:	8789

После этого появится список доступных полей класса, в котором выбираем созданный нами mtkView:

Название: 09-select-var.png
Просмотров: 1065

Размер: 19.4 Кб

И в месте откуда мы начинали тянуть связь появится следующее:

Название: 10-result-connection.png
Просмотров: 1056

Размер: 19.0 Кб

Больше к визуальному редактору возвращаться не будем и вкладку с Main.storyboard можно закрыть.

Очищаем окно и класс рендера

Закончим настройку MTKView, вернемся к файлу ViewController.swift. Для рисования надо создать device, назначить его view, а так же задать формат пикселей и цвет очистки фона. Для этого в метод viewDidLoad после super.viewDidLoad() добавляем код. Device создается с помощью функции MTLCreateSystemDefaultDevice и назначается полю device, формат пикселей в поле colorPixelFormat в виде значения .bgra8Unorm. Цвет очистки по умолчанию черный, можно не трогать, но давайте сделаем фон зеленый, это поле clearColor и цвет для нее с помощью вызова функции MTLClearColorMake со значениями 0, 1, 0, 1 (красный, зеленый, синий и альфа). Здесь же можно настроить частоту кадров, зададим 60 полю preferredFramesPerSecond.

Swift
1
2
3
4
5
let device = MTLCreateSystemDefaultDevice()!
mtkView.device = device
mtkView.colorPixelFormat = .bgra8Unorm
mtkView.clearColor = MTLClearColorMake(0, 1, 0, 1)
mtkView.preferredFramesPerSecond = 60
Теперь создадим класс рендера. Для этого в левой части окна, где все файлы проекта кликнем по папке с именем проекта (с желтым значком) и выберем New File, в открывшемся окне тип Swift, потом введем имя файла Renderer. В итоге в файлах проекта должен появится файл на том же уровне, что и остальные файлы исходного кода, с именем Renderer.swift. В нем будет всего одна строка import Foundation, добавим к ней импорт import MetalKit. И создадим класс с таким же именем как и у файла, наследуемся от класса NSObject и реализуем протокол MTKViewDelegate. Этот протокол объявляет методы, которые MTKView будет вызывать для выполнения рендеринга и уведомления вас об изменении его размера. Этот протокол требует реализации двух методов draw и mtkView. Первый метод занимается перерисовкой окна, а второй вызывается при изменении его размера. С помощью Xcode можно создать реализацию этих методов в пару кликов, просто кликнув Fix на ошибке. В итоге получим следующую заготовку кода.

Swift
1
2
3
4
5
6
7
8
9
10
import Foundation
import MetalKit
 
class Renderer: NSObject, MTKViewDelegate {    
    func draw(in view: MTKView) {
    }
    
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
    }
}
Для рисования в классе рендера нам потребуются device и view, поэтому создадим для них поля класса и конструктор принимающих их как параметры. В конце не забудем вызвать конструктор родительского класса.

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Foundation
import MetalKit
 
class Renderer: NSObject, MTKViewDelegate {
    let device: MTLDevice
    let mtkView: MTKView
        
    init(view: MTKView, device: MTLDevice) {
        mtkView = view
        self.device = device
        super.init()
    }
    
    func draw(in view: MTKView) {
    }
    
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
    }
}
Теперь вернемся к классу ViewController. Создадим в нем объект класса Renderer и поле хранящее его, а так же назначим его делегатом для MTKView. Это последние изменения для класса ViewController, вот его окончательный вид:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Cocoa
import MetalKit
 
class ViewController: NSViewController {
    @IBOutlet var mtkView: MTKView!
    var renderer: Renderer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let device = MTLCreateSystemDefaultDevice()!
        mtkView.device = device
        mtkView.colorPixelFormat = .bgra8Unorm
        mtkView.clearColor = MTLClearColorMake(0, 1, 0, 1)
        mtkView.preferredFramesPerSecond = 60
        
        renderer = Renderer(view: mtkView, device: device)
        mtkView.delegate = renderer
    }
}
Теперь займемся очисткой экрана. Начнем с создания очереди команд, управляющую доступом к графическому процессору. Создадим поле let commandQueue: MTLCommandQueue в классе для ее хранения и создадим в конструкторе используя метод makeCommandQueue у объекта device.

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

В нашем приложении мы будем генерировать один командный буфер на кадр. Когда наш MTKView попросит отрисовать кадр, мы используем следующий код для создания буфера команд, в который мы будем кодировать наши команды отрисовки:

Swift
1
2
3
4
func draw(in view: MTKView) {
    let commandBuffer = commandQueue.makeCommandBuffer()!
   //...
}
Так же нам потребуются несколько объектов из нашего view: дескриптор прохода рендеринга (renderPassDescriptor) и текущий объект рисования (drawable). Когда у нас есть эти объекты, мы можем создать кодировщик команд рендеринга (commandEncoder), являющийся объектом, который фактически записывает команды в буфер команд.

Swift
1
2
3
4
5
6
7
8
func draw(in view: MTKView) {
    let commandBuffer = commandQueue.makeCommandBuffer()!
    if let renderPassDescriptor = view.currentRenderPassDescriptor,
        let drawable = view.currentDrawable,
        let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) {
        //..
    }
 }
Для простой очистки окна надо просто завершить кадр.

Swift
1
2
3
commandEncoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
В результате запустив программу мы получим окно зеленого цвета.

Нажмите на изображение для увеличения
Название: 11-clear-window.png
Просмотров: 78
Размер:	87.0 Кб
ID:	8792

Файл Renderer.swift

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import Foundation
import MetalKit
 
class Renderer: NSObject, MTKViewDelegate {
    let device: MTLDevice
    let mtkView: MTKView
    let commandQueue: MTLCommandQueue  
     
    init(view: MTKView, device: MTLDevice) {
        mtkView = view
        self.device = device
        commandQueue = device.makeCommandQueue()!
        super.init()
    }
    
    func draw(in view: MTKView) {
    }
    
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        let commandBuffer = commandQueue.makeCommandBuffer()!
        if let renderPassDescriptor = view.currentRenderPassDescriptor,
           let drawable = view.currentDrawable,
           let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) {
            commandEncoder.endEncoding()
            commandBuffer.present(drawable)
            commandBuffer.commit()
        }
        
    }
}
Вершинные данные и Vertex Descriptor

Для рисования треугольника нам потребуются данные вершин и их описание. Не будем заморачиваться с разными координатами и нарисуем все в пространстве экрана, по горизонтали ось Х от -1.0 до 1.0, по вертикали ось Y от -1.0 до 1.0. Создадим свойство класса для буфера вершин var buffer: MTLBuffer! и их описания var vertexDescriptor: MTLVertexDescriptor!. Вершины будут в формате [x y z]. Создадим функцию loadResources в которой будем все делать. Буфер для вершин создается при помощи метода makeBuffer у девайса, в который надо передать сами вершины и их размер в байтах.

Swift
1
2
let vertices: [Float32] = [-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0]
buffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Float32>.size, options: [])!
Теперь создадим описание вершин, то есть Vertex Descriptor. Vertex Descriptor это метаданные, которые предоставляют имя, положение и формат (тип данных) для каждого атрибута вершин, составляющих данные нашей модели. Формат у нас float3, оффсет равен 0, индекс буфера 0, а stride равен размеру данных на одну вершину в байтах. В результате функция loadResources примет следующий вид:

Swift
1
2
3
4
5
6
7
8
9
func loadResources() {
    let vertices: [Float32] = [-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0]
    buffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Float32>.size, options: [])!
    vertexDescriptor = MTLVertexDescriptor()
    vertexDescriptor.attributes[0].format = .float3
    vertexDescriptor.attributes[0].offset = 0
    vertexDescriptor.attributes[0].bufferIndex = 0
    vertexDescriptor.layouts[0].stride = 3 * MemoryLayout<Float32>.size
}
И не забудем в конце конструктора вызывать нашу новую функцию

Swift
1
2
3
4
5
6
7
8
class Renderer: NSObject, MTKViewDelegate {
    //...
    init(view: MTKView, device: MTLDevice) {
        //...
        loadResources()
    }
    //...
}
Шейдеры

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

Добавим к проекту файл Metal с именем Shaders.metal. Этот файл будет содержать все написанные нами функции шейдера. Этот файл будет написан не на Swift, а на варианте языка программирования C++ (основанном на С++14), называемом Metal Shading Language (спецификация языка).

Оба шейдера будут в этом файле. В начале подключим заголовок и неймспейс.

C++
1
2
#include <metal_stdlib>
using namespace metal;
Далее определим структуру с входными данными вершинного шейдера.

C++
1
2
3
struct VertexIn {
    float3 position [[attribute(0)]];
};
У нас всего одно поле с типом float3 и именем position, пометка [[attribute(0)]] означает что данные используются описанные в нашем Vertex Descriptor под индексом 0.

Теперь определим вершинную функцию

C++
1
2
3
vertex float4 vertex_main(VertexIn vertexIn [[stage_in]]) {
    return float4(vertexIn.position, 1.0f);
}
Функция имеет имя vertex_main (имя может быть любым, но уникальным), возвращает тип float4, помечена ключевым словом vertex, а среди параметров наша структура с пометкой [[stage_in]] (данные поступают из конвеера). В теле просто возвращаем позицию вершины из нашей структуры дополнив w координатой.

Теперь определим фрагментную функцию. По аналогии с вершинной у нее имя fragment_main, возвращаемый тип float4, используется ключевое слово fragment и нет входных параметров (в нашем примере не нужны). Возвращаем просто красный цвет в формате [r, g, b, a]

C++
1
2
3
fragment float4 fragment_main() {
    return float4(1.0f, 0.0f, 0.0f, 1.0f);
}
В результате файл Shaders.metal имеет следующий вид

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <metal_stdlib>
using namespace metal;
 
struct VertexIn {
    float3 position [[attribute(0)]];
};
 
vertex float4 vertex_main(VertexIn vertexIn [[stage_in]]) {
    return float4(vertexIn.position, 1.0f);
}
 
fragment float4 fragment_main() {
    return float4(1.0f, 0.0f, 0.0f, 1.0f);
}
Загрузка шейдеров и Render Pipeline Descriptor

Вернемся к файлу Renderer.swift. Создадим функцию buildPipeline в которой загрузим наши шейдеры и создадим описание Render Pipeline.

Для загрузки шейдеров надо создать библиотеку вызовом всего метода у девайса под названием makeDefaultLibrary

Swift
1
2
3
guard let library = device.makeDefaultLibrary() else {
    fatalError("Could not load default library from main bundle")
}
Теперь создадим в классе поле для Render Pipeline State var renderPipeline: MTLRenderPipelineState!. А в нашей функции создадим объект типа
Objective-C
1
MTLRenderPipelineDescriptor
и заполним у него нужные нам параметры

Objective-C
1
2
3
4
5
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_main")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_main")
pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
pipelineDescriptor.vertexDescriptor = vertexDescriptor
Создадим шейдерные функции из библиотеки с помощью метода makeFunction передав в него название функции. Укажем формат пикселей взяв его из mtkView, а так же созданный ранее Vertex Descriptor. И завершим все вызовом метода makeRenderPipelineState у девайса, который на основе наших данных создаст нужный нам объект. Не забудем вызывать ее в конце конструктора. В итоге наша функция примет следующий вид:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func buildPipeline() {
    guard let library = device.makeDefaultLibrary() else {
        fatalError("Could not load default library from main bundle")
    }
    let pipelineDescriptor = MTLRenderPipelineDescriptor()
    pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_main")
    pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_main")
    pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
    pipelineDescriptor.vertexDescriptor = vertexDescriptor
    do {
        renderPipeline = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
    } catch {
        fatalError("Could not create render pipeline state object: \(error)")
    }
}
Привет, треугольник

Мы всего в 1 шаге от нашего первого треугольника. Осталось выполнить всего 3 простых действия в функции draw (до вызова endEncoding). Надо установить наши ранее созданные Render Pipeline State и Vertex Buffer, а так же вызвать метод drawPrimitives у commandEncoder как показано ниже:

Swift
1
2
3
commandEncoder.setRenderPipelineState(renderPipeline)
commandEncoder.setVertexBuffer(buffer, offset: 0, index: 0)
commandEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
И после запуска программы появится красный треугольник на зеленом фоне.

Нажмите на изображение для увеличения
Название: 00-result.png
Просмотров: 91
Размер:	88.4 Кб
ID:	8781

Полный код класса рендера будет выглядеть следующим образом:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import Foundation
import MetalKit
 
class Renderer: NSObject, MTKViewDelegate {
    let device: MTLDevice
    let mtkView: MTKView
    let commandQueue: MTLCommandQueue
    var buffer: MTLBuffer!
    var vertexDescriptor: MTLVertexDescriptor!
    var renderPipeline: MTLRenderPipelineState!
    
    init(view: MTKView, device: MTLDevice) {
        mtkView = view
        self.device = device
        commandQueue = device.makeCommandQueue()!
        super.init()
        loadResources()
        buildPipeline()
    }
    
    func loadResources() {
        let vertices: [Float32] = [-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0]
        buffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Float32>.size, options: [])!
        vertexDescriptor = MTLVertexDescriptor()
        vertexDescriptor.attributes[0].format = .float3
        vertexDescriptor.attributes[0].offset = 0
        vertexDescriptor.attributes[0].bufferIndex = 0
        vertexDescriptor.layouts[0].stride = 3 * MemoryLayout<Float32>.size
    }
    
    func buildPipeline() {
        guard let library = device.makeDefaultLibrary() else {
            fatalError("Could not load default library from main bundle")
        }
        let pipelineDescriptor = MTLRenderPipelineDescriptor()
        pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_main")
        pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_main")
        pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
        pipelineDescriptor.vertexDescriptor = vertexDescriptor
        do {
            renderPipeline = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
        } catch {
            fatalError("Could not create render pipeline state object: \(error)")
        }
    }
    
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
    }
    
    func draw(in view: MTKView) {
        let commandBuffer = commandQueue.makeCommandBuffer()!
        if let renderPassDescriptor = view.currentRenderPassDescriptor,
           let drawable = view.currentDrawable,
           let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) {
            commandEncoder.setRenderPipelineState(renderPipeline)
            commandEncoder.setVertexBuffer(buffer, offset: 0, index: 0)
            commandEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
            
            commandEncoder.endEncoding()
            commandBuffer.present(drawable)
            commandBuffer.commit()
        }
    }
}
В конце прикреплю архивы с XCode (hello-triangle.zip) проектом и бинарный App (hello-triangle.app.zip).
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
ИИ не может найти нужный язык в списке
Supersumestria 05.10.2026
Я ему даю вот такое изображение и прошу найти и подчеркнуть немецкий язык. Возвращает он вот это: https:/ / i. **********/ vqBWLe2. png Нужную строчку в 3й колонке просто выдумал. . Это. . .
Новая последняя моя музыка в SUNO
zorxor 05.10.2026
Здравствуйте, дорогие мои друзья! С большой радостью я хотел бы представить вам свою новую последнею музыку, которую сгенерировала мне по моей просьбе нейросеть SUNO. С уважением, zorxor. Это. . .
Nekobox - outbounds[0].transport: unknown transport type: raw
damix 01.10.2026
Фикс ошибки Правым кликом по серверу -> отладочная информация -> edit Заменить "net": "raw", на "net": "tcp", Нажать кнопку reload.
Программный домашний кинотеатр
russiannick 27.09.2026
Сподобился на программный домашний кинотеатр. В качестве ЯВУ по традиции выбрал js. В помощники взял Яндекс-Алису. Было создано три зала на разные интересы. исторические и ретро сериал Хичкок. . .
Беседа с ИИ о программистах, недопускающих к созданию и правке кода генеративные ИИ и причины этого
zorxor 21.09.2026
Раньше я радовался или получал некоторые эмоции, пусть небольшие, но всё же, от самого процесса написания кода, рекомпиляции и запуска, видя постепенное развитие программы и прочее. А теперь лень. . .
Мобильное приложение ColorStep
pavlinmavlin 17.09.2026
Реализовал приложение Красный, Зеленый, Синий в Unity3d + c#. Название изменил на ColorStep. Приложение прошло модерацию и теперь доступно для скачивания. Делал его сам, шаг за шагом — и вот,. . .
Запрет дублирования строк в табличной части
Maks 13.09.2026
Реализация из решения ниже выполнена на нетиповом справочнике "Нормы ТО" с табличной часть "Виды ТО", разработанного в КА2, со следующими реквизитами: - ВидТО (СправочникСсылка. ВидыТО); - ВидГСМ. . .
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр.
Jin X 06.09.2026
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр. Работая с форумом и нейросетями в браузере часто хочется что-то подкорректировать или добавить какого-то функционала. Ниже прикреплён. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru