第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ファイルの解析
  • メインループ: 初期化、レンダリング
  • ピクセル操作: 描画関数
  • イベント処理: キーフック、終了処理
  • 進捗表示: プログレスバー、タイミング

次章では、高度な機能と最適化を学びます。