지식의 창고
이번엔 렌더러 자료형인 SDL_Renderer 자료형과 관련된 기능 중 필수적인 것만 설명합니다. 이건 SDL_Window라는 자료형과도 밀접한 관계가 있지만 다음에 설명합니다.
전자는 하드웨어 렌더링, 후자는 소프트웨어 렌더링을 위해 존재합니다. RAM에 존재하는 surface를 쓰는 후자는 SDL 2.0에서 소프트웨어 렌더링만을 쓰겠다는고집스런 사람을 위해 존재합니다. 그러나 SDL_Window 자료형인 window를 쓰는 전자를 쓰는 것이 SDL 2.0을 쓰는 이유이겠죠?
SDL_Window는 SDL_video.h에 관련 기능이 모아져 있기에 다음 게시글에서 필수적인 것만 설명합니다.
renderdriver는 renderoption에서 설정한 그래픽카드 기능을 활용하기 위한 driver입니다. 보통은 -1을 넣어서 자동으로 찾습니다.
renderoption은 다음 옵션들을 비트 OR(|)해서 사용합니다.
SDL_RENDERER_SOFTWARE : CreateSoftwareRenderer의 기능을 사용합니다.
SDL_RENDERER_ACCELERATED : 하드웨어 가속 사용
SDL_RENDERER_PRESENTVSYNC : 수직 동기화 사용
SDL_RENDERER_TARGETTEXTURE : SDL_TEXTUREACCESS_TARGET 옵션을 준 texture를 렌더링
이것말고도 SDL_CreateWindowandRenderer도 있지만 SDL_Window에 대한 설명이 필요하므로 역시 다음 게시글로 넘깁니다.
Renderer의 제거방법은
SDL_DestroyRenderer(renderer);
renderer = NULL;
이것도 프로그램 종료될 때 SDL_DestroyRenderer를 해줘야 합니다.
renderinfo는 특수한 구조체인데 내부에 다음 변수를 포함합니다. GetRendererInfo는 renderinfo를 업데이트하며 다음 멤버를 포함합니다.
const char* name : 렌더러 이름
Uint32 flags : 위의 생성 파트에서 renderoption으로 줬던 옵션들입니다.
Uint32 num_texture_formats : 사용 가능한 texture 픽셀 포맷의 수이고
Uint32[16] texture_formats: 그 사용 가능한 texture 픽셀 포맷들입니다. 보아하니 16개까지 한 렌더러가 픽셀 포맷을 사용하도록 했군요
int max_texture_width, max_texture_height: 최대로 들어갈 수 있는 texture 넓이
순서로 렌더링을 합니다.
RenderCopyEx는 단순히 사각영역 2개를 설정해 확대/축소만을 하여 복사하는 RenderCopy와 달리 텍스처를 회전하거나 뒤집어서 renderer에 픽셀 데이터를 복사할 수 있습니다.
이제 SDL_Window 하나 남았습니다.
이전 글에서 이어집니다.
SDL_Texture 관련 함수입니다. 이곳에는 openGL과 관련된 함수는 적지 않습니다.
첫번째 파라미터인 renderer는 렌더링(화면에 표시)할 렌더러를 넣어주면 됩니다. 보통 창 하나를 생성했을 때 그 창 내부의 화면을 표시해줄 렌더러도 같이 생성해줘야 하고 그에 따라 맞는 렌더러를 넣어주면 됩니다.
두번째 함수는 Surface로부터 texture를 생성하기에 추가 설명은 하지 않겠습니다.
첫번째 함수의 format은 전 글에서 언급한 SDL_PIXELFORMAT_RGB888 같은 열거형 데이터를 넣어주면 됩니다.
그리고 accesstype이 이 함수의 핵심인데, 다음 3가지 경우 중 하나를 넣습니다.
(1) 이미지 뷰어 같이 화면이 자주 바뀌지 않는 프로그램에서 이미지 픽셀 데이터를 저장할 Texture를 만들 경우: SDL_TEXTUREACCESS_STATIC
(2) 게임 같이 자주 픽셀 데이터가 바뀔 수 있는 프로그램에서 스프라이트 등을 저장할 Texture를 만들 경우: SDL_TEXTUREACCESS_STREAMING
(3) renderer-texture 1대1 대응으로 설정하여 렌더러에 직접 그리는 용도로 쓰는 Texture: SDL_TEXTUREACCESS_TARGET
(3)의 경우 SDL_GetRenderTarget과 SDL_SetRenderTarget이 추가로 따라옵니다. 2D 게임을 만들 때 Surface에서 작업한 것을 Texture로 넘긴다면 (2)번을 쓰지만
렌더러에 직접 무언가 그리고자 할 때에는 (3)을 사용합니다. SDL_RenderDraw****이나 SDL_RenderFillRect/SDL_RenderFillRects 등으로 렌더러에 직접 그릴 수 있습니다.
즉 SDL_TEXTUREACCESS_TARGET은 과거 SDL 1.2 시절의 SDL_Surface으로 할 수 있던 화면으로 보이는 픽셀 데이터 직접 수정 방법을 렌더러로 재현할 수 있는 플래그입니다.
4번째와 5번째의 width와 height는 Texture의 넓이로 한번 정하면 제거되기 바꿀 수 없습니다.
targettexture는 픽셀 데이터를 복사할 texture 포인터(SDL_Texture *)입니다. area는 픽셀 데이터를 복사할 영역(SDL_Rect 포인터)인데, NULL로 두면 텍스처 전체 영역에 복사합니다.
그리고 pixeldata는 복사할 픽셀데이터가 저장된 포인터, pitch는 area로 설정된 texture 영역의 한 줄에 든 픽셀 데이터의 바이트 수입니다.
Surface에서 복사해온다면 (surface 변수명)->pixels, (surface 변수명)->pitch를 넣으면 됩니다.
그러나 여기서는 area를 설정한다고 픽셀 데이터로 복사할 Surface의 확대/축소가 되지 않습니다. Surface->Texture로의 픽셀 데이터 복사는 메모리 영역 간의 데이터 복사와 똑같기 때문이죠.
위 함수와 동일한 역할을 하는 다른 코드를 보여드리겠습니다.
//renderer, surface, targettexture 정의/초기화 생략
//surface 조작 생략
void* pixeldata = NULL;
int pitch = 0;
SDL_Rect area = {0, 0, 160, 160}; //x, y, w, h 순서
SDL_LockTexture(targettexture, &area, &pixeldata, &pitch);
memcpy(pixeldata, surface->pixeldata, area.h * pitch);
SDL_UnlockTexture(targettexture);
pixeldata = NULL;
pitch = 0;
SDL_LockTexture는 texture를 쓰기 전용으로 바꾸며 area에 설정된 영역에 따라 pixeldata, pitch를 texture의 해당 영역의 픽셀 데이터 포인터, 그에 따른 피치값으로 바꿔줍니다.
이 상태에서 메모리 간의 데이터 복사를 수행하고 Unlock으로 texture를 다시 읽기/쓰기 겸용으로 바꿉니다. 그리고 다시 쓸일 없는 pixeldata와 pitch를 NULL과 0으로 막습니다.
메모리 복사 과정에서 아무런 메모리 영역 재할당과 그에 따른 값 재설정 과정이 없기에 SDL_BlitSurface와 달리 SDL_UpdateTexture에서 Surface->Texture 사이에서의 픽셀데이터 확대/축소는 불가능합니다.
덤으로 픽셀 데이터가 담긴 메모리 영역을 일일이 통째로 복사하는 특성상 SDL_UpdateTexture는 퍼포먼스가 느릴 수밖에 없는 함수이기에 자주 불러서는 안됩니다.
만일 스프라이트 방식으로 표시한 이미지가 있다면 차라리 Texture에 이미지 데이터 전체를 준 뒤 Renderer로 옮길 때 다음 게시글에 적을 SDL_Rendercopy 같은 걸 쓰는 게 낫습니다.
여담으로 SDL_UpdateYUVTexture도 있으나 이건 RGB 포맷이 아닌 YUV 포맷을 쓸 때(옛날 TV의 화면 주사 방식)에나 쓰는 거라 요즘은 쓸일이 없으므로 제외합니다.
texture는 texture 포인터(SDL_Texture *)이고 format, accesstype, width, height의 메모리 주소를 넣으면
format에 targettexture의 PixelFormatenum 값(예: SDL_PIXELFORMAT_RGBA8888),
accessible에 targettexture의 SDL_TEXTUREACCESS_STATIC/SDL_TEXTUREACCESS_STREAMING/SDL_TEXTUREACCESS_TARGET 중 하나,
width와 height에 targettexture의 넓이가 나옵니다.
texture에도 투명도와 겹치기 설정을 할 수 있습니다. Surface 버전과 다른점은 첫번째 파라미터가 texture의 포인터라는 것입니다.
Texture도 더이상 쓸 일이 없을 때는 제거해줘야 합니다. targettexture 부분에 제거할 texture 포인터를 넣어주면 됩니다. 그런 뒤 NULL값으로 꼭 막으세요.
Texture 관련 함수 중 타 그래픽 라이브러리와 연동되지 않는 부분은 여기가 끝입니다. SDL_Renderer와 관련되어 본격적으로 화면에 표시하는 기능은 다음 게시글에 올립니다.
이 글에선 SDL_Surface와 SDL_Texture, SDL_Renderer 사이에서 픽셀 데이터 교환, 데이터 내부의 정보를 뽑아내는 방법과 Surface->Texture->Renderer로 이어지는 픽셀 데이터 복사를 알아볼 것입니다.
덤으로 이들 자료형 내부의 픽셀 데이터에 관한 정보를 담은 SDL_PixelFormat과 SDL_Palatte도 언급할 것입니다.
여기에는 무려 4개의 헤더(SDL_surface.h, SDL_render.h, SDL_pixels.h, SDL_rect.h)가 관여하기 때문에 이 4개 헤더에 관한 설명 게시글은 4개로 나누겠습니다.
일단 여러분이 비트(bit)와 바이트(byte)를 안다는 전제 하에 pixel data(픽셀 데이터)란 말을 알아야 합니다. 이 픽셀 데이터는 빛의 3원색(빨강, 초록, 파랑)+투명도(Alpha값) 정보를 담은 도트 하나이고 이걸 수백 수천개 뭉쳐서 우리 눈에 보이는 이미지 데이터가 구성됩니다.
문제는 SDL에선 이 픽셀 데이터가 보통 4바이트로 구성되는데, (뒤에서도 반복되는 말이겠지만) 아닐 때도 있다는 게 문제죠. 일단 픽셀 데이터가 무엇인지만 알고 넘어갑시다.
우선 SDL_Surface와 SDL_Texture의 차이를 보겠습니다.
가장 큰 차이점은 SDL_Surface의 경우 일반적인 힙 메모리에 정보가 저장되는 평범한 구조체(Structure)인 반면 SDL_Texture는 그래픽카드가 사용하는 VRAM 영역에 올라가는 통짜 데이터(구조체 아님)라는 것입니다다.
데이터가 저장되는 메모리 장치도 다르고 자료형의 분류도 다르기에 Surface는 Surface 내부에 있는 데이터(예: 픽셀 데이터 집합, Surface의 넓이 등)를 구조체의 멤버로서 직접 불러올 수 있지만 Texture는 그렇지 못하고 반드시 SDL_QueryTexture라는 함수를 이용해 Texture의 정보를 불러와야 합니다
둘째로 SDL_Surface는 이미지 파일의 데이터 로드가 가능하고 SDL_Texture는 그렇지 않습니다. 그래서 이미지 파일을 읽으려면 먼저 SDL_Surface로 읽어온 후 SDL_Texture에 변환/복사해줘야 합니다
셋째로 Surface->Surface의 픽셀 데이터 복사 함수인 SDL_BlitSurface는 Surface 간의 복사 영역을 설정하는 데에 있어 유연함이 있지만 Surface->Texture로의 픽셀 데이터 복사 함수인 SDL_UpdateTexture의 경우 무조건 복사하려는 데이터의 크기가 Texture의 넒이보다 커서는 안되고 반드시 빠져나온 부분 없이 Texture 내부의 영역에만 복사해야 합니다.
그럼 이제 위 세 가지 차이점이 무슨 뜻인지 알기 위해 두개의 게시글에 걸쳐 이 두 자료형을 활용하는 함수들을 비교해봅시다. (1)번 게시글은 SDL_Surface 포인터 변수인 Surface로 할 수 있는 기능들을 설명할 겁니다.
SDL_Surface * targetsurface = SDL_CreateRGBSurface(0, width, height, depth,RedMask,GreenMask,BlueMask, AlphaMask);
Surface의 생성은 메모리에 Surface 데이터 영역을 할당하고 초기화한 뒤 SDL_Surface 포인터 변수에 그 주소를 대입하는 방식입니다.
이 함수는 빈 Surface를 생성하는데 함수의 파라미터를 보자면...
첫번째는 무조건 0입니다. 이게 SDL 1.2에서는 다른 값을 줄 수 있었는데 2.0이 되면서 해당 기능이 depth와 4개의 Mask로 대체되었습니다. 그래서 지금은 무조건 0을 넣습니다.
2번째와 3번째는 Surface의 크기를 결정합니다. 픽셀 데이터들은 일단 넓이부터 지정하고 나중에 화면에 복사할 때에나 위치를 신경쓰게됩니다. 그러므로 일단 너비(width)와 높이(height)를 양의 정수로 설정합시다.
4번째는 depth인데 이게 SDL_Surface의 내부 구조를 이해하는 데 걸림돌이 되는 첫번째 난관입니다.
이 depth는 8을 초과하는 값을 주면 이 함수의 5~8번째 파라미터인 Mask들을 써먹어서 SDL_Surface 내부에 있는 SDL_PixelFormat 구조체의 Rmask, Gmask, Bmask,Amask 변수가 구성되고 3원색(+투명도)의 구조를 지닌 픽셀 데이터가 구성됩니다.
그리고 8 이하의 값을 주면 SDL_PixelFormat 구조체의 SDL_Palatte 자료형의 변수가 할당되어 활성화되고 16색 모드, 256색 모드 등을 사용하며 픽셀 데이터는 0~(2^depth - 1) 내의 정수가 됩니다. 그리고 SDL_Palatte형 변수는 따로 추가적인 설정을 해줘야 색이 찍힙니다
(SDL_Palatte는 실전에서는 쓸 일이 사실상 없는 자료형이니 넘어가겠습니다누가 256색 비트맵 파일을 게임에 써...).
그러므로 SDL_Surface 내부의 SDL_PixelFormat형 변수에 의해 픽셀 데이터의 구조가 결정됩니다.
여기서 Mask들은 32비트 정수로, 예를 들어 Rmask에 원하는 비트들을 1로 설정하면 그 비트들의 위치에 빨간색 밝기 값이 저장됩니다.
Rmask에 0xff000000을 집어넣으면 한 픽셀 데이터의 최상위 8비트에 빨간색 밝기 데이터가 저장됩니다.
Rmask = 0xff000000, Gmask = 0x00ff0000, Bmask = 0x0000ff00, Amask = 0x000000ff를 넣으면 RGBA순으로 8비트씩 데이터가 저장됩니다.
그.러.나.
실제로 SDL_CreateRGBSurface보다는 SDL_CreateRGBSurfaceWithFormat이나 SDL_CreateRGBSurfaceFrom, SDL_CreateRGBSurfaceWithFormatFrom을 더 쓸겁니다.
왜냐하면 SDL_CreateRGBSurface는 쓰기 불편하니까요. 특히 누가 Mask를 일일이 설정하겠습니까.
위 함수를 먼저 설명한 것은 사실 depth의 존재 이유를 설명하기 위해 먼저 설명했습니다.
그러나 이건 더 난관입니다. 유일하게 SDL_CreateRGBSurface보다 더 쓰기 쉽다는 SDL_CreateRGBSurfaceWithFormat을 볼까요.(다른 2개는 좀 뒤에 설명하겠습니다)
SDL_Surface * targetsurface = SDL_CreateRGBSurface(0, width, height, depth, format);
depth까지는 똑같습니다. 문제는 format인데 이게 Uint32형입니다. SDL_PixelFormat형이 아닙니다. 그렇다면 무슨 수치를 넣어야 할까요?
여기에는 SDL_PixelFormatEnum이라는 열거형 데이터 집합의 값이 들어갑니다. 그럼 들어갈 수 있는 값을 볼까요?
SDL_PIXELFORMAT_UNKNOWN
SDL_PIXELFORMAT_INDEX1LSB
SDL_PIXELFORMAT_INDEX1MSB
SDL_PIXELFORMAT_INDEX4LSB
SDL_PIXELFORMAT_INDEX4MSB
SDL_PIXELFORMAT_INDEX8
SDL_PIXELFORMAT_RGB332
SDL_PIXELFORMAT_RGB444
SDL_PIXELFORMAT_RGB555
SDL_PIXELFORMAT_BGR555
SDL_PIXELFORMAT_ARGB4444
SDL_PIXELFORMAT_RGBA4444
SDL_PIXELFORMAT_ABGR4444
SDL_PIXELFORMAT_BGRA4444
SDL_PIXELFORMAT_ARGB1555
SDL_PIXELFORMAT_RGBA5551
SDL_PIXELFORMAT_ABGR1555
SDL_PIXELFORMAT_BGRA5551
SDL_PIXELFORMAT_RGB565
SDL_PIXELFORMAT_BGR565
SDL_PIXELFORMAT_RGB24
SDL_PIXELFORMAT_BGR24
SDL_PIXELFORMAT_RGB888
SDL_PIXELFORMAT_RGBX8888
SDL_PIXELFORMAT_BGR888
SDL_PIXELFORMAT_BGRX8888
SDL_PIXELFORMAT_ARGB8888
SDL_PIXELFORMAT_RGBA8888
SDL_PIXELFORMAT_ABGR8888
SDL_PIXELFORMAT_BGRA8888
SDL_PIXELFORMAT_ARGB2101010
SDL_PIXELFORMAT_RGBA32
SDL_PIXELFORMAT_ARGB32
SDL_PIXELFORMAT_BGRA32
SDL_PIXELFORMAT_ABGR32
SDL_PIXELFORMAT_YV12
SDL_PIXELFORMAT_IYUV
SDL_PIXELFORMAT_YUY2
SDL_PIXELFORMAT_UYVY
SDL_PIXELFORMAT_YVYU
SDL_PIXELFORMAT_NV12
SDL_PIXELFORMAT_NV21
WTF
네. 이쯤에서 SDL을 파는 것은 정신건강에 좋지 않다는 것을 느끼실 겁니다. 저는 이미 맛이 갔으니까 이런 걸 적고 있죠.
그러나 실제로 평범한 2D게임을 평범한 이미지 데이터를 이용해 만든다면 SDL_PIXELFORMAT_ARGB8888, SDL_PIXELFORMAT_RGBA8888, SDL_PIXELFORMAT_RGB888 정도만 기억해서 CreateRGBSurfacewithFormat을 쓰면 될겁니다. 이것들이 주로 나옵니다. 물론 이미지 데이터를 3종류로 구분하는 시점에서 골치아픈 한 가지 의문이 들죠.
'내가 화면에 그리고자 하는 Surface의 픽셀포맷과 이미지 파일의 픽셀 포맷이 다르면 어쩌지?'
SDL_ConvertSurface로 한 Surface의 포맷을 원하는 포맷으로 변환시킨 새 Surface를 생성합니다.
SDL_Surface * newSurface = SDL_ConvertSurface(oldSurface, format, 0);
여기서 newSurface는 초기화되지 않은 상태에서 SDL_ConvertSurface를 받아야합니다. 즉 이미 newSurface가 CreateRGBSurface 등으로 생성이 된 상태라면 쓸 수 없고 제거를 한 뒤에야 ConvertSurface를 받습니다. 그럼 oldSurface는 newSurface와 같아선 안된다는 것, 그리고 ConvertSurface도 메모리 할당을 수행한다는 것을 알 수 있습니다.
그러므로 이 함수를 썼다면 후술할 Surface 제거 함수를 쓸 때 oldSurface와 newSurface 둘 다 제거를 해서 힙 영역 해제를 해야 합니다. 안 그러면 엄청난 수준의 메모리 누수가...
이제 이미지 파일을 로드하거나 Surface를 이미지 파일로저장할 건데 SDL은 그 자체로는 비트맵 파일만을 로드/저장할 수 있습니다.
jpg, png 같은 건 SDL_image라는 자매 프로젝트 라이브러리를 받아와야 하는데 헤더별로 설명하는 이 게시글의 특성상 나중에 하겠습니다.
SDL_Surface * imagefile = SDL_LoadBMP("이미지 파일 경로");
이미지 파일 경로를 문자열로 적으면 imagefile에 BMP 이미지 로드가 되고 Surface가 생성됩니다.
SDL_SaveBMP(imagesurface, "파일 경로 및 파일 이름");
생성된 imagesurface의 픽셀 데이터들을 BMP 파일로 저장합니다.
이것 말고 SDL_LoadBMP_RW와 SDL_SaveBMP_RW가 있는데 이건 SDL_RWops라는 자료형을 알아야 하고 이건 별도의 헤더에 정의되어 있으므로 나중에 설명하겠습니다.
(1) 이미 생성된 두개의 Surface 사이의 픽셀 데이터 복사
SDL_BlitSurface(src, srcRect, dst, dstRect)
이 함수에서 src, dst는 SDL_Surface 포인터 변수이고 srcRect, dstRect는 정수(integer)로 사각 영역을 지정하는 자료형인 SDL_Rect 자료형으로 된 변수의 주소를 넣어야 합니다.
즉 다음과 같이 씁니다.
SDL_Surface * src, dst;
SDL_Rect srcRect, dstRect;
// src, dst 생성 과정 생략
srcRect = {0, 0, 160, 160};
dstRect = {10, 40, 80, 80};
SDL_BlitSurface(src, &srcRect, dst, &dstRect);
srcRect와 dstRect에는 NULL값을 넣을 수 있는데, 이러면 각각 src의 픽셀 데이터 전체, dst의 픽셀 데이터 전체를 지정합니다.
물론 위 코드에서 srcRect와 dstRect가 다른 것을 봐도 알겠지만 srcRect와 dstRect는 달라도 됩니다. 즉 이걸로 이미지 확대/축소 등을 할 수 있습니다.
그리고 이런 것도 가능합니다.
dstRect= {-100, -80, 100, 100};
음수를 dstRect의 위치에 넣을 수 있는데 이러면 화면 밖으로 복사 시작 위치가 나갑니다만 여전히 복사가 됩니다! 당연히 화면 상에는 짤려서 나오겠죠?
(2) 한 Surface의 픽셀 데이터를 복사하여 새 Surface 생성
SDL_Surface * newSurface = SDL_CreateSurfaceFrom(oldSurface->pixels, width, height, depth, oldSurface->pitch, Rmask, Gmask, Bmask, Amask);
SDL_Surface * newSurface = SDL_CreateSurfacewithFormatFrom(oldSurface->pixels, width, height, depth, oldSurface->pitch, format);
아까 설명 안했던 이 두 함수가 이런 식으로 새 Surface를 생성합니다. 당연히 힙 영역도 할당됩니다.
다만 width와 height, depth와 Mask/format들은 oldSurface를 생성할 때 넣었던 것을 사용해야 합니다.
width와 height의 경우 oldSurface->w와 oldSurface->h를 사용할 수도 있습니다.
Surface는 CreateRGBSurface나 ConvertSurface나 LoadBMP 같은 이미지 로딩 기능 등을 써서 생성했다면 메모리의 힙 영역에서 메모리 할당을 받기 때문에
한 프로시저가 종료되어 더 이상 Surface를 쓸 이유가 없어지는 시점에 Surface에 있는 힙 영역 해제를 해줘야 합니다.
SDL_FreeSurface로 생성된 Surface를 제거합니다.
SDL_FreeSurface(targetSurface);
targetSurface = NULL;
targetSurface는 해제를 하고자 하는 Surface 변수입니다. 허상 포인터 문제를 막기 위해 NULL값으로 막는 것을 잊지 마세요.
Surface의 픽셀 데이터에 이미 있는 투명도 값뿐만 아니라 Surface 전체에 통째로 투명도를 설정할 수 있습니다.
SDL_SetSurfaceAlphaMod(surface, alphavalue);
Mod 뒤에 e 없습니다
surface는 투명도 설정할 Surface 변수, alphavalue는 투명도로 0에서부터 255(0xff)까지 설정 가능합니다.(0은 완전 투명, 255는 완전 불투명)
또 Surface들 여러개를 화면에 겹칠 때 겹치는 방식을 설정할 수 있습니다.
SDL_SetSurfaceBlendMode(surface, blendmode);
Mod 뒤에 e 있습니다
surface는 겹치기 설정을 할 Surface 변수, blendmode는 겹치는 방식인데 다음과 같이 설정가능합니다.
SDL_BLENDMODE_NONE: 이 Surface를 화면에 뿌릴 때 이전에 깔린 Surface의 픽셀 데이터를 무시하고 새로 깔아버린다.
SDL_BLENDMODE_BLEND: 이 Surface를 화면에 뿌릴 때 이전에 깔린 Surface의 픽셀 데이터와 투명도를 적절히 섞는다.(게임 만들때 추천 옵션)
SDL_BLENDMODE_ADD: 이 Surface를 화면에 뿌릴 때 RGB 값을 계속 더하는 방식으로 뿌린다 -> Surface들을 계속 화면에 겹치면 겹친 부분이 점점 밝아짐
SDL_BLENDMODE_MOD: 이 Surface를 화면에 뿌릴 때 RGB 값을 곱하고 0xffffff(최대값인 흰색)로 나누는 방식을 취한다 -> Surface들을 계속 화면에 겹치면 겹친 부분이 어두워짐
클리핑으로 Surface 픽셀 데이터의 일부만 선택하거나 컬러키로 특정한 1개의 색만 제거하는 작업(제거된 색상은 투명으로 처리->이미지 배경색 제거에 유용)를 수행할 수 있습니다.
SDL_GetClipRect로 클리핑 상태 확인, SDL_SetClipRect로 클리핑 상태 설정
SDL_GetColorKey로 컬러키로 제거된 색상 확인, SDL_SetColorKey로 제거할 색상을 설정합니다.
이중 SDL_SetColorkey의 경우
SDL_SetColorkey(targetSurface, bool, colormap);
의 형식으로 써서 targetSurface의 제거 색상을 정하는데 bool의 경우 SDL_TRUE나 SDL_FALSE로 컬러키 기능을 켜고 끕니다.
그리고 colormap의 경우 Uint32 포인터 변수 내에 전용 데이터 형식이 인코딩된 형태인데
colormap1 = SDL_MapRGB(targetSurface->format, r, g, b); // 투명도 무시
또는
colormap2 = SDL_MapRGBA(targetSurface->format, r, g, b, a); // 투명도 포함
로 써서 targetSurface의 format에 맞추어 제거할 색상의 RGB(A)값을 전용 데이터 형식으로 인코딩합니다.
그리고 SDL_GetColorkey의 경우 매핑된 전용 RGB 데이터 형식을 반환하는데 이건 SDL_MapRGB나 SDL_MapRGBA에서 인코딩한 Uint32 포인터입니다.
그래서 SDL_GetRGB 또는 SDL_GetRGBA로 해석 작업을 해야 합니다.
SDL_GetColorkey(targetSurface, colormap1);
SDL_GetRGB(colormap1, targetSurface->format, R, G, B);
하면 R, G, B에 r, g, b의 값이 나타나고
SDL_GetColorkey(targetSurface, colormap2 );
SDL_GetRGBA(colormap2, targetSurface->format, R, G, B, A);
하면 R, G, B, A에 r, g, b, a의 값이 나타납니다.
그럼 이제 Surface를 화면에 표시하는 일만 남았습니다. 그러나 이건 SDL_Texture로 변환하는 작업이 필요합니다
Texture와 관련된 함수는 다음 게시글에서 설명하겠습니다.
기본적으로 SDL.h에는 다음 두가지 함수만 있다고 봐도 무방합니다.
SDL_Init(어쩌고 저쩌고)
SDL_Quit()
이 두 함수는 SDL로 짠 프로그램의 시작과 끝을 담당하는 함수로 모든 SDL 관련 기능은 전부 이 두 함수 사이에서 이루어져야 합니다.
비디오, 음성 출력, 사용자의 입력 처리, 멀티스레딩 같은 것들도 이 두 함수 사이를 벗어나며 안됩니다.
극단적으로 이것도 엄연히 SDL을 사용한 C++ 프로그램입니다.
#include <SDL.h>
using namespace std;
int main(int argc, char* arg[] ){
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Quit();
return 0; }
물론 실행 결과는 아무것도 안하고 자동으로 종료됩니다.
남은 것은 SDL_Init(어쩌고저쩌고) 의 '어쩌고저쩌고' 부분인데, 여기는 SDL에서 쓰고 싶은 기능(예:화면 표시, 사운드 출력 등)을 선택하는 옵션이지만 그냥 SDL_INIT_EVERYTHING 하나 넣으면 해결됩니다. 물론 다른 것도 넣을 수 있지만 이거 하나로 다 퉁칠수 있습니다.
물론 무엇을 쓸 수 있느냐는 또 알아야하기 때문에 간단하게 리스트를 보여드리겠습니다. 이것들을 비트 OR 연산자 | 로 연결해 겹칠 수 있습니다.
SDL_INIT_TIMER: 타이머를 사용
SDL_INIT_AUDIO: 사운드를 사용
SDL_INIT_VIDEO: 비디오 기능(화면 출력)을 사용
SDL_INIT_JOYSTICK: 조이스틱 사용
SDL_INIT_HAPTIC: 진동 기능 (특히 스마트폰 앱을 SDL로 만들 경우 사용)
SDL_INIT_GAMECONTROLLER: 게임패드 사용
SDL_INIT_EVENTS: 이벤트 사용 - 이건 사용자로부터 키보드/마우스 입력 등을 받을 때 쓰는데 SDL_INIT_VIDEO, SDL_INIT_JOYSTICK, SDL_INIT_GAMECONTROLLER를 썼다면 이걸 추가로 써줄 필요가 없습니다.
SDL_INIT_EVERYTHING: 위에 거 전부 다 사용
그럼 다음에는 무언가를 표시해야겠죠? 그런데 바로 여기서 SDL2의 최대 단점인 복잡함이 드러납니다.
이걸 이해하려면 SDL 1.2와 SDL 2.0의 차이를 알아야 합니다.
원래 SDL 1.2에서 화면 표시는 SDL_Surface라는 자료형이 혼자 담당하고 있었습니다.
이 자료형은 정말 강력해서 창을 소환하고, 이미지를 불러오고, 창 화면에 복사하는게 정말 자유로웠습니다.
그런데 이 SDL 1.2는 하드웨어 단계에서 렌더링(쉽게 말해 그래픽카드의 힘을 빌려 화면을 표시하는 기능)과 창을 여러개 띄우는 기능이 없었습니다.
그래서 SDL 2.0으로 올라오면서 하위호환 따윈 집어치우고 하드웨어 가속을 받기 위해 SDL_Texture와 SDL_Renderer가 도입되고 창을 여러개 띄우기 위해 SDL_Window라는 자료형이 도입되었습니다.그리고 SDL_Surface는 SDL_Texture 때문에 이미지 파일 셔틀이 되었습니다
그래서 창을 띄우고 화면을 표시하기 위해 SDL_Window->SDL_Renderer->SDL_Texture->SDL_Surface라는 미친 4단 구조를 만들어줘야 한다는 문제가 생겼습니다.
다음은 창만 표시하는 Main 함수의 일반적인 구조입니다.
SDL 1.2에서 화면 표시 예제인데 창의 닫기 버튼을 누르면 프로그램이 종료됩니다.
#include <SDL.h>
using namespace std;
int main(int argc, char* arg[] ){
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Surface* ScreenSurface = SDL_SetVideoMode(640,480,32,SDL_HWSURFACE | SDL_DOUBLEBUF);
bool quit = false;
SDL_Event event; // SDL_Event는 사용자 입력이나 프로그램 상태를 확인하는 이벤트 자료형입니다. 이벤트 관련 함수들은 눈 튀어나오게 많으니 다음에 자세히 다루겠습니다
while(!quit){ //!quit으로 하면 여기서만 quit가 반전되기에 프로그램이 막 시작되었을 때는 초기값 false가 반전되어 true가 되므로 루프를 돕니다.
if(SDL_PollEvent(&event)){ //이벤트 확인
switch(event.type){ // 이벤트 조사
case SDL_QUIT: // SDL_QUIT는 프로그램 종료 이벤트입니다. 창의 닫기 (X 모양) 버튼을 눌렀을 때 발생합니다.
quit = true; //루프 탈출
break;
}
}
SDL_Flip(ScreenSurface);
SDL_Delay(10); // 0.01초의 딜레이를 줘서 프로그램이 지나치게 빨리 도는 것을 막아줍니다
}
SDL_FreeSurface( ScreenSurface );
ScreenSurface = NULL;
SDL_Quit();
return 0;
}
이렇게 그나마 간단했던 것이 이 글에서 사용할 SDL 2.0에서는...
#include <SDL.h>
using namespace std;
int main(int argc, char* arg[] ){
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Window* window = SDL_CreateWindow("window example", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, SDL_WINDOW_SHOWN);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED );
SDL_Surface* ScreenSurface = SDL_GetWindowSurface(window);
SDL_Texture* ScreenTexture = SDL_CreateTextureFromSurface(renderer,ScreenSurface);
bool quit = false;
SDL_Event event; // SDL_Event는 사용자 입력이나 프로그램 상태를 확인하는 이벤트 자료형입니다. 이벤트 관련 함수들은 눈 튀어나오게 많으니 다음에 자세히 다루겠습니다
while(!quit){ //!quit으로 하면 여기서만 quit가 반전되기에 프로그램이 막 시작되었을 때는 초기값 false가 반전되어 true가 되므로 루프를 돕니다.
if(SDL_PollEvent(&event)){ //이벤트 확인
switch(event.type){ // 이벤트 조사
case SDL_QUIT: // SDL_QUIT는 프로그램 종료 이벤트입니다. 창의 닫기 (X 모양) 버튼을 눌렀을 때 발생합니다.
quit = true; //루프 탈출
break;
}
}
SDL_RenderClear(renderer);
SDL_UpdateTexture(ScreenTexture, NULL, ScreenSurface->pixels, ScreenSurface->pitch);
SDL_RenderCopyEx(renderer, ScreenTexture, NULL, NULL, 0, NULL, SDL_FLIP_NONE);
SDL_RenderPresent(renderer);
SDL_Delay(1000); // 1초의 딜레이를 줘서 프로그램이 지나치게 빨리 도는 것을 막아줍니다. 1.2버전과 달리 여기서는 딜레이를 길게 줘야 하는데 UpdateTexture 함수가 너무 느리기 때문입니다.
}
SDL_FreeSurface(ScreenSurface);
ScreenSurface = NULL;
SDL_DestroyTexture(ScreenTexture);
ScreenTexture = NULL;
SDL_DestroyRenderer(renderer);
renderer = NULL;
SDL_DestroyWindow(window);
window = NULL;
SDL_Quit();
return 0;
}
코드가 길어졌죠? 그만큼 기능이 많아졌지만 복잡해진 건 어쩔 수 없습니다. 게다가 여기에 드러나지 않은 차이점들도 있습니다. 그건 다음 게시글에서...
<계속 작성됩니다...오류가 있으면 알려주세요>
SDL 2.0 공략 같은 것을 써보도록 하죠.
일단 SDL 2.0은 게임 개발을 타깃으로 만든 C/C++겸용 라이브러리입니다물론 실제로는 C 전용이나 다름없지만.
그래서 이걸로 게임을 만들려면 먼저 라이브러리로 게임 제작에 필요한 구체적인 기능을 지닌 프레임워크를 만든 다음에 프레임워크로 게임을 만드는 번거로운 작업이 필요합니다.
SDL 2.0을 자신의 게임 프로젝트에 어떻게든 써먹으려면 비주얼 스튜디오를 받아서 프로젝트 생성을 해준 뒤에 프로젝트 설정에서 include와 library 디렉터리 추가를 해줘야 합니다.
일단 베이스가 되는 SDL 개발 환경 구성 방법 링크를 소개하겠습니다.
(영문주의!)이 링크로 들어가면 SDL 2.0 설치를 이제는 무려 화석이 되어버린 Visual Studio 2010 버전에서 시도하는 방법이 나와있습니다. 이 링크는 공식 SDL 2.0 Wiki에 있는 튜토리얼인데 VS2010에서 시도하는 것부터 오래된 방법이라는 게 느껴집니다. 게다가 영문이기도 하니 간단하게 번역된 해설을 달겠습니다.
그러면 이제 C++로 main.cpp 생성하고 #include <SDL.h>하면 끝...은 아니고 main 함수를 추가해야 하는데 이건 다음 게시글에서 설명하겠습니다.
위 영문 사이트를 조사하면 SDL2 튜토리얼이 있고 저도 여기서 도움을 많이 받았으며 국내에도 네이버 같은데 검색해보면 초보자들에게 적합하게 눈에 보이는 기능부터 하나하나 설명하는 한글 튜토리얼도 있습니다.
그러나 #include <SDL.h>라는 코드를 보면 알겠지만 이 라이브러리는 C++ 라이브러리의 탈을 쓴 그냥 C 라이브러리입니다! 그래서 메모리 관리가 C#은 고사하고 C++보다도 더 까다로운 Hell of Hell이죠!
그러므로 첫째로, 이 SDL 라이브러리 설명글 시리즈는 C/C++를 알고 읽는다는 전제 하에 작성하며
둘째로 저는 SDL 2.0의 기능 소개를 직접 하지 않고 해당 기능이 프로그램 내부적으로 어떻게 돌아가는지 다소 기술적인 관점에서 설명을 할 겁니다. 그래서 제 SDL 2.0 라이브러리 관련 게시글 다른 튜토리얼과는 다르게 SDL2.h의 내부에 있는 하위 인클루드 파일을 중심으로 설명하겠습니다. 이 인클루드 파일들의 목록은
여기에서 보실 수 있습니다