이모티콘 플러스 가입자를 최대한으로 늘리면서,  이모티콘 플러스 가입자 수가 최대인 상태에서 판매액 또한 최대로 높일 수 있는 경우를 탐색하여 그 경우의 이모티콘 플러스 가입자 수와 이모티콘 판매액을 반환하는 문제이다.

 

모든 유저는 2가지의 기준을 따라 소비를 한다.

 

1.본인이 설정한 최소 할인율을 기준으로 동일하거나 더 많이 할인하는 이모티콘은 모두 구매한다.

 

A유저가 설정한 최소 할인율이 20%라고 가정해보자. 만약, 4개의 이모티콘이 각각 10%, 30%, 20%, 40%를 할인한다면 A유저는 2번,3번,4번 이모티콘을 구매할 것이다. 1번 이모티콘은 최소 할인율보다 적게 할인하기 때문에 구매하지 않는다.

 

2. 이모티콘 구매액이 일정 금액을 넘어가면, 이모티콘을 구매하지 않고 이모티콘 플러스에 가입한다.

 

A유저가 설정한 최대 구매 금액이 10000원이라고 해보자. 1번 기준에 의해 구매하기로 한 이모티콘의 가격에 할인까지 적용된 최종 가격이 10000원 이상이라면 이모티콘을 구매하지 않고 이모티콘 플러스에 가입할 것이다.

 

이 경우에 카카오톡 입장에선, 이모티콘 플러스의 가입자는 1명 증가하고 이모티콘 판매액은 0원이 증가하는 셈이 된다.

 

이모티콘의 할인율을 어떻게 세팅해야 카카오톡이 원하는 최적의 결과가 나올 수 있는지 구한 뒤, 해당 경우에 카카오톡 플러스의 가입자 수와 총 판매액을 구하면 된다.

 

문제 풀이

 

본인은 해당 문제를 DFS를 사용한 완전탐색으로 해결하였다.

 

문제의 조건 이모티콘의 최대 갯수는 7개로 주어진다.

이모티콘에 적용할 수 있는 할인율은 10%, 20%, 30%, 40%로 총 4개이므로 이모티콘에 할인을 적용하는 모든 경우의 수는 4^7 (16384)가 된다. 

 

이정도 개수면 완전탐색하기에 충분하고도 남는다.

 

먼저, DFS를 사용하여 모든 경우의 수를 탐색하였다.

그리고, DFS가 끝에 도달했을 때 선택된 할인율을 기준으로 유저들의 반응을 조사한 뒤, 나온 결과물을 pair (가입자 수, 총 판매액)에 저장한 뒤, 해당 pair를 set에 삽입하였다.

 

pair은 기본적으로 first를 기준으로 대소를 비교한 뒤, first가 같다면 second를 기준으로 대소를 비교하게 된다.

즉, 카카오톡 플러스 구독자 수를 기준으로 정렬을 한 뒤, 구독자 수가 같다면 총 매출액으로 정렬하는 것이다.

 

그러므로, 오름차순으로 정렬된 set의 가장 마지막 원소가 우리가 원하는 답이 되는 것이다.

 

풀이 코드

 

struct UserInfo
{
    int MinSale = 0;
    int MaxPrice = 0;
};

std::vector<UserInfo> Users;
std::vector<int> EmoPrices;
std::vector<std::vector<bool>> isVisit;
std::set<std::pair<int, int>> Answers;

 

먼저 1개의 구조체와 4개의 자료구조를 선언하였다.

UserInfo는 입력으로 들어온 User의 정보를 좀 더 효율적으로 사용하기 위해 만들었다.

UserInfo에 데이터를 저장한 뒤 Users에 삽입해주었다.

 

EmoPrice또한, 입력으로 들어온 이모티콘의 가격들을 그대로 복사해준 것이다.

전역변수로 선언하여 다른 함수에서 쉽게 참조할 수 있도록 하기 위함이다.

isVisit는 DFS를 사용하기 위한 방문체크 배열이다.

Answer은 모든 경우의 수를 담게 될 자료구조이다.

 

Users.resize(users.size());
EmoPrices.resize(emoticons.size());
isVisit.resize(emoticons.size(), std::vector<bool>(4, false));

for(int i = 0; i < users.size(); i++)
{
    UserInfo NewUser;
    NewUser.MinSale = users[i][0];
    NewUser.MaxPrice = users[i][1];

    Users[i] = NewUser;
}

for(int i = 0; i < emoticons.size(); i++)
{
    EmoPrices[i] = emoticons[i];
}

 

먼저 이렇게, 입력을 저장해주었다.

isVisit의 내부 배열을 보면 4로 resize해주고 있는데, 할인 경우의 수가 4가지이기 때문이다.(10%, 20%, 30%, 40%)

for(int i = 0; i < 4; i++)
{
    DFS(0, i);
}

std::set<std::pair<int, int>>::iterator Iter = Answers.end();
Iter--;

return {Iter->first, Iter->second};

 

그 다음은 위에서 말한 것과 같이 DFS를 사용하여 완전탐색을 하였다.

Answer의 마지막 원소의 값을 답으로 반환해주고 있다.

 

void DFS(int _CurEmoIndex, int _CurSaleIndex)
{
    if(_CurEmoIndex == EmoPrices.size())
    {
        int SubScriber = 0;
        int Revenue = 0;
        
        for(int i = 0; i < Users.size(); i++)
        {
            int Result = GetRevenue(i);
            
            if(Result == -1)
            {
                SubScriber++;
            }
            else
            {
                Revenue += Result;
            }
        }
        
        Answers.insert({SubScriber, Revenue});
        return;
    }
    
    isVisit[_CurEmoIndex][_CurSaleIndex] = true;
    
    for(int i = 0; i < 4; i++)
    {
        DFS(_CurEmoIndex + 1, i);
    }
    
    isVisit[_CurEmoIndex][_CurSaleIndex] = false;
}

 

이는 DFS함수 내부이다.

인자로는 현재 이모티콘의 인덱스와 할인율(0->10%, 1->20%, 2->30%, 3->40%)을 받고 있다.

 

가장 위의 조건문을 보자.

DFS가 모든 이모티콘을 탐색하였다면, 현재 선택된 이모티콘과 할인율에 대해 유저의 반응을 계산해주고 있다.

GetRevenue는 특정 유저가 이모티콘에 얼마를 지불하는가를 구하는 함수이다. 내부 코드는 잠시 뒤에 보도록 하자.

 

GetRevenue는 유저가 이모티콘을 구매하지 않고 이모티콘 플러스에 가입한다면, -1을 반환하도록 설정하였다.

이 값을 기반으로  SubScriber와 Revenue를 모든 유저에 대해 갱신해주었고, 해당 값을 Answer에 삽입해주었다.

 

탈출 분기 뒤엔 DFS코드가 작성되어 있다.

먼저, isVisit를 true로 만들어준 뒤, DFS가 끝나면 isVisit를 false로 만들어주었다.

 

isVisit를 false로 만드는 코드가 없다면 n개의 원소에 대해 DFS는 정확히 n번의 탐색을 한다.

하지만, isVisit를 false로 만드는 코드를 넣는다면, 가능한 모든 경우의 수를 탐색하게 된다.

int GetRevenue(int _UserIndex)
{
    int Cost = 0;
    
    for(int i = 0; i < isVisit.size(); i++)
    {
        for(int j = 0; j < isVisit[i].size(); j++)
        {
            if(isVisit[i][j] == false)
            {
                continue;
            }
            
            int Sale = (j + 1) * 10;

            if(Sale >= Users[_UserIndex].MinSale)
            {
                int CurEmoPrice = EmoPrices[i];
                CurEmoPrice *= (1.0f - (float)Sale / 100.0f);
                
                Cost += CurEmoPrice;
                break;
            }

        }
    }
    
    if(Cost >= Users[_UserIndex].MaxPrice)
    {
        //Cost가 -1이면, 이모티콘 플러스 가입.
        Cost = -1;
    }
    
    return Cost;
}

 

GetRevenue함수의 코드이다.

isVisit의 값을 기준으로 각 이모티콘에 적용된 할인율을 탐색하고 있다.

 

해당 할인율을 기준으로, 구매할 이모티콘의 총 금액을 계산한 뒤 유저의 기준 금액과 비교하여 구매할 지, 이모티콘 플러스에 가입할 지를 계산하였다.

 

만약, 이모티콘 플러스에 가입한다면 반환값을 -1로 바꿔서 반환해주었다.

 

이렇게 DFS를 사용하여 모든 경우의 수에 대해 유저들의 반응을 탐색한 뒤 나온 결과 중 최고의 결과를 출력해주면 되는 문제였다.

 

 

코드 전문

더보기
#include <string>
#include <vector>
#include <set>
#include <iostream>

using namespace std;

struct UserInfo
{
    int MinSale = 0;
    int MaxPrice = 0;
};

std::vector<UserInfo> Users;
std::vector<int> EmoPrices;
std::vector<std::vector<bool>> isVisit;
std::set<std::pair<int, int>> Answers;

int GetRevenue(int _UserIndex)
{
    int Cost = 0;
    
    for(int i = 0; i < isVisit.size(); i++)
    {
        for(int j = 0; j < isVisit[i].size(); j++)
        {
            if(isVisit[i][j] == false)
            {
                continue;
            }
            
            int Sale = (j + 1) * 10;

            if(Sale >= Users[_UserIndex].MinSale)
            {
                int CurEmoPrice = EmoPrices[i];
                CurEmoPrice *= (1.0f - (float)Sale / 100.0f);
                
                Cost += CurEmoPrice;
                break;
            }

        }
    }
    
    if(Cost >= Users[_UserIndex].MaxPrice)
    {
        //Cost가 -1이면, 이모티콘 플러스 가입.
        Cost = -1;
    }
    
    return Cost;
}

void DFS(int _CurEmoIndex, int _CurSaleIndex)
{
    if(_CurEmoIndex == EmoPrices.size())
    {
        int SubScriber = 0;
        int Revenue = 0;
        
        for(int i = 0; i < Users.size(); i++)
        {
            int Result = GetRevenue(i);
            
            if(Result == -1)
            {
                SubScriber++;
            }
            else
            {
                Revenue += Result;
            }
        }
        
        Answers.insert({SubScriber, Revenue});
        return;
    }
    
    isVisit[_CurEmoIndex][_CurSaleIndex] = true;
    
    for(int i = 0; i < 4; i++)
    {
        DFS(_CurEmoIndex + 1, i);
    }
    
    isVisit[_CurEmoIndex][_CurSaleIndex] = false;
}

vector<int> solution(vector<vector<int>> users, vector<int> emoticons) 
{
    Users.resize(users.size());
    EmoPrices.resize(emoticons.size());
    isVisit.resize(emoticons.size(), std::vector<bool>(4, false));
    
    for(int i = 0; i < users.size(); i++)
    {
        UserInfo NewUser;
        NewUser.MinSale = users[i][0];
        NewUser.MaxPrice = users[i][1];
        
        Users[i] = NewUser;
    }
    
    for(int i = 0; i < emoticons.size(); i++)
    {
        EmoPrices[i] = emoticons[i];
    }
    
    for(int i = 0; i < 4; i++)
    {
        DFS(0, i);
    }
    
    std::set<std::pair<int, int>>::iterator Iter = Answers.end();
    Iter--;
    
    return {Iter->first, Iter->second};
}

게임을 만들다 보면, 포물선을 구현할 일이 참 많다.

물건을 던진다거나, 포탄을 쏜다거나 등등 현실의 물리법칙에 기반한 포물선 운동을 구현해야 할 상황이 많은데 그 공식에 대해 정리해보고자 한다.

 

처음에 물체를 던지면, 어떠한 방향으로 힘을 가하게 될 것이다.

이 때, 힘이 가해지는 방향은 벡터로 표현할 수 있고, 해당 벡터의 길이로 가해진 힘을 구할 수 있을 것이다.

 

이렇게, 최초에 가해진 힘과 방향을 벡터 v로 표시할 수 있을 것이다.

이 벡터 v와 x축과의 사잇각을 theta라고 해보자.

 

그렇다면, 벡터 v를 아래와 같이 표현할 수 있다.

$$\vec{v} = \left (\left | v \right | * cos(\theta), \left | v \right | * sin(\theta)\right )$$

 

이 방향으로 계속 등속 운동을 한다고 가정한다면, t초 후의 벡터는 아래와 같을 것이다.

$$\vec{v} = \left (\left | v \right | * cos(\theta) * t, \left | v \right | * sin(\theta) * t\right )$$

 

하지만, 포물선 운동의 경우 x축에 대해선 등속운동을 진행하지만 y축의 경우엔 중력의 존재떄문에 등속운동을 진행할 수가 없다. 동일한 방향으로 직선 운동을 하되, 가속도의 영향을 계속 받기 때문에 y축의 경우엔 등가속도 직선 운동을 한다고 할 수 있다. 그러므로, 위의 수식의 x값은 맞다고 할 수 있지만, y값에 대해선 틀력다고 할 수 있다.

 

y축은 등가속도 직선 운동을 하기 때문에, 등가속도 직선 운동의 공식을 사용해야 한다.

 

등가속도 직선 운동에서 변위를 구하는 공식은 아래와 같다.

$$s =  v * t \; + \frac{1}{2} * a * t^{2}$$

 

벡터의 속력은 벡터가 최초에 가지고 있던 원소의 값이라고 할 수 있다.  (처음에 가한 힘)

가속도는 -9.8이며, 시간은 t가 될 것이다.

 

공식에 대입하면, t초 후의 변위는 아래와 같다.

$$s =   \left | v \right | * sin(\theta) * t \; - \frac{1}{2} * 9.8 * t^{2}$$

 

즉, 포물선 운동을 (0, 0)에서 시작했다고 한다면, t초 후의 물체의 좌표는 아래와 같아지는 것이다.

 

$$(x, y) =(\left | v \right | * cos(\theta) * t ,\ \left | v \right | * sin(\theta) * t \; - \frac{1}{2} * 9.8 * t^{2})$$

 

현실에선 공기저항과 같은 마찰의 존재때문에, 위의 공식이 정확히 성립하지는 않는다.

하지만, 게임에서 포물선다운 포물선을 그리기에는 충분할 것이다.

 

 

 

 

일단, 조명을 추가하기 전 RenderBase의 구성을 살짝 바꿔주었다.

 

기존에는 init()이 순수가상함수여서, 하위 클래스에서 구현해주도록 했지만 조명에 대한 상수버퍼는 모든 렌더러가 보유하고 있도록 하기 위해 부모함수의 Init()에 조명에 대한 상수버퍼를 생성하는 코드를 작성하고 하위 클래스의 Init에선 Renderbase::Init()을 호출해주도록 하였다.

 

하지만, 이렇게 수동으로 부모 클래스의 함수를 호출하는 경우, 실수로 빼먹을 수도 있다.

이를 방지하기 위해 bool값을 하나 선언하였다.

 

렌더러를 생성하는 함수에서 렌더러의 init함수를 호출한 뒤, 렌더러의 bool값이 false라면 콘솔창에 메세지를 출력해주도록 설정하여, Init을 호출하지 않은 경우 이를 바로 알아볼 수 있도록 해주었다.

 

void RenderBase::Init()
{
    isCallInit = true;
}

 

RenderBase의 init에 일단 isCallInit을 true로 만드는 코드를 추가해주었다.

template <typename T>
static std::shared_ptr<T> CreateRenderer()
{
    std::shared_ptr<class RenderBase> NewRenderer = std::make_shared<T>();
    NewRenderer->Init();

    if (NewRenderer->isCallInitFunction() == false)
    {
        std::cout << NewRenderer->Name << " : RenderBase::Init() is not called. " << std::endl;
    }

    EngineBase::GetInstance().AddRenderer(NewRenderer);

    return std::dynamic_pointer_cast<T>(NewRenderer);
}

 

렌더러를 생성하는 함수에서, 해당 렌더러의 isCallInit이 false라면, 콘솔창에 메세지를 출력해주도록 하였다.

bool isCallInitFunction()
{
    return isCallInit;
}

isCallInitFunction은 그냥 bool값을 반환하는 함수이다. 아무래도, 해당 bool값은 init함수 이외의 다른 곳에서 변경되면 안되기 때문에 private로 안전하게 숨겨두고 함수를 통해 값을 확인할 수 있도록 하였다.

 

이제 조명을 구성해보자.

먼저 조명 정보를 담을 구조체를 하나 선언하였다.

struct LightData
{
	DirectX::SimpleMath::Vector3 Strength = { 1.0f, 1.0f, 1.0f };
	float FallOffStart = 0.0f;

	DirectX::SimpleMath::Vector3 Direction = { 0.0f, 0.0f, 1.0f };
	float FallOffEnd = 10.0f;

	DirectX::SimpleMath::Vector3 Position = { 0.0f, 0.0f, -2.0f };
	float SpotPower = 1.0f;
};

Strength는 빛의 강도이다.

 

FallOffStart와 FallOffEnd는 빛의 감쇠 거리이다.

빛과의 거리가 FallOfStart 이하 일 때 빛은 최대 강도이고, 거리가 멀어질수록 감쇠하다 FallOfEnd에 도달하면 빛은 사라지게 된다.

Direction은 빛의 방향이며, Positon은 광원의 위치이다.

 

#define LIGHT_NUM 3

struct LightCBuffer
{
	DirectX::SimpleMath::Vector3 EyeWorld;
	float Ambient = 0.1f;

	Light Lights[LIGHT_NUM];
};

 

빛의 개수를 중간에 계속 바꿔가며 상수버퍼를 업데이트 하는 것은 매우 비효율적이기 때문에 컴파일 타임에 전역 조명의 개수를 미리 정해놓고, 고정된 개수의 조명을 사용하도록 하기 위해 위와 같이 LIGHT_NUM을 define한 뒤, 해당 개수에 맞는 조명을 가지고 있는 구조체를 만들어주었다.

 

Specular 를 계산하기 위해선 눈의 위치 정보가 필요하기 때문에 Eye의 WorldPositon도 함께 보내주었고, 저수준의 조명모델에선 ambient를 상수로 계산하는 것이 일반적이기 때문에 Ambient는 0.1f의 상수로 설정하였다.

 

LightCBuffer& GetWorldLight()
{
    return WorldLight;
}
    
LightCBuffer WorldLight;

 

EnigineBase엔 위처럼 WorldLight변수를 선언하여 주었고, 이를 반환해주는 함수도 만들어주었다.

 

void RenderBase::Init()
{
    isCallInit = true;

    CreateConstantBuffer(EngineBase::GetInstance().GetWorldLight());
}

 

RenderBase의 init에서 조명에 대한 상수버퍼를 만들어주었다.

#define LIGHT_NUM 3

struct Light
{
    float3 Strength;
    float FallOffStart;

    float3 Direction;
    float FallOffEnd;

    float3 Position;
    float SpotPower;
};

cbuffer WorldLights : register(b0)
{
    Light Lights[LIGHT_NUM];
};

 

쉐이더에서도 조명에 대한 정보를 추가해주었다.

 

이제, 쉐이더 코드를 작성할 차례이다.

버텍스 쉐이더에선 조명 계산을 하지 않고, 픽셀 쉐이더에서 할 것이다.

그렇기 때문에, 일부 트랜스폼 정보만 버텍스 쉐이더에서 계산한 뒤 픽셀쉐이더로 보내줄 것이다.

 

#include "LightHeader.hlsli"

cbuffer WorldLights : register(b0)
{
    float3 EyeWorld;
    float Ambient;
    
    Light Lights[LIGHT_NUM];
};

cbuffer WorldViewProjection : register(b1)
{
    matrix World;
    matrix View;
    matrix Projection;
};

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 Normal : NORMAL;
    float2 TexCoord : TEXCOORD;
};

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float2 TexCoord : TEXCOORD0;
    
    float3 WorldPos : POSITION1;
    float3 WorldNormal : NORMAL;
};

PixelShaderInput main(VertexShaderInput _Input)
{
    PixelShaderInput Output;
    float4 Pos = float4(_Input.pos, 1.0f);
    
    Pos = mul(Pos, World);
    Pos = mul(Pos, View);
    Pos = mul(Pos, Projection);

    Output.pos = Pos;
    Output.TexCoord = _Input.TexCoord;
    
    Output.WorldPos = mul(float4(_Input.pos, 1.0f), World).rgb;
    Output.WorldNormal = mul(float4(_Input.Normal, 0.0f), World).rgb;
    
    return Output;
}

 

보면, WorldPos와 WorldNormal을 계산한 뒤, 픽셀 쉐이더로 전달해주고 있다.

조명 계산을 world공간에서 진행할 것이기 때문이다.

 

#include "LightHeader.hlsli"

cbuffer WorldLights : register(b0)
{
    float3 EyeWorld;
    float Ambient;
    
    Light Lights[LIGHT_NUM];
};

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float2 TexCoord : TEXCOORD;
    
    float3 WorldPos : POSITION1;
    float3 WorldNormal : NORMAL;
};

Texture2D DiffuseTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = DiffuseTexture.Sample(Sampler, _Input.TexCoord);
    
    float LightSum = 0.0f;
    
    float DiffuseLight = CalDiffuseLight(Lights[0], _Input.WorldNormal);
    float SpecularLight = CalSpecular_Phong(Lights[0], _Input.WorldNormal, EyeWorld - _Input.WorldPos);
    float AmbientLight = Ambient;
    
    LightSum = DiffuseLight + SpecularLight + AmbientLight;
     
    Color.rgb *= LightSum;
    
    return Color;
}

 

 

픽셀 쉐이더에선 이렇게 WorldPos와 worldNormal을 받아서 DiffuseLight와 SpecularLight를 계산하는 함수로 보내주었다.

계산된 총 합을 현재 색상에 곱함으로써 결과 색상을 도출하였다.

 

조명 계산 함수는 아래와 같다.

float CalDiffuseLight(Light _Light, float3 _Normal)
{
    float3 LightDir = -_Light.Direction;
    LightDir = normalize(LightDir);
    
    float3 Normal = normalize(_Normal);
    
    float CosTheta = dot(LightDir, Normal);
    
    return CosTheta;
}

 

디퓨즈 라이트 계산이다. 간단하게 빛의 방향과 노멀벡터를 내적하여 값을 구해주었다.

 

float CalSpecular_Phong(Light _Light, float3 _Normal, float3 _EyeDir)
{
    float3 LightDir = _Light.Direction;
    LightDir = normalize(LightDir);
    
    float3 Normal = _Normal;
    Normal = normalize(Normal);
    
    float3 ReflectedLightDir = reflect(LightDir, Normal);
    
    float3 EyeDir = _EyeDir;
    EyeDir = normalize(EyeDir);
    
    float Specular = max(0.0f, dot(ReflectedLightDir, EyeDir));
    Specular = pow(Specular, 32.0f);
    
    return max(Specular, 0.0f);
}

 

위 코드는 스페큘러 라이트 계산식이다.

 

원래는 FallOffStart와 FallOffEnd도 적용해야 하고 이것저것 추가해야할게 많지만, 테스트를 하면서 천천히 추가하기 위해 기초적인 계산식만 작성하였다.

 

조명을 계산하기 위해, 렌더러의 트랜스폼 구조도 살짝 바꿔주었다.

시점과 관련된 뷰행렬은 총 1개만 존재한다고 가정하여, EngineBase에 선언해주었다.

그리고, 모든 렌더러는 EngineBase의 뷰행렬을 사용하여 트랜스폼을 계산하도록 하였다.

 

WorldLight.EyeWorld = DirectX::SimpleMath::Vector3::Transform(DirectX::SimpleMath::Vector3(0.0f), ViewMat.Invert());

EyeWorld는 (0.0f, 0.0f, 0.0f)에 뷰행렬의 역행렬을 곱해서 계산하였다.

 

ViewMat = DirectX::XMMatrixLookAtLH(EyePos, FocusPos, UpDir);
ViewMat *= DirectX::SimpleMath::Matrix::CreateRotationX(ViewRot.x) *
           DirectX::SimpleMath::Matrix::CreateRotationY(ViewRot.y) *
           DirectX::SimpleMath::Matrix::CreateRotationZ(ViewRot.z);

 

IMGUI를 이용해 뷰행렬의 회전을 조작하기 위해 ViewRot이라는 vector3를 선언하였고, 이를 이용해 뷰행렬을 프레임마다 갱신해줄 것이다.

 

일단 여기까지 적용한 뒤 테스트를 해보자.

 

 

얼추 잘 실행되는 듯 하다.

스페큘러도 잘 적용되는 듯 하고, 뷰행렬의 회전도 잘 적용된다.

 

다음엔 Specular의 강도를 조절하기 위한 shiness와 전체 빛의 강도를 조절할 Strengh, 그리고 빛의 감쇠까지 적용해 볼 예정이다.

CPU에서 GPU로 데이터를 보낼 땐, 그냥 데이터 덩어리로 데이터를 보낸다. C++ 코드에서 자료형이 어떻고, 변수 이름이 어떻고 이런건 아무 신경도 안쓰고, 그 안에 들어있는 데이터만 보내버리는 것이다.

 

쉐이더에서 상수버퍼의 자료형을 새로 선언해주는 이유도 이 때문이다. 들어온 데이터 중 몇 바이트를 어떤 자료형으로 사용할 것이고, 어떤 변수명을 사용할 것인지 등에 대한 정보는 CPU에서 보내주지 않기 때문에 쉐이더에서 직접 명시해야 하는 것이다.

 

이 과정에서 문제가 한 가지 생긴다. 상수버퍼는 반드시 데이터를 16바이트 단위로 정렬한다고 한다. 16바이트 경계를 넘어서면, 서로 침범할 수 없다고 한다. 데이터 덩어리가 들어오면, 우리가 C++ 코드에서 몇 바이트 단위로 데이터를 사용을 했건간에 무조건 16바이트 단위로 잘라버린다.

 

예를 들어, float3타입의 데이터 2개를 상수버퍼로 GPU에 보냈다고 해보자.

데이터를 보낼 땐 아래와 같이 보낼 것이다.

 

이렇게 데이터를 묶어서, 덩어리로 GPU에 보낼 것이다.

 

그리고 우리는 float3 자료형 2개를 보냈기 때문에, 쉐이더 코드에서도 float3 자료형으로 두 데이터를 받을 것이다.

A = {1.0f, 0.0f, 1.0f}, B = {0.7f, 0.4, 0.5f} 라고 한다면, 쉐이더에서도  A = {1.0f, 0.0f, 1.0f}, B = {0.7f, 0.4, 0.5f} 를 사용하길 원할 것이다.

 

하지만 실제로는 A = {1.0f, 0.0f, 1.0f}, B = {0.4f, 0.5f, 0.0f}가 되어버린다.

왜일까?

 

두 데이터를 보낼 때, 우리는 데이터가 연결된 상태의 24바이트로 데이터를 보냈다.

이를 GPU에서 저장할 땐, 16바이트 단위로 저장한다고 하였다. 이 때문에 데이터는 아래와 같이 저장된다.

B가 잘려서, 일부분이 A와 같은 영역에 저장되는 것이다.

이로 인해, B는 앞의 4바이트에 해당되는 0.7f데이터를 잃게되고 y와 z값이 x와 y값으로 변하게 된다.

 

A를 float3형으로 사용하게 되면 {1.0f, 0.0f, 1.0f}가 되지만, float4형으로 A를 받아보면 A는 {1.0f, 0.0f, 1.0f, 0.7f}로 B의 x값이 A의 z값에 저장되어 있음을 알 수 있다.

 

그렇기 때문에, 우리는 데이터를 의도한 대로 온전히 보내기 위해선 반드시 16바이트로 맞춰서 보내야 한다.

 

만약, float3 float3 float float 이렇게 4개의 데이터를 보낸다면, (float3 float) (float3 float)처럼 순서를 바꿔서 16바이트에 데이터가 포함되는 형태로 바꿔서 데이터를 보내야 한다는 것이다.

 

만약 따로 보낼 float형의 변수가 없다면, float4형의 변수를 선언한 뒤, xyz에 값을 저장하고 w를 비워서 데이터를 보내거나

의미없는 데이터를 float형으로 추가해서 함께 보낸다음 그냥 안쓰면 된다.

 

보통 16바이트를 채우기 위해 사용한 의미없는 데이터를 패딩이라고 한다.

 

(다른 사람들 쉐이더 코드 보면 float3 Position, float Padding 이런 코드가 자주 보인다. 이 때 padding은 16바이트를 맞추기 위해 사용한 의미없는 데이터라고 보면 된다.)

 

요약 : 상수버퍼에 데이터를 담아 GPU로 보낼 땐 반드시 16바이트 단위로 보내자.

 

광원을 추가하려고 하는데, 하기 전에 IMGUI를 먼저 프로젝트에 추가하고 나서 하는게 좋을듯 해서 IMGUI를 추가하기로 하였다.

 

if (!ImguiInit())
{
    return FALSE;
}

 

EngineBase의 Init에 IMGUI를 초기화하는 함수도 추가해주었다.

 

BOOL EngineBase::ImguiInit()
{
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGui::StyleColorsDark();

    if(!ImGui_ImplWin32_Init(hWnd))
    {
        std::cout << "IMGUI Win32 Init Failed" << std::endl;
        return FALSE;
    }

    if (!ImGui_ImplDX11_Init(Device.Get(), Context.Get()))
    {
        std::cout << "IMGUI Dx11 Init Failed" << std::endl;
        return FALSE;
    }

    return TRUE;
}

 

코드는 위와 같다. IMGUI에서 제공해주는 예제 코드에서 필요해보이는 함수만 가져와서 작성하였다.

 

 ImguiUpdate();
 ImGui::Render();

 Update();
 Render();

 ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());

 SwapChain->Present(1, 0);

 

위 코드는 EngineLoop의 코드이다. 원래는 update,Render, Present만 있었지만, Imgui에 대한 코드도 추가해주었다.

void EngineBase::ImguiUpdate()
{
    ImGui_ImplDX11_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();

    ImGui::Begin("Hello, world!");                         
    ImGui::Text("This is some useful text.");            
    ImGui::End();
}

 

일단 테스트를 위해, GUI엔 간단한 텍스트만 출력하도록 해주었다.

 

 

상단에 GUI가 잘 만들어진 것을 볼 수 있다.

 

근데 뭔가 메모리 해제가 안됐나보다.

WPARAM EngineBase::End()
{
    ImGui_ImplDX11_Shutdown();
    ImGui_ImplWin32_Shutdown();
    ImGui::DestroyContext();

    return msg.wParam;
}

 

EngineBase의 End에 Imgui의 메모리를 해제하는 함수들도 추가해주니 메모리 누수는 더이상 발견되지 않았다.

 

이제 GUI업데이트 부분을 다시 수정하고자 한다.

아무래도, 렌더러에서 GUI에 변수를 연결하는 경우가 많을 것 같아서 렌더러쪽에서 GUI 업데이트 함수를 직접 작성할 수 있도록 callback방식으로 구성하고자 한다.

 

std::list<std::function<void()>> GUIFunctions;

 

Enginebase에는 위와 같이 함수포인터를 원소로 하는 list를 선언해주었다.

 

void AddGUIFunction(const std::function<void()> _Func)
{
    GUIFunctions.push_back(_Func);
}

 

외부에선 위 함수를 이용해 함수포인터를 list에 추가해줄 수 있다.

 

void EngineBase::ImguiUpdate()
{
    ImGui_ImplDX11_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();

    ImGui::Begin("Hello, world!"); 
    
    for (const std::function<void()> _Func : GUIFunctions)
    {
        _Func();
    }

    ImGui::End();
}

 

GUI 업데이트 코드는 위와 같이 수정해주었다.

 

테스트를 해보자.

void BoxRenderer::Init()
{
    CreateVertexAndIndex();
    
    RenderBase::CreateVertexBuffer();
    RenderBase::CreateIndexBuffer();
    RenderBase::CreateConstantBuffer<Transform>(TransFormData);
    RenderBase::CreateConstantBuffer(UV);

    SetTexture("BoxTexture.png");
    SetSampler("LINEARWRAP");

    EngineBase::GetInstance().AddGUIFunction([] {ImGui::Text("AAA"); });
    EngineBase::GetInstance().AddGUIFunction([] {ImGui::Text("BBB"); });
    EngineBase::GetInstance().AddGUIFunction([] {ImGui::Text("CCC"); });
}

 

위와 같이 박스 렌더러의 init에서 AAA,BBB,CCC 텍스트를 출력하는 함수를 EngineBase에 추가해주었다.

 

결과는.. 잘 나오는 것 같긴 한데 왜 Imgui 윈도우 창 크기 조절이 안되는거지?

 

extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

LRESULT CALLBACK EngineBase::WndProc(HWND _hWnd, UINT _message, WPARAM _wParam, LPARAM _lParam)
{
    if (ImGui_ImplWin32_WndProcHandler(_hWnd, _message, _wParam, _lParam))
        return true;

    switch (_message) {
    case WM_SIZE:
        break;
    case WM_SYSCOMMAND:
        if ((_wParam & 0xfff0) == SC_KEYMENU)
            return 0;
        break;
    case WM_MOUSEMOVE:
        break;
    case WM_LBUTTONUP:
        break;
    case WM_RBUTTONUP:
        break;
    case WM_KEYDOWN:
        break;
    case WM_DESTROY:
        ::PostQuitMessage(0);
        return 0;
    }

    return ::DefWindowProc(_hWnd, _message, _wParam, _lParam);
}

 

찾아보니, 윈도우 메세지 처리 함수의 상단에 위와 같이 Imgui함수 코드를 추가해줘야 한다고 한다.

이 함수를 정삭적으로 실행하려면 코드 최상단에 있는 것처럼 전방선언을 해주어야 한다.

 

다시 실행해보면?

 

윈도우 크기 조절도 잘 되고, 텍스트도 잘 추가된 것을 볼 수 있다!

 

이번엔 프로젝트에 델타타임도 추가해보자.

IMGUI엔 계산된 Deltatime을 매 프레임마다 제공해주는 기능이 있다.

 

float CurDelta = ImGui::GetIO().DeltaTime;

위와 같은 함수를 사용하면, 현재 프레임의 델타타임을 얻을 수 있다.

 

Update(CurDelta);
Render(CurDelta);

 

이후, Update와 Render함수에 float형 인자를 추가하여, 델타타임을 인수로 전달해주었다.

void EngineBase::Update(float _DeltaTime)
{
    for (std::shared_ptr<RenderBase> Renderer : Renderers)
    {
        Renderer->Update(_DeltaTime);
    }
}

 

Renderer의 Update와 Render에도 델타타임을 추가해주었다.

 

잘 되는지, 테스트해보자.

Rotation += 18.0f * _DeltaTime;
float RotRad = Rotation / 180.0f * DirectX::XM_PI;

 

박스 렌더러의 Update에서 위의 코드처럼 1초에 18도씩 회전하도록 Rotation을 업데이트해주었다.

 

 

잘 작동하는 것을 볼 수 있다.

이제, 조명을 추가할 준비가 얼추 된 것 같다.

 

 

소들은 N개의 체크포인트를 거쳐 목적지까지 도달한다고 한다. 그 과정에서 딱 1번 체크포인트를 스킵할 수 있는데, 체크포인트를 스킵하고 이동했을 때 나올 수 있는 최소 이동거리를 구하면 된다. 

 

문제 풀이

풀이는 간단하다.

 

하나도 스킵하지 않고, 출발지부터 목적지까지 도달하기 위한 거리를 먼저 구해준다.

이후, 각 체크포인트를 3개씩 검사하며, 특정 체크포인트를 스킵했을 때 얼만큼 거리가 절약되는지의 최대값을 구한다.

이후, 전체 거리에서 절약되는 거리의 최대값을 빼주면 된다.

 

예를 들어, 1~5번까지 스킵하지 않고 가는 거리가 20이라고 해보자.

 

2번 체크포인트를 스킵하게 되면, 절약되는 거리는 [(1->2->3)의 거리 - (1->3)의 거리] 가 될 것이다.

3번 체크포인트를 스킵하게 되면, 절약되는 거리는 [(2->3->4)의 거리 - (2->4)의 거리] 가 될 것이다.

 

이 과정을 게속 거쳐서, 절약되는 거리의 최대값을 구해준다.

 

하나의 체크포인트만 스킵할 수 있다면, 당연히 가장 거리가 절약되는 체크포인트를 스킵하는 것이 최단거리가 될 것이기 때문에 구해준 최대값을 전체 거리에서 빼주면 된다.

 

풀이 코드

 

int NumOfCP = 0;
std::cin >> NumOfCP;

std::vector<std::pair<int, int>> CPs(NumOfCP);
for (int i = 0; i < NumOfCP; i++)
{
    std::cin >> CPs[i].first;
    std::cin >> CPs[i].second;
}

 

먼저, 체크포인트에 대한 정보를 입력받아주었다.

int GetDist(const std::pair<int, int>& _Start, const std::pair<int, int>& _End)
{
    return std::abs(_Start.first - _End.first) + std::abs(_Start.second - _End.second);
}

 

두 체크포인트 사이의 거리를 구하는 함수도 정의해주었다.

 

int MaxGap = 0;

for (int i = 0; i < NumOfCP - 2; i++)
{
    int NormalDistance = GetDist(CPs[i], CPs[i + 1]) + GetDist(CPs[i + 1], CPs[i + 2]);
    int SkipDistance = GetDist(CPs[i], CPs[i + 2]);

    int Gap = NormalDistance - SkipDistance;

    if (Gap > MaxGap)
    {
        MaxGap = Gap;
    }
}

 

이후, 반복문을 돌며, 각 체크포인트를 스킵하였을 때 절약할 수 있는 거리의 최대값을 갱신해주었다.

 

int SumDist = 0;

for (int i = 0; i < NumOfCP - 1; i++)
{
    SumDist += GetDist(CPs[i], CPs[i + 1]);
}

 

다음은, 하나도 스킵하지 않았을 때의 전체 거리를 구해주었다.

 

SumDist -= MaxGap;

std::cout << SumDist;

return 0;

 

전체 거리에서 절약되는 거리를 빼준 뒤, 해당 값을 출력해주면 문제 해결이다.

 

코드 전문

더보기
#include <iostream>
#include <vector>
#include <cmath>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}

int GetDist(const std::pair<int, int>& _Start, const std::pair<int, int>& _End)
{
    return std::abs(_Start.first - _End.first) + std::abs(_Start.second - _End.second);
}

int main()
{
    Init();

    int NumOfCP = 0;
    std::cin >> NumOfCP;

    std::vector<std::pair<int, int>> CPs(NumOfCP);
    for (int i = 0; i < NumOfCP; i++)
    {
        std::cin >> CPs[i].first;
        std::cin >> CPs[i].second;
    }

    int MaxGap = 0;

    for (int i = 0; i < NumOfCP - 2; i++)
    {
        int NormalDistance = GetDist(CPs[i], CPs[i + 1]) + GetDist(CPs[i + 1], CPs[i + 2]);
        int SkipDistance = GetDist(CPs[i], CPs[i + 2]);

        int Gap = NormalDistance - SkipDistance;
        
        if (Gap > MaxGap)
        {
            MaxGap = Gap;
        }
    }

    int SumDist = 0;

    for (int i = 0; i < NumOfCP - 1; i++)
    {
        SumDist += GetDist(CPs[i], CPs[i + 1]);
    }

    SumDist -= MaxGap;

    std::cout << SumDist;

    return 0;
}

이번엔, 물체 표면의 색상정보를 이미지 파일에서 읽어와서 입히는 텍스쳐링을 추가하였다.

 

먼저, 이미지를 로드하는 기능이 필요한데 이 기능은 오픈소스로 제공되고 있는 stb 라이브러리를 활용하였다.

std::string Path = "../Texture/";
Path += _TextureName;

int Width = 0;
int Height = 0;
int Channels = 0;
    
unsigned char* LoadedImage = stbi_load(Path.c_str(), &Width, &Height, &Channels, 0);
if (LoadedImage == nullptr)
{
    std::cout << "Image Load Failed" << std::endl;
    return FALSE;
}

 

stbi_load는 이미지의 정보를 읽어오는 함수이다.

해당 함수의 첫번째 인자는 파일의 이름인데, 상대경로를 통해 파일을 인식하기 때문에 경로 설정을 잘 해야한다.

본인은 프로젝트 폴더에 텍스쳐 파일을 따로 모아둘 Texture 폴더를 생성하였고, 해당 폴더에서 텍스쳐를 읽어오도록 경로를 ../Texture/(파일이름.확장자) 의 형식으로 설정하였다.

std::vector<uint8_t> Image;

Image.resize(Width * Height * Channels);
memcpy(Image.data(), LoadedImage, Image.size() * sizeof(uint8_t));

 

이후, 로드한 이미지의 색상정보를 벡터에 복사해주었다.

(굳이 복사하지 않아도 기능작동은 잘 된다. 다만 본인은 벡터가 편해서 옮겨주었다. 최적화적인 측면에선 안하는게 좋을 듯 하다.)

 Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
 Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV;

 D3D11_TEXTURE2D_DESC TexDesc = {};
 TexDesc.Width = Width;
 TexDesc.Height = Height;
 TexDesc.MipLevels = TexDesc.ArraySize = 1;
 TexDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
 TexDesc.SampleDesc.Count = 1;
 TexDesc.Usage = D3D11_USAGE_IMMUTABLE;
 TexDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;

 D3D11_SUBRESOURCE_DATA InitData;
 InitData.pSysMem = Image.data();
 InitData.SysMemPitch = TexDesc.Width * sizeof(uint8_t) * Channels;
 
 HRESULT Result = EngineBase::GetInstance().GetDevice()->CreateTexture2D(&TexDesc, &InitData, Texture.GetAddressOf());
if (Result != S_OK)
{
    std::cout << "CreateTexture2D failed " << std::endl;
    return FALSE;
}

Result = EngineBase::GetInstance().GetDevice()->CreateShaderResourceView(Texture.Get(), nullptr, SRV.GetAddressOf());
if (Result != S_OK)
{
    std::cout << "CreateTexture2D failed " << std::endl;
    return FALSE;
}

 

이후, Texture2D와 ShaderResourcesView를 만들어주었다.

TextureData NewTextureData;
NewTextureData.Texture = Texture;
NewTextureData.ShaderResourceView = SRV;

Textures.insert({ _TextureName, NewTextureData });

stbi_image_free(LoadedImage);

return TRUE;

 

이후, EngineBase에 텍스쳐 정보를 저장하기 위해 선언해둔 자료구조에 데이터를 저장한 뒤, 로드한 이미지의 메모리를 해제해주었다.

 

자료구조는 아래와 같다.

struct TextureData
{
	Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
	Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> ShaderResourceView;
};

std::unordered_map<std::string, TextureData> Textures;

 

텍스쳐를 만들었으니, 샘플러도 만들어야 한다.

 

BOOL EngineBase::CreateSampler()
{
    {
        Microsoft::WRL::ComPtr<ID3D11SamplerState> NewSampler;

        // Texture sampler 만들기
        D3D11_SAMPLER_DESC SamplerDesc;
        ZeroMemory(&SamplerDesc, sizeof(SamplerDesc));

        SamplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
        SamplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
        SamplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
        SamplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
        SamplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
        SamplerDesc.MinLOD = 0;
        SamplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

        // Create the Sample State
        HRESULT Result = EngineBase::GetInstance().GetDevice()->CreateSamplerState(&SamplerDesc, NewSampler.GetAddressOf());
        if (Result != S_OK)
        {
            std::cout << "CreateSamplerState failed : LINEARWRAP" << std::endl;
            return FALSE;
        }

        Samplers.insert({ "LINEARWRAP", NewSampler });
    }

    {
        Microsoft::WRL::ComPtr<ID3D11SamplerState> NewSampler;

        // Texture sampler 만들기
        D3D11_SAMPLER_DESC SamplerDesc;
        ZeroMemory(&SamplerDesc, sizeof(SamplerDesc));

        SamplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
        SamplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
        SamplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
        SamplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
        SamplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
        SamplerDesc.MinLOD = 0;
        SamplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

        // Create the Sample State
        HRESULT Result = EngineBase::GetInstance().GetDevice()->CreateSamplerState(&SamplerDesc, NewSampler.GetAddressOf());
        if (Result != S_OK)
        {
            std::cout << "CreateSamplerState failed : LINEARCLAMP" << std::endl;
            return FALSE;
        }

        Samplers.insert({ "LINEARCLAMP", NewSampler });
    }

    return TRUE;
}

 

샘플러는 일단 2개의 종류를 만들어두었다.

Filter는 LINEAR인 상태에서 WRAP인 샘플러와 CLAMP인 샘플러 2개를 만들었다.

이 역시 EngineBase의 자료구조에 저장해두었다.

 

void SetTexture(const std::string& _TextureName)
{
	TextureName = _TextureName;
}

void SetSampler(const std::string& _SamplerName)
{
	SamplerName = _SamplerName;
}

 

렌더러에선 위처럼 샘플러와 텍스쳐의 이름만 저장하도록 하였다.

Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV = EngineBase::GetInstance().GetTextureData(TextureName).ShaderResourceView;
Microsoft::WRL::ComPtr<ID3D11SamplerState> Sampler = EngineBase::GetInstance().GetSampler(SamplerName);

EngineBase::GetInstance().GetContext()->PSSetShaderResources(0, 1, SRV.GetAddressOf());
EngineBase::GetInstance().GetContext()->PSSetSamplers(0, 1, Sampler.GetAddressOf());

이후, Render함수에 위처럼 샘플러와 텍스쳐를 세팅하는 코드를 추가해주었다.

 

여기서, 하나의 쉐이더에 텍스쳐와 샘플러를 여러개 세팅하는 상황이 발생할 수도 있는데 일단은 1개만 세팅한다고 가정하고 구현하였다.

 

처음부터 염두에 두고 코드를 구성하는 것이 좋은 방법이지만, 본인은 수정하는 과정에서도 배울 것이 있다고 생각하여서 추후 다시 수정하는 과정을 거치려고 한다.

 

cbuffer UV : register(b1)
{
    float4 UV;
};

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

Texture2D DiffuseTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = DiffuseTexture.Sample(Sampler, _Input.TexCoord);
    return Color;
}

 

픽셀 쉐이더에 텍스쳐와 샘플러를 추가한 뒤, 색상을 샘플링해주었다.

결과는 아래와 같이 정상적으로 잘 작동되는 것을 확인할 수 있다.

 

 

 

 

후위 표기식이란, 연산자 앞에 있는 두 숫자에 대해 해당 연산을 진행하는 방식이라고 한다.

이렇게, 가장 앞에 놓여진 연산자를 기준으로 앞에 있는 두 숫자에 대해 우선적으로 연산을 진행하는 방식이다.

주어진 후위 표기식에 대한 계산 결과를 출력해주면 된다.

 

문제 풀이

 

연산자를 만났을 때, 앞에 있는 두 숫자에 대해 연산을 해주어야 한다.

이처럼 순서를 유지한 채로 연산을 진행할 때엔 Stack을 사용하는 것이 좋다.

 

 

이렇게 주어진 후위 표기식에서 한 번 연산을 진행하면 아래와 같은 결과가 나온다.

 

이 순서대로 다시 후위표기식의 연산을 진행해야 한다.

 

그러므로, 주어진 후위 표기식을 stack에 하나씩 삽입하다 연산자를 만나면 앞의 숫자 2개를 꺼내 연산을 한 뒤, 결과값을 다시 stack에 저장하고, 다시 후위 표기식을 하나씩 stack에 삽입하는 것을 반복하면 된다.

 

풀이 코드

int Size = 0;
std::cin >> Size;

std::string Command;
std::cin >> Command;

std::vector<int> Nums(Size);
for (int i = 0; i < Size; i++)
{
    std::cin >>	Nums[i];
}

 

Size는 후위표기식에 주어진 숫자가 몇개인지에 대한 변수이다.

Command는 후위 표기식이다.

 

Nums는 각 알파벳에 대응하는 숫자를 저장한 배열이다.

(A는 0번 인덱스, B는 1번 인덱스.....)

 

std::stack<double> Stack;

for (int i = 0; i < Command.size(); i++)
    
    if(Command[i] >= 'A' && Command[i] <= 'Z')
    {
        double CurNum = Nums[Command[i] - 'A'];
        Stack.push(CurNum);
    }
    else
    {
        double Num1 = Stack.top();
        Stack.pop();

        double Num2 = Stack.top();
        Stack.pop();

        if (Command[i] == '+')
        {
            Stack.push(Num2 + Num1);
        }
        else if (Command[i] == '-')
        {
            Stack.push(Num2 - Num1);
        }
        else if (Command[i] == '*')
        {
            Stack.push(Num2 * Num1);
        }
        else if(Command[i] == '/')
        {
            Stack.push(Num2 / Num1);
        }
    }
}

 

위의 코드를 보면, 현재 후위표기식의 요소가 A~Z라면, 해당 알파벳에 대응하는 숫자를 stack에 삽입해주었다.

만약, 연산자라면 앞의 두 숫자를 stack에서 꺼낸 뒤, 연산하여 다시 stack에 넣는 작업을 반복하고 있다. 

 

printf("%.2f", Stack.top());

return 0;

 

연산이 끝나면 stack에는 1개의 숫자만 남게 된다.

해당 숫자를 출력해주면 끝이다.

소숫점 n자리까지 출력하고 싶다면 위처럼 %.(n)f 의 형식을 사용하면 된다.

 

 

코드 전문

더보기
#include <iostream>
#include <vector>
#include <stack>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}

int main()
{
    Init();

    int Size = 0;
    std::cin >> Size;

    std::string Command;
    std::cin >> Command;

    std::vector<int> Nums(Size);
    for (int i = 0; i < Size; i++)
    {
        std::cin >>	Nums[i];
    }

    std::stack<double> Stack;

    for (int i = 0; i < Command.size(); i++)
    {
        if(Command[i] >= 'A' && Command[i] <= 'Z')
        {
            double CurNum = Nums[Command[i] - 'A'];
            Stack.push(CurNum);
        }
        else
        {
            double Num1 = Stack.top();
            Stack.pop();

            double Num2 = Stack.top();
            Stack.pop();

            if (Command[i] == '+')
            {
                Stack.push(Num2 + Num1);
            }
            else if (Command[i] == '-')
            {
                Stack.push(Num2 - Num1);
            }
            else if (Command[i] == '*')
            {
                Stack.push(Num2 * Num1);
            }
            else if(Command[i] == '/')
            {
                Stack.push(Num2 / Num1);
            }
        }
    }

    printf("%.2f", Stack.top());

    return 0;
}

기존의 상수버퍼는 하드코딩되어있어서, 업데이트를 할 때마나 데이터 하나하나씩 map, unmap과정을 거쳐주어야 했다.

상수버퍼가 1개일 때는 별 상관 없겠지만, 여러개를 추가하려고 했더니 너무 번거롭고 귀찮은 부분이 많아서 구조를 수정해주었다.

struct ConstantBufferData
{
	Microsoft::WRL::ComPtr<ID3D11Buffer> ConstantBuffer;
	
	void* Data = nullptr;
	UINT DataSize = 0;
};

 

먼저, 상수버퍼의 데이터를 담을 구조체를 선언해주었다.

다양한 자료형에 대응해야하기 때문에 void*타입으로 데이터를 담도록 하였다.

또한, 상수버퍼를 업데이트할 때 데이터의 size가 필요한데 void*타입으로는 이를 계산할 수 없으므로 미리 계산하여 구조체에 함께 저장하도록 하였다.

std::list<ConstantBufferData> ConstantBuffers;

 

 

각 렌더러는 위와 같이, 상수버퍼를 저장하도록 하였다.

template <typename DataType>
void CreateConstantBuffer(DataType& _Data)
{
    D3D11_BUFFER_DESC CBDesc;
    CBDesc.ByteWidth = sizeof(DataType);
    CBDesc.Usage = D3D11_USAGE_DYNAMIC;
    CBDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    CBDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    CBDesc.MiscFlags = 0;
    CBDesc.StructureByteStride = 0;

    D3D11_SUBRESOURCE_DATA InitData;
    InitData.pSysMem = &_Data;
    InitData.SysMemPitch = 0;
    InitData.SysMemSlicePitch = 0;

    Microsoft::WRL::ComPtr<ID3D11Buffer> NewBuffer;

    HRESULT Result =
        EngineBase::GetInstance().GetDevice()->CreateBuffer(&CBDesc, &InitData, NewBuffer.GetAddressOf());

    if (Result != S_OK)
    {
        std::cout << Name << " :" << "CreateConstantBuffer() failed." << std::hex << "\nResult : " << Result << std::endl;
    };

    ConstantBufferData NewBufferData;
    NewBufferData.ConstantBuffer = NewBuffer;
    NewBufferData.Data = reinterpret_cast<void*>(&_Data);
    NewBufferData.DataSize = sizeof(_Data);

    ConstantBuffers.push_back(NewBufferData);
}

 

상수버퍼를 생성하는 함수이다. 코드는 전반적으로 동일하지만, 마지막에 list에 상수버퍼데이터를 삽입하는 과정만 추가해주었다.

 

for (const ConstantBufferData& _Data : ConstantBuffers)
{
    D3D11_MAPPED_SUBRESOURCE Ms;

    EngineBase::GetInstance().GetContext()->Map(_Data.ConstantBuffer.Get(), NULL, D3D11_MAP_WRITE_DISCARD, NULL, &Ms);
    memcpy(Ms.pData, _Data.Data, _Data.DataSize);
    EngineBase::GetInstance().GetContext()->Unmap(_Data.ConstantBuffer.Get(), NULL);
}

 

업데이트는 위와 같이 list의 원소를 순회하며 하나씩 업데이트하도록 해주었다.

 

int Index = 0;
for (const ConstantBufferData& _Data : ConstantBuffers)
{
    EngineBase::GetInstance().GetContext()->VSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
    EngineBase::GetInstance().GetContext()->PSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
    Index++;
}

 

상수버퍼를 세팅하는 부분에선 위와 같이 해주었다.

여기서 본인은 동일한 상수버퍼를 버텍스쉐이더와 픽셀쉐이더에 모두 세팅하도록 해주었다. 구조체에 VS의 버퍼인지 PS의 버퍼인지 아니면 둘 다 사용하는 버퍼인지 enum 을 사용하여 분류해줄 수도 있지만, 쉐이더에서 레지스터의 번호와 상수버퍼를 일치시키는 부분에 있어서 위의 코드처럼 하는 것이 더 유리할 것이라고 생각하였다.

 

예를 들면, 위에서 버퍼를 세팅할 때, list에서 원소의 위치를 기반으로 슬롯 번호를 세팅해주고 있다.

이 때, list의 첫번째 원소는 vs의 버퍼이고 두번째 원소는 ps의 버퍼라고 했을 때, 각각 쉐이더에선 register(b0)을 사용하게 될 것이다. 상수버퍼가 많아지면 레지스터 번호를 매칭하는 것이 다소 복잡해질 수 있다.

 

하지만, 위처럼  PS와 VS를 동시에 세팅해주면, 첫번째원소는 b0을 사용하고, 두번쨰 원소는 b1을 사용하게 된다. 원소의 위치를 기반으로 레지스터 번호를 쉽게 매칭할 수 있다.

 

하지만 이 방법도 좋은 방법은 아니라고 생각한다. 왜냐하면 이렇게 하면 사용가능한 상수버퍼의 수가 줄어들기 때문이다.

PS에서만 사용하는 상수버퍼와 VS에서만 사용하는 상수버퍼를 분리하여 세팅하면 각 쉐이더당 16개의 상수버퍼를 등록할 수 있다. 하지만, 위의 코드처럼 세팅하게 되면 PS와 VS를 합쳐서 16개밖에 사용하지 못한다.

 

그래서 아래와 같은 방법도 생각해보았다.

PS에서 사용하는 상수버퍼와 VS에서 사용하는 상수버퍼를 다른 자료구조에 분리해서 저장한 뒤 사용하는 방식이다.

그런데 이렇게 하면 VS와 PS에서 둘 다 사용하는 상수버퍼를 관리하는 것이 다소 까다로워진다.

 

그래서 본인은 위의 코드와 같은 방식을 일단 사용하기로 하였다. 왜냐하면 16개 이상의 상수버퍼를 사용할 일이 없을 것 같았기 때문이다. 

 

바뀐 구조를 테스트하기 위해 상수버퍼를 하나 추가해보았다.

상수버퍼의 x값보다 텍스쳐좌표의 x값이 더 크면 파란색을 출력하고, 더 작다면 빨간색을 출력하도록 하였다.

cbuffer UV : register(b1)
{
    float4 UV;
};

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

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    if(UV.x > _Input.TexCoord.x)
    {
        return float4(1.0f, 0.0f, 0.0f, 1.0f);
    }
    else
    {
        return float4(0.0f, 0.0f, 1.0f, 1.0f);
    }
}

 

위는 간단하게 작성해본 쉐이더 코드이다.

 

 

결과는 아주 만족스럽게 잘 나온다.

x값을 기준으로 했는데 특정 면에서는 좌우가 아니라 상하가 분리되는 이유는 박스를 만들때 버텍스를 그렇게 만들어서 그렇다... 아무튼 잘된다!

우리가 코드를 작성하다 보면, 분기를 나눠야하는 경우가 상당히 많다.

분기를 나누는 이유가 여러가지가 있겠지만, 근본적으로는 상황에 맞게 필요한 연산만 실행하기 위해서일 것이다.

이는 최적화를 위해서 일 수도 있고, 올바른 결과물을 위해서일 수도 있을 것이다.

 

하지만, 만약 모든 분기가 다 실행된다면?

 

예를 들어 아래 코드를 보자.

if(현재 픽셀이 피부라면)
{
    Color = SkinShading();
}
else if(현재 픽셀이 옷이라면)
{
    Color = ClothShading();
}
else
{
    Color = HairShading();
}

 

뭐 이런 예시가 있다고 가정해보자. (일반적으로는 이런 분기는 나뉘지 않을 것 같긴 하다...  옷이랑 피부, 머리카락은 아예 다른 쉐이더를 쓰지 않을까?)

 

이 때, 우리가 기대하는 것은, 픽셀의 타입에 맞게 하나의 함수만 실행되는 것이다.

현재 픽셀이 피부라면, SkinShading만 실행하고, 옷이라면 ClothShading만 실행하고 그것도 아니라면 hairShading을 실행하는 것이다.

 

하지만 그래픽카드의 연산은 우리 의도대로 실행되지 않는다고 한다.

 

그래픽카드는 다중 병렬 연산을 통해 아주 고속으로 수많은 픽셀에 대한 연산을 처리해준다.

일반적으로는 32개나 64개의 레지스터를 묶어서 실행한다고 한다.

 

이 때, 병렬 연산을 극대화 하기 위해서는 모든 레지스터가 동일한 명령어를 처리하도록 하는 것이 가장 좋다고 한다.

즉, 분기 때문에 각 레지스터들이 다른 연산을 실행하게 되면 병렬 연산의 성능이 저하될 수 있다는 것이다.

 

이로 인해 레지스터간 처리해야 하는 명령어를 통일하기 위해 GPU에선 모든 분기를 다 실행한 뒤에 조건에 맞는 결과값만 반환하는 방식으로 실행한다고 한다.

 

하지만, 모든 분기를 다 실행하게 되면 이는 성능상에 엄청난 영향을 끼칠 수 있다. 특히나 if~else문 내부에 있는 연산이 무거울수록 그 정도는 더욱 커질것이다.

 

만약, 모든 픽셀이 동일한 분기로 진입하는 것이 확정된 상황이라면 모든 분기에 대한 연산을 다 실행하지 않고 특성 분기에 대해서만 실행하도록 최적화가 되어있다고는 한다.

 

하지만, 일반적인 상황에선 그렇지 않기 때문에 분기를 나누는 것이 아닌가?!

 

그러므로 우리는 쉐이더 내에서 if else 문을 사용할 때엔 주의를 하여서 사용해야 한다.

분기 안에 특히나 무거운 연산들이 포함되어 있을 때는 더욱 그렇다.

+ Recent posts