Visual Studio 에서 libpng 사용하기

개요

시작은 분명 예전에 만들어둔 게임 소스를 빌드했던 것일테죠. 5년전에 C언어로 만든 게임을 보니 추억이 새록새록 피어오르면서 욕심이 나기 시작합니다.

지금 만든다면 더 잘 만들 수 있는데

그래서 새로운 게임을 간단하게 구상해 봤습니다. 오목같이 간단한 보드게임은 게임의 구현보단 알고리즘의 공부가 아닐까 싶었고, 리듬게임이나 슈팅게임은 만들어 보았습니다. 물론 지금 만들면 더 잘 만들겠지만. 이전에 미로찾기도 만들어 봤습니다.

결국, 도달한 것은 지금껏 실패만 했었던 2D 횡스크롤 게임입니다.

그냥 간단하게 만드는 것 보단 CUI라도 조금은 보는 게 즐거웠으면 하는 마음에 아스키 아트로 이루어진 애니메이션을 만들어보자! 그렇게 탄생한 것이 이것입니다.


구현 방법

사실 위 애니메이션은 원본이 있습니다. itch.io 에서 무료 게임 에셋 중 하나를 다운받은 것입니다.

https://rvros.itch.io/animated-pixel-hero

아스카아트로 구현한 모션

해당 이미지를 다운받을 경우, 스프라이트 이미지들이 제공됩니다. 저는 이걸 아스키 아트로 바꾼 다음에 이어서 출력하도록 했을 뿐입니다.

물론, 이렇게도 가능하지만 문든 든 생각이 있었죠.

자동으로 아스키 아트로 변환하면 파일만 준비해도 되지 않을까?

게다가 알아보니 PNG 를 사용하면 이후 맵 관련해서 만들 때 더 괜찮게 만들 수 있을 것 같았습니다. 원랜 PNG를 해석하는 걸 직접 만들까 했지만 이미 libpng 란 좋은 라이브러리가 있으니 그걸 사용하는 것으로 했죠.


프로젝트 준비

제가 라이브러리를 빌드하고 포함시키는 데 8시간이나 걸려버렸습니다.

리눅스는 많이 다뤄봤어도 윈도우에서 프로그래밍 하는 건 이전 미로찾기 글을 쓴 이후로 3년정도 됐군요. VS는 안 된다는 게 뭐가 그리 많은지… 고생 좀 했습니다.

일단, libpng 는 zlib 을 사용하므로, zlib 소스를 먼저 다운받습니다.

https://zlib.net/

비주얼 스튜디오에서 새로운 빈 프로젝트를 생성하고, 다음과 같은 구성으로 만듭니다.

파일들은 다운받은 소스에서 해당 부분만 가져오면 됩니다.

프로젝트 속성에서 여러가지를 바꿔줄 것입니다.

1
2
3
4
5
6
타겟: Release (x86)
구성 속성 -> 일반 -> 구성 형석: 정적 라이브러리(.lib)
구성 속성 -> 고급 -> 대상 파일 확장명: .lib
구성 속성 -> C/C++ -> 일반 -> 경고 수준: 모든 경고 해제(/W0)
구성 속성 -> C/C++ -> 코드 생성 -> 스펙터 완화: 사용 안 함
구성 속성 -> C/C++ -> 고급 -> 특정 경고 사용 안 함: 4996

이렇게 바꾸고 빌드하면, 프로젝트 폴더에 Release 에 zlib.lib 와 zlib.bsc 파일이 생성됩니다.

이제 필요한 걸 챙겼으니, libpng 를 솔루션 내에서 새 프로젝트 생성으로 프로젝트를 추가해 줍니다.

http://www.libpng.org/pub/png/libpng.html

파일 목록은 다음과 같습니다.

pngconf.h 파일은 pngconf.h.prebuilt 파일을 가져다가 복사해서 이름만 바꿔주면 됩니다.

해당 프로젝트의 속성도 바꿔줄 것입니다.

추가 포함 디렉터리를 zlib 폴더로 지정을 해줘야 합니다.

1
2
3
4
5
6
7
타겟: Release (x86)
구성 속성 -> 일반 -> 구성 형석: 정적 라이브러리(.lib)
구성 속성 -> 고급 -> 대상 파일 확장명: .lib
구성 속성 -> C/C++ -> 일반 -> 추가 포함 디렉터리: $(ProjectDir)..\zlib
구성 속성 -> C/C++ -> 일반 -> 경고 수준: 모든 경고 해제(/W0)
구성 속성 -> C/C++ -> 코드 생성 -> 스펙터 완화: 사용 안 함
구성 속성 -> C/C++ -> 고급 -> 특정 경고 사용 안 함: 4996

이제, libpng 를 사용할 프로젝트를 솔루현에 추가해 줍니다.

프로젝트의 속성을 변경합니다.

1
2
3
4
5
6
7
타겟: Release (x86)
구성 속성 -> C/C++ -> 일반 -> 추가 포함 디렉터리: $(ProjectDir)..\libpng
구성 속성 -> C/C++ -> 일반 -> 경고 수준: 모든 경고 해제(/W0)
구성 속성 -> C/C++ -> 코드 생성 -> 스펙터 완화: 사용 안 함
구성 속성 -> C/C++ -> 고급 -> 특정 경고 사용 안 함: 4996
구성 속성 -> 링커 -> 일반 -> 추가 라이브러리 디렉터리: $(ProjectDir)..\Release;
구성 속성 -> 링커 -> 입력 -> 추가 종속성: zlib.lib;libpng.lib

위와 같이 설정하고, 아래 코드를 실행시키면 정상적으로 함수가 실행되는 것을 확인할 수 있습니다.

프로젝트 준비는 끝났습니다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <png.h>

int main( void )
{
png_structp png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING, NULL, NULL, NULL );
printf( "Hello World!\n" );
return 0;
}

PNG 파일 읽기

인터넷과 libpng 의 소스와 메뉴얼을 뒤적거리면서, 파일을 읽어들여 사진의 데이터를 배열로 만들 수 있었습니다. 예를 들어 3x2 크기를 가진 파일에 각각 [#ff0000, #00ff00, #0000ff, #ff0000, #00ff00, #0000ff] 코드를 가졌다면 그림은 다음과 같겠죠.

그랬을 때, 만들어질 배열의 구조는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
[
[
255, 0, 0, 255,
0, 255, 0, 255,
0, 0, 255, 255,
],
[
255, 0, 0, 255,
0, 255, 0, 255,
0, 0, 255, 255,
],
]

(R, G, B, A) 가 한 픽셀의 정보를 가진 것으로, 무조건 4바이트씩 한 픽셀의 데이터를 쭉 잇고, 2차원 배열로 높이 만큼 만든 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
png_minfo_t* read_png( char *file )
{
png_byte signature[8] = { 0, };
png_minfo_t *pinfo = NULL;

if ( PathFileExistsA( file ) ) {
FILE *fp = fopen( file, "rb" );
if ( !fp ) {
printf( "Can not file open. [%s]\n", file );
return NULL;
}

/* signature read, check png signature */
fread( signature, 1, sizeof( signature ), fp );
if ( png_sig_cmp( signature, 0, sizeof( signature )) != 0 ) {
printf("File [%s] is not png.\n", file);
fclose( fp );
return NULL;
}


/* init png struct pointer */
png_structp png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING, NULL, user_error_fn, user_warning_fn );

if ( !png_ptr ) {
printf( "png_create_read_struct faild\n" );
fclose( fp );
return NULL;
}

png_infop info_ptr = png_create_info_struct( png_ptr );

if ( !info_ptr ) {
printf( "png_create_info_struct faild\n" );
png_destroy_read_struct( &png_ptr, NULL, NULL );
fclose( fp );
return NULL;
}

png_infop end_info = png_create_info_struct( png_ptr );
if ( !end_info ) {
printf( "png_create_info_struct [end_info]\n" );
png_destroy_read_struct( &png_ptr, &info_ptr, NULL );
}

if ( setjmp( png_jmpbuf( png_ptr ) ) ) {
printf( "Error during init_io\n" );
png_destroy_read_struct( &png_ptr, &info_ptr, &end_info );
fclose( fp );
return NULL;
}

/* reading png file info IHDR */
png_init_io( png_ptr, fp );
png_set_sig_bytes( png_ptr, sizeof( signature ) );
png_read_info( png_ptr, info_ptr );


png_uint_32 width = png_get_image_width( png_ptr, info_ptr );
png_uint_32 height = png_get_image_height( png_ptr, info_ptr );
png_byte color_type = png_get_color_type( png_ptr, info_ptr );
png_byte bit_depth = png_get_bit_depth( png_ptr, info_ptr );
int pass = png_set_interlace_handling( png_ptr );
png_read_update_info( png_ptr, info_ptr );

if ( png_get_color_type( png_ptr, info_ptr ) != PNG_COLOR_TYPE_RGBA ) {
printf( "File color type must be PNG_COLOR_TYPE_RGBA\n" );
png_destroy_read_struct( &png_ptr, &info_ptr, &end_info );
fclose( fp );
return NULL;
}


/* create minimal info struct */
pinfo = (png_minfo_t*)malloc( sizeof( png_minfo_t ) );
pinfo->width = width;
pinfo->height = height;
pinfo->buf = (png_bytepp)png_malloc( png_ptr, sizeof( png_bytep ) * height );

/* reading image data */
if ( setjmp( png_jmpbuf( png_ptr ) ) ) {
printf( "Error during read image\n" );
png_destroy_read_struct( &png_ptr, &info_ptr, &end_info );
fclose( fp );
return NULL;
}

png_size_t wsize = width * sizeof( png_rgba_pixel_t );
for ( int y=0; y < height; y++ ) {
pinfo->buf[y] = (png_bytep)png_malloc(png_ptr, png_get_rowbytes(png_ptr, info_ptr));
}
//png_set_rows( png_ptr, info_ptr, pinfo->buf );
png_read_image( png_ptr, pinfo->buf );
png_read_end( png_ptr, end_info );

/* free pointer */
png_destroy_read_struct( &png_ptr, &info_ptr, &end_info );
fclose( fp );

return pinfo;
} else {
printf( "No such file [%s]\n", file );
}
return NULL;
}

위 함수의 인자로 파일 이름을 주고 실행하면, 아래 구조체로 압축이 풀린 정보를 얻을 수 있습니다.

1
2
3
4
5
typedef struct png_minimal_info {
png_uint_32 width;
png_uint_32 height;
png_bytepp buf;
} png_minfo_t;

동적할당 된 정보를 해제할 땐 아래 함수를 실행하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void free_png_minfo( png_minfo_t *minfo )
{
if ( minfo ) {
if ( minfo->buf ) {
for ( int y = 0; y < minfo->height; y++ ) {
if ( minfo->buf[y] ) {
free( minfo->buf[y] );
}
}
free( minfo->buf );
}
free( minfo );
}
}

정보를 출력해 보면 아래와 같은 내용을 얻을 수 있었습니다.

x: 24, y: 14 위치엔 (201, 149, 127) 색이 불투명도 100%로 존재한다는 뜻입니다.

해당 정보를 가지고 아스키 아트를 만들어 보았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const char PREFIX_ASCII[] = "#,.0123456789:;@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$";

typedef struct _ascii_info {
int width;
int height;
char **buf;
} ascii_info_t;

ascii_info_t* png2ascii( png_minfo_t *minfo )
{
if ( minfo == NULL ) {
printf( "Info struct is null.\n" );
return NULL;
}

ascii_info_t *info = (ascii_info_t*)malloc( sizeof( ascii_info_t ) );
info->width = ( ( minfo->width * 2 ) + 1 /* null */);
info->height = minfo->height;
info->buf = (char**)malloc( sizeof( char* ) * minfo->height );

for ( int y = 0; y < minfo->height; y++ ) {
png_bytep row = minfo->buf[y];
info->buf[y] = (char*)malloc( sizeof( char ) * info->width );
for ( int x = 0; x < minfo->width; x++ ) {
png_rgba_pixel_t pixel;
memcpy( &pixel, ( row +(x * sizeof( png_rgba_pixel_t )) ), sizeof( png_rgba_pixel_t ) );
if ( pixel.alpha == 0 ) {
snprintf( info->buf[y]+(x*2), ( info->width - (x*2)), " " );
} else {
png_byte grey = ( pixel.red + pixel.green + pixel.blue ) / 3; // average
char c = PREFIX_ASCII[grey * ( strlen( PREFIX_ASCII ) - 1 ) / 256];
snprintf( info->buf[y] + ( x * 2 ), ( info->width - ( x * 2 ) ), "%c%c", c, c );
}
}
info->buf[y][info->width - 1] = '\0';
}

return info;
}

위 함수를 실행시키면 ascii_info_t 구조체에 정보가 담기게 되는데, 출력해 보면 아래 결과가 보이게 됩니다.

원본


마무리

게임을 위해 올린 코드에서 색깔을 입히도록 했습니다.

깃허브에서 프로젝트 전체를 확인하실 수 있습니다.

https://github.com/raravel/lib-png2ascii

읽어주셔서 감사합니다.

작성자: raravel
주소: https://raravel.dev/20201031-1912/
저작권 고지: 이 블로그의 모든 기사는 별도로 명시하지 않는 한 CC BY-NC-SA 4.0 에 따라 라이선스가 부여됩니다.