DirectX

88_빌보드 #1_풀심기

devRiripong 2024. 4. 3. 01:38
반응형

개념은 쉬운데 응용해서 할 수 있는게 많다.

 

물체가 있다고 했을 때

ui도 종류가 많다. 인게임에 배치되어 있는 ui인데 표지판처럼 나타나는 경우도 있다. 물체가 플레이어를 바라보는 것을 빌보드라고 한다.

 

스크립트를 붙여서 업데이트를 할 때 마다 플레이어를 바라보게 rotation을 수정하면 된다.

간단한 물체 하나만 있으면 그렇게 해도 된다. 하지만 물체가 여러개라면 어떻게 해야할까? 

 

1. 하나의 Quad에 빌보드 적용하기 

1) BillboardDemo클래스 만들고 Quad를 하나 그리기

ButtonDemo를 복붙해서 이름을 BillboardDemo라고 한다.

Client/Game 필터에 넣는다.

코드를 수정한다.

 

쉐이더도 나중엔 새로 파줘야 한다.

 

BillboardDemo::Init의 UICamera는 제거한다.

		camera->AddComponent(make_shared<CameraScript>());

CameraScript 컴포넌트를 넣는 부분의 주석을 해제한다.

 

UI용도의 첫번째 Mesh를 삭제한다.

두번 째 Mesh를 Quad로 만들어서 몇가지를 테스트 해본다.

		auto mesh = RESOURCES->Get<Mesh>(L"Quad");

 

Main.cpp로 가서 BillboardDemo를 세팅하고

실행을 한다.

이렇게 정면을 바라보고 있다.

 

하지만 카메라를 움직여 보면 뒷면 까지 볼 수 있을 것이다.

이렇게 되지 않고 물체가 항상 카메라를 바라보고 싶게 만들고 싶다면 그 개념이 billboard라고 볼 수 있다.

회전값만 적절히 잘 조립해주면 바라보게 만들어 줄 수 있을 것이다.

 

2) Transform의 ToEulerAngles 함수를 static으로 만들기

Transform.cpp에

Vec3 ToEulerAngles(Quaternion q) 

을 활용하기 위해

Vec3 Transform::ToEulerAngles(Quaternion q)
{
	Vec3 angles;

	// roll (x-axis rotation)
	double sinr_cosp = 2 * (q.w * q.x + q.y * q.z);
	double cosr_cosp = 1 - 2 * (q.x * q.x + q.y * q.y);
	angles.x = std::atan2(sinr_cosp, cosr_cosp);

	// pitch (y-axis rotation)
	double sinp = std::sqrt(1 + 2 * (q.w * q.y - q.x * q.z));
	double cosp = std::sqrt(1 - 2 * (q.w * q.y - q.x * q.z));
	angles.y = 2 * std::atan2(sinp, cosp) - 3.14159f / 2;

	// yaw (z-axis rotation)
	double siny_cosp = 2 * (q.w * q.z + q.x * q.y);
	double cosy_cosp = 1 - 2 * (q.y * q.y + q.z * q.z);
	angles.z = std::atan2(siny_cosp, cosy_cosp);

	return angles;
}

이렇게 Transform::를 붙여 수정한다.

Transform.h에서

	static Vec3 ToEulerAngles(Quaternion q); 

딴 곳에서도 사용할 수 있게 이렇게 static으로 선언 해준다.

 

3) MonoBehaviour를 상속받은 BillboardTest클래스 정의하기

BillboardDemo.h에

#include "MonoBehaviour.h"
class BillboardTest : public MonoBehaviour
{
public:
	virtual void Update(); // GameObject를 조작할 수 있는 스크립트를 MonoBehaviour를 상속 받아 만든 다음에 걔를 컴포넌트로 붙여주면 작동을 했었던 아이다.
};

이렇게 MonoBehaviour를 상속받은 BillboardTest 클래스와 Update 함수를 선언하고,

 

4) BillboardTest::Update에서 Rotation을 세팅하기

void BillboardTest::Update()
{
	auto go = GetGameObject(); // 이 컴포넌트가 붙은 주인

	Vec3 up = Vec3(0, 1, 0); 	
	Vec3 cameraPos = CUR_SCENE->GetMainCamera()->GetTransform()->GetPosition(); 
	Vec3 myPos = GetTransform()->GetPosition(); 

	Vec3 forward = cameraPos - myPos; 
	forward.Normalize(); 
	
	Matrix lookMatrix = Matrix::CreateWorld(myPos, forward, up); 

	Vec3 S, T; 
	Quaternion R; 
	lookMatrix.Decompose(S, R, T); 

	Vec3 rot = Transform::ToEulerAngles(R); 

	GetTransform()->SetRotation(rot); 
}

이렇게 까지만 들어가도 빌보드 효과를 낼 수 있다.

5) BillboardDemo::Init에서 obj에 BillboardTest 컴포넌트를 붙이고 테스트하기

BillboardDemo::Init에서

Mesh에 BillboardTest를 붙여줘야 한다.

		obj->AddComponent(make_shared<BillboardTest>()); 

 

실행을 해보면

 

옆을 보려고 해도 카메라의 회전을 따라 이미지도 같이 회전을 하고 있다.

 

이게 기본적인 빌보드 방식의 원리다.

 

응용할 곳은 많다.

VR에서 UI를 만들 때 두리번 거려도 UI는 항상 내 쪽을 바라보게 하는 식으로 만들어 줄 때 사용하게 빌보드다.

 

이걸 가지고 여러가지 작업을 해볼 것인데, 파티클 시스템과 연관 지어서 해볼 수 있다.

500개씩 뿌려서 모델들이 내 쪽을 보도록 만들어 볼 수 있다.

 

풀이나 나무, 비, 눈, 환경 같은 부분들도 입자들이 카메라를 바라봐야 한다.

 

파티클의 뒷면은 없는 경우가 많다.

 

여러개를 만들 때 사용하는 방법이 여러개 있었다.

인스턴싱을 하면 코드가 너무 복잡하다.

두번째 방법은 실제로 정점을 한번에 많이 만들어서 각기 정점의 위치를 보정하는 걸 쉐이더에서 하는 방법이다. 더 나아가서 정점을 4개만 넣어 놓은 걸 몇 백개로 불리는 걸 쉐이더에서 할 수 있다. 실시간으로 정점 개수를 늘리는 부분을 렌더링 파이프 라인 단계에서 할 수가 있다.

 

2. 여러개의 물체에 빌보드 적용하기

오늘 해 볼 것은

먼저 물체를 여러개 만드는게 아니라 정점을 늘려가지고, 정점을 쉐이더에 건내 줄 것인데 다만 중요한건 세부적인 위치는 매번 다시 계산해서 넣어주는 그런 방식이 아니라 첫 위치를 늘려준 다음에 그 자체를 하나의 통 메쉬처럼 만들어서 건내주되 세부적인 메쉬들끼리의 좌표 계산을 쉐이더에 떠넘기는 식으로 만들 것이다.

 

1) Component를 상속받은 Billboard 클래스 생성하기

Engine/06. UI & Effect 필터에 Component를 상속받 클래스를 추가한다. 이름을 Billboard라고 한다.

 

2) Component.h의  ComponentType에 Billboard 추가하기

Component.h에 enum class ComponentType : uint8에

enum class ComponentType : uint8
{
	Transform,
	MeshRenderer,
	Camera,
	ModelRenderer,
	Animator,
	Light,
	Collider,
	Terrain,
	Button, 
	Billboard,
	// ...
	Script, 

	End, 
};

Billboard를 추가한다.

 

3) 새로운 Component의 Get함수를 GameObject에 만들기

새로운 Component가 추가됐을 때 하는 작업을 해준다.

그리고 GameObject.h에

class Billboard; 

를 전방선언한다.

	shared_ptr<Billboard> GetBillboard(); 

GameObject.cpp에

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

 

4) Billboard 클래스에 코드를 채우기

이제 Billboard로 가서

#pragma once
#include "Component.h"

struct VertexBillboard
{
	Vec3 position; 
	Vec2 uv; 
	Vec2 scale; 
};

#define MAX_BILLBOARD_COUNT 500

class Billboard : public Component
{
	using Super = Component; 

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

 

#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; // 4각형은 3각형 2개이기 때문에 

	_vertices.resize(vertexCount); 
	_vertexBuffer = make_shared<VertexBuffer>(); 
	_vertexBuffer->Create(_vertices, 0, true); // 갱신할 수 있게 CPU write를 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();

	// 인스턴싱을 하지 않는 이유는 빌보드를 포함하고 있는 객체를 늘리는게 아니라 빌보드하는 컴포넌트 안에서 동일하게 그려지는 애들을 다 묶어서 
	// 정점을 한번에 그리기 위해서 DrawIndexed를 한 번만 해주면 된다. 
	shader->DrawIndexed(0, _pass, _drawCount * 6);
}

void Billboard::Add(Vec3 position, Vec2 scale)
{
	// 같은 위치의 정점을 넣었기 때문에 아무것도 안그려질 것이다. 
	// 정점 갯수만 맞춰서 건내줄테니 shader에서 알아서 좌표 계산을 해서 위치를 배정하라는 말이다.
	// 이렇게 해야 풀 같은 거 각기 다른 크기의 다른 위치로 만들 수 있기 때문에 작업을 해주려고 하는 것이다.
	// CPU에서 모든 애들을 만들어서 넘기는게 아니라 언제든지 쉐이더 쪽에서 작업을 해주도록 유도를 해준다.
	_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++;
}

 

여기서 눈 여겨 봐야 할 부분은 Instancing 처럼 물체를 늘려야 하는게 아니라 정점을 늘리고 있다.

Billboard::Add에서 추가 할 때 마다 사각형의 개수에 따라 가지고 세팅하는 것을 늘려가지고 세팅하고 있고, 다만 신기한 건 position, scale 정보는 다 고정값으로 놓고 나중에 쉐이더에서 이걸 고쳐줄 예정이다. UV좌표는 어쨌든 사각형이니까 그거에 따라가지고 각각의 UV좌표는 기입해 놓는 것을 볼 수 있다.

 

Billboard 컴포넌트가 일종의 렌더러가 되는 것이다. BillboardRenderer라고 하는게 맞긴 할 것이다.

 

이 코드가 실행되면 정보에 맞게 기입이 될 것이고, 이걸 이용해서 BillboardDemo로 돌아가서 작업을 해본다.

 

5) 28. BillboardDemo.fx 쉐이더 만들기

새 쉐이더를 추가한다.

Client/lShaders에 Week5 필터를 추가하고,

27. StructuredBufferDemo.fx를 복붙해서 이름을 28. BillboardDemo.fx라고 한다.

Week5 필터에 넣는다.

코드의 내용을 지우고 새로 작성한다.

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

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

struct V_OUT // VertexOut은 global에서 만들어 놨어서 안겹치게 V_OUT으로 했다.
{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD;
};

V_OUT VS(VertexInput input)
{
    V_OUT output; 
    
    float4 position = mul(input.position, W); 
    
    float3 up = float3(0, 1, 0); 
    float3 forward = float3(0, 0, 1);  // TODO 카메라를 바라 보니까 바뀔 것이다.
    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; 
    
    return output; 
}

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

    
    return diffuse; 
}

technique11 T0
{
    pass P0
    {
        //SetRasterizerState(FillModeWireFrame);
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS()));
    }
};

 

이렇게 코드가 만들어졌다. 이걸 먼저 테스트를 해야 이해가 쉽다.

 

6) BillboardDemo에서 Billboard 컴포넌트와 28. BillboardDemo.fx를 이용해 500개의 Quad를 그리기

빌드를 하고 다시 BillboardDemo로 돌아가서 테스트를 해본다.

새로 만든 Billboard 컴포넌트를 사용한다.

BillboardDemo::Init에서

_shader = make_shared<Shader>(L"28. BillboardDemo.fx");

기존의 //Material 과 //Mesh를 삭제하고

Mesh에 Billboard 컴포넌트를 붙인 다음에 거기다 많은 물체들을 넣어주는 식으로 작업을 할 것이다.

// Billboard
{
    auto obj = make_shared<GameObject>();
    obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
    obj->AddComponent(make_shared<Billboard>());
    {
        // 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->GetBillboard()->SetMaterial(material);
        }
    }

    for (int32 i = 0; i < 500; i++)
    {

        Vec2 scale = Vec2(1 + rand() % 3, 1 + rand() % 3);
        Vec2 position = Vec2(-100 + rand() % 200, -100 + rand() % 200);

        obj->GetBillboard()->Add(Vec3(position.x, scale.y * 0.5f, position.y), scale);
    }

    CUR_SCENE->Add(obj);
}

실행을 하면

 

지금은 billboard 기능을 꺼놨기 때문에 카메라를 회전할 때 카메라를 바라보지 않는다.

 

7) VS를 수정해 500개의 오브젝트에 빌보드 기능을 적용하기

카메라를 바라보게 만드려면 쉐이더에서 코드를 고치면 된다.

V_OUT VS(VertexInput input)
{
    V_OUT output; 
    
    float4 position = mul(input.position, W); 
    
    float3 up = float3(0, 1, 0); 
    //float3 forward = float3(0, 0, 1);  // TODO 카메라 바라보게 
    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; 
    
    return output; 
}

 

이제는 카메라를 회전해도 앞면만 보인다.

 

8) grass 이미지로 바꾸기

풀 이미지로 바꾸면 항상 풀의 앞면만 보이게 된다.

비, 눈, 환경 만들 때 작은 입자인 경우는 괜찮게 보일 수 있다.

 

grass.png를 Resources\Textures에 넣고

 

BillboardDemo::Init에서

auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\grass.png");
//auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\veigar.jpg");

이렇게 texture를 교체해서 실행한다.

 

풀이 보이는데 알파값이 안들어갔다.

 

9) 풀에 알파값 적용하기

풀이 보이는데 알파값이 안들어갔다.

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

    //clip(diffuse.a - 0.3f); 
    if (diffuse.a < 0.3f)
        discard; 
    
    return diffuse;
}

이렇게 하면 알파값이 어느정도 이하면 안그려지게 할 수 있다.

clip(diffuse.a - 0.3f); 도 같은 의미니 둘 중 하나를 사용하면 된다.

 

Main.cpp에서

	desc.clearColor = Color(0.f, 0.f, 0.f, 0.f);

배경을 검은색으로 하고 실행하면

이렇게 바뀐다.

 

빌보드를 이용하면 이런 걸 할 수 있다.

풀이 많을 때 동일하지 않게 만들 수 있다.

 

이걸 이용하면 눈도 만들 수 있다.

deltatime에 따라 좌표가 변환이 되게 쉐이더에서 수정을 해주면 된다.

 

반응형