Microsoft MVP성태의 닷넷 이야기
Graphics: 24. Unity - unity_CameraWorldClipPlanes 내장 변수 의미 [링크 복사], [링크+제목 복사],
조회: 19381
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 151  152  153  154  155  156  [157]  158  159  160  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1124정성태9/17/201126420.NET Framework: 240. System.Collections.ArrayList가 .NET 4.5에서 지원이 안된다??? [2]
1123정성태9/17/201165266Windows: 53. 2가지 모드의 Internet Explorer 10과 ActiveX [6]
1122정성태9/16/201132929Windows: 52. 새롭게 지원되는 WinRT 응용 프로그램 [7]
1121정성태9/12/201127745Java: 5. WTP 내에서 서블릿을 실행하는 환경
1120정성태9/11/201127665.NET Framework: 239. IHttpHandler.IsReusable 속성 이야기파일 다운로드1
1119정성태9/11/201126758Java: 4. 이클립스에 WTP SDK가 설치되지 않는다면? [2]
1118정성태9/11/201138418Java: 3. 이클립스에서 서블릿 디버깅하는 방법 [4]
1117정성태9/9/201125670제니퍼 .NET: 17. 제니퍼 닷넷 적용 사례 (2) - 웹 애플리케이션 hang의 원인을 알려주다.
1116정성태9/8/201156725Java: 2. 자바에서 "Microsoft SQL Server JDBC Driver" 사용하는 방법
1115정성태9/4/201130271Java: 1. 닷넷 개발자가 처음 실습해 본 서블릿
1114정성태9/4/201134752Math: 2. "Zhang Suen 알고리즘(세선화, Thinning/Skeletonization)"의 C# 버전 [4]파일 다운로드1
1113정성태9/2/201134354개발 환경 구성: 129. Hyper-V에 CentOS 설치하기
1112정성태9/2/201151098Linux: 1. 리눅스 <-> 윈도우 원격 접속 프로그램 사용 [3]
1111정성태8/29/201125463제니퍼 .NET: 16. 적용 사례 (1) - DB Connection Pooling을 사용하지 않았을 때의 성능 저하를 알려주다. [1]
1110정성태8/26/201126850오류 유형: 136. RDP 접속이 불연속적으로 끊기는 문제
1109정성태8/26/201129722오류 유형: 135. 어느 순간 Active Directory 접속이 안되는 문제
1108정성태8/22/201131177오류 유형: 134. OLE/COM Object Viewer - DllRegisterServer in IVIEWERS.DLL failed. [1]
1107정성태8/21/201129021디버깅 기술: 43. Windows Form의 Load 이벤트에서 발생하는 예외가 Visual Studio에서 잡히지 않는 문제
1106정성태8/20/201127302웹: 26. FailedRequestTracing 설정으로 인한 iisexpress.exe 비정상 종료 문제
1105정성태8/19/201127245.NET Framework: 238. Web Site Model 프로젝트에서 Trace.WriteLine 출력이 dbgview.exe에서 확인이 안 되는 문제파일 다운로드1
1104정성태8/19/201127460웹: 25. WebDev보다 IIS Express가 더 나은 점 - 다중 가상 디렉터리 매핑 [1]
1103정성태8/19/201133361오류 유형: 133. WCF 포트 바인딩 실패 오류 - TCP error(10013) [1]
1102정성태8/19/201131057Math: 1. 방탈출3 - Room 10의 '중복가능한 조합' 문제를 위한 C# 프로그래밍 [2]파일 다운로드1
1101정성태8/19/201129705.NET Framework: 237. WCF AJAX 서비스와 JavaScript 간의 DateTime 연동 [1]파일 다운로드1
1100정성태8/17/201128821.NET Framework: 236. SqlDbType - DateTime, DateTime2, DateTimeOffset의 차이점파일 다운로드1
1099정성태8/15/201128261오류 유형: 132. 어느 순간 갑자기 접속이 안 되는 TFS 서버
... 151  152  153  154  155  156  [157]  158  159  160  161  162  163  164  165  ...