[42cursurs]Ray Tracing in One Weekend 8-Diffuse Materials

이상헌·2020년 10월 17일
0

이번엔 물체의 재질에 따라 빛의 확산 및 반사 등을 적용해 물체를 더 사실적으로 표현하는 작업을 한다. 특히 복잡한 이론이 많은 부분이라 그냥 그런 원리를 적용했구나하고 넘어가도 좋다.

A Simple Diffuse Material

이번 주제에 구현할 Diffuse Materials은 matte라는 재질로 표면이 거칠어 금속처럼 광택이 없고 고유의 색상을 가진다. Diffuse Materials은 표면에서 무작위로 난반사가 일어난다. Diffuse Materials은 이렇게 난반사된 지점의 색상은 주변의 물체들의 영향을 받는다. 반사될 때마다 광선이 재질의 영향을 받기 때문이다.

Figure 8: Light ray bounces

지금부터 물체 표면에 대해 광선을 무작위로 난반사 시킬 지에 대해 알아보자. 이 방법이 단순히 무작위로 난반사 시킬 방법 중 하나라는 것을 기억하자.

아래의 그림에서 보는 것처럼 광선과 물체의 교차점 P에 대해 접하는 두 개의 길이가 1인 단위 구를 상상할 수 있다. 이 가상의 단위 구 중 표면에 대해 광원이 있는 측면의 단위 구를 선택해서 P에서 단위 구 내부의 무작위 지점 S에 이르는 방향으로 P에서 반사된 광선을 보낸다.

Figure 9: Generating a random diffuse bounce ray

여기서 무작위 지점 S를 선택하는 가장 쉬운 rejection method라는 알고리즘을 사용한다.
1. random한 S 좌표를 -1 < x,y,z < 1 범위에서 생성한다.
2. 생성한 좌표가 단위 구 안에 있는 지 확인한다.
3. 단위 구 내부에 있다면 좌표를 반환하고 아니면 1.로 돌아간다.
(p.s -1 < x,y,z < 1 범위는 길이 1인 정육면체 이고 단위 구를 포함할 수 있을 만큼 충분히 크다.)

vec3.h

class vec3 {
  public:
    ...
    inline static vec3 random() {
        return vec3(random_double(), random_double(), random_double());
    }

    inline static vec3 random(double min, double max) {
    // 매개변수로 받은 최솟값과 최댓값 범위 내의 3차원 좌표 반환
        return vec3(random_double(min,max), random_double(min,max), random_double(min,max));
    }
vec3 random_in_unit_sphere() {
// 단위 구 내부의 무작위 지점 생성
    while (true) {
    // 무작위 지점을 뽑는다.
        auto p = vec3::random(-1,1);
    // 단위 구 내부의 좌표인지 판단
        if (p.length_squared() >= 1) continue;
        return p;
    }
}

위에서 S를 구하는 것에 성공했다. 이제 교차점에 칠할 색상을 난반사에 따라 조절한다. 위에서 언급했듯 교차점의 색상이 주변 사물에 영향을 받는 효과를 표현하는 것이다. 원리만 이해하면 다음으로 넘아가자. 다음 파트에서 재귀함수를 완성한다.

color ray_color(const ray& r, const hittable& world) {
    hit_record rec;

    if (world.hit(r, 0, infinity, rec)) {
    // 광선을 추적하다가 물체와 충돌하면 이 다음은 난반사된 광선을 추적한다.
        point3 target = rec.p + rec.normal + random_in_unit_sphere();
        // 난반사된 광선이 주변에 다른 물체에 영향을 받는지 알기 위해 다시 raycolor를 써서 추적한다.
        return 0.5 * ray_color(ray(rec.p, target - rec.p), world);
    }
// 광선이 사물에 충돌하지 않는다면 영향을 받지 않는다고 판단해 풍경의 색상을 반환한다. 
// 만약 광선이 이전에 충돌한 사물이 있다면, 색상은 그만큼 영향을 받은 값으로 반환된다.
    vec3 unit_direction = unit_vector(r.direction());
    auto t = 0.5*(unit_direction.y() + 1.0);
    return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}

Limiting the Number of Child Rays

위에서 작성한 ray_color의 재귀함수가 너무 많이 발생하지 않도록 depth로 제한을 준다.

main.cc

color ray_color(const ray& r, const hittable& world, int depth) {
    hit_record rec;

    // If we've exceeded the ray bounce limit, no more light is gathered.
    // 너무 많은 반사가 일어났다고 판단되면 검은색으로 치해버린다.
    if (depth <= 0)
        return color(0,0,0);

    if (world.hit(r, 0, infinity, rec)) {
        point3 target = rec.p + rec.normal + random_in_unit_sphere();
        return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
    }

    vec3 unit_direction = unit_vector(r.direction());
    auto t = 0.5*(unit_direction.y() + 1.0);
    return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
...

int main() {

    // Image

    const auto aspect_ratio = 16.0 / 9.0;
    const int image_width = 400;
    const int image_height = static_cast<int>(image_width / aspect_ratio);
    const int samples_per_pixel = 100;
    const int max_depth = 50;
    ...

    // Render

    std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";

    for (int j = image_height-1; j >= 0; --j) {
        std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
        for (int i = 0; i < image_width; ++i) {
            color pixel_color(0, 0, 0);
            for (int s = 0; s < samples_per_pixel; ++s) {
                auto u = (i + random_double()) / (image_width-1);
                auto v = (j + random_double()) / (image_height-1);
                ray r = cam.get_ray(u, v);
                pixel_color += ray_color(r, world, max_depth);
            }
            write_color(std::cout, pixel_color, samples_per_pixel);
        }
    }

    std::cerr << "\nDone.\n";
}

아래는 결과 이미지다.

Figure 7: First render of a diffuse sphere

Using Gamma Correction for Accurate Color Intensity

솔직히 원리는 모르겠다. 그냥 감마 보정이라는 효과로 어두웠던 이미지가 밝아진다는 것만 알고 넘어가련다.

color.h

void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) {
    auto r = pixel_color.x();
    auto g = pixel_color.y();
    auto b = pixel_color.z();

    // Divide the color by the number of samples and gamma-correct for gamma=2.0.
    auto scale = 1.0 / samples_per_pixel;
    r = sqrt(scale * r);
    g = sqrt(scale * g);
    b = sqrt(scale * b);

    // Write the translated [0,255] value of each color component.
    out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
        << static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
        << static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}
Image 8: Diffuse sphere, with gamma correction

Fixing Shadow Acne

t값이 0 혹은 0에 가까운 작은 값일 때 Shadow Acne이라는 오류가 일어날 수 있다. t값이 0에 가까울 때 물체 표면에 주변 사물의 값이 크게 영향을 미쳐 일어나는 현상인 듯 하다.(참고: https://digitalrune.github.io/DigitalRune-Documentation/html/3f4d959e-9c98-4a97-8d85-7a73c26145d7.htm)

이 문제를 수정하기 위해 아래처럼 코드를 수정하자.

main.cc

// t값이 너무 작지 않게 제한한다.
if (world.hit(r, 0.001, infinity, rec)) {

True Lambertian Reflection

확률 분포가 어쩌고 한다는데 모르겠다. 그냥 저렇게 코드를 수정해주면 좀 더 균일하게 빛을 산란 시킬 수 있다고 한다.

vec3.h

vec3 random_unit_vector() {
    auto a = random_double(0, 2*pi);
    auto z = random_double(-1, 1);
    auto r = sqrt(1 - z*z);
    return vec3(r*cos(a), r*sin(a), z);
}

main.cc

color ray_color(const ray& r, const hittable& world, int depth) {
    hit_record rec;

    // If we've exceeded the ray bounce limit, no more light is gathered.
    if (depth <= 0)
        return color(0,0,0);

    if (world.hit(r, 0.001, infinity, rec)) {
        point3 target = rec.p + rec.normal + random_unit_vector();
        return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
    }

    vec3 unit_direction = unit_vector(r.direction());
    auto t = 0.5*(unit_direction.y() + 1.0);
    return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
Image 9: Correct rendering of Lambertian spheres True Lambertian Reflection의 효과를 아래와 같이 정리한다.
  1. 그림자가 흐릿해진다.
  2. 더 밝아졌다.

An Alternative Diffuse Formulation

Lambertian을 대체하는 다른 방법에 대해 논한다. 근데 Lambertian를 이해하지 못했으므로 왜 잘못됬는 지 대체해야하는 지도 잘 모르겠다. 일단 그러려니 넘어간다. 코드만 보자.

vec3.h

vec3 random_in_hemisphere(const vec3& normal) {
    vec3 in_unit_sphere = random_in_unit_sphere();
    if (dot(in_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal
        return in_unit_sphere;
    else
        return -in_unit_sphere;
}

main.cc

color ray_color(const ray& r, const hittable& world, int depth) {
    hit_record rec;

    // If we've exceeded the ray bounce limit, no more light is gathered.
    if (depth <= 0)
        return color(0,0,0);

    if (world.hit(r, 0.001, infinity, rec)) {
        point3 target = rec.p + random_in_hemisphere(rec.normal);
        return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
    }

    vec3 unit_direction = unit_vector(r.direction());
    auto t = 0.5*(unit_direction.y() + 1.0);
    return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
Image 10: Rendering of diffuse spheres with hemispherical scattering
profile
배고픈 개발자 sayi입니다!

0개의 댓글