이번 챕터는 Dialectrics라는 material을 구현해 본다. 물, 유리, 다이아몬드 같은 투명한 물질을 Dielectrics(유전체)라고 한다. 광선이 Dialectrics에 닿으면 반사된 광선과 굴절되는 광선으로 나뉜다. 우리는 이 두 현상 중에서 무작위로 하나 골라서 일으키도록 할 것이다.
잘못된 경우를 보여주는 듯 하다. 아래와 같이 출력된다.
Image 13: Glass first 뭐가 잘못된 건지 잘 모르겠다. 넘어가자. ## Snell's Law Snell's Law라는 게 있다. >**η⋅sinθ=η′⋅sinθ′**θ 과 θ′는 normal과 광선의 각도다. η(eta), η′(eta prime)은 각 매질의 굴절률(일반적으로 공기 = 1.0, 유리 = 1.3–1.7, 다이아몬드 = 2.4)이다.
Figure 13: Ray refraction 원문에서는 이 공식을 사용해 여차저차해서 값을 구한다. Snell's Law가 어떻게 나온건 지 몰라서 넘어갔다. 어짜피 저기서 우리에게 유효한 식을 도출하려고 대입하고 이항하고 기타 등등 밖에 없다. 그런 과정을 거쳐 필요한 식을 얻었고 아래와 같이 `refract`함수를 작성했다. ### vec3.hvec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) {
// 굴절된 광선을 반환하는 함수
auto cos_theta = dot(-uv, n);
vec3 r_out_perp = etai_over_etat * (uv + cos_theta*n);
vec3 r_out_parallel = -sqrt(fabs(1.0 - r_out_perp.length_squared())) * n;
return r_out_perp + r_out_parallel;
}
위의 함수를 쓰는 Dialectrics material을 구현한다.
class dielectric : public material {
public:
dielectric(double index_of_refraction) : ir(index_of_refraction) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
// 투명한 Dialectrics는 빛의 감쇠가 없다고 가정하고 감쇠율을 1.0상수로 준다.
attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0/ir) : ir;
vec3 unit_direction = unit_vector(r_in.direction());
vec3 refracted = refract(unit_direction, rec.normal, refraction_ratio);
// 항상 굴절된 광선이 반환된다.
scattered = ray(rec.p, refracted);
return true;
}
public:
double ir; // Index of Refraction
};
빛이 항상 굴절되도록 Dielectrics을 구현했다. main.cc
적용해 보자.
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<dielectric>(1.5);
auto material_left = make_shared<dielectric>(1.5);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
다음과 같이 출력된다.
Image 14: Glass sphere that always refracts방금 처럼 항상 굴절되면 문제가 있다. 굴절이 일어 나지 않는 경우가 있다고 한다. 원문은 굴절률 값이 다른 경우의 반례를 들고 있다. 구체적으로는 유리 내부에서 광선을 쏜다면 일어나는 내부 전반사의 경우 굴절이 일어나지 않고 반사만 일어난다. 자세한 사항이 궁금하다면 내부 전반사를 찾아보자. 어렵다면 항상 굴절하는 게 현실 물리 법칙과 맞지 않아서겠지 하고 넘어가자.
Dialectrics에 닿은 광선이 반사도 할 수 있게 해주자.
if (refraction_ratio * sin_theta > 1.0) {
// 굴절이 일어나지 않는 경우다.
// Must Reflect
...
} else {
// Can Refract
...
}
굴절이 안되는 경우 반사가 일어나도록 분기를 만든다. 분기 조건은 삼각함수 공식과 내적을 이용해 작성했다.
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
if (refraction_ratio * sin_theta > 1.0) {
// Must Reflect
...
} else {
// Can Refract
...
}
위의 내용들을 적용한 material.h
는 다음과 같다.
class dielectric : public material {
public:
dielectric(double index_of_refraction) : ir(index_of_refraction) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0/ir) : ir;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
bool cannot_refract = refraction_ratio * sin_theta > 1.0;
vec3 direction;
if (cannot_refract) {
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, refraction_ratio);
scattered = ray(rec.p, direction);
return true;
}
public:
double ir; // Index of Refraction
};
새로운 Dialectrics를 적용해서 main.cc
를 실행해보자.
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto material_left = make_shared<dielectric>(1.5);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 0.0);
사실 중간의 구를 lambertian으로 한 것 말고는 바뀐점이 없어뵌다. 내부 전반사가 일어나는 상황을 만들어서 출력해보면 차이가 날지도 모르겠다.
Image 15: Glass sphere that sometimes refracts현실의 유리가 보는 각도에 따라 반사율이 달라진다는 점을 적용해 본다. 유리와 우리가 보는 각도가 클수록 거울처럼 빛을 반사한다. 이를 표현하기 위한 가장 좋은 방법이 Schlick Approximation이라고 한다. 이를 적용해서 코드를 수정한다.
class dielectric : public material {
public:
dielectric(double index_of_refraction) : ir(index_of_refraction) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0/ir) : ir;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
bool cannot_refract = refraction_ratio * sin_theta > 1.0;
vec3 direction;
if (cannot_refract || reflectance(cos_theta, refraction_ratio) > random_double())
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, refraction_ratio);
scattered = ray(rec.p, direction);
return true;
}
public:
double ir; // Index of Refraction
private:
static double reflectance(double cosine, double ref_idx) {
// Use Schlick's approximation for reflectance.
auto r0 = (1-ref_idx) / (1+ref_idx);
r0 = r0*r0;
return r0 + (1-r0)*pow((1 - cosine),5);
}
};
Dielectrics의 구의 반지름을 음수로 주면 속이 빈 유리 구를 만들 수 있다. 일종의 트릭인데 반지름을 음수로주면 생성되는 normal이 안쪽으로 향해서 그렇다.
world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground));
world.add(make_shared<sphere>(point3( 0.0, 0.0, -1.0), 0.5, material_center));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), -0.4, material_left));
world.add(make_shared<sphere>(point3( 1.0, 0.0, -1.0), 0.5, material_right));
다음과 같이 출력된다.
Image 16: A hollow glass sphere