2020 이전/DirectX

다이렉트X Stream Output을 이용한 파티클

이상해C++ 2019. 9. 19. 02:54

 

 

스트림 출력 개요

 

DirectX10부터 렌더링 파이프라인에 Stream-Out(스트림출력) 단계가 추가되었다. 스트림 출력 단계를 이용하면, GPU에서 Stream Output에 묶인 "정점 버퍼"기하 셰이더에서 정점을 출력하는 방법으로 직접 정점목록 형태를 정점 버퍼에 기록할 수있다. 그리고 그 정점 버퍼에 저장된 정점들을 차후의 렌더링 파이프라인의 입력으로 사용할 수 있다.

 

스트림 출력을 이용한 파티클 구현

스트림 출력을 이용하여 파티클을 구현하는 방법은 다음과 같다.

1. 스트림 출력을 거친 정점의 렌더링을 막기위해 픽셀 셰이더와 뎁스/스텐실을 비활성화 한다.  

2. 정점 버퍼를 스트림 출력에 묶어 새로운 정점을 기하셰이더로 정점을 출력한다. (SO 단계)

3. 스트림 출력에 묶인 정점 버퍼를 떼어낸다.

4. 2에서 만든 정점 버퍼를 이용해 실질적으로 파티클을 렌더링한다.

 

첫번째 패스

출력병합기에 렌더 타겟 뷰를 묶어서 렌더 타겟에 그리는 것처럼 스트림 출력 단계에 정점 버퍼를 묶어서 정점 목록을 작성한다. (스트림 출력)기하 셰이더가 출력한 정점은 래스터화기로 넘어가게 된다. 이 정점을 렌더링하지 않기 위해서 픽셀 셰이더와 깊이/스텐실 버퍼를 비활성화 해야한다. 또는 출력 병합기에 NULL 타겟과 NULL 깊이 스텐실 버퍼를 묶을 수 있다.

두번째 패스

스트림 출력으로 정점들을 정점 버퍼에 모두 기록한 다음, 원래대로 렌더링 파이프라인을 돌려준다. 

구현 코드

초기 버퍼를 포함한 3개의 정점 버퍼가 필요하다

초기 버퍼는 최초에 하나의 정점(이 코드에서는 이 정점이 다른 정점을 생성하는 방출 입자가 된다)을 담고있는 정점 버퍼로 필요하다면 각자 용도에 맞게 고쳐쓴다. 

 

두 단계에 걸쳐 파티클을 만들어 낸다.

 

1. StreamOutputPass()

기하셰이더에서는 SampleLevel를 사용하면 텍스처 정보를 가져올 수 있다. 난수 정보가 담겨있는 텍스처를 로드해서 쓰기위해 기하셰이더에 세팅해준다. 

DrawAuto()함수는 다이렉트가 자동으로 스트림 출력으로 출력한 정점들을 관리하여 그 개수만큼 자동으로 그릴 수 있게 해주는 함수로, 우리가 주의해야할 점은 버퍼를 생성할 때 정점 개수가 버퍼 사이즈를 초과하지 않도록 하는 것이다.  

하나의 정점 버퍼를 스트림 출력 단계와 입력 조립기 단계에 동시에 묶을 수는 없다. 따라서 스트림 출력단계에서 널 버퍼를 스트림 출력단계에 묶어줌으로 정점버퍼를 떼준다.

 

2. RenderingPass();

Particle.fx

난수 텍스처와 소프트 파티클을 위해서 텍스처를 레지스터에 세팅

///////////////////////////// Stream Output SO Particle ////////////////////////////

float3 RandomTex(float offset)
{
	float u = offset;
	float v = g_RandomTex.SampleLevel(g_LinearSmp, offset, 0.f,0).r;

	float4 vColor = g_RandomTex.SampleLevel(g_LinearSmp, u, v,0);
	float3 vRandvec = vColor.xyz;

	return vRandvec;
}

GS_SO_INPUT_PARTICLE SOParticleVS(GS_SO_INPUT_PARTICLE input)
{
	return input;
}

// 스트림 출력 셰이더에서는 파티클 입자 생성 파괴 규칙을 작성한다.
// 방출 입자는 하나를 유지한다.
[maxvertexcount(2)]
void SOParticleGS(point GS_SO_INPUT_PARTICLE input[1],
	inout PointStream<GS_SO_INPUT_PARTICLE> stream)
{
	input[0].fAge += g_fTime;

	if (input[0].fType == 0.0f)
	{
		// 0.4초마다 한번씩 입자를 생성한다.
		if (input[0].fAge >= 0.4f)
		{
			GS_SO_INPUT_PARTICLE particle;

			float t = RandomTex(g_fPassTime).x;
			t -= 0.5f;
			float size = RandomTex(t).x;

			particle.fAge = 0.0f;
			particle.vSize = float2(0.3f, 0.3f) + 0.3*size;

			particle.vPos = g_vParticleCenter + float3(5*t, size,0.f);
			particle.fType = 1.0f + t;

			stream.Append(particle);
			input[0].fAge = 0.0f;
		}
		stream.Append(input[0]);
	}
	else
	{
		if (input[0].fAge <= g_fParticleTime)
		{
			float pos = input[0].fType - 1;
			input[0].vPos += float3(pos, pos+1.f,0.f)*g_fTime*0.5f;
			stream.Append(input[0]);
		}
	}	
}

PS_OUTPUT_SINGLE SOParticlePS(GS_SO_INPUT_PARTICLE input)
{
	PS_OUTPUT_SINGLE output = (PS_OUTPUT_SINGLE)0;
	return output;
}

코드상에서 방출기 입자만 fType의 값이 0으로 0.4초마다 한번씩 fType이 1인 다른 입자를 생성한다. 그리고 fType이 1인 입자(실질적으로 렌더링이 될)들은 파티클 수명동안 우리가 원하는대로 정점 정보를 수정할 수 있다.

 

////////////////////////////// Rendering SO Particle /////////////////////////////////////

GS_SO_INPUT_PARTICLE SODrawParticleVS(GS_SO_INPUT_PARTICLE input)
{
	GS_SO_INPUT_PARTICLE output = input;
	return input;
}

[maxvertexcount(4)]
void SODrawParticleGS(point GS_SO_INPUT_PARTICLE input[1],
	inout TriangleStream<GS_SO_OUTPUT_PARTICLE> stream)
{
	float	fHalfX = 0.5f * input[0].vSize.x;
	float	fHalfY = 0.5f * input[0].vSize.y;

	float3	vPos[4];
	vPos[0] = input[0].vPos - g_vCameraAxisX * fHalfX - g_vCameraAxisY * fHalfY;
	vPos[1] = input[0].vPos - g_vCameraAxisX * fHalfX + g_vCameraAxisY * fHalfY;
	vPos[2] = input[0].vPos + g_vCameraAxisX * fHalfX - g_vCameraAxisY * fHalfY;
	vPos[3] = input[0].vPos + g_vCameraAxisX * fHalfX + g_vCameraAxisY * fHalfY;

	float2	vUV[4] =
	{
		float2(0.f, 1.f),
		float2(0.f, 0.f),
		float2(1.f, 1.f),
		float2(1.f, 0.f)
	};

	GS_SO_OUTPUT_PARTICLE	output;

	for (int i = 0; i < 4; ++i)
	{
		output.vProjPos = mul(float4(vPos[i], 1.f), g_matVP);
		output.vPos = output.vProjPos;
		output.vUV = ComputeAnimation2D(vUV[i]);
		output.fAlpha = input[0].fAge;
		stream.Append(output);
	}
}

PS_OUTPUT_SINGLE SODrawParticlePS(GS_SO_OUTPUT_PARTICLE input)
{
	PS_OUTPUT_SINGLE	output = (PS_OUTPUT_SINGLE)0;

	float4 vColor = (float4)0;
	float2 vUV = input.vUV;

	// 애니메이션이 없을때
	if (g_iAnimation2DEnable == 0)
		vColor = g_DiffuseTex.Sample(g_LinearSmp, vUV);

	else
	{
		if (g_iAnim2DType == AT_ATLAS)
			vColor = g_DiffuseTex.Sample(g_LinearSmp, vUV);

		else
			vColor = g_ArrayTex.Sample(g_LinearSmp, float3(vUV, g_iAnim2DFrame));
	}

	if (vColor.a == 0.f)
		clip(-1);

	// 이 사각형이 출력될 좌표를 이용하여 화면에서의 UV를 만들어낸다.
	float2	vScreenUV;

	vScreenUV.x = (input.vProjPos.x / input.vProjPos.w) * 0.5f + 0.5f;
	vScreenUV.y = (input.vProjPos.y / input.vProjPos.w) * -0.5f + 0.5f;

	// 여기서 소프트파티클 처리를 한다.
	float4	vDepth = g_GBufferDepth.Sample(g_PointSmp, vScreenUV);

	float	fAlpha = vDepth.w - input.vProjPos.w;

	if (fAlpha > 0.f)
		fAlpha = fAlpha / 0.5f;

	else
		fAlpha = 1.f;

	fAlpha = min(fAlpha, 1.f);

	vColor *= g_vMtrlDif;
	vColor.a *= fAlpha;
	vColor.a *= 1.5f - input.fAlpha / 2.f;

	output.vColor = vColor;

	return output;
}

 렌더링 패스쪽의 파티클 GS는 파티클을 빌보드 스퀘어 도형으로 만들어주고, PS에서는 Depth bias를 이용하여 소프트 파티클을 적용하고 fAlpha값을 이용하여 시간이 지나면 반투명해지게 만들어준다. 

 

구현 결과물

마을에 요로코롬 귀여운 나비 파티클이 생성되었습니다

 

만약 적용이 안된다면 당신이 간과했을 수도 있는 것들

 

Q. SO 출력 셰이더와 렌더 셰이더를 제대로 작성한 것 같은데 아예 출력이 되지 않아요

StreamOutput 단계에서의 기하셰이더를 생성할 때 CreateGeometryShaderWithStreamOutput이라는 특수한 함수를 써야 합니다. 이때 D3D11_SO_DECLARATION_ENTRY 디스크립션은 입력 레이아웃과 동일한 형태여야합니다. 

인풋레이아웃과 동일한 형식 정보를 넘겨줘야 StreamOutput을 사용할 수 있다.

 

Q. 랜덤 텍스처를 기하 셰이더에서 샘플링 할 수 없어요

당신은 텍스처 자원뷰를 기하셰이더에 넘겨주지 않았을 수도 있습니다 
그리고 높은 확률로 샘플러 스테이트도 같이 넘겨주지 않았을 수 있습니다

 

그래픽 디버깅할 때 기하 셰이더에서 방출 입자 pos값에(float3) 텍스처 값을 넣어서 확인해보니 텍스처 컬러에 nan값이 들어가있어서 텍스처가 제대로 기하셰이더에 들어가지 않았던 것을 알 수 있었습니다. 그래픽 디버깅 매우 찬양해 

 

 

참고 서적/사이트

1. DirectX11을 이용한 3D 프로그래밍 입문

2. https://m.blog.naver.com/sorkelf/40171948938