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

비밀번호

댓글 작성자
 




... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13022정성태3/31/20226533Windows: 202. 윈도우 11 업그레이드 - "PC Health Check"를 통과했지만 여전히 업그레이드가 안 되는 경우 해결책
13021정성태3/31/20226687Windows: 201. Windows - INF 파일을 이용한 장치 제거 방법
13020정성태3/30/20226428.NET Framework: 1187. RDP 접속 시 WPF UserControl의 Unloaded 이벤트 발생파일 다운로드1
13019정성태3/30/20226426.NET Framework: 1186. Win32 Message를 Code로부터 메시지 이름 자체를 구하고 싶다면?파일 다운로드1
13018정성태3/29/20226951.NET Framework: 1185. C# - Unsafe.AsPointer가 반환한 포인터는 pinning 상태일까요? [5]
13017정성태3/28/20226793.NET Framework: 1184. C# - GC Heap에 위치한 참조 개체의 주소를 알아내는 방법 - 두 번째 이야기 [3]
13016정성태3/27/20227642.NET Framework: 1183. C# 11에 추가된 ref 필드의 (우회) 구현 방법파일 다운로드1
13015정성태3/26/20228982.NET Framework: 1182. C# 11 - ref struct에 ref 필드를 허용 [1]
13014정성태3/23/20227592VC++: 155. CComPtr/CComQIPtr과 Conformance mode 옵션의 충돌 [1]
13013정성태3/22/20225915개발 환경 구성: 641. WSL 우분투 인스턴스에 파이썬 2.7 개발 환경 구성하는 방법
13012정성태3/21/20225276오류 유형: 803. C# - Local '...' or its members cannot have their address taken and be used inside an anonymous method or lambda expression
13011정성태3/21/20226753오류 유형: 802. 윈도우 운영체제에서 웹캠 카메라 인식이 안 되는 경우
13010정성태3/21/20225685오류 유형: 801. Oracle.ManagedDataAccess.Core - GetTypes 호출 시 "Could not load file or assembly 'System.DirectoryServices.Protocols...'" 오류
13009정성태3/20/20227234개발 환경 구성: 640. docker - ibmcom/db2 컨테이너 실행
13008정성태3/19/20226526VS.NET IDE: 176. 비주얼 스튜디오 - 솔루션 탐색기에서 프로젝트를 선택할 때 csproj 파일이 열리지 않도록 만드는 방법
13007정성태3/18/20226153.NET Framework: 1181. C# - Oracle.ManagedDataAccess의 Pool 및 그것의 연결 개체 수를 알아내는 방법파일 다운로드1
13006정성태3/17/20227187.NET Framework: 1180. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 remuxing.c 예제 포팅
13005정성태3/17/20226065오류 유형: 800. C# - System.InvalidOperationException: Late bound operations cannot be performed on fields with types for which Type.ContainsGenericParameters is true.
13004정성태3/16/20226090디버깅 기술: 182. windbg - 닷넷 메모리 덤프에서 AppDomain에 걸친 정적(static) 필드 값을 조사하는 방법
13003정성태3/15/20226235.NET Framework: 1179. C# - (.NET Framework를 위한) Oracle.ManagedDataAccess 패키지의 성능 카운터 설정 방법
13002정성태3/14/20226985.NET Framework: 1178. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 http_multiclient.c 예제 포팅
13001정성태3/13/20227393.NET Framework: 1177. C# - 닷넷에서 허용하는 메서드의 매개변수와 호출 인자의 최대 수
13000정성태3/12/20226956.NET Framework: 1176. C# - Oracle.ManagedDataAccess.Core의 성능 카운터 설정 방법
12999정성태3/10/20226446.NET Framework: 1175. Visual Studio - 프로젝트 또는 솔루션의 Clean 작업 시 응용 프로그램에서 생성한 파일을 함께 삭제파일 다운로드1
12998정성태3/10/20226062.NET Framework: 1174. C# - ELEMENT_TYPE_FNPTR 유형의 사용 예
12997정성태3/10/202210445오류 유형: 799. Oracle.ManagedDataAccess - "ORA-01882: timezone region not found" 오류가 발생하는 이유
... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...