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

How to draw the Circle

Prep

Graphics 를 다루기 앞서서, glm 과 imgui 가 필요하다는걸 말씀드리고 싶다. vcpkg 로 설치가 편하니, vcpkg 찾아보기 바란다.

How to draw the Circle in Image Coordinates (2D)

일단 Image Coordinates 에서는 쉽게 far left corner 이 (0, 0) 을가지고 있고, far right corner in bottom 은 (width - 1, height -1) 로 되어있다. 어떠한 Point 가 원안에 있는지 확인을 하려면, 어떠한 Point 와 x_center 값의 절대값이 r 보다 크기 비교를 하면 된다.

일단 Circle 이라는 class 를 만들어보자. 일단 편의성을 위해 접근지정자를 public 으로 해놓고 보면 된다. 그리고 생성자(Constructor)는 원에 필요한 인자로 받는다.

첫번째 방법 같은 경우는 약간 Brute-Force 처럼 곱셈을 할수 있다. 다른 방법같은경우 glm 을 사용해서 point - center 를 뺀값의 distance 를 구하는 방법이 있고, 더 최적화 하는 방법은 radius squared 한값을 가지고 distance squared 를 비교하는 방법이 있다.

#include <glm/glm.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtx/norm.hpp>

class Circle
{
public:
    glm::vec2 center;
    float radius;
    glm::vec4 color; 

    Circle(const glm::vec2& center, const float radius, const glm::vec4& color)
        : center(center), color(color), radius(radius)
    {}

    bool IsInside(const glm::vec2& point)
    {
        const float distance = (point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y);
        
        // 최적화 방법은 여러가지
        // const float distance = glm::length(point - center) or 
        
        // const float distanceSquared = glm::dot(point - center, point - center);
        

        if(distance <= radius){
            return true;
        }
        else{
            return false;
        }
    }
}

아래 처럼 결과를 볼수 있다.

How to draw the Circle for Transformation (2D)

가끔씩은 In Game 내부 안에서는 좌표계 변환을 할 필요가 있다. 예를 들어서 Unity 나 Unreal 같은 경우, 내부 안에 따른 카메라 모듈이 있다고 가정하면, Player 가 보는 관점과 그 카메라가 담고 있는 시점이 다르다. 즉 상대적으로 보고 있는게 다르기 때문에 좌표계 변환이 필요하다. 일단 좌표계 변환에 앞서서 aspect ratio 라는 개념이 필요하다. Screen 에서 aspect ratio 를 구할려면, 뿌려질 화면(Screen) 에 width 와 height 로 나눠줘야 한다.

그렇다면 이런식으로 나타낼수 있을거다.

const float aspectRatio = (float)width / height;

그리고 가정이 또 필요한데 Screen 좌표계에서 직접 World Coordinate System 으로 지정해야한다. Custom 하게 제작을 한다면 Screen 좌표계는 [0 x with - 1] x [0 x height -1] 이고, 정의하고자 하는 좌표계를 [-aspectRatio, +aspectRatio] x [1 + -1] 이라고 지정을 하자. 다시말하면 far left corner on the top 의 위치는 [0, 0] 이였던게 [-aspectRatio, 1] 이되고, [width - 1, height - 1] 이 였던게 [+aspectRatio, -1] 이 되는거다.

그렇다면 변환을 하는식을 코드로 표현 해보자. 설명을 해보자면 일단 도형을 그린다고 했을때 좌표계변환은 원하는 도형이 안나올수 있기때문에 여기서 Scaling 을 구하려면 aspectRatio 에 곱해주어야만 원하는 도형이 나올 수 있다(예: 원이라고 하면 타원이 나올수도 있다.) 그리고 positionScreen 값에 scale 을 곱해서 해당의 한칸당 움직임을 알수 있고 이 좌표계를 -aspectRatio 가 제일 맨윗점이 되어야하므로 -aspectRatio 를 하고, y 같은 경우도 마찬가지로 위의 방향이 -1 부터 시작해야하므로 -1 을 빼준다. 하지만 이게 다가 아니다. 이때 y 의 부호값도 뒤집어줘야 위로 갈땐 양수 아래로 갈땐 음수 이렇게 표현하기위해선 전체 부호를 뒤집어 줘야한다.

glm::vec2 TransformScreenToWorld(glm::vec2 positionScreen)
{
    const float aspectRatio = float(width) / height;
    const float xScale = 2 * aspectRatio / (width - 1);
    const float yScale = 2 / (height - 1)

    return glm::vec2(positionScreen.x * xScale - aspectRatio, -(positionScreen.y * yScale -1));
}

How to draw the Sphere

Rendering 기술이나 Brute force 를 사용한 Ray-tracing 을 보면 주로 Sphere 을 찾아 보기가 쉽다. 그 이유는 Sphere 을 그리기가 쉽기 때문이다. 그렇다면 RayTracing 을 한번 봐보자.

  1. 일단 사람의 눈을 기준으로 잡고 Screen, 즉 한 Pixel 에서 광선을 여러개 쏜다.
  2. 그 다음 각 Pixel 에 있는 Ray 중에, 하나가 구에 부딫친다.
  3. 구에 부딫힌 Ray 는, 구의 색깔의 Pixel 을 가져와서 Screen 에 보여진다 (다음 그림)

이런식으로 Simple 한 Ray Tracing 구조를 가져 올수 있다. 그러면 바로 코드로 표현 해보자. 일단, 구체적인 DirectX 에 관련된 부분은 주제와 조금 알맞지 않으므로 작성하지 않았다. 아래에서 중요한 부분은 Ray, Hit, Sphere 안에 있는 IntersectRayCollision 함수 이부분이다. 일단 Ray 같은 경우는 어떤 시작점에서 어떤방향으로 출발한다는 벡타와 그 Point 를 가지고 있어야하고, Hit 같은 경우 distance 정보와 Hit 을 했을때의 point 좌표와 그거에 해당되는 Normal vector 등 필요할것이다. 그리고 현재 위에서 했던것과 달리 World Coordinate 이 [-1, 1] x [1, -1] 로 바뀌었다는 점을 찾을수 있다. 그래서 여기서 중요한 알고리즘은 Rendering 이 Update 이 될때, RayTracing 에서 Render 라는 함수를 호출하고, Render 에서, 각각의 screen 좌표계에 있는걸 좌표계 변환으로 통해서, 바꾼 다음에 Ray 를 쏠 준비를 하는 것이다.

그런다음에 Ray 를 trace 하면서 물체의 거리를 비교하면서, Sphere 에 Hit 이 됬으면, 그 color 값을 가지고 오는것이다.

struct Vertex
{
    glm::vec4 pos;
    glm::vec2 uv;
};

class Ray
{
public:
    glm::vec3 start;    // start position of the ray
    glm::vec3 dir;      // direction
}

class Hit
{
public:
    float d;            // distance from the start to hit point
    glm::vec3 point;    // point where ray hits
    glm::vec3 normal;   // normal vector that are perpendicular to the surface of sphere 
}

class Sphere
{
public:
    // Property
    glm::vec3 center;
    float radius;
    glm::vec3 color;

    // Constructor
    Sphere(const glm::vec3 &center, const float radius, const glm::vec3 &color) 
        : center(center), color(color), radius(radius)
    {}

    Hit IntersectRayCollision(Ray &ray)
    {
        Hit hit = Hit(-1.0f, vec3(0.0f), vec3(0.0f));
        return hit;
    } 
};

class RayTracer
{
public:
    int width, height;
    shared_ptr<Sphere> sphere;

    RayTracer(const int &width, const int &height)
        : width(width), height(height)
    {
        sphere = make_shared<Sphere>(vec3(0.0f, 0.0f, 0.0f), 0.4f, vec3(1.0f, 1.0f, 1.0f));
    }

    glm::vec3 TransformScreenToWorld(glm::vec2 posScreen)
    {
        const float xScale = 2.0f / (this->width - 1);
        const float yScale = 2.0f / (this->height - 1);
        const float aspect = float(this->width) / this->height;

        return glm::vec3((posScreen.x * xScale -1.0f) * aspect, -posScreen.y * yScale + 1.0f, 0.0f);
    }

    vec3 traceRay(Ray &ray)
    {
        const Hit hit = sphere->IntersectRayCollision(ray);

        if(hit.d < 0.0f)
        {
            return vec3(0.0f);
        }
        else
        {
            return sphere->color * hit.d;
        }
    }

    void Render(std::vector<glm::vec4> &pixels)
		{
            // init all black as background color
			std::fill(pixels.begin(), pixels.end(), vec4{0.0f, 0.0f, 0.0f, 1.0f});

// multi-threading
#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));

					const auto rayDir = vec3(0.0f, 0.0f, 1.0f);

					Ray pixelRay{pixelPosWorld, rayDir};

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

class RayTracingModule
{
public:
    int width, height;
    Raytracer rayteracer;

    // DirectX11 setups..
    ID3D11Device *device;
	ID3D11DeviceContext *deviceContext;
	IDXGISwapChain *swapChain;
	D3D11_VIEWPORT viewport;
	ID3D11RenderTargetView *renderTargetView;
	ID3D11VertexShader *vertexShader;
	ID3D11PixelShader *pixelShader;
	ID3D11InputLayout *layout;

	ID3D11Buffer *vertexBuffer = nullptr;
	ID3D11Buffer *indexBuffer = nullptr;
	ID3D11Texture2D *canvasTexture = nullptr;
	ID3D11ShaderResourceView *canvasTextureView = nullptr;
	ID3D11RenderTargetView *canvasRenderTargetView = nullptr;
	ID3D11SamplerState *colorSampler;
	UINT indexCount;

public:
    RayTracingModule(HWND window, int width, int height)
        : raytracer(width, height)
    {
        Initialize(window, width, height);
    }

    void Update()
    {
        // set pixels as 1D array with color info
        std::vector<glm::vec4> pixels(width * height, glm::vec4(0.8f, 0.8f, 0.8f, 1.0f));

        // Raytracer Render
        raytracer.Render(pixels);

        // Copy CPU Mem -> GPU mem
        // 렌더링 결과를 GPU 메모리로 복사
		D3D11_MAPPED_SUBRESOURCE ms;
		deviceContext->Map(canvasTexture, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms);
		memcpy(ms.pData, pixels.data(), pixels.size() * sizeof(glm::vec4));
		deviceContext->Unmap(canvasTexture, NULL); 
    }

    void Initialize(HWND window, int width, int height)
    {
        this->width = width;
        this->height = height;

        // Swapchain... Set

        // Create Render Target

        // Set the view port

        // Create texture and rendertarget

        // Create the sample state

        // vertex buffer
    }

    void Render()
    {
        // ...
    }

    void Clean()
    {
        // ....
    }
};

int main()
{
    const int width = 1280, height = 720;
    // Imgui setup & winAPI setup

    // HWND hwnd...
     
    auto rayTracingModule = std::make_unique<RayTracingModule>(hmwd, width, height);
    
    MSG msg = {};
    while(WM_QUIT != msg.message)
    {
        if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            // ... do Something
        }
        else
        {
            // ... Imgui start setup
            rayTracingModule->Update();
            rayTracingModule->Render()

            // swap the back buffer and the front buffer
            rayTracingModule->swapChain->Present(1, 0)
        }

    }

    // clean up
}

Resource

Introduction to Raytracing Line-sphere Intersection

Tessellation

// TODO: Graphics Pipeline 추가

DirectX12 의 기준으로, Vertex Shader Stage 를 지난다음, Hull Shader Stage, Tesellator Stage, Domain Shader Stage 가 `Tessellation Stages 이다. Tesellation Stage 도 Geometric Shader Stage 처럼, 정점을 추가하는 Stage 이다.

Dynamic LOD(Level Of Detail) 에 사용, 거리에 따로 Mesh 안에 Polygon 의 숫자가 달라진다.

구성요소 Patch 안에(Control Point Group), Vertex 같은 Point 를 Control Point 라고 불림

Domain Shader 에서 모든 정점정보를 들고 오는데, 그때 정점과의 비율을 나타내는 location 정보를 가지고 있음.. 이거의 예시 필요

Terrain

X, Y 축을 건들지 않고, Z 축만 사용해서 Terrain 이 표현 가능. 높이맵이 필요. (Terrain Surface & High Texture)

Picking

Ray Casting 기술, 카메라위치에서 Ray 를 쏴서 물체가 Hit 했는지 안했는지

Shadow Mapping

결국 한마디로 그림자를 어떻게 만들지? 의 문제이다. 현실에서 그림자가 생기는 이유는, 어떠한 빛(Directional Light) 에 의해서 물체에 비추게 되어있을때, 그 물체가 만약에 빛이 통과되지 않는 물체라고 가정하에, 그 뒤에 배경이 빛을 못받아서 그림자가 생기는 이유이다.

Pixel Shader 에서 물체의 색상을 고려할때, 앞에 물체가 있는지 없는지만 판단?

Directional Light 에 카메라를 두고, WorldViewProjMatirx -> ClipPos -> W 로 나누기 -> ProjPos(투영) -> Depth = Proj.Pos.z Depth 를 RenderTarget 에 저장.

그럼 우리가 화면에서 보여지는거는, 우리가 현재 보고 있는 카메라를 기준으로 ViewPos -> ViewInverseMatrix -> WorldPos 으로 간다음에, Directional Light 에 있는 카메라로 가서 ViewPojMatrix 를 보고 있는 화면 카메라의 WorldPos 에 곱해주면 -> ClipPos 로 넘어오고 -> w 로 나누면 -> ProjPos(투영) 좌표계로 넘어오면 [-1, 1] -> [0 ~ 1], [1 ~ -1] -> [0 ~ 1] -> 이 결과는 UV 좌표계로 넘어오면 Depth 정보를 받아서 비교 후에 그림자 적용을 할수 있다.

Render Target View

Render Target

  • Forward Shader

Rendering Pipeline 물체의 Index 와 Vertex 정보(Topology) 정보를 input assembler 에게 넘겨준다. 그런다음에 이 정점들을 가지고 어떤일을 해야되는지 기술한 다음에 Vertex Shader 와 Pixel Shader 를 통해서 처리를 했다. Output merger 에서 Render Target View 에서 결과를 받아서 뿌려졌었다.

그래서 RTV 를 처리하기 위해서 Command Queue 에서, 어떤 Buffer 에다가 render 를 할지 결정을 했었다.

D3D12_CPU_DESCRIPTOR_HANDLE backBufferView = _swapChain->GetBackRTV();
_cmdList->ClearRenderTargetView(backBufferView, Colors::Black, 0, nullptr);

D3D12_CPU_DESCRIPTOR_HANDLE depthStencilView = GEngine->GetDepthStencilBuffer()->GetDSVCCpuHandle();
_cmdList->OMSetRenderTargets(1, &backBufferView, FALSE, &depthStencilView);

// --> 즉 여기서 OMSetRenderTarget 이 어디에다가 뿌려줄것인가 였다.

_cmdList->ClearDepthStencilView(depthStencilView, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
```c++

그래서 실질적으로 그려지는 부분은 Shader 쪽에서 위에서 작성된 코드와 맞물려서 실행된다.

float4 PS_MAIN(VS_OUT input) : SV_Target { float4 color = float4(1.f, 1.f, 1.f, 1.f); if(g_text_on_0) color = g_text_0.Sample(g_same_0, input.uv); float3 viewNormal = input.viewNormal; if(g_text_on_1)

LightColor totalColor = (LightColor)0.f;

for (int i = 0; i < g_lightCount; ++i)

color.xyz = (totalColor.diffuse.xyz * color.xyz) + totalColor.ambient.xyz * color.xyz + totalColor.specular.xyz;

return color; } ```

이 방식에서 아쉬운 (중요한) 점은, 우리가 열심히 중간 부품을 만들어서 사용하는데, 그게 다 날라간다는것이다. 즉 빛 연산이나, Normal 값들을 사용하지 않은채, Color 로만 return 을 하니까 중간들 계산값들이 날아가게(손실) 된다는것이다.

Compute Shader & Particle System

잠깐 언급이 됬듯이 CPU 와 GPU 의 차이점은 바로 연산을 담당하는 Core 의 개수이다. CPU 같은 경우는 “Optimized for Serial Tasks” 라는 점과 GPU 같은 경우는 “Optimized for many parallel tasks” 라는 점이다.

[TODO: Compute Shader Image 추가]

일단 Shader 파일에 사용되는 곳에 RWTexture2D<float4> g_rwtex_0 : register(u0): 이런 Register 를 사용하는걸로 보인다. u0 는 Computer Shader 전용으로 사용되는 친구이다. 여기서 궁금할수 있는게 RW 즉 Read Write 할수있는 Texture2D 이다.

예를 들어서, Microsoft 공식 문서에 가보면 ID3D12GraphicsCommandList::Dispatch 봐보면, 3 차원 처럼 ThreadGroupCount 가 X, Y, Z 로 나누어져있는걸로 보인다. 그래서

[numthreads(1024, 1, 1)]
void CS_Main(int3 threadIndex : SV_DispatchTrheadID)
{
    if (threadindex.y % 2 == 0)
        g_rwtex_0[threadIndex.xy] = float4(1.f, 0.f, 0.f, 1.f);
    else
        g_rwtex_0[threadIndex.xy] = float4(0.f, 1.f, 0.f, 1.f); 
}

Particle System

Particle System 이란 것은 결국 어떤 물체의 Effect 효과 중에 Particle 이 튀는것처럼 보이는게 Particle System 이라고 한다. 그런데, 생각을 해보면 이 Particle System 작업은, 메인 작업에 비해 생각보다 중요도가 낮다. 그래서 만약에 Particle System 이 메인이되서 Rendering 을 하게 된다면, 프로그램의 부하가 일어날것이다. 그래서 이걸 해결할수 있는게 바로 Instancing 으로 해결하면 된다. Instancing 에서 SV_InstanceID 를 사용하게 되면, 각 Particle 에 ID 가 번호가 매겨지게 된다. 그리고 Particle 의 이러한 특징으로 인해서 CPU 에서 계산하는게 아니라, GPU 에서 계산하게끔 넘겨주는 역활만 하면 된다. 그래서 VRAN 에게 Particle 에 대한 정보를 가지고 있고, GPU 에서 매번의 Tick 마다 Particle 이 죽었는지 살았는지 이런식으로 넘기면 된다.

Instancing

keyword : drawcall / instancing 이 필요한 상황 -> 많은 부하.

vertex buffer 를 여러개(1 - 15) 사용 가능. 예: 첫번째 Buffer 에서는 물체의 정점 정보를 나타내고, 두번째 buffer 는 각 물체의 Position 정보 등.

버퍼의 종류 (Structure Buffer, Constant Buffer, Instancing Buffer)

DirectX Projection

Orthographic Projection (직교 투영)

Graphic (Rendering) Pipeline Local -> World -> View -> Projection -> Screen

만약에 직교투영이 필요한 케이스는 바로 2D UI 를 작업할때 필요한데, 이때 Layer 라는게 필요하다. 그래서 한카메라는 UI 를 제외한 것을 보고, UI 의 카메라가 따로 있는거다. Unity 같은 경우 32 Layer 이 있다.

Perspective Projection (원근 투영)

Pagination


© 2021. All rights reserved.