Microsoft MVP성태의 닷넷 이야기
Graphics: 24. Unity - unity_CameraWorldClipPlanes 내장 변수 의미 [링크 복사], [링크+제목 복사],
조회: 19348
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

Unity - unity_CameraWorldClipPlanes 내장 변수 의미

Unity 도움말 문서를 보면, 내장 변수에서 left, right, bottom, top, near, far에 관한 정보를 담고 있는 unity_CameraWorldClipPlanes가 있습니다.

변수: unity_CameraWorldClipPlanes[6]
형식: float4
설명: Camera frustum plane world space equations, in this order: left, right, bottom, top, near, far.

그런데, 이 값이 C# 스크립트에서 구할 수 있는 Projection Matrix의 decomposeProjection 값은 아닙니다. 다시 도움말 문서를 자세히 보면, 이 값들은 "Camera frustum plane world space equations"라고 해서 뷰 프러스텀(View frustum)의 각 면들을 나타내는 평면 방정식(참고: http://wonjayk.tistory.com/189?category=535169)인 것입니다.

그럼 어떻게 값이 채워져 있는지 확인해 볼까요? ^^

가령, C# 스크립트에서 투영 행렬의 decomposeProjection을 shader에 전달해,

void Update () {
    Camera camera = Camera.main;

    Shader.SetGlobalFloat("_top1", camera.projectionMatrix.decomposeProjection.top);
    Shader.SetGlobalFloat("_bottom1", camera.projectionMatrix.decomposeProjection.bottom);
    Shader.SetGlobalFloat("_left1", camera.projectionMatrix.decomposeProjection.left);
    Shader.SetGlobalFloat("_right1", camera.projectionMatrix.decomposeProjection.right);
    Shader.SetGlobalFloat("_near1", camera.projectionMatrix.decomposeProjection.zNear);
    Shader.SetGlobalFloat("_far1", camera.projectionMatrix.decomposeProjection.zFar);

    Matrix4x4 projection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);

    Shader.SetGlobalFloat("_top2", projection.decomposeProjection.top);
    Shader.SetGlobalFloat("_bottom2", projection.decomposeProjection.bottom);
    Shader.SetGlobalFloat("_left2", projection.decomposeProjection.left);
    Shader.SetGlobalFloat("_right2", projection.decomposeProjection.right);
    Shader.SetGlobalFloat("_near2", projection.decomposeProjection.zNear);
    Shader.SetGlobalFloat("_far2", projection.decomposeProjection.zFar)
}

"Visual Studio Graphics Analyzer" 디버깅 환경으로 보면 다음과 같은 결과가 나오고,

[초기 Scene 화면의 기본값]
FOV = 60
Near = 0.3
Far = 1000
카메라 위치 = (0, 1, -10)

[camera.projectionMatrix.decomposeProjection]
_top1   0.173205100 
_bottom1 -0.173205100 
_left1  -0.308696200
_right1  0.308696200
_near1  0.300000000 
_far1   1000.134000000  

[GL.GetGPUProjectionMatrix(...).decomposeProjection]
_top2   0.173309100 
_bottom2 -0.173309100 
_left2  0.308881500 
_right2 -0.308881500
_near2  -0.300180100
_far2   0.300000000 

이때의 unity_CameraWorldClipPlanes 값은 이렇게 출력됩니다.

unity_CameraWorldClipPlanes[0]  x = 0.696933500, y = 0.000000000, z = 0.717135700, w = 7.171357000  == left
unity_CameraWorldClipPlanes[1]  x = -0.696933500, y = 0.000000000, z = 0.717135700, w = 7.171357000 == right
unity_CameraWorldClipPlanes[2]  x = 0.000000000, y = -0.866025400, z = 0.500000000, w = 5.866025000 == bottom
unity_CameraWorldClipPlanes[3]  x = 0.000000000, y = 0.866025400, z = 0.500000000, w = 4.133975000  == top
unity_CameraWorldClipPlanes[4]  x = 0.000000000, y = 0.000000000, z = 1.000000000, w = 10.300180000 == near
unity_CameraWorldClipPlanes[5]  x = 0.000000000, y = 0.000000000, z = 1.000000000, w = 9.700000000  == far

camera.projectionMatrix.decomposeProjection의 값을 담고 있는 _top1, _bottom1, _left1, _right1, _near1, _far1 멤버는 월드 좌표계의 값이므로 이를 unity_CameraWorldClipPlanes 평면 방정식에 대입하면 (당연히 평면 위에 있는 점이므로) 0이 나와야 합니다.

가령 _top1 좌표는 (0.0, 0.173205100, 0.0) 값이 되는데, 근 평면이 카메라의 앞에 위치하므로 실제 _top1의 좌표는 카메라의 y 축 방향과 z 축 방향의 영향을 받아 월드 좌표계 상에서 (0, 1.173205100, -9.7) 위치에 해당합니다. _bottom1 좌표 역시 (0.0, -0.173205100, 0.0) 값인데, 카메라의 위치로 인해 y-축으로 +1, z-축으로 (-10 + 0.3)의 영향을 받아 (0.0 0.8267949 -9.7 1.0) 위치가 됩니다. 그리고 이것을 각각 unity_CameraWorldClipPlanes의 평면 방정식에 대입하면 결과가 0이 나와야 합니다. 이를 octave를 이용해 계산해 보면,

format long g
more off

ucPlanes = [ 0.696933500 0.0 0.71713570, 7.1713570;
            -0.69693350, 0.0, 0.71713570, 7.1713570;
            0.0 -0.86602540 0.5 5.8660250;
            0.0 0.86602540 0.5 4.1339750;
            0.0 0.0 1.0 10.300180;
            0.0 0.0 1.0 9.700000]

topPlane = ucPlanes(3,[1: 4]);
top = [0.0 1.173205100 -9.7 1.0];
printf("topPlane * top' == %.6f\n", topPlane * top')

bottomPlane = ucPlanes(4,[1: 4]);
bottom = [0.0 0.8267949 -9.7 1.0];
printf("bottomPlane * bottom' == %.6f\n", bottomPlane * bottom')

/*
출력 결과:

topPlane * top' == -0.000000
bottomPlane * bottom' == 0.000000
*/

이렇게 0.0이 나옵니다. (정밀도를 높이면 0.0에 가깝긴 해도 소수점 7자리부터 유효 숫자가 나오긴 합니다.)

마찬가지로 left와 right에 대해서도,

left = (-0.308696200, 0.0, .0.0)
     [카메라 위치 반영] => (-0.3086962 0 -9.7)

right = (0.308696200, 0.0, .0.0)
     [카메라 위치 반영] => (0.3086962 0 -9.7)

계산해 보면,

left = [-0.3086962 0 -9.7 1.0]
leftPlane = ucPlanes(1,[1:4]);
printf("leftPlane * left' == %.6f\n", leftPlane * left')

rightPlane = ucPlanes(2,[1: 4]);
right = [0.30869620 0.0 -9.7 1.0];
printf("rightPlane * right' == %.6f\n", rightPlane * right')

/*
출력 결과:

leftPlane * left' == -0.000000
rightPlane * right' == -0.000000
*/

의도한 값이 나옵니다.




그런데, 문제는 near와 far입니다.

unity_CameraWorldClipPlanes[4]  x = 0.000000000, y = 0.000000000, z = 1.000000000, w = 10.300180000 == near
unity_CameraWorldClipPlanes[5]  x = 0.000000000, y = 0.000000000, z = 1.000000000, w = 9.700000000  == far

단적으로 far의 월드 좌표계 위치가 (0.0, 1.0, 990)이 될 텐데 "z + 9.7 = 0"의 평면 방정식에 대입해 봐야 절대 0이 나올 수 없습니다. 그렇다면 아마도 저건 평면 방정식이라기보다는 다른 의미를 나타내는 것 같은데 딱히 그에 대한 도움말을 찾을 수가 없습니다.

unity_CameraWorldClipPlanes을 사용하는 shader 코드를 검색해 봤는데,

VoxelGame / Assets / Shaders / CGIncludes / Tessellation.cginc 
; https://github.com/joetex/VoxelGame/blob/master/Assets/Shaders/CGIncludes/Tessellation.cginc

float UnityDistanceFromPlane (float3 pos, float4 plane)
{
    float d = dot (float4(pos,1.0f), plane);
    return d;
}

bool UnityWorldViewFrustumCull (float3 wpos0, float3 wpos1, float3 wpos2, float cullEps)
{
    float4 planeTest;
    
    // left
    planeTest.x = (( UnityDistanceFromPlane(wpos0, unity_CameraWorldClipPlanes[0]) > -cullEps) ? 1.0f : 0.0f ) +
                  (( UnityDistanceFromPlane(wpos1, unity_CameraWorldClipPlanes[0]) > -cullEps) ? 1.0f : 0.0f ) +
                  (( UnityDistanceFromPlane(wpos2, unity_CameraWorldClipPlanes[0]) > -cullEps) ? 1.0f : 0.0f );

    ...[생략]...
        
    // has to pass all 4 plane tests to be visible
    return !all (planeTest);
}

float4 UnityEdgeLengthBasedTessCull (float4 v0, float4 v1, float4 v2, float edgeLength, float maxDisplacement)
{
    float3 pos0 = mul(_Object2World,v0).xyz;
    float3 pos1 = mul(_Object2World,v1).xyz;
    float3 pos2 = mul(_Object2World,v2).xyz;
    float4 tess;

    if (UnityWorldViewFrustumCull(pos0, pos1, pos2, maxDisplacement))
    ...[생략]...
}

보는 바와 같이 _Object2World로 변환한 3개의 vertex를 절두체 평면 중 left, right, top, bottom까지만 비교하고 있습니다. 즉, unity_CameraWorldClipPlanes[4]와 unity_CameraWorldClipPlanes[5]에 대한 계산은 하지 않고 있는 것입니다.

한 가지 재미있는 점은, camera.projectionMatrix와 GL.GetGPUProjectionMatrix로부터 넘겨받은 decomposeProjection의 값에서,

[camera.projectionMatrix.decomposeProjection]
_top1   0.173205100 
_bottom1 -0.173205100 
_left1  -0.308696200
_right1  0.308696200
_near1  0.300000000 
_far1   1000.134000000  

[GL.GetGPUProjectionMatrix(...).decomposeProjection]
_top2   0.173309100 
_bottom2 -0.173309100 
_left2  0.308881500 
_right2 -0.308881500
_near2  -0.300180100
_far2   0.300000000 

_near1, _far1의 값이 _near2와 _far2로 변경된 것을 unity_CameraWorldClipPlanes로도 구할 수 있다는 점입니다. unity_CameraWorldClipPlanes의 near와 far에 해당하는 평면 방정식을 그냥 좌표라고 보고,

near (x = 0.000000000, y = 0.000000000, z = 1.000000000, w = 10.300180000)
far  (x = 0.000000000, y = 0.000000000, z = 1.000000000, w = 9.700000000)

View matrix, 즉 shader 내에서 UNITY_MATRIX_V(unity_MatrixV)와 곱하면 (부호가 반대지만) _near2와 _far2의 값을 구할 수 있습니다.

[이때의 view matrix]

matV = [1.0 0.0  0.0   0.0;
        0.0 1.0  0.0  -1.0;
        0.0 0.0 -1.0 -10.0;
        0.0 0.0  0.0   1.0];

ucPlanes(5, [1:4]) * matV(:,4) == 0.300180000
ucPlanes(6, [1:4]) * matV(:,4) == -0.300000000

혹시 ^^ unity_CameraWorldClipPlanes[4], unity_CameraWorldClipPlanes[5]에 대한 정체를 아시는 분은 덧글 부탁드립니다.




참고로 Unity 도움말에 보면 C# 스크립트에서도 절두체를 구할 수 있는 메서드가 있습니다.

GeometryUtility.CalculateFrustumPlanes
; https://docs.unity3d.com/ScriptReference/GeometryUtility.CalculateFrustumPlanes.html

이 메서드가 반환하는 값은 unity_CameraWorldClipPlanes 값과는 다르게 Unity 월드에서 바로 사용할 수 있는 Plane 3D 객체입니다. 그래서 도움말의 예제 코드를 보면 이를 이용해 평면을 보이도록 하는 코드가 있습니다.

void Start () {
    Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);
    for (int i = 0; i < 6; ++i)
    {
        GameObject p = GameObject.CreatePrimitive(PrimitiveType.Plane);
        p.name = "Plane " + i.ToString();
        p.transform.position = -planes[i].normal * planes[i].distance;
        p.transform.rotation = Quaternion.FromToRotation(Vector3.up, planes[i].normal);
    } 
}




마지막으로 shader에서 디버깅할 때 변숫값을 알아내기 편하도록 하는 팁을 하나 적겠습니다. ^^ 가령, 이번 예제에서처럼 C# 스크립트에서 shader에 값을 전달할 때 다음과 같이만 처리하면,

float _top1;
float _bottom1;
float _left1;
float _right1;
float _near1;
float _far1;
            
float _top2;
float _bottom2;
float _left2;
float _right2;
float _near2;
float _far2;
            
v2f vert (appdata v)
{
    float4 pos;

    v2f o;

    pos = mul(unity_ObjectToWorld, v.vertex);
    pos = mul(UNITY_MATRIX_V, pos);
    pos = mul(UNITY_MATRIX_P, pos);

    o.vertex = pos;
    return o;
}

shader 빌드 과정에서 모두 사라져 버려 디버깅 단계에서는 _top1, ... , _far2의 값들을 전혀 볼 수 없습니다. 그나마 변수를 사용하겠다고 해서 다음과 같은 식으로만 값을 추출한 후,

float n = _ProjectionParams.y;

n을 사용하는 코드가, 다시 말해 실질적으로 vertex shader의 수행 결과에 반영되지 않는 값이면 shader 빌드 과정 중에 저 값들도 모두 없애 버리므로 역시 디버깅 중에 값을 확인할 수 없습니다. 그래서 제 경우에 다음과 같은 식으로 vertex output 인자에 임의 변수를 추가하고 그 값의 출력에 기여를 하도록 일부러 코드를 넣어 둡니다.

struct v2f
{
    float4 vertex : SV_POSITION;
    float p : TEXCOORD0;
    float temp : TEXCOORD1;
};

float _top1;
float _bottom1;
float _left1;
float _right1;
float _near1;
float _far1;
            
float _top2;
float _bottom2;
float _left2;
float _right2;
float _near2;
float _far2;

float vectorTest(float m1, float4 m2)
{
    if (m1 > m2.x || m1 > m2.y || m1 > m2.z || m1 > m2.w)
    {
        return m1;
    }

    return m2.x;
}

float floatTest(float m1, float m2)
{
    if (m1 > m2)
    {
        return m1;
    }
                
    return m2;
}
            
v2f vert (appdata v)
{
    float4 pos;

    v2f o;

    o.p = floatTest(o.p, _top1);
    o.p = floatTest(o.p, _bottom1);
    o.p = floatTest(o.p, _left1);
    o.p = floatTest(o.p, _right1);
    o.p = floatTest(o.p, _near1);
    o.p = floatTest(o.p, _far1);

    o.p = floatTest(o.p, _top2);
    o.p = floatTest(o.p, _bottom2);
    o.p = floatTest(o.p, _left2);
    o.p = floatTest(o.p, _right2);
    o.p = floatTest(o.p, _near2);
    o.p = floatTest(o.p, _far2);

    o.p = vectorTest(o.p, unity_CameraWorldClipPlanes[0]);
    o.p = vectorTest(o.p, unity_CameraWorldClipPlanes[1]);
    o.p = vectorTest(o.p, unity_CameraWorldClipPlanes[2]);
    o.p = vectorTest(o.p, unity_CameraWorldClipPlanes[3]);
    o.p = vectorTest(o.p, unity_CameraWorldClipPlanes[4]);
    o.p = vectorTest(o.p, unity_CameraWorldClipPlanes[5]);

    pos = mul(unity_ObjectToWorld, v.vertex);
    pos = mul(UNITY_MATRIX_V, pos);
    pos = mul(UNITY_MATRIX_P, pos);

    o.vertex = pos;
    o.temp = unity_CameraWorldClipPlanes[1].x;
    return o;
}

이런 식으로 해주면 디버깅 중에 다음과 같이 "Locals" 창에서 값을 잘 확인할 수 있습니다.

shader_debug_helper_code.png

그렇습니다, shader의 값을 눈으로 확인하는 것은 굉장히 번거로운 절차를 요구하지만 그래도 공부하는 용도로는 딱입니다. ^^





[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 9/20/2018]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1780정성태10/15/201424182오류 유형: 249. The application-specific permission settings do not grant Local Activation permission for the COM Server application with CLSID
1779정성태10/15/201419716오류 유형: 248. Active Directory에서 OU가 지워지지 않는 경우
1778정성태10/10/201418161오류 유형: 247. The Netlogon service could not create server share C:\Windows\SYSVOL\sysvol\[도메인명]\SCRIPTS.
1777정성태10/10/201421278오류 유형: 246. The processing of Group Policy failed. Windows attempted to read the file \\[도메인]\sysvol\[도메인]\Policies\{...GUID...}\gpt.ini
1776정성태10/10/201418314오류 유형: 245. 이벤트 로그 - Name resolution for the name _ldap._tcp.dc._msdcs.[도메인명]. timed out after none of the configured DNS servers responded.
1775정성태10/9/201419433오류 유형: 244. Visual Studio 디버깅 (2) - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
1774정성태10/9/201426632개발 환경 구성: 246. IIS 작업자 프로세스의 20분 자동 재생(Recycle)을 끄는 방법
1773정성태10/8/201429783.NET Framework: 471. 웹 브라우저로 다운로드가 되는 파일을 왜 C# 코드로 하면 안되는 걸까요? [1]
1772정성태10/3/201418571.NET Framework: 470. C# 3.0의 기본 인자(default parameter)가 .NET 1.1/2.0에서도 실행될까? [3]
1771정성태10/2/201428086개발 환경 구성: 245. 실행된 프로세스(EXE)의 명령행 인자를 확인하고 싶다면 - Sysmon [4]
1770정성태10/2/201421693개발 환경 구성: 244. 매크로 정의를 이용해 파일 하나로 C++과 C#에서 공유하는 방법 [1]파일 다운로드1
1769정성태10/1/201424117개발 환경 구성: 243. Scala 개발 환경 구성(JVM, 닷넷) [1]
1768정성태10/1/201419541개발 환경 구성: 242. 배치 파일에서 Thread.Sleep 효과를 주는 방법 [5]
1767정성태10/1/201424634VS.NET IDE: 94. Visual Studio 2012/2013에서의 매크로 구현 - Visual Commander [2]
1766정성태10/1/201422493개발 환경 구성: 241. 책 "프로그래밍 클로저: Lisp"을 읽고 나서. [1]
1765정성태9/30/201426057.NET Framework: 469. Unity3d에서 transform을 변수에 할당해 사용하는 특별한 이유가 있을까요?
1764정성태9/30/201422294오류 유형: 243. 파일 삭제가 안 되는 경우 - The action can't be comleted because the file is open in System
1763정성태9/30/201423865.NET Framework: 468. PDB 파일을 연동해 소스 코드 라인 정보를 알아내는 방법파일 다운로드1
1762정성태9/30/201424552.NET Framework: 467. 닷넷에서 EIP/RIP 레지스터 값을 구하는 방법 [1]파일 다운로드1
1761정성태9/29/201421581.NET Framework: 466. 윈도우 운영체제의 보안 그룹 이름 및 설명 문자열을 바꾸는 방법파일 다운로드1
1760정성태9/28/201419854.NET Framework: 465. ICorProfilerInfo::GetILToNativeMapping 메서드가 0x80131358을 반환하는 경우
1759정성태9/27/201430980개발 환경 구성: 240. Visual C++ / x64 환경에서 inline-assembly를 매크로 어셈블리로 대체하는 방법파일 다운로드1
1758정성태9/23/201437886개발 환경 구성: 239. 원격 데스크톱 접속(RDP)을 기존의 콘솔 모드처럼 사용하는 방법 [1]
1757정성태9/23/201418416오류 유형: 242. Lync로 모임 참여 시 소리만 들리지 않는 경우 - 두 번째 이야기
1756정성태9/23/201427432기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434209오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...