
-
Metal 4 게임 심화 기능 알아보기
Metal 4의 최신 업데이트 사항을 자세히 살펴보세요. 가장 복잡하고 시각적으로 풍부한 워크로드를 Apple Silicon으로 가져오는 데 도움이 되는 새로운 레이 트레이싱 기능을 소개합니다. MetalFX의 렌더링 업스케일과 프레임 보간, 장면 노이즈 제거가 워크로드 확장에 어떤 도움이 되는지 알아보세요. 이 세션을 최대한 활용하려면 ‘Metal 4 살펴보기'와 ‘Metal 4 게임 알아보기'를 먼저 확인하는 것이 좋습니다.
챕터
- 0:00 - 서론
- 2:13 - 렌더링 업스케일링
- 7:17 - 프레임 보간
- 13:50 - Metal 4를 통한 레이 트레이싱
- 19:25 - 업스케일링 중 노이즈 제거
- 26:08 - 다음 단계
리소스
관련 비디오
WWDC25
WWDC23
WWDC22
-
비디오 검색…
안녕하세요, 제 이름은 Matias Koskela입니다 Apple 플랫폼에서 고사양 게임과 프로 앱을 더욱 발전시키는 데 도움이 될 기술과 모범 사례를 설명드리겠습니다
“Metal 4 살펴보기“와 “Metal 4 게임 알아보기“ 먼저 이 두 비디오로 Metal 4의 개요와 최신 버전 사용법에 대해 알아보시기 바랍니다 오늘 이 “심화“ 세션은 Metal 4 게임 시리즈의 두 번째 파트입니다 Metal 4로 머신 러닝과 별도 트랙의 그래픽을 결합하는 방법도 배울 수 있습니다
CyberPunk 2077과 같은 게임은 고품질 렌더링으로 현실감이 높아졌습니다 각 픽셀은 고해상도와 프레임 속도 구현이 어려운 비싼 작업이 되었죠 Metal로 고품질 프레임을 렌더링을 iPhone부터 Mac까지 Apple 플랫폼에서 할 수 있죠 래스터화나 레이 트레이싱 같은 기술을 사용하는 경우 Metal이 간편한 API를 제공합니다
MetalFX Upscaling으로 작업 부하를 해상도와 프레임 속도에 맞게 확장할 수 있습니다
여기서 더 나아가 이제 새로운 MetalFX 프레임 보간기를 사용 가능합니다
Cyberpunk 2077과 같은 최신 게임은 매우 사실적인 실시간 경로 추적을 제공합니다 이러한 실시간 렌더링의 확장된 성능을 Metal 4의 새로운 멋진 기능으로 실현합니다 레이 트레이싱과 MetalFX 노이즈 제거 업스케일러 향상으로 필요한 광선 수가 줄어들어 확장이 쉬워집니다
MetalFX Upscaler로 높은 해상도와 빠른 프레임 속도를 얻을 수 있죠 새로운 MetalFX 프레임 보간기로 플레이가 매끄러워집니다 새로운 Metal 4 레이 트레이싱 기능은 성능을 향상하고 새로 출시된 MetalFX 노이즈 제거 업스케일러와 결합할 수 있습니다
업스케일링은 많은 시나리오에서 성능을 높여 주는 널리 알려진 기술이죠 MetalFX에는 2022년부터 Apple 플랫폼에 도입되고 매년 향상되고 있는 머신 러닝 기반 업스케일러가 있습니다
MetalFX Upscaling 의 새로운 도구와 기술은 게임의 품질과 성능 개선에 도움이 될 것입니다 먼저 시간적 업스케일링을 게임에 적절하게 적용합니다 노출 입력 매개변수를 올바르게 설정해야 합니다 그런 다음 동적 해상도로 성능을 향상시킬 수 있습니다 특정 시나리오에서는 반응성 힌트로 품질을 개선할 수 있죠
일반적인 렌더링 파이프라인을 상상해 보세요 게임에서 모션 블러와 같은 사후 처리 효과를 수행하려면
먼저 프레임 래스터화나 레이 트레이싱이 필요합니다 다음으로 노출과 톤 매핑을 적용하면 UI가 렌더링되고 마침내 플레이어에게 프레임이 표시됩니다
MetalFX Upscaling의 이상적인 위치는 지터링 렌더링 후 사후 효과 전입니다 ”MetalFX Upscaling을 통한 성능 향상”에서 업스케일러 통합에 대해 더 자세히 알아보세요 올해에는 더 많은 도구와 기능을 제공해 여러분의 게임 성능을 향상합니다
업스케일러에서 올바른 노출 값을 설정해야 고품질의 결과가 나옵니다
지나치게 잘못된 값을 전달하면 깜박임과 고스팅이 발생합니다
렌더링 파이프라인에서 업스케일러의 입력, 출력 색상은 선형 색상 공간에 있습니다 업스케일러는 노출이라는 매개변수를 사용하므로 색상 입력과 곱하면 톤 매핑 노출과 유사한 밝기가 생성됩니다
프레임에 어떻게 보이고 플레이어에게 어떻게 표시되는지 업스케일러가 인식하죠 이 값은 업스케일러에 대한 힌트일 뿐이며 출력 밝기를 변경하는 것이 아닙니다 MetalFX에는 노출 입력 값 조정을 위한 새로운 도구가 값을 업스케일러로 보냅니다
노출 디버거라고 합니다 MTLFX_EXPOSURE_TOOL_ENABLED 환경변수를 설정해 활성화하죠 업스케일러가 프레임 위에 회색 체크무늬를 렌더링하고 노출 값의 역수를 적용합니다
파이프라인 마지막에서 디스플레이에 표시되는 패턴을 확인할 수 있습니다
업스케일러에 전달하는 노출 값이 톤매퍼와 일치하지 않으면 체크무늬가 너무 어둡거나 너무 밝게 보일 것입니다
게임이 실행되는 동안 체크무늬 밝기가 변하는 것도 일치하지 않는다는 또 다른 지표입니다
노출 값이 올바른 경우 일정한 회색 그리드가 표시됩니다
게임의 복잡성이 장면마다 크게 바뀔 수 있어서 많은 게임에서 동적 해상도 렌더링을 채택합니다
프레임이 복잡할수록 업스케일러 입력 해상도는 낮아집니다 복잡할수록 게임의 입력 해상도가 동적으로 낮아지죠 MetalFX 시간적 업스케일러 는 매번 똑같은 크기가 아닌 동적인 크기 입력을 지원합니다 최상의 스케일링을 위해 필요한 경우가 아니면 최대 크기를 두 배 이상으로 설정하지 마세요
MetalFX 시간적 업스케일러 의 또 다른 새 기능은 픽셀 반응성에 대한 힌트를 제공하는 선택적 기능입니다
게임이 투명한 효과나 폭죽처럼 입자가 있는 효과를 렌더링할 때 모션 텍스처와 심도 텍스처에는 렌더링하지 않습니다
스케일링 비율이 높고 입력 해상도가 낮으면 입자가 배경과 섞이거나
고스팅 현상이 발생할 수 있습니다 이 현상의 이유는 랜더링에서 마치 텍스처 디테일이나 반사 하이라이트처럼 보일 수 있기 때문이죠
입자 처리 방식을 통제할 수 있도록 이제 반응형 마스크라는 새로운 선택적 입력을 업스케일러에서 사용할 수 있습니다 이 마스크로 이러한 효과가 나타나는 영역을 표시하죠
이를 사용하려면 반응형 마스크 값을 셰이더에 설정합니다 예를 들어, G 버퍼 머티리얼 유형 기준으로요 호스트 코드는 인코딩하기 전 시간적 업스케일러 객체에 텍스처를 바인딩하세요
입력 해상도를 높일 수 없을 때에만 반응성 마스크를 사용하세요 다른 업스케일러용 반응성 마스크를 사용하지 마세요 MetalFX 업스케일러 출력에서 이미 보기 좋은 영역이 가려질 수 있습니다 MetalFX 업스케일러는 뛰어난 품질과 성능을 제공합니다 하지만 더 높은 화면 주사율을 원할 때도 있죠 MetalFX은 모든 Apple 플랫폼에 대한 프레임 보간을 올해 도입했습니다
MetalFX Frame Interpolation은 쉽게 게임과 통합할 수 있죠 먼저 보간기 객체를 설정하고 UI를 보간 프레임에 렌더링하고 프레임을 올바르게 제시하고 속도를 조절하세요
프레임 보간으로 이미 렌더링한 픽셀을 사용해 게임 경험을 매끄럽게 합니다
동일한 렌더링 파이프라인이지만 이번에는 UI 렌더링이 없습니다
톤 매핑 단계 이후 프레임을 보간합니다 더 높은 해상도와 프레임 속도도 동일한 파이프라인에서 업스케일링과 보간을 할 수 있습니다
MetalFX 프레임 보간기를 사용 하려면 앱이 모션 벡터와 심도 두 가지 렌더링된 프레임이 있어야 합니다 업스케일러를 채택했다면 동일한 모션 벡터 심도를 사용할 수 있습니다 모션 텍스처 객체는 오른쪽으로 이동하며 색채를 띱니다 이렇게 입력하면 MetalFX는 렌더링된 프레임 사이에 프레임을 생성합니다
더 높은 결합 성능을 위해 보간기를 설정하려면 보간기 설명자에 업스케일 객체를 제공합니다 보간기를 생성할 때에는 동작 규모와 심도 규칙을 정의합니다 그런 다음 텍스처 5개를 모두 보간기에 바인딩합니다
프레임이 보간되었으니 이제 UI를 렌더링할 차례입니다
일반적인 렌더링 파이프라인은 게임은 보통 각 프레임이 끝날 때 UI를 프레임 보간이 발생해야 하는 동일한 위치 주변에 렌더링하죠
UI 렌더링 알파는 요소를 프레임에 혼합합니다 프레임마다 변경되는 텍스트가 포함될 수 있으며 동작이나 깊이 텍스처는 수정하지 않습니다
프레임 보간으로 멋진 UI를 구현하는 여러 방법이 있습니다
프레임 보간 UI 렌더링에는 세 가지 기술이 주로 사용됩니다 합성 UI, 오프스크린 UI 매 프레임 UI입니다
합성 UI에서 보간기는 이전 프레임인 N-1을 가져옵니다 UI가 없는 현재 프레임 N과 UI가 있는 프레임 N이죠 합성 UI는 도입하기 가장 쉽습니다 이 모드에서는 프레임 보간기가 UI가 있거나 없는 텍스처 간 델타를 볼 수 있습니다 UI를 제거하고 보관된 프레임의 올바른 위치에 삽입할 수 있죠 하지만 이미 혼합된 픽셀을 완전히 해제할 수는 없습니다 다른 옵션을 적용해 보간기를 보조할 수 있습니다
UI가 완전히 별도의 UI 텍스처에 렌더링되는 오프스크린 UI처럼요
보간기는 이를 보간된 프레임 위에 추가합니다 보간기에 입력하면 추가로 로드하고 보관할 필요가 없습니다 보간자는 UI를 출력에 쓸 수 있습니다
매 프레임 UI에서는 코드가 UI를 처리합니다 여러분이 많은 코드를 변경해야 할 수 있습니다 이 경우 보간된 프레임의 UI도 업데이트할 수 있어서 가장 매끄러운 플레이어 경험을 제공할 수 있습니다
이제 보간된 프레임에 더해 보기 좋은 UI가 생겼습니다 이제 보간된 프레임과 원래 렌더링된 프레임을 올바른 순서, 올바른 간격으로 표시할지에 대해 생각해 볼 시간입니다
일반적으로 게임 렌더링은 렌더링 스레드, GPU 출력 스레드로 구성됩니다 렌더링 스레드는 GPU와 출력에 필요한 작업을 설정합니다 프레임이 렌더링되면 보간기는 방금 렌더링된 프레임과 이전 프레임 사이에 타임스탬프 프레임을 생성합니다 그러면 게임에서 보간된 프레임을 표현할 수 있습니다 출력 간격 후, 게임에서 최신 프레임을 표시합니다
이 간격의 길이를 일정하게 정하기는 쉽지 않습니다 하지만 그렇게 해야만 게임의 속도를 적절히 맞출 수 있습니다
새 Metal HUD는 속도가 어긋나는 부분을 탐지하는 멋진 도구입니다 “게임 수준 높이기“ 영상에서 이를 활성화하는 방법과 새로운 멋진 기능에 대해 더 알아보세요
이 프레임 간격 그래프에서 수평축이 시간이고 수직축이 프레임 간격 길이입니다
그래프 패턴이 불규칙하고 스파이크가 있는 경우 긴 프레임 업데이트 간격 즉 속도가 어긋났다는 것을 의미합니다
속도가 어긋났다는 것을 알 수 있는 또 다른 지표는 프레임 간격 히스토그램 버킷이 두 개 이상일 때입니다
속도가 고정되면 평평한 선이 보일 것입니다 목표 디스플레이 재생 빈도를 충족하거나 규칙적으로 반복되는 패턴 아래에 있는 경우 히스토그램 버킷이 최대 2개입니다
어떻게 presentHelper 클래스로 쉽고 편리하게 교정할 수 있는지 예를 보여드리죠 드로잉 루프 동안에는 낮은 해상도로 텍스처로 렌더링되며 MetalFX 업스케일러로 업스케일링됩니다 헬퍼에 UI 렌더링 시작을 알리면 UI가 렌더링되고 마지막으로 보간기 호출은 presentHelper 클래스가 처리하죠 샘플 코드에서 구현 세부 사항을 확인하세요
속도 외에 델타 시간, 카메라 매개변수도 올바르게 설정해야 합니다 모든 매개변수가 부정확하면 오클 루전 영역에 아티팩트가 발생하죠
올바른 매개변수를 사용하면 오클루전 영역이 잘 정렬됩니다
보간기가 모션 벡터를 조정해서 실제 시뮬레이션 동작의 길이에 맞출 수 있기 때문입니다
모든 입력이 끝나고 속도가 올바르다면 보간된 프레임이 보기 좋을 것입니다 보간 입력 프레임 속도는 적당히 높아야 하고요 보간 전에 초당 최소 30프레임으로 맞춰 보세요
업스케일러와 프레임 보간기는 거의 모든 렌더링 유형에 범용적으로 사용할 수 있습니다 대조적으로 레이 트레이싱은 일반적으로 높은 수준의 렌더링에 사용됩니다 Metal 4에는 많은 새로운 레이 트레이싱 기능이 가속 구조 구축과 교차 함수에 추가되었습니다
많은 Apple 플랫폼 게임이 Metal 레이 트레이싱을 사용하죠
이 데모에는 사실적인 조명과 바닥에 반사되어 보이는 드론이 있습니다 레이 트레이싱 기술과 복잡성은 게임마다 다릅니다
더 유연한 교차 함수 관리와 더 많은 가속 구조 구축 옵션이 필요하죠
두 가지 모두 간소화하는 기능들이 Metal 4에 도입되었습니다
가속 구조 구축이나 교차 함수 같은 Metal 레이 트레이싱에 대한 기초 정보는 “Metal 레이 트레이싱 가이드“ 영상을 참고하세요
나무 주변에 풀이 있는 장면을 레이 트레이싱하는 게임이 있다면
이 단순한 장면에도 알파 테스트를 거친 나뭇잎이나 불투명한 나무 몸통처럼 여러 머티리얼 유형이 있습니다
다양한 레이 트레이싱 교차 함수가 필요합니다 1차 광선과 그림자 광선에 필요하죠
교차 함수 버퍼는 장면 교차 함수의 핸들을 포함하는 인수 버퍼입니다
예를 들어 풀과 잎에는 1차 광선을 추적하려고 비슷한 기능이 필요할 수 있습니다 교차 함수 버퍼로 게임의 여러 항목이 같은 교차 함수를 향하게 할 수 있습니다
교차 함수 버퍼 인덱스를 설정하려면 이 예시에서는 2개의 인스턴스인 인스턴스 수준 설정이 필요합니다 지오메트리 수준에서 보면 풀에는 1개, 나무에는 2개가 있지요 인터섹터는 어떤 교차 함수를 나무 몸통에 닿는 그림자 광선에 사용할지 알아야 합니다
인스턴스 가속 구조를 생성할 때 인스턴스 설명자마다 intersectionFunctionTableOffset 를 설정하세요
원시 가족 구조를 빌드할 때에도 지오메트리 설명자에 intersectionFunctionTableOffset 를 설정합니다
셰이더의 인터섹터를 설정할 때에는 “intersection_function_buffer” 를 태그에 추가하세요
다음으로 인터섹터에 기하 승수를 설정합니다 승수는 교차 함수 버퍼에 광선 유형의 수입니다
이 예시에는 각 지오메트리에 2개의 광선 유형이 있죠 따라서 2를 입력해야 합니다 두 광선 유형 중 추적하는 광선에 대한 기본 인덱스를 제공해야 합니다 여기서는 1차 광선을 추적하는 기본 인덱스가 0이 됩니다
그림자를 추적하는 기본 ID는 1이고요
나무 줄기 인스턴스와 기하학적 기여가 있고 기하 승수와 그림자 광선 유형의 기본 ID가 결합되면 원하는 교차 함수에서 포인터가 종료됩니다
교차 함수 버퍼 인수를 교차 메서드에 전달해 코드를 마무리합니다
버퍼와 버퍼의 크기 간격을 지정하므로써 다른 API에서 했던 것 보다 조금 더 유연성을 가질 수 있습니다 DirectX에서 포팅한다면 셰이더 바인딩 테이블을 Metal 교차 함수 버퍼로 쉽게 포팅할 수 있죠
DirectX에서 교차 함수 버퍼 주소와 간격을 호스트 측에 설정합니다 광선 전달 설명자를 생성할 때 말이죠 Metal에서는 이를 셰이더에서 설정합니다 SIMD 그룹의 스레드 값이 동일하지 않으면 동작이 정의되지 않습니다
광선 유형 인덱스와 기하 승수는 DirectX와 Metal에서 동일한 방식으로 처리됩니다 앱은 이제 셰이더에서 이를 설정할 수 있습니다 DirectX와 Metal에서는 인스턴트 가속 구조를 생성할 때 인스턴스당 인스턴스 오프셋 인덱스를 생성합니다 DirectX에서는 기하 오프셋 인덱스가 자동 생성되지만 Metal에서는 유연하게 직접 설정할 수 있습니다
교차 함수 버퍼는 레이 트레이싱 게임을 위한 Metal의 포팅 경험을 크게 개선합니다 작업이 시작되면 Metal 4에서는 또한 Metal의 가속 구조 구축을 최적화할 수 있습니다
Metal은 가속 구조 빌드에 대한 많은 통제력을 이미 제공하고 있습니다 기본 동작 외에도 재적합 최적화나 장면 확대 더 신속한 가속 구조 구축이 가능합니다 올해에는 더 많은 유연성을 제공합니다 레이 트레이싱 시간을 빠른 교차로 줄일 수 있죠
또는 가속 구조 메모리 사용량을 최소화하도록 선택할 수도 있습니다
사용 플래그는 가속 구조 빌드당 설정할 수 있고 모든 가속 구조마다 동일할 필요는 없습니다
새로운 가속 구조 플래그로 렌더링 파이프라인의 레이 트레이싱을 필요에 따라 더욱 맞춤화할 수 있습니다 확률 효과에 사용하는 경우 노이즈 제거기가 필요합니다 이제 MetalFX 업스케일러에 노이즈 제거 기능이 생겼습니다
실시간 레이 트레이싱은 점점 더 많이 사용되고 있습니다 하이브리드 레이 트레이싱부터 복잡한 경로 추적까지요 이 예시 이미지에서는 레이 트레이싱으로 현실감이 배가되고
반사를 크게 향상합니다 레이 트레이싱에서 최고의 품질과 성능을 얻으려면 노이즈 제거가 필요합니다
MetalFX API로 업스케일링과 노이즈 결합을 약간의 추가 입력으로 간단하게 할 수 있습니다 노이즈 제거 업스케일러에 추가 정보와 정확한 세부 정보를 입력하면 품질이 더욱 향상됩니다
업스케일러와 노이즈 제거기를 결합하기 전에 전통적인 방법을 간단히 살펴보겠습니다
일반적인 실시간, 대화형 레이 트레이싱 렌더링 파이프라인은 여러 효과를 개별적으로 추적하고 개별적으로 노이즈를 제거하고 결과를 하나의 노이즈 없는 지터링 텍스처로 구성합니다 이는 MetalFX의 시간적 업스케일러로 업스케일링됩니다 그 다음에 후처리가 이어지고요
기존 노이즈 제거기는 각 장면마다 예술적 매개변수를 조정해야 했죠 예술적 매개변수 조정이 없는 경우 노이즈 제거기의 모습입니다 대조적으로 MetalFX의 노이즈 제거 업스케일러 에서는 매개변수 조정이 필요없죠 주요 렌더링 후, 후속 처리 직전에 적용되기 때문이죠 MetalFX은 머신 러닝 기술로 견고하고 고성능, 고품질의 노이즈 제거와 업스케일링을 다양한 시나리오에서 제공합니다 통합하기도 더 쉽습니다 업스케일러 통합은 노이즈 제거 업스케일러 통합의 시작점입니다 여기서 업스케일러 입력을 확인할 수 있습니다 색상, 모션, 심도죠 새로 결합된 API는 업스케일러 API의 슈퍼 세트입니다
새 API에, 왼쪽에 보이는 것처럼 노이즈 프리 추가 보조 버퍼를 추가해야 합니다 대부분은 앱에서 이미 포함하고 있을 수 있습니다 각각에 대해 더 자세히 살펴보겠습니다
첫 번째 새로운 입력은 노멀 값입니다 최상의 결과를 얻으려면 이 값이 있어야 합니다
확산 반사도는 머티리얼이 확산된 빛의 기본 색상입니다
거칠기는 표면이 매끄럽거나 거칠게 표현되는 방식을 선형 값으로 나타냅니다 마지막 입력은 경면 반사도입니다 렌더링한 확산 빈사도의 노이즈 프리 근사치여야 합니다 프레넬 성분을 포함해야 합니다 코드에서 쉽게 입력을 추가할 수 있습니다
일반적인 시간적 업스케일러 생성은 코드가 약 10줄 필요합니다 노이즈 제거 버전을 활성화하려면 스케일러 유형을 변경하고 추가 텍스처 유형을 추가해야 합니다
마찬가지로 스케일러를 인코딩하면 업스케일러 호출이 됩니다 또한 유일한 차이점은 추가 입력 텍스처를 바인딩해야 합니다
노이즈 제거기 기본 설정 후 선택 입력 항목으로 기능을 개선할 수 있습니다 전형적인 통합 오류도 피할 수 있죠
몇 가지 선택적 입력 텍스처로 품질을 개선할 수 있습니다
먼저 반사적 타격 거리는 픽셀 1차 가시성 지점에서 2차 반발 지점까지의 광선 길이를 알려 줍니다 노이즈 제거 강도 마스크는 노이즈 제거가 필요 없는 영역 표시에 사용합니다 알파 채널 기반으로 사용되는 투명 오버레이는 업스케일링만 되고 노이즈 제거는 되지 않은 색상을 블랜딩합니다
가장 일반적인 통합 문제는 너무 많은 노이즈 입력입니다 이를 수정하려면 모든 표준 경로 추적 샘플링 개선을 사용해야 합니다 다음 이벤트 추정과 중요 샘플링 방법처럼요 광원이 더 많은 큰 장면에서 영역에 실제 기여하는 광원을 대게 샘플링하죠
상관관계가 있는 난수도 레이 트레이싱 샘플 품질과 관련이 있습니다 상관 관계가 너무 높은 난수 생성기를 사용하지 않아야 합니다 공간적, 시간적 상관관계 모두에 아티팩트가 발생할 수 있습니다
보조 데이터와 관련된 잠재적인 오류 중 하나는 금속 마티리얼의 확산 반사율입니다 이 예에서 체스 말이 금속이라 경면 반사도에 색깔이 있습니다 이 경우 체스 말의 확산 반사도가 어두워야 합니다
노멀 값과 관련된 통합 오류도 있습니다 MetalFX 노이즈 제거 업스케일러는 노멀 값을 월드 공간의 노이즈 제거 결정에 사용합니다 부호 비트가 있는 텍스처 유형을 사용하지 않으면 카메라 방향에 따라 품질이 저하될 수 있습니다
이 모든 세부 사항을 올바르게 넣으면 노이즈가 제거된 업스케일링 프레임이 완성됐니다
단일 렌더러에 이 기능을 모두 넣으면 어떻게 되는지 봅시다
동료들이 데모를 준비했습니다 앞서 말한 렌더링 파이프라인을 사용하는 데모죠 새로운 Metal 4 레이 트레이싱 기능으로 렌더링의 레이 트레이싱 부분을 최적화합니다 노이즈 제거와 업스케일링을 MetalFX 노이즈 제거 업스케일러로 동시에 수행하죠 노출과 톤 매핑 후 MetalFX 프레임 보간기가 프레임을 보간합니다
이 데모에서는 글로벌 조명, 반사, 섀도우 앰비언트 오클루전 등의 고급 레이 트레이싱 조명 효과로 두 로봇이 체스를 두는 장면을 생생하게 표현합니다
오른쪽 상단 모서리에서 MetalFX 처리 전의 렌더링을 볼 수 있습니다 다른 보기에서는 다른 MetalFX 입력을 볼 수 있습니다
MetalFX의 노이즈 제거 업스케일러와 프레임 보간기를 모두 채택했습니다 노이즈 제거기는 최종 모습을 수동으로 조정하는 작업을 제거해 렌더링을 크게 단순화합니다
MetalFX 업스케일러로 통합을 마쳤다면 프레임 보간기로 업그레이드할 기회입니다 MetalFX가 처음이라면 먼저 업스케일러를 살펴보세요 사용 중인 레이 트레이싱 효과가 오늘 다룬 교차 함수 버퍼 등 모범 사례를 따르는지 확인하세요 노이즈 제거 업스케일러로 광선 예산을 줄이시고요
여러분이 이 기능을 게임에 어떻게 활용하실 지 Metal 4로 무엇을 만들실지 많은 기대가 됩니다 시청해 주셔서 감사합니다!
-
-
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 - 서론
Apple 플랫폼에서 고급 게임과 프로 앱을 개발하기 위한 고급 기술과 모범 사례 등 Metal 4 게임 시리즈에 대해 알아보세요. Metal 4의 API를 사용하면 Apple 기기에서 더 높은 해상도와 프레임 속도로 워크로드를 확장할 수 있습니다.
- 2:13 - 렌더링 업스케일링
MetalFX는 더 높은 해상도와 더 빠른 프레임 속도를 달성하는 데 도움이 되는 머신 러닝 기반 업스케일러를 갖추고 있습니다. 올해 새롭게 추가된 MetalFX 시간적 업스케일러는 동적으로 크기가 조정되는 입력을 지원하기 때문에 특히 복잡한 프레임의 경우 입력 해상도를 동적으로 낮출 수 있습니다. 반응성 힌트를 선택적으로 사용하여 투명한 효과 또는 입자가 있는 영역에 대해 더 선명한 결과를 얻기 위해 업스케일러에 픽셀 반응성에 대한 힌트를 제공할 수 있습니다. 새로운 노출 디버거 툴을 사용하면 업스케일러에 전달한 노출 값이 올바른지 확인할 수 있습니다.
- 7:17 - 프레임 보간
올해 새롭게 추가된 MetalFX Frame Interpolation 기능은 렌더링된 두 프레임 사이에 추가 프레임을 생성합니다. Frame Interpolation을 사용하여 UI를 렌더링하는 데는 여러 가지 기술이 있습니다. 보간된 프레임과 기본적으로 렌더링된 프레임을 모두 페이싱하고 표시할 때도 고려해야 할 사항이 있습니다.
- 13:50 - Metal 4를 통한 레이 트레이싱
Metal 4는 가속 구조 빌드 및 교차 함수와 관련된 여러 가지 새로운 레이 트레이싱 기능을 추가합니다. 다른 API에서 포팅하는 경우, 셰이더 바인딩 테이블을 Metal 교차 함수 버퍼로 쉽게 포팅할 수 있습니다. 실행이 완료되면 Metal 4가 가속 구조를 구축하는 방식을 최적화할 수도 있습니다.
- 19:25 - 업스케일링 중 노이즈 제거
장면을 레이 트레이싱할 때 더 적은 캐스트 광선으로 노이즈 제거를 사용하면 품질과 성능 사이에서 최적의 균형을 이룰 수 있습니다. 새로운 MetalFX API는 업스케일링 프로세스에 노이즈 제거 기능을 직접 통합하여 실시간 레이 트레이싱 렌더링 파이프라인을 향상시킵니다. 이를 통해 별도의 트레이싱, 노이즈 제거, 구성 단계가 필요한 기존 접근 방식이 간소화됩니다. 노멀, 확산 반사도, 거칠기, 경면 반사도와 같이 노이즈 없는 보조 버퍼를 지정하면 각 장면에 대해 별도의 매개변수를 조정하지 않고도 견고하고, 성능이 뛰어나며, 품질이 좋은 결과를 얻을 수 있습니다.
- 26:08 - 다음 단계
이미 MetalFX Upscaler를 통합했다면 이제 Frame Interpolation으로 업그레이드할 기회입니다. MetalFX가 처음이라면 먼저 업스케일러를 살펴보세요. 그런 다음 레이 트레이싱 효과에 교차 함수 버퍼와 노이즈 제거 업스케일러가 사용되는지 확인하세요.