Company
교육 철학
💎

11. Dielectrics (유전체)

Dielectrics(유전체)

물, 유리, 다이아몬드와 같은 투명한 재질은 유전체입니다. 광선이 이들에 부딪히면 반사 광선과 굴절(투과) 광선으로 나뉩니다. 우리는 반사와 굴절 중 하나를 무작위로 선택하여 각 상호작용마다 하나의 산란 광선만 생성하는 방식으로 이를 처리할 것입니다.
용어를 간단히 정리하면, 반사 광선은 표면에 부딪혀 새로운 방향으로 "튕겨 나가는" 광선입니다.
굴절 광선은 물질의 외부 환경에서 물질 자체로 전환되면서 굽어지는 광선입니다(유리나 물과 같은 경우). 이것이 연필을 물에 부분적으로 담그면 구부러져 보이는 이유입니다.
굴절 광선이 굽어지는 정도는 물질의 굴절률에 의해 결정됩니다. 일반적으로 이는 진공에서 물질로 빛이 들어갈 때 얼마나 굽어지는지를 나타내는 단일 값입니다. 유리의 굴절률은 약 1.5~1.7, 다이아몬드는 약 2.4, 공기는 1.000293 정도의 작은 굴절률을 가지고 있습니다.
투명한 물질이 다른 투명한 물질에 둘러싸여 있을 때는 상대 굴절률로 굴절을 설명할 수 있습니다. 이는 물체 재질의 굴절률을 주변 물질의 굴절률로 나눈 값입니다. 예를 들어, 물속에 있는 유리 구를 렌더링하려면 유리 구의 실효 굴절률은 1.125가 됩니다. 이는 유리의 굴절률(1.5)을 물의 굴절률(1.333)로 나눈 값입니다.
대부분의 일반적인 물질의 굴절률은 인터넷에서 빠르게 검색할 수 있습니다.

Refraction

굴절 광선을 디버깅하는 것이 가장 어려운 부분입니다. 저는 보통 굴절 광선이 존재하는 경우 먼저 모든 빛이 굴절되도록 합니다. 이 프로젝트에서 장면에 두 개의 유리 구를 넣어보았고, 다음과 같은 결과를 얻었습니다 (아직 이것이 옳은지 그른지 알려드리지 않았지만, 곧 알려드리겠습니다!):
*이미지 15: 첫 번째 유리*
이게 맞을까요? 유리 구는 실제로 이상하게 보입니다. 하지만 아니요, 이것은 맞지 않습니다. 세상이 거꾸로 뒤집혀 있어야 하고 이상한 검은색 부분이 없어야 합니다. 저는 이미지 중앙을 관통하는 광선을 출력해보았고 명백히 잘못되었음을 확인했습니다. 이런 방법이 종종 효과적입니다.

Snell's Law

굴절은 스넬의 법칙으로 설명됩니다:
ηsinθ=ηsinθη⋅sinθ=η′⋅sinθ′
여기서 θ와 θ′는 법선으로부터의 각도이고, η와 η′("에타"와 "에타 프라임")는 굴절률입니다. 기하학적 구조는 다음과 같습니다:
*그림 17: 광선 굴절*
굴절된 광선의 방향을 결정하려면 sinθ′를 구해야 합니다:
sinθ=ηηsinθsinθ′=ηη′⋅sinθ
표면의 굴절된 쪽에는 굴절 광선 R′와 법선 n′가 있으며, 이들 사이에 각도 θ′가 존재합니다. R′는 n′에 수직인 부분과 평행한 부분으로 나눌 수 있습니다:
R′⊥와R′∥는다음과같습니다:R′⊥와 R′∥는 다음과 같습니다:
원한다면 직접 증명해볼 수 있지만, 이를 사실로 받아들이고 진행하겠습니다. 증명을 이해할 필요는 없습니다.
cosθ를 제외한 우변의 모든 항을 알고 있습니다. 두 벡터의 내적은 그들 사이 각도의 코사인 값으로 표현됩니다:
ab=abcosθa⋅b=|a||b|cosθ
ab를단위벡터로제한하면:a와 b를 단위 벡터로 제한하면:
ab=cosθa⋅b=cosθ
이제R′⊥를알고있는값들로다시쓸수있습니다:이제 R′⊥를 알고 있는 값들로 다시 쓸 수 있습니다:
이들을 결합하면 R′를 계산하는 함수를 작성할 수 있습니다:
// Vec3.h (or Vec3 utility section) inline Vec3 Reflect(const Vec3& v, const Vec3& n) { return v - 2.0 * Dot(v, n) * n; } inline Vec3 Refract(const Vec3& uv, const Vec3& n, double etaInOverEtaOut) { const double cosTheta = std::fmin(Dot(-uv, n), 1.0); const Vec3 refractPerpendicular = etaInOverEtaOut * (uv + cosTheta * n); const Vec3 refractParallel = -std::sqrt(std::fabs(1.0 - refractPerpendicular.LengthSquared())) * n; return refractPerpendicular + refractParallel; }
C++
복사
*리스트 71: [vec3.h] 굴절 함수*
그리고 항상 굴절하는 유전체 재질은 다음과 같습니다:
// Material.h class Metal : public Material { ... }; class Dielectric : public Material { public: explicit Dielectric(double refractionIndex) : mRefractionIndex(refractionIndex) { } bool Scatter( const Ray& rayIn, const HitRecord& hitRecord, Color& attenuation, Ray& scattered ) const override { attenuation = Color(1.0, 1.0, 1.0); const double etaInOverEtaOut = hitRecord.frontFace ? (1.0 / mRefractionIndex) : mRefractionIndex; const Vec3 unitDirection = UnitVector(rayIn.Direction()); const Vec3 refracted = Refract(unitDirection, hitRecord.normal, etaInOverEtaOut); scattered = Ray(hitRecord.point, refracted); return true; } private: // Refraction index (IOR). For air/vacuum -> material, typical glass is ~1.5. double mRefractionIndex = 1.0; };
C++
복사
*리스트 72: [material.h] 항상 굴절하는 유전체 재질 클래스*
이제 왼쪽 구를 굴절률이 약 1.5인 유리로 변경하여 굴절을 설명하도록 장면을 업데이트하겠습니다.
// main.cpp (scene setup excerpt) auto materialGround = make_shared<Lambertian>(Color(0.8, 0.8, 0.0)); auto materialCenter = make_shared<Lambertian>(Color(0.1, 0.2, 0.5)); auto materialLeft = make_shared<Dielectric>(1.50); auto materialRight = make_shared<Metal>(Color(0.8, 0.6, 0.2), 1.0);
C++
복사
*리스트 73: [main.cc] 왼쪽 구를 유리로 변경*
이렇게 하면 다음과 같은 결과를 얻습니다:
*이미지 16: 항상 굴절하는 유리 구*

Total Internal Reflection

굴절과 관련된 한 가지 까다로운 실제 문제는 스넬의 법칙을 사용하여 해결할 수 없는 광선 각도가 있다는 것입니다. 광선이 충분히 완만한 각도로 더 낮은 굴절률의 매질로 들어가면 90°보다 큰 각도로 굴절될 수 있습니다. 스넬의 법칙과 sinθ′의 유도로 돌아가면:
sinθ=ηηsinθsinθ′=ηη′⋅sinθ
광선이 유리 내부에 있고 외부가 공기인 경우(η=1.5, η′=1.0):
sinθ=1.51.0sinθsinθ′=1.51.0⋅sinθ
sinθ′의 값은 1보다 클 수 없습니다. 따라서 다음과 같다면:
1.51.0sinθ>1.01.51.0⋅sinθ>1.0
방정식의 양변 사이의 등식이 깨지고 해가 존재할 수 없습니다. 해가 존재하지 않으면 유리는 굴절할 수 없으므로 광선을 반사해야 합니다:
if (ri * sin_theta > 1.0) { // 반사해야 함 ... } else { // 굴절 가능 ... }
C++
복사
*리스트 74: [material.h] 광선이 굴절할 수 있는지 판단*
여기서 모든 빛이 반사되며, 실제로는 보통 고체 물체 내부에 있기 때문에 이를 전반사라고 합니다. 이것이 물속에 잠겨 있을 때 때때로 물과 공기의 경계면이 완벽한 거울처럼 작동하는 이유입니다 . 물속에서 위를 올려다보면 물 위의 것들을 볼 수 있지만, 표면 가까이에서 옆을 볼 때는 수면이 거울처럼 보입니다.
삼각 항등식을 사용하여 sin_theta를 구할 수 있습니다:
double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0); double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta); if (ri * sin_theta > 1.0) { // 반사해야 함 ... } else { // 굴절 가능 ... }
C++
복사
*리스트 75: [material.h] 광선이 굴절할 수 있는지 판단*
그리고 (가능한 경우) 항상 굴절하는 유전체 재질은 다음과 같습니다:
class Dielectric : public Material { public: explicit Dielectric(double refractionIndex) : mRefractionIndex(refractionIndex) { } bool Scatter( const Ray& rayIn, const HitRecord& hitRecord, Color& attenuation, Ray& scattered ) const override { attenuation = Color(1.0, 1.0, 1.0); const double refractionRatio = hitRecord.frontFace ? (1.0 / mRefractionIndex) : mRefractionIndex; const Vec3 unitDirection = UnitVector(rayIn.Direction()); const double cosTheta = std::fmin(Dot(-unitDirection, hitRecord.normal), 1.0); const double sinTheta = std::sqrt(1.0 - cosTheta * cosTheta); const bool cannotRefract = refractionRatio * sinTheta > 1.0; Vec3 direction; if (cannotRefract) { direction = Reflect(unitDirection, hitRecord.normal); } else { direction = Refract(unitDirection, hitRecord.normal, refractionRatio); } scattered = Ray(hitRecord.point, direction); return true; } private: // Index of Refraction (IOR), e.g. glass ≈ 1.5 double mRefractionIndex = 1.0; };
C++
복사
*리스트 76: [material.h] 반사를 포함한 유전체 재질 클래스*
감쇠는 항상 1입니다 . 유리 표면은 아무것도 흡수하지 않습니다.
새로운 dielectric::scatter() 함수로 이전 장면을 렌더링하면... 변화가 없습니다. 응?
음, 공기보다 큰 굴절률을 가진 재질의 구가 주어졌을 때, 광선-구 입구 지점이나 광선 출구 지점 어디에서도 전반사를 일으킬 입사 각도가 없다는 것이 밝혀졌습니다. 이것은 구의 기하학적 특성 때문인데, 스치듯 들어오는 광선은 항상 더 작은 각도로 굽어지고, 출구에서 다시 원래 각도로 굽어집니다.
그렇다면 어떻게 전반사를 설명할 수 있을까요? 구의 굴절률이 그것이 놓인 매질보다 작다면, 얕은 스치는 각도로 충돌시켜 전외부 반사를 얻을 수 있습니다. 이것으로 효과를 관찰하기에 충분할 것입니다.
물(굴절률 약 1.33)로 가득 찬 세계를 모델링하고, 구의 재질을 공기(굴절률 1.00)로 변경하겠습니다 — 공기 방울입니다! 이를 위해 왼쪽 구 재질의 굴절률을 다음과 같이 변경합니다:
공기의 굴절률물의 굴절률
auto materialGround = make_shared<Lambertian>(Color(0.8, 0.8, 0.0)); auto materialCenter = make_shared<Lambertian>(Color(0.1, 0.2, 0.5)); auto materialLeft = make_shared<Dielectric>(1.00 / 1.33); auto materialRight = make_shared<Metal>(Color(0.8, 0.6, 0.2), 1.0);
C++
복사
*리스트 77: [main.cc] 왼쪽 구는 물속의 공기 방울*
이 변경은 다음과 같은 렌더링을 생성합니다:
*이미지 17: 때로는 굴절하고 때로는 반사하는 공기 방울*
여기서 거의 직접적인 광선은 굴절하고, 스치는 광선은 반사하는 것을 볼 수 있습니다.

Schlick Approximation

실제 유리는 각도에 따라 반사율이 달라집니다. 창문을 가파른 각도로 보면 거울처럼 보이는 것을 생각해보세요. 이를 정확히 계산하는 복잡한 방정식이 있지만, 대부분의 사람들은 Christophe Schlick이 만든 간단하면서도 놀라울 정도로 정확한 다항식 근사를 사용합니다. 이를 적용한 완전한 유리 재질은 다음과 같습니다:
class Dielectric : public Material { public: explicit Dielectric(double refractionIndex) : mRefractionIndex(refractionIndex) { } bool Scatter( const Ray& rayIn, const HitRecord& hitRecord, Color& attenuation, Ray& scattered ) const override { attenuation = Color(1.0, 1.0, 1.0); const double refractionRatio = hitRecord.frontFace ? (1.0 / mRefractionIndex) : mRefractionIndex; const Vec3 unitDirection = UnitVector(rayIn.Direction()); const double cosTheta = std::fmin(Dot(-unitDirection, hitRecord.normal), 1.0); const double sinTheta = std::sqrt(1.0 - cosTheta * cosTheta); const bool cannotRefract = refractionRatio * sinTheta > 1.0; Vec3 direction; if (cannotRefract || Reflectance(cosTheta, refractionRatio) > RandomDouble()) { direction = Reflect(unitDirection, hitRecord.normal); } else { direction = Refract(unitDirection, hitRecord.normal, refractionRatio); } scattered = Ray(hitRecord.point, direction); return true; } private: // 진공 또는 공기 중 굴절률, 또는 재질의 굴절률을 // 둘러싼 매질의 굴절률로 나눈 비율 double mRefractionIndex; static double Reflectance(double cosine, double refractionIndex) { // Schlick의 반사율 근사 사용 auto r0 = (1.0 - refractionIndex) / (1.0 + refractionIndex); r0 = r0 * r0; return r0 + (1.0 - r0) * std::pow((1.0 - cosine), 5); } };
C++
복사
*리스트 78: [material.h] 완전한 유리 재질*

Modeling a Hollow Glass Sphere

속이 빈 유리 구를 모델링해봅시다. 이것은 어느 정도 두께를 가진 구이며, 그 안에 공기로 채워진 또 다른 구가 있습니다. 광선이 이러한 물체를 통과하는 경로를 생각해보면, 광선은 바깥 구에 충돌하여 굴절되고, 안쪽 구에 충돌하며(충돌한다고 가정할 때), 두 번째로 굴절되고, 내부의 공기를 통과합니다. 그런 다음 계속 진행하여 안쪽 구의 내부 표면에 충돌하고, 다시 굴절되고, 바깥 구의 내부 표면에 충돌한 후, 마지막으로 굴절되어 장면의 대기로 빠져나갑니다.
바깥 구는 일반적인 유리 구로 모델링되며, 굴절률은 약 1.50입니다(외부 공기에서 유리로의 굴절을 모델링). 안쪽 구는 조금 다릅니다. 구의 굴절률은 둘러싸고 있는 바깥 구의 재질에 상대적이어야 하며, 따라서 유리에서 내부 공기로의 전환을 모델링합니다.
이것은 실제로 간단하게 지정할 수 있습니다. 유전체 재질의 refraction_index 매개변수는 물체의 굴절률을 둘러싼 매질의 굴절률로 나눈 비율로 해석될 수 있기 때문입니다. 이 경우, 안쪽 구는 공기의 굴절률(안쪽 구 재질)을 유리의 굴절률(둘러싼 매질)로 나눈 값, 즉 1.00/1.50=0.67의 굴절률을 갖게 됩니다.
코드는 다음과 같습니다:
... auto materialGround = make_shared<Lambertian>(Color(0.8, 0.8, 0.0)); auto materialCenter = make_shared<Lambertian>(Color(0.1, 0.2, 0.5)); auto materialLeft = make_shared<Dielectric>(1.50); auto materialBubble = make_shared<Dielectric>(1.0 / 1.50); auto materialRight = make_shared<Metal>(Color(0.8, 0.6, 0.2), 0.0); world.Add(make_shared<Sphere>(Point3(0.0, -100.5, -1.0), 100.0, materialGround)); world.Add(make_shared<Sphere>(Point3(0.0, 0.0, -1.2), 0.5, materialCenter)); world.Add(make_shared<Sphere>(Point3(-1.0, 0.0, -1.0), 0.5, materialLeft)); world.Add(make_shared<Sphere>(Point3(-1.0, 0.0, -1.0), 0.4, materialBubble)); world.Add(make_shared<Sphere>(Point3(1.0, 0.0, -1.0), 0.5, materialRight)); ...
C++
복사
*리스트 79: [main.cc] 속이 빈 유리 구가 있는 장면*
결과는 다음과 같습니다:
*이미지 18: 속이 빈 유리 구*