[DirectX12][DXR] 노말맵 구현

윤태웅·2023년 4월 22일
1

DirectX12

목록 보기
6/11
post-thumbnail

개요

노말맵의 개념과 DXR에서의 구현기

Normal Map


노말맵은 위 그림과 같이 평평한 표면이 마치 평평하지 않은것처럼 보이게 하는 트릭이다.
적은 계산복잡도로 월등한 퀄리티의 결과물을 낼 수 있어서 현대 게임에서도 많이 사용되는 기술이다.

형태


노말맵은 텍스처의 일종으로 rgb값들이 2차원으로 나열된 그림 파일이다.
대체로 푸르스름한 색을 띄는 이유는 다음과 같다.

텍스처에는 color값이 저장되므로 모든 픽셀마다 rgb값이 0~1값으로 저장되게 되는데,
그래서 실제 normal값은 -1 ~ 1의 범위를 가진다.
근데 normal을 이루는 3가지 값중 blue값은 모든 값이 0이상이다. 그 이유는 normal값의 의미가
평면이 바라보고 있는 방향을 나타내는데, 음의 방향을 바라보고 있는 normal은 현실에 존재하지 않기 때문이다.
그 결과 normal값이 color로 변환될 때 모든 b값은 0.5이상인것이 보장되게 되고
이것이 노말맵을 푸르게 보이게 한다.

Tangent Space



노말맵을 구현하려면 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구현

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값을 그대로 컬러로 출력


노말맵이 적용되지 않은 평면


노말맵이 적용된 평면

0개의 댓글