Part1에선 그래픽스에 흥미를 돋구고, 이해를 하기 위한 사전 지식을 채우는 느낌이었다면 Part2에선 실제로 그래픽스를 게임에 적용하기 위해 필요한 것들을 아주 꽉채워서 배울 수 있었다.

 

물론, 실제로 상용 게임에서 사용되는 화려한 효과를 구현하려면 아직도 배워야 할 것들이 많지만, 직접 엔진을 구성하면서 적용해보면서 굉장히 뿌듯함도 많이 느꼈고 실력이 늘어가는 것을 느낄 수 있어서 너무 재밌고 만족했던 시간이었다.

 

지금 당장은 아니지만, 하던거 좀 마무리 짓고 Part3까지 야무지게 들어야겠다!

 

 

 

n개의 동전이 주어졌을 때, n개의 동전으로 k원을 만들 수 있는 경우의 수 중 사용한 동전의 수가 최소인 경우를 구하면 된다.

 

예를 들어, 1원과 5원 동전이 주어진 경우 10원을 만들기 위해 1원을 10개 사용할 수도 있지만 5원을 2개만 사용하여 10원을 만들 수도 있다. 이 때는 2를 출력하면 된다.

 

문제 풀이

해당 문제는 다이나믹 프로그래밍 문제이다.

다이나믹 프로그래밍을 구현하기 위해선, DP배열과 점화식이 필요하다.

 

먼저, DP배열을 만들어보자. 해당 문제는 k원을 만드는데 필요한 동전의 최소 개수를 구하는 문제이다.

즉, DP배열의 k번째 인덱스는 k원을 만들기 위해 필요한 동전의 최소 개수를 저장하는 배열로 하면 좋을 듯 하다.

 

이젠, 점화식을 구성해보자.

 

DP[k] 의 k원을 만들기 위해 필요한 동전의 최소 개수이다.

먼저, (1, 2, 5) 이렇게 3가지 동전이 주어졌다고 해보자.

 

1개의 1원을 추가로 사용해서 k원을 만들고자 한다면 (k - 1)원에 1원을 더해야 할 것이다.

이 때, (k - 1)원을 만드는 경우에 1원을 더해서 k원을 만드는 경우를 구할 수 있다.

 

예를 들어, k가 10이라고 해보자.

그렇다면, 1원을 하나 더해서 10원을 만드는 경우의 수는 9원을 만드는 경우의 수와 같다고 할  수 있다.

9원을 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 로 만들었다면, 그 뒤에 + 1 만 추가해주면 되기 때문이다.

만약, 2 + 2 + 2 + 2 + 1 의 조합으로 9를 만들었다면, 똑같이 뒤에 + 1을 추가해 2 + 2 + 2 + 2 + 1 + 1 의 조합으로 10을 만들 수 있다.

 

그런데, 우리가 구하고자 하는 것은 10원을 만들기 위해 최소로 필요한 동전의 개수이다.

그렇다면, 9원을 만들기 위해 필요한 최소 동전의 개수 + 1을 하면 10원을 만드는데 필요한 최소 동전의 개수를 구할 수 있지 않을까?

 

즉, i원을 하나 더 사용해서 k원을 만드는 경우중 동전의 개수가 최소인 경우는 (k - 1)원을 만드는데 필요한 최소 동전의 개수 + 1인 것이다.

 

하지만, 우리는 동전의 개수가 1가지가 아니다.

2원도 있고 5원도 있다.

 

그러므로, (k - 2)원을 만드는데 필요한 최소 동전의 개수 + 1도 고려해야 하고 (k - 5)원을 만드는데 필요한 최소 동전의 개수도 고려해야 한다.

 

이를 점화식으로 세워보자.

 

주어진 동전이 (A1, A2, A3....An) 이라고 한다면

DP[K] = std::min(DP[K - A1], DP[K - A2], DP[K - A3] ..... , DP[K -An])이 되는 것이다.

이를 코드로 구현하면 답을 구할 수 있게 된다.

 

풀이 코드

int NumCoin = 0;
int TargetMoney = 0;
std::cin >> NumCoin >> TargetMoney;

std::set<int> Coins;
for (int i = 0; i < NumCoin; i++)
{
    int CurCoin = 0;
    std::cin >> CurCoin;

    Coins.insert(CurCoin);
}

입력을 받아주었다.

그런데 보면 동전을 vector에 저장하는 것이 아니라 set에 저장하고 있다.

그 이유는 문제의 조건중에 같은 가치의 동전이 주어질 수도 있다는 이유 때문이다.

 

이 문제는 2원짜리 동전이 하나만 주어져도 그 동전을 여러 번 사용할 수 있는 문제이다.

그런데 자료구조에 2원짜리 동전을 여러개 가지고 있다고 해서 결과가 달라질까?

 

2원짜리 동전이 자료구조에 1개만 있어도 100번 사용할 수 있기 때문에, 동전이 여러개 있는 것은 연산 낭비라고 생각했다. 그래서 set에 저장하여 중복을 제거하였다.

 

std::vector<int> DP(TargetMoney + 1, INT_MAX / 2);
DP[0] = 0;

std::set<int>::iterator CurIter = Coins.begin();
std::set<int>::iterator EndIter = Coins.end();

while (CurIter != EndIter)
{
    int CurCoin = *CurIter;

    for (int j = CurCoin; j <= TargetMoney; j++)
    {
        DP[j] = std::min(DP[j], DP[j - CurCoin] + 1);
    }

    CurIter++;
}

그리고, DP배열을 만들어서 위의 점화식을 코드로 구현해주었다.

모든 동전에 대해 j원을 만드는데 필요한 최소 동전 갯수를 구한 뒤, DP배열을 최소값으로 갱신해주었다.

 

이 때, DP배열은 INT_MAX가 아닌 INT_MAX / 2 로 초기화해주었는데, 그 이유는 DP[j - CurCoin] + 1 부분에서 오버플로우가 발생할 수 있기 때문이다.

 

if (DP[TargetMoney] == INT_MAX / 2)
{
    std::cout << -1;
}
else
{
    std::cout << DP[TargetMoney];
}

return 0;

마지막으로 답을 출력해주면 된다.

 

 

코드 전문

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

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

int main()
{
    Init();

    int NumCoin = 0;
    int TargetMoney = 0;
    std::cin >> NumCoin >> TargetMoney;

    std::set<int> Coins;
    for (int i = 0; i < NumCoin; i++)
    {
        int CurCoin = 0;
        std::cin >> CurCoin;

        Coins.insert(CurCoin);
    }

    std::vector<int> DP(TargetMoney + 1, INT_MAX / 2);
    DP[0] = 0;

    std::set<int>::iterator CurIter = Coins.begin();
    std::set<int>::iterator EndIter = Coins.end();

    while (CurIter != EndIter)
    {
        int CurCoin = *CurIter;

        for (int j = CurCoin; j <= TargetMoney; j++)
        {
            DP[j] = std::min(DP[j], DP[j - CurCoin] + 1);
        }

        CurIter++;
    }

    if (DP[TargetMoney] == INT_MAX / 2)
    {
        std::cout << -1;
    }
    else
    {
    	std::cout << DP[TargetMoney];
    }

    return 0;
}

 

 

동전이 여러 종류가 주어졌을 때, 각 동전을 사용해서 K원을 만드는 경우의 수를 구하면 된다.

 

주의할 점은 1 + 1 + 2 로 4원을 만드는 것과 2 + 1 + 1 로 4원을 만드는 것은 같은 경우로 친다.

(순서만 바뀌는 것은 같은 경우이다.)

 

문제 풀이

이 문제는 다이나믹 프로그래밍으로 해결할 수 있는 문제이다.

코드는 간단하지만, 아이디어를 구상하는 것이 다소 어려울 수 있다.

 

먼저, 입력으로 주어진 동전은 (1, 2, 5) 3개이며, K는 10이라고 가정해보자.

 

먼저, 다이나믹 프로그래밍은 무엇을 기준으로 DP배열을 만드느냐가 첫 번째 관문이다.

해당 문제에선 우리가 찾고자 하는 것이 k원을 만드는 대한 경우의 수이다.

그러므로, DP배열의 i번 인덱스에는 i원을 만드는 경우의 수를 저장할 것이다.

 

Ex) DP[5] 는 주어진 동전을 사용해 5원을 만드는 경우의 수

 

그렇다면, 이제 점화식을 세워보자.

 

10원을 만드는 경우의 수를 생각해보자.

 

만약, 1원부터 9원까지 만들 수 있는 경우의 수가 모두 구해진 상태라고 가정을 해보자.

이 때, 우리에게 주어진 동전은 (1, 2, 5) 총 3가지가 있다.

 

그렇다면, 1원을 더해서 10원을 만드는 경우를 생각해보자.

1원을 더해서 10원이 되려면 9원에 1원을 더해야 한다.

 

즉, 1원을 더해서 10원을 만드는 경우의 수는 9원을 만드는 경우의 수라고 할 수 있지 않을까?

 

만약, 2 + 2 + 2 + 1 이라는 조합으로 9를 만들었다고 해보자.

 

여기의 끝에 1을 더해서, 2 + 2 + 2 + 1 + 1 을 하면 10이 된다.

즉, 9에 1을 더해서 10을 만드는 경우의 수는 결국 9를 만드는 경우의 수가 된다.

 

똑같이 2원을 더해서 10원을 만드는 경우의 수는 8원을 만드는 경우의 수가 되고, 5원을 더해서 10원을 만드는 경우의 수는 5원을 만드는 경우의 수가 된다.

 

주어진 입력에 대해 점화식을 세워보면, DP[10] = DP[9] + DP[8] + DP[5] 라고 할 수 있다.

DP[9]도 동일한 방식으로 DP[8] + DP[7] + DP[4]로 표현할 수 있으며 DP[8]과 DP[5]도 마찬가지의 논리가 적용된다.

 

즉 동전의 가치가 (A1, A2, A3 ....  An)이렇게 주어진다면, DP[K] = DP[K - A1] + DP[K - A2] ...... + DP[K - An] 이 된다.

이 점화식을 코드로 구현해보면 답을 구할 수 있다.

 

풀이 코드

int NumCoin = 0;
int TargetMoney = 0;
std::cin >> NumCoin >> TargetMoney;

std::vector<int> Coins(NumCoin);
std::vector<int> DP(TargetMoney + 1);

for (int i = 0; i < NumCoin; i++)
{
    std::cin >> Coins[i];
}

먼저, 입력을 모두 저장해주자.

 

DP[0] = 1;

for (int i = 0; i < NumCoin; i++)
{
    for (int j = Coins[i]; j <= TargetMoney; j++)
    {
        DP[j] += DP[j - Coins[i]];
    }
}

다음은 DP[0]을 1로 초기화해준 뒤, 반복문을 돌려주었다.

0원을 만드는 경우도 1가지 있으니, DP[0] = 1로 초기화 해주었다.

 

이제, 각 동전에 대해 마지막으로 동전을 더해서 j원을 만드는 경우를 계산해 주었다.

동전의 가치보다 작은 금액은 만들 수 없으므로 j는 Coins[i]부터 시작하도록 하였다.

 

Coins[i] 가 2원인 경우, 마지막으로 2원을 더해서 2원을 만드는 경우, 3원을 만드는 경우, 4원을 만드는 경우 ....... 10원을 만드는 경우까지 갱신해주는 것이다.

std::cout << DP[TargetMoney];

return 0;

모두 갱신한 뒤 답을 출력해주면 된다.

 

 

코드 전문

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

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

int main()
{
    Init();

    int NumCoin = 0;
    int TargetMoney = 0;
    std::cin >> NumCoin >> TargetMoney;

    std::vector<int> Coins(NumCoin);
    std::vector<int> DP(TargetMoney + 1);

    for (int i = 0; i < NumCoin; i++)
    {
        std::cin >> Coins[i];
    }

    DP[0] = 1;

    for (int i = 0; i < NumCoin; i++)
    {
        for (int j = Coins[i]; j <= TargetMoney; j++)
        {
            DP[j] += DP[j - Coins[i]];
        }
    }

    std::cout << DP[TargetMoney];

    return 0;
}

UDP와 TCP는 전송계층에서 사용되는 네트워크 통신 프로토콜이다.
각 차이점을 알아보며 특징을 이해해보도록 하자.
 

UDP

UDP는 비 연결형 통신 프로토콜이다.
비 연결형 통신 프로토콜이란, 서버와 클라이언트 간의 연결 없이 데이터를 주고 받는다는 의미이다.
 
연결 없이 데이터를 주고받는다는 뜻은 클라이언트에서 서버를 향해 데이터를 보낼 수는 있으나, 그 반대로 서버에서 클라이언트로 데이터를 보낼 수는 없다는 뜻이다.
 
왜? 서버는 대상이 특정되어 있지만, 클라이언트는 불특정 다수이므로 대상을 특정할 수 없다.
=> 서버에서도 특정 클라이언트의 정보를 안다면 데이터를 보낼 수도 있다.
 
서버와 클라이언트가 연결되어 있지 않기 때문에, 각 패킷들은 서로 다른 경로로 이동하여 목적지에 도착하게 된다.
각 패킷의 경로가 다른 이유로, 패킷의 전송 시간이 일정하지 않아 보내는 순서와 도착하는 순서가 다를 수가 있다.
=> 데이터의 신뢰성이 보장되지 않는다.
 
클라이언트에서 A,B,C의 순서로 패킷을 서버로 보내면, 서버에서는 A,B,C의 순서로 패킷을 수신해야 하지만
UDP 통신의 경우 A,C,B 나 B,A,C 등 클라이언트가 송신한 순서와 다소 다를 가능성이 생긴다.
 
그렇다면, 데이터를 신뢰성있게 보내지도 못하는 프로토콜이 무슨 의미가 있을까?
UDP의 장점은 바로 속력에 있다. 데이터를 송수신하는데 있어서 고려하지 않는 것들이 많아 빠른 처리가 가능하기 때문에 네트워크에 부하를 적게 주며, 신뢰성은 낮더라도 연속성은 높은 통신을 할 수 있게 된다.
 
이러한 이유로 UDP는 비디오 스트리밍과 같이 신뢰성보다 연속성이 중요한 서비스에 주로 사용된다.
(비디오 스트리밍의 경우 중간 1프레임정도 문제가 발생하더라도 크게 눈치채지 못하지만, 느린 송수신으로 인해 화질이 저하되거나 프레임이 낮아지는 것은 쉽게 알아차릴 수 있다.)
 

TCP

TCP는 연결형 통신 프로토콜이다.
연결형 통신 프로토콜이란, 클라이언트와 서버가 논리적으로 연결된 상태로 데이터를 주고 받는다는 의미이다.
 
클라이언트와 서버가 서로 이어져 있기 때문에, 양방향 통신이 가능하다. 클라이언트에서 서버로 데이터를 보낼 수 있으며, 서버에서도 클라이언트로 데이터를 보낼 수 있다.
 
TCP 통신에서 클라이언트와 서버를 연결하는 이유는 신뢰성 있는 데이터의 송수신을 위해서이다. 서로 연결되어 있기 때문에, 데이터의 순서와 무결성을 보장할 수 있다. 또한, 유실된 패킷이 존재할 경우 클라이언트에 재송신을 요청할 수도 있게 된다.
 
TCP 통신에서 클라이언트와 서버의 연결, 해제에는 특별한 방법이 사용된다.
바로, 3 Way - Handshake4 Way - Handshake 이다.

3 Way - Handshake

3Way - Handshake 란, TCP 통신에서 클라이언트와 서버가 서로 연결하기 위해 사용하는 방식이다.
이 때, SYN과 ACK라는 두개의 플래그를 사용한다. (구체적로는 더 많은 플래그가 사용되는 것으로 안다.)
 
SYN : 무작위 난수를 담아, 상대방에게 보낸다. (응답을 요청하는 신호)
ACK : 상대가 보낸 SYN에 담긴 난수에 1을 더해, 상대방에게 보낸다. (SYN에 대한 응답 신호)
 
먼저, 클라이언트가 서버에 SYN을 보낸다. 
서버는 SYN을 수신한 뒤, SYN에 대한 ACK와 함께 새로운 SYN을 보낸다.
클라이언트는 서버에서 보낸 SYN에 대한 ACK를 다시 서버에 보내고, 서버가 ACK를 수신한 뒤 서로 연결이 확립된다.
 
이는 신뢰성 있는 통신을 위해, 서로 데이터를 송신하고 수신할 수 있는 준비가 되어있는지 사전에 확인하는 과정이다.

4 Way - Handshake

4Way - Handshake 란, TCP 통신에서 클라이언트와 서버가 안정적으로 연결을 해제하기 위해 사용하는 방식이다.
이 때는, FIN과 ACK라는 두개의 신호를 사용한다.
 
FIN : 연결을 종료하겠다는 신호
ACK : FIN에 대한 응답 신호
 
서버에 연결을 시도하는 것은 클라이언트로부터 시작되지만, 접속을 해제하는 것은 서버가 먼저 시도할 수도 있다.
그러므로, 서버와 클라이언트가 아니라 A와 B로 표현할 것이다.
A는 먼저 연결 해제를 요청하는 쪽이며, B는 해제를 요청받는 쪽이다.
 
먼저, A는 B에게 FIN을 보낸다. 
B는 FIN을 수신한 뒤 A에 ACK를 송신하고, 잠시 대기 상태에 머문다. 
(대기상태에 머무는 이유는 A가 보낸 패킷중 도달하지 않은 것이 있을 수 있기 때문에 이를 기다리는 것이다.)
 
B에서 충분히 대기했다고 판단하면, A에 FIN신호를 보낸다.
A는 FIN을 수신한 뒤, B에 ACK를 송신하고 잠시 대기상태에 머문다.
(역시나, B에서 보낸 패킷중 아직 도착하지 않은 것이 있을 수 있기 때문이다.)
 
B는 ACK를 수신받은 뒤, 소켓을 닫는다.
(이 때, A는 아직 소켓을 닫지 않았으므로 송신중인 패킷이 있다면 수신할 수 있다.)
 
A는 충분한 대기시간을 거친 뒤, 최종적으로 소켓을 닫는다.
 
이러한 과정을 통해, 연결 해제로 인해 데이터의 유실이 발생하거나 데드락에 빠지는 것을 방지할 수 있다.
 
TCP는 이처럼, 연결과 해제 과정에서도 여러 과정을 거치지만 패킷을 송수신할 때, 데이터의 신뢰성을 보장하기 위한 여러 절차를 거치기 때문에 UDP에 비해 상대적으로 느린 속력을 보인다.
 
TCP의 경우는 속도보다 신뢰성에 무게를 둔 프로토콜인만큼, 파일을 다운로드 받는 등의 서비스에 이용된다.
 

TCP vs UDP

두 프로토콜의 차이를 표로 한눈에 알아보자.

UDPTCP
비 연결형 통신연결형 통신
데이터의 신뢰성이 낮다.데이터의 신뢰성이 높다.
데이터 송수신 속력이 상대적으로 빠르다.데이터 송수신 속력이 상대적으로 느리다.
단방향 통신이다.양방향 통신이다.

 

DNS : 웹 사이트의 문자열 주소(도메인)를 IP로 변경하거나, IP를 문자열 주소(도메인)로 변경해주는 시스템
 
우리는 일반적으로, 어떠한 웹사이트에 접속하기 위해 문자열로 된 주소를 입력한다.
www.naver.com
www.google.com
www.daum.net 
이런 영문과 특수기호로 이루어진 문자열을 주소창에 입력하면 웹사이트에 접속이 된다.
하지만, 실제로는 영문 문자열이 아니라 IP주소를 알아야 웹 사이트에 접속할 수 있다.
 
구글에 PING을 한 번 보내보자.

 
[2404:6800:4005:814::2004] 라는 IPv6 주소를 확인할 수 있다.
www.google.com이라는 문자열은 어떠한 과정을 거쳐서 [2404:6800:4005:814::2004]라는 IP주소로 변환이 되고, 우리는 그 IP주소를 통해 웹 서버에 접속할 수 있게 되는 것이다.
 
이 때, 문자열 주소(도메인)를 IP로 변환하는 역할을 담당하는 것이 DNS이다.
 
복잡하고 직관적이지 않은 IP주소를 사이트 별로 모두 외우는 것은 유저 입장에서 접근성이 상당히 떨어지는 일이기 때문에, 쉽고 간편하게 접근할 수 있는 문자열 주소를 사용할 수 있게 도와주는 셈인 것이다.
 

DNS의 과정

 

1. Local DNS에 IP주소 요청

Local DNS란, 기지국의 DNS서버이다. 모든 PC는 인터넷 가입시, 해당 통신사 기지국의 DNS 서버를 등록하게 된다.
어떠한 도메인의 IP를 찾기 위해, PC는 Local DNS에 해당 도메인의 IP를 요청하게 된다.
 

2.Local DNS에서 Root Name Server에 IP주소 요청

기지국의 DNS 서버는 요청받은 도메인의 IP주소를 Root Name Server라는 DNS 서버에 요청하게 된다.
 
DNS서버는 트리 구조로 이어져있다고 한다. 해당 트리의 루트 노드가 바로 Root Name Server인 것이다.
해당 Root Name Server에 IP주소를 요청하면, Root Name Server는 TLD 네임 서버의 주소를 반환해준다. 
 

3.Local DNS에서 TLD 네임 서버에 IP요청

TLD 네임 서버란,  최상위 도메인(TLD)의 네임 서버이다. 도메인 주소를 보면, 닷(.)으로 분류되어 있는 것을 볼 수 있다.
www.naver.com 의 경우, www와 naver과 com으로 분류되어 있다. 이중 가장 뒤에 있는 com이 최상위 도메인이다.
www.daum.net  의 경우, net이 최상위 도메인이 된다.
 
우리가 Local DNS에 www.naver.com 의 IP를 요청했다면, Root Name Server에선 com의 DNS 서버를 알려주게 된다.
Local DNS는 Root Name Server로부터 제공받은 com의 DNS 서버에 다시 IP를 요청하게 된다. 최상위 도메인 naver.com의 DNS 서버를 찾아서 다시 local DNS에 반환해준다.
 

4.Local DNS에서 최종적인 IP를 받을 때까지 재귀적으로 IP요청

예를 들어, 요청한 주소가 A.B.C.D.E.F.G.com 이라고 해보자. (물론 이런 주소는 실제로는 없겠지만)
그렇다면, Root Name Server가 TLD DNS 서버를 제공해주고, 다시 TLD DNS 서버에 IP를 요청하면, G.com의 DNS 서버를 제공해줄 것이며, G.com의 DNS 서버는 F.G.com의 DNS 서버를 제공해줄 것이다.
local DNS는 A.B.C.D.E.F.G.com의 IP주소를 반환 받을 때까지 이 과정을 재귀적으로 반복하게 된다.
최종적으로 IP를 알게 되면, Local DNS는 해당 IP주소를 PC에 알려주게 된다.
 

DNS 캐싱

DNS는 이처럼 상당히 번거로운 과정을 거치고 있다. 하지만, 항상 저 모든 과정을 거치는 것은 아니다.
왜냐하면, 각 DNS는 최근에 탐색된 IP주소를 캐싱 하기 때문이다. 캐싱이란, 별도의 공간에 IP를 저장 해놓는 것이다.
 
예를 들어, Local DNS에 www.naver.com  의 아이피 주소가 캐싱되어 있다면, Root Name Server에 IP를 요청할 필요 없이 캐싱된 IP를 바로 PC에 반환해줄 수 있는 것이다. (컴퓨터에 사용되는 캐시 메모리와 동일한 기능이라고 생각하면 된다.)
 
어떠한 DNS 서버에서 캐싱된 IP주소를 발견할 수 있다면, 끝까지 재귀적 탐색을 하지 않고 중간에 종료하여 IP 주소를 얻어낼 수 있는 것이다. 그리고 이렇게 얻어낸 IP 주소는 각 DNS 서버에만 캐싱되는 것이 아니라 우리 PC에도 캐싱된다.
(cmd에 ipconfig /displaydns 를 입력하면, 캐싱된 데이터를 볼 수 있다.) 
 
하지만, PC에 캐싱된 IP정보는 모종의 이유로 변조될 수 있다고 한다. (바이러스, 해킹 등...)
www.naver.com을 을 쳤는데 다른 IP 주소로 접속이 되는 보안 취약점이 발생할 수 있다는 것이다.
그러므로, PC에 저장된 DNS 캐시는 주기적으로 정리해주는 것이 좋다고 한다.
(물론, 개인 PC 뿐만이 아니라 각 DNS 서버에서도 발생할 수 있는 문제이긴 하다.)

'네트워크' 카테고리의 다른 글

네트워크 - UDP vs TCP  (0) 2024.05.26
네트워크 - PING (Packet Internet Groper), Ping Of Death  (0) 2024.05.26
네트워크 - LAN, MAN, WAN  (0) 2024.05.26

PING이란, 호스트와 통신 상태를 파악하기 위해 사용하는 명령어이다.

 

게임을 하다 보면, 핑이 튄다는 말을 자주 들을 수 있을 것이다.

(게임에 따라 핑 수치가 높으면 빨간색으로 표시되는 경우가 있어, 빨핑이라는 용어를 사용하기도 한다.)

 

핑 수치가 높다는 것은, 현재 호스트와 데이터를 한 번 주고받는데 오랜 시간이 걸린다는 것이다.

 

우리가 PING명령어를 사용하게 되면, 호스트에게 echo Request를 송신하게 된다.

호스트에선 이를 수신한 뒤, echo Reply를 우리 쪽으로 다시 송신하게 된다.

 

우리는 echo Reply를 수신한 뒤, echo Request를 송신했던 시간을 기준으로 얼마나 걸렸는지를 계산하게 되고 이 것이 핑수치가 되는 것이다.

 

하지만, 호스트가 항상 응답을 하는 것은 아니다. 서버가 현재 닫혀있는 상태일 수도 있고, 여러 문제로 통신이 불가능한 상황일 수도 있다. 이런 경우엔 echo reply를 수신받을 수 없다. 

 

echo Reply를 수신받을 수 없을 땐, 두 가지의 메세지를 확인할 수 있다.

 

1. Request Timed Out

2. Destination Host Unreachable

 

Request Timed Out은 우리가 설정한 시간동안 echo Reply를 수신받지 못하는 경우에 확인할 수 있는 메세지이다.

서버 혼잡, 패킷 유실 등의 이유로 서버 측으로부터 응답을 받지 못하는 경우에 확인할 수 있다.

 

Destination Host Unreachable은 아예 통신이 불가능한 상황에 확인할 수 있다.

어떠한 이유로든 호스트와 연결할 수 있는 길이 없을 때 확인할 수 있는 메세지이다.

 

 

CMD에 이렇게 ping라는 명령어를 치면, 다양한 옵션을 확인할 수 있다.

 

이런 식으로 주소를 입력하여, 특정 웹사이트와의 통신상태를 확인할 수도 있다.

 

Ping은 유용하게 사용될 수도 있지만, 이를 이용한 공격 방식도 있다.

 

Ping Of Death

위의 cmd를 보면, ping 패킷은 일반적으로 32바이트 크기로 전송된다.

이를 의도적으로 64바이트 (최대크기)로 부풀린 다음, 타깃의 IP에 대량의 EchoRequest를 보낸다.

 

64바이트의 거대한 패킷은 송신 과정에서 여러 개의 조각으로 파편화되고, 타깃은 수많은 Echo Request의 파편을 수신받게 된다.

 

이 과정에서 수많은 Echo Request를 받고 다시 Echo Reply를 송신하는 것도 무리가 많지만, 여러개로 쪼개진 Echo Request를 조립하기 위해 기다리는 과정도 부하가 상당하기 때문에 타깃의 컴퓨터는 다른 작업을 실행할 수 없는 상태가 되어버린다.

 

이 것을 Ping Of Death라고 하며, D-DOS(디도스) 공격의 일종으로 분류된다.

'네트워크' 카테고리의 다른 글

네트워크 - UDP vs TCP  (0) 2024.05.26
네트워크 - DNS (Domain Naming Systyem)  (0) 2024.05.26
네트워크 - LAN, MAN, WAN  (0) 2024.05.26

네트워크 통신망은 크기에 따라 LAN, MAN, WAN으로 구분된다.

 

규모에 따라 통신망을 분류하는 이유는?

=> 트래픽을 분산시킴으로써, 효율적으로 처리하기 위해.

 

하나의 통신망에 전세계의 트래픽이 몰리면 병목현상도 매우 심해지고, 유지보수 측면에서도 매우 불리하다.

 

LAN

LAN : 가장 작은 규모의 통신망

가정집, 사무실, PC방 등에서 호스트를 연결하는 통신망이다.

이더넷 케이블이나 와이파이를 이용하여 서로를 물리적으 연결한다.

 

통신망의 길이가 짧은 만큼, 전송 속도가 MAN, WAN에 비해 빠르고, 안정성이 높다.

근거리에 있는 개인과 개인은 동일한 LAN에서 통신한다.

 

MAN

LAN : LAN과 WAN 의 중간 규모의 통신망

도시 내에 있는 수많은 네트워크를 하나로 묶는 통신망이다.

(도시에만 사용되는 것은 아니고, 규모가 큰 기업이나 대학 등에서도 발견할 수 있다고 한다. 다만, 도시 단위에서 가장 대표적으로 발견할 수 있으며, 그 비슷한 규모라고 생각하면 된다.)

 

광섬유, 동축 케이블을 전송매체로 사용한다고 한다.

 

LAN보다 연결 범위가 길지만 속력이 느리기 때문에, 개인과 개인을 연결하는데 사용하지는 않고 여러 기관을 연결하는데 주로 사용한다. 

 

WAN

WAN : 가장 큰 규모의 통신망

국가 혹은 대륙간의 통신에 사용되는 통신망이다.

(물론, 국가나 대륙간의 소통에 사용되는 것만은 아니고, 거리가 충분히 멀다면 어디에서든 발견할 수 있다.)

 

LAN, MAN에 비하면 매우 느린 속도를 보이지만, 아주 넒은 범위에서 통신이 가능하다.

인터넷, 위성 통신 등이 WAN에 포함된다고 한다.

 

 

도시별로 홍보 비용과 기대 효과가 주어진다.

 

A도시에서 5원을 사용하면, 3명의 고객이 확보된다는 정보가 있고

B도시에서 3원을 사용하면, 1명의 고객이 확보된다는 정보가 있다고 해보자. (정보는 확실하다고 가정한다.)

 

같은 도시의 홍보를 여러 번 할 수 있기 때문에, A도시에서 10원을 사용한다면 6명의 고객을 확보할 수 있다.

 

이 때, 내가 12명의 고객을 늘리고 싶다면, 어떻게 홍보를 선택해야 최소 비용으로 12명 이상의 고객을 늘릴 수 있을까?

 

문제 풀이

다이나믹 프로그래밍을 활용하면, 문제를 해결할 수 있다.

DP배열을 만든 뒤, i번 인덱스에 대해 i원을 사용했을 때 확보할 수 있는 고객의 최대 수를 저장할 것이다.

 

이 때, 내가 모으고 싶은 고객의 수가 12명이라고 한다면 DP배열에서 가장 먼저 12 이상의 수가 나오는 인덱스를 답으로 반환하면 된다.

 

12 2
3 5
1 1

 

위의 입력에 대해 과정을 알아보자.

위의 표는 DP 배열이다. 인덱스는 사용할 가격이며, 원소의 값은 해당 가격을 사용했을 때 확보할 수 있는 고객의 최대 수이다.

 

먼저, 첫번째 입력인 3, 5에 대해 갱신을 해보자.

 

가격은 최소가 3원 단위이기 때문에, (3, 5)의 홍보를 이용해선 1원과 2원을 사용해서는 고객을 확보할 수 없다.

즉, 1원과 2원에 대해서는 위와 같이 갱신될 것이다.

 

다음 3원에 대해서 갱신해보자. 3원의 경우엔 5명의 고객을 확보할 수 있을 것이다.

4원과 5원을 사용하더라도, 역시나 3원 단위로밖에 돈을 사용할 수 없으므로 3원을 사용했을 때와 동일하게 5명을 확보할 수 있다.

위의 표와 같이 값을 갱신할 수 있다.

동일하게 반복하면 표는 아래와 같아진다.

(3, 5)의 광고에 대해서는 이렇게 갱신할 수 있다.

 

이제 다음 입력인 (1, 1)의 광고를 기준으로 배열의 값을 한 번 더 갱신해보자.

먼저, 1원과 2원에 대해서는 기존에 0이었던 값을 1과 2로 갱신할 수 있다.

이번 광고는 가격이 1원단위이기 때문이다.

 

그런데, 3원을 보자. 현재 광고를 기준으로 하면 3원을 사용했을 때 3명밖에 확보하지 못한다.

하지만, DP배열에 저장된 값은 5이다. 그러므로 3원에 대해서는 갱신할 수 없다.

3원에 대해서는 가격이 갱신되지 않고 고정될 것이다.

반면, 4원 5원을 보자.

 

4원의 경우, 3원으로 5명을 확보한 뒤 1원으로 1명을 더 확보할 수 있다.

5원의 경우에도, 3원으로 5명을 확보한 뒤 2원으로 2명을 더 확보할 수 있다.

 

현재 광고의 가격을 Cost, 확보할 수 있는 고객의 수를 man이라고 한다면 

D(N) = D(N - Cost) + Man 이라는 점화식을 세울 수 있다.

 

현재 광고를 사용한다고 가정했을 때 최대값은 당연히 광고 가격을 뺀 가격에서 최대한으로 확보할 수 있는 고객의 수에 현재 광고를 통해 확보할 수 있는 고객의 수를 더하는 것이 최대 기대값일 것이다.

 

하지만, 그 값이 기존 값보다 항상 크다고 할 수는 없으므로 D(N) = std::max(D(N), D(N - Cost) + Man)이 된다.

 

이 점화식을 통해 배열을 갱신하면, 아래와 같이 갱신된다.

즉, 12명을 확보하기 위해서 최소값은 12 이상의 값이 처음으로 나오는 인덱스의 값인 8이 된다.

 

코드 풀이

int TargetValue = 0;
int NumCity = 0;

std::cin >> TargetValue >> NumCity;

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

먼저, 입력을 모두 저장해준다.

std::vector<int> DP(100001);
for (int i = 0; i < NumCity; i++)
{
    for (int j = 1; j <= 100000; j++)
    {
        int Cost = ADs[i].first;
        int Mans = ADs[i].second;

        if (j - Cost >= 0)
        {
            DP[j] = std::max(DP[j], DP[j - Cost] + Mans);
        }
    }
}

위에서 말했던 점화식 그대로 계속 배열을 갱신해주면 된다.

모든 광고에 대해 갱신을 계속하였다.

 

여기서, DP을 왜 100001로 resize하였나 의문이 들 수 있다.

입력조건 범위를 보면, 확보하고 싶은 고객의 최대 수는 1000명이고, 광고의 최대 비용은 100원이다.

 

이 조건으로 인해, 100원을 들여서 1명을 확보하는 경우가 가장 비싼 경우일 것이다.

이 경우에 1000명을 확보하기 위해선, 10만원이 필요하기 때문에 DP를 가능한 최대 가격인 100000으로 갱신해주었다.
(100000 + 1 인 이유는 인덱스를 직관적으로 사용하기 위해)

 

물론, 입력받은 값을 기준으로 1인당 비용의 최대값을 구한 뒤, 그 값을 기준으로 DP를 갱신할 수도 있지만 실제로 프로그램을 만드는 것이 아니니 이런 사소한 부분은 넘어가고 최대한 편하게 구현하는 것이 좋다고 생각한다.

 

int Answer = std::lower_bound(DP.begin(), DP.end(), TargetValue) - DP.begin();
std::cout << Answer;

return 0;

마지막으로 이렇게 출력해주면 된다.

 

lower_bound는 해당 값보다 크거나 같은 원소 중 가장 앞에 있는 원소의 이터레이터를 반환해준다.

이터레이터의 포인터 연산으로 인해, Lower_bound가 반환해주는 값 - begin을 하면 해당 원소의 인덱스를 구할 수 있다.

 

그렇게 구한 인덱스를 답으로 출력해주면 된다.

 

코드 전문

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

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

int main()
{
    Init();

    int TargetValue = 0;
    int NumCity = 0;

    std::cin >> TargetValue >> NumCity;

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

    std::vector<int> DP(100001);
    for (int i = 0; i < NumCity; i++)
    {
        for (int j = 1; j <= 100000; j++)
        {
            int Cost = ADs[i].first;
            int Mans = ADs[i].second;

            if (j - Cost >= 0)
            {
                DP[j] = std::max(DP[j], DP[j - Cost] + Mans);
            }
        }
    }

    int Answer = std::lower_bound(DP.begin(), DP.end(), TargetValue) - DP.begin();
    std::cout << Answer;

    return 0;
}

 

'코딩테스트 문제 풀이 (C++)' 카테고리의 다른 글

백준 2294 - 동전 2 (C++)  (0) 2024.05.26
백준 2293 - 동전 1 (C++)  (2) 2024.05.26
백준 15486 - 퇴사 2 (C++)  (0) 2024.05.25
백준 2011 - 암호코드 (C++)  (0) 2024.05.25
백준 2615 - 오목 (C++)  (0) 2024.05.22

 

 

백준이가 퇴사하기 전까지 N일동안 최대의 이익을 얻는 것이 목표이다.

각 날짜별로 상담이 잡혀있으며, 각 상담별로 상이하게 소요 시간과 상담료가 설정되어있다.

이 때, 최고 효율로 상담을 선택하여 총 이익을 최대값을 구하면 된다.

 

문제 풀이

본인은 다이나믹 프로그래밍을 활용하여 문제를 해결하였다.

 

DP배열을 만들어서, 각 인덱스에는 인덱스에 해당하는 날짜까지 얻을 수 있는 총 이익의 최대값을 저장할 것이다.

이 때, 가장 기본적으로 성립하는 것은 당연히 DP[i] >= DP[i - 1]이라는 것이다.

 

3일동안 벌 수 있는 총 이익이 2일동안 벌 수 있는 총 이익과 같을 수는 있어도 더 적을 수는 없기 때문이다.

 

그러므로, DP[i] = std::max(DP[i], DP[i - 1])의 점화식이 먼저 성립하게 된다.

 

만약, 6일부터 10일까지 상담을 진행함으로써 7일 8일 9일 10일엔 상담을 새로 받을 수 없으므로 7~10일의 값은 DP[6]의 값을 그대로 가져갈 것이다.

 

DP[i] = std::max(DP[i], DP[i - 1])이라는 점화식은 특정 날짜에 상담을 새로 배정할 수 없다면, 현재 맡고 있는 상담의 시작날짜의 값을 그대로 가져가겠다는 의미로 해석할 수도 있다. (DP배열의 초기값은 0이기 때문에)

 

그리고, 한가지 점화식을 더 만들어야 한다.

 

i일에 시작해서 n일이 걸리는 상담을 맡았다고 해보자.

그렇다면, 해당 상담의 종료 일자는 (i + n - 1)일이 될 것이다.

((i + n - 1)일까지 상담이므로, (i + n - 1)의 상담을 맡을 수는 없음)

 

해당 상담의 상담료가 p원이라고 한다면, (i + n - 1)에는 p원의 이익이 추가될 것이다.

DP[i + n - 1] = DP[i  - 1] + p; 라고 할 수 있다.

(i일 전까지 얻었던 총 이익 + i일에 시작하는 상담의 상담료)

 

하지만, 다른 날짜에서 (i + n - 1)과 동일한 날에 종료하는 상담을 계산함으로써 DP[i + n - 1]이 이미 0이 아닌 값으로 갱신되어 있을 수도 있다.

 

예를 들면, 3일에 시작해서 2일이 걸리는 상담도 5일에 끝이나며 1일에 시작해서 5일이 걸리는 상담도 5일에 끝이난다.

이런 경우엔 두 상담의 이익을 비교해서 더 큰 쪽으로 값을 갱신해주어야 한다.

 

그러므로 DP[i + n - 1] = std::max(DP[i + n - 1], DP[i - 1] + p); 라는 점화식을 세울 수 있다.

 

1일차부터 퇴사하는 날까지 모든 날에 대해 위의 두개의 점화식으로 DP배열을 갱신하게 되면, DP배열의 마지막 원소가 정답이 된다.

 

코드 풀이

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

std::vector<std::pair<int, int>> Task(Days + 1);
for (int i = 1; i <= Days; i++)
{
    std::cin >> Task[i].first;
    std::cin >> Task[i].second;
}

먼저, 모든 입력을 받아서 저장해주었다.

std::vector<int> DP(Days + 1);
for (int i = 1; i <= Days; i++)
{
    int EndDay = Task[i].first + i - 1;

    if (EndDay <= Days)
    {
        DP[EndDay] = std::max(DP[EndDay], DP[i - 1] + Task[i].second);
    }

    DP[i] = std::max(DP[i], DP[i - 1]);
}

다음은 위에서 말했던 것과 같이 DP배열을 갱신해주었다.

 

std::cout << DP[Days];

return 0;

마지막으로 출력해주면 된다.

 

 

DP문제는 코드가 길거나 구현하기 까다로운 경우는 많지 않다.

대부분 아이디어를 구상하는 것이 어렵기 때문에, 문제를 자주 풀어보고 생각해보는 것이 중요한 것 같다.

 

코드 전문

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

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

int main()
{
    Init();
    
    int Days = 0;
    std::cin >> Days;

    std::vector<std::pair<int, int>> Task(Days + 1);
    for (int i = 1; i <= Days; i++)
    {
        std::cin >> Task[i].first;
        std::cin >> Task[i].second;
    }
    
    std::vector<int> DP(Days + 1);
    for (int i = 1; i <= Days; i++)
    {
        int EndDay = Task[i].first + i - 1;

        if (EndDay <= Days)
        {
            DP[EndDay] = std::max(DP[EndDay], DP[i - 1] + Task[i].second);
        }

        DP[i] = std::max(DP[i], DP[i - 1]);
    }

    std::cout << DP[Days];

    return 0;
}

 

'코딩테스트 문제 풀이 (C++)' 카테고리의 다른 글

백준 2293 - 동전 1 (C++)  (2) 2024.05.26
백준 1106 - 호텔 (C++)  (0) 2024.05.25
백준 2011 - 암호코드 (C++)  (0) 2024.05.25
백준 2615 - 오목 (C++)  (0) 2024.05.22
백준 2436 - 공약수 (C++)  (0) 2024.05.21

오브젝트에 대한 렌더링을 모두 마친 이후에, 전체 화면을 대상으로 추가적인 후처리를 적용하는 것을 포스트 프로세스라고 한다. 포스트 프로세스를 프로젝트에 구현해볼 생각이다.

 

먼저, 렌더링 구조를 살짝 바꿔주었다.

기존에는 오브젝트를 백버퍼에 바로 그리는 형식이었지만, 현재는 렌더타겟을 추가로 만들고 해당 렌더타겟에 오브젝트를 모두 그린 뒤에, 후처리를 적용하여 백버퍼에 복사하여 그리는 구조로 바꿔주었다.

BOOL EngineBase::CreateDoubleBuffer()
{
    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;

    D3D11_TEXTURE2D_DESC txtDesc;
    ZeroMemory(&txtDesc, sizeof(txtDesc));
    txtDesc.Width = WindowWidth;
    txtDesc.Height = WindowHeight;
    txtDesc.MipLevels = txtDesc.ArraySize = 1;
    txtDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // 이미지 처리용도
    txtDesc.SampleDesc.Count = 1;
    txtDesc.Usage = D3D11_USAGE_DEFAULT;
    txtDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET | D3D11_BIND_UNORDERED_ACCESS;
    txtDesc.MiscFlags = 0;
    txtDesc.CPUAccessFlags = 0;
    
    D3D11_RENDER_TARGET_VIEW_DESC viewDesc;
    viewDesc.Format = txtDesc.Format;
    viewDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
    viewDesc.Texture2D.MipSlice = 0;

    HRESULT Result;
    Result = Device->CreateTexture2D(&txtDesc, NULL, Texture.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateTexture2D() failed" << std::endl;
        return FALSE;
    }

    Result = Device->CreateRenderTargetView(Texture.Get(), &viewDesc, DoubleBufferRTV.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateRenderTargetView() failed" << std::endl;
        return FALSE;
    }

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

    return TRUE;
}

 

이렇게, SRV와 RTV를 모두 생성해주었고, 이 doublebuffer에 1차적인 렌더링이 모두 진행될 것이다.

다음은 그렇게 그려진 렌더링 장면에 포스트 프로세스를 적용하기 위해, 화면 크기만한 사각형 메쉬를 만들어주었다.

 

#include "ScreenRenderer.h"

ScreenRenderer::ScreenRenderer()
{
}

ScreenRenderer::~ScreenRenderer()
{
}

void ScreenRenderer::Init()
{
    Renderer::Init();

    SetModelToSquare(1.0f);
    SetTransform();
}

void ScreenRenderer::Update(float _DeltaTime)
{

}

void ScreenRenderer::SetTransform()
{
    TransFormData.WorldMatrix = DirectX::SimpleMath::Matrix::CreateScale(1.0f) * DirectX::SimpleMath::Matrix::CreateRotationY(0.0f) *
        DirectX::SimpleMath::Matrix::CreateTranslation(DirectX::SimpleMath::Vector3(0.0f, 0.0f, 0.0f));

    TransFormData.WorldMatrix = TransFormData.WorldMatrix.Transpose();

    TransFormData.ViewMAtrix = EngineBase::GetInstance().ViewMat;
    TransFormData.ViewMAtrix = TransFormData.ViewMAtrix.Transpose();

    TransFormData.ProjMatrix =
        DirectX::XMMatrixOrthographicOffCenterLH(-1.0f, 1.0f, -1.0f, 1.0f,
            0.01f, 100.0f);

    TransFormData.ProjMatrix = TransFormData.ProjMatrix.Transpose();

    TransFormData.InvTranspose = TransFormData.WorldMatrix;
    TransFormData.InvTranspose.Translation({ 0.0f, 0.0f, 0.0f });
    TransFormData.InvTranspose = TransFormData.InvTranspose.Transpose().Invert();
}

 

다른 렌더러와 다를거 없어보이지만, 위의 렌더러는 직교투영을 적용하였다. 화면의 범위를 매쉬 크기에 맞춰주었기 떄문에 메쉬의 표면에 색을 입히면 화면 전체에 입혀지게 된다.

 

이 매쉬의 텍스쳐로 doublebuffer의 SRV를 사용할 것이다.

 

이 렌더러를 이용하여 포스트 프로세스를 적용할 것이기 때문에 포스트 프로세스 클래스도 만들어주었다.

 

#pragma once
#include "BaseHeader.h"

class PostProcess
{

public:

    PostProcess();
    ~PostProcess();

    PostProcess(const PostProcess& _Other) = delete;
    PostProcess(PostProcess&& _Other) noexcept = delete;
    PostProcess& operator=(const PostProcess& _Other) = delete;
    PostProcess& operator=(PostProcess&& _Other) noexcept = delete;

    void SetTexture(Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> _SRV);

    virtual void Init();
    virtual void Render(float _DeltaTime);

protected:
    std::shared_ptr<class ScreenRenderer> PostProcessRenderer;

private:
};

 

각 포스트 프로세스는 위에서 만든 ScreenRenderer를 반드시 보유하도록 하였다.

이를 상속받아 여러 종류의 포스트 프로세스를 생성할 것이다.

 

void EngineBase::Render(float _DeltaTime)
{
    float clearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
    Context->ClearRenderTargetView(DoubleBufferRTV.Get(), clearColor);
    Context->ClearDepthStencilView(DepthStencilView.Get(),
        D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    Context->OMSetRenderTargets(1, DoubleBufferRTV.GetAddressOf(), DepthStencilView.Get());
    Context->OMSetDepthStencilState(DepthStencilState.Get(), 0);
    
    if (isWireFrame == false)
    {
        Context->RSSetState(SolidRasterizerState.Get());
    }
    else
    {
        Context->RSSetState(WireRasterizerState.Get());
    }

    for (std::shared_ptr<Renderer> Renderer : Renderers)
    {
        Renderer->Render(_DeltaTime);
    }

    Context->ClearRenderTargetView(BackBufferRTV.Get(), clearColor);
    Context->OMSetRenderTargets(1, BackBufferRTV.GetAddressOf(), DepthStencilView.Get());

    for (std::shared_ptr<PostProcess> PostProcess : PostProcesses)
    {
        PostProcess->SetTexture(DoubleBufferSRV);
        PostProcess->Render(_DeltaTime);
    }
}

 

엔진의 렌더링 마지막 부분에 PostProcess의 Render를 차례대로 호출해주는 것을 몰 수 있다.

포스트 프로세스 클래스는 생성할 때마다 Engine의 자료구조에 삽입되도록 하였다.

 

(현재는 포스트 프로세스를 적용하지 않으면 모니터에 렌더링이 안되는 괴상한 구조이다. 일단 기능을 구현하느라 이런 구조가 되었는데, 블룸을 적용한 뒤에 엔진 구조를 수정할 예정이라 일단은 그대로 두었다.)

 

BloomShader를 한 번 적용해볼것이다.

먼저, BloomShader는 세가지 단계로 이루어진다.

 

1. 밝은 부분을 추출한다.

2. 화면에 블러처리를 한다.

3. 기존의 화면에 추출된 밝은 색상을 더해준다.

 

그러므로 먼저 블러처리에 대한 포스트 프로세스가 잘 적용되는지부터 테스트해볼것이다.

#pragma once
#include "PostProcess.h"

struct EBloomData
{
	int Width = 1600;
	int Height = 900;
	int Padding1 = 0;
	int Padding2 = 0;
};

class BloomPostProcess : public PostProcess
{

public:

	BloomPostProcess();
	~BloomPostProcess();

	BloomPostProcess(const BloomPostProcess& _Other) = delete;
	BloomPostProcess(BloomPostProcess&& _Other) noexcept = delete;
	BloomPostProcess& operator=(const BloomPostProcess& _Other) = delete;
	BloomPostProcess& operator=(BloomPostProcess&& _Other) noexcept = delete;

	virtual void Init() override;
	virtual void Render(float _Deltatime) override;

protected:
	
private:
	EBloomData BloomData;
};

 

포스트프로세스 클래스를 상속받은 BloomPostProcess 클래스를 위와 같이 만들어주었따.

#include "BloomPostProcess.h"
#include "ScreenRenderer.h"

BloomPostProcess::BloomPostProcess()
{
}

BloomPostProcess::~BloomPostProcess()
{
}

void BloomPostProcess::Init()
{
	PostProcessRenderer = std::make_shared<ScreenRenderer>();
	PostProcessRenderer->Init();

	PostProcessRenderer->CreateConstantBuffer(EShaderType::PSShader, L"BloomPixelShader.hlsl", BloomData);

	PostProcessRenderer->SetVSShader(L"BloomVertexShader.hlsl");
	PostProcessRenderer->SetPSShader(L"BloomPixelShader.hlsl");

	PostProcessRenderer->SetSampler("LINEARWRAP");
}

void BloomPostProcess::Render(float _DeltaTime)
{
	PostProcessRenderer->Render(_DeltaTime);

	ID3D11ShaderResourceView* SRV = NULL;
	EngineBase::GetInstance().GetContext()->PSSetShaderResources(0, 1, &SRV);
}

 

초기화는 위와 같다. 렌더러를 먼저 만들어준뒤, 상수버퍼를 연결해주고 쉐이더 세팅을 해주었다.

그 이후, Render함수에선 렌더링을 진행한 뒤, SRV를 비워주도록 하였다.

 

#include "LightHeader.hlsli"

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

cbuffer EBloomData : register(b0)
{
    int Width;
    int Height;
    int Padding1;
    int Padding2;
};

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

static float Gau[5][5] =
{
    { 1, 4, 6, 4, 1 },
    { 4, 16, 24, 16, 4 },
    { 6, 24, 36, 24, 6 },
    { 4, 16, 24, 16, 4 },
    { 1, 4, 6, 4, 1 }
};

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = (float4)0.0f;
    
    float WidthRatio = 1.0f / (float) Width;
    float HeightRatio = 1.0f / (float) Height;
    
    float2 StartTexCoord = float2(_Input.TexCoord.x - WidthRatio * 2, _Input.TexCoord.y - HeightRatio * 2);
    
    for (int i = 0; i < 5; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            StartTexCoord.y += HeightRatio;
            Color += DiffuseTexture.Sample(Sampler, StartTexCoord.xy) * Gau[j][i];
        }
        
        StartTexCoord.x += WidthRatio;
        StartTexCoord.y = _Input.TexCoord.y;
    }

    Color /= 256.0f;
    
    return Color;
}

 

위는 블러를 적용하는 픽셀쉐이더 코드이다.

전형적인 가우시안 블러 코드를 그대로 구현하였다.

 

좌측이 기본이고 우측이 블러를 적용한 상태이다.

다행히 잘 작동되는 듯 하다.

다음엔 이걸 토대로 더 많은 렌더타겟을 사용하여 블룸효과를 완전히 구현할 것이다.

그 다음엔 엔진 구조를 다소 개편해보고자 한다.

+ Recent posts