CPP로 RayTracing 구현하기 - 7. Dielectric(유전체)

그래픽스꿀잼·2026년 5월 7일

그래픽스

목록 보기
15/20
post-thumbnail

유전체? 절연체?

유전체는 더 큰 범주의 절연체임

절연체는 아예 전기가 흐르지 못하고, 전극이 와도 분극되지 못하고 그냥 아예 전기가 막힘

유전체는 전기의 흐름을 방해하는 의미에서 절연체임
유전체는 전극이 흐르면 이를 +, -로 분극을 함

따라서 좋은 유전체는 좋은 절연체이지만,
역은 성립하지 않을 수 있음

스넬 법칙

η,η\eta , \eta' = 매질에 따른 굴절율,
θ,θ\theta , \theta' = 법선으로부터의 입사각, 굴절각

스넬 법칙: ηsinθ=ηsinθ\eta * \sin \theta = \eta' * \sin \theta'

빛은 매질이 바뀔때, 빛이 닿은 경계면의 평행한 방향의 성분만 유지가 된다는걸 의미함.

각 물체는 굴절률이 다름
빛이 물체에 닿았을때 굴절이 되는데, 빛의 방향의 성분중에서 빛이 닿은 경계면과 평행한 성분만 유지가 되고, 나머지는 굴절율에 의해 굴절이 되는걸을 의미함.

노란 벡터 = 빛 방향,
파란 벡터 = 경계면에서의 Normal
노란 점선 벡터 = 굴절된 후의 빛 벡터
회색 점선 벡터 = 경계면과 평행한 방향(방향만 유지된거임)

이렇게 경계면과 평행한 방향은 유지하면서, 다른 각도로 굴절되는 것이
스넬 법칙임

중요!!!
입사 벡터, 법선 벡터는 모두 정규화된 벡터임!!

스넬 법칙 전개

ηsinθ=ηsinθ\eta * \sin \theta = \eta' * \sin \theta'

이 식을 살펴보자

우리에게 필요한 것은 sinθ\sin\theta'
왜냐고?
표면 바깥에서 굴절되어 표면 내부로 들어올 것이기 때문에ㅇㅇ

따라서 료이키 텐카이를 통해 sinθ\sin\theta'만 남기고 이항 정리를 함

sinθ=ηηsinθ\sin\theta' = \frac{\eta}{\eta'}*\sin\theta

가 됨

1. 벡터 합으로 R' 정의

RR' = 경계에 닿은 빛이 굴절된 벡터
θ\theta' = 굴절각
RR'_\bot = 법선 nn'에서 수직인 벡터
RR'_\parallel = 법선 nn'에서 평행인 벡터

위의 사진에서

R=R+RR' = R'_\bot + R'_\parallel

이라는 것을 확인할 수 있음

2. R=ηη(R(RN)N)R'_\bot = \frac{\eta}{\eta'}(R - (R \cdot N) * N) 정리

핵심은 입사각 RR을 이용하는 거임

현재 우리는 R,θ,nR, \theta, n을 알고있음

R=R+RR = R_\bot + R_\parallel

이라는 걸 알 수 있음
위에처럼 ㅇㅇ

  • 여기서 RR_\botRR'_\bot과 방향이 동일하고
    두 수직성분의 비율은 두 굴절율의 비율과 같음
    • 결국 경계면과 평행한 두 수직성분은
      굴절율의 비율에 따라 크기가 변화하게 됨

여기서 우리는 RR_\parallel을 구할 수 있음

Metal재질 반사각 전개
이 링크의 내용을 이용해 RR_\parallel을 찾은 후 값을 찾아주면됨
아래에서 다시 설명함

cosθ\cos\theta = 직각삼각형의 밑변 / 빗변
빗변 = 정규화된 벡터 = 1
cosθ\cos \theta = 밑변 = RR_\parallel

입사 벡터 \cdot(내적) 법선 = 입사벡터 * 법선벡터 cosθ\cos \theta
입사벡터 = 정규화된 벡터 = 1
법선벡터 = 정규화된 벡터 = 1
입사 벡터 \cdot 법선 = cosθ\cos \theta

입사 벡터(RR) \cdot 법선(NN) = cosθ\cos \theta = RR_\parallel

하지만 내적은 스칼라값이므로, 정규화된 벡터인 법선과 곱해서 정확한 밑변 벡터를 찾음

-> R=(RN)NR_\parallel = (R \cdot N) * N

R=R+RR = R_\bot + R_\parallel
= R=RRR_\bot = R - R_\parallel

따라서 식을 전개해보면

R=R(RN)NR_\bot = R - (R \cdot N) * N

이 되는걸 알 수 있음

여기서 RR_\botRR'_\bot과 방향이 동일하고
두 수직성분의 비율은 두 굴절율의 비율과 같음

위에서 살펴본 위 특성에 의해

R=ηη(R(RN)N)R'_\bot = \frac{\eta}{\eta'}(R - (R \cdot N) * N)

가 되는 것을 알 수 있음

3. R=1R2nR_\parallel = -\sqrt{1 - |R'_\bot|^2*n} 정리

2번은 경계와 평행한 성분인 수직성분을 구함

이번에는 경계와 수직인 성분인 평행성분을 구할 차례임

안타깝게도 수직인 성분에 대해서는
두 굴절률의 비율에 따라 값이 변화하지 않음

따라서 피타고라스 공식을 이용해서 구해주면 됨

피타고라스 정리

  • 빗변2^2 = 밑변2^2 + 높이2^2

이때 각 변은 스칼라 값임
그냥 크기라는 거임

따라서 값을 구해주면, 나중에 벡터를 곱해서 방향을 만들어줘야함

빗변 = RR' =정규화된 벡터 = 1
밑변 = |RR'_\bot| = |R(RN)NR' - (R' \cdot N) * N|
높이 = |RR'_\parallel|

이걸 정리해보면

12=(R)2+(R)21^2 = (R'_\bot)^2 + (R'_\parallel)^2

이 됨

이항정리를 하면

(R)2=12((R)N)2(R'_\parallel)^2 = 1^2 - ((R'_\bot) * N)^2

R=1((R)N)2R'_\parallel = \sqrt{1 - ((R'_\bot) * N)^2}

위처럼 정리가 됨

그리고 이미 구한 RR'_\bot을 이용하면

R=1((R(RN)N)N)2R'_\parallel = \sqrt{1 - ((R' - (R' \cdot N) * N) * N)^2}

이라는 식을 구할 수 있음

4. R=R+RR' = R'_\bot + R'_\parallel

다시 이 사진을 보면

R=R+RR' = R'_\bot + R'_\parallel 이라는 것을 알 수 있음

굴절 코드 구현

//Vector3.h...

inline Vector3 refract(const Vector3& uv, const Vector3& n, double etai_over_etat)
{
    auto cos_theta = std::fmin(dot(-uv, n), 1.0);
    Vector3 r_out_perp =  etai_over_etat * (uv + cos_theta*n);
    Vector3 r_out_parallel = -std::sqrt(std::fabs(1.0 - r_out_perp.length_squared())) * n;
    return r_out_perp + r_out_parallel;
}

먼저 cosθ\cos \theta를 구하고,
그걸 이용해서 수직성분, 평행성분을 구해준 후 더하면
굴절된 벡터인 RR'를 구하는거임!

Dielectric머티리얼 구현

class Dielectric : public Material
{
public:
    Dielectric(double refraction_index) : refraction_index(refraction_index) {}

    bool scatter(
        const Ray& r_in,
        const HitRecord& rec,
        Color& attenuation,
        Ray& scattered) const override
    {
        attenuation = Color(1,1,1);
        double ratio_of_refraction_index = rec.front_face ? 1.0 / refraction_index : refraction_index;

        Vector3 unit_direction = unit_vector(r_in.direction());
        Vector3 refracted = refract(unit_direction, rec.normal, ri);

        scattered = Ray(rec.p, refracted);
        return true;
    }

private:
    //굴절률
    double refraction_index;
};

여기서 핵심은

double ratio_of_refraction_index = rec.front_face ? 1.0 / refraction_index : refraction_index;

임!

공기의 굴절률은 1에 수렴함
공기에서 물체의 내부로 들어가는건 front_face인거임

따라서 굴절률 비율은

1(공기)/굴절률=ηη1(공기) / 굴절률 = \frac{\eta}{\eta'}

인거임!

그리고 내부에서 외부로 나올때는 front_face가 아니므로

굴절률/1(공기)=ηη=η굴절률 / 1(공기) = \frac{\eta'}{\eta} = \eta'

가 되는거임

이제 사용해보자

한번 다이아몬드의 굴절률을 사용해보자

auto material_left   = make_shared<Dielectric>(2.417);

이렇게 수정하고 렌더링을 해보면...

전반사 (Total Internal Reflection)

밀도가 높은 물질에서 밀도가 낮은 물질로 빛이 나가면 어떻게 될까
무조건 굴절이 될까?

절대 그렇지 못함

위의 세션인 스넬 법칙에서 살펴봤듯

sinθη=sinθη\sin \theta * \eta = \sin \theta' * \eta'
sinθ=ηηsinθ\sin\theta' = \frac{\eta}{\eta'} * \sin\theta

처럼 식이 정리 가능하다.

밀도 높은 물질의 굴절률 = η\eta = 2.4(다이아몬드)
밀도 낮은 물질의 굴절률 = η\eta' = 1(공기)

라고 해보자

sinθ=2.4sinθ\sin\theta' = {2.4} * \sin\theta

이 됨

근데 이러면 문제가 있음

sinθ\sin \theta가 대략 0.45정도만 되어도 sinθ\sin\theta'가 1을 넘어가버림
sinθ\sin \theta는 최대값이 1임

따라서 물질 내부에서 굴절이 안되는 각도의 빛은 반사가 됨

즉, sinθ\sin \theta가 1이 넘어가는 경우, 임계각을 넘어가는 경우는 굴절이 아닌 반사를 해줘야함

sinθ\sin\theta는 피타고라스 정리를 이용해서 구할 수 있음

sin2θ+cos2θ=1\sin^2\theta + \cos^2\theta = 1
sin2θ=1cos2θ\sin^2\theta = 1 - \cos^2\theta
sinθ=1cos2θ\sin\theta = \sqrt{1 - \cos^2\theta}

위의 스넬법칙에서 cosθ=RN\cos\theta = R\cdot N이라는 것도 알아냄

따라서 Dielectric머티리얼의 scatter메서드를 조금 수정해주면 됨

class Dielectric : public Material
{
public:
    Dielectric(double refraction_index) : refraction_index(refraction_index) {}

    bool scatter(
        const Ray& r_in,
        const HitRecord& rec,
        Color& attenuation,
        Ray& scattered) const override
    {
        attenuation = Color(1,1,1);
        double ratio_of_refraction_index = rec.front_face ? 1.0 / refraction_index : refraction_index;

        Vector3 unit_direction = unit_vector(r_in.direction());

        //add
        double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0); //코사인 세타
        double sin_theta = std::sqrt(1 - cos_theta * cos_theta);

        bool cant_refract = ratio_of_refraction_index * sin_theta > 1;
        Vector3 direction;

        if (cant_refract)
        {
            direction = reflect(unit_direction, rec.normal);
        }
        else
        {
            direction = refract(unit_direction, rec.normal, ratio_of_refraction_index);
        }
        
        scattered = Ray(rec.p, direction);
        return true;
    }

private:
    //굴절률
    double refraction_index;
};

cos_theta를 이용해서 sin_thete를 구하고
굴절 가능한지 판별을 한 후
reflect/refract를 수행함

대충 물속(1.33)의 공기(1)이라고 가정하고

auto material_left   = make_shared<Dielectric>(1.00/1.33);

과 같은 코드를 짬ㅇㅇ
그럼 다음과 같은 이미지 차이가 생김

왼쪽은 전반사 코드 추가 전/ 오른족은 전반사 코드 추가 후

전반사 추가 전전반사 추가 후

이렇게 반사가 다르게 되는걸 볼 수 있음

슐릭 근사(schlick approximation), 프레넬 효과

프레넬 효과는 유니티 쉐이더그래프를 하며 여러번 다뤄서 익숙함

프레넬은 빛의 입사각에 따라 반사되는 빛의 양이 달라지는 것을 의미함
그리고 모든 물체는 프레넬을 가짐

위 사진을 보면
프레넬을 가진 주전자는 입사각이 수직에 가까울때는 비교적 어둡고, 더 완만할때는 빛의 반사가 많은 것을 볼 수 있음(외곽부분)

우리가 만든 구체는 저렇게 프레넬 효과가 적용되지 않은 상태여서,
약간 이상하게 보임

프레넬 방정식 VS 슐릭 근사

프레넬을 구할때,
프레넬 방정식과 슐릭 근사 중 하나를 골라서 사용하면 된다

프레넬 방정식

Fequation=(ηcosθηcosθηcosθ+ηcosθ)2F_{equation} = (\frac{\eta * \cos\theta - \eta' * cos\theta'}{\eta * \cos\theta + \eta' * cos\theta'})^2

η\eta : 입사 매질 굴절률
η\eta' : 투과 매질 굴절률
θ\theta : 입사각
θ\theta' : 굴절각

슐릭 근사

Fschlick=R0+(1R0)(1cosθ)5F_{schlick} = R0 + (1- R0)(1 - \cos\theta)^5

R0R0 : 빛이 표면에 수직으로 입사했을때 반사율
cosθcos\theta : 입사각(0과 가까울수록 수직, 1과 가까울수록 평행)

R0=(1η1+η)2R0 = (\frac{1 - \eta}{1 + \eta})^2

η\eta : 굴절률

코드 구현의 난이도로 보면 슐릭근사가 더 빠르고 쉽게 계산됨


class Dielectric : public Material
{
	//...

    bool scatter(
        const Ray& r_in,
        const HitRecord& rec,
        Color& attenuation,
        Ray& scattered) const override
    {
        //...

        if (cant_refract || schlick_approximate(cos_theta, ratio_of_refraction_index) > random_double())
        {
            direction = reflect(unit_direction, rec.normal);
        }
        //...
    }

private:
    //...

    static double schlick_approximate(double cos, double ratio_of_refraction_index)
    {
        double r0 = (1 - ratio_of_refraction_index) / (1 + ratio_of_refraction_index);
        r0 = r0 * r0;

        return r0 + (1 - r0) * std::pow((1 - cos), 5);
    }
};

여기서 random_double과 비교하는 이유가 있음

특정 입사각에서 반사되어야 하는 빛의 양이 0.5라고 해보자

  1. random_double()을 통해 0.4가 나왔다면
    • 반사되어야 할때
      무조건 반사
    • 굴절되어야 할때
      조건을 만족하지 못하므로 굴절
  2. random_double()을 통해 0.6이 나왔다면
    • 반사되어야 할때
      무조건 반사
    • 굴절되어야 할때
      조건을 만족하므로 반사

이렇게 조건에 따라 확률적으로 빛을 반사시켜 프레넬 효과를 만드는거임

이걸 이용해서 기존의 왼쪽 구체를 hollow유리 구체로 만들고,
그 구체 앞에 작은 다이아몬드 구체를 만들어보도록 하겠음

유리의 굴절률은 1.5정도임

#include "Camera.h"
#include "Color.h"
#include "HittableList.h"
#include "hittable.h"
#include "RTWeekend.h"
#include "Sphere.h"

using namespace std;

int main()
{
    HittableList world;

    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));

    //hollow glass
    auto material_left = make_shared<Dielectric>(1.5);
    auto material_bubble = make_shared<Dielectric>(1.00/1.5);
    //diamond sphere
    auto material_left_front = make_shared<Dielectric>(2.417);
    
    auto material_right  = make_shared<Metal>(Color(0.8, 0.6, 0.2), 0.5);

    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.2),   0.5, material_center));

    //hollow glass
    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.45, material_bubble));
    //diamond sphere
    world.add(make_shared<Sphere>(Point3(-1.0, 0.0, -0.3), 0.1, material_left_front));
    
    world.add(make_shared<Sphere>(Point3( 1.0,    0.0, -1.0),   0.5, material_right));

    Camera cam;
    cam.aspect_ratio = 16.0 / 9.0;
    cam.image_width = 1200;
    cam.samples_per_pixel = 300;
    cam.max_depth = 50;

    cam.render(world);
}

이렇게 수정함

그러고 렌더링 해보면...

profile
그래픽스 공부중

0개의 댓글