miniRT 진행(5) 진행과정과 알아야 하는 내용 정리

naranghae·2021년 1월 18일
0

miniRT

목록 보기
7/10

광선 (ray)

(사진: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-generating-camera-rays)
그림으로 알아보자면, 여기서 origin은 있는 위치를 뜻한다.
그리고 direction은 눈이 어느한 곳을 응시할 때 그 응시하는 방향을 뜻한다.
그러므로 origin은 시작포인트로 O라고 하고 dir는 거리를 나타내어 R, t는 거리의 길이같은 것이고, 광선의 한지점을 $$\widehat{P}$$라 한다.
이 모든 것을 수식으로 나타내면 $$\widehat{P} = O + tR$$ 이 되는 것이다.
그리고 이것을 코드로 나타내면

typedef struct	s_vec
{
    double	x;
    double	y;
    double	z;
}		t_vec;


typedef struct  s_ray
{
    t_vec	orig;
    t_vec	dir;
    double	t;
}		t_ray;

t_vec		ft_ray_at(t_ray ray, double t)
{
	return (ft_vec_create(ray.orig.x + t * ray.dir.x,
	ray.orig.y + t * ray.dir.y,
	ray.orig.z + t * ray.dir.z));
}

위와 같이 되고, 그러면 ray의 출발점, ray가 가리키는 거리가 존재하게 된다.

벡터의 뺄셈

210125(월) 벡터의 뺄셈 추가

벡터의 덧셈은 이해하기 쉬우므로 적지 않겠다! 하지만 뺄셈은 좀 헷갈려서 적어 놓으려고 한다.
위의 그림에서 나오듯 a,b 벡터로 나타내면 아하! 그렇구만~~ 하고 넘어갔는데 우리는 벡터를 $(x,y,z)$로 나타내고 있기 때문에 어디서 어디로 가리키는 방향 벡터인지 헷갈릴 수 있다.
그래서 저 그림에서 노란색 점을 우리가 나타낸 벡터의 좌표라고 하고,

t_vec		ft_vec_create(double x, double y, double z)
{
	t_vec	new_vec;

	new_vec.x = x;
	new_vec.y = y;
	new_vec.z = z;
	return (new_vec);
}

t_vec		ft_vec_sub(t_vec u, t_vec v)
{
	return (ft_vec_create(u.x - v.x, u.y - v.y, u.z - v.z));
}

이러한 벡터 계산 코드를 만들었을 때 ft_vec_sub(light->pos, hit->pos) 라고 하면 ray가 오브젝트에 부딪힌 곳(hit->pos)에서 빛이 있는 곳(light->pos)으로 방향을 나타낸다, 라고 할 수 있다.

벡터의 내적 (dot product)

참고

  1. https://waraccc.tistory.com/18?category=675628
  2. https://tadis.tistory.com/entry/DirectX-3D-%EA%B8%B0%EB%B3%B8%EA%B0%9C%EB%85%90-1-Vector

보통 내적은 벡터의 방향이 얼마나 일치하는지의 용도로 쓰인다고 한다. 그리곧 두 벡터간의 cos값이 된다.

$$\underset{a}{\rightarrow}⋅ \underset{b}{\rightarrow}= \begin{bmatrix}
a_{x} & a_{y} & a_{z}
\end{bmatrix} ⋅ \begin{bmatrix}
b_{x} & b_{y} & b_{z}
\end{bmatrix} = a_{x}b_{x}+a_{y}b_{y}+a_{z}b_{z}$$
이렇게 벡터는 실수값이 나온다.
내적의 결과가 양수로 나오면 두 벡터의 사이각이 $$0^{\circ}\sim 90^{\circ}$$, $$0^{\circ}\sim -90^{\circ}$$ 라는 것이고, 음수로 나오면 두 벡터의 사이각이 $$90^{\circ}\sim 180^{\circ}$$, $$-90^{\circ}\sim -180^{\circ}$$ 라는 뜻이고, $0$이 나오면 두 백터가 수직이라는 뜻이다.
여기서 중요한 건, 각도가 몇 도 인지 까지 계산하지 않아도 내적 결과값의 부호로 각도의 범위를 알아낼 수 있다는 것이다.
벡터의 내적을 코드로 나타내면 아래와 같다.

double		ft_vec_dot(t_vec u, t_vec v)
{
	return ((u.x * v.x) + (u.y * v.y) + (u.z * v.z));
}
}

이러한 내적으로 반사값도 구할 수 있는데

이렇게 P가 입사벡터 R이 반사벡터 n이 법선벡터라고 하자.
우선, 입사벡터 P의 역벡터 -P 를 n의 연장선상에 투영시켜 투영벡터 n(-P·n)를 구한다.

입사 벡터 P 의 시작 위치를 원점에 위치시키고, 여기에 n(-P·n) 를 더하면, 입사면에 투영된 벡터의 위치를 구할수 있다.
입사벡터 P 에 n(-P·n) 를 1번 더하면, 입사면에 투영된 위치를 구할 수 있고, 2번 더하면 반사벡터 R 을 구할 수 있음을 알수 있다.

결국, 반사벡터 R 은 R = P + 2n(-P·n)
(출처: https://toymaker.tistory.com/entry/반사-벡터-Reflection-Vector [ToyMaker])

이것을 코드로 나타내보자.

t_vec		ft_vec_multi_double(t_vec v, double t)
{
	return (ft_vec_create(v.x * t, v.y * t, v.z * t));
}

t_vec		vec_reflect(t_vec v, t_vec n)
{
	t_vec	tmp;
	t_vec	refl;

	tmp = ft_vec_multi_double(n, 2 * ft_vec_dot(v, n));
	refl = ft_vec_sub(v, tmp);
	return (refl);
}

이렇게 반사벡터까지 내적을 통해 알 수 있다.

벡터의 외적(cross product)

내적과 다르게 외적은 계산값이 벡터로 나온다.
https://ko.wikipedia.org/wiki/%EB%B2%A1%ED%84%B0%EA%B3%B1
여기서 자세한 내용을 확인하기 바라며, 쉽게 말하면 외적은 두 백터의 수직인 벡터로 나오게 된다.

$$\underset{a}{\rightarrow}\times \underset{b}{\rightarrow}= \begin{bmatrix}
a_{x} & a_{y} & a_{z}
\end{bmatrix} \times \begin{bmatrix}
b_{x} & b_{y} & b_{z}
\end{bmatrix} = \begin{bmatrix}
a_{y}b_{z}-a_{z}b_{y}&a_{z}b_{x}-a_{x}b_{z} & a_{x}b_{y}-a_{y}b_{x}
\end{bmatrix}$$
이것을 코드로 나타내면 아래와 같다.

t_vec		ft_vec_cross(t_vec u, t_vec v)
{
	return (ft_vec_create(
		u.y * v.z - u.z * v.y,
		u.z * v.x - u.x * v.z,
		u.x * v.y - u.y * v.x));
}

좌표계와 카메라

참고
https://raytracing.github.io/books/RayTracingInOneWeekend.html
https://anthony012.tistory.com/entry/Tips-on-Purchasing-Tele-Lens

내가 만든 월드 좌표는 왼손 좌표계를 따른다.

여기서 $\theta$는 get_radian으로 구한다.

t_vec		set_cam(double fov, t_area resolution, int y, int x)
{
	double	d_x;
	double	d_y;
	double	aspect_ratio;
	double	h;
	
	h = tan(get_radian(fov) * 0.5);
	aspect_ratio = (double)resolution.w / (double)resolution.h;
	d_x = (2 * (x + 0.5) / (double)resolution.w - 1) * aspect_ratio * h;
	d_y = (1 - 2 * (y + 0.5) / (double)resolution.h) * h;
	return (ft_vec_create(d_x, d_y, 1));
}

우리가 빛이나 카메라, 오브젝트등의 위치를 설정할 때 위의 그림에 따른 위치에 지정된다.
그래서 z방향을 신경을 안쓰면 오브젝트를 설정해도 안보일 수 가 있다.
가령 구체가 center (0,0,5) radius 3 인데 카메라 위치가 (0,0,10)이다? 의 반지름이 3이니까 z방향으로 8까지는 보이겠지만 9라면 안보일 것이다.

그러면 우리는 카메라가 오브젝트를 본다고 할 때 어떻게 오브젝트를 봤다고 인식시켜야할까?

조잡하지만 이 그림을 보면 저 눈이 카메라라고 한다면 저렇게 한 픽셀씩 보면서 ray를 쏘아낸다. 그러면 저 ray를 나타내기 위해 각 픽셀의 위치를 구하고 카메라 위치와 더하면 바로 ray가 된다.

그럼 저렇게 '어디를 볼까'가 문제가 된다. 카메라 위치가 있고 카메라가 어디를 바라보는지 알아야 물체를 확인 할 것 아닌가.

이제 외적을 쓸 데가 왔다.
드디어 다른 축을 만들 때가 된 것이다!
먼저 카메라가 바라보는 축을 만드는 것이다.
가령 법선 벡터가 $(-1,0,0)$ 이라 하자.
그러면 x축 음의 방향을 보는 카메라가 존재하게 되는 것이다.
월드 좌표계는 위와 같고 그러면 이제 카메라가 보는 축을 구해야한다.
카메라가 가지고 있는 기본 축벡터를 rdup이라 하고 y축 양의 방향을 보게 한다.
그리고 법선벡터에 따른 카메라 축을 구해보면,

이렇게 나타내진다. 위의 그림을 보면 월드좌표계와 카메라 좌표계가 다르다는 것을 알 수 있다.
그래서 카메라를 기준으로 horiz는 x축, vup은 y축, nv는 z축이라는 것을 알 수 있다.
그리고 우선 이렇게 나온 축의 값을 ray의 dir를 구하기 위해 우선 vec_orig으로 둔다.
그리고 ray의 dir를 구해보자.
화면에 비춰질 화면이라고 하면 set_cam을 통해서 가로x세로로 만든다.
이것을 dir라고 하고 이 값을 이제 처음에 만들었던 방향벡터(look)와 내적을 통해 방향을 계산하고 카메라의 현재 위치를 더해서 화면을 방향벡터에 맞게 조정해야 한다.
그렇게 구한 vec_dir와 vec_orig을 빼면 이제 ray의 dir가 구해지고, 마지막으로 이 vec_orig와 vec_dir로 ray를 만들면 끝이 난다.

그리고 만약에 카메라가 바라보는 위치와 rvup가 같다면 이 축을 바꿔야 한다.
왜냐하면 외적을 통해 다른 축을 구해야하는데 평행하게 되면 외적 값이 0이 나오기 때문이다.
이러한 일련의 과정을 나타낸 코드이다.

t_camvar	ft_look_at(t_vec orig, t_vec nv)
{
	t_vec		rdup;
	t_vec		horiz;
	t_vec		vup;
	t_camvar	var;

	rdup = ft_vec_create(0, 1, 0);
	if (nv.y != 0.0 && (nv.x == 0 && nv.z == 0))
		rdup = ft_vec_create(0, 0, 1);
	rdup = ft_vec_unit(rdup);
	horiz = ft_vec_unit(ft_vec_cross(rdup, nv));
	vup = ft_vec_unit(ft_vec_cross(nv, horiz));
	var.rot[0] = ft_vec_create(horiz.x, vup.x, nv.x);
	var.rot[1] = ft_vec_create(horiz.y, vup.y, nv.y);
	var.rot[2] = ft_vec_create(horiz.z, vup.z, nv.z);
	var.rot[3] = ft_vec_create(orig.x, orig.y, orig.z);
	return (var);
}

t_vec		dir_calc(t_vec vec, t_camvar var)
{
	t_vec	v;

	v.x = ft_vec_dot(vec, var.rot[0]) + var.rot[3].x;
	v.y = ft_vec_dot(vec, var.rot[1]) + var.rot[3].y;
	v.z = ft_vec_dot(vec, var.rot[2]) + var.rot[3].z;
	return (v);
}

t_ray		create_ray(t_camera *camera, t_area resolution, t_area pixel)
{
	t_vec		vec_orig;
	t_vec		vec_dir;
	t_camvar	look;

	look = ft_look_at(camera->view_point, camera->normal_vec);
	vec_orig = dir_calc(ft_vec_create(0, 0, 0), look);
	vec_dir = set_cam(camera->fov, resolution, pixel.h, pixel.w);
	vec_dir = dir_calc(vec_dir, look);
	vec_dir = ft_vec_unit(ft_vec_sub(vec_dir, vec_orig));
	return (new_ray(vec_orig, vec_dir));
}

구 방정식

구 방정식의 기본형은 구의 중심이 $(0,0,0)$이라 할 때 $x^{2}+y^{2}+z^{2}=r^{2}$이다. 당연히 r은 구의 반지름이다.
그럼 구의 중심 $C$가 $(C_{x}, C_{y}, C_{z})$라고 할 때 구의 방정식은 어떻게 될까.
$(x-C_{x})^{2}+(y-C_{y})^{2}+(z-C_{z})^{2}=r^{2}$로 나타낼 수 있다.
그럼 $(x, y, z)$를 $P$라고 한다면, $(P-C)⋅(P-C) = r^{2}$ 으로 내적으로도 나타낼 수 있다.
이 식을 가지고 더 변형시켜보자.
이 방정식을 충족하는 모든 점 $P$는 구에 있다고할 때, 우리는 이 $P$를 광선으로 쏘아진 점이라고 한다면, $\widehat{P} = A + tb$으로 생각할 수 있다.
그러면 $(P-C)⋅(P-C) = r^{2}$ 은 $(A+tb-C)⋅(A+tb-C) = r^{2}$로 변형가능하다.
그럼 이제 이 공식을 풀어보자.
$t^2b⋅b+2tb⋅(A−C)+(A−C)⋅(A−C)−r^2=0$ 이렇게 풀린다.
그러면 t의 방정식으로 $at^2+bt+c=0$이라 한다면
$a = b⋅b \ b=2b⋅(A−C) \ c = (A−C)⋅(A−C)−r^2$
$a,b,c$를 구하고, 이것을 이용하여 판별식으로 $D = b^2 - 4ac$을 쓴다면 Intersection points를 확인할 수 있다.
$D < 0:$ 구를 지나지 않는다.
$D = 0:$ 구에 맞닿는 지점이 하나다.
$D > 0:$ 구에 맞닿는 지점이 두개다.
그렇다면 이러한 내용을 코드로 나타내보자.

int		intersection _sphere(t_point *center, double radius, t_ray *r)
{
	t_vec	oc;
	double	a;
	double	b;
	double	c;
	double	discriminant;

	ft_vec_sub(&oc, &(r->orig), center);
	a = ft_vec_dot(&(r->dir), &(r->dir));
	b = 2.0 * ft_vec_dot(&oc, &(r->dir));
	c = ft_vec_dot(&oc, &oc) - (radius*radius);
	discriminant = b*b - 4*a*c;
	return (discriminant > 0);
}

이렇게 해서 결과를 보면 구의 모양이라기 보다 원의 형태로 나올 것이다.
그 이유는 빛이라는 변수가 없고 그냥 공식만 넣어서 구의 방정식으로 만들었지만 원으로 보이는 것이다.
그러면 이 원을 구로 보이게 하기 위해선 어떻게 해야할 것인가.
이를 위해 Phong Shading을 공부해야한다. 주변광이라는 조명이 있고 우리가 직접 쏘아볼 수 있는 빛이 따로 존재하는데 이 때 그 빛에 따라 구형태로 보이게 하는 것이다.

t_vec3 		*ft_ray_at(t_vec3 *new_vec, t_ray *ray, double t)
{
  new_vec->x = ray->orig.x + t * ray->dir.x;
  new_vec->y = ray->orig.y + t * ray->dir.y;
  new_vec->z = ray->orig.z + t * ray->dir.z;
  return (new_vec);
}

int		intersection _sphere(t_sphere *sp, t_ray *r, t_vec *P, t_vec *N)
{
	t_vec	oc;
	double	a;
	double	b;
	double	c;
	double	discriminant;

	ft_vec_sub(&oc, &(r->orig), sp->center);
	a = ft_vec_dot(&(r->dir), &(r->dir));
	b = 2.0 * ft_vec_dot(&oc, &(r->dir));
	c = ft_vec_dot(&oc, &oc) - (radius*radius);
    	discriminant = b*b - 4*a*c;
    	if (discriminant < 0)
           return (0);
    	t1 = (-b - sqrt(discriminant))/(2*a);
    	t2 = (-b + sqrt(discriminant))/(2*a);
    	if (t2 < 0)
	   return (0);
    	if (t1 > 0)
           t = t1;
    	else
           t = t2;
    	ft_ray_at(P, r, t); // 
    	ft_vec_sub(N, P, sp->center); // 원 중심에서 ray가 원에 닿는 P까지의 벡터
        			      //구의 경우 바깥 쪽 법선은 히트 포인트에서 중심을 뺀 방향이다.
    	ft_vec_unit(N, N); // 이 위의 단위벡터 (방향만 확인)
    	return (1);
}

이런 모양이 될 것이다.
아직 제대로 정립이 되지 않는다. 저 t가 갖는 의미를 잘 모르겠다. 계속 공부하면서 이해 해야겠다.

<210123(토) 내용 추가>

참고
https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-sphere-intersection

위의 구 방정식에서 벡터의 개념으로 넘어가 2차 방정식으로 풀어쓴 공식을 보자.
$t^2b⋅b+2tb⋅(A−C)+(A−C)⋅(A−C)−r^2=0$ 으로 풀린다.
그러면 t의 방정식으로 $at^2+bt+c=0$이라 한다면
$a = b⋅b \ b=2b⋅(A−C) \ c = (A−C)⋅(A−C)−r^2$
판별식으로 $D = b^2 - 4ac$ 을 나타냈다고 했다.
여기서 근의 공식으로 t를 구하면 $t=\frac{-b\pm \sqrt{b^2-4ac}}{2a}$ 로 2개가 나온다.
그런데 이 t가 어떤 의미를 가지는 건지 잘 몰랐는데 저 위의 글을 보고 알게 되었다.
일단 $D> 0$일 때 t는 $t=\frac{-b + \sqrt{D}}{2a} and\frac{-b - \sqrt{D}}{2a}$ 이다.
$D=0$일 때, 즉 ray가 오직 한 곳만 마주쳤을 때$(t_0=t_1)$ t는 $t=\frac{-b}{2a}$이다.
$D<0$일 때는 ray가 구에 intersect하지 않았다는 뜻이다.
그럼 여기서 t가 양수일 때 음수일 때 뭘 나타내는걸까?

바로 위 글에서 나온 그림으로 알 수 있다. ray의 상태에 따라 t가 달라지는데 위와 같은 그림으로 알 수 있다.
그럼 이것을 코드로 나타내면 어떻게 될까?
위의 출처에서 나온 코드를 전에 쓴 구의 방정식과 합쳐서 아래와 같이 바꾸어보았다.

t_vec		ft_ray_at(t_ray ray, double t)
{
   return (ft_vec_create(ray.orig.x + t * ray.dir.x,
   		ray.orig.y + t * ray.dir.y, ray.orig.z + t * ray.dir.z));
}


int		sp_quadratic_equation(t_vec vec, double *t1, double *t2)
{

    double  t;

    double discriminant;
	discriminant = vec.y * vec.y - 4* vec.x * vec.z;
    if (discriminant < 0)
        return (0);
    else if (discriminant == 0)
    {
        *t1 = (-vec.y)/(2*vec.x);
        *t2 = (-vec.y)/(2*vec.x);
    }
    else
    {
        t = (vec.y > 0) ? -0.5 * (vec.y + sqrt(discriminant)): -0.5 * (vec.y - sqrt(discriminant));
        *t1 = t/ vec.x;
        *t2 = vec.z /t;
    }
    if (*t1 > *t2)
        ft_swap(t1, t2);
    return (1);
}

int         intersect_sphere(t_ray ray, t_sphere sp, t_hit *hit)
{
    double  t;
    double  t1;
    double  t2;
    t_vec   vec;
    double  a;
    double  b;
    double  c;

    vec = ft_vec_create(ray.orig.x - sp.center.x,
    ray.orig.y - sp.center.y,
    ray.orig.z - sp.center.z); 
    a = ft_dot_mine(ray.dir);
    b = 2.0 *ft_vec_dot(vec, ray.dir);
    c = ft_dot_mine(vec) - (sp.radius*sp.radius);
    if (!sp_quadratic_equation(ft_vec_create(a,b,c), &t1, &t2))
        return (0);
    if ((t1 < 0 && t2 < 0) ||(t1 > hit->dist && t2 > hit->dist))
        return (0);
    if (t2 < 0)
	return (-1.0);
    if (t1 > 0)
        t = t1;
    else
        t = t2;
    hit->dist = t;
    hit->pos = ft_ray_at(ray, t);
    hit->nomal_vec = ft_vec_normalize(ft_vec_sub(hit->pos, sp.center));
    hit->pos = ft_vec_add(hit->pos, ft_vec_multi_double(hit->nomal_vec, EPSILON));
    return (1);
}

여기서 hit는 ray가 구에 부딪치고 난 뒤의 거리, 위치, 법선벡터를 구하고 위치를 원래 위치와 법선벡터와 엡실론을 곱한 것과 더한 값으로 놓습니다.
여기서 엡실론이란 무엇일까?

(출처: https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-837-computer-graphics-fall-2012/lecture-notes/MIT6_837F12_Lec13.pdf)

많은 글을 찾아보았다.
1.http://emal.iptime.org/noriwiki/index.php/%EB%A0%88%EC%9D%B4_%ED%8A%B8%EB%A0%88%EC%9D%B4%EC%8B%B1
2.https://celdee.tistory.com/category/Library/Computer%20Graphics
3.https://render.otoy.com/forum/viewtopic.php?f=9&t=48425&p=241445&hilit=ray+epsilon#p241445
이 글도 읽어보았고 광학쪽 대학원을 다니는 친구한테도 물어보았다.
대충 얘기하면 엡실론은 상수이고 전자기학의 유전율 같이 매질에서 영향을 미치는 상수라고 했다. 그렇다면 컴퓨터 그래픽에서 말하는 엡실론을 무엇을 뜻할까?

바로 광선을 쏘아내고 그 오브젝트에 부딪치고 난 뒤 shading ray에서 shadow acne가 생길 수 있는데 여기서 자그마한 값을 넣으면 이러한 것을 방지해준다고 한다.
세 번째 글에 따르면 엡실론은 0.0001이 기본값으로 이 값을 추천하고 있다.
그래서 위의 사진을 보면 epsilon이 있고 없고의 차이가 저렇게 나타나게 된다.
그래서 1e-4의 값을 hit한 구의 법선벡터와 곱해서 shadow acne을 방지한다.

빛, 그림자에 대해

albedo

참고
https://www.researchgate.net/figure/Typical-albedo-values-of-different-kind-of-surfaces-3_tbl1_275956502

https://wiki.openmod-initiative.org/wiki/Albedo-Value 여기서 정의와 의미를 알 수 있다.

  1. albedo 값은 경사면에서 복사를 계산할 때 환경 매개 변수를 고려하는 데 사용되는데 값은 위의 사진에서 처럼 표면에 따라 albedo값이 달라진다.

  2. Albedo는 표면이 빛를 얼마나 잘 반사하는지 나타내는 무 차원의 단위없는 양이라고 한다.
    Albedo값은 $[0,1]$ 으로 일반적으로 표면의 "백색도"를 나타내며 0은 검정을 의미하고 1은 흰색을 의미한다. 0은 표면이 들어오는 모든 에너지를 흡수하는 "완벽한 흡수"임을 의미하고, 값 1은 표면이 들어오는 모든 에너지를 반사하는 "완벽한 반사체"임을 의미한다.

Phong Shading

참고

  1. https://zamezzz.tistory.com/181
  2. https://zamezzz.tistory.com/154
  3. http://it.sangji.ac.kr/~3D/ppt/CG/05.pdf
  4. https://wjdgh283.tistory.com/entry/OpenGL%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EC%BB%B4%ED%93%A8%ED%84%B0-%EA%B7%B8%EB%9E%98%ED%94%BD%EC%8A%A4-Chapter-03-%EA%B7%B8%EB%9E%98%ED%94%BD-%EC%BB%AC%EB%9F%AC%EC%B2%98%EB%A6%AC-1

우리에게 주어지는 빛은 주변광(ambient light)과 인위적으로 넣은 빛(light) 2가지이다.
주변광은 특정한 방향이 없이 주변을 덮고 있는 빛이고 인위적으로 넣은 빛은 .rt파일에 l로 시작하여 위치와 ratio 그리고 빛 색깔을 넣는다.
그러면 이러한 빛을 가지고 원을 어떻게 구체 모양으로 보이게 할 것인가.
그러기 위해서 사용하는 방법은 난반사인 분산광(diffuse)을 쓰는 방법과 반사광(specular)을 더해 정반사로 만드는 방법이 있다.
그리고 주변광, 분산광, 반사광을 모두 더해서 오브젝트를 표현하는 방식을 Phong Shading이라고 한다.

이렇게 빛에 따라 구의 모양이 달라지는 것을 출처 3번째에서 확인할 수 있었다.
그러면 분산광과 반사광에 대해 알아보자.
먼저 분산광이란 일정한 방향으로 빛이 들어올 때 오브젝트의 표면에서 여러 방향으로 분산되는 빛의 형태를 말하는데 흔히 난반사라고도 불린다.
이러한 빛은 표면이 빛의 방향을 향할 때 가장 밝고, 정 반대일 때는 가장 어두운 형태를 가진다.
테니스공에 빛을 비추었을 때를 생각해보면 된다.

그렇다면 이 성질을 통해 분산광(diffuse)식을 나타내면 $I = K_dI_lcos(\theta) = K_dI_l(N\cdot L)$ 으로
$K_d$은 diffuse 계수이고 $I_l$은 조명의 밝기, 색(r,g,b), $N$은 물체의 법선벡터, $L$은 빛의 방향 벡터이다.
이것을 코드로 나타내보자. 계수는 1이라 한다.

t_rgb	*rgb_multi_double(t_rgb rgb, double multi)
{
	return (itor(rgb.r * multi, rgb.g * multi, rgb.b * multi));
}

diffuse_reflection = FT_MAX(ft_vec_dot(hit->normal_vec, ft_vec_unit(to_light.dir)), 0.0);
diffuse_color = *rgb_multi_double(light->color, diffuse_reflection);

그리고 반사광은 특정한 방향으로 유입되어 한 방향으로 완전히 반사되는 빛을 반사광(specular light)이라 한다. 강한 반사광은 물체 표면에 밝은 점을 형성하며, 이를 반사 하이라이트(highlight)라고 한다. 당구공에 빛을 비추었을 때 당구공에 밝은 점이 나타나는 것을 생각하면 된다.
그렇다면 이 성질을 통해 반사광을 식으로 나타내보자.
$I = K_sI_lcos^n(\phi) = K_sI_l(R\cdot V)^n$ 으로
$K_d$은 specular 계수이고 $I_l$은 입사되는 빛의 밝기, 색(r,g,b), $V$은 hit된 곳에서 카메라로 향하는 방향 벡터, $R$은 입사되는 빛의 반사벡터이다.

이것을 코드로 나타내보자. 계수는 1이라 한다.

 specular_reflection = FT_MAX(pow(ft_vec_dot(ft_vec_unit(V), ft_vec_unit(R)), 200.0), 0.0);
 specular_color = *rgb_multi_double(light->color, specular_reflection);

여기서 n제곱에 대해 설명해보면, 광택에 따른 $cos^n(\phi)$의 그래프를 나타낸 것이 있다.

이 n에 따라 빛의 초점이 달라진다.

모든 광을 알아보았다. 그러면 이제 이 빛들을 전부 더하면? 바로 Phong Shading이 된다. (주변광은 $K_aI_a$)
$I = K_aI_a + K_dI_l(N\cdot L)+ K_sI_l(R\cdot V)^n$

t_rgb		*itor(int r, int g, int b)
{
	t_rgb	*rgb;

	if(!(rgb = (t_rgb *)malloc(sizeof(*rgb))))
		ft_error(MALLOC_FAIL);
	rgb->r = r;
	rgb->g = g;
	rgb->b = b;
	return (rgb);
}

//init RGB value
diff_spec = *itor(0, 0, 0);
light_color = *itor(0, 0, 0);
specular_color = *itor(0, 0, 0);
diffuse_color = *itor(0, 0, 0);

//diffuse_light calculate
diffuse_reflection = FT_MAX(ft_vec_dot(hit->normal_vec, ft_vec_unit(to_light.dir)), 0.0);
diffuse_color = *rgb_multi_double(light->color, diffuse_reflection);

//specular_light calculate
specular_reflection = FT_MAX(pow(ft_vec_dot(ft_vec_unit(V), ft_vec_unit(R)), 200.0), 0.0);
specular_color = *rgb_multi_double(light->color, specular_reflection);

light_color = *add_rgb(*add_rgb(diffuse_color, specular_color), light_color);
diff_spec = *rgb_multi_double(*add_rgb(diff_spec, light_color), ALBEDO);

//ambient_light calculate
 *color = *rgb_multi(*add_rgb(info->al.color, diff_spec), *color); //곱하는 *color은 오브젝트의 RGB값이다.

Phong Shading result by .rt

문제점

구 사이에 빛을 가까이 놓았을 때 저렇게 밝은 초승달 모양이 생기는데 반사광에서 문제가 있는 것 같다. 분산광까지는 문제가 없는데 반사광만 보았을 때 문제가 발생한다. 어떻게 해결해야할지...

210205(금) 문제 해결

빛의 반사각인 R과 hit한 곳에서 카메라를 향하는 V를 잘못 계산하여 넣고 있었다.
우선 카메라를 향하는 V를 V = ft_vec_minus(hit->pos); 이렇게 계산하고 있었는데 생각해보니 ray로 생각해야하는데 왜 이렇게 썼는지 모르겠다. 그래서 이 문장을 V = ft_vec_sub(ray.orig, hit->pos);로 하고 빛이 hit->pos로 가는 것도 이런 식으로 바꾸니 이상한 곳에서 밝아지는 현상이 없어졌다.

그림자

그림자를 구할 때 카메라를 통해 ray를 만들어서 봤을 때 hit된 곳의 거리인 hit->dist를 무한대라고 두고 픽셀 별로 찾게 된다.
카메라로 봤을 때 카메라에서 히트된 곳까지 거리를 hit->dist로 둔다.
그리고 그곳을 hit->pos라 하고 hit->pos에서 빛까지를 보면서 object가 있나 살펴본다.
이 때 오브젝트까지 걸린 거리를 hit_ob->dist라 하고 hit->pos와 빛의 위치의 거리를 계산했을 때
hit_ob->dist >= ft_vec_dist(hit->pos, var.light->light_point)이면 Phong Shading을 통해 색을 나타내고 아니면 나타내지 않는다.
고로 ray가 본 픽셀 값은 빛에 의한 색을 나타낼 수가 없어서 검은색으로 나오게 된다.

ray에 대한 평면 방정식

참고

  1. https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-plane-and-ray-disk-intersection
  2. http://www.illusioncatalyst.com/notes_files/mathematics/line_plane_intersection.php

구체를 만들었으니 이제 평면을 만들차례다. 평면을 바닥으로 하고 빛이 오브젝트를 비췄을 때 바닥에 그림자를 형성하는 것을 해 볼건데, 그림자를 형성하는 것은 ray에 hit된 곳에 법선벡터와 ray.dir를 내적을 했을 때 양수면 법선벡터를 -로 바꾸면그림자를 형성할 수 있다.

그러면 ray에 대한 평면 방정식을 알아보자.

먼저 수직 인 두 벡터의 점 (또는 스칼라) 곱이 항상 0과 같다는 것을 알고 있어야 한다.
그러면 이 공식을 만들 수 있다: $(P-P_0) \cdot n = 0$
이 공식을 풀어보면 $P \cdot n = P_0 \cdot n$ 으로 ray에 대해 풀면 $(r_0+rt)\cdot n = P_0\cdot n$ 이 되고 $t$로 정리하면 $t \cdot(r\cdot n) = P_0 \cdot n - r_0 \cdot n$ 이고 $t = \frac{P_0 \cdot n - r_0 \cdot n }{r \cdot n}$ 으로 이 $t$가 음수이면 평면과 만나지 않는다.
그러면 이것을 출처에 따라 코드로 구현해보자.

int		intersect_plane(t_ray ray, t_plane plane, t_hit *hit)
{
	double	denom;
	double	nom;
	double	t;

	denom = ft_vec_dot(plane.normal_vec, ray.dir);
	nom = ft_vec_dot(ft_vec_sub(ray.orig, plane.pos), plane.normal_vec);
	if (denom > 1e-6)
	{
		t = -nom / denom;
		if (t > EPSILON && t < hit->dist)
		{
			hit->dist = t;
			hit->normal_vec = plane.normal_vec;
			hit->pos = ft_ray_at(ray, t);
			hit->pos = ft_vec_add(hit->pos,
				ft_vec_multi_double(hit->normal_vec, EPSILON));
			return (t);
		}
	}
	return (0);
}

으로 plane에 intersect한 평면을 구할 수 있고, 구체가 있을 때 빛에 의한 그림자를 만들어 낼 수 있다.

사각형 만들기

참고
https://stackoverflow.com/questions/33685433/ray-square-intersection-3d

참고한 사이트를 통해 코드를 작성하고 사각형이 있는 위치에서 사각형을 만드는 좌표 4개를 만들어 저렇게 만들었는데 문제는 저기에 안보이는 투명한 평면이 존재하고 내가 선택한 곳만 사각형으로 만들었다는 것이다.
그리고 이렇게 만들어지긴 했는데, 문제는 빛에 의한 그림자가 형성되지 않는게 문제다. 정확히 말하면 되긴하는데 특정 위치로만 가능하다. 구체가 있고 그 앞에 사각형이 있을 때 빛을 사각형이 막아서 구체한테 빛이 들어가면 안되는데 이게 또 신기하게 들어간다.
문제가 많은 사각형이다. A4 용지를 생각하면 특정상황에서 그림자가 안보이는 건 맞는데 특정상황이여야 보이는건 문제이기 때문에 얼른 이것을 고쳐야겠다.

210216(화)

삼각형 두개를 이용하여 사각형으로 만들어보았다. 이렇게 하니까 사각형의 그림자가 나오긴 했는데 문제는 법선벡터에 따른 평면 기울기가 나타나지 않고 z축에 수직인 사각형만 나오게 된다.
너무 큰 문제인 것 같다.

210309(화)

오랬동안 고민해온 과제이고 그만큼 시도도 많이 해보고 같이 miniRT과제를 하는 사람들에게도 어떻게 만들었는지 다양하게 들을 수 있었다.
같이 miniRT를 공부하는 hekang님께서 사각형에 임의의 축을 정하고 사각형 법선 벡터와 외적을 해서 축을 그리고 그 축을 토대로 사각형을 만들 수 있다고 했다.
처음 들었을 때의 마음은

이렇게 들떴고, 콧구멍도 벌렁거렸다.
그렇게 '아싸, 알려준대로 해봐야지~'라고 생각한 때도 잠시나마 있었다.
머릿 속에서 물음표가 끊임없이 돌아다녔고, 다시 미궁 속으로 빠지게 되었다.

그리고 집에 돌아갈 때까지 완성하지 못했다.
시간이 지나고 축에 대한 이해없이 사각형 중심과 평면에 hit일 때 t값을 가지고 사각형 길이보다 크면 제외하고 작으면 그리는 형식으로 해보았다.

int		intersect_square(t_ray ray, t_square sq, t_hit *hit)
{
	double	denom;
    double  nom;
	double	t;
	double	len;
	t_vec	d;

	denom = ft_vec_dot(sq.normal_vec, ray.dir);
    nom = ft_vec_dot(ft_vec_sub(ray.orig, sq.center),sq.normal_vec);
	if (denom == 0)
		return (0);
	t = -nom / denom;
	d = ft_vec_sub(ft_ray_at(ray, t), sq.center);
	len = sq.side_size;
	if (fabs(d.x) >= len || fabs(d.y) >= len || fabs(d.z) >= len)
		return (0);
	if (t >= 0)
	{
		hit->dist = t;
		hit->pos = ft_ray_at(ray, t);
		hit->normal_vec = sq.normal_vec;
		hit->pos = ft_vec_add(hit->pos, ft_vec_multi_double(sq.normal_vec, EPSILON));
		return (1);
	}
	return (0);
}

이렇게 하니까 문제가 생겼다. 법선벡터가 1,1,-1 일 때 6각형으로 만들어진다는 것과 0,1,-1같은 법선벡터들의 사각형 변의 길이가 일정하지 않다는 것이다.
변이 수직에서 대각선으로 바뀌면서 sqrt(2)가 곱해지게 되는 것이 원인인 것 같았다.
그러다가 카메라 방향벡터 이슈에 눈이 돌아가서 고칠 때 외적 축에 대한 개념이 확실히 잡혔고 hekang님께서 말씀하신 축으로 계산하는 코드로 구현하게 됐다.(그저 빛 ㅜㅜ)
여기서 사각형을 그릴 때 삼각비에 대한 개념이 들어가게 됐는데 그 이유는 위의 그림처럼 대각선이 되면 길이가 정해진 길이보다 길어졌기 때문이다.

그림으로 설명하면 이렇다. 평면 노말벡터가 (1, 0, -1)일 때 임의의 벡터 up벡터 (0, 1, 0)과 외적을 하여 축 axis를 만든다.
예외로 만약에 노말벡터가 up벡터와 평행하다면 up벡터를 (0, 0, 1)로 축 계산을 할 수 있게 바꿔줘야 한다.
여튼 이렇게 되면 이제 사각형의 중심을 기준으로 사각형의 범위를 제외하고 전부 return (0)를 시켜야 한다.

먼저, 기준이 되는 사각형 중심과 축을 기준으로 cos값을 구한다.
두 벡터에 따른 cos을 구하는 공식은 다음과 같다.

$cos\theta = \frac{\overrightarrow{u}\cdot \overrightarrow{v}}{\left | \overrightarrow{u} \right | * \left | \overrightarrow{v} \right |}$

이 값이 사각형 중심에서 $45^{\circ}$를 기준으로 나눠져야 한다. 약간 아래 그림 느낌?

이렇게 cos값이 $\frac{\sqrt{2}}{2}$ 보다 작게 되면 $45^{\circ}$보다 크다는 뜻이므로 (특수각의 삼각비 참고) $45^{\circ}$보다 작은 각을 유지하기 위해 삼각함수의 Cofunction Identities을 이용하여 cos값을 sin값으로 바꾼다.
$sin\theta = cos(\frac{\pi}{2} - \theta)$
cos안에 있는 세타는
$cos\theta = 45^{\circ}$
$cos^{-1}45^{\circ} = \theta$
이러한 방법으로 구할 수 있다.
이 cos값을 정해준 사각형의 길이의 $\frac{1}{2}$ 과 나눈 값($cos = \frac{사각형 길이/2}{경계 길이}$)보다 사각형 중심에서 hit된 곳까지의 길이가 크면 return (0)해주면 사각형 완성이다.
$cos\theta = \frac{a}{b}$ -> $b = \frac{a}{cos\theta}$

double	vec_size(t_vec v)
{
	return (sqrt(ft_dot_mine(v)));
}

double	cosval(t_vec a, t_vec b)
{
	return (ft_vec_dot(a, b) / (vec_size(a) * vec_size(b)));
}

int		intersect_square(t_ray ray, t_square sq, t_hit *hit)
{
	double	denom;
	double  nom;
	double	t;
	double	len;
	double	cosin;
	t_vec	d;
	t_vec	up;
	t_vec	axis;

	denom = ft_vec_dot(sq.normal_vec, ray.dir);
	nom = ft_vec_dot(ft_vec_sub(ray.orig, sq.center),sq.normal_vec);
	if (denom == 0)
		return (0);
	t = -nom / denom;
	d = ft_vec_sub(ft_ray_at(ray, t), sq.center);
	up = ft_vec_create(0, 1, 0);
	if (sq.normal_vec.y != 0 &&(sq.normal_vec.x == 0 && sq.normal_vec.z == 0))
		up = ft_vec_create(0, 0, 1);
	axis = ft_vec_cross(up, sq.normal_vec);
	cosin = fabs(cosval(axis, d));	
	if (cosin < sqrt(2) / 2)
		cosin = cos((MINI_PI / 2) - acos(cosin));
	len = sq.side_size / cosin;
	if (vec_size(d) > len)
		return (0);
	if (t >= 0)
	{
		hit->dist = t;
		hit->pos = ft_ray_at(ray, t);
		hit->normal_vec = sq.normal_vec;
		hit->pos = ft_vec_add(hit->pos, ft_vec_multi_double(sq.normal_vec, EPSILON));
		return (1);
	}
	return (0);
}

이전 코드와 비교 사진

여러 방향벡터에 따른 사각형들

정육면체 만들기

참고
https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-box-intersection

참고한 사이트에서 주어진 코드를 norm에 맞춰서 작성한 후 코드를 make하니 정육면체가 나오게 되었다.
각 평면마다 법선벡터가 존재하는데 이 법선벡터를 구분할 방법을 찾아야 깔끔한 정육면체가 나올 것 같다. 지금은 정육면체 중심에서 히트한 곳을 뻗어나가는 벡터를 법선벡터라고 정의하고 있다.
하지만 보너스에서는 6개의 사각형을 조합하여 만들라라고 나와 있어서 만들었어도 보너스점수에는 안들어갈 듯하다..

삼각형 만들기

참고

  1. https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/moller-trumbore-ray-triangle-intersection
  2. https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm

삼각형 만드는 기법은 Möller–Trumbore 광선-삼각 교차 알고리즘을 사용하였다.
Möller–Trumbore 광선-삼각 교차 알고리즘은 삼각형을 포함하는 평면의 평면 방정식의 사전 계산없이 광선과 삼각형의 교차점을 3 차원으로 계산하는 빠른 방법이다.
우선 좌표에 존재하는 세 점을 나타내보자.

위의 그림처럼 세 점이 있고 그 점들을 이으면 삼각형이 되고 빛에 의해 아래에 그림자가 생기게 된다.
정말 간단한 말인데 이걸 컴퓨터 그래픽으로 나타내기 위해 기하학적인 지식을 요구한다.

먼저 p1와 p2,p3를 잇는다.

p1p2 = ft_vec_sub(rt.p2, tr.p1);
p1p3 = ft_vec_sub(rt.p3, tr.p1);

그리고 ray의 dir와 p1p3를 외적을 시킨다.
pvec = ft_vec_cross(ray.dir, p1p3);

p1p2와 pvec을 내적한 값(det)으로 삼각형이 교차하는지 안하는지를 확인한다.
det = ft_vec_dot(p1p2, pvec);
ray의 위치와 p1를 이은 것을 가지고 pvec와 내적을 한 값과 1/det를 곱한 값이 음수나 1 초과이면 교차하지 않는다.

invdet = 1 / det;
tvec = ft_vec_sub(ray.orig, tr.p1);
u = ft_vec_dot(tvec, pvec) * invdet;
if(u < 0 || u > 1)
	return (0);

tvec와 p1p2를 외적한 값을 qvec이라 한다. 이 값과 ray.dir을 내적한 값으로 invdet과 곱한 값이 음수거나 u와 더한 값이 1 초과면 교차하지 않는다.

qvec = ft_Vec_cross(t_vec, p1p2);
v = ft_vec_dot(ray.dir, qvec) * invdet;
if (v < 0 || u + v > 1)
	return (0);

마지막으로 t는 ray.orig + ray.dir * t 에서 거리의 길이를 나타내는 t이다.
t = ft_vec_dot(p1p3, qvec) * invdet;

그러면 아래와 같이 빛을 비춰주었을 때 그림자를 형성하는 모습을 볼 수 있다.

평면 공식을 이용한 삼각형 만들기

위의 사각형을 만드는 방법과 동일하다. 다른 점이 있다면 노말벡터를 주지 않고 우리가 계산을 해야하는 점이다.

tri->normal_vec = ft_vec_normalize(ft_vec_cross(
		ft_vec_sub(tri->p2, tri->p1), ft_vec_sub(tri->p3, tri->p1)));

점 3개를 이용하여 외적을 통해 손쉽게 노말벡터를 구할 수 있다.

세 점을 통해 영역을 자르는데

이런 식으로 나누어서 영역 안에 있는 부분만 나타내게 잘라낸다.
세 점을 주고, 각각 삼각형 안에 있는 벡터와 아닌 부분을 번거롭지만 계산해보자.

p1 = (-3, 0, 0)
p2 = (0, 3, 0)
p3 = (3, 0, 0)

아래 tr_outside 함수에서 나온데로 해보자.

삼각형 안에 hit된 점이 있을 때
먼저 삼각형 안에 있는 hit점을 (0, 1, 0)이라 하자.

  1. tr_except(tr.p1, tr.p2, tr.p3, hitray)
    ba = (3, 3, 0) , ca = (6, 0, 0) , ha = (3, 1, 0)
    ba x ca = (0, 0, -18)
    ha x ca = (0, 0, -6)
    dot((0, 0, -18), (0, 0, -6)) = 108

  2. tr_except(tr.p2, tr.p3, tr.p1, hitray)
    ba = (3, -3, 0) , ca = (-3, -3, 0) , ha = (0, -2, 0)
    ba x ca = (0, 0, -18)
    ha x ca = (0, 0, -6)
    dot((0, 0, -18), (0, 0, -6)) = 108

  3. tr_except(tr.p3, tr.p1, tr.p2, hitray)
    ba = (-6, 0, 0) , ca = (-3, 3, 0) , ha = (-3, 1, 0)
    ba x ca = (0, 0, -18)
    ha x ca = (0, 0, -6)
    dot((0, 0, -18), (0, 0, -6)) = 108

세 가지 예외를 통해 삼각형 안에 hit가 되는 점이 있을 때 계산해보면 3개 예외 다 dot의 값이 0보다 같거나 크므로 예외처리를 하지 않는다.

삼각형 안에 hit된 점이 없을 때
먼저 삼각형 밖에 있는 hit 점을 (-3, 2, 0)이라 하자.

  1. tr_except(tr.p1, tr.p2, tr.p3, hitray)
    ba = (3, 3, 0), ca = (6, 0, 0), ha = (0, 2, 0)
    ba x ca = (0, 0, -18)
    ha x ca = (0, 0, -12)
    dot((0, 0, -18), (0, 0, -12)) = 216

  2. tr_except(tr.p2, tr.p3, tr.p1, hitray)
    ba = (3, -3, 0) , ca = (-3, -3, 0) , ha = (-3, -1, 0)
    ba x ca = (0, 0, -18)
    ha x ca = (0, 0, 6)
    dot((0, 0, -18), (0, 0, 6)) = -108

  3. tr_except(tr.p3, tr.p1, tr.p2, hitray)
    ba = (-6, 0, 0) , ca = (-3, 3, 0) , ha = (-6, 2, 0)
    ba x ca = (0, 0, -18)
    ha x ca = (0, 0, -6)
    dot((0, 0, -18), (0, 0, -12)) = 216

예외 처리 2번째에서 dot의 값이 0보다 작은 음수로 예외처리가 되서 그 값은 나타내지 않는다.

이렇게 평면 방정식을 이용하여 삼각형을 만들 수 있다.

int		tr_except(t_vec a, t_vec b, t_vec c, t_vec hit)
{
	t_vec	ba;
	t_vec	ca;
	t_vec	ha;

	ba = ft_vec_sub(b, a);
	ca = ft_vec_sub(c, a);
	ha = ft_vec_sub(hit, a);
	if (ft_vec_dot(ft_vec_cross(ba, ca), ft_vec_cross(ha, ca)) < 0)
		return (0);
	return (1);
}


int		tr_outside(t_ray ray, t_triangle tr, double t)
{
	t_vec	hitray;

	hitray = ft_ray_at(ray, t);
	if (!tr_except(tr.p1, tr.p2, tr.p3, hitray))
		return (0);
	if (!tr_except(tr.p2, tr.p3, tr.p1, hitray))
		return (0);
	if (!tr_except(tr.p3, tr.p1, tr.p2, hitray))
		return (0);
	return (1);
}

int		intersect_triangle(t_ray ray, t_triangle tr, t_hit *hit)
{
	double		denom;
	double		nom;
	double		t;

	denom = ft_vec_dot(tr.normal_vec, ray.dir);
	nom = ft_vec_dot(ft_vec_sub(ray.orig, tr.p1), tr.normal_vec);
	if (denom == 0)
		return (0);
	t = -nom / denom;
	if (!tr_outside(ray, tr, t))
		return (0);
	if (t > EPSILON && t < hit->dist)
	{
		hit->dist = t;
		hit->pos = ft_ray_at(ray, hit->dist);
		hit->normal_vec = tr.normal_vec;
		hit->pos = ft_vec_add(hit->pos,
			ft_vec_multi_double(hit->normal_vec, EPSILON));
		return (1);
	}
	return (0);
}

원기둥 만들기

참고
http://www.illusioncatalyst.com/notes_files/mathematics/line_cylinder_intersection.php

참고한 사이트를 잘 보고 코드로 구현하여 만들어봤는데
뭔가 이상하게 나온다...

중심을 기준으로 height를 반으로 나눠서 y축 높낮이를 정했는데 위 아래로 여의봉처럼 쭈욱 뻗어 있게 됐다. 그림자도 이상하다. 어떻게 해야할지 계속 생각해봐야겠다.

210217 (수)

구체를 만들 때 공식과 비슷한게 많아서 거기 공식을 가져다가 써봤는데 높이 제한이 없는 무한 기둥이 되었다.
그래서 그 높이를 제한하는 if문을 짜고 돌려보니 아래와 같은 원기둥이 나왔다.
hit한 부분이 앞에 부분과 뒷부분이 있는데 대략적으로

이런 식으로 t가 2개 나오고 t1으로 하면 앞에 부분, t2로하면 뒤에 부분만 나와서 intersect하는 부분을 두번으로 나누어 원기둥 모양으로 형성하게 했다.

코드로 확인해보자.


void	t_select(double *t1, double *t2, int i)
{
	if (i == 1)
	{
		if (*t1 > *t2) // 원기둥 앞
			ft_swap(t1, t2);
	}
	else
	{	
		if (*t1 < *t2) // 원기둥 뒤
			ft_swap(t1, t2);
	}
}

double	intersect_check(t_cyvar var, double *t1, double *t2, int i)
{
	double	root;
	double t;

	root = (var.b * var.b) - (4 * var.a * var.c);
	if (root < 0)
		return (0);
	if (root > 0)
	{
		t = (var.b > 0) ? -0.5 * (var.b + sqrt(root)) :
			-0.5 * (var.b - sqrt(root));
		*t1 = t / var.a;
		*t2 = var.c / t;
		t_select(t1, t2, i);
	}
	else if (root == 0)
	{
		if (fabs(ft_vec_dot(var.v, var.h)) != 1)
		{
			*t1 = -var.b / (2 * var.a);
			*t2 = -var.b / (2 * var.a);
		}
	}
	return (1);
}

double	cy_calc(t_cyvar var, t_hit *hit, int i)
{
	double t;

	var.a = ft_dot_mine(var.v) - pow(ft_vec_dot(var.v, var.h), 2);
	var.b = (ft_vec_dot(var.v, var.w) - (ft_vec_dot(var.v, var.h) *
	ft_vec_dot(var.w, var.h))) * 2;
	var.c = ft_dot_mine(var.w) - pow(ft_vec_dot(var.w, var.h), 2) - var.r2;
	if (!(intersect_check(var, &var.t1,&var.t2, i)))
		return (0);
	if ((var.t1 < 0 && var.t2 < 0) ||
	(var.t1 > hit->dist && var.t2 > hit->dist))
		return (0);
	if (var.t2 < 0)
		return (0);
	if (var.t1 > 0)
		t = var.t1;
	else
		t = var.t2;
	return (t);
}

void	cy_normal(t_ray ray, t_cylinder *cy, t_hit *hit)
{
	double dir;

	hit->pos = ft_ray_at(ray, hit->dist);
	if (cy->normal_vec.x == 1 || cy->normal_vec.x == -1)
	{
		dir = hit->pos.x - cy->pos.x;
		cy->pos.x = cy->pos.x + dir;
	}
	if (cy->normal_vec.y == 1 || cy->normal_vec.y == -1)
	{
		dir = hit->pos.y - cy->pos.y;
		cy->pos.y = cy->pos.y + dir;
	}
	if (cy->normal_vec.z == 1 || cy->normal_vec.z == -1)
	{
		dir = hit->pos.z - cy->pos.z;
		cy->pos.z = cy->pos.z + dir;
	}
}

int		cy_boundary(t_ray ray, t_cylinder cy, t_cyvar var)
{
	var.p = ft_ray_at(ray, var.t);
	if ((cy.normal_vec.x != 0) &&
		(cy.normal_vec.y == 0 || cy.normal_vec.z == 0))
	{
		if (var.p.x < cy.pos.x - cy.height || var.p.x > cy.pos.x + cy.height)
			return (0);
	}
	else if ((cy.normal_vec.y != 0) &&
		(cy.normal_vec.y == 0 || cy.normal_vec.z == 0))
	{
		if (var.p.y < cy.pos.y - cy.height || var.p.y > cy.pos.y + cy.height)
			return (0);
	}
	else if ((cy.normal_vec.z != 0) &&
		(cy.normal_vec.y == 0 || cy.normal_vec.z == 0))
	{
		if (var.p.z < cy.pos.z - cy.height || var.p.z > cy.pos.z + cy.height)
			return (0);
	}
	return (1);
}

int		intersect_cylinder(t_ray ray, t_cylinder cy, t_hit *hit, int i)
{
	t_cyvar var;

	var.r2 = cy.diameter * cy.diameter;
	var.top = ft_vec_sub(cy.pos, ft_vec_multi_double(cy.normal_vec,
			cy.height));
	var.bot = ft_vec_add(cy.pos, ft_vec_multi_double(cy.normal_vec,
			cy.height));
	var.hc = ft_vec_sub(var.top, var.bot);
	var.h = ft_vec_unit(var.hc);
	var.w = ft_vec_sub(ray.orig, var.bot);
	var.v = ray.dir;
	if (!(var.t = cy_calc(var, hit, i)))
		return (0);
	if (!cy_boundary(ray, cy, var))
		return (0);
	hit->dist = var.t;
	cy_normal(ray, &cy, hit);
	hit->normal_vec = ft_vec_unit(ft_vec_sub(hit->pos, cy.pos));
	hit->pos = ft_vec_add(hit->pos, ft_vec_multi_double(hit->normal_vec,
		EPSILON));
	return (1);
}

void	ray_cylinder(t_ray ray, t_rt_info *info, t_hit *hit, void **object)
{
	int			i;
	int			flag;
	t_list		*cylinders;
	t_cylinder	*cylinder;

	cylinders = info->cylinder;
	while (cylinders->next)
	{
		i = 2;
		flag = 1;
		cylinder = (t_cylinder *)(cylinders->content);
		while (i--)
		{
			if (intersect_cylinder(ray, *cylinder, hit, flag))
			{
				i = 0;
				*object = cylinder;
				ft_memcpy(info->type, "cy\0", 3);
			}
			flag = 0;
		}
		cylinders = cylinders->next;
	}
}

여기서 i는 원기둥 앞부분과 뒷부분으로 toggle역할을 한다.
위 코드의 문제점은 때려맞추기 식으로 해서 법선벡터가 (1, 1, 0) 이렇게 기울어질 때 경계도 안나눠지고 노말벡터도 잘 나오지 않게 된다.

법선 벡터 (1,0,0), (0,0,1), (0,1,0) 일 때

문제점

210311 (목)

원기둥 문제점은 두가지였다.

  1. ray가 원기둥을 만났을 때 hit된 곳에 normal_vec를 구하는 것.
  2. 정한 높이에 맞춰 자르는 것.

먼저 첫번째 문제.

위 사진처럼 구해야한다.
그럼 어떻게 구해야할까? 다음과 같은 계산으로 간단히 구할 수 있었다.
그림에 맞춰 원기둥의 노말벡터가 (1, 1 ,0)라 하고, hit된 곳을 (14, 9, -2), 원기둥의 중심을 (10, 10, 1)이라 가정해보자.

  1. 히트된 곳과 원기둥 중심을 뺀다. -> (4, -1, -3)
  2. 원기둥의 노말벡터와 1번에 나온 값을 내적 한다. -> 3
  3. 2번 값과 원기둥의 노말벡터를 곱한다. -> (3, 3, 0)
  4. 1번과 3번을 뺀다. -> (1, -4, -3)
  5. 4번을 정규화 시킨다. -> (0.196, -0.78, -0.588)

결과를 봤을 때 x양의 방향, y음의 방향, z음의 방향을 보고 있어서 위의 그림과 같은 노말벡터가 생기게 된다.

t_vec	get_cy_normal(t_vec pos, t_cylinder cy)
{
	t_vec	tmp;
	t_vec	normal;

	tmp = ft_vec_sub(pos, cy.pos);
	normal = ft_vec_normalize(ft_vec_sub(tmp,
	ft_vec_multi_double(cy.normal_vec, ft_vec_dot(cy.normal_vec, tmp))));
	return (normal);
}

2번째 문제.
원기둥의 높이만큼 자르지 않으면 원기둥이 무한대로 길게 뻗게 된다.
이것을 방지하기 위해서 피타고라스 정리로 그 경계를 나눌 수 있었다.

위의 그림에서 보듯이 경계 최대 길이는 피타고라스 정리에 의해 나온 값이다.
그러면 원기둥 중심에서 ray가 닿은 곳의 길이가 그 값보다 크면 나타내지 않으면 된다.
즉, 최대나올 수 있는 길이보다 작으면 된다.


int		cy_boundary(t_ray ray, t_cylinder cy, t_cyvar var)
{
	double	len;

	var.p = ft_ray_at(ray, var.t);
	len = sqrt(pow(cy.diameter, 2.0) + pow(cy.height, 2.0));
	if (ft_vec_dist(cy.pos, var.p) > len)
		return (0);
	return (1);
}

아직 원기둥 뚜껑은 하지 않았는데 잘 생각해서 해야겠다.

int		intersect_cylinder(t_ray ray, t_cylinder cy, t_hit *hit, int i)
{
	t_cyvar var;

	var.r2 = cy.diameter * cy.diameter;
	var.top = ft_vec_add(cy.pos, ft_vec_multi_double(cy.normal_vec,
			cy.height));
	var.bot = ft_vec_sub(cy.pos, ft_vec_multi_double(cy.normal_vec,
			cy.height));
	var.hc = ft_vec_sub(var.top, var.bot);
	var.h = ft_vec_normalize(var.hc);
	var.w = ft_vec_sub(ray.orig, var.bot);
	var.v = ray.dir;
	if (!(var.t = cy_calc(var, hit, i)))
		return (0);
	if (!cy_boundary(ray, cy, var))
		return (0);
	hit->dist = var.t;
	hit->pos = ft_ray_at(ray, var.t);
	hit->normal_vec = get_cy_normal(hit->pos, cy);
	hit->pos = ft_vec_add(hit->pos, ft_vec_multi_double(hit->normal_vec,
		EPSILON));
	return (1);
}

보너스

원기둥 뚜껑 만들기

참고: https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-plane-and-ray-disk-intersection

참고 사이트에 disk 공식을 이용하여 만들었다.
베이스는 평면 공식에 disk를 만드는 공식을 넣어 만들 수 있었다.
약간 다른게 위 사이트에서는 반지름 보다 작을 때 그리게 한 것이고 나는 반지름 보다 클 때 제외하는 것으로 만들었다.
아래 사진은 원기둥을 뺀 상태로 잘 만들어지나 확인해본 것이다. 잘 나온다.

원기둥 + 뚜껑

피라미드 (삼각형 4개, 사각형 1개)

만들었던 오브젝트를 조합하여 만드는 보너스이다.
삼각형 4개와 사각형 1개를 조합하여 만들 수 있었다.

정육면체 (사각형 6개)

만들었던 오브젝트를 조합하여 만드는 보너스이다.
사각형 6개를 조합하여 만들 수 있었다.

sepia 필터

https://dyclassroom.com/image-processing-project/how-to-convert-a-color-image-into-sepia-image

위 사이트에서 나온 세피아를 구하는 rgb값을 이용하여 구할 수 있었다.
간단하게 필터를 확인하는 방법을 .rt파일에 F에 sepia에 s를 써서 확인해주고 화면에 sepia필터를 씌운다.

필터 적용 전

필터 적용 후

원뿔

참고
http://illusioncatalyst.com/notes_files/mathematics/line_cone_intersection.php

참고 사이트에서 수식을 가져와 코드에 적용하여 원뿔을 만들었다.

miniRT result

profile
오늘보다 더 나은 내일

0개의 댓글