HLSL Introduction

Shader Programming

Programming 언어에서도 여러가지 종류가 달라지듯이, Pipeline 안에서 각각의 Stage 마다, 안보이는 Shader Programming 을 해줘야 하고, 언어도 다른 종류가 있다.

아래의 그림을 참고해서 그림을 보자면, IA Stage 에서 Memory Resource (Buffer, Texture, Constant Buffer) 에서, IA 로 들어간 이후에 아래쪽으로 쉐이더를 통과해서 진행한다. 참고로, 이때 Memory Resource 에서 IA 로 들어가는 데이터의 배치상태(layout) 이라고한다.

DirectX 에서는, HLSL(High Level Shader Language) 를 사용한다. HLSL 에 들어가보면, 이런 구문이 보인다. HLSL is the C-like high-level shader language that you use with programmable shaders in DirectX. C Language 하고 비슷하다. 그리고 HLSL 로 Compute Shader 할때는 Direct Machine Learning 을 사용하라고 한다.

즉 일단 Shader Programming 이란, 결국에 GPU 에서 작동하는 Programming 이라고 생각하면 편할것 같다.

일단 기본적으로 아래와 같이 Shader Programming 을 할수 있다. C 나 C++ 처럼, 제일 처음에 시작되는 부분이 바로 main 부분이다. Shader 에도 main 이 따로 있다. 일단 Shader 의 종류가 여러가지가 있다. 예를들어서 Vertex Shader 가 있고, Pixel Shader 등등 있는데, 서로 연관성은 없으며, 하나의 독립적인 Module 이라고 생각하면 편하다, 독립적인 Module 이기 때문에, Compile 도 따로한다. 하지만 data 는 Share 할수 있다.

예를 들어서 아래의 HLSL Programming 을 봐보자.

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 color : COLOR0;
}

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 color : COLOR;
}

PixelShaderInput main(VertexShaderInput input)
{
    PixelShaderInput output;
    float4 pos = float4(input.pos, 1.0f);
    pos = mul(pos, model);
    pos = mul(pos, view);
    pos = mul(pos, projection);

    output.pos = pos;
    output.color = input.color;

    return output;
}

// pixel shader
float4 main(PixelShaderInput input) : SV_TARGET
{
    return float4(input.color, 1.0);
}

위의 코드 같은 경우 float3 pos : POSITION 이라고 나와있는데 colon(:) 다음에 나오는건 Sematics 인데, 어떤 Parameter 종류다라는것을 명시한다. 자세한건 Shader Semantics 참고하자.

그렇다면 Vertex Shade.r 와 Pixel Shader 를 조금 더 알아보자. 위의 코드에서 __ShaderInput 이라는 구조체가 보인다. 그렇다면 Pipeline 에서 Output 도 존재할수 있는데, 여기서 Vertex Shader 의 Output 이 Interpolation 을 거쳐서 Pixel Shader 의 Input 이 되기때문에 따로 명시하진 않았다.

그리고 위의 PixelShaderInput 구조체에서 Vertex Shader 와 비슷하게 생겼지만 SV (System-value semantics)라는게 들어가 있는데, 이 이유는 Shader의 Input 으로 들어온다 라는걸 표시한다.

Pixel Shader 에서는 Graphics Pipeline 안에서 제일 마지막에 위치해있기때문에 Semantics 가 SV_TARGET 즉 Render 를 할 Target 이라는 semantics 를 넣어주어야한다.

Shader 에서 Constant Buffer 도 거쳐서 계산하게끔 도와줘야한다. 그러기 때문에 이것에 필요한 문법도 따로 명시해줘야한다. 아래와 같이 표현 할수 있는데 여기서 register 안에 b0 이라는 인자가 들어간게 보인다. 이건 Register Type 인데 b 일 경우는 Constant Buffer, t 일때는 Texture buffer, c 일 경우 Buffer offset 등 여러가지 타입이 존재한다.

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
}

Resource

COMPTR - Window Programming & DirectX11 Initialization

DirectX 의 공부에 앞서서, c++ 에서 자주 사용하는 shared_ptr 이있는데, Microsoft 에서 제공하는 Microsoft::WRL::ComPtr is a c++ template smart-pointer for COM(Component Object Model) objects that is used extensively in Winodws Runtime (WinRT) C++ Programming. 이라고 한다.

일단 사용할때 아래와 같이 정의할수 있다.

Microsoft::WRL::ComPtr<ID3D11Device> device; // COM Interface
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;

만약 이러한 ComObject 와 관련된걸, brute-force 하게, C++ 로 만든다면, 이렇게 표현을 할것이다. std::shared_ptr<ID3D11Device> device = make_shared<ID3D11Device>(...);1D3D11Device *device = nullptr;. 그리고 make_shared or new 를 하는게 아니라, 용도에 맞게, 지정되어있는 함수로 만들어야한다.

Adapter 가 만들어졌다고 하는 가정하에, DirectX11 의 Device 와 Device Context 를 만들어줘야한다. DirectX 11 의 Device 란 Object 이며, 이 device 는 Desired Adaptor 에서, DirectX11 Render 가 사용할 object 를 생성하는 역활을 하고, 대부분 initialize 를 할때 개체가 생성되고, 소멸된다. context 는 사실 device context 인데, 이 역활은 어떤 Commands 를 submit 해주고, adaptor 가 실행을 시켜준다. 예를 들어서 Render Command 나, 어떠한 Transfer 할 Data 를 update 하는데 사용된다 (주로 Rendering Process 중일때…) 즉 둘다 Interface 라고 생각하면된다. 그래서 생성될때, ID3D11DeviceID3D11DeviceContext interface 를 통해서 생성된다.

일단 Device 에 관련되서 생성을 하려면 아래와 같이 해야한다. 그리고 필요한건 context 이다.

Microsoft::WRL::ComPtr<ID3D11Device> device; // COM Interface
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;

const D3D_FEATURE_LEVEL featureLevels[2] = {
        D3D_FEATURE_LEVEL_11_0,
        D3D_FEATURE_LEVEL_9_3};

D3D_FEATURE_LEVEL featureLevel;

HRESULT hr = D3D11CreateDevice(
    nullptr,
    D3D_DRIVER_TYPE_HARDWARE,
    0,
    creationFlags,
    featureLevels,
    ARRAYSIZE(featureLevels),
    D3D11_SDK_VERSION,
    &device,
    &m_d3dFeatureLevel,
    &context);

Device 가 생성된다고 해서, Render 를 한다는 말은 아니다. DirectX11 에서는 swapchain DXGI 를 통해서 만들어줘야한다. SwapChain 이란 backbuffer 의 개수를 관리하고, 한 buffer 마다 access 를 가능하다, 즉 이때 모니터에 갈거 따로 하나 backbuffer 에 그릴거 하나 이렇게 cover 를 한다. 그래서 아래의 그림처럼 SwapChain 안에 Buffer 를 끌어다가 Texture 를 설정해주고, 실제 Render 할 TargetView 만들어준다음에 기다리면서 switching 을 할수 있게 한다.


Introduction to Rendering Pipeline in D11

일단 어떤 기하의 정보를 정의한 이후로 Vertex Buffer 를 만들어줘야된다. 여기에서 Buffer 같은 경우는 GPU Memory 를 준비한다. 그래서 Vertex 의 정보(정점별 위치 및 Textrue Data) 를 담는 VertexBuffer 가 있고, 그 Vertex 가 어떤 순서로 이루어져야 하는지는 IndexBuffer(Rendering 할 모형의 Index 를 제공, 동일한 정점을 재사용) 에다가 넣어준다. 또 Constant BufferMVP (Model, View, Projection) 으로 정의 되어있다. Constant Buffer 의 의미상으로는 어떤 static data 를 가지고 있는데, 이게 pixel shader 호출에 필요한 정적데이터를 또는 모든 버텍스 및 Pixel 를 가르친다. 즉 Buffer 들을 CPU 가 정의했다가, GPU 에 넘기는 용도로 사용된다.

일단 구현을 봐보자면,


Resources

Implement Initialization in Direct3D

const D3D_DRIVER_TYPE driverType = D3D_DRIVER_TYPE_HARDWARE;

Lighting

Lighting in Rasterization

Ray Tracing 과 마찬가지로, Rasterozation Pipeline 에서도 Lighting 의 종류는 Directional Light / Point Light / Spotlight 이 있다. 역시 이것도 Unreal Light Type 의 일부분이다. (참고: Unreal Light Type 은 SkyLight 과 Rect Light 이 존재한다.)

일단 Shading 부분에서 구현했듯이, Directional Light 은 구하긴 쉽다. 여기에서는 Point Light 과 Spotlight 을 구현해보려고 한다.

Point Light

Point Light 의 Physical 한 부분은, 전구로 묘사 하는게 제일 알맞다. 어떤 한 빛의 지점에서 구의 형태로 부터 여러 방면으로 빛이 나간다.

아래의 그림을 참고하자. 어떠한 Arbitary point P 로 부터, Point Light 의 Origin Q 로 부터 빛이 퍼져나간다. 그래서 Light 을 구하는 방법은, 우리가 보는 시점 P 로부터 Q 의 Vector 즉 Q - P 의 Vector 로 표현이 가능하다. 그래서 Direction 을 찾자면 아래의 그림 처럼 공식이 성립이된다.

그리고 Light 의 방향 벡터는 위와 같이 구했지만, 실제 Light 의 세기인 Intensity 를 Physical Level 에서 그럴싸하게 보이려면 Attenuation Function 을 사용해야한다. Light 의 세기 가 거리에 따라서 약해지기 대문에 I(intensity) = I(initial) / d^2 이렇게 정의 할수 있고, 이건 HDR(High dynamic range) 에서 사용되고 tonemapping 에 사용된다. 근데 쉽게 구현 가능하는거는 아래와 같은 함수이다. saturate 함수인데 아래와 같이 표현이 가능하다. 일단 fallofStart 까지는 1 이라는 constant value 가 적용되고, 그리고 fallOfStart 와 fallOfEnd 의 사이는 0 까지의 직선이고, falloffEnd 부터는 0 이된다.

구현하는 방법은 아래와 같다.

Spot Light

Spotlight 을 받는다라고 하는 장면을 생각해보면, 어떤 무대에서 배경은 다 까맣고, 그 Spot Light 를 받는 인물만 빛이 들여오는걸 상상할수 있다. SpotLight 은 Point Light 과 비슷하다. 하지만 square 이 붙는다. 아래의 Figure 를 보면 P 는 빛을 받는 지점이고 Q 는 spotlight source 이다. P 를 봤을때 SpotLight 의 Cone 안에 있다는걸 확인 할수 있는데 이걸 결정할수 있는게 phi max 보다 D 와 -L 의 dot product 를 했을때 각도가 적다는걸 확인 할수 있다. 그리고 이 Cone 에서의 특징점은 Cone 의 Q 지점에서 직선으로 being lit 했을때의 지점이 제일 밝고, 그 지점으로 부터 멀어질수록, 즉 Phi max 일때는 빛의 intensity 가 0 이 된다. 그래서 max(cos(phi), 0 )^5 라고 말할수 있고, 결국 이거는 max(-L dot d, 0)^5 라고 볼수 있다.

C++ 구현

struct Light {
    vec3 strength = vec3(1.0f);
    vec3 direction = vec3(0.0f, -1.0f, 0.0f);   // directional/spot light only
    vec3 position = vec3(0.0f, 1.0f, 0.5f);     // point/spot light only
    float fallOffStart = 0.0f;                  // point/spot light only
    float fallOffEnd = 1.8f;                    // point/spot light only
    float spotPower = 0.0f;                     // spot light only
};

struct VSInput {
    vec3 position;
    vec3 normal;
    vec3 color;
    vec2 uv;
};

struct VSOutput {
    vec3 position;
    vec3 normal;
    vec3 color;
    vec2 uv;
};

vec3 BlinnPhong(vec3 lightStrength, vec3 lightVec, vec3 normal, vec3 toEye,
                Material mat) {

    vec3 halfway = normalize(toEye + lightVec);
    vec3 specular =
        mat.specular * pow(glm::max(dot(halfway, normal), 0.0f), mat.shininess);

    return mat.ambient + (mat.diffuse + specular) * lightStrength;
}

vec3 ComputeDirectionalLight(Light L, Material mat, vec3 normal, vec3 toEye) {
    vec3 lightVec = -L.direction;

    float ndotl = glm::max(dot(lightVec, normal), 0.0f);
    vec3 lightStrength = L.strength * ndotl;
    return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

float Saturate(float x) { return glm::max(0.0f, glm::min(1.0f, x)); }

float CalcAttenuation(float d, float falloffStart, float falloffEnd) {
    // Linear falloff
    return Saturate((falloffEnd - d) / (falloffEnd - falloffStart));
}

vec3 ComputePointLight(Light L, Material mat, vec3 pos, vec3 normal,
                       vec3 toEye) {
    vec3 lightVec = L.position - pos;
    float d = length(lightVec);
    if (d > L.fallOffEnd)
        return vec3(0.0f);

    lightVec /= d;

    float ndotl = glm::max(dot(lightVec, normal), 0.0f);
    vec3 lightStrength = L.strength * ndotl;

    float att = CalcAttenuation(d, L.fallOffStart, L.fallOffEnd);
    lightStrength *= att;

    return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

vec3 ComputeSpotLight(Light L, Material mat, vec3 pos, vec3 normal,
                      vec3 toEye) {
    vec3 lightVec = L.position - pos;
    float d = length(lightVec);
    if (d > L.fallOffEnd)
        return vec3(0.0f);

    lightVec /= d;

    float ndotl = glm::max(dot(lightVec, normal), 0.0f);
    vec3 lightStrength = L.strength * ndotl;

    float att = CalcAttenuation(d, L.fallOffStart, L.fallOffEnd);
    lightStrength *= att;

    float spotFactor = glm::pow(glm::max(dot(-lightVec, L.direction), 0.0f), L.spotPower);
    lightStrength *= spotFactor;

    return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

// Vertex Shader
VSOutput VertexShader(const VSInput vsInput) {
    VSOutput vsOutput;

    vsOutput.position =
        RotateAboutX(
            RotateAboutY(vsInput.position * constants.transformation.scale,
                         constants.transformation.rotationY),
            constants.transformation.rotationX) +
        constants.transformation.translation;
    
    vsOutput.normal = RotateAboutX(
        RotateAboutY(vsInput.normal, constants.transformation.rotationY),
        constants.transformation.rotationX);

    return vsOutput;
}

// Pixel Shader
struct PSInput {
    vec3 position;
    vec3 normal;
    vec3 color;
    vec2 uv;
};

vec4 PixelShader(const PSInput psInput) {

    vec3 eye = vec3(0.0f, 0.0f, -1.0f); // -distEyeToScreen
    vec3 toEye = glm::normalize(eye - psInput.position);

    vec3 color;

    if (constants.lightType == 0) {
        color = ComputeDirectionalLight(constants.light, constants.material,
                                        psInput.normal, toEye);
    } else if (constants.lightType == 1) {
        color = ComputePointLight(constants.light, constants.material,
                                  psInput.position, psInput.normal, toEye);
    } else {
        color = ComputeSpotLight(constants.light, constants.material,
                                 psInput.position, psInput.normal, toEye);
    }

    return vec4(color, 1.0f);
}

Reference

DirectX12

Shading

Shading

Vertex shader 에서 주로 shading 파트를 한다, 요즘엔 그래픽 카드가 좋아서, pixel shader 에서 종종한다고 한다. 일단 shading 이란 조명과 재질을 고려해서 색을 결정하는 작업을 말한다. 주로 방법론은 두가지 방법이 있는데 Vertex shader 에서 vertex 의 색을 결정한 이후에 pixel shader 로 보내줘서 pixel shader 에서 interpolation 을 하는 방법이 있고, ㅏ나머지 방법은 shading algorithm 을 pixel shader 계산하는 방법이 있다. 두가지 방법을 구지 고려했을때 정점의 개수가 확실히 적기 때문에 shading 을 Vertex shader 에서 하는 편이다. Ray Tracing 에서 구현한 Phong shading model 의 개선된 Blinn-Phong Shading 을 알아보자.

Material

일단 Shading 을 하기 앞서, 조명에 관련된걸 잠깐 언급하려고 한다. 조명에도 2 가지가 있는데, Local Illuimination (직접 조명) 이 있고, Global Illumination (간접조명)이 있다. 이 두가지의 차이는 직접 조명 같은 경우는 직접적으로 조명을 주기때문에 빛춘곳만 shading 이 생기는거고, Global illumination 은 그 빛에만 조명효과가 일어나는게 아니라 빛이 분배되는것처럼 구현이 된다.

예를 들어서 빛이 물체를 비췄을때, 조명과 물체가 비스듬하게 비췄을때의 그 suface 를 Lambertian Surface 라고 하는데, 이것도 마찬가지로 조명과 물체가 수직일때 물체에 받는 intensity 가 강할껀데 이공식이 I(diffuse) = Kd * I(light) * cos(theta) 라 정리가 된다. Lambertian Surface 에서 정의하는게 어떤 조명이 표면이 울퉁불퉁하는 곳에 부딫쳤을때, 난반사가 일어나는데 이때 난반사의 모양이 반쪽의 구의 모형으로 균일하게 반사하는걸 표현한다. 여기에서 더 extend 를 하자면 반구의 크기가 결국에는 입사각에 따라서 달라진다 라는 말이 된다.

결국엔 물체의 색깔을 결정하기 위해선, Reflection 이 중요하고, Reflection 을 정의하려면 물체의 표면을 고려해야한다. 그래서 Specular 과 Diffuse 는 물체의 표면을 생각했을때, 떼어낼수 없는 존재이다. 그 예는 아래와 같이 Image 를 참고하자

Blinn-Phong Shading

일단 Phong 보다 개선된점은 바로 계산의 속도 문제이다. Phong 모델에서는 R dot V 를 했었다. 하지만 Blinn Phong 에서는 N dot H 를 한다는 점이 포인트이다. 이때 H 는 Halfway Vector 라고 하는데 왜 Half 냐면 N 과 L 사이의 중간지점인 Vector 이기 때문이다. H 같은 경우는 L + V / ||L + V|| 로 구할수 있다.

역시 Phong 과 Blinn-Phong 을 비교 해봐야 어떤게 좋은지 알수 있다.

OpenGL 일단 Phong Model 은 Reflection vector 와 View Vector 의 각도가 크기 때문에 제곱을 했을때 더 날카롭게 표현되기때문에 Phong model 을 봤을때 directional light 가 더 강하게 보인다. 하지만 Blinn-phong 같은 경우 halfway vector 와 Normal vector 의 각도가 작기때문에 제곱을 해도 부드럽게 나오는걸 확인할수 있다.

계산에서 중요한점은 Point 같은 경우는 이동이 가능하지만, Vector 는 이동이 불가능하다. 이 점이 물체가 이동을 할때 Point 도 옮겨줘야하지만, Normal Vector 도 같이 움직여줘야한다는 부분이다.

그렇다면 C++ 코드로 GPU 에서 해야될 shader 를 잠깐 구현한다고 하면, 아래와 같다.

#include <glm/glm.hpp>
#include <vector>

// Helper function for rotating about certain axis 
vec3 RotateAboutZ(const vec3 &v, const float &theta) {
    return vec3(v.x * cos(theta) - v.y * sin(theta),
                v.x * sin(theta) + v.y * cos(theta), v.z);
}

vec3 RotateAboutY(const vec3 &v, const float &theta) {
    return vec3(v.x * cos(theta) + v.z * sin(theta), v.y,
                -v.x * sin(theta) + v.z * cos(theta));
}

vec3 RotateAboutX(const vec3 &v, const float &theta) {
    return vec3(v.x, v.y * cos(theta) - v.z * sin(theta),
                v.y * sin(theta) + v.z * cos(theta));
}

struct Transformation {
  vec3 scale = vec3(1.0f);
  vec3 translation = vec3(0.0f);
  float rotationX = 0.0f;
  float rotationY = 0.0f;
  float rotationZ = 0.0f;
};

struct Material{
  vec3 ambient = vec3(0.1f);
  vec3 diffuse = vec3(1.0f);
  vec3 specular = vec3(1.0f);
  float shininess;
};

sturct Light{
  vec3 strength = vec3(1.0f);
  vec3 direction = vec3(0.0f, -1.0f, 0.0f);
};

struct Constants {
  Transformation transformation;
  Light light;
  Material material;
} constants;

struct VSInput{
  vec3 position;
  vec3 normal;
  vec3 color;
};

struct VSOutput{
  vec3 position;
  vec3 normal;
};

vec3 BlinnPhong(vec3 lightStrength, vec3 lightVec, vec3 normal, vec3 toEye,
              Material mat){
  vec3 halfway = normalize(toEye + lightVec);
  vec3 specular = mat.specular * pow(glm::max(dot(halfway, normal), 0.0f), mat.shininess);
  return mat.ambient + (mat.diffuse + specular) * lightStrength;
}

vec3 ComputeDirectionalLight(Light L, Material mat, vec3 normal, vec3 toEye){
  vec3 lightVec = -L.direction;
  float ndotl = glm::max(dot(lightVec, normal), 0.0f); // exception for negative value
  vec3 lightStrength = L.strength * ndotl;
  return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

// Vertex Shader
VSOutput MyVertexShader(const VSInput vsInput){
  VSOutput vsOutput;

  vsOutput.position = RotateAboutX(
    RotateAboutY(vsInput.position * constants.transformation.scale,
      constants.transformation.rotationY),
      constants.transformation.rotationX) + constants.transformation.translation;

  vsOutput.normal = RotateAboutX(
    RotateAboutY(vsInput.normal, constants.transformation.rotationY),
      constants.transformation.rotationX);

  return vsOutput;


// Pixel Shader 
struct PSInput {
  vec3 position; 
  vec3 normal;
  vec3 color;
  vec2 uv;
}

vec4 MyPixelShader(const PSInput psInput){
  vec3 eye = vec3(0.0f, 0.0f, -1.0f);
  vec3 toEye = glm::normalize(eye - psInput.position);
  vec3 color = ComputeDirectionalLight(constants.light, constants.material,
                                      psInput.normal, toEye);

  return vec4(color, 1.0f);
}
}

이런식으로 구현이 가능하다. 여기서 Minor 한점은 glm::max(…) 를 통해서 각이 90 도 이상 일때 빛의 세기가 덜받으면 덜받았지 음수가 될수 없다는 점과. Blinn-phong algorithm 을 사용해서 halfway vector 의 방향을 구한다음에 color 값을 return 해주는 함수가 있다는 점을 한번 참고하자.

Resource

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 이 적용되어있는걸 확인할수 있다.

Pagination


© 2021. All rights reserved.