Rasterization

Introduction (Bottleneck of Ray Tracing Method)

Ray Tracing 으로 Rendering 을 하게 되면, 여러 Bottleneck 이 존재한다. 일단 모든 물체에 대해서 물체가 부딫혀서 색깔을 가져와야하며, 물체의 Material 이 Transparncy 나 Reflection 이 있다면 반사 또는 굴절(reflection ray & refracted ray) 를 만들어서 다시 쏴주는것도 한계가 있으며, 또 물체의 그림자를 표현하려면, shadow ray 를 만들어서 물체의 충돌을 계산해야한다. 그래서 실시간으로 Rendering 을 하기에는 Computation Expensive 하다. 물론, 다른 Data Structure 를 사용한다면, 분명 최적화가 가능하지만, 일단 이러한 점을 CPU 에서 해결하는게 효율적이지 못하다.

이걸 해결하기위한 방법이 Rasterization Algorithm 이다. 일단 Approach 자체가 다르다.

Rasterization Algorithm

일단 앞서서, Ray Tracing 을 사용할때, 역방향으로 Ray 를 쏴줬다. 더 자세하게 설명을 하면, 우리가 보는 위치(눈) 에서 Ray 를 Screen 으로 100 개를쏴서, 그 100 개가 물체에 충돌하는지 충돌 안하는지를 계산을 했었다. 하지만, Rasterization 에서는 물체의 형상을 Screen 좌표계로 투영을 시킨다는 점이다. 즉 정점들을 Screen 좌표로 투영을 시켜서, 삼각형이라고 한다면, 그 3 개의 Vertex 를 정점을 투영시켜, 정점들을 연결시켜서 Render 또는 그리면 된다. 그래서 Screen 좌표 Block 에 삼각형이 들어있는지 없는지만 체크하면된다. 마치 충돌처럼 보일수는 있어도, Pixel 이 있는지 없는지만 확인을 하면 되기때문에, 더 빠르다. 아래의 그림을 보면 조금더 이해가 잘될거다.

Rasterization 의 알고리즘의 Step 은 Ray Tracing 과 비슷하면서도 다르다. 예를 들어서 Ray Tracing 같은 경우, 모든 Pixel 에 대해서 Loop 을 돌면서 모든 물체에 Hit 을 하는지 안하는지를 체크 한이후에, 물체에 부딫히면 물체의 색을 결정했었다. Rasterization 같은 경우, 기준이 scene 에있는 모든 물체에서 Loop 을 돌고, 그 이후에 모든 물체의 Vertex 들을 투영을 시킨다음에, 그다음에 Pixel 을 돌면서, 그 Pixel 이 물체안에 들어가있는지 없는지를 체크를 한이후에 들어가 있다면, color 값을 가지고 오면 된다. 여기서 중요한건 Rasterization Algorithm 은 object-centric 이라는 점이다. 즉 도형의 Geometry 를 image 좌표계로 바꿔서, 그 Image 를 Loop 을 돌기 때문이다.

즉, 가상공간에 있는 삼각형을 가지고, Screen 좌표계로 투영을 시킨 이후에, pixel 마다 체크 하면서, 삼각형 밖에 있는 Pixel 인가, 안에 있는 Pixel 인가 체크 하면서, 들어있을때는 Screen 에다가 색깔을 칠해주고, 아니면 색깔을 안칠하면 되는 식이다. 근데 모든 Pixel 을 돌게 되면 되게 비효율적이다. 그래서 가장 작은 Bounding Box 를 그려서 효율성을 높인다.

Rasterization Prep

  • Baycentric Coordinates 여기에서 알아볼 점은, 삼각형 정점 PQR 이 존재한다고 했을때 그 안에 T 를 Affine Sum 으로 표현하는게 중요하다.

위의 그림과 같이 R 의 좌표와 T 의 좌표를 이미 알기때문에, R 과 T 를 포함하는 직선을 그려서, PQ 직선의 점 S 에 그리게 되면 P 와 S 의 거리, S 와 Q 의 거리의 비를 찾을수 있다. 그러면 S 의 위치를 S = P + (1 - alpha)(Q - P) 라는 식을 구할수있다.

그 이후에, 아래와 같이 T 의 위치를 PQ 에서 S 의 위치를 구하듯이, Beta 를 이용해서 구할수 있다. 그래서 T = beta * S + (1 - beta)*R 이런식으로 구한다음에 S 의 값을 위의 식에 대입을 하면 T 의 좌표를 Affine Combination 을 구할수 있다. T = alpha * beta * P + beta(1 - alpha)*Q + (1 - beta)*R 이런식으로 표현이 된다.

Rasterization Implementation (How to draw Triangle)

언제나 삼각형 그리는게 기본중에 기본이다. 일단 Rasterization 어떻게 구현해야될지 class 구조를 짜보자. 일단 Header 파일에서 Rasterization 의 생성자를 만들때, with 와 height 의 정보를 가지고 오며, ProjectWorldToRaster 같은 경우는 World 좌표계에 정의된 정점들을 Screen 좌표계로 이동시키면 되는 함수가 있으며, Util Function 으로는 Edge Function 이 있다. 그리고 Render 와 매 Frame 마다 Update 를 하는 Update 함수가 있다.

using namespace glm;
using namespace std;

struct Vertex
{
  vec3 pos;
  vec3 color;
};

struct Triangle
{
  Vertex v0, v1, v2;
};

class Rasterization
{
public:
  Rasterization(const int &width, const int &height)
  vec2 ProjectWorldToRaster(vec3 point);
  float EdgeFunction(const vec2 &v0, const vec2 &v1, const vec3 &point);
  void Render(vector<vec4> &pixels);
  void Update();


public:
  int width;
  int height;
  Triangle triangle;
};

void Rasterization::Rasterization(const int &width, const int &height)
  : width(width), height(height)
{
  triangle.v0.pos = {0.0, 0.5, 1.0f};
  triangle.v1.pos = {1.0, -0.5, 1.0f};
  triangle.v2.pos = {-1.0, -0.5, 1.0f};
  triangle.v0.color = {1.0f, 0.0f, 0.0f}; // Red
  triangle.v1.color = {0.0f, 1.0f, 0.0f}; // Green
  triangle.v2.color = {0.0f, 0.0f, 1.0f}; // Blue
}

void Rasterization::Render(vector<vec4> &pixels)
{
  // Compute World Coordinates to Screen Coordinates
  const auto v0 = ProjectWorldToRaster(triangle.v0.pos);
  const auto v1 = ProjectWorldToRaster(triangle.v1.pos);
  const auto v2 = ProjectWorldToRaster(triangle.v2.pos);

  // Find the bounding box
  const auto xMin = size_t(glm::clamp(glm::floor(std::min({v0.x, v1.x, v2.x})), 0.0f, float(width - 1)));
  const auto yMin = size_t(glm::clamp(glm::floor(std::min({v0.y, v1.y, v2.y})), 0.0f, float(height - 1)));
  const auto xMax = size_t(glm::clamp(glm::ceil(std::max({v0.x, v1.x, v2.x})), 0.0f, float(width - 1)));
  const auto yMax = size_t(glm::clamp(glm::ceil(std::max({v0.y, v1.y, v2.y})), 0.0f, float(height - 1)));

  for(size_t j = yMin; j<= yMax; j++){
    for(size_t i = xMin; i <= xMax; i++){
      // Check if the pixel is inside of triangle
      // Get the pixel info
      // A Parallel Algorithm for Polygon Rasterization
      const vec2 point = vec2(float(i), float(j));

      const float alpha0 = EdgeFunction(v1, v2, point);
      const float alpha1 = EdgeFunction(v2, v0, point);
      const float alpha2 = EdgeFunction(v0, v1, point);

      if (alpha0 >= 0.0f && alpha1 >= 0.0f && alpha2 >= 0.0f) {
          const float area = alpha0 + alpha1 + alpha2;

          const float w0 = alpha0 / area;
          const float w1 = alpha1 / area;
          const float w2 = alpha2 / area;

          const vec3 color = (w0 * triangle.v0.color + w1 * triangle.v1.color + w2 * triangle.v2.color);

          pixels[i + width * j] = vec4(color, 1.0f);
      }
    }
  }
}

vec2 Rasterization::ProjectWorldToRaster(vec3 point)
{
  // ** Orthographics Projection ** //
  // Convert to NDC(Normalized Device Coordinates)
  // NDC Range [-1, 1] x [-1, 1]

  const float aspect = float(width) / height;
  const vec2 pointNDC = vec2(point.x / aspect, point.y)

  // Rasterization Coordinates Range: [-0.5, width -1 + 0.5] x [-0.5, height - 1 + 0.5]
  const float xScale = 2.0f / width;
  const float yScale = 2.0f / height;

  // NDC -> Rasterization
  return vec2((pointNDC.x + 1.0f) / xScale - 0.5f, (1.0f - pointNDC.y) / yScale - 0.5f);
}

float Rasterization::EdgeFunction(const vec2 &v0, const vec2 &v1, const vec2 &point)
{
  const vec2 a = v1 - v0;
  const vec2 b = point - v0;
  return (a.x * b.y - a.y * b.x) * 0.5;
}

여기서 중요한건 위와 같이 Render 를 할때, 각 정점을 Orthographics Projection 해주는 함수 ProjectWorldToRaster 를 통해서, Screen 좌표계로 옮겨주고, Bounding Box 를 찾을수 있게 해준다음에, Bounding Box 안에서 Edge Function 알 사용해서, Pixel 이 삼각형 안에 들어가져있는지 없는지 확인을 한 이후에, Barycentric Coordinates 을 사용해서 Pixel 값을 정해주면 된다. 참고: Edge Function 같은 경우, Pixel 이 삼각형안에 들어왔는지 없는지를 확인 하는 함수이다. 아래의 그림을 참고하자.

그래서 삼각형의 결과는 이러하다.

Resource

Backface Culling (Ray Tracing / Rasterization)

Culling 이라는 단어는 “select from a large quantity; obtain from a variety of sources.” 이렇게 정의가 된다. 여기서는 Backface culling 은 마치 Backside 를 골라서 없앤다라고 더 이해하면 될것같다. 즉 우리눈에 보이지 않는걸 구지 그릴 필요라는 의미에서 생긴다. 그렇다면 그 다음 질문은 왜? 그리지 않을까? 당연히 Rendering 을 할때, 구지 뒷면을 할 필요가 없다.

즉 우리 눈에 보이는 위치에서 물체를 뒷면을 고려하지 않고 그리는거다. 일단, 그렇다면 여기서 중요한거는 바로 수학이다. 어떻게 뒷면이고 앞면인지를 알수 있을까? 일단 Screen 좌표계가 있다고 가정하면, 세점의 vertex 의 normal 의 방향을 계산 하는 cross product 를 하는 방식에 따라서 앞면과 뒷면으로 나누어진다.

Follow your Heart

Follow Your Heart

The reason why I purchase this book was a random decision that I made while taking the subway. I can’t really recall when it was, but i was having some sad moments for sure. It took an while to read this book because this type of book takes a lot of time for me, and it just too obvious how the story will move along. The main character this book struggle some kind of neuralogical disorder. I am not quite sure name of disorder to be honest.

Texturing

Texturing

쉽게 말해서, 우리가 일일히 도형의 Pixel 에다가 색깔을 넣어서 그림을 넣어주기는 너무 번거롭다. 그렇다면 2D 이미지를 가지고 와서 그 도형에다가 덮어버리는 기술을 Texture Mapping 이라고 한다. 관련된건 Resource 를 참고 하자. 일단 브레인 스토밍을 해보자, 어떤 Texture 이미지를 Screen 안에 있는 또는 가상 공간안에 있는 도형에 맞춰서 그릴려고 한다. 다시 말해서, Texture 위치에 어느 색깔값을 가지고 와서, 도형에 그릴까? 라는것이 되어야한다. 그럴려면 좌표계 변환이 필요하다. 아래의 그림을 한번 참고 해보자.

위의 그림과 같이 Texture Coordinates 를 uv 좌표계라고 한다. 왼손좌표계를 DirectX 에서 사용하므로(OpenGL 은 반대), 맨윗 오른쪽이 (0, 0) 을가지고 width 가 1 이고, height 가 1 좌표계를 만들수 있다. 그래서 본론으로 돌아와서, 이 Texture 을 어떻게 도형에다가 매칭 시켜줄지, 즉 임의의 UV 좌표에 있는 색깔값을 도형에 넣는 방법을 Sampling 이라고 한다. 여기서 Sampling 방법은 두가지가 있다. 하나는 Mapping 을 할때, 임의의 UV 좌표를 이미지 좌표에 매핑이 되었을때 가장 가까운 Pixel 값만 가져오는게, Point Sampling 이라고 하고, Interpolation 을 해서 Linear 하게 부드럽게 표현하기위해서 Sampling 하는 기법을 Linear Sampling 이라고 한다.

일단 uv 좌표계에서, 이미지의 좌표계로 변환하는과정을 살펴보자. 텍스춰링의 좌표 범위는 앞서 말한것 처럼 [0.0, 1.0] x [0.0, 1.0] 여기에서 이미지 좌표를 변경하려면, [-0.5, width - 1 + 0.5] x [-0.5, height - 1 + 0.5] 의 좌표에 Mapping 을 해야한다. 여기에서 0.5 가 Padding 처럼 붙여있는이유는, 일단 UV 좌표계에서 한 pixel 이 pixel block 정중앙에 위치해있다고 생각했을때, 실제 UV 좌표의 array 또는 메모리가 저장되는 측면에서는 [0, width-1] x [0, height - 1] 인덱싱을 할수 있지만, 어떤 임의점을 표현을 하려면 0.5 씩 벌어져야 그 중앙에 가까운 값들을 가지고 올수 있어서 표현되어있다.

vec2 xy = uv * vec2(float(width), float(height)) - vec2(0.5f);

Sampling (Point & Linear)

여기에서 Sampling 을 하는 것을 설명을 하려고한다.

Point Sampling Code 는 아래와 같다. GetClamp 함수는 이미지가 좌표가 넘어가는걸 Clamping 하기위해서, 그리고 Point 같은 경우는 Pixel 이 Pixel block 정중앙에 있기 때문에 제일 가까운걸 가져다 쓰고 싶어서 glm::round 를 사용했다.

class Texture
{
public:
    int width, height, channels;
    std::vector<uint8_t> image;
    Texture(const std::string &fileName);
    Texture(const int& width, const int &height, const std::vector<vec3> &pixels);

    vec3 GetClamped(int i, int j)
	{
		i = glm::clamp(i, 0, width - 1);
		j = glm::clamp(j, 0, height - 1);
		const float r = image[(i + width * j) * channels + 0] / 255.0f;
		const float g = image[(i + width * j) * channels + 1] / 255.0f;
		const float b = image[(i + width * j) * channels + 2] / 255.0f;
		return vec3(r, g, b);
	}

    vec3 SamplePoint(const vec2 &uv) // Nearest sampling이라고 부르기도 함
	{
		vec2 xy = uv * vec2(float(width), float(height)) - vec2(0.5f);
		int i = glm::round(xy.x);
		int j = glm::round(xy.y);
		// return GetClamped(i, j);
		return GetClamped(i, j);
	}
}

결과는 아래와같다.

또 다른건 바로 Linear Sampling 이다. Linear Sampling 같은 경우 Pixel 을 가지고 오지만, 그 Point 와 Point 사이의 Pixel 값을 자연스럽게 추가해줘야되기 때문에 Linear Interpolation 을 사용해야 한다. 또한 가로축 Sampling 을 할뿐만아니라, 세로축도 Sampling 을 해야 한다. 즉 Linear Interpolation 을 두번하는걸 Bilinear Interpolation 이라고 한다.

구현은 비슷하지만, GetWrapped 와 GetClamp 에 따라서 결과값이 달라진다. 그리고 Bilinear Interpolation 가로축에 해당되는 부분을 a 라고 지정했으며, 세로축에 해당되는 부분을 b 로 저장해서 사용되었다.


vec3 InterpolateBilinear(const float &dx, const float &dy,
    const vec3 &c00, const vec3 &c10,
	const vec3 &c01, const vec3 &c11)
{
	vec3 a = c00 * (1.0f - dx) + c10 * dx;
	vec3 b = c01 * (1.0f - dx) + c11 * dx
	return a * (1.0f - dy) + b * dy;
}

vec3 SampleLinear(const vec2 &uv)
{
	const vec2 xy = uv * vec2(float(width), float(height)) - vec2(0.5f);
	const int i = int(glm::floor(xy.x));
	const int j = int(glm::floor(xy.y));
	const float dx = xy.x - float(i);
	const float dy = xy.y - float(j)
	return InterpolateBilinear(dx, dy, GetClamped(i, j), GetClamped(i + 1, j), GetClamped(i, j + 1), GetClamped(i + 1, j + 1));
}

결과는 아래와같다.

Texturing

위와같이 사용을했을때 GetWrapped 와 GetClamped 를 사용해서 원하는 Domain 에 따라 사용되는게 다를수도있고, 그리고 현재 Texture 에 빛의 효과를 따로 줄수도 있다.

SuperSampling

Super Smapling 기법 같은 경우 Alisaing 을 지우기 위해서 사용이 된다. Alias 는 이미지나 물체가 Sample 될때, distortion 이 되는 효과를 말을 한다. 아래의 이미지를 참고 하기 바란다.

이런 상황일때 사용할수 있는게 Supersampling 기법이다. 요즘엔 Deep Learning 을 사용한 Super Sampling 이 있다. 일단 Super Sampling Sampling 을 하는 방법은 여러가지가 있다. 구현하고자 하고 싶은건 Grid algorithm in uniform distribution 으로 Sampling 을 할거다.

일단 Recursive 방식으로 할거고, 4 개의 점을 이용해서 Sampling 을 하려고 하니까 2x2 filter 처럼 구현된걸 볼수 있다. Recursive Level 이 높을수록 Sampling 하는게 다르다.

vec3 traceRay2x2(vec3 eyePos, vec3 pixelPos, const float dx, const int recursiveLevel)
{
	if (recursiveLevel == 0)
	{
		Ray myRay{pixelPos, glm::normalize(pixelPos - eyePos)};
		return traceRay(myRay);
	
	const float subdx = 0.5f * dx
	vec3 pixelColor(0.0f);
	pixelPos = vec3(pixelPos.x - subdx * 0.5f, pixelPos.y - subdx * 0.5f, pixelPos.z)
	for (int j = 0; j < 2; j++)
	{
		for (int i = 0; i < 2; i++)
		{
			vec3 subPos(pixelPos.x + float(i) * subdx, pixelPos.y + float(j) * subdx, pixelPos.z);
			Ray subRay{ subPos, glm::normalize(subPos - eyePos) };
			pixelColor += traceRay2x2(eyePos, subPos, subdx, recursiveLevel - 1);
		}
	}
	return pixelColor * 0.25f;
}

결과는 아래와 같다.

Sample = 2

Sample = 4

Resource

How to create the shadow

Shadowing

빛이 있는곳에 우리가 걷거나 어떤 행동을 하게 된다면, 우리의 모습과 똑같게 그림자가 있는걸 확인 할수 있다. 그림자는 결국 물체에 빛이 부딛혔을때, 그 뒷부분에다가 광원과 비슷한 방향으로 그림자를 그려주면 된다. 즉 조명의 위치나 물체의 위치에따라서 그림자가 결정된다.

그리고 참고로 Real-time graphics 에서는 광원에서 물체가 있다고 하면은 뒤에는 그림자를 붙여주는 형식으로 한다.

조금 원리를 짚고 넘어가자.

  1. 눈에서 나오는 Ray 가 Screen 을 지나서 어떤 바닥에 부딫힌다.
  2. 그 바닥에 부딫힌 Ray 에서, 다시 Shadow Ray 를 쏴준다.
  3. 그때 물체가 있다면 충돌할거고, 없으면 바로 광원쪽으로 갈거다.

여기서 두번째 스텝 같은경우 Shadow Ray 를 쏜다는 의미가 바닥에서 쏜다는 의미보다는 Shadow Ray 가 광원까지 도달할수 있는지 없는지를 체크하는것이다. 그러면 Diffuse 와 Specular 은 없애버리고, ambient color 만 검은색으로 칠해주면된다. 참고로 Shadow Ray 에서 쏠때, 작은 수치의값을 움직인다. 위의 내용은 아래의 그림과 같다.

코드는 아래와 같다.

glm::vec3 color(hit.obj->amb);

const vec3 dirToLight = glm::normalize(light.pos - hit.poin)
Ray ShadowRay = { hit.point + dirToLight * 1e-4f, dirToLight }
if (FindClosestCollision(ShadowRay).d < 0.0f)
{
	const float diff = glm::max(dot(hit.normal, dirToLight), 0.0)
	const vec3 reflectDir = 2.0f * dot(hit.normal, dirToLight) * hit.normal - dirToLight;
	const float specular = glm::pow(glm::max(glm::dot(-ray.dir, reflectDir), 0.0f), hit.obj->alph)
	color += hit.obj->dif * diff + hit.obj->spec * specular;
}
return color;

결과는 아래와 같다.

Resource

Baycentric Coordinates

Introduction to Barycentric Coordinates

이제는 도형을 그려봤으니, 도형에 색깔을 입혀보는것도 중요하다. 도형에 어떻게 색상을 정해줄지는 유저의 자유지만, 내부 알고리즘이 어떻게 돌아가는지는 알아야한다.

근데 만약에 각 Vertex 당 색이 정해졌다고 하면, 편하게 알아서 그라데이션을 해줄수 있는 방법이 있을까하면, 바로 Barycentric Coordinates 이 있다. 즉 가중치를 줘서 Pixel 값에 영향을 끼치는거다.

여기서 Barycentric Coordinates 바로 넘어가기전에 Interpolation 을 한번 생각해보자.

Interpolation or Linear Interpolation

아래의 그림을 살펴보자.

일단 v1 과 v0 이 존재한다고 했을때, 임의의 V 의 점을 찾으려고 할때 Linear Interpolation 을 사용해서 그 Pointer 를 구할수 있다. 예를 들어서 V 의 값이 v0 에 가까워진다고 했을때 b 에 가중치가 더들어가서, b / a + b 는 커질것이다. 마찬가지로 반대 일 경우이는 a / a + b 의 가중치가 거질것이다. 또 이 가중치의 합은 1 이므로, 이미지에 표현된것과 같이 표현이 가능하다.

Barycentric Coordinates

위의 내용과 같이 생각을 해보자면 삼각형에도 적용이 가능할것이다. 어떠한 점 P 를 잡고, Vertex 를 표현했을때 P = v0 + v1 + v2 / 3 이라고 표현될것이다. 더 쉽게 표현한다고 하면 W 를 사용해서 p = w0v0 + w1v1 + w2v2 이런식으로 표현이 될것이다. 그리고 각 W 는 Point (P) 를 사용해서 작은 삼각형의 생각한다면 넓이의 비율로 구할수 있을것이다. 넓이 뿐만아니라 각 Vertex 의 색상이 주어졌다면, 똑같이 구할수 있다.

Point (P) 의 색깔 Color(c) 가 있다고 가정을 한다면, 위에 표시한것처럼 Color 도 c = w0c0 + w1c1 + w2c2 로 표현이 가능할거다.

설명은 이쯤하고 구현하는 코드를 공유하려고 한다.

const vec3 cross0 = glm::cross(point - v2, v1 - v2);
const vec3 cross1 = glm::cross(point - v0, v2 - v0);
const vec3 cross2 = glm::cross(v1 - v0, point - v0
if (dot(cross0, faceNormal) < 0.0f)
    return false;
if (dot(cross1, faceNormal) < 0.0f)
    return false;
if (dot(cross2, faceNormal) < 0.0f)
    return fals

const float area0 = glm::length(cross0) * 0.5;
const float area1 = glm::length(cross1) * 0.5;
const float area2 = glm::length(cross2) * 0.
const float areaSum = area0 + area1 + area

float w0 = area0 / areaSum;
float w1 = area1 / areaSum;
float w2 = 1 - w0 - w1;

이 코드를 사용한다고 하면, 아래와 같이 삼각형에 부드럽게 Pixel 이 적용되어있는걸 확인할수 있다.

How to draw a triangle

How to draw a Triangle

앞에 Post 에서는 원의 방정식을 이용해서 물체를 그렸었다. 하지만, 더 Computer Graphics 를 접한다면 삼각형을 먼저 그려보아야한다. 왜냐하면 바로 모든 물체는 삼각형으로 그려지고 기본이기 때문이다. 그래서 도형에서는 Mesh 가 많으면 많을수록 그 도형이나 물체가 더 정밀하게 보인다.

일단 왜 삼각형이 기본의 도형이냐면 첫째는 점을 이용해서 접혀지지 않는다. 사각형같은 경우 Vertex 가 4 개 이기때문에 양쪽 대각선을 이용해서 삼각형을 만들수 있다.

삼각형을 구조를 한번 짜보자. 일단 아래와 같이 필요한 생성자들이 있고, Ray 의 충돌 여부를 확인 하는 함수가 있다. 물론 모든 물체를 Object 부모 클래스를 만들어서, Hit CheckRayCollision 을 순수 가상함수로 만들어서 관리를 해도된다.

class Traiangle
{

public:
    vec3 v0, v1, v2;

    Triangle()
        : v0(vec3(0.0f)), v1(vec3(0.0f)), vec3(vec3(0.0f))
    {}
    
    Triangle(vec3 v0, vec3 v1, vec3 v2)
        : v0(v0), v1(v1), v2(v2)
    {}

    Hit CheckRayCollision(Ray &ray)
    {}
};

원같은 경우 원의 반지름안에 임의의 Point 가 들어오느냐 안들어오느냐는 생각보다 easy, but Triangle 은 살짝 다르다. 일단 나중에 공부할 Barycentric Coordinate 을 생각하면 편할텐데. 일단 삼각형 안에 들어 왔는지 안들어왔는지는 수식으로 보면 편하다.

예를 들어서 ray 의 식을 P = O + tD 라는 식이있다. 그리고 어떤 Point 가 삼각형의 임의의 Point 에 맞는다고 생각하고, 그 평면 안에 있다고 생각을 하고 세운것이다. 그렇다고 하면 그 Point 같은경우 어떠한 Vertex (v0) 이 존재한다고 했을때, Point 에서 v0 까지 가는 Vector 는 삼각형 평면의 Normal 값과 90 이어야한다. 즉 glm::dot((P - V0), Normal) = 0 이 나와야한다, 그 P 는 Ray 가 쏘는 방향이므로 Ray 의 식을 대입을 해보면 t 의 값을 찾을수 있다. 여기에서 t 는 거리인 scalar 값이다. 그렇다고 한다면, 우리는 식을 고쳐서 t 를 기준으로 세울수 있다.

t = (glm::dot(v0, Normal) - glm::dot(orig, Normal)) / glm::dot(dir, Normal) 여기에서 dir 은 Ray 의 방향 벡터이고, Orig 는 Ray 의 시작 점이다. 이러한 방법으로 t 에 따라서 충돌점 Point 를 찾을수 있다.

바로 코드로 한번 봐보자.

bool IntersectRayTriangle(const vec3 &orig, const vec3 &dir,
const vec3 &v0, const vec3 &v1, const vec3 &v2, vec3 &point, vec3 &faceNormal, float &t, float &u, float &v)
{
    faceNormal = glm::normalize(glm::cross(v1 - v0, v2-v0));
	
    // Backface culling
	if (glm::dot(-dir, faceNormal) < 0.0f) return false;
	
    // 광선과 평면의 Normal 과 수평일경우
    if (glm::abs(glm::dot(dir, faceNormal)) < 1e-2f) return false;

	t = (glm::dot(v0, faceNormal) - glm::dot(orig, faceNormal)) / glm::dot(dir, faceNormal);
	
    // 광선이 뒤에서 충돌했을때? retrun false
    if (t < 0.0f) return false;
	
    point = orig + t * dir; // 충돌점

	// 작은 삼각형들 3개의 normal 계산
	const vec3 normal0 = glm::normalize(glm::cross(point - v2, v1 - v2));
	const vec3 normal1 = glm::normalize(glm::cross(point - v0, v2 - v0));
	const vec3 normal2 = glm::normalize(glm::cross(v1 - v0, point - v0));

	// 아래에서 cross product의 절대값으로 작은 삼각형들의 넓이 계산
	 if (dot(normal0, faceNormal) < 0.0f) return false;
	 if (dot(normal1, faceNormal) < 0.0f) return false;
	 if (dot(normal2, faceNormal) < 0.0f) return false;

	return true;
}

Cross Product 의 성질로 인해서 Normal 값을 계산 할때에 주의할점은 주로 왼손좌표계로 시계방향으로 웬만하면 맞춰줘야하므로, Cross Product 할때 주의하기 바란다. 하지만 어떻게 계산을 하든 시계방향으로 맞춰줘야한다는것만 조심하자.

Resource

Projection

Orthographic Projection

Orthographics Projection 은 정투영, 즉 물체를 원근감없이 그리는거다. 어렸을때 집을 그릴때 원근감을 표현하기 위해서 노력했지만 그럴싸하게 안나오는 이유는 바로 원근감을 생각하지 않고 그리기때문이다. 이럴 경우 집의 표면만 그대로 그리는 건데 이게 바로 정투영이다.

그래서 Computer Grpahics 에서 RayTracing 을 구현할때도 마찬가지이다. 어떤 세도형이 있고 물체가 앞에 있는데도 불구하고 그리는 순서 대로 그렸을때가 바로 정투영 현상이다.

예를 들어서, 일단 코드에서는 빨간색을 먼저 그리고, 초록색 그리고, 파란색을 그렸고, 각 구의 Vertex 들의 Z 는 빨간색, 초록색, 파란색순으로 앞순서로 만들었다. [그림을 참고]

Perspective Projection

어찌됬든 정투영 보다는 더 realistic 한 거는 Perspective Projection (원근 투영) 이다. 앞에 정투영 같은 경우, 각 Screen 에 있는 pixel 들이 똑같은 위치에 시작해서 Ray 를 쏘는 반면, Perspective Projection 같은 경우 각 Pixel 들의 Ray 들이 방향이 다다르게 설정된다, 바로 눈의 시점의 Pointer 를 이용해서. 이럴때 주의점은 그 Ray 들을 전부다 Normalize 를 해줘야한다. 그렇다면 쉽게 코드를 작성해보자. 테스트는 위의 사진을 가지고 첫번째로 그릴거다. 여기서 보면, 일단 Render 만 하기때문에 아직은 어떤 물체가 Ray 에 부딫히는 걸 무시하고, 결과를 써봤다.

일단 제일 중요한거 eye position 을 우리 스크린 뒷방향 즉 1.5 f 에서 바라본다고 하고 작성을 했다. Screen 의 각 Ray 들은 screen 에서 우리 눈의 방향 Vector 를 뺀값들이다.

void Render(std::vector<glm::vec4>& pixels)
{
	std::fill(pixels.begin(), pixels.end(), vec4{ 0.0f, 0.0f, 0.0f, 1.0f })
	const vec3 eyePos(0.0f, 0.0f, -1.5f);

#pragma omp parallel for
	for (int j = 0; j < height; j++)
	    for (int i = 0; i < width; i++)
		{
			const vec3 pixelPosWorld = TransformScreenToWorld(vec2(i, j));

			Ray pixelRay{ pixelPosWorld, glm::normalize(pixelPosWorld - eyePos)};

			pixels[i + width * j] = vec4(glm::clamp(traceRay(pixelRay), 0.0f, 1.0f), 1.0f);
		}
}

근데 여기에서 문제가 제대로 그려지는건 맞지만, Red Sphere 가 green sphere 를 감추게 해야한다. 그러면 제대로 동작하지 않았다는걸 볼수 있다. 그리고 모든 물체의 Radius 는 동일하다.

그래서 고쳐야 할부분은 바로 Closest Hit 이 누구인지 확인만 하면 된다.

Hit FindClosestCollision(Ray& ray)
{
	float closestD = 10000.0;
	Hit closestHit = Hit{ -1.0, dvec3(0.0), dvec3(0.0) }
	for (int l = 0; l < objects.size(); l++)
	{
		auto hit = objects[l]->CheckRayCollision(ray)
		if (hit.d >= 0.0f)
		{
			if (hit.d < closestD)
			{
				closestD = hit.d;
				closestHit = hit;
				closestHit.obj = objects[l];
			}
		}
	}	
	return closestHit;
}

Resource

Lighting Effect

Lighting Effect

일반적으로 물체에 빛을 받아서 반사되어서 나온 색깔을 우리가 보고 “아 저건 빨간색이다, 오 저건 초록색이다” 라고 말을 할수 있을거다. 그래서 Computer Graphics 에서는 물체에 조명에따라서 어떻게 Shaindg 을 하는지 알아보자.

일단 단순한 백색광 Light 를 c++ 로 단순하게 만들어보자면 아래와 같다.

class Light
{
public:
    glm::vec3 pos;
}

이후에 초기화를 할때는

Light light;

light = Light{ {0.0f, 0.0f, -1.0f}} // 화면 뒷쪽

일단 여기에 앞서서 어떤 물체가 어떤 색상을 띄고 있는지, 물체의 표면에 따라 달라질수 있다. 이게 바로 물체의 Material 에 따라 달라진다.

Phong Models & Phong Shading

물체가 조명을 받았을때, Ambient + Diffuse + Specular 이렇게 나누어진다. 왜? 라고 물어본다면, 사실 나도 잘모르겠지만, 어찌됬든 이런 식으로 나눠서 aggregate 하면 뭔가 그럴듯하게 Reflection 이 나온다는 Model 이 Phong Model 이라고 Wiki 에는 나와있다.

그러면 Ambient, Diffuse, Specular 의 어떻게 구하는지, 속성이 뭔지를 알아보자.

Ambient 같은 경우는, 어떤 물체가 색깔을 빛내고 있다고 생각을 하면된다. 즉 이말은 빛이 도달했을때 물체 그대로의 색깔을 return 한다고 생각을 하면된다.

Diffuse 의 원리는 아래의 그림을 참고하면 된다. 만약에 어떤 Light 가 중앙에 물체를 90 degree 로 내려짼다고 했을때, 빛의 세기는 강하다. 하지만 Light Source 가 가 기울어지게 쎄면, 확실히 빛의 세기는 각각 다를것이다. 이럴때 생각을 해보면, 어떤 물체의 Normal 값과 Light Source 의 Opposite direction 의 Vector 의 각도에 따라 빛의 영향이 달라질것이다. 그래서 이 각도를 사용을 하려면 cos(theta) 를 사용하면 된다. [기본적으로 Diffuse 같은 경우는 표면이 거친 상태에서 가정한다. 그래서 난반사 느낌으로 재질을 설정한다.]

Cos Graph 같은 경우 pi / 2 일때 0 이고, 0 일때 1 이다 그걸 생각을 해보면 max(cos(theta), 0.0f) 로 clipping 처리가 가능하다. 물론 여기서 cos(theta) 를 구지 구할필요가 없다. 어떤 경우에 normal vector 와 Light source 의 반대방향의 Vector 가 Unit Vector 일때 glm::dot(n, l) = cos(theta) 를 사용한다면 max(glm::dot(n,l), 0.0f) 의 식으로 변경이 가능하다.

Diffuse 의 구현방식은 생각보다 쉽다.

const vec3 dirToLight = glm::normalize(light.pos - hit.d) // 이미 물체에서 Light 를 바라보는 Vector
const float diff = glm::max(glm::dot(hit.normal, dirToLight), 0.0f);

그래서 diffuse 값의 제곱을 하는게 보이는데, 이거는 Light Power 를 보여주기 위해서 return 을 하게 되면 아래의 사진처럼 나온다.

그 다음 마지막으로 Specular 를 한번 봐보자. 위에서 그림과 같이 Specular 같은 경우, 재질자체가 매끈할때, 금속이나 거울이 완전 반사를 하는 걸 정의한다. 금속이나 거울이 빛을 많이 받았을때, 완전반사가 되어서 우리눈에 부시게 하는 성질을 표현할때 사용된다.

Specular 는 특히나 보는 관점에 따라서 다르다. 즉 눈의 시점에 따라서 아무리 강력한 빛이 오더라도, Light 에 관련된 Power 가 다를 수도 있다.

구현을 해보자면, 아래와 같다

const vec3 reflectDir = 2 * glm::dot(hit.normal, dirToLight) * hit.normal - dirToLight;
const float specular = glm::pow(glm::max(glm::dot(- ray.dir, reflectDir), 0.0f), sphere->alpha);

그래서 전부다 종합하면 아래와 같이 return 을 하면 result 를 볼수 있을거다.

return sphere->amb + sphere->diff * diff + sphere->spec * specular * sphere->ks;

Light Reflection

이제 Light 의 Reflection 을 추가하려면 어떻게 해야될까? 지금까지는 Phong Model 을 사용했더라면, 이제는 Material 에 reflection 을 넣어줘어야한다. 그리고 어떤 물체를 사용할지 모르니 일단 Object Manager 처럼 Object class 에다가 넣어주자.

class Object
{
public:
  vec3 amb = vec3(0.0f);  // Ambient
  vec3 dif = vec3(0.0f);  // Diffuse
  vec3 spec = vec3(0.0f); // Specular

  float alpha = 10.0f;
  float reflection = 0.0f;
  float transparency = 0.0f;

  std::shared_ptr<Texture> ambTexture;
  std::shared_ptr<Texture> difTexture;

  Object(const vec3 &color = {1.0f, 1.0f, 1.0f})
    : amb(color), dif(color), spec(color)
  {}

  virtual Hit CheckRayCollision(Ray &ray) = 0; 
};

이제 어떤 물체를 Spawn 이나 Create 할때, 이 Object class 를 상속받아서 만들면 된다. 그리고 Reflection 과 transparency 가 추가 되어있는 걸 확인 할수 있다. 일단 구현원리는 아래의 노트를 한번 참고 하면서 생각을 해보자.

일단 눈에서부터 나오는 Ray 들을 Screen 좌표에서 각 Pixel 에다가 쏴주고, 그게 어떤 물체에 부딫친다. 만약 그 물체의 Material 특성상 reflected 한 성질을 가지고 있다면, 다시 Reflection Ray 를 다시 쏴준다. 그렇지 않으면, ambient/diffuse/specular 값만 return 을 해준다. 근데 만약 계속 쏘다가 물체와 물체끼리만 reflected ray 를 핑퐁한다고 하면 어떡할까? 이건 어쩔수 없지만 implemenation 에서 recursive level 을 지정해줘서, ray 를 못쓰게 하면 된다. 일단 사용한 모형은 아래와 같다.

아래와 같은 Implementation 을 사용해서, 위의 두물체에 대해서 Reflection 을 적용할수있다. reflection 할 percentage 는 50% 이다.

if (hit.obj->reflection)
{
  const vec3 reflectedDirection = glm::normalize(hit.normal * 2.0f * dot(-ray.dir, hit.normal) + ray.dir);
	Ray ReflectionRay{ hit.point + reflectedDirection * 1e-4f, reflectedDirection };
	color += traceRay(ReflectionRay, recurseLevel - 1) * hit.obj->reflection;
}

이 Core 부분을 적절히 사용을 하면 아래와 같은 결과가 나온다.

Light Refraction

Light Reflection 과 달리 Light Refraction(굴절)은 Material 에 Transparency 를 가지고 있는 물체가 존재 한다는 가정하에 조명이 투과하는 현상이다. 물론 물체가 둘다 존재한다면 일부는 Reflected 하고 어떤부분은 굴절을 할거다. 그게 더 Realistic 한 Object 일거다. 일단 굴절과 관련된 Note 를 참고해보자.

처음에 Ray 가 물체에 부딫힌다고 한다면 그리고 material 에 transparency 가 있다고 가정하면, Ray 가 부딫히는 지점은 두군데이다. 일반적인 Ray Tracing 기법을 사용해서 Ray 를 쏘아줬을때, 제일 먼저 부딫히는 지점을 retrun 을 하지만 굴절을한다고 가정하면, 두번째에 부딫히는 point 가 중요하다. 그리고 Refraction 이 됬을때의 Ray 의 물체의 normal 한 각도와, incoming Ray 의 각도는 과학적으로 증명된 (eta = sin(theta1) / sin(theta2)) 이식이 constant 값을 반환한다. 그렇다고 한다면 우리가 구해야할 vector 는 바로 t 이다. t 위의 노트와 마찬가지로 구할수 있다.

처음에 Implementation 하기전의 모형을 보여주려고 한다. 일단 이 도형의 Material 의 transparency 는 1.0 으로 주어져있다. 그말은 굴절값이 없다는걸 표현 하려고 했었다.

그렇다면 위의 개념으로 코드를 짜보았다.

const float ior = 1.5f; // Index of refraction (유리: 1.5, 물: 1.3)

float eta; // sinTheta1 / sinTheta2
vec3 normal;

if (glm::dot(ray.dir, hit.normal) < 0.0f)// (ex: 공기->유리)
{
	eta = ior;
	normal = hit.normal;
}
else // (ex 유리->공기)
{
	eta = 1.0f / ior;
	normal = -hit.normal;
}
const float cosTheta1 = glm::dot(hit.normal, -ray.dir);
const float sinTheta1 = glm::sqrt(1 - cosTheta1 * cosTheta1);
const float sinTheta2 = sinTheta1 / eta;
const float cosTheta2 = glm::sqrt(1 - sinTheta2 * sinTheta2)
const vec3 m = glm::normalize(dot(hit.normal, -ray.dir) + ray.dir);
const vec3 a = -normal * cosTheta2;
const vec3 b = m * sinTheta2;

const vec3 refractionDir = glm::normalize(a + b);
Ray refractionRay{ hit.point + refractionDir + 0.0001f, refractionDir };
color += traceRay(refractionRay, recurseLevel - 1) * hit.obj->transparency;

return color;

결과는 아래와 같다.

Resource

Pagination


© 2021. All rights reserved.