IBL (Image Based Lighting)
IBL은 이미지를 통해서 주변광을 제공하는 방법이다.
IBL의 유무에 따른 결과를 비교하면 다음과 같다.

이 예시에 사용된 건 금속 재질이다.
금속은 대부분의 빛을 반사하는데, Roughness가 낮은 경우에는 반사되는 빛이 한 방향으로 모이게 된다.
이런 경우에는 빛이 반사되는 좁은 영역을 제외하고는 전체적으로 어둡게 보이므로 물체를 알아보기 어렵다.
금속 재질을 더 밝게 표현하기 위해 주변에 조명을 많이 배치할 수는 있지만, 계산해야 하는 양 또한 늘어나므로 추가 가능한 조명의 개수에 한계가 있다.
이때, IBL을 적용하면 새로운 조명을 추가하지 않고도 사방에 조명을 추가한 것처럼 보이게 할 수 있다.
특히 PBR은 사실적인 묘사를 하는 것이 목적이므로, 현실의 이미지를 활용할 수 있는 IBL은 필수라고 할 수 있다.
IBL을 통해 주변광을 계산하려면...
이전 글에서 언급했던 렌더링 방정식을 보면, IBL을 어디에서 계산해야 하는지 알 수 있다.
\[L_o(p,\mathbf{w}_o)=L_e(p,\mathbf{w}_o)+\int_{\Omega}f(p,\mathbf{w}_i,\mathbf{w}_o)L_i(p,\mathbf{w}_i)(\mathbf{n}\cdot \mathbf{w}_i)\,d\mathbf{w}_i\]
여기에서 $\int_{\Omega}$는 반구의 모든 방향에서 들어오는 빛을 모두 누적한다는 것을 의미한다.
따라서, 이미지가 제공하는 주변광에 대한 정보를 적용하려면 이 적분을 해야 한다.
직접광의 경우에는 이 적분 과정이 비교적 단순했다.
각 조명에 대해서 루프를 돌면서 BRDF를 계산한 결과를 누적하면 됐다.
하지만, 주변광의 경우에는 정의 그대로 반구 방향에서 들어오는 모든 빛에 대해서 계산을 해야 한다.
이를 위해서는 반구 면적에 대한 조명 정보를 기반으로 BRDF를 계산해서 누적해야 한다.
이 누적 과정을 매 프레임 진행하면 성능에 많은 부담을 주게 되어 실시간 렌더링에 사용하기 부적합하다.
따라서, IBL은 미리 계산할 수 있는 부분을 먼저 계산하여, 실시간 연산의 부담을 낮추는 방식으로 진행한다.
이때, 약간의 근사(approximation)를 통해 더 많은 부분을 미리 계산할 수 있고, 덕분에 실시간 렌더링이 가능할 정도로 최적화할 수 있다.
준비해야 할 것
IBL을 위해 준비해야 할 것은 4가지 정도로 정리할 수 있다.
- HDRI (High Dynamic Range Image)
- Cube map
- Pre-filtered environmnet map
- Irradiance environment map
이 중에서 Pre-filtered map과 Irradiance map에 대해서는 다뤄야 할 내용이 많기 때문에, 다음 글에서 다룰 예정이다.
HDRI

HDRI는 IBL에 사용되는 이미지다.
즉, 이미지 기반의 조명 정보를 가지고 있다.
명칭의 의미로만 보면 HDR 이미지(High Dynamic Range Image)긴 한데, 위의 예시처럼 구체 표면을 펼쳐놓은 듯한 이미지(Equirectangular projection)는 IBL에 사용할 수 있다.

HDR 이미지를 사용하는 이유는, 1.0보다 더 큰 값을 표현할 수 있기 때문이다. (float 형식)
따라서, 태양과 같은 밝은 빛도 표현할 수 있어 IBL에 사용하기 적합하다.
이와 반대로 SDR의 경우에는, 0.0과 1.0 사이의 값을 0에서 255 사이의 정수 값으로 양자화하여 표현하기 때문에 표현 가능한 범위가 제한적이다.
SDR도 사용은 가능하지만 물리에 기반한 렌더링을 위해서는 HDR을 사용하는 것이 권장된다.
HDRI는 Poly Haven이라는 곳에서 다양한 이미지를 무료로 제공하고 있다.
파일 포맷은 .hdr과 .exr을 제공하는데, 둘 중 원하는 것을 사용하면 된다. (일반적으로 .hdr 포맷 파일의 크기가 더 작다.)
HDRIs • Poly Haven
Previously known as HDRI Haven. Hundreds of free HDRI environments, ready to use for any purpose. No login required.
polyhaven.com
Cube map

큐브맵은 스카이 박스처럼 주로 주변 배경을 표현하기 위해 사용된다.
위의 HDRI를 구체에 매핑했을 때, 구체의 중심에서 각 방향에 대한 색상 값을 구체에 외접한 정육면체에 투영하는 방식으로 제작된다.
큐브맵은 HDRI를 기반으로 제작되는데, 외부 프로그램으로 미리 만들어 둘 수도 있고 셰이더를 통해 필요할 때 제작할 수도 있다.
외부 프로그램으로 제작해 두면 사용하기에는 편한데, 다양한 HDRI를 바꿔가며 적용하기에는 번거롭다.
대신, 셰이더를 통해 제작하면 어떠한 HDRI든 내부에서 큐브맵으로 변환되므로, 다양한 이미지를 적용해 보기에 좋다.
로직도 어렵지 않으므로, 셰이더를 통해 큐브맵으로 변환하는 게 훨씬 나은 방법이다.
구현
HDRI 로드
HDR 파일을 읽으려면 DirectXTex 라이브러리가 필요하다. (DirectXTK와는 별개로 필요)
https://github.com/microsoft/DirectXTex
GitHub - microsoft/DirectXTex: DirectXTex texture processing library
DirectXTex texture processing library. Contribute to microsoft/DirectXTex development by creating an account on GitHub.
github.com
HDR 이미지 로드 과정은 어느 정도 정해져 있으니 참고하여 진행했다.
#include "DirectXTex/DirectXTex.h"
HRESULT FResourceManager::LoadTextureFromHDR(ID3D11Device* Device, const wchar_t* Filename)
{
HRESULT hr = S_OK;
DirectX::TexMetadata MetaData;
DirectX::ScratchImage ScratchImage;
hr = DirectX::LoadFromHDRFile(Filename, &MetaData, ScratchImage);
if (FAILED(hr))
{
return hr;
}
// to Resource
ID3D11Resource* TextureResource = nullptr;
hr = DirectX::CreateTexture(
Device,
ScratchImage.GetImages(),
ScratchImage.GetImageCount(),
MetaData,
&TextureResource
);
if (FAILED(hr))
{
return hr;
}
// to Texture2D
ID3D11Texture2D* Texture = nullptr;
hr = TextureResource->QueryInterface(__uuidof(ID3D11Texture2D), (void**)&Texture);
TextureResource->Release();
if (FAILED(hr))
{
return hr;
}
// to SRV
ID3D11ShaderResourceView* TextureSRV = nullptr;
hr = DirectX::CreateShaderResourceView(
Device,
ScratchImage.GetImages(),
ScratchImage.GetImageCount(),
MetaData,
&TextureSRV
);
if (FAILED(hr))
{
return hr;
}
// 그 외 작업할 것을 진행 (e.g. 리소스 관리 등)
// Description을 가져오는 방법 예시
D3D11_TEXTURE2D_DESC TexDesc;
Texture->GetDesc(&TexDesc);
uint32 Width = TexDesc.Width;
uint32 Height = TexDesc.Height;
return hr;
}
Cube map 제작
큐브맵 제작은 컴퓨트 셰이더(CS)에서 하면 쉽고 빠르게 할 수 있다.
그리고 이후에도 컴퓨트 셰이더를 많이 쓰게 되므로, 큐브맵 제작을 통해 활용 방법을 익혀갈 수도 있다고 생각한다.
제작 방식은 위에서 언급했듯이 HDRI를 구체에 매핑하고, 구체의 중심에서 각 방향에 대한 색상 값을 큐브에 투영하는 방식이다.
HDRI는 구체의 표면을 직사각형의 평면에 펼쳐놓은 건데, 이를 다시 구체로 되돌리는 셈이다.
일단 구현을 시작하기 전에 알아야 할 것을 정리하면 다음과 같다.
- DirectX에서 큐브맵은 Texture2D의 배열로 구성되며, 배열의 길이는 6이다.
- Shader Resource View에서 텍스처 큐브로 설정하면, HLSL에서는 TextureCube 타입으로 접근 가능하다.
- 텍스처 6개로 큐브를 구성해 주는 정해진 규칙이 있다.
- TextureCube는 방향벡터를 통해 해당 방향의 픽셀 값을 샘플 할 수 있다.
- 컴퓨트 셰이더에서는 UAV(Unordered Access View)를 통해 리소스의 값에 접근하고 작성할 수 있다.
- Texture2D 배열 리소스의 경우에는, 픽셀의 X, Y 좌표와 텍스처의 인덱스로 구성된 int3 타입을 통해 하나의 픽셀에 접근하고 값을 작성할 수 있다.
예시:TextureArrayUAV[int3(X, Y, Index)] = float4(R, G, B, A); - 읽기만 가능한 Shader Resource View와는 반대 느낌이다.
- Texture2D 배열 리소스의 경우에는, 픽셀의 X, Y 좌표와 텍스처의 인덱스로 구성된 int3 타입을 통해 하나의 픽셀에 접근하고 값을 작성할 수 있다.
- 컴퓨트 셰이더는 여러 스레드를 그룹으로 묶어서 작업을 동시에 처리(SIMT)한다.
- 컴퓨트 셰이더에서 해야 할 작업의 크기에 따라 스레드 그룹의 크기와 그룹의 개수를 지정하면 된다.
- 스레드 그룹은 3차원으로 구성되며, 컴퓨트 셰이더를 실행(Dispatch)할 때에는 각 차원에 대한 그룹의 개수를 지정한다.
예시:DeviceContext->Dispatch(NumGroupsX, NumGroupsY, NumGroupsZ); - 최적의 그룹 크기는 GPU에 따라 달라지며, Nvidia의 경우 32의 배수(Warp), AMD의 경우 64의 배수(Wavefront)다.
- 셰이더에서는 이 작업을 수행할 스레드에 대한 정보를 얻을 수 있다.
예시: 몇 번째 그룹인지(SV_GroupID), 몇 번째 스레드인지(SV_DispatchThreadID), 그룹에서의 몇 번째 스레드인지(SV_GroupThreadID)
다음으로, 큐브맵을 어떻게 제작할지를 결정해야 한다.
큐브맵에 사용할 리소스는 Texture2D Array로 생성하고, 컴퓨트 셰이더에서 각 픽셀에 값을 작성해야 한다.
이때, 하나의 스레드가 하나의 픽셀을 담당하게 된다.
문제는 각 픽셀에 작성해야 하는 색상이 무엇인지에 대한 것이다.
여기에는 아래의 이미지처럼 정해진 규칙이 있기 때문에 약간의 과정만 거치면 쉽게 알 수 있다.

- 위 이미지는 큐브맵을 구성하는 6개의 텍스처가 Texture2D Array에서 각각 몇 번째 인덱스(괄호 안의 숫자)에 해당하는지를 보여준다.
- 컴퓨트 셰이더에서는 현재 작업하는 스레드에 대한 정보를 알 수 있고, 이를 통해 현재 작업할 픽셀이 큐브맵의 중심으로부터 어느 방향에 해당하는지를 알 수 있다.
- 이 방향에 대한 정보를 통해, HDRI에서 어느 UV의 색상 값을 샘플해야 하는지를 알 수 있다. 이때 구면좌표계가 사용된다.
구면 좌표계에 대한건 나중에 다루기로 하고, 일단 컴퓨트 셰이더를 위한 설정부터 한다.
먼저 큐브맵을 위한 Texture2D Array, UAV, SRV를 생성한다.
// 큐브맵을 위한 리소스 생성.
/* 멤버 변수 선언 참고
ID3D11Texture2D* CubeMapTexture = nullptr;
ID3D11UnorderedAccessView* CubeMapUAV = nullptr;
ID3D11ShaderResourceView* CubeMapSRV = nullptr;
uint32 TextureSize = 1024;
*/
// Texture2D 생성.
D3D11_TEXTURE2D_DESC Desc = {};
Desc.Width = TextureSize;
Desc.Height = Desc.Width;
Desc.MipLevels = 0;
Desc.ArraySize = 6; // 큐브맵으로 활용할 것이므로 배열의 길이는 6.
Desc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; // HDR이므로, 16비트 사용.
Desc.SampleDesc.Count = 1;
Desc.SampleDesc.Quality = 0;
Desc.Usage = D3D11_USAGE_DEFAULT;
// SRV, UAV에 바인딩. 또한 밉맵 생성을 위해 렌더 타겟에도 바인딩.
Desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_RENDER_TARGET;
Desc.CPUAccessFlags = 0;
// 큐브맵 Flag 지정. 또한, 밉맵 생성 예정.
Desc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE | D3D11_RESOURCE_MISC_GENERATE_MIPS;
HRESULT hr = Graphics->Device->CreateTexture2D(&Desc, nullptr, &CubeMapTexture);
if (FAILED(hr))
{
return hr;
}
// UAV 생성.
D3D11_UNORDERED_ACCESS_VIEW_DESC UAVDesc = {};
UAVDesc.Format = Desc.Format; // 포맷은 텍스처와 동일하게.
UAVDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2DARRAY;
UAVDesc.Texture2DArray.MipSlice = 0;
UAVDesc.Texture2DArray.FirstArraySlice = 0;
UAVDesc.Texture2DArray.ArraySize = Desc.ArraySize;
hr = Graphics->Device->CreateUnorderedAccessView(CubeMapTexture, &UAVDesc, &CubeMapUAV);
if (FAILED(hr))
{
return hr;
}
// SRV 생성.
// SRV의 경우에는 제작된 큐브맵을 화면에 렌더하기 위함도 있지만, 밉맵을 만들기 위한 목적도 있음.
D3D11_SHADER_RESOURCE_VIEW_DESC SRVDesc = {};
SRVDesc.Format = Desc.Format; // 포맷은 텍스처와 동일하게.
SRVDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE; // 텍스처 큐브로 지정.
SRVDesc.TextureCube.MostDetailedMip = 0;
SRVDesc.TextureCube.MipLevels = -1; // 모든 밉 레벨 사용.
hr = Graphics->Device->CreateShaderResourceView(CubeMapTexture, &SRVDesc, &CubeMapSRV);
if (FAILED(hr))
{
return hr;
}
컴퓨트 셰이더를 위한 HLSL 파일도 작성한다.
// Shaders/Bake/EquirectToCube.hlsl
Texture2D SourceTexture : register(t0); // HDRI SRV.
RWTexture2DArray<float4> OutputCubeMap : register(u0); // 큐브맵 UAV.
// 한 그룹의 스레드 개수는 32 * 32 * 1개로 구성하였으나, 원하는 대로 설정 가능.
// 2차원인 이미지를 일정하게 나누어서 작업하기 위해, 스레드 그룹은 2차원 평면같은 느낌으로 구성한다.
[numthreads(32, 32, 1)]
void main(uint3 DispatchThreadID : SV_DispatchThreadID) // 이 셰이더에서는 SV_DispatchThreadID만 필요하다.
{
}
셰이더를 구현하기 전에, C++ 쪽에서의 코드를 마무리하겠다.
// Bind
ID3D11ShaderResourceView* SourceTextureSRV = /* HDRI 이미지의 SRV */;
Graphics->DeviceContext->CSSetShaderResources(0, 1, &SourceTextureSRV);
UINT InitialCounts[] = { 0 };
Graphics->DeviceContext->CSSetUnorderedAccessViews(0, 1, &CubeMapUAV, InitialCounts);
ID3D11SamplerState* SamplerState_LinearWrap = /* HDRI 이미지를 샘플하기 위한 샘플러 */;
Graphics->DeviceContext->CSSetSamplers(0, 1, &SamplerState_LinearWrap);
// Shader
ID3D11ComputeShader* ComputeShader = /* 컴파일 후 생성한 컴퓨트 셰이더로, 컴파일 방법은 다른 셰이더와 비슷하다 */;
Graphics->DeviceContext->CSSetShader(ComputeShader, nullptr, 0);
// 스레드 그룹 개수 계산
// 텍스처의 크기에 기반해서 계산한다.
// 텍스처의 크기와 스레드 그룹 크기가 나누어 떨어지지 않아도 놓치는 픽셀이 없도록 Ceil 한다.
constexpr UINT ThreadGroupSize = 32;
const UINT NumGroupsX = (Desc.Width + ThreadGroupSize - 1) / ThreadGroupSize; // Ceil
const UINT NumGroupsY = (Desc.Height + ThreadGroupSize - 1) / ThreadGroupSize; // Ceil
// 실행
Graphics->DeviceContext->Dispatch(
NumGroupsX,
NumGroupsY,
6 // Z 값에 배열의 길이인 6을 넣는다. 2차원 이미지를 일정하게 나누어서 하는 작업을 6번 반복하게 된다.
);
// 밉맵 생성. 밉맵은 이후 Pre-filtered map과 Irradiance map을 제작할 때 활용된다.
Graphics->DeviceContext->GenerateMips(CubeMapSRV);
// Unbind
ID3D11ShaderResourceView* NullSRV = nullptr;
Graphics->DeviceContext->CSSetShaderResources(0, 1, &NullSRV);
ID3D11UnorderedAccessView* NullUAV = nullptr;
Graphics->DeviceContext->CSSetUnorderedAccessViews(0, 1, &NullUAV, nullptr);
// 더이상 사용되지 않는 UAV를 Release.
CubeMapUAV->Release();
// TODO: 이후, 제작된 큐브맵을 직접 관리해야 함.
마지막으로, 셰이더 구현을 끝낸다.
먼저, 현재의 스레드 ID가 텍스처 크기를 벗어난 경우에는 값을 작성할 수 없으므로, 바로 리턴한다.
[numthreads(32, 32, 1)]
void main(uint3 DispatchThreadID : SV_DispatchThreadID) // SV_DispatchThreadID는 스레드의 ID를 가지고 있다. 각 차원의 인덱스 정보를 가진 uint3 타입이다.
{
uint Width;
uint Height;
uint Elements;
OutputCubeMap.GetDimensions(Width, Height, Elements);
if (DispatchThreadID.x >= Width || DispatchThreadID.y >= Height)
{
return;
}
}
다음으로, DispatchThreadID를 통해, 현재 작업할 픽셀이 큐브맵에서 어느 방향에 있는지를 구한다.
float3 GetDirection(uint3 DispatchThreadID, float CubeMapWidth, float CubeMapHeight)
{
const uint FaceIndex = DispatchThreadID.z;
float2 UV = float2(DispatchThreadID.xy + 0.5f) / float2(CubeMapWidth, CubeMapHeight); // 픽셀의 중심
float2 Scan = UV * 2.0f - 1.0f; // [0, 1] 범위의 UV 값을 [-1, 1] 사이로 매핑.
float3 Direction;
switch (FaceIndex)
{
case 0: // Right (DirectX Coord: +X)
Direction = float3(1.0f, -Scan.y, -Scan.x);
break;
case 1: // Left (DirectX Coord: -X)
Direction = float3(-1.0f, -Scan.y, Scan.x);
break;
case 2: // Top (DirectX Coord: +Y)
Direction = float3(Scan.x, 1.0f, Scan.y);
break;
case 3: // Bottom (DirectX Coord: -Y)
Direction = float3(Scan.x, -1.0f, -Scan.y);
break;
case 4: // Front (DirectX Coord: +Z)
Direction = float3(Scan.x, -Scan.y, 1.0f);
break;
case 5: // Back (DirectX Coord: -Z)
Direction = float3(-Scan.x, -Scan.y, -1.0f);
break;
}
return normalize(Direction);
}
이렇게 스레드 ID를 통해 현재 색상을 결정해야 할 픽셀의 방향을 구할 수 있다.
FaceIndex(Texture2D Array에서의 인덱스)에 따른 큐브맵의 면이 3D 공간에서 어디에 해당하는지는 이전에 다뤘던 이미지에서 확인 가능한다.

다음은 이 방향 정보를 통해 HDRI에서 색상을 샘플해야 한다.
여기에서 구면좌표계의 개념이 사용된다.

위 이미지에서 파란색으로 표시된 게 $\phi$(phi)로, 수평 각도를 의미한다.
초록색은 $\theta$(theta)로, 수직 각도를 의미한다.
HDRI와의 관계는 다음과 같다.

이를 활용하면, 방향 정보를 통해 이미지의 UV를 얻을 수 있다.
float2 SampleSphericalMap(float3 Direction)
{
float Phi_Rad = atan2(Direction.y, Direction.x); // [-PI rad, PI rad]. 언리얼 엔진 좌표계 기준.
float Theta_Rad = -asin(Direction.z); // [ -PI/2 rad, PI/2 rad]. 언리얼 엔진 좌표계 기준.
float2 UV = float2(Phi_Rad, Theta_Rad);
UV *= float2(0.1591549, 0.3183099); // float2(1/2pi, 1/pi). 라디안 값을 [-0.5, 0.5] 사이로 매핑
UV += 0.5; // [0.0, 1.0] 사이로 매핑
return UV;
}
[numthreads(THREADS_X, THREADS_Y, 1)]
void main(uint3 DispatchThreadID : SV_DispatchThreadID)
{
uint Width;
uint Height;
uint Elements;
OutputCubeMap.GetDimensions(Width, Height, Elements);
if (DispatchThreadID.x >= Width || DispatchThreadID.y >= Height)
{
return;
}
float3 Direction = GetDirection(DispatchThreadID, Width, Height);
float2 UV = SampleSphericalMap(Direction);
float3 Color = SourceTexture.SampleLevel(SamplerLinearWrap, UV, 0).rgb;
OutputCubeMap[DispatchThreadID.xyz] = float4(Color, 1.0f); // 텍스처에 값 작성
}
방향 벡터의 수평 각도와 수직 각도를 구해서 [0, 1] 사이의 값으로 매핑하면 UV가 된다.
이렇게 구한 UV 값으로 HDRI에서 색상을 샘플한 뒤에 큐브맵에 작성한다.
참고로 이 엔진은 언리얼 엔진 좌표계(X forward, Z up)를 사용하고 있으므로, atan2와 asin 함수에는 이 좌표계에 맞는 인자를 전달하고 있다.
결과
다음과 같은 스카이 박스 셰이더를 추가해서 렌더 해보면 결과를 확인할 수 있다.
// SkyBoxShader.hlsl
TextureCube SkyBoxTexture : register(t0);
static const float3 CubeVertices[8] = { // from .obj file
float3(-1.000000, -1.000000, 1.000000),
float3(-1.000000, 1.000000, 1.000000),
float3(-1.000000, -1.000000, -1.000000),
float3(-1.000000, 1.000000, -1.000000),
float3(1.000000, -1.000000, 1.000000),
float3(1.000000, 1.000000, 1.000000),
float3(1.000000, -1.000000, -1.000000),
float3(1.000000, 1.000000, -1.000000),
};
static const int CubeIndices[36] = { // from .obj file
3, 2, 1,
7, 4, 3,
5, 8, 7,
1, 6, 5,
1, 7, 3,
6, 4, 8,
4, 2, 3,
8, 4, 7,
6, 8, 5,
2, 6, 1,
5, 7, 1,
2, 4, 6,
};
struct VS_OUTPUT
{
float4 Position : SV_POSITION;
float3 UV : TEXCOORD0;
};
VS_OUTPUT mainVS(uint VertexID : SV_VertexID)
{
VS_OUTPUT Output = (VS_OUTPUT)0;
float3 LocalPosition = CubeVertices[CubeIndices[VertexID] - 1]; // .obj 파일의 인덱스는 1에서 시작하므로 1 감소시킴.
Output.Position = float4(LocalPosition, 0.0f);
Output.Position = mul(Output.Position, ViewMatrix);
Output.Position = mul(Output.Position, ProjectionMatrix);
Output.Position.z = Output.Position.w * 0.9999; // 핵심: Perspective Divide(Z/W) 후 깊이 값이 1.0에 가깝게 되도록 함.
Output.UV = LocalPosition;
return Output;
}
float4 mainPS(VS_OUTPUT Input) : SV_Target
{
return SkyBoxTexture.SampleLevel(SamplerLinearClamp, normalize(Input.UV), 0); // 0번 밉맵에서 샘플.
}
셰이더에서 큐브를 생성해서 렌더 하는 방식을 사용했다.
중요한 건 Output.Position.z = Output.Position.w * 0.9999;이다.
스카이 박스에 사용되는 큐브는 변의 길이가 2인 작은 큐브지만, 이 방식을 통해 카메라에서 제일 멀리 있는 것으로 지정할 수 있다.
먼저, 큐브맵에 이미지가 아닌 UV 값을 그대로 넣어서 출력해 보면 다음과 같아야 한다.

만약 색상이 부드럽에 이어지지 않고 끊어짐이 발생한다면, 컴퓨트 셰이더에서 DispatchThreadID를 방향벡터로 변환하는 부분을 다시 확인해봐야 한다.
HDRI를 사용해서 출력한 결과는 다음과 같다.

이렇게 제작된 큐브맵은 스카이 박스뿐만 아니라, 이후 Pre-filtered map과 Irradiance map을 제작할 때 사용된다.
'그래픽스' 카테고리의 다른 글
| 물리 기반 렌더링(PBR)의 핵심 이론과 BRDF (0) | 2026.01.05 |
|---|
