**주말 동안 배우는 레이 트레이싱** [Peter Shirley][], [Trevor David Black][], [Steve Hollasch][]
버전 4.0.2, 2025-04-25
Copyright 2018-2024 Peter Shirley. 모든 권리를 보유합니다. 개요 ==================================================================================================== 저는 수년간 많은 그래픽스 강의를 해 왔습니다. 저는 종종 레이 트레이싱으로 강의를 진행하는데, 모든 코드를 직접 작성해야 하지만 API 없이도 멋진 이미지를 얻을 수 있기 때문입니다. 저는 여러분이 가능한 한 빨리 멋진 프로그램을 만들 수 있도록 제 강의 노트를 따라 하기 안내서로 바꾸기로 했습니다. 모든 기능을 갖춘 레이 트레이서는 아니지만, 레이 트레이싱을 영화의 필수 요소로 만든 간접 조명은 갖추고 있습니다. 이 단계를 따르면, 여러분이 흥미를 느껴 더 나아가고 싶어질 경우 여러분이 만든 레이 트레이서의 구조는 더 광범위한 레이 트레이서로 확장하기에 좋을 것입니다. 누군가 “레이 트레이싱”이라고 말할 때 그것은 여러 가지를 의미할 수 있습니다. 제가 설명하려는 것은 엄밀히 말하면 경로 추적기이며, 상당히 범용적인 것입니다. 코드는 꽤 단순하겠지만(작업은 컴퓨터에게 맡기십시오!) 여러분이 만들 수 있는 이미지에 매우 만족하실 것이라고 생각합니다. 제가 레이 트레이서를 작성하는 순서대로 몇 가지 디버깅 팁과 함께 여러분을 안내하겠습니다. 마지막에는 훌륭한 이미지를 만들어 내는 레이 트레이서를 갖게 될 것입니다. 주말 동안 이 작업을 해낼 수 있을 것입니다. 더 오래 걸리더라도 걱정하지 마십시오. 저는 C++를 주 언어로 사용하지만, 여러분이 꼭 그럴 필요는 없습니다. 그러나 C++를 사용하시기를 권합니다. 빠르고, 이식성이 좋으며, 대부분의 상용 영화 및 비디오 게임 렌더러가 C++로 작성되어 있기 때문입니다. 저는 C++의 “현대적 기능” 대부분을 피하지만, 상속과 연산자 오버로딩은 레이 트레이서에 너무 유용하므로 포기할 수 없었다는 점에 유의하십시오. > 저는 코드를 온라인으로 제공하지 않지만, 코드는 실제로 존재하며 `vec3` 클래스의 몇 가지 간단한 > 연산자를 제외하고는 모두 보여 드립니다. 저는 코드를 직접 입력하면서 배우는 것을 굳게 믿지만, 코드가 > 제공되면 저도 그것을 사용하므로, 코드가 제공되지 않을 때만 제가 말한 바를 실천하게 됩니다. 그러니 > 요청하지 마십시오! 제가 얼마나 180도 바뀌었는지가 재미있어서 이 마지막 부분을 남겨 두었습니다. 여러 독자가 미묘한 오류를 겪었는데, 코드를 비교해 보았을 때 도움이 되었습니다. 그러니 코드를 직접 입력해 보시기 바랍니다. 다만 각 책의 완성된 소스는 [GitHub의 RayTracing 프로젝트][repo]에서 찾을 수 있습니다. 이 책들의 구현 코드에 관한 참고 사항입니다 -- 포함된 코드에 대한 저희의 철학은 다음 목표를 우선시합니다: - 코드는 책에서 다루는 개념을 구현해야 합니다. - 저희는 C++를 사용하지만, 가능한 한 단순하게 사용합니다. 저희의 프로그래밍 스타일은 C와 매우 비슷하지만, 코드를 더 쉽게 사용하거나 이해할 수 있게 해 주는 경우에는 현대적 기능을 활용합니다. - 저희의 코딩 스타일은 연속성을 위해 원래 책에서 확립된 스타일을 가능한 한 이어 갑니다. - 코드베이스와 책에 실린 코드 목록 사이에서 줄을 일관되게 유지하기 위해, 줄 길이는 한 줄당 96자로 유지합니다. 따라서 이 코드는 기본 구현을 제공하며, 독자가 즐길 수 있는 수많은 개선 여지를 남겨 둡니다. 코드를 최적화하고 현대화하는 방법은 끝없이 많지만, 저희는 단순한 해법을 우선시합니다. 저희는 독자가 벡터(내적과 벡터 덧셈 등)에 어느 정도 익숙하다고 가정합니다. 이를 모르신다면 간단히 복습하십시오. 그 복습이 필요하거나 처음 배우려는 경우에는 Morgan McGuire의 온라인 [_Graphics Codex_][gfx-codex], Steve Marschner와 Peter Shirley의 _Fundamentals of Computer Graphics_, 또는 J.D. Foley와 Andy Van Dam의 _Computer Graphics: Principles and Practice_ 를 참고하십시오(모두 그래픽스 입문서입니다). 이 프로젝트에 관한 정보, GitHub 저장소, 디렉터리 구조, 빌드 및 실행, 그리고 수정 사항과 기여를 하거나 참조하는 방법은 [프로젝트 README][readme] 파일을 참고하십시오. 프로젝트와 관련된 추가 자료는 [추가 읽을거리 위키 페이지][wiki-further]를 참고하십시오. 이 책들은 브라우저에서 바로 잘 인쇄되도록 서식이 지정되어 있습니다. 또한 [각 릴리스마다][releases] "Assets" 섹션에 각 책의 PDF를 포함하고 있습니다. 저희에게 연락하고 싶으시면 언제든지 다음 주소로 이메일을 보내 주십시오: - Peter Shirley, ptrshrl@gmail.com - Steve Hollasch, steve@hollasch.net - Trevor David Black, trevordblack@trevord.black 마지막으로, 구현 중에 문제가 생기거나, 일반적인 질문이 있거나, 자신의 아이디어나 작업을 공유하고 싶으시면 GitHub 프로젝트의 [GitHub Discussions 포럼][discussions]을 참고하십시오. 이 프로젝트에 도움을 주신 모든 분께 감사드립니다. 그분들은 이 책 끝의 감사의 글 절에서 확인하실 수 있습니다. 이제 시작하겠습니다! 이미지 출력하기 ==================================================================================================== PPM 이미지 형식 --------------------- 렌더러를 시작할 때마다 이미지를 볼 방법이 필요합니다. 가장 간단한 방법은 이미지를 파일로 쓰는 것입니다. 문제는 형식이 너무나 많다는 점입니다. 그중 상당수는 복잡합니다. 저는 항상 일반 텍스트 ppm 파일로 시작합니다. 다음은 Wikipedia에 있는 좋은 설명입니다: ![Figure [ppm]: PPM 예제](../images/fig-1.01-ppm.jpg)
이러한 파일을 출력하는 C++ 코드를 작성해 보겠습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include int main() { // Image int image_width = 256; int image_height = 256; // Render std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; for (int j = 0; j < image_height; j++) { for (int i = 0; i < image_width; i++) { auto r = double(i) / (image_width-1); auto g = double(j) / (image_height-1); auto b = 0.0; int ir = int(255.999 * r); int ig = int(255.999 * g); int ib = int(255.999 * b); std::cout << ir << ' ' << ig << ' ' << ib << '\n'; } } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-initial]: [main.cc] 첫 번째 이미지 만들기]
이 코드에서 주목할 점이 몇 가지 있습니다: 1. 픽셀은 행 단위로 기록됩니다. 2. 픽셀의 각 행은 왼쪽에서 오른쪽으로 기록됩니다. 3. 이 행들은 위에서 아래로 출력됩니다. 4. 관례상 빨강/초록/파랑 각 성분은 내부적으로 0.0에서 1.0 범위의 실숫값 변수로 표현됩니다. 이 값들은 출력하기 전에 0에서 255 사이의 정수 값으로 변환해야 합니다. 5. 빨강은 왼쪽에서 오른쪽으로 갈수록 완전히 꺼진 상태(검정)에서 완전히 켜진 상태(밝은 빨강)로 변하고, 초록은 위쪽의 완전히 꺼진 상태(검정)에서 아래쪽의 완전히 켜진 상태(밝은 초록)로 변합니다. 빨간빛과 초록빛을 더하면 노란색이 되므로 오른쪽 아래 모서리는 노란색일 것으로 예상할 수 있습니다. 이미지 파일 만들기 ----------------------- 파일이 표준 출력 스트림에 기록되므로 이를 이미지 파일로 리디렉션해야 합니다. 일반적으로 이 작업은 명령줄에서 `>` 리디렉션 연산자를 사용하여 수행합니다. Windows에서는 다음 명령을 실행하여 CMake로 디버그 빌드를 만들 수 있습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ cmake -B build cmake --build build ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 그런 다음 새로 빌드한 프로그램을 다음과 같이 실행하십시오. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ build\Debug\inOneWeekend.exe > image.ppm ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 나중에는 속도를 위해 최적화된 빌드를 실행하는 것이 더 좋습니다. 그 경우에는 다음과 같이 빌드합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ cmake --build build --config release ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 그리고 최적화된 프로그램은 다음과 같이 실행합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ build\Release\inOneWeekend.exe > image.ppm ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 위 예시는 포함된 소스의 `CMakeLists.txt` 파일과 같은 방식으로 CMake를 사용하여 빌드한다고 가정합니다. 가장 편한 빌드 환경(및 언어)을 사용하십시오. Mac이나 Linux에서 릴리스 빌드의 경우 다음과 같이 프로그램을 실행합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ build/inOneWeekend > image.ppm ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 전체 빌드 및 실행 방법은 [프로젝트 README][readme]에서 확인할 수 있습니다. 출력 파일을 열면(저는 Mac의 `ToyViewer`에서 열었지만, 즐겨 쓰는 이미지 뷰어에서 열어 보고 뷰어가 지원하지 않으면 “ppm viewer”를 Google에서 검색해 보십시오) 다음과 같은 결과가 나타납니다. ![Image 1: 첫 번째 PPM 이미지 ](../images/img-1.01-first-ppm-image.png class='pixel') 만세! 이것이 그래픽스의 “hello world”입니다. 이미지가 이와 같지 않다면 출력 파일을 텍스트 편집기로 열어 어떤 모습인지 확인하십시오. 대략 다음과 같이 시작해야 합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ P3 256 256 255 0 0 0 1 0 0 2 0 0 3 0 0 4 0 0 5 0 0 6 0 0 7 0 0 8 0 0 9 0 0 10 0 0 11 0 0 12 0 0 ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [first-img]: 첫 번째 이미지 출력] PPM 파일이 이와 같지 않다면 서식 지정 코드를 다시 확인하십시오. 이와 같이 _보이는데도_ 렌더링되지 않는다면 줄 끝 문자 차이나 그와 비슷한 무언가가 이미지 뷰어를 혼란스럽게 하고 있을 수 있습니다. 디버깅에 도움이 되도록 Github 프로젝트의 `images` 디렉터리에 `test.ppm` 파일을 준비해 두었습니다. 이 파일은 뷰어가 PPM 형식을 처리할 수 있는지 확인하고, 직접 생성한 PPM 파일과 비교하는 데 도움이 될 것입니다. 일부 독자는 Windows에서 생성한 파일을 보는 데 문제가 있다고 보고했습니다. 이 경우 문제는 대개 PPM이 UTF-16으로 기록되는 것이며, 주로 PowerShell에서 발생합니다. 이 문제가 발생하면 [Discussion 1114](https://github.com/RayTracing/raytracing.github.io/discussions/1114)에서 도움을 받으십시오. 모든 것이 올바르게 표시된다면 시스템 및 IDE 관련 이슈는 거의 해결된 것입니다 -- 이 시리즈의 나머지 부분에서는 모두 렌더링된 이미지를 생성할 때 이와 같은 간단한 방식을 사용합니다. 다른 이미지 형식을 만들고 싶다면, 저는 GitHub 에서 구할 수 있는 헤더 전용 이미지 라이브러리인 `stb_image.h`를 좋아합니다. 진행 표시기 추가하기 ---------------------------- 계속하기 전에 출력에 진행 표시기를 추가해 보겠습니다. 이는 오래 걸리는 렌더링의 진행 상황을 추적하고, 무한 루프나 기타 문제로 인해 멈춘 실행을 식별하는 데에도 유용한 방법입니다.
우리 프로그램은 이미지를 표준 출력 스트림(`std::cout`)으로 출력하므로, 이는 그대로 두고 대신 로그 출력 스트림(`std::clog`)에 기록합니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ for (int j = 0; j < image_height; ++j) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ for (int i = 0; i < image_width; i++) { auto r = double(i) / (image_width-1); auto g = double(j) / (image_height-1); auto b = 0.0; int ir = int(255.999 * r); int ig = int(255.999 * g); int ib = int(255.999 * b); std::cout << ir << ' ' << ig << ' ' << ib << '\n'; } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight std::clog << "\rDone. \n"; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-progress]: [main.cc] 진행 상황 보고가 포함된 메인 렌더 루프]
이제 실행하면 남은 스캔라인 수가 계속 갱신되며 표시되는 것을 볼 수 있습니다. 아마 너무 빨리 실행되어서 보지도 못할 것입니다! 걱정하지 마십시오 -- 레이 트레이서를 확장해 나가면서 앞으로 천천히 갱신되는 진행 표시줄을 지켜볼 시간이 충분히 있을 것입니다. vec3 클래스 ==================================================================================================== 거의 모든 그래픽스 프로그램에는 기하 벡터와 색상을 저장하기 위한 클래스가 하나 이상 있습니다. 많은 시스템에서 이러한 벡터는 4D입니다(기하의 경우 3D 위치에 동차 좌표를 더한 것, 색상의 경우 RGB에 알파 투명도 성분을 더한 것). 우리의 목적에는 세 개의 좌표로 충분합니다. 색상, 위치, 방향, 오프셋 등 무엇이든 동일한 `vec3` 클래스를 사용하겠습니다. 어떤 사람들은 이 방식이 색상에서 위치를 빼는 것과 같은 어리석은 일을 막아 주지 못하기 때문에 좋아하지 않습니다. 일리 있는 지적이지만, 명백히 잘못된 것이 아니라면 우리는 항상 “코드가 더 적은” 쪽을 택하겠습니다. 그럼에도 불구하고 `vec3`에 대해 `point3`와 `color`라는 두 개의 별칭을 선언합니다. 이 두 타입은 `vec3`의 별칭일 뿐이므로 `point3`를 기대하는 함수에 `color`를 전달해도 경고가 발생하지 않으며, `color`에 `point3`를 더하는 것도 막을 수 없지만, 코드를 조금 더 읽고 이해하기 쉽게 만들어 줍니다. 새 `vec3.h` 헤더 파일의 위쪽 절반에서 `vec3` 클래스를 정의하고, 아래쪽 절반에서 유용한 벡터 유틸리티 함수들을 정의합니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef VEC3_H #define VEC3_H #include #include class vec3 { public: double e[3]; vec3() : e{0,0,0} {} vec3(double e0, double e1, double e2) : e{e0, e1, e2} {} double x() const { return e[0]; } double y() const { return e[1]; } double z() const { return e[2]; } vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); } double operator[](int i) const { return e[i]; } double& operator[](int i) { return e[i]; } vec3& operator+=(const vec3& v) { e[0] += v.e[0]; e[1] += v.e[1]; e[2] += v.e[2]; return *this; } vec3& operator*=(double t) { e[0] *= t; e[1] *= t; e[2] *= t; return *this; } vec3& operator/=(double t) { return *this *= 1/t; } double length() const { return std::sqrt(length_squared()); } double length_squared() const { return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; } }; // point3 is just an alias for vec3, but useful for geometric clarity in the code. using point3 = vec3; // Vector Utility Functions inline std::ostream& operator<<(std::ostream& out, const vec3& v) { return out << v.e[0] << ' ' << v.e[1] << ' ' << v.e[2]; } inline vec3 operator+(const vec3& u, const vec3& v) { return vec3(u.e[0] + v.e[0], u.e[1] + v.e[1], u.e[2] + v.e[2]); } inline vec3 operator-(const vec3& u, const vec3& v) { return vec3(u.e[0] - v.e[0], u.e[1] - v.e[1], u.e[2] - v.e[2]); } inline vec3 operator*(const vec3& u, const vec3& v) { return vec3(u.e[0] * v.e[0], u.e[1] * v.e[1], u.e[2] * v.e[2]); } inline vec3 operator*(double t, const vec3& v) { return vec3(t*v.e[0], t*v.e[1], t*v.e[2]); } inline vec3 operator*(const vec3& v, double t) { return t * v; } inline vec3 operator/(const vec3& v, double t) { return (1/t) * v; } inline double dot(const vec3& u, const vec3& v) { return u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]; } inline vec3 cross(const vec3& u, const vec3& v) { return vec3(u.e[1] * v.e[2] - u.e[2] * v.e[1], u.e[2] * v.e[0] - u.e[0] * v.e[2], u.e[0] * v.e[1] - u.e[1] * v.e[0]); } inline vec3 unit_vector(const vec3& v) { return v / v.length(); } #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [vec3-class]: [vec3.h] vec3 정의와 헬퍼 함수] 여기서는 `double`을 사용하지만, 일부 레이 트레이서는 `float`을 사용합니다. `double`은 정밀도와 범위가 더 크지만, `float`에 비해 크기가 두 배입니다. 이러한 크기 증가는 제한된 메모리 조건(예: 하드웨어 셰이더)에서 프로그래밍하는 경우 중요할 수 있습니다. 어느 쪽이든 괜찮습니다 -- 취향에 따라 선택하십시오. 색상 유틸리티 함수 ------------------------ 새 `vec3` 클래스를 사용하여 새 `color.h` 헤더 파일을 만들고, 단일 픽셀의 색상을 표준 출력 스트림에 기록하는 유틸리티 함수를 정의하겠습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef COLOR_H #define COLOR_H #include "vec3.h" #include using color = vec3; void write_color(std::ostream& out, const color& pixel_color) { auto r = pixel_color.x(); auto g = pixel_color.y(); auto b = pixel_color.z(); // Translate the [0,1] component values to the byte range [0,255]. int rbyte = int(255.999 * r); int gbyte = int(255.999 * g); int bbyte = int(255.999 * b); // Write out the pixel color components. out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n'; } #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [color]: [color.h] 색상 유틸리티 함수]
이제 이 두 가지를 모두 사용하도록 main을 수정할 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "color.h" #include "vec3.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include int main() { // Image int image_width = 256; int image_height = 256; // Render std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; for (int j = 0; j < image_height; j++) { std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; for (int i = 0; i < image_width; i++) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto pixel_color = color(double(i)/(image_width-1), double(j)/(image_height-1), 0); write_color(std::cout, pixel_color); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } } std::clog << "\rDone. \n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ppm-2]: [main.cc] 첫 번째 PPM 이미지를 위한 최종 코드]
그러면 이전과 정확히 같은 그림을 얻을 수 있을 것입니다. 광선, 간단한 카메라, 그리고 배경 ==================================================================================================== ray 클래스 -------------- 모든 레이 트레이서가 공통으로 갖는 한 가지는 광선 클래스와, 광선을 따라 보이는 색을 계산하는 과정입니다. 광선을 함수 $\mathbf{P}(t) = \mathbf{A} + t \mathbf{b}$로 생각해 봅시다. 여기서 $\mathbf{P}$ 는 3D 공간의 직선 위에 있는 3D 위치입니다. $\mathbf{A}$ 는 광선의 원점이고 $\mathbf{b}$ 는 광선 방향입니다. 광선 매개변수 $t$는 실수(코드에서는 `double`)입니다. 다른 $t$를 대입하면 $\mathbf{P}(t)$는 광선을 따라 점을 이동시킵니다. 음수 $t$ 값까지 더하면 3D 직선 위의 어느 곳으로든 갈 수 있습니다. 양수 $t$에 대해서는 $\mathbf{A}$ 앞쪽 부분만 얻게 되며, 이것이 흔히 반직선 또는 광선이라고 부르는 것입니다. ![Figure [lerp]: 선형 보간](../images/fig-1.02-lerp.jpg)
광선이라는 개념을 클래스로 표현하고, 함수 $\mathbf{P}(t)$를 `ray::at(t)`라고 부를 함수로 표현할 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef RAY_H #define RAY_H #include "vec3.h" class ray { public: ray() {} ray(const point3& origin, const vec3& direction) : orig(origin), dir(direction) {} const point3& origin() const { return orig; } const vec3& direction() const { return dir; } point3 at(double t) const { return orig + t*dir; } private: point3 orig; vec3 dir; }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-initial]: [ray.h] 광선 클래스]
(C++에 익숙하지 않은 분들을 위해 설명하자면, 함수 `ray::origin()`과 `ray::direction()`은 둘 다 해당 멤버에 대한 불변 참조를 반환합니다. 호출하는 쪽에서는 필요에 따라 참조를 그대로 사용하거나 변경 가능한 복사본을 만들 수 있습니다.) 장면으로 광선 보내기 ---------------------------- 이제 본격적으로 레이 트레이서를 만들 준비가 되었습니다. 레이 트레이서의 핵심은 픽셀을 통과하도록 광선을 보내고 그 광선 방향으로 보이는 색을 계산하는 것입니다. 관련 단계는 다음과 같습니다. 1. “눈”에서 픽셀을 통과하는 광선을 계산하고, 2. 광선이 어떤 물체와 교차하는지 판별하고, 3. 가장 가까운 교차점의 색을 계산합니다. 레이 트레이서를 처음 개발할 때, 저는 코드를 작동시키기 위해 항상 간단한 카메라부터 만듭니다. 저는 디버깅할 때 정사각형 이미지를 사용하다가 $x$와 $y$를 너무 자주 뒤바꾸는 바람에 곤란을 겪은 적이 많으므로, 정사각형이 아닌 이미지를 사용하겠습니다. 정사각형 이미지는 너비와 높이가 같으므로 종횡비가 1∶1입니다. 정사각형이 아닌 이미지를 원하므로, 매우 흔히 쓰이는 16∶9를 선택하겠습니다. 16∶9 종횡비란 이미지 너비 대 이미지 높이의 비율이 16∶9라는 뜻입니다. 다시 말해, 종횡비가 16∶9인 이미지가 주어지면, $$\text{width} / \text{height} = 16 / 9 = 1.7778$$ 실제 예를 들면, 너비 800픽셀, 높이 400픽셀인 이미지의 종횡비는 2∶1입니다. 이미지의 종횡비는 너비 대 높이의 비율로 결정할 수 있습니다. 하지만 우리는 이미 정해 둔 종횡비가 있으므로, 이미지의 너비와 종횡비를 설정한 다음 이를 이용해 높이를 계산하는 편이 더 쉽습니다. 이렇게 하면 이미지 너비를 바꾸는 것만으로 이미지를 확대하거나 축소할 수 있으며, 원하는 종횡비가 어긋나지 않습니다. 다만 이미지 높이를 구할 때 결과 높이가 최소 1이 되도록 해야 합니다. 렌더링된 이미지의 픽셀 크기를 설정하는 것 외에도, 장면 광선이 통과할 가상의 _뷰포트_ 를 설정해야 합니다. 뷰포트는 이미지 픽셀 위치의 격자를 담고 있는 3D 세계 속의 가상 직사각형입니다. 픽셀의 가로 간격과 세로 간격이 같은 거리라면, 픽셀을 둘러싸는 뷰포트는 렌더링된 이미지와 같은 종횡비를 갖게 됩니다. 인접한 두 픽셀 사이의 거리를 픽셀 간격이라고 하며, 정사각형 픽셀이 표준입니다. 우선 뷰포트 높이를 임의로 2.0으로 정하고, 원하는 종횡비가 되도록 뷰포트 너비를 조정하겠습니다. 이 코드는 대략 다음과 같은 모습입니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto aspect_ratio = 16.0 / 9.0; int image_width = 400; // Calculate the image height, and ensure that it's at least 1. int image_height = int(image_width / aspect_ratio); image_height = (image_height < 1) ? 1 : image_height; // Viewport widths less than one are ok since they are real valued. auto viewport_height = 2.0; auto viewport_width = viewport_height * (double(image_width)/image_height); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [image-setup]: 렌더링된 이미지 설정] `viewport_width`를 계산할 때 왜 그냥 `aspect_ratio`를 사용하지 않는지 궁금하시다면, 그 이유는 `aspect_ratio`에 설정된 값은 이상적인 비율일 뿐, `image_width`와 `image_height` 사이의 _실제_ 비율이 아닐 수 있기 때문입니다. `image_height`가 정수로만 제한되지 않고 실수 값을 가질 수 있다면 `aspect_ratio`를 사용해도 괜찮을 것입니다. 하지만 `image_width`와 `image_height` 사이의 _실제_ 비율은 코드의 두 부분에 의해 달라질 수 있습니다. 첫째, `image_height`는 가장 가까운 정수로 내림되므로 비율이 커질 수 있습니다. 둘째, `image_height`가 1보다 작아지지 않도록 하므로 이 역시 실제 종횡비를 바꿀 수 있습니다. `aspect_ratio`는 이상적인 비율이며, 우리는 이를 이미지 너비를 이미지 높이로 나눈 정수 기반 비율로 최대한 근사한다는 점에 유의하십시오. 뷰포트의 비율이 이미지의 비율과 정확히 일치하도록, 계산된 이미지 종횡비를 사용하여 최종 뷰포트 너비를 결정합니다. 다음으로 카메라 중심을 정의하겠습니다. 카메라 중심은 모든 장면 광선이 출발하는 3D 공간상의 한 점입니다(흔히 _시점(eye point)_ 이라고도 합니다). 카메라 중심에서 뷰포트 중심으로 향하는 벡터는 뷰포트와 직교합니다. 처음에는 뷰포트와 카메라 중심점 사이의 거리를 1 단위로 설정하겠습니다. 이 거리를 흔히 _초점 거리(focal length)_ 라고 합니다. 간단하게 하기 위해 카메라 중심을 $(0,0,0)$에 두고 시작하겠습니다. 또한 y축은 위쪽, x축은 오른쪽을 향하게 하고, 음의 z축이 바라보는 방향을 가리키도록 하겠습니다. (이를 흔히 _오른손 좌표계_ 라고 합니다.) ![Figure [camera-geom]: 카메라 기하 구조](../images/fig-1.03-cam-geom.jpg) 이제 피할 수 없는 까다로운 부분입니다. 우리의 3D 공간은 위와 같은 규칙을 따르지만, 이는 이미지 좌표와 충돌합니다. 이미지 좌표에서는 0번째 픽셀이 왼쪽 위에 있고, 아래로 내려가며 오른쪽 아래의 마지막 픽셀에 이르기를 원하기 때문입니다. 즉, 이미지 좌표의 Y축은 반전되어 있습니다. Y는 이미지 아래쪽으로 갈수록 증가합니다. 이미지를 스캔할 때는 왼쪽 위 픽셀(픽셀 $0,0$)에서 시작하여 각 행을 왼쪽에서 오른쪽으로 스캔하고, 위에서 아래로 한 행씩 스캔합니다. 픽셀 격자를 쉽게 탐색하기 위해 왼쪽 가장자리에서 오른쪽 가장자리로 향하는 벡터($\mathbf{V_u}$)와 위쪽 가장자리에서 아래쪽 가장자리로 향하는 벡터($\mathbf{V_v}$)를 사용하겠습니다. 픽셀 격자는 뷰포트 가장자리에서 픽셀 간 거리의 절반만큼 안쪽으로 들어가 있습니다. 이렇게 하면 뷰포트 영역이 너비 × 높이 개의 동일한 영역으로 균등하게 나뉩니다. 뷰포트와 픽셀 격자는 다음과 같습니다: ![Figure [pixel-grid]: 뷰포트와 픽셀 격자](../images/fig-1.04-pixel-grid.jpg) 이 그림에는 뷰포트, 7×5 해상도 이미지의 픽셀 격자, 뷰포트의 왼쪽 위 모서리 $\mathbf{Q}$, 픽셀 $\mathbf{P_{0,0}}$의 위치, 뷰포트 벡터 $\mathbf{V_u}$(`viewport_u`), 뷰포트 벡터 $\mathbf{V_v}$(`viewport_v`), 그리고 픽셀 델타 벡터 $\mathbf{\Delta u}$와 $\mathbf{\Delta v}$가 나타나 있습니다.
이 모든 내용을 바탕으로 카메라를 구현하는 코드는 다음과 같습니다. 주어진 장면 광선의 색상을 반환하는 함수 `ray_color(const ray& r)`를 임시로 만들어 두겠습니다. 지금은 이 함수가 항상 검은색을 반환하도록 설정합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "color.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "ray.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "vec3.h" #include ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight color ray_color(const ray& r) { return color(0,0,0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { // Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto aspect_ratio = 16.0 / 9.0; int image_width = 400; // Calculate the image height, and ensure that it's at least 1. int image_height = int(image_width / aspect_ratio); image_height = (image_height < 1) ? 1 : image_height; // Camera auto focal_length = 1.0; auto viewport_height = 2.0; auto viewport_width = viewport_height * (double(image_width)/image_height); auto camera_center = point3(0, 0, 0); // Calculate the vectors across the horizontal and down the vertical viewport edges. auto viewport_u = vec3(viewport_width, 0, 0); auto viewport_v = vec3(0, -viewport_height, 0); // Calculate the horizontal and vertical delta vectors from pixel to pixel. auto pixel_delta_u = viewport_u / image_width; auto pixel_delta_v = viewport_v / image_height; // Calculate the location of the upper left pixel. auto viewport_upper_left = camera_center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2; auto pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Render std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; for (int j = 0; j < image_height; j++) { std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; for (int i = 0; i < image_width; i++) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto pixel_center = pixel00_loc + (i * pixel_delta_u) + (j * pixel_delta_v); auto ray_direction = pixel_center - camera_center; ray r(camera_center, ray_direction); color pixel_color = ray_color(r); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ write_color(std::cout, pixel_color); } } std::clog << "\rDone. \n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [creating-rays]: [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는 항상 다음과 같은 형태입니다. $$ \mathit{blendedValue} = (1-a)\cdot\mathit{startValue} + a\cdot\mathit{endValue}, $$ 여기서 $a$는 0에서 1까지 변합니다.
이 모든 것을 종합하면 다음과 같습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "color.h" #include "ray.h" #include "vec3.h" #include color ray_color(const ray& r) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-blue-white-blend]: [main.cc] 파란색에서 흰색으로 변하는 그라데이션 렌더링하기]
이 경우 다음과 같은 결과가 나옵니다. ![Image 2: 광선의 Y 좌표에 따른 파란색-흰색 그라데이션 ](../images/img-1.02-blue-to-white.png class='pixel')
구 추가하기 ==================================================================================================== 레이 트레이서에 객체 하나를 추가해 보겠습니다. 광선이 구에 맞는지 계산하는 것이 비교적 간단하기 때문에 레이 트레이서에서는 구를 자주 사용합니다. 광선-구 교차 ------------------------ 원점을 중심으로 하는 반지름 $r$인 구의 방정식은 중요한 수학 방정식입니다. $$ x^2 + y^2 + z^2 = r^2 $$ 이것은 주어진 점 $(x,y,z)$가 구의 표면 위에 있으면 $x^2 + y^2 + z^2 = r^2$이라는 뜻으로 생각할 수도 있습니다. 주어진 점 $(x,y,z)$가 구의 _내부_ 에 있으면 $x^2 + y^2 + z^2 < r^2$이고, 주어진 점 $(x,y,z)$가 구의 _외부_ 에 있으면 $x^2 + y^2 + z^2 > r^2$입니다. 구의 중심이 임의의 점 $(C_x, C_y, C_z)$에 있을 수 있도록 하려면 방정식이 훨씬 덜 깔끔해집니다. $$ (C_x - x)^2 + (C_y - y)^2 + (C_z - z)^2 = r^2 $$ 그래픽스에서는 거의 항상 공식을 벡터로 표현하여 모든 $x$/$y$/$z$ 관련 내용을 `vec3` 클래스로 간단히 나타낼 수 있기를 원합니다. 점 $\mathbf{P} = (x,y,z)$에서 중심 $\mathbf{C} = (C_x, C_y, C_z)$로 향하는 벡터가 $(\mathbf{C} - \mathbf{P})$라는 것을 알 수 있습니다. 내적의 정의를 사용하면 다음과 같습니다. $$ (\mathbf{C} - \mathbf{P}) \cdot (\mathbf{C} - \mathbf{P}) = (C_x - x)^2 + (C_y - y)^2 + (C_z - z)^2 $$ 그러면 구의 방정식을 벡터 형태로 다음과 같이 다시 쓸 수 있습니다. $$ (\mathbf{C} - \mathbf{P}) \cdot (\mathbf{C} - \mathbf{P}) = r^2 $$ 이는 "이 방정식을 만족하는 모든 점 $\mathbf{P}$는 구 위에 있다"라고 읽을 수 있습니다. 우리는 광선 $\mathbf{P}(t) = \mathbf{Q} + t\mathbf{d}$가 구의 어느 곳에든 부딪히는지 알고 싶습니다. 광선이 구에 부딪힌다면, $\mathbf{P}(t)$가 구의 방정식을 만족하게 하는 어떤 $t$가 존재합니다. 따라서 우리는 다음이 참이 되는 $t$를 찾고 있습니다: $$ (\mathbf{C} - \mathbf{P}(t)) \cdot (\mathbf{C} - \mathbf{P}(t)) = r^2 $$ 이는 $\mathbf{P}(t)$를 전개된 형태로 바꾸어 구할 수 있습니다: $$ (\mathbf{C} - (\mathbf{Q} + t \mathbf{d})) \cdot (\mathbf{C} - (\mathbf{Q} + t \mathbf{d})) = r^2 $$ 왼쪽의 세 벡터가 오른쪽의 세 벡터와 내적되어 있습니다. 내적을 전부 풀어 쓰면 아홉 개의 벡터가 나옵니다. 모든 항을 하나하나 풀어 쓸 수도 있지만, 그렇게까지 애쓸 필요는 없습니다. 기억하시겠지만 우리는 $t$에 대해 풀고자 하므로, $t$가 있는지 없는지에 따라 항을 분리하겠습니다: $$ (-t \mathbf{d} + (\mathbf{C} - \mathbf{Q})) \cdot (-t \mathbf{d} + (\mathbf{C} - \mathbf{Q})) = r^2 $$ 이제 벡터 대수의 규칙에 따라 내적을 분배합니다: $$ t^2 \mathbf{d} \cdot \mathbf{d} - 2t \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) + (\mathbf{C} - \mathbf{Q}) \cdot (\mathbf{C} - \mathbf{Q}) = r^2 $$ 반지름의 제곱을 좌변으로 옮깁니다: $$ t^2 \mathbf{d} \cdot \mathbf{d} - 2t \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) + (\mathbf{C} - \mathbf{Q}) \cdot (\mathbf{C} - \mathbf{Q}) - r^2 = 0 $$ 이 방정식이 정확히 무엇인지 알아보기는 어렵지만, 방정식에 있는 벡터들과 $r$은 모두 상수이며 알려진 값입니다. 게다가 우리가 가진 벡터들은 모두 내적에 의해 스칼라로 축약됩니다. 유일한 미지수는 $t$이며, $t^2$ 항이 있으므로 이 방정식은 이차방정식입니다. 이차방정식 $ax^2 + bx + c = 0$은 근의 공식을 사용하여 풀 수 있습니다: $$ \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$ 따라서 광선-구 교차 방정식을 $t$에 대해 풀면 $a$, $b$, $c$의 값은 다음과 같습니다: $$ a = \mathbf{d} \cdot \mathbf{d} $$ $$ b = -2 \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) $$ $$ c = (\mathbf{C} - \mathbf{Q}) \cdot (\mathbf{C} - \mathbf{Q}) - r^2 $$ 위의 모든 것을 사용하면 $t$에 대해 풀 수 있지만, 제곱근 부분은 양수(두 개의 실근을 의미), 음수(실근이 없음을 의미), 또는 0(하나의 실근을 의미)일 수 있습니다. 그래픽스에서 대수는 거의 항상 기하와 매우 직접적으로 연관됩니다. 우리가 얻는 결과는 다음과 같습니다: ![Figure [ray-sphere]: 광선-구 교차 결과](../images/fig-1.05-ray-sphere.jpg) 첫 번째 레이 트레이싱 이미지 만들기 ----------------------------------- 이 수식을 프로그램에 하드코딩하면, z축의 -1 위치에 작은 구를 놓고 그 구와 교차하는 모든 픽셀을 빨간색으로 칠하여 코드를 테스트할 수 있습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight bool hit_sphere(const point3& center, double radius, const ray& r) { vec3 oc = center - r.origin(); auto a = dot(r.direction(), r.direction()); auto b = -2.0 * dot(r.direction(), oc); auto c = dot(oc, oc) - radius*radius; auto discriminant = b*b - 4*a*c; return (discriminant >= 0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ color ray_color(const ray& r) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (hit_sphere(point3(0,0,-1), 0.5, r)) return color(1, 0, 0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-red-sphere]: [main.cc] 빨간 구 렌더링하기]
결과는 다음과 같습니다: ![Image 3: 간단한 빨간 구 ](../images/img-1.03-red-sphere.png class='pixel')
아직 셰이딩, 반사 광선, 여러 개의 물체 등 온갖 것이 빠져 있지만, 이제 시작점보다는 절반 지점에 더 가까워졌습니다! 한 가지 주의할 점은 이차방정식을 풀어 해가 존재하는지 확인하는 방식으로 광선이 구와 교차하는지 테스트하고 있는데, $t$가 음수인 해도 문제없이 통과한다는 것입니다. 구의 중심을 $z = +1$로 바꾸어도 정확히 같은 그림을 얻게 되는데, 이 해법이 _카메라 앞에 있는_ 물체와 _카메라 뒤에 있는_ 물체를 구별하지 않기 때문입니다. 이것은 의도된 기능이 아닙니다! 이 문제들은 다음에 고치겠습니다. 표면 법선과 여러 개의 물체 ==================================================================================================== 표면 법선을 이용한 셰이딩 ----------------------------- 먼저, 셰이딩을 할 수 있도록 표면 법선을 구해 보겠습니다. 표면 법선은 교차점에서 표면에 수직인 벡터입니다. 코드에서 법선 벡터와 관련하여 중요한 설계 결정을 내려야 합니다. 법선 벡터가 임의의 길이를 가지도록 할 것인지, 아니면 단위 길이로 정규화할 것인지입니다. 필요하지 않을 경우를 대비해, 벡터 정규화에 수반되는 비용이 큰 제곱근 연산을 생략하고 싶은 유혹이 들 수 있습니다. 그러나 실제로는 세 가지 중요한 점이 있습니다. 첫째, 단위 길이 법선 벡터가 _한 번이라도_ 필요하다면, 단위 길이가 필요한 모든 위치에서 "만일을 대비해" 거듭 정규화하는 대신 처음에 한 번 해 두는 편이 낫습니다. 둘째, 우리는 여러 곳에서 단위 길이 법선 벡터를 _실제로_ 필요로 합니다. 셋째, 법선 벡터가 단위 길이여야 한다면, 특정 기하 클래스에 대한 이해를 바탕으로 그 생성자나 `hit()` 함수 안에서 해당 벡터를 효율적으로 생성할 수 있는 경우가 많습니다. 예를 들어, 구의 법선은 구의 반지름으로 나누기만 하면 단위 길이로 만들 수 있으므로 제곱근 연산을 완전히 피할 수 있습니다. 이 모든 점을 고려하여, 모든 법선 벡터는 단위 길이를 가진다는 방침을 채택하겠습니다. 구의 경우, 바깥쪽 법선은 교차점에서 중심을 뺀 방향입니다: ![Figure [sphere-normal]: 구의 표면 법선 기하 구조](../images/fig-1.06-sphere-normal.jpg) 지구에 비유하면, 이는 지구 중심에서 여러분을 향하는 벡터가 곧장 위를 가리킨다는 뜻입니다. 이제 이것을 코드에 넣고 셰이딩해 보겠습니다. 아직 조명 같은 것이 없으므로, 컬러 맵으로 법선을 시각화해 보겠습니다. 법선을 시각화할 때 흔히 쓰는 요령은($\mathbf{n}$이 단위 길이 벡터라고 가정하는 것이 쉽고 어느 정도 직관적이기 때문입니다 -- 따라서 각 성분은 -1과 1 사이에 있습니다) 각 성분을 0에서 1 사이의 구간으로 매핑한 다음, $(x, y, z)$를 $(\mathit{red}, \mathit{green}, \mathit{blue})$로 매핑하는 것입니다. 법선을 구하려면 교차 여부만이 아니라(현재 계산하고 있는 것은 이것뿐입니다) 교차점이 필요합니다. 장면에는 구가 하나뿐이고 카메라 바로 앞에 있으므로, $t$의 음수 값에 대해서는 아직 걱정하지 않겠습니다. 가장 가까운 교차점(가장 작은 $t$)이 우리가 원하는 교차점이라고 가정하겠습니다. 코드를 다음과 같이 변경하면 $\mathbf{n}$을 계산하고 시각화할 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double hit_sphere(const point3& center, double radius, const ray& r) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ vec3 oc = center - r.origin(); auto a = dot(r.direction(), r.direction()); auto b = -2.0 * dot(r.direction(), oc); auto c = dot(oc, oc) - radius*radius; auto discriminant = b*b - 4*a*c; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (discriminant < 0) { return -1.0; } else { return (-b - std::sqrt(discriminant) ) / (2.0*a); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } color ray_color(const ray& r) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto t = hit_sphere(point3(0,0,-1), 0.5, r); if (t > 0.0) { vec3 N = unit_vector(r.at(t) - vec3(0,0,-1)); return 0.5*color(N.x()+1, N.y()+1, N.z()+1); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [render-surface-normal]: [main.cc] 구의 표면 법선 렌더링하기]
그러면 다음과 같은 그림이 나옵니다: ![Image 4: 법선 벡터에 따라 색이 입혀진 구 ](../images/img-1.04-normals-sphere.png class='pixel')
광선-구 교차 코드 단순화하기 --------------------------------------------- 레이-구 함수를 다시 살펴보겠습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ double hit_sphere(const point3& center, double radius, const ray& r) { vec3 oc = center - r.origin(); auto a = dot(r.direction(), r.direction()); auto b = -2.0 * dot(r.direction(), oc); auto c = dot(oc, oc) - radius*radius; auto discriminant = b*b - 4*a*c; if (discriminant < 0) { return -1.0; } else { return (-b - std::sqrt(discriminant) ) / (2.0*a); } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-sphere-before]: [main.cc] 광선-구 교차 코드 (이전)] 먼저, 벡터를 자기 자신과 내적하면 그 벡터 길이의 제곱과 같다는 점을 떠올리십시오. 둘째, `b` 에 대한 방정식에 -2라는 인수가 포함되어 있다는 점에 주목하십시오. $b = -2h$일 때 이차 방정식이 어떻게 되는지 생각해 보십시오: $$ \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$ $$ = \frac{-(-2h) \pm \sqrt{(-2h)^2 - 4ac}}{2a} $$ $$ = \frac{2h \pm 2\sqrt{h^2 - ac}}{2a} $$ $$ = \frac{h \pm \sqrt{h^2 - ac}}{a} $$ 식이 깔끔하게 단순화되므로 이를 사용하겠습니다. 따라서 $h$에 대해 풀면 다음과 같습니다: $$ b = -2 \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) $$ $$ b = -2h $$ $$ h = \frac{b}{-2} = \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) $$
이러한 점들을 이용하면 이제 구 교차 코드를 다음과 같이 단순화할 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ double hit_sphere(const point3& center, double radius, const ray& r) { vec3 oc = center - r.origin(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto a = r.direction().length_squared(); auto h = dot(r.direction(), oc); auto c = oc.length_squared() - radius*radius; auto discriminant = h*h - a*c; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ if (discriminant < 0) { return -1.0; } else { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight return (h - std::sqrt(discriminant)) / a; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-sphere-after]: [main.cc] 광선-구 교차 코드(이후)]
hittable 객체를 위한 추상화 ------------------------------------ 그렇다면 구가 둘 이상이면 어떨까요? 구의 배열을 두고 싶은 유혹이 들기도 하지만, 매우 깔끔한 해결책은 광선이 맞을 수 있는 모든 것에 대해 “추상 클래스”를 만들고, 구와 구의 리스트를 모두 단지 맞을 수 있는 무언가로 만드는 것입니다. 이 클래스의 이름을 무엇으로 할지는 다소 고민거리입니다. "객체 지향" 프로그래밍만 아니었다면 "객체(object)"라고 부르는 것이 좋았을 것입니다. "표면(surface)"도 자주 쓰이지만, 나중에 볼륨(안개, 구름 같은 것)을 다루고 싶을 수도 있다는 약점이 있습니다. "hittable"은 이들을 하나로 묶는 멤버 함수를 강조합니다. 어느 것도 썩 마음에 들지는 않지만, “hittable”로 하겠습니다. 이 `hittable` 추상 클래스는 광선을 인자로 받는 `hit` 함수를 가집니다. 대부분의 레이 트레이서는 충돌에 대해 $t_{\mathit{min}}$부터 $t_{\mathit{max}}$까지의 유효 구간을 추가하는 것이 편리하다는 것을 알게 되었으며, 이렇게 하면 $t_{\mathit{min}} < t < t_{\mathit{max}}$인 경우에만 충돌이 “인정”됩니다. 초기 광선의 경우 이는 양수 $t$이지만, 앞으로 보게 되듯이 $t_{\mathit{min}}$부터 $t_{\mathit{max}}$까지의 구간을 두면 코드를 단순하게 만들 수 있습니다. 설계상의 한 가지 질문은 무언가에 충돌했을 때 법선 계산 같은 일을 할 것인지 여부입니다. 탐색하는 도중에 더 가까운 무언가에 충돌하게 될 수도 있으며, 우리에게는 가장 가까운 것의 법선만 필요합니다. 저는 간단한 해결책을 택하여, 어떤 구조체에 저장할 여러 값을 한꺼번에 계산하겠습니다. 추상 클래스는 다음과 같습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef HITTABLE_H #define HITTABLE_H #include "ray.h" class hit_record { public: point3 p; vec3 normal; double t; }; class hittable { public: virtual ~hittable() = default; virtual bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const = 0; }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hittable-initial]: [hittable.h] hittable 클래스]
그리고 구는 다음과 같습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef SPHERE_H #define SPHERE_H #include "hittable.h" #include "vec3.h" class sphere : public hittable { public: sphere(const point3& center, double radius) : center(center), radius(std::fmax(0,radius)) {} bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override { vec3 oc = center - r.origin(); auto a = r.direction().length_squared(); auto h = dot(r.direction(), oc); auto c = oc.length_squared() - radius*radius; auto discriminant = h*h - a*c; if (discriminant < 0) return false; auto sqrtd = std::sqrt(discriminant); // Find the nearest root that lies in the acceptable range. auto root = (h - sqrtd) / a; if (root <= ray_tmin || ray_tmax <= root) { root = (h + sqrtd) / a; if (root <= ray_tmin || ray_tmax <= root) return false; } rec.t = root; rec.p = r.at(rec.t); rec.normal = (rec.p - center) / radius; return true; } private: point3 center; double radius; }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [sphere-initial]: [sphere.h] 구 클래스] (여기서는 두 부동소수점 인수 중 최댓값을 반환하는 C++ 표준 함수 `std::fmax()` 를 사용한다는 점에 유의하십시오. 마찬가지로, 나중에는 두 부동소수점 인수 중 최솟값을 반환하는 `std::fmin()` 을 사용할 것입니다.)
앞면과 뒷면 ------------------------------ 법선에 관한 두 번째 설계 결정은 법선이 항상 바깥쪽을 향해야 하는지 여부입니다. 현재 구한 법선은 항상 중심에서 교차점으로 향하는 방향입니다(법선이 바깥쪽을 향합니다). 광선이 구의 바깥에서 구와 교차하면, 법선은 광선과 반대 방향을 향합니다. 광선이 구의 안쪽에서 구와 교차하면, (항상 바깥쪽을 향하는) 법선은 광선과 같은 방향을 향합니다. 또는, 법선이 항상 광선과 반대 방향을 향하도록 할 수도 있습니다. 광선이 구의 바깥에 있으면 법선은 바깥쪽을 향하지만, 광선이 구의 안에 있으면 법선은 안쪽을 향하게 됩니다. ![Figure [normal-sides]: 구의 표면 법선 기하에서 가능한 방향 ](../images/fig-1.07-normal-sides.jpg) 결국 광선이 표면의 어느 쪽에서 오는지 판단해야 하므로, 이 가능성 중 하나를 선택해야 합니다. 이는 양면 종이에 쓰인 글자처럼 면마다 다르게 렌더링되는 객체나, 유리 공처럼 안쪽과 바깥쪽이 있는 객체에 중요합니다. 법선이 항상 바깥쪽을 향하도록 결정한다면, 광선에 색을 칠할 때 광선이 어느 쪽에 있는지 판단해야 합니다. 이는 광선과 법선을 비교하여 알아낼 수 있습니다. 광선과 법선이 같은 방향을 향하면 광선은 객체의 안쪽에 있는 것이고, 광선과 법선이 반대 방향을 향하면 광선은 객체의 바깥쪽에 있는 것입니다. 이는 두 벡터의 내적을 구하여 판단할 수 있으며, 내적이 양수이면 광선은 구의 안쪽에 있는 것입니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ if (dot(ray_direction, outward_normal) > 0.0) { // ray is inside the sphere ... } else { // ray is outside the sphere ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-normal-comparison]: 광선과 법선 비교하기]
법선이 항상 광선의 반대 방향을 가리키도록 정하면, 광선이 표면의 어느 쪽에 있는지 판단하는 데 내적을 사용할 수 없게 됩니다. 대신 그 정보를 저장해야 합니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ bool front_face; if (dot(ray_direction, outward_normal) > 0.0) { // ray is inside the sphere normal = -outward_normal; front_face = false; } else { // ray is outside the sphere normal = outward_normal; front_face = true; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [normals-point-against]: 표면의 어느 쪽인지 기억하기]
법선이 항상 표면에서 “바깥쪽”을 가리키도록 설정할 수도 있고, 항상 입사 광선의 반대 방향을 가리키도록 설정할 수도 있습니다. 이 결정은 표면의 어느 쪽인지를 기하 교차 시점에 판단할지, 아니면 색상 계산 시점에 판단할지에 따라 달라집니다. 이 책에서는 기하 유형보다 재질 유형이 더 많으므로, 작업량을 줄이기 위해 기하 교차 시점에 판단하도록 하겠습니다. 이는 단순히 선호의 문제이며, 문헌에서는 두 가지 구현을 모두 볼 수 있습니다. `hit_record` 클래스에 `front_face` bool을 추가합니다. 또한 이 계산을 대신 처리해 줄 함수 `set_face_normal()`도 추가하겠습니다. 편의상 새로운 `set_face_normal()` 함수에 전달되는 벡터는 단위 길이라고 가정하겠습니다. 매개변수를 언제든 명시적으로 정규화할 수도 있지만, 기하 코드에서 이를 처리하는 것이 더 효율적입니다. 보통 특정 기하에 대해 더 많이 알고 있을 때 더 쉽기 때문입니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class hit_record { public: point3 p; vec3 normal; double t; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight bool front_face; void set_face_normal(const ray& r, const vec3& outward_normal) { // Sets the hit record normal vector. // NOTE: the parameter `outward_normal` is assumed to have unit length. front_face = dot(r.direction(), outward_normal) < 0; normal = front_face ? outward_normal : -outward_normal; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [front-face-tracking]: [hittable.h] hit_record에 앞면 추적 기능 추가하기]
그런 다음 클래스에 표면의 어느 쪽인지 판단하는 기능을 추가합니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class sphere : public hittable { public: ... bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const { ... rec.t = root; rec.p = r.at(rec.t); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 outward_normal = (rec.p - center) / radius; rec.set_face_normal(r, outward_normal); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return true; } ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [sphere-final]: [sphere.h] 법선을 결정하는 구 클래스]
hittable 객체의 리스트 --------------------------- 광선이 교차할 수 있는 `hittable`이라는 범용 객체가 있습니다. 이제 `hittable`의 목록을 저장하는 클래스를 추가합니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef HITTABLE_LIST_H #define HITTABLE_LIST_H #include "hittable.h" #include #include using std::make_shared; using std::shared_ptr; class hittable_list : public hittable { public: std::vector> objects; hittable_list() {} hittable_list(shared_ptr object) { add(object); } void clear() { objects.clear(); } void add(shared_ptr object) { objects.push_back(object); } bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override { hit_record temp_rec; bool hit_anything = false; auto closest_so_far = ray_tmax; for (const auto& object : objects) { if (object->hit(r, ray_tmin, closest_so_far, temp_rec)) { hit_anything = true; closest_so_far = temp_rec.t; rec = temp_rec; } } return hit_anything; } }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hittable-list-initial]: [hittable_list.h] hittable_list 클래스] 몇 가지 새로운 C++ 기능 ---------------------- `hittable_list` 클래스 코드는 평소 C++ 프로그래머가 아니라면 헷갈릴 수 있는 몇 가지 C++ 기능인 `vector`, `shared_ptr`, `make_shared`를 사용합니다. `shared_ptr`은 참조 카운팅 의미론을 갖는, 할당된 어떤 타입에 대한 포인터입니다. 그 값을 다른 공유 포인터에 대입할 때마다(보통 단순 대입으로) 참조 카운트가 증가합니다. 공유 포인터가 스코프를 벗어나면(블록이나 함수의 끝처럼) 참조 카운트가 감소합니다. 카운트가 0이 되면 객체는 안전하게 삭제됩니다.
일반적으로 공유 포인터는 다음과 같이 새로 할당된 객체로 먼저 초기화됩니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ shared_ptr double_ptr = make_shared(0.37); shared_ptr vec3_ptr = make_shared(1.414214, 2.718281, 1.618034); shared_ptr sphere_ptr = make_shared(point3(0,0,0), 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [shared-ptr]: shared_ptr을 사용한 할당 예시]
`make_shared(thing_constructor_params ...)`는 생성자 매개변수를 사용하여 `thing` 타입의 새 인스턴스를 할당합니다. 이 함수는 `shared_ptr`을 반환합니다.
타입은 `make_shared(...)`의 반환 타입으로부터 자동으로 추론될 수 있으므로, 위의 코드는 C++의 `auto` 타입 지정자를 사용하여 더 간단하게 표현할 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto double_ptr = make_shared(0.37); auto vec3_ptr = make_shared(1.414214, 2.718281, 1.618034); auto sphere_ptr = make_shared(point3(0,0,0), 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [shared-ptr-auto]: auto 타입과 shared_ptr을 사용한 할당 예시]
코드에서는 공유 포인터를 사용할 것입니다. 여러 기하 객체가 공통 인스턴스를 공유할 수 있게 해 주고(예를 들어 모두 같은 색상 재질을 사용하는 여러 구), 메모리 관리를 자동화하여 더 쉽게 추론할 수 있게 해 주기 때문입니다. `std::shared_ptr`는 `` 헤더에 포함되어 있습니다. 여러분에게 익숙하지 않을 수 있는 두 번째 C++ 기능은 `std::vector`입니다. 이것은 임의의 타입을 담는 제네릭 배열 형태의 컬렉션입니다. 위에서는 `hittable`에 대한 포인터의 컬렉션을 사용합니다. `std::vector`는 값이 추가될수록 자동으로 커집니다. `objects.push_back(object)`는 `std::vector` 멤버 변수 `objects`의 끝에 값을 추가합니다. `std::vector`는 `` 헤더에 포함되어 있습니다. 마지막으로, listing [hittable-list-initial]의 `using` 문은 `shared_ptr`와 `make_shared`를 `std` 라이브러리에서 가져온다는 것을 컴파일러에게 알려 주므로, 이들을 참조할 때마다 `std::` 접두사를 붙일 필요가 없습니다. 공통 상수와 유틸리티 함수 --------------------------------------- 몇 가지 수학 상수가 필요한데, 편의상 이를 별도의 헤더 파일에 정의합니다. 지금은 무한대만 필요하지만, 나중에 필요할 pi의 정의도 함께 넣어 두겠습니다. 또한 자주 쓰이는 유용한 상수와 앞으로 사용할 유틸리티 함수도 이곳에 넣겠습니다. 이 새 헤더 `rtweekend.h`는 우리의 일반적인 메인 헤더 파일이 될 것입니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef RTWEEKEND_H #define RTWEEKEND_H #include #include #include #include // C++ Std Usings using std::make_shared; using std::shared_ptr; // Constants const double infinity = std::numeric_limits::infinity(); const double pi = 3.1415926535897932385; // Utility Functions inline double degrees_to_radians(double degrees) { return degrees * pi / 180.0; } // Common Headers #include "color.h" #include "ray.h" #include "vec3.h" #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [rtweekend-initial]: [rtweekend.h] rtweekend.h 공통 헤더] 프로그램 파일은 `rtweekend.h`를 가장 먼저 포함하므로, 다른 모든 헤더 파일(코드의 대부분이 위치할 곳)은 `rtweekend.h`가 이미 포함되었다고 암묵적으로 가정할 수 있습니다. 그래도 헤더 파일은 그 밖에 필요한 헤더 파일을 명시적으로 포함해야 합니다. 이러한 가정을 염두에 두고 몇 가지를 수정하겠습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete #include ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [assume-rtw-color]: [color.h] color.h에서 rtweekend.h 포함을 가정하기] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete #include "ray.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [assume-rtw-hittable]: [hittable.h] hittable.h에서 rtweekend.h 포함을 가정하기] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete #include ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete using std::make_shared; using std::shared_ptr; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [assume-rtw-hittable-list]: [hittable_list.h] hittable_list.h에서 rtweekend.h 포함을 가정하기] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete #include "vec3.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [assume-rtw-sphere]: [sphere.h] sphere.h에서 rtweekend.h 포함을 가정하기] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete #include #include ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [assume-rtw-vec3]: [vec3.h] vec3.h에서 rtweekend.h 포함을 가정하기]
이제 새로운 main은 다음과 같습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "rtweekend.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete #include "color.h" #include "ray.h" #include "vec3.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "hittable.h" #include "hittable_list.h" #include "sphere.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete #include ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete double hit_sphere(const point3& center, double radius, const ray& r) { ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight color ray_color(const ray& r, const hittable& world) { hit_record rec; if (world.hit(r, 0, infinity, rec)) { return 0.5 * (rec.normal + color(1,1,1)); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } int main() { // Image auto aspect_ratio = 16.0 / 9.0; int image_width = 400; // Calculate the image height, and ensure that it's at least 1. int image_height = int(image_width / aspect_ratio); image_height = (image_height < 1) ? 1 : image_height; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight // World hittable_list world; world.add(make_shared(point3(0,0,-1), 0.5)); world.add(make_shared(point3(0,-100.5,-1), 100)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Camera auto focal_length = 1.0; auto viewport_height = 2.0; auto viewport_width = viewport_height * (double(image_width)/image_height); auto camera_center = point3(0, 0, 0); // Calculate the vectors across the horizontal and down the vertical viewport edges. auto viewport_u = vec3(viewport_width, 0, 0); auto viewport_v = vec3(0, -viewport_height, 0); // Calculate the horizontal and vertical delta vectors from pixel to pixel. auto pixel_delta_u = viewport_u / image_width; auto pixel_delta_v = viewport_v / image_height; // Calculate the location of the upper left pixel. auto viewport_upper_left = camera_center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2; auto pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); // Render std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; for (int j = 0; j < image_height; j++) { std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; for (int i = 0; i < image_width; i++) { auto pixel_center = pixel00_loc + (i * pixel_delta_u) + (j * pixel_delta_v); auto ray_direction = pixel_center - camera_center; ray r(camera_center, ray_direction); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight color pixel_color = ray_color(r, world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ write_color(std::cout, pixel_color); } } std::clog << "\rDone. \n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-with-rtweekend-h]: [main.cc] hittable을 사용하는 새로운 main]
이렇게 하면 구들이 어디에 있는지를 표면 법선과 함께 시각화한 그림이 만들어집니다. 이는 기하 모델의 결함이나 특정한 특성을 살펴보는 데 매우 유용한 방법인 경우가 많습니다. ![Image 5: 지면과 함께 법선으로 색칠된 구의 렌더링 결과 ](../images/img-1.05-normals-sphere-ground.png class='pixel') 구간 클래스 ------------------ 계속하기 전에, 최솟값과 최댓값을 가진 실수 구간을 관리하는 interval 클래스를 구현하겠습니다. 앞으로 진행하면서 이 클래스를 꽤 자주 사용하게 될 것입니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef INTERVAL_H #define INTERVAL_H class interval { public: double min, max; interval() : min(+infinity), max(-infinity) {} // Default interval is empty interval(double min, double max) : min(min), max(max) {} double size() const { return max - min; } bool contains(double x) const { return min <= x && x <= max; } bool surrounds(double x) const { return min < x && x < max; } static const interval empty, universe; }; const interval interval::empty = interval(+infinity, -infinity); const interval interval::universe = interval(-infinity, +infinity); #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [interval-initial]: [interval.h] 새로운 interval 클래스 소개] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Common Headers #include "color.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "interval.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "ray.h" #include "vec3.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [interval-rtweekend]: [rtweekend.h] 새로운 interval 클래스 포함하기] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class hittable { public: ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight virtual bool hit(const ray& r, interval ray_t, hit_record& rec) const = 0; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hittable-with-interval]: [hittable.h] interval을 사용하는 hittable::hit()] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class hittable_list : public hittable { public: ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight bool hit(const ray& r, interval ray_t, hit_record& rec) const override { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ hit_record temp_rec; bool hit_anything = false; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto closest_so_far = ray_t.max; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ for (const auto& object : objects) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (object->hit(r, interval(ray_t.min, closest_so_far), temp_rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ hit_anything = true; closest_so_far = temp_rec.t; rec = temp_rec; } } return hit_anything; } ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hittable-list-with-interval]: [hittable_list.h] interval을 사용하는 hittable_list::hit()] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class sphere : public hittable { public: ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight bool hit(const ray& r, interval ray_t, hit_record& rec) const override { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... // Find the nearest root that lies in the acceptable range. auto root = (h - sqrtd) / a; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (!ray_t.surrounds(root)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ root = (h + sqrtd) / a; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (!ray_t.surrounds(root)) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return false; } ... } ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [sphere-with-interval]: [sphere.h] interval을 사용하는 구] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ color ray_color(const ray& r, const hittable& world) { hit_record rec; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (world.hit(r, interval(0, infinity), rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return 0.5 * (rec.normal + color(1,1,1)); } vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-with-interval]: [main.cc] interval을 사용하는 새로운 main] 카메라 코드를 별도의 클래스로 옮기기 ==================================================================================================== 계속 진행하기 전에, 지금이 카메라 코드와 장면 렌더링 코드를 하나의 새로운 클래스인 `camera` 클래스로 통합하기에 좋은 시점입니다. 카메라 클래스는 두 가지 중요한 일을 담당합니다. 1. 광선을 생성하여 월드로 보냅니다. 2. 이 광선들의 결과를 사용하여 렌더링된 이미지를 구성합니다. 이번 리팩터링에서는 `ray_color()` 함수와 함께 메인 프로그램의 이미지, 카메라, 렌더링 부분을 한데 모읍니다. 새로운 카메라 클래스에는 두 개의 public 메서드 `initialize()`와 `render()`, 그리고 두 개의 private 헬퍼 메서드 `get_ray()`와 `ray_color()`가 포함됩니다. 궁극적으로 카메라는 우리가 생각할 수 있는 가장 단순한 사용 패턴을 따릅니다: 인수 없이 기본 생성된 다음, 이를 소유하는 코드가 간단한 대입을 통해 카메라의 public 변수들을 수정하고, 마지막으로 `initialize()` 함수를 호출하여 모든 것을 초기화합니다. 이 패턴은 소유자가 수많은 매개변수를 가진 생성자를 호출하거나, 여러 setter 메서드를 정의하고 호출하는 방식 대신 선택된 것입니다. 이렇게 하면 소유하는 코드는 명시적으로 관심 있는 부분만 설정하면 됩니다. 마지막으로, 소유하는 코드가 `initialize()`를 호출하도록 할 수도 있고, 카메라가 `render()`를 시작할 때 이 함수를 자동으로 호출하도록 할 수도 있습니다. 우리는 두 번째 방식을 사용하겠습니다. main은 카메라를 생성하고 기본값을 설정한 후 `render()` 메서드를 호출합니다. `render()` 메서드는 렌더링을 위해 카메라를 준비한 다음 렌더 루프를 실행합니다.
새로운 `camera` 클래스의 뼈대는 다음과 같습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef CAMERA_H #define CAMERA_H #include "hittable.h" class camera { public: /* Public Camera Parameters Here */ void render(const hittable& world) { ... } private: /* Private Camera Variables Here */ void initialize() { ... } color ray_color(const ray& r, const hittable& world) const { ... } }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-skeleton]: [camera.h] 카메라 클래스의 골격]
먼저 `main.cc`에 있던 `ray_color()` 함수를 채워 넣겠습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { ... private: ... color ray_color(const ray& r, const hittable& world) const { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight hit_record rec; if (world.hit(r, interval(0, infinity), rec)) { return 0.5 * (rec.normal + color(1,1,1)); } vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-ray-color]: [camera.h] camera::ray_color 함수]
이제 `main()` 함수에 있던 거의 모든 것을 새로운 카메라 클래스로 옮깁니다. `main()` 함수에 남는 것은 월드를 구성하는 부분뿐입니다. 새로 옮겨 온 코드가 포함된 카메라 클래스는 다음과 같습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { public: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double aspect_ratio = 1.0; // Ratio of image width over height int image_width = 100; // Rendered image width in pixel count void render(const hittable& world) { initialize(); std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; for (int j = 0; j < image_height; j++) { std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; for (int i = 0; i < image_width; i++) { auto pixel_center = pixel00_loc + (i * pixel_delta_u) + (j * pixel_delta_v); auto ray_direction = pixel_center - center; ray r(center, ray_direction); color pixel_color = ray_color(r, world); write_color(std::cout, pixel_color); } } std::clog << "\rDone. \n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ private: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight int image_height; // Rendered image height point3 center; // Camera center point3 pixel00_loc; // Location of pixel 0, 0 vec3 pixel_delta_u; // Offset to pixel to the right vec3 pixel_delta_v; // Offset to pixel below void initialize() { image_height = int(image_width / aspect_ratio); image_height = (image_height < 1) ? 1 : image_height; center = point3(0, 0, 0); // Determine viewport dimensions. auto focal_length = 1.0; auto viewport_height = 2.0; auto viewport_width = viewport_height * (double(image_width)/image_height); // Calculate the vectors across the horizontal and down the vertical viewport edges. auto viewport_u = vec3(viewport_width, 0, 0); auto viewport_v = vec3(0, -viewport_height, 0); // Calculate the horizontal and vertical delta vectors from pixel to pixel. pixel_delta_u = viewport_u / image_width; pixel_delta_v = viewport_v / image_height; // Calculate the location of the upper left pixel. auto viewport_upper_left = center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2; pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ color ray_color(const ray& r, const hittable& world) const { ... } }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-working]: [camera.h] 동작하는 카메라 클래스]
그리고 훨씬 간결해진 main은 다음과 같습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "rtweekend.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "camera.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "hittable.h" #include "hittable_list.h" #include "sphere.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete color ray_color(const ray& r, const hittable& world) { ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight hittable_list world; world.add(make_shared(point3(0,0,-1), 0.5)); world.add(make_shared(point3(0,-100.5,-1), 100)); camera cam; cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 400; cam.render(world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-with-new-camera]: [main.cc] 새로운 카메라를 사용하는 새로운 main]
새로 리팩터링한 이 프로그램을 실행하면 이전과 동일한 렌더링된 이미지가 나와야 합니다. 안티앨리어싱 ==================================================================================================== 지금까지 렌더링한 이미지를 확대해 보면, 렌더링된 이미지의 가장자리가 거친 "계단" 모양을 띠고 있음을 알 수 있습니다. 이러한 계단 현상은 흔히 "에일리어싱(aliasing)" 또는 "재기(jaggies)"라고 부릅니다. 실제 카메라로 사진을 찍으면 보통 가장자리를 따라 재기가 나타나지 않는데, 가장자리 픽셀이 전경과 배경의 일부가 섞인 결과이기 때문입니다. 지금까지의 렌더링된 이미지와 달리, 세상의 실제 이미지는 연속적이라는 점을 생각해 보십시오. 달리 말하면, 세상(그리고 세상의 모든 실제 이미지)은 사실상 무한한 해상도를 가집니다. 각 픽셀에 대해 여러 샘플을 평균하면 같은 효과를 얻을 수 있습니다. 각 픽셀의 중심을 지나는 광선 하나만 사용하면, 흔히 _점 샘플링(point sampling)_ 이라고 부르는 것을 수행하게 됩니다. 점 샘플링의 문제는 멀리 있는 작은 체커보드를 렌더링하는 예로 설명할 수 있습니다. 이 체커보드가 검은색과 흰색 타일로 이루어진 8×8 격자로 구성되어 있는데 광선 네 개만 여기에 닿는다면, 네 광선 모두 흰색 타일에만 교차하거나, 검은색 타일에만 교차하거나, 또는 이상한 조합으로 교차할 수 있습니다. 실제 세계에서 멀리 있는 체커보드를 눈으로 보면, 우리는 그것을 선명한 검은색과 흰색 점이 아니라 회색으로 인식합니다. 이는 우리 눈이 우리가 레이 트레이서에게 바라는 일, 즉 렌더링된 이미지의 특정 (이산적인) 영역에 떨어지는 빛(의 연속 함수)을 적분하는 일을 자연스럽게 하고 있기 때문입니다. 픽셀 중심을 지나는 같은 광선을 여러 번 다시 샘플링하는 것만으로는 분명히 얻는 것이 없습니다 -- 매번 같은 결과가 나올 뿐입니다. 대신 픽셀 _주변_ 에 떨어지는 빛을 샘플링한 다음, 그 샘플들을 적분하여 실제 연속적인 결과를 근사하려 합니다. 그렇다면 픽셀 주변에 떨어지는 빛은 어떻게 적분할 수 있을까요? 가장 단순한 모델을 채택하겠습니다. 픽셀을 중심으로 하여 네 개의 이웃 픽셀 각각을 향해 절반 거리까지 뻗어 있는 정사각형 영역을 샘플링하는 것입니다. 이것이 최적의 접근 방식은 아니지만, 가장 직관적인 방식입니다. (이 주제를 더 깊이 알고 싶다면 [_픽셀은 작은 정사각형이 아니다_][square-pixels]를 참고하십시오.) ![Figure [pixel-samples]: 픽셀 샘플](../images/fig-1.08-pixel-samples.jpg) 몇 가지 난수 유틸리티 ----------------------------- 실수 난수를 반환하는 난수 생성기가 필요합니다. 이 함수는 관례적으로 $0 ≤ n < 1$ 범위에 속하는 표준 난수를 반환해야 합니다. 1 앞의 “미만”이라는 조건은 중요한데, 때때로 이 점을 활용할 것이기 때문입니다. 이를 위한 간단한 방법은 ``에 있는 `std::rand()` 함수를 사용하는 것으로, 이 함수는 0과 `RAND_MAX` 사이 범위의 임의의 정수를 반환합니다. 따라서 `rtweekend.h`에 다음 코드 조각을 추가하여 원하는 대로 실수 난수를 얻을 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include #include #include ... // Utility Functions inline double degrees_to_radians(double degrees) { return degrees * pi / 180.0; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight inline double random_double() { // Returns a random real in [0,1). return std::rand() / (RAND_MAX + 1.0); } inline double random_double(double min, double max) { // Returns a random real in [min,max). return min + (max-min)*random_double(); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [random-double]: [rtweekend.h] random_double() 함수] C++에는 전통적으로 표준 난수 생성기가 없었지만, 최신 버전의 C++에서는 `` 헤더로 이 문제를 해결했습니다(일부 전문가들에 따르면 불완전하게나마). 이를 사용하고 싶다면, 다음과 같이 필요한 조건을 만족하는 난수를 얻을 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight inline double random_double() { static std::uniform_real_distribution distribution(0.0, 1.0); static std::mt19937 generator; return distribution(generator); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ inline double random_double(double min, double max) { // Returns a random real in [min,max). return min + (max-min)*random_double(); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [random-double-alt]: [rtweekend.h] random_double(), 대체 구현] 여러 샘플로 픽셀 생성하기 ---------------------------------------- 여러 샘플로 구성된 단일 픽셀에 대해, 픽셀을 둘러싼 영역에서 샘플을 선택하고 그 결과로 나온 빛(색상) 값들을 평균합니다. 먼저 사용하는 샘플 수를 반영하도록 `write_color()` 함수를 수정하겠습니다. 취한 모든 샘플에 대한 평균을 구해야 합니다. 이를 위해 각 반복에서 전체 색상을 더한 다음, 색상을 출력하기 전에 마지막에 (샘플 수로) 한 번 나누는 것으로 마무리합니다. 최종 결과의 색상 성분이 적절한 $[0,1]$ 범위 안에 머물도록, 작은 헬퍼 함수 `interval::clamp(x)` 를 추가하여 사용하겠습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class interval { public: ... bool surrounds(double x) const { return min < x && x < max; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double clamp(double x) const { if (x < min) return min; if (x > max) return max; return x; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [clamp]: [interval.h] interval::clamp() 유틸리티 함수] 다음은 interval 클램핑 함수를 적용한 수정된 `write_color()` 함수입니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "interval.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "vec3.h" using color = vec3; void write_color(std::ostream& out, const color& pixel_color) { auto r = pixel_color.x(); auto g = pixel_color.y(); auto b = pixel_color.z(); // Translate the [0,1] component values to the byte range [0,255]. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight static const interval intensity(0.000, 0.999); int rbyte = int(256 * intensity.clamp(r)); int gbyte = int(256 * intensity.clamp(g)); int bbyte = int(256 * intensity.clamp(b)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Write out the pixel color components. out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n'; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [write-color-clamped]: [color.h] 다중 샘플 write_color() 함수] 이제 카메라 클래스를 수정하여 각 픽셀마다 서로 다른 샘플을 생성하는 새로운 `camera::get_ray(i,j)` 함수를 정의하고 사용하겠습니다. 이 함수는 원점을 중심으로 하는 단위 정사각형 안의 무작위 샘플 점을 생성하는 새 헬퍼 함수 `sample_square()`를 사용합니다. 그런 다음 이 이상적인 정사각형에서 얻은 무작위 샘플을 현재 샘플링 중인 특정 픽셀로 다시 변환합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { public: double aspect_ratio = 1.0; // Ratio of image width over height int image_width = 100; // Rendered image width in pixel count ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight int samples_per_pixel = 10; // Count of random samples for each pixel ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ void render(const hittable& world) { initialize(); std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; for (int j = 0; j < image_height; j++) { std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; for (int i = 0; i < image_width; i++) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight color pixel_color(0,0,0); for (int sample = 0; sample < samples_per_pixel; sample++) { ray r = get_ray(i, j); pixel_color += ray_color(r, world); } write_color(std::cout, pixel_samples_scale * pixel_color); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } } std::clog << "\rDone. \n"; } ... private: int image_height; // Rendered image height ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double pixel_samples_scale; // Color scale factor for a sum of pixel samples ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ point3 center; // Camera center point3 pixel00_loc; // Location of pixel 0, 0 vec3 pixel_delta_u; // Offset to pixel to the right vec3 pixel_delta_v; // Offset to pixel below void initialize() { image_height = int(image_width / aspect_ratio); image_height = (image_height < 1) ? 1 : image_height; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight pixel_samples_scale = 1.0 / samples_per_pixel; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ center = point3(0, 0, 0); ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight ray get_ray(int i, int j) const { // Construct a camera ray originating from the origin and directed at randomly sampled // point around the pixel location i, j. auto offset = sample_square(); auto pixel_sample = pixel00_loc + ((i + offset.x()) * pixel_delta_u) + ((j + offset.y()) * pixel_delta_v); auto ray_origin = center; auto ray_direction = pixel_sample - ray_origin; return ray(ray_origin, ray_direction); } vec3 sample_square() const { // Returns the vector to a random point in the [-.5,-.5]-[+.5,+.5] unit square. return vec3(random_double() - 0.5, random_double() - 0.5, 0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ color ray_color(const ray& r, const hittable& world) const { ... } }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-spp]: [camera.h] 픽셀당 샘플 수 매개변수를 갖춘 카메라] (위의 새로운 `sample_square()` 함수 외에도, Github 소스 코드에서 `sample_disk()` 함수도 찾을 수 있습니다. 이 함수는 정사각형이 아닌 픽셀로 실험해 보고 싶은 경우를 위해 포함되어 있지만, 이 책에서는 사용하지 않습니다. `sample_disk()`는 뒤에서 정의할 `random_in_unit_disk()` 함수에 의존합니다.)
새로운 카메라 매개변수를 설정하도록 main을 수정합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { ... camera cam; cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 400; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight cam.samples_per_pixel = 100; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-spp]: [main.cc] 새로운 픽셀당 샘플 수 매개변수 설정]
생성된 이미지를 확대해 보면 가장자리 픽셀에서 차이를 확인할 수 있습니다. ![Image 6: 안티앨리어싱 적용 전과 후 ](../images/img-1.06-antialias-before-after.png class='pixel')
확산 재질 ==================================================================================================== 이제 물체와 픽셀당 여러 개의 광선이 준비되었으므로, 사실적으로 보이는 재질을 만들 수 있습니다. 먼저 확산 재질(_무광_ 이라고도 합니다)부터 시작하겠습니다. 한 가지 고민거리는 기하 구조와 재질을 자유롭게 조합할 수 있게 할 것인지(그러면 하나의 재질을 여러 구에 할당하거나 그 반대도 가능합니다), 아니면 기하 구조와 재질을 긴밀하게 묶을 것인지(기하 구조와 재질이 연결된 절차적 물체에 유용할 수 있습니다)입니다. 대부분의 렌더러에서 일반적인 방식인 분리 방식을 택하겠지만 -- 다른 접근 방식도 있다는 점은 알아 두십시오. 간단한 확산 재질 -------------------------- 스스로 빛을 내지 않는 확산 물체는 단지 주변의 색을 띨 뿐이지만, 그 색을 자신의 고유한 색으로 변조합니다. 확산 표면에서 반사되는 빛은 방향이 무작위로 바뀌므로, 두 확산 표면 사이의 틈으로 광선 세 개를 보내면 각 광선은 서로 다른 무작위 동작을 보입니다: ![Figure [light-bounce]: 광선의 튕김](../images/fig-1.09-light-bounce.jpg) 광선은 반사되지 않고 흡수될 수도 있습니다. 표면이 어두울수록 광선이 흡수될 가능성이 높습니다(그래서 어두운 것입니다!). 사실 방향을 무작위로 만드는 알고리즘이라면 어떤 것이든 무광으로 보이는 표면을 만들어 냅니다. 가장 직관적인 것부터 시작하겠습니다: 광선을 모든 방향으로 균등하게 무작위로 튕겨 내는 표면입니다. 이 재질에서는 표면에 부딪힌 광선이 표면 바깥쪽의 어느 방향으로든 튕겨 나갈 확률이 동일합니다. ![Figure [random-vec-hor]: 수평선 위로의 균등한 반사 ](../images/fig-1.10-random-vec-horizon.jpg) 이 매우 직관적인 재질은 가장 단순한 종류의 확산이며 -- 실제로 -- 초기 레이 트레이싱 논문 중 상당수가 이 확산 방식을 사용했습니다(조금 뒤에 구현할 더 정확한 방식을 채택하기 전까지는 말입니다). 현재는 광선을 무작위로 반사할 방법이 없으므로, 벡터 유틸리티 헤더에 몇 가지 함수를 추가해야 합니다. 가장 먼저 필요한 것은 임의의 무작위 벡터를 생성하는 기능입니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class vec3 { public: ... double length_squared() const { return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight static vec3 random() { return vec3(random_double(), random_double(), random_double()); } static vec3 random(double min, double max) { return vec3(random_double(min,max), random_double(min,max), random_double(min,max)); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [vec-rand-util]: [vec3.h] vec3 무작위 유틸리티 함수] 다음으로, 반구 표면 위에 있는 결과만 얻도록 무작위 벡터를 조작하는 방법을 알아내야 합니다. 이를 위한 해석적 방법들이 있지만, 실제로는 이해하기가 놀라울 만큼 복잡하고 구현하기도 상당히 복잡합니다. 대신, 일반적으로 가장 쉬운 알고리즘인 기각법(rejection method)을 사용하겠습니다. 기각법은 원하는 기준을 만족하는 샘플이 나올 때까지 무작위 샘플을 반복해서 생성하는 방식으로 동작합니다. 다시 말해, 좋은 샘플을 찾을 때까지 나쁜 샘플을 계속 기각하는 것입니다. 기각법을 사용하여 반구 위의 무작위 벡터를 생성하는 방법에는 똑같이 타당한 방법이 많이 있지만, 여기서는 가장 단순한 다음 방법을 사용하겠습니다: 1. 단위 구 내부에서 무작위 벡터를 생성합니다 2. 이 벡터를 정규화하여 구 표면까지 늘립니다 3. 정규화된 벡터가 잘못된 반구에 놓이면 반전합니다
먼저, 기각법을 사용하여 단위 구(즉, 반지름이 1인 구) 내부의 무작위 벡터를 생성하겠습니다. 단위 구를 둘러싸는 정육면체 내부(즉, $x$, $y$, $z$가 모두 $[-1,+1]$ 범위에 있는 곳)에서 무작위 점을 하나 선택합니다. 이 점이 단위 구 바깥에 있으면, 단위 구 내부 또는 표면 위에 있는 점을 찾을 때까지 새로운 점을 생성합니다. ![Figure [sphere-vec]: 좋은 벡터를 찾기 전에 두 벡터가 기각되었습니다(정규화 전) ](../images/fig-1.11-sphere-vec.jpg) ![Figure [sphere-vec]: 채택된 무작위 벡터를 정규화하여 단위 벡터를 만듭니다 ](../images/fig-1.12-sphere-unit-vec.jpg) 다음은 함수의 첫 번째 초안입니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... inline vec3 unit_vector(const vec3& v) { return v / v.length(); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight inline vec3 random_unit_vector() { while (true) { auto p = vec3::random(-1,1); auto lensq = p.length_squared(); if (lensq <= 1) return p / sqrt(lensq); } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [random-unit-vector-1]: [vec3.h] random_unit_vector() 함수, 첫 번째 버전]
안타깝게도, 처리해야 할 작은 부동소수점 추상화 누수가 있습니다. 부동소수점 수는 정밀도가 유한하기 때문에, 매우 작은 값은 제곱할 때 0으로 언더플로될 수 있습니다. 따라서 세 좌표가 모두 충분히 작으면(즉, 구의 중심에 매우 가까우면) 벡터의 노름이 0이 되고, 결과적으로 정규화하면 잘못된 벡터 $[\pm\infty, \pm\infty, \pm\infty]$가 생성됩니다. 이를 해결하기 위해, 중심 주변의 이 "블랙홀" 내부에 있는 점들도 기각하겠습니다. 배정밀도(64비트 부동소수점)를 사용하면 $10^{-160}$보다 큰 값을 안전하게 지원할 수 있습니다. 다음은 더 견고해진 함수입니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ inline vec3 random_unit_vector() { while (true) { auto p = vec3::random(-1,1); auto lensq = p.length_squared(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (1e-160 < lensq && lensq <= 1) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return p / sqrt(lensq); } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [random-unit-vector-2]: [vec3.h] random_unit_vector() 함수, 두 번째 버전]
이제 무작위 단위 벡터가 있으므로, 표면 법선과 비교하여 이 벡터가 올바른 반구에 있는지 판단할 수 있습니다: ![Figure [normal-hor]: 법선 벡터는 우리에게 필요한 반구가 어느 쪽인지 알려 줍니다 ](../images/fig-1.13-surface-normal.jpg)
표면 법선과 무작위 벡터의 내적을 구하여 이 벡터가 올바른 반구에 있는지 판단할 수 있습니다. 내적이 양수이면 벡터는 올바른 반구에 있습니다. 내적이 음수이면 벡터를 반전해야 합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... inline vec3 random_unit_vector() { while (true) { auto p = vec3::random(-1,1); auto lensq = p.length_squared(); if (1e-160 < lensq && lensq <= 1) return p / sqrt(lensq); } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight inline vec3 random_on_hemisphere(const vec3& normal) { vec3 on_unit_sphere = random_unit_vector(); if (dot(on_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal return on_unit_sphere; else return -on_unit_sphere; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [random-in-hemisphere]: [vec3.h] random_on_hemisphere() 함수]
광선이 재질에서 튕겨 나가면서 색의 100%를 유지하면, 그 재질을 _흰색_ 이라고 합니다. 광선이 재질에서 튕겨 나가면서 색의 0%를 유지하면, 그 재질을 검은색이라고 합니다. 새로운 확산 재질의 첫 번째 시연으로, `ray_color` 함수가 한 번 튕길 때마다 색의 50%를 반환하도록 설정하겠습니다. 보기 좋은 회색이 나올 것으로 예상할 수 있습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { ... private: ... color ray_color(const ray& r, const hittable& world) const { hit_record rec; if (world.hit(r, interval(0, infinity), rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 direction = random_on_hemisphere(rec.normal); return 0.5 * ray_color(ray(rec.p, direction), world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-random-unit]: [camera.h] 무작위 광선 방향을 사용하는 ray_color()]
... 실제로 꽤 보기 좋은 회색 구가 나옵니다: ![Image 7: 확산 구의 첫 번째 렌더링 ](../images/img-1.07-first-diffuse.png class='pixel')
자식 광선의 수 제한하기 ---------------------------------- 여기에는 잠재적인 문제가 하나 숨어 있습니다. `ray_color` 함수가 재귀적이라는 점에 주목하십시오. 재귀는 언제 멈출까요? 아무것에도 부딪히지 않을 때입니다. 그러나 경우에 따라서는 그때까지 오랜 시간이 걸릴 수 있으며, 스택이 넘칠 만큼 길어질 수도 있습니다. 이를 방지하기 위해 최대 재귀 깊이를 제한하고, 최대 깊이에 도달하면 빛의 기여를 반환하지 않도록 하겠습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { public: double aspect_ratio = 1.0; // Ratio of image width over height int image_width = 100; // Rendered image width in pixel count int samples_per_pixel = 10; // Count of random samples for each pixel ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight int max_depth = 10; // Maximum number of ray bounces into scene ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ void render(const hittable& world) { initialize(); std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; for (int j = 0; j < image_height; j++) { std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; for (int i = 0; i < image_width; i++) { color pixel_color(0,0,0); for (int sample = 0; sample < samples_per_pixel; sample++) { ray r = get_ray(i, j); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight pixel_color += ray_color(r, max_depth, world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } write_color(std::cout, pixel_samples_scale * pixel_color); } } std::clog << "\rDone. \n"; } ... private: ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight color ray_color(const ray& r, int depth, const hittable& world) const { // If we've exceeded the ray bounce limit, no more light is gathered. if (depth <= 0) return color(0,0,0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ hit_record rec; if (world.hit(r, interval(0, infinity), rec)) { vec3 direction = random_on_hemisphere(rec.normal); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight return 0.5 * ray_color(ray(rec.p, direction), depth-1, world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-depth]: [camera.h] 깊이 제한을 적용한 camera::ray_color()]
이 새로운 깊이 제한을 사용하도록 main() 함수를 수정합니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { ... camera cam; cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 400; cam.samples_per_pixel = 100; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight cam.max_depth = 50; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-ray-depth]: [main.cc] 새로운 광선 깊이 제한 사용하기]
이처럼 매우 간단한 장면에서는 기본적으로 같은 결과를 얻을 것입니다: ![Image 8: 튕김 횟수를 제한한 확산 구의 두 번째 렌더링 ](../images/img-1.08-second-diffuse.png class='pixel')
섀도 애크니 수정하기 ------------------- 또한 해결해야 할 미묘한 버그가 하나 있습니다. 광선은 표면과 교차할 때 교차점을 정확하게 계산하려고 합니다. 안타깝게도 이 계산은 부동 소수점 반올림 오차에 취약하며, 이 오차로 인해 교차점이 아주 미세하게 어긋날 수 있습니다. 즉, 다음 광선, 곧 표면에서 무작위로 산란되는 광선의 원점이 표면과 완벽하게 일치할 가능성은 낮습니다. 원점이 표면 바로 위에 있을 수도 있습니다. 원점이 표면 바로 아래에 있을 수도 있습니다. 광선의 원점이 표면 바로 아래에 있으면 그 광선은 같은 표면과 다시 교차할 수 있습니다. 이는 광선이 $t=0.00000001$ 또는 hit 함수가 제공하는 어떤 부동 소수점 근삿값에서 가장 가까운 표면을 찾게 된다는 뜻입니다. 이 문제를 해결하는 가장 간단한 방법은 계산된 교차점에 매우 가까운 충돌을 그냥 무시하는 것입니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { ... private: ... color ray_color(const ray& r, int depth, const hittable& world) const { // If we've exceeded the ray bounce limit, no more light is gathered. if (depth <= 0) return color(0,0,0); hit_record rec; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (world.hit(r, interval(0.001, infinity), rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ vec3 direction = random_on_hemisphere(rec.normal); return 0.5 * ray_color(ray(rec.p, direction), depth-1, world); } vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [reflect-tolerance]: [camera.h] 허용 오차를 고려한 반사된 광선의 원점 계산]
이렇게 하면 섀도 애크니(shadow acne) 문제가 해결됩니다. 네, 정말로 그렇게 부릅니다. 결과는 다음과 같습니다: ![Image 9: 섀도 애크니가 없는 확산 구 ](../images/img-1.09-no-acne.png class='pixel')
진정한 램버시안 반사 --------------------------- 반사된 광선을 반구 전체에 균일하게 산란시키면 부드럽고 보기 좋은 확산 모델이 만들어지지만, 분명히 더 개선할 수 있습니다. 실제 확산 물체를 더 정확하게 표현하는 것은 _램버시안_ 분포입니다. 이 분포는 반사된 광선을 $\cos (\phi)$에 비례하는 방식으로 산란시키며, 여기서 $\phi$는 반사된 광선과 표면 법선 사이의 각도입니다. 이는 반사된 광선이 표면 법선에 가까운 방향으로 산란될 가능성이 가장 높고, 법선에서 먼 방향으로 산란될 가능성은 낮다는 것을 의미합니다. 이 비균일한 램버시안 분포는 이전의 균일한 산란보다 현실 세계의 재질 반사를 더 잘 모델링합니다. 이 분포는 법선 벡터에 무작위 단위 벡터를 더하여 만들 수 있습니다. 표면 위의 교차점에는 교차점 $\mathbf{p}$와 표면의 법선 $\mathbf{n}$이 있습니다. 교차점에서 이 표면에는 정확히 두 면이 있으므로, 임의의 교차점에 접하는 고유한 단위 구는 두 개만 존재할 수 있습니다(표면의 각 면마다 고유한 구가 하나씩 있습니다). 이 두 단위 구는 반지름의 길이만큼 표면에서 떨어져 있으며, 단위 구의 반지름은 정확히 1입니다. 한 구는 표면 법선 방향($\mathbf{n}$)으로 떨어져 있고, 다른 구는 반대 방향($\mathbf{-n}$)으로 떨어져 있습니다. 따라서 교차점에서 표면에 _딱_ 닿기만 하는 단위 크기의 구 두 개가 남습니다. 이로부터 한 구의 중심은 $(\mathbf{P} + \mathbf{n})$에 있고, 다른 구의 중심은 $(\mathbf{P} - \mathbf{n})$에 있게 됩니다. 중심이 $(\mathbf{P} - \mathbf{n})$에 있는 구는 표면의 _안쪽_ 으로 간주되고, 중심이 $(\mathbf{P} + \mathbf{n})$인 구는 표면의 _바깥쪽_ 으로 간주됩니다. 우리는 표면을 기준으로 광선의 원점과 같은 쪽에 있는 접하는 단위 구를 선택하려고 합니다. 이 반지름이 1인 구 위에서 무작위 점 $\mathbf{S}$를 고르고, 교차점 $\mathbf{P}$에서 무작위 점 $\mathbf{S}$로 광선을 보냅니다(이것이 벡터 $(\mathbf{S}-\mathbf{P})$입니다): ![Figure [rand-unitvec]: 램버시안 분포에 따라 무작위로 벡터 생성하기 ](../images/fig-1.14-rand-unitvec.jpg)
변경 사항은 사실 매우 적습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { ... color ray_color(const ray& r, int depth, const hittable& world) const { // If we've exceeded the ray bounce limit, no more light is gathered. if (depth <= 0) return color(0,0,0); hit_record rec; if (world.hit(r, interval(0.001, infinity), rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 direction = rec.normal + random_unit_vector(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return 0.5 * ray_color(ray(rec.p, direction), depth-1, world); } vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-unit-sphere]: [camera.h] 대체 확산을 적용한 ray_color()]
렌더링하면 비슷한 이미지를 얻습니다: ![Image 10: 램버시안 구의 올바른 렌더링 ](../images/img-1.10-correct-lambertian.png class='pixel')
두 구로 이루어진 장면이 매우 단순하기 때문에 이 두 확산 방식의 차이를 구별하기는 어렵지만, 두 가지 중요한 시각적 차이는 알아챌 수 있을 것입니다: 1. 변경 후 그림자가 더 뚜렷해졌습니다 2. 변경 후 두 구 모두 하늘의 영향으로 푸른빛을 띱니다 이 두 가지 변화는 모두 광선의 산란이 덜 균일해졌기 때문입니다--더 많은 광선이 법선 쪽으로 산란됩니다. 이는 확산 물체의 경우 카메라 쪽으로 튕겨 나가는 빛이 줄어들기 때문에 _더 어둡게_ 보인다는 것을 의미합니다. 그림자의 경우 더 많은 빛이 곧장 위로 튕겨 나가므로 구 아래 영역이 더 어두워집니다. 일상에서 흔히 볼 수 있는 물체 중 완벽하게 확산하는 물체는 많지 않으므로, 이러한 물체가 빛 아래에서 어떻게 동작하는지에 대한 우리의 시각적 직관은 제대로 형성되어 있지 않을 수 있습니다. 책이 진행되면서 장면이 더 복잡해지면, 여기에서 소개한 여러 확산 렌더러를 바꿔 가며 사용해 보시기 바랍니다. 관심을 가질 만한 대부분의 장면에는 확산 재질이 많이 포함됩니다. 서로 다른 확산 방식이 장면의 조명에 어떤 영향을 미치는지 이해하면 귀중한 통찰을 얻을 수 있습니다. 정확한 색상 강도를 위한 감마 보정 사용 ---------------------------------------------------- 구 아래의 그림자에 주목하십시오. 그림이 매우 어둡지만, 우리의 구는 튕길 때마다 에너지의 절반만 흡수하므로 50% 반사체입니다. 구는 꽤 밝게(실제로는 밝은 회색으로) 보여야 하지만, 오히려 상당히 어둡게 보입니다. 확산 재질의 전체 밝기 범위를 차례로 살펴보면 이를 더 분명하게 확인할 수 있습니다. 먼저 `ray_color` 함수의 반사율을 `0.5`(50%)에서 `0.1`(10%)로 설정합니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { ... color ray_color(const ray& r, int depth, const hittable& world) const { // If we've exceeded the ray bounce limit, no more light is gathered. if (depth <= 0) return color(0,0,0); hit_record rec; if (world.hit(r, interval(0.001, infinity), rec)) { vec3 direction = rec.normal + random_unit_vector(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight return 0.1 * ray_color(ray(rec.p, direction), depth-1, world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-gamut]: [camera.h] 반사율이 10%인 ray_color()] 이 새로운 10% 반사율로 렌더링합니다. 그런 다음 반사율을 30%로 설정하고 다시 렌더링합니다. 50%, 70%, 마지막으로 90%에 대해서도 반복합니다. 원하는 사진 편집기에서 이 이미지들을 왼쪽에서 오른쪽으로 겹쳐 놓으면, 선택한 범위에서 밝기가 증가하는 모습을 매우 보기 좋게 시각적으로 표현할 수 있습니다. 지금까지 우리가 사용해 온 것은 다음과 같습니다: ![Image 11: 지금까지의 렌더러 밝기 범위 ](../images/img-1.11-linear-gamut.png class='pixel') 자세히 보거나 색상 선택기를 사용해 보면, 50% 반사율 렌더링(가운데 이미지)이 흰색과 검은색의 중간(중간 회색)이라고 하기에는 너무 어둡다는 것을 알 수 있습니다. 실제로 70% 반사체가 중간 회색에 더 가깝습니다. 그 이유는 거의 모든 컴퓨터 프로그램이 이미지가 이미지 파일에 기록되기 전에 “감마 보정”되었다고 가정하기 때문입니다. 이는 0에서 1 사이의 값이 바이트로 저장되기 전에 어떤 변환이 적용된다는 뜻입니다. 변환 없이 기록된 데이터를 가진 이미지는 _선형 공간_ 에 있다고 하고, 변환된 이미지는 _감마 공간_ 에 있다고 합니다. 여러분이 사용하는 이미지 뷰어는 감마 공간의 이미지를 기대하고 있을 가능성이 높지만, 우리는 선형 공간의 이미지를 제공하고 있습니다. 이것이 우리 이미지가 부정확하게 어둡게 보이는 이유입니다. 이미지를 감마 공간에 저장해야 하는 타당한 이유는 많지만, 우리의 목적상 그런 것이 있다는 것만 알면 됩니다. 이미지 뷰어가 이미지를 더 정확하게 표시할 수 있도록 데이터를 감마 공간으로 변환하겠습니다. 간단한 근사로 “감마 2”를 변환으로 사용할 수 있는데, 이는 감마 공간에서 선형 공간으로 변환할 때 사용하는 거듭제곱입니다. 우리는 선형 공간에서 감마 공간으로 변환해야 하므로 "감마 2"의 역을 취해야 하며, 이는 지수 $1/\mathit{gamma}$, 즉 제곱근을 의미합니다. 또한 음수 입력도 견고하게 처리하도록 해야 합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight inline double linear_to_gamma(double linear_component) { if (linear_component > 0) return std::sqrt(linear_component); return 0; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ void write_color(std::ostream& out, const color& pixel_color) { auto r = pixel_color.x(); auto g = pixel_color.y(); auto b = pixel_color.z(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight // Apply a linear to gamma transform for gamma 2 r = linear_to_gamma(r); g = linear_to_gamma(g); b = linear_to_gamma(b); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Translate the [0,1] component values to the byte range [0,255]. static const interval intensity(0.000, 0.999); int rbyte = int(256 * intensity.clamp(r)); int gbyte = int(256 * intensity.clamp(g)); int bbyte = int(256 * intensity.clamp(b)); // Write out the pixel color components. out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n'; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [write-color-gamma]: [color.h] 감마 보정을 적용한 write_color()]
이 감마 보정을 사용하면 이제 어두운 쪽에서 밝은 쪽으로 훨씬 더 일관된 변화를 얻을 수 있습니다: ![Image 12: 감마 보정을 적용한 렌더러 밝기 범위 ](../images/img-1.12-gamma-gamut.png class='pixel')
금속 ==================================================================================================== 재질을 위한 추상 클래스 -------------------------------- 서로 다른 객체가 서로 다른 재질을 갖게 하려면 설계상의 결정을 내려야 합니다. 매개변수가 많은 범용 재질 타입을 하나 두고, 개별 재질 타입은 자신에게 영향을 주지 않는 매개변수를 그냥 무시하게 할 수 있습니다. 이는 나쁘지 않은 접근 방식입니다. 또는 고유한 동작을 캡슐화하는 추상 재질 클래스를 둘 수도 있습니다. 저는 후자의 접근 방식을 선호합니다. 우리 프로그램에서 재질은 두 가지 일을 해야 합니다: 1. 산란된 광선을 생성합니다(또는 입사 광선을 흡수했다고 알립니다). 2. 산란되었다면 광선이 얼마나 감쇠되어야 하는지 알립니다.
이를 바탕으로 다음과 같은 추상 클래스를 생각할 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef MATERIAL_H #define MATERIAL_H #include "hittable.h" class material { public: virtual ~material() = default; virtual bool scatter( const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered ) const { return false; } }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [material-initial]: [material.h] material 클래스]
광선-객체 교차를 기술하는 데이터 구조 ------------------------------------------------------ `hit_record`는 수많은 인수를 피하기 위한 것으로, 원하는 정보를 무엇이든 그 안에 넣을 수 있습니다. 캡슐화된 타입 대신 인수를 사용해도 되며, 이는 단지 취향의 문제입니다. hittable과 재질은 코드에서 서로의 타입을 참조할 수 있어야 하므로, 참조에 어느 정도 순환성이 생깁니다. C++에서는 `class material;` 줄을 추가하여 `material`이 나중에 정의될 클래스임을 컴파일러에 알립니다. 클래스에 대한 포인터만 지정하는 것이므로 컴파일러는 클래스의 세부 사항을 알 필요가 없으며, 이로써 순환 참조 문제가 해결됩니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight class material; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class hit_record { public: point3 p; vec3 normal; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight shared_ptr mat; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ double t; bool front_face; void set_face_normal(const ray& r, const vec3& outward_normal) { front_face = dot(r.direction(), outward_normal) < 0; normal = front_face ? outward_normal : -outward_normal; } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hit-with-material]: [hittable.h] 재질 포인터를 추가한 히트 레코드] `hit_record`는 여러 인수를 하나의 클래스에 담아 한꺼번에 전달하기 위한 방법일 뿐입니다. 광선이 표면(예를 들어 특정 구)에 닿으면, `hit_record`의 재질 포인터는 프로그램을 시작할 때 `main()`에서 구를 설정하면서 그 구에 부여한 재질 포인터를 가리키도록 설정됩니다. `ray_color()` 루틴이 `hit_record`를 받으면 재질 포인터의 멤버 함수를 호출하여 어떤 광선이 산란되는지(산란되는 광선이 있다면)를 알아낼 수 있습니다.
이를 위해 `hit_record`는 구에 할당된 재질을 전달받아야 합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class sphere : public hittable { public: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight sphere(const point3& center, double radius) : center(center), radius(std::fmax(0,radius)) { // TODO: Initialize the material pointer `mat`. } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ bool hit(const ray& r, interval ray_t, hit_record& rec) const override { ... rec.t = root; rec.p = r.at(rec.t); vec3 outward_normal = (rec.p - center) / radius; rec.set_face_normal(r, outward_normal); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight rec.mat = mat; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return true; } private: point3 center; double radius; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight shared_ptr mat; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [sphere-material]: [sphere.h] 재질 정보를 추가한 광선-구 교차]
빛의 산란과 반사율 모델링 --------------------------------------- 여기서부터 이 책 시리즈 전체에 걸쳐 _알베도_ (라틴어로 "흰색"을 뜻함)라는 용어를 사용하겠습니다. 알베도는 일부 분야에서는 정밀한 기술 용어이지만, 어떤 경우든 일종의 _부분 반사율_ 을 정의하는 데 사용됩니다. 알베도는 재질의 색상에 따라 달라지며, (나중에 유리 재질에서 구현할 것처럼) 입사하는 시선 방향(들어오는 광선의 방향)에 따라서도 달라질 수 있습니다. 램버시안(확산) 반사율은 항상 산란하면서 반사율 $R$에 따라 빛을 감쇠시킬 수도 있고, 때때로(확률 $1-R$로) 감쇠 없이 산란할 수도 있습니다(이때 산란되지 않은 광선은 재질에 그대로 흡수됩니다). 또한 이 두 전략을 혼합할 수도 있습니다. 여기서는 항상 산란하는 방식을 선택하므로, 램버시안 재질의 구현은 간단한 작업이 됩니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class material { ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight class lambertian : public material { public: lambertian(const color& albedo) : albedo(albedo) {} bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override { auto scatter_direction = rec.normal + random_unit_vector(); scattered = ray(rec.p, scatter_direction); attenuation = albedo; return true; } private: color albedo; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [lambertian-initial]: [material.h] 새로운 램버시안 재질 클래스] 세 번째 방법도 있다는 점에 유의하십시오. 어떤 고정된 확률 $p$로 산란시키고, 감쇠를 $\mathit{albedo}/p$로 둘 수도 있습니다. 알베도를 어떻게 다룰지는 여러분이 선택하면 됩니다. 선택은 여러분의 몫입니다. 위의 코드를 주의 깊게 읽어 보면 약간의 말썽이 생길 가능성이 있다는 것을 알아차릴 것입니다. 생성한 무작위 단위 벡터가 법선 벡터와 정확히 반대 방향이면 두 벡터의 합이 0이 되고, 그 결과 산란 방향 벡터가 영벡터가 됩니다. 이는 나중에 좋지 않은 상황(무한대와 NaN)으로 이어지므로, 이 조건을 다음 단계로 넘기기 전에 가로채야 합니다. 이를 위해 벡터가 모든 차원에서 0에 매우 가까우면 true를 반환하는 새로운 벡터 메서드 `vec3::near_zero()`를 만들겠습니다. 다음 변경 사항에서는 입력값의 절댓값을 반환하는 C++ 표준 라이브러리 함수 `std::fabs`를 사용합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class vec3 { ... double length_squared() const { return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight bool near_zero() const { // Return true if the vector is close to zero in all dimensions. auto s = 1e-8; return (std::fabs(e[0]) < s) && (std::fabs(e[1]) < s) && (std::fabs(e[2]) < s); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [vec3-near-zero]: [vec3.h] vec3::near_zero() 메서드] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class lambertian : public material { public: lambertian(const color& albedo) : albedo(albedo) {} bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override { auto scatter_direction = rec.normal + random_unit_vector(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight // Catch degenerate scatter direction if (scatter_direction.near_zero()) scatter_direction = rec.normal; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ scattered = ray(rec.p, scatter_direction); attenuation = albedo; return true; } private: color albedo; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [lambertian-catch-zero]: [material.h] 견고한 램버시안 산란] 거울 같은 빛 반사 -------------------------- 광택이 나는 금속에서는 광선이 무작위로 산란되지 않습니다. 핵심 질문은 이것입니다. 광선은 금속 거울에서 어떻게 반사될까요? 여기서는 벡터 수학이 도움이 됩니다. ![Figure [reflection]: 광선 반사](../images/fig-1.15-reflection.jpg) 빨간색으로 표시된 반사된 광선 방향은 단순히 $\mathbf{v} + 2\mathbf{b}$입니다. 우리 설계에서 $\mathbf{n}$은 단위 벡터(길이가 1)이지만, $\mathbf{v}$는 그렇지 않을 수 있습니다. 벡터 $\mathbf{b}$를 구하려면 $\mathbf{v}$를 $\mathbf{n}$에 투영한 길이만큼 법선 벡터를 스케일링합니다. 이 길이는 내적 $\mathbf{v} \cdot \mathbf{n}$으로 구할 수 있습니다. ($\mathbf{n}$이 단위 벡터가 아니라면 이 내적을 $\mathbf{n}$의 길이로도 나누어야 합니다.) 마지막으로 $\mathbf{v}$는 표면 _안쪽으로_ 향하고 $\mathbf{b}$는 표면 _바깥쪽으로_ 향해야 하므로, 이 투영 길이의 부호를 반전해야 합니다. 이 모든 것을 종합하면 반사 벡터를 다음과 같이 계산할 수 있습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... inline vec3 random_on_hemisphere(const vec3& normal) { ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight inline vec3 reflect(const vec3& v, const vec3& n) { return v - 2*dot(v,n)*n; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [vec3-reflect]: [vec3.h] vec3 반사 함수]
금속 재질은 이 공식을 사용해 광선을 반사하기만 합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... class lambertian : public material { ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight class metal : public material { public: metal(const color& albedo) : albedo(albedo) {} bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override { vec3 reflected = reflect(r_in.direction(), rec.normal); scattered = ray(rec.p, reflected); attenuation = albedo; return true; } private: color albedo; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [metal-material]: [material.h] 반사율 함수를 갖춘 금속 재질]
이 모든 변경 사항을 반영하려면 `ray_color()` 함수를 수정해야 합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "hittable.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "material.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... class camera { ... private: ... color ray_color(const ray& r, int depth, const hittable& world) const { // If we've exceeded the ray bounce limit, no more light is gathered. if (depth <= 0) return color(0,0,0); hit_record rec; if (world.hit(r, interval(0.001, infinity), rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight ray scattered; color attenuation; if (rec.mat->scatter(r, rec, attenuation, scattered)) return attenuation * ray_color(scattered, depth-1, world); return color(0,0,0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } vec3 unit_direction = unit_vector(r.direction()); auto a = 0.5*(unit_direction.y() + 1.0); return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-scatter]: [camera.h] 산란 반사율을 적용한 광선 색상]
이제 재질 포인터 `mat`를 초기화하도록 `sphere` 생성자를 수정하겠습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class sphere : public hittable { public: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight sphere(const point3& center, double radius, shared_ptr mat) : center(center), radius(std::fmax(0,radius)), mat(mat) {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [sphere-ctor-material]: [sphere.h] 재질로 구 초기화하기] 금속 구가 있는 장면 --------------------------- 이제 장면에 금속 구를 몇 개 추가해 보겠습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "rtweekend.h" #include "camera.h" #include "hittable.h" #include "hittable_list.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "material.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "sphere.h" int main() { hittable_list world; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.1, 0.2, 0.5)); auto material_left = make_shared(color(0.8, 0.8, 0.8)); auto material_right = make_shared(color(0.8, 0.6, 0.2)); world.add(make_shared(point3( 0.0, -100.5, -1.0), 100.0, material_ground)); world.add(make_shared(point3( 0.0, 0.0, -1.2), 0.5, material_center)); world.add(make_shared(point3(-1.0, 0.0, -1.0), 0.5, material_left)); world.add(make_shared(point3( 1.0, 0.0, -1.0), 0.5, material_right)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ camera cam; cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 400; cam.samples_per_pixel = 100; cam.max_depth = 50; cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-with-metal]: [main.cc] 금속 구가 있는 장면]
결과는 다음과 같습니다. ![Image 13: 반짝이는 금속 ](../images/img-1.13-metal-shiny.png class='pixel')
흐릿한 반사 ----------------- 작은 구를 사용해 광선의 새로운 끝점을 선택하는 방식으로 반사 방향을 무작위화할 수도 있습니다. 원래 끝점을 중심으로 하는 구의 표면에서 무작위 점을 선택하고, 이를 퍼지 계수로 스케일링하여 사용하겠습니다. ![Figure [reflect-fuzzy]: 퍼지 반사 광선 생성하기](../images/fig-1.16-reflect-fuzzy.jpg) 퍼지 구가 클수록 반사가 더 흐릿해집니다. 이는 구의 반지름을 그대로 값으로 쓰는 퍼지 정도 매개변수를 추가하면 된다는 뜻입니다(따라서 0이면 섭동이 없습니다). 문제는 구가 크거나 광선이 표면을 스치듯 입사하는 경우 표면 아래로 산란될 수 있다는 점입니다. 이런 광선은 표면이 흡수하도록 하면 됩니다. 또한 퍼지 구가 의미를 가지려면 길이가 임의로 달라질 수 있는 반사 벡터에 대해 일관된 비율로 스케일링되어야 한다는 점에 유의하십시오. 이를 해결하려면 반사된 광선을 정규화해야 합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class metal : public material { public: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight metal(const color& albedo, double fuzz) : albedo(albedo), fuzz(fuzz < 1 ? fuzz : 1) {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override { vec3 reflected = reflect(r_in.direction(), rec.normal); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight reflected = unit_vector(reflected) + (fuzz * random_unit_vector()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ scattered = ray(rec.p, reflected); attenuation = albedo; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight return (dot(scattered.direction(), rec.normal) > 0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } private: color albedo; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double fuzz; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [metal-fuzz]: [material.h] 금속 재질의 퍼지 정도]
금속에 퍼지 정도 0.3과 1.0을 추가하여 시험해 볼 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { ... auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.1, 0.2, 0.5)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto material_left = make_shared(color(0.8, 0.8, 0.8), 0.3); auto material_right = make_shared(color(0.8, 0.6, 0.2), 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [metal-fuzz-spheres]: [main.cc] 퍼지 정도가 적용된 금속 구] ![Image 14: 퍼지 금속 ](../images/img-1.14-metal-fuzz.png class='pixel')
유전체 ==================================================================================================== 물, 유리, 다이아몬드와 같은 투명한 재질은 유전체입니다. 광선이 이러한 물질에 닿으면 반사된 광선과 굴절된(투과된) 광선으로 나뉩니다. 여기서는 반사와 굴절 중 하나를 무작위로 선택하여 상호 작용마다 산란 광선을 하나만 생성하는 방식으로 이를 처리하겠습니다. 용어를 간단히 복습하자면, _반사된_ 광선은 표면에 닿은 뒤 새로운 방향으로 "튕겨" 나갑니다. _굴절된_ 광선은 재질을 둘러싼 환경에서 재질 내부로 넘어가면서 휘어집니다(유리나 물의 경우처럼). 연필을 물에 일부 담갔을 때 휘어져 보이는 이유가 바로 이것입니다. 굴절된 광선이 휘는 정도는 재질의 _굴절률_ 에 의해 결정됩니다. 일반적으로 이 값은 빛이 진공에서 재질로 들어갈 때 얼마나 휘는지를 나타내는 단일 값입니다. 유리의 굴절률은 대략 1.5–1.7이고, 다이아몬드는 약 2.4이며, 공기는 1.000293이라는 작은 굴절률을 가집니다. 투명한 재질이 다른 투명한 재질 안에 들어 있을 때는 상대 굴절률로 굴절을 기술할 수 있습니다: 이는 물체 재질의 굴절률을 주변 재질의 굴절률로 나눈 값입니다. 예를 들어 물속에 있는 유리 구슬을 렌더링하려는 경우, 유리 구슬의 유효 굴절률은 1.125가 됩니다. 이 값은 유리의 굴절률(1.5)을 물의 굴절률(1.333)로 나누어 얻습니다. 대부분의 일반적인 재질의 굴절률은 인터넷에서 간단히 검색하여 찾을 수 있습니다. 굴절 ----------- 디버깅하기 가장 어려운 부분은 굴절된 광선입니다. 저는 보통 처음에는 굴절 광선이 존재하기만 하면 모든 빛이 굴절되도록 합니다. 이 프로젝트에서는 장면에 유리 구슬 두 개를 배치해 보았고, 다음과 같은 결과를 얻었습니다(이것이 올바른지 잘못되었는지는 아직 말씀드리지 않았지만, 곧 알려 드리겠습니다!): ![Image 15: 첫 번째 유리 ](../images/img-1.15-glass-first.png class='pixel') 이것이 맞는 결과일까요? 실제로도 유리구슬은 이상하게 보이기는 합니다. 하지만 아닙니다. 이것은 올바른 결과가 아닙니다. 세상이 위아래로 뒤집혀 보여야 하며, 이상한 검은 부분도 없어야 합니다. 이미지 정중앙을 곧바로 통과하는 광선을 출력해 보았더니 명백히 잘못되어 있었습니다. 이 방법으로 문제를 찾아내는 경우가 많습니다. 스넬의 법칙 ------------ 굴절은 스넬의 법칙으로 설명됩니다: $$ \eta \cdot \sin\theta = \eta' \cdot \sin\theta' $$ 여기서 $\theta$와 $\theta'$는 법선으로부터의 각도이고, $\eta$와 $\eta'$("에타", "에타 프라임"이라고 읽습니다)는 굴절률입니다. 기하학적 구성은 다음과 같습니다: ![Figure [refraction]: 광선 굴절](../images/fig-1.17-refraction.jpg) 굴절된 광선의 방향을 구하려면 $\sin\theta'$에 대해 풀어야 합니다: $$ \sin\theta' = \frac{\eta}{\eta'} \cdot \sin\theta $$ 표면의 굴절된 쪽에는 굴절된 광선 $\mathbf{R'}$와 법선 $\mathbf{n'}$가 있으며, 이 둘 사이에는 각도 $\theta'$가 존재합니다. $\mathbf{R'}$를 $\mathbf{n'}$에 수직인 성분과 $\mathbf{n'}$에 평행한 성분으로 나눌 수 있습니다: $$ \mathbf{R'} = \mathbf{R'}_{\bot} + \mathbf{R'}_{\parallel} $$ $\mathbf{R'}_{\bot}$와 $\mathbf{R'}_{\parallel}$에 대해 풀면 다음을 얻습니다: $$ \mathbf{R'}_{\bot} = \frac{\eta}{\eta'} (\mathbf{R} + |\mathbf{R}| \cos(\theta) \mathbf{n}) $$ $$ \mathbf{R'}_{\parallel} = -\sqrt{1 - |\mathbf{R'}_{\bot}|^2} \mathbf{n} $$ 원한다면 직접 증명해 보셔도 되지만, 여기서는 이를 사실로 받아들이고 넘어가겠습니다. 이 책의 나머지 부분을 이해하는 데 이 증명은 필요하지 않습니다. 우변의 항 중 $\cos\theta$를 제외한 모든 항의 값은 알고 있습니다. 두 벡터의 내적은 두 벡터 사이 각도의 코사인으로 나타낼 수 있다는 것은 잘 알려져 있습니다: $$ \mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| |\mathbf{b}| \cos\theta $$ $\mathbf{a}$와 $\mathbf{b}$를 단위 벡터로 제한하면 다음과 같습니다: $$ \mathbf{a} \cdot \mathbf{b} = \cos\theta $$ 이제 $\mathbf{R'}_{\bot}$를 이미 알고 있는 값들로 다시 쓸 수 있습니다: $$ \mathbf{R'}_{\bot} = \frac{\eta}{\eta'} (\mathbf{R} + (\mathbf{-R} \cdot \mathbf{n}) \mathbf{n}) $$
이들을 다시 합치면 $\mathbf{R'}$를 계산하는 함수를 작성할 수 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... inline vec3 reflect(const vec3& v, const vec3& n) { return v - 2*dot(v,n)*n; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight inline vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) { auto cos_theta = std::fmin(dot(-uv, n), 1.0); vec3 r_out_perp = etai_over_etat * (uv + cos_theta*n); vec3 r_out_parallel = -std::sqrt(std::fabs(1.0 - r_out_perp.length_squared())) * n; return r_out_perp + r_out_parallel; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [refract]: [vec3.h] 굴절 함수]
그리고 항상 굴절하는 유전체 재질은 다음과 같습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... class metal : public material { ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight class dielectric : public material { public: dielectric(double refraction_index) : refraction_index(refraction_index) {} bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override { attenuation = color(1.0, 1.0, 1.0); double ri = rec.front_face ? (1.0/refraction_index) : refraction_index; vec3 unit_direction = unit_vector(r_in.direction()); vec3 refracted = refract(unit_direction, rec.normal, ri); scattered = ray(rec.p, refracted); return true; } private: // Refractive index in vacuum or air, or the ratio of the material's refractive index over // the refractive index of the enclosing media double refraction_index; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [dielectric-always-refract]: [material.h] 항상 굴절하는 유전체 재질 클래스]
이제 굴절을 보여 주기 위해 왼쪽 구를 굴절률이 약 1.5인 유리로 바꾸어 장면을 수정하겠습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.1, 0.2, 0.5)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto material_left = make_shared(1.50); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto material_right = make_shared(color(0.8, 0.6, 0.2), 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [two-glass]: [main.cc] 왼쪽 구를 유리로 변경하기]
그러면 다음과 같은 결과를 얻습니다: ![Image 16: 항상 굴절하는 유리 구 ](../images/img-1.16-glass-always-refract.png class='pixel')
전반사 -------------------------- 굴절과 관련된 한 가지 까다로운 실제 문제는 스넬의 법칙으로는 해를 구할 수 없는 광선 각도가 존재한다는 것입니다. 광선이 충분히 비스듬한 각도로 굴절률이 더 낮은 매질에 들어가면 90°보다 큰 각도로 굴절될 수 있습니다. 스넬의 법칙과 $\sin\theta'$의 유도 과정을 다시 살펴보면 다음과 같습니다: $$ \sin\theta' = \frac{\eta}{\eta'} \cdot \sin\theta $$ 광선이 유리 내부에 있고 외부가 공기라면($\eta = 1.5$ 및 $\eta' = 1.0$) 다음과 같습니다: $$ \sin\theta' = \frac{1.5}{1.0} \cdot \sin\theta $$
$\sin\theta'$의 값은 1보다 클 수 없습니다. 따라서 만약 다음과 같다면, $$ \frac{1.5}{1.0} \cdot \sin\theta > 1.0 $$ 방정식 양변 사이의 등식이 깨지므로 해가 존재할 수 없습니다. 해가 존재하지 않으면 유리는 굴절할 수 없으므로 광선을 반사해야 합니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ if (ri * sin_theta > 1.0) { // Must Reflect ... } else { // Can Refract ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [dielectric-can-refract-1]: [material.h] 광선이 굴절할 수 있는지 판단하기]
이 경우 모든 빛이 반사되며, 실제로는 대개 고체 물체 내부에서 일어나기 때문에 이를 _전반사_ 라고 합니다. 그래서 물속에 잠겨 있을 때 물과 공기의 경계가 때때로 완벽한 거울처럼 작용합니다. 물속에서 위를 올려다보면 물 위의 사물이 보이지만, 표면 가까이에서 옆을 바라보면 수면이 거울처럼 보입니다. 삼각함수 항등식을 사용하여 `sin_theta`를 구할 수 있습니다: $$ \sin\theta = \sqrt{1 - \cos^2\theta} $$ 그리고 $$ \cos\theta = \mathbf{R} \cdot \mathbf{n} $$ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0); double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta); if (ri * sin_theta > 1.0) { // Must Reflect ... } else { // Can Refract ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [dielectric-can-refract-2]: [material.h] 광선이 굴절할 수 있는지 판단하기]
그리고 (가능한 경우) 항상 굴절하는 유전체 재질은 다음과 같습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class dielectric : public material { public: dielectric(double refraction_index) : refraction_index(refraction_index) {} bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override { attenuation = color(1.0, 1.0, 1.0); double ri = rec.front_face ? (1.0/refraction_index) : refraction_index; vec3 unit_direction = unit_vector(r_in.direction()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0); double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta); bool cannot_refract = ri * sin_theta > 1.0; vec3 direction; if (cannot_refract) direction = reflect(unit_direction, rec.normal); else direction = refract(unit_direction, rec.normal, ri); scattered = ray(rec.p, direction); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return true; } private: // Refractive index in vacuum or air, or the ratio of the material's refractive index over // the refractive index of the enclosing media double refraction_index; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [dielectric-with-refraction]: [material.h] 반사를 포함한 유전체 재질 클래스]
감쇠는 항상 1입니다 -- 유리 표면은 아무것도 흡수하지 않습니다. 새로운 `dielectric::scatter()` 함수로 이전 장면을 렌더링하면 … 아무런 변화가 없습니다. 어라? 그런데 공기보다 굴절률이 큰 재질로 된 구에서는 전반사를 일으키는 입사 각도가 존재하지 않습니다. 광선이 구로 들어가는 지점에서도, 광선이 빠져나가는 지점에서도 마찬가지입니다. 이는 구의 기하학적 특성 때문인데, 스치듯 들어오는 광선은 항상 더 작은 각도로 꺾였다가 빠져나갈 때 원래 각도로 다시 꺾이기 때문입니다. 그렇다면 전반사를 어떻게 보여 줄 수 있을까요? 구의 굴절률이 구를 둘러싼 매질보다 _더 작다면_, 얕게 스치는 각도로 구에 광선을 쏘아 전 _외부_ 반사를 얻을 수 있습니다. 이 정도면 그 효과를 관찰하기에 충분할 것입니다. 물(굴절률 약 1.33)로 가득 찬 세계를 모델링하고 구의 재질을 공기(굴절률 1.00)로 바꾸겠습니다 -- 바로 공기 방울입니다! 이를 위해 왼쪽 구 재질의 굴절률을 다음과 같이 변경하십시오 $$\frac{\text{index of refraction of air}}{\text{index of refraction of water}}$$ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.1, 0.2, 0.5)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto material_left = make_shared(1.00 / 1.33); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto material_right = make_shared(color(0.8, 0.6, 0.2), 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [two-glass]: [main.cc] 왼쪽 구는 물속의 공기 방울입니다]
이렇게 변경하면 다음과 같은 렌더링 결과가 나옵니다: ![Image 17: 때로는 굴절하고 때로는 반사하는 공기 방울 ](../images/img-1.17-air-bubble-total-reflection.png class='pixel') 여기서 거의 정면으로 들어오는 광선은 굴절하고, 비스듬히 스치는 광선은 반사하는 것을 볼 수 있습니다.
슐릭 근사 ---------------------- 실제 유리는 각도에 따라 반사율이 달라집니다. 창문을 가파른 각도에서 바라보면 거울처럼 보입니다. 이를 설명하는 크고 복잡한 방정식이 있지만, 거의 모든 사람이 Christophe Schlick이 고안한 저렴하면서도 놀라울 정도로 정확한 다항식 근사를 사용합니다. 이렇게 하면 완전한 유리 재질이 만들어집니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class dielectric : public material { public: dielectric(double refraction_index) : refraction_index(refraction_index) {} bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override { attenuation = color(1.0, 1.0, 1.0); double ri = rec.front_face ? (1.0/refraction_index) : refraction_index; vec3 unit_direction = unit_vector(r_in.direction()); double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0); double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta); bool cannot_refract = ri * sin_theta > 1.0; vec3 direction; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (cannot_refract || reflectance(cos_theta, ri) > random_double()) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ direction = reflect(unit_direction, rec.normal); else direction = refract(unit_direction, rec.normal, ri); scattered = ray(rec.p, direction); return true; } private: // Refractive index in vacuum or air, or the ratio of the material's refractive index over // the refractive index of the enclosing media double refraction_index; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight static double reflectance(double cosine, double refraction_index) { // Use Schlick's approximation for reflectance. auto r0 = (1 - refraction_index) / (1 + refraction_index); r0 = r0*r0; return r0 + (1-r0)*std::pow((1 - cosine),5); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [glass]: [material.h] 완전한 유리 재질] 속이 빈 유리 구 모델링하기 ------------------------------- 속이 빈 유리 구를 모델링해 보겠습니다. 이것은 어느 정도 두께가 있는 구이며, 그 안에 공기로 된 또 다른 구가 들어 있습니다. 이러한 물체를 통과하는 광선의 경로를 생각해 보면, 광선은 바깥쪽 구에 부딪혀 굴절하고, 안쪽 구에 부딪힌 뒤(실제로 부딪힌다고 가정할 때) 두 번째로 굴절하여 내부의 공기를 통과합니다. 그런 다음 계속 진행하여 안쪽 구의 내부 표면에 부딪혀 다시 굴절하고, 이어서 바깥쪽 구의 내부 표면에 부딪힌 뒤, 마지막으로 굴절하여 장면의 대기 속으로 다시 빠져나갑니다. 바깥쪽 구는 굴절률이 약 1.50인 표준 유리 구로 모델링하면 됩니다(바깥 공기에서 유리로 들어가는 굴절을 모델링합니다). 안쪽 구는 조금 다릅니다. _그_ 굴절률은 이를 둘러싼 바깥쪽 구의 재질을 기준으로 한 상대값이어야 하며, 이를 통해 유리에서 내부 공기로 넘어가는 전이를 모델링하기 때문입니다. 유전체 재질의 `refraction_index` 매개변수는 물체의 굴절률을 둘러싼 매질의 굴절률로 나눈 _비율_ 로 해석할 수 있으므로, 실제로는 간단하게 지정할 수 있습니다. 이 경우 안쪽 구의 굴절률은 공기(안쪽 구의 재질)의 굴절률을 유리(둘러싼 매질)의 굴절률로 나눈 값, 즉 $1.00/1.50 = 0.67$이 됩니다. 코드는 다음과 같습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.1, 0.2, 0.5)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto material_left = make_shared(1.50); auto material_bubble = make_shared(1.00 / 1.50); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto material_right = make_shared(color(0.8, 0.6, 0.2), 0.0); world.add(make_shared(point3( 0.0, -100.5, -1.0), 100.0, material_ground)); world.add(make_shared(point3( 0.0, 0.0, -1.2), 0.5, material_center)); world.add(make_shared(point3(-1.0, 0.0, -1.0), 0.5, material_left)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight world.add(make_shared(point3(-1.0, 0.0, -1.0), 0.4, material_bubble)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ world.add(make_shared(point3( 1.0, 0.0, -1.0), 0.5, material_right)); ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-hollow-glass]: [main.cc] 속이 빈 유리 구가 있는 장면]
결과는 다음과 같습니다: ![Image 18: 속이 빈 유리 구 ](../images/img-1.18-glass-hollow.png class='pixel')
위치를 조정할 수 있는 카메라 ==================================================================================================== 카메라는 유전체와 마찬가지로 디버깅하기가 까다롭기 때문에, 저는 항상 점진적으로 개발합니다. 먼저, 시야각(_fov_)을 조정할 수 있도록 하겠습니다. 이것은 렌더링된 이미지의 한쪽 가장자리에서 반대쪽 가장자리까지의 시각적 각도입니다. 이미지가 정사각형이 아니므로 fov는 가로 방향과 세로 방향이 서로 다릅니다. 저는 항상 세로 fov를 사용합니다. 또한 저는 보통 이를 도 단위로 지정하고 생성자 내부에서 라디안으로 변환합니다. 이는 개인 취향의 문제입니다. 카메라 시야 기하 구조 ------------------------ 먼저, 광선이 원점에서 출발하여 $z = -1$ 평면을 향하도록 유지하겠습니다. $h$를 그 거리에 대한 비율로 만든다면 $z = -2$ 평면이든 다른 무엇이든 상관없습니다. 설정은 다음과 같습니다: ![Figure [cam-view-geom]: 카메라 시야 기하 구조(측면에서 본 모습) ](../images/fig-1.18-cam-view-geom.jpg)
이는 $h = \tan(\frac{\theta}{2})$임을 의미합니다. 이제 카메라는 다음과 같이 됩니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { public: double aspect_ratio = 1.0; // Ratio of image width over height int image_width = 100; // Rendered image width in pixel count int samples_per_pixel = 10; // Count of random samples for each pixel int max_depth = 10; // Maximum number of ray bounces into scene ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double vfov = 90; // Vertical view angle (field of view) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ void render(const hittable& world) { ... private: ... void initialize() { image_height = int(image_width / aspect_ratio); image_height = (image_height < 1) ? 1 : image_height; pixel_samples_scale = 1.0 / samples_per_pixel; center = point3(0, 0, 0); // Determine viewport dimensions. auto focal_length = 1.0; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto theta = degrees_to_radians(vfov); auto h = std::tan(theta/2); auto viewport_height = 2 * h * focal_length; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto viewport_width = viewport_height * (double(image_width)/image_height); // Calculate the vectors across the horizontal and down the vertical viewport edges. auto viewport_u = vec3(viewport_width, 0, 0); auto viewport_v = vec3(0, -viewport_height, 0); // Calculate the horizontal and vertical delta vectors from pixel to pixel. pixel_delta_u = viewport_u / image_width; pixel_delta_v = viewport_v / image_height; // Calculate the location of the upper left pixel. auto viewport_upper_left = center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2; pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); } ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-fov]: [camera.h] 시야각(fov)을 조정할 수 있는 카메라]
서로 맞닿은 두 구로 이루어진 간단한 장면과 90° 시야각을 사용하여 이 변경 사항을 테스트해 보겠습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { hittable_list world; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto R = std::cos(pi/4); auto material_left = make_shared(color(0,0,1)); auto material_right = make_shared(color(1,0,0)); world.add(make_shared(point3(-R, 0, -1), R, material_left)); world.add(make_shared(point3( R, 0, -1), R, material_right)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ camera cam; cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 400; cam.samples_per_pixel = 100; cam.max_depth = 50; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight cam.vfov = 90; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-wide-angle]: [main.cc] 광각 카메라를 사용한 장면(넓은 각도)]
이렇게 하면 다음과 같은 렌더링 결과를 얻습니다: ![Image 19: 광각(넓은 각도) 뷰 ](../images/img-1.19-wide-view.png class='pixel')
카메라의 위치와 방향 지정하기 ------------------------------------- 임의의 시점을 얻기 위해, 먼저 우리가 관심을 두는 점들에 이름을 붙이겠습니다. 카메라를 놓는 위치를 _lookfrom_ 이라고 부르고, 바라보는 점을 _lookat_ 이라고 부르겠습니다. (나중에 원한다면 바라볼 점 대신 바라볼 방향을 정의할 수도 있습니다.) 또한 카메라의 롤(roll), 즉 옆으로 기울어짐을 지정하는 방법도 필요합니다. 이는 lookat-lookfrom 축을 중심으로 한 회전입니다. 달리 생각해 보면, `lookfrom`과 `lookat`을 고정하더라도 여전히 코를 중심으로 머리를 회전할 수 있다는 것입니다. 우리에게 필요한 것은 카메라의 “위쪽” 벡터를 지정하는 방법입니다. ![Figure [cam-view-dir]: 카메라 시선 방향](../images/fig-1.19-cam-view-dir.jpg) 시선 방향과 평행하지만 않다면 원하는 어떤 위쪽 벡터든 지정할 수 있습니다. 이 위쪽 벡터를 시선 방향에 직교하는 평면에 투영하여 카메라 기준의 위쪽 벡터를 얻습니다. 저는 이를 “뷰 업(view up)” (_vup_) 벡터라고 부르는 일반적인 관례를 따릅니다. 몇 번의 외적과 벡터 정규화를 거치면, 이제 카메라의 방향을 기술하는 완전한 정규 직교 기저 $(u,v,w)$를 얻게 됩니다. $u$는 카메라의 오른쪽을 가리키는 단위 벡터이고, $v$는 카메라의 위쪽을 가리키는 단위 벡터이며, $w$는 (오른손 좌표계를 사용하므로) 시선 방향의 반대쪽을 가리키는 단위 벡터이고, 카메라 중심은 원점에 있습니다. ![Figure [cam-view-up]: 카메라 뷰 업 방향](../images/fig-1.20-cam-view-up.jpg) 이전에 고정된 카메라가 $-Z$를 향했던 것처럼, 임의 시점 카메라는 $-w$를 향합니다. vup을 지정할 때 월드 업 $(0,1,0)$을 사용할 수 있지만, 반드시 그래야 하는 것은 아니라는 점을 기억하십시오. 이렇게 하면 편리하며, 기발한 카메라 각도로 실험해 보기로 하기 전까지는 카메라가 자연스럽게 수평을 유지하게 됩니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { public: double aspect_ratio = 1.0; // Ratio of image width over height int image_width = 100; // Rendered image width in pixel count int samples_per_pixel = 10; // Count of random samples for each pixel int max_depth = 10; // Maximum number of ray bounces into scene double vfov = 90; // Vertical view angle (field of view) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight point3 lookfrom = point3(0,0,0); // Point camera is looking from point3 lookat = point3(0,0,-1); // Point camera is looking at vec3 vup = vec3(0,1,0); // Camera-relative "up" direction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... private: int image_height; // Rendered image height double pixel_samples_scale; // Color scale factor for a sum of pixel samples point3 center; // Camera center point3 pixel00_loc; // Location of pixel 0, 0 vec3 pixel_delta_u; // Offset to pixel to the right vec3 pixel_delta_v; // Offset to pixel below ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 u, v, w; // Camera frame basis vectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ void initialize() { image_height = int(image_width / aspect_ratio); image_height = (image_height < 1) ? 1 : image_height; pixel_samples_scale = 1.0 / samples_per_pixel; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight center = lookfrom; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Determine viewport dimensions. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto focal_length = (lookfrom - lookat).length(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto theta = degrees_to_radians(vfov); auto h = std::tan(theta/2); auto viewport_height = 2 * h * focal_length; auto viewport_width = viewport_height * (double(image_width)/image_height); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight // Calculate the u,v,w unit basis vectors for the camera coordinate frame. w = unit_vector(lookfrom - lookat); u = unit_vector(cross(vup, w)); v = cross(w, u); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Calculate the vectors across the horizontal and down the vertical viewport edges. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 viewport_u = viewport_width * u; // Vector across viewport horizontal edge vec3 viewport_v = viewport_height * -v; // Vector down viewport vertical edge ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Calculate the horizontal and vertical delta vectors from pixel to pixel. pixel_delta_u = viewport_u / image_width; pixel_delta_v = viewport_v / image_height; // Calculate the location of the upper left pixel. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto viewport_upper_left = center - (focal_length * w) - viewport_u/2 - viewport_v/2; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); } ... private: }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-orient]: [camera.h] 위치와 방향을 지정할 수 있는 카메라]
이전 장면으로 되돌리고 새로운 시점을 사용하겠습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { hittable_list world; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.1, 0.2, 0.5)); auto material_left = make_shared(1.50); auto material_bubble = make_shared(1.00 / 1.50); auto material_right = make_shared(color(0.8, 0.6, 0.2), 1.0); world.add(make_shared(point3( 0.0, -100.5, -1.0), 100.0, material_ground)); world.add(make_shared(point3( 0.0, 0.0, -1.2), 0.5, material_center)); world.add(make_shared(point3(-1.0, 0.0, -1.0), 0.5, material_left)); world.add(make_shared(point3(-1.0, 0.0, -1.0), 0.4, material_bubble)); world.add(make_shared(point3( 1.0, 0.0, -1.0), 0.5, material_right)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ camera cam; cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 400; cam.samples_per_pixel = 100; cam.max_depth = 50; cam.vfov = 90; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight cam.lookfrom = point3(-2,2,1); cam.lookat = point3(0,0,-1); cam.vup = vec3(0,1,0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-free-view]: [main.cc] 다른 시점에서 본 장면]
결과는 다음과 같습니다: ![Image 20: 먼 거리에서 본 뷰 ](../images/img-1.20-view-distant.png class='pixel')
그리고 시야각을 변경할 수도 있습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight cam.vfov = 20; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [change-field-view]: [main.cc] 시야각 변경]
결과는 다음과 같습니다: ![Image 21: 확대하기](../images/img-1.21-view-zoom.png class='pixel')
디포커스 블러 ==================================================================================================== 이제 마지막 기능인 _디포커스 블러(defocus blur)_ 입니다. 참고로, 사진작가들은 이것을 _피사계 심도(depth of field)_ 라고 부르므로, _디포커스 블러_ 라는 용어는 레이 트레이싱을 하는 친구들 사이에서만 사용하십시오. 실제 카메라에서 디포커스 블러가 생기는 이유는 빛을 모으기 위해 (단순한 바늘구멍이 아닌) 큰 구멍이 필요하기 때문입니다. 구멍이 크면 모든 것이 디포커스(초점이 흐려짐)되지만, 필름/센서 앞에 렌즈를 두면 모든 것이 초점이 맞는 특정 거리가 생깁니다. 그 거리에 놓인 물체는 초점이 맞아 보이며, 그 거리에서 멀어질수록 선형적으로 더 흐리게 보입니다. 렌즈는 이렇게 생각할 수 있습니다. 초점 거리에 있는 특정한 점 _에서_ 나와 렌즈에 닿는 모든 광선은 다시 꺾여 이미지 센서 위의 한 점 _으로_ 모입니다. 카메라 중심과 모든 것에 완벽하게 초점이 맞는 평면 사이의 거리를 _초점 거리(focus distance)_ 라고 부릅니다. 초점 거리는 보통 초점 길이와 같지 않다는 점에 유의하십시오. _초점 길이(focal length)_ 는 카메라 중심과 이미지 평면 사이의 거리입니다. 하지만 우리 모델에서는 이 두 값이 같습니다. 픽셀 격자를 카메라 중심에서 _초점 거리_ 만큼 떨어진 초점 평면 위에 바로 놓을 것이기 때문입니다. 실제 카메라에서 초점 거리는 렌즈와 필름/센서 사이의 거리로 조절됩니다. 그래서 초점을 맞출 대상을 바꾸면 렌즈가 카메라에 대해 상대적으로 움직이는 것을 볼 수 있습니다(휴대폰 카메라에서도 이런 일이 일어날 수 있지만, 그 경우에는 센서가 움직입니다). “조리개”는 렌즈의 실질적인 크기를 조절하는 구멍입니다. 실제 카메라에서는 더 많은 빛이 필요하면 조리개를 키우게 되며, 그러면 초점 거리에서 멀리 떨어진 물체에 블러가 더 많이 생깁니다. 우리의 가상 카메라에서는 완벽한 센서를 가질 수 있어 더 많은 빛이 필요하지 않으므로, 디포커스 블러를 원할 때에만 조리개를 사용합니다. 얇은 렌즈 근사 -------------------------- 실제 카메라에는 복잡한 복합 렌즈가 있습니다. 코드에서는 센서, 렌즈, 조리개의 순서를 시뮬레이션할 수도 있습니다. 그런 다음 광선을 어디로 보낼지 계산하고, 이미지를 계산한 후에 뒤집을 수 있습니다(이미지는 필름에 거꾸로 투영됩니다). 하지만 그래픽스 분야 사람들은 보통 얇은 렌즈 근사를 사용합니다: ![Figure [cam-lens]: 카메라 렌즈 모델](../images/fig-1.21-cam-lens.jpg) 카메라 내부는 전혀 시뮬레이션할 필요가 없습니다 -- 카메라 바깥의 이미지를 렌더링하는 목적에서는 불필요한 복잡성일 뿐입니다. 대신 저는 보통 무한히 얇은 원형 "렌즈"에서 광선을 출발시켜 초점 평면(렌즈에서 `focal_length` 만큼 떨어진 곳) 위의 관심 픽셀을 향해 보냅니다. 3D 세계에서 이 평면 위에 있는 모든 것은 완벽하게 초점이 맞습니다. 실제로는 뷰포트를 이 평면에 배치하여 이를 구현합니다. 모든 것을 종합하면 다음과 같습니다: 1. 초점 평면은 카메라 시선 방향에 직교합니다. 2. 초점 거리는 카메라 중심과 초점 평면 사이의 거리입니다. 3. 뷰포트는 초점 평면 위에 있으며, 카메라 시선 방향 벡터를 중심으로 합니다. 4. 픽셀 위치 격자는 (3D 세계에 위치한) 뷰포트 안에 있습니다. 5. 무작위 이미지 샘플 위치는 현재 픽셀 위치 주변 영역에서 선택됩니다. 6. 카메라는 렌즈 위의 무작위 점에서 현재 이미지 샘플 위치를 지나도록 광선을 발사합니다. ![Figure [cam-film-plane]: 카메라 초점 평면](../images/fig-1.22-cam-film-plane.jpg) 샘플 광선 생성 ----------------------- 디포커스 블러가 없으면 모든 장면 광선은 카메라 중심(또는 `lookfrom`)에서 시작합니다. 디포커스 블러를 구현하기 위해 카메라 중심을 중심으로 하는 원판을 만듭니다. 반지름이 클수록 디포커스 블러도 커집니다. 원래의 카메라는 반지름이 0인 디포커스 원판(블러가 전혀 없음)을 가진 것으로 생각할 수 있으며, 따라서 모든 광선은 원판 중심(`lookfrom`)에서 시작했습니다. 그렇다면 디포커스 원판은 얼마나 커야 하겠습니까? 이 원판의 크기가 디포커스 블러의 정도를 결정하므로, 이는 카메라 클래스의 매개변수가 되어야 합니다. 원판의 반지름을 그대로 카메라 매개변수로 사용할 수도 있지만, 그렇게 하면 블러가 투영 거리에 따라 달라집니다. 조금 더 다루기 쉬운 매개변수는 꼭짓점이 뷰포트 중심에 있고 밑면(디포커스 원판)이 카메라 중심에 있는 원뿔의 각도를 지정하는 것입니다. 이렇게 하면 한 장면에서 초점 거리를 바꿀 때 더 일관된 결과를 얻을 수 있습니다. 디포커스 원판에서 무작위 점을 선택할 것이므로, 이를 수행할 함수가 필요합니다: `random_in_unit_disk()`. 이 함수는 `random_unit_vector()`에서 사용한 것과 같은 종류의 방법으로 동작하며, 다만 2차원에서 동작합니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... inline vec3 unit_vector(const vec3& u) { return v / v.length(); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight inline vec3 random_in_unit_disk() { while (true) { auto p = vec3(random_double(-1,1), random_double(-1,1), 0); if (p.length_squared() < 1) return p; } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [rand-in-unit-disk]: [vec3.h] 단위 원판 내부의 무작위 점 생성] 이제 디포커스 원판에서 광선이 시작되도록 카메라를 수정하겠습니다: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { public: double aspect_ratio = 1.0; // Ratio of image width over height int image_width = 100; // Rendered image width in pixel count int samples_per_pixel = 10; // Count of random samples for each pixel int max_depth = 10; // Maximum number of ray bounces into scene double vfov = 90; // Vertical view angle (field of view) point3 lookfrom = point3(0,0,0); // Point camera is looking from point3 lookat = point3(0,0,-1); // Point camera is looking at vec3 vup = vec3(0,1,0); // Camera-relative "up" direction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double defocus_angle = 0; // Variation angle of rays through each pixel double focus_dist = 10; // Distance from camera lookfrom point to plane of perfect focus ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... private: int image_height; // Rendered image height double pixel_samples_scale; // Color scale factor for a sum of pixel samples point3 center; // Camera center point3 pixel00_loc; // Location of pixel 0, 0 vec3 pixel_delta_u; // Offset to pixel to the right vec3 pixel_delta_v; // Offset to pixel below vec3 u, v, w; // Camera frame basis vectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 defocus_disk_u; // Defocus disk horizontal radius vec3 defocus_disk_v; // Defocus disk vertical radius ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ void initialize() { image_height = int(image_width / aspect_ratio); image_height = (image_height < 1) ? 1 : image_height; pixel_samples_scale = 1.0 / samples_per_pixel; center = lookfrom; // Determine viewport dimensions. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete auto focal_length = (lookfrom - lookat).length(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto theta = degrees_to_radians(vfov); auto h = std::tan(theta/2); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto viewport_height = 2 * h * focus_dist; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto viewport_width = viewport_height * (double(image_width)/image_height); // Calculate the u,v,w unit basis vectors for the camera coordinate frame. w = unit_vector(lookfrom - lookat); u = unit_vector(cross(vup, w)); v = cross(w, u); // Calculate the vectors across the horizontal and down the vertical viewport edges. vec3 viewport_u = viewport_width * u; // Vector across viewport horizontal edge vec3 viewport_v = viewport_height * -v; // Vector down viewport vertical edge // Calculate the horizontal and vertical delta vectors to the next pixel. pixel_delta_u = viewport_u / image_width; pixel_delta_v = viewport_v / image_height; // Calculate the location of the upper left pixel. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto viewport_upper_left = center - (focus_dist * w) - viewport_u/2 - viewport_v/2; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight // Calculate the camera defocus disk basis vectors. auto defocus_radius = focus_dist * std::tan(degrees_to_radians(defocus_angle / 2)); defocus_disk_u = u * defocus_radius; defocus_disk_v = v * defocus_radius; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } ray get_ray(int i, int j) const { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight // Construct a camera ray originating from the defocus disk and directed at a randomly // sampled point around the pixel location i, j. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto offset = sample_square(); auto pixel_sample = pixel00_loc + ((i + offset.x()) * pixel_delta_u) + ((j + offset.y()) * pixel_delta_v); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto ray_direction = pixel_sample - ray_origin; return ray(ray_origin, ray_direction); } vec3 sample_square() const { ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight point3 defocus_disk_sample() const { // Returns a random point in the camera defocus disk. auto p = random_in_unit_disk(); return center + (p[0] * defocus_disk_u) + (p[1] * defocus_disk_v); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ color ray_color(const ray& r, int depth, const hittable& world) const { ... } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-dof]: [camera.h] 피사계 심도를 조절할 수 있는 카메라]
큰 조리개를 사용하는 경우: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { ... camera cam; cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 400; cam.samples_per_pixel = 100; cam.max_depth = 50; cam.vfov = 20; cam.lookfrom = point3(-2,2,1); cam.lookat = point3(0,0,-1); cam.vup = vec3(0,1,0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight cam.defocus_angle = 10.0; cam.focus_dist = 3.4; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-camera-dof]: [main.cc] 피사계 심도가 적용된 장면 카메라]
다음과 같은 결과를 얻습니다 ![Image 22: 피사계 심도가 적용된 구 ](../images/img-1.22-depth-of-field.png class='pixel')
다음은 무엇입니까? ==================================================================================================== 최종 렌더링 --------------- 이 책의 표지에 있는 이미지, 즉 수많은 무작위 구를 만들어 보겠습니다. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { hittable_list world; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto ground_material = make_shared(color(0.5, 0.5, 0.5)); world.add(make_shared(point3(0,-1000,0), 1000, ground_material)); for (int a = -11; a < 11; a++) { for (int b = -11; b < 11; b++) { auto choose_mat = random_double(); point3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double()); if ((center - point3(4, 0.2, 0)).length() > 0.9) { shared_ptr sphere_material; if (choose_mat < 0.8) { // diffuse auto albedo = color::random() * color::random(); sphere_material = make_shared(albedo); world.add(make_shared(center, 0.2, sphere_material)); } else if (choose_mat < 0.95) { // metal auto albedo = color::random(0.5, 1); auto fuzz = random_double(0, 0.5); sphere_material = make_shared(albedo, fuzz); world.add(make_shared(center, 0.2, sphere_material)); } else { // glass sphere_material = make_shared(1.5); world.add(make_shared(center, 0.2, sphere_material)); } } } } auto material1 = make_shared(1.5); world.add(make_shared(point3(0, 1, 0), 1.0, material1)); auto material2 = make_shared(color(0.4, 0.2, 0.1)); world.add(make_shared(point3(-4, 1, 0), 1.0, material2)); auto material3 = make_shared(color(0.7, 0.6, 0.5), 0.0); world.add(make_shared(point3(4, 1, 0), 1.0, material3)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ camera cam; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 1200; cam.samples_per_pixel = 500; cam.max_depth = 50; cam.vfov = 20; cam.lookfrom = point3(13,2,3); cam.lookat = point3(0,0,0); cam.vup = vec3(0,1,0); cam.defocus_angle = 0.6; cam.focus_dist = 10.0; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-final]: [main.cc] 최종 장면] (위 코드는 프로젝트 샘플 코드와 약간 다르다는 점에 유의하십시오. 위에서는 고품질 이미지를 위해 `samples_per_pixel`을 500으로 설정했으며, 이 경우 렌더링에 상당히 오랜 시간이 걸립니다. 프로젝트 소스 코드는 개발 및 검증 과정에서 적절한 실행 시간을 위해 10이라는 값을 사용합니다.)
결과는 다음과 같습니다: ![Image 23: 최종 장면](../images/img-1.23-book1-final.jpg)
흥미로운 점은 유리구에 그림자가 거의 없어서 떠 있는 것처럼 보인다는 것입니다. 이는 버그가 아닙니다 -- 실생활에서 유리구를 볼 일은 많지 않지만, 실제 유리구도 약간 이상하게 보이며 흐린 날에는 정말로 떠 있는 것처럼 보입니다. 유리구 아래에 있는 큰 구 위의 점에는 여전히 많은 빛이 닿는데, 하늘이 가려지는 것이 아니라 재배열되기 때문입니다. 다음 단계 ----------- 이제 멋진 레이 트레이서가 완성되었습니다! 다음은 무엇을 해야 하겠습니까? ### 2권: _레이 트레이싱: 다음 주_ 이 시리즈의 두 번째 책은 여기서 개발한 레이 트레이서를 바탕으로 합니다. 여기에는 다음과 같은 새로운 기능이 포함됩니다: - 모션 블러 -- 움직이는 물체를 사실적으로 렌더링합니다. - 경계 볼륨 계층 구조 -- 복잡한 장면의 렌더링 속도를 높입니다. - 텍스처 맵 -- 물체에 이미지를 입힙니다. - 펄린 노이즈 -- 여러 기법에 매우 유용한 무작위 노이즈 생성기입니다. - 사각형 -- 구 말고도 렌더링할 대상이 생깁니다! 또한 원판, 삼각형, 고리 등 거의 모든 2D 프리미티브를 구현하기 위한 기반이 됩니다. - 조명 -- 장면에 광원을 추가합니다. - 변환 -- 물체를 배치하고 회전하는 데 유용합니다. - 볼륨 렌더링 -- 연기, 구름 및 기타 기체 볼륨을 렌더링합니다. ### 3권: _레이 트레이싱: 남은 인생_ 이 책은 두 번째 책의 내용을 다시 한번 확장합니다. 이 책의 상당 부분은 렌더링된 이미지의 품질과 렌더러의 성능을 모두 개선하는 데 관한 것이며, _올바른_ 광선을 생성하고 이를 적절히 누적하는 데 초점을 맞춥니다. 이 책은 전문가 수준의 레이 트레이서를 작성하는 데 진지하게 관심이 있거나, 표면 하 산란이나 중첩된 유전체 같은 고급 효과를 구현하기 위한 토대에 관심이 있는 독자를 위한 것입니다. ### 다른 방향 여기서부터 나아갈 수 있는 추가적인 방향은 매우 많으며, 여기에는 이 시리즈에서 (아직?) 다루지 않은 기법도 포함됩니다. 여기에는 다음이 포함됩니다: **삼각형** -- 멋진 모델은 대부분 삼각형 형태로 되어 있습니다. 모델 I/O는 가장 골치 아픈 부분이어서, 거의 모든 사람이 다른 사람의 코드를 가져다 이 작업을 처리하려고 합니다. 여기에는 대규모 삼각형 _메시_ 를 효율적으로 처리하는 것도 포함되며, 이는 그 나름의 어려움이 있습니다. **병렬 처리** -- 서로 다른 난수 시드로 $N$개의 코어에서 코드의 복사본 $N$개를 실행합니다. $N$번의 실행 결과를 평균합니다. 이 평균 작업은 계층적으로 수행할 수도 있는데, $N/2$개의 쌍을 평균하여 $N/4$개의 이미지를 얻고, 다시 그 쌍들을 평균할 수 있습니다. 이러한 병렬 처리 방법은 매우 적은 코딩만으로도 수천 개의 코어까지 잘 확장될 것입니다. **그림자 광선** -- 광원을 향해 광선을 쏘면 특정 지점이 어떻게 그림자에 가려지는지 정확히 판단할 수 있습니다. 이를 통해 선명하거나 부드러운 그림자를 렌더링하여 장면에 한층 더 사실감을 더할 수 있습니다. 즐겁게 작업하시고, 멋진 이미지를 만드시면 꼭 보내 주십시오! (insert acknowledgments.md.html here) 이 책 인용하기 ==================================================================================================== 일관된 인용은 이 저작물의 출처, 위치 및 버전을 식별하기 쉽게 해 줍니다. 이 책을 인용하시는 경우, 가능하면 다음 형식 중 하나를 사용해 주시기 바랍니다. 기본 정보 ----------- - **제목(시리즈)**: “주말 동안 배우는 레이 트레이싱 시리즈” - **제목(책)**: “주말 동안 배우는 레이 트레이싱” - **저자**: Peter Shirley, Trevor David Black, Steve Hollasch - **버전/판**: v4.0.2 - **날짜**: 2025-04-25 - **URL(시리즈)**: - **URL(책)**: 스니펫 --------- ### Markdown ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [_Ray Tracing in One Weekend_](https://raytracing.github.io/books/RayTracingInOneWeekend.html) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### HTML ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ray Tracing in One Weekend ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### LaTeX 및 BibTex ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~\cite{Shirley2025RTW1} @misc{Shirley2025RTW1, title = {Ray Tracing in One Weekend}, author = {Peter Shirley, Trevor David Black, Steve Hollasch}, year = {2025}, month = {April}, note = {\small \texttt{https://raytracing.github.io/books/RayTracingInOneWeekend.html}}, url = {https://raytracing.github.io/books/RayTracingInOneWeekend.html} } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### BibLaTeX ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ \usepackage{biblatex} ~\cite{Shirley2025RTW1} @online{Shirley2025RTW1, title = {Ray Tracing in One Weekend}, author = {Peter Shirley, Trevor David Black, Steve Hollasch}, year = {2025}, month = {April}, url = {https://raytracing.github.io/books/RayTracingInOneWeekend.html} } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### IEEE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ “Ray Tracing in One Weekend.” raytracing.github.io/books/RayTracingInOneWeekend.html (accessed MMM. DD, YYYY) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### MLA: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ray Tracing in One Weekend. raytracing.github.io/books/RayTracingInOneWeekend.html Accessed DD MMM. YYYY. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Peter Shirley]: https://github.com/petershirley [Steve Hollasch]: https://github.com/hollasch [Trevor David Black]: https://github.com/trevordblack [discussions]: https://github.com/RayTracing/raytracing.github.io/discussions/ [gfx-codex]: https://graphicscodex.com/ [readme]: ../README.md [releases]: https://github.com/RayTracing/raytracing.github.io/releases/ [repo]: https://github.com/RayTracing/raytracing.github.io/ [square-pixels]: https://www.researchgate.net/publication/244986797 [wiki-further]: https://github.com/RayTracing/raytracing.github.io/wiki/Further-Readings