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

댓글()