[42서울 / fdf] mlx와 fdf 문제해결 정리

Hans Park·2022년 4월 30일
0

42서울 본과정

목록 보기
13/15
post-thumbnail
💡 42seoul 본과정

노션이 보기 편합니다.

🚀 fdf


[42Seoul]MiniLibx
<mlx 함수 설명 페이지>

"mlx.h"
<mlx 유래 및 상세 설명 페이지>

[42Seoul]FDF
<fdf 알고리즘 및 계산식 설명 페이지>

(1) mlx 활용과 color 표현 하는 방법!
<mlx 설명 팔만코딩경>

MiniLibX 파헤치기
<MiniLibX 파헤치기>

🚀 개발 전 기록사항


🖋 메모

  • 점찍는건 이제 할 수 있겠다.
  • 점과 점사이의 사이를 무수한 점들로 찍으려면 어떻게 해야할까?
  • 점과 점 사이를 보여주는데 시야가 약간 틀어져있다.
  • image를 사용하는걸 강력히 권장하는데 왜?
  • 예외처리는 뭐가있을까?
    • fdf 파일 open 가능한지
    • fdf파일 내용물이 적절한지
    • 2차원포인터 널가드
  • 창 크기는 얼마나 잡아야 하는가 (점과 점 사이 거리는 얼마나?)
  • 이미지로 넣으면 overflow(창보다 더 긴 이미지) 넣어도 자동으로 예외처리하는듯!!!
  • 파일 내 숫자 int범위로만하기

🖋 테스트 및 구현순서

  1. 창 열어보기
  2. 점 찍어보기
  3. pixel_put 말고 image로해보기
  4. 사각형만들어보기
  5. 점만 넘기면 자동으로 이미지 찍을 함수 만들기
  6. 점과 점 사이 이어보기
  7. 점 시야각 옮겨보기(?)
  8. map파싱
  9. 파싱한 데이터를 offset으로 변환
  10. 투영 정리 (offset을 등축투영)

🚀 mlx 써보기


🖋 컴파일

  1. 우선 mlx(minilibx)파일을 받는다.
    맥에 내장된 mlx가 아니라 42서울에서 제공하는 mlx를 사용하는 것이 좋다.

💡 You must use the miniLibX. Either in the version that is available on the system, or from its sources. If you choose to work with the sources, you will need to apply the same rules for your libft as those written above.

2021년 버전이 슬랙에 있으니 활용해보자.

  1. 프로젝트 루트 폴더에 받은 mlx 폴더를 둔다.
  2. 직접 만든 프로그램을 링킹할 때 mlx가 필요하다.

이 때, 직접만든 Makefile 내에서 mlx의 Makefile까지 실행시켜야 하며,
libmlx.dylib 파일은 런타임 중 참조하는 동적 라이브러리이기 때문에 프로젝트 루트 폴더 (실행파일이 있는 곳)으로 옮겨야 한다.

💡 컴파일러는 컴파일 시 실행파일 안에 라이브러리 코드를 포함하지 않는다.
대신 실행파일에 라이브러리를 연결하기 위한 코드만 포함시키고, 실행 시 라이브러리에 연결한다.

  1. 목적파일 링킹 시, -L 옵션과 -l 옵션을 사용하여 라이브러리를 같이 링킹한다.

🖋 창띄우기

  1. "mlx.h" 헤더를 추가한다.
  2. void *mlx_init()의 리턴값을 받는다. (이하 mlx값이라 설명함)
    이 리턴값이 우리가 만들 프로그램과 display의 연결을 초기화하는 함수이다.
    실패 시 NULL을 리턴한다.
  3. void *mlx_new_window함수의 리턴값을 받는다.
    위 함수의 매개변수로는
    1. mlx_init()의 리턴값
    2. 창 크기 x
    3. 창 크기 y
    4. title → 이 타이틀의 window의 타이틀 바가 된다.
    실패시 NULL을 리턴한다.
    
    ex) 
    
    ```c
    void *mlx = mlx_init();
    void *win = mlx_new_window (mlx, 1000, 1000, "테스트");
    ```
  4. 이 상태로 컴파일하여 실행하면 창이 뜨지 않는다.
    X-Window와 MacOSX 그래픽 시스템은 양방향으로, 한쪽에서는 픽셀, 이미지와 같은 것들을 명령을 화면에 보내고, 반대쪽에서는 키보드와 마우스 정보들을 받는다. 따라서 event들을 받아오기 위해 mlx loop를 사용해야 한다.
    ex)
    
    ```c
    void *mlx = mlx_init();
    void *win = mlx_new_window (mlx, 1000, 1000, "테스트");
    mlx_loop(mlx);
    ```

🖋 점 찍기

점을 찍기 위해선 mlx_pixel_put명령어를 사용하는 방법과, mlx_put_image_to_window를 사용하는 방법이 있다.

mlx_pixel_put

mlx_loop(mlx)를 실행하기 전, 미리 이미지에 어느 위치에 어느 색의 픽셀을 찍을 것인지 알려주어야 한다.

int color = create_trgb(0,255,0,0);
mlx_pixel_put (mlx, win, 200, 300, color)

💡 color는 아래에서 설명한다.

매개변수는 순서대로 mlx_init의 리턴값, window, 점을 찍을 x좌표, y좌표, 이다.

💡 만약 x, y좌표가 window의 범위를 벗어나면, segmentation fault가 뜬다.

mlx_put_image_to_window

서브젝트를 보면 image를 사용하는 것을 강력히 권장하고 있다.

  1. 우선 void *형의 mlx_new_image() 리턴값을 저장한다.

    void *image = mlx_new_image(mlx, x길이, y길이);
  2. char *형의 mlx_get_data_addr의 리턴값을 받는다.

    리턴값은, 위 1번의 이미지의 pixel 색상 정보를 저장하는 1차원 배열의 첫 주소값이다.

    매개변수로 넣는 숫자들은 각각 다음과 같다.

    • bits_per_pixel(pixel_color를 표현하기 위해 필요한 bit 수, 사실 상 32비트(ARGB))
    • size_line(가로 한 줄의 최대 길이, 200*200 사이즈라고 배열의 길이가 40000이 아니다.)
    • endian(이미지의 pixel_color가 little endian(0)인지, big endian(1)인지 알려준다, 사실 상 0)
    💡 우리는 숫자(색)을 비트연산 후 대입할 뿐, 읽을 일은 없으므로 엔디안이 크게 상관없음

    위 매개변수들은 int 변수의 주소값을 넘겨야 하며, size_line을 제외한 나머지는 사실 상 쓸 일이 없다.

    int bit_per_pixel1 = 0;
    int size_line1 = 0;
    int endian1 = 0;
    char *ptr = mlx_get_data_addr (image, &bit_per_pixel1,&size_line1, &endian1);
  1. ptr로 받은 주소는, 해당 이미지의 첫번째 픽셀의 주소이다.
    1차원 배열에 ARGB씩 32비트 크기로 나열되어있다.
    픽셀 하나하나의 ARGB값을 조절할 수 있고, 이를 활용하여 선, 면 등을 나타낼 수 있다.

    ```c
    int		create_argb(int a, int r, int g, int b)
    {
    	return (a << 24 | r << 16 | g << 8 | b);
    }
    
    for (int i =0 ; i< 200  ; i++)
    {
    		for(int k = 0; k<200; k++){
    			char *dst2;
    			// 8 = sizeof(char)
    			dst2 = ptr2 + (k * size_line1 + i * bit_per_pixel1 / 8 );
    			*(unsigned int*)dst2 = create_argb(0,0,255,0);
    		}
    }
    ```
  2. mlx_put_image_to_window함수를 사용하여 띄울 윈도우에 image를 넣는다.

    mlx_put_image_to_window(mlx, win, image, 0/*이미지시작x좌표*/, 0/*"y좌표*/);
  3. mlx_loop함수를 사용하여 창을 띄운다.

    mlx_loop(mlx);

[FdF] Graphics
<투영방법들>

등축투영 pdf
<등축투영pdf>

🚀문제 해결


문제풀이순서

  1. 매개변수 예외처리
  2. init
  3. parsing
  4. 점 좌표구하기
  5. 등축투영
  6. window 크기 구하기 및 생성
  7. align 맞추기
  8. 출력
    1. 점과 점 사이 보내면 출력하는 함수 작성
    2. 화면 넘어가는 픽셀 안그리는 예외처리
  9. main hook
  10. keyboard hook

🖋 매개변수 예외처리

매개변수의 예외처리는 간단하다.

우선 기본적으로 고려해야 할 사항은

  • 매개변수 없음
  • 매개변수 많을 때

정도이며, 추가로 해결해야 할 것이

  • fdf파일인지 여부

가 있을 것이다.

매개변수 개수

메인함수의 매개변수 중 하나인 argc를 통해 정확히 1개의 인자를 받았을 때만 프로그램을 실행시킨다.

매개변수 타입

매개변수로 보낸 파일명의 파일 type을 확인해야한다.

서브젝트 기준 상 .fdf 파일만 허용하기에, 이를 확인하여야 한다.

구현방법은 split함수를 통해 .을 기준으로 문자열을 분리시킨 후,

맨 뒤 파일명이 fdf가 맞는지 검사한다.

이때 strncmp로 검사했는데 fdf의 길이가 3이라고 3만 검사할 경우,

.fdf와 .fdfd 등 그 뒤 문자를 확인하지 않기 때문에, null문자까지 합친 4개의 길이를 비교해야 한다.

int	is_valid_file_type(char *file_path)
{
	char	**path_divided;
	int		divided_length;
	int		answer;

	answer = 0;
	divided_length = 0;
	path_divided = ft_split(file_path, '.');
	if (!path_divided)
		ft_error("split error");
	while (path_divided[divided_length] != 0)
		divided_length++;
	if (ft_strncmp(path_divided[divided_length - 1], "fdf", 4) == 0)
		answer = 1;
	divided_length = 0;
	free_2d_array(path_divided);
	return (answer);
}

🖋 Init

mlx_new_image의 사이즈 등 파싱 후에 만들어야 할 변수들이 몇 가지 있다.

이를 제외한 나머지 변수들을 초기화해준다.

이번 과제에서는 모든 파일의 변수를 구조체로 만들어 관리하였으며, 함수들에게는 주소값을 넘겨 관리하였다.

t_var	*init(char **argv)
{
	t_var	*var;

	var = (t_var *)malloc(sizeof(t_var));
	if (!var)
		ft_error("var malloc error");
	init_struct(var); // 변수 처리
	init_file(var, argv[1]); //parsing
	calculate_gap(var); 
	init_offset(var); // 좌표처리
	return (var);
}

🖋 parsing

제대로 파싱을 하기 위해선, 파일 내 정보가 적절한지부터 확인을 해야한다.

파일 파싱 순서

서브젝트와 예시파일들을 분석해보았을 때,
어느 한 줄에만 좌표가 더 많다거나 하는 예외는 있을 수 없고,
직사각형의 좌표값이 구해져야 정확한 파일인 것으로 판단하였다.

따라서 처음 파일을 열어 파일의 width, height값을 찾았고,
모든 행에서 같은 크기의 열을 가지는지 확인하였다.

그 후 close한 후 다시 파일을 열어 파싱을 진행하였다.

void	init_file(t_var *var, char *path)
{
	int		fd;
	int		i;

	fd = open(path, O_RDONLY);
	if (fd < 0)
		ft_error("file open error");
	get_map_size(var, fd);
	close(fd);
	if (var->map_height <= 0 || var->map_width <= 0)
		ft_error("file values error: no values");
	i = 0;
	var->map = (int **)malloc(sizeof(int *) * var->map_height);
	if (!(var->map))
		ft_error("map malloc error");
	while (i < var->map_height)
	{
		var->map[i] = malloc(sizeof(int) * var->map_width);
		if (!(var->map[i++]))
			ft_error("map malloc error");
	}
	fd = open(path, O_RDONLY);
	if (fd < 0)
		ft_error("file open error");
	save_map_data(var, fd);
	close(fd);
}

예외처리

또한, 예시파일을 보면 파일 숫자 옆에 ,0xff와 같은 색상정보가 있는 파일들이 있는데,

이는 예전 과제의 잔재로 현 서브젝트(21.04.13)에서는 필요가 없기에 예외처리하였다.

서브젝트에서도 Each Number라는 문구를 확인할 수 있다.

즉 예외처리는 아래와 같이

  • 파일 내 정보가 직사각형의 좌표값으로 구해져 있는지
  • 모든 행에서 같은 크기의 열을 가지고 있는지
  • 파일 내 정보고 숫자로만 이루어져 있는지

를 확인하였다.

🖋 점 좌표 구하기

calculate_gap

파싱한 파일 정보들을 좌표로 변환하기 전, 점과 점 사이의 길이를 정해야 한다.

파일의 첫번째와 두번째 숫자가 각각 0, 4였다고 가정하였을 때,

좌표는 (0,0,0), (0,1,4)가 아니기 때문이다.

처음 점과 점 사이의 거리를 35라고 가정하였고,

Window의 최대 사이즈를 2000, 1500이라고 정한 후,

(점의 가로 개수(파싱한 파일 열의 개수)) * gap > Window_Max_X

||

(점의 세로 개수(파싱한 파일 행의 개수)) * gap > Window_Max_Y

일 경우 gap을 줄여나가 위 조건을 만족하는 형식으로 계산하였다.

init_offset

파일 내 숫자들의 위치가 x, y 값들이며 숫자 value가 z값을 의미하므로,

while (y < var->map_height)
{
	x = 0;
	while (x < var->map_width)
	{
		(var->offset[y][x]).x = x * var->gap;
		(var->offset[y][x]).y = y * var->gap;
		(var->offset[y][x]).z = var->map[y][x];
		x++;
	}
	y++;
}

로 계산하였다.

🖋 투영

처음 구해진 좌표는 위에서 아래를 바라보는 것 마냥 z축의 정보를 볼 수 없다.

좌표를 과제에서 요구한 isometric projection을 통해 변환해주어야 한다.

링크로 대체

[FdF] Graphics
등축투영pdf

void	rotate_x(double *x, double *y, double *z, double rot)
{
	double	prev_x;
	double	prev_y;
	double	prev_z;
	double	theta;

	prev_x = *x;
	prev_y = *y;
	prev_z = *z;
	theta = M_PI / 6 * rot;
	*x = prev_x;
	*y = prev_y * cos(theta) - prev_z * sin(theta);
	*z = prev_y * sin(theta) + prev_z * cos(theta);
}

...

void	ft_isometric(t_var *var, double *x, double *y, double *z)
{
	*z *= var->gap * 0.125 * var->z_height;
	rotate_x(x, y, z, var->theta_x_divider);
	rotate_y(x, y, z, var->theta_y_divider);
	rotate_z(x, y, z, var->theta_z_divider);
}

...

while (++i < var->map_height)
{
	k = -1;
	while (++k < var->map_width)
	{
		ft_isometric(var, &(((var->offset)[i][k]).x),
			&(((var->offset)[i][k]).y), &(((var->offset)[i][k]).z));
	}
}

🖋 window 크기 구하기 및 생성

이미 윈도우 최대 크기를 설정해두었지만, (ex, WINDOW_MAX_X) 이는 대략적인 점과 점 사이를 구하기 위함이고,
좌표 변환으로 인해 변경된 좌표를 살펴보며 정확한 창 크기를 구할 필요가 있다

좌표변환하면서 미리 저장한 X, Y의 최대값과 최소값을 이용해 와이어프레임의 전체크기를 구하고,

창과 와이어프레임 사이의 여백을 조금 주어 창 크기를 계산한다.

void	set_window_size(t_var *var)
{
	var->window_x = (var->max_x - var->min_x) + WINDOW_MARGIN;
	var->window_y = (var->max_y - var->min_y) + WINDOW_MARGIN;
	if (WINDOW_MAX_X + WINDOW_MARGIN < (var->max_x - var->min_x))
		var->window_x = (var->max_x - var->min_x) + WINDOW_MARGIN;
	if (WINDOW_MAX_Y + WINDOW_MARGIN < (var->max_y - var->min_y))
		var->window_y = (var->max_y - var->min_y) + WINDOW_MARGIN;
}

🖋 image align

mlx_image를 생성하여 창에 대입한다.

이때, 이미지의 픽셀 값을 변경할 때 (0, 0)부터 채워넣게 된다면,

여백으로 인해 와이어프레임이 치우쳐저 나올 것이다.

image 좌표 출력시, 좌표 전체를 위/왼쪽 여백많큼 이동시킬 필요가 있다.

🖋 draw

DDA나 bresenham 알고리즘을 사용하여 점과 점 사이를 구할 수 도 있지만,

우리가 중학교 때 배운 점과 점을 알 때 구할 수 있는 일차방정식을 이용하여 해결하였다.

두 점이 다른 x값을 가지고 있다면, 점과 점 사이의 방정식을 이용하고 x를 0.0n씩 증가시키며 해당하는 y값에 픽셀값을 수정하면 된다.

같은 x값을 가지고 있다면, y의 차이만큼만 y의 값을 증가시켜 픽셀값을 수정한다.

static void	draw_edge(t_var *var, t_offset s, t_offset e)
{
	double			x_flag;
	double			x;
	double			y;
	unsigned int	*dst1;

	x = s.x;
	x_flag = 1;
	if (s.x > e.x)
		x_flag = -1;
	if (s와 e의 두 점이 같으면)
		same_x(var, s, e);
	else
	{
		while ((int)x != (int)e.x)
		{
			y = round((e.y - s.y) / (e.x - s.x) * (x - s.x) + s.y); //직선의방정식
			if (창 크기 안에 들어오는 픽셀위치면) // 확대했을 경우 창 밖에 점이 나갈 수 있음
			{
				dst1 = var->img_data->addr + ((int)y * var->img_data->size_line
						+ (int)x * var->img_data->bit_per_pixel / 8);
				*dst1 = create_argb(0, 255, 0, 0);
			}
			x += (0.05 * x_flag);
		}
	}
}

void	draw(t_var *var)
{
	int		x;
	int		y;

	y = 0;
	while (y < var->map_height)
	{
		x = 0;
		while (x < var->map_width)
		{
			if (x + 1 < var->map_width)
				draw_edge(var, var->offset[y][x], var->offset[y][x + 1]);
			if (y + 1 < var->map_height)
				draw_edge(var, var->offset[y][x], var->offset[y + 1][x]);
			x++;
		}
		y++;
	}
}

Bonus

🖋 mlx_loop_hook

회전 및 이동 등의 좌표변화가 있을 경우, 변환된 좌표로 다시 그림을 그려야 한다.

mlx_loop_hook함수를 이용하여 좌표변환 ~ draw 함수까지의 절차를 loop해야한다.

int	main_loop(t_var *var)
{
	int	i;
	int	k;

	i = -1;
	k = -1;
	free_offset(var->offset, var->map_height);
	init_offset(var);
	init_max_min_data(var);
	projection(var);
	mlx_destroy_image(var->mlx, var->img_data->img);
	init_mlx_image(var);
	set_image_align(var);
	translate(var);
	draw(var);
	mlx_put_image_to_window(var->mlx, var->win, var->img_data->img, 0, 0);
	return (0);
}

💡 메모리 릭을 잘 검사해야 한다.

🖋 mlx_hook

키보드, 마우스 등의 입력값을 대기하고 있다가, 어느 입력이 들어오면 설정한 함수를 실행시킬 수 있다.

mlx_hook(var->win, 02, 1L << 0, hooks, var);
mlx_hook(var->win, 17, 1L << 5, red_cross_hook, var);

//02 -> button 클릭
//17 -> window distroy

int	hooks(int keycode, t_var *var)
{
	if (keycode == KEY_ESC)
		esc_hook(var);
	if (keycode == KEY_PLUS)
		zoom_in_hook(var);
	if (keycode == KEY_MINUS)
		zoom_out_hook(var);
	if (keycode == KEY_UP || keycode == KEY_DOWN
		|| keycode == KEY_LEFT || keycode == KEY_RIGHT)
		trans_hook(var, keycode);
	if (keycode == KEY_ROTATE_Y_CLOCK || keycode == KEY_ROTATE_Y_CLOCK_R
		|| keycode == KEY_ROTATE_X_CLOCK || keycode == KEY_ROTATE_X_CLOCK_R
		|| keycode == KEY_ROTATE_Z_CLOCK || keycode == KEY_ROTATE_Z_CLOCK_R)
		rotate_hook(var, keycode);
	if (keycode == KEY_Z_HEIGHT_UP || keycode == KEY_Z_HEIGHT_DOWN)
		z_height_hook(var, keycode);
	return (1);
}
profile
장안동 개발새발

0개의 댓글