C++ / SDL로 망델브로 집합 그려보기 – 3

SDL로 망델브로 집합 그려보기 시리즈

3-1. 윈도우 리사이징

지금까진 SDL 윈도우에 크기 변경을 허용하지 않아서 화면 해상도가 항상 고정이었습니다. 이번엔 윈도우에 리사이징 기능을 추가해 보겠습니다. 먼저 mandelbrot.h에 resize() 메서드를 추가합니다. Mandelbrot 클래스 내부에 window 포인터를 저장하고 있기 때문에 변경된 크기를 알 수 있으므로 resize()에 따로 변경된 윈도우 크기는 넘기지 않아도 됩니다.

mandelbrot.h
	Mandelbrot(SDL_Renderer* renderer);
	~Mandelbrot();

	void draw();

	void resize();
	

mandelbrot.cpp에서 resize() 메서드를 구현합니다. 사용중이던 surfce와 texture의 크기가 변하기 때문에 기존에 있던 걸 지우고 새로 할당합니다. 물론 width, height 및 종횡비인 aspect도 다시 설정합니다.

mandelbrot.cpp
void Mandelbrot::draw()
{
	if (!updated) {
		drawSurface();
		SDL_UpdateTexture(texture, NULL, surface->pixels, surface->pitch);
		updated = true;
	}

	SDL_RenderCopy(renderer, texture, nullptr, nullptr);
}

void Mandelbrot::resize()
{
	SDL_FreeSurface(surface);
	SDL_DestroyTexture(texture);

	SDL_GetWindowSize(window, &width, &height);
	aspect = (real_t)width / height;
	
	surface = SDL_CreateRGBSurface(0, width, height, 32, 0, 0, 0, 0);
	texture = SDL_CreateTextureFromSurface(renderer, surface);
	updated = false;
}

main.cpp의 EventProc()에서 SDL_WINDOWEVENT이벤트 처리를 추가합니다. 이 이벤트는 윈도우 리사이즈 뿐만 아니라 윈도우 생성, 파괴, 이동 등 모든 윈도우 관련 이벤트에서 생성됩니다. 때문에 이 이벤트들 중에서 SDL_WINDOWEVENT_RESIZED 항목만을 걸러서 Mandelbrot 클래스에 resize() 메서드를 호출합니다. 그리고 SDL 윈도우를 생성할 때 SDL_WINDOW_RESIZABLE 플래그를 넘겨줘서 화면 크기 변경을 허용합니다. 이제 윈도우의 가장자리에 커서를 올리면 윈도우 크기를 변경할 수 있음을 알 수 있습니다.

main.cpp
		case SDL_MOUSEWHEEL:
			if (e.wheel.y != 0) {
				int px, py;
				SDL_GetMouseState(&px, &py);
				auto scale = mandelbrot->getScale();
				scale *= e.wheel.y > 0 ? 1.1 : 1. / 1.1;
				mandelbrot->setScaleTo(scale, px, py);
			}
			break;
		case SDL_WINDOWEVENT:
			if (e.window.event == SDL_WINDOWEVENT_RESIZED)
				mandelbrot->resize();
			
	SDL_Window* window = SDL_CreateWindow(
		"mandelbrot sample",
		SDL_WINDOWPOS_CENTERED,
		SDL_WINDOWPOS_CENTERED,
		INITIAL_WIDTH, 
		INITIAL_HEIGHT, 
		SDL_WINDOW_RESIZABLE);
	SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

3-2. ImGui 라이브러리로 GUI 만들기

ImGui 라이브러리는 간단한 사용법과 작은 크기로 많은 프로그램에서 사용되는 Immediate mode GUI 라이브러리 입니다(그렇습니다, ImGui가 이것의 약자입니다). 심지어 호환성도 좋아서 많이 쓰이는 그래픽 라이브러리는 대부분 지원합니다(OpenGL, Vulkan, DirectX, Metal, SDL 등). 물론 SDL로 그 중 하나입니다.

3-2-1. ImGui 라이브러리 설치

vcpkg로 imgui 라이브러리를 설치하기 위해 다음 명령어를 실행합니다.

vcpkg install imgui[sdl2-binding,sdl2-renderer-binding]:x64-windows

설치 후 아래 코드를 빌드할 때 혹여나 imgui_impl_sdlrenderer.cpp 파일의 다음 부분에 오류가 생긴다면 아래와 같이 수정해 주시기 바랍니다.

imgui_impl_sdlrenderer.cpp
                // Bind texture, Draw
		SDL_Texture* tex = (SDL_Texture*)pcmd->GetTexID();  
                SDL_RenderGeometryRaw(bd->SDLRenderer, tex,
                    xy, (int)sizeof(ImDrawVert),
                    (SDL_Color*)color, (int)sizeof(ImDrawVert),
                    uv, (int)sizeof(ImDrawVert),
                    cmd_list->VtxBuffer.Size,
                    idx_buffer + pcmd->IdxOffset, pcmd->ElemCount, sizeof(ImDrawIdx));

3-2-2. GUI 클래스 만들기

ImGui 라이브러리를 사용하기 위해 GUI 클래스를 만듭니다. 먼저 gui.h 파일부터 작성합니다. render() 메서드는 내부적으로 버튼 입력과 같은 이벤트도 처리하면서 GUI를 렌더링해서 저장합니다. draw() 메서드는 내부에 저장해놓은 렌더링 데이터를 SDL의 윈도우에 렌더링합니다. 이렇게 구분하는 이유는 시간이 많이 걸리는 망델브로 집합의 연산을 수행시켜놓고 GUI를 render() 메서드로 그려놓은 후 집합의 연산이 끝나면 draw() 메서드로 윈도우에 렌더링하기 위합니다. 이렇게 하면 UI 반응성을 더 끌어올릴 수 있습니다.

gui.h
#pragma once

#include <SDL2/SDL.h>

class Mandelbrot;

class GUI
{
	enum class Acc {
		CPU     = 0,
		CPU_TBB = 1,
	};

public:
	GUI(SDL_Renderer* renderer);
	~GUI();

	void processEvent(SDL_Event* event);
	bool mouseCaptured();

	void render(const Mandelbrot* mandelbrot);
	void draw();

	struct {
		Acc accelerator;
		float move_speed;
		float scroll_scale;
		bool scaleToCursor;
		bool reset_params;
	} settings;

private:
	SDL_Renderer* renderer;
	SDL_Window* window;
};
C++

이제 gui.cpp파일을 만들어 GUI클래스를 구현합니다. 30번 줄에서 폰트를 불러오는데 저는 consola 폰트를 사용하도록 하겠습니다. 윈도우의 경우 기본 폰트 경로에 저 폰트가 기본으로 설치되어 있기 때문에 저 경로를 그대로 입력하시면 consola 폰트를 사용할 수 있습니다. consola 폰트를 사용하는 이유는 글자크기가 같아서 UI가 좀 더 깔끔해 보이기 때문입니다. render() 메서드에서 Mandelbrot 클래스를 받아서 UI를 만드는데 UI를 수정하고 싶으시다면 이 메서드를 수정하시면 됩니다.

gui.cpp
#include "gui.h"

#include <sstream>
#include <imgui_impl_sdl2.h>
#include <imgui_impl_sdlrenderer2.h>

#include "mandelbrot.h"
#include "time.h"

using namespace std;

template <class T>
ostream& operator<<(ostream& os, complex<T> c) {
	os << c.real() << (c.imag() < 0 ? " - " : " + ") << abs(c.imag()) << "i";
	return os;
}

GUI::GUI(SDL_Renderer* renderer)
	: renderer(renderer)
{
	window = SDL_RenderGetWindow(renderer);

	IMGUI_CHECKVERSION();
	ImGui::CreateContext();
	auto& io = ImGui::GetIO();

	ImGui_ImplSDL2_InitForSDLRenderer(window, renderer);
	ImGui_ImplSDLRenderer2_Init(renderer);

	io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\consola.ttf", 15);

	settings.accelerator   = Acc::CPU;
	settings.move_speed    = 300.f;
	settings.scroll_scale  = 1.1f;
	settings.scaleToCursor = true;
}

GUI::~GUI()
{
	ImGui_ImplSDLRenderer2_Shutdown();
	ImGui_ImplSDL2_Shutdown();
	ImGui::DestroyContext();
}

void GUI::processEvent(SDL_Event* event)
{
	ImGui_ImplSDL2_ProcessEvent(event);
}

bool GUI::mouseCaptured()
{
	return ImGui::GetIO().WantCaptureMouse;
}

void GUI::render(const Mandelbrot* mandelbrot)
{
	ImGui_ImplSDLRenderer2_NewFrame();
	ImGui_ImplSDL2_NewFrame(window);
	ImGui::NewFrame();
	ImGui::Begin("control", nullptr, ImGuiWindowFlags_NoResize);

	stringstream ss;
	int px, py, width, height;

	SDL_GetMouseState(&px, &py);
	SDL_GetWindowSize(window, &width, &height);

	ss << "resolution: " << width << "X" << height << "\n";
	ss << "mouse : (" << px << ", " << py << ")\n";
	ss << "fps   : " << round(Time::fps * 10) / 10;
	ss << "(" << round(10000. * Time::dt) / 10. << "ms)\n";
	ss << "cursor: " << mandelbrot->pixelToComplex(px, py) << "\n";
	ss << "pos   : " << mandelbrot->getPosition() << "\n";
	ss << "scale : " << mandelbrot->getScale() << "\n";
	ss << "iter  : " << mandelbrot->getIteration() << "\n";
	ImGui::Text(ss.str().c_str());

	if (ImGui::Button("reset parameters"))
		settings.reset_params = true;

	if (ImGui::TreeNode("more settings...")) {
		static const char* items[] = { "CPU", "CPU - TBB" };
		ImGui::Text("accelerator :");
		ImGui::Combo("", (int*)&settings.accelerator, items, 2);

		ImGui::Text("move speed (pixel/s) :");
		ImGui::SliderFloat("##1", &settings.move_speed, 200.f, 600.f);

		ImGui::Text("scroll scale :");
		ImGui::SliderFloat("##2", &settings.scroll_scale, 1.1f, 10.f, "%f", 32);

		ImGui::Checkbox("scale to cursor", &settings.scaleToCursor);
		ImGui::TreePop();
	}

	auto content_hegiht = ImGui::GetContentRegionAvail().y;
	ImGui::SetWindowSize({ 260, content_hegiht });

	ImGui::End();
	ImGui::Render();
}

void GUI::draw()
{
	ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData());
}
C++

main.cpp를 수정해서 GUI 클래스를 사용할 수 있도록 합니다. 아래와 같이 코드를 수정하면 UI에서 변경한 세팅값을 적용할 수 있습니다.

main.cpp
void EventAsync(unique_ptr<GUI>& gui, unique_ptr<Mandelbrot>& mandelbrot) {
	auto* keyStates = SDL_GetKeyboardState(NULL);
	if (keyStates[SDL_SCANCODE_W] || keyStates[SDL_SCANCODE_UP])
		mandelbrot->move(0, gui->settings.move_speed * Time::dt);
	if (keyStates[SDL_SCANCODE_A] || keyStates[SDL_SCANCODE_LEFT])
		mandelbrot->move(gui->settings.move_speed * Time::dt, 0);
	if (keyStates[SDL_SCANCODE_S] || keyStates[SDL_SCANCODE_DOWN])
		mandelbrot->move(0, -gui->settings.move_speed * Time::dt);
	if (keyStates[SDL_SCANCODE_D] || keyStates[SDL_SCANCODE_RIGHT])
		mandelbrot->move(-gui->settings.move_speed * Time::dt, 0);
}
...
bool EventProc(unique_ptr<GUI>& gui, unique_ptr<Mandelbrot>& mandelbrot) {
	static bool mouse_pressed = false;
	
	SDL_Event e;
	while (SDL_PollEvent(&e)) {
		gui->processEvent(&e);
		switch (e.type) {
		case SDL_QUIT: return true;
		case SDL_KEYDOWN:
			KeyProc(e.key, mandelbrot);
			break;
		case SDL_MOUSEBUTTONDOWN:
		case SDL_MOUSEBUTTONUP:
			if (e.button.button == SDL_BUTTON_LEFT)
				mouse_pressed = e.button.state;
			break;
		case SDL_MOUSEMOTION:
			if (gui->mouseCaptured()) break;
			if (mouse_pressed)
				mandelbrot->move(e.motion.xrel, e.motion.yrel);
			break;
		case SDL_MOUSEWHEEL:
			if (e.wheel.y != 0) {
				auto scale = mandelbrot->getScale();
				if (e.wheel.y > 0) scale *= gui->settings.scroll_scale;
				else scale /= gui->settings.scroll_scale;

				if (gui->settings.scaleToCursor) {
					int px, py;
					SDL_GetMouseState(&px, &py);
					mandelbrot->setScaleTo(scale, px, py);
				} else {
					mandelbrot->setScale(scale);
				}
			}
			break;
		case SDL_WINDOWEVENT:
			if (e.window.event == SDL_WINDOWEVENT_RESIZED)
				mandelbrot->resize();
		}
	}

	EventAsync(gui, mandelbrot);
	return false;
}
...
	unique_ptr<Mandelbrot> mandelbrot = make_unique<Mandelbrot>(renderer);
	unique_ptr<GUI> gui               = make_unique<GUI>(renderer);

	bool closed = false;
	while (!closed) {
		closed = EventProc(gui, mandelbrot);
		
		mandelbrot->draw();
		gui->render(mandelbrot.get());
		
		if (gui->settings.reset_params) {
			mandelbrot->setPosition(0., 0.);
			mandelbrot->setScale(1.);
			mandelbrot->setIteration(32);
			gui->settings.reset_params = false;
		}

		gui->draw();
		SDL_RenderPresent(renderer);

		Time::update();
	}
	
	gui.reset();
	mandelbrot.reset();

이제 코드를 실행시켜보면 깜찍한 GUI를 확인하실 수 있습니다. 아래의 more settings…를 누르면 추가로 설정할 수 있는 항목이 표시됩니다. 근데 화면 해상도가 너무 낮아서 GUI가 너무 공간을 많이 잡아먹는 느낌입니다. main.cpp의 INITIAL_WIDTH와 INITIAL_HEIGHT를 수정해서 해상도를 더 올리도록 합시다.

main.cpp
#define INITIAL_WIDTH  1280
#define INITIAL_HEIGHT 720
#define FRAME_LIMIT 60

이제 좀 더 볼만해 졌습니다.

다음 편에는 스크린샷 기능을 추가하고, TBB 라이브러리로 멀티 스레딩을 지원하도록 만들어 보겠습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다