장렬하게 전사했다..
이젠 정말 시간이 없어서 애초에 오브젝트 1000개의 프레임 방어가 주 과제였던지라
오브젝트 material별 인스턴싱을 안해도 프레임 드랍이 없어
타협을 하고 평가준비를 하기로 했다.
너무너무너무 아쉬운데
일단 실패 원인과 과정을 조금 적어보자면..
우선 인스터싱을 고려하지 않고 설계한게 문제이다.
지금 생각해보면 게임 엔진인데
인스터싱을 처음부터 고려하지 않은 내 무지함이 만든 스노우볼이다.
오브젝트가 수천개 떠다닐텐데 그거마다 descriptorset을 만든다는게 말이 안된다.
초창기에는 vulkan 배우기도 바빠 최적화를 전혀 고려하지 않았다는게 이런식의 실패를 만들줄 몰랐다.
현재 우리엔진의 draw콜 구조는 블로그에 적은 적이 없어 잠깐 적어보자면
draw콜이 불릴 때 가장 윗단이
MeshRenedererComponent이다.
엔티티에 들어가는 실질적인 컴포넌트이고 드로우 불릴 때 TransformComponent와 같이 가져온다.
MeshRendererComponent는 core를 구현한 친구(ui 친구)가 만든 컴포넌트이고
실질적인 작업은 이 컴포넌트가 들고있는 RenderingComponent에서 한다.
RenderingComponent는 역시 렌더링 될 오브젝트마다 생기고
이 녀석이 Model, ShaderResourceManager, Material을 들고있는데
각각 살펴본다면
Model은 Mesh들과 Material들을 들고 있다.
이때 Mesh와 Material은 1대 1매칭이 되고
model이 만들어질 때
들어오는 material 이미지들이 없으면 default 이미지를 받아와 생성하고,
들어오는 material 이미지들이 있으면 그 이미지로 material을 생성한다.
이 material은 결국 model에 있어서의 default material이 된다.
model에 있는 material은 만들어지고 변경할 수 없다.
윗단에서 여러놈들이 공유하고 있는 녀석이기 때문에 안의 정보를 함부로 바꾸면 안된다.
ShaderResourceManager는 descirptorset과 uniform버퍼가 있는 내가 따로 묶어놓은 친구인데
처음에는 좋다고 묶어놨는데 점점 해보니깐 묶어놓았으면 안됐다. 이유는 뒤에서 설명하고
이 녀석이 생성될 때는 윗단인 RenderingComponent가 생성될 때이고
이 친구의 주목적은 Model을 보고 descriptorset을 생성하는 녀석이다.
model이 여러 mesh가 있으면 그 mesh마다 material이 달라질 수 있기 때문에
mesh의 개수에 비례하여 descriptorset을 생성해준다. 당연히 이때 material 이미지 넣는다.
이 친구는 RenderingComponent생성될 때마다 생성되므로
object가 많아지면 당연히 그만큼 생긴다.
descriptorset이 많아지는 주 원인이다.
(descirptorset 개수 = object 개수 * 각 object의 mesh 개수 * 작업할 프레임 수 2개)
오브젝트의 material 변경도 이 친구가 하는데
변경하고 싶으면 model을 받아서 그 모델의 default material의 이미지를 받아서 descriptorset을 업데이트 한다.
오브젝트당 descriptorset이 있으니 descirptorset을 update를 해줘도 됐었다.
변경된 material의 주솟값은 윗단인 RenderingComponent가 들고있어
씬을 재로딩할 때 이 Material을 보고 update하여 오브젝트가 같은 model이지만 다른 material을 보일 수 있게 해줬었다.
Material은 그래서 RenderingCompont가 생성될 때 생성되고
그 Model의 default material이 될 수도 있고
update한 다른 model의 material이 될 수도 있는 것이다.
다시 돌아와서
그럼 이제 Renderer에서 MeshRendererComponent들을 순회하며 드로우 호출을 하면
unifom buffer에 박을 정보들과 draw호출하기위한 필요한 vulkan리소스들 싹다 들고
RenderingComponent까지 들어가 descriptorset들 들고 다시
Model까지 들어가서 mesh별로 순회하며 그 mesh에 맞는 descriptorset 바인드 해주면서
uniform buffer를 업데이트 해준다.
그리고 다시 mesh까지 들어가서 draw호출하면
object 마다 draw호출하는 멋진 draw 콜이 된다.. ㅎ
이제 이걸 개선하기위해서는 구조의 변경이 필수적이었는데
우선 descriptorset 생성 시기가 바뀌어야했다.
현재는 object의 MeshRendererComponent가 생성될 때마다 Model을 보며 descriptorset이 생성됐다면
이제는 Model생성시 material을 보고 descriptorset을 생성해줘야 했다.
material의 uniform과 이미지는 공유하고 modol matrix만 storage buffer로 둬서
이 material을 사용하는 mesh별로 model matrix를 정렬하여 집어넣고
descirptorset을 바인딩했을 때
각 mesh별로 draw호출을 한다면
그때는 material개수 * 작업할 프레임 수 2개 = descriptor 개수
로 descriptorset이 획기적으로 줄어들게 된다.
여기서 ShaderResourceManager의 한계가 들어났는데
사실 proj, view matrix는 그 때 한 프레임 작업할 오브젝트의 모든 드로우 호출에 동일할 것이므로
이 uniform버퍼 하나만 생성하며 다른 모든 descriptorset에 바인딩 해줬으면
descriptorset마다 proj, view matrix를 넣어주지 않았어도 됐다.
내가 편의상 묶어버려서 유연하지 못한 구조를 만들어냈다 ㅠㅠ
ShaderResourceManager를 분할해서 descriptorset과 buffer들로 다시 분할해 관리하기에는
지금까지한 작업들을 다 갈아엎어야해서 그럴수는 없었고
쨋든 다시 구조 변경으로 들어와서
material별로 descriptorset을 생성했다면
남은 것은 결국 mesh별로 잘 정렬하여 storage buffer에 model matrix를 잘 넣고
mesh별로 구간 잘 나눠 draw호출만 잘 해주면 끝날 일이었다.
이 정렬이 문제였는데
component 받아오는게 material별로 model별로 정렬되어온다는 보장이 없으니
받아온 component의 mesh마다 material을 확인하고 (어느 material의 descriptorset에 정보를 넣어야할지 확인하고)
그럼 거기에 model matrix를 적재해야하는데
확인하는 시점에는 어느 인덱스에 넣어야할지 모르는 상황이니
(뒤의 컴포넌트가 어느 material의 어느 mesh인지 모르는 상황이라)
잠깐 다른 곳에 저장을 해놨다가
material별 -> mesh별로 저장을 해놓고 정렬을 시킨다음
각 storage buffer에 model matrix를 적재하고
material에 해당하는 descriptorset을 바인딩한 후
mesh별로 draw호출을 하면 됐다.
여기까지는 방법이고 이제 어떻게 구현하는가가 남았는데
특히 이부분
"잠깐 다른 곳에 저장을 해놨다가"에 신경을 많이 써서
나는 std::vector<std::map<uint32_t, std::map<uint32_t, std::vector<alglm::mat4> > > > 를 사용했다.
이게 아마 내 패착요인이 아닌가 싶은데
일단 설명하자면 구조는 복잡해보이지만 간단하다.
각 요소가 [currentFrame][materialId][meshId] 이런식으로 되어있고
여기에 model matrix를 달아주는 형식으로 정렬을 시킨 후
material ID에 해당하는 descriptorset을 바인딩하고
해당 맵에있는 모든 matrix를 storage buffer에 적재 후
mesh ID별로 구간을 나눠 draw호출을 해주었다.
실재로 이렇게 하면 draw콜을 확실히 줄여 렌더링 하는 것을 확인했다만..
descirptorset 개수는 줄어들었지만
기존 1000개의 오브젝트렌더링 할 때 240프레임이 120프레임까지 반토막나는 것을 볼 수 있었다.
예상되는 병목지점은
1. map 두번의 참조면 각 삽입당 O(logN + logN)이고 N번 삽입이면 O(2 * NlogN)-> O(NlogN)이다.
2. alglm::mat4는 1000개 있는데 이게 일단 최소 2번씩 복사가 일어난다.
(transformComponent -> 나의 괴랄한 자료구조 -> storageBuffer)
다시 적어보니깐 이정도는 괜찮은 거 아닌가..? 반토막 날 정도는 아닌거 같은데..
파이프라인 설정을 잘못해줬나..? 또 실수한게 있나..?
다시 한번 도전하고 싶은데 이제 정말 시간이 없다 ㅠㅠ
이제 평가받을 씬을 명세에 맞춰 만들고 다시 평가를 받아야한다.
일단 평가를 받고나서 좀 더 할지 새로운 거를 할지 고민인데
이제는 새로운 거에 좀 더 마음이 가서..
어찌저찌하여 정말 가고싶은 곳에 입사를 하게 되었는데 (인턴이지만)
그곳이 DirectX12를 쓴다고 하길래
이번 프로젝트는 빠르게 갈무리짓고
DirectX 해보면서 못해봤던것들 구현하면서 연습하려고 한다.
그때 인스터싱 고려하면서 구현을 한번 해봐야겠다.
이제 진짜 엔진 마무리다.
씬 만들고 평가받고 온 뒤
엔진 소개를 마무리로 요번 블로그 포스트 마무리 짓겠다!
'Computer Graphics > Vulkan' 카테고리의 다른 글
Vulkan Game Engine - 25. 후기 (0) | 2025.03.26 |
---|---|
Vulkan Game Engine - 23. deferred renderpass의 descriptorset 줄이기 고민 (0) | 2025.03.17 |
Vulkan Game Engine - 22. shadowmap에 storage buffer, push constants 적용 (0) | 2025.03.17 |
Vulkan Game Engine - 21. 엔진 문제와 해결 방안 고민 (최적화) (0) | 2025.03.12 |
Vulkan Game Engine - 20. point light PCF 그림자 수정 (0) | 2025.03.10 |