김현우
  • FDF - 와이어프레임 모델
    2026년 04월 12일 01시 19분 33초에 업로드 된 글입니다.
    작성자: kugorang

    들어가며

    FDF는 프랑스어 ‘fil de fer’의 약자로, ‘와이어프레임 모델’을 의미한다.

    fdf.fdf 파일 안에 들어 있는 숫자를 읽고, 각 점을 선으로 연결해서 3차원 지형의 와이어프레임(wireframe) 을 창에 그리는 과제다.
    여기서 와이어프레임은 면을 채우지 않고 선만 그리는 형태를 뜻한다.

    이 글은 fdfmandatorybonus를 구현할 때 반드시 알아야 하는 개념을, 아주 쉬운 말로 하나씩 정리한 글이다.


    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, write
    • malloc, 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는 보통 아래 순서로 동작한다.

    1. .fdf 파일을 연다
    2. 파일의 숫자를 읽는다
    3. 각 숫자를 (x, y, z) 점으로 생각한다
    4. 이 점을 화면에 맞는 (screen_x, screen_y)로 바꾼다
    5. 오른쪽 점, 아래 점과 선으로 연결한다
    6. 모든 선을 이미지 버퍼에 그린다
    7. 이미지를 창에 올린다
    8. 키 입력과 창 닫기 이벤트를 처리한다

    한 줄로 쓰면 아래와 같다.

    파일 읽기 -> 지도 저장 -> 좌표 계산 -> 투영 -> 선 그리기 -> 이미지 출력 -> 이벤트 처리

    이 흐름을 이해하면 과제 전체가 한 번에 정리된다.


    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번 읽기

    가장 단순한 방법은 파일을 두 번 읽는 것이다.

    1. 첫 번째 읽기에서 width, height를 센다
    2. 그 크기에 맞게 메모리를 할당한다
    3. 두 번째 읽기에서 실제 숫자를 넣는다

    이 방식은 이해하기 쉽고 구현도 단순하다.

    어떤 함수가 잘 맞는가

    • 한 줄씩 읽기: 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에서 가장 중요한 것은 계산 순서다.
    보통 아래 순서로 처리한다.

    1. x, y, z 원본 값 만들기
    2. 모델 중심을 기준으로 좌표 옮기기
    3. zoom 적용하기
    4. z_scale 적용하기
    5. 회전 적용하기 (bonus)
    6. 투영 적용하기
    7. 화면 중앙으로 평행 이동하기
    8. 최종 픽셀 좌표로 사용하기

    순서를 바꾸면 결과도 달라진다.

    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)

    728x90

    과제 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이 적용된 값이어야 한다
    • pzz_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 방식의 생각 순서

    1. dx = x1 - x0
    2. dy = y1 - y0
    3. 둘 중 더 큰 거리만큼 몇 번 찍을지 정한다
    4. 한 번에 x를 얼마나 움직일지 계산한다
    5. 한 번에 y를 얼마나 움직일지 계산한다
    6. 반복하면서 픽셀을 찍는다

    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라고 적고 있다.
    즉, 보통 아래 순서가 필요하다.

    1. mlx_new_image()로 이미지 만들기
    2. mlx_get_data_addr()로 이미지 메모리 주소 받기
    3. 직접 픽셀을 기록하기
    4. 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() 함수 하나를 만들어서 아래 순서로 처리하면 편하다.

    1. 이미지 버퍼를 배경색으로 비우기
    2. 모든 점을 기준으로 선을 다시 계산해서 그리기
    3. 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()는 다음 일을 한다.

    1. 첫 번째 점의 z를 읽는다
    2. 두 번째 점의 z를 읽는다
    3. 두 점을 각각 화면 좌표로 변환한다
    4. 그 두 화면 점 사이에 선을 그린다

    점 하나를 화면 좌표로 바꾸는 함수를 따로 두면 좋다

    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() 같은 함수를 만들어서 아래 순서로 끝내면 된다.

    1. 이미지 제거
    2. 창 제거
    3. map 해제
    4. 필요한 MLX 정리
    5. 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.c
    • init.c
    • parse.c
    • render.c
    • project.c
    • rotate.c
    • line.c
    • hooks.c
    • free.c
    • fdf.h

    이렇게 역할을 나누면 평가 중 작은 수정 요청이 들어와도 바꾸기 쉽다.

    README도 빼먹으면 안 된다

    현재 PDF는 저장소 루트에 README.md를 요구한다.
    최소한 아래 내용이 있어야 한다.

    • 첫 줄: 이탤릭체 문장
    • Description
    • Instructions
    • Resources
    • AI를 어디에 어떻게 사용했는지 설명

    첫 줄은 정확히 아래 형식이다.

    *This project has been created as part of the 42 curriculum by <login1>[, <login2>[, <login3>[...]]].*

    demo용 .fdf 파일도 준비하자

    제출 파일 목록에는 demo .fdf 파일도 포함된다.
    작은 테스트 맵과 큰 테스트 맵을 같이 준비해 두면 좋다.


    15. Bonus를 하려면 추가로 알아야 하는 것

    728x90

    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까지 한 번에 넣지 말고 아래 순서로 가는 것이 가장 안전하다.

    1. .fdf 파일 읽기
    2. width, height, z[y][x] 저장
    3. 빈 창 열기
    4. 이미지 생성하기
    5. put_pixel() 만들기
    6. 선 그리기 함수 만들기
    7. 투영 없이 평면 격자부터 그리기
    8. z 높이 적용하기
    9. 등각 투영 적용하기
    10. 화면 중앙 정렬하기
    11. ESC와 창 닫기 처리
    12. 메모리 누수 정리
    13. bonus로 zoom
    14. bonus로 translate
    15. bonus로 rotate
    16. bonus로 projection toggle
    17. 마지막 bonus 1개 추가

    이 순서를 지키면 어디서 문제가 생겼는지 찾기 쉽다.


    17. 자주 틀리는 부분

    1) mlx_pixel_put()만 사용하기

    mandatory는 image 사용이 필수다.
    반드시 이미지 버퍼 방식으로 그려야 한다.

    2) 전역 변수 사용

    금지다.
    구조체 하나에 상태를 모으는 쪽이 안전하다.

    3) 화면 밖 좌표를 그대로 쓰기

    범위 체크가 없으면 잘못된 메모리에 접근할 수 있다.

    4) zoomz_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의 본질은 분명하다.

    1. 파일을 읽는다
    2. 좌표를 만든다
    3. 3차원 좌표를 2차원으로 바꾼다
    4. 점들을 선으로 잇는다
    5. 이미지를 창에 보여준다
    6. 입력에 따라 다시 계산해서 다시 그린다

    이 과제에서 진짜 중요한 것은 복잡한 수학 자체보다 아래 세 가지다.

    • 데이터를 깔끔하게 저장하는 것
    • 좌표 계산 순서를 정확히 지키는 것
    • clean exit까지 완성하는 것

    이 세 가지를 이해하면 mandatory를 끝낼 수 있고,
    그 위에 zoom, translate, rotate, projection toggle을 얹어서 bonus까지 확장할 수 있다.

    728x90
    728x90
    댓글