[RT in One Weekend Series 8] Diffuse Materials (번역)

목차: Series 1: Index and Overview (Link)
이전 글: Series 7: Anti-aliasing (Link)
다음 글: Series 9: Metal (Link)

MK: 이 장을 여러 번 읽었지만 이해를 정확하게 하지 못한 부분이 몇 군데 존재한다. 추가로 Diffuse (Matte) Material은 무광택 물체를 의미하는 것 같다. 이번 글에서 Diffuse (Matte) Material을 무광택 물체(Object)로 작성하였다. 추가로 출처 2에서 Diffuse Lighting이란 개념을 소개한다. Diffuse Material과 Diffuse Lighting의 개념이 동일한지는 모르겠으나 결과적으로 Object(물체)의 Surface 색상을 결정하는 방법에 대한 설명인 것은 동일해 보인다.

그림 1: Ray가 반사되는 방향

이전 글에서 한 픽셀에 여러 개의 Ray를 보내는 방법(Anti-Aliasing)과 여러 개의 물체를 보여주기 위한 코드를 작성하였다. 이번 글에서는 Object에 Diffuse 색상을 적용하여서 조금 더 멋진 그림을 만들어 볼 차례이다. 이를 구현하기 위해서 색상을 결정하는 부분과 Geometry(위치)를 결정하는 부분을 따로 구현하는 방법을 사용한다. Geometry(위치) & 색상을 결정하는 부분을 같이 구현해도 무방하다고 한다. Light(빛)가 Diffuse Object의 Surface와 부딪히게 되면 Random으로 빛이 반사되는 경향이 있다. 만약 2개의 Diffuse Object 사이에서 빛이 반사되면 그림 1과 같이 모두 다른 방향으로 Random 하게 반사된다.

또한, 특정 빛들은 반사되지 않고 흡수되는 경우도 있다. Object가 Ray를 많이 흡수할수록 검은색에 가까워진다. 결과적으로 Ray가 Object와 부딪히는 순간 Ray를 Random 방향으로 날려주는 코드만 작성하면 무광택 Surface를 가진 Object를 만들어 낼 수 있다.

그림 2: Random Ray 생성 방법

그림 2는 이글에서 Ray를 Random으로 생성하는 방법을 보여주는 그림이다 (솔직히 이 부분이 제일 이해가 안 되는 부분임). 먼저 Ray가 Surface에 도달하는 부분을 그림 2의 P 위치라고 가정한다. 표면 P에서 Normal Vector 값을 더한 위치를 N으로 표시하였다. N 위치에서 Sphere를 하나 그린다. N 위치를 기준으로 반지름이 1인 구를 하나 더 그린다 (아마 Normal Vector가 Unit Vector이기 때문에 1인 구를 그리면 Hit Point(P)를 지나게 된다). 해당 구 안에서 Random 위치를 선택하여서 반사된 위치로 Ray를 보낸다. 그림 2의 S로 표시된 부분이 최종적으로 Ray가 반사되는 위치를 의미한다.

MK: 아마도 위와 방법은 Random Ray를 생성하는 여러개의 방법 중 하나인 것 같다. 약간 궁금한 부분은 S로 반사되는 Ray의 방향 값을 Unit Vector로 변경해야 하는게 아닌가 하는 부분이다. Ray의 이동 위치를 A+B*t로 표현을 하고 B값이 Unit Vector로 표현해야 할 것 같다. 하지만, t 값이 Float로 표현하기 때문에 크게 문제가 되지 않는것 같다.

위 설명을 코드로 작성하면 아래와 같다.

#include <iostream>
#include <fstream>
#include "mkray.h"
#include "mkhitablelist.h"
#include "float.h"
#include "mksphere.h"
#include "mkcamera.h"

using namespace std;

//MK: Unit Sphere안의 Random 포인트 계산
vec3 randomInUnitSphere(){
    vec3 ret;
    do{
        ret = 2.0 * vec3(drand48(), drand48(), drand48()) - vec3(1, 1, 1);
    }while(ret.squared_length() >= 1.0);
    return ret;
}


vec3 color(const ray &r, hitable *world){
    hitRecord rec;
    if(world->hit(r, 0.0, MAXFLOAT, rec)){
        //MK: Ray가 반사되는 위치 찾기
        vec3 target = rec.p + rec.normal + randomInUnitSphere();
        //MK: 50%의 에너지를 흡수하고 Ray를 새로운 위치로 보내는 코드
        return 0.5 * color( ray(rec.p, unitVector(target-rec.p)), world );
    }
    else{
        vec3 unitDirection = unitVector(r.direction());
        float t = 0.5 * (unitDirection.y() + 1.0);
        return (1.0 - t)*vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
    }
}

int main(){
    int nx = 400;
    int ny = 200;
    int ns = 100;
    string fileName = "Ch7_1.ppm";
    ofstream writeFile(fileName.data());
    if(writeFile.is_open()){
        writeFile.flush();
        writeFile << "P3\n" << nx << " " << ny << "\n255\n";
        hitable *list[2];
        list[0] = new sphere(vec3(0, 0, -1), 0.5);
        list[1] = new sphere(vec3(0, -100.5, -1), 100);
        hitable *world = new hitableList(list, 2);
        camera cam;
        for(int j = ny - 1; j >= 0; j--){
            for(int i = 0; i < nx; i++){
                vec3 col(0.0, 0.0, 0.0);
                for(int s = 0; s < ns; s++){
                    float u = float(i + drand48()) / float(nx);
                    float v = float(j + drand48()) / float(ny);
                    ray r = cam.getRay(u, v);
                    col += color(r, world);
                }
                col /= float(ns);
                int ir = int(255.99 * col[0]);
                int ig = int(255.99 * col[1]);
                int ib = int(255.99 * col[2]);
                writeFile << ir << " " << ig << " " << ib << "\n";
            }
        }
        writeFile.close();
    }
    return 0;
}

그림 3: Diffuse Material 결과 이미지

위 코드를 작성하여서 실행하면 그림 3 결과 이미지를 확인할 수 있다. 위 코드에서 빛 에너지의 50%만 흡수하도록 구현되어 있다. 만약 빛 에너지의 50%만 흡수를 하는 경우 현실에서는 회색 모양의 구가 그려져야 한다. 하지만, 그림 3의 결과는 어두운 파란 색상으로 보인다. 그 이유는 Gamma Correction이 적용되지 않아서 색상 값이 다르게 표시된다. Gamma Correction은 모니터가 표시하는 색상을 우리가 보는 색상으로 차이를 보정하는 기법이다. 상세한 설명은 출처 3을 확인하면 된다. 아래 코드는 Gamma Correction을 구현한 Main 함수 코드이다.

#include <iostream>
#include <fstream>
#include "mkray.h"
#include "mkhitablelist.h"
#include "float.h"
#include "mksphere.h"
#include "mkcamera.h"

using namespace std;

vec3 randomInUnitSphere(){
    vec3 ret;
    do{
        ret = 2.0 * vec3(drand48(), drand48(), drand48()) - vec3(1, 1, 1);
    }while(ret.squared_length() >= 1.0);
    return ret;
}


vec3 color(const ray &r, hitable *world){
    hitRecord rec;
    if(world->hit(r, 0.0, MAXFLOAT, rec)){
        vec3 target = rec.p + rec.normal + randomInUnitSphere();
        return 0.5 * color( ray(rec.p, unitVector(target-rec.p)), world );
    }
    else{
        vec3 unitDirection = unitVector(r.direction());
        float t = 0.5 * (unitDirection.y() + 1.0);
        return (1.0 - t)*vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
    }
}

int main(){
    int nx = 400;
    int ny = 200;
    int ns = 100;
    string fileName = "Ch7_2.ppm";
    ofstream writeFile(fileName.data());
    if(writeFile.is_open()){
        writeFile.flush();
        writeFile << "P3\n" << nx << " " << ny << "\n255\n";
        hitable *list[2];
        list[0] = new sphere(vec3(0, 0, -1), 0.5);
        list[1] = new sphere(vec3(0, -100.5, -1), 100);
        hitable *world = new hitableList(list, 2);
        camera cam;
        for(int j = ny - 1; j >= 0; j--){
            for(int i = 0; i < nx; i++){
                vec3 col(0.0, 0.0, 0.0);
                for(int s = 0; s < ns; s++){
                    float u = float(i + drand48()) / float(nx);
                    float v = float(j + drand48()) / float(ny);
                    ray r = cam.getRay(u, v);
                    col += color(r, world);
                }
                col /= float(ns);
                //MK: Gamma Correction 코드
                col = vec3( sqrt(col[0]), sqrt(col[1]), sqrt(col[2]) );
                int ir = int(255.99 * col[0]);
                int ig = int(255.99 * col[1]);
                int ib = int(255.99 * col[2]);
                writeFile << ir << " " << ig << " " << ib << "\n";
            }
        }
        writeFile.close();
    }
    return 0;
}

그림 4: Gamma Correction 코드 결과

위 코드를 컴파일해서 실행하면 그림 4 결과 이미지를 확인할 수 있다. 위 그림도 Floating 연산에 따른 약간의 오류가 존재한다. 컴퓨터 Floating 연산오류로 인해서 t 값이 정확하게 0이 아닌 경우가 있다. t 값이 0.0000001 또는 -0.0000001인 경우가 있다고 한다. 이로 인해서 Shadow Acne Problem이 발생한다고 한다. 이로 인한 오류를 제거하기 위해서 아래와 같이 tMin값을 수정한다.

MK: Shadow Acne Problem은 Shadow(그림자)를 만드는 Object의 그림자가 Object에 표시되는 현상을 의미한다 (Self-Shadowed) (출처 4). 정확하게 이 문제가 발생하는 원인을 잘 모르겠다. 추측하기를 아마도 t=0이 아닌 경우 예를 들어 0.0000001과 같이 아주 작은 경우 빛을 반사한 Object가 다시 Hit 되는 문제가 생기게 되면서 에너지를 더 흡수해서 발생하는 문제인것으로 추측된다.

#include <iostream>
#include <fstream>
#include "mkray.h"
#include "mkhitablelist.h"
#include "float.h"
#include "mksphere.h"
#include "mkcamera.h"

using namespace std;

vec3 randomInUnitSphere(){
    vec3 ret;
    do{
        ret = 2.0 * vec3(drand48(), drand48(), drand48()) - vec3(1, 1, 1);
    }while(ret.squared_length() >= 1.0);
    return ret;
}


vec3 color(const ray &r, hitable *world){
    hitRecord rec;
    //MK: Shadow Acne Problem 제거 코드
    if(world->hit(r, 0.001, MAXFLOAT, rec)){
        vec3 target = rec.p + rec.normal + randomInUnitSphere();
        return 0.5 * color( ray(rec.p, unitVector(target-rec.p)), world );
    }
    else{
        vec3 unitDirection = unitVector(r.direction());
        float t = 0.5 * (unitDirection.y() + 1.0);
        return (1.0 - t)*vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
    }
}

int main(){
    int nx = 400;
    int ny = 200;
    int ns = 100;
    string fileName = "Ch7_3.ppm";
    ofstream writeFile(fileName.data());
    if(writeFile.is_open()){
        writeFile.flush();
        writeFile << "P3\n" << nx << " " << ny << "\n255\n";
        hitable *list[2];
        list[0] = new sphere(vec3(0, 0, -1), 0.5);
        list[1] = new sphere(vec3(0, -100.5, -1), 100);
        hitable *world = new hitableList(list, 2);
        camera cam;
        for(int j = ny - 1; j >= 0; j--){
            for(int i = 0; i < nx; i++){
                vec3 col(0.0, 0.0, 0.0);
                for(int s = 0; s < ns; s++){
                    float u = float(i + drand48()) / float(nx);
                    float v = float(j + drand48()) / float(ny);
                    ray r = cam.getRay(u, v);
                    col += color(r, world);
                }
                col /= float(ns);
                col = vec3( sqrt(col[0]), sqrt(col[1]), sqrt(col[2]) );
                int ir = int(255.99 * col[0]);
                int ig = int(255.99 * col[1]);
                int ib = int(255.99 * col[2]);
                writeFile << ir << " " << ig << " " << ib << "\n";
            }
        }
        writeFile.close();
    }
    return 0;
}

그림 5: Shadow Acne Problem 제거 결과

그림 5 결과 이미지는 Shadow Acne Problem을 제거한 결과 그림이다.

출처

  1. http://www.realtimerendering.com/raytracing/Ray%20Tracing%20in%20a%20Weekend.pdf
  2. https://learnopengl.com/Lighting/Basic-Lighting
  3. https://learnopengl.com/Advanced-Lighting/Gamma-Correction
  4. https://digitalrune.github.io/DigitalRune-Documentation/html/3f4d959e-9c98-4a97-8d85-7a73c26145d7.htm

2 thoughts on “[RT in One Weekend Series 8] Diffuse Materials (번역)”

    • 안녕하세요. 현재 읽으면서 공부를 하고 있는 책에서는 광원의 시작 위치와 카메라의 위치가 동일한 것 같습니다. 책 뒤에 부분에서 카메라의 위치를 변경하는 부분이 있습니다. 이 부분에서 카메라와 광원의 위치가 달라지지 않을까 추측하고 있습니다. 예전에 Ray Tracing 관련 공부를 할때는 광원의 위치는 빛을 발산하는 예를 들어 태양과 같은 부분에서 시작한다고 하였습니다. 현재 책은 아직 빛을 발산하는 물체가 없는 관계로 단순히 에너지의 특정 Percentage를 흡수하는 형태로 Ray Tracing을 구현하고 있는것 같습니다.

      Reply

Leave a Reply to RT Cancel reply