스카이박스는 일반적으로 큐브 형태로 구현하는 경우가 많다. 카메라를 중심으로 큐브를 배치하고, 각 면에 하늘 텍스처를 입히는 방식이다. 하지만 이는 큐브 6면에 각각 텍스처를 입혀야 하고, 텍스처 좌표 계산도 번거롭다.
그래서 우리는 더 간단하게 구(Sphere) 하나를 카메라에 따라 움직이게 만들고, 그 안쪽 면에 하늘 텍스처를 입히는 방식으로 처리한다. 이렇게 하면 텍스처 하나로 전체 하늘을 표현할 수 있고, 구현도 훨씬 간편해진다.
기본적으로 3D 엔진에서는 카메라가 보는 방향과 반대편(뒷면)의 폴리곤은 렌더링하지 않는다(Back-face culling). 하지만 Sky Sphere는 안쪽에서 바깥을 보는 구조이므로, 오히려 뒷면이 보이도록 설정해야 한다.
이를 위해 HLSL 쉐이더의 RasterizerState에서 다음과 같이 설정한다:
RasterizerState FrontCounterClockwiseTrue
{
FrontCounterClockwise = true;
};
이렇게 설정하면, 시계 방향이 아닌 반시계 방향의 삼각형을 앞면으로 인식하게 되며, 구의 안쪽 면이 카메라에서 보이도록 처리할 수 있다.
카메라의 원근 투영(perspective projection) 시스템에서 z-값이 멀수록 해당 객체는 더 멀리 있고, 때로는 렌더링 범위(Far Clip Plane) 를 넘어 렌더링되지 않을 수 있다.
이를 해결하려면:
이 두 조건을 동시에 만족시키기 위해 우리는 쉐이더의 버텍스 단계에서 강제로 깊이(z)를 조작한다.
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.xyz를 w=0으로 하여 곱하면 카메라의 위치(이동) 에는 영향을 받지 않고, 회전만 적용된다.clipPos는 원근 투영을 거쳐 나온 좌표다. 원래는 이 값을 레스터라이저가 처리하면서 w로 나눠 깊이값(z/w)을 계산한다.z에 강제로 w * 0.999999를 대입했다. 이렇게 하면 z/w = 0.999999가 되어 거의 최대로 멀리 있는 물체처럼 보이게 된다.⚠️ 만약
z = w로 설정해버리면, 깊이 테스트에서 정확히 1.0으로 판정되어 렌더링되지 않을 수 있다. 보통LESS테스트 조건이 기본이기 때문에 "같으면 버린다"는 조건이 된다. 그래서0.999999f처럼 살짝만 줄이는 것이다.
Sky Sphere는 항상 카메라 중심에 있어야 한다. 즉, 게임 오브젝트의 월드 위치는 중요하지 않다.
그래서 쉐이더에서 월드 행렬(W)을 생략하고, View 행렬만 적용하는 구조로 처리한다:
// 월드 생략, View만 적용
float4 viewPos = mul(float4(input.position.xyz, 0), V);
여기서 w = 0을 넣으면 View 행렬 내의 이동 성분은 무시되고, 회전만 적용된다. 덕분에 Sky Sphere는 항상 카메라 위치에 붙어 있고, 회전만 따라가므로 자연스러운 배경이 된다.
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()));
}
};
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);
_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>());