Computer Graphics/Vulkan

Vulkan - 2. Scene 구성

surkim 2024. 12. 20. 21:19

오랜만에 블로그 글을 쓰는것 같다

너무 정신없이 코드를 짜다보니 글을 쓸 정신력이 없었다.

 

일단 얼추 구상은 맞췄고 하나하나 왜 이렇게 짰고 어떻게 짰는지 써보려고 한다.

마지막에는 시간에 쫒겨 너무 대충한 느낌이 나지만..

하면서 또 손볼 것이다.

 

일단 다시 복습

main에 App class가 있고

App에는 Window, Renderer, Scene, Physics class가 있다.

 

Scene class는 말그대로 오브젝트들이 있고 카메라, 빛이 있는 곳이다.

 

Window class는 이벤트를 관리하여 Scene에 있는 Object들에게 영향을 준다.

 

Physics class는 내 동료들이 짜고 있고 Scene에 있는 Object의 위치와 회전에 물리법칙을 적용한다.

 

Renderer class는 vulkan의 모든 인스턴스를 관리하며 Scene의 Object들을 렌더링 해줄 것이다.

 

우선 동료들이 볼 코드는 깔끔하게 짠거 같아서 이건 공개

int main() {
	App app;

	try {
		app.run();
	} catch (const std::exception& e) {
		std::cerr << e.what() << std::endl;
		return EXIT_FAILURE;
	}

	return EXIT_SUCCESS;
}


main 이다.

 

class App {
public:
	void run() {
		window = Window::createWindow();
		renderer = Renderer::createRenderer(window->getWindow());
		scene = Scene::createScene();
		renderer->loadScene(scene.get());

		mainLoop();
		cleanup();
	}

private:
	std::unique_ptr<Window> window;
	std::unique_ptr<Renderer> renderer;
	std::unique_ptr<Scene> scene;
	// physics class


	void mainLoop() {
		while (!glfwWindowShouldClose(window->getWindow())) {
			glfwPollEvents();
			renderer->drawFrame(scene.get());
		}
		vkDeviceWaitIdle(renderer->getDevice());
	}

	void cleanup() {
		scene->cleanup();
		renderer->cleanup();
		window->cleanup();
	}
};

App class는 run할 때 가지고 있는 모든 클래스를 초기화 한 후 main loop에 들어간다.
mainLoop에서 renderer위에 physics가 들어갈 거 같다.

 

그 다음이 오늘의 주인공인 Scene인데 요놈은 쉬울줄 알았는데 만만치 않았다.

우선 인자들과 초기화 부분을 본 뒤,

어디서 만만치 않았는지를 이야기 해보겠다.

 

    std::shared_ptr<Model> m_boxModel;
    std::shared_ptr<Model> m_sphereModel;
    std::shared_ptr<Model> m_planeModel;

    // obj file
    std::shared_ptr<Model> m_vikingModel;
    std::shared_ptr<Model> m_catModel;


    std::shared_ptr<Texture> m_vikingTexture;
    std::shared_ptr<Texture> m_sampleTexture;
    std::shared_ptr<Texture> m_catTexture;
    std::shared_ptr<Texture> m_karinaTexture;

    std::vector< std::shared_ptr<Object> > m_objects;

    float m_cameraPitch { 0.0f };
    float m_cameraYaw { 0.0f };
    glm::vec3 m_cameraFront { glm::vec3(0.0f, 0.0f, -1.0f) };
    glm::vec3 m_cameraPos { glm::vec3(0.0f, 0.0f, 5.0f) };
    glm::vec3 m_cameraUp { glm::vec3(0.0f, 1.0f, 0.0f) };

    glm::vec3 m_lightPos { 0.0f, 10.0f, 0.0f };
    size_t m_objectCount;

우선 Scene에 있는 인자들이다.

 

void initScene() {

    m_boxModel = Model::createBoxModel();
    m_sphereModel = Model::createSphereModel();
    m_planeModel = Model::createPlaneModel();

    m_vikingModel = Model::createModel("models/viking_room.obj");
    m_catModel = Model::createModel("models/cat.obj");

    m_vikingTexture = Texture::createTexture("textures/viking_room.png");
    m_sampleTexture = Texture::createTexture("textures/texture.png");
    m_catTexture = Texture::createTexture("textures/cat.bmp");
    m_karinaTexture = Texture::createTexture("textures/karina.jpg");

    m_objects.push_back(Object::createObject(m_boxModel, m_sampleTexture, 
    glm::vec3(-2.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f, 1.0f, 1.0f)));

    m_objects.push_back(Object::createObject(m_vikingModel, m_vikingTexture, 
    glm::vec3(2.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f, 1.0f, 1.0f)));

    m_objects.push_back(Object::createObject(m_sphereModel, m_vikingTexture, 
    glm::vec3(0.3f, 1.3f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f, 1.0f, 1.0f)));

    m_objects.push_back(Object::createObject(m_planeModel, m_karinaTexture, 
    glm::vec3(0.0f, 0.0f, -3.0f), glm::vec3(0.0f, 0.0f, 180.0f), glm::vec3(5.0f * 0.74f, 5.0f, 1.0f)));

    m_objects.push_back(Object::createObject(m_catModel, m_catTexture, 
    glm::vec3(0.5f, 0.0f, 0.0f), glm::vec3(-90.0f, 0.0f, 45.0f), glm::vec3(0.01f, 0.01f, 0.01f)));


    m_objectCount = m_objects.size();
}

이게 Scene을 초기화 해주는 부분인데 나중에는 동적으로 초기화 될 것을 염두해두고 (게임 엔진이니깐!) 최대한 쉽게 짜보았다.

 

우선 쉐입과 텍스쳐를 전방 선언으로 할당해준뒤 오브젝트를 생성할 때는 이것들을 조합해 위치, 회전, 스케일만 입력해주면 renderer가 알아서 다 해줄 것 이다.

 

model은 기본적으로 mesh가 내장되어있는 plane, sphere, box를 선언할 수 있고 외부의 obj파일을 가져올 수도 있다.

이런식으로 Scene을 초기화 시켜주면

이런식으로 렌더링이 가능하다!!!

 

좀 힘들었던 점을 이야기 해보자면 결국은 OpenGL과 Vulkan의 차이점 때문에 힘들었는데

이게 다 uniform 때문이었다!

 

우선 OpenGL은 uniform버퍼라는 개념이 없고 그냥 보내버리면 됐다.

왜냐하면 상태기반이니깐! 이 유니폼은 반드시 아래의 드로우호출에 쓰이니깐!

 

Vulkan은 당연하게도 이게 아니니깐 uniform 버퍼가 필요했다.

커맨드버퍼로 그리기 작업을 보내기 전까지 uniform버퍼에 각 오브젝트의 uniform을 넣어주는 작업이 필요한 것..

 

그래서 어떻게 해야했냐면 오브젝트의 개수만큼 uniform버퍼를 만들고

동일한 개수만큼 DescriptorSet을 만들고 이 uniform버퍼를 넣어준다. 이때 texture image 역시 넣어주고

 

나중에 그릴 때 이제 이 DescriptorSet을 바인딩하고 uniform버퍼메모리에다 uniform을 계산해 넣어주고 그리기 호출을 반복하면 된다.

 

우선 고민을 많이 했던게 이 uniform버퍼의 관리 주체가 누구일까 고민을 참 많이 했었다.

처음은 object가 uniform버퍼와 DescriptorSet의 관리 주체로 놓을까 고민을 했었는데

잘 생각해보니 문제가 uniform버퍼를 쓸 일이 오브젝트 그릴 때만 있는게 아니라는 점에 있었다.

 

지연 렌더링을 적용할 예정이고 후처리도 해야하는데

그렇다면 오브젝트 뿐만 아니라 여러 셰이더의 유니폼 버퍼도 만들어야하고

차라리 renderer에서 통합관리를 하는게 좋겠다고 생각을 해서 그렇게 짰더니..

const std::vector<std::shared_ptr<Object>>& objects = scene->getObjects();
size_t objectCount = scene->getObjectCount();

for (size_t i = 0; i < objectCount; i++) {
    vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[MAX_FRAMES_IN_FLIGHT * i + currentFrame], 0, nullptr);
    UniformBufferObject ubo{};
    ubo.model = objects[i]->getModelMatrix();
    ubo.view = scene->getViewMatrix();
    ubo.proj = scene->getProjMatrix(swapChainExtent);
    ubo.proj[1][1] *= -1;
    m_uniformBuffers[MAX_FRAMES_IN_FLIGHT * i + currentFrame]->updateUniformBuffer(&ubo, sizeof(ubo));
    objects[i]->draw(commandBuffer);
}

 

일단 관리주체가 object가 아니니깐 index처리가 어지러워진다.

동시에 작업하는 프레임이 최대 2개여서 그만큼 uniform버퍼가 늘어나고

그래서 인덱스 관리가

{object1-1, object1-2, object2-1, object2-2, object3-1, object3-2..}

이런식으로 동작한다.

마찬가지로 DescriptorSet도 이런식으로 관리가 된다.

 

보기에 더럽지만..

나만 안 햇갈리면 된다..

 

심지어 처음에는 { object1-1, object2-1, ...  } 으로 했었는데 이러니 런타임중 object 확장을 생각하니깐 인덱스가 꼬여서 바꿨다.

 

 

결론

Scene클래스를 Object 추가를 용이하게 만들다보니

uniform버퍼와 descriptorSet을 동적으로 생성하고 할당하게 만들어 주느라 힘들었다!

opengl이 더 좋은거 같다..