View in English

  • Global Nav Open Menu Global Nav Close Menu
  • Apple Developer
Search
Cancel
  • Apple Developer
  • News
  • Discover
  • Design
  • Develop
  • Distribute
  • Support
  • Account
Only search within “”

Quick Links

5 Quick Links

Videos

Abrir menú Cerrar menú
  • Colecciones
  • Temas
  • Todos los videos
  • Información

Más videos

  • Información
  • Resumen
  • Transcripción
  • Código
  • Ve más allá con los juegos de Metal 4

    Profundiza en los últimos avances de Metal 4. Presentaremos las nuevas funciones de trazado de rayos que te ayudarán a llevar las cargas de trabajo más complejas y visualmente ricas a Apple Chip. Descubre cómo MetalFX puede ayudar a escalar cargas de trabajo mejorando renderizados, interpolando fotogramas y eliminando ruido de escenas. Para aprovechar al máximo esta sesión, te recomendamos que primero eches una mirada a “Descubrir Metal 4” y “Explorar juegos de Metal 4”.

    Capítulos

    • 0:00 - Introducción
    • 2:13 - Mejorar renderizados
    • 7:17 - Interpolar fotogramas
    • 13:50 - Trazar rayos con Metal 4
    • 19:25 - Eliminar ruido y mejorar calidad
    • 26:08 - Próximos pasos

    Recursos

      • Video HD
      • Video SD

    Videos relacionados

    WWDC25

    • Combina el aprendizaje automático y los gráficos de Metal 4
    • Descubrir Metal 4
    • Explorar juegos de Metal 4
    • Novedades del renderizado de Metal para apps inmersivas
    • Sube de nivel tus juegos

    WWDC23

    • Your guide to Metal ray tracing

    WWDC22

    • Boost performance with MetalFX Upscaling
  • Buscar este video…

    Hola, soy Matías Koskela. Hoy voy a presentar las técnicas y prácticas recomendadas para ir más allá con tus juegos avanzados y apps profesionales en las plataformas de Apple.

    Antes, quizás quieras ver “Descubre Metal 4” para una descripción general de Metal 4, y “Explora los juegos de Metal 4” para usar la versión de Metal más reciente. Esta charla es la segunda parte de nuestras charlas de juegos Metal 4. También puedes aprender cómo Metal 4 puede combinar el aprendizaje automático y los gráficos en una pista separada.

    Los juegos, como CyberPunk 2077, son cada vez más realistas con una renderización de alta calidad.

    Esto vuelve cada pixel una tarea costosa, que hace que las resoluciones y frecuencia de fotogramas sean desafiantes. Con Metal, puedes renderizar fotogramas de alta calidad en todas las plataformas de Apple, desde el iPhone hasta la Mac. Si usas técnicas como rasterización o trazado de rayos, Metal te proporciona API fáciles de usar.

    Puedes escalar tus cargas de trabajo para resoluciones y frecuencia de fotogramas más altas con MetalFX Upscaling.

    Si quieres ir aún más allá, ahora puedes usar el nuevo MetalFX frame interpolator.

    Los juegos recientes, como Cyberpunk 2077, ofrecen un trazado de ruta en tiempo real muy realista. Estas capacidades de renderizado en tiempo real se logran con las nuevas e increíbles funcionalidades de Metal 4. Estas incluyen mejoras en el trazado de rayos y MetalFX denoised upscaler, para escalar fácilmente, al reducir la cantidad de rayos necesarios.

    MetalFX Upscaler te permite lograr una mayor resolución y frecuencias de fotogramas. Puedes suavizar aún más tu juego con el nuevo MetalFX frame interpolator. Las nuevas funcionalidades de trazado de rayos de Metal 4 pueden mejorar el rendimiento y puedes combinarlas con MetalFX denoised upscaler.

    El aumento de escala es una técnica que te permitirá mejorar el rendimiento en la mayoría de los escenarios. MetalFX tiene un escalador basado en aprendizaje automático que es parte de las plataformas de Apple desde 2022 y mejora cada año.

    MetalFX Upscaling ahora incluye nuevas herramientas y técnicas que puedes usar para mejorar la calidad y el rendimiento de tu juego. El primer paso es aplicar correctamente el escalado temporal a tu juego. Una parte del proceso es ajustar el parámetro de entrada de exposición. Puedes mejorar aún más el rendimiento con la resolución dinámica y también mejorar la calidad en ciertos escenarios con sugerencias de reactividad.

    Imagina una canalización de renderizado típica. Se renderiza el fotograma o se traza un rayo, antes de que tu juego aplique efectos de posprocesamiento, como el desenfoque de movimiento. Luego aplica la exposición y el mapeo de tonos, luego renderiza la IU y, finalmente, muestra el fotograma.

    El lugar ideal para MetalFX upscaling es después de la renderización distorsionada y antes de los efectos posteriores. Puedes ver “Mejora el rendimiento con MetalFX Upscaling” para más detalles sobre la integración del escalador. Tendrás aún más herramientas y funcionalidades disponibles para mejorar el rendimiento de tu juego.

    Establecer un valor de exposición correcto en el escalador es esencial para un resultado de alta calidad.

    Si pasas un valor incorrecto, puede haber parpadeos e imágenes superpuestas.

    En el proceso de renderizado, el color de entrada y salida del escalador se encuentran en un espacio de color lineal. El escalador toma un parámetro llamado exposure, que, cuando se multiplica con la entrada de color, da como resultado un brillo que debería coincidir con la exposición del mapeo de tonos.

    Esto garantiza que el escalador comprenda cuáles son las funcionalidades visibles del fotograma cuando se muestran. El valor es solo una sugerencia para el escalador y no cambia el brillo de la salida. MetalFX incluye una herramienta para ajustar el valor de entrada de exposición que envías al escalador.

    Se llama exposure debugger. Para habilitarla, configura la variable de entorno MTLFX_EXPOSURE_TOOL_ENABLED. Ahora el escalador genera un tablero gris en la parte superior del fotograma y le aplica el inverso del valor de exposición.

    Puedes revisar cómo se ve el patrón al final de tu canalización en la pantalla.

    Si el valor de exposición que pasa al escalador no coincide con el del mapeador de tonos, el tablero será demasiado oscuro o brillante.

    Otro indicador es cuando el brillo del tablero cambia mientras el juego está en ejecución.

    Cuando el valor de exposición es correcto, el patrón es un gris constante.

    Como la complejidad de los juegos cambia de una escena a otra, muchos juegos adoptaron Dynamic Resolution Rendering.

    Cuando el fotograma es más complejo, se reduce la resolución de entrada del escalador. Cuando el desafío es aún mayor, el juego reduce dinámicamente aún más la resolución de entrada. MetalFX temporal upscaler admite entradas de tamaño dinámico, en lugar de requerir que pases una entrada del mismo tamaño en cada fotograma. Para la mejor calidad de escala, tu juego no debe establecer la escala máxima en un valor superior a 2x si no es necesario.

    Otra novedad de MetalFX temporal upscaler es una nueva funcionalidad opcional para avisar al escalador sobre la reactividad de los pixeles.

    Cuando tu juego renderiza efectos transparentes o partículas como fuegos artificiales, no los convierte en texturas de movimiento y profundidad.

    Con relaciones de escala altas y resoluciones de entrada bajas, las partículas se pueden mezclar con el fondo o se superpongan. Esto sucede porque en la renderización aparecen como detalles de textura o reflejos especulares.

    Para darte control sobre cómo se manejan tus partículas, el escalador ahora acepta una entrada opcional llamada reactive mask. Esta máscara te permite marcar áreas cubiertas por estos efectos.

    Para usarla, configura un valor de máscara reactiva en el sombreador, por ejemplo, basada en el tipo de material del búfer G. En el código del host, vincule la textura al objeto escalador temporal antes de codificarlo.

    Usa la máscara reactiva solo si no es posible alcanzar resoluciones de entrada más altas. No uses una máscara reactiva que esté ajustada para otro escalador, porque podría estar enmascarando áreas que ya se ven muy bien en la salida de MetalFX upscaler. El uso de Upscaler brinda un gran rendimiento con una gran calidad. A veces es mejor alcanzar frecuencias de actualización más altas. MetalFX introduce la interpolación de fotogramas en todas las plataformas de Apple. MetalFX frame interpolation es realmente fácil de integrar en tu juego. Configura un objeto interpolador, renderiza la IU en tus fotogramas interpolados y presenta y ajusta el ritmo de tu fotograma.

    La interpolación de fotogramas te permite usar pixeles que ya renderizaste para una experiencia de juego fluida.

    Este es el mismo flujo de renderizado, esta vez sin renderizado de IU.

    Interpola tus fotogramas después del paso de mapeo de tonos. Para resoluciones y frecuencia de fotogramas más altas, puedes tener tanto aumento de escala como interpolación en la canalización.

    Para usar MetalFX frame interpolator, tu app brinda dos fotogramas renderizados, vectores de movimiento y profundidad. Si adoptaste el escalador, se pueden usar los mismos vectores de movimiento y profundidad. La textura de movimiento tiene color para los objetos porque se movió a la derecha. Con estas entradas, MetalFX genera un fotograma entre esos dos fotogramas renderizados.

    Para configurar el interpolador, para un rendimiento combinado, brinda el objeto de escala al descriptor del interpolador. Al crear el interpolador, define su escala de movimiento y la convención de profundidad. Luego vincula las cinco texturas necesarias al interpolador.

    Cuando comiences a obtener fotogramas interpolados, es hora de pensar en la renderización de la IU.

    En el renderizado típico, un juego renderiza su IU al final de cada fotograma, casi en la misma ubicación en la que se produciría la interpolación de fotogramas.

    La IU hace una mezcla alfa de los elementos en el fotograma, puede contener texto que cambia y no modifica las texturas de movimiento ni profundidad.

    Tienes varias formas de lograr una IU atractiva con la interpolación de fotogramas habilitada.

    Hay tres técnicas más comunes para renderizar la IU con interpolación de fotogramas. IU compuesta, IU fuera de pantalla e IU en cada fotograma.

    En la IU compuesta, el interpolador obtiene el fotograma anterior N - 1, el fotograma actual N sin IU y el mismo fotograma N con IU. La IU compuesta es la más fácil de adoptar. En este modo, el interpolador de fotogramas puede ver la diferencia entre la textura con IU y sin IU. Así, puede intentar eliminar la IU y colocarla en la ubicación correcta en el fotograma interpolado. Pero deshacer la fusión de un pixel ya fusionado no se puede hacer a la perfección. Por lo tanto, puedes ayudar al interpolador con una de las otras opciones.

    Al igual que la IU fuera de pantalla, donde la IU se procesa en una textura separada.

    El interpolador lo agrega encima del fotograma interpolado. Al introducirlo en el interpolador se ahorra carga y almacenamiento adicionales. Dado que el interpolador puede escribir la IU en su salida.

    En la IU en cada fotograma, el manejo de la IU queda en manos de su código, lo que puede requerir cambios de código. Pero en este caso también puedes actualizar la IU para el fotograma interpolado, brindando una experiencia más fluida.

    Ahora también tienes una IU atractiva encima del fotograma interpolado. Es hora de pensar en cómo se pueden presentar los fotogramas interpolados y renderizados de forma nativa en el orden correcto y con los intervalos adecuados.

    Normalmente, la renderización del juego consta del hilo Render, el GPU y el hilo Present. El hilo Render configura el trabajo necesario para el GPU y la presentación. Cuando se renderiza un fotograma, el interpolador puede generar un fotograma con una marca de tiempo entre el recién renderizado y el anterior. Tu juego entonces podrá presentar el fotograma interpolado. Después de un intervalo, tu juego podrá mostrar el fotograma renderizado más recientemente.

    Determinar la duración de este intervalo de manera consistente es difícil. Pero es necesario para conseguir el ritmo perfecto del juego.

    Metal HUD es una gran herramienta para identificar cuando tu ritmo no es el adecuado. Ve “Sube de nivel tus juegos” para más detalles sobre cómo habilitarla y conocer más sobre las nuevas y fabulosas funcionalidades que tiene.

    Observa el gráfico de intervalo de fotogramas, donde el eje horizontal es el tiempo y el vertical es la longitud del intervalo.

    Si el gráfico muestra un patrón irregular y los picos de intervalos de actualización de fotogramas más largos son aleatorios, el ritmo está desfasado.

    Otra forma de saber si tu ritmo está desfasado es que tienes más de dos intervalos del histograma de fotogramas.

    Al arreglar el ritmo, deberías ver una línea plana si cumples con tu frecuencia de actualización de pantalla, o un patrón repetitivo regular si no, con un máximo de dos segmentos de histograma.

    Aquí hay un ejemplo de cómo se hace correctamente con una clase presentHelper. En el bucle de dibujo, todo se procesa en una textura de baja resolución y se amplía mediante MetalFX upscaler. La IU se renderiza después de informar al asistente que comienza. La invocación al interpolador es manejada por la clase presentHelper. Consulta el código de muestra para más detalles de implementación.

    Además del ritmo, también es importante acertar con el tiempo delta y los parámetros de la cámara. El área de oclusión puede tener artefactos, si no todos los parámetros son correctos. Con los parámetros correctos el área de oclusión se alinea perfectamente.

    El interpolador ahora ajusta los vectores de movimiento para que coincidan con la longitud del movimiento de simulación real.

    Después de obtener las entradas y el ritmo correctos, los fotogramas interpolados deberían verse genial. Tu entrada de interpolación debe tener una alta frecuencia de fotogramas. Intenta tener al menos 30 fotogramas por segundo antes de la interpolación.

    El escalador y el interpolador son técnicas que puedes usar para escalar casi cualquier estilo de renderizado. Por el contrario, el trazado de rayos se usa normalmente en renderizado de gama alta. Metal 4 agrega nuevas funcionalidades de trazado de rayos en torno a la creación de estructuras de aceleración y de intersección.

    Cada vez más juegos usan el trazado de rayos de Metal en las plataformas de Apple. En esta demostración, la iluminación es realista y el dron es visible en los reflejos del suelo. Las técnicas de trazado de rayos y las complejidades varían de un juego a otro.

    Esto requiere flexibilidad en la administración de funcionalidades de intersección y opciones para la construcción de estructuras de aceleración. Metal 4 presenta nuevas funcionalidades para agilizar ambos.

    Para los conceptos básicos, como la construcción de estructuras de aceleración y funcionalidades de intersección, ve “Guía para el trazado de rayos de Metal.”

    Imaginemos un juego en el que se traza con rayos una escena de césped y un árbol.

    En esta escena sencilla, hay múltiples tipos de materiales, como el follaje probado en alfa y el tronco opaco del árbol.

    Se requieren funcionalidades de intersección de trazado de rayos diferentes. Por separado para los rayos primarios y de sombra.

    Un búfer de funcionalidad de intersección es un búfer de argumentos con controladores para las funcionalidades de intersección de la escena.

    La hierba y las hojas podrían necesitar una funcionalidad similar para trazar rayos primarios. Los búferes de función de intersección hacen que tu juego tenga múltiples entradas que apunten a la misma función de intersección.

    Para configurar los índices de búfer de la función de intersección debes configurar el estado en el nivel de instancia, esta escena tiene dos instancias. A nivel de geometría, donde la hierba sólo tiene una geometría y el árbol tiene dos. El intersector necesita saber qué funcionalidad de intersección usar para los rayos de sombra en el tronco.

    Al crear sus estructuras de aceleración de instancias, especifica intersectionFunctionTableOffset en cada descriptor de instancia.

    Al construir tu estructura de aceleración primitiva, también estableces intersectionFunctionTableOffset en los descriptores de geometría.

    Al configurar el intersector en tu sombreador, agrega “intersection_function_buffer” a sus etiquetas.

    Luego configura el multiplicador de geometría en el intersector. El multiplicador es el número de tipos de rayos en el búfer de función de intersección.

    El ejemplo tiene dos tipos de rayos para cada geometría. Por lo tanto el valor correcto aquí es dos. Dentro de esos dos tipos de rayos, debes proporcionar el índice base para el tipo de rayo que estás trazando. En el ejemplo, el índice base para trazar rayos primarios sería 0.

    Para trazar sombras, el id base es 1.

    Cuando se combinan la instancia y la contribución geométrica del tronco, el multiplicador de geometría y el id base del rayo de sombra, el puntero termina en la función deseada.

    Termina tu código pasando los argumentos del búfer de función de intersección al método de intersección.

    Especificando el búfer, su tamaño y paso. Esto te da cierta flexibilidad adicional en comparación con la que te brindan otras API. Si estás portando desde DirectX, puedes portar tus tablas de enlace de sombreadores a búferes de funciones de intersección de Metal.

    En DirectX, se configura la dirección y el tamaño del búfer de funciones de intersección en el host al crear el descriptor para lanzar los rayos. En Metal, se configura en el sombreador. Los subprocesos del grupo SIMD deben establecer el mismo valor o el comportamiento no estará definido.

    El índice de tipo de rayo y el multiplicador de geometría se manejan igual en DirectX y Metal. Tu app puede configurarlos en tu sombreador. En DirectX y Metal, se establece el índice de desplazamiento de instancia por instancia al crear la estructura de aceleración de instancia. Aunque el índice de desplazamiento de geometría se genera automáticamente en DirectX, Metal te da la flexibilidad de configurarlo tú.

    Los búferes de función de intersección mejoran la experiencia de portabilidad de Metal para tu juego. Una vez que estés en marcha, Metal 4 también te permite optimizar cómo Metal construye tus estructuras de aceleración.

    El Metal ya te brinda control sobre la construcción de estructuras de aceleración. Además del comportamiento, puedes optimizar para reacondicionar, habilitar escenas más grandes o construir la estructura más rápido. Este año, obtendrás más flexibilidad y podrás preferir la intersección rápida para reducir el tiempo de trazado.

    También puedes minimizar el uso de memoria de tu estructura de aceleración.

    Los indicadores de uso se pueden configurar por cada estructura de aceleración, y no es necesario que sean los mismos.

    Los nuevos indicadores de estructura de aceleración hacen que el trazado de rayos se adapte aún más a sus necesidades. Si lo usas para efectos estocásticos, necesitarás un eliminador de ruido. Ahora, la eliminación de ruido puede ser parte de MetalFX upscaler.

    El trazado de rayos en tiempo real se usa cada vez más, tanto el híbrido más simple como el de trayectorias más complejo. En esta imagen, el trazado de rayos hace que todo tenga más cuerpo y mejora significativamente los reflejos. El mejor equilibrio entre calidad y rendimiento en el trazado de rayos se logra con la eliminación de ruido con menos rayos.

    Con la nueva API de MetalFX, combinar el aumento de escala y la eliminación de ruido es tan fácil como agregar entradas. Pero puedes mejorar aún más la calidad ayudando al escalador de ruido, agregando entradas adicionales y obteniendo los detalles correctos.

    Antes de poder combinar el amplificador y el eliminador de ruido, veamos cómo se hacen tradicionalmente.

    Las canalizaciones típicas de renderizado interactivo y en tiempo real con trazado de rayos procesan varios efectos por separado, eliminan el ruido y componen el resultado en una única textura sin ruido. La cual se amplía mediante MetalFX temporal upscaler. Seguido del posprocesamiento.

    Los eliminadores de ruido tradicionales requieren un ajuste de parámetros para cada escena. Aquí puedes ver cómo se ven los eliminadores de ruido sin parámetros ajustados. Con MetalFX denoised upscaler no es necesario ajustar los parámetros. Que se aplica después del renderizado principal y antes del posprocesamiento. Las técnicas con aprendizaje automático en MetalFX brindan eliminación de ruido y aumento de escala, de alto rendimiento y calidad en muchos escenarios. Es más fácil de integrar. Integrar el escalador es un buen comienzo para integrar el escalador con eliminación de ruido. Aquí podemos ver las entradas al escalador. Color, movimiento y profundidad. La nueva API combinada es un superconjunto de la API del escalador.

    Para la nueva API, necesitamos agregar búferes auxiliares adicionales sin ruido, que se ven a la izquierda. Estos son elementos que tu app probablemente ya tenga. Profundizaremos en cada uno de ellos.

    La primera entrada es normals. Para mejores resultados, deben estar en el espacio mundial.

    Luego está diffuse albedo, que es el color base de la radiancia difusa del material.

    Roughness representa qué tan lisa o rugosa es la superficie, lo cual es un valor lineal. La última entrada es el specular albedo. Esta debería ser una aproximación libre de ruido de la radiancia especular de tu renderizado. Debería incluir un componente fernel. En el código, la adición de estas nuevas entradas es sencilla.

    La creación de un escalador temporal típico solo requiere unas 10 líneas de código. Para habilitar la versión con eliminación de ruido, debes cambiar el tipo de escalador y agregar los tipos de las texturas adicionales.

    Al codificar el escalador, esta sería la invocación al escalador. La única diferencia es que necesitas vincular las texturas de entrada adicionales.

    Después de configurar el uso básico del eliminador de ruido, puedes mejorarlo con entradas opcionales. Y evitando algunos de los típicos errores de integración.

    Hay algunas texturas de entrada opcionales que pueden mejorar la calidad.

    Primero está la distancia de impacto especular, que indica la longitud del rayo desde el punto de visibilidad principal del pixel hasta el rebote secundario. Luego está la máscara de potencia de eliminación de ruido, que se usa para marcar áreas que no necesitan eliminación de ruido. Por último, la superposición de transparencia, que se usa en función del canal alfa para mezclar el color que solo se amplía y no se elimina el ruido.

    El problema de integración más típico es una entrada muy ruidosa. Para solucionarlo, debes usar todas las mejoras de seguimiento de ruta estándar, como la estimación del próximo evento, las técnicas de muestreo de importancia y, en una escena grande con muchas fuentes de luz, muestrear las fuentes que realmente contribuyen.

    Otra cosa relacionada con la calidad son los números aleatorios correlacionados. No debes usar generadores de números aleatorios demasiado correlacionados. La correlación espacial y la temporal pueden provocar artefactos.

    Un problema relacionado con los datos auxiliares es el albedo difuso del material metálico. En este ejemplo, las piezas de ajedrez son metálicas y tienen color en albedo especular. En ese caso, el albedo difuso de las piezas debería ser más oscuro.

    Por último, hay algunos errores comunes relacionados con las normales. MetalFX denoised upscaler espera que las normales estén en el espacio del mundo para tomar mejores decisiones. Debes usar un tipo de dato de textura que tenga bit de signo, sino la calidad puede ser subóptima, dependiendo de la orientación de la cámara.

    Después de ajustar estos detalles, deberías obtener fotogramas bien escalados y sin ruido.

    Veamos qué sucede cuando ponemos todas estas funcionalidades en un único renderizador.

    Mis colegas hicieron una demostración que usa el flujo de trabajo de renderizado del que hablé. La demostración usa las nuevas funcionalidades Metal 4 para optimizar la parte de trazado de rayos de la renderización. Realiza eliminación de ruido y aumento de escala al mismo tiempo con MetalFX denoised upscaler. Después de la exposición y el mapeo de tonos, los fotogramas se interpolan con MetalFX frame interpolator.

    Esta demostración usa efectos de iluminación de trazado de rayos avanzados, como iluminación global, reflejos, sombras y oclusiones ambientales, para una escena de dos robots jugando ajedrez.

    En la esquina superior derecha se ve la renderización antes del procesamiento de MetalFX. Y otras entradas de MetalFX en otras vistas.

    Adoptamos tanto MetalFX denoised upscaler como el interpolador de fotogramas. El eliminador de ruido también simplificó la renderización al eliminar todo ajuste manual del aspecto final.

    Si ya integraste el escalador MetalFX, esta es tu oportunidad de actualizar a la interpolación de fotogramas. Si eres nuevo en MetalFX, mira el escalador primero. verifica que tus efectos de trazado usen las mejores prácticas, como los búferes de intersección vistos hoy. Reduce el presupuesto de rayos con el escalador con eliminación de ruido.

    No puedo esperar a ver las nuevas funcionalidades en tus juegos, y lo que crearás usando Metal 4. ¡Gracias por acompañarnos!

    • 6:46 - Reactive Mask

      // Create reactive mask setup in shader
      out.reactivity = m_material_id == eRain ? (m_material_id == eSpark ? 1.0f : 0.0f) : 0.8f;
      
      // Set reactive mask before encoding upscaler on host
      temporalUpscaler.reactiveMask = reactiveMaskTexture;
    • 8:35 - MetalFX Frame Interpolator

      // Create and configure the interpolator descriptor
      MTLFXFrameInterpolatorDescriptor* desc = [MTLFXFrameInterpolatorDescriptor new];
      desc.scaler = temporalScaler;
      // ...
      
      // Create the effect and configure your effect
      id<MTLFXFrameInterpolator> interpolator = [desc newFrameInterpolatorWithDevice:device];
      interpolator.motionVectorScaleX = mvecScaleX;
      interpolator.motionVectorScaleY = mvecScaleY;
      interpolator.depthReversed = YES;
      
      // Set input textures
      interpolator.colorTexture = colorTexture;
      interpolator.prevColorTexture = prevColorTexture;
      interpolator.depthTexture = depthTexture;
      interpolator.motionTexture = motionTexture;
      interpolator.outputTexture = outputTexture;
    • 12:45 - Interpolator present helper class

      #include <thread>
      #include <mutex>
      #include <sys/event.h>
      #include <mach/mach_time.h>
      
      
      class PresentThread
      {
          int m_timerQueue;
          std::thread m_encodingThread, m_pacingThread;
          std::mutex m_mutex;
          std::condition_variable m_scheduleCV, m_threadCV, m_pacingCV;
          float m_minDuration;
          
          uint32_t m_width, m_height;
          MTLPixelFormat m_pixelFormat;
          
          const static uint32_t kNumBuffers = 3;
          uint32_t m_bufferIndex, m_inputIndex;
          bool m_renderingUI, m_presentsPending;
          
          CAMetalLayer *m_metalLayer;
          id<MTLCommandQueue> m_presentQueue;
      
          id<MTLEvent> m_event;
          id<MTLSharedEvent> m_paceEvent, m_paceEvent2;
          uint64_t m_eventValue;
          uint32_t m_paceCount;
          
          int32_t m_numQueued, m_framesInFlight;
          
          id<MTLTexture> m_backBuffers[kNumBuffers];
          id<MTLTexture> m_interpolationOutputs[kNumBuffers];
          id<MTLTexture> m_interpolationInputs[2];
          id<MTLRenderPipelineState> m_copyPipeline;
          
          std::function<void(id<MTLRenderCommandEncoder>)> m_uiCallback = nullptr;
          
          void PresentThreadFunction();
          void PacingThreadFunction();
          
          void CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label);
      
      public:
          
          PresentThread(float minDuration, CAMetalLayer *metalLayer);
          ~PresentThread()
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              m_numQueued = -1;
              m_threadCV.notify_one();
              m_encodingThread.join();
          }
          void StartFrame(id<MTLCommandBuffer> commandBuffer)
          {
              [commandBuffer encodeWaitForEvent:m_event value:m_eventValue++];
          }
      
          void StartUI(id<MTLCommandBuffer> commandBuffer)
          {
              assert(m_uiCallback == nullptr);
              if(!m_renderingUI)
              {
                  CopyTexture(commandBuffer, m_interpolationInputs[m_inputIndex], m_backBuffers[m_bufferIndex], @"Copy HUDLESS");
                  m_renderingUI = true;
              }
          }
          
          void Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue);
          
          id<MTLTexture> GetBackBuffer()
          {
              return m_backBuffers[m_bufferIndex];
          }
      
          void Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat);
          
          void DrainPendingPresents()
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              while(m_presentsPending)
                  m_scheduleCV.wait(lock);
          }
          
          bool UICallbackEnabled() const
          {
              return m_uiCallback != nullptr;
          }
          
          void SetUICallback(std::function<void(id<MTLRenderCommandEncoder>)> callback)
          {
              m_uiCallback = callback;
          }
          
      };
      
      PresentThread::PresentThread(float minDuration, CAMetalLayer *metalLayer)
          : m_encodingThread(&PresentThread::PresentThreadFunction, this)
          , m_pacingThread(&PresentThread::PacingThreadFunction, this)
          , m_minDuration(minDuration)
          , m_numQueued(0)
          , m_metalLayer(metalLayer)
          , m_inputIndex(0u)
          , m_bufferIndex(0u)
          , m_renderingUI(false)
          , m_presentsPending(false)
          , m_framesInFlight(0)
          , m_paceCount(0)
          , m_eventValue(0)
      {
          id<MTLDevice> device = metalLayer.device;
          m_presentQueue = [device newCommandQueue];
          m_presentQueue.label = @"presentQ";
          m_timerQueue = kqueue();
          
          metalLayer.maximumDrawableCount = 3;
          
          Resize(metalLayer.drawableSize.width, metalLayer.drawableSize.height, metalLayer.pixelFormat);
          
          m_event = [device newEvent];
          m_paceEvent = [device newSharedEvent];
      	m_paceEvent2 = [device newSharedEvent];
      }
      
      
      void PresentThread::Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue)
      {
          id<MTLCommandBuffer> commandBuffer = [queue commandBuffer];
          
          if(m_renderingUI)
          {
              frameInterpolator.colorTexture = m_interpolationInputs[m_inputIndex];
              frameInterpolator.prevColorTexture = m_interpolationInputs[m_inputIndex^1];
              frameInterpolator.uiTexture = m_backBuffers[m_bufferIndex];
          }
          else
          {
              frameInterpolator.colorTexture = m_backBuffers[m_bufferIndex];
              frameInterpolator.prevColorTexture = m_backBuffers[(m_bufferIndex + kNumBuffers - 1) % kNumBuffers];
              frameInterpolator.uiTexture = nullptr;
          }
          
          frameInterpolator.outputTexture = m_interpolationOutputs[m_bufferIndex];
      
          [frameInterpolator encodeToCommandBuffer:commandBuffer];
          [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
              std::unique_lock<std::mutex> lock(m_mutex);
              m_framesInFlight--;
              m_scheduleCV.notify_one();
              m_paceCount++;
              m_pacingCV.notify_one();
          }];
          [commandBuffer encodeSignalEvent:m_event value:m_eventValue++];
          [commandBuffer commit];
      
          std::unique_lock<std::mutex> lock(m_mutex);
          m_framesInFlight++;
          m_numQueued++;
          m_presentsPending = true;
          m_threadCV.notify_one();
          while((m_framesInFlight >= 2) || (m_numQueued >= 2))
              m_scheduleCV.wait(lock);
      
          m_bufferIndex = (m_bufferIndex + 1) % kNumBuffers;
          m_inputIndex = m_inputIndex^1u;
          m_renderingUI = false;
      }
      
      void PresentThread::CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label)
      {
          MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor new];
          desc.colorAttachments[0].texture = dest;
          desc.colorAttachments[0].loadAction = MTLLoadActionDontCare;
          desc.colorAttachments[0].storeAction = MTLStoreActionStore;
          id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:desc];
          [renderEncoder setFragmentTexture:src atIndex:0];
          [renderEncoder setRenderPipelineState:m_copyPipeline];
          [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
          if(m_uiCallback)
              m_uiCallback(renderEncoder);
          renderEncoder.label = label;
          [renderEncoder endEncoding];
      }
      
      
      void PresentThread::PacingThreadFunction()
      {
          NSThread *thread = [NSThread currentThread];
          [thread setName:@"PacingThread"];
          [thread setQualityOfService:NSQualityOfServiceUserInteractive];
          [thread setThreadPriority:1.f];
          
          mach_timebase_info_data_t info;
          mach_timebase_info(&info);
          
          // maximum delta (0.1ms) in machtime units
          const uint64_t maxDeltaInNanoSecs = 100000000;
          const uint64_t maxDelta = maxDeltaInNanoSecs * info.denom / info.numer;
          
          uint64_t time = mach_absolute_time();
          
          uint64_t paceEventValue = 0;
          
          for(;;)
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              while(m_paceCount == 0)
                  m_pacingCV.wait(lock);
              m_paceCount--;
              lock.unlock();
              
              // we get signal...
              const uint64_t prevTime = time;
              time = mach_absolute_time();
      		m_paceEvent.signaledValue = ++paceEventValue;
      
              const uint64_t delta = std::min(time - prevTime, maxDelta);
              const uint64_t timeStamp = time + ((delta*31)>>6);
              
              struct kevent64_s timerEvent, eventOut;
              struct timespec timeout;
              timeout.tv_nsec = maxDeltaInNanoSecs;
              timeout.tv_sec = 0;
              EV_SET64(&timerEvent,
                       0,
                       EVFILT_TIMER,
                       EV_ADD | EV_ONESHOT | EV_ENABLE,
                       NOTE_CRITICAL | NOTE_LEEWAY | NOTE_MACHTIME | NOTE_ABSOLUTE,
                       timeStamp,
                       0,
                       0,
                       0);
              
              kevent64(m_timerQueue, &timerEvent, 1, &eventOut, 1, 0, &timeout);
              
              // main screen turn on...
              m_paceEvent2.signaledValue = ++paceEventValue;
          }
      }
      
      
      void PresentThread::PresentThreadFunction()
      {
          NSThread *thread = [NSThread currentThread];
          [thread setName:@"PresentThread"];
          [thread setQualityOfService:NSQualityOfServiceUserInteractive];
          [thread setThreadPriority:1.f];
          
      
          uint64_t eventValue = 0;
          uint32_t bufferIndex = 0;
      
          uint64_t paceEventValue = 0;
      
          for(;;)
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              
              if(m_numQueued == 0)
              {
                  m_presentsPending = false;
                  m_scheduleCV.notify_one();
              }
              
              while(m_numQueued == 0)
                  m_threadCV.wait(lock);
              
              if(m_numQueued < 0)
                  break;
              lock.unlock();
      
              @autoreleasepool
              {
                  id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable];
      
      			lock.lock();
      			m_numQueued--;
      			m_scheduleCV.notify_one();
      			lock.unlock();
      
                  id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer];
                  [commandBuffer encodeWaitForEvent:m_event value:++eventValue];
                  CopyTexture(commandBuffer, drawable.texture, m_interpolationOutputs[bufferIndex], @"Copy Interpolated");
                  [commandBuffer encodeSignalEvent:m_event value:++eventValue];
      			[commandBuffer encodeWaitForEvent:m_paceEvent value:++paceEventValue];
      
                  if(m_minDuration > 0.f)
                      [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration];
                  else
                      [commandBuffer presentDrawable:drawable];
                  [commandBuffer commit];
              }
              
              @autoreleasepool
              {
                  id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer];
                  id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable];
                  CopyTexture(commandBuffer, drawable.texture, m_backBuffers[bufferIndex], @"Copy Rendered");
      			[commandBuffer encodeWaitForEvent:m_paceEvent2 value:++paceEventValue];
                  if(m_minDuration > 0.f)
                      [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration];
                  else
                      [commandBuffer presentDrawable:drawable];
                  [commandBuffer commit];
              }
              
              bufferIndex = (bufferIndex + 1) % kNumBuffers;
          }
      }
      
      void PresentThread::Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat)
      {
          if((m_width != width) || (m_height != height) || (m_pixelFormat != pixelFormat))
          {
              id<MTLDevice> device = m_metalLayer.device;
      
              if(m_pixelFormat != pixelFormat)
              {
                  id<MTLLibrary> lib = [device newDefaultLibrary];
                  MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new];
                  pipelineDesc.vertexFunction = [lib newFunctionWithName:@"FSQ_VS_V4T2"];
                  pipelineDesc.fragmentFunction = [lib newFunctionWithName:@"FSQ_simpleCopy"];
                  pipelineDesc.colorAttachments[0].pixelFormat = pixelFormat;
                  m_copyPipeline = [device newRenderPipelineStateWithDescriptor:pipelineDesc error:nil];
                  m_pixelFormat = pixelFormat;
              }
              
              DrainPendingPresents();
              
              m_width = width;
      		m_height = height;
              
              MTLTextureDescriptor *texDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat width:width height:height mipmapped:NO];
      		texDesc.storageMode = MTLStorageModePrivate;
              for(uint32_t i = 0; i < kNumBuffers; i++)
              {
                  texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageShaderWrite|MTLTextureUsageRenderTarget;
                  m_backBuffers[i] = [device newTextureWithDescriptor:texDesc];
                  texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget;
                  m_interpolationOutputs[i] = [device newTextureWithDescriptor:texDesc];
              }
              texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget;
              m_interpolationInputs[0] = [device newTextureWithDescriptor:texDesc];
              m_interpolationInputs[1] = [device newTextureWithDescriptor:texDesc];
      
          }
      }
    • 13:00 - Set intersection function table offset

      // Set intersection function table offset on host-side geometry descriptors
      NSMutableArray<MTLAccelerationStructureGeometryDescriptor *> *geomDescs ...;
      for (auto g = 0; g < geomList.size(); ++g)
      {
          MTLAccelerationStructureGeometryDescriptor *descriptor = ...;
          descriptor.intersectionFunctionTableOffset = g;
          ...
          [geomDescs addObject:descriptor];
      }
    • 13:01 - Set up the intersector

      // Set up the intersector
      metal::raytracing::intersector<intersection_function_buffer, instancing, triangle> trace;
      trace.set_geometry_multiplier(2); // Number of ray types, defaults to 1
      trace.set_base_id(1);             // Set ray type index, defaults to 0
    • 13:02 - Ray trace intersection function buffers

      // Ray trace intersection function buffers
      
      // Set up intersection function buffer arguments
      intersection_function_buffer_arguments ifb_arguments;
      ifb_arguments.intersection_function_buffer = raytracingResources.ifbBuffer;
      ifb_arguments.intersection_function_buffer_size = raytracingResources.ifbBufferSize;
      ifb_arguments.intersection_function_stride = raytracingResources.ifbBufferStride;
      
      // Set up the ray and finish intersecting
      metal::raytracing::ray r = { origin, direction };
      auto result = trace.intersect(r, ads, ifb_arguments);
    • 13:02 - Change of temporal scaler setup to denoised temporal scaler setup

      // Change of temporal scaler setup to denoised temporal scaler setup
      
      MTLFXTemporalScalerDescriptor* desc = [MTLFXTemporalScalerDescriptor new];
      desc.colorTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB;
      desc.outputTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB;
      desc.depthTextureFormat = DepthStencilFormat;
      desc.motionTextureFormat = MotionVectorFormat;
      
      desc.diffuseAlbedoTextureFormat = DiffuseAlbedoFormat;
      desc.specularAlbedoTextureFormat = SpecularAlbedoFormat;
      desc.normalTextureFormat = NormalVectorFormat;
      desc.roughnessTextureFormat = RoughnessFormat;
      
      desc.inputWidth = _mainViewWidth;
      desc.inputHeight = _mainViewHeight;
      desc.outputWidth = _screenWidth;
      desc.outputHeight = _screenHeight;
      temporalScaler = [desc newTemporalDenoisedScalerWithDevice:_device];
    • 13:04 - Change temporal scaler encode to denoiser temporal scaler encode

      // Change temporal scaler encode to denoiser temporal scaler encode
      
      temporalScaler.colorTexture = _mainView;
      temporalScaler.motionTexture = _motionTexture;
      
      temporalScaler.diffuseAlbedoTexture = _diffuseAlbedoTexture;
      temporalScaler.specularAlbedoTexture = _specularAlbedoTexture;
      temporalScaler.normalTexture = _normalTexture;
      temporalScaler.roughnessTexture = _roughnessTexture;
      
      temporalScaler.depthTexture = _depthTexture;
      temporalScaler.jitterOffsetX = _pixelJitter.x;
      temporalScaler.jitterOffsetY = -_pixelJitter.y;
      temporalScaler.outputTexture = _upscaledColorTarget;
      temporalScaler.motionVectorScaleX = (float)_motionTexture.width;
      temporalScaler.motionVectorScaleY = (float)_motionTexture.height;
      [temporalScaler encodeToCommandBuffer:commandBuffer];
    • 16:04 - Creating instance descriptors for instance acceleration structure

      // Creating instance descriptors for instance acceleration structure
      MTLAccelerationStructureInstanceDescriptor *grassInstanceDesc, *treeInstanceDesc = . . .;
      grassInstanceDesc.intersectionFunctionTableOffset = 0;
      treeInstanceDesc.intersectionFunctionTableOffset  = 1;
      
      // Create buffer for instance descriptors of as many trees/grass instances the scene holds
      id <MTLBuffer> instanceDescs = . . .;
      for (auto i = 0; i < scene.instances.size(); ++i)
      . . .
    • 0:00 - Introducción
    • Obtén información sobre la serie de juegos Metal 4, que incluye técnicas avanzadas y prácticas recomendadas para desarrollar juegos de alto nivel y apps profesionales en las plataformas de Apple. Las API de Metal 4 te permiten escalar tus cargas de trabajo para lograr resoluciones y frecuencias de fotogramas más altas en todos los dispositivos Apple.

    • 2:13 - Mejorar renderizados
    • MetalFX tiene un escalador basado en aprendizaje automático que puede ayudarte a lograr resoluciones y frecuencias de fotogramas más altas. Como novedad este año, el escalador temporal de MetalFX admite entradas de tamaño dinámico para que puedas reducir dinámicamente la resolución de entrada en fotogramas de especial complejidad. De manera opcional, puedes usar las sugerencias de reactividad para indicarle al escalador la reactividad de los píxeles y así obtener resultados más nítidos en áreas con efectos o partículas transparentes. Además, puedes verificar que el valor de exposición que pasas al escalador sea correcto con la nueva herramienta de depuración de exposición.

    • 7:17 - Interpolar fotogramas
    • Como novedad este año, MetalFX Frame Interpolation genera un fotograma adicional entre los dos fotogramas renderizados. Existen varias técnicas para renderizar la interfaz de usuario con Frame Interpolation. También hay consideraciones que debes tener en cuenta al definir el ritmo y la presentación de fotogramas interpolados y aquellos renderizados de forma nativa.

    • 13:50 - Trazar rayos con Metal 4
    • Metal 4 incorpora varias funciones nuevas de trazado de rayos relacionadas con la creación de estructuras de aceleración y las funciones de intersección. Si migras desde otras API, es fácil portar tus tablas de enlace de sombreadores a los búferes de funciones de intersección de Metal. Una vez que tengas todo en marcha, también puedes optimizar cómo Metal 4 crea tus estructuras de aceleración.

    • 19:25 - Eliminar ruido y mejorar calidad
    • Al realizar el trazado de rayos en escenas, puedes lograr el mejor equilibrio entre calidad y rendimiento con la reducción de ruido con menos rayos lanzados. La nueva API MetalFX mejora los procesos de renderizado de trazado de rayos en tiempo real integrando la reducción de ruido directamente en el proceso de aumento de escala. Esto simplifica el enfoque tradicional, que supone pasos separados para el trazado, la reducción de ruido y la composición. Al especificar búferes auxiliares libres de ruido, como normales, albedo difuso, rugosidad y albedo especular, puedes obtener resultados robustos, de alto rendimiento y de alta calidad sin la necesidad de ajustar parámetros por separado para cada escena.

    • 26:08 - Próximos pasos
    • Si ya integraste MetalFX Upscaler, esta es tu oportunidad de actualizar a la interpolación de fotogramas. Si aún no utilizas MetalFX, primero familiarízate con el escalador. A continuación, asegúrate de que tus efectos de trazado de rayos utilicen búferes de función de intersección y el escalador con reducción de ruido.

Developer Footer

  • Videos
  • WWDC25
  • Ve más allá con los juegos de Metal 4
  • Open Menu Close Menu
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • Icon Composer
    • SF Symbols
    Open Menu Close Menu
    • Accessibility
    • Accessories
    • App Store
    • Audio & Video
    • Augmented Reality
    • Business
    • Design
    • Distribution
    • Education
    • Fonts
    • Games
    • Health & Fitness
    • In-App Purchase
    • Localization
    • Maps & Location
    • Machine Learning & AI
    • Open Source
    • Security
    • Safari & Web
    Open Menu Close Menu
    • Documentation
    • Sample Code
    • Tutorials
    • Downloads
    • Forums
    • Videos
    Open Menu Close Menu
    • Support Articles
    • Contact Us
    • Bug Reporting
    • System Status
    Open Menu Close Menu
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles
    • Feedback Assistant
    Open Menu Close Menu
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • News Partner Program
    • Video Partner Program
    • Security Bounty Program
    • Security Research Device Program
    Open Menu Close Menu
    • Meet with Apple
    • Apple Developer Centers
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Academies
    • WWDC
    Get the Apple Developer app.
    Copyright © 2025 Apple Inc. All rights reserved.
    Terms of Use Privacy Policy Agreements and Guidelines