Company
교육 철학
📷

4. Rays, a Simple Camera, and Background (광선, 간단한 카메라, 그리고 배경)

4. Rays, a Simple Camera, and Background(광선, 간단한 카메라, 그리고 배경)

4.1 The ray Class(ray 클래스)

모든 레이 트레이서가 가지고 있는 한 가지는 광선 클래스와 광선을 따라 어떤 색상이 보이는지 계산하는 것입니다. 광선을 함수 P(t)=A+tb로 생각해봅시다. 여기서 P는 3D 선을 따른 3D 위치입니다. A는 광선의 원점이고 b는 광선의 방향입니다. 광선 매개변수 t는 실수입니다(코드에서는 double). 다른 t를 대입하면 P(t)는 광선을 따라 점을 이동시킵니다. 음수 t 값을 추가하면 3D 선의 어디든 갈 수 있습니다. 양수 t의 경우, A 앞의 부분만 얻으며, 이것을 종종 반직선 또는 광선이라고 합니다.
*그림 2: 선형 보간*
광선의 개념을 클래스로 표현할 수 있으며, 함수 P(t)를 다음과 같이 호출할 함수로 표현할 수 있습니다:
ray::at(t)
Plain Text
복사
#pragma once #ifndef RAY_H #define RAY_H #include "Vec3.h" class Ray { public: Ray() {} 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; }; #endif
C++
복사
*리스트 7: [ray.h] ray 클래스*
(C++에 익숙하지 않은 분들을 위해 설명하자면, ray::origin()ray::direction() 함수는 모두 멤버에 대한 불변 참조를 반환합니다. 호출자는 참조를 직접 사용하거나 필요에 따라 변경 가능한 복사본을 만들 수 있습니다.)

4.2 Sending Rays Into the Scene(장면에 광선 보내기)

이제 본격적으로 레이 트레이서를 만들 준비가 되었습니다. 레이 트레이서의 핵심은 픽셀을 통해 광선을 보내고 그 방향에서 보이는 색상을 계산하는 것입니다. 주요 단계는 다음과 같습니다:
1.
"눈"에서 픽셀을 통과하는 광선을 계산합니다.
2.
광선이 교차하는 객체를 결정합니다.
3.
가장 가까운 교차점의 색상을 계산합니다.
레이 트레이서를 처음 개발할 때는 항상 간단한 카메라로 시작하여 코드를 작동시킵니다.
디버깅 시 정사각형 이미지를 사용하면 x와 y를 자주 혼동하여 문제가 생기기 때문에, 정사각형이 아닌 이미지를 사용하겠습니다. 정사각형 이미지는 너비와 높이가 같아 1:1 종횡비를 가집니다. 여기서는 매우 일반적인 16:9 종횡비를 선택하겠습니다. 16:9 종횡비는 이미지 너비 대 높이의 비율이 16:9임을 의미합니다. 다시 말해,
width/height=16/9=1.7778width/height=16/9=1.7778
실용적인 예로, 너비 800픽셀, 높이 400픽셀의 이미지는 2:1 종횡비를 가집니다.
이미지의 종횡비는 너비 대 높이의 비율로 결정됩니다. 하지만 특정 종횡비를 염두에 두고 있으므로, 이미지 너비와 종횡비를 먼저 설정한 다음 이를 사용하여 높이를 계산하는 것이 더 쉽습니다. 이렇게 하면 이미지 너비를 변경하여 크기를 조정할 때 원하는 종횡비를 유지할 수 있습니다. 이미지 높이를 계산할 때는 결과가 최소 1이 되도록 해야 합니다.
렌더링된 이미지의 픽셀 치수를 설정하는 것 외에도, 장면 광선을 통과시킬 가상 뷰포트를 설정해야 합니다. 뷰포트는 3D 공간에서 이미지 픽셀 위치의 그리드를 포함하는 가상 직사각형입니다. 픽셀이 가로와 세로로 동일한 간격을 가지면, 뷰포트는 렌더링된 이미지와 같은 종횡비를 가집니다. 인접한 두 픽셀 사이의 거리를 픽셀 간격이라고 하며, 정사각형 픽셀이 표준입니다.
시작하기 위해 임의의 뷰포트 높이 2.0을 선택하고, 원하는 종횡비를 얻기 위해 뷰포트 너비를 조정하겠습니다. 코드 스니펫은 다음과 같습니다:
auto aspectRatio = 16.0 / 9.0; int imageWidth = 400; // Calculate the image height, and ensure that it's at least 1. int imageHeight = int(imageWidth / aspectRatio); imageHeight = (imageHeight < 1) ? 1 : imageHeight; // Viewport widths less than one are ok since they are real valued. auto viewportHeight = 2.0; auto viewportWidth = viewportHeight * (double(imageWidth) / imageHeight);
C++
복사
*리스트 8: 렌더링된 이미지 설정*
viewport_width를 계산할 때 왜 aspect_ratio를 직접 사용하지 않는지 궁금할 수 있습니다. aspect_ratio에 설정된 값은 이상적인 비율이며, image_widthimage_height 사이의 실제 비율과 다를 수 있기 때문입니다. image_height가 실수 값을 가질 수 있다면 aspect_ratio를 사용해도 괜찮을 것입니다. 하지만 image_widthimage_height 사이의 실제 비율은 두 가지 이유로 달라질 수 있습니다. 첫째, image_height는 가장 가까운 정수로 내림되어 비율이 증가할 수 있습니다. 둘째, image_height가 1보다 작아지는 것을 허용하지 않으므로, 이것도 실제 종횡비를 변경할 수 있습니다.
aspect_ratio는 이상적인 비율이며, 정수 기반 이미지 너비 대 높이의 비율로 최대한 근사합니다. 뷰포트 비율이 이미지 비율과 정확히 일치하도록, 계산된 이미지 종횡비를 사용하여 최종 뷰포트 너비를 결정합니다.
다음으로 카메라 중심을 정의하겠습니다. 카메라 중심은 모든 장면 광선이 시작되는 3D 공간의 점입니다(일반적으로 시점이라고도 합니다). 카메라 중심에서 뷰포트 중심까지의 벡터는 뷰포트에 직교합니다. 처음에는 뷰포트와 카메라 중심점 사이의 거리를 1단위로 설정하겠습니다. 이 거리를 초점 거리라고 합니다.
단순함을 위해 카메라 중심을 (0,0,0)에 배치하겠습니다. y축은 위로, x축은 오른쪽으로, 음의 z축은 보는 방향을 가리키도록 설정합니다. 이를 오른손 좌표계라고 합니다.
*그림 3: 카메라 기하학*
이제 피할 수 없는 까다로운 부분입니다. 우리의 3D 공간은 위의 규칙을 따르지만, 이미지 좌표와는 충돌합니다. 이미지 좌표에서는 왼쪽 위에 0번째 픽셀이 있고 오른쪽 아래까지 작업합니다. 따라서 이미지 좌표의 Y축은 반전됩니다. Y는 이미지 아래로 갈수록 증가합니다.
이미지를 스캔할 때 왼쪽 위 픽셀(픽셀 0,0)에서 시작합니다. 각 행을 왼쪽에서 오른쪽으로 스캔한 다음, 위에서 아래로 행별로 스캔합니다. 픽셀 그리드를 탐색하기 위해 두 벡터를 사용합니다: 왼쪽 가장자리에서 오른쪽 가장자리로의 벡터(Vu)와 위쪽 가장자리에서 아래쪽 가장자리로의 벡터(Vv)입니다.
픽셀 그리드는 뷰포트 가장자리에서 픽셀 간 거리의 절반만큼 안쪽에 위치합니다. 이렇게 하면 뷰포트 영역이 너비 × 높이의 동일한 영역으로 균등하게 분할됩니다.
*그림 4: 뷰포트와 픽셀 그리드*
이 그림에는 뷰포트, 7×5 해상도 이미지의 픽셀 그리드, 뷰포트 왼쪽 위 모서리 Q, 픽셀 P0,0 위치, 뷰포트 벡터 Vu (viewport_u), 뷰포트 벡터 Vv (viewport_v), 그리고 픽셀 델타 벡터 Δu와 Δv가 표시되어 있습니다.
이를 바탕으로 카메라를 구현하는 코드는 다음과 같습니다. 주어진 장면 광선에 대한 색상을 반환하는 함수
Color RayColor(const Ray& r)
Plain Text
복사
를 스텁으로 만들겠습니다. 지금은 항상 검정색을 반환하도록 설정합니다.
#include "Color.h" #include "Ray.h" #include "Vec3.h" #include <iostream> Color RayColor(const Ray& r) { return Color(0,0,0); } int main() { // Image auto aspectRatio = 16.0 / 9.0; int imageWidth = 400; // 이미지 높이를 계산하고 최소 1이 되도록 합니다. int imageHeight = int(imageWidth / aspectRatio); imageHeight = (imageHeight < 1) ? 1 : imageHeight; // Camera auto focalLength = 1.0; auto viewportHeight = 2.0; auto viewportWidth = viewportHeight * (double(imageWidth)/imageHeight); auto cameraCenter = Point3(0, 0, 0); // 뷰포트의 수평 및 수직 가장자리를 가로지르는 벡터를 계산합니다. auto viewportU = Vec3(viewportWidth, 0, 0); auto viewportV = Vec3(0, -viewportHeight, 0); // 픽셀 간 수평 및 수직 델타 벡터를 계산합니다. auto pixelDeltaU = viewportU / imageWidth; auto pixelDeltaV = viewportV / imageHeight; // 왼쪽 위 픽셀의 위치를 계산합니다. auto viewportUpperLeft = cameraCenter - Vec3(0, 0, focalLength) - viewportU/2 - viewportV/2; auto pixel00Loc = viewportUpperLeft + 0.5 * (pixelDeltaU + pixelDeltaV); // 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++) { auto pixelCenter = pixel00Loc + (i * pixelDeltaU) + (j * pixelDeltaV); auto rayDirection = pixelCenter - cameraCenter; Ray r(cameraCenter, rayDirection); Color pixelColor = RayColor(r); WriteColor(std::cout, pixelColor); } } std::clog << "\rDone. \n"; }
C++
복사
*리스트 9: [main.cc] 장면 광선 생성*
위 코드에서 ray_direction을 단위 벡터로 만들지 않았다는 점에 주목하세요. 단위 벡터로 만들지 않는 것이 코드를 더 간단하고 빠르게 만들기 때문입니다.
이제 ray_color(ray) 함수를 채워서 간단한 그라디언트를 구현하겠습니다. 이 함수는 광선 방향을 단위 길이로 정규화한 , y 좌표의 높이에 따라 흰색과 파란색을 선형으로 혼합합니다(−1.0<y<1.0). 벡터 정규화 후 y 높이를 사용하기 때문에, 수직 그라디언트 외에도 수평 그라디언트가 색상에 나타납니다.
0.0≤a≤1.0을 선형으로 스케일링하는 표준 그래픽 기법을 사용하겠습니다. a=1.0일 때는 파란색, a=0.0일 때는 흰색을 원합니다. 그 사이에는 두 색상의 혼합을 원합니다. 이것을 "선형 혼합" 또는 "선형 보간"이라고 합니다. 일반적으로 두 값 사이의 lerp라고도 부릅니다. lerp는 항상 다음 형태를 따릅니다.
blendedValue = (1−a)⋅startValue + a⋅endValue,
C++
복사
여기서 a는 0에서 1로 변합니다.
이 모든 것을 종합하면 다음과 같습니다:
#include "color.h" #include "ray.h" #include "vec3.h" #include <iostream> Color RayColor(const Ray& r) { Vec3 unitDirection = UnitVector(r.Direction()); auto a = 0.5 * (unitDirection.Y() + 1.0); return (1.0 - a) * Color(1.0, 1.0, 1.0) + a * Color (0.5, 0.7, 1.0); } ...
C++
복사
*리스트 10: [main.cc] 파란색에서 흰색으로의 그라디언트 렌더링*
우리의 경우 다음과 같은 결과가 나옵니다:
*이미지 2: 광선 Y 좌표에 따른 파란색에서 흰색으로의 그라디언트*