TECH.ART.FLOW.IO

[번역]Rendering a Scene with Deferred Lighting in Objective-C

jplee 2024. 4. 4. 14:09

단일 페스 지연 렌더링과 모바일 디바이스에서의 특성을 살표 볼 수 있는 애플 개발자 문서가 있습니다.
이후 이와 관련하여 또 다른 자료들도 함께 모아보면서 언리얼엔진 5.4와 모바일 디퍼드 렌더링을 준비 할 때 지켜야할 명세등을 이해 해 보고 프로젝트 진행할 때 명세 기준으로 효과를 쌓아 올려보도록 해야겠습니다.


개요


이 샘플은 섀도 맵을 사용하여 그림자를 구현하고 스텐실 버퍼를 사용하여 라이트 볼륨을 컬링하는 디퍼드 라이팅 렌더러를 보여줍니다.

디퍼드 조명은 포워드 조명보다 많은 수의 조명을 더 쉽게 렌더링할 수 있습니다. 예를 들어, 포워드 조명을 사용하면 조명이 많은 씬에서 모든 프래그먼트가 모든 조명의 기여도를 계산하는 것은 불가능합니다. 복잡한 정렬 및 비닝 알고리즘을 구현하여 각 프래그먼트 에 영향을 미치는 조명으로만 조명 기여도 계산을 제한해야 합니다. 디퍼드 조명을 사용하면 여러 조명을 씬에 쉽게 적용할 수 있습니다.

샘플 코드 프로젝트 구성
Xcode 프로젝트에는 macOS, iOS 또는 tvOS에서 샘플을 실행하기 위한 스키마가 포함되어 있습니다. 기본 구성표는 Mac에서 샘플을 그대로 실행하는 macOS입니다.

참고
프래그먼트 함수 실행을 위해 렌더링 타깃을 별도의 그룹으로 분할하려면 래스터 순서 그룹을 지원하는 macOS 또는 iOS 디바이스가 필요합니다. 지원 여부를 확인하려면 장치의 rasterOrderGroupsSupported 속성을 쿼리하세요.

이 샘플에는 앱 구성을 제어하기 위해 수정할 수 있는 다음과 같은 전처리기 조건문이 포함되어 있습니다:

#define USE_EYE_DEPTH              1
#define LIGHT_STENCIL_CULLING      1
#define SUPPORT_BUFFER_EXAMINATION 1

앱의 동작에서 수정되는 내용은 다음과 같습니다:

- USE_EYE_DEPTH - 활성화하면 아이 스페이스의 깊이 값을 G-버퍼 깊이 컴포넌트에 씁니다. 이렇게 하면 디퍼드 패스가 Eye Space Fragment 위치를 더 쉽게 계산하여 조명을 적용할 수 있습니다. 비활성화하면 화면 깊이가 G-버퍼 깊이 컴포넌트에 기록되고 디퍼드 패스에서 조명 기여도를 계산하기 위해 화면 공간에서 Eye Space로의 추가 역변환이 필요합니다.
- LIGHT_STENCIL_CULLING - 활성화하면 스텐실 버퍼를 사용하여 3D 조명 볼륨과 교차하지 않는 프래그먼트에 대한 조명 계산 실행을 피합니다. 비활성화하면 화면 공간에서 조명으로 덮인 모든 프래그먼트에 조명 계산이 실행됩니다. 즉, 실제로 필요한 것보다 훨씬 더 많은 프래그먼트 비싼 조명 계산을 실행하게 됩니다.
- SUPPORT_BUFFER_EXAMINATION - 런타임에 버퍼 검사 모드의 토글을 활성화합니다. 이 정의로 보호되는 코드는 기본 구현의 일부를 검사하거나 디버깅할 때만 유용합니다.
macOS에서는 런타임에 장면을 검사하려면 다음 키를 누릅니다:

- 1을 누르면 모든 검사 보기를 동시에 볼 수 있습니다.
- 2를 누르면 G-버퍼 알베도 데이터를 볼 수 있습니다.
- 3을 누르면 G-버퍼 노멀 데이터를 볼 수 있습니다.
- 4를 누르면 G-버퍼 깊이 데이터를 볼 수 있습니다.
- 5를 누르면 G-버퍼 스페큘러 데이터를 볼 수 있습니다.
- 6을 누르면 G-버퍼 섀도 데이터를 볼 수 있습니다.
- 7을 누르면 섀도 맵을 볼 수 있습니다.
- 8을 누르면 마스크된 라이트 볼륨 커버리지를 볼 수 있습니다.
- 9를 누르면 전체 라이트 볼륨 커버리지를 볼 수 있습니다.
- 0 또는 Return을 눌러 검사 보기를 종료하고 표준 보기로 돌아갑니다.
iOS에서는 화면을 탭하여 런타임에 표준 보기와 검사 보기 간에 전환합니다.

중요 개념 검토
샘플 앱을 시작하기 전에 다음 개념을 검토하여 디퍼드 라이팅 렌더러의 주요 세부 사항과 몇 가지 고유한 Metal 기능을 더 잘 이해하세요.

기존 디퍼드 조명 렌더러

기존 디퍼드 조명 렌더러는 일반적으로 두 개의 렌더 패스로 구분됩니다:

첫 번째 패스: G-버퍼 렌더링. 렌더러가 씬의 모델을 그리고 변환하면 프래그먼트 함수가 그 결과를 지오메트리 버퍼 또는 G 버퍼라고 하는 텍스처 모음에 렌더링합니다. G-버퍼에는 모델의 머티리얼 색상과 프래그먼트 별 노멀, 그림자 및 깊이 값이 포함됩니다.
두 번째 패스: 디퍼드 조명 및 컴포지션. 렌더러는 G-버퍼 데이터를 사용하여 각 프래그먼트의 위치를 재구성하고 조명 계산을 적용하여 각 조명 볼륨을 그립니다. 조명이 그려질 때 각 조명의 출력은 이전 조명 출력 위에 블렌딩됩니다. 마지막으로 렌더러는 전체 화면 쿼드 또는 컴퓨팅 커널을 실행하여 그림자 및 방향성 조명과 같은 다른 데이터를 씬에 합성합니다.

참고

일부 macOS GPU에는 IMR(인스턴트 모드 렌더링) 아키텍처가 있습니다. IMR GPU에서 디퍼드 라이팅 렌더러는 최소 두 번의 렌더 패스로만 구현할 수 있습니다. 따라서 이 샘플은 macOS 버전의 앱에 대해 2패스 디퍼드 조명 알고리즘을 구현합니다. iOS 및 tvOS 시뮬레이터는 macOS Metal 구현에서 실행되므로 이 시뮬레이터도 2패스 디퍼드 조명 알고리즘을 사용합니다.

Apple 실리콘 GPU의 싱글 패스 디퍼드 조명

모든 iOS 및 tvOS 기기와 현재 특정 macOS 기기에서 볼 수 있는 Apple 실리콘 GPU는 타일 기반 지연 렌더링(TBDR) 아키텍처를 사용하여 데이터를 GPU 내의 타일 메모리에 렌더링할 수 있습니다. 타일 메모리에 렌더링함으로써 장치는 대역폭이 제한된 메모리 버스를 통해 GPU와 시스템 메모리 간의 잠재적으로 비용이 많이 드는 왕복 작업을 피할 수 있습니다. GPU가 타일 메모리를 시스템 메모리에 쓸지 여부는 이러한 구성에 따라 달라집니다:

  • 앱의 렌더링 명령 인코더의 저장 작업.
  • 앱 텍스처의 저장 모드입니다.

MTLStoreActionStore가 저장 액션으로 설정되면 렌더 패스의 렌더링 타깃에 대한 출력 데이터가 타일 메모리에서 시스템 메모리로 쓰여지고, 여기서 렌더링 타깃은 텍스처로 뒷받침됩니다. 그런 다음 이 데이터를 후속 렌더 패스에 사용하면 이러한 텍스처의 입력 데이터가 시스템 메모리에서 GPU의 텍스처 캐시로 읽혀집니다. 따라서 시스템 메모리에 액세스하는 기존의 지연 조명 렌더러는 첫 번째와 두 번째 렌더링 패스 사이에 G-버퍼 데이터를 시스템 메모리에 저장해야 합니다.

그러나 TBDR 아키텍처로 인해 Apple 실리콘 GPU는 언제든지 타일 메모리에서 데이터를 읽을 수 있습니다. 따라서 프래그먼트 셰이더는 타일 메모리에 데이터를 다시 쓰기 전에 타일 메모리의 렌더링 타깃에서 데이터를 읽고 계산을 수행할 수 있습니다. 이 기능을 사용하면 첫 번째와 두 번째 렌더 패스 사이에 G-버퍼 데이터를 시스템 메모리에 저장하지 않아도 되므로 단일 렌더 패스로 디퍼드 조명 렌더러를 구현할 수 있습니다.

G-버퍼 데이터는 단일 렌더 패스 내에서 CPU가 아닌 GPU가 독점적으로 생성하고 소비합니다. 따라서 이 데이터는 렌더 패스가 시작되기 전에 시스템 메모리에서 로드되지 않으며, 렌더 패스가 완료된 후에도 시스템 메모리에 저장되지 않습니다. 시스템 메모리의 텍스처에서 G-버퍼 데이터를 읽는 대신 조명 프래그먼트 함수는 렌더 패스에 렌더 타깃으로 연결된 상태에서 G-버퍼에서 데이터를 읽습니다. 따라서 시스템 메모리를 G-버퍼 텍스처에 할당할 필요가 없으며, 이러한 각 텍스처는 MTLStorageModeMemoryless 스토리지 모드로 선언할 수 있습니다.

참고
TBDR GPU가 프래그먼 함수에서 연결된 렌더 타깃을 읽을 수 있도록 하는 기능을 프로그래머블 블렌딩이라고도 합니다.

래스터 순서 그룹( Raster Order Groups )을 사용한 디퍼드 라이팅
기본적으로 프래그먼트 셰이더가 픽셀에 데이터를 쓸 때 GPU는 셰이더가 해당 픽셀에 대한 쓰기를 완전히 마칠 때까지 기다렸다가 같은 픽셀에 대한 다른 프래그먼트 셰이더의 실행을 시작합니다.

래스터 순서 그룹을 사용하면 앱에서 GPU의 프래그먼트 셰이더의 병렬화를 높일 수 있습니다. 래스터 순서 그룹을 사용하면 프래그먼트 함수가 렌더링 타깃을 여러 실행 그룹으로 분리할 수 있습니다. 이렇게 분리하면 프래그먼트 셰이더의 이전 인스턴스가 다른 그룹의 픽셀에 데이터 쓰기를 완료하기 전에 GPU가 한 그룹의 렌더링 타겟에서 데이터를 읽고 계산을 수행할 수 있습니다.

이 샘플에서 일부 조명 프래그먼트 함수는 이러한 래스터 순서 그룹을 사용합니다:

  • 래스터 순서 그룹 0. 조명 계산 결과가 포함된 렌더링 타깃에 AAPLLightingROG가 사용됩니다.
  • 래스터 순서 그룹 1. 조명 기능의 G-버퍼 데이터에는 AAPLGBufferROG가 사용됩니다.

이러한 래스터 순서 그룹을 사용하면 이전 프래그먼트 셰이더 인스턴스의 조명 계산이 출력 데이터 쓰기를 완료하기 전에 GPU가 프래그먼트 셰이더에서 G-버퍼를 읽고 조명 계산을 실행할 수 있습니다.

디퍼드 조명 프레임 렌더링

샘플은 이러한 단계를 순서대로 렌더링하여 각 풀 프레임을 렌더링합니다:

  1. Shadow map
  2. G-buffer
  3. Directional light
  4. Light mask
  5. Point lights
  6. Skybox
  7. Fairy lights

샘플의 싱글 패스 디퍼드 렌더러는 G-버퍼를 생성하고 모든 후속 단계를 단일 렌더링 패스로 수행합니다. 이 싱글 패스 구현은 기기가 타일 메모리의 렌더링 타겟에서 G-버퍼 데이터를 읽을 수 있는 iOS 및 tvOS GPU의 TBDR 아키텍처 덕분에 가능합니다.

id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_viewRenderPassDescriptor];
renderEncoder.label = @"Combined GBuffer & Lighting Pass";


[super drawGBuffer:renderEncoder];


[self drawDirectionalLight:renderEncoder];


[super drawPointLightMask:renderEncoder];


[self drawPointLights:renderEncoder];


[super drawSky:renderEncoder];


[super drawFairies:renderEncoder];


[renderEncoder endEncoding];

샘플의 기존 디퍼드 렌더러는 한 렌더링 패스에서 G-버퍼를 생성한 다음 다른 렌더링 패스에서 모든 후속 단계를 수행합니다. 이 2패스 구현은 프래그먼트 함수에서 렌더링 대상 색상 데이터 읽기를 지원하지 않는 IMR 아키텍처를 사용하는 GPU에서 필요합니다.

id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_GBufferRenderPassDescriptor];
renderEncoder.label = @"GBuffer Generation";


[super drawGBuffer:renderEncoder];


[renderEncoder endEncoding];
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_finalRenderPassDescriptor];
renderEncoder.label = @"Lighting & Composition Pass";


[self drawDirectionalLight:renderEncoder];


[super drawPointLightMask:renderEncoder];


[self drawPointLights:renderEncoder];


[super drawSky:renderEncoder];


[super drawFairies:renderEncoder];


[renderEncoder endEncoding];

Render the Shadow Map

이 샘플은 빛의 관점에서 모델을 렌더링하여 씬의 단일 방향 광원(태양)에 대한 섀도 맵을 렌더링합니다.

섀도 맵의 렌더 파이프라인에는 버텍스 함수는 있지만 프래그먼트 함수는 없으므로 샘플은 렌더 파이프라인의 추가 단계를 실행하지 않고도 섀도 맵에 기록된 화면 공간 깊이 값을 결정할 수 있습니다. (또한 프래그먼트 함수가 없기 때문에 렌더링이 빠르게 실행됩니다.)

MTLRenderPipelineDescriptor *renderPipelineDescriptor = [MTLRenderPipelineDescriptor new];
renderPipelineDescriptor.label = @"Shadow Gen";
renderPipelineDescriptor.vertexDescriptor = nil;
renderPipelineDescriptor.vertexFunction = shadowVertexFunction;
renderPipelineDescriptor.fragmentFunction = nil;
renderPipelineDescriptor.depthAttachmentPixelFormat = shadowMapPixelFormat;


_shadowGenPipelineState = [_device newRenderPipelineStateWithDescriptor:renderPipelineDescriptor
                                                                  error:&error];

섀도 맵의 지오메트리를 그리기 전에 샘플은 섀도 아티팩트를 줄이기 위해 깊이 편향 값을 설정합니다:
 

[encoder setDepthBias:0.015 slopeScale:7 clamp:0.02];

그런 다음 G-버퍼 단계의 프래그먼트 함수에서 샘플이 프래그먼트가 가려지고 그림자가 생기는지 테스트합니다:

half shadow_sample = shadowMap.sample_compare(shadowSampler, in.shadow_uv, in.shadow_depth);

샘플은 sample_compare 함수의 결과를 normal_shadow 렌더 타겟의 w 컴포넌트에 저장합니다:

gBuffer.normal_shadow = half4(eye_normal.xyz, shadow_sample);

방향광과 점광 구성 단계에서 샘플은 G-버퍼에서 그림자 값을 읽어와 조각에 적용합니다.

 

Render the G-Buffer

샘플의 G-버퍼에는 이러한 텍스처가 포함되어 있습니다:

  • 알베도 및 스페큘러 데이터를 저장하는 알베도_스페큘러_GBuffer. 알베도 데이터는 x, y, z 컴포넌트에 저장되고 스페큘러 데이터는 w 컴포넌트에 저장됩니다.
  • 정상 및 그림자 데이터를 저장하는 normal_shadow_GBuffer. 노멀 데이터는 x, y, z 컴포넌트에 저장되고 섀도 데이터는 w 컴포넌트에 저장됩니다.
  • 깊이 값을 Eye Space에 저장하는 depth_GBuffer.

샘플이 G-버퍼를 렌더링할 때 기존 및 단일 패스 디퍼드 렌더러 모두 모든 G-버퍼 텍스처를 렌더링 패스에 대한 렌더 타깃으로 첨부합니다. 그러나 TBDR 아키텍처를 사용하는 디바이스는 단일 렌더 패스에서 G-버퍼를 렌더링하고 읽을 수 있으므로 샘플에서는 메모리리스 스토리지 모드로 G-버퍼 텍스처를 생성하므로 시스템 메모리가 이러한 텍스처에 할당되지 않습니다. 대신 이러한 텍스처는 렌더링 패스 기간 동안 타일 메모리에만 할당되고 채워집니다. 
이 샘플은 일반적인 drawableSizeWillChange:withGBufferStorageMode: 메서드에서 G 버퍼 텍스처를 생성하지만 싱글 패스 디퍼드 렌더러는 storageMode 변수를 MTLStorageModeMemoryless로 설정하는 반면 기존 디퍼드 렌더러는 이를 MTLStorageModePrivate로 설정합니다:

_GBufferStorageMode = MTLStorageModeMemoryless;

기존 디퍼드 렌더러의 경우 샘플이 G-버퍼 텍스처에 데이터 쓰기를 완료한 후 endEncoding 메서드를 호출하여 G-버퍼 렌더링 패스를 마무리합니다. 렌더링 명령 인코더의 저장 액션이 MTLStoreActionStore로 설정되어 있으므로 인코더가 실행을 완료하면 GPU는 각 렌더링 대상 텍스처를 비디오 메모리에 씁니다. 이를 통해 샘플은 이후의 디퍼드 조명 및 컴포지션 렌더링 패스에서 비디오 메모리에서 이러한 텍스처를 읽을 수 있습니다. 
단일 패스 디퍼드 렌더러의 경우 샘플이 G-버퍼 텍스처에 데이터 쓰기를 완료한 후에는 렌더링 명령 인코더를 마무리하지 않고 후속 단계에서 계속 사용합니다.

디렉셔널 라이팅 및 그림자 적용하기

이 샘플은 디스플레이로 향하는 드로어블에 방향성 조명과 그림자를 적용합니다. 
기존의 디퍼드 렌더러는 프래그먼트 함수에 대한 인수로 설정된 텍스처에서 G-버퍼 데이터를 읽습니다:

fragment half4
deferred_directional_lighting_fragment_traditional(
    QuadInOut                in                      [[ stage_in ]],
    constant AAPLFrameData & frameData               [[ buffer(AAPLBufferIndexFrameData) ]],
    texture2d<half>          albedo_specular_GBuffer [[ texture(AAPLRenderTargetAlbedo) ]],
    texture2d<half>          normal_shadow_GBuffer   [[ texture(AAPLRenderTargetNormal) ]],
    texture2d<float>         depth_GBuffer           [[ texture(AAPLRenderTargetDepth)  ]])

싱글 패스 디퍼드 렌더러는 렌더 패스에 어테치 된 렌더 타깃에서 G-버퍼 데이터를 읽습니다:
 

struct GBufferData
{
    half4 lighting        [[color(AAPLRenderTargetLighting), raster_order_group(AAPLLightingROG)]];
    half4 albedo_specular [[color(AAPLRenderTargetAlbedo),   raster_order_group(AAPLGBufferROG)]];
    half4 normal_shadow   [[color(AAPLRenderTargetNormal),   raster_order_group(AAPLGBufferROG)]];
    float depth           [[color(AAPLRenderTargetDepth),    raster_order_group(AAPLGBufferROG)]];
};
fragment AccumLightBuffer
deferred_directional_lighting_fragment_single_pass(
    QuadInOut                in        [[ stage_in ]],
    constant AAPLFrameData & frameData [[ buffer(AAPLBufferIndexFrameData) ]],
    GBufferData              GBuffer )

이러한 프래그먼트 함수는 입력값이 다르지만 일반적으로 deferred_directional_lighting_fragment_common 프래그먼트 함수에서 구현됩니다. 이 함수는 이러한 연산을 수행합니다:
 

 

  • G-버퍼 노멀 데이터에서 노멀을 재구성하여 디퓨즈 항을 계산합니다.
  • G-버퍼 깊이 데이터에서 Eye Space 위치를 재구성하여 스페큘러 하이라이트를 적용합니다.
  • G-버퍼 그림자 데이터를 사용하여 프래그먼트를 어둡게 하고 장면에 그림자를 적용합니다.

이 단계는 드로어블에 렌더링하는 첫 번째 단계이므로 iOS 및 tvOS 렌더러는 이전 G-버퍼 단계 이전에 드로어블을 가져와서 이후 단계의 출력과 병합할 수 있도록 합니다. 그러나 기존의 디퍼드 렌더러는 G-버퍼 단계가 완료된 후 방향성 조명 단계가 시작될 때까지 드로어블을 가져오는 것을 지연시킵니다. 이 지연은 앱이 드로어블을 보유하는 시간을 줄여 성능을 향상시킵니다.

참고 
_directionLightDepthStencilState의 상태 때문에 deferred_directional_lighting_fragment 함수는 조명이 켜져야 하는
프래그먼트에 대해서만 실행됩니다. 이 최적화는 간단하지만 중요하며 많은 프래그먼트 셰이더 실행 주기를 절약할 수 있습니다.

라이트 볼륨 컬링

이 샘플은 많은 프래그먼트 에 대해 값비싼 조명 계산을 실행하지 않도록 하는 데 사용되는 스텐실 마스크를 만듭니다. 이 스텐실 마스크는 G-버퍼 패스의 깊이 버퍼와 스텐실 버퍼를 사용하여 라이트 볼륨이 지오메트리와 교차하는지 여부를 추적하여 생성합니다. (교차하지 않으면 아무것도 비추지 않습니다.)
drawPointLightMask: 구현에서 샘플은 _lightMaskPipelineState 렌더 파이프라인을 설정하고 인스턴스화된 드로 콜을 인코딩하여 포인트 라이트의 볼륨을 포함하는 아이코사헤드론의 뒷면만 그립니다. 이 드로 콜 내의 프래그먼트 이 깊이 테스트에 실패하면 이 결과는 아이코사면체의 뒷면이 일부 지오메트리 뒤에 있다는 것을 나타냅니다.

[renderEncoder setRenderPipelineState:_lightMaskPipelineState];
[renderEncoder setDepthStencilState:_lightMaskDepthStencilState];


[renderEncoder setStencilReferenceValue:128];
[renderEncoder setCullMode:MTLCullModeFront];


[renderEncoder setVertexBuffer:_frameDataBuffers[_frameDataBufferIndex] offset:0 atIndex:AAPLBufferIndexFrameData];
[renderEncoder setFragmentBuffer:_frameDataBuffers[_frameDataBufferIndex] offset:0 atIndex:AAPLBufferIndexFrameData];
[renderEncoder setVertexBuffer:_lightsData offset:0 atIndex:AAPLBufferIndexLightsData];
[renderEncoder setVertexBuffer:_lightPositions[_frameDataBufferIndex] offset:0 atIndex:AAPLBufferIndexLightsPosition];


MTKMeshBuffer *vertexBuffer = _icosahedronMesh.vertexBuffers[AAPLBufferIndexMeshPositions];
[renderEncoder setVertexBuffer:vertexBuffer.buffer offset:vertexBuffer.offset atIndex:AAPLBufferIndexMeshPositions];


MTKSubmesh *icosahedronSubmesh = _icosahedronMesh.submeshes[0];
[renderEncoder drawIndexedPrimitives:icosahedronSubmesh.primitiveType
                          indexCount:icosahedronSubmesh.indexCount
                           indexType:icosahedronSubmesh.indexType
                         indexBuffer:icosahedronSubmesh.indexBuffer.buffer
                   indexBufferOffset:icosahedronSubmesh.indexBuffer.offset
                       instanceCount:AAPLNumLights];

lightMaskPipelineState에는 프래그먼트 함수가 없으므로 이 렌더 파이프라인에서 컬러 데이터가 기록되지 않습니다. 그러나 설정된 _lightMaskDepthStencilState 깊이와 스텐실 상태로 인해 깊이 테스트에 실패한 모든 프래그먼트 은 해당 프래그먼트 의 스텐실 버퍼를 증가시킵니다. 지오메트리가 포함된 프래그먼트 의 시작 깊이 값은 128이며, 이는 샘플이 G-버퍼 단계에서 설정한 값입니다. 따라서 _lightMaskDepthStencilState가 설정된 상태에서 깊이 테스트에 실패하는 모든 프래그먼트는 깊이 값을 128보다 크게 증가시킵니다. (앞면 컬링이 활성화되어 있으므로 깊이 테스트에 실패하고 값이 128보다 큰 프래그먼트는 적어도 아이코사면체의 뒷면 절반이 모든 지오메트리 뒤에 있다는 것을 나타냅니다.)
다음 그리기 호출에서 drawPointLightsCommon 구현에서 샘플은 포인트 라이트의 기여도를 드로어블에 적용합니다. 이 샘플은 아이코사체드론의 앞쪽 절반이 모든 지오메트리 앞에 있는지 테스트하여 볼륨이 일부 지오메트리와 교차하는지, 따라서 프래그먼트 에 조명이 켜져야 하는지 여부를 결정합니다. 이 그리기 호출에 대해 설정된 깊이 및 스텐실 상태인 _pointLightDepthStencilState는 프래그먼트 의 스텐실 값이 기준값인 128보다 큰 경우에만 프래그먼트 함수를 실행합니다. (스텐실 테스트 값이 MTLCompareFunctionLess로 설정되어 있으므로 샘플은 기준값 128이 스텐실 버퍼의 값보다 작은 경우에만 테스트를 통과합니다).

[renderEncoder setDepthStencilState:_pointLightDepthStencilState];


[renderEncoder setStencilReferenceValue:128];
[renderEncoder setCullMode:MTLCullModeBack];


[renderEncoder setVertexBuffer:_frameDataBuffers[_frameDataBufferIndex] offset:0 atIndex:AAPLBufferIndexFrameData];
[renderEncoder setVertexBuffer:_lightsData offset:0 atIndex:AAPLBufferIndexLightsData];
[renderEncoder setVertexBuffer:_lightPositions[_frameDataBufferIndex] offset:0 atIndex:AAPLBufferIndexLightsPosition];


[renderEncoder setFragmentBuffer:_frameDataBuffers[_frameDataBufferIndex] offset:0 atIndex:AAPLBufferIndexFrameData];
[renderEncoder setFragmentBuffer:_lightsData offset:0 atIndex:AAPLBufferIndexLightsData];
[renderEncoder setFragmentBuffer:_lightPositions[_frameDataBufferIndex] offset:0 atIndex:AAPLBufferIndexLightsPosition];


MTKMeshBuffer *vertexBuffer = _icosahedronMesh.vertexBuffers[AAPLBufferIndexMeshPositions];
[renderEncoder setVertexBuffer:vertexBuffer.buffer offset:vertexBuffer.offset atIndex:AAPLBufferIndexMeshPositions];


MTKSubmesh *icosahedronSubmesh = _icosahedronMesh.submeshes[0];
[renderEncoder drawIndexedPrimitives:icosahedronSubmesh.primitiveType
                          indexCount:icosahedronSubmesh.indexCount
                           indexType:icosahedronSubmesh.indexType
                         indexBuffer:icosahedronSubmesh.indexBuffer.buffer
                   indexBufferOffset:icosahedronSubmesh.indexBuffer.offset
                       instanceCount:AAPLNumLights];

drawPointLightMask: 의 드로우 호출은 지오메트리 뒤에 있는 프래그먼트 의 스텐실 값을 증가시키므로 샘플이 프래그먼트 함수를 실행하는 프래그먼트 은 이 두 조건을 모두 충족하는 프래그먼트 뿐입니다:

  • 프래그먼트의 앞면이 깊이 테스트를 통과하고 일부 지오메트리 앞에 있는 프래그먼트 입니다.
  • 뒷면이 깊이 테스트에 실패하고 일부 지오메트리 뒤에 있는 프래그먼트 입니다.

다음 다이어그램은 이 스텐실 마스크 알고리즘을 사용하는 렌더링 프레임과 그렇지 않은 프레임 간의 프래그먼트 적용 범위 차이를 보여줍니다. 알고리즘이 활성화된 경우 녹색으로 표시된 픽셀은 포인트 조명 프래그먼트 함수가 실행된 픽셀입니다.

알고리즘이 비활성화되면 녹색과 빨간색으로 표시된 픽셀은 포인트 라이트 프래그먼트 기능이 실행된 픽셀입니다.

Render the Skybox and Fairy Lights

최종 조명 단계에서 샘플은 훨씬 더 간단한 조명 기술을 씬에 적용합니다. 

샘플은 사원의 지오메트리에 대해 스카이박스에 깊이 테스트를 적용하여 렌더러가 드로어블의 일부 지오메트리로 채워지지 않은 영역에만 렌더링합니다.

[renderEncoder setRenderPipelineState:_skyboxPipelineState];
[renderEncoder setDepthStencilState:_dontWriteDepthStencilState];
[renderEncoder setCullMode:MTLCullModeFront];


[renderEncoder setVertexBuffer:_frameDataBuffers[_frameDataBufferIndex] offset:0 atIndex:AAPLBufferIndexFrameData];
[renderEncoder setFragmentTexture:_skyMap atIndex:AAPLTextureIndexBaseColor];


// Set mesh's vertex buffers
for (NSUInteger bufferIndex = 0; bufferIndex < _skyMesh.vertexBuffers.count; bufferIndex++)
{
    __unsafe_unretained MTKMeshBuffer *vertexBuffer = _skyMesh.vertexBuffers[bufferIndex];
    if((NSNull*)vertexBuffer != [NSNull null])
    {
        [renderEncoder setVertexBuffer:vertexBuffer.buffer
                                offset:vertexBuffer.offset
                               atIndex:bufferIndex];
    }
}


MTKSubmesh *sphereSubmesh = _skyMesh.submeshes[0];
[renderEncoder drawIndexedPrimitives:sphereSubmesh.primitiveType
                          indexCount:sphereSubmesh.indexCount
                           indexType:sphereSubmesh.indexType
                         indexBuffer:sphereSubmesh.indexBuffer.buffer
                   indexBufferOffset:sphereSubmesh.indexBuffer.offset];

이 샘플은 드로어블에 페어리 라이트를 2D 원으로 렌더링하고 텍스처를 사용하여 해당 프래그먼트 의 알파 블렌딩 인자를 결정합니다.
 

half4 c = colorMap.sample(linearSampler, float2(in.tex_coord));


half3 fragColor = in.color * c.x;


return half4(fragColor, c.x);

See Also

Lighting Techniques

 
Rendering a Scene with Forward Plus Lighting Using Tile Shaders
Implement a forward plus renderer using the latest features on Apple GPUs.
 
Rendering a Scene with Deferred Lighting in Swift
Avoid expensive lighting calculations by implementing a deferred lighting renderer optimized for immediate mode and tile-based deferred renderer GPUs.
 
Rendering a Scene with Deferred Lighting in C++
Avoid expensive lighting calculations by implementing a deferred lighting renderer optimized for immediate mode and tile-based deferred renderer GPUs.
 
Rendering Reflections with Fewer Render Passes
Use layer selection to reduce the number of render passes needed to generate an environment map.

원문
https://developer.apple.com/documentation/metal/metal_sample_code_library/rendering_a_scene_with_deferred_lighting_in_objective-c

 

Rendering a Scene with Deferred Lighting in Objective-C | Apple Developer Documentation

Avoid expensive lighting calculations by implementing a deferred lighting renderer optimized for immediate mode and tile-based deferred renderer GPUs.

developer.apple.com