1️⃣ 왜 Sphere를 선택했는가?

스카이박스는 일반적으로 큐브 형태로 구현하는 경우가 많다. 카메라를 중심으로 큐브를 배치하고, 각 면에 하늘 텍스처를 입히는 방식이다. 하지만 이는 큐브 6면에 각각 텍스처를 입혀야 하고, 텍스처 좌표 계산도 번거롭다.

그래서 우리는 더 간단하게 구(Sphere) 하나를 카메라에 따라 움직이게 만들고, 그 안쪽 면에 하늘 텍스처를 입히는 방식으로 처리한다. 이렇게 하면 텍스처 하나로 전체 하늘을 표현할 수 있고, 구현도 훨씬 간편해진다.


2️⃣ Sky Sphere의 문제점과 해결책

🔹 문제 1: 뒷면이 보이지 않음

기본적으로 3D 엔진에서는 카메라가 보는 방향과 반대편(뒷면)의 폴리곤은 렌더링하지 않는다(Back-face culling). 하지만 Sky Sphere는 안쪽에서 바깥을 보는 구조이므로, 오히려 뒷면이 보이도록 설정해야 한다.

이를 위해 HLSL 쉐이더의 RasterizerState에서 다음과 같이 설정한다:

RasterizerState FrontCounterClockwiseTrue
{
    FrontCounterClockwise = true;
};

이렇게 설정하면, 시계 방향이 아닌 반시계 방향의 삼각형을 앞면으로 인식하게 되며, 구의 안쪽 면이 카메라에서 보이도록 처리할 수 있다.


🔹 문제 2: Sky Sphere가 너무 멀리 있어 안 보이는 문제

카메라의 원근 투영(perspective projection) 시스템에서 z-값이 멀수록 해당 객체는 더 멀리 있고, 때로는 렌더링 범위(Far Clip Plane) 를 넘어 렌더링되지 않을 수 있다.

이를 해결하려면:

  • Sky Sphere를 항상 카메라 근처(혹은 내부) 에 있게 만든다.
  • 하지만 동시에 멀리 있는 것처럼 보여야 한다.

이 두 조건을 동시에 만족시키기 위해 우리는 쉐이더의 버텍스 단계에서 강제로 깊이(z)를 조작한다.


3️⃣ 버텍스 쉐이더에서 깊이 조작하는 방법

float4 viewPos = mul(float4(input.position.xyz, 0), V);
float4 clipPos = mul(viewPos, P);
output.position = clipPos;
output.position.z = output.position.w * 0.999999f;

📌 여기에 핵심이 숨어있다!

  • input.position.xyzw=0으로 하여 곱하면 카메라의 위치(이동) 에는 영향을 받지 않고, 회전만 적용된다.
  • clipPos는 원근 투영을 거쳐 나온 좌표다. 원래는 이 값을 레스터라이저가 처리하면서 w로 나눠 깊이값(z/w)을 계산한다.
  • 하지만 우리는 z에 강제로 w * 0.999999를 대입했다. 이렇게 하면 z/w = 0.999999가 되어 거의 최대로 멀리 있는 물체처럼 보이게 된다.

⚠️ 만약 z = w로 설정해버리면, 깊이 테스트에서 정확히 1.0으로 판정되어 렌더링되지 않을 수 있다. 보통 LESS 테스트 조건이 기본이기 때문에 "같으면 버린다"는 조건이 된다. 그래서 0.999999f처럼 살짝만 줄이는 것이다.


4️⃣ 월드 변환을 왜 건너뛰는가?

Sky Sphere는 항상 카메라 중심에 있어야 한다. 즉, 게임 오브젝트의 월드 위치는 중요하지 않다.

그래서 쉐이더에서 월드 행렬(W)을 생략하고, View 행렬만 적용하는 구조로 처리한다:

// 월드 생략, View만 적용
float4 viewPos = mul(float4(input.position.xyz, 0), V);

여기서 w = 0을 넣으면 View 행렬 내의 이동 성분은 무시되고, 회전만 적용된다. 덕분에 Sky Sphere는 항상 카메라 위치에 붙어 있고, 회전만 따라가므로 자연스러운 배경이 된다.


5️⃣ 최종 쉐이더 구조

struct VS_OUT
{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD;
};

VS_OUT VS(VertexTextureNormalTangent input)
{
    VS_OUT output;

    float4 viewPos = mul(float4(input.position.xyz, 0), V);
    float4 clipPos = mul(viewPos, P);

    output.position = clipPos;
    output.position.z = output.position.w * 0.999999f; // 깊이값 보정
    output.uv = input.uv;

    return output;
}

float4 PS(VS_OUT input) : SV_TARGET
{
    float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
    return color;
}

그리고 technique11에서는 뒷면 렌더링이 되도록 설정:

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

6️⃣ SkyDemo.cpp에서의 처리 요약

📌 재질(Material) 설정

shared_ptr<Material> material = make_shared<Material>();
material->SetShader(_shader);
auto texture = RESOURCES->Load<Texture>(L"Sky", L"..\\Resources\\Textures\\Sky01.jpg");
material->SetDiffuseMap(texture);
RESOURCES->Add(L"Sky", material);

📌 객체(Object) 설정

_obj = make_shared<GameObject>();
_obj->GetOrAddTransform();
_obj->AddComponent(make_shared<MeshRenderer>());
_obj->GetMeshRenderer()->SetMesh(RESOURCES->Get<Mesh>(L"Sphere"));
_obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Sky"));

📌 카메라 설정

_camera = make_shared<GameObject>();
_camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
_camera->AddComponent(make_shared<Camera>());
_camera->AddComponent(make_shared<CameraScript>());

profile
李家네_공부방

0개의 댓글