MetalKit: Introducción al marco de gráficos de Apple
HogarHogar > Noticias > MetalKit: Introducción al marco de gráficos de Apple

MetalKit: Introducción al marco de gráficos de Apple

Aug 20, 2023

MetalKit es un marco de alto nivel que facilita el manejo del Metal. A continuación se explica cómo empezar a utilizar el marco 3D de Apple.

Metal es el canal de juegos y gráficos 3D de Apple para renderizar objetos 3D en dispositivos Apple. Diseñado para reemplazar OpenGL y otros marcos 3D, Metal tiene la ventaja de estar optimizado para que el hardware de Apple alcance el máximo rendimiento.

Apple proporciona una representación 3D fluida en dispositivos Apple a niveles de rendimiento que no son posibles con otros marcos 3D.

Probablemente hayas visto un ejemplo de renderizado de Metal en un dispositivo iOS o macOS si te has suscrito y ejecutas la aplicación de juegos Arcade de Apple. La breve animación de introducción en Arcade está renderizada con Metal:

En 2015, en la WWDC, Apple presentó otro marco de nivel superior para Metal llamado MetalKit. Este marco facilita el uso de Metal al proporcionar algunas funciones de nivel superior que simplifican el desarrollo de aplicaciones 3D.

Específicamente, MetalKit proporciona API de Metal adicionales en las siguientes áreas:

Con MetalKit, la carga de activos y texturas es más fácil mediante la clase MTKTextureLoader. Esta clase proporciona una manera fácil de cargar recursos y texturas, así como también establecer opciones de textura.

Estas opciones incluyen uso y carga de mapas MIP, modos de almacenamiento y caché, transformación de coordenadas de textura, uso de textura de cubo y opciones de color RGB.

Un mapa mip (o mapa MIP) es simplemente una imagen de varias capas en la que cada capa tiene una resolución progresivamente menor que la capa anterior. Los mapas Mipmap se utilizan para acelerar la representación de imágenes y eliminar artefactos de alias, como los patrones Moire.

Un patrón Moiré es un patrón de artefactos de color o bandas que distraen la atención que a veces aparece en gráficos por computadora que consisten en líneas o patrones de píxeles regulares, como cuadrículas de píxeles alternas:

La documentación completa para MTKTextureLoader está disponible en el sitio web para desarrolladores de Apple en la documentación de Metal framework en Documentation/MetalKit/MTKTextureLoader.

Model I/O es el marco de desarrollo de Apple para gestionar activos 3D y 2D. La integración de Model I/O de MetalKit incluye API para cargar rápidamente texturas en búferes de Metal y utilizar datos de malla mediante contenedores.

Actualmente hay alrededor de media docena de clases relacionadas con Model I/O en MetalKit, la mayoría relacionadas con mallas. (Llegaremos a las clases y la programación orientada a objetos en un minuto).

La mayoría de las aplicaciones de iOS y macOS utilizan vistas: clases estándar que presentan información visual y elementos de la interfaz de usuario en pantalla. Las diferentes subclases de vistas proporcionan diferentes tipos de vistas.

Por ejemplo, en iOS, UIView es la clase base de vista, pero UIButton es una clase de vista de botón derivada de UIView. Al utilizar clases de vista orientadas a objetos en iOS o macOS, puede crear funciones adicionales que se basan en funciones estándar ya definidas por las clases de Apple.

Esto se conoce como herencia de objetos. Piense en un objeto en una aplicación como un paquete de código que encapsula tanto el código como los datos sobre los que opera el código. Al agrupar ambos en objetos, el código se puede reutilizar y reutilizar fácilmente mediante objetos adicionales.

En particular, en MetalKit, se proporciona una nueva clase, MTKView, que permite a los desarrolladores crear vistas Metal completas en aplicaciones. Al tener una clase de vista Metal dedicada, Metal puede dibujar y administrar la vista de manera óptima sin ningún código adicional.

La documentación de Apple para MTKView se encuentra en el sitio web del desarrollador en Documentation/MetalKit/MTKView. MTKView también requiere que primero configures un MTLDevice en una de sus propiedades para indicarle en qué dispositivo y pantalla renderizar objetos metálicos.

MTKView también proporciona un MTLRenderPassDescriptor cuando se le pregunta en qué puede renderizar sus texturas. Consulte la sección Documentación/Metal/Pases de renderizado del sitio para desarrolladores de Apple.

En la programación orientada a objetos (OOP), los objetos se definen mediante clases. Una clase es una definición en un archivo de código fuente que define lo que contiene un objeto y, en Swift, la implementación real de un objeto.

Una clase define métodos (funciones) que pueden recibir mensajes que les envían otros objetos para realizar alguna función. Cada método contiene código para realizar algún trabajo.

Una clase también define propiedades o variables que pueden contener datos. Normalmente, los métodos de una clase realizan algún trabajo sobre las propiedades de la clase. La mayoría de los métodos, pero no todos, pueden leer la mayoría (pero no todas) las propiedades contenidas en la clase o en una de sus superclases (clases principales).

Todo lo anterior se conoce como encapsulación de objetos. Los objetos encapsulan tanto datos como métodos para mantener todo ordenado y organizado. Es más fácil transportar, hacer referencia, copiar y utilizar objetos con sus datos asociados en un solo lugar que tener que realizar un seguimiento de los datos por separado.

La herencia es una característica de programación orientada a objetos mediante la cual se pueden definir nuevas clases a partir de otra clase. El objeto derivado se llama subclase y la clase padre se llama superclase.

Se pueden definir largas cadenas de objetos mediante subclases. La herencia es poderosa porque le permite reutilizar el código existente casi sin trabajo.

Las subclases heredan todo el comportamiento y las propiedades de sus clases principales casi sin trabajo adicional. Las subclases pueden agregar métodos adicionales que sólo ellas (o sus subclases) conocen.

Aún mejor, cuando creas una instancia (una copia) de un objeto en un programa, también crea una instancia de todos sus objetos de superclase automáticamente.

Con una línea de código, puede obtener amplios niveles de funcionalidad del programa simplemente creando una instancia de una clase.

La creación de instancias consiste simplemente en crear un objeto, asignarle memoria en la RAM y ponerlo a disposición de un programa.

Todo esto suele definirse en uno o, en el caso de Objective-C, dos archivos de código fuente (normalmente uno o dos archivos por clase).

Entonces, en nuestra discusión anterior, un MTKView se define como una clase (por Apple) y se crea una instancia cuando se crea en código (por usted). El resultado es un objeto MTKView en la memoria, listo para usar. Cuando el objeto MTKView ya no es necesario, se desasigna, lo que lo elimina de la memoria y lo destruye.

La mayoría de las aplicaciones son programas que crean, usan, administran y destruyen cientos de dichos objetos.

El paradigma de programación orientada a objetos es poderoso porque reduce enormemente la cantidad de código necesario mediante la subclasificación y la reutilización, y mantiene los programas más modulares y reutilizables al encapsular código y datos.

Una vez que haya escrito una clase para realizar un trabajo específico, simplemente puede reutilizar la clase o crear una subclase en otro programa para crear otra aplicación rápidamente.

Como muchas vistas estándar de iOS o macOS, MTKView también tiene una capa de animación central. Core Animation es el marco de animación 2D de alto rendimiento de Apple.

La mayoría de las vistas tienen un CALayer, un objeto de capa de animación central que puede dibujar y animar gráficos 2D. Los CALayers se pueden agrupar y combinar para crear animaciones complejas.

MTKView tiene su propia subclase CALayer llamada CAMetalLayer en la que Metal puede renderizar. Puede combinar CAMetalLayer con otras capas de CA para crear animaciones combinadas en 2D y 3D.

En la mayoría de los casos, tanto para CALayers 2D como 3D, el dibujo es mucho más rápido y más eficiente que el dibujo que ocurre en UIViews. También puede establecer la opacidad o alfa de las capas CA para hacer que algunas de ellas sean transparentes.

MTKView admite tres modos de dibujo:

En el dibujo cronometrado, la vista se actualiza a intervalos regulares establecidos internamente en el objeto. La mayoría de los juegos utilizan este modo cuando la escena de un juego se representa o dibuja a una velocidad específica descrita en fotogramas por segundo (fps).

Con el modo Temporizado, también puede configurar o borrar la propiedad isPaused para iniciar y detener la animación de la vista.

En el modo de notificación, el redibujado ocurre cuando una parte de toda la vista queda invalidada. Esto le permite volver a dibujar solo una parte de la vista o capa, lo que lleva menos tiempo y mejora el rendimiento del juego.

Para forzar un nuevo dibujo usando el modo de notificación, simplemente envíe al objeto de vista un mensaje setNeedsDisplay() para forzarlo a volver a dibujar. Esto obliga a la vista a volver a dibujar todas sus subvistas enviándoles también a cada una un mensaje setNeedsDisplay().

En el dibujo explícito, se vuelve a dibujar el contenido de la vista enviando al objeto de la vista un mensaje draw() directamente. Por lo general, no se recomienda esto a menos que tenga algún flujo de trabajo de dibujo personalizado que utilice y que haga algo fuera de la jerarquía de vista/subvista estándar.

También puede volver a dibujar solo partes de una vista enviando el mensaje setNeedsDisplay() de sus subvistas, evitando así el redibujo de la vista de nivel superior. En general, cuantos menos objetos se vuelvan a dibujar, mejor será el rendimiento.

En el caso de un MTKView o una subclase del mismo, en su método de dibujo, obtiene un MTLRenderPassDescriptor de la vista, lo representa y luego presenta el elemento de diseño resultante para su visualización.

Un dibujable es cualquier objeto metálico que haya sido codificado y esté listo para mostrarse.

En los lenguajes de programación Swift y Objective-C de Apple, un delegado es un objeto que realiza algún trabajo en nombre de otro objeto.

Por lo general, un objeto declarará un objeto delegado como una de sus propiedades y el delegado declara qué métodos (funciones) proporciona.

Los delegados son poderosos porque puedes cambiar el comportamiento de un objeto simplemente cambiando su propiedad de delegado. Los delegados también se utilizan para proporcionar funcionalidad adicional a los objetos sin tener que subclasificar un objeto para crear otro.

MTKView tiene su propio objeto delegado llamado clase MTKViewDelegate que también se describe en la documentación de Apple. MTKViewDelegate responde principalmente para ver eventos de redibujado y cambio de tamaño.

MTKViewDelegate también hereda de un protocolo estándar Objective-C común a todos los objetos de Apple llamado NSObjectProtocol.

Piense en los delegados y protocolos como objetos y métodos adicionales que pueden adjuntarse o "pegarse" a otros objetos.

En Objective-C y Swift, un protocolo es simplemente una lista de métodos adicionales que una clase debe implementar. El contenido de cada método en un protocolo depende de cada clase para definirlo.

MTKViewDelegate se ocupa principalmente de cambiar el diseño de una vista (en la rotación del dispositivo, por ejemplo) y dibujar.

Por ejemplo, podría definir varios objetos MTKViewDelegate, cada uno con un comportamiento diferente, luego cambiar el comportamiento de dibujo o rotación de su MTKView simplemente restableciendo su propiedad delegada a cualquiera de los objetos delegados a voluntad y volviendo a dibujar.

Cuando utilice MTKView, implemente los métodos de MTKViewDelegate en su renderizador. Esto permite que su renderizador interactúe con MTKView y proporcione cambios de dibujo y diseño.

Puede obtener información cuando llegue el momento de renderizar cada fotograma utilizando la propiedad currentRenderPassDescriptor de MTKView. Esto le permite interactuar con cada cuadro que se va a renderizar.

Por ejemplo en Swift:

si deja onscreenDescriptor = view.currentRenderPassDescriptor

Esto obtiene el descriptor de paso de renderizado actual de MTKView y lo almacena en una variable llamada onscreenDescriptor.

Después de renderizar, debes usar el dibujable para actualizar el contenido de la vista. Para hacerlo, llame al método present(_:) en el objeto MTLCommandBuffer, luego envíe el mensaje commit() y el búfer de comando a la GPU para su visualización.

Hay una discusión más detallada de este proceso en la documentación de MTKView.

Apple también tiene un marco relacionado con las matemáticas llamado SIMD que resulta útil al manipular objetos 3D y 2D y realizar cálculos sobre ellos y matrices. La mayoría de estas funciones se utilizan para realizar cálculos matemáticos de punto flotante rápidos y eficientes que prevalecen en los cálculos 3D.

SIMD puede resultar útil cuando necesita transformar objetos 3D y vértices de objetos. La estructura de datos más común y útil en SIMD es simd_float4x4, que es una matriz de cuatro por cuatro de valores flotantes de precisión simple.

Armado con todo este conocimiento previo, ahora está listo para crear una aplicación MetalKit en Xcode. En el siguiente ejemplo, asumiremos que creará una aplicación 3D simple que contiene una sola escena que contiene un solo objeto Metal 3D.

Para escribir una aplicación Xcode MetalKit necesitarás estar familiarizado con los lenguajes de programación Swift y Objective-C de Apple, y un poco de ANSI-C, un lenguaje anterior solo C inventado en Bell Labs en 1972 cuando se creó UNIX.

Para comenzar, abra Xcode y seleccioneArchivo->Nuevo proyecto desde el menú Archivo. En el selector de plantilla de proyecto, elijaiOSoMac OSen la parte superior, luego eligeJuegode los íconos a continuación y haga clicPróximo:

En el siguiente panel, ingrese el nombre de la aplicación, el ID del paquete y la información de la organización y seleccioneRápidoyMetaldesde los dos menús emergentes inferiores:

Hacer clicPróximoy guarde su nuevo proyecto Xcode en el disco.

También necesitarás definir una imagen de textura para tu objeto 3D como un archivo .png y agregarla a tu proyecto Xcode. Este archivo de textura se "envuelve" en su objeto 3D en el momento del renderizado.

La aplicación de plantilla de juego Metal de Xcode proporciona los archivos fuente de plantilla predeterminados mínimos que necesitará para su aplicación, pero primero, deberá agregar los marcos de Metal para indicarle a Xcode que vincule esos marcos a su aplicación en tiempo de ejecución.

Para hacerlo, en el editor de proyectos de Xcode, seleccione el nombre de su proyecto seleccionando el ícono del proyecto en la esquina superior izquierda de la ventana del proyecto, luego seleccione el nombre del objetivo a la derecha en la sección Objetivos:

Desplácese hasta la parte inferior de la ventana y, en la sección "Marcos, bibliotecas y contenido integrado", haga clic en"+" botón. Aparecerá el panel de selección de marco.

Escriba "metal" en el cuadro de búsqueda en la parte superior y presione Comando y haga clic en seis de los siete marcos enumerados, excluyendo "MetalFX.framework". Hay cientos de marcos Xcode disponibles.

También querrás agregar la biblioteca libswiftsimd.tbd, los marcos de Core Services y, opcionalmente, el marco Accelerate.

"tbd" es un marcador de posición para "Por determinar", ya que los números de versión de las bibliotecas de códigos reales pueden cambiar. Incluir una biblioteca .tbd en Xcode le dice a Xcode que use la versión más reciente de esa biblioteca.

Si desea utilizar Model I/O para administrar activos, agregue también "libswiftModelIO.tbd" y "ModelIO.framework".

Si creó una aplicación de iOS en el selector de plantillas, agregue también UIKit.framework. Si creó una aplicación macOS en el selector de plantillas, agregue también Cocoa.framework.

Finalmente, incluya Foundation.framework y CoreFoundation.framework. Foundation es un marco central en lenguaje C que utilizan la mayoría de las aplicaciones de iOS y macOS. Todas las llamadas a Foundation API están en C simple.

El código completo para una aplicación de juego Metal está más allá del alcance de este artículo, por lo que aquí cubriremos brevemente solo los conceptos básicos para nuestro ejemplo de un solo objeto. La plantilla de proyecto de muestra de Apple crea un único cubo 3D que gira en el espacio.

Xcode crea un archivo delegado de la aplicación que controla el bucle de eventos general de la aplicación en sí, y un archivo ShaderTypes.h que es un archivo de encabezado que define la malla del sombreador y la información del vértice junto con una estructura C que define la matriz de proyección y la matriz de vista del modelo.

Estos son utilizados por el sombreador al dibujar.

El archivo "Shaders.metal" importa el archivo de encabezado "ShaderTypes.h" definido anteriormente, que se comparte entre el renderizador y el archivo GameViewController.swift, que lo obtendremos en un momento. Importa archivos de encabezado a otros archivos de código fuente Swift u Objective-C utilizando la directiva de preprocesador de importación:

#importar "ShaderTypes.h"

Las directivas del preprocesador son instrucciones del compilador que se ejecutan antes de la compilación real y generalmente comienzan con un signo "#".

"Shaders.metal" también importa otros dos archivos, metal_stdlib y simd.h usando la anterior directiva de importación ANSI-C #include. Tanto #import como #include son similares y no entraremos en las diferencias detalladas entre los dos aquí.

Debajo verás esta línea:

usando metal de espacio de nombres;

Los espacios de nombres son un modismo de C++ que permite definir y aislar secciones de código similares o idénticas definiéndolas bajo un espacio de nombres. Cada espacio de nombres tiene su propio nombre, en este caso metal.

En Shaders.metal usted define una estructura Vertex y ColorInOut, y varias funciones que definen los sombreadores; en este caso, solo un sombreador de vértices y un sombreador de fragmentos. El sombreador de fragmentos también contiene una variable de muestra que le permite definir y usar mapas mip si lo desea.

La función fragmentShader toma como argumentos información de color, una estructura Uniforms definida en SharderTypes.h y una textura2d como se define en el encabezado de la biblioteca Metal "metal_texture".

El parámetro Uniformes contiene, como se analizó anteriormente, la matriz de proyección y la matriz de vista del modelo.

El siguiente archivo, Renderer.swift, define la clase Renderer del objeto que hereda de la clase base Objective-C, NSObject y se ajusta al protocolo MTKViewDelegate.

Como nota un poco histórica, NSObject se remonta a los días de NeXT Computer, la segunda compañía de Steve Jobs después de que fue despedido de Apple en 1985. NeXT inventó Objective-C y tenía un sistema operativo y un marco llamado NeXTStep. "NS" en NSObject significa "NeXTStep".

La mayoría de los primeros objetos NeXTStep tenían el prefijo "NS" para diferenciarlos de los objetos de terceros. Cuando Apple compró NeXT Computer Inc. en 1997, adquirió toda la tecnología de NeXT, incluido NeXTStep.

Hasta el día de hoy, macOS e iOS se basan en NeXTStep.

Las propiedades de la clase Renderer incluyen MTLDevice, MTLCommandQueue, MTLBuffer, MTLRenderPipelineState, MTLDepthStencilState y MTLTexture, así como propiedades para matrices, rotación, malla y un semáforo.

Un semáforo es un hilo (ruta de ejecución) que se basa en una bandera para indicarle cuándo puede ejecutarse y cuándo no.

Cuando creas una instancia de un objeto Render, le pasas un MTKView en su método init, al que llegaremos en un momento.

Tan pronto como se crea el objeto, se ejecuta su método init y se ejecuta todo el código de ese método.

El método init configura y asigna todas sus propiedades en la parte superior del método, luego crea un búfer de renderizado a través de la línea self.device.makeBuffer().

Luego, establece algunas propiedades en metalKitView pasado al método init, crea un descriptor de vértice mediante Renderer.buildMetalVertexDescriptor() y luego construye la canalización de renderizado mediante Renderer.buildRenderPipelineWithDevice().

A continuación, el código crea información de profundidad y plantilla, y luego crea una malla a través de Renderer.buildMesh.

Finalmente, crea un mapa de color y una textura a través de Renderer.loadTexture().

Necesitará usar el método de carga de texturas del Renderer, loadTexture:device:textureName: para cargar su textura desde el archivo .png que creó anteriormente, pasando el método del nombre de archivo de su textura, en este ejemplo "ColorMap".

La construcción Swift do/catch es para el manejo de errores. Se intenta ejecutar el código contenido en do y, si falla, se ejecuta el bloque catch; de lo contrario, la ejecución del programa continúa normalmente.

Finalmente, al final del método init del renderizador, se ejecuta el método init de la superclase:

super.init()

Enviar el mensaje super.init() a la superclase al final del método init de una clase Swift garantiza que se cree toda la cadena de objetos en la jerarquía de clases. Este es un patrón estándar en Swift y Objective-C.

Si no llama al método init de una superclase, es muy probable que el objeto falle o, en el mejor de los casos, no funcione correctamente, o que su propia aplicación falle.

Dado que las subclases dependen de los métodos de la superclase mientras se ejecutan, si el objeto de la superclase no existe, la llamada al método de una subclase puede enviarse a un espacio de memoria aleatorio donde el código que espera no existe.

Cuando eso sucede y el procesador intenta ejecutar el código en esa ubicación de memoria, es seguro que se producirá un fallo: no hay ningún código allí para ejecutar.

super.init() generalmente se llama al final en Swift y Objective-C para darle tiempo a su objeto para realizar cualquier configuración que necesite antes de configurar el objeto de superclase.

Finalmente, el método init del Renderer termina con la llave de cierre "}".

Inmediatamente después del método init() en Renderer.swift están las implementaciones reales de los otros métodos en la clase Renderer. Cada función Swift tiene el prefijo class func seguido del nombre de la función y cualquier parámetro de la función entre paréntesis.

Si un método Swift devuelve un valor al finalizar, ese tipo de valor de retorno se define mediante la construcción ->. Por ejemplo:

clase func buildMetalVertexDescriptor() -> MTLVertexDescriptor define un método (función) llamado buildMetalVertexDescriptor que devuelve un MTLVertexDescriptor al completarse exitosamente. Esto se denomina valor de retorno o tipo de retorno.

Como vimos anteriormente, el método buildMetalVertexDescriptor se llama internamente al crear instancias de objetos desde el método init(). Muchos objetos funcionan de esta manera.

Pero la mayoría de los métodos también pueden ser llamados desde objetos externos a menos que una definición de clase lo prohíba explícitamente.

El bucle de juego del Renderer controla la mayoría de los juegos de Metal, junto con los métodos de dibujo del Renderer y MTKView. Esto, combinado con el bucle de eventos principal monitoreado en el objeto delegado de la aplicación, impulsa la aplicación mientras se ejecuta en un dispositivo.

En el archivo Render.swift notarás un método llamado función privada updateGameState(). Este método se puede ejecutar periódicamente para actualizar cualquier estado almacenado en el juego, como posiciones de objetos, entradas del mouse, teclado o controlador de juego, posición, tiempo, puntuaciones, etc

La palabra clave Swift private significa que este método es privado y solo se puede llamar desde este objeto y cualquier extensión definida en este archivo fuente únicamente; los objetos externos no pueden enviar ese mensaje al objeto Renderer.

Este control de acceso adicional garantiza la ejecución correcta del programa solo desde dentro y por ciertos objetos; en este caso, dado que el Renderizador es responsable de la ejecución general y el control del juego, no querrás que ningún objeto externo interfiera con él.

Apple tiene una sección completa de Control de acceso a objetos en la documentación para desarrolladores de Swift en el sitio web de documentación de Swift.

A continuación, en Renderer.swift, vemos el método draw():

función dibujar (en vista: MTKView)

Pasa el MTKView en el que desea realizar el dibujo. Tenga en cuenta que esta función no tiene valor de retorno. Estas funciones en Swift y Objective-C se denominan funciones nulas.

En el método draw(), que se llama una vez por cuadro, se le dice al semáforo que espere una cierta cantidad de tiempo:

enFlightSemaphore.wait()

Luego, el búfer de comando se crea y se envía al semáforo para su procesamiento, agregando un controlador de finalización.

Un controlador de finalización es una función que se ejecuta automáticamente cuando finalizan otras tareas o subprocesos. Los controladores de finalización son una forma de ejecutar código de forma secuencial, pero solo cuando finaliza alguna otra sección del código.

Los controladores de finalización proporcionan una ejecución garantizada del código, pero sin tener que escribir código para gestionar algoritmos de temporizador complejos y condiciones de espera.

A continuación, los buffers de objetos 3D y el estado del juego se actualizan en ese orden:

self.updateDynamicBufferState()

self.updateGameState()

A continuación, se obtiene un descriptor de paso de renderizado de MTKView y se actualizan las propiedades del codificador de paso de renderizado:

dejar renderPassDescriptor = view.currentRenderPassDescriptor

Luego se ejecuta un bucle corto para obtener los diseños y los búferes de los descriptores de vértices de malla y almacenarlos en el codificador de renderizado. Luego se establece la información de textura del fragmento del codificador de renderizado:

renderEncoder.setFragmentTexture(colorMap, índice: TextureIndex.color.rawValue)

A continuación, para cada malla (objeto) en la matriz .submeshes, se llama a renderEncoder.drawIndexedPrimitives(). Aquí es donde se codifica cada objeto de la escena.

Para finalizar la fase de codificación, se llama a renderEncoder.endEncoding(). Ahora todos los objetos están listos para ser dibujados.

El elemento dibujable de la vista se obtiene mediante:

si deja dibujable = view.currentDrawable

y si tiene éxito, todo el búfer de comando se dibuja con:

comandoBuffer.commit()

La llamada a confirmar en realidad envía el fotograma de la escena a la GPU para mostrarlo en pantalla.

Todo lo anterior sucede a treinta, sesenta o ciento veinte fps.

El archivo Renderer.swift termina con algunas transformaciones matemáticas 3D generales y funciones de rotación.

Notarás dos archivos adicionales en el proyecto Xcode: GameViewController.swift y Main.storyboard. Estos son archivos típicos que se encuentran en la mayoría de las aplicaciones de iOS.

Una aplicación iOS típica contiene una clase UIViewController central de nivel superior definida en el marco UIKIt. Un UIViewController es una clase que administra y controla otra clase UIKIt: UIView.

Una clase UIView es una clase que contiene otras subclases de UIView, como botones, imágenes, texto y otros objetos UIKIt. UIView es cómo se representa en pantalla la interfaz de usuario de una aplicación de iOS.

Cada clase UIViewController tiene una propiedad llamada vista que es una instancia de UIView. El controlador de vista administra UIView.

Si observa la documentación de Apple para UIViewController, notará media docena de métodos para administrar la vista, es decir, métodos para cargar la vista, recibir notificaciones cuando la vista se carga y descargar vistas.

En la mayoría de las aplicaciones de iOS, no carga la vista de nivel superior directamente; la carga creando una instancia de una subclase UIViewController que usted define (en este ejemplo, un GameViewController). La parte de la interfaz de usuario de la vista se crea en el editor Interface Builder de Xcode o mediante una vista SwiftUI de solo texto.

Normalmente, al crear una aplicación para iOS, diseñas cada vista en Interface Builder arrastrando componentes visuales desde la biblioteca Xcode y soltándolos en un objeto controlador de vista en pantalla.

Una vez que todos los objetos de la interfaz de usuario estén dispuestos en la pantalla, los conectará a las propiedades de un controlador de vista mediante Control-clic y arrastre desde cada elemento de la interfaz de usuario hasta el primer respondedor del controlador de vista. Un primer respondedor es el primer objeto en una jerarquía de objetos de controlador de vista que es capaz de responder a los mensajes de ese objeto.

Cuando suelta el botón del mouse desde Control, hace clic y arrastra hacia arriba, Xcode muestra una lista de propiedades de objeto disponibles para conectar el objeto.

Eso es todo, normalmente no es necesario realizar ninguna codificación para cada elemento de la interfaz de usuario; cuando se crea una instancia del controlador de vista y se carga en la memoria, el tiempo de ejecución de Swift u Objective-C realiza todas las conexiones de la interfaz de usuario automáticamente.

Esto simplifica enormemente el desarrollo de aplicaciones.

Posteriormente se agregaron archivos de guión gráfico a Xcode para simplificar aún más el diseño de la interfaz de usuario: con los guiones gráficos usted define las transiciones entre vistas: cuando los usuarios navegan entre cada vista en el dispositivo, se llama a la función Segue que usted indica, donde luego puede realizar cualquier configuración de vista inicial o limpiar.

Las secuencias eliminan la mayor parte del código de carga de vistas.

En cualquier caso, cuando un controlador de vista termina de cargar una vista, se llama al método viewDidLoad() del controlador. Es en viewDidLoad() donde realiza cualquier configuración de vista adicional que necesite. Una vez que sale viewDidLoad(), la vista está lista para usar y se muestra en pantalla al usuario.

Puede subclasificar UIViewController y UIView para que sus vistas sean altamente personalizables. Lo único que hay que recordar es que la mayoría de los elementos de la interfaz de usuario en iOS se almacenan como propiedades en una subclase UIViewController.

Es posible crear vistas y controladores de vista completamente en código sin un Storyboard o Interface Builder, pero hacerlo es mucho más complejo y requiere mucho más tiempo.

Echemos un vistazo a GameViewController.swift

La clase se define en la parte superior del archivo:

clase GameViewController: UIViewController

Esto significa que GameViewController es una subclase de UIViewController.

La definición de clase está contenida entre corchetes abiertos y cerrados ("{" y "}").

Tenga en cuenta que la clase GameViewController es muy corta: poco más de una página. La mayor parte del trabajo de procesamiento del juego se realiza en los sombreadores y renderizadores.

A continuación, vemos dos propiedades Swift definidas por la palabra clave var:

var renderizador: ¡Renderizador!

var mtkView: ¡MTKView!

A continuación vemos que GameViewController anula el método UIViewController viewDidLoad() usando la palabra clave de anulación Swift:

anular la función viewDidLoad()

Esto significa que cuando el controlador de vista carga la vista y envía el mensaje viewDidLoad(), se ejecutará la versión GameViewController del método en lugar de la versión UIViewController. Este es un ejemplo perfecto de herencia en acción: puedes elegir dejar que se ejecute el método de una superclase o anularlo en una subclase y usar ese método en su lugar.

Tenga en cuenta que para que esto funcione, las declaraciones de ambos métodos en ambas clases deben ser idénticas.

Lo primero que hace la función de anulación viewDidLoad() es enviar a la superclase (UIViewController) un mensaje viewDidLoad(). Esto permite que UIViewController realice cualquier inicialización del diseño de vista de UI que necesite.

Sin esta "súper" llamada, la vista no funcionará correctamente porque algunas de sus partes internas nunca se inicializarán.

A continuación, el objeto GameViewController carga MTKView y lo almacena en su propiedad interna mtkView:

guard let mtkView = ver como? MTKVer más

guard es simplemente una prueba condicional de Swift para ver si algo tuvo éxito, similar a if.

GameViewController luego también almacena una referencia al dispositivo Metal del dispositivo en su propiedad interna defaultDevice.

guard let defaultDevice = MTLCreateSystemDefaultDevice() más

Lo importante a entender aquí es que las dos propiedades o variables internas:

var renderizador: ¡Renderizador!

var mtkView: ¡MTKView!

almacena referencias a otros objetos en la memoria, en este caso el renderizador y la vista Metal. Una vez almacenado, el objeto GameViewController puede acceder a esos objetos a voluntad. Este patrón es cómo funcionan la mayoría de los objetos en Swift y Objective-C.

En Objective-C estas dos propiedades se habrían declarado como:

Renderizador *renderizador = nil;

MTKView *mtkView = nulo;

nil es un marcador de posición de Objective-C que significa "nada" o, más específicamente, ninguna dirección en la memoria. nil se usa para indicar que una propiedad o variable de Objective-C no contiene nada.

El '*' es un indicador estándar para un puntero C u Objective-C: una variable que contiene una dirección de memoria para un objeto en lugar de un valor. Los consejos son un tema complejo, por lo que no los abordaremos aquí.

También tenga en cuenta que las líneas de código Objective-C y C deben terminar con un ';' (punto y coma). Esto no es opcional: sin el punto y coma, el código no se compilará y obtendrá un error.

Swift eliminó el punto y coma (pero aún puedes usarlos si lo deseas).

A continuación, GameViewController almacena más referencias a otros objetos, pero esta vez dentro del objeto de propiedad mtkView:

mtkView.device = dispositivo predeterminado

mtkView.backgroundColor = UIColor.negro

Esto significa almacenar el dispositivo de renderizado predeterminado en la propiedad mtkView.device y almacenar un UIColor negro en tkView.backgroundColor.

UIColor es un objeto UIKit estándar para indicar el color; en este caso, establecido en negro, que se utilizará como color de fondo de la escena. Cada objeto UIColor tiene una propiedad .backgroundColor.

Tenga en cuenta que lo que realmente está haciendo aquí es almacenar referencias a objetos en propiedades que son a su vez propiedades de las propiedades de esta clase. Esto resulta confuso al principio, pero una vez que lo dominas es fácil de entender.

Al encadenar propiedades entre objetos, en realidad solo estás encadenando objetos en Dasiy.

Puede tener propiedades que apunten a propiedades, que apunten a otros objetos. En teoría, no hay límite en cuanto a la profundidad que pueden alcanzar las referencias a propiedades.

Antes de liberar (destruir) un objeto, debe establecer todas sus propiedades en nulas en el método deinit() de la clase para garantizar que se liberen todas las referencias a otros objetos. De lo contrario, se pueden producir pérdidas de memoria y ciclos de retención no deseados.

En Objective-C, deinit() se llama dealloc.

Continuando, se crea el objeto Renderer, pasando el objeto MTKView y se almacena una referencia (puntero) al Renderer en la propiedad renderer del controlador de vista:

guard let newRenderer = Renderer(metalKitView: mtkView) más

renderizador = nuevorenderizador

Primero, crea el objeto y luego almacena una referencia a él en una propiedad.

Luego, al puntero del renderizador a MTKView se le envía el mensaje drawableSizeWillChange:

renderer.mtkView(mtkView, drawableSizeWillChange: mtkView.drawableSize)

Esto le permite al renderizador saber cuál es el tamaño dibujable actual de la vista para saber cómo y dónde escalar la escena cuando se envía a la GPU. Tenga en cuenta que el tamaño del diseño ya está almacenado en MTKView en su propiedad .drawableSize.

Esto demuestra que puedes pasar las propiedades de un objeto a métodos como parámetros.

Finalmente, el delegado de la vista se establece en el propio renderizador:

mtkView.delegate = renderizador

Recuerde que en el archivo Renderer.swft la clase Renderer se declara conforme al protocolo MTKViewDelegate:

Renderizador de clases: NSObject, MTKViewDelegate

Esto es lo que permite que la propiedad mtkView.delegate se establezca en un objeto Renderer. Sin la conformidad del protocolo MTKViewDelegate en la definición de la clase Renderer, la línea mtkView.delegate = renderer probablemente arrojaría una advertencia o un error cuando se compila diciendo que la propiedad del renderizador no se ajusta al protocolo MTKViewDelegate.

También tenga en cuenta que un problema crítico para los recién llegados a Xcode es que antes de destruir un objeto de controlador de vista, primero debe establecer su propiedad .delegate en nulo. De lo contrario, se garantizará que su aplicación falle.

De hecho, esto se aplica a cualquier objeto Swift u Objective-C que contenga delegados, no solo a los controladores de vista.

¿Por qué? Porque si no libera primero la referencia almacenada en la propiedad delegada, entre el momento en que el objeto que lo contiene realmente desaparece de la memoria y el momento en que el sistema se da cuenta de que el objeto ha sido destruido, es posible que algún otro objeto haya enviado otro mensaje al objeto delegado.

Sin darse cuenta de que el objeto que contenía la propiedad delegado ya no existe, es posible que el mensaje enviado al delegado aún esté esperando a ser procesado, y cuando se procesa, el delegado ya no es válido porque el objeto que lo contiene ya no existe.

El delegado queda colgando en la memoria, pero el objeto que lo contiene ya no existe y, por lo tanto, el sistema no tiene forma de localizar el objeto delegado al que está destinado el mensaje.

Boom: un choque.

Enviar un mensaje a cero en Swift y Objective-C no tendrá ningún efecto dañino y es válido, pero enviar un mensaje a una dirección en la memoria donde se supone que está un objeto pero no lo está definitivamente causará un bloqueo.

Ahora finalmente estás listo para ejecutar la aplicación de muestra Metal.

Haga clic en elJugar en la parte superior de la ventana de Xcode y el código se compilará. Si no hay errores y todo funciona, Xcode iniciará el Simulador de iOS y ejecutará la aplicación en él:

Tenga en cuenta que algunos códigos de Metal, pero no todos, no se ejecutarán en el simulador. En su lugar, tendrás que ejecutar esos programas de Metal en un dispositivo iOS real.

Como último vistazo al proyecto de muestra, debemos repasar algunos elementos de Interface Builder.

Si es nuevo en Xcode e Interface Builder, tenga en cuenta que un aspecto crítico del desarrollo de iOS que la mayoría de los recién llegados pasan por alto es el de los nombres de clases. Los nombres de clase que tiene cada elemento en Xcode deben coincidir exactamente con cada nombre de clase tal como se define en los archivos de código fuente.

Si no lo hacen, su aplicación no funcionará.

Por ejemplo, el controlador de vista debe tener su nombre de clase establecido en el campo Clase personalizada en el panel de información del objeto de Xcode en el lado derecho. Para hacerlo, debe hacer clic en el archivo Storyboard o .nib (Interface Builder), luego hacer clic en el nombre de la clase en la escena o jerarquía de vista, luego verificar o establecer el nombre de la clase en el inspector de la derecha:

Lo mismo se aplica a las Vistas y sus nombres de clase, y a otros objetos como las propiedades delegadas. No establecer ni siquiera un nombre de clase o propiedad puede hacer que una aplicación no funcione.

La mayoría de estos generalmente se configuran en archivos de plantilla creados por Xcode, pero no está de más comprobarlo.

Una cosa que curiosamente no se configura en los archivos de plantilla de Xcode son las conexiones entre los controladores de vista y sus propiedades de vista. Tienes que realizar estas conexiones manualmente o tu aplicación no funcionará.

Por ejemplo, en nuestro proyecto de muestra, si presiona Control y hace clic en el objeto Controlador de vista de juego en la jerarquía de vistas, notará que la propiedad Ver está establecida en cero. Deberá conectar la Vista al Controlador de vista del juego haciendo clic con la tecla Control y luego arrastrando desde el Controlador de vista del juego a la Vista en la jerarquía.

Cuando lo hagas, aparecerá el panel "Outlets" y deberás conectarte manualmente a la propiedad "view" del objeto Game View Controller:

Sin esta conexión, la aplicación no funcionará. Y los archivos de plantilla de muestra creados por Xcode no realizan esta conexión de forma predeterminada.

Tenga en cuenta que el pequeño punto junto a los nombres de los tomacorrientes en el panel Tomacorrientes indica si un tomacorriente determinado está conectado o no.

También habrás notado que el archivo AppDelegate.swift contiene una subclase de AppDelegate que contiene código repetitivo vacío, pero no hay referencias a GameViewController en ninguna parte del archivo delegado de la aplicación.

Entonces, ¿cómo se carga GameViewController cuando se ejecuta la aplicación?

La respuesta es que el archivo Storyboard define el controlador de vista inicial y lo carga automáticamente cuando la aplicación se ejecuta por primera vez. Si estuviera usando archivos y código antiguos de estilo .nib (Interface Builder) para cargar el controlador de vista inicial, su aplicación habría creado y cargado manualmente una instancia de objeto GameViewController de la aplicación de AppDelegate: método didFinishLaunchingWithOptions.

Una vez que el controlador de vista cargó la vista, obtendría el mensaje viewDidLoad() en el delegado de la aplicación si configura AppDelegate como delegado del controlador de vista.

Además de la documentación en línea de MetalKit y Metal de Apple, hay otros buenos recursos de Metal que quizás quieras consultar.

Asegúrese de visitar metalkit.org y metalbyexample.com, que tienen muchos tutoriales excelentes sobre MetalKit y el propio Metal.

Además, asegúrese de obtener el libro definitivo de terceros sobre Metal, la Guía de programación de Metal: tutorial y referencia a través de Swift de Janie Clayton, que le enseña casi todo lo que hay que saber sobre la programación de Metal.

Este ha sido un tutorial largo, pero ahora debería tener una comprensión mucho mayor de cómo funcionan las aplicaciones Metal y cómo usar MetalKit en sus aplicaciones para cargar texturas y renderizar fácilmente objetos Metal en vistas en aplicaciones iOS.

Chip es un veterano de 30 años en la industria de Apple, es autor de 18 productos de software comerciales para Mac y ex empleado de Apple y Sony.

Archivo->Nuevo proyectoiOSMac OSJuegoPróximoRápidoMetalPróximo"+"Jugar