스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Core Image, Metal 및 SwiftUI로 EDR 콘텐츠 표시
Core Image 기반의 멀티 플랫폼 SwiftUI 응용 프로그램에서 EDR(Extended Dynamic Range) 렌더링에 대한 지원을 추가하는 방법을 살펴볼 수 있습니다. ViewRepresentable을 사용하여 MTKView에 CIImage를 표시하는 모범 사례를 개괄적으로 살펴보겠습니다. 또한 EDR 렌더링을 활성화하기 위한 간단한 단계를 공유하고 EDR을 지원하는 150개 이상의 내장된 CIFilter 중 일부를 살펴보겠습니다.
리소스
관련 비디오
WWDC22
Tech Talks
-
다운로드
♪♪ Core Image, Metal, SwiftIU를 사용한 EDR 콘텐츠 표시 반갑습니다, 여러분 저는 David Hayward입니다 저는 Core Image팀 소속 소프트웨어 엔지니어입니다 오늘은 Core Image 응용 프로그램에서 Extended Dynamic Range 콘텐츠를 표시하는 방법에 대해 설명하겠습니다 이번 세션은 네 부분으로 나뉠 겁니다 우선, 플랫폼에서 EDR과 관련된 몇 가지 중요한 용어를 소개하고 두 번째로는 새로운 Core Image 샘플 프로젝트에 대해 설명하고 EDR에 대한 지원을 추가하는 방법을 보여 드릴 겁니다 마지막으로 CIFilters를 사용해 EDR 콘텐츠를 구성하는 이미지 생성법을 보여 드리죠
그럼 몇 가지 핵심 용어부터 시작해 보겠습니다 SDR 또는 스탠다드 다이내믹 레인지는 전형적인 표현 방식으로 검은색의 0부터 흰색의 1까지 정형화된 RGB 색의 범위를 사용합니다 익스텐디드 다이내믹 레인지 EDR은 그와는 대조적으로 일반적인 범위를 벗어난 RGB 색상을 표현하는 데 권장되는 방법입니다
SDR에서는 0이 검은색을 나타내고 SDR에서 1이 나타내는 밝기는 흰색과 동일합니다 하지만 EDR에서 1보다 큰 값은 흰색보다 더 밝은 픽셀에 사용될 수도 있습니다
하지만 기억해야 할 건 1보다 큰 값은 허용되지만 헤드룸을 넘는 값은 잘려 나갑니다
헤드룸은 디스플레이의 현재 최대 니트를 SDR 화이트 값으로 나눈 값입니다
헤드룸 값은 디스플레이마다 다를 수 있고 디스플레이 밝기가 변경될 때나 주변 환경에 따라 바뀔 수 있습니다
'iOS의 EDR 살펴보기’ 프레젠테이션을 보시면 이 개념에 대해 좀 더 자세히 알 수 있습니다 응용 프로그램에 표시할 수 있는 EDR 콘텐츠 소스는 많습니다 우선, TIFF나 OpenEXR 같은 일부 파일 형식은 EDR의 부동 소수점 값을 저장할 수 있습니다
AVFoundation으로 HDR 비디오 형식의 프레임을 가져올 수도 있죠
Metal API는 EDR 환경을 텍스처로 렌더링하는 데 사용될 수 있습니다 ProRAW DNG 파일을 렌더링해 EDR 하이라이트를 표시할 수도 있죠
2021년 'Capture and process ProRAW images'에 자세히 설명되어 있습니다
다음으로는 SwiftUI 응용 프로그램에서 Metal의 Core Image 사용법에 대해 설명할 겁니다 응용 프로그램에 EDR 지원을 추가하는 것도 간략히 얘기해 보죠
저희가 최근에 새로운 샘플 코드 프로젝트를 출시했는데 SwiftUI 멀티플랫폼 앱에서 Core Image와 Metal Kit View를 결합하는 방법에 대한 모범 사례를 보여 줍니다 샘플을 다운받아서 코드를 살펴보시는 걸 추천합니다 제가 이번 기회에 어떻게 작동하는지 보여 드릴게요
샘플이 영상으로 된 CIImage를 그리고 그 이미지가 Metal 뷰에 나타납니다 최적의 성능을 위해 샘플은 MTKView를 사용합니다 코드를 단순하게 유지하기 위해서 앱은 움직이는 체커보드 CIImage를 앱이 원하는 콘텐츠 대신 표시합니다
게다가 앱은 SwiftUI를 사용하기 때문에 macOS와 iOS iPadOS 플랫폼에서 공통 코드 기반을 사용할 수 있습니다
프로젝트는 몇 개의 짧은 소스 파일로 구성되므로 클래스끼리 어떻게 상호 작용을 하는지 설명해 드리도록 하죠
이 응용 프로그램에는 세 가지 중요한 요소가 있습니다 첫 번째로 가장 중요한 건 'MetalView'입니다 이는 SwiftUI와 호환되는 View를 실행하며 MTKView 클래스를 포함합니다
MTKView 클래스는 macOS의 NSView와 다른 플랫폼의 UIView를 기반으로 하기 때문에 MetalView를 실행할 때는 ViewRepresentable을 사용해서 플랫폼 종속 MTKView 클래스와 SwiftUI 사이를 연결합니다
하지만 MTKView는 직접적으로 렌더링을 하진 않습니다 그 대신 다른 프로그램을 이용하죠
이 앱에서 렌더러 클래스는 MTKView의 델리게이트로 그래픽 상태 객체의 초기화를 담당하는데 Metal 명령 대기열과 Core Image 콘텍스트인 셈입니다
MetalView 델리게이트가 되는 데 필요한 드로우() 메서드도 실행합니다
하지만 렌더러는 그리는 이미지를 결정할 직접적인 권한은 없습니다 대신 imageProvider 블록을 사용해 그릴 CIImage를 가져옵니다
이 앱에서 ContentView 클래스는 렌더링할 CIImage를 제공하는 코드 블록을 실행합니다
요약하자면, MetalView는 델리게이트를 호출해 그림을 그리고 렌더러 드로우() 메서드는 ContentView를 호출해서 그림을 그릴 이미지를 제공합니다
이제 세 클래스의 코드에 대해 더 자세히 말씀드리죠 MetalView 클래스의 makeView() 코드부터 시작할게요 makeView()를 호출해서 MTKView를 생성하면 뷰의 델리게이트를 렌더러 상태 객체로 설정합니다 이는 SwiftUI 뷰를 실행하기 위한 표준이 되는 접근 방식으로 NSView 또는 UIView를 포합합니다
다음으로는 preferredFramesPerSecond에서 뷰를 렌더링할 빈도수를 설정합니다 뷰의 도면을 그릴 드라이브를 정하는 거라서 이 프로퍼티가 중요합니다 이게 어떻게 작동하는지 설명해 드릴게요
이 샘플은 움직이는 응용 프로그램이기 때문에 view.preferredFramesPerSecond를 원하는 프레임 속도로 설정합니다
이 설정을 통해 MTKView가 구성되고 뷰가 드로우 이벤트의 타이밍을 맞춥니다
이렇게 하면 뷰의 렌더 델리게이트가 주기적으로 그림을 그리게 되고 다시 콘텐츠 공급자가 시간에 맞는 CIImage를 만들도록 요청하는 거죠
애니메이션이 멈출 때까지 이 과정이 계속해서 반복됩니다
이미지 편집 앱같이 다른 경우에는 뷰를 그려야 할 때 사용자와 제어 장치와 상호 작용을 하며 움직이는 게 좋죠
enableSetNeedsDisplay를 true로 설정하면 MTKView가 구성되고 제어 장치가 드로우 이벤트의 타이밍을 조절할 수 있습니다 제어 장치가 움직이면 updateView() 메서드를 호출해야 합니다
그다음 뷰의 델리게이트가 드로우()를 한 번 더 호출하고
각 드로우는 콘텐츠 공급자에게 현재 제어 상태에 대한 CIImage를 생성하도록 요청합니다
이 접근 방식은 비디오 프레임 도입으로 드로우 이벤트를 실행해야 할 때도 적합합니다
MetalView 클래스에 대한 얘기는 이걸로 마치도록 하겠습니다 다음으로 렌더러 델리게이트에서 가장 중요한 코드인 드로우() 메서드에 대해 얘기해 보죠
렌더러의 드로우() 메서드는 주기적인 프레임 속도로 호출됩니다 드로우() 메서드가 호출되면 콘텐츠 스케일 팩터를 결정해야 하는데 뷰가 켜져 있는 디스플레이의 해상도가 반영되죠 CIImage는 포인트가 아닌 픽셀로 측정되기 때문에 이 과정이 필요합니다 드로우() 메서드가 호출될 때마다 이 과정을 진행하는 게 중요합니다 다른 디스플레이로 뷰가 이동하면 프로퍼티가 변경될 수 있기 때문이죠
다음은 mtlTextureProvider로 CIRenderDestination를 생성합니다
그리고 콘텐츠 공급자를 호출해 현재 시간 및 스케일 팩터에 사용할 CIImage를 생성합니다 반환된 이미지는 뷰의 중앙에 배치되고 불투명한 배경과 혼합돼서 CIImage를 뷰 대상에 렌더링하는 작업을 시작합니다
ContentView 클래스에서 가장 중요한 코드는 이니트() 메서드입니다
이니트() 메서드는 Content view의 중심을 담당합니다 중심을 만들면서 렌더러와 MetalView 클래스를 연결하는 거죠
우선, 이미지 공급자 블록으로 렌더러 객체를 생성합니다
해당 블록은 요청된 시간 및 스케일에 대한 CIImage를 반환하는 역할을 합니다
마지막으로 ContentView의 중심을 해당 렌더러를 사용하는 MetalView로 설정합니다
이제 이 부분도 다 끝났습니다 Core Image로 렌더링할 수 있는 간단한 SwiftUI 앱이 있습니다 이제 이 앱에서 EDR 헤드룸 렌더링을 지원하도록 수정하는 법을 알아보죠
이 응용 프로그램에 EDR 기능을 추가하는 건 정말 쉽습니다 첫 번째, EDR에 대한 뷰를 초기화합니다 두 번째, 렌더링을 하기 전에 헤드룸을 계산합니다 세 번째, 가능한 헤드룸을 이용해서 CIImage를 구축합니다 이 과정과 관련된 실제 코드를 보여 드리죠 일단, MetalView 클래스에 추가할 게 있습니다 뷰를 만들 때 wantsExtendedDynamicRangeContent라고 레이어에 알려야 합니다 그리고 뷰는 pixelFormat이 .rgba16Float라고 알려야 하죠 그리고 색 공간은 긴 선형이어야 합니다
다음으로 렌더러 클래스의 드로우() 메서드에 약간의 변화가 필요합니다
드로우() 메서드에 코드를 추가해야 합니다 뷰의 현재 화면을 가져오고 현재 EDR 헤드룸을 표시하도록 요청하는 거죠
그다음 헤드룸이 이미지 공급자 블록에 매개 변수로 전달됩니다 드로우() 메서드가 호출될 때마다 이 작업을 수행하는 게 중요합니다 헤드룸은 역동적인 프로퍼티로 디스플레이 밝기가 변경될 때나 주변 환경에 따라 바뀔 수 있습니다
세 번째 변경 사항은 ContentView 클래스의 제공자 블록입니다
여기서 이미지 공급자 블록 선언에 헤드룸 인수를 추가해야 합니다 그다음 CIFilters가 있는 헤드룸을 통해 사용자의 EDR 디스플레이에 훌륭하게 나타나는 CIImage를 반환할 수 있습니다 응용 프로그램에 EDR 기능을 추가하려면 간단한 세 가지 단계만 따르면 됩니다 EDR에 대한 뷰를 초기화하고 렌더링을 하기 전에 헤드룸을 계산하고 가능한 헤드룸을 이용해서 CIImage를 구축하면 되죠 이것이 나머지 프레젠테이션의 주제가 될 겁니다 이제 앱이 EDR을 지원하기 때문에 CIFilters를 이용해 CIImage를 만들고 EDR 콘텐츠를 표시해 보도록 하죠
Core Image에 있는 150개 이상의 필터가 EDR을 지원합니다 이 모든 필터가 EDR 콘텐츠로 이미지를 생성할 수도 있고 EDR 콘텐츠가 포함된 이미지를 처리할 수도 있단 거죠 예를 들면, CIColorControls과 CIExposureAdjust 필터가 여러분 앱에서 EDR 색상과 관련해 이미지의 밝기와 색조, 채도 대비를 변경할 수 있습니다 그라디언트 필터 같은 여러 가지 필터는 주어진 EDR 색상 매개 변수로 이미지를 생성할 수 있습니다
올해 새로 추가한 세 가지 필터도 EDR 이미지를 지원합니다 가장 주목할 만한 건 CIAreaLogarithmicHistogram으로 밝기 값의 임의 범위에 대해 히스토그램을 생성할 수 있습니다
CIColorCube 필터는 EDR 인풋 이미지에서 더 잘 실행되도록 올해 업데이트한 필터입니다
모든 기본 제공 필터는 Core Image가 생성되는 색 공간이 고정되지 않고 선형이기 때문에 작동하는 겁니다 RGB 값이 0-1 범위를 벗어날 수 있게 하기 때문이죠 여러분이 앱을 개발할 때 해당 필터가 EDR을 지원하는지 확인할 수 있습니다
그렇게 하려면 필터 인스턴스를 생성하고 필터의 범주 속성을 요청한 다음 kCICategoryHighDynamicRange가 배열에 포함되어 있는지 확인합니다 저희가 새로 추가한 기능은 CIfilter 변수에 대한 Xcode QuickLook 디버깅 기능입니다 여기에는 각 필터 클래스에 대한 설명이 표시되는데 각 인풋 매개 변수의 범주 및 요구 사항이 포함되죠
이렇게 모든 EDR 필터를 고려할 때 여러분의 앱이 콘텐츠에 적용할 수 있는 효과는 무궁무진합니다 제가 오늘 설명할 예시에서는 샘플 앱의 체커보드 패턴에 밝게 거울 반사가 되도록 리플 효과를 추가할 겁니다
이 효과를 만들기 위해서는 rippleTransition 필터의 인스턴스가 필요합니다
다음으로 인풋과 타깃 이미지를 모두 체커 이미지로 설정합니다
그다음 물결의 중심과 물결의 변환 시간을 제어하는 설정값을 입력합니다
그리고 shadingImage에 기울기를 설정해서 물결이 반사되면서 강조되는 부분이 생성되도록 합니다
마지막으로 인풋 이미지에 필터를 설정이 끝났으면 outputImage에도 필터를 요청합니다 명암 이미지를 생성하는 법도 알려 드리죠 리플 효과의 반사 하이라이트를 만드는 데 사용되는 이미지입니다 비트맵 데이터로 이미지를 만들 수 있습니다 하지만 더 나은 성능을 위해선 순서대로 CIImage를 생성합니다
우선, linearGradient 필터의 인스턴스를 생성합니다 이 필터는 각 두 개의 포인트와 CIColors로 구성된 경사를 만듭니다
저희는 경사면을 흰색으로 할 겁니다 현재 헤드룸을 기준으로 한 밝기에 최대 밝기는 적당한 수준으로 제한합니다
여러분이 적용하려는 효과의 모양에 따라 한계점이 달라집니다
색상0은 고정되지 않은 선형 색 공간에서 화이트 레벨을 사용해 생성되어야 합니다
색상 1은 청색으로 설정돼 있습니다
포인트 0과 포인트 1은 좌표로 설정합니다 그래서 경사면이 왼쪽 상단에 나타나게 한 거죠
그다음 필터의 outputImage를 리플 필터가 필요한 크기로 자릅니다
그 결과 발생하는 반사 효과의 물결은 앱에서 할 수 있는 것에 대한 대체물에 불과합니다 하지만 중요한 원리를 알려 주고 있죠 일반적으로는 밝은 픽셀을 사용하는 것이 가장 좋고 적을수록 좋죠 그렇게 하면 밝은 픽셀이 더 돋보일 겁니다
지금까지 EDR 효과를 위한 두 가지 CIFilters를 써 봤는데요 다른 EDR 필터도 자유롭게 확인해 보세요 다음으로 얘기할 주제는 CIColorCube 필터의 사용법과 사용자 정의 필터를 입력할 때 규칙입니다
아주 인기 있는 필터 중 하나가 CIColorCubeWithColorSpace입니다 보통 이 필터는 SDR 이미지에 적용되죠 이 필터는 프로세스나 인스턴트, 토날 같이 여러 사진 앱에서 효과를 구현하는 데 사용합니다
일반적으로 이런 필터에 쓰이는 큐브 데이터는 치명적인 한계가 있습니다 데이터는 오로지 0-1사이의 RGB 색만 입력하고 출력합니다
이런 한계를 피할 수 있는 방법은 CIColorCubeWithColorSpace 필터에 HLG나 PQ 같은 EDR 색 공간을 쓰도록 지시하는 겁니다
이렇게 하면 EDR 콘텐츠에 대해 최상의 결과를 낼 수 있죠 하지만 그러려면 새로운 큐브 데이터를 생성해야 하는데 이는 색 공간 범위에서만 유효합니다 뿐만 아니라 큐브 면적을 늘려야 할 수도 있습니다 대신 EDR 이미지에 계속 SDR 큐브 데이터를 사용할 수 있죠 올해는 필터에 SDR 큐브 데이터를 추정하도록 지시할 수 있습니다 이 기능을 사용하려면 평소처럼 SDR 큐브 데이터를 설정하면 됩니다 그 다음 필터의 새로운 추정 프로퍼티를 설정합니다
이 부분을 'true'로 설정하면 필터에 EDR 인풋 이미지를 제공하고 EDR 아웃풋 이미지를 얻을 수 있죠
오늘 마지막으로 다룰 주제는 여러분이 직접 CIKernels를 생성하는 경우에 대한 몇 가지 모범 사례입니다
우선, 여러분의 커널 코드를 확인하세요 RGB 값은 0-1 범위로 제한해야 합니다 clamp나 min, max 함수를 사용하세요
대부분의 경우에 이런 한계는 안전하게 제거할 수 있고 그러면 커널이 올바르게 작동합니다
다음으로 RGB 값이 0-1 범위를 넘을 수 있다고 해도 알파 값은 0-1 사이로 설정해야 합니다 그렇지 않으면 이미지를 혼합하거나 표시할 때 정의되지 않은 동작이 나오게 됩니다
이 예시에서는 커널이 알파 채널에 실수로 5를 곱했습니다 올바른 동작은 RGB 값에 5를 곱했을 때만 실행됩니다
이걸로 오늘 프레젠테이션을 마치겠습니다 오늘은 Core Image SwiftUI 응용 프로그램에 EDR 헤드룸 기능을 추가하는 방법을 배웠고 다양한 내장 CIFilters를 이용해서 EDR 콘텐츠를 생성하고 수정하는 법도 배웠습니다 시청해 주셔서 감사합니다!
-
-
5:17 - Metal View
// Metal View struct MetalView: ViewRepresentable { @StateObject var renderer: Renderer func makeView(context: Context) -> MTKView { let view = MTKView(frame: .zero, device: renderer.device) view.delegate = renderer // Suggest to Core Animation, through MetalKit, how often to redraw the view. view.preferredFramesPerSecond = 30 // Allow Core Image to render to the view using Metal's compute pipeline. view.framebufferOnly = false return view }
-
7:12 - Renderer
// Renderer func draw(in view: MTKView) { if let commandBuffer = commandQueue.makeCommandBuffer(), let drawable = view.currentDrawable { // Calculate content scale factor so CI can render at Retina resolution. #if os(macOS) var contentScale = view.convertToBacking(CGSize(width: 1.0, height: 1.0)).width #else var contentScale = view.contentScaleFactor #endif let destination = CIRenderDestination(width: Int(view.drawableSize.width), height: Int(view.drawableSize.height), pixelFormat: view.colorPixelFormat, commandBuffer: commandBuffer, mtlTextureProvider: { () -> MTLTexture in return drawable.texture }) let time = CFTimeInterval(CFAbsoluteTimeGetCurrent() - self.startTime) // Create a displayable image for the current time. var image = self.imageProvider(time, contentScaleFactor) image = image.transformed(by: CGAffineTransform(translationX: shiftX, y: shiftY)) image = image.composited(over: self.opaqueBackground) _ = try? self.cicontext.startTask(toRender: image, from: backBounds, to: destination, at: CGPoint.zero)
-
8:09 - ContentView
// ContentView import CoreImage.CIFilterBuiltins init(struct ContentView: View { var body: some View { // Create a Metal view with its own renderer. let renderer = Renderer( imageProvider: { (time: CFTimeInterval, scaleFactor: CGFloat) -> CIImage in var image: CIImage // create image using CIFilter.checkerboardGenerator... return image }) MetalView(renderer: renderer) } }
-
9:17 - MetalView changes
if let caMtlLayer = view.layer as? CAMetalLayer { caMtlLayer.wantsExtendedDynamicRangeContent = true view.colorPixelFormat = MTLPixelFormat.rgba16Float view.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearDisplayP3) }
-
9:35 - Get headroom
let screen = view.window?.screen; #if os(macOS) let headroom = screen?.maximumExtendedDynamicRangeColorComponentValue ?? 1.0 #else let headroom = screen?.currentEDRHeadroom ?? 1.0 #endif var image = self.imageProvider(time, contentScaleFactor, headroom)
-
10:05 - Use headroom
imageProvider: { (time: CFTimeInterval, scaleFactor: CGFloat, headroom: CGFloat) -> CIImage in var image: CIImage // Use CIFilters to create image for time / scale / headroom / ... return image })
-
12:42 - Ripple effect
let ripple = CIFilter.rippleTransition() ripple.inputImage = image ripple.targetImage = image ripple.center = CGPoint(x: 512.0, y: 384.0) ripple.time = Float(fmod(time*0.25, 1.0)) ripple.shadingImage = shading image = ripple.outputImage
-
13:34 - Generating the shading image
let gradient = CIFilter.linearGradient() let w = min( headroom, 8.0 ) gradient.color0 = CIColor(red: w, green: w, blue: w, colorSpace: CGColorSpace(name: CGColorSpace.extendedLinearSRGB)!)! gradient.color1 = CIColor.clear gradient.point0 = CGPoint(x: sin(angle)*90.0 + 100.0, y: cos(angle)*90.0 + 100.0) gradient.point1 = CGPoint(x: sin(angle)*85.0 + 100.0, y: cos(angle)*85.0 + 100.0) let shading = gradient.outputImage?.cropped(to: CGRect(x: 0, y: 0, width: 200, height: 200))
-
16:13 - CIColorCube and EDR
let f = CIFilter.colorCubeWithColorSpace() f.cubeDimension = 32 f.cubeData = sdrData f.extrapolate = true f.inputImage = edrImage let edrResult = f.outputImage
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.