第5章:実装とシーン管理
はじめに
本章では、miniRTの全体構造、.rtファイルのパース、メインレンダリングループを学びます。
---
1. プロジェクト構造
1.1 ディレクトリ構成
miniRT/
├── Makefile
├── includes/
│ ├── minirt.h # メインヘッダー
│ ├── vectors.h # ベクトル演算
│ ├── objects.h # オブジェクト定義
│ ├── parser.h # パーサー
│ └── render.h # レンダリング
├── srcs/
│ ├── main.c
│ ├── vectors/
│ │ ├── vec3_basic.c
│ │ ├── vec3_ops.c
│ │ └── vec3_utils.c
│ ├── objects/
│ │ ├── sphere.c
│ │ ├── plane.c
│ │ └── cylinder.c
│ ├── parser/
│ │ ├── parse_file.c
│ │ ├── parse_elements.c
│ │ └── validate.c
│ ├── render/
│ │ ├── ray.c
│ │ ├── camera.c
│ │ ├── lighting.c
│ │ └── render.c
│ └── utils/
│ ├── error.c
│ ├── color.c
│ └── memory.c
└── scenes/
├── basic.rt
└── example.rt
1.2 メインヘッダー
#ifndef MINIRT_H
# define MINIRT_H
# include <math.h>
# include <stdlib.h>
# include <unistd.h>
# include <fcntl.h>
# include <stdio.h>
# include "mlx.h"
# include "vectors.h"
# include "objects.h"
# define WIDTH 1280
# define HEIGHT 720
# define EPSILON 1e-6
# define MAX_DEPTH 5
typedef struct s_mlx {
void *mlx;
void *win;
void *img;
char *addr;
int bpp;
int line_len;
int endian;
} t_mlx;
typedef struct s_scene {
t_ambient ambient;
t_camera camera;
t_light *lights;
int light_count;
t_object *objects;
int object_count;
} t_scene;
typedef struct s_data {
t_mlx mlx;
t_scene scene;
int render_done;
} t_data;
// プロトタイプ
int parse_scene(t_scene *scene, char *filename);
void render_scene(t_data *data);
void cleanup(t_data *data);
#endif
---
2. .rtファイルのパース
2.1 ファイル形式
# コメント(オプション)
A 0.2 255,255,255
C -50,0,20 0,0,1 70
L -40,50,0 0.6 255,255,255
pl 0,0,0 0,1.0,0 255,0,225
sp 0,0,20 20 255,0,0
cy 50.0,0.0,20.6 0,0,1.0 14.2 21.42 10,0,255
2.2 パーサーの実装
// 行を読み込んでトークンに分割
char **tokenize(char *line) {
char **tokens = ft_split(line, ' ');
// タブも処理
return tokens;
}
// メインパース関数
int parse_scene(t_scene *scene, char *filename) {
int fd = open(filename, O_RDONLY);
if (fd < 0)
return error_exit("Cannot open file");
// シーンを初期化
init_scene(scene);
char *line;
while ((line = get_next_line(fd)) != NULL) {
// 空行とコメントをスキップ
if (is_empty_or_comment(line)) {
free(line);
continue;
}
// 要素をパース
if (parse_element(scene, line) < 0) {
free(line);
close(fd);
return -1;
}
free(line);
}
close(fd);
return validate_scene(scene);
}
// 要素の種類に応じてパース
int parse_element(t_scene *scene, char *line) {
char **tokens = tokenize(line);
if (ft_strcmp(tokens[0], "A") == 0)
return parse_ambient(scene, tokens);
else if (ft_strcmp(tokens[0], "C") == 0)
return parse_camera(scene, tokens);
else if (ft_strcmp(tokens[0], "L") == 0)
return parse_light(scene, tokens);
else if (ft_strcmp(tokens[0], "sp") == 0)
return parse_sphere(scene, tokens);
else if (ft_strcmp(tokens[0], "pl") == 0)
return parse_plane(scene, tokens);
else if (ft_strcmp(tokens[0], "cy") == 0)
return parse_cylinder(scene, tokens);
else
return error_msg("Unknown element type");
}
2.3 各要素のパース
// 環境光: A ratio R,G,B
int parse_ambient(t_scene *scene, char **tokens) {
if (scene->ambient.parsed)
return error_msg("Duplicate ambient light");
scene->ambient.intensity = parse_double(tokens[1]);
scene->ambient.color = parse_color(tokens[2]);
scene->ambient.parsed = 1;
// 検証
if (scene->ambient.intensity < 0 || scene->ambient.intensity > 1)
return error_msg("Ambient intensity out of range");
return 0;
}
// カメラ: C x,y,z dx,dy,dz FOV
int parse_camera(t_scene *scene, char **tokens) {
if (scene->camera.parsed)
return error_msg("Duplicate camera");
t_vec3 origin = parse_vec3(tokens[1]);
t_vec3 direction = parse_vec3(tokens[2]);
double fov = parse_double(tokens[3]);
// 方向ベクトルの検証
if (vec3_length(direction) < EPSILON)
return error_msg("Camera direction is zero");
camera_init(&scene->camera, origin, vec3_normalize(direction),
fov, (double)WIDTH / HEIGHT);
scene->camera.parsed = 1;
return 0;
}
// 光源: L x,y,z brightness R,G,B
int parse_light(t_scene *scene, char **tokens) {
t_light light;
light.position = parse_vec3(tokens[1]);
light.intensity = parse_double(tokens[2]);
light.color = parse_color(tokens[3]);
// リストに追加
add_light(scene, light);
return 0;
}
// 球: sp x,y,z diameter R,G,B
int parse_sphere(t_scene *scene, char **tokens) {
t_object obj;
obj.type = OBJ_SPHERE;
obj.data.sphere.center = parse_vec3(tokens[1]);
obj.data.sphere.radius = parse_double(tokens[2]) / 2.0;
obj.color = parse_color(tokens[3]);
add_object(scene, obj);
return 0;
}
// 平面: pl x,y,z nx,ny,nz R,G,B
int parse_plane(t_scene *scene, char **tokens) {
t_object obj;
obj.type = OBJ_PLANE;
obj.data.plane.point = parse_vec3(tokens[1]);
obj.data.plane.normal = vec3_normalize(parse_vec3(tokens[2]));
obj.color = parse_color(tokens[3]);
add_object(scene, obj);
return 0;
}
// 円柱: cy x,y,z nx,ny,nz diameter height R,G,B
int parse_cylinder(t_scene *scene, char **tokens) {
t_object obj;
obj.type = OBJ_CYLINDER;
obj.data.cylinder.center = parse_vec3(tokens[1]);
obj.data.cylinder.axis = vec3_normalize(parse_vec3(tokens[2]));
obj.data.cylinder.radius = parse_double(tokens[3]) / 2.0;
obj.data.cylinder.height = parse_double(tokens[4]);
obj.color = parse_color(tokens[5]);
add_object(scene, obj);
return 0;
}
2.4 ユーティリティ関数
// "x,y,z" → t_vec3
t_vec3 parse_vec3(char *str) {
char **parts = ft_split(str, ',');
t_vec3 v;
v.x = ft_atof(parts[0]);
v.y = ft_atof(parts[1]);
v.z = ft_atof(parts[2]);
free_split(parts);
return v;
}
// "R,G,B" → t_color(0-255 → 0-1)
t_color parse_color(char *str) {
char **parts = ft_split(str, ',');
t_color c;
c.r = ft_atoi(parts[0]) / 255.0;
c.g = ft_atoi(parts[1]) / 255.0;
c.b = ft_atoi(parts[2]) / 255.0;
free_split(parts);
return c;
}
// 文字列を浮動小数点数に変換
double ft_atof(char *str) {
double result = 0;
double decimal = 0;
int sign = 1;
int decimal_places = 0;
if (*str == '-') {
sign = -1;
str++;
}
while (*str && *str != '.') {
result = result * 10 + (*str - '0');
str++;
}
if (*str == '.') {
str++;
while (*str) {
decimal = decimal * 10 + (*str - '0');
decimal_places++;
str++;
}
}
return sign * (result + decimal / pow(10, decimal_places));
}
2.5 検証
int validate_scene(t_scene *scene) {
// 必須要素の確認
if (!scene->ambient.parsed)
return error_msg("Missing ambient light");
if (!scene->camera.parsed)
return error_msg("Missing camera");
// 値の範囲チェック
if (scene->camera.fov < 0 || scene->camera.fov > 180)
return error_msg("FOV out of range");
// オブジェクトの確認
for (int i = 0; i < scene->object_count; i++) {
t_object *obj = &scene->objects[i];
if (obj->type == OBJ_SPHERE && obj->data.sphere.radius <= 0)
return error_msg("Invalid sphere radius");
if (obj->type == OBJ_CYLINDER && obj->data.cylinder.height <= 0)
return error_msg("Invalid cylinder height");
}
return 0;
}
---
3. メインループ
3.1 初期化
int init_mlx(t_data *data) {
data->mlx.mlx = mlx_init();
if (!data->mlx.mlx)
return error_exit("MLX init failed");
data->mlx.win = mlx_new_window(data->mlx.mlx, WIDTH, HEIGHT, "miniRT");
if (!data->mlx.win)
return error_exit("Window creation failed");
data->mlx.img = mlx_new_image(data->mlx.mlx, WIDTH, HEIGHT);
if (!data->mlx.img)
return error_exit("Image creation failed");
data->mlx.addr = mlx_get_data_addr(
data->mlx.img,
&data->mlx.bpp,
&data->mlx.line_len,
&data->mlx.endian
);
return 0;
}
int main(int argc, char **argv) {
t_data data;
if (argc != 2)
return error_exit("Usage: ./miniRT <scene.rt>");
// 拡張子チェック
if (!check_extension(argv[1], ".rt"))
return error_exit("Invalid file extension");
// シーンをパース
if (parse_scene(&data.scene, argv[1]) < 0)
return error_exit("Failed to parse scene");
// MLX初期化
if (init_mlx(&data) < 0)
return 1;
data.render_done = 0;
// フック登録
mlx_hook(data.mlx.win, 2, 1L<<0, key_hook, &data);
mlx_hook(data.mlx.win, 17, 0, close_hook, &data);
mlx_loop_hook(data.mlx.mlx, render_loop, &data);
mlx_loop(data.mlx.mlx);
return 0;
}
3.2 レンダリングループ
int render_loop(t_data *data) {
if (data->render_done)
return 0;
render_scene(data);
data->render_done = 1;
mlx_put_image_to_window(
data->mlx.mlx,
data->mlx.win,
data->mlx.img,
0, 0
);
return 0;
}
void render_scene(t_data *data) {
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
// UV座標
double u = (double)x / (WIDTH - 1);
double v = (double)(HEIGHT - 1 - y) / (HEIGHT - 1);
// レイを生成
t_ray ray = camera_get_ray(&data->scene.camera, u, v);
// レイトレース
t_color color = trace_ray(&data->scene, &ray, 0);
// ピクセルを設定
put_pixel(&data->mlx, x, y, color_to_int(color));
}
// 進捗表示(オプション)
if (y % 50 == 0)
printf("Rendering: %d%%\n", y * 100 / HEIGHT);
}
}
3.3 レイトレース関数
t_color trace_ray(t_scene *scene, t_ray *ray, int depth) {
if (depth >= MAX_DEPTH)
return color_new(0, 0, 0);
// 最も近い交差点を探す
t_hit hit = find_closest_hit(scene, ray);
if (!hit.valid) {
// 背景色
return background_color(ray);
}
// ライティングを計算
t_color color = phong_lighting(scene, &hit, ray);
// ボーナス: 反射
if (hit.reflective > 0) {
t_vec3 reflect_dir = reflect(ray->direction, hit.normal);
t_vec3 reflect_origin = vec3_add(
hit.point,
vec3_scale(hit.normal, EPSILON)
);
t_ray reflect_ray = {reflect_origin, reflect_dir};
t_color reflected = trace_ray(scene, &reflect_ray, depth + 1);
color = color_add(
color_scale(color, 1 - hit.reflective),
color_scale(reflected, hit.reflective)
);
}
return color;
}
t_hit find_closest_hit(t_scene *scene, t_ray *ray) {
t_hit closest = hit_miss();
closest.t = INFINITY;
for (int i = 0; i < scene->object_count; i++) {
t_object *obj = &scene->objects[i];
t_hit hit = intersect_object(obj, ray);
if (hit.valid && hit.t < closest.t) {
closest = hit;
closest.color = obj->color;
}
}
return closest;
}
t_color background_color(t_ray *ray) {
// グラデーション背景
double t = 0.5 * (ray->direction.y + 1.0);
t_color white = color_new(1.0, 1.0, 1.0);
t_color blue = color_new(0.5, 0.7, 1.0);
return color_add(
color_scale(white, 1.0 - t),
color_scale(blue, t)
);
}
---
4. ピクセル操作
4.1 ピクセルの書き込み
void put_pixel(t_mlx *mlx, int x, int y, unsigned int color) {
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT)
return;
char *dst = mlx->addr + (y * mlx->line_len + x * (mlx->bpp / 8));
*(unsigned int *)dst = color;
}
4.2 エンディアン対応
void put_pixel_endian(t_mlx *mlx, int x, int y, unsigned int color) {
char *dst = mlx->addr + (y * mlx->line_len + x * (mlx->bpp / 8));
if (mlx->endian == 0) {
// Little endian
dst[0] = color & 0xFF;
dst[1] = (color >> 8) & 0xFF;
dst[2] = (color >> 16) & 0xFF;
dst[3] = (color >> 24) & 0xFF;
} else {
// Big endian
dst[0] = (color >> 24) & 0xFF;
dst[1] = (color >> 16) & 0xFF;
dst[2] = (color >> 8) & 0xFF;
dst[3] = color & 0xFF;
}
}
---
5. イベント処理
5.1 キーフック
int key_hook(int keycode, t_data *data) {
if (keycode == KEY_ESC) {
cleanup(data);
exit(0);
}
// ボーナス: カメラ移動
if (keycode == KEY_W)
move_camera(&data->scene.camera, 1);
if (keycode == KEY_S)
move_camera(&data->scene.camera, -1);
// 再レンダリング
if (keycode == KEY_R) {
data->render_done = 0;
}
return 0;
}
int close_hook(t_data *data) {
cleanup(data);
exit(0);
return 0;
}
5.2 クリーンアップ
void cleanup(t_data *data) {
// オブジェクトを解放
if (data->scene.objects)
free(data->scene.objects);
if (data->scene.lights)
free(data->scene.lights);
// MLXリソースを解放
if (data->mlx.img)
mlx_destroy_image(data->mlx.mlx, data->mlx.img);
if (data->mlx.win)
mlx_destroy_window(data->mlx.mlx, data->mlx.win);
}
---
6. 進捗表示
6.1 プログレスバー
void print_progress(int current, int total) {
int percent = current * 100 / total;
int bar_width = 50;
int filled = bar_width * current / total;
printf("\r[");
for (int i = 0; i < bar_width; i++) {
if (i < filled)
printf("=");
else if (i == filled)
printf(">");
else
printf(" ");
}
printf("] %3d%%", percent);
fflush(stdout);
}
void render_with_progress(t_data *data) {
printf("Rendering...\n");
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
// ... レンダリング処理 ...
}
print_progress(y + 1, HEIGHT);
}
printf("\nDone!\n");
}
6.2 タイマー
#include <sys/time.h>
void render_with_timing(t_data *data) {
struct timeval start, end;
gettimeofday(&start, NULL);
// レンダリング
render_scene(data);
gettimeofday(&end, NULL);
double elapsed = (end.tv_sec - start.tv_sec) +
(end.tv_usec - start.tv_usec) / 1000000.0;
printf("Render time: %.2f seconds\n", elapsed);
printf("Pixels per second: %.0f\n", (WIDTH * HEIGHT) / elapsed);
}
---
まとめ
本章で学んだこと:
- プロジェクト構造: ディレクトリ構成、ヘッダー
- パーサー: .rtファイルの解析
- メインループ: 初期化、レンダリング
- ピクセル操作: 描画関数
- イベント処理: キーフック、終了処理
- 進捗表示: プログレスバー、タイミング
次章では、高度な機能と最適化を学びます。