SDL로 망델브로 집합 그려보기 시리즈 전체 목차
- 1편 – SDL 설치와 망델브로 집합
- 1-1. 들어가며…
- 1-1-1. 망델브로 집합
- 1-1-2. SDL
- 1-2. SDL 설치 및 확인
- 1-3. SDL의 SDL_Surface와 SDL_Texture 그리고 Mandelbrot 클래스
- 1-4. 처음으로 만나는 망델브로 집합
- 1-1. 들어가며…
- 2편 – FPS 제한과 키보드, 마우스 입력
- 2-1. FPS 제한과 루프 시간 관리
- 2-2. 키 입력 추가 – 이동 및 iteration 조정
- 2-3. 마우스 입력 추가 – 이동 및 확대/축소
- 3편 – 윈도우 리사이징과 GUI
- 3-1. 윈도우 리사이징
- 3-2. ImGui 라이브러리로 GUI 만들기
- 3-2-1. ImGui 라이브러리 설치
- 3-2-2. GUI 클래스 만들기
- 4편 – 스크린샷기능, 비동기 렌더링과 OneTBB 멀티스레드 렌더링
- 4-1. 스크린샷 기능 추가
- 4-1-1. stb image 라이브러리 설치
- 4-1-2. GUI 클래스에 스크린샷 기능 추가
- 4-2. 비동기 렌더링 기능 추가
- 4-3. OneTBB 라이브러리를 이용한 멀티스레드 렌더링
- 4-3-1. OneAPI OneTBB 라이브러리 설치
- 4-3-2. MandelbrotTBB 클래스 만들기
- 4-1. 스크린샷 기능 추가
- 5편 – 최적화와 GUI개선
- 5-1. 최적화의 필요성
- 5-1-1. Mandelbrot::setIteration() 메서드 최적화
- 5-1-2. Mandelbrot::setScale() 메서드 최적화
- 5-1-3. Mandelbrot::setScaleTo() 메서드 최적화
- 5-1-4. Mandelbrot::move() 메서드 최적화
- 5-2. GUI 클래스 개선
- 5-1. 최적화의 필요성
- 6편 – 스레드 선택 기능과 컬러맵 입히기
- 6-1. 스레드 개수 선택 기능 추가
- 6-2. 다양한 색상 입히기
- 6-2-1. 컬러맵 입히기
- 6-2-2. 연속적인 색 입히기
- 7편 – CUDA 및 멀티 샘플링
- 7-1. CUDA GPU 가속기능 추가
- 7-1-1. CUDA 설치
- 7-1-2. MandelbrotCUDA 클래스
- 7-1-3. MandelbrotCUDA UI추가
- 7-2. 멀티 샘플링
- 7-2-1. 멀티 샘플링 기능 추가
- 7-1. CUDA GPU 가속기능 추가
1-1. 들어가며…
저번에 콘솔로 망델브로 집합을 그렸었습니다. 이번엔 SDL로 윈도우를 만들어 망델브로 집합을 띄워주는 프로그램을 만들어 보도록 하겠습니다. 이번 시리즈의 각 편에서 사용된 코드는 아래 깃허브 페이지에 전부 올려두었습니다.
https://github.com/dandevlog0206/SDL-Mandelbrot
1-1-1. 망델브로 집합
망델브로 집합은 다음 점화식이 발산하지 않는 복소수 c의 집합입니다. 여기서 발산한다는 것은 n이 커짐에 따라 z의 값이 양의 무한이나 음의 무한으로 한없이 진행한다는 뜻입니다.
컴퓨터에선 n의 값을 무한하게 증가시킨다는 것은 불가능하므로 보통 망델브로 집합을 계산할 때 n을 정해진 최댓값까지 증가시키면서 z의 값이 수렴반경을 벗어나지 않는 c의 집합을 선택합니다. 정해진 최댓값이란 변수인 iteration(반복횟수)으로 망델브로 집합을 정확히 계산할수록 커져야 합니다. 수렴반경이란 복소평면 상에서 z의 값이 수렴할 가능성이 있는 범위를 말합니다. 즉 점화식 계산 중에 z값이 수렴반경을 한번이라도 벗어나면 그 c에서의 점화식은 무조건 발산합니다. 이때 수렴반경은 2로 고정된 값입니다.
아래는 Robert W. Brooks에 의해 최초로 게재된 망델브로 집합의 이미지입니다.

1-1-2. SDL(Simple DirectMedia Layer)
SDL은 C언어로 작성된 크로스 플랫폼 멀티미디어 라이브러리입니다. 크로스 플랫폼이라 함은 플랫폼(리눅스, 윈도우, 맥 등)에 상관없이 공통적인 인터페이스를 가져서 한번 만들어 놓으면 별도의 수정없이 다른 플랫폼에서 작동시킬 수 있다는 뜻입니다. SDL은 윈도우, 렌더링 기능을 내장하고 있어서 간편하게 화면을 만들어서 그래픽 요소를 렌더링 할 수 있습니다. 또한 오디오, 네트워크, 타이머, 스레드, 이벤트 등도 지원합니다.
1-2. SDL 설치 및 확인
이제 본격적으로 프로그램을 만들어 보도록 하겠습니다. SDL 라이브러리는 수동으로 설치할 수도 있지만vcpkg로 다운받을 수도 있습니다. 터미널 창에서 다음 명령어를 사용합니다(환경에 따라 뒤에 오는 x64-windows를 적절히 바꾸시기 바랍니다).
vcpkg install sdl:x64-windows
SDL 라이브러리는 vcpkg로 설치하더라도 SDL2main.lib을 수동으로 링크해줘야 합니다. 비주얼 스튜디오를 사용한다면 메뉴 – 프로젝트 – 프로젝트 속성 – 링커 – 일반 – 추가 라이브러리 디렉토리에 C:\vcpkg\installed\x64-windows\lib\manual-link를 추가해 줍니다(자신이 vcpkg를 설치한 위치에 맞추셔야 합니다). 그리고 프로젝트 속성 – 링커 – 입력 – 추가 종속성에 SDL2main.lib을 추가합니다.
또 SDL이 main을 매크로로 덮어쓰지 않도록 프로젝트 속성 – C/C++ – 전처리기 – 전처리기 정의에 SDL_MAIN_HANDLED을 추가합니다.
아래 코드를 복사 후 붙여넣기해서 그 아래 스크린샷처럼 나오는지 확인하시기 바랍니다.
#include <iostream>
#include <thread>
#include <SDL2/SDL.h>
#define INITIAL_WIDTH 680
#define INITIAL_HEIGHT 480
using namespace std;
void Init() {
if (SDL_Init(SDL_INIT_VIDEO)) {
cout << "error initializing SDL: " << SDL_GetError() << endl;
exit(-1);
}
}
void Terminate() {
SDL_Quit();
}
int main(int argc, char* argv[]) {
Init();
SDL_Window* window = SDL_CreateWindow(
"mandelbrot sample",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
INITIAL_WIDTH,
INITIAL_HEIGHT,
NULL);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
bool closed = false;
while (!closed) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
switch (e.type) {
case SDL_QUIT: closed = true; break;
}
}
this_thread::sleep_for(30ms);
}
SDL_DestroyWindow(window);
SDL_DestroyRenderer(renderer);
Terminate();
return 0;
}
C++
1-3. SDL의 SDL_Surface와 SDL_Texture 그리고 Mandelbrot 클래스
SDL 라이브러리 에는 픽셀 데이터를 위한 자료형이 크게 두가지 있습니다. 하나는 SDL_Surface이고 하나는 SDL_Texture입니다. 간단하게 SDL_Surface는 CPU에서 데이터를 보관하기 위한 자료형이고 SDL_Texture는 GPU에서 데이터를 보관하기 위한 자료형입니다. 일반적인 작업 형태는 SDL_Surface을 하나 만들어서 거기다가 CPU로 소프트웨어 렌더링 작업을 하고, 이를 SDL_Texture를 생성하면서 GPU로 복사해 렌더링 합니다. SDL_Texture는 CPU에서 직접적으로 건드릴 수 없기 때문에 위와 같이 SDL_Surface를 SDL_Texture로 변경해야 합니다.
먼저 망델브로 집합의 여러 파라미터를 관리하면서 렌더링하는 Mandelbrot 클래스를 하나 만들겠습니다. 클래스 내부엔 여러 멤버가 있습니다. 렌더링 된 망델브로 집합의 이미지를 담을 변수인 surface, texture와 SDL 윈도우와 렌더러의 포인터인 window, renderer가 있습니다. 그리고 현재 윈도우의 크기를 담는 width, height 그 종횡비인 aspect가 있습니다. 망델브로 집합과 관련된 파라미터들은 화면에 표시되는 집합 이미지의 중심인 pos_x, pos_y, 그 배율인 scale, 반복 횟수인 iteration이 있습니다. 마지막으로 프레임이 업데이트 되었는지 표시하는 bool 변수인 updated가 있습니다. 추후에 float와 double을 번갈아가면서 실행 속도를 비교할 수 있도록 계산에 사용할 실수 타입을 using을 사용해 타입 앨리어스로 만들어 줍니다. 먼저 헤더파일인 mandelbrot.h는 다음과 같습니다.
#pragma once
#include <SDL2/SDL.h>
class Mandelbrot
{
public:
using real_t = double;
Mandelbrot(SDL_Renderer* renderer);
~Mandelbrot();
void draw();
private:
void drawSurface();
private:
SDL_Window* window;
SDL_Renderer* renderer;
SDL_Surface* surface;
SDL_Texture* texture;
int width;
int height;
real_t aspect;
real_t pos_x;
real_t pos_y;
real_t scale;
uint32_t iter;
bool updated;
};
C++다음으로 클래스를 구현하기 위해 mandelbrot.cpp 소스파일을 만듭니다. make_color() 함수는 8비트 색상을 각각 RGBA로 받아 32비트 색상을 만드는 함수입니다. surface의 픽셀 포맷이 ARGB이기 때문에 이 함수가 필요합니다. drawSurface() 메서드의 이중 반복문 안에서 망델브로 집합을 렌더링 하기 위한 모든 연산이 이루어집니다. 따라서 프로그램 전체에서 가장 최적화되어야 할 부분입니다. 52번 줄은 테스트 이미지를 출력하기 위한 용도이므로 나중에 지울겁니다.
#include "mandelbrot.h"
using namespace std;
inline uint32_t make_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xff) {
return (a << 24) + (r << 16) + (g << 8) + b;
}
Mandelbrot::Mandelbrot(SDL_Renderer* renderer)
: renderer(renderer)
{
window = SDL_RenderGetWindow(renderer);
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);
pos_x = 0.;
pos_y = 0.;
scale = 1.;
iter = 10;
updated = false;
}
Mandelbrot::~Mandelbrot()
{
SDL_FreeSurface(surface);
SDL_DestroyTexture(texture);
}
void Mandelbrot::draw()
{
if (!updated) {
drawSurface();
SDL_UpdateTexture(texture, NULL, surface->pixels, surface->pitch);
updated = true;
}
SDL_RenderCopy(renderer, texture, nullptr, nullptr);
}
void Mandelbrot::drawSurface()
{
for (int h = 0; h < height; ++h) {
for (int w = 0; w < width; ++w) {
uint32_t& pixel = *((uint32_t*)surface->pixels + h * surface->w + w);
// for test
pixel = make_color(h % 255, w % 255, h % 255);
}
}
}
C++마지막으로 Mandelbrot 클래스를 사용하기 위해 main.cpp를 수정합니다. 이번 편의 튜토리얼에선 싱글 스레드만을 이용해 이미지를 계산하지만 후편에서 멀티스레드 렌더링을 만들어 볼 것입니다. 나중에 Mandelbrot 클래스의 멤버를 가상함수로 만들고 그것을 상속받는 클래스를 만들어 다형성을 활용할 것이기 때문에 스마트 포인터인 std::unique_ptr을 사용합니다.
#include <iostream>
#include <thread>
#include <memory>
#include <SDL2/SDL.h>
#include "mandelbrot.h"
...
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
auto mandelbrot = make_unique<Mandelbrot>(renderer);
bool closed = false;
while (!closed) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
switch (e.type) {
case SDL_QUIT: closed = true; break;
}
}
mandelbrot->draw();
SDL_RenderPresent(renderer);
this_thread::sleep_for(30ms);
}
mandelbrot.reset();
정상적으로 수정이 되었다면 빌드 후 아래와 같은 화면이 나옵니다. 정확하게 아래와 일치하도록 화면 색상과 방향에 주의해 주시기 바랍니다.

1-4. 처음으로 만나는 망델브로 집합
드디어 망델브로 집합의 이미지를 그리기 위한 준비가 끝났습니다… 마지막으로 실제 망델브로 집합을 그릴 수 있도록 mandelbrot.cpp 소스파일을 수정하도록 하겠습니다.
다행히 C++ STL에 복소수 라이브러리가 존재하기 때문에 <complex>를 포함해서 복소수를 사용할 수 있습니다. 사실 없어도 공식이 크게 복잡하지 않기 때문에 쉽게 만들 순 있습니다. mandelbrot() 함수는 위에서 소개한 망델브로 집합의 점화식을 계산해서 몇 번째의 반복에서 점화식이 발산했는지 리턴하는 함수입니다. 프로그램 전체를 통틀어 가장 많이 실행되는 함수이기 때문에 inline으로 선언했습니다. 아까 망델브로 집합의 수렴반경이 2라고 했으므로 12번 줄에 들어가야 하는 식은 원래 abs(z) > 2.입니다. 복소수의 절대값인 abs()는 복소평면에서 원점과의 거리로 정의되므로 제곱근 연산이 필요합니다. 하지만 제곱근 연산은 비용이 꽤 큰 함수이므로 양 변을 제곱해서 제곱근을 없애버립니다. 결론적으로 abs() 함수는 절대값의 제곱을 반환하는 norm() 함수로 대체되고 비교하는 값은 수렴반경인 2의 제곱인 4가 됩니다.
여담으로 위키피디아의 문서에서 Optimized escape time algorithms항목을 보면 점화식을 좀 더 빠르게 계산하는 방법을 소개하고 있습니다. 내용인즉슨 기존에 변수간 5번이 필요한 곱셈을 3번으로 줄여주기 때문에 더 빠르다라는 겁니다. 정수에선 곱셈이 덧셈보다 몇 배는 느리다고 하니 그럴듯해서 바로 벤치마킹을 돌려보니… 충격적이게도 위키피디아의 방법이 기존보다 30%정도 더 느렸습니다. 더 찾아보니 요즘 프로세서의 경우에 실수 덧셈과 곱셈은 오버헤드가 거의 비슷했습니다. 그런데 위 방법이 곱셈을 2개 줄이고 덧셈을 더 늘리니 더 느려질수 밖에 없었던 것입니다.
drawSurface()에선 우선 화면상에서 계산해야 할 픽셀의 좌표를 복소평면 상의 실수값으로 변환합니다. scale이 1일 때 화면의 높이가 복소평면 상에서 4가 되도록 합니다(수렴 반경이 2이기 때문입니다). 61, 62번 줄에서 0.5를 픽셀 좌표에 더하는 이유는 픽셀의 가운데를 기준으로 샘플링을 하기 때문입니다. 66, 67번 줄에선 계산된 iterated가 iter값과 같다면, 다시 말해 발산하지 않았다면 픽셀을 흰색으로 칠하고 아니라면 검은색으로 칠합니다.
#include "mandelbrot.h"
#include <complex>
using namespace std;
template <class T>
inline uint32_t mandelbrot(complex<T> c, uint32_t max_iter) {
complex<T> z;
for (int i = 0; i < max_iter; ++i) {
z = z * z + c;
if (norm(z) > 4.) return i;
}
return max_iter;
}
...
void Mandelbrot::drawSurface()
{
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;
for (int h = 0; h < height; ++h) {
for (int w = 0; w < width; ++w) {
uint32_t& pixel = *((uint32_t*)surface->pixels + h * surface->w + w);
real_t cx = min_x + dx * (w + 0.5f);
real_t cy = max_y - dy * (h + 0.5f);
auto iterated = mandelbrot<real_t>({ cx, cy }, iter);
if (iterated == iter) pixel = 0xffffffff;
else pixel = 0xff000000;
}
}
}
여기까지 잘 따라오셨다면 프로그램을 실행했을 때 아래와 같은 영롱한 망델브로 집합의 자태를 몸소 확인하실 수 있습니다.

여기서 종료된 반복 횟수에 따라 색을 다르게 입히기 위해서 drawSurface() 메서드를 살짝 수정합니다.
void Mandelbrot::drawSurface()
{
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;
for (int h = 0; h < height; ++h) {
for (int w = 0; w < width; ++w) {
uint32_t& pixel = *((uint32_t*)surface->pixels + h * surface->w + w);
real_t cx = min_x + dx * (w + 0.5f);
real_t cy = max_y - dy * (h + 0.5f);
auto iterated = mandelbrot<real_t>({ cx, cy }, iter);
uint8_t col = 255.99f * iterated / iter;
pixel = make_color(col, col, col);
}
}
}
그럼 아래와 같은 이미지를 얻을 수 있습니다.

mandelbrot.cpp의 35번 줄에 있는 iter 값을 변경한다면 다양한 이미지를 얻을 수 있습니다. 이 해상도와 배율에선 32보다 큰 값은 별 의미가 없기 때문에 그 아래에서 원하는 양의 정수를 입력하면 됩니다.
pos_x = 0.;
pos_y = 0.;
scale = 1.;
iter = 10;
updated = false;






다음 편에선 fps 제한, 이동, 확대/축소, iter 조정 기능을 만들어보도록 하겠습니다.
최신 댓글