레이 트레이싱에서 사물의 외관을 결정짓는 핵심 요소는 광선이 표면과 충돌했을 때 어떻게 상호작용하는지를 정의하는 재질(Material) 시스템입니다. 또한, 실제 카메라와 유사한 물리적 특성을 부여하기 위해 시야각(FOV)과 초점 거리, 조리개 설정을 시뮬레이션하는 과정이 필요합니다.
재질 시스템의 설계와 추상화
다양한 물리적 속성을 가진 물체를 표현하기 위해 재질을 추상 클래스로 정의합니다. 각 재질은 광선이 충돌했을 때 (1) 광선이 산란되는 방식과 (2) 빛의 감쇄(Attenuation) 정도를 계산하는 인터페이스를 제공해야 합니다.
class IMaterial {
public:
// 산란 함수: 입사광, 충돌 정보, 감쇄율을 바탕으로 산란된 광선을 생성
virtual bool handle_scatter(
const Ray& ray_in,
const HitRecord& record,
Vec3& attenuation,
Ray& scattered_ray
) const = 0;
};
충돌 정보 구조체(HitRecord)에는 어떤 재질과 부딪혔는지 알 수 있도록 해당 재질의 포인터를 포함시킵니다.
금속 재질의 반사 모델링
금속과 같은 매끄러운 표면은 입사각과 반사각이 동일한 정반사(Specular Reflection)의 원리를 따릅니다. 반사 벡터는 다음과 같은 공식을 통해 계산할 수 있습니다.
Vec3 reflect_vector(const Vec3& v, const Vec3& n) {
return v - 2.0f * dot(v, n) * n;
}
실제 금속은 완벽하게 매끄럽지 않으므로, 반사 벡터의 끝점에 무작위성을 부여하여 거친 표면(Roughness/Fuzz)을 표현할 수 있습니다.
class MetalMaterial : public IMaterial {
public:
MetalMaterial(const Vec3& color, float blur) : reflectance(color), roughness(blur < 1 ? blur : 1) {}
virtual bool handle_scatter(const Ray& ray_in, const HitRecord& record, Vec3& attenuation, Ray& scattered_ray) const {
Vec3 reflected = reflect_vector(unit_vector(ray_in.direction()), record.normal);
// 거칠기 계수를 적용한 산란광 생성
scattered_ray = Ray(record.p, reflected + roughness * random_in_unit_sphere());
attenuation = reflectance;
return (dot(scattered_ray.direction(), record.normal) > 0);
}
Vec3 reflectance;
float roughness;
};
유리 및 투명체(Dielectrics)의 굴절
물이나 유리 같은 유전체는 광선이 통과할 때 굴절(Refraction)이 발생합니다. 이는 스넬의 법칙(Snell's Law)에 의해 제어되며, 입사각과 매질의 굴절률에 따라 결정됩니다. 또한, 특정 각도에서는 굴절 대신 전반사가 일어나기도 합니다.
굴절률에 따른 반사율의 변화는 쉴릭의 근사식(Schlick's Approximation)을 사용하여 효율적으로 계산할 수 있습니다.
float schlick_approximation(float cosine, float ref_idx) {
float r0 = (1 - ref_idx) / (1 + ref_idx);
r0 = r0 * r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}
유전체 재질은 빛을 흡수하지 않는다고 가정할 때 감쇄율을 항상 1로 설정하며, 반사율과 굴절률을 확률적으로 선택하여 시뮬레이션합니다.
위치 및 방향 조절이 가능한 카메라 모델
카메라는 단순히 고정된 위치에서 보는 것이 아니라, 임의의 위치(LookFrom)에서 특정 지점(LookAt)을 바라볼 수 있어야 합니다. 이를 위해 수직 시야각(vFOV)과 종횡비(Aspect Ratio)를 활용하여 투영 평면을 정의합니다.
class PhysicsCamera {
public:
PhysicsCamera(Vec3 origin_pos, Vec3 look_at, Vec3 world_up, float vertical_fov, float aspect_ratio) {
float theta = vertical_fov * M_PI / 180.0f;
float h = tan(theta / 2.0f);
float viewport_height = 2.0f * h;
float viewport_width = aspect_ratio * viewport_height;
// 카메라 좌표계(w, u, v) 생성
w_axis = unit_vector(origin_pos - look_at);
u_axis = unit_vector(cross(world_up, w_axis));
v_axis = cross(w_axis, u_axis);
origin = origin_pos;
horizontal = viewport_width * u_axis;
vertical = viewport_height * v_axis;
lower_left_corner = origin - horizontal/2.0f - vertical/2.0f - w_axis;
}
Ray create_ray(float s, float t) {
return Ray(origin, lower_left_corner + s*horizontal + t*vertical - origin);
}
private:
Vec3 origin, lower_left_corner, horizontal, vertical;
Vec3 u_axis, v_axis, w_axis;
};
피사체 심도(Depth of Field) 구현
현실의 카메라는 유한한 크기의 조리개를 가지고 있어, 초점이 맞지 않는 부분에 산란이 발생하는 '보케(Bokeh)' 효과가 나타납니다. 이를 위해 광선을 생성할 때 가상의 렌즈 원판(Lens Disk) 위의 임의의 지점에서 광선을 출발시킵니다.
Ray get_defocus_ray(float s, float t) {
// 렌즈 반지름 내의 무작위 위치 계산
Vec3 random_disk = lens_radius * random_in_unit_disk();
Vec3 offset = u_axis * random_disk.x() + v_axis * random_disk.y();
// 오프셋을 적용하여 초점 평면을 향하는 광선 생성
return Ray(origin + offset, lower_left_corner + s*horizontal + t*vertical - origin - offset);
}
이러한 기법들을 종합하면, 수많은 구체와 다양한 재질이 어우러진 복잡한 씬을 렌더링할 수 있습니다. 램버시안(Lambertian) 확산광, 금속의 광택, 유리의 투명함, 그리고 피사체 심도가 적용된 최종 결과물은 물리적 정교함을 더해줍니다.
이후 단계에서는 더욱 복잡한 렌더링 기법인 모션 블러(Motion Blur), 경계 볼륨 계층 구조(BVH)를 통한 성능 최적화, 텍스처 매핑 및 광원 구현 등을 통해 레이 트레이서의 기능을 확장할 수 있습니다.