SDL로 망델브로 집합 그려보기 시리즈
- 1편 – SDL 설치와 망델브로 집합
- 2편 – FPS 제한과 키보드, 마우스 입력
- 3편 – 윈도우 리사이징과 GUI
- 4편 – 스크린샷기능, 비동기 렌더링과 OneTBB 멀티스레드 렌더링
- 5편 – 최적화와 GUI개선
- 6편 – 스레드 선택 기능과 컬러맵 입히기
- 7편 – CUDA 및 멀티 샘플링
2-1. FPS 제한과 루프 시간 관리
먼저 전체 루프에서 FPS를 제한하고 프레임 시간을 관리하는 클래스인 Time 클래스를 만듭니다. 모든 멤버 변수가 static으로 선언되어 있으므로 사용시에 어떤 소스파일이나 헤더파일이든지 Time::fps처럼 클래스 인스턴스를 만들지 않고 사용할 수 있습니다. 하지만 static 멤버 변수의 특성상 하나의 소스파일에서 초기화를 해줘야 합니다. 따라서 매크로를 사용해서 이를 처리하도록 합니다.
#pragma once
#include <thread>
#include <chrono>
class Time {
Time() = delete;
public:
using clock_t = std::chrono::high_resolution_clock;
using time_point_t = clock_t::time_point;
using milliseconds = std::chrono::milliseconds;
static inline void update() {
double delta = elapsed();
int32_t delay = 1e3 * (1. / fps_limit - delta);
if (delay > 0) {
std::this_thread::sleep_for(milliseconds(delay));
delta = elapsed();
}
fps = 1. / delta;
dt = delta;
begin = end;
}
static uint32_t fps_limit;
static double fps;
static double dt;
private:
static inline double elapsed() {
end = clock_t::now();
return (end - begin).count() / 1e9;
}
static time_point_t begin;
static time_point_t end;
};
#ifdef TIME_IMPL
uint32_t Time::fps_limit = 60;
double Time::fps = 0.;
double Time::dt = 0.;
Time::time_point_t Time::begin = Time::clock_t::now();
Time::time_point_t Time::end = Time::clock_t::now();
#endif
C++time.h에서 이미 포함했으므로 main.cpp에서 원래 2번 줄에 있던 #include <thread>를 지웁니다. 아래 처럼 time.h를 포함하기 전에 #define TIME_IMPL을 하면 main.cpp 소스파일에서만 Time 클래스의 static 변수들이 초기화 됩니다(그렇다고 다른 소스파일에서 Time 클래스를 못쓰는 것은 아닙니다).
#include <iostream>
#include <memory>
#include <SDL2/SDL.h>
#define TIME_IMPL
#include "mandelbrot.h"
#include "time.h"
#define INITIAL_WIDTH 680
#define INITIAL_HEIGHT 480
#define FRAME_LIMIT 60
using namespace std;
...
int main(int argc, char* argv[]) {
Time::fps_limit = FRAME_LIMIT;
Init();
...
while (!closed) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
switch (e.type) {
case SDL_QUIT: closed = true; break;
}
}
mandelbrot->draw();
SDL_RenderPresent(renderer);
Time::update();
cout << Time::fps << " " << Time::dt << endl;
}
C++코드를 수정하고 아래 화면과 같이 콘솔에서 각 행에 fps와 각 루프 실행 시간이 표시되면 됩니다. 콘솔 출력의 모든 첫번째 열에서 main.cpp의 11번 줄에서 설정한 FPS 제한 값인 60 언저리의 값이 출력되는 것을 볼 수 있습니다.

2-2. 키 입력 추가 – 이동 및 iteration 조정
다음으로 이동 기능과 iteration 조정 기능을 만들어 보겠습니다. 먼저 mandelbrot.h와 mandelbrot.cpp를 수정합니다. 확대/축소를 위한 getScale() 메서드와 setScale() 메서드도 미리 만듭니다.
class Mandelbrot
{
public:
using real_t = double;
Mandelbrot(SDL_Renderer* renderer);
~Mandelbrot()
void draw();
std::complex<real_t> getPosition() const { return { pos_x, pos_y }; }
void setPosition(real_t x, real_t y);
void move(int32_t rel_px, int32_t rel_py);
inline real_t getScale() const { return scale; };
void setScale(real_t scale);
inline uint32_t getIteration() const { return iter; }
void setIteration(uint32_t iter);
private:
void drawSurface();
setPosition() 메서드는 화면상의 중심이 복소평면의 어디에 위치하게 하라라는 메서드입니다. 반면에 move() 메서드는 화면의 픽셀값을 기준으로 망델브로 집합을 이동시킵니다. set이 붙은 메서드는 멤버 변수 updated를 updated = false로 설정해서 변경사항이 화면에 적용되도록 합니다.
#include "mandelbrot.h"
#include <complex>
using namespace std;
using real_t = Mandelbrot::real_t;
...
void Mandelbrot::draw()
{
if (!updated) {
drawSurface();
SDL_UpdateTexture(texture, NULL, surface->pixels, surface->pitch);
updated = true;
}
SDL_RenderCopy(renderer, texture, nullptr, nullptr);
}
void Mandelbrot::setPosition(real_t x, real_t y)
{
pos_x = x;
pos_y = y;
updated = false;
}
void Mandelbrot::move(int32_t rel_px, int32_t rel_py)
{
real_t dx = 4. * scale * aspect / width;
real_t dy = 4. * scale / height;
pos_x -= dx * rel_px;
pos_y += dy * rel_py;
updated = false;
}
void Mandelbrot::setScale(real_t scale)
{
this->scale = scale;
updated = false;
}
void Mandelbrot::setIteration(uint32_t iter)
{
this->iter = iter;
updated = false;
}
이제 새로운 이벤트를 처리할 수 있도록 main.cpp를 수정합니다. 기존에 main() 안에 있던 switch 문을 EventProc() 함수로 밖으로 빼냅니다. 그리고 비동기 키 입력을 처리하는 EventAsync() 함수와 키 이벤트를 처리하는 KeyProc() 함수를 만듭니다. 키 입력에 이 두 가지를 구분해야 합니다. 키가 눌리는 동안 이동이 되어야 하기 때문에 EventAsync() 함수에서 매 루프마다 키가 눌려있다면 시점을 등속으로 이동합니다. 만약 KeyProc()에서 이를 처리한다면 부드럽게 이동이 안되고 키를 연타해서 이동해야 합니다. EventAsync()함수에서 100 * Time::dt로 된 식을 볼 수 있는데 이는 속력 x 시간으로 fps가 변경되어도 등속으로 이동할 수 있게 해줍니다. 정확하게 따지자면 초당 100픽셀을 이동합니다.
void Init() {
if (SDL_Init(SDL_INIT_VIDEO)) {
cout << "error initializing SDL: " << SDL_GetError() << endl;
exit(-1);
}
}
void Terminate() {
SDL_Quit();
}
void EventAsync(unique_ptr<Mandelbrot>& mandelbrot) {
auto* keyStates = SDL_GetKeyboardState(NULL);
if (keyStates[SDL_SCANCODE_W] || keyStates[SDL_SCANCODE_UP])
mandelbrot->move(0, 100 * Time::dt);
if (keyStates[SDL_SCANCODE_A] || keyStates[SDL_SCANCODE_LEFT])
mandelbrot->move(100 * Time::dt, 0);
if (keyStates[SDL_SCANCODE_S] || keyStates[SDL_SCANCODE_DOWN])
mandelbrot->move(0, -100 * Time::dt);
if (keyStates[SDL_SCANCODE_D] || keyStates[SDL_SCANCODE_RIGHT])
mandelbrot->move(-100 * Time::dt, 0);
}
void KeyProc(SDL_KeyboardEvent& e, unique_ptr<Mandelbrot>& mandelbrot) {
switch (e.keysym.scancode) {
case SDL_SCANCODE_COMMA: {
auto iter = mandelbrot->getIteration();
if (iter > 1)
mandelbrot->setIteration(iter - 1);
break;
}
case SDL_SCANCODE_PERIOD:
auto iter = mandelbrot->getIteration();
mandelbrot->setIteration(iter + 1);
break;
}
}
bool EventProc(unique_ptr<Mandelbrot>& mandelbrot) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
switch (e.type) {
case SDL_QUIT: return true;
case SDL_KEYDOWN:
KeyProc(e.key, mandelbrot);
break;
}
}
EventAsync(mandelbrot);
return false;
}
...
bool closed = false;
while (!closed) {
closed = EventProc(mandelbrot);
mandelbrot->draw();
SDL_RenderPresent(renderer);
Time::update();
cout << Time::fps << " " << Time::dt << endl;
}
이제 프로그램을 release로 빌드하고 실행해 봅니다. 움직이는 이미지라 계산량이 상당하기 때문에 debug로 빌드한다면 프레임이 매우 끊기기 때문입니다. 실행해보면 WASD키나 방향키로 시점을 자유롭게 움직일 수 있습니다. 그리고 키보드의 <, >키로 iteration 값을 조정할 수 있습니다.


2-3. 마우스 입력 추가 – 이동 및 확대/축소
마우스 하나만 있으면 드래그와 스크롤로 이동과 확대/축소를 모두 할 수 있습니다. 이번엔 마우스 입력을 받도록 해봅시다. main.cpp의 EventProc을 다음과 같이 수정합니다.
bool EventProc(unique_ptr<Mandelbrot>& mandelbrot) {
static bool mouse_pressed = false;
SDL_Event e;
while (SDL_PollEvent(&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 (mouse_pressed)
mandelbrot->move(e.motion.xrel, e.motion.yrel);
break;
case SDL_MOUSEWHEEL:
if (e.wheel.y != 0) {
auto scale = mandelbrot->getScale();
scale *= e.wheel.y > 0 ? 1.1 : 1. / 1.1;
mandelbrot->setScale(scale);
}
break;
}
}
EventAsync(mandelbrot);
return false;
}
이제 마우스로 망델브로 집합을 움직일 수 있게 되었습니다. 그리고 확대/축소도 가능한데… 흠… 근데 뭔가 부족해 보입니다. 지금은 화면 중심을 기준으로 하지만, 마우스 커서를 올려놓은 부분을 기준으로 확대/축소할 수 있으면 좋을텐데 말입니다.


여기서 확대/축소 기능을 개선하기 위해 코드를 더 수정합니다. 먼저 mandelbrot.cpp의 #include <complex>를 옮기고 mandelbrot.h에 메서드 두개를 추가합니다.
#pragma once
#include <complex>
#include <SDL2/SDL.h>
inline real_t getScale() const { return scale; }
void setScale(real_t scale);
void setScaleTo(real_t scale, real_t px, real_t py);
inline uint32_t getIteration() const { return iter; }
void setIteration(uint32_t iter);
std::complex<real_t> pixelToComplex(real_t px, real_t py) const;
private:
void drawSurface();
mandelbrot.cpp에 #include <complex>를 없애고 추가한 두 메서드를 구현합니다. setScaleTo() 메서드는 새로운 scale과 화면상의 커서 위치를 인자로 받습니다. 이 함수는 setScale() 함수와는 다르게 커서가 가리키고 있는 복소평면 상의 좌표가 scale을 수정하고도 변하지 않도록 해줍니다. 간단하게 앞의 설명을 그대로 수식으로 옮기고 이항을 하면 그 공식을 구할 수 있습니다. pixelToComplex() 메서드는 화면 상의 커서 위치를 복소평면 상의 좌표로 변환하는 함수입니다. 클래스 외부에서도 사용할 수 있으므로 public으로 선언합니다.
#include "mandelbrot.h"
// #include <complex> -> moved to mandelbrot.h !!
using namespace std;
using real_t = Mandelbrot::real_t;
...
void Mandelbrot::setScaleTo(real_t scale, real_t px, real_t py)
{
auto point = pixelToComplex(px, py);
this->scale = scale;
real_t dx = 4. * scale * aspect / width;
real_t dy = 4. * scale / height;
pos_x = point.real() + 2. * scale * aspect - px * dx;
pos_y = point.imag() - 2. * scale + py * dy;
updated = false;
}
...
std::complex<real_t> Mandelbrot::pixelToComplex(real_t px, real_t py) const
{
real_t min_x = pos_x - 2. * scale * aspect;
real_t max_y = pos_y + 2. * scale;
real_t dx = 4. * scale * aspect / width;
real_t dy = 4. * scale / height;
return { min_x + px * dx, max_y - py * dy };
}
마지막으로 main.cpp에서 EventProc()을 새로 만든 메서드를 사용하도록 변경합니다.
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;
이제 실행시켜보면 마우스 커서를 기준으로 확대/축소하는 것을 확인할 수 있습니다.

이번 편까지 진행한 내용을 바탕으로 파라미터들을 조정해가면서 얻은 이미지입니다.




현재 윈도우는 크기를 조정할 수 없지만, 다음편에서 윈도우 리사이징을 허용하고 유명한 GUI 라이브러리인 ImGui 라이브러리로 GUI를 추가해보겠습니다.
최신 댓글