광선을 쏘았을 때의 궤적인 1차 방정식과 구체인 원을 나타낼 수 있는 2차식이 존재한다. 근의 공식을 이용해 이 둘을 조합해 근의 유무에 따른 충돌을 구해낼 수 있다.
jseo님이 작성하신 게시물을 참조하여 개발하였다.
원기둥이란 기본적으로 원과 평면을 합친 것과 같다. 원기둥의 원통은 벡터 방향을 바라보고 원을 차곡차곡 쌓은 것과 같고, 위아래 뚜껑은 범위가 정해져있는 평면을 구하는 식이다.
t_bool hit_cylinder(t_object *cy_obj, t_ray *ray, t_hit_record *rec)
{
t_cylinder *cy;
double a;
double half_b;
double c;
t_vec3 u;
t_vec3 o;
t_vec3 delta_P;
double r;
double discriminant; // 판별식
double sqrtd;
double root;
double hit_height;
cy = cy_obj->element;
u = ray->dir;
o = cy->dir;
r = cy->diameter / 2;
delta_P = vminus(ray->origin, cy->center);
a = vlength2(vcross(u, o));
half_b = vdot(vcross(u, o), vcross(delta_P, o));
c = vlength2(vcross(delta_P, o)) - pow(r, 2);
discriminant = half_b * half_b - a * c;
if (discriminant < 0)
return (FALSE);
// 이 시점에서 판별식이 참이 나왔기에 근이 존재한다고 판단한다.
sqrtd = sqrt(discriminant);
root = (-half_b - sqrtd) / a; // 근의 공식 해, 작은 근부터 고려.
if (root < rec->tmin || rec->tmax < root)
{
root = (-half_b + sqrtd) / a;
if (root < rec->tmin || rec->tmax < root)
return (FALSE);
}
if (!(hit_height = cy_boundary(cy, ray_at(ray, root))))
return (FALSE);
rec->t = root; // 광선의 원점과 교점까지의 거리를 rec에 저장한다.
rec->p = ray_at(ray, root); // 교점의 좌표를 rec에 저장한다.
rec->normal = get_cylinder_normal(cy, rec->p, hit_height); // vmult(ray->dir, root)하면 안돼!!!
set_face_normal(ray, rec);
rec->albedo = cy_obj->albedo;
return (TRUE);
}
이것을 우리가 잘 아는 근의 공식으로 바꾸어 주면 다음과 같다.
b를 간략화시킨다면 다음과 같이 변형할 수 있다. (b = b/2)
판별식을 지나왔기 때문에 근이 존재(충돌했다)한다고 판정
판별식(b^2 - ac)의 제곱근이 +-를 하는 값이 되므로 광선과 충돌 지점까지의 거리는 (-half_b +- sqrtd) / a;
가 된다. 작은 근이 가장 먼저 부딪힌 근이 될 것이므로 작은 근부터 체크해서 광선에 부딪힌 광선 중 가장 가까운 지를 체크한다.
평면의 공식과 같은 공식을 사용한다. 범위가 한정된 평면이라고 생각하면 이해하기가 쉽다.
int hit_cylinder_cap(t_object *cy_obj, t_ray *ray, t_hit_record *rec, double height)
{
const t_cylinder *cy = cy_obj->element;
const double r = cy->diameter / 2;
const t_vec3 circle_center = vplus(cy->center, vmult(cy->dir, height));
const float root = vdot(vminus(circle_center, ray->origin), cy->dir) \
/ vdot(ray->dir, cy->dir);
const float diameter = vlength(vminus(circle_center, ray_at(ray, root)));
if (fabs(r) < fabs(diameter))
return (0);
if (root < rec->tmin || rec->tmax < root)
return (0);
rec->t = root;
rec->p = ray_at(ray, root);
rec->normal = vunit(vminus(circle_center, ray->origin)); // vmult(ray->dir, root)하면 안돼!!!
set_face_normal(ray, rec);
rec->albedo = cy_obj->albedo;
return (1);
}
광선의 원점에서부터 C까지의 벡터가 F라고 했을 때, F와 원기둥의 방향벡터인 o와의 내적으로 t를 구할 수 있다.
이를 토대로 작성한 최종 원기둥 구현 코드는 다음과 같다.
#include "structures.h"
#include "utils.h"
#include "trace.h"
int cy_boundary(t_cylinder *cy, t_vec3 at_point, t_cylinops *c)
{
double hit_height;
double max_height;
hit_height = vdot(vminus(at_point, cy->center), cy->dir);
max_height = cy->height / 2;
c->hit_height = hit_height;
if (fabs(c->hit_height) > max_height)
return (0);
return (1);
}
t_vec3 get_cylinder_normal(t_cylinder *cy, t_vec3 at_point, double hit_height)
{
t_point3 hit_center;
t_vec3 normal;
hit_center = vplus(cy->center, vmult(cy->dir, hit_height));
normal = vminus(at_point, hit_center);
return (vunit(normal));
}
int hit_cylinder_cap(t_object *cy_obj, t_ray *ray, t_hit_record *rec, double height)
{
const t_cylinder *cy = cy_obj->element;
const double r = cy->diameter / 2;
const t_vec3 circle_center = vplus(cy->center, vmult(cy->dir, height));
const float root = vdot(vminus(circle_center, ray->origin), cy->dir)
const float diameter = vlength(vminus(circle_center, ray_at(ray, root)));
if (fabs(r) < fabs(diameter))
return (0);
if (root < rec->tmin || rec->tmax < root)
return (0);
rec->t = root;
rec->p = ray_at(ray, root);
rec->tmax = rec->t;
if (0 < height)
rec->normal = cy->dir;
else
rec->normal = vmult(cy->dir, -1);
// rec->normal = vunit(vminus(circle_center, ray->origin)); // vmult(ray->dir, root)하면 안돼!!!
set_face_normal(ray, rec);
rec->albedo = cy_obj->albedo;
return (1);
}
int hit_cylinder_side(t_object *cy_obj, t_ray *ray, t_hit_record *rec)
{
t_cylinder *cy;
//a, b, c는 각각 t에 관한 근의 공식 2차 방정식의 계수
double a;
double half_b;
double c;
t_vec3 u;
t_vec3 o;
t_vec3 delta_P;
double r;
double discriminant; // 판별식
double sqrtd;
double root;
double hit_height;
cy = cy_obj->element;
u = ray->dir;
o = cy->dir;
r = cy->diameter / 2;
delta_P = vminus(ray->origin, cy->center);
a = vlength2(vcross(u, o));
half_b = vdot(vcross(u, o), vcross(delta_P, o));
c = vlength2(vcross(delta_P, o)) - pow(r, 2);
discriminant = half_b * half_b - a * c;
if (discriminant < 0)
return (0);
// 이 시점에서 판별식이 참이 나왔기에 근이 존재한다고 판단한다.
sqrtd = sqrt(discriminant);
root = (-half_b - sqrtd) / a; // 근의 공식 해, 작은 근부터 고려.
if (root < rec->tmin || rec->tmax < root)
{
root = (-half_b + sqrtd) / a;
if (root < rec->tmin || rec->tmax < root)
return (0);
}
// print_vec(vmult(ray->dir, root));
// print_vec(ray_at(ray, root));
if (!(hit_height = cy_boundary(cy, ray_at(ray, root))))
return (0);
rec->t = root; // 광선의 원점과 교점까지의 거리를 rec에 저장한다.
rec->p = ray_at(ray, root); // 교점의 좌표를 rec에 저장한다.
rec->normal = get_cylinder_normal(cy, rec->p, hit_height);
set_face_normal(ray, rec);
rec->albedo = cy_obj->albedo;
return (1);
}
t_bool hit_cylinder(t_object *cy_obj, t_ray *ray, t_hit_record *rec)
{
const t_cylinder *cy = cy_obj->element;
int result;
result = 0;
result += hit_cylinder_cap(cy_obj, ray, rec, cy->height / 2);
result += hit_cylinder_cap(cy_obj, ray, rec, -(cy->height / 2));
result += hit_cylinder_side(cy_obj, ray, rec);
return (result);
}