DirectX

89_빌보드 #2_눈 내리기

devRiripong 2024. 4. 3.
반응형

눈 내리는 걸 해볼 것이다.

 

풀깔기를 응용해서 하면 된다.

 

1. SnowBillboard 클래스 만들기

Billboard 클래스를 복붙해서 이름을 SnowBillboard라고 한다.

Engine/06. UI & Effect 필터에 넣는다.

이건 나중에 파티클 시스템에 통합이 되어야 한다.

보통은 하드코딩 해서 클래스 늘리는 방식으로 안 하긴 하지만 강의 측면에서 파티클 시스템 전단계로 해본다.

SnowBillboard에 맞게 수정을 한다.

 

인위적으로 몇 개를 넣어서 위치를 지정하는 개념이 아니라 구역을 하나 지정해서 그 구역에서 원하는 카운트 만큼을 눈 송이 같은 걸 뿌려주면서 작업을 할 예정이다. 보통은 파티클 시스템에서 몇 개를 뿌릴 것인지 같은 옵션이 분명 있을 것이어서 거기서 작업을 한다고 볼 수 있다.

 

2.  BindShaderDesc.h에 struct  SnowBillboardDesc 정의하기

쉐이더 코드에서 한번만 넣어주고 잊고 사는 개념이 아니라 시간이 얼마나 경과 되었는지 이런 부분들을 넣어줘야 되기 때문에 constant buffer로 넘겨줄 예정이다.

지금까지는 상수 관련된 부분들을 BindShaderDesc.h에서 관리하고 있었다. 여기서 타입을 파준 다음에 그거에 따라가지고 Shader 쪽에서 push data 코드를 넣어놔서 작업을 했으니까 똑같이 만들어볼 예정이다.

BindShaderDesc.h에

// 눈을 뿌리고 싶을 때 어떻게 뿌리고 싶을지와 관련된 정보
struct SnowBillboardDesc
{
	Color color = Color(1, 1, 1, 1);

	Vec3 velocity = Vec3(0, -5, 0); // 속도
	float drawDistance = 0;

	Vec3 origin = Vec3(0, 0, 0);
	float turbulence = 5; // 흔들리는 강도

	Vec3 extent = Vec3(0, 0, 0);
	float time;
};

나중에 만들고 싶은 공식에 따라 적절하게 바꿔주면 된다.

 

3. Shader 클래스에서 Push 시리즈에 PushSnowData 추가하기

Shader 코드로 가서 Push 시리즈를 늘리면 된다.

	void PushSnowData(const SnowBillboardDesc& desc);

 

SnowDesc관련된 부분도 넣어준다.

	SnowBillboardDesc _snowDesc;
	shared_ptr<ConstantBuffer<SnowBillboardDesc>> _snowBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _snowEffectBuffer; q

 

void Shader::PushSnowData(const SnowBillboardDesc& desc)
{
	if (_snowEffectBuffer == nullptr)
	{
		_snowBuffer = make_shared<ConstantBuffer<SnowBillboardDesc>>();
		_snowBuffer->Create();
		_snowEffectBuffer = GetConstantBuffer("SnowBuffer");
	}

	_snowDesc = desc;
	_snowBuffer->CopyData(_snowDesc);
	_snowEffectBuffer->SetConstantBuffer(_snowBuffer->GetComPtr().Get());
}

SnowBuffer라는 이 이름을 찾아서 만들어 준 다음에 거기다가 밀어넣는 시도를 해주게 될 것이다.

 

4. Component.h의 ComponentType에 SnowBillboard를 추가하고, GameObject에 GetSnowBillboard 추가하기

Component.h의 enum class ComponentType에 SnowBillboard를 추가한다.

 

GameObject에 가서 새로운 컴포넌트를 추가할 때 해준 작업을 한다.

 

GameObject.h에

class SnowBillboard; 

전방선언을 하고,

	shared_ptr<SnowBillboard> GetSnowBillboard(); 

 

GameObject.cpp에

#include "SnowBillboard.h"
shared_ptr<SnowBillboard> GameObject::GetSnowBillboard()
{
	shared_ptr<Component> component = GetFixedComponent(ComponentType::SnowBillboard);
	return static_pointer_cast<SnowBillboard>(component);
}

Engine을 빌드한다.

 

5. MathUtils.h에 렌덤값 구하는 함수들 만들기

MathUtils.h에서 수학 연습을 해봤는데 이건 라이브러리에 들어가 있는 함수들이니까 삭제한다.

#pragma once
#include "Primitive3D.h"

struct MathUtils
{
	static float Random(float r1, float r2);
	static Vec2 RandomVec2(float r1, float r2);
	static Vec3 RandomVec3(float r1, float r2);
};

사용할 함수들을 넣어준다.

float MathUtils::Random(float r1, float r2)
{
	float random = ((float)rand()) / (float)RAND_MAX;
	float diff = r2 - r1;
	float val = random * diff;

	return r1 + val;
}

Vec2 MathUtils::RandomVec2(float r1, float r2)
{
	Vec2 result;
	result.x = Random(r1, r2);
	result.y = Random(r1, r2);

	return result;
}

Vec3 MathUtils::RandomVec3(float r1, float r2)
{
	Vec3 result;
	result.x = Random(r1, r2);
	result.y = Random(r1, r2);
	result.z = Random(r1, r2);

	return result;
}

범위 안의 Random을 만들어 내는 함수들이다.

 

6. SnowBillboard 컴포넌트 클래스 구현하기 

SnowBililboard로 다시 돌아가서

나머지 코드들을 만들어주면 된다.

SnowBililboard.h에

	SnowBillboardDesc _desc;
	float _elpasedTime = 0.f; // 얼마만큼 시간이 경과 했는지 

SnowBillboardDesc 를 갖고 있다가 Update할 때 Push 해주면 된다.

 

경우에 따라 Deltatime같은 경우는 이리저리서 많이 필요하니까 쉐이더에다가 또는 글로벌데이터에다가 끼워 넣어서 넘겨주는 것도 방법이다.

 

생성자에서 SnowBillboardDesc의 내용들을 채워준 다음에 건내줘야 한다.

 

SnowBillboard.h

#pragma once
#include "Component.h"

#define MAX_BILLBOARD_COUNT 500

struct VertexSnow
{
	Vec3 position;
	Vec2 uv;
	Vec2 scale;
	Vec2 random;
};

class SnowBillboard : public Component
{
	using Super = Component; 

public: 
	SnowBillboard(Vec3 extent, int32 drawCount = 10);
	~SnowBillboard();
	
	void Update(); 
	
	void SetMaterial(shared_ptr<Material> material) { _material = material; }
	void SetPass(uint8 pass) { _pass = pass; }

private: 
	vector<VertexSnow> _vertices;
	vector<uint32> _indices; 
	shared_ptr<VertexBuffer> _vertexBuffer; 
	shared_ptr<IndexBuffer> _indexBuffer; 

	int32 _drawCount = 0;

	shared_ptr<Material> _material; 
	uint8 _pass = 0; 

	SnowBillboardDesc _desc;
	float _elpasedTime = 0.f; // 얼마만큼 시간이 경과 했는지 
};

 

SnowBillboard.cpp

#include "pch.h"
#include "SnowBillboard.h"
#include "Material.h"
#include "Camera.h"
#include "MathUtils.h"

SnowBillboard::SnowBillboard(Vec3 extent, int32 drawCount /*= 100*/)
	: Super(ComponentType::SnowBillboard)
{

	_desc.extent = extent;
	_desc.drawDistance = _desc.extent.z * 2.0f;
	_drawCount = drawCount;

	const int32 vertexCount = _drawCount * 4; // 사각형
	_vertices.resize(vertexCount);

	// 렌덤한 위치에 눈을 만들어 주기
	for (int32 i = 0; i < _drawCount * 4; i += 4)
	{
		Vec2 scale = MathUtils::RandomVec2(0.1f, 0.5f);

		Vec3 position;
		position.x = MathUtils::Random(-_desc.extent.x, _desc.extent.x);
		position.y = MathUtils::Random(-_desc.extent.y, _desc.extent.y);
		position.z = MathUtils::Random(-_desc.extent.z, _desc.extent.z);

		Vec2 random = MathUtils::RandomVec2(0.0f, 1.0f);

		_vertices[i + 0].position = position;
		_vertices[i + 1].position = position;
		_vertices[i + 2].position = position;
		_vertices[i + 3].position = position;

		_vertices[i + 0].uv = Vec2(0, 1);
		_vertices[i + 1].uv = Vec2(0, 0);
		_vertices[i + 2].uv = Vec2(1, 1);
		_vertices[i + 3].uv = Vec2(1, 0);

		_vertices[i + 0].scale = scale;
		_vertices[i + 1].scale = scale;
		_vertices[i + 2].scale = scale;
		_vertices[i + 3].scale = scale;

		_vertices[i + 0].random = random;
		_vertices[i + 1].random = random;
		_vertices[i + 2].random = random;
		_vertices[i + 3].random = random;
	}

	_vertexBuffer = make_shared<VertexBuffer>();
	_vertexBuffer->Create(_vertices, 0); // cpuWrite를 fasle로 꺼놨다. 굳이 뭔가 설정해 주는게 아니라 기본적인 vertex는 정해져있고, 달라지는 건 constant buffer를 이용해 나머지 상세수치는 조정할 거라서 

	const int32 indexCount = _drawCount * 6; // index는 6개로 정점을 이어줘야 하니
	_indices.resize(indexCount);

	for (int32 i = 0; i < _drawCount; i++)
	{
		_indices[i * 6 + 0] = i * 4 + 0;
		_indices[i * 6 + 1] = i * 4 + 1;
		_indices[i * 6 + 2] = i * 4 + 2;
		_indices[i * 6 + 3] = i * 4 + 2;
		_indices[i * 6 + 4] = i * 4 + 1;
		_indices[i * 6 + 5] = i * 4 + 3;
	}

	_indexBuffer = make_shared<IndexBuffer>();
	_indexBuffer->Create(_indices);
}

SnowBillboard::~SnowBillboard()
{
}

void SnowBillboard::Update()
{
	_desc.origin = CUR_SCENE->GetMainCamera()->GetTransform()->GetPosition();
	_desc.time = _elpasedTime;
	_elpasedTime += DT;

	auto shader = _material->GetShader();

	// Transform
	auto world = GetTransform()->GetWorldMatrix();
	shader->PushTransformData(TransformDesc{ world });

	// GlobalData
	shader->PushGlobalData(Camera::S_MatView, Camera::S_MatProjection);

	// SnowData
	shader->PushSnowData(_desc);

	// Light
	_material->Update();

	// IA
	_vertexBuffer->PushData();
	_indexBuffer->PushData();

	shader->DrawIndexed(0, _pass, _drawCount * 6);
}

 

 

SnowBillboard::Update에서 _desc을 shader→PushSnowData에 인자로 넣어줘서

void Shader::PushSnowData(const SnowBillboardDesc& desc)
{
	if (_snowEffectBuffer == nullptr)
	{
		_snowBuffer = make_shared<ConstantBuffer<SnowBillboardDesc>>();
		_snowBuffer->Create();
		_snowEffectBuffer = GetConstantBuffer("SnowBuffer");
	}

	_snowDesc = desc;
	_snowBuffer->CopyData(_snowDesc);
	_snowEffectBuffer->SetConstantBuffer(_snowBuffer->GetComPtr().Get());
}

결국 ConstantBuffer에 들어가서 SnowBuffer에 있는 데이터를 갱신해준다.

 

이거에 따라서 Shader코드가 돌아가게 해주면 된다.

 

Engine을 빌드해서 문제가 없는지 테스트 해보고 고칠게 없으면 Shader 코드로 넘어가면 된다.

 

7.  29. SnowDemo.fx 쉐이더와 SnowDemo 클래스 만들기

28. BillboardDmo.fx를 복붙해서 29. SnowDemo.fx라고 한다.

Client/Shaders/Week5 필터에 넣는다.

 

BillboardDemo클래스도 복붙해서 이름을 SnowDemo라고 한다.

Client/Game 필터에 넣는다.

코드를 SnowDemo에 맞게 수정한다.

	_shader = make_shared<Shader>(L"29. SnowDemo.fx");

BillboardTest는 삭제한다.

 

Main에 가서 세팅한다.

 

8. SnowDemo::Init에서 SnowBillboard컴포넌트를 이용해 어떤 부위에 얼마나 눈을 뿌릴지 설정하기

SnowDemo::Init의 //Billboard 이부분을 새로운 방식으로 교체한다.

#include "SnowBillboard.h"
// Billboard
{
	auto obj = make_shared<GameObject>();
	obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
	obj->AddComponent(make_shared<SnowBillboard>(Vec3(100, 100, 100), 10000));
	{
		// Material
		{
			shared_ptr<Material> material = make_shared<Material>();
			material->SetShader(_shader);
			auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\grass.png");
			//auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\veigar.jpg");
			material->SetDiffuseMap(texture);
			MaterialDesc& desc = material->GetMaterialDesc();
			desc.ambient = Vec4(1.f);
			desc.diffuse = Vec4(1.f);
			desc.specular = Vec4(1.f);
			RESOURCES->Add(L"Veigar", material);

			obj->GetSnowBillboard()->SetMaterial(material);
		}
	}

	CUR_SCENE->Add(obj);
}

어떤 범위에 얼마만큼의 눈을 뿌릴 건지를 정해주고 있었는데

그 부분을 먼저 지정해줄 것이고, 나머지는 다를바가 없다.

 

9. 00. Global.fx에서 BlendState와 AlphaBlend 사용을 위한 MACRO를 추가하고 29. SnowDemo.fx 에서 적용하기

남은 건 Shader만 신경쓰면 된다.

알파 블렌딩을 사용할 예정이니 RasterizerState말고 BlendState 가 추가가 되어야 한다.

이를 위해 00. Global.fx로 가서

 

이를 위해 00. Global.fx로 가서

////////////////
// BlendState //
////////////////

BlendState AlphaBlend
{
	AlphaToCoverageEnable = false;

	BlendEnable[0] = true;
	SrcBlend[0] = SRC_ALPHA;
	DestBlend[0] = INV_SRC_ALPHA;
	BlendOp[0] = ADD;

	SrcBlendAlpha[0] = One;
	DestBlendAlpha[0] = Zero;
	BlendOpAlpha[0] = Add;

	RenderTargetWriteMask[0] = 15;
};

BlendState AlphaBlendAlphaToCoverageEnable
{
	AlphaToCoverageEnable = true;

	BlendEnable[0] = true;
	SrcBlend[0] = SRC_ALPHA;
	DestBlend[0] = INV_SRC_ALPHA;
	BlendOp[0] = ADD;

	SrcBlendAlpha[0] = One;
	DestBlendAlpha[0] = Zero;
	BlendOpAlpha[0] = Add;

	RenderTargetWriteMask[0] = 15;
};

BlendState AdditiveBlend
{
	AlphaToCoverageEnable = true;

	BlendEnable[0] = true;
	SrcBlend[0] = One;
	DestBlend[0] = One;
	BlendOp[0] = ADD;

	SrcBlendAlpha[0] = One;
	DestBlendAlpha[0] = Zero;
	BlendOpAlpha[0] = Add;

	RenderTargetWriteMask[0] = 15;
};

BlendState AdditiveBlendAlphaToCoverageEnable
{
	AlphaToCoverageEnable = true;

	BlendEnable[0] = true;
	SrcBlend[0] = One;
	DestBlend[0] = One;
	BlendOp[0] = ADD;

	SrcBlendAlpha[0] = One;
	DestBlendAlpha[0] = Zero;
	BlendOpAlpha[0] = Add;

	RenderTargetWriteMask[0] = 15;
};

 

오늘은 AlphaBlend를 사용할 것이다.

이를 사용하기 위해

///////////
// Macro //
///////////

#define PASS_BS_VP(name, bs, vs, ps)				\\
pass name											\\
{													\\
	SetBlendState(bs, float4(0, 0, 0, 0), 0xFF);	\\
    SetVertexShader(CompileShader(vs_5_0, vs()));	\\
    SetPixelShader(CompileShader(ps_5_0, ps()));	\\
}

이렇게 넣어준다. 

 

29. SnowDemo.fx로 돌아가 사용하면

technique11 T0
{
    PASS_BS_VP(P0, AlphaBlend, VS, PS)
};

이렇게 사용할 수 있다.

 

10. 29. SnowDemo.fx에 눈을 컨트롤 하는 코드를 작성하고 테스트하기 

BindShaderDesc.h에 넣어준

struct SnowBillboardDesc
{
	Color color = Color(1, 1, 1, 1);

	Vec3 velocity = Vec3(0, -5, 0); // 속도
	float drawDistance = 0;

	Vec3 origin = Vec3(0, 0, 0);
	float turbulence = 5; // 흔들리는 강도

	Vec3 extent = Vec3(0, 0, 0);
	float time;
};

이것과 포멧을 맞춰가지고, 이것에 해당하는 constantBuffer를 29. SnowDemo.fx에 파주면 된다.

cbuffer SnowBuffer
{
    float4 Color;
    float3 Velocity;
    float DrawDistance;

    float3 Origin;
    float Turbulence;

    float3 Extent;
    float Time;
};

VertexInput, V_OUT도 이런식으로 수정한다.

 

#include "00. Global.fx"
#include "00. Light.fx"
#include "00. Render.fx"

cbuffer SnowBuffer
{
    float4 Color;
    float3 Velocity;
    float DrawDistance;

    float3 Origin;
    float Turbulence;

    float3 Extent;
    float Time;
};

struct VertexInput
{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float2 scale : SCALE;
    float2 random : RANDOM; 
};

struct V_OUT // VertexOut은 global에서 만들어 놨어서 안겹치게 V_OUT으로 했다.
{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD;
    float alpha : ALPHA; // 눈이 반투명하게 보이게 하기 위해
};

V_OUT VS(VertexInput input)
{
    V_OUT output;

    float3 displace = Velocity * Time * 100;

    // 눈을 컨트롤 하는 공식 테스트
    // input.position.y += displace;
    input.position.y = Origin.y + Extent.y - (input.position.y - displace) % Extent.y;
    input.position.x += cos(Time - input.random.x) * Turbulence;
    input.position.z += cos(Time - input.random.y) * Turbulence;
    //input.position.xyz = Origin + (Extent + (input.position.xyz + displace) % Extent) % Extent - (Extent * 0.5f);

    float4 position = mul(input.position, W);

    float3 up = float3(0, 1, 0);
    //float3 forward = float3(0, 0, 1);
    float3 forward = position.xyz - CameraPosition(); // BillBoard
    float3 right = normalize(cross(up, forward));

    position.xyz += (input.uv.x - 0.5f) * right * input.scale.x;
    position.xyz += (1.0f - input.uv.y - 0.5f) * up * input.scale.y;
    position.w = 1.0f;

    output.position = mul(mul(position, V), P);
    output.uv = input.uv;

    output.alpha = 1.0f;

    // Alpha Blending
    //float4 view = mul(position, V);
    //output.alpha = saturate(1 - view.z / DrawDistance) * 0.8f;

    return output;
}

float4 PS(V_OUT input) : SV_Target
{ 
    float4 diffuse = DiffuseMap.Sample(LinearSampler, input.uv); 

    // alpha 값에 따라서 색상을 보간하는 코드, 눈대중으로 조정
    diffuse.rgb = Color.rgb * input.alpha * 2.0f;
    diffuse.a = diffuse.a * input.alpha * 1.5f;
    
    return diffuse;
}

technique11 T0
{
    PASS_BS_VP(P0, AlphaBlend, VS, PS)
};

Clinet를 빌드한다.

 

VS에서 눈송이들이 어느 위치일지 정해주고 있다고 결론을 내릴 수 있다.

 

실행을 하면

 

이렇게 나온다. 내리는 건 아니고 흔들리고 있는 걸 만들었다.

눈은 아니고 사실 grass.png이다.

색을 조정해서 눈처럼 보이게 한 것이다.

방향을 조절해서에 내리는 걸로 바꿀 수 있다.

이것저것 테스트 하면서 다양하게 만들 수 있다.

공식은 중요하지 않고 이런 느낌으로 여러개의 입자들을 조절할 수 있다는 게 핵심이다.

 

 

VS에서

    // Alpha Blending
    float4 view = mul(position, V);
    output.alpha = saturate(1 - view.z / DrawDistance) * 0.8f;

이걸 주석 해제하면 은은한 효과를 볼 수 있다.

지금은 다 검정색이라 티가 안나지만 배경이 있을 때 멀리 있는 눈은 은은하게 보이는 효과를 내게 할 때 쓸 수 있다.

 

색상이 두개 있을 때 섞어 주는데 알파값을 넣어주고, value만큼 1-value 만큼 곱해서 더해서 최종 색상을 구해주는 식으로 하는게 기본이다.

 

Particle은 유니티에서 굉장히 옵션이 많다. 프로그래밍의 영역이 아니라 아트의 영역이다.

 

11. 정리

요약하자면 Billboard는 카메라를 쳐다보는거였다.

Billboard를 여러개 만들어서 뿌려서 풀도 눈도 만들 수 있었다. 인스턴싱을 활용하지 않아도 많은 입자들을 배치하는게 가능하다는 걸 알 수 있었다.

 

갯수가 많다고 해서 꼭 instancing을 만들 필요 없이 정점을 만들어서 관리할 수 있다는 걸 알게 되었고, 경우에 따라서 VS에서 인위적으로 정점을 실시간으로 늘릴 수 있는 기능이 있다고 하면 input을 최소한으로 하되 GeometryShader 단계에서 늘려서 작업을 하는 것도 가능하다는 것을 알 수 있었다. 

반응형

'DirectX' 카테고리의 다른 글

88_빌보드 #1_풀심기  (0) 2024.04.03
87_Button 실습  (0) 2024.04.01
86_직교투영  (0) 2024.03.31
85_Raycasting(수학)  (0) 2024.03.31
84_Intersection (수학)  (0) 2024.03.31

댓글