작성자 : 오석주 테크니컬 디렉터( TD, Technical Director ).

모바일에서 사용할 수 있는 SSS 에 대해서 소개한 글입니다. 저는 오탈자 교정 및 비문 수정만 했습니다.


Surbsurface Scattering( 이하 'SSS' ) 은 주로 빛이 표면의 안쪽으로 투과되어 들어 갔다가 다른 곳으로 튀어 나가는 표면에서 일어나는 현상이다. 주로 피부, 대리석, 우유, 양초 등이 이해 해당하는 물질들이다. 

우리가 주로 사용하는 BRDF( Bidirectional Reflectance Distribution Function ) 는 그림 1의 (a) 에서 볼 수 있듯이 빛이 입사한 지점과 반사되는 지점이 동일하다는 가정에서 만들어져 있다.

그림 1. (a) BRDF 와 (b) BSSRDF 에서의 빛의 산란.

출처 : https://graphics.stanford.edu/papers/bssrdf/bssrdf.pdf


그러나 SSS 를 계산하려면 Figure 1 의 (b) 와 같이 입사한 지점과 반사되는 지점이 다르다. 그러므로 아래와 같은 BSSRDF( Bidirectional Scattering-Surface Reflectance Distribution Function ) 을 사용해야 한다.

그림2. BSSRDF 모델.

출처 : https://graphics.stanford.edu/papers/bssrdf/bssrdf.pdf

편집자 주 :

앞에서 언급한 피부, 대리석, 우유, 양초 등을 이런 모델을 통해 렌더링하면 더욱 사실적인 결과를 산출한다. 여기에서는 피부를 중심으로 이야기할 것이다.

피부를 제대로 렌더링하기 위해 BSSRDF 를 실시간 그래픽스에 적용하는 것은 어렵다. 너무 무겁기 때문이다. 그래서 최적화를 위해 Texture-Space SSS 나 Scren-Space SSS( S5 ), Pre-Integrated SSS 등 여러가지 기법들이 만들어지게 됐다. 하지만 이런 기법들은 여전히 모바일에서 사용하기에는 무겁다.

그래서 Texture-Space SSS 나 Screen-Space SSS 의 개념을 기반으로 모바일에서도 사용할 수 있을 정도의 가볍고 근사된( approximated ) SSS 를 만드는 것을 목표로 했다.

일단 앞에서 언급한 두 가지 모델에 대해서 살펴 보도록 하겠다.

그림 3. Texture-Space Blur.

출처 : https://developer.download.nvidia.com/books/HTML/gpugems/gpugems_ch16.html

그림 3 은 Texture-Space( UV-Space ) 에서 라이팅 결과를 블러( Blur )해서 BSSRDF 를 흉내내는 것을 보여 준다. BSSRDF 에서, 블러의 크기는 빛이 입사된 지점과 반사된 지점 간의 차이를 나타내기 위해서 사용된다.

그림 4. Texture-Space SSS 와 Screen-Space SSS 의 비교.

출처 : http://www.iryoku.com/stare-into-the-future

그림 4 에서는 Texture-Space 블러와 Screen-Space 블러의 차이를 보여주고 있다.

그림 5. Screen-Space 블러에서 SSS 까지의 과정.

출처 : https://www.derschmale.com/blog/wp-content/weights.jpg

그림 5 는 Screen-Space 에서 블러한 결과를 사용해서 SSS 의 느낌을 달성하는 방법에 대해서 보여 주고 있다.

여기에서는 Texture-Space 와 Screen-Space 기법의 장단점에 대해서 논의하지는 않겠다. 이 기법들을 실제로 구현해 보면 자잘한 문제들을 가지고 있기 때문에 그것을 해결하는 방법까지 논의하면 할 이야기가 굉장히 많기 때문이다. 

어쨌든 여기에서 중요한 점은 대부분의 기법들이 라이팅 결과를 블러해서 SSS 효과를 만들고 있다는 것이다.

Normal Blur SSS 는 "모든 라이팅을 노멀 벡터와 라이트 벡터의 연산이라고 볼 때, Blur( dot( N, L ) ) 과 dot( Blur( N ), L ) 은 어느 정도 유사성을 띄고 있지 않을까?" 라는 아이디어에서 시작했다.

그림 6. 3x3 노멀 블러 커널.

그림 6 과 같은 커널을 사용해서 노멀맵을 블러한다고 가정해 보자( 여기에서 편의상 L 은 Directional-Light Vector 라 가정한다 ). 그러면 다음과 같은 결과를 얻을 수 있다.

위의 식은 다음과 같다.

실제로는 내적 결과를 0 이하로 잘라내는 과정인 max( dot( N, L ), 0 ) 을 수행하지 않았기 때문에 완전히 같다고는 할 수 없지만 근사치로는 쓸만하다고 할 수 있다. 포인트 라이트와 같은 punctual 라이트를 사용하면, 각 픽셀에서의 라이트 벡터는 달라지지만, 주변 픽셀을 블러한 것이므로 근사치로 사용하는 데 무리가 없다고 생각했다.

그러므로 노멀 벡터를 미리 블러해서 디퓨즈 라이팅을 계산하면 어느 정도 SSS 결과를 근사계산할 수 있다고 생각했다. 그리고 이 방법의 가장 큰 장점은 노멀 텍스쳐 한 장을 더 사용하고 dot( N, L ) 연산을 한 번 더 하는 정도의 비용으로 SSS 를 근사계산할 수 있다는 것이다. 문제는 "어느 정도로 SSS 가 근사되느냐" 이다.

 

그림 7. Object-Space Normal Map 블러. (좌) 원래 버텍스 노멀, (우) 블러된 버텍스 노멀.

일단 이해하기 쉽게 Object-Space Normal Map 을 뽑아 보았다. 여기에서는 블러를 할 것이므로 픽셀 노멀이 아니라 버텍스 노멀을 사용했다. 그리고 실제 적용도 버텍스 노멀에서 뽑아 낸 값으로 적용한다.

샘플에 사용된 모델은 Virtual Model 에서 무료로 제공하는 Louise 모델이다.

그림 8. Normal Blur SSS 결과. Unity3D, Android Platform. Custom PBR, Beckmann Specular.

그림 9. Normal Blur SSS 결과. Unity3D, Android Platform. Custom PBR, Beckmann Specular.

위 샘플들은 모두 Unity3D 의 Android Platform 에서 렌더링되었다. Custom PBR 셰이더를 사용했으며 Specular 는 Beckmann 모델을 사용했다. Normal Blur SSS 의 결과는 상당히 만족할만 했다. 실제로 사용된 노멀맵은 그림 10 과 같다.

그림 10. Louise 에서 사용된 2 장의 노멀맵.

실제로 이 기법을 모바일 기기에서 테스트해 봤을 때 최신 하드웨어에서는 사용했을 때와 그렇지 않을 때의 차이가 거의 없을 정도였다. 일반적인 렌더링 연산에서 늘어난 것은 블러된 노멀맵에 대한 한 번의 샘플링 비용과 dot( N, L ) 연산 몇 개 정도이다. 그러므로 최신 하드웨어에서는 차이를 거의 느낄 수가 없었다.

효과를 더욱 극단적으로 보이게 하기 위해서 과장된 다른 샘플의 예를 들어 보겠다. 아래 모델은 Free 3D Model 에서 무료로 제공되는 3D Scan Head 이다.

그림 11. 과장된 효과를 보여 주는 샘플.

주의 : 답이 틀릴 수도 있습니다. 그냥 정리하는 용도로 올립니다. 혹시라도 도움이 필요한 분이 있다면 도움이 되었으면 좋겠네요.

경고 : 숙제하려고 베끼는 데 사용하지 마십시오. 본인의 미래를 망칠 뿐입니다. 나중에 저를 원망하지 마세요.

부탁 : 문제 풀이가 잘못되었으면 지적해 주셨으면 좋겠습니다.



지표면 가까이서 도는 인공위성은 지상 100 km 정도의 낮은 궤도로 돌기도 한다. 지구 반지름이 약 6400 km 인 점을 고려하면, 그런 위성은 사실상 지표면을 따라 돈다고 볼 수 있다. 인공위성 궤도의 반지름을 6500 km 라고 할 때, 인공위성의 빠르기와 위성의 주기를 구하라. 단, 그 높이의 중력가속도 크기는 GM/R2 ≒ 10 m/s2 으로 가정하라.


[ 연습문제 5-5 ] 를 통해 공전하는 물체의 속도의 크기와 가속도 크기는 다음과 같다는 것을 알고 있습니다( 다시 증명하는 게 귀찮으니 링크만 겁니다 ).


식 1.


그런데 문제 1 에서 중력가속도의 크기가 주어졌습니다. 중력가속도의 크기는 위성의 가속도의 크기와 같으므로 다음과 같이 식을 정리할 수 있습니다.


식 2.


식 2 가 의심이 간다면 다음과 같이 중력이 두 물체의 질량과 중력상수에 비례하고 거리에 반비례한다는 사실을 이용하시면 확인할 수 있습니다.


식 3.


어쨌든 식 2 를 풀면 다음과 같이 속도의 크기를 구할 수 있습니다.


식 4.


하지만 해답은 7.85 X 10-3 이더군요. 물잘알 친구 햄에게도 풀어보라고 했지만 햄도 저와 같은 답을 산출했습니다. 그러므로 해답이 오답인 것으로 보입니다.


이제 속도를 알았으므로 식 1 을 사용해서 주기를 구할 수 있습니다.


식 5.


이 역시 햄과 동일한 답이 나왔습니다.


해답이 오답인 것으로 보입니다.

주의 : 답이 틀릴 수도 있습니다. 그냥 정리하는 용도로 올립니다. 혹시라도 도움이 필요한 분이 있다면 도움이 되었으면 좋겠네요.

경고 : 숙제하려고 베끼는 데 사용하지 마십시오. 본인의 미래를 망칠 뿐입니다. 나중에 저를 원망하지 마세요.

부탁 : 문제 풀이가 잘못되었으면 지적해 주셨으면 좋겠습니다.



지구는 태양 주위를 약 365 일 주기로 원운동을 한다. 지구에서 태양까지의 거리는 149.6 X 106 km 이다. 지구가 공전하는 가속도 크기를 구하고, 그로부터 태양의 질량을 계산하라.


어떤 원에서 각 θ 일때의 위치 함수 P 를 생각해 봅시다. 그러면 위치함수는 x 와 y 성분으로 이루어진 벡터를 반환합니다.


그림 1.


그림 1 에서 P 함수의 값은 삼각함수를 통해 구해졌습니다.


그런데 우리가 주어진 문제를 풀기 위해서는 θ 를 시간에 대한 함수로 나타낼 필요가 있습니다. 왜냐하면 지구가 태양 주위를 회전하는 데 걸리는 시간을 의미하는 주기를 고려해야 하기 때문입니다.


그림 1 의 원점이 태양의 위치라 가정하면, 지구는 그것을 중심으로 원운동을 합니다. 문제에서 365 일이라고 지정된 주기를 T 라 합시다. 단위시간( 여기에서는 1일 )에 회전하는 각도를 각속도라고 하고 그것을 ω 라 표현하면, 식 1 이 성립합니다. T 일 동안 한 바퀴를 돌면 1 일 동안 ω 를 돌게 된다는 것이죠.


식 1.


자, 이렇게 되면 현재 각 θ 는 시간 t 에 대해 다음과 같이 표현될 수 있습니다.


식 2.


만약 t 가 T 라면 결과는 2π 이고 한바퀴의 각도를 의미하게 됩니다.


이제 위치함수 P 는 다음과 같이 표현될 수 있습니다.


식 3.


위치 함수를 적분하면 속도함수가 나옵니다. 문제는 이 위치함수가 합성함수라는 점입니다. 여기에서 합성함수 미분법을 증명하는 것은 주제에서 벗어나기 때문에 "합성함수 미분" 이나 "미분 연쇄법칙" 등을 키워드로 해서 검색해 보시기 바랍니다.


어쨌든 P 함수를 성분별로 미분하면 속도 함수가 나옵니다.


식 4.


식 5.


속도 함수를 미분하면 가속도 함수가 나옵니다.


식 6.


식 7.


지금까지의 식들을 정리해 보도록 하겠습니다.


식 8.


속도 벡터와 가속도 벡터의 크기는 다음과 같습니다.


식 9.


식 10.


이제 식 10 에 주어진 값들을 대입해 보겠습니다.


식 11.


해답은 5.939 X 10-3 입니다. 반올림을 한 거니 답이 맞다고 가정합니다.


여기에서 구한 가속도는 태양과 지구 사이에서 작용하는 원심력인 중력과 같습니다.


중력은 만유인력 상수 및 두 물체의 질량에 비례하며, 거리에 반비례합니다. 그러므로 다음과 같은 식을 세울 수 있습니다.


식 12.


해답은 1.992 X 1030 인데 첫번째 문제에서 반올림하지 않은 것의 영향이라고 보기에는 값이 너무 차이가 납니다. 


5.939 X 10-3 를 넣어 봤는데 1.9756 X 1030 이 나왔습니다.


해답이 오답인 것으로 보입니다.

주의 : 답이 틀릴 수도 있습니다. 그냥 정리하는 용도로 올립니다. 혹시라도 도움이 필요한 분이 있다면 도움이 되었으면 좋겠네요.

경고 : 숙제하려고 베끼는 데 사용하지 마십시오. 본인의 미래를 망칠 뿐입니다. 나중에 저를 원망하지 마세요.

부탁 : 문제 풀이가 잘못되었으면 지적해 주셨으면 좋겠습니다.



높이가 H 인 나무 위에 있는 원숭이를 향해 사냥꾼이 총을 발사했다. 총알의 빠르기는 v0 로 일정하고, 사냥꾼에서부터 나무까지의 수평거리는 R 이다.



(a) 총알이 나무 위치에 도달하는데 설린 시간은 얼마인가?


(b) 그 시각 총알의 높이는 얼마인가?


(c) 총알이 발사되는 순간 원숭이가 떨어지기 시작했다면, 그 시각 원숭이가 있는 높이는 얼마인가?


처음에는 답이 틀렸다고 생각했는데, 물잘알 햄이 풀어 보더니 답 맞다고 합니다. 삼각함수의 정의를 이용해서 풀면 맞더군요. 아직 저는 응용력이 많이 부족한 것 같습니다.


중력가속도를 g 라 합니다.


먼저 H 와 R 로 이루어진 직각삼각형의 빗변을 L 이라 하겠습니다.


식 1.


그러면 이제 사인과 코사인을 구할 수 있습니다.


식 2.


총알을 발사하면 총알 속도의 수직성분은 중력에 의해서 감속됩니다. 수평성분은 그대로입니다.  그러므로 시간 t 일 때의 수평성분과 수직성분은 다음과 같이 나타낼 수 있습니다.


식 3.


시간 t 일 때 위치 함수는 식 3 을 부정적분함으로써 구할 수 있습니다.


식 4.


식 5.


식 4 와 식 5 를 각각 정적분하게 되면 [0, t] 까지의 이동거리가 나옵니다.


식 6.


식 7.


( a )


총알이 나무에 도달하는 시간을 th 라 하겠습니다. 이 때까지의 수평성분 이동거리는 R 이 됩니다. 식 6 을 사용해 이동거리를 구하면 도달하는 시간을 구할 수 있습니다.


식 8.


( b )


총알의 높이는 식 5 를 사용해서 구할 수 있습니다.


식 9.


( c )


원숭이는 자유낙하를 합니다. 시간 t 에 원숭이의 속도는 다음과 같습니다.


식 10.


그러면 식 10 을 부정적분해서 원숭이의 위치 함수를 구할 수 있습니다.


식 11.


식 11 에  th 를 대입하면 원숭이의 위치가 나옵니다.


식 12.

주의 : 초심자 튜토리얼은 아닙니다. 그러므로, 실제 API 호출 용례를 알고자 한다면, 샘플이나 튜토리얼을 찾아서 확인해 보세요.

주의 : 완전히 이해하고 작성한 글이 아니므로 잘못된 내용이 포함되어 있을 수 있습니다.

주의 : 이상하면 참고자료를 확인하세요.



일단 LunarG 나 Khronos group 의 문서들을 보면 셰이더 언어로 GLSL 을 리플렉션 라이브러리로 spirv-cross 를 소개하는 경우가 많습니다. 


그런데 HLSL 을 SPIR-V 로 변환하는 것과 관련해 설명하는 문서들에서는 리플렉션을 위해 spirv-reflect 를 소개하는 경우가 많습니다. 보통 이런 문서는 구글의 "Lei Zhang" 이라는 사람이 주로 발표하는데요, 이 분이 dxc 와 spirv-relfect 구현에 참여를 한 것으로 보입니다. 이 분은 github 에서 "antiagainst" 라는 계정을 사용하고 있습니다. 구글에서는 dxc 를 통해서 spirv-reflect 를 사용하도록 권장하는 모양인 것 같습니다. MS 도 아니고 구글이 그런 작업을 한다는 사실이 참 아이러니합니다.


spirv-reflect 는 매우 가볍습니다. 헤더 하나에 소스 하나입니다. [ SPIRV-Reflect ] 에 올라와 있으니 자유롭게 받으실 수 있습니다.


사용방법은 매우 간단한데요, 위의 링크를 참고하시면 쉽게 따라할 수 있습니다. 


단지 문제가 하나 있다면 해당 페이지의 코드 조각 중에서 input_vars 가 SpvReflectInterfaceVariable* 타입이 아니라 SpvReflectInterfaceVariable** 타입이어야 한다는 것입니다. 무작정 따라 하시면 나중에 크래시납니다. 게다가 input_vars 에 대한 메모리 해제도 안 하고 있으니 주의하셔야 합니다.


예제는 spvReflectEnumerateInputVariables() 함수만 소개하고 있는데요, 여러 종류의 열거 함수들이 있습니다. 자신의 필요에 맞게 함수를 호출해서 사용하면 됩니다.



위의 함수와 동일한 기능을 하는 ShaderModule 클래스의 래퍼 메서드들도 존재합니다. 어떤 스타일을 선호하느냐는 사용자가 결정하면 됩니다.



그런데 이건 좀 사용하기에 불편한 점이 있습니다. 예를 들면 아래와 같은 HLSL 코드를 살펴 봅시다.



여기에 대해 spvReflectEnumerateInputVariables() 를 호출하면 main() 함수의 인자인 PSInput 의 내용을 출력해 줍니다. 하지만 이름이 맹글링되어 있고 제대로 유지되지 않는 것을 알 수 있습니다.  Color, Alpha, Scaling 등의 정보가 시맨틱과 비슷하면서도 다릅니다.


Name = in.var.NORMAL

Name = in.var.COLOR_1

Name = in.var.OPACITY_512

Name = in.var.SCALE_987654321

Name = in.var.TEXCOORD0

Name = in.var.TEXCOORD1

Name = in.var.TEXCOORD2


그런데 dxc 의 옵션으로 "-fspv-reflect" 를 지정해서 컴파일했다면 정확한 이름/시맨틱 쌍을 얻을 수 있습니다.


Name = in.var.NORMAL

Semantic = NORMAL

Name = in.var.COLOR_1

Semantic = COLOR_00001

Name = in.var.OPACITY_512

Semantic = OPACITY_512

Name = in.var.SCALE_987654321

Semantic = SCALE_987654321

Name = in.var.TEXCOORD0

Semantic = TEXCOORD0

Name = in.var.TEXCOORD1

Semantic = TEXCOORD1

Name = in.var.TEXCOORD2

Semantic = TEXCOORD2


시맨틱을 얻게 되면 다음과 같은 식으로 시맨틱을 사용해서 리플렉션 정보를 획득하는 것도 가능합니다.



사용자들은 맹글링된 이름이 아니라 원래 이름으로 검색하고 싶겠지만, 안타깝게도 그런 방법은 존재하지 않습니다. dxc 에 이런 저런 옵션을 다 넣어서 확인해 봤지만 정보가 안 나오더군요. 만약 필요하다면 dxc 를 커스터마이징하는 수밖에 없는 것 같습니다.


그래도 확실하게 정보가 유지되는 것은 시맨틱이기 때문에 시맨틱을 사용해서 리플렉션을 하는 것이 현명해 보입니다.

+ Recent posts