
개발 일자 : 2022년 3월 14일 ~ 18일


현재 제작 중인 일본 테마의 맵 중앙에는 이렇게 큰 냄비와 모닥불이 있습니다.
모닥불에는 당연히 불이 있어야될 것 같아서 파티클 시스템 구현을 시작했습니다.
구현 방식을 요약하자면 다음과 같습니다.
- 현재 파티클 리스트를 스트림 출력(Stream Output) 전용으로 그립니다. 래스터화기가 비활성화되어 있으므로 그 어떤 파티클도 화면에 렌더링되지는 않습니다.
- 기하 쉐이더는 주어진 조건에 따라 파티클들을 생성하거나 파괴합니다 (구체적인 조건은 구체적인 파티클 시스템에 따라 다릅니다. 불 파티클 시스템은 불.fx에 정의된 조건을 따릅니다.)
- 갱신된 파티클 리스트를 스트림 출력(Stream Output)을 통해 정점 버퍼에 기록합니다.
- 현재 프레임에 갱신된 파티클 목록을 렌더합니다.
스트림 출력
스트림 출력 단계를 이용하면 GPU에서 스트림 출력 단계에 묶인 정점 버퍼 V에 기하구조 자체(정점 목록 형태)를 직접 기록할 수 있습니다. 즉, 기하 셰이더에서 출력한 정점들이 V에 기록되게 할 수 있습니다. 그리고 V에 저장된 기하구조를 나중에 렌더링 파이프라인에 입력해서 그리는 것이 가능합니다. 이를 파티클 시스템에 활용하였습니다.

스트림 출력(Stream Output)을 사용하는 경우 특별한 설정이 없는 한 기하 셰이더가 출력한 정점은 스트림으로 출력될 뿐만 아니라 렌더링 파이프라인의 다음 단계(래스터화기)로도 입력됩니다. 자료를 스트림으로 출력하기만 하고 렌더링하지 않기 위해서 픽셀 셰이더와 DepthStencil 버퍼를 비활성화 했습니다.

위의 과정을 진행하는 코드는 다음과 같습니다.
void ParticleSystem::Draw(const EMath::Matrix& view, const EMath::Matrix& proj)
{
EMath::Matrix _vp = view * proj;
ID3D11DeviceContext* _dc = m_DX11Core->GetDC();
ParticleEffect* _fx = Effects::FireFX;
// 리소스 매니저로부터 파티클 텍스쳐를 가져온다.
if(m_TexArraySRV == nullptr)
m_TexArraySRV = m_ResourceManager->GetParticle(m_ParticleSystemData->m_Name);
// cb 세팅
_fx->SetViewProj(_vp);
_fx->SetGameTime(m_GameTime);
_fx->SetTimeStep(m_TimeStep);
_fx->SetEmitPosW(m_ParticleSystemData->m_EmitPos);
_fx->SetEmitDirW(m_ParticleSystemData->m_EmitDir);
_fx->SetTexArray(m_TexArraySRV);
_fx->SetRandomTex(m_RandomTexSRV);
// IA 세팅
_dc->IASetInputLayout(InputLayouts::Particle);
_dc->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
UINT stride = sizeof(Vertex::Particle);
UINT offset = 0;
// 최초 실행이면 초기화용 정점 버퍼를 사용하고,
// 그렇지 않으면 현재의 입자 목록을 담은 정점 버퍼를 사용한다.
if (m_IsFirstRun)
_dc->IASetVertexBuffers(0, 1, &m_InitVB, &stride, &offset);
else
_dc->IASetVertexBuffers(0, 1, &m_DrawVB, &stride, &offset);
// 현재 입자 목록을 스트림 출력 전용 기법으로 그려서 입자들을 갱신한다.
// 갱신된 입자들은 스트림 출력을 통해서 대상 정점 버퍼에 기록된다.
_dc->SOSetTargets(1, &m_StreamOutVB, &offset);
D3DX11_TECHNIQUE_DESC techDesc;
_fx->StreamOutTech->GetDesc(&techDesc);
for (UINT p = 0; p < techDesc.Passes; ++p)
{
_fx->StreamOutTech->GetPassByIndex(p)->Apply(0, _dc);
if (m_IsFirstRun)
{
_dc->Draw(1, 0);
m_IsFirstRun = false;
}
else
{
_dc->DrawAuto();
}
}
// 스트림 전용 패스가 끝났다. 정점 버퍼를 떼어낸다.
ID3D11Buffer* bufferArray[1] = { 0 };
_dc->SOSetTargets(1, bufferArray, &offset);
// 정점 버퍼들을 맞바꾼다 (핑퐁)
std::swap(m_DrawVB, m_StreamOutVB);
// 방금 스트림 출력된, 갱신된 입자 시스템을 화면에 그린다.
_dc->IASetVertexBuffers(0, 1, &m_DrawVB, &stride, &offset);
_fx->DrawTech->GetDesc(&techDesc);
for (UINT p = 0; p < techDesc.Passes; ++p)
{
_fx->DrawTech->GetPassByIndex(p)->Apply(0, _dc);
_dc->DrawAuto();
}
}
위의 코드에서 DrawVB와 StreamOutVB를 나눈 이유는, 하나의 정점 버퍼를 출력 병합기 단계와 입력 조립기 단계에 동시에 묶어 둘 수 없기 때문입니다. StreamOutVB는 위의 StreamOutTech 패스, DrawVB는 DrawTech 패스에 사용됩니다.
게임 엔진과 그래픽스 엔진에서 파티클 시스템을 초기화, Update, Draw, 해제하는 흐름은 다음과 같습니다.




파티클 시스템 매니저의 Update()에서는 공유 데이터에 접근해, 활성화되어 있는지 확인하고 활성화되어 있으면 큐에 넣습니다.
Draw()는 큐에 들어있는 파티클 시스템을 모두 그립니다.
void ParticleSystemManager::Update(float dTime, float totalTime)
{
// 활성화되어있는지 확인하고,
// Queue에 먼저 담는다.
for (const auto& it : m_ParticleSystemVec)
{
// 활성화되어 있는지
if (it->GetParticleSystemData()->m_IsActive)
{
// 리셋 해야한다면
if (it->GetParticleSystemData()->m_IsReset)
{
it->Reset();
it->GetParticleSystemData()->m_IsReset = false;
}
// 활성화되어있으면 업데이트
it->Update(dTime, totalTime);
// 후에 큐에 넣어줌
m_ParticleSystemQueue.push(it.get());
}
}
}
void ParticleSystemManager::Draw(const EMath::Matrix& view, const EMath::Matrix& proj)
{
// Queue를 순회하며 Draw 시킨다.
while (!m_ParticleSystemQueue.empty())
{
ParticleSystem* ps = m_ParticleSystemQueue.front();
ps->Draw(view, proj);
m_ParticleSystemQueue.pop();
}
}
참고
- DirectX 11을 이용한 3D 게임 프로그래밍 입문 책
'DirectX 자체엔진 개발' 카테고리의 다른 글
[DirectX 11] HDR, Tone Mapping (0) | 2022.04.15 |
---|---|
[DirectX 11] 외곽선 (0) | 2022.04.14 |
[DirectX 11] Emissive (0) | 2022.03.14 |
[DirectX 11] Bloom (1) | 2022.03.11 |
[DirectX 11] 자체 포맷 개발 (0) | 2022.02.25 |