개요
Chapter 9에서는 유전체(Dielectric) 재질을 추가해서 유리, 물처럼 빛이 통과하는 물질을 렌더링한다.
레이가 경계면에 닿으면 상황에 따라
•
굴절(refraction): 다른 매질로 들어가며 진행 방향이 꺾임
•
반사(reflection): 거울처럼 튕겨 나감
•
전반사(total internal reflection): 굴절이 물리적으로 불가능해서 100% 반사
가 발생한다.
이 챕터의 핵심은 “매번 반사/굴절을 동시에 계산해서 섞는 것”이 아니라,
프레넬(Fresnel) 반사율을 확률로 보고 반사 또는 굴절 중 하나를 샘플링하는 것이다. (GPU에서는 특히 이 방식이 깔끔하다.)
굴절의 기하: 스넬의 법칙
굴절률이 다른 두 매질의 경계를 지날 때 다음이 성립한다.
•
: 입사 매질 굴절률 (예: 공기 1.0)
•
: 출사 매질 굴절률 (예: 유리 1.5)
•
: 법선 기준 입사각
•
: 법선 기준 굴절각
실제 구현에서는 각도를 직접 구하기보다, 단위벡터로부터
•
•
를 구해서 굴절 가능 여부부터 판단한다.
굴절률이 큰 매질에서 작은 매질로 나갈 때(예: 유리 → 공기), 입사각이 커지면 굴절이 불가능해지고 전부 반사된다.
bool cannotRefract = refractionRatio * sinTheta > 1.0;
C++
복사
여기서 refractionRatio는 에 해당한다.
프레넬 효과와 Schlick 근사
유리는 정면에서는 대부분 통과하지만, 비스듬할수록 반사가 강해진다.
이 프레넬 반사율을 매번 정확히 계산할 수도 있지만, 레이트레이싱 입문에서는 Schlick 근사가 매우 널리 쓰인다.
•
: 굴절률 (IOR)
•
(정면)에서 유리(1.5)의 (약 4%)
•
로 갈수록
구현에서는 R(θ)를 반사 확률로 보고 난수로 분기한다.
그림으로 이해하기 (굴절 / 전반사)
flowchart LR A["공기 (IOR=1.0)"] -->|"입사"| B((경계면)) B -->|"굴절 (대부분)"| C["유리 (IOR=1.5)"] B -->|"반사 (프레넬)"| A
Mermaid
복사
flowchart LR G["유리 (IOR=1.5)"] -->|"큰 입사각"| H((경계면)) H -->|"전반사 (굴절 불가)"| G H -. "굴절 시도" .-> I["공기 (IOR=1.0)"]
Mermaid
복사
속이 빈 유리 구체 (Hollow Glass Sphere)
유리 구체를 “겉껍질 + 속공간”으로 만들고 싶으면, 같은 중심에 두 개의 구체를 겹친다.
•
바깥 구체: 반지름 +0.5
•
안쪽 구체: 반지름 -0.45 (음수)
음수 반지름을 쓰면 기하적으로는 동일한 구체지만
법선 방향이 뒤집힌(outward normal이 반대로) 것처럼 동작한다.
그 결과 유리 껍질 내부에 빈 공간이 생기고, 레이가
외부 → 유리 → 공기(빈공간) → 유리 → 외부
처럼 여러 번 굴절한다.
GPU 구현에서 중요한 점은 “법선/앞면 판정”을 확실히 통일하는 것이다. 그래야 굴절률 비율이 깔끔하게 결정된다.
변경 파일 요약
•
Dielectric 클래스: 굴절률(IOR) 기반 유전체 재질
•
scatter()
◦
전반사 여부 판단
◦
Schlick 근사로 반사 확률 계산
◦
난수로 반사/굴절 중 하나를 선택
•
reflectance()
◦
Schlick 근사 구현
•
bFrontFace 활용
◦
레이가 “겉면”에서 들어오는지 “안쪽면”에서 나가는지에 따라 굴절률 비율을 자동 전환
•
bFrontFace: 레이가 표면의 앞면을 맞았는지 저장
•
SetFaceNormal(ray, outwardNormal)
◦
bFrontFace 계산
◦
노말을 항상 레이 진행의 반대 방향을 향하도록 통일
•
직접 법선 대입 대신 SetFaceNormal() 사용
•
음수 반지름도 정상적으로 동작
•
#include "Dielectric.h"
•
CreateWorld()에 유전체 구체 2개 추가
•
FreeWorld() 루프 및 리스트 할당 크기 수정
C++ 코드 (핵심 부분)
아래는 책의 의도를 유지하면서 C++ 스타일로 정리한 예시다. (CUDA에서도 거의 동일하게 옮길 수 있다.)
// reflect: v - 2*dot(v,n)*n
inline vec3 reflect(const vec3& v, const vec3& n) {
return v - 2.0 * dot(v, n) * n;
}
// refract: Snell's law 형태로 분해해서 계산
inline vec3 refract(const vec3& uv, const vec3& n, double etaiOverEtat) {
auto cosTheta = fmin(dot(-uv, n), 1.0);
vec3 rOutPerp = etaiOverEtat * (uv + cosTheta * n);
vec3 rOutParallel = -sqrt(fabs(1.0 - rOutPerp.length_squared())) * n;
return rOutPerp + rOutParallel;
}
inline double reflectance(double cosine, double refIdx) {
// Schlick approximation
auto r0 = (1 - refIdx) / (1 + refIdx);
r0 = r0 * r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}
class dielectric : public material {
public:
explicit dielectric(double indexOfRefraction)
: ir(indexOfRefraction) {}
bool scatter(
const ray& rIn,
const hit_record& rec,
color& attenuation,
ray& scattered,
random_engine& rng
) const override {
attenuation = color(1.0, 1.0, 1.0); // 유전체는 흡수 없다고 가정
double refractionRatio = rec.front_face ? (1.0 / ir) : ir;
vec3 unitDirection = unit_vector(rIn.direction());
double cosTheta = fmin(dot(-unitDirection, rec.normal), 1.0);
double sinTheta = sqrt(1.0 - cosTheta * cosTheta);
bool cannotRefract = refractionRatio * sinTheta > 1.0;
double prob = reflectance(cosTheta, refractionRatio);
bool shouldReflect = cannotRefract || (uniform01(rng) < prob);
vec3 direction = shouldReflect
? reflect(unitDirection, rec.normal)
: refract(unitDirection, rec.normal, refractionRatio);
scattered = ray(rec.p, direction);
return true;
}
private:
double ir; // Index of Refraction
};
C++
복사
random_engine, uniform01() 등은 프로젝트에 맞게 curand 혹은 기존 RNG로 대체하면 된다.
GPU 구현 포인트 (정리)
•
SetFaceNormal로 법선을 통일하면, 굴절률 비율을 front_face 하나로 결정할 수 있다.
•
음수 반지름 트릭은 “기하를 바꾸지 않고 내부 경계면의 노말 방향만” 바꾸는 효과가 있어, hollow glass를 간단히 만든다.
•
유전체는 한 번 히트할 때마다 반사 또는 굴절 하나만 선택하므로, 경로 추적이 자연스럽게 분기된다.




