[Snow Footprint] 04.프로젝트 분석 (完)
- -
이번 포스팅에선 앞서 말씀드린 대로 Zhen Geng 님의 Snow Footprint 포스팅과 소스를 분석 할 것 입니다.
(Zhen Geng님 (이하 저자)의 본 포스팅은 다음 링크에서 확인 하실 수 있습니다 : http://zhengeng.net/snow-footprint)
해당 포스팅의 저자는 기존 Decal, Normal Map 등의 방법을 통한 발자국을 표시하는 방법은 현실과의 이질감을 주어, 사용자의 몰입을 방해 할 수 있다고 생각했습니다. 그래서 그는 Tessellator를 통해 실제로 바닥과 발자국이 상호작용을 할 수 있게 만들고 싶어했고, 그 과정에서 다음과 같은 문제에 직면했습니다.
1. 모든 지형을 덮을 만큼 큰 텍스쳐와 displacement map을 실시간으로 색칠하는 것도 하나의 간단한 방법이 될 수 있지만, 지형의 크기가 커짐에 따라 해상도 저하는 여전히 큰 문제가 됩니다.
2. normal 과 tangent를 효율적으로 계산하는 방법이 필요합니다.
3. 발자국의 Occlusion map과 normal map의 UV coordinate를 계산할 수 있어야 합니다.
사실 우리는 문제 2번을 해결했던 경험이 있습니다. 바로 근사 수치 기법을 이용하는 것 인데요, 앞서 경험했던 짬밥이 있음으로 쉽게 이해할 수 있을 것 입니다.
그럼 이제 문제 1번부터 솔루션을 확인해 보도록 하겠습니다.
그는 발자국 이미지를 하나의 텍스쳐에 레코딩하면 해상도가 낮아지는 것을 염두해 새로운 방법을 고안했습니다.
바로 표현해야 할 이미지와, 이미지를 표시 할 때 필요한 정보를 나눠서 저장하는 것 입니다.
표현해야 할 이미지는 발자국 이미지기에, 이후 적용할 쉐이더의 sampler에 property 중 하나에 할당해주면 됩니다. 문제는 정보의 표현이었죠.
그래서 저자는 다음과 같이 Sprite의 색에 정보를 담았습니다.
A |
R |
G |
B |
T |
X |
Y |
Z |
이렇게 정보를 담은 Sprite는 Camera와 RenderTexture를 통해
위와 같이 하나의 Texture에 표현되며 우리는 이 Texture를 Shader의 sampler 중 하나로 지정함으로써 위치 정보를 읽고 해당 위치에 해상도 손실 없이 이미지를 표현 할 수 있게 됩니다.
이 과정에는 아래의 스크립트와 쉐이더가 사용됩니다.
- footCollide.cs
- DistanceMapPainter.cs
- Sprite-Distance.shader
footCollide 객체는 발과 표면의 충돌 이벤트를 감지하고 해당 정보를 DistanceMapPainter 객체에 전달합니다.
그리고 DoAction 함수에서 전달된 정보를 다음과 같이 가공합니다.
void DoAction(Vector3 position,float angle)
{
GameObject spriteObj;
spriteObj = (GameObject)Instantiate(Resources.Load("default_sprite"));
spriteObj.transform.parent = this.transform;
spriteObj.transform.localPosition = new Vector3(position.x-0.5f,position.z-0.5f,0); // y 위치에 z 값을 넣음
Color positionInfo = new Color(spriteObj.transform.localPosition.x + 0.5f, angle, spriteObj.transform.localPosition.y + 0.5f, 1.0f);
spriteObj.GetComponent().color = positionInfo; //pass the position info color to sprite shader
spriteCounter++;
}
이어서 2번째 문제와 3번째 문제를 저자는 어떻게 해결했는지 확인해보겠습니다.
저자는 아래 사진과 같은 솔루션을 통해 Deformed된 normal, tangent, bitangent를 도출함으로써 2번 문제를 해결했다고 했습니다.
눈에 보이시나요? 저희가 앞서 공부했던 Game Jam 1권 42장의 Deformer에서 설명됐던 '근사 수치 기법' 입니다.
그도 역시 마찬가지로 가상의 두 정점을 만들어 tangent와 bitangent를 구한 뒤 이 두 값을 통해 normal를 도출하는 방법을 사용했습니다.
이 과정은 SnowGroundShader.shader 내 ds (Domain Shader) 함수에 아래와 같이 구현돼 있습니다.
//sample distancefield map data this function will be called in vs/ds/fs shader in total 3 times
inline float4 distData(sampler2D distanceField, float3 worldPosition, float4 anchorPoint, float coverSize )
{
float2 relativePos = (worldPosition.xz - anchorPoint.xz) / coverSize; // coversize는 땅의 Axis 크기를 의미합니다
//UV 맵에 대해 상대좌표를 표현하기 편하게 coverSize로 나눈 것 이다.
return tex2Dlod(distanceField,float4(relativePos,0.0f,0.0f)); // tex2Dlod 함수를 통해 RenderTexture에 기록된 발자국의 정보가 담긴 Color를 반환합니다.
}
//helper function to clip the uv into the (0,1)range
inline float2 clipRect(float2 rect) {
int sx = step(0, rect.x)*step(rect.x, 1);
int sy = step(0, rect.y)*step(rect.y, 1);
int sxy = sx*sy;
return float2(rect.x * sxy, rect.y * sxy);
}
[domain("tri")]
// SV_DomainLocation값은 Tessellator를 통해 산출된 중심좌표를 나타내기 위한 가중치입니다.
// 도메인 쉐이더에서 Displacement Mapping이 진행되며, 실제 버텍스의 위치 변환이 이루어집니다.
GS_INPUT ds(HS_PER_PATCH_OUTPUT i,const OutputPatch vi, float3 bary : SV_DomainLocation)
{
GS_INPUT o;
// Patch를 이루는 성분들을 가중치를 바탕으로 재설정합니다.
o.vertex = vi[0].vertex * bary.x + vi[1].vertex * bary.y + vi[2].vertex * bary.z;
o.worldPos = vi[0].worldPos * bary.x + vi[1].worldPos * bary.y +vi[2].worldPos * bary.z;
float3 normal = vi[0].normal * bary.x + vi[1].normal * bary.y + vi[2].normal * bary.z;
float4 tangent = vi[0].tangent * bary.x +vi[1].tangent * bary.y + vi[2].tangent * bary.z;
o.uv = vi[0].texcoord * bary.x + vi[1].texcoord * bary.y + vi[2].texcoord * bary.z;
// distance data need to resample
float2 relativePos = o.worldPos.xz - _AnchorPoint.xz;
float4 distanceData = distData(_DistanceFiled,o.worldPos,_AnchorPoint,_CoverSize);
float displaceAmount = _Displacement*distanceData.a;
// start to deform the vertex position
// get the center position xz
float2 centerPos = distanceData.xz * _CoverSize;
// data y is the angle/2pi data
float angle = (distanceData.y * 2.0f -1.5f) * 3.1416f;
relativePos -= centerPos;
float cosine = cos(angle);
float sine = sin(angle);
// 회전변환을 이용해 rotatedPos 산출 합니다.
float2 rotatedPos = float2(relativePos.x*cosine - relativePos.y*sine, relativePos.y*cosine + relativePos.x*sine);
// calculat the uv used to sample the height map
float2 heightUV = clipRect(rotatedPos / _TextureWorldSize + float2(0.5f, 0.5f));
o.detailuv = heightUV; //ouput the detailed uv
// sample the height map
float d = (tex2Dlod(_DispTex, float4(heightUV, 0, 0)).r - 0.5)* displaceAmount + _DispOffset;
o.vertex.y += d; //displace along the y direction
// reconstruct the normal and tangent value
float3 wNormal = UnityObjectToWorldNormal(normal);
float3 wTangent = UnityObjectToWorldDir(tangent.xyz);
// 상식 : 유니티에서 tanget.w는 -1과 1 의 값을 가진다. -1은 주로 binormal(bitangent)을 flip 할 때 사용한다.
float tangentSign = tangent.w*unity_WorldTransformParams.w;
float3 wBitangent = cross(wNormal, wTangent) * tangentSign;
// 근사수치 기법을 통해 Normal을 계산할 것 이기에, 다음과 같이 2개의 가상 정점을 정의합니다.
float3 pointAtBitangent_world = o.worldPos + wBitangent*EPS;
float3 pointAtTangent_world = o.worldPos + wTangent*EPS;
// _DispTex 즉, displace map 데이터를 통해 밟은 부위의 y축 값을 내리는 작업을 합니다.
// 회전 변환에 용이하게 relative 좌표계로 바꿔줍니다.
float2 pointAtBitangent_relative = pointAtBitangent_world.xz - _AnchorPoint.xz - centerPos;
float2 pointAtTangent_relative = pointAtTangent_world.xz - _AnchorPoint.xz -centerPos;
// 회전변환을 적용합니다.
float2 pointAtBitangent_rotated = float2(pointAtBitangent_relative.x*cosine - pointAtBitangent_relative.y*sine,
pointAtBitangent_relative.y*cosine + pointAtBitangent_relative.x*sine);
float2 pointAtTangent_rotated = float2(pointAtTangent_relative.x*cosine - pointAtTangent_relative.y*sine,
pointAtTangent_relative.y*cosine + pointAtTangent_relative.x*sine);
// uv좌표계로 변환합니다. (문제 3번을 해결하는 과정입니다.)
float2 uvAtBitanget = clipRect(pointAtBitangent_rotated / _TextureWorldSize + float2(0.5f, 0.5f));
float2 uvAtTangent = clipRect(pointAtTangent_rotated / _TextureWorldSize + float2(0.5f, 0.5f));
// _DispTex 위 uv 좌표에 있는 컬러 값을 통해 bitangent와 tangent에서의 displacement 양을 얻습니다.
float dAtBitangent = (tex2Dlod(_DispTex, float4(uvAtBitanget, 0, 0)).r - 0.5) * displaceAmount + _DispOffset;
float dAtTangent = (tex2Dlod(_DispTex, float4(uvAtTangent, 0, 0)).r - 0.5)* displaceAmount + _DispOffset;
// displace합니다.
pointAtBitangent_world.y += dAtBitangent;
pointAtTangent_world.y += dAtTangent;
// new tangent direction
float3 new_worldPos = mul(unity_ObjectToWorld, o.vertex).xyz;
float3 new_wTangent = normalize(pointAtTangent_world - new_worldPos);
float3 new_wBitangent = normalize(pointAtBitangent_world - new_worldPos);
float3 new_wNormal = tangentSign*cross(new_wTangent, new_wBitangent);
o.tspace0 = half3(new_wTangent.x, new_wBitangent.x, new_wNormal.x);
o.tspace1 = half3(new_wTangent.y, new_wBitangent.y, new_wNormal.y);
o.tspace2 = half3(new_wTangent.z, new_wBitangent.z, new_wNormal.z);
o.worldPos = new_worldPos;
return o;
}
회전 정보를 적용해야 하기 때문에 기존과 달리 다소 코드가 길어졌지만, 분명 저희가 사용했던 근사 수치 기법이 사용됐음을 확인 할 수 있습니다.
이후 Geometry Shader Stage 에서 다음과 같이 보간 중 발생하는 UV 오류를 중화시킵니다.
Geometry Shader Stage에 대한 보다 자세한 내용은
https://msdn.microsoft.com/ko-kr/library/windows/desktop/mt787170(v=vs.85) 에서 확인 할 수 있습니다.
이렇게 가공된 데이터는 Fragment Shader에서 아래와 같이 재가공되어 사용됩니다.
// fs...
half3 tnormal = UnpackNormal(tex2D(_NormalMap,i.uv*_NormalMap_ST.xy));
half3 tnormal_decal = UnpackNormal(tex2D(_DispNormal,i.detailuv));
half3 tnormal_sum = normalize(tnormal + tnormal_decal); //add two normal together
half3 worldNormal;
worldNormal.x = dot(i.tspace0,tnormal_sum);
worldNormal.y = dot(i.tspace1,tnormal_sum);
worldNormal.z = dot(i.tspace2,tnormal_sum);
o.outNormal = half4(worldNormal,0);
이후 기존 03. Wave Deformer에서 사용 됐던 UnityGI Setup 코드를 활용해 normal과 position을 적용함으로써 모든 Shader 작업을 마쳤습니다.
또한 위 첨부파일을 통해 제가 포스팅에 기제하지 못 한 코드와 주석을 확인 하실 수 있습니다.
얼떨결에 크리스마스에 맞게 눈이라는 주제로 Snow Footprint 분석을 마치게 되네요.
처음으로 공부를 하면서 블로그에 정리를 해봤는데, 생각보다 시간은 오래 걸렸지만 머릿속에 있던 개념을 정리하는데 큰 도움이 된 것 같아 개인적으론 꽤나 기쁩니다만 제 부족한 글솜씨를 확실히 깨닫게 되서 슬프기도 했습니다...ㅠ
포스팅 과정에서 얻은 지식을 다시 한 번 정리해보면
- 비선형 변환에서의 normal을 계산하는 법
- Tessellator의 개념과 활용
- 위치 정보를 저장하고 활용하는 새로운 방법
- tex2Dlod 함수의 존재
- 선형 변환으로 수정된 오브젝트 좌표를 uv 좌표계로 바꾸는 법
- Deformed 된 normal과 기존 normal map을 같이 적용하는 법
- UnityGI를 소스코드로 다루는 방법 등..
위와 같은 정보를 다뤘던게 기억나네요.
제가 분석한 내용 중 틀린 내용이나, 추가적으로 알려주실 수 있는 정보가 있으시다면 코멘트를 활용해 알려주시면 최대한 빨리 내용을 수정하겠습니다.
'개발 > Shader' 카테고리의 다른 글
[Image Effect] 유니티 블러(Blur) 이미지 효과- 02 Trail Renderer 모션벡터 획득 (0) | 2021.01.04 |
---|---|
[Image Effect] 유니티 블러(Blur) 이미지 효과- 01 기본 셰이더 (0) | 2021.01.03 |
[Snow Footprint] 03.Wave Deformer에 Tessellation 적용 (0) | 2017.12.21 |
[Snow Footprint] 02.Tessellator - 02 (Tessellator and Domain Stage) (0) | 2017.12.16 |
[Snow Footprint] 02.Tessellator - 01 (Hull-Shader) (0) | 2017.12.16 |
소중한 공감 감사합니다