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 (원근 투영)

Motivation

가끔씩 현업에서 Unreal Engine 이나, 대학원때 공부했었던 Parallel Computing System 을 공부했었을때, 뭔가 부족한 점도 많았고, Wording 이 친숙하지 않아서 인지 아쉬움이 많았다. 그래서 GPU 를 다뤄보는 내용으로 블로그를 쓰려고 한다.


Index

CPU / GPU 설계 철학

  • 반응 시간 (Latency) 우선
  • 처리량 (throughput) 우선

대규모 병렬 컴퓨팅(massively parallel computing)

  • MPC = massivlely parallel computing

Introduction

알다 싶이, CPU (Central Processing Unit) 처음에는 폰 노이만 구조를 가지고 있는 Single Core CPU 로 이루어져있었다. 하나의 긴 Data Bus 아래에 Memory, Processor( CU + ALU ), and I/O Device 로 되어있었다; Control Unit (ALU 의 제어 담당) and Arithemetic-logic unit. 즉 이게 하나였던 Architecture 이였다. 그리고 Dual Core 로 CU / ALU 가 두개가 생겨났었다. 이게 점점 더 늘어나면서 32+ Core 들이 만들어졌다. 하지만 GPU 는 many core 이라고 해서, 애초에 1024~8192+ core 로 GPU 를 만들게 되었다.

고성능 멀티 코어 CPU 같은 경우의 철학 같은 경우, 반응 시간 (Latency) 단축 시키는 철학을 가지고 만들었으며, 그 이유로는 순차 처리 sequential processing 에 적합 하게끔, 그리고 기존 고성능 코어를 추가하는식으로 만들어져있다.

하지만 대규모로 만들어지는 Many-Core 을 철학으로 만든 GPU 같은 경우, 처리량 Thoughput 확대를 위주로 만드는 철학을 가지고 있었다. 즉 Parallel processing 에 집중을 했었고, 성능과는 무관하게, 코어 숫자 증가에 집중을 했다. 즉 Control Unit 와 Cache 는 작지만, 1 개의 Control Unit 자체가 여러개의 ALU 를 관리하고 동시에 실행이 되니 Thread Pool 에 효과적으로 집중을 할수 있고, 이 결과로 단위시간당 처리량 대폭 확대를 할수 있게 됬다.

Models

여기서 Model 은 결국 Device + Programming Language + Compiler + Library 를 뜻한다. 예를 들어서 종류는 아래와 같다.

  1. OpenMP : Open Multi-Processing
    1. 멀티 코어 CPU 용
    2. GPU 로 확장중
  2. CUDA: Compute Unified Device Architecture
    1. NVIDIA GPU 전용 으로 사용되고, 현재는 Cloud Computing 을 사용 가능
  3. OpenCL: Open Computing Language
    1. CPU / GPU / FPGA 모두 제공
    2. Apple, Intel, AMD/ATI, NVIDIA …
    3. 범용성을 추구 하고, 좀 더 복잡한 모델, 교육용으로는 레벨이 높다.

CUDA (Compute Unified Device Architecture) Programming

CUDA 라고 하면 주로 Computer Vision 이나 Computer Graphics 쪽에서 많이 이야기 하게 된다. 여기서는 Computer Graphics 를 조금 집중적으로 이야기를 하려고 한다.

Computer Graphics 은 결국 현실과 똑같은 걸 Computer 를 사용해서 표현 해야하므로, Grpahics Model 들이 있고, 이런 Graphics Model 들은 Physics 나 Optics 에 대한 법칙에 기반에서 만들어져야 했다. 그리고 이러한 Model 들은 현실 세계와 마찬 가지로 부드럽게 Motion 들이 생성이 되어야한다. 더 나아가서 더 빠르게 실시간에서 처리를 해야한다라는게 중요한 포인트의 학문이 바로 Computer Graphics 라고 말을 할 수 있다.

잠시 아래의 reference 된 그림들을 보자면, 아래의 그림들은 흔히 알고 있는 Graphics Pipeline 이다. 결국엔 Host 즉 CPU 에서 대용량으로 처리할 수 있는 데이터를 GPU 로 보낸 다음 저런 Pipeline 을 통해서, 마지막 Frame Buffer 에다가 넘겨서, 우리 화면에 게임과 같은 가상현실들을 볼수 있는거다.

두번째 그림에서는 결국엔 GPU 의 처리 성능 부분이 어디에서 주로 이루어졌냐 라고 물어봤을때는 Vertex ProcessingFrament Processing 즉 수많은 Vertex 로 도형을 만들고, 그리고 이 도형들을 색을 결정하는데에 처리량이 많이 들어가므로, 특히 GPU 의 처리 기능이 저런 Processing 쪽에 들어간다.

CUDA 프로그래밍 (0) - C/C++/GPU 병렬 컴퓨팅(1) CUDA 프로그래밍 (0) - C/C++/GPU 병렬 컴퓨팅(2)

이런 처리 기능 성능 부분들을 어떤 방식으로 처리를 했냐면 바로 GPGPU 의 도임을 말을 할 수 있다. 바로 GPGPU(general purpose graphics processing unit) 이라고 말을 할수 있고, GPU 의 하드웨어를 좋은 계산기로 사용할 수 있는 Techniqe 이라고 볼수 있다.

조금더 구체적으로 CPU 에서 GPU 가 이해 할 수 있는 입력으로 변환을 하고, GPU 가 처리한 이후 어떠한 Image Data 로 다시 CPU 가 받을수 있게 하거나 GPU 의 출력해서 모니터로 불수 있게끔하게 된다. 이러한 방식이 바로 GPGPU (General Purpose GPU Programming) 의 방식이다. CUDA 프로그래밍 (0) - C/C++/GPU 병렬 컴퓨팅(3)

CUDA Architecture

  • Compute Unified Device Architecture
  • GPU 에서 대규모 쓰레드 Thread 를 실행
  • GPU = 대규모 병렬 처리 코프로세서 Massively Data Parallel Co-Processor
  • Model = Device / Computer Architecture + Programming Language + Compiler + Much More
  • CPU 를 범용으로 사용하는 ToolKit 으로 구성 (C / C++)
    • CUDA Driver -> GPU 로 구동
    • CUDA Library -> API 함수들
    • GPU 기능을 직접 제어 가능 -> 최고 효율 획득 GPU Computing Applications

Develop Environment

  • CUDA SDK+ C++ compiler
  • NVIDA Graphics Hardware
  • C++11

Resource

  • https://www.inflearn.com/course/cuda-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%86%8C%EA%B0%9C/dashboard
  • https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html

DirectX Initialization

DirectX12 Background

일단 장치 초기화 하기 이전에, Hardware 를 알아보자. 컴퓨터를 뜯어봤으면 알수 있는건 바로 CPU 와 GPU 를 한번쯤은 봤을것이다. 아래의 구조를 한번 보자.

CPU 는 마치 고급인력 처럼 보인다. CPU 는 뭔가 큰작업을 빠르게 처리할수 있는 ALU 4 개가 있고, 반대로 GPU 같은 경우는 수많은 ALU 가 존재하는데 약간 인력집단처럼 정말 많다. 바로 여기서 힌트를 얻을수 있는게, 단순 또는 값싼 인력을이 많아서, 독립적인 일을 병렬로 처리할수 있게 보인다. 만약 독립적인일을 CPU 로 하기엔 뭔가 고급 인력들을 잡일로 시키는것 같다. 그래서 주로 CPU 같은 경우, 뭔가 데이터나 연산들이 서로가 서로 연관관계일때 주로 사용되는게 좋다. 다시 정리해서 말하면 CPU 는 고급인력이고, GPU 는 약간 외주(outsourcing) 을 하는 느낌이다. GPU 에게 외주를 맡길거면, 어떤 일을 해야 하는지, 사인을 어느 부분에서 해야하는지, 등 알려주는게 좋아서, 바로 Rendering Pipeline 이라는게 정의가 되었다.

Rendering Pipeline in DirectX

아래의 그림이 DirectX3D 11 의 Rendering Pipeline 이다. 각각이 뭐를 하는지 briefly 하게 넘어가보자.

Input Assembler 에서는 정점(Vertex) 의 정보들을 전달해주는거라고 생각을 하면 된다. 예를들어서 삼각형의 정점들이 어떻게 이루어졌는지. Vertex Shader 같은 경우, 아까의 정점들을 가지고 연산을 하는 단계이다. 예를들어서, Morphing, Translation, 등 정점의 이동등을 연산한다. Tessellation StageGeometry Shader Stage 같은경우 정점을 추가시켜주는 작업을 맡는다. 정점을 추가하는 이유는 예를 들어서 선명도를 좀더 표현하기 위해서 정점을 더 더해줘서 quality 를 올리는 등이 있다.

Rasterization Stage 는 정말 중요하다. 이 stage 가 오기전에는 정점을 가지고 놀았다면, 이제 각 정점에 Pixel 들을 입혀줘서, 삼각형이라고 한다면 어느정도 색깔과 모양을 그럴싸하게 표현해주는 stage 라고 생각하면 된다. 그런다음에 Pixel Shader 같은 경우 색상을 변경또는 조절을 담당한다. 그 다음에 최종 결과물을 이제 Output 으로 넘겨주는 형식이다.

이런 PipeLine 을 더 이해하고, 더 나아가서, GPU 에 다가 어떻게 일감을 던져주는지를 알려면 Device Initialization 이 필요하다. 이 아래에 설명이 있으니 한번 정리 해볼려고 한다.

Device Initialization (Component Descriptions)

일단 설계구조는 이렇다. Engine 안에 CommandQueue, DescriptorHeap, Device, SwapChain, RootSignautre 등이 들어간다. 그리고 Engine 내부에서는 초기화(Init) 이 들어가고, Rendering 을 할수 있는 Render 가 들어갈것이다.

일단 Device 를 한번 보자. Device 같은 경우, 인력 사무소라고 생각하면된다. 즉 GPU 와 소통할 공간이라고 생각하면 된다. 일단 Device 에서는 DirectX 를 지원하기위해서 ComPtr 타입을 사용한다. COM(Component Object Model) 인데, 이건 DX 의 programming 의 독립성과 하위 호환성을 위해서 만들어진 객체이며, COM 객체를 통해서, GPU 에 접근을 할수 있다. 즉 COM 을 사용하려면, ComPtr 을 접근을 통해서 COM 을 관리할수 있다.

Window 에서 device 를 사용하려면, Window API 를 사용해야하는데, 이때 CreateDXGIFactory 들지, D3D12CreateDevice 등 사용된다. 이때 사용되는 함수들은 Direct3D 와 같이 사용될수 있게끔 잘만들어줘야한다. 그래서 Device 객체안에서는 Init 이 존재한다.

그 다음 ComandQueue 를 알아보자. DX12 에 나온 친구인데, 뭔가 외주를 요청했을떄 따로 따로 일을 보내주면, 되게 비효율적이다. 그래서 한꺼번에 Command Queue 를 일들을 넣어줘서 실행시키는게 더 효율적이다. 그리고 Fence 라는 개념도 등장하는데, CPU 와 GPU 의 동기화를 맞춰줄떄 사용된다. 그냥 봐서는 일감만 던져주면 된다고 생각하지만, 만약에 그 일감이 돌아오기까지를 기다려서, 그다음 업무를 실행시킬떄는 Synchronize 를 해줘야하는데 그게 Fence 의 역활이다. 여기에서는 Init 과 sync 를 맞추기 위한 WaitSync 가 필요하다. 그리고 실질적으로 Rendering 이 시작되는 부분과 끝부분이 여기에서 필요하다. Rendering 을 한 결과물을 여기에서 RenderEnd 와 RenderBeging 을 통해서 RenderTarget 을 swapchain 에서 불러 들어와, 지금 현재 뿌려져있는걸 뿌리는걸 Handle 하고, RenderEnd 에서는 Backbuffer 와 현재 screen 에 뿌려진걸 바꿔주면서 다시 일감을 준다. 그리고 RenderEnd 에서 실제 수행한다.

그 다음 SwapChain 대해 알아보자. 일단은 직역을 하자면, 교환사슬이다. 이게 어떻게 사용되는지 생각을 해보면 일단 외주과정을 한번 살펴보면, 게임 세상에 어떤 상황을 묘사해서, 어떤 공식으로 계산을 할것인지를 외주 목록으로 던져줘서, GPU 가 열심히 계산을 한다음에 결과물을 받는다. 이 외주과정을 살펴보면, 한번만 한다고 하면 GPU 에서 처리한다음에 그대로 일을 보여주기만 하면 끝난다. 하지만 실제 게임은 그렇지 않다. 매 Frame 마다 게임은 실행되어야되며, 게임이 실행된다는건, 매 frame 마다 화면이 다르기때문에 GPU 의 계산한값과 CPU 로 부터 보여지는 화면등이 계속 바뀌어야한다는것이다. 그럴려면 SwapChain 이라는게 필요하다. 그리고 외주 결과물은 어떻게 받느냐라고 하면, 어떤 종이(컴퓨터에 관점에서는 buffer) 에 그려달라고 건내주면, 뭔가 특수 종이를 만들어서, GPU 에게 건내준다음에 그려줘서 결과물을 받아서 화면에 출력한다. 즉 이떄 하나의 종이로만 받지말고 CPU 와 GPU 가 사용하는 종이를 두개를 만들어서 바꿔주면 된다. 즉 하나는 현재 화면을 그리고, 나머지 하나는 외주의 결과물을 담으면 된다. 즉 이러한 개념이 Double Buffering 이라고 한다. 결론적으로 BackBuffer 에서는 GPU 의 결과값을 담고, 현재화면에서는 현재의 buffer 를 담고있으면 된다.

SwapChain 에는 backBuffer 의 index 를 가지고 있고, renderTarget 이 array 로 존재하기 때문에 buffer 의 Count 에 따라서 renderTarget 이 변경이된다. 그리고 SwapChain 에서 구현해야되는 함수는 어떤 Index 에 renderingTarget 이 있는지와, 현재 어떤것을 뿌려야해는지 등 있다.

그 다음 descriptorHeap 이다. 일단 외주를 하는건 좋다. 하지만 기안서 즉, 어떤 어떤것을 실행해주고, 어떤 형태의 양식을 넘겨줘서 일을 맡겨야 속이 시원하다. 아무형태나 같다주면, 이게 뭐야 쓰레기인가 하면서 처리하지 못한다. 즉 descriptorHeap 에서는 각종 리소스를 어떤 용도로 사용하는지 꼼꼼하게 적어서 넘겨주는 역활이다.

DescriptorHeap 같은 경우 DX12 에서 생겨난건데, 이 descriptor Heap 을 통해서 RenderTargetView(RTV) 를 설정한다. 즉 기안서를 실행을 하면, 어떠한곳에서 Handle 을 하라고 넘겨줄거고, SwapChaing 에서 descriptor 를 실행해 그리고, RenderTargetView 를 생성해서, 만들어지면 SwapChain 에서 바꿔준다. 그리고 Resource 를 넘겨줄때, 그냥 넘겨주는게 아니라, 기안서(View) 를 통해 넘겨줘야하는데 backbufferview 로 넘겨주는 형식이다. 결론적으로 말해서 DescriptorHeap 은 어떤 Array 의 형식으로 이뤄져있는데, array 의 element 가 여러개의 View 를 가지고 있고, 그게 원본 resource 를 가르키고 있다.

즉 Engine main code 에서는 Render 를 할떄 CommandQueue 의 RenderBegin 과 RenderEnd() 가 계속 돌아간다.

그 다음 Root Signature 이다. 이건 약간 결재 또는 계약서이다. 즉 무엇을 대상으로 데이터를 가공하는지를 GPU 가 알고 있어야한다. 즉 CPU 에서 GPU 로 일감을 넘길때 어떤 data 나 resource 를 주는데 가공방법이나 이런걸주는데 서명을 해야 된다. 이것이 root signature 이다. 더나아가서 어떤 버퍼에서는 이런걸을 하기로 계약을하고 다른 버퍼에서는 이걸 계약하겠다라는 승인이 필요하다.

Root Signatuer 을 이해하려면 CPU 와 GPU 의 메모리구조의 이해가 필요하다. Cache 나 Register 안은 메모리의 양도 적지만 빠르다. 하지만 물리적인 관점에서 봤을때 우리가 사용하는 하드메모리는 평민같아서 되게 멀기도하고 딱히 외주를 넘겨줄때 승인이 필요하지 않다. 하지만 Register 나 Cache 같은 친구들은 비싼친구들이기 때문에 조금 승인받는것도 빡세다. 그래서 root signature 같은 경우 option 이 필요하다. 즉 성명 Type 이 필요하다. 처음에는 API 의 칸을 설정을 해주고, 중간에는 어떠한 용도로 사용 될지가 들어가고, 마지막은 shader programming 에서 register 의 bind 할 slot 이라고 생각하면 된다. 여기에서 중요한게 data 를 실제로 넣으려고 하는게 아니라, 계약서에 이러한 이러한 정보를 담겠다가 맞다. 즉 Resource 를 어떻게 어디서 붙힌다라는걸 설명해주는 문서 라고 생각하면 된다. 그리고 Register 를 선택한 이후에, Option 이 들어가는게 Pipeline 안에 stage 들을 보여줄수있는 옵션도 있다.

Root signautre 를 사용할때, constant buffer 를 사용하는데 CPU 와 연결되어있는 메모리를 GPU 에 있는 메모리에 옮겨준다음에, register 에 buffer 의 주소값만 던져주고, 나중에 CommandQueue 에서 처리할때, GPU가 일을 할수 있게 된다.

더 자세한 정보는 여기서 보면 된다 Constant Buffer View.

그 다음 Meshshader 이다. Mesh 같은 경우 즉 정점의 정보들을 가지고 있어야한다. resource 를 바로 보내는게 아니라 descriptorHeap 처럼 bufferview 라는걸 보여줘야한다. 여기에서 중요한건 뭐냐, 일단 vertex 의 정보를 buffer 에 넣어서 CPU 에서 만들어준 다음, 그 buffer 를 GPU 메모리에 할당을 해준다. 그렇다면 Mesh 가 Render 되는 부분은 CommmandQueue 안에 RenderBegin 과 RenderEnd 에 나머지부분을 그려주는 형태이다. 그리고 그리는 방식 또는 Topology 형태를 Parameter 로 지정이 가능하고, vertex buffer view 를 slot 에 끼워넣어서 vertex 를 그릴수 있다.

Shader 는 무엇이냐? 라고 묻는다면, 바로 외주인력들이 무엇을 해야하는지 말해주는거다. 뭔가 거대한 거물들이 계약서도 받고 도장도 찍었으면, 이제 그 거물급들이 각각의 인력들에게 오케이 이제 오늘부터 이거 해야되라고 말하는것이다. 즉 일감 기술서 이다. shader 프로그램을 읽고 로딩해서 실행한다. shader 에는 pixel shader (part of fragment shader) 와 vertex shader 가 존재한다. 잠깐 shader program? shader program 이 다행히 c++ 와 비슷한 syntax 를 가지고 있어서, 짜기도 쉬운데 문제는 Main 에서 Parameter 를 한정적으로 받을수 있다는게 문제이다. 아래의 Graphic Pipeline 을 한번 봐보자. 아래의 그림을 보면 Input Assember Stages 에서는 멋대로 data 값을 넘겨줄수 없다.(다른 stage 와 달리)

Resource

Memoir

2022 Memoir

항상 연말에는 페이스북에 많은 개발자 회고록이 올라온다. 계속 보고만 있었는데, 많은 사람들이 여러 책을 읽고, 자기 개발을 하는거 보고 있다. 나도 나름대로 노력을 한것 같다. 그리고 2022 년에는 되게 많은 일들이 있었던것 같다. 그리고 한국 1년 개발자가 되어있는 나를 보며 회고록을 쓰려고 한다.

Adjustment & Changes

일단 저번해가 지나고 나서 많은 공부와 노력이 따로 필요했었다. 새로운 팀으로 넘어가 지속적인 개발이 필요했었고, 그때만큼은 내 몸을 아끼지 않았다. 그래서 운동은 따로 했었지만 허약해 있었고, 과호흡이 왔어서, 응급실을 갔었고 또 코로나에 걸려, 정해진 날짜에 훈련소를 가지 못했어서, 되게 당황했었지만, 나름 그 주 뒤에 바로가서 훈련받고 오고 무사히 돌아왔다. 한국에 적응을 할 시간 조차 없이 너무 이번 한해에 많은 일들이 있었다. 미국에서 바로 한국으로 넘어올시기에 몸이 않좋아 입원, 다음에 바로 취직, 훈련소.. 너무 많은일들이 짧고 굵게 휘몰아 쳤지만, 그래도 나름 생활을 잘했던 2022 년 이였다.

Development Perspective

개발은 정말 많은걸 목표로 두고 있었다. 일단 파이썬으로 개발해서, OpenGL Python, gRPC Interface 에 맞게 request & receive 를 하는 interface 를 만들었었고, 자율주행에서 중요한 맵과 맵을 합치는 것과, Command Pattern 을 이용해서 Undo / Redo Interface 만드는거 까지 했었다. 이걸로 더 나아가 최근에 특허까지 승인이된 우리 회사 Scenario Runner Software는 수많은 Refactoring 과 PyQt UI 구현 및 데이터 전달 부터 OpenScenario에서 정의된 시나리오를 실현시키기 까지 엄청난 노력을 썻다고 해도 무관하다. 나는 그 과정속에서 깔끔한 코드, 그리고 가독성이 높은 코드를 짜려고 많이 고민을 했었다. 사실 협업이라는게 쉽지 않은것도 맞다. 난 코드를 짤때 최대한 남들에게 피해가 안가게끔 짜는 편이였지만 그 방법이 틀리다는걸 잘알고 있다. 조금씩 조금씩 바뀌는것 보다는 조금 일이 많더라도, 방향을 잘잡아야한다 라는 말이 끝까지 와닿었던 Sid meyer 의 말도 참 내 경험상 많은걸 build up 해주었다.

2023 New Goal

2023 년은 되게 많은 일들이 있을것 같으면서도 어느정도의 변화의 짐작이 간다. 일단 일적으로는 MORAI SIM V2 작업을 새로운 언어로 포팅하는 작업을 해야한다. 정말 큰 일이기도 하면서도 어느정도의 시간이 흐를지 반복 테스틑 얼마나 해야될지 가늠이 안가지만, 팀의 성향이 다른 team 들의 영향을 많이 받는 Working Group 이라 어쩔수 없다. 하지만 정말 필요한 스킬은 Communication Skill 이다. 이제는 한국에 적응할 시기가 충분히 지났다.

일단 아래와 같이 나의 Goal 을 맞춰나갈꺼다. 그리고 잘지켜졌으면 좋겠다라는 바램이 있다.

Career 측면에서 일단 세갈래로 크게 나가고 싶다.

  1. Computer Vision
  2. Computer Graphics
  3. Game Dev in C++

일단 Vision 쪽에서는, 카메라에 대한 기초와 3D Geometry 에 대한 공부를 시작 해야한다. 또 이거 말고도 2D 에서 사용되는 feature matching.. 등 pyramid 를 다시 복습하고 싶다. 그리고 graphics 는 DirectX11 이나 Vulkan 으로 rendering pipeline 을 구현 및 설명할수 있는 정도가 되는게 목표이다. 그 이후에 딥러닝을 병합한 dlss 도 한번 살펴봐야한다.

3번쨰는 굉장히 많은 노력이 필요한것같다. 대체적으로 게임 서버 운영 및 다중 플레이어 호환 등 시작해봐야 되며, 게임에 필요한 수학 (especiall on geometry), path planning algorithm, 등 algorithm 공부도 필요하다. 그리고 마지막으로 나만의 게임을 만들어보는게 이번년의 목표이다.

물론 계획이 rough 하지만 필요한부분을 간추렸고, direction 을 못찾았을때, 항상 지표가 되는게 되었으면 좋겠다. 이직도 4월이나 5월에 생각은 하고 있지만 경제상황을 봐서 지혜롭게 움직였으면 좋겠다는 생각이든다.

그리고 책을 이번엔 20권을 목표로 읽을 예정이다. 가끔씩은 이해를 잘못해서 그냥 넘어가는 경향이 있는데, 책을 읽는데만 그치는게 아니라, 생각하는 힘도 키워나아가야한다.

그리고 건강 및 가족 측면에서는 일단 운동은 계속 꾸준히 해야한다. 그리고 단커피와 술을 줄여서 간수치를 내리는것도 목표이다. 가족같은 경우는 더더욱 지금 상태를 유지하고 싶고, 매순간 잘하기로 마음을 먹었다. 취미는 서핑과 기타를 계속 치는게 목표이다. 하지만 많은 (검)토끼를 잡을수 있으니 잠시 내려 놔야할때는 내려놓을 필요도 있어보인다.

2023 년 어려울수도 있고, 그냥 문안하게 잘 지나갈수도 있지만, 조금 더 열심히, 부족한게 있으면 조금더 채워나가는 식으로 갔으면 좋겠다.

Pagination


© 2021. All rights reserved.