[DirectX12][DXR] PBR 구현하기

윤태웅·2023년 5월 7일
1

DirectX12

목록 보기
7/11

개요


DXR프로젝트에 구현한 PBR내용 정리

PBR 컨셉

PBR이 대세가 되기전에 자주 사용하던 Phong Shading등의 방식들은 다음과 같은 문제가 있다.

1. 에너지 보존 법칙이 고려되지 않음(ex. 입사된 빛보다 반사된 빛의 더 강한경우 존재)
2. 물체 표면의 특성을 전적으로 아티스트의 '감'에 의지하게 됨(specular의 cos 계수를 조절하며)


그래서 미세면(Microfacet)의 특성을 고려한 물리적으로 말이되는 렌더링방식인 PBR이 등장한다.

PBR 구현

참고한 사이트: https://learnopengl.com/PBR/Theory

Diffuse BRDF

diffuse BRDF로 램버시안 BRDF를 선택했다. 분모에 원주율이 존재하는 이유는 난반사되는 빛을 모두 적분했을때 입사되는 빛의 에너지와 같아지게 하기 위함이다(에너지 보존).

namespace Diffuse
{
    float3 CalculateLambertianBRDF(in float3 albedo)
    {//램버시안 표면의 난반사 BRDF계산
        return albedo / PI;
    }
}

Specular BRDF


specular BRDF로 CookTorrance BRDF를 선택했다. D는 정반사되는 분포도를 의미하고 F는 입사각에 대해 정반사되는 빛의 비율을 의미하고 G는 미세면의 그림자처리를 의미한다.

namespace Specular
{
    float DistributionGGX(in float3 normal,in float3 halfVector,in float roughness)
    {
        float a = roughness * roughness;
        float a2 = a * a;
        float NdotH = max(dot(normal, halfVector), 0.0);
        float NdotH2 = NdotH * NdotH;
	
        float num = a2;
        float denom = (NdotH2 * (a2 - 1.0) + 1.0);
        denom = PI * denom * denom;
	
        return num / denom;
    }

    float GeometrySchlickGGX(in float normalDotPointToCamera, in float roughness)
    {
        float r = (roughness + 1.0);
        float k = (r * r) / 8.0;

        float num = normalDotPointToCamera;
        float denom = normalDotPointToCamera * (1.0 - k) + k;
	
        return num / denom;
    }
    float GeometrySmith(in float3 normal,in float3 pointToCamera,in float3 pointToLight,in float roughness)
    {
        float normalDotPointToCamera = max(dot(normal, pointToCamera), 0.0);
        float normalDotPointToLight = max(dot(normal, pointToLight), 0.0);
        float ggx2 = GeometrySchlickGGX(normalDotPointToCamera, roughness);
        float ggx1 = GeometrySchlickGGX(normalDotPointToLight, roughness);
	
        return ggx1 * ggx2;
    }
    float3 fresnelSchlick(float cosTheta, float3 F0)
    {//프레넬 식(표면마다 다른 프레넬 상수(F0),Normal벡터와의 각도 cosTheta가 주어졌을때 반사되는 '비율'
        return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
    }
    float3 CalculateCookTorranceBRDF(
        in float3 normal,
        in float3 pointToCamera,
        in float3 halfVector,
        in float3 pointToLight,
        in float roughness,
        in float3 F
    )
    {
        float NDF = DistributionGGX(normal, halfVector, roughness); //미세면 분포도 NDF계산
        float G = GeometrySmith(normal, pointToCamera, pointToLight, roughness); //미세면 그림자 계산
                
        float3 numerator = NDF * G * F;
        float denominator = 4.0 * max(dot(normal, pointToCamera), 0.0) * max(dot(normal, pointToLight), 0.0) + 0.0001f;
        float3 specular = numerator / denominator;
                
        return specular;
    }
}

Shading처리

float3 PBRShade(
    in float3 ambientMap,
    in float3 albedoMap,
    in float roughnessMap,
    in float metallicMap,
    in float3 normal,
    in float3 pointToLights[NUM_LIGHT],
    in float3 pointToCamera,
    in float3 lightColor,
    in float lightAttenuation,
    in float shadowAmount
)
{
    float3 ambient = ambientMap * albedoMap * 0.2f;
    float3 color = float3(0.f, 0.f, 0.f);
    [unroll(NUM_LIGHT)]
    for (uint i = 0; i < NUM_LIGHT; i++)
    {
        float3 halfVector = normalize(pointToLights[i] + pointToCamera);
        
        float3 diffuse = BxDF::BRDF::Diffuse::CalculateLambertianBRDF(albedoMap);
        
        float3 F0 = float3(0.04f, 0.04f, 0.04f); //일반적인 프레넬 상수수치를 0.04로 정의
        F0 = lerp(F0, albedoMap, metallicMap);
        float3 F = BxDF::BRDF::Specular::fresnelSchlick(max(dot(halfVector, pointToCamera), 0.0), F0); //반사정도 정의
        
        float3 kS = F; //Specular상수
        float3 kD = float3(1.f, 1.f, 1.f) - kS; //Diffuse 상수
        kD = kD * float3(1.f - metallicMap, 1.f - metallicMap, 1.f - metallicMap); //Diffuse에 metallic반영
        
        float3 specular = BxDF::BRDF::Specular::CalculateCookTorranceBRDF(normal, pointToCamera, halfVector, pointToLights[i], roughnessMap, F);
        if (shadowAmount > 0.2f)
        { //그림자 효과 반영
            diffuse = saturate(diffuse * (1.f - shadowAmount + 0.1f));
            specular = saturate(specular * (1.f - shadowAmount + 0.1f));
        }
        float NdotL = max(dot(normal, pointToLights[i]), 0.0);
        color += (kD * diffuse + specular) * lightColor * lightAttenuation * NdotL;
    }
    color += ambient;
    {//감마변환    
        color = color / (color + float3(1.0f, 1.0f, 1.0f));
        color = pow(color, float3(1.0f / 2.2f, 1.0f / 2.2f, 1.0f / 2.2f));
    }
    return color;
        
}

diffuse brdf, specular brdf식으로 계산한 값들을 합해서 최종 색을 결정해준다.

Closest Hit Shader에서

StructuredBuffer<Vertex> l_vertices : register(t2, space0);
ByteAddressBuffer l_indices : register(t3, space0);
Texture2D l_diffuseTexture : register(t4);
Texture2D l_normalTexture : register(t5);
Texture2D l_specularTexture : register(t6);
Texture2D l_roughnessTexture : register(t7);
Texture2D l_metallicTexture : register(t8);

...

float roughness = l_meshCB.roughness;
if(l_meshCB.hasRoughnessTexture)
{
    roughness = l_roughnessTexture.SampleLevel(l_sampler, triangleUV, 0).x;
}
float metallic = l_meshCB.metallic;
if (l_meshCB.hasMetallicTexture)
{
    metallic = l_metallicTexture.SampleLevel(l_sampler, triangleUV, 0).x;
}
            
float3 color = BxDF::PBRShade(
    ambientColor,
    diffuseColor,
    roughness,
    metallic,
    triangleNormal,
    pointToLights,
    pointToCamera,
    lightColor,
    lightAttenuation,
    shadowAmount
);
payload.color = float4(color, 1);

CookTorrance BRDF에서는 Roughness, Metallic값이 추가로 필요하므로
각 Mesh마다 Roughness, Metallic텍스처에 대한 매핑을 추가로 처리해준다.(텍스처 없는 경우도 고려하기 위해 roughness, metallic 상수값도 Constant Buffer에 추가해준다)

결과


왼쪽 하단은 Roughness 0, Metallic 0. 우측 상단은 Roughness 1, Metallic 1으로
구 모델들을 같은 간격으로 배치시켜봤다.
Metallic수치가 증가할수록 정반사광의 비중이 커져서 Specular Highlight가 두드러지는것을 확인할 수있고 Roughness수치가 증가할수록 색이 전체적으로 같은 톤을 띄게되는것을 확인할 수 있다.

0개의 댓글