Search
Duplicate
📦

(1) mlx 활용과 color 표현 하는 방법!

간단소개
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
42seoul
Scrap
태그
9 more properties
minirt 뽀개기!

화면에 색 표현하기! mlx 활용!

minirt, cub3D과제는 subject에서 제공하는 mlx 그래픽 라이브러리를 활용해야한다. mlx에 대해서 공부를 하는데 생각보다 양이 많았다. 이미 다른 멋진 카뎃분들이 mlx에 대해 잘 정리해 주었기 때문에 여기서는 mlx 라이브러리를 직접 띄우는 과정을 순차적으로 제시해보려고 한다!
그래서 이번 포스팅에서는 딱 필요한 부분만 소개하고 바로 minirt에 필요한 벡터공부로 넘어가려고 한다! mlx라이브러리에 대해 더 궁금하다면 아래 나오는 42Docs페이지를 참고하길 바란다!

Minilibx 간단 소개!

Mninilibx란?

MiniLibX는 X-Window 및 Cocoa에 대한 지식 없이도 화면에서 무언가를 그리기위한 기본적인 그래픽 라이브러리이다. 간단한 창 생성, 그리기 도구, 이미지 기능 및 이벤트 관리 시스템을 제공한다.
아래에 대한 내용은 해외의 카뎃이 열심히 MiniLibX에 대한 사용법을 문서화 해둔 것을 번역한 내용이다. 본문을 보고 싶다면 아래 링크를 참고하자.
X-Window란? X-Window는 유닉스 용 네트워크 지향 그래픽 시스템이다. 원격 데스크톱에 연결할 때 사용된다. 이러한 구현의 가장 일반적인 것은 TeamViewer이다.
MacOS.. MacOS는 화면에 대한 그래픽 액세스를 처리하지만 이에 액세스하려면 화면, 윈도우 시스템, 키보드 및 마우스를 처리하는 기본 MacOS 그래픽 프레임 워크에 애플리케이션을 등록해야한다.

화면을 띄워보자!

사실 어려운 설명 필요없이 직접 화면을 띄워 보면서 공부하는게 최고다!! 바로 화면을 하나 만들어보자!

1) mlx 초기화 하기!

mlx.h 파일에 선언되어 있는 mlx_init함수를 활용해서 포인터를 초기화할 수 있다. 이제 mlx_ptr은 mlx_init()함수가 리턴하는 주소를 가리키게 된다.
#include <mlx.h> int main(void) { void *mlx_ptr; mlx_ptr = mlx_init(); return (0); }
C
복사
이제 컴파일을 진행해보자!!

2) Makefile 미리 만들기!

이제 코드를 완성했으니 컴파일을 해야한다. 하지만 컴파일을 하려고 하는데.. 컴파일 옵션이 너무나 길다.. 매번 테스트 할 때마다 이 명령어를 복붙할 순 없으니 미리 Makefile을 만들어 두자!
gcc -Wextra -Werror -Wall -Lmlx -lmlx -framework OpenGL \ -framework Appkit $(SRCS) -o $(NAME)
Makefile
복사
혹시 Makefile에 대해 더 궁금하다면 여기를 참고하도록 하자. (클러스터 사파리에서는 문자가 깨지는 경우가 생겨서 크롬으로 여는 것을 추천)
1차적으로 생성할 파일의 Makefile 예시!

3) make!

완성하고 자신만만하게 make를 쳤지만 아무것도 나오지 않았다!!!
사실 아무것도 나오지 않는게 당연하다^_^ 아직 창을 생성하지 않았기 때문이다. mlx_init()함수는 디스플레이와 우리의 프로그램을 연결시켜주는 초기화 함수이다.

4) 창 생성하기

이제 진짜로 창을 만들어 보자! 위의 코드에서 조금만 추가하면 된다!
#include <mlx.h> int main() { void *mlx_ptr; void *win_ptr; // 생성할 윈도우를 가리키는 포인터 mlx_ptr = mlx_init(); win_ptr = mlx_new_window(mlx_ptr, 500, 500, "Hellow World!"); mlx_loop(mlx_ptr); // loop를 돌면서 event를 기다리고, 생성한 윈도우를 Rendering한다. return (0); }
C
복사
일단 위 코드를 그대로 작성한 후 실행해보자!!
짠!!! 아무것도 없는 검은 화면이지만 일단 화면을 띄웠다!!!!
이제 창을 띄웠으니 추가된 함수들이 어떤 역할을 하는지 알아보자!
창을 생성하는 함수의 prototype은 아래와 같다.
void mlx_new_window(void mlx_ptr, int size_x, int size_y, char *title)
C
복사
mlx_ptr : mlx_init이 반환한 연결 식별자
size_x : 창의 가로 사이즈
size_y : 창의 세로 사이즈
title : 타이틀 바에 표시될 문자열
만약 초기화 하지 않은 mlx_ptrmlx_new_window함수로 전달된다면 segfault를 발생한다.
이 함수는 창을 생성하는 것 까지만 작동하고 실제로 모니터상에 창을 띄우진 않는다. mlx_loop 함수를 주석처리해보면 실행파일을 실행했을 때, 아무것도 보이지 않는 것을 알 수 있다.
이벤트를 받는 함수의 prototype은 아래와 같다.
int mlx_loop(void *mlx_ptr)
C
복사
mlx_ptr : 디스플레이에 연결되어 있는 mlx_ptr
return value : no return
이벤트를 받기 위해서 꼭 사용해야 한다고 나와있지만, 실제로 이 함수를 사용하지 않으면 윈도우 자체가 뜨지 않는다.

4) 창에 이미지 넣기 !

이제 검은 창을 띄웠으니 화면에 색칠을 해보자! 일단 화면에 띄워지는걸 봐야 이해가 되니 무작정 아래 예시 코드를 따라해서 화면을 하얗게 만들어 보자!
아래는 화면을 하얗게 만드는 두가지 방법이 나와있다. mlx.h에 선언되어있는 mlx_pixel_put()을 활용해서 출력하는 방법이미지를 생성해서 생성한 이미지를 화면에 올리는 방법이다. 아래 코드는 첫번째 방법으로 화면이 출력되고 있고 주석 처리된 부분을 모두 지우면 두번째 방법을 활용해서 출력할 수 있다.
#include <unistd.h> #include <mlx.h> //이미지의 정보를 나타내는 변수를 저장한 구조체 typedef struct s_data { void *img; char *addr;L int bits_per_pixel; int line_length; int endian; } t_data; //원하는 좌표에 해당하는 주소에 color값을 넣는 함수 void my_mlx_pixel_put(t_data *data, int x, int y, int color) { char *dst; dst = data->addr + (y * data->line_length + x * (data->bits_per_pixel / 8)); *(unsigned int*)dst = color; } int main() { void *mlx_ptr; void *win_ptr; t_data image; mlx_ptr = mlx_init(); win_ptr = mlx_new_window(mlx_ptr, 500, 500, "Hellow World!"); image.img = mlx_new_image(mlx_ptr, 500, 500); // 이미지 객체 생성 image.addr = mlx_get_data_addr(image.img, &image.bits_per_pixel, &image.line_length, &image.endian); // 이미지 주소 할당 for (int i = 0 ; i < 500 ; i++) { for (int j = 0 ; j < 500 ; j++) { mlx_pixel_put (mlx_ptr, win_ptr, i, j, 0x00FFFFFF); //my_mlx_pixel_put(&image, i, j, 0x00FFFFFF); } } //mlx_put_image_to_window(mlx_ptr, win_ptr, image.img, 0, 0); mlx_loop(mlx_ptr); return (0); }
C
복사
이제 위에서 쓰인 함수에 대해 천천히 알아보자!
mlx_new_image()는 메모리에 새 이미지를 만든다. 즉, 우리가 지정한 가로 세로 만큼의 그림을 보이지 않는 공간에 미리 그려두는 것이다! 이후 이 이미지를 mlx_put_image_to_window()함수를 활용해서 생성한 윈도우 위에 살포시 올려둘 것이다!
void *mlx_new_image(void *mlx_ptr, int width, int height)
C
복사
mlx_ptr : 디스플레이에 연결되어 있는 mlx_ptr
width : 이미지의 너비
height : 이미지의 높이
mlx_get_data_addr()는 생성된 이미지에 대한 정보를 리턴한다.
char *mlx_get_data_addr(void *img_ptr, int *bits_per_pixel, \ int *size_line, int *endian);
C
복사
img_ptr : 사용할 이미지를 지정한다
bits_per_pixel : 픽셀 색상을 나타내는데 필요한 비트 수
size_line : 이미지 한 줄을 메모리에 저장하는데 사용되는 바이트 수. 이 정보는 이미지의 한 줄에서 다른 줄로 이동하는 데 필요하다.
endian : 이미지의 픽셀 색상 저장 방식(little endian = 0, big endian = 1)
endian이란? 엔디언(Endianness)은 컴퓨터의 메모리와 같은 1차원의 공간에 여러 개의 연속된 대상을 배열하는 방법을 뜻하며, 바이트를 배열하는 방법을 특히 바이트 순서(Byte order)라 한다. endian에 대해 자세히 알고싶다면?
mlx_put_image_to_window()는 생성한 이미지를 원하는 윈도우에 올려주는 함수이다.
int mlx_put_image_to_window(void *mlx_ptr, void *win_ptr, void *img_ptr, \ int x, int y);
C
복사
mlx_ptr : 디스플레이에 연결되어 있는 mlx_ptr
win_ptr : 사용할 윈도우
img_ptr : 사용할 이미지
x, y : 이미지가 위치할 좌표
return value : ?????
나머지는 라이브러리라서 어렵지 않았지만, 대체 아래의 코드는 뭘까???
void my_mlx_pixel_put(t_data *data, int x, int y, int color) { char *dst; dst = data->addr + (y * data->line_length + x * (data->bits_per_pixel / 8)); *(unsigned int*)dst = color; }
C
복사
위 코드는 메모리의 입장에서 보게 된다면 이해가 어렵지 않다! 아래 이미지는 이해를 돕기위해 작성된 그림이므로 메모리가 이동하는 칸 수가 정확하지 않을 수 있다!!
y좌표에 line_length를 곱해서 원하는 y좌표가 있는 주소까지 점프시킨다!(주황색 화살표)
이제 x좌표 만큼만 이동하면 된다. x에 곱해주는 (data->bits_per_pixel / 8)은 픽셀에 들어있는 비트수를 8로 나눈 값을 곱하게 된다! 우리는 픽셀당 32개의 bit가 들어있기 때문에 x×4x \times 4 가 되는 것이다.(주소는 byte단위로 이동하기 때문에!!)
우리는 그 위치에 우리가 매개변수로 넘겨준 color값을 넣어주면 끝이다! 나중에 프로그램이 해당 메모리를 읽으면서 좌표마다 32비트의 color값을 읽게 되고 우리가 띄운 디스플레이 상의 윈도우 x, y좌표에 해당하는 color를 표현하게 된다!

5) esc를 누르면 꺼지는 윈도우를 만들어보자!

이제 화면을 띄웠으니 우리가 원할때 화면을 종료할 수 있도록 만들어보자! 지금까지는 창을 종료해도 프로그램이 종료되지 않아서 control + c키를 활용해 프로그램을 종료해 주어야 했다. 이번 과제에서 우리는 exit()함수를 사용할 수 있기 때문에 이 함수를 활용해서 특정 조건에 프로그램이 종료되도록 만들 것이다.
우리는 키를 누르는 이벤트를 발생시킬때 윈도우가 종료되도록 만들고 싶다! 이럴 때에는 Hooking 을 활용하면 된다! mlx라이브러리에도 hooking을 도와주는 함수들이 있다!
Hooking? 컴퓨터 프로그래밍에서 Hooking이라는 용어는 소프트웨어 구성 요소간에 전달되는 함수 호출이나 메시지 또는 이벤트를 가로 채서 운영 체제, 응용 프로그램 또는 기타 소프트웨어 구성 요소의 동작을 변경하거나 강화하는 데 사용되는 다양한 기술을 포함한다. 이러한 가로채는 함수 호출, 이벤트 또는 메시지를 처리하는 코드를 Hook라고한다.
아래 세가지 함수는 모두 hooking을 도와주는 함수들입니다.
int mlx_key_hook ( void win_ptr, int (*funct_ptr)(), void *param ); int mlx_mouse_hook ( void win_ptr, int (*funct_ptr)(), void *param ); int mlx_expose_hook ( void win_ptr, int (*funct_ptr)(), void *param );
C
복사
win_ptr : 적용할 윈도우
(*funct_ptr)() : 호출하고 싶은 함수 포인터
*param : 필요한 매개변수를 저장하는데 사용
위 함수들과 비슷하지만 조금 더 범용적으로 사용가능한 mlx_hook함수가 있다.
int mlx_hook(void *win_ptr, int x_event, int x_mask, int (*funct)(), void *param);
C
복사
아래는 mac키보드의 keycode 표이다. 확대해서 보면 각 key 왼쪽위에 숫자가 적혀있다! 우리가 원하는 esc의 code 는 53이다.
그렇다면 우리는 어떻게 코드를 만들어야할까? 우리는 특정 keycode 가 들어왔을 때 원하는 동작을 하는 함수를 미리 만들어둔 뒤 mlx_*hook 함수의 int (*funct_ptr)() 자리에 우리가 만든 함수를 넣어주면 된다!
// esc key press event int key_hook(int keycode, t_vars *vars) { if(keycode == 53) { mlx_destroy_window(vars->mlx, vars->win); exit(0); } return (0); }
C
복사
이제 이 함수를 추가해서 keyhook이 작동하는 함수를 만들어보자!
#include <unistd.h> #include <stdlib.h> #include <mlx.h> // mlx 구조체 mlx 포인터와 생성할 win 포인터를 가지고 있다. typedef struct s_vars { void *mlx; void *win; } t_vars; //이미지의 정보를 나타내는 변수를 저장한 구조체 typedef struct s_data { void *img; char *addr; int bits_per_pixel; int line_length; int endian; } t_data; //원하는 좌표에 해당하는 주소에 color값을 넣는 함수 void my_mlx_pixel_put(t_data *data, int x, int y, int color) { char *dst; dst = data->addr + (y * data->line_length + x * (data->bits_per_pixel / 8)); *(unsigned int*)dst = color; } // esc key press event int key_hook(int keycode, t_vars *vars) { if(keycode == 53) { mlx_destroy_window(vars->mlx, vars->win); exit(0); } return (0); } int main() { t_vars vars; t_data image; vars.mlx = mlx_init(); vars.win = mlx_new_window(vars.mlx, 500, 500, "Hellow World!"); image.img = mlx_new_image(vars.mlx, 500, 500); // 이미지 객체 생성 image.addr = mlx_get_data_addr(image.img, &image.bits_per_pixel, &image.line_length, &image.endian); // 이미지 주소 할당 for (int i = 0 ; i < 500 ; i++) { for (int j = 0 ; j < 500 ; j++) { my_mlx_pixel_put(&image, i, i, 0x00FF0000); } } mlx_put_image_to_window(vars.mlx, vars.win, image.img, 0, 0); mlx_key_hook(vars.win, key_hook, &vars); // esc key press event mlx_loop(vars.mlx); return (0); }
C
복사

6) mlx 라이브러리 활용하기!

이전에는 하나의 색을 활용해서 화면을 만들었다면 이제는 우리가 원하는 색을 한번 만들어 보자!
아래의 코드에는 close button(창 종료버튼)을 눌렀을때의 이벤트와 rgb를 따로 계산해서 색을 표현하는 방법이 나와있다! 색 표현은 아래에 다시 설명이 되어있으니 일단 코드를 따라해보고 천천히 다시 공부해보자!
#include <unistd.h> #include <mlx.h> #include <stdlib.h> // mlx 구조체 typedef struct s_vars { void *mlx; void *win; } t_vars; // image data 구조체 typedef struct s_data { void *img; char *addr; int bits_per_pixel; int line_length; int endian; } t_data; //함수 선언부 void my_mlx_pixel_put(t_data *data, int x, int y, int color); int prtimage(); int exit_hook(); int key_hook(int keycode, t_vars *vars); // main function! int main() { prtimage(); return (0); } void my_mlx_pixel_put(t_data *data, int x, int y, int color) { char *dst; dst = data->addr + (y * data->line_length + x * (data->bits_per_pixel / 8)); *(unsigned int*)dst = color; } int prtimage() { int color; t_vars vars; t_data image; int img_width = 1920; int img_height = 1080; vars.mlx = mlx_init(); vars.win = mlx_new_window(vars.mlx, img_width, img_height, "Hellow World!"); image.img = mlx_new_image(vars.mlx, img_width, img_height); // 이미지 객체 생성 image.addr = mlx_get_data_addr(image.img, &image.bits_per_pixel, &image.line_length, &image.endian); // 이미지 주소 할당 for (int i = 0 ; i < img_height - 1 ; ++i) { for (int j = 0 ; j < img_width - 1; ++j) { double r = (double)(img_width - j) / (img_width - 1); double g = (double)(i) / (img_height - 1); double b = 1; color = ((int)(255.999 * r) << 16) + ((int)(255.999 * g) << 8) + ((int)(255.999 * b)); my_mlx_pixel_put(&image, j, i, color); } } mlx_put_image_to_window(vars.mlx, vars.win, image.img, 0, 0); mlx_key_hook(vars.win, key_hook, &vars); // esc key press event mlx_hook(vars.wizn, 17, 0, exit_hook, 0); // close button press event mlx_loop(vars.mlx); return (0); } // esc key press event int key_hook(int keycode, t_vars *vars) { if(keycode == 53) { mlx_destroy_window(vars->mlx, vars->win); exit(0); } return (0); } // close button press event int exit_hook() { exit(0); }
C
복사

Color int값을 만드는 함수 만들기!

색상은 int형식으로 표현된다. 우리는 ARGB값을 포함하는 int를 얻으려면 몇가지 규칙이 필요하다.
우리는 TRGB 형식을 사용하기 위해 비트연산을 활용한다. 색상을 표현하기 위현 형식은 0xTTRRGGBB와 같이 초기화해서 사용한다. 각 코드는 다음을 의미한다.
T Transparency;
R Red color;
G Green color;
B Blue color.
RGB 는 위와같이 초기화 할 수 있고, 몇 가지 예시는 아래와 같다.
Red: 0x00FF0000;
Green: 0x0000FF00;
Blue: 0x000000FF;
각 바이트는 2 ^ 8 개의 값을 포함하고 rgb 값의 범위는 0에서 255까지이므로 정수를 완벽하게 맞출 수 있다. (int가 4 바이트이므로). 프로그래밍 방식으로 값을 설정하기 위해 비트시프팅을 사용한다. rgb각각의 값을 가져와서 하나의 color를 만들어주는 함수를 작성해보자!
int create_trgb(int t, int r, int g, int b) { return (t << 24 | r << 16 | g << 8 | b); }
C
복사
비트연산을 활용하면 이렇게 인코딩된 TRGB값을 활용해서 다시 각 코드의 값을 뽑아낼 수 있다.
int get_t(int trgb) { return (trgb & (0xFF << 24)); } int get_r(int trgb) { return (trgb & (0xFF << 16)); } int get_g(int trgb) { return (trgb & (0xFF << 8)); } int get_b(int trgb) { return (trgb & 0xFF); }
C
복사
이제 우리는 위 예시코드의 color부분을 함수를 활용해 만들 수 있다!
// 이전코드 color = (int)(255.999 * r) << 16 + (int)(255.999 * g) << 8 + (int)(255.999 * b);
C
복사
아래는 함수를 활용해 수정한 코드이다!.
color = create_trgb(0, 255.999 * r, 255.999 * g, 255.999 * b);
C
복사

Prev

Reference