[DirectX] Particle Spawn Module


현재 파티클을 다루는 모듈들은 스폰 모듈, 색상 변경 모듈, 크기 변경 모듈이 있다.

여기서, 스폰 모듈을 다루고, 파티클의 모듈 활성화 여부에 따라 작동되도록 해보자.

Spawn Module


particle_update.fx 파일


#ifndef _PARTICLE_UPDATE
#define _PARTICLE_UPDATE

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

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

#define NoiseTexResolution  g_vec2_0

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

#define SpawnModule         ParticleModuleData[0].Spawn
#define ColorChangeModule   ParticleModuleData[0].ColorChange
#define ScaleChangeModule   ParticleModuleData[0].ScaleChange


[numthreads(128, 1, 1)]
void CS_ParticleUpdate(int3 _ID : SV_DispatchThreadID)
{
    // 스레드 ID 가 파티클버퍼 최대 수를 넘긴경우 or 스레드 담당 파티클이 비활성화 상태인 경우
    if (ParticleMaxCount <= _ID.x)
        return;
        
    tParticle particle = ParticleBuffer[_ID.x];
           
    if (SpawnModule)
    {
        // 파티클이 비활성화 상태인 경우
        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)
                {                   
                    // 랜덤 결과를 받을 변수
                    float3 vOut = (float3) 0.f;
                    
                    // 전체 유효 스레드의 아이디를 0 ~ 1 로 정규화
                    float fNormalizeThreadID = (float) _ID.x / (float) ParticleMaxCount;
                    GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID, vOut);
                    
                    particle.Age = 0.f;
                    particle.LifeTime = 10.f;
                    break;
                }
            }
        }
    }
           
    
    // 파티클이 활성화인 경우
    if(0.f <= particle.Age)
    {
        // 속도에 따른 파티클위치 이동
        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

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 = 1;
	m_ModuleData.ModuleCheck[(UINT)PARTICLE_MODULE::PARTICLE_SPAWN] = true;

	// 입자 메쉬
	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.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_ModuleDataBuffer->SetData(&m_ModuleData);

	m_UpdateCS->SetParticleBuffer(m_ParticleBuffer);
	m_UpdateCS->SetRWParticleBuffer(m_RWBuffer);
	m_UpdateCS->SetModuleData(m_ModuleDataBuffer);
	m_UpdateCS->SetNoiseTexture(CResMgr::GetInst()->FindRes<CTexture>(L"Noise_03"));

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

아래 코드를 true, false 값을 지정해서 모듈을 켜고 끌 수 있다.

m_ModuleData.ModuleCheck[(UINT)PARTICLE_MODULE::PARTICLE_SPAWN] = true;


HLSL의 Random 기능 부여


func.fx 파일에다가 랜덤함수를 구현해보자.

HLSL에서의 Random 사용


보통 상황에서 난수를 쓸 때에는 시간을 이용한다. (time.h 라이브러리)

시간을 키로하는 랜덤 값을 불러와서 시간에 따라서 다른 수가 출력되는 난수를 이용한다.

문제는 컴퓨트 쉐이더, 병렬 처리는 동 시간대에 진행돼서 같은 시간에는 모든 쓰레드가 같은 값을 참조하게 되는 것이다.

같은 시간이라도 쓰레드 별로 다른 값을 참조하도록 만들어줘야 한다.

노이즈 텍스쳐를 이용할 것이다.

노이즈 텍스쳐는 각 픽셀 별로 rgba 값이 불규칙하게 분포되어 있다.

이 텍스쳐의 특정 UV좌표에 있는 픽셀의 값을 추출해서 랜덤한 값을 가지고 올 수 있도록 만들 것이다.

우선, 노이즈 텍스쳐부터 로딩해보자.

// CResMgr.cpp 중에서

// NoiseTexture 로딩
Load<CTexture>(L"Noise_01", L"texture\\Noise\\noise_01.png");
Load<CTexture>(L"Noise_02", L"texture\\Noise\\noise_02.png");
Load<CTexture>(L"Noise_03", L"texture\\Noise\\noise_03.jpg");

// CParticleUpdateShader.h 중에서

private:
    Ptr<CTexture>   m_NoiseTex;

public:
    void SetNoiseTexture(Ptr<CTexture> _NoiseTex) { m_NoiseTex = _NoiseTex; }
// CParticleUpdateShader.cpp 중에서

m_NoiseTex->UpdateData_CS(21, true); // 21번 레지스터에 바인딩
// CParticleSystem.cpp 중에서

// 파티클 업데이트 컴퓨트 쉐이더
	m_ModuleDataBuffer->SetData(&m_ModuleData);

	m_UpdateCS->SetParticleBuffer(m_ParticleBuffer);
	m_UpdateCS->SetRWParticleBuffer(m_RWBuffer);
	m_UpdateCS->SetModuleData(m_ModuleDataBuffer);
	m_UpdateCS->SetNoiseTexture(CResMgr::GetInst()->FindRes<CTexture>(L"Noise_03"));

	m_UpdateCS->Execute();


그리고, 필터를 만들어보면 아래와 같다.

// ======
// Random
// ======
static float GaussianFilter[5][5] =
{
    0.003f,  0.0133f, 0.0219f, 0.0133f, 0.003f,
    0.0133f, 0.0596f, 0.0983f, 0.0596f, 0.0133f,
    0.0219f, 0.0983f, 0.1621f, 0.0983f, 0.0219f,
    0.0133f, 0.0596f, 0.0983f, 0.0596f, 0.0133f,
    0.003f,  0.0133f, 0.0219f, 0.0133f, 0.003f,
};

위와 같이 가우시안 필터가 나타나있다. 위의 값을 모두 합하면 1(100%)이 나오며, 중심을 기준으로 저 비율을 따라서 샘플링을 진행하겠다는 의미이다.

  • 정 가운데 값을 약 16퍼센트 참조할 것이다. (가장 큰 값)
  • 가장자리로 갈 수록 적은 비율로 참조할 것이다.

위의 필터를 이용해서 샘플링을 하는 함수를 구현해보자.

// particle_update.fx 중 에서

Texture2D NoiseTexture : register(t21);

#define NoiseTexResolution g_vec2_0

우선, 노이즈 텍스쳐와 바인딩할 레지스터 번호를 위와 같이 등록한다.

그리고 NoiseTexResolution 을 통해 텍스쳐의 해상도정보를 받아올 수 있도록 매크로를 지정한다.

그리고 func.fx에 함수를 구현한다.

필요한 입력 인자는 노이즈 텍스쳐, 해상도정보, 쓰레드의 ID 값, 아웃풋 정보이다.

// func.fx 중에서

void GaussianSample(in Texture2D _NoiseTex, float2 _vResolution, float _NomalizedThreadID, out float3 _vOut)
{
    float2 vUV = float2(_NomalizedThreadID, 0.5f);       
    
    vUV.x += g_AccTime * 0.5f;
    
    // sin 그래프로 텍스쳐의 샘플링 위치 UV 를 계산
    vUV.y -= (sin((_NomalizedThreadID - (g_AccTime/*그래프 우측 이동 속도*/)) * 2.f * 3.1415926535f * 10.f/*반복주기*/) / 2.f);
    
    if( 1.f < vUV.x)
        vUV.x = frac(vUV.x);
    else if(vUV.x < 0.f)
        vUV.x = 1.f + frac(vUV.x);
    
    if( 1.f < vUV.y)
        vUV.y = frac(vUV.y);
    else if (vUV.y < 0.f)
        vUV.y = 1.f + frac(vUV.y);
        
    int2 pixel = vUV * _vResolution;           
    int2 offset = int2(-2, -2);
    float3 vOut = (float3) 0.f;    
    
    for (int i = 0; i < 5; ++i)
    {
        for (int j = 0; j < 5; ++j)
        {            
            vOut += _NoiseTex[pixel + offset + int2(j, i)] * GaussianFilter[i][j];
        }
    }        
    
    _vOut = vOut;    
}

그리고 particle_update.fx 파일에서 아래와 같이 적용해본다.

    if (orgvalue == outvalue)
    {   
        particle.Active = 1;
        
        // 랜덤 결과를 받을 변수
        float3 vOut1 = (float3) 0.f;
        float3 vOut2 = (float3) 0.f;
        
        // 전체 유효 스레드의 아이디를 0 ~ 1 로 정규화
        float fNormalizeThreadID = (float) _ID.x / (float) ParticleMaxCount;
        GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID, vOut1);
        GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID + 0.1f, vOut2);
        
        // 반지름이 500인 원 이내에 랜덤으로 생성되도록 만들기
        float fRadius = 500.f; //vOut1.r * 200.f;
        float fAngle = vOut2.r * 2 * 3.1415926535f;                    
        //particle.vWorldPos.xyz = float3(fRadius * cos(fAngle), fRadius * sin(fAngle), 100.f);                    
        
        particle.vWorldPos.xyz = float3(fRadius * vOut1.r - fRadius * 0.5f, fRadius * vOut2.r - fRadius * 0.5f, 100.f);
        particle.vWorldScale.xyz = float3(10.f, 10.f, 1.f);
        
        particle.Age = 0.f;
        particle.LifeTime = 10.f;
        break;
    }

쓰레드 자신의 고유한 아이디 값을 최대 파티클의 수로 나누어서 정규화를 한다.

그리고 가우시안 샘플에서 정규화된 ID값을 이용해서 UV값을 랜덤하게 추출한다.

추출 하는 원리는 샘플링 위치를 누적 시간 값과 사인(Sin) 그래프를 이용해서 시간에 따라서 다르게 지정을 하는 것이다.

좌표 상에서 X 값은 고정이지만, Y 값은 달라지는 것이다.

그래서 시간마다 랜덤한 위치를 지정해서 샘플링을 진행할 수 있다.

또한, UV좌표가 1을 초과하거나, 0보다 작아질 경우 상황에 따라서 UV좌표를 조정해준다.

그리고 텍스쳐의 해당 UV 좌표에 있는 r, g, b 값을 랜덤하게 불러낼 수 있게 된다.

그리고 텍스쳐의 색상을 참조해서 난수를 추출하므로 텍스쳐의 색상이 픽셀 별로 고르게 분포되어 있지 않다면 랜덤값이 의도와 다르게 특정 구간에 쏠려서 나오게 된다.

Spawn Module의 개선


Age가 아니라 활성화 여부를 Particle의 속성에 추가해주자.

// struct.h 중에서

// Particle
struct tParticle
{
    Vec4    vLocalPos;      // 파티클의 로컬 포지션
	Vec4	vWorldPos;		// 파티클의 최종 위치
	Vec4	vWorldScale;	// 파티클 크기
	Vec4	vColor;			// 파티클 색상
	Vec4	vVelocity;		// 파티클 현재 속도
	Vec4	vForce;			// 파티클에 주어진 힘

	float   Age;			// 생존 시간
	float   NomalizedAge;	// 수명대비 생존시간을 0~1로 정규화 한 값
	float	LifeTime;		// 수명
	float	Mass;			// 질량

	int     Active;			// 파티클 활성화 여부
	int     pad[3];
};

Active의 값이 0인가 여부에 따라서 비활성화, 활성화를 결정할 것이다.

또한, 파티클의 크기도 랜덤으로 배율로 결정을 해줄것이다.

그래서 vStartScale, vEndScale이 Vec4로부터 float로 자료형을 바꿔준다.

// struct.h 중에서

struct tParticleModule
{
	// 스폰 모듈
	Vec4    vSpawnColor;
	Vec4	vSpawnScaleMin;
	Vec4	vSpawnScaleMax;
	Vec3	vBoxShapeScale;	
	float	fSphereShapeRadius;
	int		SpawnShapeType;		// 0 : Box, 1 : Sphere 
	int		SpawnRate;			// 초당 생성 개수
    int     Space;              // 파티클 업데이트 좌표계 (0 : World, 1 : Local)
	int     spawnpad[1];

	// Color Change 모듈
	Vec4	vStartColor;		// 초기 색상
	Vec4	vEndColor;			// 최종 색상

	// Scale Change 모듈
	float	vStartScale;		// 초기 배율
	float	vEndScale;			// 최종 배율	

	// Module Check
	int		ModuleCheck[(UINT)PARTICLE_MODULE::END];

	// 버퍼 최대크기
	int		iMaxParticleCount;
	int		ipad;
};

그리고 SpawnShapeType가 있는데 이 스폰 모양에 따라서 원형으로 스폰될지, 네모모양으로 스폰될지를 결정할 것이다.

그래서 particle_update.fx의 hlsl코드는 아래와 같이 바뀔 수 있다.

// particle_update.fx 중에서

if (SpawnModule)
    {
        // 파티클이 비활성화 상태인 경우
        if (particle.Active == 0)
        {
            // SpawnCount 를 확인
            // 만약 SpawnCount 가 0 이상이라면, 파티클을 활성화시킴      
            while (0 < SpawnCount)
            {
                int orgvalue = SpawnCount;
                int outvalue = 0;
                InterlockedCompareExchange(SpawnCount, orgvalue, SpawnCount - 1, outvalue);
            
                if (orgvalue == outvalue)
                {   
                    particle.Active = 1;
                    
                    // 랜덤 결과를 받을 변수
                    float3 vOut1 = (float3) 0.f;
                    float3 vOut2 = (float3) 0.f;
                    float3 vOut3 = (float3) 0.f;
                    
                    // 전체 유효 스레드의 아이디를 0 ~ 1 로 정규화
                    float fNormalizeThreadID = (float) _ID.x / (float) ParticleMaxCount;
                    GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID, vOut1);
                    GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID + 0.1f, vOut2);
                    GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID + 0.2f, vOut3);
                    
                    // Box 스폰
                    if (ModuleData.SpawnShapeType == 0)
                    {                           
                        particle.vLocalPos.xyz = float3(ModuleData.vBoxShapeScale.x * vOut1.r - ModuleData.vBoxShapeScale.x * 0.5f
                                                      , ModuleData.vBoxShapeScale.y * vOut2.r - ModuleData.vBoxShapeScale.y * 0.5f
                                                      , ModuleData.vBoxShapeScale.z * vOut3.r - ModuleData.vBoxShapeScale.z * 0.5f);                        
                        particle.vWorldPos.xyz = particle.vLocalPos.xyz + ObjectPos.xyz;
                        
                        
                        // 스폰 크기 범위내에서 랜덤 크기로 지정 (Min, Max 가 일치하면 고정크기)
                        float4 vSpawnScale = ModuleData.vSpawnScaleMin + (ModuleData.vSpawnScaleMax - ModuleData.vSpawnScaleMin) * vOut3.x;                                                
                        particle.vWorldScale.xyz = vSpawnScale.xyz;
                    }      
                    
                    // Sphere 스폰
                    else if (ModuleData.SpawnShapeType == 1)
                    {
                        float fRadius = 500.f; //vOut1.r * 200.f;
                        float fAngle = vOut2.r * 2 * 3.1415926535f;
                        //particle.vWorldPos.xyz = float3(fRadius * cos(fAngle), fRadius * sin(fAngle), 100.f);
                    }
                    
                    
                    particle.vColor = ModuleData.vSpawnColor;                    
                                      
                    particle.Age = 0.f;
                    particle.LifeTime = ModuleData.MinLifeTime + (ModuleData.MaxLifeTime - ModuleData.MinLifeTime) * vOut2.r;
                    break;
                }
            }
        }
    }

그리고 오브젝트의 위치에 따라서 파티클이 스폰될 수 있도록 만들어줄 필요가 있다.

그래서 오브젝트의 위치, 특히 부모 오브젝트의 값을 받아와야 한다.

#define ObjectPos       g_vec4_0

그리고 CParticleUpdateShader 클래스에서 포지션을 세팅해주고, CParticleSystem 클래스에서 부모 오브젝트의 WorldPosition을 추출해서 세팅을 해줄 것이다.

// CParticleUpdateShader.h 중에서

#pragma once
#include "CComputeShader.h"

#include "ptr.h"
#include "CTexture.h"

class CStructuredBuffer;

class CParticleUpdateShader :
    public CComputeShader
{
private:
    CStructuredBuffer*  m_ParticleBuffer;
    CStructuredBuffer*  m_RWBuffer;
    CStructuredBuffer*  m_ModuleData;
    Ptr<CTexture>       m_NoiseTex;


public:
    void SetParticleBuffer(CStructuredBuffer* _Buffer);
    void SetRWParticleBuffer(CStructuredBuffer* _Buffer) {m_RWBuffer = _Buffer;}
    void SetModuleData(CStructuredBuffer* _Buffer) {m_ModuleData = _Buffer;}
    void SetNoiseTexture(Ptr<CTexture> _tex)
    { 
        m_NoiseTex = _tex; 
        m_Const.arrV2[0] = Vec2(m_NoiseTex->Width(), m_NoiseTex->Height());
    }

    void SetParticleObjectPos(Vec3 _vPos) { m_Const.arrV4[0] = _vPos; }

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

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

// 파티클 업데이트 컴퓨트 쉐이더
	m_ModuleDataBuffer->SetData(&m_ModuleData);

	m_UpdateCS->SetParticleBuffer(m_ParticleBuffer);
	m_UpdateCS->SetRWParticleBuffer(m_RWBuffer);
	m_UpdateCS->SetModuleData(m_ModuleDataBuffer);
	m_UpdateCS->SetNoiseTexture(CResMgr::GetInst()->FindRes<CTexture>(L"Noise_01"));
	m_UpdateCS->SetParticleObjectPos(Transform()->GetWorldPos());

	m_UpdateCS->Execute();


파티클에 여러 효과를 입혀보기


파티클에다가 텍스쳐를 적용해보자.


// CParticleSystem.cpp 중에서 render 함수

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

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

	// Particle Render	
	Ptr<CTexture> pParticleTex = CResMgr::GetInst()->Load<CTexture>(L"Particle_0", L"texture\\particle\\AlphaCircle.png");
	GetMaterial()->SetTexParam(TEX_0, pParticleTex);

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

	// 파티클 버퍼 바인딩 해제
	m_ParticleBuffer->Clear();
}
// Particle_render.fx 중에서 텍스쳐 체크 구간

float4 PS_ParticleRender(GS_OUT _in) : SV_Target
{   
    float4 vOutColor = float4(1.f, 0.f, 1.f, 1.f);
    
    if(g_btex_0)
    {
        vOutColor = g_tex_0.Sample(g_sam_0, _in.vUV);        
        vOutColor.rgb *= ParticleBuffer[_in.iInstID].vColor.rgb;
    }
    
    return vOutColor;
}


파티클의 크기변화 모듈을 켜보자.


// CParticleSystem.cpp 중에서 생성자 함수

	m_ModuleData.vSpawnScaleMin = Vec3(15.f, 15.f, 1.f);
	m_ModuleData.vSpawnScaleMax = Vec3(50.f, 50.f, 1.f);

	m_ModuleData.ModuleCheck[(UINT)PARTICLE_MODULE::SCALE_CHANGE] = true;
	m_ModuleData.StartScale = 2.f;
	m_ModuleData.EndScale = 0.1f;
// particle_update.fx 중에서

	// 크기 변화 모듈이 활성화 되어있으면
	if(ModuleData.ScaleChange)
		particle.ScaleFactor = ModuleData.StartScale + particle.NomalizedAge * (ModuleData.EndScale - ModuleData.StartScale);                    
	else
		particle.ScaleFactor = 1.f;


파티클의 색상변화 모듈을 켜보자


// CParticleSystem.cpp 중에서 생성자 함수

	m_ModuleData.ModuleCheck[(UINT)PARTICLE_MODULE::COLOR_CHANGE] = true;
	m_ModuleData.vStartColor = Vec3(0.2f, 0.3f, 1.0f);
	m_ModuleData.vEndColor = Vec3(0.4f, 1.f, 0.4f);
// particle_update.fx 중에서

	// 색상 변화모듈이 활성화 되어있으면
	if(ModuleData.ColorChange)
	{
		particle.vColor = ModuleData.vStartColor + particle.NomalizedAge * (ModuleData.vEndColor - ModuleData.vStartColor);
	}        


파티클에 속도를 적용해보자


// define.h 중에서 PARTICLE_MODULE enum class

enum class PARTICLE_MODULE
{
	PARTICLE_SPAWN,
	COLOR_CHANGE,
	SCALE_CHANGE,
	ADD_VELOCITY,
	END,
};

// struct.h 중에서

	// Add Velocity 모듈
	Vec4	vVelocityDir;
	int     AddVelocityType;	// 0 : From Center, 1 : Fixed Direction	
	float	OffsetAngle;		
	float	Speed;
	int     addvpad;
// struct.fx 중에서

    // Add Velocity 모듈
    float4  vVelocityDir;
    int     AddVelocityType; // 0 : From Center, 1 : Fixed Direction	
    float   OffsetAngle;
    float   Speed;
    int     addvpad;
// CParticleSystem.cpp 중에서

	m_ModuleData.ModuleCheck[(UINT)PARTICLE_MODULE::ADD_VELOCITY] = true;
	m_ModuleData.AddVelocityType = 0; // From Center
	m_ModuleData.Speed = 200.f;
	m_ModuleData.vVelocityDir;
	m_ModuleData.OffsetAngle;
// particle_update.fx 중에서

    // 속도에 따른 파티클위치 이동
    // Sim 좌표계에 따라서 이동방식 분기
    if (ModuleData.Space == 0)
    {                        
        particle.vWorldPos += particle.vVelocity * g_DT;
    }
    else if(ModuleData.Space == 1)
    {
        particle.vLocalPos += particle.vVelocity * g_DT;
        particle.vWorldPos.xyz = particle.vLocalPos.xyz + ObjectPos.xyz;
    }

또한, 질량을 세팅하고, 힘을 가하면, 즉 가속도가 있다면 방향을 바꾸게 만들수도 있다.

// particle_update.fx 중에서

    // 파티클 질량 설정
    particle.Mass = 1.f;

    // 파티클에 힘이 적용된 경우, 힘에 의한 속도의 변화량 계산
    float3 vAccel = particle.vForce / particle.Mass;
    particle.vVelocity += vAccel * g_DT;

    // 속도에 따른 파티클 위치 이동
    if (ModuleData.Space == 0)
    {
        particle.vWorldPos += particle.vVelocity * g_DT;
    }


아래 코드는 랜덤으로 힘을 가했을 때의 모듈(NoiseForce 모듈)이다.

// struct.h 중에서

// 파티클 입자 속성
struct tParticle
{
	Vec4	vLocalPos;		// 오브젝트로부터 떨어진 거리
	Vec4	vWorldPos;		// 파티클 최종 월드위치
	Vec4	vWorldScale;	// 파티클 크기
	Vec4	vColor;			// 파티클 색상
	Vec4	vVelocity;		// 파티클 현재 속도
	Vec4	vForce;			// 파티클에 주어진 힘
	Vec4	vRandomForce;	// 파티클에 적용되는 랜덤방향 힘

	float   Age;			// 생존 시간
	float   PrevAge;		// 이전 프레임 생존 시간
	float   NomalizedAge;	// 수명대비 생존시간을 0~1로 정규화 한 값
	float	LifeTime;		// 수명
	float	Mass;			// 질량
	float   ScaleFactor;	// 추가 크기 배율

	int     Active;			// 파티클 활성화 여부
	int     pad;
};

    // NoiseForce 모듈 - 랜덤 힘 적용
    float   fNoiseTerm;  // 랜덤 힘을 적용하는 쿨타임, 텀.
    float   fNoiseForce; // 랜덤힘의 크기
// struct.fx 중에서

// Particle
struct tParticle
{
    float4  vLocalPos;
    float4  vWorldPos; // 파티클 위치
    float4  vWorldScale; // 파티클 크기
    float4  vColor; // 파티클 색상
    float4  vVelocity; // 파티클 현재 속도
    float4  vForce; // 파티클에 주어진 힘
    float4  vRandomForce; // 파티클에 적용되는 랜덤 힘

    float   Age; // 생존 시간
    float   PrevAge;  // 이전 프레임 생존시간
    float   NomalizedAge; // 수명대비 생존시간을 0~1로 정규화 한 값
    float   LifeTime; // 수명
    float   Mass; // 질량
    float   ScaleFactor; // 추가 크기 배율
    
    int     Active;
    int     pad;
};

// NoiseForce 모듈
    float fNoiseTerm;
    float fNoiseForce;
// CParticleSystem.cpp 의 생성자 함수 중에서<br/>

	m_ModuleData.ModuleCheck[(UINT)PARTICLE_MODULE::NOISE_FORCE] = true;
	m_ModuleData.fNoiseTerm = 0.3f;
	m_ModuleData.fNoiseForce = 50.f;

// particle_update.fx 중에서

    // NoiseForce 모듈 (랜덤으로 힘) 적용 모듈
    if (ModuleData.NoiseForce)
    {            
        if (particle.PrevAge == 0.f)
        {
                // 랜덤 결과를 받을 변수
            float3 vOut1 = (float3) 0.f;
            float3 vOut2 = (float3) 0.f;
            float3 vOut3 = (float3) 0.f;
                
            // 전체 유효 스레드의 아이디를 0 ~ 1 로 정규화
            float fNormalizeThreadID = (float) _ID.x / (float) ParticleMaxCount;
            GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID, vOut1);
            GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID + 0.1f, vOut2);
            GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID + 0.2f, vOut3);
            
            float3 vForce = normalize(float3(vOut1.x, vOut2.x, vOut1.z));
            particle.vRandomForce.xyz = vForce * ModuleData.fNoiseForce;
        }
        else
        {
            int Age = int(particle.Age * (1.f / ModuleData.fNoiseTerm));
            int PrevAge = int(particle.PrevAge * (1.f / ModuleData.fNoiseTerm));

            // 지정한 간격을 넘어간 순간, 새로운 랜덤 Force 를 준다.
            if (Age != PrevAge)
            {
                // 랜덤 결과를 받을 변수
                float3 vOut1 = (float3) 0.f;
                float3 vOut2 = (float3) 0.f;
                float3 vOut3 = (float3) 0.f;
                
                // 전체 유효 스레드의 아이디를 0 ~ 1 로 정규화
                float fNormalizeThreadID = (float) _ID.x / (float) ParticleMaxCount;
                GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID, vOut1);
                GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID + 0.1f, vOut2);
                GaussianSample(NoiseTexture, NoiseTexResolution, fNormalizeThreadID + 0.2f, vOut3);
            
                float3 vForce = normalize(float3(vOut1.x, vOut2.x, vOut1.z) * 2.f - 1.f);
                particle.vRandomForce.xyz = vForce * ModuleData.fNoiseForce;
            }
        }   
        
        particle.vForce.xyz += particle.vRandomForce.xyz;
    }                


파티클의 속도를 제한하는 모듈 (Drag Module)


시간이 지날수록 파티클의 속도가 줄어들게 해보자.

// define.h 중에서

enum class PARTICLE_MODULE
{
	PARTICLE_SPAWN,
	COLOR_CHANGE,
	SCALE_CHANGE,
	ADD_VELOCITY,

	DRAG,
	DUMMY_1,
	DUMMY_2,
	DUMMY_3,

	END,
};
// struct.h 중에서 tParticleModule 구조체 중

    // Drag 모듈 - 속도 제한
	float StartDrag;
	int	  DragPad;

// struct.fx 중에서 tParticleModule 구조체 중

    // Drag 모듈
    float StartDrag;
    float EndDrag;
    int dragpad;
// CParticleSystem.cpp 중에서 CParticleSystem의 생성자함수

	m_ModuleData.ModuleCheck[(UINT)PARTICLE_MODULE::DRAG] = true;
	m_ModuleData.StartDrag = 200.f;
	m_ModuleData.EndDrag = 0.f;
// particle_update.fx 중에서

    // 속도 제한(Drag) 모듈
    if (ModuleData.Drag)
    {
        // 파티클의 현재 속력
        float Speed = length(particle.vVelocity);
        float fDrag = ModuleData.StartDrag + (ModuleData.EndDrag - ModuleData.StartDrag) * particle.NomalizedAge;
        
        // 빠른 감속을 위해서 설정한 fDrag가 음수일 경우 예외처리
        if (fDrag < 0.f)
            fDrag = 0.0001f;

        if (fDrag < Speed)
        {
            particle.vVelocity = normalize(particle.vVelocity) * fDrag;
        }
    }


여기서 Drag 모듈은 감속을 위한 모듈로, 속도 값이 음수로 떨어져서 반대로 이동하도록 하면 안된다.

이를 방지하기 위한 예외처리도 해주자.

if (fDrag < 0.f)
    fDrag = 0.0001f;

0.0001을 준 이유는 멈춘 것 처럼 보이게 하기 위해서이다.

정확하게 0을 줘버리면 속도벡터가 0이 되어버리기 때문에 파티클을 렌더링 하는 과정에서 오류가 생긴다. 그래서 멈춘것처럼 보이게 하되, Normalize가 가능하도록 0에 가까운 값을 주었다.


렌더링 관련 모듈


// define.h 중에서 PARTICLE_MODULE 목록

enum class PARTICLE_MODULE
{
	PARTICLE_SPAWN,
	COLOR_CHANGE,
	SCALE_CHANGE,
	ADD_VELOCITY,

	DRAG,
	NOISE_FORCE,
	RENDER,
	DUMMY_3,

	END,
};

// struct.h 중에서

// Render 모듈

int     VelocityAlignment; // 속도 정렬, 속도의 방향으로 이미지를 회전시킨다.
                           // 값이 1이라면 속도정렬을 사용한다.
                           // 값이 0이라면 쓰지 않는다.
int     VelocityScale;     // 속도에 따른 크기 변화
Vec4    vMaxVelocityScale; // 속력에 따른 크기 변화량 최대치
float   vMaxSpeed;         // 최대 크기에 도달하는 속력

int     renderpad;
// struct.fx 중에서

// Render 모듈
    int     VelocityAlignment;  // 1 : 속도정렬 사용(이동 방향으로 회전) 0 : 사용 안함
    int     VelocityScale;      // 1 : 속도에 따른 크기 변화 사용, 0 : 사용 안함	
    float   vMaxSpeed;          // 최대 크기에 도달하는 속력
    float4  vMaxVelocityScale;  // 속력에 따른 크기 변화량 최대치
    int     renderpad;


// Module check
    int Spawn;
    int ColorChange;
    int ScaleChange;
    int AddVelocity;    
    
    int Drag;
    int NoiseForce;
    int Render;
    int modulepad;    

// CParticleSystem.cpp 파일 중에서

// 생성자함수에서 Render모듈을 켜주기
// 진행 방향으로 크기를 20배 키워줌.

	m_ModuleData.ModuleCheck[(UINT)PARTICLE_MODULE::RENDER] = true;
	m_ModuleData.VelocityAlignment = true;
	m_ModuleData.VelocityScale = true;
	m_ModuleData.vMaxVelocityScale = Vec3(20.f, 1.f, 1.f);
	m_ModuleData.vMaxSpeed = 500.f;

// Render 함수 중에서

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


// 모듈 데이터 t21에 바인딩
	m_ModuleDataBuffer->UpdateData(21, PIPELINE_STAGE::PS_GEOMETRY);

// particle_render.fx 중에서

// t21번 레지스터에 바인딩
StructuredBuffer<tParticleModule>   ParticleModuleData : register(t21);

// 연산 개시
[maxvertexcount(6)]
void GS_ParticleRender (point VS_OUT _in[1], inout TriangleStream<GS_OUT> _outstream)
{    
    uint id = _in[0].iInstID;
    
    if (0 == ParticleBuffer[id].Active)
        return;

    float3 vParticleViewPos = mul(float4(ParticleBuffer[id].vWorldPos.xyz, 1.f), g_matView).xyz;
    float2 vParticleScale = ParticleBuffer[id].vWorldScale.xy * ParticleBuffer[id].ScaleFactor;
   
    // 0 -- 1
    // |    |
    // 3 -- 2
    float3 NewPos[4] =
    {
        float3(-vParticleScale.x / 2.f, +vParticleScale.y / 2.f, 0.f),
        float3(+vParticleScale.x / 2.f, +vParticleScale.y / 2.f, 0.f),
        float3(+vParticleScale.x / 2.f, -vParticleScale.y / 2.f, 0.f),
        float3(-vParticleScale.x / 2.f, -vParticleScale.y / 2.f, 0.f)
    };
    
    
    if (ModuleData.Render)
    {        
        if (ModuleData.VelocityScale)
        {
            // 현재 파티클의 속력을 알아낸다.
            float fCurSpeed = length(ParticleBuffer[id].vVelocity);            
            if (ModuleData.vMaxSpeed < fCurSpeed)
                fCurSpeed = ModuleData.vMaxSpeed;
            
            // 최대속도 대비 현재 속도의 비율을 구한다.
            float fRatio = saturate(fCurSpeed / ModuleData.vMaxSpeed);            
          
            // 비율에 맞는 크기변화량을 구한다.
            float3 vDefaultScale = float3(1.f, 1.f, 1.f);
            float3 fScale = vDefaultScale + (ModuleData.vMaxVelocityScale.xyz - vDefaultScale) * fRatio;
                      
            NewPos[0] = NewPos[0] * fScale;
            NewPos[3] = NewPos[3] * fScale;            
        }
        
        
        if (ModuleData.VelocityAlignment)
        {
            // 파티클 월드 기준 속도를 View 공간으로 변환
            float3 vVelocity = normalize(ParticleBuffer[id].vVelocity);
            vVelocity = mul(float4(vVelocity, 0.f), g_matView).xyz;
                       
            // 파티클 Right 방향과 이동 방향을 내적해서 둘 사이의 각도를 구한다.
            float3 vRight = float3(1.f, 0.f, 0.f);
            float fTheta = acos(dot(vRight, vVelocity));
            
            // 내적의 결과가 코사인 예각을 기준으로 하기 때문에, 2파이 에서 반대로 뒤집어 준다.
            if (vVelocity.y < vRight.y)
            {
                fTheta = (2.f * 3.1415926535f) - fTheta;
            }
            
            // 구한 각도로 Z 축 회전 행렬을 만든다.
            float3x3 matRotZ =
            {
                cos(fTheta),  sin(fTheta),      0,
                -sin(fTheta), cos(fTheta),      0,
                          0,            0,    1.f,
            };
            
            // 4개의 정점을 회전시킨다.
            for (int i = 0; i < 4; ++i)
            {
                NewPos[i] = mul(NewPos[i], matRotZ);
            }
        }        
    }
    
    
    GS_OUT output[4] = { (GS_OUT) 0.f, (GS_OUT) 0.f, (GS_OUT) 0.f, (GS_OUT) 0.f };
    
    output[0].vPosition = mul(float4(NewPos[0] + vParticleViewPos, 1.f), g_matProj);
    output[0].vUV = float2(0.f, 0.f);
    output[0].iInstID = id;
    
    output[1].vPosition = mul(float4(NewPos[1] + vParticleViewPos, 1.f), g_matProj);
    output[1].vUV = float2(1.f, 0.f);
    output[1].iInstID = id;
    
    output[2].vPosition = mul(float4(NewPos[2] + vParticleViewPos, 1.f), g_matProj);
    output[2].vUV = float2(1.f, 1.f);
    output[2].iInstID = id;
    
    output[3].vPosition = mul(float4(NewPos[3] + vParticleViewPos, 1.f), g_matProj);
    output[3].vUV = float2(0.f, 1.f);
    output[3].iInstID = id;
    
    
    // 정점 생성
    _outstream.Append(output[0]);
    _outstream.Append(output[1]);
    _outstream.Append(output[2]);
    _outstream.RestartStrip();
    
    _outstream.Append(output[0]);
    _outstream.Append(output[2]);
    _outstream.Append(output[3]);
    _outstream.RestartStrip();
}


float4 PS_ParticleRender(GS_OUT _in) : SV_Target
{   
    float4 vOutColor = float4(1.f, 0.f, 1.f, 1.f);
    
    if(g_btex_0)
    {
        vOutColor = g_tex_0.Sample(g_sam_0, _in.vUV);        
        vOutColor.rgb *= ParticleBuffer[_in.iInstID].vColor.rgb;
    }
    
    return vOutColor;
}

VelocityScale에서는 파티클의 속력을 알아내어 비율(fRatio)을 구해준다.

이 때, saturate를 이용해서 0~1사이의 값이 나오도록 한다.

이 부분이 작동한다면 속력에 따라서 파티클이 늘어지는 크기가 달라지게 된다.

속력이 빠를수록 타원형이나 막대기에 가깝고, 느려지면 점점 원형이나 정사각형 모양에 가깝게 나올 것이다.


VelocityAlignment에서는 파티클을 회전시키기 위해서 각도(fTheta)를 추출한다.

여기서 문제는 내적을 한 값에서 acos를 할 경우 항상 작은 각도 쪽으로 fTheta가 반환된다는 점이다.

예를 들어, 벡터1과 벡터2 사이의 각이 120도, 240도 일 경우 240도를 반환받고 싶어도 120도를 반환받게 된다.

그래서 예외조건을 걸어줘야한다.

속도의 y값이 더 낮을 경우, 정확히 말하면 속도벡터가 더 아래쪽으로 치우쳐져있을 경우 예외처리를 해주면 된다.

// 내적의 결과가 코사인 예각을 기준으로 하기 떄문에, 2파이 에서 반대로 뒤집어 준다.

if (vVelocity.y < vRight.y)
{
    fTheta = (2.f * 3.1415926535f) - fTheta;
}

또 다른 문제는 회전 연산, 시뮬레이션은 ViewSpace에서 하는데 비해,

속도 시뮬레이션, 연산을 World좌표계에서 진행중이다.

그래서 속도 벡터를 뷰 스페이스로 끌고와야한다.

그렇기 때문에 아래와 같은 코드가 작성이 된다.

vVelocity = mul(float4(vVelocity, 0.f), g_matView).xyz;

이 과정을 거치고 나서 파티클 좌표계의 Right 방향과 이동 방향을 내적해서 둘 사이의 각도를 구하고 z축으로 회전시킨다.

float3x3을 이용해서 회전행렬을 만들 수 있다.

float3x3 matRotZ =
{
    cos(fTheta), sin(fTheta), 0,
   -sin(fTheta), cos(fTheta), 0,
              0,           0, 1.f
};

© 2022.07. by Wookey_Kim

Powered by Hydejack v7.5.2