TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역] UE5.4 커스텀 렌더패스로 인터랙티브 워터 퍼포먼스 최적화

jplee 2024. 7. 25. 01:57

저자: Asuka9
이 글의 주요 내용: 이 글은 커스텀 렌더 패스를 사용한 물 상호작용 최적화에 대한 첫 번째 글(앞으로 두 편이 더 있을 예정)으로, 5.4의 씬 캡처에 커스텀 렌더 패스가 정확히 무엇을 최적화하는지에 초점을 맞춥니다. 최적화의 성능은 어떤가요? 커스텀 렌더 패스를 단독으로 사용할 때와 비교하면 어떤 차이가 있나요? 최적화된 씬 캡처가 인터랙티브 워터의 요구 사항을 충족할 수 있나요? 요구 사항을 충족하려면 커스텀 렌더 패스를 직접 작성해야 하나요?
이전 콘텐츠:UE C++와、Scene Capture Component、Render Target

소개

최근 5.4의 새로운 기능인 커스텀 렌더 패스 (나중에 CRP라고 함) 분석 및 구현에 대한 많은 큰형님들의 기사들이 게시되었으며,이 클래스는 깊이 맵 플로팅 및 SceneColor 플로팅의 최적화와 동일하며 깊이 또는 SceneColor 정보의 관점에서 RDG로 이동하지 않아도 편리 할 수 있으며 기존의 경우처럼 BasePass + 조명 + 포스트 프로세싱 과정을 거치기 위해 다른 Render로 이동하지 않아도 될 수 있습니다. 씬 캡처 2D는 다른 렌더를 열어 베이스패스 + 라이팅 + 포스트 프로세싱 프로세스를 거치기 때문에 여러 프리미티브 컴포넌트의 씬에 더 친숙합니다.

https://zhuanlan.zhihu.com/p/682088207

zhuanlan.zhihu.com

 

https://zhuanlan.zhihu.com/p/683783506

zhuanlan.zhihu.com

 

https://zhuanlan.zhihu.com/p/689152468

zhuanlan.zhihu.com

그래서 저는 이 CRP를 사용하여 깊이 정보를 얻기 위해 씬 캡처가 필요한 일부 애플리케이션 시나리오를 최적화하고 CRP의 성능 향상과 구체적인 사용성을 검증하기로 결정했습니다. 프리미티브가 많은 씬에서 모든 씬을 다시 렌더링하는 데 드는 비용이 매우 높기 때문에(바로 여러분! 씬 캡처), 씬 캡처에서 원래 성능을 유지하면서 성능을 쉽게 최적화할 수 있다면 매우 가치 있는 선택입니다. 그래서 연구를 위해 선택한 매우 유명한 인터랙티브 워터 공개 사례 연구를 소개합니다:
 

FREE Responsive 3D Water with render Targets

Based on the UE4 Render Target Liquid System I created a Blueprint to increase its size 😃 Comments are welcome. Download: https://goo.gl/2g4Mct V1 FREE Unreal Engine 4 Reactive Dynamic Water 3D Project Download [UE4.19] - YouTube](YouTube) V2 FREE Unrea

forums.unrealengine.com

 

GitHub - marvelmaster/UE4_Dynamic_Water_Project_V3: Unreal Engine 4 Dynamic Water Project by marvelmaster

Unreal Engine 4 Dynamic Water Project by marvelmaster - marvelmaster/UE4_Dynamic_Water_Project_V3

github.com

아래는 CRP를 사용한 최적화 후의 결과와 성능 비교입니다:
 

아래는 최적화 전과 후의 성능을 비교한 것입니다:

최적화 전 프레임 속도가 30% 미만이었습니다.

최적화 후에는 GPU 시간이 3ms 단축되고 드로와 프리미티브 수가 모두 줄어들어 액터 수가 많은 씬에서 더 뚜렷한 효과를 볼 수 있습니다.

이 글에 사용된 씬은 애픽 마켓의 빅토리아 시대 기차역 및 철도 모듈형 세트 리소스의 데모 맵입니다. 1688개의 스태틱 메시 액터(기타 액터까지 합치면 약 2200개)가 있는 시나리오입니다. 이 글에서는 인터랙션 워터 최적화를 위해 CRP를 사용하는 방법을 세 부분으로 나누어 공유합니다:
1. 씬캡처와 커스텀 렌더 패스 함수 살펴보기(이 포스트): 커스텀 렌더 패스는 5.4에 공개된 새로운 기능으로, 싱글 레이어 워터와 씬 캡처 컴포넌트에서 사용되므로 에픽은 확실히 이 기능을 사용할 것 같습니다. 클래스를 사용하여 씬 캡처의 현재 문제점을 최적화할 것으로 예상되므로 이 글에서는 커스텀 렌더 패스가 씬 캡처를 최적화하는 데 어떤 역할을 하는지에 초점을 맞출 것입니다. 어떤 성능을 발휘하나요? 커스텀 렌더 패스를 단독으로 사용할 때와 비교하면 어떤 차이가 있나요? 최적화된 씬 캡처가 인터랙티브 워터의 요구 사항을 충족할 수 있나요?

2. 요구 사항을 충족하도록 CustomRenderPass 클래스 설계: 첫 번째 분석을 통해 원래의 커스텀 렌더 패스 클래스와 최적화된 씬 캡처가 깊이 정보를 얻기 위한 인터랙티브 워터의 요구를 충족할 수 없다는 것을 알 수 있으므로 두 번째 기사의 주요 내용은 인터랙티브 워터, 인터랙티브 스노우 및 기타 시나리오에 따라 일부 최적화 및 최적화를 커스텀 렌더 패스에 적용하는 것입니다. 커스텀 렌더 패스로 최적화 및 변환을 수행하고, 요구 사항을 충족하는 커스텀 렌더 패스 클래스를 디자인합니다.

3. 인터랙티브 워터 블루프린트 실습 최적화: 커스텀 렌더 패스의 두 번째 변환을 통해 이미 인터랙티브 워터의 요구를 충족할 수 있으므로 세 번째 기사의 주요 내용은 인터랙티브 워터에 대한 커스텀 렌더 패스 액세스, 인터랙티브 워터에서 장면 캡처 구성 요소의 원래 사용을 대체하여 원래 효과를 유지하기 위해 동시에 성능을 향상시키는 것입니다. 원래 효과는 유지됩니다.

그럼 본론을 시작하겠습니다!

워터 상호작용에 대한 블루프린트 분석

프로젝트의 워터 인터랙션 블루프린트인 WaterBody_BP0은 Scene Capture를 사용하여 수평면의 깊이 정보를 캡처한 다음 깊이 정보를 ForceSplat 머티리얼에 전달하여 동작을 계산하고 HeightSimulation을 통해 노멀로 높이 물결을 계산한 다음 인터랙션 물결을 가져와야 합니다. 여기서 제가 분석한 내용은 매우 간단하며, 자세한 내용과 원리를 보려면 LEAF 큰형님의 또 다른 기사를 참조하세요:

https://zhuanlan.zhihu.com/p/676028629

zhuanlan.zhihu.com

여기서는 씬 캡처의 작동 방식에 초점을 맞춥니다(WaterBody_BP0에 대한 자세한 분해 및 분석은 세 번째 글에서 공유할 예정입니다). 블루프린트를 분석하면 씬 캡처가 절단면과 함께 수면의 깊이 정보를 수직 아래 방향으로 캡처하는 것을 알 수 있습니다. 씬 캡처는 직교 투영 모드에서 클리핑 평면과 결합하여 수면의 수심 정보를 수직 아래 방향으로 캡처합니다:
 

수직 워터샷

절단면은 수면의 높이입니다.

직교 투영 모드

촬영한 물체가 수면에 나타나면 그림과 같이 깊이가 0에 가까운 값으로 매우 가깝고 검은색으로 나타나며 ForceSplat 머티리얼에 의해 인식됩니다:

워터바디의 씬 캡처 컴포넌트는 파이널 컬러로 캡처하고 씬 텍스처의 뎁스는 포스트 프로세싱 머티리얼을 통해 캡처합니다.

그러면 WaterBody에서 Scene Capture가 하는 일이 명확해집니다. 먼저 Clipping plane 을 설정하여 촬영된 내용이 수평면임을 확인합니다(사실 3편에서 공유되는 높이 조절된 precision 파라미터도 있습니다). 그런 다음 Final Color를 직교로 촬영하고 포스트 프로세스 머티리얼를 사용하여 SceneDepth를 꺼내어 간단한 나눗셈을 하여 Force Splat 재료에 사용할 수 있도록 빨간색과 검은색 대비 그래픽으로 바꿉니다.
따라서 커스텀 렌더 패스로 대체하려면 직교 투영을 사용하고, 원거리 크롭면을 설정하고, 씬의 씬 뎁스를 촬영하고, 요구 사항을 충족하기 위해 그에 따라 포스트 프로세싱을 할 수 있어야 합니다.
직교 투영 및 씬 뎁스 촬영의 경우 CRP가 필요하며 해당 포스트프로세스의 경우 Draw Material To Render Target을 사용하여 달성 할 수 있지만이 프로세스에는 CRP 캡처 용 RT 1 개와 Draw Material 용 RT 1 개를 추가해야하지만 Farr Clipping plane은 현재 설정할 수 없으므로 더 많은 탐색이 필요합니다. 

Scene Capture 및 Custom Render Pass의 기능 탐색

5.4 버전의 씬 캡처 소스 코드를 살펴보면 5.4 버전에 최적화 옵션인 RenderInMainRenderer가 있음을 알 수 있습니다.

/** Render scene capture as additional render passes of the main renderer rather than as an independent renderer. Can only apply to scene depth and device depth modes. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = SceneCapture, meta = (EditCondition = "CaptureSource == ESceneCaptureSource::SCS_SceneDepth || CaptureSource == ESceneCaptureSource::SCS_DeviceDepth"))
bool bRenderInMainRenderer = false;

즉, 씬 캡처를 위해 추가 렌더링을 열지 않고 메인 렌더링 스레드에서 뎁스 캡처 패스를 호출하는 것은 물 상호작용을 최적화하기 위해 커스텀 렌더 패스를 작성할 때와 같은 목적, 즉 각도에 대한 깊이 정보를 빠르게 가져오는 것과 같은 역할을 합니다;
Scene Capture는 Clipping Plane 과 포스트프로세스가 가능한 옵션으로 Scene Capture를 켜면 과연 성능을 절약하고 맞춤형 Clipping Plane을 사용할 수 있다면 Custom Render Pass를 커스터마이징 할 필요 없이 Scene Capture를 사용하여 깊이 있는 수중 촬영을 할 수 있습니다. 그러면 이제 씬 캡처의 심도 있는 촬영 성능, 기능에 대해서 탐색을 해보도록 하겠습니다. 성능을 최적화하는 것이 본문의 출발점이기 때문에 먼저 성능이 어떤지 살펴봐야 합니다.

Scene Capture와 Custom Render Pass의 성능 대비

RenderInMainRenderer는 Capture Source가 Scene Depth일 때와 Device Depth일 때 켜야 함

두 가지 캡처 소스를 적용할 수 있습니다.

다른 캡처 소스에는 이 최적화 옵션을 켤 수 없습니다.

성능 분석을 위해 플레이 후 맵의 각도를 선택하면(제 컴퓨터가 꽤 느립니다) 표준 시나리오에서의 성능은 아래와 같습니다:

46.1fps,GPU Time 15.27ms
실험 설정: 여기서는 전체 화면 씬(1920*1080), 동일한 각도와 프레임을 사용하고 씬 캡처와 CRP의 성능을 샷의 깊이와 비교합니다.Scene Capture와 CRP는 모두 직교 투영으로 설정하고 Ortho Width는 1024, Primitive Render Mode는 Render로 설정합니다. Scene Primitives, Capture Source 는 Scene Depth 로 설정하고, Scene Capture 의 RenderInMainRenderer 옵션을 체크하면, CRP 구현은 @Shilin 의 UE5.4 CustomRenderPass 구현(코드 포함)과 동일하며, 테스트 결과는 다음과 같습니다. 테스트 결과는 다음과 같습니다:

현재 결과에 따르면 뎁스 캡처 장면의 경우 최적화된(RenderInMainRendererer 선택) scene capture component와 Custom Render Pass를 사용하여 성능 소모가 더 가깝고 custom render Pass의 성능이 더 좋습니다(조금만).
따라서 Scene Capture Component의 성능이 Custom의 소모량과 비슷한 상황에서 Clipping Plane(및 후처리)을 사용할 수 있다면, 자신의 Custom Render Pass를 만드는 데 많은 노력을 기울이지 않아도 됩니다(정말 필요하지 않을까요).

최적화된 Scene Capture 기능 탐색

먼저 색상을 0으로 설정한 아주 간단한 포스트 프로세싱 머티리얼을 만들어 포스트 프로세싱 머티리얼이 작동하는지 테스트합니다:

후처리 재질 아래에 설정:

그리고 이 scene capture가 포착한 RT의 깊이 값을 debug 재질을 통해 보여줍니다.

결과는 실행 중인 상태로 표시됩니다:

실패했습니다!
테스트 후 장면 색상 및 최종 색상이 설정된 경우에만 후처리가 작동합니다.(사실 여기서 테스트할 필요는 없습니다. 이미 공개된 정보이기 때문입니다.)

다음으로 Clipping이 최적화된 깊이 캡처에 미치는 효과를 테스트하기 위해 scene capture에서 위의 후처리 재질을 제거한 후 debug 재질을 다음과 같이 수정하였습니다.

Clipping Plane 활성화를 켜려면 Global Clipping Plane을 켜야 하며, 이 경우 베이스패스가 15% 더 소모된다는 사실을 알게 되었습니다:

효과를 테스트하기 위해 먼저 열어보는 것이 좋습니다:

최적화 모드를 선택할 때 직교 투영을 선택하고 표준 조건은 다음과 같습니다.

Clip Plane 켜기, 거리 1000으로 변경없이 설정:

그러나 최적화 항목의 선택을 취소하고 평면을 자르는 것이 효과적입니다.

소스 코드의 관점에서 커스텀 Clipping Plane을 사용하려면 Global Clipping Plane 옵션을 켜서 basePass 소비를 늘릴 뿐만 아니라 독립적으로 View를 켜고 Render를 추가해야 커스터마이징이 가능하므로 좋은 선택이 아닙니다.

따라서 최적화된 씬 캡처 2D 컴포넌트는 커스텀 기반 커스텀 렌더 패스와 비슷한 역할을 할 수 있지만 평면을 자를 수 없으므로 5.4 버전에서는 씬 캡처를 직접 사용해도 최적화된 물 상호작용의 요구 사항을 충족할 수 없습니다.
하지만 다행히도 물 상호작용, 눈 상호작용, 자체 제작한 섀도 맵(그렇지 않은 경우)에 상관없이 직교 투영을 우선적으로 사용할 것이며 직교 투영의 투영 행렬 구성도 비교적 간단하므로 커스텀 커스텀 렌더 패스에서 약간의 수정을 수행하고 직교 투영 행렬을 구성할 때 근거리 및 원거리 평면을 커스터마이징할 수 있습니다.

아래 요약 및 목표

커스텀렌더패스를 사용하여 인터랙티브 워터 성능을 최적화한다는 목표를 달성하기 위해 씬 캡처를 CRP로 대체하려고 합니다. 먼저 물 상호작용 블루프린트에서 씬 캡처의 현재 역할을 분석한 결과, 수평면에서 상호작용하는 오브젝트의 깊이를 캡처하는 것이므로 평면을 자르는 것을 지원해야 한다는 것을 알았습니다.그런 다음 5.4 버전의 씬 캡처가 CRP를 사용하여 깊이 캡처의 성능을 최적화한다는 사실을 발견하고 휠 중복을 피하기 위해 최적화된 씬 캡처가 요구 사항을 충족할 수 있는지 테스트했지만 결과는 '아니오'였습니다.
하지만 근거리 및 원거리 평면의 직교 투영을 커스터마이징하는 것은 매우 쉽기 때문에 다음 글에서는 렌더 모드 선택, 숨겨진 액터 및 액터만 표시 설정을 지원하여 근거리 및 원거리 평면의 커스텀 직교 투영을 달성하기 위해 자체 커스텀 렌더 패스를 설계해 보겠습니다.
다음 단계는 이 글의 서두에서 질문했던 질문에 답하는 것입니다:
커스텀 렌더 패스는 씬 캡처에 정확히 무엇을 최적화하나요?A: 뎁스 캡처에만 최적화되어 있으며, 뎁스 정보를 캡처할 때 RenderInMainRenderer 상자를 체크하면 됩니다.
최적화된 성능은 어떤가요?A: 나쁘지 않으며, 깊이 촬영 시 커스텀 렌더 패스보다 약간만 더 소비합니다.
커스텀 렌더 패스를 단독으로 사용할 때와 비교하면 어떻게 되나요?A: RenderInMainRenderer를 체크한 경우의 씬 캡처는 성능은 커스텀 렌더 패스와 매우 비슷하고, 이 옵션을 체크한 경우의 씬 캡처는 CRP로 할 수 있는 작업은 기본적으로 차이가 없으며, 자체 후처리 및 크롭 평면도 유효하지 않습니다.단, 개발 유형에 따른 표시 플래그는 테스트되지 않았으며 추후 추가될 예정입니다.
최적화된 씬 캡처로 인터랙티브 워터의 요구 사항을 충족할 수 있나요?A: 아니요, 클리핑  평면이 부족하기 때문에 수면 평면의 깊이 정보가 필요합니다.
 
 
 
요구 사항을 충족하려면 커스텀 렌더 패스를 직접 작성해야 하나요?A: 예. 클리핑 평면을 지원하고 추가 뷰 및 렌더러를 사용하지 않도록 투영 매트릭스를 구축할 때 약간의 커스터마이징을 수행해야 합니다.
UE5.4 커스텀 렌더패스로 인터랙티브 워터 퍼포먼스 최적화 (II) - 커스텀 커스텀 렌더 패스
이 문서의 주요 내용: 커스텀 커스텀 렌더 패스 구현, 근거리 및 원거리 자르기 평면의 직교 투영 지원, 액터/컴포넌트만 표시 및 숨겨진 액터/컴포넌트 지원 (주로 다음 씬 캡처(● ˇ ∀ ˇ ●) 이동)
前置内容:UE C++开发、Scene Capture Component、Render Target、Custom Render Pass
이 글은 커스텀 렌더 패스(이후 CRP라고 함)를 사용한 인터랙티브 워터 퍼포먼스 최적화에 대한 시리즈 글 중 두 번째 글입니다.첫 번째 글에서는 먼저 요구 사항을 분석하고 특정 시점에 대한 깊이 정보를 기록하기 위해 CRP를 사용해야 했습니다.휠 중복을 피하기 위해 5.4의 씬 캡처 컴포넌트를 분석한 결과 뎁스 샷 최적화가 켜져 있을 때 근거리 및 원거리 평면 설정, 액터만 표시 등의 요구 사항을 충족할 수 없다는 것을 알았습니다.따라서 이 글에서는 인터랙티브 워터 사용을 위한 자체 커스텀 렌더 패스 컴포넌트를 구현할 것입니다.이 컴포넌트에는 직교 투영을 위한 근거리 및 원거리 평면 설정과 액터만 표시 및 숨겨진 액터를 지원합니다.사용 편의성을 위해 편집자 전용 카메라 모델 디스플레이도 추가되었습니다.
저자의 매우 식물적인 이유로 인해 많은 곳의 코드가 매우 거칠게 작성 될 수 있으므로 큰 형제를 부드럽게 뿌려주세요.(그리고 수문학적 의심의 두 번째 부분은 매우 큽니다.)
이 글에서 구현된 기능은 가까운 시일 내에 Github에 업로드될 예정이며, 여기에 링크 되어 있습니다.
수정된 Custom Render Pass 기능은 다음과 같으며 그림 1은 기본 기능, 그림 2, 3, 4는 Show Only 및 Hidden 지원, 그림 5는 절단 평면 지원입니다.

기본 심도 촬영 기능

Show Only 모드, Show Only Actor를 원뿔로 설정

Show Only설정

Hidden Actor 기능

클립핑 플랜으로 근거리 플랜을 130으로 설정하면 의자의 일부가 잘립니다.

Custom Render Component 설계.

렌더 타깃에 깊이를 렌더링해야 하는 필요성은 인터랙티브 워터 외에도 존재하므로, 개인적으로 커스텀 렌더 패스 기능을 컴포넌트에 캡슐화하여 다른 비인터랙티브 상황에서도 쉽게 사용할 수 있도록 해야 한다고 생각합니다.
USceneComponent->USceneCaptureComponent->USceneCaptureComponent2D의 상속 로직을 참조하면, 여기에서도 CustomRenderComponent가 USceneComponent의 서브클래스로 설정되어 있습니다. 위치 및 이동에 대한 SceneComponent의 지원으로 컴포넌트의 기본 기능은 CustomRenderPass의 기능과 결합하는 것만으로 달성할 수 있습니다.

다음은 CRP 렌더링 프로세스에 대한 간략한 요약입니다:
커스텀렌더패스는 뷰포트를 정의하고 뷰포트의 뷰위치 , 프로젝션매트릭스 , 뷰로테이션매트릭스 및 뷰액터를 정의합니다. 이 클래스는 베이스컬러 또는 뎁스 또는 둘 다에 그릴 수 있습니다.
디퍼드 렌더링을 예로 들면, 디퍼드 렌더링은 DBufferTextures가 생성된 후 FDeferredShadingSceneRenderer::Render() 함수에서 CRP가 발생합니다. 씬의 커스텀 렌더링 패스 입력 배열에 있는 요소를 하나씩 반복하여 CRP를 위한 메모리 리소스와 공간을 초기화한 후 다음 코드를 사용하여 해당 뷰에 추가합니다(자세한 내용은 SceneRendering.cpp 참조):

    int32 NumSceneCaptureViews = 0;
    //입력을 하나씩 반복합니다.
    for (int32 i = 0; i < Scene->CustomRenderPassRendererInputs.Num(); i++)
    {
        //처리할 n번째 커스텀렌더패스를 가져옵니다.
        const FScene::FCustomRenderPassRendererInput& PassInput = Scene->CustomRenderPassRendererInputs[i];
        FCustomRenderPassBase* CustomRenderPass = PassInput.CustomRenderPass;
        check(CustomRenderPass);
        CustomRenderPassInfos[i].CustomRenderPass = CustomRenderPass;
        //CRP 구성을 위한 기본 정보
        FSceneViewInitOptions ViewInitOptions;
        ViewInitOptions.SetViewRectangle(FIntRect(0, 0, CustomRenderPass->GetRenderTargetSize().X, CustomRenderPass->GetRenderTargetSize().Y));
        ViewInitOptions.ViewOrigin = PassInput.ViewLocation;
        ViewInitOptions.ViewRotationMatrix = PassInput.ViewRotationMatrix;
        ViewInitOptions.ProjectionMatrix = PassInput.ProjectionMatrix;
        ViewInitOptions.bIsSceneCapture = true;
        ViewInitOptions.ViewFamily = &ViewFamily;
        ViewInitOptions.ViewActor = PassInput.ViewActor;
        ViewInitOptions.ShowOnlyPrimitives = PassInput.ShowOnlyPrimitives;
        ViewInitOptions.HiddenPrimitives = PassInput.HiddenPrimitives;
        //CRP 구성을 위한 기본 정보
        FSceneView NewView(ViewInitOptions);
        FViewInfo* ViewInfo = &CustomRenderPassInfos[i].Views.Emplace_GetRef(&NewView);
        // Must initialize to have a GPUScene connected to be able to collect dynamic primitives.
        ViewInfo->DynamicPrimitiveCollector = FGPUScenePrimitiveCollector(&GPUSceneDynamicContext);
        ViewInfo->bDisableQuerySubmissions = true;
        ViewInfo->bIgnoreExistingQueries = true;
        ViewInfo->CustomRenderPass = CustomRenderPass;
        CustomRenderPass->Views.Add(ViewInfo);
 
        NumSceneCaptureViews++;
    }
    //CRP 해당 뷰 추가
    AllViews.Empty(Views.Num() + NumSceneCaptureViews);
    for (int32 i = 0; i < Views.Num(); ++i)
    {
        AllViews.Add(&Views[i]);
    }
    for (FCustomRenderPassInfo& PassInfo : CustomRenderPassInfos)
    {
        for (FViewInfo& View : PassInfo.Views)
        {
            AllViews.Add(&View);
        }
    }

따라서 CRP 데이터를 초기화하여 씬의 CustomRenderPassRendererInputs에 넣기만 하면 됩니다.MVP 행렬, 표시 및 HiddenPrimitives, 뷰포인트 위치 및 기타 데이터가 필요하다는 것을 알 수 있습니다.
자세한 프로세스는 1편 기사(위)에서 언급한 세 개의 글에서 확인할 수 있으며, 여기서는 코드의 흐름에 초점을 맞출 수 있습니다.

FDepthCustomRenderPass 구현

이 단계에 대한 많은 문서가 있습니다(예: 첫 번째 포스트에서 언급한 세 가지 문서).여기서는 FCustomRenderPass를 직접 상속하고 뎁스 렌더링 결과를 출력하기 위한 렌더타깃을 생성하기만 하면 됩니다:

class FMyCustomRenderPass final: public FCustomRenderPassBase
{
public:
	IMPLEMENT_CUSTOM_RENDER_PASS(FMyCustomRenderPass)
	
	FMyCustomRenderPass(const FString& InDebugName, ERenderMode InRenderMode, ERenderOutput InRenderOutput, UTextureRenderTarget2D* InRenderTarget)
		:FCustomRenderPassBase(InDebugName, InRenderMode, InRenderOutput,FIntPoint(InRenderTarget->GetSurfaceWidth(),InRenderTarget->GetSurfaceHeight()))
	,RenderTarget(InRenderTarget->GameThread_GetRenderTargetResource()){}
	
	
	virtual void OnPreRender(FRDGBuilder& GraphBuilder) override
	{
		RenderTargetTexture = RenderTarget->GetRenderTargetTexture(GraphBuilder);
	};
	
	FRenderTarget* RenderTarget = nullptr;

};

这样就可以创建FDepthCustomRenderPass的对象了。在创建好对象后,我们需要相关的数据来初始化对象,并传入CustomRenderPassRendererInputs数组中。首先来看投影方式的选择,有了确定的投影方式,才能构建相关的矩阵。

투영 방식 선택의 현실화

CameraTypes.h에는 카메라 투영 모드에 대한 열거형 클래스가 정의되어 있으며, 컴포넌트 사용 시 직교 투영으로 촬영할지 원근 투영으로 촬영할지를 선택하기 위해 컴포넌트에 노출될 수 있습니다.

 

UENUM()
namespace ECameraProjectionMode
{
	enum Type : int
	{
		Perspective,
		Orthographic
	};
}

따라서 컴포넌트의 U프로퍼티로 ECameraProjectionMode를 도입하고 에디터 인터페이스에서 선택할 수 있습니다.그리고 커스텀 렌더 패스 렌더러 입력을 빌드할 때 투영 유형에 따라 다른 유형의 투영 행렬을 생성합니다:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Projection, meta=(DisplayName = "Projection Type"))
TEnumAsByte<ECameraProjectionMode::Type> ProjectionType;

//Build projection matrix by projection mode
if(ProjectionType == ECameraProjectionMode::Perspective)
{
	BuildProjectionMatrixActor(CaptureSize,FOV,ClippingPlane,ProjectionMatrix);
}
else
{
	BuildOrthoMatrixActor(CaptureSize, OrthoWidth, -1, 0, 0, ProjectionMatrix, NearP, FarP);
}

여기서 두 투영 행렬의 생성자 함수는 투영 유형을 전환할 수 있는 SceneCaptureRendering.cpp에서 가져옵니다.원근 투영 행렬은 다른 여러 문서에서 찾을 수 있습니다. 인터랙티브 워터의 특정 요구 사항을 충족하려면 직교 투영을 위해 근거리 및 원거리 평면을 설정해야 합니다.

직교 투영 행렬 변환하기

이전 글에서 시간 직교 투영 행렬을 만들 때 근거리 및 원거리 평면을 설정해야 한다고 언급했습니다.그러나 사실 이 요구는 매우 간단합니다. 다음 행렬을 채우기만 하면 n과 f를 얻을 수 있으며, 구체적인 파생은 GAMES101과 네트워크의 다양한 기사를 살펴볼 수 있습니다.예: GAMES101 투영 매트릭스 도출 세부 정보 및 분석

구체적으로 UE의 SceneCaptureRendering.cpp의 BuildOrthoMatrix에서 기본 NearPlane = 0 및 FarPlane = UE_FLOAT_HUGE_DISTANCE / 4.0f를 입력하려는 근거리 및 원거리 값으로 변경하기만 하면 됩니다.

//근거리 및 원거리 매개변수 추가
static void BuildOrthoMatrix(FIntPoint InRenderTargetSize, float InOrthoWidth, int32 InTileID, int32 InNumXTiles, int32 InNumYTiles, FMatrix& OutProjectionMatrix, float Near = 0, float Far = UE_FLOAT_HUGE_DISTANCE / 4.0f)
{
	check((int32)ERHIZBuffer::IsInverted);
	float const XAxisMultiplier = 1.0f;
	float const YAxisMultiplier = InRenderTargetSize.X / float(InRenderTargetSize.Y);

	const float OrthoWidth = InOrthoWidth / 2.0f;
	const float OrthoHeight = InOrthoWidth / 2.0f * XAxisMultiplier / YAxisMultiplier;

	const float NearPlane = Near;//여기에서 수정
	const float FarPlane = Far;//여기에서 수정

	const float ZScale = 1.0f / (FarPlane - NearPlane);
	//printf("%f\n", ZScale);
	const float ZOffset = -NearPlane;

	if (InTileID == -1)
	{
		OutProjectionMatrix = FReversedZOrthoMatrix(
			OrthoWidth,
			OrthoHeight,
			ZScale,
			ZOffset
		);
		
		return;
	}

//중간 계산 단계 생략 -------

	OutProjectionMatrix = FMatrix(
		FPlane(2.0f / (r-l), 0.0f, 0.0f, 0.0f),
		FPlane(0.0f, 2.0f / (t-b), 0.0f, 0.0f),
		FPlane(0.0f, 0.0f, -ZScale, 0.0f),
		FPlane(-((r+l)/(r-l)), -((t+b)/(t-b)), 1.0f - ZOffset * ZScale, 1.0f)
	);
}

이러한 방식으로 직교 투영을 실현할 수 있으며, 주요 목적은 초기 프러스텀의 n과 f 위치를 수정하는 것입니다.다음으로 ShowOnlyPrimitives와 HiddenPrimitives 를 구현할 수 있습니다.

PrimitiveRenderMode 선택 구현하기.

ShowOnlyPrimitives 와 HiddenPrimitives를 구현하기 전에 Primitive Render mode의 개념을 이해하는 것이 중요합니다.이 모드는 CRP 렌더링 모드를 의미하며, 렌더링 모드에 따라 Primitive에 대한 필터링 방법이 다릅니다.
PRM_RenderScenePrimitives :숨겨진 액터 이외의 액터 렌더링
PRM_UseShowOnlyList: ShowOnlyActor에서 액터만 렌더링하기
PRM_LegacySceneCapture:단순히 씬을 렌더링하는 전통적인 방식

UENUM()
enum class ESceneCapturePrimitiveRenderMode : uint8
{
	/** Legacy */
	PRM_LegacySceneCapture UMETA(DisplayName = "Render Scene Primitives (Legacy)"),
	/** Render primitives in the scene, minus HiddenActors. */
	PRM_RenderScenePrimitives UMETA(DisplayName = "Render Scene Primitives"),
	/** Render only primitives in the ShowOnlyActors list, or components specified with ShowOnlyComponent(). */
	PRM_UseShowOnlyList UMETA(DisplayName = "Use ShowOnly List")
};

对于Primitive渲染方式的选择方式与实现投影的方式同理,也是通过枚举完成的。这里的枚举类型ESceneCapturePrimitiveRenderMode是在SceneCaptureComponent.h中实现的。对应的对于ShowOnly和Hidden Primitives数组的添加和删除,主要参考SceneCaptureRendering的实现方法。在为CustomRenderPassRendererInput构建添加HiddenPrimitives和ShowOnlyPrimitives的过程中,相关的函数会利用所选的PrimitiveMode来添加数组元素。

Show Only와Hidden Primitives 구현

接下来,我们需要实现HiddenPrimitives和ShowOnlyPrimitives数组的构建函数。以及供蓝图使用的HiddenActors、HiddenComponents、ShowOnlyActors、ShowOnlyComponents的增删函数。以上函数均基于SceneCaptureRendering.cpp中的对应函数修改。
首先是HiddenPrimitives和ShowOnlyPrimitives数组的构建函数:GetShowOnlyAndHiddenComponents

void UCustomRenderComponent::GetShowOnlyAndHiddenComponents(TSet<FPrimitiveComponentId>& HiddenPrimitives, TOptional<TSet<FPrimitiveComponentId>>& ShowOnlyPrimitives)
{
        //将HiddenActors和HiddenComponents两个数组的元素添加到HiddenPrimitives中
	for (auto It = HiddenComponents.CreateConstIterator(); It; ++It)
	{
		// If the primitive component was destroyed, the weak pointer will return NULL.
		UPrimitiveComponent* PrimitiveComponent = It->Get();
		if (PrimitiveComponent)
		{
			HiddenPrimitives.Add(PrimitiveComponent->GetPrimitiveSceneId());
		}
	}

	for (auto It = HiddenActors.CreateConstIterator(); It; ++It)
	{
		AActor* Actor = *It;

		if (Actor)
		{
			for (UActorComponent* Component : Actor->GetComponents())
			{
				if (UPrimitiveComponent* PrimComp = Cast<UPrimitiveComponent>(Component))
				{
					HiddenPrimitives.Add(PrimComp->GetPrimitiveSceneId());
				}
			}
		}
	}
        //在确定RenderMode为UseShowOnlyList的情况下,将ShowOnlyActors和ShowOnlyComponents两个数组的元素添加到ShowOnlyPrimitives中
	if (PrimitiveRenderMode == ESceneCapturePrimitiveRenderMode::PRM_UseShowOnlyList)
	{
		ShowOnlyPrimitives.Emplace();

		for (auto It = ShowOnlyComponents.CreateConstIterator(); It; ++It)
		{
			// If the primitive component was destroyed, the weak pointer will return NULL.
			UPrimitiveComponent* PrimitiveComponent = It->Get();
			if (PrimitiveComponent)
			{
				ShowOnlyPrimitives->Add(PrimitiveComponent->GetPrimitiveSceneId());
			}
		}

		for (auto It = ShowOnlyActors.CreateConstIterator(); It; ++It)
		{
			AActor* Actor = *It;

			if (Actor)
			{
				for (UActorComponent* Component : Actor->GetComponents())
				{
					if (UPrimitiveComponent* PrimComp = Cast<UPrimitiveComponent>(Component))
					{
						ShowOnlyPrimitives->Add(PrimComp->GetPrimitiveSceneId());
					}
				}
			}
		}
	}
}

接下来就是在蓝图中使用的工具函数,分别是:

// Set Show Only and Hidden Actors/Components
	UFUNCTION(BlueprintCallable, Category = "Rendering|Capture")
	void HideComponent(UPrimitiveComponent* InComponent);
	UFUNCTION(BlueprintCallable, Category = "Rendering|Capture")
	void HideActorComponents(AActor* InActor, const bool bIncludeFromChildActors);
	UFUNCTION(BlueprintCallable, Category = "Rendering|Capture")
	void ShowOnlyComponent(UPrimitiveComponent* InComponent);
	UFUNCTION(BlueprintCallable, Category = "Rendering|Capture")
	void ShowOnlyActorComponents(AActor* InActor, const bool bIncludeFromChildActors);
	UFUNCTION(BlueprintCallable, Category = "Rendering|Capture")
	void RemoveShowOnlyComponent(UPrimitiveComponent* InComponent);
	UFUNCTION(BlueprintCallable, Category = "Rendering|Capture")
	void RemoveShowOnlyActorComponents(AActor* InActor, const bool bIncludeFromChildActors);
	UFUNCTION(BlueprintCallable, Category = "Rendering|Capture")
	void ClearShowOnlyComponents();
	UFUNCTION(BlueprintCallable, Category = "Rendering|Capture")
	void ClearHiddenComponents();

具体的实现可以看Github中的代码,以及SceneCaptureRendering中的相关函数。

实现仅编辑器可见的摄像机Mesh

为了用户(还有自己)使用组件时能方便知道摄像机的指向,为CustomRenderComponent增加一个摄像机的Mesh。首先增加相关EditorOnlyData数据,包括相关的StaticMesh资产和用于挂载的StaticMeshComponent。

#if WITH_EDITORONLY_DATA
	/** The mesh used by ProxyMeshComponent */
	UPROPERTY(transient)
	TObjectPtr<class UStaticMesh> CaptureMesh;

	/** The mesh to show visually where the camera is placed */
	UPROPERTY(transient)
	TObjectPtr<class UStaticMeshComponent> ProxyMeshComponent;
#endif

并在构造函数中,加载相关的Mesh资源,这里我们使用SceneCaptureComponent的同样资源:"/Engine/EditorMeshes/MatineeCam_SM",注意ConstructorHelpers::FObjectFinder<UStaticMesh>只能在构造函数中使用,如果在后续的环节加载,需要使用LoadObject函数。

UCustomRenderComponent::UCustomRenderComponent()
{
	// 初始化数据部分,已省略
	
#if WITH_EDITORONLY_DATA
	if (!IsRunningCommandlet())
	{
		static ConstructorHelpers::FObjectFinder<UStaticMesh> EditorMesh(TEXT("/Engine/EditorMeshes/MatineeCam_SM"));
		CaptureMesh = EditorMesh.Object;
	}
#endif
}

컴포넌트가 월드에 등록되면(RegisterComponentWithWorld), 컴포넌트의 모델을 커스텀 렌더 컴포넌트에 추가할 수 있습니다. 특히 컴포넌트의 OnRegister 함수가 재정의됩니다:

 

//.h
virtual void OnRegister() override;

//.cpp
void UCustomRenderComponent::OnRegister()
{
#if WITH_EDITORONLY_DATA
	AActor* MyOwner = GetOwner();
	if ((MyOwner != nullptr) && !IsRunningCommandlet())
	{
		if (ProxyMeshComponent == nullptr)
		{
			ProxyMeshComponent = NewObject<UStaticMeshComponent>(MyOwner, NAME_None, RF_Transactional | RF_TextExportTransient);
			ProxyMeshComponent->SetupAttachment(this);
			ProxyMeshComponent->SetIsVisualizationComponent(true);
			ProxyMeshComponent->SetStaticMesh(CaptureMesh);
			ProxyMeshComponent->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
			ProxyMeshComponent->bHiddenInGame = true;
			ProxyMeshComponent->CastShadow = false;
			ProxyMeshComponent->CreationMethod = CreationMethod;
			ProxyMeshComponent->RegisterComponentWithWorld(GetWorld());
		}
	}
#endif
	
	Super::OnRegister();
}

用WITH_EDITORONLY_DATA宏包裹相关的数据和函数,来确保仅在编辑器中生效;同时注意将ProxyMeshComponent的bHiddenInGame设置为true,避免影响编辑器中的预览。
액터가 생성되면 모든 컴포넌트를 월드에 등록해야 컴포넌트가 Tick할 수 있습니다.OnRegister 함수는 ExecuteRegisterEvents() 가 실행될 때 실행됩니다.구체적인 타임스탬프는:

after Scene is set, but before CreateRenderState_Concurrent or OnCreatePhysicsState are called. 씬이 완성된 후, 물리적 상태와 렌더링된 상태를 만들기 전에...

UE의 내부 엔진 소스 코드를 참조할 뿐만 아니라 로딩 타이밍을 위해 OnRegister에서 스태틱 메시를 로드하기로 선택했습니다.또한 생성자에서 위 함수를 직접 사용하는 데 문제가 있어서 OnRegister에서 사용하는 것으로 전환했는데 모든 것이 잘 작동합니다.
이것이 기본적으로 이야기의 끝입니다.

아래 요약 및 목표

이 글에서는 인터랙티브 워터의 뎁스 렌더링 기능을 사용하기 위해 커스텀 렌더 컴포넌트를 커스터마이징하여 평면 클리핑 및 표시만/숨긴 프리미티브를 지원하도록 했으며, 다음 글에서는 인터랙티브 워터에 사용된 원래 씬 캡처 컴포넌트를 이 글에서 정의한 것으로 대체하고 그에 맞게 조정하여 원래 효과를 유지하면서 성능을 개선하도록 하겠습니다.다음 글에서는 이 글에서 정의한 커스텀 렌더 컴포넌트를 사용하여 인터랙티브 워터의 원래 씬 캡처 컴포넌트를 대체하고, 원래 효과를 유지하면서 성능을 개선하기 위해 적절히 조정해 보겠습니다.


원문

https://zhuanlan.zhihu.com/p/689761988?utm_psn=1799042952271638530

zhuanlan.zhihu.com

 

https://zhuanlan.zhihu.com/p/695491062

zhuanlan.zhihu.com