Post Lists

2018년 12월 20일 목요일

Ray Tracing in One Weekend 번역

https://twitter.com/Peter_shirley/status/984947257035243520

Ray Tracing in One Weekend

Chapter 0 : Overview
나는 몇 년간 많은 그래픽스 수업들을 가르쳐왔다. 종종 나는 레이 트레이싱에서 가르치는데, 왜냐하면 보통 학생들이 모든 코드를 쓰도록 강요되지만, 여전히 어떠한 API도 없이 멋진 이미지를 얻을 수 있기 때문이다. 나는 내 course note를 how-to로 적용시키기로 결정했다. 이것은 너가 가능한 빠르게 빠르게 멋진 프로그램을 얻게 하기 위해서 이다. 그것은 full-featured ray tracer는 아닐 것이지만, 그것은 ray tracing이 영화에서 주로 쓰이게 만드는 indirect lighting을 가지게 된다. 이러한 단계들을 따라라, 그러면 너가 만드는 ray tracer의 아키텍쳐는 좀 더 넓은 ray tracer로 확장되는데 좋을 것이다. 만약 너가 재미있고, 그것을 추구한다면.

누군가 "ray tracing"을 말할 때, 그것은 많은 것을 의미할 수 있다. 내가 설명하려는 것은 기술적으로 path tracer이고, 꽤 일반적인 것이다. 코드는 꽤 간단하지만 (컴퓨터가 일을 하게 하자!), 나는 너가 너가 만들 수 있는 이미지에 행복할 것이라고 생각한다.

나는 내가 그것을 한 순서대로 ray tracer를 작성하도록 할 것인데, 몇 가지 디버깅 팁들도 있다. 끝에 가서, 너는 어떤 훌륭한 이미지를 만들어내는 ray tracer를 가질 것이다. 너는 한 주말안에 이것을 할 수 있을 것이다. 만약 더 오래걸린다면, 걱정하지 말라. 나는 C++를 주도적인 언어로 사용하지만, 너는 그럴 필요가 없다. 그러나, 나는 너가 그렇게 하기를 제안하는데, 그것이 빠르고, 이식가능하고, 많은 production movie와 video game renderer는 C++로 쓰이기 때문이다. 내가 C++의 대부분의 modern features를 피하지만, 상속과 연산자 오버로딩은 ray tracers에 지나치기에 너무 유용하다는 것에 주목해라. 나는 온라인으로 코드를 제공하지 않는다, 그렇지만 그 코드는 진짜이고, 나는 vec3 class에서 간단한 연산자를 제외하고 그것 모두를 보여준다. 나는 코드를 타이핑해서 배우는 것에 대한 신봉자이지만, 코드가 이용가능할 때, 나는 그것을 사용한다. 그래서 난 오직 코드가 이용하지 않을 때, 내가 설파하는 것을 실행한다. 그래서 묻지말아라!

나는 마지막 파트에 내가 한 것을 두었는데, 내가 한 것이 재밌기 때문이다. 몇 몇 독자들은 몇 가지 에러를 가지게 되었는데, 우리가코드를 비교할 때 도움이 되었었다. 그래서 그 코드를 타이핑해보지만, 너가 내 것을 보고 싶다면, 여기에 있다:
https://github.com/petershirley/raytracinginoneweekend

나는 벡터와 친숙하다고 가정한다 (내적과 벡터 합 같은). 만약 너가 그것을 모른다면, 작은 리뷰를 보아라. 만약 너가 그 리뷰가 필요하거나, 처음으로 그것을 배우려고 한다면, 다음의 자료들을 보아라.
https://www.amazon.com/Fundamentals-Computer-Graphics-Fourth-Marschner/dp/1482229390/ref=sr_1_1?ie=UTF8&qid=1453335683&sr=8-1&keywords=peter+shirley
https://www.amazon.com/Computer-Graphics-Principles-Practice-3rd/dp/0321399528/ref=sr_1_1?ie=UTF8&qid=1453335739&sr=8-1&keywords=foley+graphics
http://graphicscodex.com/

만약 너가 문제에 빠지거나, 누군가에게 보여주고 싶은 멋진것을 한다면, 여기로 메일을 보내달라. ptrshrf@gmail.com

나는 더 읽을 것들을 포함한 책과 관련된 사이트를 유지할 것이고, 이 책과 관련된 블로그에서 http://in1weekend.blogspot.com/ 리소스에 대한 링크도.

이제 시작해보자!

Chapter 1: Output an image
렌더러를 시작할 때 마다, 한 이미지를 볼 방법이 필요하다. 가장 간단한 방법은 그것을 파일에 쓰는 것이다. 요점은, 매우 많은 formats들이 있고, 그러한 많은 것들이 복잡하다. 나는 항상 plain text ppm file로 시작한다. 여기에 Wikipedia에서 좋은 설명이 있다:

그러한 것을 만드는 C++ 코드를 만들어보자:


#include <iostream>

int main()
{
 int nx = 200;
 int ny = 100;

 // Colors in ASCII
 // nx Columns, ny Rows
 // 255 for max color
 std::cout << "P3\n" << nx << " " << ny << "\n255\n";

 for (int j = ny - 1; j >= 0; j--)
 {
  for (int i = 0; i < nx; i++)
  {
   float r = float(i) / float(nx);
   float g = float(j) / float(ny);
   float b = 0.2;
   int ir = int(255.99 * r);
   int ig = int(255.99 * g);
   int ib = int(255.99 * b);
   std::cout << ir << " " << ig << " " << ib << '\n';
  }
 }
}

그 코드에서 주의해야 할 몇 가지 것들이 있다:

  1. 픽셀들은 왼쪽에서 오른쪽으로 행으로 쓰여진다.
  2. 그 행들은 위에서 아래로 쓰여진다.
  3. 전통적으로 red/green/blue 컴포넌트들 각각은 0.0 ~ 1.0의 범위를 갖는다. 우리는 그것을 나중에 relax하는데, 우리가 내부적으로 high dynamic range를 쓸 때 이다, 그러나 output 전에 우리는 0 ~ 1 범위로 tone map을 하고, 그래서 이 코드는 변하지 않을 것이다.
  4. Red는 검정색에서 완전한 색으로 왼쪽에서 오른쪽으로 간다. 그리고 초록색은 바탁에서 검정색이고 꼭대기에서 완전한 색이다. Red and green은 함께 노란색을 만들고, 그래서 우리는 upper right corner가 노란색이기를 기대한다.
그 output file을 여는 것은 (나의 mac에서 ToyViewer에서,그러나 그것을 너가 가장 좋아하는 viewer로 시도해보아라, 만약 너의 viewer가 그것을 지원하지 않는다면 "ppm viewer"

를 구글링해라) 다음을 보여준다:

{
나는 이것을 하기 위해서 저자의 추천대로 stb_image.h 라이브러리를 이용하기로 했다.
이 라이브러리를 통해 이미지를 만드는 것은
http://psgraphics.blogspot.com/2015/06/a-small-image-io-library-stbimageh.html
여기에 설명되어 있다.

나는 여기에서


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int main()
{
 const int width = 512;
 const int height = 512;
 float* pixels = new float[width * height * 3];

 int index = 0;
 for (int j = height - 1; j >= 0; --j)
 {
  for (int i = 0; i < width; ++i)
  {
   float r = float(i) / float(width);
   float g = float(j) / float(height);
   float b = 0.2;

   pixels[index++] = r;
   pixels[index++] = g;
   pixels[index++] = b;
  }
 }

 savePNGfile(pixels, width, height, "structTest.png");

 delete[] pixels;
 return 0;
}

template <class T>
inline void savePNGfile(T* arr, int width, int height, const std::string& fileName)
{
 // pmm(p6) -> png convert
 std::ofstream ofs;
 ofs.open("imageTemp/raytemp.ppm", std::ios::out | std::ios::binary);
 ofs << "P6\n" << width << ' ' << height << "\n255\n";
 for (unsigned i = 0; i < width * height * 3; ++i)
 {
  unsigned char n = static_cast<unsigned char>(arr[i] * 255);
  ofs << n;
 }
 ofs.close();

 int x, y, n;
 unsigned char* data = stbi_load("imageTemp/raytemp.ppm", &x, &y, &n, 0);

 std::string destination = "imageResult/" + fileName;
 stbi_write_png(destination.c_str(), x, y, n, data, x * n);
}

 먼저 ppm파일로 저장하고 png파일로 바꾸어서 저장하도록 했다.
stbi image header를 사용해서 png를 사용하는게 viewer가 있어서 더 편하기 때문이다. 용량도 작고. 근데, 그것을 할 때 값을 unsigned char 형태로 바꿔주는게 중요하다.

}


만세! 이것이 그래픽스 "hello world" 이다. 만약 너의 이미지가 저것 처럼 보이지 않는다면, text editor에서  output file 을 열고, 그것이 어떻게 생겼는지를 보아라. 그것은 이것처럼 시작해야 한다:

만약 그렇지 않다면, 그러면 너는 아마도 어떤 newlines or 이미지 reader를 헷갈리게 하는 어떤 것을 가지고 있다.

만약 너가 PPM보다 더 많은 이미지 타입들을 만들고 싶다면, 나는 github에서 이용가능한 stb_image.h의 팬이다.

Chapter 2 : The vec3 class
거의 모든 그래픽스 프로그램들은 기하 벡터와 컬러들을 저장하기 위한 어떤 클래스들을 갖는다. 많은 시스템에서, 이러한 벡터들은 4D이다 (3D와 geometry를 위한 homogeneous coordinate를 더해서, 그리고 RGB와 colors를 위한 alpha transparency channel를 더해서). 우리의 목적을 위해서, 세 개의 좌표가 충분하다. 우리는 colors, locations, directions, offsets, 무엇이든지를 위해 같은 클래스 vec3를 사용할 것이다. 어떤 사람들은 이것을 좋아하지 않는다. 왜냐하면 그것은 너가 어떤 어리석은 것을 하게하는 것을 막지 않기 때문이다. color에 location을 더하는 것처럼. 그것은 좋은 요점이지만, 우리는 명백히 틀릴 때 "less code" route를 갈 것이다.

여기 나의 vec3 class의 to part이다:

vec3 챕터는 스킵

Chapter 3: Rays, a simple camera, and background
모든 ray tracers가 가지고 있는 한 가지 것은 ray class이다. 그리고 그것은 한 ray를 따라 무슨 컬러가 보이는지에 대한 연산이다. 한 ray가 함수 p(t) = A + t*B라고 생각하자. 여기에서 p는 3D에서 line을 따라 있는 한 3D position이다. A는 ray origin이고, B는 ray direction이다. 그 ray parameter t는 실수이다 (code에서는 float). 다른 t를 넣어라 그러면 p(t)는 그 ray를 따라서 움직인다. 음수 t를 넣어라. 그러면 너는 3D line에서 어디든지 갈 수 있다. 양수 t에 대해, 너는 A 앞에 있는 부분들을 얻고, 이것이 half-line or ray라고 불려지는 것이다. C = p(2)의 예제가 여기에서 보여진다.

좀 더 말이 긴 코드 형태에서 p(t)는 "point_at_parameter(t)"를 호출한다:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma once
#ifndef __RAY_H__
#define __RAY_H__

#include <glm/glm.hpp>

class ray
{
public:
 ray() { }
 ray(const glm::vec3& a, const glm::vec3& b)
  : A(a), B(b)
 { }
 glm::vec3 origin() const { return A; }
 glm::vec3 direction() const { return B; }
 glm::vec3 point_at_parameter(float t) const { return A + t * B; }

 glm::vec3 A;
 glm::vec3 B;
};

#endif

이제 우리는 모퉁이를 돌아서 ray tracer를 만들 준비가 되었다. ray tracer의 핵심은 픽셀을 통해 rays를 보내고, 그러한 rays의 방향으로 어떤 컬러가 보이는지를 연산하는 것이다. 이것은 eye에서 pixel로 가는 어떤 ray를 계산하고, 그 ray가 교차하는지를 연산하고, 그 교차점의 컬러를 연산하는 형태이다. 처음에 ray tracer를 개발할 때, 나는 항상 코드가 되게 하기 위해서 간단한 카메라를 한다. 나는 또한 background의 컬러를 반환하는 (simple gradient) simple color(ray) function을 만든다.

나는 종종 디버깅을 위해서 square images를 사용하는 문제에 겪었는데, 왜냐하면 나는 x와 y를 너무 종종 transpose하기 때문이다. 그래서 나는 200x100 iamge를 고수할 것이다. 나는 "eye"를 (0, 0, 0)에 놓을 것이다 (또는 카메라 center 만약 너가 한 카메라를 생각한다면). 나는 y-axis를 위로 향하게 할 것이고, x축은 오른쪽이다. 오른손 좌표계의 전통을 존중하기 위해서, screen쪽으로 음의 z-axis이다. 나는 lower left hand corner에서 screen을 가로질러 ray endpoint를 움직이기 위해 screen sides을 가로지르고, 그 스크린을 따라서 두 개의 offset vectors를 사용할 것이다. 내가 ray direction을 unit length vector로 만들지 않은 것에 주목해라. 왜냐하면 나는 그렇게 하지 않는 것이 더 간단하고 다소 더 빠른 코드를 만든다고 생각하기 때문이다.

아래의 코드에서, 그 ray r은 대략 pixel centers로 간다 (나는 정확성에 대해서 걱정하지 않을 것이다. 지금은. 왜냐하면 우리는 나중에 antialiasing을 더할 것이기 때문이다):


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
glm::vec3 color(const ray& r)
{
 glm::vec3 unit_direction = glm::normalize(r.direction());
 float t = 0.5f * (unit_direction.y + 1.0);
 return (1.f - t) * glm::vec3(1.0) + t * glm::vec3(0.5, 0.7, 1.0);
}

int main()
{
 const int nx = 200;
 const int ny = 100;
 float* pixels = new float[nx * ny * 3];

 glm::vec3 lower_left_corner(-2, -1, -1);
 glm::vec3 horizontal(4.0, 0.0, 0.0);
 glm::vec3 vertical(0.0, 2.0, 0.0);
 glm::vec3 origin(0.0);

 int index = 0;
 for (int j = ny - 1; j >= 0; --j)
 {
  for (int i = 0; i < nx; ++i)
  {
   float u = float(i) / float(nx);
   float v = float(j) / float(ny);
   ray r(origin, lower_left_corner + u * horizontal + v * vertical);
   glm::vec3 col = color(r);

   pixels[index++] = col.r;
   pixels[index++] = col.g;
   pixels[index++] = col.b;
  }
 }

 savePNGfile(pixels, nx, ny, "rayTracerPrac.png");

 delete[] pixels;
 return 0;

그 color(ray) 함수는 선형으로 white and blue를 blends한다, y 좌표의 up/downess에 따라서. 나는 처음에 그것을 단위 벡터로 만든다. 그래서 그것은 -1.0 < y < 1.0이다. 나는 그러고나서 표준의 그래픽스 트릭을 썼는데, 그것을 0.0 < t < 1.0으로 스케일링한다. t = 1.0일 때, 나는 blue를 원한다. t = 0.0일 떄 나는 white를 원한다. 그 사이에서, 나는 blend를 원한다. 이것은 "linear blend", or "linear interpolation", or 짧게 해서 "lerp"를 형성한다. 두 개의 것들 사이에서. lerp는 항상 다음의 형태이다 : blended_value = (1-t) * start_value + t * end_value, with t가 0에서 1까지). 우리의 경우에 이것을 만든다:



Chapter 4 : Adding a sphere
단일 오브젝트를 우리의 ray tracer에 넣자. 사람들은 종종 ray tracers에 spheres를 사용하느데, 왜냐하면 ray가 sphere에 닿는지를 계산하는 것이 꽤 간단하기 때문이다. 반지름이 R이고 원점에 중심을 둔 sphere에 대한 방정식이 x*x + y* y + z* z = R*R이라는 것을 기억해라. 너가 그 방정식을 읽을 수 있는 방법은 "어떤 (x,y,z)에 대해, x*x + y* y + z* z = R*R이면, 그러면 (x,y,z)는 sphere 에 있고 그렇지 않으면, 그렇지 않다. 만약 sphere가 (cx,cy,cz)에 있다면 그것은 좀 더 못생겨지는데:

(x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz) = R * R;

그래픽스에ㅓㅅ, 너는 항상 너의 공식들이 벡터들의 관점에 있기를 원한다. 그래서 모든 x/y/z stuff가 vec3 class아래에 있는다. 너는 center C = (cx,cy,cz)에서 점 p = (x, y, z)로 가는 벡터가 (p - C)라는 것을 주목할지도 모른다. 그리고 dot(p - C, p - C)는 = (x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz) 이다. 그래서 vector form에서 구의 방정식은:

dot(p - c), (p -c)) = R * R이다.

우리는 이것은 이 방정식을 만족하는 어떤 점 p가 sphere에 있다"라고 읽을 수 있다. 우리는 우리의 ray p(t) = A + t * B가 그 sphere의 어딘가에 부딪치는지를 알길 원한다. 만약 그것이 sphere와 부딪친다면, p(t)가 그 sphere equation을 만족하는 어떤 t가 있다. 그래서 우리는 이것이 true인 어떤 t를 찾을 것이다:

dot((p(t) - c), p(t) - c)) = R * R;

또는 그 ray p(t)의 완전한 형태를 확장해서:

dot((A + t * B - C), (A + t * B - C)) = R * R

벡터 대수의 규칙은 우리가 여기에서 원하는 모든 것이다. 그래서 만약 우리가 그 방정식을 풀어서 모든 항을 왼쪽으로 옮기면 우리는 다음을 얻는다

t * t dot(B, B) + 2 * t dot(B, A - C) + dot(A - C, A - C) - R * R = 0

그 방정식에서 벡터들과 R은 모두 상수이고 알려져 있다. 미지수는 t이다. 그리고 그 방정식은 2차방정식이다. 마치 너가 고등학교 수학 수업에서 보았던 것 처럼. 너는 t에 대해 풀 수 있고, 양수인 square root part가 있다 (두 개의 실수근을 의미하는), 음수는 (어떠한 실수근이 없다) 또는 zero (한 개의 실수근을 의미한다). 그래픽스에서, 그 대수는 거의 항상 geometry와 직접 연결된다. 우리가 가진 것은:

만약 우리가 그 math를 취하고 그것을 우리의 프로그램에 하드코딩한다면, 우리는 그것이 z축의 -1에 위치한 작은 sphere를 부딪친다면 그 픽셀을 빨갛게 칠하여 테스트할 수 있다:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
bool hit_sphere(const glm::vec3& center, float radius, const ray& r)
{
 glm::vec3 m = r.origin() - center;
 float b = glm::dot(m, glm::normalize(r.direction()));
 float c = glm::dot(m, m) - radius * radius;
 if (c > 0.0 && b > 0.0) return false;
 float discr = b * b - c;
 if (discr < 0.0) return false;
 
 return true;
}

glm::vec3 color(const ray& r)
{
 if (hit_sphere(glm::vec3(0, 0, -1), 0.5, r))
  return glm::vec3(1.0, 0.0, 0.0);

 glm::vec3 unit_direction = glm::normalize(r.direction());
 float t = 0.5f * (unit_direction.y + 1.0);
 return (1.f - t) * glm::vec3(1.0) + t * glm::vec3(0.5, 0.7, 1.0);
}

{
나는 hit_sphere 코드를 바꾸었다. 저렇게 하면 더 빠르다.
}

우리가 얻는 것은 이것이다:

이제 이것은 모든 종류의 것들이 부족하다 - shading and reflection rays and 한 개 이상의 오브젝트들 - 그러나 우리는 우리의 시작보다 반쯤 더 가까이 왔다. 인지해야할 한 가지 것은 우리가 ray가 sphere랑 부딪히는지를 테스트했다는 것인데, t < 0 솔루션들도 잘 작동한다. 만약 너가 너의 sphere center를 z = +1로 바꾼다면, 너는 정확히 같은 사진을 얻을 것인데 왜냐하면 너의 뒤에 있는 것들을 보고있기 때문이다. 이것은 feature가 아니다! 우리는 그러한 문제를 다음에 고칠것이다.

Chapter 5: Surface normals and multiple objects
처음에, surface normal를 얻어보자. 그래야 우리가 색을 칠할 수 있다. 이것은 표면에 수직인 벡터이고, 전통적으로 바깥을 가리킨다. 한 가지 디자인 결정은 이러한 normals들이 (다시 전통적으로) unit length이냐는 것이다. shading에 관해서 그것이 편리하다. 그래서 나는 yes라고 말할 것이지만, 나는 코드에서 그것을 강요하지 않을 것이다. 이것은 미묘한 버그를 허용할 수 있다. 그래서 이것이 개인적인 선호라고 인지해라. 대부분의 디자인 결정들이 그렇듯이. sphere에 대해서, normals는 hitpoint - center의 방향이다:

지구에서, 이것은 지구의 중심에서 너를 향하는 벡터가 바로 뚫고 나오는 것을 암시한다. 그것을 코드에 이제 넣어보고 그것을 칠해보자. 우리는 아직 어떤 lights도 가지고 있지않고 어떤 것도 가지고 있지 않다. 그래서, 그 normals를 color map을 가지고 시각화 해보자. normals를 시각화하는데 사용되는 common trick은 (왜냐하면 그것은 쉽고 어느정도 N이 단위 길이 벡터라고 가정하는 것이 직관적이기 때문에 - 그래서 각 컴포넌트는 -1 ~ 1 이다) 각 컴포넌트를 0 ~ 1의 구간으로 매핑하는 것이다. 그러고나서 x/y/z를 r/g/b로 매핑한다. 그 normal에 대해 우리는 hit point가 필요하다. 우리가 hit or not이 아니라. 그 가장 가까운 점 (smallest t)라고 가정하자. 코드에서 이러한 변화들은 우리가 N을 연산하고 시각화하도록 한다:

{
나의 코드 및 결과물


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
float hit_sphere(const glm::vec3& center, float radius, const ray& r)
{
 glm::vec3 m = r.origin() - center;
 float b = glm::dot(m, glm::normalize(r.direction()));
 float c = glm::dot(m, m) - radius * radius;
 if (c > 0.0 && b > 0.0) return -0.1;
 float discr = b * b - c;
 if (discr < 0.0) return -0.1;
 float t = -b - sqrtf(discr);
 
 // If t is negative, ray started inside sphere so clamp t to zero
 if (t < 0.0) t = 0.0;

 return t;
}

glm::vec3 color(const ray& r)
{
 float t = hit_sphere(glm::vec3(0, 0, -1), 0.5, r);
 if (t > 0.0)
 {
  glm::vec3 N = glm::normalize(r.point_at_parameter(t) - glm::vec3(0, 0, -1));
  return 0.5f * glm::vec3(N.x + 1, N.y + 1, N.z + 1);
 }

 glm::vec3 unit_direction = glm::normalize(r.direction());
 t = 0.5f * (unit_direction.y + 1.0);
 return (1.f - t) * glm::vec3(1.0) + t * glm::vec3(0.5, 0.7, 1.0);
}


}

이제 여러개의 spheres들은 어떤가? spheres들의 배열을 가지는 것이 유혹적일지라도, 매우 깔끔한 솔루션은 ray가 부딪힐지도 모르는 어떤 것에 대한 "추상 클래스"를 만드는 것이고, sphere 그리고 spheres들의 리스트를 너가 부딪힐 수 있는 것으로 만드는 것이다. 그 클래스가 불려져야 하는 것은 quandry의 어떤 것이다 - 그것을 "object"라고 부르는 것은 좋을 것이다, 만약 "object oriented" 프로그래밍을 위한 것이 아니라면. "Surface"는 종종 사용되는데, 우리가 volumes을 원한다는 약점이 있다. "Hitable"은 그것들을 통합하는 멤버 함수를 강조한다. 나는 이러한 어떤 것도 좋아하지 않지만, 나는 "hitable"로 갈 것이다.

이 hitable 추상 클래스는 ray를 취하는 hit function을 가질 것이다. 대부분의 ray tracers들은 hits의 tmin에서 tmax 사이의 valid interval를 추가하는 것이 편리하다는 것을 알았다. 그래서 그 hit은 오직 tmin < t < tmax 일 때에만 중요하다.  초기 rays에 대해 이것은 양수 t이지만, 우리가 보게 되듯이, 그것은 코드에서 tmin ~ tmax의 구간을 갖는 것이 어떤 세부사항을 도울 수 있다. 한 가지 디자인 질문은 만약 우리가 어떤 것과 부딪혔을 때, normal을 계산하는 것 같읕 것들을 하는 것이냐는 것이다; 우리는 우리의 search를 함에 따라 어떤 것과 가깝게 부딪히고 끝날 것이고, 우리는 그 가장 가까운 것과의 normal를 필요할 것이다. 나는 간단한 솔루션으로 할 것이고, 내가 어떤 structure에  저장할 stuff의 bundle을 연산할 것이다. 나는 우리가 어떤 시점에서 motion blur를 원할 것이라는 것을 안다. 그래서 나는 시간 input 변수를 추가할 것이다. 여기에 abstract class가 있다:

그리고 여기에 sphere가 있다 (내가 서로를 상쇄시키는 중복하는 2들의 bunch를 제거했다):

그리고 오브젝트들의 한 리스트:

그리고 새로운 main:

이것은 spheres가 그것들의 suirface normal를 따라 있는 곳에서 시각화를 하는 picture를 만든다. 이것은 종종 너의 모델에서 flaws나 특징을 보느 훌륭한 방법이다.


Chapter 6 : Antialiasing
실제 카메라가 사진을 찍을 때, 보통 edges를 따라서 jaggies(들쭉날쭉함)가 없다. 왜냐하면 edge pixels들이 어떤 foreground와 background와 blend 된 것이기 때문이다. 우리는 각 픽셀 안에서 samples들의 bunch를 평균화하여 같은 효과를 얻을 수 있다. 우리는 stratification(계층화)로 시달리지 않을 것인데, 그 계층화는 논란의 여지가 있지만, 나의 프로그램에서는 보통이다. 어떤 레이 트레이서에 대해서, 그것은 중요하지만, 우리가 쓰려는 일반적인 것의 종류는 그것으로 부터 많은 이익을 얻지 않고, 코드를 더 못생기게 만든다. 우리는 카메라 클래스를 조금 추상화하고, 그래서 우리는 더 멋진 카메라를 나중에 만들 수 있다.

우리가 필요한 한 가지 것은 실수 숫자들을 반환하는 random number generator이다. C++은 전통적으로 standard random number generator가 없지만, 대부분의 시스템들은 drand48()을 어느곳에 밀어넣어놓았고, 그것이 내가 여기에서 사용하려는 것이다. 그러나, C++의 더 새로운 버전들은 <random> 헤더로 이 문제를 다루었다 (만약 어떤 전문가에 따라서 불완전하다면). 너의 구조가 무엇이든, 전통적으로 0 <= ran < 1의 랜덤 실수를 반환하는 canonical(고전적인) 랜덤 넘버를 반환하는 함수를 찾아라. 1보다 미마은 우리가 가끔 그것을 이용하는 것에 있어서 중요하다.

주어진 한 픽셀에 대해, 우리는 그 픽셀 안에 몇 가지 샘플들을 가지고, 그 샘플의 각각에 rays를 보낸다. 이러한 rays들의 컬러는 그러고나서 평균화된다:

그것을 모두 함께 넣는 것은 이전으로 부터 우리의 간단한 axis-aligned camera를 캡슐화한 카메라 클래를 만든다:

main 또한 바뀐다:

{


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#define CHANNEL_NUM 3
int main()
{
 const int nx = 200;
 const int ny = 100;
 const int ns = 100;
 float* pixels = new float[nx * ny * CHANNEL_NUM];

 rcHitable* list[2];
 list[0] = new rcSphere(glm::vec3(0, 0, -1), 0.5);
 list[1] = new rcSphere(glm::vec3(0, -100.5, -1), 100);
 (*(*list));
 (*(list + 1));
 rcHitable* world = new rcHitable_List(list, 2);

 rcCamera cam;
 std::mt19937 gen(123456);
 std::uniform_real_distribution<float> distrFloat;
 auto randFloat = std::bind(distrFloat, gen);

 int index = 0;
 for (int j = ny - 1; j >= 0; --j)
 {
  for (int i = 0; i < nx; ++i)
  {
   glm::vec3 col(0);
   for (int s = 0; s < ns; ++s)
   {
    float u = float(i + randFloat()) / float(nx);
    float v = float(j + randFloat()) / float(ny);
    ray r = cam.get_ray(u, v);
    col += color(r, world);
   }
   
   col /= float(ns);

   pixels[index++] = col.r;
   pixels[index++] = col.g;
   pixels[index++] = col.b;
  }
 }


 savePNGfile(pixels, nx, ny, "AArayTraceAgain.png");

 delete[] pixels;
 delete list[0];
 delete list[1];
 delete world;
 return 0;
}


- AA 적용안함

- AA 적용

확실히 차이가 난다. 간단한 AA였다.

}

만들어진 이미지를 zoom in하여, 그 변화는 배경과 일부 foreground에 있는 edge pixels들에 있다:

Chapter 7: Diffuse Materials
우리가 오브젝트들과 다양한 픽셀당 rays를 가졌으니, 우리는 어떤 현실적으로 보이는 materials를 만들 수 있다. 우리는 diffuse (matte) materials로 시작할 것이다. 한 가지 질문은 우리가 shapes와 materials를 섞고 match할 수 있거나 (그래서 우리는 한 sphere에 material를 할당할 수 잇다) 또는 만약 그것이 합쳐진다면, 그래서 그 geometry와 material은 tightly bound된다 (그것은 procedural objects에 유용할 수 있다. 거기에서 geometry와 material은 연결된다). 우리는 벼랙로 갈 것이다 - 그리고 이것은 대부분의 렌더러에서 보통이다 -- 그러나 그 한계를 인지해라.

light를 emit하지 않는 Diffuse objects은 단순히 그것들의 주변의 color를 취하지만, 그것들은 그것들 자신의 intrinsic color로 그것을 조절한다. diffuse surface를 reflects off하는 Light는 그것 자신의 방향을 랜덤하게 한다. 그래서, 만약 우리가 두 개의 diffuse surfaces 사이에 있는 한 crack에 세 개의 rays를 보낸다면, 그것들은 가각 다른 random behavior를 가질 것이다:

그것들은 또한 반사되기보다는 흡수될지도 모른다. 그 표면이 어두울수록, 흡수는 더 일어날 가능성이 높다. (그게 어두운 이유이다!) 방향을 랜덤화하는 어떤 알고리즘은 matte(무광의)하게 보이는 표면을 만들것이다. 이것을 하는 가장 간단한 방법들 중 하나는 이상적인 diffuse surfaces에 대해 옳게 하는 것으로 밝혀졌다 (나는 그것을 수학적으로 이상적인 Lambertian을 근사하는 lazy hack으로서 하곤했다.)

hitpoint에 접하는 unit radius sphere로부터 random point s를 골라라, 그리고 그 hitpoint p로 부터 그 random point s로 가는 ray를 보내라. 그 sphere는 center p + N을 가진다:

우리는 또한 origin에 중심을 두는 unit radius sphere에서 random point를 선택할 한 방법이 필요하다. 우리는 보통 가장 간단한 알고리즘인 것을 사용할 것이다: rejection method. 처음에, 우리는 x,y,z가 모두 -1 ~  + 1인 unit cube에서 random point를 고른다. 우리는 이 점을 거절하고 다시 시도한다, 만약 그 점이 sphere 밖이라면. do/while construct는 그것에 완벽하다:



댓글 없음:

댓글 쓰기