본문 바로가기
그래픽스

Cascade Shadow Map Pratical Split Scheme

by greenherb 2021. 7. 30.

이전 포스팅에서는 임의로 절두체 간격값을 정해줬습니다 (Z값) 그런데 이렇게 임의로 지정하는것외에도 Z값을 절두체 분할갯수만큼 적절하게 나눠주는 방법이있습니다.

https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-10-parallel-split-shadow-maps-programmable-gpus

 

Chapter 10. Parallel-Split Shadow Maps on Programmable GPUs

Chapter 10. Parallel-Split Shadow Maps on Programmable GPUs Fan Zhang The Chinese University of Hong Kong Hanqiu Sun The Chinese University of Hong Kong Oskari Nyman Helsinki University of Technology Shadow mapping (Williams 1978) has been used extensively

developer.nvidia.com

GPU gems3 문서에서 발견한 방법으로 문서에서 어떻게 나누는 위치를 정할지에 대한 얘기가 나옵니다.

영어와 이해하기 어려운 이미지가 많이나오지만 ㅠㅠ 설명내용은 그림자 얼라이어싱을 설명하기위한 그림과 내용이구요 (저도.. 그림을 이해하긴 어렵다만 언더샘플링 얘기 같습니다.. 특히 ShadowMap Aliasing 부분은 이해가안가서.. 누가 알려줬슴합니다.)

요점만 보자면 The Pratical Split Sceme 부분부터 시작합니다.

1-1

위 공식이 절두체를 분할할 위치를 구하는 공식입니다. Ci(log)와 Ci(uni) 두 함수의 값에대한 보간값이 Ci Z값이 됩니다.

Logarithmic 과 Uniform Scheme의 합성

문서내용을 완전하게 이해하기는 어렵고 간단하게만 설명하자면 왼쪽 A의 Uniform Spilt Scheme 은 Near과 Far 사이에 나눌 위치를 균등하게 나눕니다. 하지만 이경우 Z값이 너무 고르게 나누어져 가까이있는 물체에 대해 그림자매핑을 수행할때 언더샘플링이 일어날수 있씁니다. 반대로 Logarithmic Split Scheme 은 (b) 그림으로 나눠진 간격에 Near에 근접하게되고 마지막 Far 부분이 절두체의 대부분을 차지하는 상황이 만듭어지게 됩니다. 이런경우 가까이있는 물체의 오버샘플링이 일어나게 됩니다. 이러하여 Pratical Split Scheme 은 두 식을 합성하여 적절하게 배분된 Z값을 보여주게됩니다.

Logarithmic 식을 유도하는 공식이 있지만.. 저에겐 설명할 능력이 없으므로 최종식만 알려드리겠습니다.

logarithmic Split Scheme

위 공식을 이용하여 분할할 Z값을 구하게되는데 이때 i/m은 m=0이아니고 분할갯수를 의미하게됩니다. i는 Near부터 Far 까지 가는 분할선의 인덱스 구요

위식을 간단하게 직접 설명하자면 코드로는 아래와 같습니다!

float logarithmSplit = nearz * std::powf((farz / nearz), (i/amount));

실제로 4개의 선으로 분할한다고하면 (부분절두체 3개) 0번째 인덱스부터 시작하고 near = 1 ,far =1000 으로 지정할시

1 * (1000)^0/4 가나옵니다. 이경우 0/4는 0이나오고 어떤 수던지 0승은 1이 나오므로 1*1로써 가장 처음 0번째 인덱스는 near값인 1이나오게됩니다. 순서대로 적어내려가면

1 * (1000)^1/4  = 5.62.... ,1 * (1000)^2/4  = 31.6... , 1 * (1000)^3/4  = 177..... ,1 * (1000)^4/4  = 1000 의값이 나오게됩니다. 위에서 설명한것처럼 logarithmic Scheme은 대부분의 선이 near에 집중된것을 알수있습니다.

Uniform Split Scheme 식은 아래와같습니다.

Uniform Split Scheme

Uniform의 경우도 하나씩 알아보도록하겠습니다.

1 + (1000-1)*0/4 = 1, 1 + (1000-1)*1/4 = 250.75,1 + (1000-1)*2/4 = 500.5 , 1 + (1000-1)*3/4 = 750.25

1 + (1000-1)*4/4 = 1000

말그대로 균등하게 분할선이 분포된것을 알 수 있습니다. 한쪽은 너무 몰려있고 한쪽은 너무 균등하여 다운샘플링이 걱정된다면 이두값을 Lamda 값에의해 적절하게 보간해준값을 사용하는것이 Pratical Split Scheme 입니다! 정말 간단한 내용이죠? 사실 영어문서를 읽으면서 이해가 안됬지만 이것만 건져냈습니다 ㅠㅠ

그렇다면 각각의 Scheme 값에대해 Lamda =0.5 값을 가지고 보간한다면 어떤값이 나올까요? 간단하게 선형보간식을 사용한다면 

(1-Lamda) * 1 + Lamda *1 =  1 ,

(1-Lamda) *  5.62.... + Lamda *250.75128.185

(1-Lamda) * 31.6... + Lamda *500.5 =  15.8 + 250.25  = 266.05

(1-Lamda) * 177... + Lamda *750.25 =   88.5 + 375.125 = 463.625

(1-Lamda) * 1000 + Lamda *1000 =  1000

이런식으로 Pratical Scheme 의 값이 나오게됩니다 Lamda 값을 조절하여 Logarithm에 가까운값에할지 아니면 균등하게 Uniform 값에 가까이할지 정할수있습니다. 이제 코드로 나타내보자면

std::vector<float> zSplits;
zSplits.resize(amount);
zSplits[0] = nearz;
zSplits[amount - 1] = farz;
for (int i = 1; i < amount-1; ++i)
{
	float index = (i / (float)amount);
	float uniformSplit = nearz + (farz - nearz) * index;
	float logarithmSplit = nearz * std::powf((farz / nearz), index);
	zSplits[i] = std::lerp(logarithmSplit, uniformSplit, lamda);
}

위와 같은 코드가 될것입니다! 첫부분과 마지막부분은 Near과 Far 값이므로 구지 계산하지않고 중간에있는 분할선의 Z값만 계산합니다.

결과이미지는 이전 그림자와 동일하여 올리지않습니다^^ 다만 직접 값을 확인해보시면 Lamda값에따라 분할선의 위치가 달라지는것을 확인할 수 있습니다. 이제 사용자가 신경써야할부분은 직접 분할선 위치가아닌 Lamda값과 몇번 분할할지에대한 값만 전달해주면됩니다.