DXR프로젝트에 구현한 PBR내용 정리
PBR이 대세가 되기전에 자주 사용하던 Phong Shading등의 방식들은 다음과 같은 문제가 있다.
1. 에너지 보존 법칙이 고려되지 않음(ex. 입사된 빛보다 반사된 빛의 더 강한경우 존재)
2. 물체 표면의 특성을 전적으로 아티스트의 '감'에 의지하게 됨(specular의 cos 계수를 조절하며)
그래서 미세면(Microfacet)의 특성을 고려한 물리적으로 말이되는 렌더링방식인 PBR이 등장한다.
참고한 사이트: https://learnopengl.com/PBR/Theory
diffuse BRDF로 램버시안 BRDF를 선택했다. 분모에 원주율이 존재하는 이유는 난반사되는 빛을 모두 적분했을때 입사되는 빛의 에너지와 같아지게 하기 위함이다(에너지 보존).
namespace Diffuse
{
float3 CalculateLambertianBRDF(in float3 albedo)
{//램버시안 표면의 난반사 BRDF계산
return albedo / PI;
}
}
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;
}
}
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식으로 계산한 값들을 합해서 최종 색을 결정해준다.
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수치가 증가할수록 색이 전체적으로 같은 톤을 띄게되는것을 확인할 수 있다.