DirectX11 Tutorial 39: 파티클 시스템

강좌번역/DirectX 11 2018. 12. 14. 00:44 by 빠재

원문: Tutorial 39: Particle Systems

이번 예제에서는 DirectX 11에서 HLSL과 C++를 사용하여 파티클 시스템을 만드는 방법을 다룹니다.

파티클은 대개 사각형 하나와 텍스쳐 한 장으로 구성됩니다. 그리고 매 프레임마다 그 사각형을 수백 번씩 그리게 되는데, 기본적인 물리학을 적용하여 눈이나 비, 연기, 불, 풀 등등 많은 작고 비슷한 것들을 흉내냅니다. 이번 파티클 예제에서는 다이아몬드 텍스쳐 하나를 사용하여 매 프레임마다 수백번씩 그려 휘황찬란한 다이아몬드 폭포 느낌의 이펙트를 만들 것입니다. 그에 더해 블렌딩을 적용하여 파티클들이 서로의 색상을 혼합하게끔 할 것입니다.

예제를 다 설명하고 뒤에 나오는 정리하기 파트를 꼭 읽어 주시기 바랍니다. 이 기본적인 파티클을 더욱 개선하여 더 고급지고, 더 성능이 좋고 효율적인 구현을 하는 방법을 설명할 것입니다.

Framework

평소와 같이 이번 예제의 프레임워크도 기본은 갖춰져 있습니다. 또한 파티클을 언제 방출할 지 정하는 데 TimerClass를 사용합니다. 파티클을 그리는 데 사용하는 클래스는 ParticleShaderClass입니다. 마지막으로 파티클 시스템 자체도 ParticleSystemClass로 캡슐화합니다.

ParticleSystemClass를 시작으로 예제 코드를 살펴보겠습니다.

Particlesystemclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: particlesystemclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _PARTICLESYSTEMCLASS_H_
#define _PARTICLESYSTEMCLASS_H_


//////////////
// INCLUDES //
//////////////
#include <d3d11.h>
#include <d3dx10math.h>


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "textureclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class name: ParticleSystemClass
////////////////////////////////////////////////////////////////////////////////
class ParticleSystemClass
{
private:

파티클을 정의하기 위해서는 임의의 개수의 속성들을 가질 수 있어야 합니다. 이 구현에서는 그 모든 속성들을 ParticleType 구조체에 몰아넣습니다. 더 많은 속성을 추가해도 되지만 이번에는 위치, 속도, 색상만을 다루도록 하겠습니다.

    struct ParticleType
    {
        float positionX, positionY, positionZ;
        float red, green, blue;
        float velocity;
        bool active;
    };

파티클을 그리기 위해 필요한 VertexType 구조체에는 위치, 텍스쳐 좌표 및 색상을 넣어 ParticleType 속성들과 일치하게 했습니다.

    struct VertexType
    {
        D3DXVECTOR3 position;
        D3DXVECTOR2 texture;
        D3DXVECTOR4 color;
    };

public:
    ParticleSystemClass();
    ParticleSystemClass(const ParticleSystemClass&);
    ~ParticleSystemClass();

클래스 함수로는 일반적으로 쓰이는 초기화, 정리, 프레임, 렌더링 함수들을 만듭니다. 참고로 Frame 함수에서 매 프레임마다 갱신, 정렬, 정점 버퍼 재구성 작업들을 하여 파티클이 올바로 그려질 수 있도록 합니다.

    bool Initialize(ID3D11Device*, WCHAR*);
    void Shutdown();
    bool Frame(float, ID3D11DeviceContext*);
    void Render(ID3D11DeviceContext*);

    ID3D11ShaderResourceView* GetTexture();
    int GetIndexCount();

private:
    bool LoadTexture(ID3D11Device*, WCHAR*);
    void ReleaseTexture();

    bool InitializeParticleSystem();
    void ShutdownParticleSystem();

    bool InitializeBuffers(ID3D11Device*);
    void ShutdownBuffers();

    void EmitParticles(float);
    void UpdateParticles(float);
    void KillParticles();

    bool UpdateBuffers(ID3D11DeviceContext*);

    void RenderBuffers(ID3D11DeviceContext*);

private:

다음에 나오는 private 멤버 변수들은 파티클 속성을 위해 사용됩니다. 이 변수들은 파티클 시스템이 어떻게 동작할지를 정하고 각각의 변수들이 값이 변했을 때 서로 다르게 반응하게 됩니다. 만약 이 파티클 시스템에 다른 기능을 추가하게 된다면 이곳에 변수를 추가하여 파티클을 바꿀 수 있습니다.

    float m_particleDeviationX, m_particleDeviationY, m_particleDeviationZ;
    float m_particleVelocity, m_particleVelocityVariation;
    float m_particleSize, m_particlesPerSecond;
    int m_maxParticles;

파티클들의 방출 시간을 위한 누적 시간 및 갯수 정보가 필요합니다.

    int m_currentParticleCount;
    float m_accumulatedTime;

이 예제에서는 모든 파티클에 하나의 텍스쳐를 사용할 것입니다.

    TextureClass* m_Texture;

파티클 시스템은 ParticleType 구조체들의 배열이라고 볼 수 있습니다.

    ParticleType* m_particleList;

파티클 시스템은 하나의 정점 및 인덱스 버퍼로 그립니다. 정점 버퍼는 동적으로 만들어진다는 점을 참고하시기 바랍니다.

    int m_vertexCount, m_indexCount;
    VertexType* m_vertices;
    ID3D11Buffer *m_vertexBuffer, *m_indexBuffer;
};

#endif

Particlesystemclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: particlesystemclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "particlesystemclass.h"

생성자에서 private 변수들을 null로 초기화합니다.

ParticleSystemClass::ParticleSystemClass()
{
    m_Texture = 0;
    m_particleList = 0;
    m_vertices = 0;
    m_vertexBuffer = 0;
    m_indexBuffer = 0;
}


ParticleSystemClass::ParticleSystemClass(const ParticleSystemClass& other)
{
}


ParticleSystemClass::~ParticleSystemClass()
{
}

Initialize 함수는 파티클에 사용할 텍스쳐를 불러옵니다. 텍스쳐를 불러오고 나서 파티클 시스템을 초기화합니다. 초기화가 끝나면 빈 정점 및 인덱스 버퍼를 생성합니다. 이 버퍼들은 아직 아무 파티클도 만들지 않았기 때문에 비어있는 채로 생성됩니다.

bool ParticleSystemClass::Initialize(ID3D11Device* device, WCHAR* textureFilename)
{
    bool result;


    // 파티클에 사용할 텍스쳐를 불러옵니다.
    result = LoadTexture(device, textureFilename);
    if(!result)
    {
        return false;
    }

    // 파티클 시스템을 초기화합니다.
    result = InitializeParticleSystem();
    if(!result)
    {
        return false;
    }

    // 파티클을 그리는 데 사용할 버퍼들을 생성합니다.
    result = InitializeBuffers(device);
    if(!result)
    {
        return false;
    }

    return true;
}

Shutdown 함수는 버퍼, 파티클 시스템, 파티클 텍스쳐를 해제합니다.

void ParticleSystemClass::Shutdown()
{
    // 버퍼를 해제합니다.
    ShutdownBuffers();

    // 파티클 시스템을 해제합니다.
    ShutdownParticleSystem();

    // 파티클에 사용한 텍스쳐를 해제합니다.
    ReleaseTexture();

    return;
}

Frame 함수는 파티클 시스템의 동작에서 가장 중요한 일들이 일어나는 곳입니다. 매 프레임마다 우선 렌더링 시간이 끝나 없애야 할 파티클이 있는지 확인합니다. 그리고 나서 새로운 파티클을 방출하고 지금까지 방출되어 있는 모든 파티클들을 갱신합니다. 모든 파티클들을 갱신하고 나서 각 파티클들의 위치를 정점 버퍼에 반영해야 합니다. 정점 버퍼는 동적으로 만들었기 때문에 어렵지 않게 할 수 있습니다.

bool ParticleSystemClass::Frame(float frameTime, ID3D11DeviceContext* deviceContext)
{
    bool result;


    // 오래된 파티클들을 해제합니다.
    KillParticles();

    // 새 파티클을 방출합니다.
    EmitParticles(frameTime);
    
    // 파티클들의 위치를 갱신합니다.
    UpdateParticles(frameTime);

    // 동적 정점 버퍼에 각 파티클의 갱신된 위치를 반영합니다.
    result = UpdateBuffers(deviceContext);
    if(!result)
    {
        return false;
    }

    return true;
}

Render 함수는 private 함수인 RenderBuffers를 호출하여 파티클들을 그립니다.

void ParticleSystemClass::Render(ID3D11DeviceContext* deviceContext)
{
    // 정점 및 인덱스 버퍼를 그래픽 파이프라인에 넣어 그릴 준비를 합니다.
    RenderBuffers(deviceContext);

    return;
}

GetTexture 함수는 파티클 텍스쳐 리소스의 포인터를 반환합니다.

ID3D11ShaderResourceView* ParticleSystemClass::GetTexture()
{
    return m_Texture->GetTexture();
}

GetIndexCount 함수는 인덱스 버퍼에 있는 인덱스의 개수를 반환합니다.

int ParticleSystemClass::GetIndexCount()
{
    return m_indexCount;
}

LoadTexture 함수는 star.dds 파일을 텍스쳐 리소스로 불러들여 파티클을 그릴 때 쓰이도록 합니다.

bool ParticleSystemClass::LoadTexture(ID3D11Device* device, WCHAR* filename)
{
    bool result;


    // 텍스쳐 객체를 생성합니다.
    m_Texture = new TextureClass;
    if(!m_Texture)
    {
        return false;
    }

    // 텍스쳐 객체를 초기화합니다.
    result = m_Texture->Initialize(device, filename);
    if(!result)
    {
        return false;
    }

    return true;
}

ReleaseTexture 함수는 파티클을 그리는 데 사용했던 텍스쳐 리소스를 해제합니다.

void ParticleSystemClass::ReleaseTexture()
{
    // 텍스쳐 객체를 해제합니다.
    if(m_Texture)
    {
        m_Texture->Shutdown();
        delete m_Texture;
        m_Texture = 0;
    }

    return;
}

InitializeParticleSystem 함수는 모든 변수들을 초기화하고 파티클 시스템이 프레임 처리를 할 준비가 되어 있게 합니다.

bool ParticleSystemClass::InitializeParticleSystem()
{
    int i;

우선 파티클 속성으로 사용할 모든 요소들을 초기화합니다. 이 파티클 시스템에서는 파티클이 생겨날 위치에 대한 임의의 분산 값을 설정합니다. 그리고 나서 파티클의 크기를 정합니다. 마지막으로 매 초마다 얼마나 많은 파티클들이 생길 것인지 설정하고 한번에 방출할 파티클들의 최대 수치도 정합니다.

    // 어디에 파티클이 생겨날지 임의의 분산값을 설정합니다.
    m_particleDeviationX = 0.5f;
    m_particleDeviationY = 0.1f;
    m_particleDeviationZ = 2.0f;

    // 파티클의 속도 및 속도 편차를 정합니다.
    m_particleVelocity = 1.0f;
    m_particleVelocityVariation = 0.2f;

    // 파티클의 실제 크기입니다.
    m_particleSize = 0.2f;

    // 파티클이 초당 생겨날 갯수입니다.
    m_particlesPerSecond = 250.0f;

    // 파티클 시스템에게 허락된 파티클의 최대 개수입니다.
    m_maxParticles = 5000;

그리고 나서 최대 파티클 개수에 개반하여 나중에 사용할 파티클 배열을 생성합니다.

    // 파티클 배열을 생성합니다.
    m_particleList = new ParticleType[m_maxParticles];
    if(!m_particleList)
    {
        return false;
    }

배열에 있는 각각의 파티클을 일단 비활성화합니다.

    // 파티클 목록을 초기화합니다.
    for(i=0; i<m_maxParticles; i++)
    {
        m_particleList[i].active = false;
    }

시작하기 위해 두 카운트 변수를 0으로 초기화합니다.

    // 아무 파티클도 방출하지 않았기 때문에 현재 파티클 개수를 0으로 초기화합니다.
    m_currentParticleCount = 0;

    // 초당 방출 비율을 위해 누적 시간을 초기화합니다.
    m_accumulatedTime = 0.0f;

    return true;
}

ShutdownParticleSystem 함수는 종료되는 동안 파티클 배열을 해제합니다.

void ParticleSystemClass::ShutdownParticleSystem()
{
    // 파티클 배열을 해제합니다.
    if(m_particleList)
    {
        delete [] m_particleList;
        m_particleList = 0;
    }

    return;
}

InitializeBuffers 함수는 파티클을 그리는 데 사용할 정점 및 인덱스 버퍼를 준비합니다. 매 프레임마다 파티클들을 갱신할 것이기 때문에 정점 버퍼는 동적 버퍼로 생성되어야 합니다. 처음에는 아무 파티클도 나가 있지 않을 것이기 때문에 빈 버퍼로 생성됩니다.

bool ParticleSystemClass::InitializeBuffers(ID3D11Device* device)
{
    unsigned long* indices;
    int i;
    D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc;
    D3D11_SUBRESOURCE_DATA vertexData, indexData;
    HRESULT result;


    // 정점 배열의 최대 정점 개수입니다.
    m_vertexCount = m_maxParticles * 6;

    // 인덱스 배열의 최대 인덱스 개수입니다.
    m_indexCount = m_vertexCount;

    // 그려지는 파티클들을 위한 정점 배열을 생성합니다.
    m_vertices = new VertexType[m_vertexCount];
    if(!m_vertices)
    {
        return false;
    }

    // 인덱스 배열을 생성합니다.
    indices = new unsigned long[m_indexCount];
    if(!indices)
    {
        return false;
    }

    // 정점 배열을 0으로 초기화합니다.
    memset(m_vertices, 0, (sizeof(VertexType) * m_vertexCount));

    // 인덱스 배열을 초기화합니다.
    for(i=0; i<m_indexCount; i++)
    {
        indices[i] = i;
    }

    // 동적 정점 버퍼의 디스크립션을 설정합니다.
    vertexBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
    vertexBufferDesc.ByteWidth = sizeof(VertexType) * m_vertexCount;
    vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    vertexBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    vertexBufferDesc.MiscFlags = 0;
    vertexBufferDesc.StructureByteStride = 0;

    // 서브 리소스 구조체에 정점 데이터의 포인터를 할당합니다.
    vertexData.pSysMem = m_vertices;
    vertexData.SysMemPitch = 0;
    vertexData.SysMemSlicePitch = 0;

    // 마지막으로 정점 버퍼를 생성합니다.
    result = device->CreateBuffer(&vertexBufferDesc, &vertexData, &m_vertexBuffer);
    if(FAILED(result))
    {
        return false;
    }

    // 정적 인덱스 버퍼의 디스크립션을 설정합니다.
    indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    indexBufferDesc.ByteWidth = sizeof(unsigned long) * m_indexCount;
    indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
    indexBufferDesc.CPUAccessFlags = 0;
    indexBufferDesc.MiscFlags = 0;
    indexBufferDesc.StructureByteStride = 0;

    // 서브 리소스 구조체에 인덱스 데이터의 포인터를 할당합니다.
    indexData.pSysMem = indices;
    indexData.SysMemPitch = 0;
    indexData.SysMemSlicePitch = 0;

    // 인덱스 버퍼를 생성합니다.
    result = device->CreateBuffer(&indexBufferDesc, &indexData, &m_indexBuffer);
    if(FAILED(result))
    {
        return false;
    }

    // 더 이상 사용하지 않는 인덱스 배열을 해제합니다.
    delete [] indices;
    indices = 0;

    return true;
}

ShutdownBuffers 함수는 종료될 때 정점 및 인덱스 버퍼를 해제합니다.

void ParticleSystemClass::ShutdownBuffers()
{
    // 인덱스 버퍼를 해제합니다.
    if(m_indexBuffer)
    {
        m_indexBuffer->Release();
        m_indexBuffer = 0;
    }

    // 정점 버퍼를 해제합니다.
    if(m_vertexBuffer)
    {
        m_vertexBuffer->Release();
        m_vertexBuffer = 0;
    }

    return;
}

EmitParticles는 새로운 파티클이 방출될 때 호출됩니다. 프레임 시간과 초당 파티클 개수 변수를 참고하여 언제 파티클이 나와야 할 지 정합니다. 만약 방출되어야 한다면 새 파티클이 생성되고 속성들이 설정됩니다. 그리고 나서 파티클 배열에 Z 뎁스 순서대로 정렬되어 삽입됩니다. 이 파티클 배열은 알파블렌딩이 제대로 동작하기 위하여 깊이값으로 올바르게 정렬되어 있어야 합니다. 만약 정렬되지 않았다면 약간 어색해 보이는 결과를 얻을 것입니다.

void ParticleSystemClass::EmitParticles(float frameTime)
{
    bool emitParticle, found;
    float positionX, positionY, positionZ, velocity, red, green, blue;
    int index, i, j;


    // 프레임 시간을 더합니다.
    m_accumulatedTime += frameTime;

    // 파티클 방출 여부를 false로 합니다.
    emitParticle = false;
    
    // 새 파티클을 낼 시간인지 확인합니다.
    if(m_accumulatedTime > (1000.0f / m_particlesPerSecond))
    {
        m_accumulatedTime = 0.0f;
        emitParticle = true;
    }

    // 파티클이 나와야 한다면 프레임마다 하나씩 방출합니다.
    if((emitParticle == true) && (m_currentParticleCount < (m_maxParticles - 1)))
    {
        m_currentParticleCount++;

        // 임의로 파티클 속성을 생성합니다.
        positionX = (((float)rand()-(float)rand())/RAND_MAX) * m_particleDeviationX;
        positionY = (((float)rand()-(float)rand())/RAND_MAX) * m_particleDeviationY;
        positionZ = (((float)rand()-(float)rand())/RAND_MAX) * m_particleDeviationZ;

        velocity = m_particleVelocity + (((float)rand()-(float)rand())/RAND_MAX) * m_particleVelocityVariation;

        red   = (((float)rand()-(float)rand())/RAND_MAX) + 0.5f;
        green = (((float)rand()-(float)rand())/RAND_MAX) + 0.5f;
        blue  = (((float)rand()-(float)rand())/RAND_MAX) + 0.5f;

        // 블렌딩하기 위해서는 파티클들을 뒤에서부터 앞으로 그려야 하기 때문에 배열을 정렬합니다.
        // Z깊이값으로 정렬할 것이기 때문에 파티클이 어느 지점에 삽입되어야 할 지 찾아야 합니다.
        index = 0;
        found = false;
        while(!found)
        {
            if((m_particleList[index].active == false) || (m_particleList[index].positionZ < positionZ))
            {
                found = true;
            }
            else
            {
                index++;
            }
        }

        // 위치가 정해졌으므로 그 위치에서부터 배열을 하나씩 밀어 복사해서 새 파티클을 위한 공간을 만듭니다.
        i = m_currentParticleCount;
        j = i - 1;

        while(i != index)
        {
            m_particleList[i].positionX = m_particleList[j].positionX;
            m_particleList[i].positionY = m_particleList[j].positionY;
            m_particleList[i].positionZ = m_particleList[j].positionZ;
            m_particleList[i].red       = m_particleList[j].red;
            m_particleList[i].green     = m_particleList[j].green;
            m_particleList[i].blue      = m_particleList[j].blue;
            m_particleList[i].velocity  = m_particleList[j].velocity;
            m_particleList[i].active    = m_particleList[j].active;
            i--;
            j--;
        }

        // 올바른 깊이 순서대로 파티클 배열에 추가합니다.
        m_particleList[index].positionX = positionX;
        m_particleList[index].positionY = positionY;
        m_particleList[index].positionZ = positionZ;
        m_particleList[index].red       = red;
        m_particleList[index].green     = green;
        m_particleList[index].blue      = blue;
        m_particleList[index].velocity  = velocity;
        m_particleList[index].active    = true;
    }

    return;
}

UpdateParticles 함수에서 매 프레임마다 파티클의 속성들을 업데이트합니다. 이 예제에서는 각자의 속도에 기반하여 높이값을 갱신하여 폭포 모양의 파티클 효과를 만들어냅니다. 이 함수를 확장하여 다른 많은 파티클 효과 및 움직임들을 만들어낼 수 있습니다.

void ParticleSystemClass::UpdateParticles(float frameTime)
{
    int i;


    // Each frame we update all the particles by making them move downwards using their position, velocity, and the frame time.
    for(i=0; i<m_currentParticleCount; i++)
    {
        m_particleList[i].positionY = m_particleList[i].positionY - (m_particleList[i].velocity * frameTime * 0.001f);
    }

    return;
}

KillParticles 함수는 렌더링 주기가 끝난 파티클들을 시스템에서 제거하는 일을 합니다. 매 프레임마다 이 함수가 호출되어 제거되어야 할 파티클이 있는지 확인합니다. 이 예제에서는 파티클의 높이가 -3.0미만으로 내려갔는지만 확인하고 만약 그렇다면 배열에서 제거하고 다시 깊이 순서대로 배열을 재정렬합니다.

void ParticleSystemClass::KillParticles()
{
    int i, j;


    // 특정 높이 범위 아래로 내려간 파티클들을 전부 제거합니다.
    for(i=0; i<m_maxParticles; i++)
    {
        if((m_particleList[i].active == true) && (m_particleList[i].positionY < -3.0f))
        {
            m_particleList[i].active = false;
            m_currentParticleCount--;

            // 아직 살아 있는 모든 파티클들을 앞으로 당겨 정렬 순서는 유지하면서 사라진 파티클들은 지워지도록 합니다.
            for(j=i; j<m_maxParticles-1; j++)
            {
                m_particleList[j].positionX = m_particleList[j+1].positionX;
                m_particleList[j].positionY = m_particleList[j+1].positionY;
                m_particleList[j].positionZ = m_particleList[j+1].positionZ;
                m_particleList[j].red       = m_particleList[j+1].red;
                m_particleList[j].green     = m_particleList[j+1].green;
                m_particleList[j].blue      = m_particleList[j+1].blue;
                m_particleList[j].velocity  = m_particleList[j+1].velocity;
                m_particleList[j].active    = m_particleList[j+1].active;
            }
        }
    }

    return;
}

UpdateBuffers 함수는 매 프레임마다 호출되어 동적 파티클 시스템의 모든 갱신된 파티클들의 위치를 이용하여 동적 정점 버퍼를 재구성합니다.

bool ParticleSystemClass::UpdateBuffers(ID3D11DeviceContext* deviceContext)
{
    int index, i;
    HRESULT result;
    D3D11_MAPPED_SUBRESOURCE mappedResource;
    VertexType* verticesPtr;

    // 정점 배열을 0으로 초기화합니다.
    memset(m_vertices, 0, (sizeof(VertexType) * m_vertexCount));

    // 파티클 목록 배열을 가지고 정점 배열을 만듭니다. 각 파티클은 두 개의 삼각형으로 이루어진 사각형입니다.
    index = 0;

    for(i=0; i<m_currentParticleCount; i++)
    {
        // 왼쪽 아래
        m_vertices[index].position = D3DXVECTOR3(m_particleList[i].positionX - m_particleSize, m_particleList[i].positionY - m_particleSize, m_particleList[i].positionZ);
        m_vertices[index].texture = D3DXVECTOR2(0.0f, 1.0f);
        m_vertices[index].color = D3DXVECTOR4(m_particleList[i].red, m_particleList[i].green, m_particleList[i].blue, 1.0f);
        index++;

        // 왼쪽 위
        m_vertices[index].position = D3DXVECTOR3(m_particleList[i].positionX - m_particleSize, m_particleList[i].positionY + m_particleSize, m_particleList[i].positionZ);
        m_vertices[index].texture = D3DXVECTOR2(0.0f, 0.0f);
        m_vertices[index].color = D3DXVECTOR4(m_particleList[i].red, m_particleList[i].green, m_particleList[i].blue, 1.0f);
        index++;

        // 오른쪽 아래
        m_vertices[index].position = D3DXVECTOR3(m_particleList[i].positionX + m_particleSize, m_particleList[i].positionY - m_particleSize, m_particleList[i].positionZ);
        m_vertices[index].texture = D3DXVECTOR2(1.0f, 1.0f);
        m_vertices[index].color = D3DXVECTOR4(m_particleList[i].red, m_particleList[i].green, m_particleList[i].blue, 1.0f);
        index++;

        // 오른쪽 아래
        m_vertices[index].position = D3DXVECTOR3(m_particleList[i].positionX + m_particleSize, m_particleList[i].positionY - m_particleSize, m_particleList[i].positionZ);
        m_vertices[index].texture = D3DXVECTOR2(1.0f, 1.0f);
        m_vertices[index].color = D3DXVECTOR4(m_particleList[i].red, m_particleList[i].green, m_particleList[i].blue, 1.0f);
        index++;

        // 왼쪽 위
        m_vertices[index].position = D3DXVECTOR3(m_particleList[i].positionX - m_particleSize, m_particleList[i].positionY + m_particleSize, m_particleList[i].positionZ);
        m_vertices[index].texture = D3DXVECTOR2(0.0f, 0.0f);
        m_vertices[index].color = D3DXVECTOR4(m_particleList[i].red, m_particleList[i].green, m_particleList[i].blue, 1.0f);
        index++;

        // 오른쪽 위
        m_vertices[index].position = D3DXVECTOR3(m_particleList[i].positionX + m_particleSize, m_particleList[i].positionY + m_particleSize, m_particleList[i].positionZ);
        m_vertices[index].texture = D3DXVECTOR2(1.0f, 0.0f);
        m_vertices[index].color = D3DXVECTOR4(m_particleList[i].red, m_particleList[i].green, m_particleList[i].blue, 1.0f);
        index++;
    }
    
    // 정점 버퍼를 잠급니다.
    result = deviceContext->Map(m_vertexBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
    if(FAILED(result))
    {
        return false;
    }

    // 정점 버퍼의 데이터 포인터를 가져옵니다.
    verticesPtr = (VertexType*)mappedResource.pData;

    // 정점 버퍼에 데이터를 복사합니다.
    memcpy(verticesPtr, (void*)m_vertices, (sizeof(VertexType) * m_vertexCount));

    // 정점 버퍼의 잠금을 해제합니다.
    deviceContext->Unmap(m_vertexBuffer, 0);

    return true;
}

RenderBuffers 함수는 파티클 버퍼를 그리는 데 사용합니다. 이 함수에서 지오메트리를 파이프라인이 배치하여 셰이더가 그릴 수 있도록 합니다.

void ParticleSystemClass::RenderBuffers(ID3D11DeviceContext* deviceContext)
{
    unsigned int stride;
    unsigned int offset;


    // 정점 버퍼의 스트라이드와 오프셋입니다.
    stride = sizeof(VertexType); 
    offset = 0;
    
    // 입력 어셈블러에 정점 버퍼를 활성화하여 그려질 수 있도록 합니다.
    deviceContext->IASetVertexBuffers(0, 1, &m_vertexBuffer, &stride, &offset);

    // 임력 어셈블러에 인덱스 버퍼를 활성화아여 그려질 수 있도록 합니다.
    deviceContext->IASetIndexBuffer(m_indexBuffer, DXGI_FORMAT_R32_UINT, 0);

    // 이 정점 버퍼에서 그릴 도형의 타입을 설정합니다.
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

    return;
}

Particle.vs

particle.vs와 particle.ps HLSL 셰이더 프로그램은 파티클을 그리는 데 사용됩니다. 이 셰이더는 기본적인 텍스쳐 셰이더에 색상을 변경하는 기능이 더해져 있습니다.

////////////////////////////////////////////////////////////////////////////////
// Filename: particle.vs
////////////////////////////////////////////////////////////////////////////////


/////////////
// GLOBALS //
/////////////
cbuffer MatrixBuffer
{
    matrix worldMatrix;
    matrix viewMatrix;
    matrix projectionMatrix;
};


//////////////
// TYPEDEFS //
//////////////

파티클들은 각각의 색상이 기본 텍스쳐 색상에 더해져야 하기 때문에 아래 두 입력 타입들은 모두 color 변수를 갖고 있습니다.

struct VertexInputType
{
    float4 position : POSITION;
    float2 tex : TEXCOORD0;
    float4 color : COLOR;
};

struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
    float4 color : COLOR;
};


////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
PixelInputType ParticleVertexShader(VertexInputType input)
{
    PixelInputType output;
    

    // 올바른 행렬 연산을 위해 위치 벡터의 성분이 4개가 되게 합니다.
    input.position.w = 1.0f;

    // 정점의 위치를 각각 월드, 뷰, 투영 행렬에 대하여 계산합니다.
    output.position = mul(input.position, worldMatrix);
    output.position = mul(output.position, viewMatrix);
    output.position = mul(output.position, projectionMatrix);
    
    // 픽셀 셰이더에 전달할 텍스쳐 좌표를 저장합니다.
    output.tex = input.tex;

color변수는 픽셀 셰이더로 전달합니다.

    // 픽셀 셰이더에 전달할 파티클 색상을 저장합니다.
    output.color = input.color;

    return output;
}

Particle.ps

////////////////////////////////////////////////////////////////////////////////
// Filename: particle.ps
////////////////////////////////////////////////////////////////////////////////


/////////////
// GLOBALS //
/////////////
Texture2D shaderTexture;
SamplerState SampleType;


//////////////
// TYPEDEFS //
//////////////

픽셀 셰이더의 PixelInputType에도 color 변수가 추가됩니다.

struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
    float4 color : COLOR;
};


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
float4 ParticlePixelShader(PixelInputType input) : SV_TARGET
{
    float4 textureColor;
    float4 finalColor;


    // 샘플러를 이용하여 현제 텍스쳐 좌표 위치에서 텍스쳐의 픽셀 색상을 샘플링합니다.
    textureColor = shaderTexture.Sample(SampleType, input.tex);

텍스쳐의 색상과 입력된 파티클의 색상을 혼합하여 최종 출력 색상을 구합니다.

    // 텍스쳐의 색상과 파티클의 색상을 혼합하여 최종 색상값을 구합니다.
    finalColor = textureColor * input.color;

    return finalColor;
}

Particleshaderclass.h

ParticleShaderClassTextureShaderClass에 파티클을 위한 color 변수를 사용하도록 고친 것입니다.

////////////////////////////////////////////////////////////////////////////////
// Filename: particleshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _PARTICLESHADERCLASS_H_
#define _PARTICLESHADERCLASS_H_


//////////////
// INCLUDES //
//////////////
#include <d3d11.h>
#include <d3dx10math.h>
#include <d3dx11async.h>
#include <fstream>
using namespace std;


////////////////////////////////////////////////////////////////////////////////
// Class name: ParticleShaderClass
////////////////////////////////////////////////////////////////////////////////
class ParticleShaderClass
{
private:
    struct MatrixBufferType
    {
        D3DXMATRIX world;
        D3DXMATRIX view;
        D3DXMATRIX projection;
    };

public:
    ParticleShaderClass();
    ParticleShaderClass(const ParticleShaderClass&);
    ~ParticleShaderClass();

    bool Initialize(ID3D11Device*, HWND);
    void Shutdown();
    bool Render(ID3D11DeviceContext*, int, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, ID3D11ShaderResourceView*);

private:
    bool InitializeShader(ID3D11Device*, HWND, WCHAR*, WCHAR*);
    void ShutdownShader();
    void OutputShaderErrorMessage(ID3D10Blob*, HWND, WCHAR*);

    bool SetShaderParameters(ID3D11DeviceContext*, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, ID3D11ShaderResourceView*);
    void RenderShader(ID3D11DeviceContext*, int);

private:
    ID3D11VertexShader* m_vertexShader;
    ID3D11PixelShader* m_pixelShader;
    ID3D11InputLayout* m_layout;
    ID3D11Buffer* m_matrixBuffer;
    ID3D11SamplerState* m_sampleState;
};

#endif

Particleshaderclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: particleshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "particleshaderclass.h"


ParticleShaderClass::ParticleShaderClass()
{
    m_vertexShader = 0;
    m_pixelShader = 0;
    m_layout = 0;
    m_matrixBuffer = 0;
    m_sampleState = 0;
}


ParticleShaderClass::ParticleShaderClass(const ParticleShaderClass& other)
{
}


ParticleShaderClass::~ParticleShaderClass()
{
}


bool ParticleShaderClass::Initialize(ID3D11Device* device, HWND hwnd)
{
    bool result;

particle.vs 및 particle.ps HLSL 프로그램을 로드합니다.

    // 정점 및 픽셀 셰이더를 초기화합니다.
    result = InitializeShader(device, hwnd, L"../Engine/particle.vs", L"../Engine/particle.ps");
    if(!result)
    {
        return false;
    }

    return true;
}


void ParticleShaderClass::Shutdown()
{
    // 정점 및 픽셀 셰이더, 관련된 객체들을 해제합니다.
    ShutdownShader();

    return;
}


bool ParticleShaderClass::Render(ID3D11DeviceContext* deviceContext, int indexCount, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, 
                 D3DXMATRIX projectionMatrix, ID3D11ShaderResourceView* texture)
{
    bool result;


    // 렌더링에 사용할 셰이더 인자들을 설정합니다.
    result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix, texture);
    if(!result)
    {
        return false;
    }

    // 준비된 버퍼에 셰이더를 이용하여 그립니다.
    RenderShader(deviceContext, indexCount);

    return true;
}


bool ParticleShaderClass::InitializeShader(ID3D11Device* device, HWND hwnd, WCHAR* vsFilename, WCHAR* psFilename)
{
    HRESULT result;
    ID3D10Blob* errorMessage;
    ID3D10Blob* vertexShaderBuffer;
    ID3D10Blob* pixelShaderBuffer;
    D3D11_INPUT_ELEMENT_DESC polygonLayout[3];
    unsigned int numElements;
    D3D11_BUFFER_DESC matrixBufferDesc;
    D3D11_SAMPLER_DESC samplerDesc;


    // 이 함수에서 사용할 포인터들을 null로 초기화합니다.
    errorMessage = 0;
    vertexShaderBuffer = 0;
    pixelShaderBuffer = 0;

ParticleVetexShaderParticlePixelShader 프로그램을 불러옵니다.

    // 정점 셰이더 코드를 컴파일합니다.
    result = D3DX11CompileFromFile(vsFilename, NULL, NULL, "ParticleVertexShader", "vs_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL, 
                       &vertexShaderBuffer, &errorMessage, NULL);
    if(FAILED(result))
    {
        // If the shader failed to compile it should have writen something to the error message.
        if(errorMessage)
        {
            OutputShaderErrorMessage(errorMessage, hwnd, vsFilename);
        }
        // If there was nothing in the error message then it simply could not find the shader file itself.
        else
        {
            MessageBox(hwnd, vsFilename, L"Missing Shader File", MB_OK);
        }

        return false;
    }

    // 픽셀 셰이더 코드를 컴파일합니다.
    result = D3DX11CompileFromFile(psFilename, NULL, NULL, "ParticlePixelShader", "ps_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL, 
                       &pixelShaderBuffer, &errorMessage, NULL);
    if(FAILED(result))
    {
        // If the shader failed to compile it should have writen something to the error message.
        if(errorMessage)
        {
            OutputShaderErrorMessage(errorMessage, hwnd, psFilename);
        }
        // If there was  nothing in the error message then it simply could not find the file itself.
        else
        {
            MessageBox(hwnd, psFilename, L"Missing Shader File", MB_OK);
        }

        return false;
    }

    // 버퍼에서 정점 셰이더를 생성합니다.
    result = device->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), NULL, &m_vertexShader);
    if(FAILED(result))
    {
        return false;
    }

    // 버퍼에서 픽셀 셰이더를 생성합니다.
    result = device->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), pixelShaderBuffer->GetBufferSize(), NULL, &m_pixelShader);
    if(FAILED(result))
    {
        return false;
    }

파티클 셰이더에 사용할 세 레이아웃을 생성하는데, 세 번째는 각 파티클의 색상이 됩니다.

    // 정점 입력 레이아웃 디스크립션을 설정합니다.
    polygonLayout[0].SemanticName = "POSITION";
    polygonLayout[0].SemanticIndex = 0;
    polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;
    polygonLayout[0].InputSlot = 0;
    polygonLayout[0].AlignedByteOffset = 0;
    polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
    polygonLayout[0].InstanceDataStepRate = 0;

    polygonLayout[1].SemanticName = "TEXCOORD";
    polygonLayout[1].SemanticIndex = 0;
    polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT;
    polygonLayout[1].InputSlot = 0;
    polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
    polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
    polygonLayout[1].InstanceDataStepRate = 0;

    polygonLayout[2].SemanticName = "COLOR";
    polygonLayout[2].SemanticIndex = 0;
    polygonLayout[2].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
    polygonLayout[2].InputSlot = 0;
    polygonLayout[2].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
    polygonLayout[2].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
    polygonLayout[2].InstanceDataStepRate = 0;

    // 레이아웃의 개수를 구합니다.
    numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);

    // 정점 입력 레이아웃을 생성합니다.
    result = device->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), 
                       &m_layout);
    if(FAILED(result))
    {
        return false;
    }

    // 더 이상 사용하지 않는 정점 셰이더 및 픽셀 셰이더 버퍼를 해제합니다.
    vertexShaderBuffer->Release();
    vertexShaderBuffer = 0;

    pixelShaderBuffer->Release();
    pixelShaderBuffer = 0;

    // 정점 셰이더에서 사용하는 동적 행렬 상수 버퍼에 대한 디스크립션을 설정합니다.
    matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
    matrixBufferDesc.ByteWidth = sizeof(MatrixBufferType);
    matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    matrixBufferDesc.MiscFlags = 0;
    matrixBufferDesc.StructureByteStride = 0;

    // 이 클래스에서 정점 셰이더 상수 버퍼에 접근할 수 있도록 상수 버퍼의 포인터를 생성합니다.
    result = device->CreateBuffer(&matrixBufferDesc, NULL, &m_matrixBuffer);
    if(FAILED(result))
    {
        return false;
    }

    // 텍스쳐 샘플러 상태 디스크립션을 설정합니다.
    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.MipLODBias = 0.0f;
    samplerDesc.MaxAnisotropy = 1;
    samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
    samplerDesc.BorderColor[0] = 0;
    samplerDesc.BorderColor[1] = 0;
    samplerDesc.BorderColor[2] = 0;
    samplerDesc.BorderColor[3] = 0;
    samplerDesc.MinLOD = 0;
    samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

    // 텍스쳐 샘플러 상태를 생성합니다.
    result = device->CreateSamplerState(&samplerDesc, &m_sampleState);
    if(FAILED(result))
    {
        return false;
    }

    return true;
}


void ParticleShaderClass::ShutdownShader()
{
    // 샘플러 상태를 해제합니다.
    if(m_sampleState)
    {
        m_sampleState->Release();
        m_sampleState = 0;
    }

    // 행렬 상수 버퍼를 해제합니다.
    if(m_matrixBuffer)
    {
        m_matrixBuffer->Release();
        m_matrixBuffer = 0;
    }

    // 레이아웃을 해제합니다.
    if(m_layout)
    {
        m_layout->Release();
        m_layout = 0;
    }

    // 픽셀 셰이더를 해제합니다.
    if(m_pixelShader)
    {
        m_pixelShader->Release();
        m_pixelShader = 0;
    }

    // 정점 셰이더를 해제합니다.
    if(m_vertexShader)
    {
        m_vertexShader->Release();
        m_vertexShader = 0;
    }

    return;
}


void ParticleShaderClass::OutputShaderErrorMessage(ID3D10Blob* errorMessage, HWND hwnd, WCHAR* shaderFilename)
{
    char* compileErrors;
    unsigned long bufferSize, i;
    ofstream fout;


    // Get a pointer to the error message text buffer.
    compileErrors = (char*)(errorMessage->GetBufferPointer());

    // Get the length of the message.
    bufferSize = errorMessage->GetBufferSize();

    // Open a file to write the error message to.
    fout.open("shader-error.txt");

    // Write out the error message.
    for(i=0; i<bufferSize; i++)
    {
        fout << compileErrors[i];
    }

    // Close the file.
    fout.close();

    // Release the error message.
    errorMessage->Release();
    errorMessage = 0;

    // Pop a message up on the screen to notify the user to check the text file for compile errors.
    MessageBox(hwnd, L"Error compiling shader.  Check shader-error.txt for message.", shaderFilename, MB_OK);

    return;
}


bool ParticleShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, 
                          D3DXMATRIX projectionMatrix, ID3D11ShaderResourceView* texture)
{
    HRESULT result;
    D3D11_MAPPED_SUBRESOURCE mappedResource;
    MatrixBufferType* dataPtr;
    unsigned int bufferNumber;


    // Transpose the matrices to prepare them for the shader.
    D3DXMatrixTranspose(&worldMatrix, &worldMatrix);
    D3DXMatrixTranspose(&viewMatrix, &viewMatrix);
    D3DXMatrixTranspose(&projectionMatrix, &projectionMatrix);

    // Lock the constant buffer so it can be written to.
    result = deviceContext->Map(m_matrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
    if(FAILED(result))
    {
        return false;
    }

    // Get a pointer to the data in the constant buffer.
    dataPtr = (MatrixBufferType*)mappedResource.pData;

    // Copy the matrices into the constant buffer.
    dataPtr->world = worldMatrix;
    dataPtr->view = viewMatrix;
    dataPtr->projection = projectionMatrix;

    // Unlock the constant buffer.
    deviceContext->Unmap(m_matrixBuffer, 0);

    // Set the position of the constant buffer in the vertex shader.
    bufferNumber = 0;

    // Now set the constant buffer in the vertex shader with the updated values.
    deviceContext->VSSetConstantBuffers(bufferNumber, 1, &m_matrixBuffer);

    // Set shader texture resource in the pixel shader.
    deviceContext->PSSetShaderResources(0, 1, &texture);

    return true;
}


void ParticleShaderClass::RenderShader(ID3D11DeviceContext* deviceContext, int indexCount)
{
    // Set the vertex input layout.
    deviceContext->IASetInputLayout(m_layout);

    // Set the vertex and pixel shaders that will be used to render this triangle.
    deviceContext->VSSetShader(m_vertexShader, NULL, 0);
    deviceContext->PSSetShader(m_pixelShader, NULL, 0);

    // Set the sampler state in the pixel shader.
    deviceContext->PSSetSamplers(0, 1, &m_sampleState);

    // Render the triangle.
    deviceContext->DrawIndexed(indexCount, 0, 0);

    return;
}

D3dclass.cpp

D3DClass에서는 Initialize 함수를 수정합니다. 알파 블렌딩 공식을 수정해야 합니다.

bool D3DClass::Initialize(int screenWidth, int screenHeight, bool vsync, HWND hwnd, bool fullscreen, float screenDepth, float screenNear)
{
    HRESULT result;
    IDXGIFactory* factory;
    IDXGIAdapter* adapter;
    IDXGIOutput* adapterOutput;
    unsigned int numModes, i, numerator, denominator, stringLength;
    DXGI_MODE_DESC* displayModeList;
    DXGI_ADAPTER_DESC adapterDesc;
    int error;
    DXGI_SWAP_CHAIN_DESC swapChainDesc;
    D3D_FEATURE_LEVEL featureLevel;
    ID3D11Texture2D* backBufferPtr;
    D3D11_TEXTURE2D_DESC depthBufferDesc;
    D3D11_DEPTH_STENCIL_DESC depthStencilDesc;
    D3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc;
    D3D11_RASTERIZER_DESC rasterDesc;
    D3D11_VIEWPORT viewport;
    float fieldOfView, screenAspect;
    D3D11_BLEND_DESC blendStateDescription;


    // 수직동기화 설정을 저장합니다.
    m_vsync_enabled = vsync;

    // DirectX 그래픽스 인터페이스 팩토리를 생성합니다.
    result = CreateDXGIFactory(__uuidof(IDXGIFactory), (void**)&factory);
    if(FAILED(result))
    {
        return false;
    }

    // 팩토리 객체를 사용하여 첫 번째 그래픽스 인터페이스(그래픽 카드)의 어댑터를 생성합니다.
    result = factory->EnumAdapters(0, &adapter);
    if(FAILED(result))
    {
        return false;
    }

    // 첫 번째 어댑터의 출력(모니터)을 나열합니다.
    result = adapter->EnumOutputs(0, &adapterOutput);
    if(FAILED(result))
    {
        return false;
    }

    // 어댑터 출력(모니터)에 맞는 DXGI_FORMAT_R8G8B8A8_UNORM 포맷을 지원하는 모드의 개수를 구합니다.
    result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, NULL);
    if(FAILED(result))
    {
        return false;
    }

    // 이 모니터/그래픽카드 조합을 지원하는 모드들을 저장할 리스트를 생성합니다.
    displayModeList = new DXGI_MODE_DESC[numModes];
    if(!displayModeList)
    {
        return false;
    }

    // 디스플레이 모드 리스트 구조체를 채웁니다.
    result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, displayModeList);
    if(FAILED(result))
    {
        return false;
    }

    // 모든 디스플레이 모드를 돌아보면서 현재 화면 너비와 높이와 가장 일치하는 것을 찾습니다.
    // 맞는 것을 찾으면 그 모니터 주사율의 분자 및 분모값을 저장합니다.
    for(i=0; i<numModes; i++)
    {
        if(displayModeList[i].Width == (unsigned int)screenWidth)
        {
            if(displayModeList[i].Height == (unsigned int)screenHeight)
            {
                numerator = displayModeList[i].RefreshRate.Numerator;
                denominator = displayModeList[i].RefreshRate.Denominator;
            }
        }
    }

    // 어댑터(그래픽카드) 디스크립션을 가져옵니다.
    result = adapter->GetDesc(&adapterDesc);
    if(FAILED(result))
    {
        return false;
    }

    // 전용 그래픽 카드 메모리의 크기를 메가바이트 단위로 저장합니다.
    m_videoCardMemory = (int)(adapterDesc.DedicatedVideoMemory / 1024 / 1024);

    // 그래픽 카드의 이름을 문자열 배열로 바꾸어 저장합니다.
    error = wcstombs_s(&stringLength, m_videoCardDescription, 128, adapterDesc.Description, 128);
    if(error != 0)
    {
        return false;
    }

    // 디스플레이 모드 리스트를 해제합니다.
    delete [] displayModeList;
    displayModeList = 0;

    // 어댑터 출력을 해제합니다.
    adapterOutput->Release();
    adapterOutput = 0;

    // 어댑터를 해제합니다.
    adapter->Release();
    adapter = 0;

    // 팩토리 객체를 해제합니다.
    factory->Release();
    factory = 0;

    // 스왑 체인 디스크립션을 초기화합니다.
    ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));

    // 하나의 백버프를 사용합니다.
    swapChainDesc.BufferCount = 1;

    // 백버퍼의 너비와 높이를 설정합니다.
    swapChainDesc.BufferDesc.Width = screenWidth;
    swapChainDesc.BufferDesc.Height = screenHeight;

    // 백버퍼를 일반적인 32비트 표면으로 설정합니다.
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

    // 백버퍼의 새로고침 비율을 설정합니다.
    if(m_vsync_enabled)
    {
        swapChainDesc.BufferDesc.RefreshRate.Numerator = numerator;
        swapChainDesc.BufferDesc.RefreshRate.Denominator = denominator;
    }
    else
    {
        swapChainDesc.BufferDesc.RefreshRate.Numerator = 0;
        swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
    }

    // 백버퍼의 용도를 설정합니다.
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

    // 그려질 윈도우의 핸들을 설정합니다.
    swapChainDesc.OutputWindow = hwnd;

    // 멀티샘플링 기능을 끕니다.
    swapChainDesc.SampleDesc.Count = 1;
    swapChainDesc.SampleDesc.Quality = 0;

    // 풀스크린인지 윈도우 모드인지 설정합니다.
    if(fullscreen)
    {
        swapChainDesc.Windowed = false;
    }
    else
    {
        swapChainDesc.Windowed = true;
    }

    // 스캔 라인 순서와 확대 여부를 unspecified로 설정합니다.
    swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;

    // 표시가 완료한 백버퍼 내용은 버리도록 설정합니다.
    swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;

    // 고급 플래그는 설정하지 않습니다.
    swapChainDesc.Flags = 0;

    // 피쳐 레벨을 DirectX 11로 설정합니다.
    featureLevel = D3D_FEATURE_LEVEL_11_0;

    // 스왑 체인, Direct3D 장치, Direct3D 장치 컨텍스트를 생성합니다.
    result = D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, &featureLevel, 1, 
                           D3D11_SDK_VERSION, &swapChainDesc, &m_swapChain, &m_device, NULL, &m_deviceContext);
    if(FAILED(result))
    {
        return false;
    }

    // 백버퍼의 포인터를 얻어옵니다.
    result = m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&backBufferPtr);
    if(FAILED(result))
    {
        return false;
    }

    // 백버퍼의 포인터를 이용하여 렌더 타겟 뷰를 생성합니다.
    result = m_device->CreateRenderTargetView(backBufferPtr, NULL, &m_renderTargetView);
    if(FAILED(result))
    {
        return false;
    }

    // 더 이상 사용하지 않는 백버퍼의 포인터를 해제합니다.
    backBufferPtr->Release();
    backBufferPtr = 0;

    // 깊이 버퍼의 디스크립션을 초기화합니다.
    ZeroMemory(&depthBufferDesc, sizeof(depthBufferDesc));

    // 깊이 버퍼의 디스크립션을 설정합니다.
    depthBufferDesc.Width = screenWidth;
    depthBufferDesc.Height = screenHeight;
    depthBufferDesc.MipLevels = 1;
    depthBufferDesc.ArraySize = 1;
    depthBufferDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
    depthBufferDesc.SampleDesc.Count = 1;
    depthBufferDesc.SampleDesc.Quality = 0;
    depthBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    depthBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
    depthBufferDesc.CPUAccessFlags = 0;
    depthBufferDesc.MiscFlags = 0;

    // 채워진 디스크립션으로 깊이 버퍼의 텍스쳐를 생성합니다.
    result = m_device->CreateTexture2D(&depthBufferDesc, NULL, &m_depthStencilBuffer);
    if(FAILED(result))
    {
        return false;
    }

    // 스텐실 상태의 디스크립션을 초기화합니다.
    ZeroMemory(&depthStencilDesc, sizeof(depthStencilDesc));

    // 스텐실 상태의 디스크립션을 설정합니다.
    depthStencilDesc.DepthEnable = true;
    depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
    depthStencilDesc.DepthFunc = D3D11_COMPARISON_LESS;

    depthStencilDesc.StencilEnable = true;
    depthStencilDesc.StencilReadMask = 0xFF;
    depthStencilDesc.StencilWriteMask = 0xFF;

    // 픽셀이 정면인 경우의 스텐실 동작을 설정합니다.
    depthStencilDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
    depthStencilDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_INCR;
    depthStencilDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
    depthStencilDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;

    // 픽셀이 뒷면인 경우의 스텐실 동작을 설정합니다.
    depthStencilDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
    depthStencilDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_DECR;
    depthStencilDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
    depthStencilDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;

    // 깊이 스텐실 상태를 생성합니다.
    result = m_device->CreateDepthStencilState(&depthStencilDesc, &m_depthStencilState);
    if(FAILED(result))
    {
        return false;
    }

    // 깊이 스텐실 상태를 설정합니다.
    m_deviceContext->OMSetDepthStencilState(m_depthStencilState, 1);

    // 깊이 스텐실 뷰 디스크립션을 초기화합니다.
    ZeroMemory(&depthStencilViewDesc, sizeof(depthStencilViewDesc));

    // 깊이 스텐실 뷰 디스크립션을 설정합니다.
    depthStencilViewDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
    depthStencilViewDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
    depthStencilViewDesc.Texture2D.MipSlice = 0;

    // 깊이 스텐실 뷰를 생성합니다.
    result = m_device->CreateDepthStencilView(m_depthStencilBuffer, &depthStencilViewDesc, &m_depthStencilView);
    if(FAILED(result))
    {
        return false;
    }

    // 렌더 타겟 뷰와 깊이 스텐실 버퍼를 출력 렌더링 파이프라인에 바인딩합니다.
    m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);

    // 어떤 다각형이 어떻게 그려질 지 정하는 래스터라이저의 디스크립션을 설정합니다.
    rasterDesc.AntialiasedLineEnable = false;
    rasterDesc.CullMode = D3D11_CULL_BACK;
    rasterDesc.DepthBias = 0;
    rasterDesc.DepthBiasClamp = 0.0f;
    rasterDesc.DepthClipEnable = true;
    rasterDesc.FillMode = D3D11_FILL_SOLID;
    rasterDesc.FrontCounterClockwise = false;
    rasterDesc.MultisampleEnable = false;
    rasterDesc.ScissorEnable = false;
    rasterDesc.SlopeScaledDepthBias = 0.0f;

    // 방금 채운 디스크립션으로 래스터라이저 상태를 생성합니다.
    result = m_device->CreateRasterizerState(&rasterDesc, &m_rasterState);
    if(FAILED(result))
    {
        return false;
    }

    // 래스터라이저 상태를 설정합니다.
    m_deviceContext->RSSetState(m_rasterState);
    
    // 랜더링에 사용할 뷰포트를 설정합니다.
    viewport.Width = (float)screenWidth;
    viewport.Height = (float)screenHeight;
    viewport.MinDepth = 0.0f;
    viewport.MaxDepth = 1.0f;
    viewport.TopLeftX = 0.0f;
    viewport.TopLeftY = 0.0f;

    // 뷰포트를 생성합니다.
    m_deviceContext->RSSetViewports(1, &viewport);

    // 투영 행렬을 설정합니다.
    fieldOfView = (float)D3DX_PI / 4.0f;
    screenAspect = (float)screenWidth / (float)screenHeight;

    // 3D 렌더링에 사용하는 투영 행렬을 생성합니다.
    D3DXMatrixPerspectiveFovLH(&m_projectionMatrix, fieldOfView, screenAspect, screenNear, screenDepth);

    // 월드 행렬을 단위 행렬로 초기화합니다.
    D3DXMatrixIdentity(&m_worldMatrix);

    // 2D 렌더링에 사용하는 직교 투영 행렬을 생성합니다.
    D3DXMatrixOrthoLH(&m_orthoMatrix, (float)screenWidth, (float)screenHeight, screenNear, screenDepth);

    // 블렌드 상태 디스크립션을 초기화합니다.
    ZeroMemory(&blendStateDescription, sizeof(D3D11_BLEND_DESC));

이제 블렌딩 공식을 수정합니다. 파티클들이 겹칠 때 서로 블렌딩하게 할 것이기 때문에 블렌드 상태를 파티클에 맞게 설정해야 합니다. 이 예제에서는 파티클이 겹치면 서로의 색상을 더하는 additive 블렌딩을 사용합니다. 그렇게 하려면 SrcBlend(들어오는 파티클 텍스쳐)를 D3D11_BLEND_ONE로 하여 파티클의 색상이 결과에 더해지도록 합니다. 또한 DestBlend(텍스쳐가 쓰여지는 백버퍼) 역시 D3D11_BLEND_ONE로 하여 이미 그려진 파티클의 색상에 더해 씌워지도록 합니다. 따라서 공식은 color = (1 * source) + (1 * destination)이 됩니다.

이 공식이 동작하기 위해서는 파티클들이 깊이값에 따라 정렬되어 있어야 합니다. 만약 정렬되어 있지 않다면 일부 파티클들에 검은 외곽선의 결함이 생겨 예상했던 결과를 망치게 될 것입니다.

    // 블렌드 상태 디스크립션에 알파를 사용함으로 설정합니다.
    blendStateDescription.RenderTarget[0].BlendEnable = TRUE;
    blendStateDescription.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE;
    blendStateDescription.RenderTarget[0].DestBlend = D3D11_BLEND_ONE;
    blendStateDescription.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
    blendStateDescription.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
    blendStateDescription.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
    blendStateDescription.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
    blendStateDescription.RenderTarget[0].RenderTargetWriteMask = 0x0f;

    // 디스크립션을 이용하여 블렌드 상태를 생성합니다.
    result = m_device->CreateBlendState(&blendStateDescription, &m_alphaEnableBlendingState);
    if(FAILED(result))
    {
        return false;
    }

    // 디스크립션을 수정하여 알파를 사용하지 않도록 만듭니다.
    blendStateDescription.RenderTarget[0].BlendEnable = FALSE;

    // 디스크립션을 이용하여 블렌드 상태를 생성합니다.
    result = m_device->CreateBlendState(&blendStateDescription, &m_alphaDisableBlendingState);
    if(FAILED(result))
    {
        return false;
    }

    return true;
}

Graphicsclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "d3dclass.h"
#include "cameraclass.h"

GraphicsClass 헤더 파일에 ParticleShaderClassParticleSystemClass의 헤더파일을 추가합니다.

#include "particleshaderclass.h"
#include "particlesystemclass.h"


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = true;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;


////////////////////////////////////////////////////////////////////////////////
// Class name: GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
    GraphicsClass();
    GraphicsClass(const GraphicsClass&);
    ~GraphicsClass();

    bool Initialize(int, int, HWND);
    void Shutdown();
    bool Frame(float);

private:
    bool Render();

private:
    D3DClass* m_D3D;
    CameraClass* m_Camera;

ParticleShaderClassParticleShaderClass의 전용 멤버 변수를 추가합니다.

    ParticleShaderClass* m_ParticleShader;
    ParticleSystemClass* m_ParticleSystem;
};

#endif

Graphicsclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "graphicsclass.h"


GraphicsClass::GraphicsClass()
{
    m_D3D = 0;
    m_Camera = 0;

생성자에서 파티클 셰이더와 파티클 시스템의 포인터를 null로 초기화합니다.

    m_ParticleShader = 0;
    m_ParticleSystem = 0;
}


GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}


GraphicsClass::~GraphicsClass()
{
}


bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
    bool result;


    // Direct3D 객체를 생성합니다.
    m_D3D = new D3DClass;
    if(!m_D3D)
    {
        return false;
    }

    // Direct3D 객체를 초기화합니다.
    result = m_D3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize Direct3D.", L"Error", MB_OK);
        return false;
    }

    // 카메라 객체를 생성합니다.
    m_Camera = new CameraClass;
    if(!m_Camera)
    {
        return false;
    }

    // 카메라의 처음 위치를 설정합니다.
    m_Camera->SetPosition(0.0f, -2.0f, -10.0f);

파티클 셰이더 객체를 생성하고 초기화합니다.

    // 파이클 셰이더 객체를 생성합니다.
    m_ParticleShader = new ParticleShaderClass;
    if(!m_ParticleShader)
    {
        return false;
    }

    // 파티클 셰이더 객체를 초기화합니다.
    result = m_ParticleShader->Initialize(m_D3D->GetDevice(), hwnd);
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize the particle shader object.", L"Error", MB_OK);
        return false;
    }

파티클 시스템 객체를 생성하고 초기화합니다.

    // 파티클 시스템 객체를 생성합니다.
    m_ParticleSystem = new ParticleSystemClass;
    if(!m_ParticleSystem)
    {
        return false;
    }

    // 파티클 시스템 객체를 초기화합니다.
    result = m_ParticleSystem->Initialize(m_D3D->GetDevice(), L"../Engine/data/star.dds");
    if(!result)
    {
        return false;
    }

    return true;
}


void GraphicsClass::Shutdown()
{

Shutdown 함수에서 파티클 셰이더와 파티클 시스템을 해제합니다.

    // 파티클 시스템 객체를 해제합니다.
    if(m_ParticleSystem)
    {
        m_ParticleSystem->Shutdown();
        delete m_ParticleSystem;
        m_ParticleSystem = 0;
    }

    // 파티클 셰이더 객체를 해제합니다.
    if(m_ParticleShader)
    {
        m_ParticleShader->Shutdown();
        delete m_ParticleShader;
        m_ParticleShader = 0;
    }

    // 카메라 객체를 해제합니다.
    if(m_Camera)
    {
        delete m_Camera;
        m_Camera = 0;
    }

    // D3D 객체를 해제합니다.
    if(m_D3D)
    {
        m_D3D->Shutdown();
        delete m_D3D;
        m_D3D = 0;
    }

    return;
}


bool GraphicsClass::Frame(float frameTime)
{
    bool result;

매 프레임마다 파티클 시스템을 업데이트합니다.

    // 파티클 시스템의 프레임 처리를 실행합니다.
    m_ParticleSystem->Frame(frameTime, m_D3D->GetDeviceContext());

    // 그래픽스 장면을 그립니다.
    result = Render();
    if(!result)
    {
        return false;
    }

    return true;
}


bool GraphicsClass::Render()
{
    D3DXMATRIX worldMatrix, viewMatrix, projectionMatrix;
    bool result;


    // 장면을 시작하기 위하여 버퍼를 초기화합니다.
    m_D3D->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

    // 카메라의 위치에 기반한 뷰 행렬을 생성합니다.
    m_Camera->Render();

    // 카메라와 d3d 객체에서 월드, 뷰, 투영 행렬을 가져옵니다.
    m_Camera->GetViewMatrix(viewMatrix);
    m_D3D->GetWorldMatrix(worldMatrix);
    m_D3D->GetProjectionMatrix(projectionMatrix);

렌더링을 시작하기 전에 알파블렌딩을 켜야 합니다.

    // 알파블렌딩을 켭니다.
    m_D3D->EnableAlphaBlending();

파티클 시스템을 그립니다.

    // 파티클 시스템의 정점 및 인덱스 버퍼를 그래픽스 파이프라인에 넣어 그려질 수 있도록 합니다.
    m_ParticleSystem->Render(m_D3D->GetDeviceContext());

    // 텍스쳐 셰이더를 이용하여 모델을 그립니다.
    result = m_ParticleShader->Render(m_D3D->GetDeviceContext(), m_ParticleSystem->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, 
                      m_ParticleSystem->GetTexture());
    if(!result)
    {
        return false;
    }

파티클이 그려진 뒤에는 알파블렌딩을 끕니다.

    // 알파 블렌딩을 끕니다.
    m_D3D->DisableAlphaBlending();

    // 그려진 장면을 화면에 표시합니다.
    m_D3D->EndScene();

    return true;
}

마치면서

이동 변수를 사용하여 파티클들을 그려내는 매우 기본적인 파티클 시스템을 만들었습니다. 하지만 좋은 파티클 시스템이 되려면 제가 보여드린 것보다는 더 효율적이어야 합니다.

파티클 시스템을 확장하기 위해서는 우선 완전히 데이터 주도적(data driven)이어야 합니다. 파티클 시스템을 정의하는 모든 변수들을 텍스트 파일에서 읽어오게 함으로 다시 컴파일 하지 않고도 변경 내용을 확인할 수 있게 할 수 있습니다. 결국에는 슬라이드를 조절하며 실시간으로 파티클 시스템의 속성들을 바꾸는 것과 같은 것들이 될 것입니다.

그 다음으로 바꿔야 할 것은 인스턴싱의 이점을 충분히 살려야 한다는 것입니다. 이 DirectX 10의 기능은 모양은 똑같지만 매 프레임마다 색상이나 위치가 조금씩만 변경되는 이런 시스템을 위한 것입니다.

세 번째는 파티클들이 빌보드여야 하고 정렬이 카메라까지의 거리가 아니라 단순히 Z 깊이값에 따라 이루어지도록 수정되어야 합니다. 3D 환경에서 움직이면서 파티클 역시 카메라를 마주보기 위해 Y축으로 회전해야 하기 때문입니다.

네 번째는 파티클 배열 역시 효율적으로 정렬되도록 수정해야 합니다. 사용할 수 있는 많은 종류의 정렬 알고리즘들이 있고, 여러분이 각각을 테스트하여 가장 좋은 결과를 내는 것을 찾아볼 수도 있습니다. 이 예제에서는 메모리 파편화를 위해 링크드 리스트와 같은 것들 대신 배열 복사를 사용하였습니다. 여러분이 파티클 메모리 풀을 위한 메모리 할당자를 만들었다면 링크드 리스트 역시 얼마든지 사용 가능하고 어떤 정렬에 경우는 성능도 잘 나올 것입니다.

덧붙여 추가적으로 gpu에서 파티클에 물리 연산을 해 볼 수도 있습니다. 상당한 물리 지식이 있다면 이를 이용하여 gpu에서 파티클의 위치를 바꾸어 cpu에서 위치를 계산하는 것에 비해 속도 향상을 얻을 수 있을 것입니다.

연습 문제

  1. 프로그램을 다시 컴파일하고 실행해 보십시오. 폭포 모양의 파티클을 볼 수 있습니다.

  2. 기본 변수들을 좀 바꾸어 파티클 시스템을 고쳐 보십시오.

  3. 파티클에 사용되는 텍스쳐를 바꾸어 보십시오.

  4. 매 프레임마다 파티클의 색상을 랜덤하게 만들어 보십시오.

  5. 파티클을 빌보드로 만들어 보십시오.

  6. 코사인 곡선 움직임을 넣어 아래 방향의 나선 효과를 만들어 보십시오.

  7. 인스턴싱된 파티클 시스템을 구현해 보십시오.

  8. 파티클에 여러 장의 텍스쳐를 사용해 보십시오.

  9. 알파 블렌딩을 끄고 어떤 모습으로 보이는지 확인해 보십시오.

소스 코드

Visual Studio 2010 프로젝트: dx11tut39.zip

소스코드만: dx11src39.zip

실행파일만: dx11exe39.zip

Nav