FDF - 와이어프레임 모델
들어가며

fdf는 .fdf 파일 안에 들어 있는 숫자를 읽고, 각 점을 선으로 연결해서 3차원 지형의 와이어프레임(wireframe) 을 창에 그리는 과제다.
여기서 와이어프레임은 면을 채우지 않고 선만 그리는 형태를 뜻한다.
이 글은 fdf의 mandatory와 bonus를 구현할 때 반드시 알아야 하는 개념을, 아주 쉬운 말로 하나씩 정리한 글이다.
1. 과제에서 정확히 요구하는 것
Mandatory에서 해야 하는 것
- 프로그램 이름은
fdf - 실행 인자로
.fdf파일 하나를 받기 - 파일 안의 숫자를 이용해 점
(x, y, z)를 만들기 - 점들을 선분으로 연결해서 지형 그리기
- 결과를 창(window) 안에 보여주기
- 반드시 isometric projection(등각 투영) 으로 보여주기
- 반드시 MiniLibX의 image 를 사용해서 그리기
ESC키로 정상 종료하기- 창의 닫기 버튼으로 정상 종료하기
- 창 전환, 최소화 등 기본 창 관리가 자연스럽게 동작하기
- 전역 변수 사용 금지
- 메모리 누수 없이 종료하기
Bonus에서 추가로 하면 좋은 것
- 추가 투영 1개
- 확대/축소
- 이동
- 회전
- 추가 보너스 1개
꼭 기억할 규칙
- bonus는 mandatory가 완벽할 때만 평가된다
- Makefile은
$(NAME),all,clean,fclean,re규칙이 있어야 한다 - bonus를 제출할 때는
bonus규칙도 있어야 한다 libft를 쓰면libft/폴더와 그 Makefile도 같이 준비해야 한다- bonus 파일/함수는
_bonus.c,_bonus.h형태로 분리하는 습관이 안전하다
사용할 수 있는 주요 함수
open,close,read,writemalloc,free,perror,strerror,exit- 수학 라이브러리 함수들 (
sin,cos,fabs등) - MiniLibX 함수들
gettimeofday()- 직접 만든
ft_printf또는 그와 같은 출력 함수 libft사용 가능
2. 먼저 단어부터 정확히 알기
- 픽셀(pixel): 화면 위의 가장 작은 점
- 점(point): 지도 위의 한 칸을 뜻하는 데이터
- 선분(line segment): 점 두 개를 잇는 선
- 좌표(coordinate): 위치를 숫자로 나타낸 것
- x: 가로 위치
- y: 세로 위치
- z: 높이
- 투영(projection): 3차원 좌표를 2차원 화면 좌표로 바꾸는 계산
- 평행 이동(translate): 전체 모델을 왼쪽/오른쪽/위/아래로 옮기는 것
- 회전(rotate): 좌표를 일정 각도만큼 돌리는 것
- zoom: 점 사이 간격을 키우거나 줄이는 값
- z_scale: 높이
z를 더 높게 또는 더 낮게 보이게 하는 값 - image buffer: 화면에 올리기 전에 픽셀을 미리 그려 두는 메모리 공간
여기서 가장 중요한 사실은 이것이다.
- 원본 데이터는 3차원 좌표
- 실제 모니터는 2차원 화면
그래서 fdf의 핵심은 좌표를 바꾸는 계산이다.
3. fdf가 실제로 하는 일 전체 흐름
fdf는 보통 아래 순서로 동작한다.
.fdf파일을 연다- 파일의 숫자를 읽는다
- 각 숫자를
(x, y, z)점으로 생각한다 - 이 점을 화면에 맞는
(screen_x, screen_y)로 바꾼다 - 오른쪽 점, 아래 점과 선으로 연결한다
- 모든 선을 이미지 버퍼에 그린다
- 이미지를 창에 올린다
- 키 입력과 창 닫기 이벤트를 처리한다
한 줄로 쓰면 아래와 같다.
파일 읽기 -> 지도 저장 -> 좌표 계산 -> 투영 -> 선 그리기 -> 이미지 출력 -> 이벤트 처리
이 흐름을 이해하면 과제 전체가 한 번에 정리된다.
4. .fdf 파일은 어떻게 읽어야 하는가
예를 들어 파일이 아래와 같다고 하자.
0 0 0
0 10 0
0 0 0
이 뜻은 다음과 같다.
- 첫 번째 줄은
y = 0 - 두 번째 줄은
y = 1 - 세 번째 줄은
y = 2 - 각 줄 안에서 왼쪽부터
x = 0, 1, 2 - 숫자 값은
z
즉, 가운데 점만 높이가 10인 지도다.
좌표를 만드는 법
- 파일의 열 번호가
x - 파일의 행 번호가
y - 그 칸의 숫자가
z
그래서 가장 단순한 저장 방식은 아래와 같다.
map->z[y][x]
너비와 높이
width: 한 줄에 있는 숫자 개수height: 전체 줄 수
파싱에서 알아야 하는 것
과제 PDF는 지도가 정상 형식이라고 가정한다고 말한다.
즉, 이 과제의 중심은 복잡한 입력 검증이 아니라 렌더링이다.
하지만 그렇다고 해서 프로그램이 죽으면 안 된다.
최소한 아래는 처리해야 한다.
- 파일 열기 실패
- 메모리 할당 실패
- 빈 파일
- 이미지 생성 실패
가장 쉬운 파싱 방법: 2번 읽기
가장 단순한 방법은 파일을 두 번 읽는 것이다.
- 첫 번째 읽기에서
width,height를 센다 - 그 크기에 맞게 메모리를 할당한다
- 두 번째 읽기에서 실제 숫자를 넣는다
이 방식은 이해하기 쉽고 구현도 단순하다.
어떤 함수가 잘 맞는가
- 한 줄씩 읽기:
get_next_line() - 공백 기준으로 나누기:
ft_split() - 문자열을 정수로 바꾸기:
ft_atoi()또는 직접 만든 변환 함수
음수 높이도 생각하기
높이값 z는 음수일 수도 있다.
그래서 z는 보통 int로 저장한다.
색 정보에 대해
현재 PDF 본문에는 mandatory에서 색 파싱을 필수라고 직접 적지 않는다.
그래서 먼저 높이값만 정확하게 처리하는 구현을 끝내는 것이 좋다.
5. 전역 변수 없이 짜려면 구조체가 거의 필수다
이 과제는 전역 변수 금지다.
따라서 프로그램 전체 상태를 구조체 안에 넣어야 한다.
가장 많이 쓰는 구조는 아래와 비슷하다.
typedef struct s_map
{
int width;
int height;
int **z;
} t_map;
typedef struct s_img
{
void *img;
char *addr;
int bpp;
int line_len;
int endian;
} t_img;
typedef struct s_view
{
double zoom;
double z_scale;
double angle;
double shift_x;
double shift_y;
double rot_x;
double rot_y;
double rot_z;
int projection;
} t_view;
typedef struct s_app
{
void *mlx;
void *win;
t_map map;
t_img img;
t_view view;
} t_app;
왜 이렇게 나누는가
t_map: 파일에서 읽은 원본 데이터t_img: 화면에 그릴 이미지 버퍼 정보t_view: 지금 어떤 크기와 각도로 보여줄지에 대한 값t_app: 프로그램 전체 상태
이렇게 하면 함수마다 t_app *app 하나만 넘겨도 필요한 정보 대부분을 사용할 수 있다.
아주 중요한 설계 원칙
원본 map은 그대로 두고, 화면에 그릴 때마다 다시 계산하는 구조가 가장 안전하다.
왜냐하면 bonus가 들어가면
- zoom이 바뀌고
- shift가 바뀌고
- rotation이 바뀌고
- projection이 바뀌기 때문이다
이미 변환된 좌표를 또 변환하는 식으로 누적 계산하면 오차가 쌓이고 디버깅이 어려워진다.
6. 좌표 계산 순서를 이해하면 절반은 끝난다
fdf에서 가장 중요한 것은 계산 순서다.
보통 아래 순서로 처리한다.
x,y,z원본 값 만들기- 모델 중심을 기준으로 좌표 옮기기
zoom적용하기z_scale적용하기- 회전 적용하기 (bonus)
- 투영 적용하기
- 화면 중앙으로 평행 이동하기
- 최종 픽셀 좌표로 사용하기
순서를 바꾸면 결과도 달라진다.
1) 먼저 격자 좌표를 만든다
파일의 칸 번호를 그대로 쓰면 된다.
px = x;
py = y;
pz = map->z[y][x];
2) 모델 중심을 기준으로 옮긴다
회전과 중앙 정렬을 쉽게 하려면 모델 중심이 (0, 0) 근처에 오도록 맞추는 것이 좋다.
px = x - (map->width - 1) / 2.0;
py = y - (map->height - 1) / 2.0;
pz = map->z[y][x];
이 계산을 하지 않으면 모델이 왼쪽 위를 기준으로 돌아서 다루기 어렵다.
3) zoom 적용
zoom은 점 사이 간격을 조절한다.
px *= view->zoom;
py *= view->zoom;
4) z_scale 적용
z_scale은 높이를 더 크게 또는 더 작게 보이게 한다.
pz *= view->z_scale;
여기서 중요한 것은 이것이다.
zoom: 가로/세로 간격 조절z_scale: 높이 강조 정도 조절
둘은 완전히 다른 값이다.
반드시 분리해서 생각해야 한다.
처음 시작할 때 적당한 zoom
맵이 아주 크면 처음 실행했을 때 화면 밖으로 나갈 수 있다.
그래서 처음에는 대충 아래처럼 잡는 경우가 많다.
view->zoom = fmin(WIN_W / (double)(map->width + 1),
WIN_H / (double)(map->height + 1)) * 0.5;
정답 공식이 하나로 정해진 것은 아니다.
핵심은 처음 시작할 때 모델이 창 안에 들어오게 만드는 것이다.
7. Mandatory의 핵심: 등각 투영(isometric projection)
과제 PDF는 mandatory에서 등각 투영을 요구한다.
등각 투영은 3차원 좌표 (x, y, z)를 2차원 화면 좌표 (screen_x, screen_y)로 바꾸는 계산이다.
많이 쓰는 기본 공식은 아래와 같다.
angle = 30 * M_PI / 180.0;
iso_x = (px - py) * cos(angle);
iso_y = (px + py) * sin(angle) - pz;
그리고 마지막에 화면 중앙으로 옮긴다.
screen_x = iso_x + view->shift_x;
screen_y = iso_y + view->shift_y;
이 공식에서 중요한 점
px,py는 이미 중심 정렬과zoom이 적용된 값이어야 한다pz는z_scale이 적용된 값이어야 한다shift_x,shift_y는 보통 창 중심에서 시작한다
예:
view->shift_x = WIN_W / 2.0;
view->shift_y = WIN_H / 2.0;
왜 마지막에 shift를 더하는가
투영만 하면 좌표 중심이 (0, 0) 근처에 있다.
그런데 화면의 (0, 0)은 왼쪽 위이다.
그래서 화면 중앙 쪽으로 옮겨야 모델이 보기 좋은 위치에 나온다.
mandatory에서 꼭 기억할 점
mandatory는 자유로운 시점이 아니라 등각 투영 결과가 나오면 된다.
그러므로 먼저 등각 투영 하나를 정확히 완성하는 것이 가장 중요하다.
8. 점만 계산하면 끝이 아니다: 선을 그려야 한다
fdf는 점을 찍는 과제가 아니라 점들을 선으로 연결하는 과제다.
보통 한 점에서 아래 두 방향만 연결하면 충분하다.
- 오른쪽 점
- 아래쪽 점
예를 들어 (x, y) 점에서
(x + 1, y)가 있으면 선을 그리고(x, y + 1)가 있으면 선을 그린다
이렇게 하면 필요한 선이 모두 그려진다.
왜 오른쪽과 아래만 연결하는가
왼쪽과 위쪽까지 모두 연결하면 같은 선을 두 번 그리게 된다.
따라서 중복 없이 그리려면 오른쪽, 아래만 처리하는 방식이 가장 단순하다.
9. 선 그리기 알고리즘은 꼭 알아야 한다
핵심은 두 점 사이를 여러 개의 픽셀로 채우는 것이다.
가장 이해하기 쉬운 방식은 DDA 방식이다.
DDA 방식의 생각 순서
dx = x1 - x0dy = y1 - y0- 둘 중 더 큰 거리만큼 몇 번 찍을지 정한다
- 한 번에 x를 얼마나 움직일지 계산한다
- 한 번에 y를 얼마나 움직일지 계산한다
- 반복하면서 픽셀을 찍는다
DDA 예시 코드
void draw_line(t_img *img, t_point a, t_point b, int color)
{
double dx;
double dy;
double step;
double x_inc;
double y_inc;
double x;
double y;
int i;
dx = b.x - a.x;
dy = b.y - a.y;
step = fabs(dx) > fabs(dy) ? fabs(dx) : fabs(dy);
if (step == 0)
{
put_pixel(img, round(a.x), round(a.y), color);
return ;
}
x_inc = dx / step;
y_inc = dy / step;
x = a.x;
y = a.y;
i = 0;
while (i <= step)
{
put_pixel(img, round(x), round(y), color);
x += x_inc;
y += y_inc;
i++;
}
}
Bresenham도 많이 쓴다
많은 구현에서 Bresenham 알고리즘도 사용한다.
그 알고리즘도 좋다.
하지만 처음 이해하기에는 DDA가 더 단순하다.
중요한 것은 어떤 알고리즘을 쓰든 두 점 사이를 끊기지 않게 그릴 수 있느냐이다.
선 색은 어떻게 할까
현재 PDF 기준 mandatory의 핵심은 정확한 렌더링과 동작이다.
가장 단순하게는 모든 선을 같은 색으로 그려도 된다.
나중에 bonus로 높이에 따른 색 변화를 추가할 수 있다.
10. Mandatory에서 매우 중요함: mlx_pixel_put만으로 끝내면 안 된다
과제 PDF는 MiniLibX의 image 사용이 mandatory라고 적고 있다.
즉, 보통 아래 순서가 필요하다.
mlx_new_image()로 이미지 만들기mlx_get_data_addr()로 이미지 메모리 주소 받기- 직접 픽셀을 기록하기
mlx_put_image_to_window()로 창에 올리기
이미지 버퍼가 왜 중요한가
- 픽셀을 하나씩 창에 바로 찍는 방식보다 관리하기 쉽다
- bonus에서 다시 그리기(re-render)가 편하다
- 한 번에 이미지를 갱신하는 구조를 만들기 좋다
기본 put_pixel 함수 예시
void put_pixel(t_img *img, int x, int y, int color)
{
char *dst;
if (x < 0 || y < 0 || x >= WIN_W || y >= WIN_H)
return ;
dst = img->addr + (y * img->line_len + x * (img->bpp / 8));
*(unsigned int *)dst = color;
}
이 계산이 뜻하는 것
y * line_len: y번째 줄까지 이동x * (bpp / 8): 그 줄 안에서 x번째 픽셀 위치까지 이동- 그 위치에 색 값을 기록
반드시 해야 하는 범위 체크
화면 밖 좌표에 쓰면 잘못된 메모리를 건드릴 수 있다.
그래서 put_pixel() 안에서 x, y 범위를 먼저 검사해야 한다.
다시 그릴 때의 기본 패턴
render() 함수 하나를 만들어서 아래 순서로 처리하면 편하다.
- 이미지 버퍼를 배경색으로 비우기
- 모든 점을 기준으로 선을 다시 계산해서 그리기
mlx_put_image_to_window()호출
이 패턴을 쓰면 zoom, translate, rotate가 바뀔 때마다 render()만 다시 호출하면 된다.
11. 전체 렌더링 함수는 보통 이런 흐름이다
void render(t_app *app)
{
int x;
int y;
clear_image(&app->img);
y = 0;
while (y < app->map.height)
{
x = 0;
while (x < app->map.width)
{
if (x + 1 < app->map.width)
draw_segment(app, x, y, x + 1, y);
if (y + 1 < app->map.height)
draw_segment(app, x, y, x, y + 1);
x++;
}
y++;
}
mlx_put_image_to_window(app->mlx, app->win, app->img.img, 0, 0);
}
여기서 draw_segment()는 다음 일을 한다.
- 첫 번째 점의
z를 읽는다 - 두 번째 점의
z를 읽는다 - 두 점을 각각 화면 좌표로 변환한다
- 그 두 화면 점 사이에 선을 그린다
점 하나를 화면 좌표로 바꾸는 함수를 따로 두면 좋다
t_screen project_point(t_app *app, int x, int y)
{
t_screen p;
double px;
double py;
double pz;
px = x - (app->map.width - 1) / 2.0;
py = y - (app->map.height - 1) / 2.0;
pz = app->map.z[y][x];
px *= app->view.zoom;
py *= app->view.zoom;
pz *= app->view.z_scale;
apply_rotation(&px, &py, &pz, &app->view);
apply_projection(&px, &py, pz, &app->view);
p.x = px + app->view.shift_x;
p.y = py + app->view.shift_y;
return (p);
}
실제 코드 모양은 달라도 괜찮다.
핵심은 점 하나를 화면 점으로 바꾸는 책임을 한 함수에 모으는 것이다.
12. 입력과 창 닫기 처리
mandatory에서 꼭 필요한 이벤트는 두 가지다.
ESC키로 종료- 창 닫기 버튼으로 종료
bonus가 들어가면 여기에 zoom, move, rotate 키가 추가된다.
기본 패턴
- 키 입력 함수에서 값 변경
- 값이 바뀌면
render()호출 - 종료 이벤트에서는 자원 정리 후
exit(0)
int handle_key(int keycode, t_app *app)
{
if (keycode == KEY_ESC)
close_app(app);
render(app);
return (0);
}
창 닫기 버튼도 같은 종료 함수로 보내기
창의 닫기 버튼을 눌렀을 때도 close_app()로 들어가게 만들면 정리 흐름을 한 곳에 모을 수 있다.
창 관리가 부드러워야 한다는 뜻
- 창을 다른 창 뒤로 보냈다가 다시 가져와도 멈추지 않아야 한다
- 최소화/복원 뒤에도 정상 동작해야 한다
- 이벤트 루프를 막는 긴 무한 루프를 두지 않는 것이 안전하다
이벤트 번호와 키 코드는 환경에 따라 다를 수 있다
MiniLibX는 사용하는 환경에 따라 키 코드와 이벤트 상수가 다를 수 있다.
그래서 자신의 MLX 헤더와 캠퍼스 환경을 기준으로 확인하는 습관이 중요하다.
13. 메모리 관리와 종료 처리는 필수다
fdf는 그래픽 과제이지만, 평가에서 메모리 관리도 매우 중요하다.
반드시 정리해야 하는 것
- 파일 디스크립터
ft_split()로 만든 문자열 배열- 맵의 2차원 배열
- MiniLibX 이미지
- MiniLibX 창
- MLX 컨텍스트
한 곳에서 정리하는 함수가 좋다
void free_map(t_map *map)
{
int i;
i = 0;
while (i < map->height)
{
free(map->z[i]);
i++;
}
free(map->z);
}
그리고 close_app() 같은 함수를 만들어서 아래 순서로 끝내면 된다.
- 이미지 제거
- 창 제거
- map 해제
- 필요한 MLX 정리
exit(0)
에러가 나도 clean exit
예를 들어 아래 상황이 생길 수 있다.
- 파일 열기 실패
malloc실패- 이미지 생성 실패
- 창 생성 실패
이때도 그냥 중간에 끝내지 말고, 이미 만든 것들만 골라서 정리한 뒤 종료해야 한다.
Linux MLX에서 자주 보는 마무리
환경에 따라 아래가 필요한 경우가 많다.
mlx_destroy_image(mlx, img);
mlx_destroy_window(mlx, win);
mlx_destroy_display(mlx);
free(mlx);
현재 사용하는 MLX 버전에 맞는 종료 함수를 꼭 확인하자.
14. Makefile과 파일 구성도 평가 대상이다
Makefile에서 체크할 것
cc-Wall -Wextra -Werror- 불필요한 relink 금지
- 규칙:
$(NAME),all,clean,fclean,re - bonus 제출 시
bonus규칙 추가
libft를 쓴다면
libft/폴더 안에 소스와 Makefile 넣기- 프로젝트 Makefile에서 먼저 libft를 빌드한 뒤
fdf를 빌드하기
추천 파일 분리 예시
main.cinit.cparse.crender.cproject.crotate.cline.chooks.cfree.cfdf.h
이렇게 역할을 나누면 평가 중 작은 수정 요청이 들어와도 바꾸기 쉽다.
README도 빼먹으면 안 된다
현재 PDF는 저장소 루트에 README.md를 요구한다.
최소한 아래 내용이 있어야 한다.
- 첫 줄: 이탤릭체 문장
DescriptionInstructionsResources- AI를 어디에 어떻게 사용했는지 설명
첫 줄은 정확히 아래 형식이다.
*This project has been created as part of the 42 curriculum by <login1>[, <login2>[, <login3>[...]]].*
demo용 .fdf 파일도 준비하자
제출 파일 목록에는 demo .fdf 파일도 포함된다.
작은 테스트 맵과 큰 테스트 맵을 같이 준비해 두면 좋다.
15. Bonus를 하려면 추가로 알아야 하는 것
bonus의 핵심은 이것이다.
원본 map은 그대로 두고, 보여주는 방식만 바꾼다.
그래서 t_view 구조체 안에 값을 넣어 두고, 입력이 들어올 때마다 바꾼 뒤 render()를 다시 호출하는 구조가 가장 좋다.
15-1. 확대/축소(zoom)
가장 쉽다.view.zoom 값만 바꾸고 다시 render() 하면 된다.
if (keycode == KEY_PLUS)
app->view.zoom *= 1.1;
if (keycode == KEY_MINUS)
app->view.zoom *= 0.9;
15-2. 이동(translate)
shift_x, shift_y를 바꾸면 된다.
if (keycode == KEY_LEFT)
app->view.shift_x -= 10;
if (keycode == KEY_RIGHT)
app->view.shift_x += 10;
if (keycode == KEY_UP)
app->view.shift_y -= 10;
if (keycode == KEY_DOWN)
app->view.shift_y += 10;
15-3. 회전(rotate)
회전은 bonus에서 가장 수학적인 부분이다.
보통 x, y, z 축 회전을 따로 함수로 만든다.
x축 회전
new_y = y * cos(a) - z * sin(a);
new_z = y * sin(a) + z * cos(a);
y축 회전
new_x = x * cos(b) + z * sin(b);
new_z = -x * sin(b) + z * cos(b);
z축 회전
new_x = x * cos(c) - y * sin(c);
new_y = x * sin(c) + y * cos(c);
회전에서 꼭 기억할 것
- 각도는 보통 라디안으로 계산한다
cos,sin을 쓰려면math.h와-lm이 필요하다- 회전은 중심 정렬 후 적용하는 것이 좋다
- 회전 순서를 바꾸면 결과도 달라진다
도 단위를 라디안으로 바꾸기
radian = degree * M_PI / 180.0;
15-4. 추가 투영 1개
가장 쉬운 추가 투영은 평행 투영 계열이다.
예를 들어 아래처럼 아주 단순한 형태를 만들 수 있다.
proj_x = x;
proj_y = y - z;
이 값도 마지막에는 shift_x, shift_y를 더해서 화면으로 옮기면 된다.
핵심은 투영 방식을 키 하나로 바꿀 수 있게 만드는 것이다.
if (keycode == KEY_P)
app->view.projection = PROJ_PARALLEL;
if (keycode == KEY_I)
app->view.projection = PROJ_ISO;
15-5. 추가 bonus 1개는 무엇이 쉬운가
초보자에게 비교적 쉬운 선택은 아래 중 하나다.
- 초기 시점으로 되돌리는
reset키 z_scale조절- 현재 projection 이름이나 조작 키를 화면에 표시
- 높이에 따라 색을 다르게 주기
가장 구현이 단순한 것은 보통 reset 또는 z_scale 조절이다.
bonus에서 다른 함수 사용
PDF는 bonus를 위해 다른 함수 사용도 허용할 수 있다고 말한다.
하지만 평가 때 왜 썼는지 설명할 수 있어야 한다.
16. 구현 순서 추천
처음부터 bonus까지 한 번에 넣지 말고 아래 순서로 가는 것이 가장 안전하다.
.fdf파일 읽기width,height,z[y][x]저장- 빈 창 열기
- 이미지 생성하기
put_pixel()만들기- 선 그리기 함수 만들기
- 투영 없이 평면 격자부터 그리기
z높이 적용하기- 등각 투영 적용하기
- 화면 중앙 정렬하기
ESC와 창 닫기 처리- 메모리 누수 정리
- bonus로 zoom
- bonus로 translate
- bonus로 rotate
- bonus로 projection toggle
- 마지막 bonus 1개 추가
이 순서를 지키면 어디서 문제가 생겼는지 찾기 쉽다.
17. 자주 틀리는 부분
1) mlx_pixel_put()만 사용하기
mandatory는 image 사용이 필수다.
반드시 이미지 버퍼 방식으로 그려야 한다.
2) 전역 변수 사용
금지다.
구조체 하나에 상태를 모으는 쪽이 안전하다.
3) 화면 밖 좌표를 그대로 쓰기
범위 체크가 없으면 잘못된 메모리에 접근할 수 있다.
4) zoom과 z_scale을 섞어 쓰기
둘은 다른 값이다.
5) 회전 전에 중심 정렬을 하지 않기
모델이 원하지 않는 기준점으로 돌아서 다루기 어렵다.
6) 소수 계산을 너무 일찍 int로 바꾸기
중간 계산은 double이 더 안전하다.
마지막에 픽셀로 찍을 때만 반올림하거나 형변환하자.
7) 같은 선을 여러 번 그리기
오른쪽과 아래쪽만 연결하면 충분하다.
8) 에러 처리 없이 바로 끝내기
이미 할당한 메모리와 MLX 자원을 먼저 정리해야 한다.
9) mandatory가 완벽하지 않은데 bonus부터 붙이기
이 과제는 mandatory가 먼저다.
10) 코드 설명을 못 하기
평가 중에는 작은 수정 요청이 들어올 수 있다.
각 함수가 정확히 무슨 일을 하는지 설명할 수 있어야 한다.
18. 평가 전에 스스로 확인할 체크리스트
-
.fdf파일 하나를 인자로 받아서 실행되는가 - 창이 열리는가
- 등각 투영으로 와이어프레임이 보이는가
- 오른쪽/아래 이웃 연결이 제대로 되는가
- 이미지 버퍼를 사용했는가
-
ESC로 정상 종료되는가 - 창 닫기 버튼으로 정상 종료되는가
- 메모리 누수가 없는가
- 전역 변수가 없는가
- Makefile 규칙이 맞는가
-
libft연결이 올바른가 - README.md가 있는가
- demo
.fdf파일이 있는가 - bonus를 넣었다면
bonus규칙과_bonus파일 구성이 맞는가 - bonus를 직접 설명할 수 있는가
19. 핵심 공식만 다시 보기
중심 정렬
px = x - (width - 1) / 2.0;
py = y - (height - 1) / 2.0;
pz = z;
zoom / z_scale
px *= zoom;
py *= zoom;
pz *= z_scale;
isometric projection
angle = 30 * M_PI / 180.0;
screen_x = (px - py) * cos(angle) + shift_x;
screen_y = (px + py) * sin(angle) - pz + shift_y;
x축 회전
new_y = y * cos(a) - z * sin(a);
new_z = y * sin(a) + z * cos(a);
y축 회전
new_x = x * cos(b) + z * sin(b);
new_z = -x * sin(b) + z * cos(b);
z축 회전
new_x = x * cos(c) - y * sin(c);
new_y = x * sin(c) + y * cos(c);
픽셀 주소 계산
dst = img->addr + (y * img->line_len + x * (img->bpp / 8));
마치며
fdf의 본질은 분명하다.
- 파일을 읽는다
- 좌표를 만든다
- 3차원 좌표를 2차원으로 바꾼다
- 점들을 선으로 잇는다
- 이미지를 창에 보여준다
- 입력에 따라 다시 계산해서 다시 그린다
이 과제에서 진짜 중요한 것은 복잡한 수학 자체보다 아래 세 가지다.
- 데이터를 깔끔하게 저장하는 것
- 좌표 계산 순서를 정확히 지키는 것
- clean exit까지 완성하는 것
이 세 가지를 이해하면 mandatory를 끝낼 수 있고,
그 위에 zoom, translate, rotate, projection toggle을 얹어서 bonus까지 확장할 수 있다.