[DirectX] Particle
in Study on WinAPI&DirectX
구조화 버퍼, 컴퓨트 쉐이더를 이용해서 파티클을 만들어보자!
Particle에 대한 설계
구조화 버퍼를 이용해서 파티클을 GPU에서 관리할 수 있다.
파티클, 즉 입자 하나에 대한 설계를 진행해보자.
// struct.h 중에서
struct tParticle
{
Vec4 vWorldPos; // 파티클의 위치
Vec4 vWorldScale; // 파티클의 크기
Vec4 vColor; // 파티클의 색상
Vec4 vVelocity; // 파티클의 속도
Vec4 vForce; // 파티클에 주어진 힘
float Age; // 파티클이 얼마나 존재했는지 시간
float LifeTime; // 파티클의 최대 수명
float NomalizedAge; // 수명대비 생존 시간을 정규화한 값 (0 ~ 1 까지)
float Mass; // 파티클의 질량
};
// struct.fx 중에서
struct tParticle
{
float4 vWorldPos; // 파티클의 위치
float4 vWorldScale; // 파티클의 크기
float4 vColor; // 파티클의 색상
float4 vVelocity; // 파티클의 속도
float4 vForce; // 파티클에 주어진 힘
float Age; // 파티클이 얼마나 존재했는지 시간
float LifeTime; // 파티클의 최대 수명
float NomalizedAge; // 수명대비 생존 시간을 정규화한 값 (0 ~ 1 까지)
float Mass; // 파티클의 질량
};
그리고 이 구성을 HLSL에도 구성을 할 것이다.
파티클 업데이트를 위한 ComputeShader도 구성해준다.
particle_update.fx 의 기본 구성
#ifndef _PARTICLE_UPDATE
#define _PARTICLE_UPDATE
#include "value.fx"
#include "struct.fx"
RWStructuredBuffer<tParticle> ParticleBuffer : register(u0);
[numthreads(128, 1, 1)]
void CS_ParticleUpdate(int3 _ID : SV_DispatchThreadID)
{
ParticleBuffer[_ID.x];
}
#endif
여기서 ParticleBuffer[_ID.x] 부분을 조작하면서 파티클에 대한 수정이 가능하다.
ParticleBuffer[_ID.x].vWorldPos = float4(0.f, 0.f, 0.f, 0.f);
이렇게라든지..
CStructuredBuffer 클래스의 변화
// 헤더파일
#pragma once
#include "CEntity.h"
class CStructuredBuffer :
public CEntity
{
private:
ComPtr<ID3D11Buffer> m_SB;
ComPtr<ID3D11ShaderResourceView> m_SRV;
ComPtr<ID3D11UnorderedAccessView> m_UAV;
D3D11_BUFFER_DESC m_tDesc;
UINT m_iElementSize;
UINT m_iElementCount;
SB_TYPE m_Type;
public:
void Create(UINT _iElementSize, UINT _iElementCount, SB_TYPE _Type, void* _pSysMem = nullptr);
void SetData(void* _pSrc, UINT _iSize = 0);
// PIPELINE_STAGE
void UpdateData(UINT _iRegisterNum, UINT _iPipeLineStage);
UINT GetElementSize() { return m_iElementSize; }
UINT GetElementCount() { return m_iElementCount; }
CLONE_DISABLE(CStructuredBuffer);
public:
CStructuredBuffer();
~CStructuredBuffer();
};
// cpp 파일
#include "pch.h"
#include "CStructuredBuffer.h"
#include "CDevice.h"
CStructuredBuffer::CStructuredBuffer()
: m_iElementSize(0)
, m_iElementCount(0)
{
}
CStructuredBuffer::~CStructuredBuffer()
{
}
void CStructuredBuffer::Create(UINT _iElementSize, UINT _iElementCount, SB_TYPE _Type, void* _pSysMem)
{
m_SB = nullptr;
m_SRV = nullptr;
m_Type = _Type;
m_iElementSize = _iElementSize;
m_iElementCount = _iElementCount;
UINT iBufferSize = m_iElementSize * _iElementCount;
// 16바이트 단위 메모리 정렬
assert(!(iBufferSize % 16));
// 상수버퍼 생성
m_tDesc.ByteWidth = iBufferSize; // 버퍼 크기
m_tDesc.StructureByteStride = m_iElementSize; // 데이터 간격
if (SB_TYPE::READ_ONLY == m_Type)
{
m_tDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; // Texture 레지스터에 바이딩하기 위한 플래그
}
else
{
m_tDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS;
}
m_tDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; // 구조화 버퍼 체크
m_tDesc.Usage = D3D11_USAGE_DYNAMIC;
m_tDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
if (FAILED(DEVICE->CreateBuffer(&m_tDesc, nullptr, m_SB.GetAddressOf())))
{
assert(nullptr);
}
// ShaderResourceView 생성
D3D11_SHADER_RESOURCE_VIEW_DESC m_SRVDesc = {};
m_SRVDesc.ViewDimension = D3D_SRV_DIMENSION_BUFFEREX;
m_SRVDesc.BufferEx.NumElements = m_iElementCount;
if (FAILED(DEVICE->CreateShaderResourceView(m_SB.Get(), &m_SRVDesc, m_SRV.GetAddressOf())))
{
assert(nullptr);
}
if (SB_TYPE::READ_WRITE == m_Type)
{
D3D11_UNORDERED_ACCESS_VIEW_DESC m_UABDesc = {};
m_UABDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
m_UABDesc.Buffer.NumElements = m_iElementCount;
if (FAILED(DEVICE->CreateUnorderedAccessView(m_SB.Get(), &m_UABDesc, m_UAV.GetAddressOf())))
{
assert(nullptr);
}
}
}
void CStructuredBuffer::SetData(void* _pSrc, UINT _iSize)
{
D3D11_MAPPED_SUBRESOURCE tSub = {};
CONTEXT->Map(m_SB.Get(), 0, D3D11_MAP::D3D11_MAP_WRITE_DISCARD, 0, &tSub);
memcpy(tSub.pData, _pSrc, _iSize);
CONTEXT->Unmap(m_SB.Get(), 0);
}
void CStructuredBuffer::UpdateData(UINT _iRegisterNum, UINT _iPipeLineStage)
{
if (PIPELINE_STAGE::PS_VERTEX & _iPipeLineStage)
{
CONTEXT->VSSetShaderResources(_iRegisterNum, 1, m_SRV.GetAddressOf());
}
if (PIPELINE_STAGE::PS_HULL & _iPipeLineStage)
{
CONTEXT->HSSetShaderResources(_iRegisterNum, 1, m_SRV.GetAddressOf());
}
if (PIPELINE_STAGE::PS_DOMAIN & _iPipeLineStage)
{
CONTEXT->DSSetShaderResources(_iRegisterNum, 1, m_SRV.GetAddressOf());
}
if (PIPELINE_STAGE::PS_GEOMETRY & _iPipeLineStage)
{
CONTEXT->GSSetShaderResources(_iRegisterNum, 1, m_SRV.GetAddressOf());
}
if (PIPELINE_STAGE::PS_PIXEL & _iPipeLineStage)
{
CONTEXT->PSSetShaderResources(_iRegisterNum, 1, m_SRV.GetAddressOf());
}
}
- 멤버변수에 UAV를 추가해준다.
- UAV가 추가되었으니 바인딩 옵션을 지정해주어야한다.
구조화버퍼 타입을 enum 값으로 추가해주고, 생성시 타입을 입력을 받는다.
enum class SB_TYPE
{
READ_ONLY,
READ_WRITE,
}
- Type값이 READ_ONLY였으면 바인딩 플래그는 SHADER_RESOURCE로,
- READ_WRITE였으면 바인딩 플래그는 SHADER_RESOURCE와 UNORDERED_ACCESS를 같이 부여한다. 그리고 UAV도 같이 생성한다.
particle_render.fx
파티클을 렌더링하기위한 쉐이더 코드도 작성해준다.
// particle_render.fx
#ifndef _PARTICLE_RENDER
#define _PARTICLE_RENDER
#include "value.fx"
#include "struct.fx"
// ========================
// Particle Render Shader
// mesh : RectMesh
// Parameter
// g_int_0 : Particle Index
// =========================
StructuredBuffer<tParticle> ParticleBuffer : register(t20);
struct VS_IN
{
float3 vPos : POSITION;
float2 vUV : TEXCOORD;
};
struct VS_OUT
{
float4 vPosition : SV_Position;
float2 vUV : TEXCOORD;
};
VS_OUT VS_ParticleRender(VS_IN _in)
{
VS_OUT output = (VS_OUT) 0.f;
// Local Mesh 의 정점에 파티클 배율을 곱하고 월드 위치로 이동시킨다.
float3 vWorldPos = _in.vPos * ParticleBuffer[g_int_0].vWorldScale.xyz + ParticleBuffer[g_int_0].vWorldPos.xyz;
// View, Proj 행렬을 곱해서 NDC 좌표계로 이동시킨다.
float4 vViewPos = mul(float4(vWorldPos, 1.f), g_matView);
output.vPosition = mul(vViewPos, g_matProj);
// UV 전달
output.vUV = _in.vUV;
return output;
}
float4 PS_ParticleRender(VS_OUT _in) : SV_Target
{
return float4(1.f, 0.f, 0.f, 1.f);
}
#endif
재질을 리소스 매니저에 추가하기
// CResMgr.cpp 중에서
// 쉐이더 추가
// ============================
// ParticleRender
//
// RS_TYPE : CULL_NONE
// DS_TYPE : NO_WRITE
// BS_TYPE : ALPHA_BLEND
// Parameter
// g_int_0 : Particle Index
//
// Domain : TRANSPARENT
// ============================
pShader = new CGraphicsShader;
pShader->SetKey(L"ParticleRenderShader");
pShader->CreateVertexShader(L"shader\\particle_render.fx", "VS_ParticleRender");
pShader->CreatePixelShader(L"shader\\particle_render.fx", "PS_ParticleRender");
pShader->SetRSType(RS_TYPE::CULL_NONE);
pShader->SetDSType(DS_TYPE::NO_WRITE);
pShader->SetBSType(BS_TYPE::ALPHA_BLEND);
pShader->SetDomain(SHADER_DOMAIN::DOMAIN_TRANSPARENT);
AddRes(pShader->GetKey(), pShader);
// 재질 추가
// Particle Render Material
pMtrl = new CMaterial;
pMtrl->SetShader(FindRes<CGraphicsShader>(L"ParticleRenderShader"));
AddRes(L"ParticleRenderMtrl", pMtrl);
CParticleSystem 클래스의 구성
// 헤더파일
#pragma once
#include "CRenderComponent.h"
class CStructuredBuffer;
class CParticleSystem :
public CRenderComponent
{
private:
CStructuredBuffer* m_ParticleBuffer;
UINT m_iMaxParticleCount;
public:
virtual void finaltick() override;
virtual void render() override;
CLONE(CParticleSystem);
public:
CParticleSystem();
~CParticleSystem();
};
// cpp 파일
#include "pch.h"
#include "CParticleSystem.h"
#include "CDevice.h"
#include "CStructuredBuffer.h"
#include "CResMgr.h"
CParticleSystem::CParticleSystem()
: CRenderComponent(COMPONENT_TYPE::PARTICLESYSTEM)
, m_iMaxParticleCount(5)
{
SetMesh(CResMgr::GetInst()->FindRes<CMesh>(L"RectMesh"));
SetMaterial(CResMgr::GetInst()->FindRes<CMaterial>(L"ParticleRenderMtrl"));
tParticle arrParticle[5] = { };
float fStep = 100.f;
arrParticle[0].vWorldPos = Vec3(-2.f * fStep, 0.f, 100.f);
arrParticle[1].vWorldPos = Vec3(-1.f * fStep, 0.f, 100.f);
arrParticle[2].vWorldPos = Vec3(0.f , 0.f, 100.f);
arrParticle[3].vWorldPos = Vec3(1.f * fStep, 0.f, 100.f);
arrParticle[4].vWorldPos = Vec3(2.f * fStep, 0.f, 100.f);
arrParticle[0].vWorldScale = Vec3(10.f, 10.f, 1.f);
arrParticle[1].vWorldScale = Vec3(10.f, 10.f, 1.f);
arrParticle[2].vWorldScale = Vec3(10.f, 10.f, 1.f);
arrParticle[3].vWorldScale = Vec3(10.f, 10.f, 1.f);
arrParticle[4].vWorldScale = Vec3(10.f, 10.f, 1.f);
m_ParticleBuffer = new CStructuredBuffer;
m_ParticleBuffer->Create(sizeof(tParticle), m_iMaxParticleCount, SB_TYPE::READ_ONLY, arrParticle);
}
CParticleSystem::~CParticleSystem()
{
if (nullptr != m_ParticleBuffer)
delete m_ParticleBuffer;
}
void CParticleSystem::finaltick()
{
}
void CParticleSystem::render()
{
Transform()->UpdateData();
m_ParticleBuffer->UpdateData(20, PIPELINE_STAGE::PS_ALL);
for (int i = 0; i < m_iMaxParticleCount; ++i)
{
// Particle Render
GetMaterial()->SetScalarParam(INT_0, &i);
GetMaterial()->UpdateData();
GetMesh()->render();
}
}
m_ParticleBuffer를 통해서 파티클을 전담할 구조화 버퍼를 지정해준다.
그리고 구조화 버퍼는 읽고 쓸수 있도록 READ_WRITE 타입으로 생성시킨다.
- 바인딩 플래그 Unordered Access랑 Usage_Dynamic 옵션끼리는 조합이 안된다. 그래서 READ_ONLY로 일단 생성시킨다.
생성자에서 메쉬와 재질을 지정해주고, 파티클을 지정해준다.
우선 5개를 지정해보자.
Render함수에서 렌더링하기 직전에 구조화 버퍼를 바인딩을 진행해준다.
그리고 파티클이 있는 만큼 for문을 돌려서 파티클을 렌더링한다.
적용해보기
// CLevelMgr.cpp 중에서
// Particle Object
CGameObject* pParticleObj = new CGameObject;
pParticleObj->AddComponent(new CTransform);
pParticleObj->AddComponent(new CParticleSystem);
m_pCurLevel->AddGameObject(pParticleObj, L"Default", false);
CStructuredBuffer 클래스의 조정
바인딩 플래그 Unordered Access랑 Usage_Dynamic 옵션끼리는 조합이 안된다고 했다.
그래픽 카드 구조상의 문제인진 모르겠으나, 시스템메모리 - GPU (Usage_Dynamic) 그리고 GPU - GPU 쉐이더 코드 (Unordered Access) 간의 동시 바인딩이 진행이 안되기 때문이라고 한다.
동시 바인딩을 하고 싶을 때에는 구조화 버퍼 2개를 만든다.
1개는 시스템메모리와 묶이고, 다른 1개의 버퍼와 바인딩을 진행하고,
나머지 1개의 구조화 버퍼에서 texture register와 unordered register와 바인딩을 진행한다.
버퍼를 2개 사용할 수 있도록 서브 버퍼를 추가한다.
// CStructuredBuffer.h
#pragma once
#include "CEntity.h"
class CStructuredBuffer :
public CEntity
{
private:
ComPtr<ID3D11Buffer> m_SB; // register binding
ComPtr<ID3D11ShaderResourceView> m_SRV;
ComPtr<ID3D11UnorderedAccessView> m_UAV;
ComPtr<ID3D11Buffer> m_SB_CPU_Read; // GPU -> Sys
ComPtr<ID3D11Buffer> m_SB_CPU_Write; // Sys -> GPU
D3D11_BUFFER_DESC m_tDesc;
UINT m_iElementSize;
UINT m_iElementCount;
SB_TYPE m_Type;
bool m_bSysAccess;
public:
void Create(UINT _iElementSize, UINT _iElementCount, SB_TYPE _Type, bool _bUseSysAccess, void* _pSysMem = nullptr);
void SetData(void* _pSrc, UINT _iSize = 0);
void GetData(void* _pDst);
// PIPELINE_STAGE
void UpdateData(UINT _iRegisterNum, UINT _iPipeLineStage);
UINT GetElementSize() { return m_iElementSize; }
UINT GetElementCount() { return m_iElementCount; }
CLONE_DISABLE(CStructuredBuffer);
public:
CStructuredBuffer();
~CStructuredBuffer();
};
// CStructuredBuffer.cpp 중에서 Create 함수
void CStructuredBuffer::Create(UINT _iElementSize, UINT _iElementCount
, SB_TYPE _Type, bool _bSysAccess, void* _pSysMem)
{
m_SB = nullptr;
m_SRV = nullptr;
m_UAV = nullptr;
m_SB_CPU_Read = nullptr;
m_SB_CPU_Write = nullptr;
m_Type = _Type;
m_bSysAccess = _bSysAccess;
m_iElementSize = _iElementSize;
m_iElementCount = _iElementCount;
UINT iBufferSize = m_iElementSize * _iElementCount;
// 16바이트 단위 메모리 정렬
assert(!(iBufferSize % 16));
// 상수버퍼 생성
m_tDesc.ByteWidth = iBufferSize; // 버퍼 크기
m_tDesc.StructureByteStride = m_iElementSize; // 데이터 간격
if (SB_TYPE::READ_ONLY == m_Type)
{
m_tDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; // Texture 레지스터에 바이딩하기 위한 플래그
}
else if(SB_TYPE::READ_WRITE == m_Type)
{
m_tDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS;
}
m_tDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; // 구조화 버퍼 체크
m_tDesc.Usage = D3D11_USAGE_DEFAULT;
m_tDesc.CPUAccessFlags = 0;
if (nullptr == _pSysMem)
{
if (FAILED(DEVICE->CreateBuffer(&m_tDesc, nullptr, m_SB.GetAddressOf())))
{
assert(nullptr);
}
}
else
{
D3D11_SUBRESOURCE_DATA tSubData = {};
tSubData.pSysMem = _pSysMem;
HRESULT hr = DEVICE->CreateBuffer(&m_tDesc, &tSubData, m_SB.GetAddressOf());
if (hr)
{
assert(nullptr);
}
}
// ShaderResourceView 생성
D3D11_SHADER_RESOURCE_VIEW_DESC m_SRVDesc = {};
m_SRVDesc.ViewDimension = D3D_SRV_DIMENSION_BUFFEREX;
m_SRVDesc.BufferEx.NumElements = m_iElementCount;
if (FAILED(DEVICE->CreateShaderResourceView(m_SB.Get(), &m_SRVDesc, m_SRV.GetAddressOf())))
{
assert(nullptr);
}
if (SB_TYPE::READ_WRITE == m_Type)
{
D3D11_UNORDERED_ACCESS_VIEW_DESC m_UABDesc = {};
m_UABDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
m_UABDesc.Buffer.NumElements = m_iElementCount;
if (FAILED(DEVICE->CreateUnorderedAccessView(m_SB.Get(), &m_UABDesc, m_UAV.GetAddressOf())))
{
assert(nullptr);
}
}
// CPU Access 보조 버퍼
if (m_bSysAccess)
{
m_tDesc.BindFlags = D3D11_BIND_FLAG::D3D11_BIND_SHADER_RESOURCE;
// GPU -> CPU Read
m_tDesc.Usage = D3D11_USAGE_DEFAULT;
m_tDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
if (FAILED(DEVICE->CreateBuffer(&m_tDesc, nullptr, m_SB_CPU_Read.GetAddressOf())))
{
assert(nullptr);
}
// CPU -> GPU Write
m_tDesc.Usage = D3D11_USAGE_DYNAMIC;
m_tDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
if (FAILED(DEVICE->CreateBuffer(&m_tDesc, nullptr, m_SB_CPU_Write.GetAddressOf())))
{
assert(nullptr);
}
}
}
헤더파일에서 메인버퍼 1개, 서브버퍼 2개로 구성한다.
그리고 SB_TYPE(구조화 버퍼 타입)뿐만 아니라 GPU - 시스템 메모리와 GPU - 쉐이더 코드 간의 소통이 동시에 진행되어야 하는지 여부도 추가할 것이다.
cpp 파일에서 구조화 버퍼를 생성할 때, 마지막 조건으로 CPU Access와 Unordered Access register 바인딩이 동시에 진행되어야 하는지 여부를 따져서 보조버퍼를 생성할지를 결정한다.
그리고 SetData 함수도 수정해준다.
void CStructuredBuffer::SetData(void* _pSrc, UINT _iSize)
{
// CPU -> CPU WriteBuffer
D3D11_MAPPED_SUBRESOURCE tSub = {};
CONTEXT->Map(m_SB_CPU_Write.Get(), 0, D3D11_MAP::D3D11_MAP_WRITE_DISCARD, 0, &tSub);
memcpy(tSub.pData, _pSrc, _iSize);
CONTEXT->Unmap(m_SB_CPU_Write.Get(), 0);
// CPU WriteBuffer -> Main Buffer
CONTEXT->CopyResource(m_SB.Get(), m_SB_CPU_Write.Get());
}
map, unmap을 할 때 쓰기 버퍼를 매핑 시켜서 m_SB_CPU_Write로 보내고, m_SB_CPU_Write에서 다시 m_SB 버퍼로 데이터를 복사하는 식으로 변경한다.
그리고 GPU에서 다시 CPU로 데이터를 가져오는 과정을 구현하기 위해서 GetData함수를 추가한다.
void CStructuredBuffer::GetData(void* _pDst)
{
// Main Buffer -> CPU ReadBuffer
CONTEXT->CopyResource(m_SB_CPU_Read.Get(), m_SB.Get());
// CPU ReadBuffer -> CPU
D3D11_MAPPED_SUBRESOURCE tSub = {};
CONTEXT->Map(m_SB_CPU_Read.Get(), 0, D3D11_MAP::D3D11_MAP_READ, 0, &tSub);
memcpy(_pDst, tSub.pData, m_iElementCount * m_iElementSize);
CONTEXT->Unmap(m_SB_CPU_Read.Get(), 0);
}
먼저 m_SB에서 m_SB_CPU_Read 버퍼로 데이터를 가져오고, map을 진행해서 m_SB_CPU_Read 데이터를 시스템 메모리로 memcpy를 시킨다. 그러고 나서 unmap을 진행한다.
이제 파티클 시스템에서 생성자의 일부는 아래와 같이 수정될 수 있다.
// CParticleSystem.cpp 의 생성자 중에서
m_ParticleBuffer = new CStructuredBuffer;
m_ParticleBuffer->Create(sizeof(tParticle), m_iMaxParticleCount, SB_TYPE::READ_WRITE, false, arrParticle);
버퍼 작동 테스트 해보기
// CLevelMgr.cpp의 init() 중에서
// 구조화버퍼 테스트
struct tTestStruct
{
int a;
float f;
long long l;
};
CStructuredBuffer* pBuffer = new CStructuredBuffer;
pBuffer->Create(sizeof(tTestStruct), 1, SB_TYPE::READ_ONLY, true);
tTestStruct testParam = {};
testParam.a = 10;
testParam.f = 1.4f;
testParam.l = 1000000000000;
pBuffer->SetData(&testParam);
tTestStruct testParam2 = {};
pBuffer->GetData(&testParam2);
delete pBuffer;
SetData의 예외처리(_iSize = 0일 때)
void CStructuredBuffer::SetData(void* _pSrc, UINT _iSize)
{
UINT iSize = _iSize;
if (0 == iSize)
{
iSize = GetBufferSize();
}
// CPU -> CPU WriteBuffer
D3D11_MAPPED_SUBRESOURCE tSub = {};
CONTEXT->Map(m_SB_CPU_Write.Get(), 0, D3D11_MAP::D3D11_MAP_WRITE_DISCARD, 0, &tSub);
memcpy(tSub.pData, _pSrc, iSize);
CONTEXT->Unmap(m_SB_CPU_Write.Get(), 0);
// CPU WriteBuffer -> Main Buffer
CONTEXT->CopyResource(m_SB.Get(), m_SB_CPU_Write.Get());
}