저번에 버퍼 리팩토링을 통해 GPUBuffer를 만들면서
Texture도 이 구조로 가면 좋을것 같다.라고 생각하고 작업에 착수했는데
막상 보니까 현재 Texture랑 연계되어 있는 클래스가 굉장히 많았고 그 클래스들을 어떻게 바꿀지 살펴보다 보니
현재의 렌더러가 가지는 구조가 마음에 들지 않아서 지금 구축해놓은 렌더링 파이프라인를 갈아엎기로 했다.
근데 이게 Texture가 많은 곳에서 쓰이다 보니 Texture 클래스 구조를 변경하는 순간 다른 객체들에서 연쇄적으로 오류가 발생했다. 그래서 모든 문제들을 해결하다보니 기간이 늘어졌다.
작업한 것들은
- GPUTexture 클래스 구현(+API 백엔드별 구현)
- GPUTexture를 멤버로 가지는 Texture클래스
- Texture 클래스를 상속하는 Texture2D, TextureCube클래스(더이상 백엔드 구현을 가지지 않음)
- Texture가 멤버로 가지던 Sampler를 분리
- Texture가 멤버로 가지던 View를 분리
- TextureView 클래스 구현 (+API 백엔드별 구현)
- RenderPass, Framebuffer 클래스 삭제
- RenderPass, Framebuffer를 사용하던 Swapchain 클래스 수정
- RenderGraph 수정
- 바뀐 구조가 정상동작하도록 만들기
이러한 작업을 거쳤다.
우선 GPUTexture를 GPUBuffer처럼 구현했다.

이런식이고 Texture2D나 TextureCube나 어차피 다 ID3D11Resource, vk::Image, ID3D12Resource등 같은 객체에 담기기 때문에 굳이 각각 백엔드구현을 할 필요가 없다고 생각했다.

대신 기존의 Texture2D나 TextureCube는 이제 GPUTexture를 멤버로 가지는 Texture 클래스를 상속받아서 각자 필요한 기능을 추가로 가지게 했다.

GPUTexture를 보면 TextureDesc를 인자로 받고
TextureCube에서는 Texture2DDesc를 인자로 받는다.
왜 따로 만들었냐하면 TextureDesc는
텍스쳐가 2D인지 Cube인지 2DArray인지 이런 타입을 설정하기 때문에
TextureCube를 만드는 데 type에 Texture3D를 넣어 버린다거나
문제를 사전에 방지해두고 싶었기 때문에 Texture2DDesc를 따로 두고 Create 함수 내부에서 TextureDesc를 완성하는 형태로 바꿨다.
Texture가 멤버로 가지던 Sampler를 분리
그리고 더이상 Texture가 Sampler를 가지지 않는데
Texture가 Sampler를 멤버로 소유하는 구조를 구상한 이유가 Vulkan Tutorial에서 셰이더 리소스를 바인딩할때 CombinedImageSampler라는 타입으로 텍스쳐와 샘플러를 한번에 바인딩 하는 방식으로 만들어서 그걸 보고
"텍스쳐랑 샘플러를 한 클래스로 두면 되겠다."
라고 생각했는데 텍스쳐마다 SetSampler를 호출해줘야 하는점이 매우 불편해서 바꿨다.

셰이더리소스 바인딩할때 텍스쳐랑 샘플러를 각각 인자로 전달하면 훨씬 편한거였다.
Texture가 멤버로 가지던 View를 분리

그리고 Texture가 가지던 SRV,DSV,RTV 등 View객체를 Texture에서 분리했는데
저런식으로 Texture내부의 SRV를 void*로 리턴해주면 바로 ImGui::Image로 쓸 수 있었기 때문에
처음에는 편하게 잘 만들었다고 생각했다.
근데 Texture2D말고 TextureCube에서 문제를 알았다.
처음에는 그냥 환경매핑용 ShaderResourceView(편의상 SRV) 하나만 있으면 되겠지 해서
SRV를 하나 만들어서 멤버변수로 뒀다.
그런데 나중에 IBL만들면서 보니까 PrefilterMap의 x번째 면의 y번째 mipLevel만 따로 SRV로 만들어서
ImGui::image로 나오게 싶다던가 하는 경우가 생겼는데 이때
TextureCube객체에 새로운 SRV 를 멤버로 추가해야되고
TextureCube에 또 다른 용도의 SRV가 필요하면 또 새로운 멤버로 추가해야 되고
딱봐도 굉장히 안좋은 구조같아 보인다는 것을 알 수 있다.
기존의 그래픽스 백엔드 구조를 봐도
ID3D11Texture2D - ShaderResourceView
Image - ImageView
ID3D12Resource - DescriptorHandle
이런 식으로 텍스쳐 객체가 뷰를 가진다거나 하는 일 없이 분리해놨다.
백엔드가 항상 답을 알려주고 있는데 또 잘못된 길을 가서 고생이다.

그래서 Texture에 대해서 용도에 맞는 커버를 씌워주는 역할이 TextureView이지
하위멤버로 가지면 안된다고 생각해서 View 래퍼용 TextureView 클래스를 따로 만들고 백엔드별로 구현해줬다.
GetUIHandle은 Imgui::Image에 사용하려고 만들었고
백엔드별로 다른 객체를 리턴하면서 각 백엔드의 객체(ID3D11ShaderResourceView, vk::DescriptorSet... 등등)을 노출하지 않기 위해서 void*를 리턴하도록 처리했다.
Framebuffer 문제
Framebuffer와 RenderPass를 삭제했는데 일단 Framebuffer는 처음부터 잘못 설계한 객체였다.
ImageBasedLighting(편의상 IBL)를 만드는 방식에서 문제를 처음 인지했다.
LearnOpenGL에서 IBL을 위한 베이킹을 할 때 TextureCube의 각 면을 RenderTargetView(편의상 RTV)으로 만들어서 거기에 렌더링을 해주면 6면을 출력하게된다.
여기서 또 멍청한 이슈가 발생하는데 Framebuffer 클래스를 처음 디자인 할 때는 저런 식으로 TextureCube를 RTV로 만들 생각을 못했다.
"렌더링은 무조건 평면에 하는거니까 Framebuffer는 걍 Texture2D만 들고 있으면 되겠구나"
이런 생각을 했기 때문에 Framebuffer 생성 시 Description을 전달하면
내부적으로 Texture2D를 만들고 그걸 렌더 타겟으로 사용하는 식으로 구현해버렸다.
그래서 막상 LearnOpenGL에서 알려준대로 IBL을 구우려고 보니까
내가 만든 Framebuffer는 Texture2D만 멤버로 가질 수 있는 구조였다.
Framebuffer에 멤버로 TextureCube를 추가할까? 생각해봤는데 딱봐도 잘못된 방향이었고
이때 바로 바꾸는 게 맞았지만 당시에는 너무 하기 싫었기 때문에
일단 Framebuffer구조를 그대로 둔 채로 IBL을 어떻게든 만들어내는 방식을 사용했다.
1. TextureCube의 각 면에 복사하기 위한 6장의 Texture2D Framebuffer을 만든다.
2. 6장의 Framebuffer에 각 면을 그린다.
3. Framebuffer로 출력된 6장을 TextureCube의 각 면에 하나씩 복사
이걸 통해서 TextureCube를 정상적으로 출력했지만 매우 비효율적이었다.
PrefilterTextureCube를 만들기 위해서 mipLevel*6개의 Framebuffer를 생성해야 했다.
그런데 마침 Texture 클래스 리팩토링을 하면서 이 클래스와 연관이 있는 Framebuffer도 에러가 터지길래 이번 기회에 다 지우고 새롭게 구조를 만들었다.
그리고 리팩토링 과정에서 생각해보니 Framebuffer와 RenderPass 객체가 더 이상 필요 없다는걸 알게되었다.
RenderPass의 문제점
Vulkan Tutorial에서 VkRenderPass만들고 그걸로 VkFramebuffer만들어야 렌더링을 할 수 있다고 설명하는데
다른 백엔드들은 그냥 RTV만 설정해주면 되는데
이걸 동일한 방식으로 추상화를 하려면 다른 백엔드들도 RenderPass랑 Framebuffer 클래스를 만들어서 통일해 주는게
추상화 구조를 짜기 편해서 억지로 구조를 짰었다.

Vulkan에서도 자기들 방식이 불편한 걸 인지했는지는 모르겟는데
이전에 내가 vkCmdBindDescriptorSets의 대안으로 찾아낸 vkCmdPushDescriptorSet 함수처럼
RenderTarget을 RenderPass랑 Framebuffer없이 바로 렌더링 할 수 있는 기능이 최신 Vulkan에는 찾아보니 있었다.
vk::RenderingInfo 이 기능을 활용하면 DynamicRendering이라는 기능을 통해서 다른 API처럼 vkImageView만 바인딩 해주는 걸로 렌더링을 할 수 있다. 진짜 왜 API 처음 설계할 때 이렇게 안 만들었는지 모르겠음 ;
Vulkan 공부할 때 여기를 보지 말고
Introduction - Vulkan Tutorial
Read before following this tutorial This tutorial was written shortly after Vulkan was initially released, back in 2016. A lot has changed since then and this tutorial no longer reflects the best way to use Vulkan today. Instead of reading this website, I
vulkan-tutorial.com
여기를 봐야된다 여기는 vulkan.hpp로 설명되어 있긴 한데 위에서 사용한 RenderingInfo같은 편한 거 알려준다.
https://docs.vulkan.org/tutorial/latest/00_Introduction.html
Khronos Vulkan® Tutorial :: Vulkan Documentation Project
We’ll start with an overview of how Vulkan works and the work we’ll have to do to get the first triangle on the screen. The purpose of all the smaller steps will make more sense after you’ve understood their basic role in the whole picture. Next, we
docs.vulkan.org

아무튼 그래서 RenderPass랑 Framebuffer를 더이상 사용할 이유가 없어졌기 때문에 삭제해버리고 RenderingInfo라는 구조체를 추가했다.

이제 그냥 여기 AttachmentDesc에 위에서 만들어준 TextureView를 RenderTargetView로 생성해서 넣어주고 새로 만든 BeginRendering이라는 함수에 전달하면 알아서 렌더타겟으로 바인딩 한다.

세부 구현은 이런식으로 했다. 다른 백엔드도 비슷하다.
RenderingInfo에 들어있는 RTV와 DSV를 바인딩하고 Viewport를 새롭게 설정해주는 흐름으로 구현했다.
다른 백엔드들은 위와 같은 방식으로 구현했지만
OpenGL은 백버퍼가 아닌 다른 Texture를 RenderTarget으로 지정하기 위해서 반드시 glFramebuffer라는 객체를 사용해야 한다. Framebuffer 개념을 이미 없애버렸는데 Framebuffer가 필요하게 된 것이다.

이 문제를 해결하기 위해 OpenGLRenderContext 생성 시점에 framebuffer를 하나 생성하고

BeginRendering이 호출될 때 마다 framebuffer에 바인딩 되는 텍스쳐를 설정해주는 방식으로 문제를 해결했다.
성능 상 문제가 있는지는 아직 잘 모르겠다.
Swapchain 수정
Framebuffer와 RenderPass를 삭제했기 때문에 이제 swapchain 도 변경해줘야 했다.


OpenGL의 구현을 예시로 보면
Framebuffer대신 이제 Texture와 그 Texture의 RenderTargetView를 멤버로 가진다.

OpenGL이나 DX11은 상관없지만 DX12와 Vulkan은 렌더링 스레드 사용 시
이전에 작성한
를 막기 위해서 swapchain 전용 함수를 마찬가지로 따로 만들어 주었다.
여기서 OpenGL로 인한 문제가 있는데
OpenGL은 백버퍼 텍스쳐를 따로 제공하지도 않고 백버퍼를 위한 glFramebuffer도 따로 제공하지 않는다.
어떻게 하냐면
glBindFramebuffer(GL_FRAMEBUFFER, 0);
바인딩 되어있는 Framebuffer를 해제하면 백버퍼에 그리는게 된다.
위의 BeginRendering 함수를 보면 desc에서 swapchain의 TextureView를 가져가서 렌더링에 사용하는 데
OpenGL은 이 텍스쳐를 제공하지 않기 때문에 당연히 넘겨줄 View도 없고
Attachment에 isSwapchain같은 bool 집어넣어서 또 복잡하게 if문 걸고 해야 될 것 같은데 이렇게 하기가 싫었다.


AI가 OpenGL 잡기술을 알려줘서 문제를 해결할 수 있었다.
위의 OpenGLSwapchain 클래스를 보면 blitFBO라는 변수가 있다.
이건 스왑체인 전용 glFramebuffer인데 다른 백엔드들과 똑같은 구조로 Texture, TextureView를 만들어주고
TextureView를 blitFBO에 바인딩해준다.
그리고 아까의 BeginRendering 함수에도 Swapchain에서 만든 TextureView를 전달한다.

그리고 프레임이 끝날 때 이 blitFBO의 내용을 0(백버퍼)로 blit함수를 통해서 그대로 복사해서 Present를 하게 되면
glBindFramebuffer(GL_FRAMEBUFFER, 0)을 한것과 똑같은 결과를 얻을 수 있다.
그래서 결과가 뭐냐??

아직 뷰포트가 고장난 엔진화면이다.
이제 기존에 대충 만들어놨던 렌더그래프를 갈아엎고 뷰포트에 다시 렌더링이 되도록 고치는 것을 목표로 할 것이다.
'개인 공부 및 프로젝트 > Daydream Engine' 카테고리의 다른 글
| 자체 게임엔진 DevLog - 파일 시스템, 렌더러 수정 (0) | 2026.06.01 |
|---|---|
| 자체 게임엔진 DevLog - ImGui Vulkan이 ImageView를 사용하게 하기 (0) | 2026.05.27 |
| 자체 게임엔진 DevLog - 디버깅 자체가 뻗어버리는 문제 (0) | 2026.05.05 |
| 자체 게임엔진 DevLog - RenderGraph 작성 (0) | 2026.04.29 |
| 자체 게임엔진 DevLog - 버퍼 클래스 구조 수정 (0) | 2026.04.29 |