Console Project/3. 이미지

C++|2019. 12. 10. 13:39

 문자로 이루어진 이미지를 불러오고 화면에 출력하는 방법을 진행한다. 이 프로젝트의 게임의 해상도가 높지 않기 때문에 아스키 아트를 확대하거나 축소하는 변형은 작업하지 않고, 16x16 사이즈의 픽셀 아트 이미지를 구해 이미지와 해상도가 1:1 대응하도록 작업할 것이다.

이미지

 아스키 아트 이미지는 다음과 같이 정의할 수 있다.

class Image {
private:
    TCHAR* image;
    bool* mask;
    int width;
    int height;
    
public:
    inline int GetWidth() { return this->width; }
    inline int GetHeight() { return this->height; }
    inline TCHAR GetPixel(int x, int y) { return this->image[y * this->width + x]; }
    inline bool GetMask(int x, int y) { return this->mask[y * this->width + x]; }
}

 이미지를 생성하는 방법

 이미지를 생성하는 방법은 여러가지가 있는데 그 중 크게 두가지가 있다. 웹에서 Ascii art generator를 통해 이미지를 아스키 아트로 변환한 후, 텍스트 파일로 저장하여 이를 불러오는 방법과 프로그램 내에서 이미지 파일을 텍스트로 직접 변환하는 방법이다.

 이 프로젝트에서는 CImg라는 라이브러리를 사용하여 직접 이미지를 불러와 아스키 아트로 변환하도록 한다.

const TCHAR map[] = TEXT(" .,:;ox%#@");
int mapSize = 10;

// create image from file
CImg<int> temp(filename);
    
int width = temp.width();
int height = temp.height();

CImg<int> grayWeight(width, height, 1, 1, 0),
  imgR(width, height, 1, 3, 0),
  imgG(width, height, 1, 3, 0),
  imgB(width, height, 1, 3, 0);

CImg<int> mask(width, height, 1, 1, 0);

// for all pixels x,y in image
cimg_forXY(temp, x, y) {
  imgR(x, y, 0, 0) = temp(x, y, 0, 0),    // Red component of image sent to imgR
  imgG(x, y, 0, 1) = temp(x, y, 0, 1),    // Green component of image sent to imgG
  imgB(x, y, 0, 2) = temp(x, y, 0, 2);    // Blue component of image sent to imgB

  // Separation of channels
  int R = (int)temp(x, y, 0, 0);
  int G = (int)temp(x, y, 0, 1);
  int B = (int)temp(x, y, 0, 2);

  // Real weighted addition of channels for gray
  int grayValueWeight = (int)(0.299 * R + 0.587 * G + 0.114 * B);

  // saving píxel values into image information
  grayWeight(x, y, 0, 0) = grayValueWeight;
  mask(x, y, 0, 0) = (R == 255 && G == 0 && B == 255) ? 0 : 1;
}

TCHAR *image = new TCHAR[width * height];
bool *m = new bool[width * height];

for (int h = 0; h < height; h++) {
  for (int w = 0; w < width; w++) {
    unsigned char v = grayWeight(w, h, 0, 0);

    image[h * width + w] = map[(v) *mapSize / 256];
    m[h * width + w] = mask(w, h, 0, 0) == 1;
  }
}

return new Image(image, m, width, height);

 이미지를 그레이스케일 이미지로 변환 후, 밝기에 따라 map의 문자를 할당하였다. 색상이 255, 0, 255일 경우 투명 처리 되도록 마스크를 적용하였다.

CImg로 그레이스케일로 변환하는 코드는 http://obsessive-coffee-disorder.com/rgb-to-grayscale-using-cimg/ 참조.

이미지를 그리는 방법

 이미지를 원하는 위치에, 또한 이미지를 자유롭게 잘라 낼 수 있어야 한다. 다만 확대 및 축소는 작업하지 않는다.

void RenderBuffer::Draw(Image* data, const POINT& leftTop, const RECT& imageRect)
{
    const int left = leftTop.x;
    const int top = leftTop.y;
    const int width = min(data->GetWidth(), imageRect.right) - imageRect.left;
    const int height = min(data->GetHeight(), imageRect.bottom) - imageRect.top;

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int sx = x + imageRect.left;
            int sy = y + imageRect.top;
            int dx = x + left;
            int dy = y + top;

            if (dx < 0 || dx >= this->width || dy < 0 || dy >= this->height) {
                continue;
            }

            //int sIndex = sy * data.imageWidth + sx;
            int dIndex = dy * this->width + dx;

            if (!data->GetMask(sx, sy)) {
                continue;
            }

            buffer[dIndex].Char.UnicodeChar = data->GetPixel(sx, sy);
        }
    }
}

잘라낸 이미지의 왼쪽 위를 기준으로 imageRect 범위 만큼 픽셀 하나씩 그려가는 코드이다.

Sprite, TiledSprite

스프라이트는 이미지와 이미지를 그릴 범위 값을 가지고 있는 단순한 클래스이다. TiledSprite도 마찬가지로 이미지를 타일마다 잘라 원하는 타일을 그릴 수 있도록 하는 클래스이다.

class Sprite
{
private:
    Image* image;
    RECT rect;

public:
    Sprite(Image* image)
        :image(image)
    {
        rect = RECT {
            0, 0, image->GetWidth(), image->GetHeight()
        };
    }
    
    Sprite(Image* image, const RECT& rect)
        :image(image)
        ,rect(rect)
    {
    }
    
    void DrawTo(RenderBuffer* buffer, const POINT& pos)
    {
        buffer->Draw(image, pos, rect);
    }
};

class TiledSprite
{
private:
    Image* image;
    int col;
    int row;
    int cellWidth;
    int cellHeight;

public:
    TiledSprite(Image* image, int col, int row)	
        :image(image)
        ,col(col)
        ,row(row)
    {
        this->cellWidth = this->image->GetWidth() / col;
        this->cellHeight = this->image->GetHeight() / row;
    }
    
    void DrawTo(RenderBuffer* buffer, const POINT& pos, int colNum, int rowNum)
    {
        RECT rect = {
            colNum * this->cellWidth,
            rowNum * this->cellHeight,
            (colNum + 1) * this->cellWidth,
            (rowNum + 1) * this->cellHeight
        };

        buffer->Draw(this->image, pos, rect);
    }
};

 

'C++' 카테고리의 다른 글

Console Project/4.5 중간보고  (0) 2019.12.10
Console Project/4. GameLoop  (0) 2019.12.10
Console Project/2. 출력 버퍼  (0) 2019.12.09
Console Project/1. 화면  (0) 2019.12.09
Console Project  (0) 2019.12.09

댓글()

Console Project/2. 출력 버퍼

C++|2019. 12. 9. 19:22

 문자 출력이 가능한 콘솔 창에서 2D 이미지를 출력하기 위해서는 이미지를 ASCII Art로 변환하여 출력해야 한다. 하지만 이미지의 픽셀 개수 만큼의 길이를 가진 문자열을 printf 혹은 std::cout 등으로 출력하는 방식은 대체로 느리다. 여러 이미지를 하나하나 화면에 그리다 보면 그려지는 순서가 보일 지경이 될 것이다.

 이를 개선하기 위해 출력 버퍼에 여러장의 이미지를 그린 후, 버퍼에 바뀜이 있을 때 화면을 갱신(출력)하면 보다 깔끔하게 출력이 가능할 것이다.

출력 버퍼

 ASCII Art는 텍스트로 이루어져 있으므로 char 배열로 buffer를 만든다.

int width = 200;
int height = 200;
char *buffer = new char[width * height];

// clear
for (int i = 0; i < width * height; i++) {
    buffer[i] = ' ';
}

// output to console
gotoXY(0, 0);
for (int i = 0; i < height; i++) {
    char line[width + 1];
    for (int j = 0; j < width; j++) {
        line[j] = buffer[i * width + j];
    }
    line[width] = '/0'; // end of string
    std::cout << line << std::endl;
}

이제 원하는 위치에 이미지를 버퍼에 그려 화면에 출력 할 수 있다. 아래는 이미지를 버퍼에 쓰는 작업을 하는 코드이다.

int posX = 10;
int posY = 25;

int imageWidth = image.width;
int imageHeight = image.height;
char* asciiArt = imageToAscii(image);

// draw image to buffer
for (int y = 0; y < iamgeHeight; y++) {
    for (int x = 0; x < imageWidth; x++) {
    
        // 이미지 픽셀에 대한 버퍼의 위치를 구한다.
        int bufferX = x + posX;
        int bufferY = y + posY;
        
        // 버퍼 밖으로 그리게 될 경우는 무시한다.
        if (bufferX < 0 || bufferX >= bufferWidth || bufferY < 0 || bufferY >= bufferHeight) {
            continue;
        }
        
        // 이미지의 픽셀 위치와 버퍼의 픽셀 위치를 구한다.
        int imageIndex = y * imageWidth + x;
        int bufferIndex = bufferY * bufferWidth + bufferX;
        
        // 이미지의 픽셀을 버퍼의 픽셀에 대입한다.
        buffer[bufferIndex] = asciiArt[imageIndex];
    }
}

// Output to Console
...

WriteConsoleOutput

 위의 코드로 이미지를 그릴 경우 초당 몇 ~ 십몇 프레임이상 그리지 못할 것이다. 이때 빠르게 콘솔에 출력하는 함수가 WriteConsoleOutput이다. 자세한 내용은 링크 참조.

2019/12/09 - [C++] - WriteConsoleOutput - Console 화면에 빠르게 출력하는 함수.

 

WriteConsoleOutput를 사용하려면 CHAR_INFO 배열이 필요한데 버퍼를 아예 CHAR_INFO 배열로 사용할 수 있다.

...

CHAR_INFO* buffer = new CHAR_INFO[width * height];

// clear
for (int i = 0; i < width * height; i++) {
    buffer[i].Char.AsciiChar = ' ';
    buffer[i].Attribute = 7; // white
}

// output to console
COORD pos = { 0, 0 }, size = { width, height };
SMALL_RECT rect = { 0, 0, width, height };
WriteConsoleOutput(GetStdHandle(STD_OUTPUT_HANDLE), buffer, size, pos, &rect);

클래스

기능을 클래스화하여 사용한다. 자세한 내용은 링크.

https://github.com/wakeup5/Console-Project/blob/master/Project/RenderBuffer.h

https://github.com/wakeup5/Console-Project/blob/master/Project/RenderBuffer.cpp

'C++' 카테고리의 다른 글

Console Project/4. GameLoop  (0) 2019.12.10
Console Project/3. 이미지  (0) 2019.12.10
Console Project/1. 화면  (0) 2019.12.09
Console Project  (0) 2019.12.09
WriteConsoleOutput - Console 화면에 빠르게 출력하는 함수.  (0) 2019.12.09

댓글()

Console Project/1. 화면

C++|2019. 12. 9. 18:37

 콘솔창에 ASCII Art를 출력하기 위해서는 적절한 글자 크기와 창 크기가 필요하다. 보다 높은 해상도의 이미지를 그리기 원한다면 가능한 작은 글자 크기로 셋팅해야 할 것이다.

글자 크기 조절

콘솔의 글자 크기는 SetCurrentConsoleFontEx함수를 통해 임의의 크기로 설정할 수 있다. cfi.dwFontSize에 원하는 크기의 값을 입력하면 된다. 체크하면서 폭과 높이가 일치하는 사이즈를 찾으면 된다.

CONSOLE_FONT_INFOEX cfi;
cfi.cbSize = sizeof(cfi);
cfi.nFont = 0;
cfi.dwFontSize.X = 2;
cfi.dwFontSize.Y = 2;
cfi.FontFamily = FF_DONTCARE;
cfi.FontWeight = FW_NORMAL;
wcscpy_s(cfi.FaceName, TEXT("Raster Fonts"));

SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);

화면 크기 조절

 게임의 해상도를 미리 정해 놓고 그에 맞는 화면 크기를 설정한다. 이 프로젝트에서는 200x200으로 한다.

SMALL_RECT windowSize = { 10, 10, 210, 210 };
SetConsoleWindowInfo(GetStdHandle(STD_OUTPUT_HANDLE), TRUE, &windowSize)

타이틀

 게임 타이틀 설정은 다음과 같다.

SetConsoleTitle(TEXT("Console Project"));

'C++' 카테고리의 다른 글

Console Project/3. 이미지  (0) 2019.12.10
Console Project/2. 출력 버퍼  (0) 2019.12.09
Console Project  (0) 2019.12.09
WriteConsoleOutput - Console 화면에 빠르게 출력하는 함수.  (0) 2019.12.09
C++ Image Library - CImg  (0) 2019.12.08

댓글()

Console Project

C++|2019. 12. 9. 17:03

 요즘 유니티로만 개발을 하다보니 C#을 너무 사랑했던 나머지 배웠던 C++을 다 잊어버릴 지경에 다다랐다. 오랜만에 C++도 다뤄보고 학원에 다닐 때 Console용 포트폴리오 게임을 만들었던 기억이 나 Console용으로 가벼운 게임을 하나 만들기로 결심했다.

 다만 텍스트로 표현했던 기존의 콘솔 프로그램 게임 대신 텍스트로 이미지를 표현하여 2D게임을 만들어 보고자 한다.

링크

모든 코드는 https://github.com/wakeup5/Console-Project에서 볼 수 있다.

목차

2019/12/09 - [C++] - Console Project/1. 화면

2019/12/09 - [C++] - Console Project/2. 출력 버퍼

2019/12/10 - [C++] - Console Project/3. 이미지

2019/12/10 - [C++] - Console Project/4. GameLoop

2019/12/10 - [C++] - Console Project/4.5 중간보고

2019/12/10 - [C++] - Console Project/5. 문자 출력

2019/12/12 - [C++] - Console Project/6. 키 입력

2019/12/14 - [C++] - Console Project/7. TileMap

 

개발 하면서 얻은 팁

2019/12/08 - [C++] - C++ Image Library - CImg

2019/12/09 - [C++] - WriteConsoleOutput - Console 화면에 빠르게 출력하는 함수.

'C++' 카테고리의 다른 글

Console Project/3. 이미지  (0) 2019.12.10
Console Project/2. 출력 버퍼  (0) 2019.12.09
Console Project/1. 화면  (0) 2019.12.09
WriteConsoleOutput - Console 화면에 빠르게 출력하는 함수.  (0) 2019.12.09
C++ Image Library - CImg  (0) 2019.12.08

댓글()

WriteConsoleOutput - Console 화면에 빠르게 출력하는 함수.

C++|2019. 12. 9. 15:39

 일반적으로 Console에 출력할 때에 printf 혹은 std::cout을 사용하게 된다. 하지만 Console 프로그램 게임에서 전체 이미지 등을 표시하기에는 느리다는 단점이 있다.

 이를 해결하기 위한 방법으로는 WriteConsoleOutput 함수가 있다. CHAR_INFO array로 이루어진 buffer에 먼저 쓴 후, 해당 함수에 전달하면 빠르게 콘솔에 출력할 수 있다.

int width = 10;
int height = 10;

CHAR_INFO buffer[width * height];

for (int i = 0; i < width * height; i++) {
    buffer[i].Char.UnicodeChar = 'a';
    buffer[i].Attributes = 7; 
    // 7을 입력할 경우 흰색으로 출력.
    // 자세한 내용은 
    // https://docs.microsoft.com/en-us/windows/console/char-info-str
    // 참조.
}

COORD pos = { 0, 0 }, size = { width, height };
SMALL_RECT rect = { 0, 0, width, height };
WriteConsoleOutput(GetStdHandle(STD_OUTPUT_HANDLE), buffer, size, pos, &rect);

 

'C++' 카테고리의 다른 글

Console Project/3. 이미지  (0) 2019.12.10
Console Project/2. 출력 버퍼  (0) 2019.12.09
Console Project/1. 화면  (0) 2019.12.09
Console Project  (0) 2019.12.09
C++ Image Library - CImg  (0) 2019.12.08

댓글()

C++ Image Library - CImg

C++|2019. 12. 8. 23:10

CImg는 C++프로젝트에서 간편하게 사용할 수 있는 이미지 라이브러리이다.

다운로드

http://cimg.eu/에서 다운로드 및 압축 해제.

설치

프로젝트의 포함 디렉토리에 "\CImg_latest\CImg-{Version}" 경로를 추가.

혹은,

CImg.h파일을 프로젝트에 직접 추가.

cimg_use_jpeg 사용 시

http://libjpeg.sourceforge.net/에서 최신버전 다운로드 및 압축 해제.

Developer Command Prompt for VS 2019를 실행.

NMAKE /f makefile.vc setup-v15

해당 커맨드를 입력하여 *.sln 파일 생성.

생성된 jpeg.sln을 실행한 후 빌드.

프로젝트 포함 디렉토리에 "\CImg_latest\CImg-{Version}" 추가,

프로젝트 라이브러리 디렉토리에 "\CImg_latest\CImg-{Version}\Release" 추가 및 링커에 jpeg.lib 추가.

 

'C++' 카테고리의 다른 글

Console Project/3. 이미지  (0) 2019.12.10
Console Project/2. 출력 버퍼  (0) 2019.12.09
Console Project/1. 화면  (0) 2019.12.09
Console Project  (0) 2019.12.09
WriteConsoleOutput - Console 화면에 빠르게 출력하는 함수.  (0) 2019.12.09

댓글()