-
[DirectX11] 텍스쳐와 노멀매핑DirectX11 2023. 12. 13. 17:58
모델에 텍스쳐를 적용하는 과정이다.
필요한 것
- 텍스쳐를 저장할 쉐이더 리소스 뷰 (
ID3D11ShaderResourceView
) - 텍스쳐에서 값을 읽을 방식을 결정하는 샘플러 (
ID3D11SamplerState
) - GPU에서 텍스쳐를 받을
Texture2D
타입 변수 - GPU에서 텍스쳐를 읽을 방식을 지정받을
SamplerState
타입 변수
텍스쳐를 읽을때에는 DirectXTK의
CreateTextureFromFile
함수를 사용한다.DirectXTK: https://github.com/microsoft/DirectXTK/tree/main
GitHub - microsoft/DirectXTK: The DirectX Tool Kit (aka DirectXTK) is a collection of helper classes for writing DirectX 11.x co
The DirectX Tool Kit (aka DirectXTK) is a collection of helper classes for writing DirectX 11.x code in C++ - GitHub - microsoft/DirectXTK: The DirectX Tool Kit (aka DirectXTK) is a collection of h...
github.com
CreateTextureFromFile
함수만 사용할 경우에는 위의 링크에서Src/DDSTextureLoader
의 헤더와 cpp파일을 그대로 복붙해서 사용해도 된다.DDS 파일을 읽는 함수라서 텍스쳐도 DDS로 준비해야한다.
모델에는 BaseColor, Roughness, Normal 등 여러개의 텍스쳐가 사용되므로 여러 텍스쳐들을 구조체로 관리할 것이고, 텍스쳐 파일들은 폴더로 구분해서 보관했다.
// 텍스쳐 구조체 struct Textures { ID3D11ShaderResourceView* pTextureDiffuse = NULL; ID3D11ShaderResourceView* pTextureSpecular = NULL; ID3D11ShaderResourceView* pTextureGloss = NULL; ID3D11ShaderResourceView* pTextureNormal = NULL; ID3D11ShaderResourceView* pTextureAO = NULL; ID3D11ShaderResourceView* pTextureCavity = NULL; ID3D11ShaderResourceView* pTextureEmissive = NULL; BYTE texture_flag = 0; ~Textures() { Release(); } void Release() { if (pTextureDiffuse) pTextureDiffuse->Release(); if (pTextureSpecular) pTextureSpecular->Release(); if (pTextureGloss) pTextureGloss->Release(); if (pTextureNormal) pTextureNormal->Release(); if (pTextureAO) pTextureAO->Release(); if (pTextureCavity) pTextureCavity->Release(); if (pTextureEmissive) pTextureEmissive->Release(); } }; // ... // 폴더 내부의 텍스쳐들을 읽는 함수 void LoadTextures(std::wstring texture_name, Textures*& OutTextures) { HRESULT hr = S_OK; std::wstring folder_dir = L"textures/" + texture_name; OutTextures->texture_flag = 0; // Load Textures hr = CreateDDSTextureFromFile(g_pd3dDevice, (folder_dir + L"/diffuse.dds").c_str(), nullptr, &OutTextures->pTextureDiffuse); if (SUCCEEDED(hr)) OutTextures->texture_flag |= (1 << 0); hr = CreateDDSTextureFromFile(g_pd3dDevice, (folder_dir + L"/specular.dds").c_str(), nullptr, &OutTextures->pTextureSpecular); if (SUCCEEDED(hr)) OutTextures->texture_flag |= (1 << 1); hr = CreateDDSTextureFromFile(g_pd3dDevice, (folder_dir + L"/gloss.dds").c_str(), nullptr, &OutTextures->pTextureGloss); if (SUCCEEDED(hr)) OutTextures->texture_flag |= (1 << 2); hr = CreateDDSTextureFromFile(g_pd3dDevice, (folder_dir + L"/normal.dds").c_str(), nullptr, &OutTextures->pTextureNormal); if (SUCCEEDED(hr)) OutTextures->texture_flag |= (1 << 3); hr = CreateDDSTextureFromFile(g_pd3dDevice, (folder_dir + L"/ao.dds").c_str(), nullptr, &OutTextures->pTextureAO); if (SUCCEEDED(hr)) OutTextures->texture_flag |= (1 << 4); hr = CreateDDSTextureFromFile(g_pd3dDevice, (folder_dir + L"/cavity.dds").c_str(), nullptr, &OutTextures->pTextureCavity); if (SUCCEEDED(hr)) OutTextures->texture_flag |= (1 << 5); hr = CreateDDSTextureFromFile(g_pd3dDevice, (folder_dir + L"/emissive.dds").c_str(), nullptr, &OutTextures->pTextureEmissive); if (SUCCEEDED(hr)) OutTextures->texture_flag |= (1 << 6); }
텍스쳐의 종류는 내가 사용할 모델을 기준으로 해서 작성했다.
그리고 해당 텍스쳐가 존재하는지 여부를 flag로 저장해서 텍스쳐가 없는 경우에는 기본값을 사용하게 했다.
샘플러는 다음과 같이 지정했다.
// Create the sample state D3D11_SAMPLER_DESC sampDesc; ZeroMemory(&sampDesc, sizeof(sampDesc)); sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP; sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP; sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER; sampDesc.MipLODBias = -1; sampDesc.MinLOD = 0; sampDesc.MaxLOD = D3D11_FLOAT32_MAX; hr = g_pd3dDevice->CreateSamplerState(&sampDesc, &g_pSamplerLinear); if (FAILED(hr)) return hr; g_pImmediateContext->PSSetSamplers(0, 1, &g_pSamplerLinear);
샘플러 디스크립션의
Filter
는 필터링 방식이다.여기에 지정되어있는
D3D11_FILTER_MIN_MAG_MIP_LINEAR
는 minimize, magnify, mip map에 선형 보간을 사용하는 방법이다.Linear 대신 Point를 지정하면 neareat 필터를 사용한다.
다른 예를 들면
D3D11_FILTER_MIN_MAG_POINT_MIP_LINEAR
의 경우에는 minimize, magnify에 nearest를, mip map에 선형 보간을 사용하는 방식이다.Address는 UV가 0과 1사이를 넘어가는 경우 텍스쳐를 어떻게 읽을지를 결정한다.
위처럼
D3D11_TEXTURE_ADDRESS_WRAP
으로 되어있으면 소수점 자리만 읽어서 계속 반복되게 한다.예를들어 좌표가 1.2라면 0.2로 읽는 방식으로, 1을 넘어가면 0부터 다시 시작한다.
D3D11_TEXTURE_ADDRESS_MIRROR
는 이름 그대로 뒤집히면서 반복된다.D3D11_TEXTURE_ADDRESS_CLAMP
는 좌표 값을 0과 1사이로 클램핑한다.D3D11_TEXTURE_ADDRESS_BORDER
의 경우에는 아래와 같은 방식으로 테두리의 값을 직접 지정할 수 있다.sampDesc.BorderColor[0] = 1.0f; // r sampDesc.BorderColor[1] = 1.0f; // g sampDesc.BorderColor[2] = 1.0f; // b sampDesc.BorderColor[3] = 1.0f; // a
ComparisonFunc
는 comparison 샘플러인 경우에 사용한다.여기에서는 UV값으로 텍스쳐의 값만 읽어오면 되기 때문에 사용하지 않는다.
LOD 관련 옵션은 밉맵과 관련되어있다.
그 중에 LOD bias는 float값을 지정할 수 있는데, 음수를 넣어도 된다.
bias에 -1을 지정하면 LOD 1이 사용될 상황에 LOD 0을 사용하고, LOD 2가 사용되어야 할 때 LOD 1을 사용한다.
옵션을 설정한 다음에 샘플러를 생성해서
ID3D11SamplerState*
타입 변수에 저장하고 Context에 설정하면 된다.이때 사용한
PSSetSamplers
함수의 첫번째 파라미터는 슬롯 번호인데 0번으로 지정했으니 쉐이더 코드에도 이 슬롯에 맞게 지정해줘야 한다.// hlsl SamplerState samLinear : register(s0);
register의 괄호 안에 적혀있는 s0은 샘플러 0번 슬롯이라는 뜻이다.
쉐이더 코드에는 아래처럼 텍스쳐를 받을 수 있게 했다.
// hlsl Texture2D txDiffuse : register(t2); Texture2D txSpecular : register(t3); Texture2D txGloss : register(t4); Texture2D txNormal : register(t5); Texture2D txAO : register(t6); Texture2D txCavity : register(t7); Texture2D txEmissive : register(t8);
레지스터의 t0과 t1는 다른 용도로 사용중이어서 2번부터 연결했다.
마지막으로 텍스쳐들을 GPU로 보내고 렌더링 한다.
// 각 오브젝트들을 렌더할때 사용될 값들 struct CBChangesEveryObject { XMMATRIX mWorld; // 64 바이트 XMFLOAT4 vDiffuse; // 16 바이트 XMFLOAT4 vSpecular; // 16 바이트 XMFLOAT4 vEmissive; // 16 바이트 FLOAT sGloss; // 4 바이트 INT TextureFlag; // 4 바이트 INT Padding[2]; // 8 바이트, 16배수를 맞추기 위함 }; // ... ID3D11ShaderResourceView* textures[] = { material->GetTextures()->pTextureDiffuse, material->GetTextures()->pTextureSpecular, material->GetTextures()->pTextureGloss, material->GetTextures()->pTextureNormal, material->GetTextures()->pTextureAO, material->GetTextures()->pTextureCavity, material->GetTextures()->pTextureEmissive }; g_pImmediateContext->PSSetShaderResources(2, 7, textures); CBChangesEveryObject cbO; cbO.vDiffuse = material->diffuse; cbO.vSpecular = material->specular; cbO.vEmissive = material->emissive; cbO.sGloss = material->gloss; cbO.TextureFlag = material->GetTextureFlag(); cbO.mWorld = Mesh->GetWorldMatrix(); g_pImmediateContext->UpdateSubresource(g_pCBChangesEveryObject, 0, NULL, &cbO, 0, 0); // Draw
텍스쳐와 기본 값들을 가지고있는 간단한 머티리얼 클래스를 만들어서 각 오브젝트에 지정했다.
이건 간단하게 구현한거라 굳이 적지는 않겠다.
그냥 필요한 정보를 가지고있는 역할을 하고 해당 오브젝트가 렌더링 되어야할 때 필요한 정보들을 가져올 수 있게 했다.
쉐이더 리소스 뷰에 저장되어있는 텍스쳐들을 픽셀 쉐이더에서 사용할 수 있게 설정해줘야하는데 이때
PSSetShaderResources
함수가 사용된다.함수 이름대로 여러개의 쉐이더 리소스 뷰가 들어있는 배열을 전달해줘도 된다.
쉐이더 코드에서
register(t2)
부터 텍스쳐들을 지정 했으니 2번 슬롯에서부터 시작하고 전체 7개의 텍스쳐들을 설정한다.픽셀 쉐이더에서 텍스쳐의 값을 읽으려면
Texture2D
의Sample
함수를 쓰면 된다.bool IsDiffuseExist = TextureFlag & (1 << 0); float3 Diffuse = vDiffuse; if (IsDiffuseExist) { Diffuse = txDiffuse.Sample(samLinear, input.TexCoord); }
노멀 매핑
노멀맵을 사용하기 위해서는 먼저 각 정점의 탄젠트 스페이스를 계산해야한다.
일반적으로 노멀맵은 탄젠트 스페이스를 기준으로 제작되어있기 때문이다.
따라서 모델의 삼각형 면과 텍스쳐를 정렬하는 탄젠트 스페이스 변환 매트릭스가 필요하다.
탄젠트 스페이스에는 Tangent, Bitangent, Normal이 있고, 이 셋은 Orthonormal하다.
각각 x, y, z에 대응되고 u, v, n과 같다.
그리고 Normal은 정점의 노멀과 같다.
그러므로 Tangent만 추가로 알고있으면 Bitangent는 쉐이더에서 Normal과 Tangent의 외적으로 구할 수 있다.
먼저 탄젠트 계산을 하는 코드는 아래와 같다.
struct VertexInput { XMFLOAT3 Position; XMFLOAT3 Normal; XMFLOAT2 TexCoord; XMFLOAT3 Tangent; }; // ... XMFLOAT3 CalcTangent(const VertexInput& p0, const VertexInput& p1, const VertexInput& p2) { float s1 = p1.TexCoord.x - p0.TexCoord.x; float t1 = p1.TexCoord.y - p0.TexCoord.y; float s2 = p2.TexCoord.x - p0.TexCoord.x; float t2 = p2.TexCoord.y - p0.TexCoord.y; float E1x = p1.Position.x - p0.Position.x; float E1y = p1.Position.y - p0.Position.y; float E1z = p1.Position.z - p0.Position.z; float E2x = p2.Position.x - p0.Position.x; float E2y = p2.Position.y - p0.Position.y; float E2z = p2.Position.z - p0.Position.z; float f = 1 / (s1 * t2 - s2 * t1); float Tx = f * (t2 * E1x - t1 * E2x); float Ty = f * (t2 * E1y - t1 * E2y); float Tz = f * (t2 * E1z - t1 * E2z); XMVECTOR T = XMVector3Normalize(XMVectorSet(Tx, Ty, Tz, 0.f)); XMFLOAT3 ret; XMStoreFloat3(&ret, T); return ret; }
과정을 설명하면 다음과 같다.
하나의 면을 이루는 정점중 하나를 고른뒤에 면을 보면 같은 면을 이루고있는 두개의 다른 정점이 존재한다.
서로의 위치 정보를 알고있으니 오브젝트 스페이스에서의 두 정점을 향한 각각의 벡터를 구할 수 있다.
그리고 서로의 텍스쳐 좌표도 알고있으니 텍스쳐에서의 두 정점을 향한 각각의 벡터도 구할 수 있다.
먼저 현재 내가 보고있는 정점 하나를 $P_0$으로, 나머지 정점을 $P_1$과 $P_2$로 정한다.
그리고 $P_0$의 텍스쳐 좌표는 $(u_0, v_0)$, $P_1$은 $(u_1, v_1)$, $P_2$는 $(u_2, v_2)$로 정한다.
목표는 탄젠트 방향인 $T$와 바이탄젠트 방향인 $B$를 구하는 것이다.
$P_0$에서 $P_1$으로 향하는 벡터와 거기에 대응되는 텍스쳐에 대한 방향은 다음과 같다.
\[P_1-P_0=(u_1-u_0)T+(v_1-v_0)B\]
그리고 $(u_1-u_0)=s_1$, $(v_1-v_0)=t_1$이라고 하면 다음과 같다.
\[P_1-P_0=s_1T+t_1B\]
마찬가지로 $P_0$에서 $P_2$으로 향하는 벡터도 아래와 같이 된다.
\[P_2-P_0=(u_2-u_0)T+(v_2-v_0)B\]
\[=s_2T+t_2B\]
$P_1-P_0$을 $E_1$으로, $P_2-P_0$을 $E_2$라고 하겠다.
그러면 아래처럼 정리할 수 있다.
\[ E_1 =s_1T+t_1B\]
\[ E_2 =s_2T+t_2B\]
$$\begin{pmatrix}E_1\\E_2\end{pmatrix}=\begin{pmatrix}s_1&t_1\\s_2&t_2\\\end{pmatrix}\begin{pmatrix}T\\B\end{pmatrix}$$
$$\begin{pmatrix}T\\B\end{pmatrix}=\frac{1}{s_1t_2-s_2t_1}\begin{pmatrix}t_2&-t_1\\-s_2&s_1\\\end{pmatrix}\begin{pmatrix}E_1\\E_2\end{pmatrix}$$
$$=\frac{1}{s_1t_2-s_2t_1}\begin{pmatrix}t_2E_1-t_1E_2\\-s_2E_1+s_1E_2\end{pmatrix}$$
$$T=\frac{t_2E_1-t_1E_2}{s_1t_2-s_2t_1}$$
$$B=\frac{-s_2E_1+s_1E_2}{s_1t_2-s_2t_1}$$
$s$와 $t$, $E$값은 모두 알고있는 값이므로 목표였던 $T$와 $B$값을 계산할 수 있다.
이렇게 계산한 탄젠트 방향을 쉐이더에 같이 전달해주면 된다.
이렇게 구한 탄젠트를 이용해서 노멀을 탄젠트 스페이스에서 월드 스페이스로 변환해서 빛을 계산했다.
탄젠트 스페이스에서 오브젝트 스페이스로 변환할때는 다음 행렬을 곱하면 된다.
\[\begin{pmatrix}T_x&B_x&N_x\\T_y&B_y&N_y\\T_z&B_z&N_z\\\end{pmatrix}\]
반대로 오브젝트 스페이스에서 탄젠트 스페이스로 변환할때 곱하는 행렬은 이렇다.
\[\begin{pmatrix}T_x&T_y&T_z\\B_x&B_y&B_z\\N_x&N_y&N_z\\\end{pmatrix}\]
HLSL는 열 우선(column major)이므로 행렬을 생성할때 열 하나를 한 줄에 작성해야한다.
그리고 노멀맵 텍스쳐에는 벡터의 값이 0에서 1사이의 값으로 저장되어있으니 다시 -1에서 1 사이의 값으로 변환해주는 과정이 필요하다.
struct VS_INPUT { float4 Pos : POSITION0; float3 Norm : NORMAL; float2 TexCoord : TEXCOORD0; float3 Tangent : TANGENT; }; struct VS_OUTPUT { float4 Pos : SV_POSITION; float3 Norm : NORMAL; float2 TexCoord : TEXCOORD0; float3x3 mTBN : TBN; matrix mWorldMatrix : WORLDMATRIX; }; VS_OUTPUT VS(VS_INPUT input) { VS_OUTPUT output = (VS_OUTPUT) 0; input.Pos.w = 1.f; output.Pos = mul(input.Pos, mWorld); output.Pos = mul(output.Pos, mView); output.Pos = mul(output.Pos, mViewProjection); output.Norm = mul(input.Norm, (float3x3) mWorld); output.TexCoord = input.TexCoord; output.mWorldMatrix = mWorld; float3 BiTangent = cross((float3) input.Norm, input.Tangent); matrix<float, 3, 3> TBN = { input.Tangent.x, input.Tangent.y, input.Tangent.z, // column 0 BiTangent.x, BiTangent.y, BiTangent.z, // column 1 input.Norm.x, input.Norm.y, input.Norm.z // column 2 }; output.mTBN = TBN; return output; } float4 PS(VS_OUTPUT input) : SV_Target { // ... bool bIsNormalExist = TextureFlag & (1 << 3); // ... // Normal float3 Normal = normalize(input.Norm); if (bIsNormalExist) { Normal = normalize(2.f * (float3) txNormal.Sample(samLinear, input.TexCoord) - 1.f); Normal = normalize(mul(mul(Normal, input.mTBN), (float3x3) input.mWorldMatrix)); } // ... }
결과
기본 모델 노멀맵 적용 Diffuse, Specular, Gloss, Normal, AO 모두 적용 'DirectX11' 카테고리의 다른 글
[DirectX11] 1인칭 카메라 조작 (0) 2023.12.18 [DirectX11] 쉐도우 맵과 PCF(Percentage Closer Filtering) (3) 2023.12.15 [DirectX11] Disney Diffuse (0) 2023.12.12 [DirectX11] Specular BRDF (3) 2023.12.11 [DirectX11] DrawIndexedInstanced로 여러개의 인스턴스 그리기 (0) 2023.12.07 - 텍스쳐를 저장할 쉐이더 리소스 뷰 (