Blog
개발중인 미완성 페이지로, 일부 기능이 동작하지 않을 수 있습니다.

2 - raytracer 프로젝트 - 카메라

2024. 6. 17.|2024. 10. 6.

이 글은 raytracer 시리즈입니다.

목차

유틸

util.h
#pragma once

#include <stddef.h>

void *alloc(size_t size);
void *memdup(void *mem, size_t length);

우선 공통으로 쓰일 부분들을 따로 함수로 빼 두자.

util.c
#include "util.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void *alloc(size_t size) {
  void *const result = malloc(size);
  if (!result) {
    fprintf(stderr, "Failed to allocate memory\n");
    exit(EXIT_FAILURE);
  }
  return result;
}

void *memdup(void *mem, size_t length) {
  void *const result = alloc(length);
  memcpy(result, mem, length);
  return result;
}

원근 카메라

이전 글에서 간이로 간단한 직교 카메라를 구현했었다.

카메라를 바꾸고 비교하기 위해 먼저 씬을 좀 바꿔보자.

--- ../../2024-06-16-the-raytracer-project-start/2/main.c
+++ main.c
@@ -15,18 +15,24 @@
   float radius;
 } sphere_t;
 
-const float orthogonal_camera_scale = 2.0f;
+const float orthogonal_camera_scale = 3.0f;
 const f3_t camera_position = {0, 0, 0};
 const f3_t camera_direction = {0, 1, 0};
 const bmp_pixel_t white = {255, 255, 255};
-const sphere_t sphere = {{0, 5, 0}, 1};
+const sphere_t spheres[] = {
+    {{-2, 3, 0}, 1},
+    {{3, 5, 0}, 1},
+    {{0, 5, 0}, 0.1f},
+};
 
 static inline float sqr(float x) { return x * x; }
 
-bool collide(ray_t ray, sphere_t sphere, f3_t *out_normal) {
+bool collide(ray_t ray, sphere_t sphere, f3_t *out_normal,
+             float *out_distance) {
   // 카메라가 구 안에 있는지
   if (f3_length(f3_sub(ray.origin, sphere.center)) < sphere.radius) {
     *out_normal = f3_mul1(ray.direction, -1);
+    *out_distance = 0;
     return true;
   }
 
@@ -90,6 +96,8 @@
   // 하지만 길이가 1이 아닐 수 있으므로 정규화를 해야 함
   *out_normal = f3_normalize(*out_normal);
 
+  *out_distance = t;
+
   return true;
 }
 
@@ -100,9 +108,17 @@
   const f3_t origin = f3_add(camera_position, f3(x, 0, z));
   const ray_t ray = {origin, camera_direction};
 
-  f3_t normal;
-  if (!collide(ray, sphere, &normal)) {
-    return white;
+  f3_t tmp_normal;
+  f3_t normal = {0, 0, 0};
+  float tmp_distance;
+  float distance = -1.0f;
+  for (size_t i = 0; i < sizeof(spheres) / sizeof(spheres[0]); i++) {
+    if (collide(ray, spheres[i], &tmp_normal, &tmp_distance)) {
+      if (distance < tmp_distance) {
+        distance = tmp_distance;
+        normal = tmp_normal;
+      }
+    }
   }
   return (bmp_pixel_t){
       // normal의 각 요소는 -1에서 1 사이이므로, 0에서 255 사이로 만든다
결과 이미지

이제 카메라를 아주 간단히 바꿔보자.

--- ../0/main.c
+++ main.c
@@ -15,7 +15,7 @@
   float radius;
 } sphere_t;
 
-const float orthogonal_camera_scale = 3.0f;
+const float perspective_camera_fov = 1.5707963267948966f; // 90도
 const f3_t camera_position = {0, 0, 0};
 const f3_t camera_direction = {0, 1, 0};
 const bmp_pixel_t white = {255, 255, 255};
@@ -102,11 +102,12 @@
 }
 
 bmp_pixel_t renderer(void *context, float x_on_image, float y_on_image) {
-  const float x = (2.0f * x_on_image - 1.0f) * orthogonal_camera_scale;
-  // y_on_image는 높을수록 아래인데 z는 높을수록 위이므로, 반대로 처리
-  const float z = -(2.0f * y_on_image - 1.0f) * orthogonal_camera_scale;
-  const f3_t origin = f3_add(camera_position, f3(x, 0, z));
-  const ray_t ray = {origin, camera_direction};
+  const float x =
+      tanf((2.0f * x_on_image - 1.0f) * perspective_camera_fov / 2.0f);
+  const float z =
+      -tanf((2.0f * y_on_image - 1.0f) * perspective_camera_fov / 2.0f);
+  const f3_t direction = f3_normalize((f3_t){x, 1, z});
+  const ray_t ray = {camera_position, direction};
 
   f3_t tmp_normal;
   f3_t normal = {0, 0, 0};

여기에서 fov란 Field Of View, 시야각을 의미한다.

결과 이미지

일단 여기에는 문제가 좀 있다. 딱 봐도 이상해 보인다.

각도가 기준일 때 거리가 다른 모습

각도를 기준으로 하면 평면에 투영했을 때 거리가 달라진다.

--- ../1/main.c
+++ main.c
@@ -102,10 +102,9 @@
 }
 
 bmp_pixel_t renderer(void *context, float x_on_image, float y_on_image) {
-  const float x =
-      tanf((2.0f * x_on_image - 1.0f) * perspective_camera_fov / 2.0f);
-  const float z =
-      -tanf((2.0f * y_on_image - 1.0f) * perspective_camera_fov / 2.0f);
+  const float tan_fov = tanf(perspective_camera_fov / 2.0f);
+  const float x = (2.0f * x_on_image - 1.0f) * tan_fov;
+  const float z = -(2.0f * y_on_image - 1.0f) * tan_fov;
   const f3_t direction = f3_normalize((f3_t){x, 1, z});
   const ray_t ray = {camera_position, direction};
 

그렇다면 거리를 기준으로 하면 어떨까?

결과 이미지

각도가 달라진다. 어떻게 하든 왜곡은 생기지만, 이는 어쩔 수 없는 현상이다.

거리가 기준일 때 각도가 다른 모습

어쨌든 본격적으로 카메라를 구현해보자.

ray.h
#pragma once

#include "f3.h"

typedef struct ray {
  f3_t origin;
  f3_t direction;
} ray_t;

일단 ray.h를 공통으로 쓰도록 빼 두고,

camera.h
#pragma once

#include "ray.h"

/**
 * @brief get ray from (x, y) in image
 */
typedef ray_t (*camera_get_ray_t)(void *context, float x, float y);

typedef struct camera {
  void *context;
  camera_get_ray_t get_ray;
} camera_t;

typedef enum camera_fov_mode {
  FOV_X,
  FOV_Y,
} camera_fov_mode_t;

camera_t perspective_camera(f3_t camera_position, float yaw, float pitch,
                            float roll, float aspect_ratio,
                            camera_fov_mode_t fov_mode, float fov);
camera_t perspective_camera_by_position(f3_t camera_position,
                                        f3_t look_at_position, float roll,
                                        float aspect_ratio,
                                        camera_fov_mode_t fov_mode, float fov);

typedef enum camera_size_mode {
  SIZE_WIDTH,
  SIZE_HEIGHT,
} camera_size_mode_t;

camera_t orthogonal_camera(f3_t camera_position, float yaw, float pitch,
                           float roll, float aspect_ratio,
                           camera_size_mode_t size_mode, float size);
camera_t orthogonal_camera_by_position(f3_t camera_position,
                                       f3_t look_at_position, float roll,
                                       float aspect_ratio,
                                       camera_size_mode_t size_mode,
                                       float size);

카메라의 타입과 카메라를 만드는 함수를 선언한다.

위에서는 가로/세로 비율이 같아서 fov 하나로 가로/세로에 같은 fov를 사용했지만, 가로/세로 비율이 다르면 각자 다른 fov를 사용해야 한다.

가로/세로 비율(이하 종횡비)이 주어지면 가로나 세로 fov 중 한 쪽에 따라 나머지 한 쪽이 정해진다.

즉, 가로 fov나 세로 fov 중 하나만 받아도 된다는 의미이다.

여기서 camera_fov_mode_t가 주어진 fov가 가로인지 세로인지 결정하는 데 쓰인다.

perspective_camera.c
#include "camera.h"

#include <math.h>

#include "f3x3.h"
#include "util.h"

typedef struct perspective_camera_context {
  float tan_fov_x;
  float tan_fov_y;
  f3_t origin;
  f3x3_t rotation;
} perspective_camera_context_t;

static ray_t get_ray(void *context, float x, float y) {
  perspective_camera_context_t *const ctx = context;
  const float direction_x = ctx->tan_fov_x * (x * 2.0f - 1.0f);
  const float direction_y = 1.0f;
  const float direction_z = -ctx->tan_fov_y * (y * 2.0f - 1.0f);
  const f3_t direction = {direction_x, direction_y, direction_z};
  const f3_t normal = f3_normalize(direction);
  const f3_t rotated = f3x3_mul_f3(normal, ctx->rotation);
  const ray_t result = {ctx->origin, rotated};
  return result;
}

camera_t perspective_camera(f3_t camera_position, float yaw, float pitch,
                            float roll, float aspect_ratio,
                            camera_fov_mode_t fov_mode, float fov) {
  perspective_camera_context_t context;
  if (fov_mode == FOV_X) {
    const float half_fov_x = fov / 2;
    context.tan_fov_x = tanf(half_fov_x);
    context.tan_fov_y = context.tan_fov_x / aspect_ratio;
  } else {
    const float half_fov_y = fov / 2;
    context.tan_fov_y = tanf(half_fov_y);
    context.tan_fov_x = context.tan_fov_y * aspect_ratio;
  }
  context.origin = camera_position;
  context.rotation = f3x3_rotate(yaw, pitch, roll);
  const camera_t result = {memdup(&context, sizeof(context)), get_ray};
  return result;
}

이렇게 카메라를 간단히 구현 후, 이 카메라를 적용해서 테스트 해 보자.

--- ../2/main.c
+++ main.c
@@ -1,20 +1,18 @@
 #include <math.h>
 #include <stdint.h>
-#include <stdio.h>
 
+#include "../../camera.h"
 #include "../../f3.h"
+#include "../../ray.h"
 #include "../../write_image.h"
 
-typedef struct ray {
-  f3_t origin;
-  f3_t direction;
-} ray_t;
-
 typedef struct sphere {
   f3_t center;
   float radius;
 } sphere_t;
 
+const size_t width = 960;
+const size_t height = 540;
 const float perspective_camera_fov = 1.5707963267948966f; // 90도
 const f3_t camera_position = {0, 0, 0};
 const f3_t camera_direction = {0, 1, 0};
@@ -102,12 +100,10 @@
 }
 
 bmp_pixel_t renderer(void *context, float x_on_image, float y_on_image) {
-  const float tan_fov = tanf(perspective_camera_fov / 2.0f);
-  const float x = (2.0f * x_on_image - 1.0f) * tan_fov;
-  const float z = -(2.0f * y_on_image - 1.0f) * tan_fov;
-  const f3_t direction = f3_normalize((f3_t){x, 1, z});
-  const ray_t ray = {camera_position, direction};
+  camera_t *const camera = context;
 
+  const ray_t ray = camera->get_ray(camera->context, x_on_image, y_on_image);
+
   f3_t tmp_normal;
   f3_t normal = {0, 0, 0};
   float tmp_distance;
@@ -133,5 +129,8 @@
     return 1;
   }
 
-  write_image(argv[1], renderer, NULL, 1024, 1024);
+  camera_t camera =
+      perspective_camera(f3(0, 0, 0), 0, 0, 0, width / (float)height, FOV_X,
+                         perspective_camera_fov);
+  write_image(argv[1], renderer, &camera, width, height);
 }
결과 이미지

이미지 가로/세로 비율을 바꿔도 대충 잘 되는 것 같다.

--- ../3/main.c
+++ main.c
@@ -130,7 +130,7 @@
   }
 
   camera_t camera =
-      perspective_camera(f3(0, 0, 0), 0, 0, 0, width / (float)height, FOV_X,
-                         perspective_camera_fov);
+      perspective_camera(f3(0, 0, 0), 0.2f, 0.1f, 0.5f, width / (float)height,
+                         FOV_X, perspective_camera_fov);
   write_image(argv[1], renderer, &camera, width, height);
 }
결과 이미지

카메라 회전을 넣어도 마찬가지.

직교 카메라

orthogonal_camera.c
#include "camera.h"

#include "f3x3.h"
#include "util.h"

typedef struct orthogonal_camera_context {
  float width;
  float height;
  f3_t origin;
  f3_t direction;
  f3x3_t rotation;
} orthogonal_camera_context_t;

static ray_t get_ray(void *context, float x, float y) {
  orthogonal_camera_context_t *const ctx = context;
  const float offset_x = ctx->width * (x * 2.0f - 1.0f);
  const float offset_y = 0.0f;
  const float offset_z = -ctx->height * (y * 2.0f - 1.0f);
  const f3_t offset = {offset_x, offset_y, offset_z};
  const f3_t rotated = f3x3_mul_f3(offset, ctx->rotation);
  const f3_t origin = f3_add(ctx->origin, rotated);
  const ray_t result = {origin, ctx->direction};
  return result;
}

camera_t orthogonal_camera(f3_t camera_position, float yaw, float pitch,
                           float roll, float aspect_ratio,
                           camera_size_mode_t size_mode, float size) {
  orthogonal_camera_context_t context;
  if (size_mode == SIZE_WIDTH) {
    context.width = size;
    context.height = size / aspect_ratio;
  } else {
    context.width = size * aspect_ratio;
    context.height = size;
  }
  context.origin = camera_position;
  context.rotation = f3x3_rotate(yaw, pitch, roll);
  context.direction = f3x3_mul_f3(f3(0, 1, 0), context.rotation);
  const camera_t result = {memdup(&context, sizeof(context)), get_ray};
  return result;
}

직교 카메라는 origin이 픽셀의 화면상의 위치에 따라 적절히 떨어져 있으면 된다.

--- ../4/main.c
+++ main.c
@@ -13,7 +13,7 @@
 
 const size_t width = 960;
 const size_t height = 540;
-const float perspective_camera_fov = 1.5707963267948966f; // 90도
+const float orthogonal_camera_size = 5.0f;
 const f3_t camera_position = {0, 0, 0};
 const f3_t camera_direction = {0, 1, 0};
 const bmp_pixel_t white = {255, 255, 255};
@@ -130,7 +130,7 @@
   }
 
   camera_t camera =
-      perspective_camera(f3(0, 0, 0), 0.2f, 0.1f, 0.5f, width / (float)height,
-                         FOV_X, perspective_camera_fov);
+      orthogonal_camera(f3(0, 0, 0), 0.2f, 0.1f, 0.5f, width / (float)height,
+                        SIZE_WIDTH, orthogonal_camera_size);
   write_image(argv[1], renderer, &camera, width, height);
 }
결과 이미지
C
그래픽스
레이트레이싱
raytracer

Comments