[DirectX] Particle 2


파티클의 개수가 늘어날 경우를 고려해보자

Particle의 입자가 늘어날 때


maxparticlecount가 1000개로 늘어나게 된다면?

particle의 rendering을 1000번 진행하게 된다.

파티클을 칠하는 렌더링 파이프라인 전체 과정을 1000번 진행하기 때문에 프레임 드랍이 일어나게 된다.

그래서 최적화를 생각해두어야 한다.

현재 입자들은 모양은 같고 위치만 다르다.

같은 메쉬와 재질을 참조하고 있는 개체들을 묶어서 렌더링 하는것과 같은 원리로 최적화가 가능하지 않을까?

render_particle


그래서 입자를 렌더링하는 전용 렌더링 함수를 만들 것이다.

// CMesh.cpp 중에서 render_particle 함수

void CMesh::render_particle(UINT _iParticleCount)
{
	UpdateData();

	// 인스턴싱
	CONTEXT->DrawIndexedInstanced(m_IdxCount, _iParticleCount, 0, 0, 0);
}

DrawIndexedInstanced는 파티클의 개수만큼, 즉 내가 지정한 횟수만큼 렌더를 반복하고 렌더링을 종료하겠다는 의미이다.

보통 처럼 렌더링 1회차 준비 - 렌더링 - 렌더링 종료 - 렌더링 2회차 준비 - 렌더링 - 렌더링 종료… 이렇게 하는 것이 아니라,

렌더링 준비 - 렌더링 1000회 - 렌더링 종료 이렇게 과정이 진행되는 것이다.

이런 과정을 인스턴싱이라 한다.

그래서 CParticleSystem.cpp에 있는 render함수의 반복문은 없어지게 된다.

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

	m_ParticleBuffer->UpdateData(20, PIPELINE_STAGE::PS_ALL);

	// Particle Render	
	GetMaterial()->UpdateData();
	GetMesh()->render_particle(m_iMaxParticleCount);
}


인덱스 체크


그리고 render_particle 함수 내부에서 인덱스를 체크할 수 있도록 조치를 취해야 한다.

// particle_render.fx 중에서

struct VS_IN
{
    float3 vPos : POSITION;
    float2 vUV : TEXCOORD;
    uint iInstID : SV_InstanceID;
};

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[_in.iInstID].vWorldScale.xyz + ParticleBuffer[_in.iInstID].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;
}


컴퓨트 쉐이더를 작성해서 입자를 움직여보기


컴퓨트 쉐이더를 통해서 파티클(입자)를 업데이트 시켜보자.

ComputeShader를 상속받아서 입자를 업데이트할 쉐이더 클레스를 만들어줘야 한다.

CParticleUpdateShader 클래스의 구성


// 헤더파일

#pragma once
#include "CComputeShader.h"

class CStructuredBuffer;

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

public:
    void SetParticleBuffer(CStructuredBuffer* _Buffer);

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

public:
    CParticleUpdateShader(UINT _iGroupPerThreadX, UINT _iGroupPerThreadY, UINT _iGroupPerThreadZ);
    ~CParticleUpdateShader();
};


// cpp 파일

#include "pch.h"
#include "CParticleUpdateShader.h"

#include "CStructuredBuffer.h"

CParticleUpdateShader::CParticleUpdateShader(UINT _iGroupPerThreadX, UINT _iGroupPerThreadY, UINT _iGroupPerThreadZ)
	: m_ParticleBuffer(nullptr)
{
	m_iGroupPerThreadX = _iGroupPerThreadX;
	m_iGroupPerThreadY = _iGroupPerThreadY;
	m_iGroupPerThreadZ = _iGroupPerThreadZ;
}

CParticleUpdateShader::~CParticleUpdateShader()
{

}

void CParticleUpdateShader::SetParticleBuffer(CStructuredBuffer* _Buffer)
{
	m_ParticleBuffer = _Buffer;
	m_Const.arrInt[0] = m_ParticleBuffer->GetElementCount();
}

void CParticleUpdateShader::UpdateData()
{
	m_ParticleBuffer->UpdateData_CS(0);

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

void CParticleUpdateShader::Clear()
{
	m_ParticleBuffer->Clear_CS();
}

그리고 리소스 매니저에서 파티클 업데이터 쉐이더를 컴퓨트 쉐이더로 통해서 생성을 시켜준다.

// CResMgr.cpp 중에서

	// Particle Update 쉐이더
	pCS = new CParticleUpdateShader(128, 1, 1);
	pCS->SetKey(L"ParticleUpdateCS");
	pCS->CreateComputeShader(L"shader\\particle_update.fx", "CS_ParticleUpdate");
	AddRes(pCS->GetKey(), pCS);

이 때, hlsl 쉐이더 코드에서 스레드의 구성을 (128, 1, 1)로 했으므로 이를 따라서 업데이트 쉐이더를 생성시킨다.

그리고 ParticleSystem 클래스에서도 참조할 업데이트 쉐이더를 멤버변수로 추가해준다.

// CParticleSystem.h 중에서

    Ptr<CParticleUpdateShader>  m_UpdateCS;
// CParticleSystem의 생성자 중에서

	// 파티클 전용 재질
	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_iMaxParticleCount, SB_TYPE::READ_WRITE, false, arrParticle);	

파티클을 업데이트 시키려면 파티클의 정보를 담고 있는 구조화 버퍼를 입력받아야 한다.

이 기능을 하는 것이 CParticleUpdateShader에 있는 m_ParticleBuffer 포인터이다.

finaltick 함수에서 연산을 진행한 다음에 render을 진행하는 것이다.

또한, 버퍼가 오버되지 않도록 파티클의 g_int_0을 통해서 최대 개수도 전달을 해줘야 한다.

이렇게 하면 초과 인덱스에 해당하는 버퍼에 해당하는 경우는 hlsl 코드 상에서 더 이상 진행되지 않고 바로 return된다.

// particle_update.fx 중에서

#define ParticleMaxCount g_int_0

[numthreads(128, 1, 1)]
void CS_ParticleUPdate(int3 _ID : SV_DispatchThreadID)
{
    if (ParticleMaxCount <= _ID.x)
        return;
}

CStructuredBuffer의 재구성


그리고 구조화 버퍼의 코드도 아래와 같이 수정한다.

최근에 바인딩 했던 곳을 저장할 변수도 추가해주고, 버퍼를 비워주는 Clear 함수도 추가해준다.

이 때, 텍스쳐(SRV 등)가 사용할 일반버전 Clear함수와 UAV가 사용할 Clear_CS 함수를 구현해준다.

// 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;

    UINT                                m_iRecentRegisterNum;

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);
    void UpdateData_CS(UINT _iRegisterNum);

    void Clear();
    void Clear_CS();

    UINT GetElementSize() { return m_iElementSize; }
    UINT GetElementCount() { return m_iElementCount; }
    UINT GetBufferSize() { return m_iElementSize * m_iElementCount;}


    CLONE_DISABLE(CStructuredBuffer);
public:
    CStructuredBuffer();
    ~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, 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);
		}
	}
}

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());
}

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, GetBufferSize());
	CONTEXT->Unmap(m_SB_CPU_Read.Get(), 0);
}

void CStructuredBuffer::UpdateData(UINT _iRegisterNum, UINT _iPipeLineStage)
{
	m_iRecentRegisterNum = _iRegisterNum;

	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());
	}
}

void CStructuredBuffer::UpdateData_CS(UINT _iRegisterNum)
{
	m_iRecentRegisterNum = _iRegisterNum;

	UINT i = -1;
	CONTEXT->CSSetUnorderedAccessViews(_iRegisterNum, 1, m_UAV.GetAddressOf(), &i);
}

void CStructuredBuffer::Clear()
{
	ID3D11ShaderResourceView* pSRV = nullptr;
	CONTEXT->VSSetShaderResources(m_iRecentRegisterNum, 1, &pSRV);
	CONTEXT->HSSetShaderResources(m_iRecentRegisterNum, 1, &pSRV);
	CONTEXT->DSSetShaderResources(m_iRecentRegisterNum, 1, &pSRV);
	CONTEXT->GSSetShaderResources(m_iRecentRegisterNum, 1, &pSRV);
	CONTEXT->PSSetShaderResources(m_iRecentRegisterNum, 1, &pSRV);
	CONTEXT->CSSetShaderResources(m_iRecentRegisterNum, 1, &pSRV);

}

void CStructuredBuffer::Clear_CS()
{
	ID3D11UnorderedAccessView* pUAV = nullptr;
	UINT i = -1;
	CONTEXT->CSSetUnorderedAccessViews(m_iRecentRegisterNum, 1, &pUAV, &i);
}


예시 적용해보기


흩어지는 파티클의 구현


// CParticleSystem.cpp 중에서

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

for (UINT i = 0; i < 100; ++i)
{
    arrParticle[i].vWorldPos = Vec3(fRadius * cosf(fAngle * (float)i), sinf(fAngle * (float)i));
}

// particle_update.fx

#ifndef _PARTICLE_UPDATE
#define _PARTICLE_UPDATE

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

RWStructuredBuffer<tParticle> ParticleBuffer : register(u0);

#define ParticleMaxCount g_int_0

[numthreads(128, 1, 1)]
void CS_ParticleUpdate(int3 _ID : SV_DispatchThreadID)
{
    if (ParticleMaxCount <= _ID.x)
        return;
    
    tParticle particle = ParticleBuffer[_ID.x];
    
    particle.vWorldPos += particle.vVelocity * g_DT;    
    
    ParticleBuffer[_ID.x] = particle;
}

#endif



© 2022.07. by Wookey_Kim

Powered by Hydejack v7.5.2