[RT in One Weekend Series 9] Metal (번역)

목차: Series 1: Index and Overview (Link)
이전 글: Series 8: Diffuse Materials (Link)
다음 글: Series 10: Dielectrics (Link)

이제 여러 개의 다른 Material을 가진 Object를 그려볼 차례이다. Material이 다른 여러 종류의 Object를 그리기 위해서는 Design Decision이 필요하다. 한 가지 방법은 Universal Material Class를 생성하고 필요 없는 Parameter를 0으로 설정하는 방법이다. 다른 방법은 Abstract Material Class를 생성하고 해당 클래스는 Ray의 반사 Behavior 함수만 포함한다. 새로운 Material을 구현하기 위해서는 새로운 Class를 생성한 후 Abstract Class를 참조하여 Behavior 함수를 구현하는 방법이다. 출처 1의 경우 2번째 방법을 사용한다. 우리가 만들고 있는 Ray Tracer에서 Material Class는 아래 2가지 연산을 수행하게 된다.

  1. Scattered Ray (Reflected Ray로 해석해도 될 것 같음)을 생성
  2. Scattered Ray가 되는 경우 Ray의 강도를 감소시킴

아래 코드1은 Abstract Material Class를 포함하고 있다 (코드 1-1 참조). 해당 Class는 간단히 Scatter이란 함수만을 포함하고 있다.

코드 1: mkmaterial.h 파일 코드

#ifndef MKMATERIAL_H
#define MKMATERIAL_H

#include "mkvec3.h"

//MK: 코드 1-1
//MK: Abstract Material Class 추가
class material{
    public:
        virtual bool scatter(const ray &rIn, const hitRecord &rec, vec3 &attenuation, ray &scattered) const = 0;
};

//MK: 코드 1-2
//MK: lambertian class 코드 부분
class lambertian : public material{
    public:
        lambertian(const vec3 &a): albedo(a) {}
        virtual bool scatter(const ray &rIn, const hitRecord &rec, vec3 &attenuation, ray &scattered) const {
            vec3 target = rec.p + rec.normal + randomInUnitSphere();
            scattered = ray(rec.p, unitVector(target-rec.p));
            attenuation = albedo;
            return true;
        }
    private:
        vec3 albedo;
};

class metal : public material{
    public:
        metal(const vec3 &a): albedo(a){}
        virtual bool scatter(const ray &rIn, const hitRecord &rec, vec3 &attenuation, ray &scattered) const{
            vec3 reflected = reflect(unitVector(rIn.direction()), rec.normal);
            scattered = ray(rec.p, unitVector(reflected));
            attenuation = albedo;
            return (dot(scattered.direction(), rec.normal) > 0);
        }
    private:
        vec3 albedo;
};

#endif

지난 글에서 작성한 코드 중 hitRecord라는 Struct 구조가 있다. 해당 Struct는 함수로 전달되는 Parameter(인자)의 개수를 간소화하기 위해서 만들어진 Struct라고 한다. Hitable Class와 Material Class는 서로 Reference를 해야 하는 관계로 hitRecord Struct에 Material Pointer(포인터)를 추가한다. 아래 코드2는 Hitable 파일을 수정한 코드이다 (코드 2-1).

코드 2: mkhitable.h 파일 코드

#ifndef MKHITABLE_H
#define MKHITABLE_H

#include "mkvec3.h"

class material;

//MK: 코드 2-1
//MK: material class Pointer을 추가한 부분
struct hitRecord{
    float t;
    vec3 p;
    vec3 normal;
    material *matPtr;
};

class hitable{
    public: 
        virtual bool hit(const ray &r, float tMin, float tMax, hitRecord &rec) const = 0;
};

#endif

Material Pointer는 Ray가 Object의 Surface에 Intersect (Hit) 한 경우 Ray가 반사되는 경로를 계산하기 위해서 사용한다. Ray가 Object를 Hit 하게 되면 Hit 된 Object의 Material 정보를 hitRecord에 추가한다. 이렇게 추가된 포인터 정보를 활용하여 새로운 Ray (Scattered Ray or Reflected Ray)를 생성한다.

MK: 출처 1 글에서 Ray가 Object와 Intersection(Hit) 하였을 때 Material Pointer를 수정하는 코드 변경에 대한 내용이 없다. Ray가 Hit이 되었을 때 Object의 Material Pointer를 변경하기 위해서 sphere 파일에서 아래 코드3과 같이 수정이 필요하다 (코드 3-1 참조).

코드 3: mksphere.h 파일 코드

#ifndef MKSPHERE_H
#define MKSPHERE_H

#include "mkhitable.h"

class sphere: public hitable{
    public:
        sphere(){}
        sphere(vec3 cen, float r, material *ptr) : center(cen), radius(r), matPtr(ptr) {}

        virtual bool hit(const ray &r, float tMin, float tMax, hitRecord &rec) const {
            vec3 oc = r.origin() - center;
            float a = dot(r.direction(), r.direction());
            float b = dot(oc, r.direction());
            float c = dot(oc, oc) - radius * radius;
            float discriminant = b*b - a*c;
            if(discriminant > 0){
                float temp = (-b - sqrt(discriminant))/a;
                if(temp < tMax && temp > tMin){
                    rec.t = temp;
                    rec.p = r.pointAtParameter(rec.t);
                    rec.normal = (rec.p - center) / radius;
                    //MK: 코드 3-1
                    //MK: Ray와 Hit된 Object Material Pointer를 저장하는 부분
                    rec.matPtr = matPtr;
                    return true;
                }
                temp = (-b + sqrt(discriminant))/a;
                if(temp < tMax && temp > tMin){
                    rec.t = temp;
                    rec.p = r.pointAtParameter(rec.t);
                    rec.normal = (rec.p - center) / radius;
                    //MK: 코드 3-1 
                    //MK: Ray와 Hit된 Object Material Pointer를 저장하는 부분
                    rec.matPtr = matPtr;
                    return true;
                }
            }
            return false;
        }

    private:
        vec3 center;
        float radius;
        material *matPtr;
};

#endif

Lambertian (Diffuse) Material의 Scatter 함수는 코드1과 같다 (코드 1-2 참조). Lambertian Material은 모든 방향으로 Ray를 분산(Scatter)시키거나, Ray의 에너지를 줄인 다음 분산시키거나, Ray의 에너지를 흡수하고 분산시키지 않는 등 다양한 결과를 계산할 수 있다. 앞에 설명한 여러 개의 행동을 섞어서 계산하는 것도 가능하다.

MK: Lambertian Surface는 모든 방향에서 보아도 똑같은 밝기로 보이는 표면을 뜻한다 (출처 2). 아마 앞장에서 구현한 Diffuse Surface가 Lambertian Surface와 동일한 것으로 추측된다. 추가로 Lambertian (Diffuse) Material의 경우 Smooth 하지 않은 표면(Surface)을 가진 물체를 의미한다. 우리가 작성하는 코드는 100%의 확률 Scatter 되고 Albedo/P (Albedo/1)만큼 Ray의 에너지를 흡수하도록 구현하였다.

표면이 부드러운(Smooth) Object의 경우 Scatter(분산) 되는 Ray가 Random이 아니다. 표면이 부드러운 Object의 경우 아래 그림 1과 같이 Ray를 Reflect(반사) 시키게 된다.

그림 1: Ray가 Reflect되는 그림

반사하는 Ray를 방향을 계산하기 위해서 Dot Product를 사용한다 (출처 3). 그림 2는 Reflected Ray를 계산하기 위해서 정리한 그림이다.

그림 2: Reflect Ray 계산 (출처 3)

그림 2를 사용해서 Reflected Ray를 계산하는 방법은 다음과 같다. L⃗을 기존 Ray라고 가정하면 R⃗은 반사된 Ray를 의미한다. N⃗은 Ray와 Object가 Hit 되는 부분의 Normal Vector를 의미한다. ΘR과 ΘL은 동일한 각을 가진다. 그럼 아래와 같이 EQ1으로 표현할 수 있다.

  • EQ1: ΘR = ΘL

ΘR과 ΘL은 Dot Product를 사용해서 계산이 가능하다. ΘR과 ΘL은 아래 EQ2와 EQ3로 표현할 수 있다.

  • EQ2: ΘR = R⃗ · N⃗
  • EQ3: Θ= L⃗ · N⃗

EQ2와 EQ3을 EQ1에 대입하면 아래와 같이 EQ4로 표현할 수 있다.

  • EQ4: R⃗ · N⃗ = L⃗ · N⃗

위 그림 3에 U⃗’와 U⃗”의 관계를 아래 EQ5와 같이 표현할 수 있다.

  • EQ5: U⃗’ = -U⃗”

U⃗’와 U⃗”은 아래 EQ8, EQ11와 같이 표현 할 수 있다.

  • EQ6: U⃗’ = R⃗ – N⃗'(Normal Vector가 아님)
  • EQ7: N⃗’ = (R⃗ · N⃗) N⃗ (출처 4: Vector Projection 연산)
  • EQ8: U⃗’ = R⃗ – (R⃗ · N⃗) N⃗
  • EQ9: U⃗” = L⃗ – N⃗” (Normal Vector가 아님)
  • EQ10: N⃗” = (L⃗ · N⃗) N⃗ (출처 4: Vector Projection 연산)
  • EQ11: U⃗” = L⃗ – (L⃗ · N⃗) N⃗

이제 EQ5, EQ8을 EQ11에 대입하면 아래와 EQ12로 표현할 수 있다.

  • EQ12: R⃗ – (R⃗ · N⃗) N⃗ = -(L⃗ – (L⃗ · N⃗) N⃗)

R⃗을 왼쪽에 남겨두고 식을 정리하면 아래와 같은 EQ13이 완성된다.

  • EQ13: R⃗ = 2(N⃗ · L⃗) N⃗ – L⃗

위 그림3은 Ray L과 Ray R이 모두 Surface에서 나가는 방향이다. 하지만 우리가 작성하는 코드에서는 그림1과 같이 L Ray가 Surface 방향으로 향한다. 우리가 작성하고 있는 코드에서는 EQ13에 -(마이너스) 사인을 곱해줘야 한다. 그럼 Reflected Ray를 계산하는 식은 아래 EQ14와 같다.

  • EQ14: R⃗ = L⃗ – 2(N⃗ · L⃗) N⃗

Dot Product를 사용하여 Reflect Ray를 계산하는 코드를 아래 코드 4에 추가하였다 (코드 4-1). 추가로 아래 코드는 기존 main.cpp에 있던 randomInUnitSphere 함수를 mkvec3.h파일로 이동하였다. 개인적으로 정리를 하기 위해서 해당 함수를 mkvec3.h로 이동하였다. 원하지 않으면 main.cpp에 저장해도 무방하다.

코드 4: mkvec3.h 파일 코드

#ifndef MKVEC3_H
#define MKVEC3_H

#include <math.h>
#include <stdlib.h>
#include <iostream>

class vec3{
    public:
      vec3(){}
      vec3(float e0, float e1, float e2){
        element[0] = e0;
        element[1] = e1;
        element[2] = e2;
      }

      inline float x() const{ return element[0];}
      inline float y() const{ return element[1];}
      inline float z() const{ return element[2];}

      inline float r() const{ return element[0];}
      inline float g() const{ return element[1];}
      inline float b() const{ return element[2];}

      inline const vec3& operator+() const{ return *this;}
      inline vec3 operator-() const {return vec3(-element[0], -element[1], -element[2]);}
      inline float operator[] (int i) const {return element[i];}
      inline float &operator[] (int i) {return element[i];}

      inline vec3& operator+=(const vec3 &v){
          element[0] += v.element[0];
          element[1] += v.element[1];
          element[2] += v.element[2];
          return *this;
      }
      inline vec3& operator-=(const vec3 &v){
          element[0] -= v.element[0];
          element[1] -= v.element[1];
          element[2] -= v.element[2];
          return *this;
      }
      inline vec3& operator*=(const vec3 &v){
          element[0] *= v.element[0];
          element[1] *= v.element[1];
          element[2] *= v.element[2];
          return *this;
      }
      inline vec3& operator/=(const vec3 &v){
          element[0] /= v.element[0];
          element[1] /= v.element[1];
          element[2] /= v.element[2];
          return *this;
      }
      inline vec3& operator*=(const float t){
          element[0] *= t;
          element[1] *= t;
          element[2] *= t;
          return *this;
      }
      inline vec3& operator/=(const float t){
          float k = 1.0/t;
          element[0] *= k;
          element[1] *= k;
          element[2] *= k;
          return *this;
      }

      inline float length() const{
          return sqrt(element[0] * element[0] + element[1] * element[1] + element[2] * element[2]);
      }
      inline float squared_length() const{
          return (element[0] * element[0] + element[1] * element[1] + element[2] * element[2]);
      }
      inline void make_unit_vector(){
          float k = 1.0 / (sqrt(element[0] * element[0] + element[1] * element[1] + element[2] * element[2]));
          element[0] *= k;
          element[1] *= k;
          element[2] *= k;
      };

      float element[3];
};

inline std::istream& operator>>(std::istream &is, vec3 &t){
    is >> t.element[0] >> t.element[1] >> t.element[2];
    return is;
}

inline std::ostream& operator<<(std::ostream &os, const vec3 &t){
    os << t.element[0] << t.element[1] << t.element[2];
    return os;
}

inline vec3 operator+(const vec3 &v1, const vec3 &v2){
    return vec3(v1.element[0] + v2.element[0], v1.element[1] + v2.element[1], v1.element[2] + v2.element[2]);
}

inline vec3 operator-(const vec3 &v1, const vec3 &v2){
    return vec3(v1.element[0] - v2.element[0], v1.element[1] - v2.element[1], v1.element[2] - v2.element[2]);
}

inline vec3 operator*(const vec3 &v1, const vec3 &v2){
    return vec3(v1.element[0] * v2.element[0], v1.element[1] * v2.element[1], v1.element[2] * v2.element[2]);
}

inline vec3 operator/(const vec3 &v1, const vec3 &v2){
    return vec3(v1.element[0] / v2.element[0], v1.element[1] / v2.element[1], v1.element[2] / v2.element[2]);
}

inline vec3 operator*(const float t, const vec3 &v){
    return vec3(t * v.element[0], t * v.element[1], t * v.element[2]);
}

inline vec3 operator/(const vec3 &v, const float t){
    return vec3(v.element[0]/t, v.element[1]/t, v.element[2]/t);
}

inline vec3 operator*(const vec3 &v, const float t){
    return vec3(v.element[0] * t, v.element[1] * t, v.element[2] * t);
}

inline float dot(const vec3 &v1, const vec3 &v2){
    return (v1.element[0] * v2.element[0] + v1.element[1] * v2.element[1] + v1.element[2] * v2.element[2]);
}

inline vec3 cross(const vec3 &v1, const vec3 &v2){
    return vec3(
                (v1.element[1] * v2.element[2] - v1.element[2] * v2.element[1]),
                -(v1.element[0] * v2.element[2] - v1.element[2] * v2.element[0]),
                (v1.element[0] * v2.element[1] - v1.element[1] * v2.element[0])
            );
}

inline vec3 unitVector(vec3 v){
    return (v/v.length());
}

//MK: 코드 4-1
//MK: Reflect 코드와 기존에 Main에 있던 randomInUnitSphere 함수를 mkvec3.h파일로 이동함
vec3 randomInUnitSphere(){
    vec3 ret;
    do{
        ret = 2.0 * vec3(drand48(), drand48(), drand48()) - vec3(1, 1, 1);
    }while(ret.squared_length() >= 1.0);
    return ret;
}

//MK: 코드 4-1 
//MK: Reflected Ray 계산하는 부분
vec3 reflect(const vec3 &v, const vec3 & n){
    return v - 2 * dot(v, n) * n;
}

#endif

이제 Metal Object를 그리기 위해서 color() 함수와 main() 함수를 수정한다. 아래 코드 5는 main.cpp 파일의 최종 코드이다.

MK: attenuation이 아마도 Ray의 에너지를 흡수하기 위해서 추가된 부분으로 판단된다.

코드 5: main.cpp 함수 코드 (Color + Main 함수)

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

using namespace std;

//MK: 코드 5-1
//MK: 특정 에너지를 흡수하고 새로운 Ray를 생성하기 위한 코드 (최종 색상 결정)
vec3 color(const ray &r, hitable *world, int depth){
    hitRecord rec;
    if(world->hit(r, 0.001, MAXFLOAT, rec)){
        ray scattered;
        vec3 attenuation;
        if(depth < 50 && rec.matPtr->scatter(r, rec, attenuation, scattered)){
            return attenuation * color(scattered, world, depth + 1);
        }
        else{
            return vec3(0, 0, 0);
        }
    }
    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);
    }
}

//MK: 코드 5-2
//MK: 새로 생성한 Metal Object를 추가한 Main 함수
int main(){
    int nx = 400;
    int ny = 200;
    int ns = 100;
    string fileName = "Ch8_1.ppm";
    ofstream writeFile(fileName.data());
    if(writeFile.is_open()){
        writeFile.flush();
        writeFile << "P3\n" << nx << " " << ny << "\n255\n";
        hitable *list[4];
        list[0] = new sphere(vec3(0, 0, -1), 0.5, new lambertian(vec3(0.8, 0.3, 0.3)));
        list[1] = new sphere(vec3(0, -100.5, -1), 100, new lambertian(vec3(0.8, 0.8, 0.0)));
        list[2] = new sphere(vec3(1, 0, -1), 0.5, new metal(vec3(0.8, 0.6, 0.2)));
        list[3] = new sphere(vec3(-1, 0, -1), 0.5, new metal(vec3(0.8, 0.8, 0.8)));
        hitable *world = new hitableList(list, 4);
        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, 0);
                }
                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;
}

위 코드를 모두 작성하여 컴파일하면 아래 그림 3과 같은 결과 이미지를 확인 할 수 있다. 그림 3에서 Metal은 완벽하게 거울처럼 Object를 보여준다. 아마도 거울처럼 완벽하게 이미지를 보여주는 이유는 처음 Object에 Hit 되는 Ray와 Reflected Ray의 Normal Vector 기준 각도가 동일하기 때문으로 추측된다 (그림 2, EQ1 참조).

그림 3: Metal 결과 이미지 

코드를 약간 수정하여서 완벽하게 Reflected Ray를 생성하지 않고 약간의 Random 값이 들어간 Reflected Ray를 생성해볼 계획이다. 이러한 Random 값이 추가된 것을 Fuzziness라고 정의하였다. 약간의 Random 값을 추가하는 방법은 아래 그림 4와 같다. Reflected Ray의 끝부분에 조그만 구를 그린다. 그리고 해당 구에서 Random Point를 선택하여서 Reflected Ray를 선택된 포인터로 이동하도록 Ray를 생성한다. 구의 크기가 클수록 Reflected Ray가 반사되는 방향이 많이 달라진다. 그 경우 그림 3과 달리 더 흐릿한 형태로 Metal 표면에 붉은색 구가 표현된다.

그림 4: Fuzziness를 추가한 Reflected Ray 생성 방법

아래 코드 6은 그림 4와 같이 Reflected Ray를 생성하는 부분을 적용한 코드이다 (코드 6-1 참조). 기존에 작성하였던 randomInUnitSphere() 함수를 사용하여서 코드를 완성하였다.

코드 6: Fuzziness를 추가한 Color 코드

#ifndef MKMATERIAL_H
#define MKMATERIAL_H

#include "mkvec3.h"

class material{
    public:
        virtual bool scatter(const ray &rIn, const hitRecord &rec, vec3 &attenuation, ray &scattered) const = 0;
};

class lambertian : public material{
    public:
        lambertian(const vec3 &a): albedo(a) {}
        virtual bool scatter(const ray &rIn, const hitRecord &rec, vec3 &attenuation, ray &scattered) const {
            vec3 target = rec.p + rec.normal + randomInUnitSphere();
            scattered = ray(rec.p, unitVector(target-rec.p));
            attenuation = albedo;
            return true;
        }
    private:
        vec3 albedo;
};

//MK: 코드 6-1 
//MK: Fuzz 값을 추가한 Metal Class
class metal : public material{
    public:
        metal(const vec3 &a, float f): albedo(a){
            if (f < 1.0){
                fuzz = f;
            }
            else{
                fuzz = 1.0;
            }
        }
        virtual bool scatter(const ray &rIn, const hitRecord &rec, vec3 &attenuation, ray &scattered) const{
            vec3 reflected = reflect(unitVector(rIn.direction()), rec.normal);
            scattered = ray(rec.p, unitVector(reflected+fuzz*randomInUnitSphere()));
            attenuation = albedo;
            return (dot(scattered.direction(), rec.normal) > 0);
        }
    private:
        vec3 albedo;
        float fuzz;
};

#endif

그림 5는 위 코드를 컴파일하여서 실행한 결과 이미지이다. 기존 그림 3의 결과 이미지와 달리 Metal 표면에 표현되는 구의 선명도가 달라진 것을 확인 할 수 있다.

그림 5: Fuzziness 추가 결과 이미지

MK: 개인적인 일과 Reflected Ray를 생성하는 과정에서 수학적인 부분을 이해하지 못하여서 글을 작성하는 데 오랜 시간이 걸린 것 같다. 가능하다면 Reflected Ray를 계산하는 부분을 완벽하게 이해하고 싶지만, 아직도 정확한 계산 방법을 이해하지 못하였다.

출처

  1. http://www.realtimerendering.com/raytracing/Ray%20Tracing%20in%20a%20Weekend.pdf
  2. https://blog.naver.com/jetarrow82/221252045857
  3. https://www.fabrizioduroni.it/2017/08/25/how-to-calculate-reflection-vector.html
  4. https://en.wikipedia.org/wiki/Vector_projection

추가 관련 사이트 정리

  1. https://www.mathsisfun.com/algebra/vectors.html
  2. https://wndrlf2003.blog.me/221540829487
  3. https://webmasters.stackexchange.com/questions/39777/mathml-html-symbol-for-mathematical-vector
  4. https://www.w3schools.com/charsets/ref_utf_greek.asp
  5. https://www.w3schools.com/tags/tag_sub.asp

Leave a Comment