5. 왜 물은 물처럼 보이는 걸까?

반응형

앞의 글 참조
1. http://mgun.tistory.com/1282
2. http://mgun.tistory.com/1294
3. http://mgun.tistory.com/1304
4. http://mgun.tistory.com/1306

5번째 글이 늦은 이유는.....
글 적다가 한번 날려서 멘붕이 왔다...
글을 적을 때 한번에 쫘악~ 다 적을 능력이 안 되서
하나의 글을 몇일동안 조금씩 적는데 그걸 날려먹으니
멘탈이 쫀득쫀득해져서 한동안 다시 쓸 엄두가 안났다는...ㅜㅜ...

무튼~!
기본적인 라이팅도 했고 반사도 했으니 이젠 굴절(Refraction)이라는 녀석을 살펴 볼 차례다.
굴절이란 무었인가??

그림 1.
출처 : http://mathforum.org/mathimages/index.php/Snell's_Law

위 사진을 보면 물이 담긴 유리잔에 숟가락이 들어 있는데
물 표면을 경계로 위쪽과 아래쪽이 매칭이 안 된다.
마치 어긋나 있는 것 처럼 보인다.


그림 2.
출처 : http://terms.naver.com/entry.nhn?cid=200000000&docId=1067924&mobile&categoryId=200000457

위 설명에서 그림이 워낙 직관적이라서 글은 안읽어도 될 듯.
사실 위 그림을 확대한 듯한 그림을 예전에 한번 올렸었다.
BRDF를 설명할 때 였었는데 2번째 글에서 언급했었다.
http://mgun.tistory.com/1294
부연 설명을 하자면 물과 공기라는 다른 성질을 가지고 있는 각 층을 나누어 주는 것이 바로 물 표면(surface)다.
그래서 빛이 공기에서 물로 들어갈 때 물 표면을 기점으로 꺽이게 되고, 그래서 우리가 물에 담긴
연필이나 스푼을 보면 마치 휘어지거나 어긋나 있는 것 처럼 보인다는 것이다.

그리고 여기 water 예제로 유명한 "Pond Water"(연못) 을 보자.

그림 3.
 원본 PDF 파일 :



한글 번역 : http://blog.naver.com/swoosungi/90086817771
나름 상당한 퀄리티를 자랑한다.

기본적인 내용은 아래와 같다.
1. 물 표면을 제외한 현재 화면을 텍스처에 렌더링 한다. (이를 굴절맵이라고 부른다.)
2. 물 표면을 제외하고 물표면에 대해 반사되는 장면을 텍스처에 렌더링 한다. (이를 반사맵이라고 부른다.)
3. 하늘과 다른 메시들을 그리고 반사맵과 굴절맵을 사용하여 물 표면을 그린다.

여기서는 반사맵도, 굴절맵도 동적으로 그렸다.
반사맵 같은 경우는 정적으로도 많이 그리지만 당연히 동적으로 매번 그려주는게 퀄리티는 훨씬 좋다.

실제 코드를 보면 아래와 같다.
1. 굴절맵에 하늘과 프랍들(메시들)을 그린다.
refractMap->beginScene();
mSky->draw(0);
drawSceneMesh(0);
refractMap->endScene();

2. 반사맵에 하늘과 프랍들을 그린다.
reflectMap->beginScene();
mSky->draw(&waterPlaneW);       // 반사된 하늘을 반사맵에 그리기 위한 인자. 카메라를 수면 아래로 한다.
drawSceneMesh(&waterPlaneW);
reflectMap->endScene();

중복 사진이지만 대략 이렇게...



3. 하늘과 프랍과 물을 그린다.
mSky->draw(0);
drawSceneMesh(0);
mWater->draw();

2번에서 반사맵에 하늘과 프랍들을 그릴 때 인자로 넣어준 waterPlaneW는 dx에서 제공하는 평면 구조체이다.
이 값은 a,b,c,d 값을 가지는 데 a=normal.x  b=normal.y  c=normal.z  d= dot(-normal,임의의 한점p)로 나타낸다.


// 지역공간에서의 반사평면
D3DXPLANE waterPlaneL(0.0f, -1.0f, 0.0f, 0.0f);


// 월드 공간에서의 반사평면
D3DXMATRIX WInvTrans;
D3DXMatrixInverse(&WInvTrans, 0, &(mWaterWorld));

// 전치행렬로 전환
D3DXMatrixTranspose(&WInvTrans, &WInvTrans);
D3DXPLANE waterPlaneW;
D3DXPlaneTransform(&waterPlaneW, &waterPlaneL, &WInvTrans);

// 동차 클립공간 (wvp)에서의 반사평면
D3DXMATRIX WVPInvTrans;
D3DXMatrixInverse(&WVPInvTrans, 0, &(mWaterWorld*gCamera->viewProj()));
D3DXMatrixTranspose(&WVPInvTrans, &WVPInvTrans);
D3DXPLANE waterPlaneH;
D3DXPlaneTransform(&waterPlaneH, &waterPlaneL, &WVPInvTrans);

// 수면에 걸친 오브젝트를 제외하고 장면을 그리기 위해 clip plane을 이용.
// 인자로 들어가는 평면은 projection까지 끝난 scene 좌표.

float f[4] = {waterPlaneH.a, waterPlaneH.b, waterPlaneH.c, waterPlaneH.d};
HR(gd3dDevice->SetClipPlane(0, (float*)f));
HR(gd3dDevice->SetRenderState(D3DRS_CLIPPLANEENABLE, 1));
참고 : http://kesegyu.com/tt/480


법선벡터의 변환은 신경을 좀 써 줘야 한다.
리얼 타임 렌더링 56P를 보면 "법선 벡터 변환(Normal Transform)"이라는 항목이 있다.
 "법선 벡터들은 기하 구조 변환에 사용된 특정 행렬의 역행렬의 전치행렬에 의해서 변환되어야만 한다."

이동의 경우는 변할것이 없지만 회전일 경우 똑같이 회전해 줘야 하고
크기조정일 경우는 균등크기 조정이라면 그 값으로 나누어 주면 되지만 비 균등 이라면
역행렬을 새로 구해줘야 한다.

참고 글 : http://scosco.com.ne.kr/ES20Html/basic_normal.htm
             http://tails.tistory.com/archive/20111121

참고로 타카시 책의 279p에도 설명이 되어 있다. 
float3 N = normal(mul(Normal, (float3x3)mWIT)); // 월드 좌표계에서의 법선.
법선 벡터 N은 mWIT라는 행렬로 변환하는데 이는 로컬좌표를 월드좌표로 변환하는 행렬의 역전치행렬이다.
법선 벡터는 특별한 벡터로써 물체를 평행이동 하면 변하지 않으며 물체를 회전할 경우에는 
동일한 방향으로 방향이 바뀐다
. 또한, 물체를 크기 변환할 때도 방향을 바꾼다.
즉, 단순하게 월드행렬로 변환되는 것이 아니다.


참고 글 : http://www.gpgstudy.com/forum/viewtopic.php?t=1337&highlight=
 

그림 4.

그럼 이런 멋진 굴절들을 어떻게 만들 수 있을까?

우선 udk를 통해서 한번 살펴보자.
 

그림 5.

위 사진은 UDK material editor에서 만든 왜곡(Distortion)이다.
이 머티리얼을 static mesh를 하나 띄워서 그 mesh에 넣어 보면 차이는 더욱 명확해 진다.

1. 왜곡없는 머티리얼.
그림 6
.
2. 왜곡있는 머티리얼.

그림 7.
왜곡이 추가된 모습

수면을 기준으로 어긋나 보이는 것을 알 수 있다.
위 결과물은 가장 처음 사진인 유리잔에 스푼이 담겨있는 것과 꼭 닮았다.


UDK Material Editor에는 기본적으로 Distortion(왜곡)이라는 항목이 존재한다.

그림 8.

아잉, UDK 넘흐 편해유~! 사랑해유~!

그림 9.

우선 뒤에있는 와이어좀 보이게 투명좀 먹였다.
그리고 Distortion 항목에 적당한 값을 넣어주었다.
첫번째 값 5는 좌우 어긋남을, 두번째 값 5는 상하 어긋남을 표현 해 준다.

자동완성 코드에는 아래와 같이 되어 있다.
float2 GetMaterialDistortion(FMaterialPixelParameters Parameters)
{
 float3 Local10 = (UniformPixelVector_0.rgb * GetPerInstanceSelectionMask(Parameters));
 return float3(5.00000000,5.00000000,0.00000000).rg;
}
오잉? 내가 생각했던 것 만큼 자세히 나오지 않는다.
distortion 계산은 오프셋값만 노출되어있고 실제 계산은 실제 shader 파일에서 한다.

이제 이 Distortion값을 내 시야와 연계해서 작동되게 만들어 줘야 한다.
시야와 노말을 기반으로 감쇠를 해주는 녀석이 있었는데 바로 "프레넬"이다.
이녀석은 표면의 노멀이 카메라를 향하면 0, 표면의 노멀이 카메라에 수직인 경우 1의 값을 가진다.
프레넬에 대한 설명은 http://mgun.tistory.com/1304 을 참고하면 된다.

즉 끝부분일 왜곡이 심하고 현재 내가바라보는 부분일 수록 왜곡이 없다는 것이다.
한번 테스트 해 보자.

그림 10.

끝 부분(시야벡터와 직각에 가까운 부분)쪽의 그리드가 왜곡되어 있는게 보인다.

어차피 프레넬의 결과값은 0~1사이다.
이 0~1사이의 비율을 좀 넓혀서 사용해 보자.

그림 11.

단순히 5,5 벡터를 곱해 0~1사이의값을 0~5사이의 값으로 뻥튀기 해주었다.
그런데 내가 원하는 왜곡은 사실 끝에서 왜곡되는 것이 아니라 가장자리에서 왜곡 되는 것이다.
즉, 물 표면에서 끊어지는 것 처럼 왜곡되는 것이 아니라 물 내에서 늘어지는 것처럼 왜곡 되는 것이다.

그렇다면 프레넬의 성질을 반대로 이용하면 된다.
프레넬의 값은 0~1사이니까 1에 가까운 값일수록 0을, 0에 가까운 값일수록 1의 값을 가지도록
1 - value 를 해 주면 된다.

그림 12.

확실히 결과물이 다르다.
결과물을 좀 더 확실히 알 수 있도록 5,5의 값을 좀 변경하자.

그림 13.
결과물이 확실히 다르다.

지금 물은 그냥 투명하다. 전혀 물 처럼 보이지 않는다.
물에 색깔을 좀 입혀보자.

그림 14.

Diffuse로 사용하기 위해 환경맵(여기서는 큐브맵)을 사용하는데 이 큐브맵의 UV좌표로 Transform을 통과시킨
Reflection이 사용된다.
Transform은 좌표시스템을 변환해 주는 표현식이다.
좌표 시스템의 변환 대한 이해가 필요하다면 아래 글을 보면 간단하게 적혀 있다.
http://mgun.tistory.com/1342

기본적으로 머티리얼의 모든 셰이더 계산은 탄젠트 공간에서 수행되는데
벡터상수, 카메라 벡터, 라이트 벡터등은 모두 머티리얼에 사용되기 전에 탄젠트 공간으로 변형된다.
이러한 벡터를 탄젠트 공간이나 월드공간, 로컬공간, 뷰공간 좌표시스템으로 변환시켜 주는 것이 Transform이다.

그림 15.

여기서는 Reflection을 Tangent공간에서 World 공간으로 변환했다.


표면 노말에 걸쳐 반사된 카메라 방향을 나타내는 3채널 벡터값인 ReflectionVector을 Transform에 통과시켜
환경맵이 할당된 TextureSample표현식에 대한 UV텍스처 좌표로 사용해야 큐브맵을 메시의 표면에 매핑시킬때
올바르게 나온다.

여기에 프레넬을 곱해서 표면이 카메라에서 멀어질 수록 반사가 뚜렵해 지도록 만들어 준다.

그림 16.

이렇게 하면 카메라가 바라보는 부분은 반사가 거의 일어나지 않고 카메라가 멀어질 수록, 즉 바깥부분은 더 반사가
확실하게 일어나도록 표현된다.
 

그림 17.

Opacity(투명)값에도 프레넬을 적용해서 Diffuse와 같은 느낌을 살려준다.
이렇게 해 줘야 현재 카메라벡터와 같은 노말(즉, 마주보는)을 가진 표면에서는 투명하고
노멀이 직각에 가까울 수록(멀리있을수록) 반사가 뚜렷하게 일어나면서 불투명 해 진다.

추가적으로 specular과 specular power을 주면 좀 더 그럴듯하게 보일 수 있다.
이 값은 적당히 원하는 const 수치를 주면 된다.

여기까지가 UDK에서 기본적으로 제공하는 Distortion을 사용해서 Cubemap을 왜곡시킨 결과물 이다.

내부적으로 이 distortion이 어떻게 만들어 지는지 좀 더 살펴보자...다음글에서.ㅇㅇ

TAGS.

Comments