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

비밀번호

댓글 작성자
 




... [91]  92  93  94  95  96  97  98  99  100  101  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11363정성태11/23/201715304사물인터넷: 10. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스 + 키보드로 쓰는 방법 (두 번째 이야기)
11362정성태11/22/201713006오류 유형: 428. 윈도우 업데이트 KB4048953 - 0x800705b4 [2]
11361정성태11/22/201715543오류 유형: 427. 이벤트 로그 - Filter Manager failed to attach to volume '\Device\HarddiskVolume??' 0xC03A001C
11360정성태11/22/201715354오류 유형: 426. 이벤트 로그 - The kernel power manager has initiated a shutdown transition.
11359정성태11/16/201714722오류 유형: 425. 윈도우 10 Version 1709 (OS Build 16299.64) 업그레이드 시 발생한 문제 2가지
11358정성태11/15/201718982사물인터넷: 9. Visual Studio 2017에서 Raspberry Pi C++ 응용 프로그램 제작 [1]
11357정성태11/15/201719791개발 환경 구성: 336. 윈도우 10 Bash 쉘에서 C++ 컴파일하는 방법
11356정성태11/15/201721153사물인터넷: 8. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스 + 키보드로 쓰는 방법 [4]
11355정성태11/15/201717645사물인터넷: 7. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스로 쓰는 방법 [2]파일 다운로드2
11354정성태11/14/201720928사물인터넷: 6. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 키보드로 쓰는 방법 [8]
11353정성태11/14/201718700사물인터넷: 5. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 이더넷 카드로 쓰는 방법 [1]
11352정성태11/14/201714285사물인터넷: 4. Samba를 이용해 윈도우와 Raspberry Pi간의 파일 교환 [1]
11351정성태11/7/201717266.NET Framework: 698. C# 컴파일러 대신 직접 구현하는 비동기(async/await) 코드 [6]파일 다운로드1
11350정성태11/1/201713580디버깅 기술: 108. windbg 분석 사례 - Redis 서버로의 호출을 기다리면서 hang 현상 발생
11349정성태10/31/201713496디버깅 기술: 107. windbg - x64 SOS 확장의 !clrstack 명령어가 출력하는 Child SP 값의 의미 [1]파일 다운로드1
11348정성태10/31/201710909디버깅 기술: 106. windbg - x64 역어셈블 코드에서 닷넷 메서드 호출의 인자를 확인하는 방법
11347정성태10/28/201714412오류 유형: 424. Visual Studio - "클래스 다이어그램 보기" 시 "작업을 완료할 수 없습니다. 해당 인터페이스를 지원하지 않습니다." 오류 발생
11346정성태10/25/201710742오류 유형: 423. Windows Server 2003 - The client-side extension could not remove user policy settings for 'Default Domain Policy {...}' (0x8007000d)
11338정성태10/25/201710807.NET Framework: 697. windbg - SOS DumpMT의 "BaseSize", "ComponentSize" 값에 대한 의미파일 다운로드1
11337정성태10/24/201711775.NET Framework: 696. windbg - SOS DumpClass/DumpMT의 "Vtable Slots", "Total Method Slots", "Slots in VTable" 값에 대한 의미파일 다운로드1
11336정성태10/20/201712224.NET Framework: 695. windbg - .NET string의 x86/x64 메모리 할당 구조
11335정성태10/18/201711788.NET Framework: 694. 닷넷 - <Module> 클래스의 용도
11334정성태10/18/201712933디버깅 기술: 105. windbg - k 명령어와 !clrstack을 조합한 호출 스택을 얻는 방법
11333정성태10/17/201712346오류 유형: 422. 윈도우 업데이트 - Code 9C48 Windows update encountered an unknown error.
11332정성태10/17/201712994디버깅 기술: 104. .NET Profiler + 디버거 연결 + .NET Exceptions = cpu high
11331정성태10/16/201711967디버깅 기술: 103. windbg - .NET 4.0 이상의 환경에서 모든 DLL에 대한 심벌 파일을 로드하는 파이썬 스크립트
... [91]  92  93  94  95  96  97  98  99  100  101  102  103  104  105  ...