들어가며
이 장의 핵심은 픽셀마다 광선을 쏘아 가장 가까운 물체의 색을 계산하는 것이다.
이를 위해 아래와 같은 과정을 준비하는 단계다.
- 카메라(눈) 설정
- 이미지 해상도 설정 (16:9 유지)
- 3D 공간에 가상의 뷰포트(이미지 평면) 생성
Ray.h
광선은 한 점에서 시작하여, 한 방향으로 무한히 뻗는 3D 반직선이다.
수학적으로는 다음과 같이 표현된다.
$$광선의\ 한\ 지점 = 시작점 + (방향 \times 거리) \\ P(t) =A + (dir \times t)$$
t 값을 변화시켜 광선 위의 한 점을 구할 수 있다.
이때 우리는 단순히 한 점을 구하는 것이 아니라, 물체와 충돌지점을 찾기 위함이다.
즉, 레이 트레이싱은 이 충돌 지점에 해당되는 t값을 구하는 문제라고 볼 수 있다.
#include "Vec3.h"
class Ray
{
public:
Ray() {}
// 시작점 : origin | 방향 : direction
Ray(const Point3& origin, const Vector3& direction)
: mOrig(origin), mDir(direction) {}
const Point3& Origin() const { return mOrig; }
const Vector3& Direction() const { return mDir; }
// 광선 위 한 점을 구하는 함수
Point3 At(double t) const
{
return mOrig + t * mDir;
}
private:
Point3 mOrig;
Vector3 mDir;
};
이제 광선을 쏘아볼 수 있는데, 어디서 쏘아야 할까?
실제 빛은 광원에서 시작하여, 수많은 반사와 굴절을 거쳐 우리 눈으로 들어오게 된다.
하지만 그 중 일부의 빛만 우리의 눈에 도달하게 된다.
그래서 모든 빛을 계산하게 된다면 비효율적이기 때문에,
역발상으로 눈에서부터 시작하는 광선을 역추적한다는 아이디어로부터 레이 트레이싱이 시작된다.!
그래서 광선은 우리의 눈인, 카메라에서 시작하게 된다.
이미지 사이즈 조정
우선, 디버깅 시 정사각형 이미지를 사용하면 x와 y를 혼동할 수 있는 문제가 생긴다.
그렇기 때문에 이미지의 종횡비를 적당한 16:9로 설정한 후, 너비만 가져와 높이는 자동으로 계산하는 방식으로 진행한다.
double aspectRatio = 16.0 / 9.0;
int imageWidth = 400;
int imageHeight = static_cast<int>(imageWidth / aspectRatio);
// 최소 크기가 1은 되어야 하기 때문에 1보다 작으면 1로 고정한다.
imageHeight = (imageHeight < 1) ? 1 : imageHeight;
이미지를 조정하게 되면 뷰포트도 그에 맞춰 비율을 맞춰주어야 한다.
double viewportHeight = 2.0;
double viewportWidth = viewportHeight * (static_cast<double>(imageWidth) / imageHeight);
여기서 왜 viewportWidth = viewportHeight * aspect_ratio; 를 사용하지 않냐면,
비율은 실수, 이미지 크기는 정수 이기 때문에 변환 과정에서 버림이 생기기 마련인데,
이때 오차가 생겨 aspect_ratio가 정확한 16/9의 비율이 아닐 수 있다.
그래서 뷰포트는 이상적인 비율보다는, 실제 렌더링 되는 이미지의 비율로 맞추는 게 더 중요하기 때문에 이미지의 비율을 또 계산해서 사용하는 것이다.
근데 여기서 뷰포트의 크기와 이미지 크기의 관계성이 좀 헷갈렸다.
간단히 하면, 뷰포트 크기는 시야각에 관련이 있고,
이미지의 크기는 뷰포트를 몇칸으로 쪼개서 볼 것인지 결정하는 픽셀 개수다.


Camera & Viewport
모든 광선의 시작점인 카메라를 간단하게 구현한다.
광선들은 카메라에서 출발하여 뷰포트의 각 픽셀의 중심으로 향하게 된다.

뷰포트 Y축 맞추기
하지만 뷰포트의 각 픽셀의 중심 위치를 구할 때 문제가 발생한다.
우리의 카메라는 오른손 좌표계를 사용하여 Y가 증가할수록 위로 올라간다.
하지만 뷰포트는 픽셀 좌표계를 사용하여 Y가 증가할 수록 아래로 내려간다.

이때 뷰포트의 중심에서 위로 30만큼의 위치를 구하려고 할 때, 아무 생각 없이 y를 양수로 아래처럼 작성할 수 있다.
Vector3 viewport_y = Vector3(0, 30, 0)
이렇게 만들면 위쪽으로 광선을 쏠 생각이었지만, 실상 아래로 발사되어 화면이 상하 반전되어 출력될 수 있다.
문제 설명은 장황했지만, 사실 좌표계를 맞추는 방법은 간단하다..
그저 뷰포트의 Y에 음수를 붙여주면 된다..!
Vector3 viewport_y = Vector3(0, -30, 0)
픽셀 중심 계산하기
이제 편하게 뷰포트 좌표계를 이용할 수 있다.
그럼 광선의 목적지인 각 픽셀의 중심을 구해보자.
먼저 뷰포트의 중심을 기준으로한 크기 벡터를 구한다.
Vec3 viewportU = Vec3(viewportWidth, 0, 0);
Vec3 viewportV = Vec3(0, -viewportHeight, 0);
그 후 뷰포트의 좌측상단 모서리 위치를 구한다.
Vec3 viewportUpperLeft =
cameraCenter
- Vec3(0, 0, focalLength) /* 카메라에서 뷰포트까지의 거리를 빼 뷰포트의 중심 구하기 */
- viewportU / 2 /* 뷰포트의 중심에서 전체 가로길이의 반을 빼 가장 좌측 위치 구하기 */
- viewportV / 2; /* 뷰포트의 좌측에서 전체 세로길이의 반을 빼 상단 위치 더하기 */
픽셀 1개의 크기를 구한다.
Vec3 pixelDeltaU = viewportU / imageWidth;
Vec3 pixelDeltaV = viewportV / imageHeight;
좌측상단 모서리에서 (픽셀 1개의 크기 / 2)를 구하여 더하면 좌측상단 픽셀의 중심을 구할 수 있다.
Vec3 pixel00Loc = viewportUpperLeft + 0.5 * (pixelDeltaU + pixelDeltaV);
RayColor
아직 광선의 충돌을 만들지 않았으므로, 광선을 정규화하여 흰색과 하늘색을 선형 보간하여 색을 추출한다.
Color RayColor(const Ray& r)
{
// 광선의 방향 정규화
Vec3 unitDirection = UnitVector(r.Direction());
double a = 0.5 * (unitDirection.Y() + 1.0);
// 광선의 Y값을 푸른색과 곱한다.
return (1.0 - a) * Color(1.0, 1.0, 1.0) + a * Color(0.5, 0.7, 1.0);
}
Render
모든 픽셀의 중심을 구할 수 있고, 광선을 이용해 색을 추출하는 함수도 있으니 광선을 쏘아본다.
// Render
std::cout << "P3\n" << imageWidth << " " << imageHeight << "\n255\n";
for (int j = 0; j < imageHeight; j++)
{
std::clog << "\rScanlines remaining: " << (imageHeight - j) << ' ' << std::flush;
for (int i = 0; i < imageWidth; i++)
{
// [j][i]번째 픽셀의 중심 구하기
auto pixelCenter = pixel00Loc + (i * pixelDeltaU) + (j * pixelDeltaV);
// 광선의 방향 구하기
auto rayDirection = pixelCenter - cameraCenter;
// 광선 생성
Ray r(cameraCenter, rayDirection);
// 생성한 광선을 통해 색 추출
Color pixelColor = RayColor(r);
WriteColor(std::cout, pixelColor);
}
}
Result

2D 픽셀 좌표를 3D 공간상의 한 점으로 변환할 수 있게 됐다.
즉, 카메라 공간과 이미지 공간을 연결하는 매핑을 완성했다!
'C++ > Ray Tracing' 카테고리의 다른 글
| [03] Adding a Sphere (0) | 2026.03.03 |
|---|---|
| [01] Vector class (0) | 2026.02.24 |
| [00] Output an Image (0) | 2026.02.20 |
