🧭 1. Billboard란?

📘 개념

Billboard는 3D 공간에 존재하지만 항상 카메라를 바라보는 특성을 가진 2D 사각형(Quad) 형태의 오브젝트야. 마치 게임 속의 NPC 이름표, 체력바, UI 표지판, 파티클(비, 눈, 연기 등)이 항상 정면을 유지해야 할 때 쓰이지.

👀 왜 쓰는가?

3D 모델링보다 훨씬 가볍고, 오브젝트가 많아질 때 성능 면에서 유리해. 특히 반복되는 요소들(풀, 나무, 파티클 등)엔 필수.


🧱 2. 하나의 오브젝트에 Billboard 적용

2-1. BillboardTest 클래스 만들기

먼저 하나의 오브젝트가 항상 카메라를 바라보도록 회전(Rotation)을 조정해주는 컴포넌트 스크립트를 만들어야 해.

class BillboardTest : public MonoBehaviour
{
public:
    virtual void Update();
};

이 스크립트는 MonoBehaviour를 상속받아 Update()에서 매 프레임마다 자신의 회전값을 수정함으로써 카메라를 바라보게 만들어.


2-2. 회전을 맞추는 핵심 로직

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); // 행렬을 Scale, Rotation(Quaternion), Translation으로 분해

    Vec3 rot = Transform::ToEulerAngles(R); // Quaternion → EulerAngles(회전 벡터)
    GetTransform()->SetRotation(rot); // 최종적으로 적용
}

🔧 왜 Quaternion → Euler로 변환?

SetRotation은 Vec3 타입의 오일러 각도를 받기 때문이야. Quaternion은 회전을 정확하게 표현하기엔 좋지만, 일반적인 3D 게임 엔진에서 사용할 때는 오일러 각도로 변환해서 사용해.


🔁 3. 다수 오브젝트에 Billboard 적용하기 (정점 방식)

🚫 문제점

500개 오브젝트에 각각 BillboardTest를 붙이면 성능상 비효율적이고 관리도 어렵다.

✅ 해결책

정점을 늘리고, 쉐이더에서 좌표 연산을 수행하는 방식으로 변경한다.


🎯 4. Billboard 컴포넌트 구현 (쉐이더 기반)

4-1. VertexBillboard 구조체 정의

struct VertexBillboard
{
    Vec3 position; // 시작 위치
    Vec2 uv;       // 텍스처 좌표
    Vec2 scale;    // 크기
};

하나의 Billboard는 정점 4개(사각형)를 사용하므로, 이 구조체는 사각형 하나에 필요한 정보를 담는다.


4-2. 정점과 인덱스를 준비하고 한 번에 GPU에 전달

const int32 vertexCount = MAX_BILLBOARD_COUNT * 4;
const int32 indexCount = MAX_BILLBOARD_COUNT * 6;

_vertices.resize(vertexCount);
_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;
}

4-3. 정점 추가 함수

void Billboard::Add(Vec3 position, Vec2 scale)
{
    // 위치와 크기는 같고, 쉐이더에서 좌표 계산함
    for (int i = 0; i < 4; ++i)
    {
        _vertices[_drawCount * 4 + i].position = position;
        _vertices[_drawCount * 4 + i].scale = scale;
    }

    _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);

    _drawCount++;
}

🎨 5. 쉐이더 코드 (BillboardDemo.fx)

5-1. 정점 셰이더에서 실제 위치 계산

V_OUT VS(VertexInput input)
{
    V_OUT output;

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

    float3 up = float3(0, 1, 0);
    float3 forward = position.xyz - CameraPosition(); // 카메라 기준
    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;

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

    return output;
}

5-2. 픽셀 셰이더에서 알파값 처리

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

    if (diffuse.a < 0.3f)
        discard;

    return diffuse;
}

알파 블렌딩을 적용해서 텍스처의 투명도를 활용할 수 있게 해준다.


❄️ 6. 눈 내리기 (SnowBillboard)

Billboard를 응용해 눈처럼 떨어지는 파티클 효과를 만든다.

💡 핵심 차이점

  • Billboard는 정적
  • SnowBillboard는 시간에 따라 위치 변화

6-1. SnowBillboardDesc 구조체 (쉐이더에서 사용)

struct SnowBillboardDesc
{
    Color color;
    Vec3 velocity; // 떨어지는 속도
    float drawDistance;
    Vec3 origin;
    float turbulence; // 흔들림
    Vec3 extent;
    float time; // 경과 시간
};

6-2. 쉐이더에서 시간과 랜덤값을 활용한 위치 조정

input.position.y = Origin.y + Extent.y - (input.position.y - Velocity.y * Time * 100) % Extent.y;
input.position.x += cos(Time - input.random.x) * Turbulence;
input.position.z += cos(Time - input.random.y) * Turbulence;

시간 기반으로 눈이 떨어지고, X/Z축으로 흔들림 효과가 들어간다.


6-3. 알파 블렌딩 처리

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

diffuse.rgb = Color.rgb * input.alpha * 2.0f;
diffuse.a = diffuse.a * input.alpha * 1.5f;

카메라에서 멀어질수록 더 은은하게 보이도록 만들어주는 표현 방식이다.


profile
李家네_공부방

0개의 댓글