본문 바로가기
그래픽스

Cascade Shadow Map Seam artifact해결 및 Fade 효과

by greenherb 2021. 7. 29.

그림자맵 단계가 바뀔때 생기는 경계

이전 Cascade Shadpw Map 포스팅에서 그림자맵 만들기와 그림자까지 렌더링해보았습니다. 시야공간의 Z값을 기준으로 텍스쳐 배열안에있는 어떤 그림자맵을 사용할지 정하는데.

이렇게 Z값을 기준으로 고르면 현재 배열의 텍스쳐와 다음배열의 텍스쳐를 사용하여 그림자를 렌더링하는 경계선부분에 그림자의 전환이 이루어질때 보여지는 시각적 아티팩트가 있습니다.

이번 포스팅에서는 이것을 없애는것 뿐만아니라. 그림자의 최대 거리값을 적용하여 최대단계의 그림자맵에서 그림자 최대거리를 벗어날경우 부자연스럽게 그림자가 사라지는것이아닌 Fade 효과를 주어보도록 해보겠습니다.

1. 그림자 단계 사이의 보간

첫번째로 그림자맵의 전환이 이루어지는 경계선부근을 렌더링 할 때 다음단계의 그림자맵으로 얻은 그림자계수와 현재 그림자맵의 계수를 적절하게 보간하는 방법을 생각해보았는데요

이전포스팅에서 시야절두체를 나누고 투영행렬로 그려지는 공간에 대해 보여주는 이미지가있는데.

단계의 경계선에 겹치는 부분이있다.

이렇게 겹치는 부분이 존재하기마련입니다. 더욱이 우리는 Boudning Sphere 을 사용하고있으므로 위 이미지보다 더 크게 겹칠것입니다.  보간을 경계선부터 정확하게 시작하는것이아닌 다음 단계로 넘어가기전 10%~20% 영역에 접근했을부터 보간을 시작하는것이 자연스러운 보간에 도움이 될것같습니다.

그렇다면 어떻게 경계선에 10%~20% 영역에 있다는것을 알 수 있을까요? 제가 생각한 첫번째 방법은 투영행렬의 NDC 범위를 사용하는것입니다. X,Y,Z 범위가 GL의 경우 -1~1 DX의경우 Z제외 -1~1이므로 이범위를 hlsl의 Saturate() 함수나 glsl의 clamp 함수로 0~1사이의 값으로 변환후에 원하는 비율만큼 비교하는것이지요

const float3 cascade_Edge = saturate(saturate(abs(ShadowPosition)) - 0.8)* 5.0f;
const float fade = max(cascade_Edge.x,max(cascade_Edge.y,cascade_Edge.z));

이런식으로 예를들어 x좌표의값이 -0.9 이거나 0.9의경우 첫줄코드를 넘어가게되면 0.5가 되게됩니다. 좌우에 상관없이 경계선에서 20% 영역의 반에 해당하는 위치가됩니다. 투영공간은 3축이 다있으므로 그중 가장큰값이 경계선에서 얼만큼 넘어갔는지 알수있을거라 생각해 그값을 Fade 값으로 지정햇습니다.

결국 20% 멀리떨어진 구역부터 경계선까지 Fade 값은 0~1로 증가하는 값을 가지게됩니다. 하지만 이방법은 이이상으로 더 생각나지 않더라구요. 일단 위사진의경우 Near 절두체의 빛기준 투영공간은 좌측이 -값을 가지게됩니다. 만약 오브젝트의 빛공간 위치가 (투영) -1~-0.8의 범위를 가진다면 그래도 Middle영역의 그림자맵과 보간을 진행하게됩니다. 이경우는 맞지않는것같다고 생각했습니다. 왜냐하면 Near 절두체의 투영공간의 -1~-0.8 부분은 위에 Middle의 투영공간과 겹치지 않는다고 생각해서입니다.(이부분에있어서 더 좋은 아이디어가 있으신분은 댓글남겨주세요 ^^)

두번째로는 우리가 배열안의 그림자맵을 고르는데 사용한 클립공간 Z 값을 사용하는것입니다. 현재 시야기준 Z값을 항상 앞을 보고있으므로 그림자맵의 전환이 이루어지는 구간은 절두체를 나누는데 사용한 Z이 경계선이 될것입니다. 이 Z값을 기준으로 20% 영역에 접근하면 보간을 수행하도록 합니다.(이전코드에서 수정한버전입니다)

float nearZ = cascadeEndClipSpace[i];
float farZ = cascadeEndClipSpace[i + 1];
float cascadeEdge = (farZ - nearZ) * 0.2;
float csmx = farZ - cascadeEdge;
float shadowMain = 1.0f;
if (input.clipSpacePosZ >= nearZ && input.clipSpacePosZ <= farZ)
{
	float4 cascadeLightPos = mul(lightPVW[i], float4(input.fragPos, 1.0f));
	shadowMain = CalcCascadeShadowFactor(i, cascadeLightPos);
	float shadowFallback = 1.0f;
	if (input.clipSpacePosZ >= csmx)
	{
		float ratio = (input.clipSpacePosZ - csmx) / (farZ - csmx);
		if (i < 2)
		{
			cascadeLightPos = mul(lightPVW[i + 1],float4(input.fragPos, 1.0f));
			shadowFallback = CalcCascadeShadowFactor(i + 1, cascadeLightPos);
		}
		shadowMain = lerp(shadowMain, shadowFallback, ratio);
	}
	shadowFactor = shadowMain;
	debugColor = checkcolor[i];
	break;
}

코드를 하나씩보면 현재 절두체를 이루는 nearZ와 FarZ를 얻습니다. 그리고 시야공간의 Z값이 기준이므로 Z방향은 양의값입니다 (이부분은 왼손좌표계냐 오른손좌표계냐에 따라 다르지만 제경우는 GL,DX 둘다 왼손좌표계를 사용합니다) 그후 nearZ로 FarZ를 빼고 0.2를 곱한 영역을 구하고 그것을이용하여 FarZ를 빼면 그것이 경계선에서 20%되는 영역입니다.(아래이미지는 해당 영역을 디버깅하기위한 이미지입니다)

대략적으로 경계선과 그밑 20%영역

그후 연산은 간단합니다. 처음에는 현재 오브젝트에대한 빛공간으로 변환후 해당 정점에대한 그림자계수를 계산합니다. 이때 해당 정점의 카메라기준 클립공간의 위치가 위에서 구한 경계선 기준 20% 되는값을 가지는 csmx값과 정점의 Z값을 비교하여 같거나 크다면 20% 영역안에 있다는뜻입니다. 이제 그후에는 경계선과 20% 정도 경계선 아래지점사이에 clipSpacePosZ 가 위치하는지 즉 비율을 알아냅니다. 

그다음은 현재 부분절두체가 마지막이 아닌지를 확인합니다. 왜냐하면 마지막 절두체는 다음절두체영역이 없으므로 보간할 그림자맵이 없기때문입니다. 마지막 절두체가 아니라면 루프문에서 인덱스+1을통해 다음 그림자맵과 다음 절두체영역의 빛투영행렬을 이용하여 그림자위치를 이용하여 그림자계수를 알아냅니다.

그렇게하여 얻은 두개의 계수를 lerp 함수와 비율을 사용하여 보간합니다!(GL은 mix 함수입니다 ,이전포스팅도 그렇고 GL 코드가 필요하신분은 따로 댓글에 적어주세요!)

하지만 만약 마지막절두체인경우는 어떻게할까요? 마지막절두체는 보간할 다음 절두체가 없으므로 그림자의 최대거리를 지정하는것처럼 그림자가 사라지면 될것같습니다. 하지만 그냥 사라지면 부자연스러우므로 그림자가 드리우지않는곳의값 즉 1.0f 의 값과 보간하도록합니다.

 

확실히 경계선이 부드러워진것을 확인할수 있습니다.

그림자의 최대 사정거리너머로 진입할시 부드럽게 그림자가 사라지는것을 확인할 수 있습니다.

CSM의 Fade 효과나 절두체사이 보간방법이 인터넷에서 검색되지않아서 직접 만들어봤습니다. 코드가 엉성하고 잘못된 점이 있을 수 있어요! 혹시 더 좋은 방법이있으면 알려주시면 감사하겠습니다. ^^

다음포스팅은 CSM 절두체를 나눌때 사용하는 알고리즘을 알아보려고합니다.