-
[DirectX11] 빌보드(Billboard)와 파티클(Particle)2024년 03월 14일
- 유니얼
-
작성자
-
2024.03.14.:48
728x90DirectX 11로 게임 엔진 아키텍처 만들기
게임 개발과 3D 그래픽스에서 빌보드(Billboard)와 파티클(Particle) 시스템은 환경을 풍부하게 만들고 시각적인 효과를 더하기 위해 널리 사용됩니다. 이 글에서는 빌보드와 파티클의 기본 개념과 DirectX 11에서의 구현 방법을 소개합니다.
참고강의 링크:
빌보드(Billboard) 개념
빌보드는 항상 카메라를 향해 정면을 유지하는 2D 객체입니다. 3D 환경에서 특정 객체가 카메라의 방향에 관계없이 항상 사용자에게 잘 보이도록 하고 싶을 때 사용됩니다. 예를 들어, 원거리의 나무나 구름을 표현할 때 3D 모델링 대신 빌보드를 사용하여 렌더링 성능을 향상시킬 수 있습니다.
파티클(Particle) 시스템 개념
파티클 시스템은 수많은 작은 입자들을 이용하여 불, 연기, 안개, 폭발과 같은 시각적 효과를 생성합니다. 각 파티클은 독립적인 속성(위치, 속도, 색상, 수명 등)을 가지며, 이를 통해 다양한 자연 현상이나 추상적 효과를 표현할 수 있습니다.
DirectX 11에서 빌보드 구현
DirectX 11에서 빌보드를 구현하기 위해, 먼저 정점 셰이더에서 빌보드가 카메라를 항상 바라보도록 하는 변환을 적용합니다. 이를 위해 빌보드의 위치를 월드 공간에서 뷰 공간으로 변환한 뒤, Z 축 방향을 카메라 방향으로 설정합니다.
Billboard.h
#pragma once #include "Component.h" // 빌보드를 구성하는 각 정점을 정의하는 구조체 struct VertexBillboard { Vec3 position; // 정점의 위치 Vec2 uv; // 텍스처 좌표 Vec2 scale; // 빌보드의 크기 }; // 한 번에 렌더링할 수 있는 최대 빌보드 수를 정의 #define MAX_BILLBOARD_COUNT 500 // Component 클래스를 상속받아 빌보드 기능을 구현한 클래스 class Billboard : public Component { using Super = Component; // 부모 클래스의 별칭을 Super로 정의 public: Billboard(); // 생성자 ~Billboard(); // 소멸자 void Update(); // 매 프레임마다 호출되는 업데이트 함수 void Add(Vec3 position, Vec2 scale); // 새로운 빌보드를 추가하는 함수 // 빌보드에 사용될 재질을 설정하는 함수 void SetMaterial(shared_ptr<Material> material) { _material = material; } // 렌더링에 사용될 패스를 설정하는 함수 void SetPass(uint8 pass) { _pass = pass; } private: vector<VertexBillboard> _vertices; // 빌보드의 정점들을 저장하는 벡터 vector<uint32> _indices; // 인덱스 버퍼에 사용될 인덱스들을 저장하는 벡터 shared_ptr<VertexBuffer> _vertexBuffer; // 정점 버퍼 shared_ptr<IndexBuffer> _indexBuffer; // 인덱스 버퍼 int32 _drawCount = 0; // 현재 렌더링할 빌보드의 수 int32 _prevCount = 0; // 이전 프레임에서 렌더링한 빌보드의 수 shared_ptr<Material> _material; // 빌보드에 사용될 재질 uint8 _pass = 0; // 렌더링 패스 };
Billboard.cpp
#include "pch.h" #include "Billboard.h" #include "Material.h" #include "Camera.h" Billboard::Billboard() : Super(ComponentType::Billboard) { // 최대 빌보드 수에 따라 버텍스와 인덱스 버퍼 크기를 결정 int32 vertexCount = MAX_BILLBOARD_COUNT * 4; // 각 빌보드당 4개의 버텍스 int32 indexCount = MAX_BILLBOARD_COUNT * 6; // 각 빌보드당 6개의 인덱스 (2개의 삼각형) _vertices.resize(vertexCount); _vertexBuffer = make_shared<VertexBuffer>(); _vertexBuffer->Create(_vertices, 0, true); // 동적 업데이트를 위해 cpuWrite 옵션을 true로 설정 _indices.resize(indexCount); // 각 빌보드를 위한 인덱스 설정 for (int32 i = 0; i < MAX_BILLBOARD_COUNT; 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); // 인덱스 버퍼 생성 } Billboard::~Billboard() { } void Billboard::Update() { // 빌보드의 수가 변경되면 버텍스 버퍼를 업데이트 if (_drawCount != _prevCount) { _prevCount = _drawCount; D3D11_MAPPED_SUBRESOURCE subResource; DC->Map(_vertexBuffer->GetComPtr().Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource); { memcpy(subResource.pData, _vertices.data(), sizeof(VertexBillboard) * _vertices.size()); } DC->Unmap(_vertexBuffer->GetComPtr().Get(), 0); } auto shader = _material->GetShader(); // Transform auto world = GetTransform()->GetWorldMatrix(); shader->PushTransformData(TransformDesc{ world }); // GlobalData shader->PushGlobalData(Camera::S_MatView, Camera::S_MatProjection); // Light _material->Update(); // IA _vertexBuffer->PushData(); _indexBuffer->PushData(); shader->DrawIndexed(0, _pass, _drawCount * 6); // 빌보드 그리기 } void Billboard::Add(Vec3 position, Vec2 scale) { // 새로운 빌보드를 추가할 때 각 버텍스에 위치, UV 좌표, 크기 정보를 설정 _vertices[_drawCount * 4 + 0].position = position; _vertices[_drawCount * 4 + 1].position = position; _vertices[_drawCount * 4 + 2].position = position; _vertices[_drawCount * 4 + 3].position = position; _vertices[_drawCount * 4 + 0].uv = Vec2(0, 1); _vertices[_drawCount * 4 + 1].uv = Vec2(0, 0); _vertices[_drawCount * 4 + 2].uv = Vec2(1, 1); _vertices[_drawCount * 4 + 3].uv = Vec2(1, 0); _vertices[_drawCount * 4 + 0].scale = scale; _vertices[_drawCount * 4 + 1].scale = scale; _vertices[_drawCount * 4 + 2].scale = scale; _vertices[_drawCount * 4 + 3].scale = scale; _drawCount++; // 빌보드 수 증가 }
DirectX 11에서 파티클 시스템 구현
파티클 시스템을 구현하기 위해서는 파티클의 속성을 저장할 버퍼와 이를 업데이트할 수 있는 로직이 필요합니다. 각 파티클은 생성 시점, 위치, 속도, 색상 등의 정보를 가지며, 시간에 따라 이 속성들이 변경됩니다. 일반적으로 파티클 시스템은 GPU에서 계산하는 것이 효율적입니다.
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: // 생성자는 눈이 내리는 영역의 크기(extent)와 그려질 빌보드의 수(drawCount)를 매개변수로 받습니다. SnowBillboard(Vec3 extent, int32 drawCount = 100); ~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); // 빌보드의 위치를 extent 범위 내에서 랜덤하게 설정 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); // 빌보드의 버텍스에 위치, UV 좌표, 크기, 랜덤 값을 설정 _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); // 인덱스 버퍼 생성을 위한 인덱스 설정 const int32 indexCount = _drawCount * 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()->GetWorldPosition(); _desc.time = _elpasedTime; _elpasedTime += DT; // 경과 시간 업데이트 auto shader = _material->GetShader(); // 변환 데이터, 글로벌 데이터, 눈 데이터를 셰이더에 전달 auto world = GetTransform()->GetWorldMatrix(); shader->PushTransformData(TransformDesc{ world }); shader->PushGlobalData(Camera::S_MatView, Camera::S_MatProjection); shader->PushSnowData(_desc); // 빛 데이터 업데이트 _material->Update(); // 입력 어셈블러에 데이터 전달 _vertexBuffer->PushData(); _indexBuffer->PushData(); // 셰이더를 사용하여 빌보드 렌더링 shader->DrawIndexed(0, _pass, _drawCount * 6); } void SnowBillboard::Add(Vec3 position, Vec2 scale) { // 특정 위치에 새로운 빌보드 추가하는 로직은 본 예제에서 구현되지 않았음 }
프로젝트 호출
SnowDemo.h
#pragma once #include "IExecute.h" class SnowDemo :public IExecute { public: void Init() override; void Update() override; void Render() override; private: shared_ptr<Shader> _shader; };
SnowDemo.cpp
#include "pch.h" #include "RawBuffer.h" #include "TextureBuffer.h" #include "Material.h" #include "SnowDemo.h" #include "GeometryHelper.h" #include "Camera.h" #include "GameObject.h" #include "CameraScript.h" #include "MeshRenderer.h" #include "Mesh.h" #include "Material.h" #include "Model.h" #include "ModelRenderer.h" #include "ModelAnimator.h" #include "Mesh.h" #include "Transform.h" #include "VertexBuffer.h" #include "IndexBuffer.h" #include "Light.h" #include "Graphics.h" #include "SphereCollider.h" #include "Scene.h" #include "AABBBoxCollider.h" #include "OBBBoxCollider.h" #include "Terrain.h" #include "Camera.h" #include "Button.h" #include "Billboard.h" #include "SnowBillboard.h" void SnowDemo::Init() { _shader = make_shared<Shader>(L"29. SnowDemo.fx"); // Camera { auto camera = make_shared<GameObject>(); camera->GetOrAddTransform()->SetWorldPosition(Vec3{ 0.f, 0.f, -5.f }); camera->AddComponent(make_shared<Camera>()); camera->AddComponent(make_shared<CameraScript>()); camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, true); CUR_SCENE->Add(camera); } // Light { auto light = make_shared<GameObject>(); light->AddComponent(make_shared<Light>()); LightDesc lightDesc; lightDesc.ambient = Vec4(0.4f); lightDesc.diffuse = Vec4(1.f); lightDesc.specular = Vec4(0.1f); lightDesc.direction = Vec3(1.f, 0.f, 1.f); light->GetLight()->SetLightDesc(lightDesc); CUR_SCENE->Add(light); } // 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"UnityLogo", L"..\\Resources\\Texture\\UnityLogo.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"UnityLogo", material); obj->GetSnowBillboard()->SetMaterial(material); } } CUR_SCENE->Add(obj); } } void SnowDemo::Update() { } void SnowDemo::Render() { }
결론
게임 개발과 3D 그래픽스에서 빌보드와 파티클 시스템은 강력한 시각적 도구입니다. 빌보드는 간단하면서도 효과적으로 원거리 객체를 표현할 수 있게 해주며, 파티클 시스템은 복잡하고 다이나믹한 시각적 효과를 생성하는 데 사용됩니다. 이러한 기술들은 게임의 몰입감을 극대화하고, 사용자 경험을 풍부하게 만드는 데 중요한 역할을 합니다.
반응형다음글이전글이전 글이 없습니다.댓글