[DirectX] Particle과 Compute Shader


공유 버퍼를 활용해보자

SharedBuffer


파티클 전체가 공유하는 데이터를 넣어줄 공유 버퍼(Shared Buffer)를 만들 것이다.

CParticleSystem 클래스


// struct.h 중에서

struct tRWParticleBuffer
{
	int		SpawnCount;  	// 이번 프레임에 스폰할 파티클 개수
	int		padding[3];
};

struct tParticleModule;
{
	// 스폰 모듈
	Vec4	vSpawnColor; 	// 스폰 시 색상
	Vec4	vSpawnScale; 	// 스폰 시 크기
	Vec3	vBoxShapeScale;
	int		iMaxParticleCount; // 파티클 최대 개수
	float   fSphereShapeRadius;
	int 	SpawnShapeType; // Sphere 타입, Box 타입 여부 나누기


	// 색상 변경 모듈
	Vec4	vStartColor; 	// 초기 색상
	Vec4	vEndColor;	 	// 최종 색상

	// 크기 변경 모듈
	Vec4	vStartScale; 	// 초기 크기
	Vec4	vEndScale;	 	// 최종 크기

	// 모듈 작동여부 체크
	int		ModuleCheck[(UINT)PARTICLE_MODULE::END];
};
// define.h 중에서

enum class PARTICLE_MODULE
{
	PARTICLE_SPAWN,
	COLOR_CHANGE,
	SCALE_CHANGE,

	END,
};
// CParticleSystem.h 중에서

private:
	CStructuredBuffer*		   m_ParticleBuffer;
	CStructuredBuffer*		   m_RWBuffer;

	tParticleModule			   m_ModuleData;
	Ptr<CParticleUpdateShader> m_UpdateCS;
// CParticleSystem.cpp

#include "pch.h"
#include "CParticleSystem.h"

#include "CDevice.h"
#include "CStructuredBuffer.h"

#include "CResMgr.h"
#include "CTransform.h"

#include "CTimeMgr.h"

CParticleSystem::CParticleSystem()
	: CRenderComponent(COMPONENT_TYPE::PARTICLESYSTEM)
	, m_ParticleBuffer(nullptr)
	, m_RWBuffer(nullptr)
	, m_ModuleData{}
	, m_AccTime(0.f)
{
	m_ModuleData.iMaxParticleCount = 100;
	m_ModuleData.SpawnRate = 10;


	// 입자 메쉬
	SetMesh(CResMgr::GetInst()->FindRes<CMesh>(L"PointMesh"));

	// 파티클 전용 재질
	SetMaterial(CResMgr::GetInst()->FindRes<CMaterial>(L"ParticleRenderMtrl"));

	// 파티클 업데이트 컴퓨트 쉐이더	
	m_UpdateCS = (CParticleUpdateShader*)CResMgr::GetInst()->FindRes<CComputeShader>(L"ParticleUpdateCS").Get();

	// 파티클 버퍼 초기 데이터
	tParticle arrParticle[100] = { };
	float fAngle = XM_2PI / 100.f;
	float fRadius = 20.f;
	float fSpeed = 100.f;

	for (UINT i = 0; i < 100; ++i)
	{
		arrParticle[i].vWorldPos = Vec3(fRadius * cosf(fAngle * (float)i), fRadius * sinf(fAngle * (float)i), 100.f);
		arrParticle[i].vVelocity = arrParticle[i].vWorldPos;
		arrParticle[i].vVelocity.z = 0.f;
		arrParticle[i].vVelocity.Normalize();
		arrParticle[i].vVelocity *= fSpeed;
		arrParticle[i].vWorldScale = Vec3(10.f, 10.f, 1.f);
	}
	
	m_ParticleBuffer = new CStructuredBuffer;
	m_ParticleBuffer->Create(sizeof(tParticle), m_ModuleData.iMaxParticleCount, SB_TYPE::READ_WRITE, false, arrParticle);

	m_RWBuffer = new CStructuredBuffer;
	m_RWBuffer->Create(sizeof(tRWParticleBuffer), 1, SB_TYPE::READ_WRITE, true);

	m_ModuleDataBuffer = new CStructuredBuffer;
	m_ModuleDataBuffer->Create(sizeof(tParticleModule), 1, SB_TYPE::READ_ONLY, true);
}

CParticleSystem::~CParticleSystem()
{
	if (nullptr != m_ParticleBuffer)
		delete m_ParticleBuffer;

	if (nullptr != m_RWBuffer)
		delete m_RWBuffer;

	if (nullptr != m_ModuleDataBuffer)
		delete m_ModuleDataBuffer;
}


void CParticleSystem::finaltick()
{
	// 스폰 레이트 계산
	// 1개 스폰 시간
	float fTimePerCount = 1.f / (float)m_ModuleData.SpawnRate;
	m_AccTime += DT;

	// 누적시간이 개당 생성시간을 넘어서면
	if (fTimePerCount < m_AccTime)
	{
		// 초과 배율 ==> 생성 개수
		float fData = m_AccTime / fTimePerCount;

		// 나머지는 남은 시간
		m_AccTime = fTimePerCount * (fData - floor(fData));

		tRWParticleBuffer rwbuffer = { (int)fData, };
		m_RWBuffer->SetData(&rwbuffer);
	}


	// 파티클 업데이트
	m_ModleDataBuffer->SetData(&m_ModuleData);

	m_UpdateCS->SetParticleBuffer(m_ParticleBuffer);
	m_UpdateCS->SetRWParticleBuffer(m_RWBuffer);
	m_UpdateCS->SetModuleData(m_ModuleDataBuffer);

	m_UpdateCS->Execute();
}

void CParticleSystem::render()
{
	Transform()->UpdateData();

	// 파티클버퍼 t20 에 바인딩
	m_ParticleBuffer->UpdateData(20, PIPELINE_STAGE::PS_ALL);

	// Particle Render	
	GetMaterial()->UpdateData();
	GetMesh()->render_particle(m_ModuleData.iMaxParticleCount);

	// 파티클 버퍼 바인딩 해제
	m_ParticleBuffer->Clear();
}

m_ParticleBuffer와 m_RWBuffer, 그리고 m_ModuleDataBuffer을 구조체 버퍼로 만들어준다.

  • m_ParticleBuffer는 READ_WRITE 용도로, 1개 당 파티클의 크기로, 파티클의 최대 개수만큼 만들어준다.
  • m_RWBuffer는 READ_WRITE용도로, RW파티클버퍼의 크기로, 1개를 형성시킨다.
  • m_ModuleDataBuffer는 READ_ONLY로 생성한다.

소멸자에서 m_ParticleBuffer와 m_RWBuffer를 할당 해제시켜주는 작업을 하고,

finaltick에서 파티클에 대한 업데이트를 진행하기 위한 모듈을 작동시킨다.

Particle_update.fx 와 CParticleUpdateShader 클래스


우선 바인딩을 위한 레지스터를 아래와 같이 등록해준다.

// Particle_update.fx 중에서

#ifndef _PARTICLE_UPDATE
#define _PARTICLE_UPDATE

#include "value.fx"
#include "struct.fx"

StructuredBuffer<tParticle> ParticleBuffer : register(u0);
StructuredBuffer<int4> ParticleSpawnCount : register(u1);
StructuredBuffer<tParticleModule> ParticleModuleData : register(t20);

#define ParticleMaxCount g_int_0
#define SpawnCount ParticleSpawnCount[0].x
#define ModuleData ParticleModuleData[0]

...

그리고 struct.fx에서 자료를 맞추어준다.

// struct.fx 중에서

struct tParticleModule;
{
	// 스폰 모듈
	float4	vSpawnColor; 	// 스폰 시 색상
	float4	vSpawnScale; 	// 스폰 시 크기
	float3	vBoxShapeScale;
	int		iMaxParticleCount; // 파티클 최대 개수
	float   fSphereShapeRadius;
	int 	SpawnShapeType; // Sphere 타입, Box 타입 여부 나누기


	// 색상 변경 모듈
	float4	vStartColor; 	// 초기 색상
	float4	vEndColor;	 	// 최종 색상

	// 크기 변경 모듈
	float4	vStartScale; 	// 초기 크기
	float4	vEndScale;	 	// 최종 크기

	// 모듈 작동여부 체크
	int		ModuleCheck[3];
};

그리고 바인딩을 진행한다.

컴퓨트 쉐이더를 실행하기 직전에 알맞은 레지스터 번호에서 업데이트를 진행한다.

컴퓨트 쉐이더에서도 쉐이더 리소스 뷰를 텍스쳐 레지스터로 보낼 수 있도록 구성을 수정해준다.

// CParticleUpdateShader.h 중에서

#pragma once
#include "CComputeShader.h"

class CStructuredBuffer;

class CParticleUpdateShader :
    public CComputeShader
{
private:
    CStructuredBuffer*  m_ParticleBuffer;
    CStructuredBuffer*  m_RWBuffer;
    CStructuredBuffer*  m_ModuleData;


public:
    void SetParticleBuffer(CStructuredBuffer* _Buffer);
    void SetRWParticleBuffer(CStructuredBuffer* _Buffer) {m_RWBuffer = _Buffer;}
    void SetModuleData(CStructuredBuffer* _Buffer) {m_ModuleData = _Buffer;}

public:
    virtual void UpdateData() override;
    virtual void Clear() override;

public:
    CParticleUpdateShader(UINT _iGroupPerThreadX, UINT _iGroupPerThreadY, UINT _iGroupPerThreadZ);
    ~CParticleUpdateShader();
};
// CParticleUpdateShader.cpp 중에서 UpdateData함수와 Claer 함수

void CParticleUpdateShader::UpdateData()
{
	m_ParticleBuffer->UpdateData_CS(0, false);
	m_RWBuffer->UpdateData_CS(1, false);
	m_ModuleData->UpdateData_CS(20, true);

	// 그룹 수
	m_iGroupX = (m_ParticleBuffer->GetElementCount() / m_iGroupPerThreadX) + 1;
}

void CParticleUpdateShader::Clear()
{
	m_ParticleBuffer->Clear_CS(false);
	m_RWBuffer->Clear_CS(false);
	m_ModuleData->Clear_CS(true);
}
// CStructuredBuffer.cpp 중에서 UpdateData_CS 함수

void CStructuredBuffer::UpdateData_CS(UINT _iRegisterNum, bool _IsShaderRes)
{
	m_iRecentRegisterNum = _iRegisterNum;
	
	if (_IsShaderRes)
	{
		CONTEXT->CSSetShaderResources(_iRegisterNum, 1, m_SRV.GetAddressOf());
	}
	else
	{
		UINT i = -1;
		CONTEXT->CSSetUnorderedAccessViews(_iRegisterNum, 1, m_UAV.GetAddressOf(), &i);
	}	
}


적용해보기


CParticleSystem.cpp의 생성자에 코드를 다음과 같이 적용해보자.

CParticleSystem::CParticleSystem()
	: CRenderComponent(COMPONENT_TYPE::PARTICLESYSTEM)
	, m_ParticleBuffer(nullptr)
	, m_RWBuffer(nullptr)
	, m_ModuleData{}
	, m_AccTime(0.f)
{
	m_ModuleData.iMaxParticleCount = 100;
	m_ModuleData.SpawnRate = 10;


	// 입자 메쉬
	SetMesh(CResMgr::GetInst()->FindRes<CMesh>(L"PointMesh"));

	// 파티클 전용 재질
	SetMaterial(CResMgr::GetInst()->FindRes<CMaterial>(L"ParticleRenderMtrl"));

	// 파티클 업데이트 컴퓨트 쉐이더	
	m_UpdateCS = (CParticleUpdateShader*)CResMgr::GetInst()->FindRes<CComputeShader>(L"ParticleUpdateCS").Get();

	// 파티클 버퍼 초기 데이터
	tParticle arrParticle[100] = { };
	float fAngle = XM_2PI / 100.f;
	float fRadius = 20.f;
	float fSpeed = 100.f;

	for (UINT i = 0; i < 100; ++i)
	{
		arrParticle[i].vWorldPos = Vec3(fRadius * cosf(fAngle * (float)i), fRadius * sinf(fAngle * (float)i), 100.f);
		arrParticle[i].vVelocity = arrParticle[i].vWorldPos;
		arrParticle[i].vVelocity.z = 0.f;
		arrParticle[i].vVelocity.Normalize();
		arrParticle[i].vVelocity *= fSpeed;
		arrParticle[i].vWorldScale = Vec3(10.f, 10.f, 1.f);
		arrParticle[i].age = -1;
	}
	
	m_ParticleBuffer = new CStructuredBuffer;
	m_ParticleBuffer->Create(sizeof(tParticle), m_ModuleData.iMaxParticleCount, SB_TYPE::READ_WRITE, false, arrParticle);

	m_RWBuffer = new CStructuredBuffer;
	m_RWBuffer->Create(sizeof(tRWParticleBuffer), 1, SB_TYPE::READ_WRITE, true);

	m_ModleDataBuffer = new CStructuredBuffer;
	m_ModleDataBuffer->Create(sizeof(tParticleModule), 1, SB_TYPE::READ_ONLY, true);
}

그리고 finaltick함수에서 프레임 당 생성개수를 연산해줘야 한다.

	// 스폰 레이트 계산
	// 1개 당 스폰 소요 시간
	float fTimePerCount = 1.f / (float)m_ModuleData.SpawnRate;
	m_AccTime += DT;

	if (fTimePerCount < m_AccTime)
	{
		m_AccTime = m_AccTime - fTimePerCount;
		tRWParticleBuffer rwbuffer = {1, };
		m_RWBuffer->SetData(&rwbuffer);
	}

또한, Spawnrate가 FPS보다 더 큰 값을 가지는 경우도 대비해야한다.

그래서 동시에 몇개를 생성해야하는지도 체크해주어야 한다.

그래서 코드를 아래와 같이 작성한다.

	// 스폰 레이트 계산
	// 1개 당 스폰 소요 시간
	float fTimePerCount = 1.f / (float)m_ModuleData.SpawnRate;
	m_AccTime += DT;

	// 누적시간이 개당 생성시간을 넘어섰을 때
	if (fTimePerCount < m_AccTime)
	{
		// 초과 배율 ==> 생성 개수
		float fData = m_AccTime / fTimePerCount;

		// 나머지는 남은 시간
		m_AccTime = fTimePerCount * (fData - floor(fData));

		tRWParticleBuffer rwbuffer = { (int)fData, };
		m_RWBuffer->SetData(&rwbuffer);
	}

여기서 floor함수는 math에 내장된 함수로, 실수의 정수부만을 가져온다.

파티클을 모두 비활성화 시키고, (Age = -1 적용) 쉐이더 코드 파일을 아래와 같이 변경시키면?

// particle_update.fx 파일

#ifndef _PARTICLE_UPDATE
#define _PARTICLE_UPDATE

#include "value.fx"
#include "struct.fx"

RWStructuredBuffer<tParticle>       ParticleBuffer : register(u0);
RWStructuredBuffer<int4>            ParticleSpawnCount : register(u1);
StructuredBuffer<tParticleModule>   ParticleModuleData : register(t20);

#define ParticleMaxCount g_int_0
#define SpawnCount ParticleSpawnCount[0].x
#define ModuleData ParticleModuleData[0]


[numthreads(128, 1, 1)]
void CS_ParticleUpdate(int3 _ID : SV_DispatchThreadID)
{
    // 스레드 ID 가 파티클버퍼 최대 수를 넘긴경우 or 스레드 담당 파티클이 비활성화 상태인 경우
    if (ParticleMaxCount <= _ID.x)
        return;
        
    tParticle particle = ParticleBuffer[_ID.x];
        
    // 파티클이 비활성화 상태인 경우
    if (particle.Age < 0.f)
    {
        particle.Age = 0.f;
        particle.LifeTime = 10.f;
    }    
    
    // 파티클이 활성화인 경우
    else
    {
        // 속도에 따른 파티클위치 이동
        particle.vWorldPos += particle.vVelocity * g_DT;   
        
        // 파티클의 Age 에 시간을 누적시킴
        particle.Age += g_DT;
        
        // 파티클의 수명이 끝나면, 다시 비활성화 상태로 되돌림
        if(particle.LifeTime <= particle.Age)
        {
            particle.Age = -1.f;
        }
    }           
    
    
    // 변경점 적용
    ParticleBuffer[_ID.x] = particle;
}

#endif

파티클이 활성화된 상태인 경우 속도에 따라서 파티클이 이동하도록 한다.

반대로, 파티클이 비활성화 된 경우는 SpawnCount를 확인하고, SpawnCount가 0 이상이라면 파티클을 활성화 시킨다.

주의사항 : 병렬처리하는 GPU


이때, GPU는 병렬처리를 하는 하드웨어이다.

[numthreads(128, 1, 1)]는 128개의 스레드가 동시에 진행되고 있다는 것이다.

128개의 스레드가 동시에 SpawnCount를 체크하게 되며, 본인의 담당 파티클을 활성화 시킬 것이다.

하나씩 활성화 시키고 싶은데, 128개가 동시에 활성화되는 일이 벌어지는 것이다.

Atomic Function (원자단위함수)


원자단위함수는 쓰레드가 반드시 1개씩 실행되는 함수이다.

함수레퍼런스링크

여기서 InterlockedExchange함수를 사용해보자.

  • dest : 교체할 값의 목적지
  • value : dest에 새로이 넣어줄 값
  • originalvalue : 함수가 실행되기 전의 원래 값
// partice_update.fx 중에서

// 파티클이 비활성화 상태인 경우
    if (particle.Age < 0.f)
    {
        // SpawnCount 를 확인
        // 만약 SpawnCount 가 0 이상이라면, 파티클을 활성화시킴      
        while (0 < SpawnCount)
        {
            int orgvalue = SpawnCount;
            int outvalue = 0;
            InterlockedExchange(SpawnCount, SpawnCount - 1, outvalue);
            
            if(orgvalue == outvalue)
            {
                particle.Age = 0.f;
                particle.LifeTime = 10.f;
                break;
            }
        }
    }

SpawnCount가 0보다 크면 InterlockedExchange를 통해서 SpawnCount를 조정해준다.

SpawnCount가 2일 경우를 생각해보자.

while문 - InterlockedExchange를 통해서 100개의 쓰레드 중에서 2개의 쓰레드만 if문 안에 들어갈 수 있도록 만든다.

이렇게 해서 2개의 입자만 활성화 시킬 수 있다.

주의사항 : 예상보다 많은 파티클이 활성화되는 경우


가끔 SpawnCount 값이 커질 경우 InterlockedExchange 함수의 특성에 의해서 파티클이 예상보다 더 많이 활성화되는 경우가 있다.

이를 보완하기 위해서 InterlockedCompareExchange를 활용할 수 있다.

// partice_update.fx 중에서

// 파티클이 비활성화 상태인 경우
    if (particle.Age < 0.f)
    {
        // SpawnCount 를 확인
        // 만약 SpawnCount 가 0 이상이라면, 파티클을 활성화시킴      
        while (0 < SpawnCount)
        {
            int orgvalue = SpawnCount;
            int outvalue = 0;
            InterlockedCompareExchange(SpawnCount, orgvalue, SpawnCount - 1, outvalue);
            
            if(orgvalue == outvalue)
            {
                particle.Age = 0.f;
                particle.LifeTime = 10.f;
                break;
            }
        }
    }

위의 코드에서는 SpawnCount값과 orgvalue값이 일치할 때에만 교체를 진행하겠다는 의미이다.


© 2022.07. by Wookey_Kim

Powered by Hydejack v7.5.2