렌더링 특징
CSEngine은 OpenGL을 기반으로 다양한 종류의 렌더링을 지원합니다.
1. PBR Shading
PBR 쉐이더를 구현한 계기
저는 기본 쉐이더로 PBR(Physically Based Rendering) 쉐이더를 선택했습니다.
이전에는 일반적인 블린-퐁 쉐이더를 사용하고 있었지만, PBR이라는 개념에 흥미를 느껴 PBR 기반 렌더링을 구현하기로 결심했습니다. PBR은 현실적인 재질 표현을 위해 사용되며, 그 결과물은 더 현실적이고 입체적인 렌더링을 제공합니다.
PBR에 대해
PBR을 공부하며 작성한 블로그 글은 제가 PBR을 공부하면서 작성한 내용을 담고 있습니다. 이러한 공부를 통해 PBR 쉐이더를 구현하게 되었고, 특히 금속 재질의 표현이 더 효과적이라는 것을 알게 되었습니다.
PBR은 빛의 반사와 굴절을 물체 표면 특성에 따라 정밀하게 처리하므로, 금속과 같은 광택이 있는 재질을 표현할 때 뛰어난 결과를 얻을 수 있습니다.
공부하며 느꼈던 PBR과 기존 쉐이딩의 차이
PBR로 전환하면서 초기에는 기존의 쉐이딩 방식과 큰 차이를 느끼지 못했습니다. 그러나 IBL(Image-Based Lighting)을 도입하면서 PBR의 강점이 뚜렷하게 드러났습니다.
IBL을 사용하면 물체는 주변 환경의 색상과 조명에 맞게 자연스럽게 표현됩니다. 어떤 물체를 렌더링하더라도 주변 환경에 어울리도록 색상과 빛을 반영하므로 현실적이고 일관된 결과를 얻을 수 있습니다.
이것이 PBR과 IBL의 조합으로 더 현실적이고 멋진 렌더링을 달성한 경험입니다. PBR은 재질의 물리적 특성을 정확하게 표현하고, IBL은 주변 환경과의 상호작용을 반영하여 렌더링을 더 생생하고 매력적으로 만들어줍니다.
2. Grouping All Renderers (Hybrid Rendering)
다양한 렌더링의 불편한 동거
다양한 렌더링은 드로우 콜의 단위까지 서로 다르기 때문에 모순이 발생했습니다.
모든 렌더링 기법을 때려넣었던 과거의 RenderMgr 클래스
처음에는 포워드 렌더링을 구현하는 데 큰 문제가 없어 보였으나, 디퍼드 렌더링을 구현하면서 코드가 혼란스럽게 더러워지기 시작했습니다.
모든 렌더링 기법은 RenderMgr이라는 렌더링 총괄 클래스에 통합되었으며, 그림자 처리를 담당하는 깊이 버퍼는 LightMgr이라는 라이팅 총괄 클래스에 포함되어 있었습니다.
이러한 문제와 함께 다음과 같은 의문이 생겼습니다.
- 디퍼드 렌더링과 포워드 렌더링을 결합한 하이브리드 렌더링을 구현하려면 어떻게 지오메트리 패스를 처리해야 할까?
- 블렌딩 처리된 특수한 포워드 렌더링의 깊이 문제는 어떻게 해결할 수 있을까?
- 그림자 렌더링을 위해 라이팅 코어와 렌더 코어 중 어떤 부분이 관여해야 할까?
모든 이러한 고민의 주요 원인은 서로 다른 렌더링 로직이 선형적인 과정을 거치고 렌더링 단위가 서로 다른 것이었습니다. 이것은 OOP를 준수하며 꾸역꾸역 구현한 방식이 한계에 도달했음을 의미하죠.
따라서 저는 아래의 조건을 충족시키며 클래스를 깔끔하게 개선하기 위해 연구를 진행했습니다.
- 포워드, 디퍼드, 깊이, 포스트 프로세싱 등 다양한 렌더링 기법을 객체화
- 총 6종류(윈도우, 리눅스, android, webgl, macOS, iOS)의 정상적인 구동을 목표
최소한의 인터페이스만 유지하는 Render Group 객체
이런식으로 RenderGroup 클래스를 도입하여, 각각의 렌더링 기법에 따라 로직을 맞춤으로 구현하고 조금의 하드 코딩을 허용하는 방식을 선택했습니다.
여기서 렌더링을 위한 오브젝트들을 각 렌더링 특징에 따라 독립적으로 정의했습니다. 이로써 RenderGroup 클래스는 인터페이스의 역할만 하게 되었으며, 여러 렌더링 기법을 한 번에 처리할 수 있도록 기반을 다시 정립함으로써 렌더링 클래스를 더 깔끔하고 안정적으로 역할 분배할 수 있었습니다.
지금까지의 구현 여정은 블로그 글에서도 확인하실 수 있습니다.
3. Material System
머테리얼의 객체화를 통해 유니티와 언리얼처럼 파일로 저장하고 수정도 가능합니다. 머테리얼은 리소스를 관리하는 ResMgr에 임시로 저장되며, Render Component가 머테리얼을 받아오는 즉시 GPU에 관련 값을 전달하는 실질적인 데이터가 생성됩니다.
이걸 만들면서 PBR기반 쉐이딩은 지원하지만 평범한 블린-퐁 쉐이딩도 지원하게 해야하지 않을까 생각이 들어 계속 고심한 끝에 유니티와 비슷한 머테리얼 형식으로 가자고 마음먹고 위와 같이 구현하였습니다.
4. Custom Framebuffers
프레임버퍼를 객체화하여 언리얼의 렌더 타깃, 유니티의 렌더 텍스쳐를 구현하였습니다.
프레임버퍼를 이용하여 카메라를 통해 따로 렌더링 된 이미지를 텍스쳐로 쉽게 얻어올 수 있습니다. 이를 응용하여 CCTV 같은 효과와 실시간 인게임 리플렉션 맵을 구현할 수 있습니다.
처음으로 제작한 당시엔 위 블로그 게시글과 같이 한 프레임버퍼엔 하나만 렌더링되는 형식으로만 제작하였습니다.
그러나 디퍼드 렌더링을 위해 G버퍼같은 특수 프레임버퍼를 구현해야하기 때문에 다중 렌더 버퍼 및 텍스쳐를 지원하도록 수정을 하였습니다.
크로스 플랫폼 형태로 문제점을 찾던 중 프레임버퍼가 렌더링되지 않는 문제점이 발생하였습니다.
해당 문제점은 ES버전에서 텍스쳐 및 렌더 버퍼의 포맷이 너무 추상적이여서 나타난 문제였습니다.
이를 통해 ES환경에선 상세한 포맷 설정이 필수이고, 상용화된 게임엔진에서 상세한 포맷설정이 무조건적으로 따로 작성된 이유도 알게되었습니다.
다양한 Renderers
CSEngine은 Render Group 클래스에 의해 다음과 같은 렌더러를 구성했습니다.
1. Forward Renderer
드로우콜 호출 방식
저에겐 드로우콜을 어떻게 관리하느냐도 큰 고민 중 하나였습니다. 렌더링엔 레이어라는 개념도 있고, 같은 머테리얼끼리 한번에 렌더링하는 개념도 있었습니다.
먼저 같은 쉐이더 프로그램(머테리얼)끼리 묶어 한번에 드로우콜을 요청하였습니다.
그러나 렌더링 레이어도 생각을 미리 해놔야 UI 렌더링이라던가 Z버퍼가 비활성된 특수 렌더링도 적용할 수 있을 것 같아 나중에 렌더링 레이어도 추가하였습니다.
그렇게 현재 드로우콜이 호출되는 방식은 렌더 레이어 → 머테리얼끼리 드로우콜 호출로 구현되었습니다.
현재 디퍼드 렌더링을 구현하면서 일부 드로우콜 호출 방식이 수정될 예정입니다.
2. Deferred Renderer
디퍼드 렌더링은 위 이미지와 같은 4개의 텍스쳐로 구성된 G버퍼입니다. 이러한 구성을 위해 객체화 된 프레임버퍼를 응용하여 G버퍼 전용 클래스를 제작하고, 이를 통해 서브 프레임버퍼에서도 정상적으로 디퍼드 렌더링이 구현되도록 설정하였습니다.
디퍼드 렌더링과 포워드 렌더링은 깊이값을 통해 자연스럽게 동시에 렌더링되도록 설계를 하였으나 리눅스의 OpenGL Compatibility 프로파일에서 정상적으로 렌더링 되지 않는 문제가 발생하였습니다. 지금까지 알아본 원인은 깊이값을 제대로 받아오지 못하는 것으로 확인했으나 어디서 잘못되었는지는 지금까지도 확인 중에 있습니다.
3. Raymarching Renderer from SDF Voxels
레이마칭을 이용한 SDF로 실시간 렌더링한 리플렉션 맵을 적용한 모습
Raymarching Renderer from SDF Voxels의 로직 흐름도
제가 이전에 기존 레거시 렌더링을 통해 무작정 글로벌 일루미네이션을 위한 프로브 맵을 시도했었습니다.
그리고 대실패로 돌아갔었지만 레이 마칭으로는 충분히 할만해 보인다고 말하며 글을 마쳤습니다.
그리고 2달 동안 SDF를 이용한 글로벌 일루미네이션을 구현하고자 목표를 잡고 구현을 진행했었고, 아직 온전하진 않지만 SDF로 생성한 리플렉션 맵을 적용하는 것 까진 성공적으로 구현하게 되었습니다!🥳🥳
관련 내용은 좀 더 디테일하게 작성해놓은 블로그 글에서 소개해드리겠습니다!
렌더링 로직 다이어그램
해당 렌더링 관련 클래스 다이어그램은 렌더링 파이프라인에 도달하는 방식을 설명합니다.
디퍼드 렌더링과 포워드 렌더링이 적용된 해당 파이프라인은 이와 같은 순서로 돌아갑니다.
클래스와 해당 로직의 전반적인 구조는 소스코드는 아래의 문서에서 확인해주시면 감사하겠습니다.