Microsoft MVP성태의 닷넷 이야기
Graphics: 24. Unity - unity_CameraWorldClipPlanes 내장 변수 의미 [링크 복사], [링크+제목 복사],
조회: 19422
글쓴 사람
정성태 (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)
1756정성태9/23/201427486기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434280오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
1754정성태9/22/201424647VC++: 80. 내 컴퓨터에서 C++ AMP 코드가 실행이 될까요? [1]
1753정성태9/22/201420611오류 유형: 240. Lync로 세미나 참여 시 소리만 들리지 않는 경우 [1]
1752정성태9/21/201441071Windows: 100. 윈도우 8 - RDP 연결을 이용해 VNC처럼 사용자 로그온 화면을 공유하는 방법 [5]
1751정성태9/20/201438945.NET Framework: 464. 프로세스 간 통신 시 소켓 필요 없이 간단하게 Pipe를 열어 통신하는 방법 [1]파일 다운로드1
1750정성태9/20/201423832.NET Framework: 463. PInvoke 호출을 이용한 비동기 파일 작업파일 다운로드1
1749정성태9/20/201423732.NET Framework: 462. 커널 객체를 위한 null DACL 생성 방법파일 다운로드1
1748정성태9/19/201425385개발 환경 구성: 238. [Synergy] 여러 컴퓨터에서 키보드, 마우스 공유
1747정성태9/19/201428478오류 유형: 239. psexec 실행 오류 - The system cannot find the file specified.
1746정성태9/18/201426106.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 [6]파일 다운로드1
1745정성태9/17/201423035개발 환경 구성: 237. 리눅스 Integration Services 버전 업그레이드 하는 방법 [1]
1744정성태9/17/201431063.NET Framework: 460. GetTickCount / GetTickCount64와 0x7FFE0000 주솟값 [4]파일 다운로드1
1743정성태9/16/201420985오류 유형: 238. 설치 오류 - Failed to get size of pseudo bundle
1742정성태8/27/201426972개발 환경 구성: 236. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 [2]
1741정성태8/26/201421334.NET Framework: 459. GetModuleHandleEx로 알아보는 .NET 메서드의 DLL 모듈 관계파일 다운로드1
1740정성태8/25/201432508.NET Framework: 458. 닷넷 GC가 순환 참조를 해제할 수 있을까요? [2]파일 다운로드1
1739정성태8/24/201426541.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201422047.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201419765VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201425576VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201418211.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201419873오류 유형: 237. Windows Media Player cannot access the file. The file might be in use, you might not have access to the computer where the file is stored, or your proxy settings might not be correct.
1733정성태8/13/201426362.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201434479Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201427081개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...