第4章:実装ガイドとベストプラクティス
4.1 プロジェクト構成
4.1.1 ディレクトリ構造
fdf/
├── Makefile
├── includes/
│ └── fdf.h
├── srcs/
│ ├── main.c
│ ├── init.c
│ ├── parse/
│ │ ├── parse_file.c
│ │ ├── parse_line.c
│ │ └── validate.c
│ ├── draw/
│ │ ├── draw.c
│ │ ├── line.c
│ │ └── color.c
│ ├── transform/
│ │ ├── project.c
│ │ ├── rotate.c
│ │ └── matrix.c
│ ├── hooks/
│ │ ├── key_hooks.c
│ │ └── mouse_hooks.c
│ └── utils/
│ ├── error.c
│ └── cleanup.c
├── libft/
│ └── ...
├── minilibx/
│ └── ...
└── maps/
├── 42.fdf
└── test_maps/
4.1.2 ヘッダーファイル
/* fdf.h */
#ifndef FDF_H
# define FDF_H
/*
** ================== インクルード ==================
*/
# include <mlx.h>
# include <stdlib.h>
# include <unistd.h>
# include <fcntl.h>
# include <math.h>
# include "libft.h"
/*
** ================== 定数定義 ==================
*/
# define WIN_WIDTH 1920
# define WIN_HEIGHT 1080
# define WIN_TITLE "FdF"
/* キーコード(macOS) */
# define KEY_ESC 53
# define KEY_W 13
# define KEY_A 0
# define KEY_S 1
# define KEY_D 2
# define KEY_PLUS 24
# define KEY_MINUS 27
# define KEY_LEFT 123
# define KEY_RIGHT 124
# define KEY_UP 126
# define KEY_DOWN 125
/* デフォルト値 */
# define DEFAULT_ZOOM 20
# define DEFAULT_COLOR 0xFFFFFF
# define BG_COLOR 0x1E1E2E
/* 投影タイプ */
# define ISOMETRIC 0
# define PARALLEL 1
# define PERSPECTIVE 2
/*
** ================== 構造体定義 ==================
*/
/* 3D点 */
typedef struct s_point
{
int x;
int y;
int z;
int color;
int has_color;
} t_point;
/* 画面座標 */
typedef struct s_screen
{
int x;
int y;
int color;
} t_screen;
/* イメージバッファ */
typedef struct s_img
{
void *ptr;
char *addr;
int bpp;
int line_len;
int endian;
} t_img;
/* マップ */
typedef struct s_map
{
int width;
int height;
int z_min;
int z_max;
t_point **points;
} t_map;
/* カメラ */
typedef struct s_camera
{
float rot_x;
float rot_y;
float rot_z;
float zoom;
int offset_x;
int offset_y;
int projection;
float z_scale;
} t_camera;
/* メイン構造体 */
typedef struct s_fdf
{
void *mlx;
void *win;
t_img img;
t_map *map;
t_camera cam;
} t_fdf;
/*
** ================== 関数プロトタイプ ==================
*/
/* 初期化 */
int init_fdf(t_fdf *fdf, char *filename);
void init_camera(t_camera *cam, t_map *map);
/* パース */
t_map *parse_map(const char *filename);
int parse_line(const char *line, int width, t_point *points);
int validate_file(const char *filename);
/* 描画 */
void render(t_fdf *fdf);
void draw_wireframe(t_fdf *fdf);
void draw_line(t_fdf *fdf, t_screen p0, t_screen p1);
void my_mlx_pixel_put(t_img *img, int x, int y, int color);
/* 変換 */
t_screen project(t_fdf *fdf, int x, int y);
void apply_rotation(float *x, float *y, float *z, t_camera *cam);
void apply_isometric(float *x, float *y, float z);
/* 色 */
int get_color(t_fdf *fdf, int z);
int interpolate_color(int c1, int c2, float ratio);
/* フック */
int key_hook(int keycode, t_fdf *fdf);
int close_hook(t_fdf *fdf);
/* ユーティリティ */
void error_exit(const char *msg);
void cleanup(t_fdf *fdf);
void free_map(t_map *map);
#endif
4.2 段階的実装ガイド
4.2.1 ステップ1: 基本フレームワーク
/* main.c - 最小限の動作確認 */
#include "fdf.h"
int main(int argc, char **argv)
{
t_fdf fdf;
if (argc != 2)
{
ft_putendl_fd("Usage: ./fdf <filename.fdf>", 2);
return (1);
}
/* MLX初期化 */
fdf.mlx = mlx_init();
if (!fdf.mlx)
error_exit("MLX init failed");
/* ウィンドウ作成 */
fdf.win = mlx_new_window(fdf.mlx, WIN_WIDTH, WIN_HEIGHT, WIN_TITLE);
if (!fdf.win)
error_exit("Window creation failed");
/* イメージ作成 */
fdf.img.ptr = mlx_new_image(fdf.mlx, WIN_WIDTH, WIN_HEIGHT);
fdf.img.addr = mlx_get_data_addr(fdf.img.ptr, &fdf.img.bpp,
&fdf.img.line_len, &fdf.img.endian);
/* テスト: 画面中央に点を描画 */
my_mlx_pixel_put(&fdf.img, WIN_WIDTH / 2, WIN_HEIGHT / 2, 0xFFFFFF);
mlx_put_image_to_window(fdf.mlx, fdf.win, fdf.img.ptr, 0, 0);
/* ESCで終了 */
mlx_hook(fdf.win, 2, 1L << 0, key_hook, &fdf);
mlx_hook(fdf.win, 17, 0, close_hook, &fdf);
mlx_loop(fdf.mlx);
return (0);
}
/* ステップ1の確認事項:
* □ ウィンドウが表示される
* □ 中央に白い点が見える
* □ ESCで終了できる
* □ ウィンドウ閉じるボタンで終了できる
*/
4.2.2 ステップ2: ライン描画
/* line.c - ブレゼンハムアルゴリズム */
void draw_line(t_fdf *fdf, t_screen p0, t_screen p1)
{
int dx;
int dy;
int sx;
int sy;
int err;
int e2;
dx = abs(p1.x - p0.x);
dy = abs(p1.y - p0.y);
sx = (p0.x < p1.x) ? 1 : -1;
sy = (p0.y < p1.y) ? 1 : -1;
err = dx - dy;
while (1)
{
my_mlx_pixel_put(&fdf->img, p0.x, p0.y, p0.color);
if (p0.x == p1.x && p0.y == p1.y)
break ;
e2 = 2 * err;
if (e2 > -dy)
{
err -= dy;
p0.x += sx;
}
if (e2 < dx)
{
err += dx;
p0.y += sy;
}
}
}
/* ステップ2の確認:
* 画面に線を描画するテストコード
*/
void test_line(t_fdf *fdf)
{
t_screen p0;
t_screen p1;
p0 = (t_screen){100, 100, 0xFF0000};
p1 = (t_screen){300, 200, 0x00FF00};
draw_line(fdf, p0, p1);
p0 = (t_screen){300, 100, 0x0000FF};
p1 = (t_screen){100, 300, 0xFFFF00};
draw_line(fdf, p0, p1);
}
4.2.3 ステップ3: ファイルパース
/* parse_file.c */
t_map *parse_map(const char *filename)
{
int fd;
char *line;
t_list *lines;
t_map *map;
if (!validate_file(filename))
return (NULL);
fd = open(filename, O_RDONLY);
if (fd < 0)
return (NULL);
/* 行を収集 */
lines = NULL;
while ((line = get_next_line(fd)) != NULL)
ft_lstadd_back(&lines, ft_lstnew(line));
close(fd);
/* マップを構築 */
map = build_map(lines);
/* クリーンアップ */
ft_lstclear(&lines, free);
return (map);
}
/* ステップ3の確認:
* マップデータをパースして内容を表示
*/
void test_parse(char *filename)
{
t_map *map;
int x, y;
map = parse_map(filename);
if (!map)
{
printf("Parse failed\n");
return ;
}
printf("Map: %d x %d\n", map->width, map->height);
y = 0;
while (y < map->height)
{
x = 0;
while (x < map->width)
{
printf("%3d ", map->points[y][x].z);
x++;
}
printf("\n");
y++;
}
free_map(map);
}
4.2.4 ステップ4: 座標変換
/* project.c */
t_screen project(t_fdf *fdf, int grid_x, int grid_y)
{
t_screen result;
float x;
float y;
float z;
/* グリッド座標を中心基準に変換 */
x = (grid_x - fdf->map->width / 2.0) * fdf->cam.zoom;
y = (grid_y - fdf->map->height / 2.0) * fdf->cam.zoom;
z = fdf->map->points[grid_y][grid_x].z * fdf->cam.zoom * fdf->cam.z_scale;
/* 回転適用 */
apply_rotation(&x, &y, &z, &fdf->cam);
/* 投影 */
if (fdf->cam.projection == ISOMETRIC)
apply_isometric(&x, &y, z);
/* 画面座標に変換 */
result.x = (int)x + WIN_WIDTH / 2 + fdf->cam.offset_x;
result.y = (int)y + WIN_HEIGHT / 2 + fdf->cam.offset_y;
result.color = get_color(fdf, fdf->map->points[grid_y][grid_x].z);
return (result);
}
void apply_rotation(float *x, float *y, float *z, t_camera *cam)
{
float tmp;
/* X軸回転 */
tmp = *y;
*y = *y * cos(cam->rot_x) - *z * sin(cam->rot_x);
*z = tmp * sin(cam->rot_x) + *z * cos(cam->rot_x);
/* Y軸回転 */
tmp = *x;
*x = *x * cos(cam->rot_y) + *z * sin(cam->rot_y);
*z = -tmp * sin(cam->rot_y) + *z * cos(cam->rot_y);
/* Z軸回転 */
tmp = *x;
*x = *x * cos(cam->rot_z) - *y * sin(cam->rot_z);
*y = tmp * sin(cam->rot_z) + *y * cos(cam->rot_z);
}
void apply_isometric(float *x, float *y, float z)
{
float prev_x;
float prev_y;
prev_x = *x;
prev_y = *y;
*x = (prev_x - prev_y) * cos(0.523599); /* cos(30°) */
*y = -z + (prev_x + prev_y) * sin(0.523599); /* sin(30°) */
}
4.2.5 ステップ5: ワイヤーフレーム描画
/* draw.c */
void render(t_fdf *fdf)
{
/* 背景をクリア */
clear_image(&fdf->img, BG_COLOR);
/* ワイヤーフレーム描画 */
draw_wireframe(fdf);
/* 画面に転送 */
mlx_put_image_to_window(fdf->mlx, fdf->win, fdf->img.ptr, 0, 0);
}
void draw_wireframe(t_fdf *fdf)
{
int x;
int y;
y = 0;
while (y < fdf->map->height)
{
x = 0;
while (x < fdf->map->width)
{
/* 右への線 */
if (x < fdf->map->width - 1)
draw_segment(fdf, x, y, x + 1, y);
/* 下への線 */
if (y < fdf->map->height - 1)
draw_segment(fdf, x, y, x, y + 1);
x++;
}
y++;
}
}
void draw_segment(t_fdf *fdf, int x0, int y0, int x1, int y1)
{
t_screen p0;
t_screen p1;
p0 = project(fdf, x0, y0);
p1 = project(fdf, x1, y1);
draw_line(fdf, p0, p1);
}
4.3 カメラコントロール
4.3.1 キーボード操作
/* key_hooks.c */
int key_hook(int keycode, t_fdf *fdf)
{
/* 終了 */
if (keycode == KEY_ESC)
return (close_hook(fdf));
/* 回転 */
if (keycode == KEY_W)
fdf->cam.rot_x += 0.05;
else if (keycode == KEY_S)
fdf->cam.rot_x -= 0.05;
else if (keycode == KEY_A)
fdf->cam.rot_z -= 0.05;
else if (keycode == KEY_D)
fdf->cam.rot_z += 0.05;
/* ズーム */
else if (keycode == KEY_PLUS)
fdf->cam.zoom *= 1.1;
else if (keycode == KEY_MINUS)
fdf->cam.zoom /= 1.1;
/* 移動 */
else if (keycode == KEY_LEFT)
fdf->cam.offset_x -= 20;
else if (keycode == KEY_RIGHT)
fdf->cam.offset_x += 20;
else if (keycode == KEY_UP)
fdf->cam.offset_y -= 20;
else if (keycode == KEY_DOWN)
fdf->cam.offset_y += 20;
/* 再描画 */
render(fdf);
return (0);
}
/* 追加操作(オプション) */
void handle_projection(int keycode, t_fdf *fdf)
{
if (keycode == KEY_I) /* Isometric */
fdf->cam.projection = ISOMETRIC;
else if (keycode == KEY_P) /* Parallel */
fdf->cam.projection = PARALLEL;
}
void handle_height_scale(int keycode, t_fdf *fdf)
{
if (keycode == KEY_H) /* Increase height */
fdf->cam.z_scale *= 1.1;
else if (keycode == KEY_L) /* Decrease height */
fdf->cam.z_scale /= 1.1;
}
void reset_camera(t_fdf *fdf)
{
init_camera(&fdf->cam, fdf->map);
}
4.3.2 マウス操作(ボーナス)
/* mouse_hooks.c */
/* マウスボタン定義 */
# define MOUSE_LEFT 1
# define MOUSE_RIGHT 2
# define MOUSE_MIDDLE 3
# define MOUSE_SCROLL_UP 4
# define MOUSE_SCROLL_DOWN 5
typedef struct s_mouse
{
int pressed;
int x;
int y;
} t_mouse;
int mouse_press(int button, int x, int y, t_fdf *fdf)
{
if (button == MOUSE_LEFT)
{
fdf->mouse.pressed = 1;
fdf->mouse.x = x;
fdf->mouse.y = y;
}
else if (button == MOUSE_SCROLL_UP)
{
fdf->cam.zoom *= 1.1;
render(fdf);
}
else if (button == MOUSE_SCROLL_DOWN)
{
fdf->cam.zoom /= 1.1;
render(fdf);
}
return (0);
}
int mouse_release(int button, int x, int y, t_fdf *fdf)
{
(void)x;
(void)y;
if (button == MOUSE_LEFT)
fdf->mouse.pressed = 0;
return (0);
}
int mouse_move(int x, int y, t_fdf *fdf)
{
int dx;
int dy;
if (fdf->mouse.pressed)
{
dx = x - fdf->mouse.x;
dy = y - fdf->mouse.y;
fdf->cam.rot_z += dx * 0.01;
fdf->cam.rot_x += dy * 0.01;
fdf->mouse.x = x;
fdf->mouse.y = y;
render(fdf);
}
return (0);
}
4.4 色の実装
4.4.1 高さベースの色付け
/* color.c */
int get_color(t_fdf *fdf, int z)
{
float ratio;
int range;
range = fdf->map->z_max - fdf->map->z_min;
if (range == 0)
return (DEFAULT_COLOR);
ratio = (float)(z - fdf->map->z_min) / range;
/* 低い→高い: 青→緑→黄→赤 */
if (ratio < 0.25)
return (interpolate_color(0x0000FF, 0x00FF00, ratio * 4));
else if (ratio < 0.5)
return (interpolate_color(0x00FF00, 0xFFFF00, (ratio - 0.25) * 4));
else if (ratio < 0.75)
return (interpolate_color(0xFFFF00, 0xFF8000, (ratio - 0.5) * 4));
else
return (interpolate_color(0xFF8000, 0xFF0000, (ratio - 0.75) * 4));
}
int interpolate_color(int c1, int c2, float ratio)
{
int r;
int g;
int b;
if (ratio <= 0)
return (c1);
if (ratio >= 1)
return (c2);
r = ((c1 >> 16) & 0xFF) + (((c2 >> 16) & 0xFF)
- ((c1 >> 16) & 0xFF)) * ratio;
g = ((c1 >> 8) & 0xFF) + (((c2 >> 8) & 0xFF)
- ((c1 >> 8) & 0xFF)) * ratio;
b = (c1 & 0xFF) + ((c2 & 0xFF) - (c1 & 0xFF)) * ratio;
return ((r << 16) | (g << 8) | b);
}
4.4.2 グラデーション付きライン
void draw_line_gradient(t_fdf *fdf, t_screen p0, t_screen p1)
{
int steps;
float x_inc;
float y_inc;
float x;
float y;
int i;
steps = MAX(abs(p1.x - p0.x), abs(p1.y - p0.y));
if (steps == 0)
{
my_mlx_pixel_put(&fdf->img, p0.x, p0.y, p0.color);
return ;
}
x_inc = (float)(p1.x - p0.x) / steps;
y_inc = (float)(p1.y - p0.y) / steps;
x = p0.x;
y = p0.y;
i = 0;
while (i <= steps)
{
int color = interpolate_color(p0.color, p1.color, (float)i / steps);
my_mlx_pixel_put(&fdf->img, (int)x, (int)y, color);
x += x_inc;
y += y_inc;
i++;
}
}
4.5 エラー処理とクリーンアップ
4.5.1 安全な終了
/* cleanup.c */
void cleanup(t_fdf *fdf)
{
if (!fdf)
return ;
if (fdf->map)
free_map(fdf->map);
if (fdf->img.ptr && fdf->mlx)
mlx_destroy_image(fdf->mlx, fdf->img.ptr);
if (fdf->win && fdf->mlx)
mlx_destroy_window(fdf->mlx, fdf->win);
#ifdef __linux__
if (fdf->mlx)
mlx_destroy_display(fdf->mlx);
free(fdf->mlx);
#endif
}
int close_hook(t_fdf *fdf)
{
cleanup(fdf);
exit(0);
return (0);
}
void free_map(t_map *map)
{
int i;
if (!map)
return ;
if (map->points)
{
i = 0;
while (i < map->height)
{
if (map->points[i])
free(map->points[i]);
i++;
}
free(map->points);
}
free(map);
}
4.5.2 エラーメッセージ
/* error.c */
void error_exit(const char *msg)
{
ft_putstr_fd("Error: ", 2);
ft_putendl_fd(msg, 2);
exit(1);
}
void *error_null(const char *msg)
{
ft_putstr_fd("Error: ", 2);
ft_putendl_fd(msg, 2);
return (NULL);
}
4.6 Makefile
# **************************************************************************** #
# #
# FdF Makefile #
# #
# **************************************************************************** #
NAME = fdf
# Directories
SRCS_DIR = srcs
OBJS_DIR = objs
INC_DIR = includes
LIBFT_DIR = libft
MLX_DIR = minilibx
# Source files
SRCS = main.c \
init.c \
parse/parse_file.c \
parse/parse_line.c \
parse/validate.c \
draw/draw.c \
draw/line.c \
draw/color.c \
transform/project.c \
transform/rotate.c \
hooks/key_hooks.c \
utils/error.c \
utils/cleanup.c
OBJS = $(addprefix $(OBJS_DIR)/, $(SRCS:.c=.o))
# Compiler and flags
CC = cc
CFLAGS = -Wall -Wextra -Werror
INCLUDES = -I$(INC_DIR) -I$(LIBFT_DIR) -I$(MLX_DIR)
# Libraries
LIBFT = $(LIBFT_DIR)/libft.a
MLX = $(MLX_DIR)/libmlx.a
# OS detection
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
MLX_FLAGS = -L$(MLX_DIR) -lmlx -lX11 -lXext -lm
else
MLX_FLAGS = -L$(MLX_DIR) -lmlx -framework OpenGL -framework AppKit
endif
# Colors
GREEN = \033[0;32m
RESET = \033[0m
# Rules
all: $(NAME)
$(NAME): $(LIBFT) $(MLX) $(OBJS)
@echo "$(GREEN)Linking $(NAME)...$(RESET)"
@$(CC) $(CFLAGS) $(OBJS) $(LIBFT) $(MLX_FLAGS) -o $(NAME)
@echo "$(GREEN)Done!$(RESET)"
$(OBJS_DIR)/%.o: $(SRCS_DIR)/%.c
@mkdir -p $(dir $@)
@$(CC) $(CFLAGS) $(INCLUDES) -c ___CODE_BLOCK_13___lt; -o $@
$(LIBFT):
@make -C $(LIBFT_DIR)
$(MLX):
@make -C $(MLX_DIR)
clean:
@rm -rf $(OBJS_DIR)
@make -C $(LIBFT_DIR) clean
@make -C $(MLX_DIR) clean
fclean: clean
@rm -f $(NAME)
@make -C $(LIBFT_DIR) fclean
re: fclean all
.PHONY: all clean fclean re
4.7 評価対策
4.7.1 必須要件チェックリスト
基本機能:
□ 42.fdfマップを正しく表示できる
□ アイソメトリック投影が正しい
□ ワイヤーフレームが表示される
□ ESCキーで終了できる
□ ウィンドウ閉じるボタンで終了
入力処理:
□ 引数エラー(引数なし、複数)に対応
□ 存在しないファイルでエラー表示
□ 不正なファイル形式でエラー表示
□ 空ファイルでエラー表示
メモリ管理:
□ Valgrindでメモリリークなし
□ 正常終了時にリークなし
□ エラー時にリークなし
コード品質:
□ Normチェッククリア
□ 42ヘッダー付き
□ 禁止関数を使用していない
4.7.2 ボーナス要件
ボーナス機能:
□ ズーム(+/-キー)
□ 移動(矢印キー)
□ 回転(複数軸)
□ 投影タイプ切り替え
□ 色のグラデーション
□ マウスでの回転(ドラッグ)
□ マウスホイールでズーム
□ 高さスケール調整
4.7.3 よくある問題と対策
問題1: 大きなマップでクラッシュ
対策:
- メモリ確保のエラーチェック
- 極端なズーム時の数値オーバーフロー対策
- クリッピングの実装
問題2: 回転がおかしい
対策:
- 回転の順序を確認(X→Y→Z等)
- 回転中心が正しいか確認
- ラジアン/度の変換確認
問題3: 色が正しくない
対策:
- エンディアンの確認
- 色成分の順序確認(ARGB vs BGRA)
- 補間計算の確認
問題4: Linux/macの互換性
対策:
- 条件付きコンパイルの使用
- キーコードの分離
- Makefileの分岐
4.8 本章のまとめ
実装の優先順位:
1. 基本動作(必須)
├── MLX初期化
├── ウィンドウ表示
├── ファイルパース
├── 座標変換
└── ワイヤーフレーム描画
2. 操作機能(必須)
├── ESC終了
└── ウィンドウ閉じる
3. インタラクション(ボーナス)
├── ズーム
├── 移動
├── 回転
└── マウス操作
4. 視覚効果(ボーナス)
├── 色のグラデーション
├── 投影切り替え
└── 高さスケール
FdFは、3Dグラフィックスプログラミングの基礎を学ぶ素晴らしいプロジェクトです。数学的な概念をコードに落とし込む経験は、今後のcub3dやminiRTプロジェクトに直結します。