노말맵의 개념과 DXR에서의 구현기
노말맵은 위 그림과 같이 평평한 표면이 마치 평평하지 않은것처럼 보이게 하는 트릭이다.
적은 계산복잡도로 월등한 퀄리티의 결과물을 낼 수 있어서 현대 게임에서도 많이 사용되는 기술이다.
노말맵은 텍스처의 일종으로 rgb값들이 2차원으로 나열된 그림 파일이다.
대체로 푸르스름한 색을 띄는 이유는 다음과 같다.
텍스처에는 color값이 저장되므로 모든 픽셀마다 rgb값이 0~1값으로 저장되게 되는데,
그래서 실제 normal값은 -1 ~ 1의 범위를 가진다.
근데 normal을 이루는 3가지 값중 blue값은 모든 값이 0이상이다. 그 이유는 normal값의 의미가
평면이 바라보고 있는 방향을 나타내는데, 음의 방향을 바라보고 있는 normal은 현실에 존재하지 않기 때문이다.
그 결과 normal값이 color로 변환될 때 모든 b값은 0.5이상인것이 보장되게 되고
이것이 노말맵을 푸르게 보이게 한다.
노말맵을 구현하려면 Tangent Space에 대한 개념이 있어야한다.
노말맵은 결국 Vector들이 저장된 텍스처이다.
노말매핑을 하는 이유는 3D물체의 표면에 더 리얼한 노말벡터를 사용해서 Shading하기 위함이다.
하지만 필요한 World Space Normal값은 텍스처에 저장할 수 없다.
그 이유는 3D모델이 회전한다면, World Space Normal값들도 달라지기 때문이다.
그렇기에 Normal값들은 Tangent Space에서 정의한다.
Tangent Space에서 정의함으로 인해서 어떠한 회전에도 정상적인 World Normal값을 얻을 수 있다.
계산하는 과정은 Vertex마다 Tangent, BiTangent값을 추가로 부여해서
Tangent Space -> World Space 변환 매트릭스 계산을 한다.
이때 사용되는 변환 행렬을 TBN Matrix라고 한다.
DXR에서 노말매핑을 구현한다.
struct LocalRootArgument { MeshConstantBuffer cb;//Root Constant D3D12_GPU_VIRTUAL_ADDRESS vbGPUAddress;//Inline Descriptor D3D12_GPU_VIRTUAL_ADDRESS ibGPUAddress;//Inline Descriptor D3D12_GPU_DESCRIPTOR_HANDLE diffuseTextureDescriptorHandle;//Descriptor Table D3D12_GPU_DESCRIPTOR_HANDLE normalTextureDescriptorHandle;//Descriptor Table D3D12_GPU_DESCRIPTOR_HANDLE specularTextureDescriptorHandle;//Descriptor Table };
먼저, GPU에 노말맵을 업로드하기 위해서 Local Root Signature에 담기는 LocalRootArgument를 수정한다.
for (UINT i = 0; i < meshes.size(); i++) { rootArgument.cb.world = XMMatrixTranspose(meshes[i]->GetWorldMatrix()); rootArgument.cb.albedo = meshes[i]->GetColor(); rootArgument.cb.reflectivity = meshes[i]->GetMaterial()->GetReflectivity(); rootArgument.vbGPUAddress = meshes[i]->GetVertexBuffer()->GetGPUVirtualAddress(); rootArgument.ibGPUAddress = meshes[i]->GetIndexBuffer()->GetGPUVirtualAddress(); rootArgument.cb.hasDiffuseTexture = 0u; rootArgument.cb.hasNormalTexture = 0u; rootArgument.cb.hasSpecularTexture = 0u; if (meshes[i]->GetMaterial()->HasDiffuseTexture()) { rootArgument.cb.hasDiffuseTexture = 1u; rootArgument.diffuseTextureDescriptorHandle = meshes[i]->GetMaterial()->GetDiffuseTexture()->GetDescriptorHandle(); } if (meshes[i]->GetMaterial()->HasNormalTexture()) { rootArgument.cb.hasNormalTexture = 1u; rootArgument.normalTextureDescriptorHandle = meshes[i]->GetMaterial()->GetNormalTexture()->GetDescriptorHandle(); } if (meshes[i]->GetMaterial()->HasSpecularTexture()) { rootArgument.cb.hasSpecularTexture = 1u; rootArgument.specularTextureDescriptorHandle = meshes[i]->GetMaterial()->GetSpecularTexture()->GetDescriptorHandle(); } for (UINT j = 0; j < RayType::Count; j++) { void* hitGroupIdentifier = stateObjectProperties->GetShaderIdentifier(HIT_GROUP_NAMES[j]); Push_back(ShaderRecord(hitGroupIdentifier, shaderIdentifierSize, &rootArgument, sizeof(rootArgument))); } }
그리고 HitGroupShaderTable에 Discriptor에 대한 GPU Handle값을 매핑해서 DXR Shader에서 사용할 수 있게 한다.
//노말맵이 적용된 새로운 노말값 리턴 float3 CalculateNormalmapNormal(float3 originNormal,float3 tangent,float3 biTangent,float2 uv) { if(!l_meshCB.hasNormalTexture) return originNormal; float3 newNormal = l_normalTexture.SampleLevel(l_sampler, uv, 0).xyz; newNormal = (newNormal * 2.0f) - 1.0f; newNormal = (newNormal.x * tangent) + (newNormal.y * biTangent) + (newNormal.z * originNormal);//TBN변환 return normalize(newNormal); }
그리고 closest hit shader에서 노말맵을 샘플링해서 노말맵의 노말값으로 무게중심좌표계로 변환된 노말값을 대체한다.
Normal값을 그대로 컬러로 출력
노말맵이 적용되지 않은 평면
노말맵이 적용된 평면