이번엔, 커다란 박스를 하나 만들어서 그 안쪽에 큐브맵을 매핑할 것이다.

이를 이용하여, 배경을 만들어볼 생각이다.

 

void ResourceManager::LoadTexture(const std::string& _TextureName, ETextureType _Type)
{
    if (_Type == ETextureType::Diffuse)
    {
        LoadDiffuseTexture(_TextureName);
    }
    else if (_Type == ETextureType::CubeMap)
    {
        LoadCubeMapTexture(_TextureName);
    }
}

 

먼저 텍스쳐를 로드하는 곳에서 디퓨즈텍스쳐인지 큐브맵 텍스쳐인지 분류하여 로드하도록 하였다.

이유는 로드를 하면서 SRV까지 만들어주고 있는데, 큐브맵은 세팅을 조금 다르게 해야하기 때문이다.

void ResourceManager::LoadCubeMapTexture(const std::string& _TextureName)
{
    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
    Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV;

    std::wstring Path = L"../Texture/";
    std::wstring TextureName;
    TextureName.assign(_TextureName.begin(), _TextureName.end());

    Path += TextureName;

    HRESULT Result = DirectX::CreateDDSTextureFromFileEx(
        EngineBase::GetInstance().GetDevice().Get(), Path.c_str(), 0, D3D11_USAGE_DEFAULT,
        D3D11_BIND_SHADER_RESOURCE, 0,
        D3D11_RESOURCE_MISC_TEXTURECUBE,
        DirectX::DDS_LOADER_FLAGS(false), (ID3D11Resource**)Texture.GetAddressOf(),
        SRV.GetAddressOf(), nullptr);

    if (Result != S_OK)
    {
        std::cout << "LoadCubeMapTexture failed " << std::endl;
        return;
    }

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

    LoadedTextures.insert({ _TextureName, NewTextureData });
}

 

큐브맵 텍스쳐를 로드하는 과정은 이와 같다.

큐브맵 텍스쳐를 로드하면서 DDS파일로 로드하다보니 코드가 위와 같아졌다.

 

그런데 구조가 조금 이상한 것 같다. 큐브맵 텍스쳐를 png로 사용할 수도 있고, 디퓨즈 텍스쳐를 dds로 사용할 수도 있기 때문에 조만간 텍스쳐 로드 과정을 수정할 계획이다.

 

 그리고 프로젝트 초반에 만들어두었던 박스형태의 매쉬를 사용할 것이다.

하지만, 기존의 매쉬는 상자 바깥부분에 텍스쳐를 매핑하고 있었지만, 큐브매핑을 하면서 안쪽에 텍스쳐를 매핑할 것이기 때문에 인덱스 버퍼를 뒤집어서 세팅해주었다.

 

그리고 큐브맵 전용 쉐이더도 하나 만들어주었다.

먼저, 빛과 같은 기능들은 모두 제거하고 트랜스폼만 상수버퍼로 전달해주었다.

버텍스 쉐이더는 기존과 동일한 코드로 만들었다. 완전히 동일한 코드이기 때문에 기존의 버텍스 쉐이더를 재활용해도 되지만, 나중에 큐브맵에만 적용할 무언가를 작성할 수도 있기 때문에 일단 분리하여 생성하였다.

 

픽셀 쉐이더는 아래와 같다.

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

TextureCube CubeMapTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = CubeMapTexture.Sample(Sampler, _Input.WorldPos.xyz);
    return Color;
}

 

정말 간단하다. 텍스쳐를 세팅할 때 큐브맵 텍스쳐로 설정하였다면, 샘플링하면서 알아서 큐브맵 매핑을 해준다.

 

실행해서 확인을 해보자.

 

카메라가 박스 범위를 벗어나지만 않으면 잘 작동하는 듯 하다.

이렇게 큐브맵도 세팅을 해보았다!

다음엔 환경매핑을 해볼 예정이다.

림 라이트란, 역광을 의미한다.

밝은 빛을 등지고 서있으면 물체의 테두리가 밝게 빛나는 현상을 우리는 현실에서도 자주 볼 수 있다.

 

컴퓨터 그래픽스 - Rim Light (역광) (tistory.com)

 

컴퓨터 그래픽스 - Rim Light (역광)

사진 출처 : What is Rim Lighting? - NYIP Photo Articles 위의 사진을 보면, 동물의 테두리 부분이 밝게 빛나는 것을 볼 수 있다.이처럼, 광원을 등지고 있을 때 물체의 테두리가 밝게 빛나는 것을 림라이트

yuu5666.tistory.com

 

위의 게시글에 림라이트에 관한 글을 정리해놓았다.

해당 게시글의 내용을 토대로 림라이트를 적용하였다.

 

struct ERimLight
{
    DirectX::SimpleMath::Vector3 RimColor = { 1.0f, 1.0f, 1.0f };
    float RimPower = 3.0f;
    
    float RimStrength = 3.0f;
    DirectX::SimpleMath::Vector3 Padding;
};

 

먼저, RimLight의 정보를 담은 구조체를 선언해주었다.

 

CreateConstantBuffer(EShaderType::PSShader, L"PixelTest.hlsl", RimLightData);

 

그리고 상수버퍼를 쉐이더에 연결해주었다.

 

float Rim = (1 - dot(EyeDir, _Input.WorldNormal));
Rim = pow(abs(Rim), RimLightPower);

float3 RimLight = Rim * RimLightStrength * RimLightColor;
Color.rgb += RimLight;

 

쉐이더에 블린 퐁 쉐이딩을 모두 적용한 뒤, 림라이트를 계산하여 물체의 색상에 더해주었다.

 

결과는 아래와 같다.

 

 

구현 자체가 까다로운 효과가 아니다 보니, 쉽게 적용할 수 있었다.

사진 출처 : What is Rim Lighting? - NYIP Photo Articles

위의 사진을 보면, 동물의 테두리 부분이 밝게 빛나는 것을 볼 수 있다.

이처럼, 광원을 등지고 있을 때 물체의 테두리가 밝게 빛나는 것을 림라이트 혹은 역광이라고 한다.

 

이 림라이트는 현실에선 빛을 등지고 있을 때 나타나지만, 컴퓨터 그래픽스에선 특정 물체를 강조하거나 배경과 분리하기 위해 사용되기도 한다.

 

여기서 설명할 림라이트는 광원의 위치를 고려하지 않는 림라이트이다.

기본적으로 림라이트는 외곽선을 강조하기 위해 사용된다.

그러므로 물체의 외곽을 구별하는 것이 중요하다.

 

어떻게 외곽을 구분하는가?

위의 그림을 보자.빨간색 화살표는 해당 부위의 노말벡터를 표현한 것이다.

외곽이라고 하면, 카메라가 바라보고 있는 방향으로부터 수직에 가까운 경우가 많다.

위의 그림을 보면, 표현한 빨간 화살표는 사진 평면과 평행에 가까울 것이다.

 

하지만, 카메라가 바라보고 있는 동물의 몸통 한가운데에서 뻗어나오는 노말이라고 한다면, 카메라를 향하는 벡터와 유사할 것이다.

 

즉, 물체에서 카메라를 향하는 방향과 물체의 노말의 사잇각이 90도에 가까울수록 외곽이라고 판단할 수 있으며, 0도에 가까울수록 물체의 중심쪽이라고 판단할 수 있을 것이다.

 

두 벡터의 사잇각은 내적을 통해 구할 수 있다.

 

카메라를 향하는 방향 벡터  : v

물체의 노말 벡터 : n

 

위와 같이 가정한다면, 두 벡터의 사잇각은 dot(v, n)이 된다. (v , n 은 유닛벡터라고 가정하자.)

90도에 가까울수록 외곽이라고 했으니, dot(v,n)이 0에 가까울수록 외곽선이고 림라이트는 강해질 것이다.

 

그러므로 림라이트 공식은 아래와 같이 표현할 수 있다.

RimLight = (1 - dot(v, n));

 

이 RimLight를 픽셀의 컬러에 더해주면 물체의 테두리에 림라이트를 적용할 수 있다.

하지만, 위 공식 그대로만 계산하게 되면 몇 가지 문제가 있다.

 

 

 

이는 게임에서 위 공식대로 직접 적용한 것이다.

물론 림라이트가 잘 적용된 것을 볼 수 있다. 그런데 무슨 문제가 있다는 것일까?

엄밀히 말하자면, 문제가 있다기보단 빛의 강도나 범위 등을 조절할 수가 없다는 것이다.

 

상황에 따라 림라이트를 더 강하게 넣고 싶을 수도 있고, 더 약하게 넣고 싶을 수도 있는데 말이다.

그렇기 때문에 RimLight를 계산하는 식에는 몇 가지 변수가 더 추가된다.

 

먼저 강도이다. 외곽을 얼마나 더 빛나게 할 것인가를 조절하는 Strengh이다.

RimLight = (1 - dot(v, n)) * Strength;

 

이렇게 간단하게 값을 곱해주기만 하면 된다.

 

이렇게 Strength의 값에 따라 빛의 밝기가 달라지는 것을 알 수 있다.

다만, 이 경우엔 Strength가 커질수록 림라이트가 적용되는 범위또한 넓어지고 있다.

빛을 밝게 하되, 외곽선만 강조하고 싶은 경우엔 한 가지 변수를 더 추가할 수 있다.

 

Specular Light를 강조하기 위해서, 거듭제곱을 하는 것과 동일하다.

 

RimPower라는 변수를 연결한 뒤, 해당 변수의 값만큼 거듭제곱을 진행해주면 된다.

 

RimLight = (1 - dot(v, n));

RimLight = pow(RimLight, RimPower);

RimLight = RimLight * Strength;

 

위의 결과를 보면, Strength가 동일한 상태에서 RimPower를 조절하니 빛이 적용되는 범위가 외곽으로 집중되는 것을 확인할 수 있다.

이제 외부에서 3D 모델링 파일을 로드하는 기능을 추가할 것이다.

 

일단, 리소스를 관리하는 클래스 ResourceManager 클래스를 만들어주었다.

기존엔 텍스쳐 로드를 EngineBase에서 하고있었는데, 텍스쳐를 로드하는 코드도 모두 ResourceManager로 옮겨주었다.

 

ResourceManager에선 외부에서 기능을 쉽게 사용할 수 있도록 변수와 함수를 모두 static으로 선언해주었다.

그리고, 3D 모델링 파일을 로드하는 기능을 추가하기 위해, Assimp 라이브러리를 프로젝트에 추가하였다.

void ResourceManager::Load(const std::string& _FileName, const std::string& _Path)
{
    if (LoadedMeshes.find(_FileName) != LoadedMeshes.end())
    {
        std::cout << _FileName << " is Already Loaded!" << std::endl;
        return;
    }

    Assimp::Importer Importer;

    std::string FullName = _Path + _FileName;
    const aiScene* Scene = Importer.ReadFile(FullName, aiProcess_Triangulate | aiProcess_ConvertToLeftHanded);

    DirectX::SimpleMath::Matrix Transform;

    ProcessNode(Scene->mRootNode, Scene, LoadedMeshes[_FileName], Transform);
}

 

로드 함수이다. 외부에서 파일 로드를 할 때 이 함수만 호출해주면 된다.

함수를 호출하게 되면, ResourceManager 내부에서 static으로 선언된 자료구조에 MeshData를 저장해준다.

그리고 렌더러에서 메시를 세팅할 때, 해당 자료구조 내부의 데이터를 참조하여 메시를 세팅하게 된다.

 

위의 코드를 보면 PrecessNode 함수가 있는데, 이 함수가 실질적으로 파일을 로드하는 코드이다.

 

void ResourceManager::ProcessNode(aiNode* _Node, const aiScene* _Scene, std::list<EMeshData>& _MeshList, DirectX::SimpleMath::Matrix _Transform)
{
    DirectX::SimpleMath::Matrix Mat;
    ai_real* Temp = &_Node->mTransformation.a1;
    
    float* MatTemp = &Mat._11;
    for (int i = 0; i < 16; i++)
    {
        MatTemp[i] = Temp[i];
    }

    Mat = Mat.Transpose() * _Transform;

    for (UINT i = 0; i < _Node->mNumMeshes; i++)
    {
        aiMesh* Mesh = _Scene->mMeshes[_Node->mMeshes[i]];
        ProcessMesh(Mesh, _Scene, _MeshList);

        EMeshData& NewMesh = _MeshList.back();
        for (auto& Vertex : NewMesh.Vertices)
        {
            Vertex.Position = DirectX::SimpleMath::Vector3::Transform(Vertex.Position, Mat);
        }
    }

    for (UINT i = 0; i < _Node->mNumChildren; i++)
    {
        ProcessNode(_Node->mChildren[i], _Scene, _MeshList, Mat);
    }
}

 

보면, PrecessNOde를 재귀적으로 호출하고 있다.

그 이유는, 하나의 3D 모델링 파일엔 매쉬가 1개만 있는 것이 아니라 부모 자식 관계로 여러개의 메쉬가 얽혀있다.

이를 재귀적으로 탐색하여 모든 메쉬를 로드하는 것이다.

 

중간에 있는ProcessMesh 함수가 메쉬의 정보를 읽어오는 함수이다.

 

void ResourceManager::ProcessMesh(aiMesh* _Mesh, const aiScene* _Scene, std::list<EMeshData>& _MeshList)
{
    EMeshData NewMesh;
    NewMesh.Indices;

    UINT IndicesCount = 0;
    for (UINT i = 0; i < _Mesh->mNumFaces; i++)
    {
        IndicesCount += _Mesh->mFaces[i].mNumIndices;
    }

    NewMesh.Vertices.reserve(_Mesh->mNumVertices);
    NewMesh.Indices.reserve(IndicesCount);

    for (UINT i = 0; i < _Mesh->mNumVertices; i++)
    {
        EVertex NewVertex;

        NewVertex.Position.x = _Mesh->mVertices[i].x;
        NewVertex.Position.y = _Mesh->mVertices[i].y;
        NewVertex.Position.z = _Mesh->mVertices[i].z;

        NewVertex.Normal.x = _Mesh->mNormals[i].x;
        NewVertex.Normal.y = _Mesh->mNormals[i].y;
        NewVertex.Normal.z = _Mesh->mNormals[i].z;

        NewVertex.Normal.Normalize();

        if (_Mesh->mTextureCoords[0])
        {
            NewVertex.TexCoord.x = (float)_Mesh->mTextureCoords[0][i].x;
            NewVertex.TexCoord.y = (float)_Mesh->mTextureCoords[0][i].y;
        }

        NewMesh.Vertices.push_back(NewVertex);
    }

    for (UINT i = 0; i < _Mesh->mNumFaces; i++) 
    {
        aiFace& Face = _Mesh->mFaces[i];
        
        for (UINT j = 0; j < Face.mNumIndices; j++)
        {
            NewMesh.Indices.push_back(Face.mIndices[j]);
        }
    }

    if (_Mesh->mMaterialIndex >= 0) 
    {
        aiMaterial* Material = _Scene->mMaterials[_Mesh->mMaterialIndex];

        if (Material->GetTextureCount(aiTextureType_DIFFUSE) > 0) 
        {
            aiString TexturePath;
            Material->GetTexture(aiTextureType_DIFFUSE, 0, &TexturePath);
        
            std::string TextureName = std::string(std::filesystem::path(TexturePath.C_Str()).filename().string());

            NewMesh.TextureName = TextureName;

            if (LoadedTextures.find(TextureName) == LoadedTextures.end())
            {
                LoadTexture(TextureName);
            }
        }
    }

    _MeshList.push_back(NewMesh);
}

 

버텍스, 노말, 텍스쳐좌표를 받아와서 저장하고 있으며, 해당 메쉬에 사용되는 텍스쳐의 이름도 받아주고 있다.

해당 텍스쳐가 로드되지 않은 상태라면, 로드까지 해주도록 하였다.

 

이렇게 메쉬 정보를 읽어오면, 렌더러에서 세팅을 해줄것이다.

 

SetModel("zeldaPosed001.fbx");

SetVSShader(L"VertexTest.hlsl");
SetPSShader(L"PixelTest.hlsl");
SetSampler("LINEARWRAP");

 

렌더러에서는 이렇게 모델, 쉐이더, 샘플러를 세팅해주도록 하였다.

 

void Renderer::SetModel(const std::string& _Name)
{
    const std::list<EMeshData>& MeshData = ResourceManager::GetLoadedMeshList(_Name);

    for (const EMeshData& Mesh : MeshData)
    {
        std::shared_ptr<RenderBase> NewRenderUnit = std::make_shared<RenderBase>();
        
        NewRenderUnit->SetMesh(Mesh);
        NewRenderUnit->CreateBuffer();

        RenderUnits.push_back(NewRenderUnit);
    }
}

 

모델 세팅하는 부분을 보면, 매쉬의 개수만큼 RenderUnit을 생성하여 각 유닛별로 메쉬를 1개씩 세팅해주었다.

서로 다른 매쉬이지만, 트랜스폼은 렌더러의 것을 모두 공유하기 때문에 여러개의 매쉬가 렌더러에 의해서 통제될 수 있도록 하였다.

 

void Renderer::SetVSShader(const std::wstring& _Shadername)
{
    for (std::shared_ptr<RenderBase> _RenderUnit : RenderUnits)
    {
        _RenderUnit->SetVSShader(_Shadername);
    }
}

void Renderer::SetPSShader(const std::wstring& _Shadername)
{
    for (std::shared_ptr<RenderBase> _RenderUnit : RenderUnits)
    {
        _RenderUnit->SetPSShader(_Shadername);
    }
}

void Renderer::SetSampler(const std::string& _Sampler)
{
    for (std::shared_ptr<RenderBase> _RenderUnit : RenderUnits)
    {
        _RenderUnit->SetSampler(_Sampler);
    }
}

 

렌더러에서 쉐이더, 샘플러를 세팅할 때에도 렌더유닛에 모두 세팅해주도록 하였다.

만약 특정 유닛에만 다른 쉐이더를 세팅하고 싶다면, 직접 호출하여 변경할 수도 있다.

 

잘 되는지 결과를 보도록 하자.

 

 

로드도 잘 되었고, 트랜스폼도 잘 작동한다.

이제 모델링 로드 기능을 구현하였으니, 이걸 토대로 더 많은 쉐이더 효과를 적용하며 공부해보고자 한다.

이제 3D모델을 파일로부터 읽어와서 렌더링하는 기능을 추가하려고 하였으나, 엔진에서 문제가 발견되었다

 

일단, 현재 구조는 하나의 렌더러가 하나의 매쉬를 렌더링하는 방식인데 실제 3D 모델은 대부분 여러개의 메쉬로 구성되어 있다. 각각 다른 쉐이더를 적용할 수도 있고, 서로 다른 버텍스버퍼, 인덱스 버퍼 등을 가지고 있어야 한다. 그렇기 때문에, 하나의 렌더러가 여러개의 메쉬 정보를 보유하도록 구조를 바꾸기로 하였다,.

 

struct EMeshData
{
	std::vector<struct EVertex> Vertices;
	std::vector<uint16_t> Indices;
	std::string TextureName = "";
};

 

일단, 버텍스와 인덱스 그리고 텍스쳐 이름을 하나의 구조체에 담아주었다.

파일로부터 데이터를 읽어올 때, 구조체를 사용하는 편이 아무래도 편할 것 같았다.

std::wstring VSShader = L"";
std::wstring PSShader = L"";

 

멤버변수에 세팅할 버텍스 쉐이더와 픽셀 쉐이더의 이름도 저장해주도록 하였다.

//std::list<EConstantBufferData> ConstantBuffers;

std::unordered_map<std::wstring, std::list<EConstantBufferData>> VSConstantBuffers;
std::unordered_map<std::wstring, std::list<EConstantBufferData>> PSConstantBuffers;

상수버퍼를 담고 있던 자료구조도 수정해주었다.

어떤 쉐이더에 대해 어떤 상수버퍼를 추가할 것인가를 설정하는 것이다.

 

기존에는 모든 상수버퍼를 모든 쉐이더에 세팅하도록 설정했었지만, 앞으로 다양한 쉐이더를 사용할 것에 대비하 이렇게 수정해주었다.

VertexShaderData VSData = EngineBase::GetInstance().GetVertexShaderData(VSShader);
Microsoft::WRL::ComPtr<ID3D11PixelShader> PS = EngineBase::GetInstance().GetPixelShaderData(PSShader);

 

렌더함수에서 쉐이더를 세팅하던 것을 기존엔 리터럴로 하고 있었는데, 이젠 설정된 쉐이더의 이름으로 하도록 하였다.

int Index = 0;
for (const EConstantBufferData& _Data : VSConstantBuffers[VSShader])
{
    EngineBase::GetInstance().GetContext()->VSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
    Index++;
}

Index = 0;
for (const EConstantBufferData& _Data : PSConstantBuffers[PSShader])
{
    EngineBase::GetInstance().GetContext()->PSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
    Index++;
}

 

상수버퍼를 세팅하는 함수도 이처럼 VS와 PS를 따로 세팅해주도록 하였다.

 

상수버퍼가 지금은 RenderBase에 있지만, 나중엔 렌더러에서 가지고 있도록 할 것이다.

 

바꿀 구조는 대충 이렇다.

RenderBase는 메쉬별로 렌더링을 담당하는 객체로 바꿀 것이다. Renderer클래스는 메쉬의 수만큼 RenderBase를 보유할 것이며 트랜스폼과 같은 상수버퍼는 Renderer단위로 보유할 것이다.

 

본격적으로 구조를 수정해보도록 하겠다.

#pragma once
#include "RenderBase.h"

class Renderer
{

public:

    Renderer();
    ~Renderer();

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

protected:

private:
    std::list<RenderBase> RenderUnits;
};

 

먼저 이렇게 렌더러를 만들어 주었다.

멤버변수에는 RenderBase를 저장할 자료구조를 선언해주었고, 하나하나가 렌더링을 실행하는 단위이기 때문에 RenderUnits라는 이름을 붙혀주었다.

 

RenderBase에는 RenderSetting이라는 함수를 만들어주었다.

void RenderBase::RenderSetting()
{
    UINT Stride = sizeof(EVertex);
    UINT Offset = 0;

    VertexShaderData VSData = EngineBase::GetInstance().GetVertexShaderData(VSShader);
    Microsoft::WRL::ComPtr<ID3D11PixelShader> PS = EngineBase::GetInstance().GetPixelShaderData(PSShader);

    EngineBase::GetInstance().GetContext()->IASetVertexBuffers(0, 1, VertexBuffer.GetAddressOf(), &Stride, &Offset);
    EngineBase::GetInstance().GetContext()->IASetIndexBuffer(IndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);
    EngineBase::GetInstance().GetContext()->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    EngineBase::GetInstance().GetContext()->VSSetShader(VSData.VertexShader.Get(), 0, 0);
    EngineBase::GetInstance().GetContext()->IASetInputLayout(VSData.InputLayout.Get());
    EngineBase::GetInstance().GetContext()->PSSetShader(PS.Get(), 0, 0);

    //추후 텍스쳐 여러개 세팅할 수도 있다.
    Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV = EngineBase::GetInstance().GetTextureData(MeshData.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함수에 있던 코드 일부이다.

Context에 쉐이더, 버퍼 등을 세팅하는 코드이다.

 

void RenderBase::Render(float _DeltaTime)
{
    UINT IndexCount = (UINT)MeshData.Indices.size();
    EngineBase::GetInstance().GetContext()->DrawIndexed(IndexCount, 0, 0);
}

 

RenderBase의 Render에는 이렇게 드로우콜을 보내는 코드만 남겨두었다.

 

void Renderer::Render(float _DeltaTime)
{
    for (const std::shared_ptr<RenderBase> _RenderUnit : RenderUnits)
    {
        _RenderUnit->RenderSetting();
        SetConstantBuffer(_RenderUnit->GetVSShaderName(), _RenderUnit->GetPSShaderName());
        _RenderUnit->Render(_DeltaTime);
    }
}

void Renderer::SetConstantBuffer(const std::wstring& _VSShaderName, const std::wstring& _PSShaderName)
{
    int Index = 0;
    for (const EConstantBufferData& _Data : VSConstantBuffers[_VSShaderName])
    {
        EngineBase::GetInstance().GetContext()->VSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
        Index++;
    }

    Index = 0;
    for (const EConstantBufferData& _Data : PSConstantBuffers[_PSShaderName])
    {
        EngineBase::GetInstance().GetContext()->PSSetConstantBuffers(Index, 1, _Data.ConstantBuffer.GetAddressOf());
        Index++;
    }
}

 

Renderer의 Render함수에선, 먼저 RenderUnit의 RenderSetting을 호출해준 뒤,Renderer가 가지고 있는 상수버퍼를 세팅해주었고, 이후 RenderUnit의 Render함수를 호출하여 드로우콜을 보내도록 하였다.

 

Update코드도 Renderer로 옮겨주었고, Init함수도 옮겨주었다. 그 외에 Name이나 Material같은 변수들도 모두 Renderer로 옮겨주었다.

 

std::list<std::shared_ptr<class Renderer>> Renderers;

 

EngineBase에서도 RenderBase가 아니라 Renderer단위로 렌더링하도록 자료구조를 수정해주었다.

 

기존의 BoxRenderer가 Renderer를 상속받도록 하였고, 임시로 RenderUnit을 하나 추가하여 렌더링을 테스트해보았다.

 

 

 

일단은 실행이 잘된다.

아마 놓친 부분이 꽤 있을 것 같은데, 3D 모델 로딩을 구현하고 테스트하면서 천천히 찾아가면서 고쳐가면 될 듯 하다.

 

 

A년도를 카잉 달력으로 표현하였을 때, <x, y>가 된다고 하면, <x, y>가 주어졌을 때 A가 몇인지 구하는 문제이다.

 

카잉 달력은 <x, y>의 형식으로 표현이 되며, x와 y에는 최대값이 정해져있다. 현실에서도 12월이 지나면 1월이 되듯이, x와 y도 일정 숫자(M, N)를 지나면 1로 돌아간다.

 

예를 들어, M이 10, N이 11이라고 해보자.

 

1년도 -> <1, 1> 

2년도 -> <2, 2>

.

.

.

10년도 -> <10, 10> 이 될 것이다.

 

다음 11년도는 <11, 11>이 될 것 같지만, x(11)가 M(10)을 초과하였으므로, 1로 돌아간다.

그러므로 11년도는 <1, 11> 이 된다.

 

동일한 규칙으로 12년도는 <2, 1>이 된다.

 

이 규칙을 역산하여, <x, y>가 주어졌을 때, 현재 몇년도인지 구하면 되는 문제이다.

 

문제 풀이

 

간단하다. 현재 연도를 A라고 했을 때, A % M = x, A % N = y 가 될 것이다.

(주의할 점은 x와 y의 범위가 0이 아닌 1부터 시작하기 때문에 A % M 이 0이라면, x는 M이고, A % N 이 0이라면, y는 N이다.)

 

이렇게 구한 x, y가 입력으로 주어진 x, y와 동일해지는 A를 구하면 된다.

 

하지만, 이 것을 구하기 위해 A를 무작정 1부터 탐색하게 되면 시간초과가 발생하게 된다.

왜냐하면, M,N의 최대값은 40000이기 때문에, 최악의 경우엔 1600000000 가량의 반복문을 실행해야 하기 때문이다.

 

이를 막기 위해, 본인은 2가지 방법을 사용하였다.

 

1. 반복문의 최대 횟수를 M과 N의 최소공배수로 한다.

 

이 문제에선 x,y가 올바른 년도가 아닌 경우도 존재한다. 이 경우를 바로 캐치하여 반복문을 종료하지 못하면 무한루프가 발생한다. 그러므로, 반복문은 M과 N의 최소공배수만큼만 돌도록 하였고, 마지막까지 조건을 만족하는 A를 발견하지 못하면, -1을 출력하도록 하였다.

 

M와 N의 최소공배수로 한 이유는, x와 y가 M과 N의 나머지로 정해지기 때문이다. <x, y>가 <M, N>이 되는 년도가 마지막 년도이다. 해당하는 카잉 달력의 연도를 A라고 한다면, A % M = 0이고, A % N 도 0일 것이다. 이를 만족하는 숫자는 M과 N의 최소공배수이다.

 

2. A를 1씩 증가하며 탐색하지 않고, M씩 증가시키며 탐색한다.

 

기본적으로, 문제의 조건에 맞는 년도를 찾기 위한 조건은 A % M == x  , A % N == y 두 조건문을 만족하는 것이다.

(물론 A % M이 0일 땐 M으로 바꿔주어야 하고, A  % N 이 0일 땐 N으로 바꿔주어야 한다.)

 

두 조건을 모두 만족하는 경우를 찾는 것이기 때문에, 한가지 경우를 무조건 만족하는 숫자를 기준으로 탐색할 수 있다면 더욱 빠를 것이다.

 

최초에 A를 입력받은 x로 초기화해주자.

그 이후, A에 1씩 더해가며 탐색하는 것이 아니라 A에 M씩 더해가며 탐색하게 되면, A % M 이 x인 숫자를 대상으로만 탐색하게 된다. (M이 아닌 숫자를 더하게 되면 어차피 A % M == x 를 만족하지 않으므로 스킵해도 된다.)

 

M이 40000인 경우, 한 번에 4만씩 넘어가며 탐색을 하기 때문에 최악의 경우에도 4만 번만 탐색하면 된다.

 

이 규칙을 토대로 반복문을 돌며 답을 탐색하면 된다.

 

풀이 코드

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

std::vector<int> Answer(NumCase);

 

먼저, 케이스의 수를 입력받고 답을 저장할 Answer 벡터를 선언해주자.

int M = 0;
int N = 0;
int X = 0;
int Y = 0;

std::cin >> M >> N >> X >> Y;

int MaxYears = GetLCM(M, N);

int CurYears = X;

while (CurYears <= MaxYears)
{
    int RemainX = CurYears % M;
    int RemainY = CurYears % N;

    if (RemainX == 0)
    {
        RemainX = M;
    }

    if (RemainY == 0)
    {
        RemainY = N;
    }

    if (RemainX == X && RemainY == Y)
    {
        break;
    }

    CurYears += M;
}

if (CurYears <= MaxYears)
{
    Answer[i] = CurYears;
}
else
{
    Answer[i] = -1;
}

 

위 코드는 각 케이스에 대한 반복문 내부 코드이다.

 

먼저, M, N, X, Y를 입력받아 준다.

이후 M과 N의 최소공배수를 구해, 반복문의 종료 조건을 설정해준다.

 

CurYears는 최초에 X로 초기화해주고, M씩 더해가며 탐색할 것이다.

 

내부에선 CurYear를 M과 N으로 나눈 나머지를 구해주고 있다. 그리고 만약 나머지가 0이라면, 해당 값을 M과 N으로 대체해주고 있다.

 

나머지가 X, Y와 같다면 반복문을 종료하고 Answer에 답을 삽입해주었다.

 

만약 반복문을 나간 이후, CurYears가 MaxYears보다 크다면, 답을 찾지 못하고 반복문을 종료한 것이기 때문에 이 경우엔 -1을 삽입해주었다.

 

for (int i = 0; i < Answer.size(); i++)
{
    std::cout << Answer[i] << "\n";
}

 

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

 

 

코드 전문

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

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

int GetGCD(int _A, int _B)
{
    int Remain = 0;

    while (_B > 0)
    {
        Remain = _A % _B;
        _A = _B;
        _B = Remain;
    }

    return _A;
}

int GetLCM(int _A, int _B)
{
    int LCM = _A * _B / GetGCD(_A, _B);

    return LCM;
}

int main()
{
    Init();

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

    std::vector<int> Answer(NumCase);

    for (int i = 0; i < NumCase; i++)
    {
        int M = 0;
        int N = 0;
        int X = 0;
        int Y = 0;

        std::cin >> M >> N >> X >> Y;

        int MaxYears = GetLCM(M, N);

        int CurYears = X;

        while (CurYears <= MaxYears)
        {
            int RemainX = CurYears % M;
            int RemainY = CurYears % N;

            if (RemainX == 0)
            {
                RemainX = M;
            }

            if (RemainY == 0)
            {
                RemainY = N;
            }

            if (RemainX == X && RemainY == Y)
            {
                break;
            }

            CurYears += M;
        }

        if (CurYears <= MaxYears)
        {
            Answer[i] = CurYears;
        }
        else
        {
            Answer[i] = -1;
        }
    }

    for (int i = 0; i < Answer.size(); i++)
    {
        std::cout << Answer[i] << "\n";
    }

    return 0;
}

조명에 디테일을 더 넣기 위해 렌더러별로 Material을 보유하게 하였고 해당 Material을 상수버퍼로 전달하여 쉐이더에서 사용하도록 하였다.

struct Material
{
	DirectX::SimpleMath::Vector3 Ambient = { 0.1f, 0.1f, 0.1f };
	float Shininess = 50.0f;

	DirectX::SimpleMath::Vector3 Diffuse = { 0.5f, 0.5f, 0.5f };
	float Padding1;

	DirectX::SimpleMath::Vector3 Specular = { 1.0f, 1.0f, 1.0f };
	float Padding2;
};

 

Material구조체는 위와 같다.

오브젝트 별로 Ambient, Diffuse, Specular를 보유하고 있고, 이는 계산된 빛의 최종 색상에 영향을 주는 변수들이다.

 

상자의 왼쪽에는 빨간색 포인트라이트를 비추고, 위쪽엔 파란색 스팟라이트를 비추고, 정면엔 하얀색 디렉셔널 라이트를 비출 것이다.

 

{
    float3 EyeDir = EyeWorld - _Input.WorldPos;

    float3 LightStrength = Lights[0].Strength * CalDirectionalLight(Lights[0], _Input.WorldNormal);
    LightSum += BlinnPhong(Lights[0], MaterialData, LightStrength, _Input.WorldNormal, EyeDir);
}

{
    float3 EyeDir = EyeWorld - _Input.WorldPos;

    float3 LightStrength = Lights[1].Strength * CalPointLight(Lights[1], _Input.WorldPos, _Input.WorldNormal);
    LightSum += BlinnPhong(Lights[1], MaterialData, LightStrength, _Input.WorldNormal, EyeDir);
}

{
    float3 EyeDir = EyeWorld - _Input.WorldPos;

    float3 LightStrength = Lights[2].Strength * CalSpotLight(Lights[2], _Input.WorldPos, _Input.WorldNormal);
    LightSum += BlinnPhong(Lights[2], MaterialData, LightStrength, _Input.WorldNormal, EyeDir);
}

 

픽셀 쉐이더에선 이렇게 3개의 빛을 계산하도록 하였다.

 

WorldLight.Lights[0].Direction = { 0.0f, 0.0f, 1.0f };

WorldLight.Lights[1].Position = { -2.0f, 0.0f, 1.0f };
WorldLight.Lights[1].Direction = { 0.0f, 1.0f, 0.0f };
WorldLight.Lights[1].Strength = { 1.0f, 0.0f, 0.0f };
WorldLight.Lights[1].FallOffStart = 1.0f;
WorldLight.Lights[1].FallOffEnd = 5.0f;

WorldLight.Lights[2].Position = { 0.0f, 2.0f, 1.0f };
WorldLight.Lights[2].Direction = { 0.0f, -1.0f, 0.0f };
WorldLight.Lights[2].Strength = { 0.0f, 1.0f, 0.0f };
WorldLight.Lights[2].FallOffStart = 1.0f;
WorldLight.Lights[2].FallOffEnd = 2.0f;
WorldLight.Lights[2].SpotPower = 10.0f;

 

C++ 코드에선 빛을 이렇게 세팅해주었다.

 

float3 BlinnPhong(Light _Light, Material _Mat, float3 _LStrength, float3 _Normal, float3 _EyeDir)
{
    float3 LightStrength = _LStrength;
    float3 Specular = _Mat.Specular * CalSpecular_BlinnPhong(_Light, _Mat, _Normal, _EyeDir);
    
    float3 ReturnValue = _Mat.Ambient + (_Mat.Diffuse + Specular) * LightStrength;
    
    return ReturnValue;
}

 

BlinnPhong함수 내부 코드이다.

외부에서 DiffuseLight를 구한 뒤 해당 값을 _LStrength로 보내주면 BlinnPhong에서 최종 빛을 결정하여 반환해준다.

float3 CalSpecular_BlinnPhong(Light _Light, Material _Mat, float3 _EyeDir, float3 _Normal)
{
    float3 LightVec = normalize(-_Light.Direction);
    float3 EyeDir = normalize(EyeDir);
    
    float3 HalfWay = normalize(_EyeDir + LightVec);
    float3 Normal = normalize(_Normal);
    
    float HalfDotN = dot(HalfWay, Normal);
    
    float3 Specular = max(HalfDotN, 0.0f);
    
    Specular = pow(Specular, _Mat.Shininess);
    Specular *= _Mat.Specular;
    
    return Specular;
}

 

위 코드는 Specular을 계산해주는 함수이다. BlinnPhong 공식을 사용하였다.

EyeVector와 Light벡터의 중간벡터를 구한 뒤, 해당 벡터를 Normal과 내적하여 Specular를 구하였다.

 

이제 적용된 결과물을 확인해보자.

먼저 정면에 적용된 디렉셔널 라이트이다.

시야를 바꾸면 SPecular가 사라지는 것도 볼 수 있고, 우측이 정면보다 어두운 것도 확인할 수 있다.

(좌측의 빨간 빛은 포인트 라이트이다. 아래 영상에서 확인하자.)

 

다음은 좌측에 적용된 포인트라이트이다.

 

 

광원의 위치를 옮길수록 빛이 적용되는 위치가 변경되는 것을 확인할 수 있다.

 

다음은 스포트라이트이다.

 

 

SpotFactor를 키울수록 중심에 빛이 더 집중되고, 줄일수록 빛이 확산되는 것을 확인할 수 있다.

이로서 간단한 조명효과는 적용 끝이다.

 

n보다 큰 자연수 중에서, 2진수로 표현했을 때 1의 개수가 n과 같은 숫자들 중 가장 작은 수를 출력하면 된다.

 

문제 풀이

 

문제는 어렵지 않다.

 

n을 2진수로 변환했을 때, 1이 몇개있는지 먼저 구해준다.

 

이후, n을 1씩 증가시키면서 해당 숫자를 2진수로 변환했을 때, 1이 몇개있는지를 구해준다.

 

1의 개수가 n과 같아지는 숫자가 발견되면, 해당 숫자를 반환하면 된다.

 

2진수의 각 숫자들은 10진수를 2로 나누었을 때 나머지이다.

그러므로, 숫자를 0이 될 때까지 계속 나누면서 나머지가 1이 되는 경우가 몇 번 있는지를 구해주면 2진수에 1이 몇개 있는지 구할 수 있다.

 

풀이 코드

 

int GetOneCount(int _Num)
{
    int CurOneCount = 0;
    
    while(_Num > 0)
    {
        if(_Num % 2 == 1)
        {
            CurOneCount++;
        }
        
        _Num /= 2;
    }
    
    return CurOneCount;
}

 

특정 숫자를 2진수로 변환했을 때, 1이 몇개있는지 구하는 함수이다.

위에서 설명했듯이, 계속 2로 나누면서 나머지가 1이 되는 경우가 몇 번 나오는지를 구해주고 있다.

 

int solution(int n) 
{
    int CurOneCount = GetOneCount(n);

    int PlusCount = 1;
    int CurNum = 0;
    
    while(true)
    {
        CurNum = n + PlusCount;
        
        int NextOneCount = GetOneCount(CurNum);
        
        if(NextOneCount == CurOneCount)
        {
            break;
        }
        
        PlusCount++;
    }
    
    return CurNum;
}

 

솔루션도 간단하다.

 

먼저, 처음 주어진 숫자에 대해 1의 개수를 구해준다.

 

이후, 수를 1씩 증가시키면서 1의 개수를 계속 구해주다가 처음 주어진 수와 1의 개수가 동일한 수가 발견되면 해당 수를 반환하면 된다.

 

 

코드 전문

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

using namespace std;

int GetOneCount(int _Num)
{
    int CurOneCount = 0;
    
    while(_Num > 0)
    {
        if(_Num % 2 == 1)
        {
            CurOneCount++;
        }
        
        _Num /= 2;
    }
    
    return CurOneCount;
}

int solution(int n) 
{
    int CurOneCount = GetOneCount(n);

    int PlusCount = 1;
    int CurNum = 0;
    
    while(true)
    {
        CurNum = n + PlusCount;
        
        int NextOneCount = GetOneCount(CurNum);
        
        if(NextOneCount == CurOneCount)
        {
            break;
        }
        
        PlusCount++;
    }
    
    return CurNum;
}

 

문제 자체를 이해하는 것은 어렵지 않았으나, 출력 조건이 다소 헷갈린 문제였다.

먼저, 0~N까지 숫자를 말한다. 하지만, 숫자가 2자리 3자리수가 된다면 그 중 1자리 수만 말하면 된다.

12라면, 한사람이 12를 외치는 것이 아니라, 첫사람이 1을 외치고 두번째 사람이 2를 외치는 것이다.

 

이렇게 계속 번갈아가면서 숫자를 말하면 된다.

다만, 방법이 10진법만 사용하는 것이 아니라, 2진법부터 16진법까지 다양하게 사용한다고 한다.

 

10진법과 동일하게 1자리씩 외치면 되지만, 2진법이라면 2진법을 기준으로 1자리씩 외쳐야 한다.

512라면, 2진법으로 1000000000 이기 때문에, 5, 1, 2 를 번갈아가며 외치는 것이 아니라 1,0,0,0,0,0,0,0,0,0을 번갈아가며 외치는 것이다. 숫자만 달라질 뿐 규칙 자체는 동일하다.

 

이렇게 숫자를 외칠 때, 튜브는 본인이 외쳐야 하는 숫자 t개를 미리 구하고자 한다.

예를 들어, 멤버 2명이서 게임을 진행하고 튜브가 첫번째 순서라고 가정해보자.

 

이 때, 7개의 숫자를 미리 구하고자 한다면?

 

10진법으로 숫자를 외치는 순서는 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 0, 1, 1, 1, 2, 1, 3, 1, 4 .... 이렇게 된다.

이 중 튜브가 말해야 하는 숫자는 0, 2, 4, 6, 8, 1, 1, 1, 1, 1, .... 이렇게 된다.

 

이 중 7개를 미리 구할 것이기 때문에, 답은 (0, 2, 4, 6, 8, 1, 1)이 된다.

 

문제 풀이

 

문제 해결은 간단하다.

 

0부터 시작해서 숫자를 N진수로 변환하여, 하나의 문자열에 쭉 더해준다.

그리고, 해당 문자열에서 튜브가 말해야 하는 순서의 문자만 뽑아서 t개만큼 다른 문자열에 저장해주면 된다.

그러면 그 문자열이 정답 문자열이 된다.

 

이 문제에서 중요한 점은 N진법 변환방법을 알고 있느냐 모르고 있느냐이다.

 

기본적으로 10진법 숫자를 N진법으로 숫자를 변환하는 방법은 나눗셈의 몫과 나머지를 이용한다.

 

10을 3진법으로 표현한다고 해보자.

 

10을 3으로 나누면 몫은 3, 나머지는 1이 된다.

다시 몫을 3으로 나누면 몫은 1, 나머지는 0이 된다.

다시 몫을 3으로 나누면 몫은 0, 나머지는 1이 된다.

 

이렇게 몫이 0이 될 때까지 계속 나누고, 그 과정에서 생기는 나머지만 모으면 그 것이 N진수가 된다.

위의 과정에선 나머지가 1, 0, 1이다.

그렇다면 10의 3진수는 101이 된다.

 

그런데 주의할 점은, 숫자가 반대라는 것이다

예를 들어 11을 N진수로 표현해보자.

 

11을 3으로 나누면 몫은 3, 나머지는 2이다.

3을 3으로 나누면 몫은 1, 나머지는 0이다.

1을 3으로 나누면 몫은 0, 나머지는 1이다.

 

그렇다면 11의 N진수는 201일까? 아니다. 102이다.

가장 처음에 구한 나머지가 N진수의 가장 끝자리수가 된다.

 

이 방법을 5진법이든 7진법이든 동일하게 적용할 수 있기 때문에, 함수만 하나 잘 만들어두면 모든 진법에 대해 대응할 수 있다.

 

코드 풀이

 

std::string FullStr;

 

먼저, FullStr이라는 문자열을 하나 선언하였다.

해당 문자열은 순서 상관 없이, 모두가 외쳐야 하는 숫자를 담은 문자열이다.

 

예를 들어 10진법의 경우, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 0, 1, 1, 1, 2, .... 이렇게 순서대로 외쳐야하기 때문에

FullStr은 0123456789101112..... 이 될 것이다..

 

int CurNum = 0;

while(true)
{
    if(FullStr.size() / m >= t)
    {
        break;
    }

    AddNumToFullStr(n, CurNum);
    CurNum++;
}

 

다음은 반복문을 돌며, 숫자를 N진수로 변환한 뒤 FullStr에 추가해줄 것이다.

이 때, FullStr의 길이가 충분하다면, 반복문을 종료해 줄 것이다.

 

튜브는 본인이 말해야 할 숫자 t개를 구하고자 한다.

 

m명이 참여하는 게임에선, m번당 1번씩 숫자를 외칠것이다. 그러므로 FullStr을 m으로 나눈 몫이 t보다 크거나 같다면 FullStr에는 튜브가 말해야 할 숫자 t개가 이미 포함되어 있기 때문에 더이상 FullStr을 늘릴 필요가 없다.

 

void AddNumToFullStr(int _n, int _CurNumber)
{
    std::string CurStr;
    
    if(_CurNumber == 0)
    {
        FullStr += '0';
        return;
    }
    
    while(_CurNumber > 0)
    {
        int Share = _CurNumber / _n;
        int Remain = _CurNumber % _n;
        
        CurStr += GetDigit(Remain);
        
        _CurNumber = Share;
    }
    
    for(int i = 0; i < CurStr.size(); i++)
    {
        FullStr += CurStr[CurStr.size() - i - 1];
    }
}

 

AddNumToFulLStr은 위와 같다.

 

몫이 이미 0이라면, FullStr에 0을 더한 뒤 함수를 종료해준다.

아니라면, 몫이 0이될 때까지 숫자를 나누며 나머지를 문자열에 더해준다.

그리고 해당 문자열을 거꾸로 뒤집어서 FullStr에 더해준 뒤 함수를 종료한다.

 

안에 있는 GetDigit함수는 11이상의 진수에서 사용되는 알파벳에 대응하기 위한 함수이다.

char GetDigit(int _Num)
{
    if(_Num < 10)
    {
        return _Num + '0';
    }
    else
    {
        int Gap = _Num - 10;
        char ReturnChar = Gap + 'A';
            
        return ReturnChar;
    }
}

 

만약 나머지가 10, 11,12 이런 값이라면 A,B,C 등의 알파벳을 사용해야 하기 때문에 이 함수를 사용하여 대응하였다.

 

std::string Answer;
Answer.reserve(t);

for(int i = 0; i < FullStr.size(); i++)
{
    if(Answer.size() == t)
    {
        break;
    }

    if(i % m == p - 1)
    {
        Answer += FullStr[i];
    }
}

return Answer;

 

그렇게 FullStr을 구해준 뒤, FullStr에서 튜브 차례에 말해야 할 숫자만 뽑아서 Answer에 저장해주었다.

Answer의 size가 t가 되었다면 반복문을 종료하고 Answer을 반환하여 문제를 해결하였다.

 

 

코드 전문

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

using namespace std;

std::string FullStr;

char GetDigit(int _Num)
{
    if(_Num < 10)
    {
        return _Num + '0';
    }
    else
    {
        int Gap = _Num - 10;
        char ReturnChar = Gap + 'A';
            
        return ReturnChar;
    }
}

void AddNumToFullStr(int _n, int _CurNumber)
{
    std::string CurStr;
    
    if(_CurNumber == 0)
    {
        FullStr += '0';
        return;
    }
    
    while(_CurNumber > 0)
    {
        int Share = _CurNumber / _n;
        int Remain = _CurNumber % _n;
        
        CurStr += GetDigit(Remain);
        
        _CurNumber = Share;
    }
    
    for(int i = 0; i < CurStr.size(); i++)
    {
        FullStr += CurStr[CurStr.size() - i - 1];
    }
}

string solution(int n, int t, int m, int p) 
{
    FullStr.reserve(10000);
    
    int CurNum = 0;
    
    while(true)
    {
        if(FullStr.size() / m >= t)
        {
            break;
        }
        
        AddNumToFullStr(n, CurNum);
        CurNum++;
    }
    
    std::string Answer;
    Answer.reserve(t);
    
    for(int i = 0; i < FullStr.size(); i++)
    {
        if(Answer.size() == t)
        {
            break;
        }
        
        if(i % m == p - 1)
        {
            Answer += FullStr[i];
        }
    }
       
    return Answer;
}

 

명령어를 순서대로 처리하면 되는 문제이다.

 

특정 자료구조에 숫자를 삽입하고, 최댓값을 삭제하거나 최솟값을 삭제하는 작업을 반복한 뒤, 마지막에 남는 원소중 최댓값과 최솟값을 반환하면 된다.

 

문제 풀이

 

처음엔 문제 이름 때문에 우선순위 큐를 사용해야 하나, 덱을 사용해야 하나 고민을 했다.

하지만, 생각해보니 그냥 set을 사용하면 되는 것이었다. 정렬도 알아서 해주고, 앞이든 뒤든 중간이든 마음껏 참조하고 삭제할 수도 있기 때문이다. 다만, 동일한 숫자가 중복으로 입력될 수도 있으므로 multiset을 사용해야 한다.


(근데 해당 문제는 그냥 set을 써도 통과하긴 하더라. 테스트 케이스가 다소 부실한듯)

 

본인은 multiset 을 사용해서 명령어를 주어진 그대로 처리하였다.

 

(I 숫자)의 경우, 값을 multiset 에 삽입하였고, (D 1)의 경우 multiset 의 마지막 원소를 제거하였고, (D -1)의 경우 multiset 의 가장 앞 원소를 제거해주었다.

 

풀이 코드

std::multiset<int> Nums;

for (int i = 0; i < operations.size(); i++)
{
    std::string& CurStr = operations[i];

    if (CurStr[0] == 'I')
    {
        std::string NumberStr = CurStr.substr(2, operations.size() - 2);
        int NumberInt = std::stoi(NumberStr);

        Nums.insert(NumberInt);
    }
    else if (Nums.size() >= 1 && CurStr == "D 1")
    {
        std::multiset<int>::iterator MaxNum = Nums.end();
        MaxNum--;

        Nums.erase(MaxNum);
    }
    else if (Nums.size() >= 1 && CurStr == "D -1")
    {
        std::multiset<int>::iterator MinNum = Nums.begin();

        Nums.erase(MinNum);
    }
}

 

 

multiset을 선언하였고, 명령어를 주어진대로 처리하고 있다.

만약, 현재 문자열의 맨 앞이 I라면, 숫자 부분만 추출하여 multiset에 삽입해주었다.

 

문자열이 D 1 이라면, multiset의 가장 마지막 원소를 제거해주었다.

문자열이 D -1이라면, multiset의 가장 앞 원소를 제거해주었다.

 

만약 multiset의 size가 0이라면, 명령어를 실행하지 않고 그냥 버린다.

 

vector<int> Answer(2);

if (Nums.size() != 0)
{
    std::multiset<int>::iterator Iter = Nums.end();
    Iter--;
    Answer[0] = *Iter;

    Iter = Nums.begin();
    Answer[1] = *Iter;
}

return Answer;

 

 

모든 명령어를 실행한 뒤, set에 남아있는 원소중 가장 앞 원소와 가장 뒤의 원소를 꺼내서 답으로 출력해주었다.

 

코드 전문

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

using namespace std;

vector<int> solution(vector<string> operations)
{
    std::multiset<int> Nums;

    for (int i = 0; i < operations.size(); i++)
    {
        std::string& CurStr = operations[i];

        if (CurStr[0] == 'I')
        {
            std::string NumberStr = CurStr.substr(2, operations.size() - 2);
            int NumberInt = std::stoi(NumberStr);

            Nums.insert(NumberInt);
        }
        else if (Nums.size() >= 1 && CurStr == "D 1")
        {
            std::multiset<int>::iterator MaxNum = Nums.end();
            MaxNum--;

            Nums.erase(MaxNum);
        }
        else if (Nums.size() >= 1 && CurStr == "D -1")
        {
            std::multiset<int>::iterator MinNum = Nums.begin();

            Nums.erase(MinNum);
        }
    }

    vector<int> Answer(2);

    if (Nums.size() != 0)
    {
        std::multiset<int>::iterator Iter = Nums.end();
        Iter--;
        Answer[0] = *Iter;

        Iter = Nums.begin();
        Answer[1] = *Iter;
    }

    return Answer;
}

+ Recent posts