[RT in One Weekend Series 6] Surface Normals and Multiple Objects (번역)

목차: Series 1: Index and Overview (Link)
이전 글: Series 5: Adding a Sphere (Link)
다음 글: Series 7: Antialiasing (Link)

이번 글에서는 구의 표면 색상값을 Normal Vector 값으로 변경하는 코드를 작성한다.

MK: 그래픽에서 Normal Vector 값은 아주 많이 사용된다. 특히 Light (빛) 색상을 결정하는 단계에서 아주 기본적인 개념이다.

그림 1: 구의 Normal Vector 값을 계산하는 방법

그림1은 구에서 Normal Vector 값을 계산하는 방법이다. 간단하게 구의 표면 위치에서 구의 중심부를 빼는 연산을 수행하면 Normal Vector 계산이 가능하다. 그래픽 연산에서 보통 Normal Vector는 Unit Vector로 변경하여서 사용한다. Unit Vector 계산 방법은 출처2를 참조하면 된다. Normal Vector의 값은 -1에서 1사이에 존재한다. 색상을 표시하기 위해서 Normal Vector 값을 0~1 사이 값으로 변경해야 한다. 이와 더불어 X/Y/Z를 R/G/B로 변경한다. 아래 코드는 구의 Normal Vector값을 계산 후  X/Y/Z값을 색상 값인 R/G/B로 변경하는 코드이다.

#include <iostream>
#include <fstream>
#include "mkray.h"

using namespace std;

float hitSphere(const vec3 &center, float radius, const ray &r){
    vec3 oc = r.origin() - center;
    float a = dot(r.direction(), r.direction());
    float b = 2.0 * dot(oc, r.direction());
    float c = dot(oc, oc) - radius * radius;
    float discriminant = b*b - 4*a*c;
    if (discriminant < 0){
        return -1.0; //MK: 구와 Ray가 Intersect하지 않음
    }
    else{
        return (-b -sqrt(discriminant))/(2.0 * a); //MK: 구와 Ray가 Intersect함
    }
}

vec3 color(const ray &r){
    float t = hitSphere(vec3(0, 0, -1), 0.5, r);
    vec3 ret = vec3(1.0f, 0.0f, 0.0f);
    if(t > 0.0){
        //MK: 구와 Ray가 Intersect함으로 Normal Vector값을 R/G/B 값으로 변경함
        vec3 N = unit_vector(r.pointAtParameter(t) - vec3(0, 0, -1));
        ret = 0.5 * vec3(N.x() + 1, N.y() + 1, N.z() + 1);
        return ret;
    }
    //MK: Ray가 구와 만나지 않음으로 바탕화면 색상을 결정함
    vec3 unitDirection = unit_vector(r.direction());
    t = 0.5 * (unitDirection.y() + 1.0);
    ret = (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
    return ret;
}

int main(){
    int nx = 400;
    int ny = 200;
    string fileName = "Ch5_1.ppm";
    ofstream writeFile(fileName.data());
    if(writeFile.is_open()){
        writeFile.flush();
        writeFile << "P3\n" << nx << " " << ny << "\n255\n";
        vec3 lowerLeftCorner(-2.0, -1.0, -1.0);
        vec3 horizontal(4.0, 0.0, 0.0);
        vec3 vertical(0.0, 2.0, 0.0);
        vec3 origin(0.0, 0.0, 0.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, (lowerLeftCorner + u * horizontal + v * vertical));
                vec3 col = color(r);
                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;
}

그림 2: Normal 값을 Color값으로 변경한 이미지 결과

그림 2는 위 코드를 실행한 결과 이미지이다. 이제 여러 개의 구를 그리는 코드를 추가로 작성할 계획이다. 여러 개의 구를 그리는 가장 좋은 방법은 추상클래스(Abstract Class)를 하나 생성하여 모든 물체(Object)를 추상클래스를 사용(상속)하여서 클래스를 생성하는 방법이다. 해당 추상클래스는 Ray와 Hit 여부를 판단할 수 있는 클래스이다. 아래 코드는 hitable이라는 추상 클래스를 생성하는 코드이다.

#ifndef MKHITABLE_H
#define MKHITABLE_H

#include "mkvec3.h"

//MK: Ray와 Hit한 Object의 위치를 파악하기 위해 사용
struct hitRecord{
    float t;
    vec3 p;
    vec3 normal;
};

//MK: Hit여부를 판단하기 위한 추상클래스
class hitable{
    public: 
        virtual bool hit(const ray &r, float tMin, float tMax, hitRecord &rec) const = 0;
};

#endif

위 코드를 보면 Struct 을 사용해서 Ray와 Object가 Hit 한 결과를 저장한다. 추상 클래스에 tMax, tMin 값이 존재한다. 여기서 tMin, tMax값은 Ray가 Travel(이동) 하는 거리를 의미한다. tMax보다 먼 거리는 Ray가 도달하지 않는다고 판단한다. 반대로 tMin의 경우 너무 가까운 Object의 무시하기 위해서 사용한다. Ray가 주어진 거리 안에서 Object와 Hit이 발생하는 경우 Struct(hitRecord) 값을 업데이트한다. 아래 코드는 hitable 추상 클래스를 사용하여 sphere 클래스를 생성한 코드이다.

MK: 뒤에 코드를 작성하다 보면 tMax값은 변경된다. 예를 들어서 카메라에서 멀리 있는 구를 먼저 hitableList 에서 Hit여부를 판단하는 경우 tMax값이 줄어든다. 하지만, tMin의 경우 존재하는 이유를 아직은 잘 모르겠다. 코드를 계속 작성해봐야 하겠지만 이번 장에서는 tMin값은 사용하지 않는 값으로 판단된다.

#ifndef MKSPHERE_H
#define MKSPHERE_H

#include "mkhitable.h"

//MK: Sphere Object를 생성하여 Ray와 Hit여부를 판단하는 부분
class sphere: public hitable{
    public:
        sphere(){}
        sphere(vec3 cen, float r) : center(cen), radius(r) {}

        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;
                //MK: 구의 가까운 부분 부터 Hit여부를 판단함
                if(temp < tMax && temp > tMin){
                    rec.t = temp;
                    rec.p = r.pointAtParameter(rec.t);
                    rec.normal = (rec.p - center) / radius;
                    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;
                    return true;
                }
            }
            return false;
        }

    private:
        vec3 center;
        float radius;
};

#endif

다음은 hitable 추상클래스를 사용하여 hitableList 클래스를 생성하는 코드이다. 해당 코드는 모든 hitable Object를 순차적으로 확인하여서 Ray와 Hit하는 가장 가까운 Object를 판단한다. hitableList 클래스의 경우 앞에 작성한 Object를 모두 List(리스트) 형태 저장한다.

#ifndef HITABLELIST_H
#define HITABLELIST_H

#include "mkhitable.h"

//MK: 모든 Hitable Object를 리스트로 가지고 있음
//MK: Ray와 모든 Ojbect의 Hit(Intersection)여부를 판단함
class hitableList: public hitable{
    public: 
        hitableList(){}
        hitableList(hitable **l, int n){
            list = l;
            listSize = n;
        }
        virtual bool hit(const ray &r, float tMin, float tMax, hitRecord &rec) const {
            hitRecord tempRec;
            bool hitAnything = false;
            double closestSoFar = tMax;
            for(int i = 0; i < listSize; i++){
                if(list[i]->hit(r, tMin, closestSoFar, tempRec)){
                    hitAnything = true;
                    closestSoFar = tempRec.t;
                    rec = tempRec;
                }
            }
            return hitAnything;
        }
    private:
        hitable **list;
        int listSize;

};

#endif

마지막으로 Main 코드에서  hitableList 의 hit 함수를 호출하도록 수정한 코드이다.

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

using namespace std;

//MK: 배경 및 Ray가 도달하는 위치의 색상을 결정함
vec3 color(const ray &r, hitable *world){
    hitRecord rec;
    if(world->hit(r, 0.0, MAXFLOAT, rec)){
        return 0.5 * vec3(rec.normal.x() + 1, rec.normal.y() + 1, rec.normal.z() + 1);
    }
    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;
    string fileName = "Ch5_2.ppm";
    ofstream writeFile(fileName.data());
    if(writeFile.is_open()){
        writeFile.flush();
        writeFile << "P3\n" << nx << " " << ny << "\n255\n";
        vec3 lowerLeftCorner(-2.0, -1.0, -1.0);
        vec3 horizontal(4.0, 0.0, 0.0);
        vec3 vertical(0.0, 2.0, 0.0);
        vec3 origin(0.0, 0.0, 0.0);
        hitable *list[2];
        //MK: 2개의 구를 추가하는 부분
        list[0] = new sphere(vec3(0, 0, -1), 0.5);
        list[1] = new sphere(vec3(0, -100.5, -1), 100);
        //MK: 모든 Hitable Object를 hitableList에 추가함
        hitable *world = new hitableList(list, 2);
        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, (lowerLeftCorner + u * horizontal + v * vertical));
                vec3 col = color(r, world);
                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은 위 코드를 컴파일하여서 실행하면 보여주는 결과 이미지이다. 2개의 구가 그려진 것을 확인할 수 있다. 하나의 구의 경우 사이즈가 큰 관계로 구의 위만 보인다. 아래 구의 색상이 거의 동일한 이유는 구의 Normal Vector 값이 거의 동일하게 위로 향하기 때문이다. 작은 구의 위에 부분을 확대한 결과라고 보면 된다. 작은 구의 윗부분 역시 초록색인 것을 확인할 수 있다.

그림 3: 여러개의 구를 그린 결과

출처

  1. http://www.realtimerendering.com/raytracing/Ray%20Tracing%20in%20a%20Weekend.pdf
  2. https://en.wikipedia.org/wiki/Unit_vector

Leave a Comment