Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

(시리즈 글이 6개 있습니다.)
Graphics: 15. Unity - shader의 World matrix(unity_ObjectToWorld)를 수작업으로 구성
; https://www.sysnet.pe.kr/2/0/11633

Graphics: 17. Unity - World matrix(unity_ObjectToWorld)로부터 TRS(이동/회전/크기) 행렬로 복원하는 방법
; https://www.sysnet.pe.kr/2/0/11637

Graphics: 18. Unity - World matrix(unity_ObjectToWorld)로부터 Position, Rotation, Scale 값을 복원하는 방법
; https://www.sysnet.pe.kr/2/0/11640

Graphics: 22. Unity - shader의 Camera matrix(UNITY_MATRIX_V)를 수작업으로 구성
; https://www.sysnet.pe.kr/2/0/11692

Graphics: 23. Unity - shader의 원근 투영(Perspective projection) 행렬(UNITY_MATRIX_P)을 수작업으로 구성
; https://www.sysnet.pe.kr/2/0/11695

Graphics: 25. Unity - shader의 직교 투영(Orthographic projection) 행렬(UNITY_MATRIX_P)을 수작업으로 구성
; https://www.sysnet.pe.kr/2/0/11700




Unity - shader의 Camera matrix(UNITY_MATRIX_V)를 수작업으로 구성

지난 글에서 월드 행렬을 수작업으로 구성해 봤으니,

Unity - shader의 World matrix(unity_ObjectToWorld)를 수작업으로 구성
; https://www.sysnet.pe.kr/2/0/11633

이번에는 View Matrix(Camera Matrix)를 수작업으로 구성해 보겠습니다. 다음의 책을 보면,

유니티로 배우는 게임 수학  기초 개념부터 모바일까지, 게임 개발에 필요한 수학 원리 설명서 
; http://www.yes24.com/24/goods/30119802

View Matrix에 대한 구성 공식을 다음과 같이 소개하고 있습니다.

${
V = RT = \begin{bmatrix} X_x & X_y & X_z & 0 \\ Y_x & Y_y & Y_z & 0 \\ Z_x & Z_y & Z_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & -C_x \\ 0 & 1 & 0 & -C_y \\ 0 & 0 & 1 & -C_z \\ 0 & 0 & 0 & 1 \end{bmatrix}
}$


위의 공식에서 X, Y, Z는 카메라의 회전 값이고 C는 카메라의 위치입니다. (View 행렬의 특성상 Scale 값은 무시합니다. 실제로 Unity의 Inspector 창에서 카메라의 Scale 값을 입력해도 아무런 변화가 없는 것을 볼 수 있습니다.)

예를 들어, 유니티 초기 카메라 좌표가 (0, 1, -10)입니다. 따라서 이대로 월드 좌표계 기준으로 보면 다음과 같은 Position을 갖도록 T(이동) 행렬을 구성할 수 있습니다.

float4x4 posView;

posView[0] = float4(1, 0, 0, -0);
posView[1] = float4(0, 1, 0, -1);
posView[2] = float4(0, 0, 1, -(-10));
posView[3] = float4(0, 0, 0, 1);

이것을 일반화하려면 Unity Shader에서 카메라의 위치를 나타내는 내장 변수인 _WorldSpaceCameraPos를 사용하면 됩니다.

float4x4 posView;

posView[0] = float4(1, 0, 0, -_WorldSpaceCameraPos.x);
posView[1] = float4(0, 1, 0, -_WorldSpaceCameraPos.y);
posView[2] = float4(0, 0, 1, -_WorldSpaceCameraPos.z);
posView[3] = float4(0, 0, 0, 1);

그다음, 카메라의 회전을 다뤄야 하는데요. 이게 좀 복잡합니다. 자세하게 들어가기 전 위의 posView가 정상적인 데이터를 가지고 있는지 다음과 같이 확인해 볼 수 있습니다.

float4x4 rotView;
float4x4 posView;
float4x4 viewMatrix;

posView[0] = float4(1, 0, 0, -_WorldSpaceCameraPos.x);
posView[1] = float4(0, 1, 0, -_WorldSpaceCameraPos.y);
posView[2] = float4(0, 0, 1, -_WorldSpaceCameraPos.z);
posView[3] = float4(0, 0, 0, 1);

float4x4 m = UNITY_MATRIX_V;

rotView[0] = float4(m[0].xyz, 0);
rotView[1] = float4(m[1].xyz, 0);
rotView[2] = float4(m[2].xyz, 0);
rotView[3] = float4(0, 0, 0, 1);

viewMatrix = mul(rotView, posView);

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

V=RT 공식에서 보면 뷰 행렬의 경우 X, Y, Z의 회전 값이 V 행렬에 그대로 반영되기 때문에 위와 같이 UNITY_MATRIX_V로부터 회전 행렬 요소들을 구해올 수 있는 것입니다.




자, 그럼 이제 카메라의 회전 행렬 R을 구성하는 기저 벡터 X, Y, Z를 구해 보겠습니다. 이 방법에 대해서도 "유니티로 배우는 게임 수학 기초 개념부터 모바일까지, 게임 개발에 필요한 수학 원리 설명서" 책에서 잘 설명해 주고 있습니다. 우선 Z 요소의 경우 다음과 같이 공식을 제시하고 있는데,

${
Z = { C - P \over |C - P| }
}$


여기서 C는 카메라의 위치이고 P는 카메라가 바라보는 시선의 끝점이라고 합니다. 그런데, C - P는 점과 점을 뺀 연산이기 때문에 vector입니다. 그리고 그 vector의 의미는 결국 View Direction이 되는 것입니다. 그러고 보니, view direction을 보관하고 있는 unity shader의 내장 변수를 지난 글에서 소개했습니다.

UNITY_MATRIX_IT_MV - Inverse transpose of model * view matrix.

vector viewDir = UNITY_MATRIX_IT_MV[2];

따라서 Z 값은 다음과 같이 구할 수 있습니다.

vector rorZ = normalize(viewDir);

그다음 기저 벡터 X는 위에서 구한 Z 벡터와 카메라의 상단을 향한 "up vector"를 외적해 구할 수 있습니다.

${
X = { U \times Z \over |U \times Z| }
}$


"up vector" 역시 UNITY_MATRIX_IT_MV[1]을 통해 구할 수 있으므로 다음과 같이 계산할 수 있습니다.

vector rorXpt = vector(cross(upvec, rorZ), 0);
float lenghX = length(rorXpt);

vector rorX = (rorXpt / lenghX);

// 또는,

vector rorX = normalize(vector(cross(upvec, rorZ), 0));

외적한 결과는 오른손 좌표계(RHS)인 경우에 해당하므로 왼손 좌표계(LHS)를 따르는 Unity를 위해 결괏값을 음수로 바꿔야 합니다.

vector rorX = -normalize(vector(cross(upvec, rorZ), 0));

X, Z에 대한 기저 벡터를 구했으니 나머지 Y에 대한 기저 벡터는 X, Z를 외적해서 구할 수 있습니다.

${
Y = { Z \times X \over |Z \times X| }
}$


따라서 shader에서는 다음과 같이 구할 수 있습니다. (마찬가지로 왼손 좌표계를 따르므로 외적의 결과에 음수 처리합니다.)

vector rorY = -normalize(vector(cross(rorZ, rorX), 0));

지금까지의 모든 결과를 취합하면 다음과 같이 수작업으로 구성한 View 행렬을 shader에서 사용할 수 있습니다.

Shader "Unlit/NewUnlitShader"
{
    Properties
    {
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            float4x4 _viewMatrix;
            float4x4 _projectionMatrix;

            v2f vert(appdata v)
            {
                float4 pos;

                v2f o;

                float4x4 rotView;
                float4x4 posView;
                float4x4 viewMatrix;

                posView[0] = float4(1, 0, 0, -_WorldSpaceCameraPos.x);
                posView[1] = float4(0, 1, 0, -_WorldSpaceCameraPos.y);
                posView[2] = float4(0, 0, 1, -_WorldSpaceCameraPos.z);
                posView[3] = float4(0, 0, 0, 1);

                vector upvec = UNITY_MATRIX_IT_MV[1];
                vector viewDir = UNITY_MATRIX_IT_MV[2];

                vector rorZ = normalize(viewDir);
                vector rorX = -normalize(vector(cross(upvec, rorZ), 0));
                vector rorY = -normalize(vector(cross(rorZ, rorX), 0));

                rotView[0] = rorX;
                rotView[1] = rorY;
                rotView[2] = rorZ;
                rotView[3] = float4(0, 0, 0, 1);

                viewMatrix = mul(rotView, posView);

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

                o.vertex = pos;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                 return fixed4(1, 0, 0, 1);
            }

            ENDCG
        }
    }
}

위에서 어렵게 X, Y, Z 기저 벡터를 계산해서 구했지만 결국 처음으로 돌아가서 다음의 코드와 다를 바가 없습니다.

vector rorX = vector(UNITY_MATRIX_V._m00_m01_m02, 0);
vector rorY = vector(UNITY_MATRIX_V._m10_m11_m12, 0);
vector rorZ = vector(UNITY_MATRIX_V._m20_m21_m22, 0);




다음의 글에도 나오지만,

Advanced info on Unity3D's camera matrix
; https://stackoverflow.com/questions/24165915/advanced-info-on-unity3ds-camera-matrix

Model matrix. In scripts: Transform.localToWorldMatrix. In vertex shaders: _Object2World.
View matrix. In scripts: Camera.worldToCameraMatrix. In vertex shaders: UNITY_MATRIX_V.
Projection matrix. In scripts: Camera.projectionMatrix. In vertex shaders: UNITY_MATRIX_P.


UNITY_MATRIX_V나 UNITY_MATRIX_P 행렬은 C# 스크립트에서 Camera.worldToCameraMatrix, Camera.projectionMatrix로 각각 대응한다고 합니다. 따라서 이 값을 shader에 전달해 연산하면 이전의 결과와 동일한 동작을 얻게 됩니다.

실제로 해볼까요? ^^ 다음과 같이 C# 스크립트를 구성하고,

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class SetMatrix : MonoBehaviour {

    void Start () {
    }

    void Update () {
        Camera camera = Camera.main;
        Shader.SetGlobalMatrix("_viewMatrix", camera.worldToCameraMatrix);
    }
}

전달한 _viewMatrix를 이용해 vertex shader를 구성하면,

Shader "Unlit/NewUnlitShader"
{
    Properties
    {
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            float4x4 _viewMatrix;

            v2f vert(appdata v)
            {
                float4 pos;
                v2f o;

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

                o.vertex = pos;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                 return fixed4(1, 0, 0, 1);
            }

            ENDCG
        }
    }
}

Scene 편집 화면에 다음과 같이 출력됩니다.

camera_matrix_1.png

보는 바와 같이 물체의 그려진 위치가 원래 있던 곳에서 (높은 확률로) 벗어나 있습니다. 게다가 Scene 편집 모드 상태에서는 카메라를 돌려도 언제나 같은 자리에 위치하게 됩니다. 이로 인해 자칫 잘못되었다고 생각할 수 있는데요, 아닙니다. ^^ 정상적으로 동작하고 있는 것입니다. 실제로 실행해 Game 뷰로 보면 잘 나오는 것을 확인할 수 있습니다.

Scene 편집 화면에서의 저런 동작은 C# 스크립트가 편집 모드에서 실행될 때 Update 메서드 내에서의 camera.worldToCameraMatrix 값이 순수하게 "Inspector"에 지정된 카메라의 위치로 고정되어 전달하기 때문입니다. 즉, Scene 편집 화면에서도 마우스를 이용해 카메라의 위치와는 전혀 다르게 바라보도록 움직일 수 있는데 그 카메라의 정보를 C# 스크립트에서 사용하지 않고 편집 화면에 떠 있는 카메라 객체의 위치 값만을 고정적으로 사용하기 때문에 저런 현상이 발생하는 것입니다.

그러니까, C# 스크립트에서 shader에 값을 넘겨주는 경우에는 편집 화면을 너무 믿어서는 안 됩니다.

참고로, 다음은 Unity 스크립트에서 main camera에 대한 속성의 출력 예를 보여줍니다.

Main camera
    Transform
        Position (0, 1, -10)
        Rotation (0, 0, 0)
        Scale    (1, 1, 1)

    .aspect 1.353497
    .fieldOfView 60
    .focalLength 50
    .lensShift (0,0)
    .nearClipPlane 0.3
    .pixelRect (x:0, y:0, width: 716.00, height: 529.00)
    .sensorSize (36.0, 24.0)
    .cameraToWorldMatrix ( == worldToCameraMatrix.inverse)
        1  0  0   0
        0  1  0   1
        0  0 -1 -10
        0  0  0   1
        .transpose
            1  0   0  0
            0  1   0  0
            0  0  -1  0
            0  1 -10  1          
        .rotation 
            (0, 0, 0, 1);
    .cullingMatrix
        1.279686 0        0         0
        0        1.732051 0        -1.732051 
        0        0        1.0006    9.405821
        0        0        1         10
        .transpose
            1.279686 0        0         0
            0        1.732051 0         0
            0        0        1.0006    1
            0       -1.732051 9.405821  10
        .rotation 
            (0, 0, 0, 1);
    .projectionMatrix
        1.279686 0              0         0
        0        1.732051       0         0
        0        0        -1.0006  -0.60018
        0        0             -1         0

        .transpose
            1.279686 0              0         0
            0        1.732051       0         0
            0        0        -1.0006        -1
            0        0        -0.60018        0
        .rotation 
            (0, 0, 0, 1);
    .worldToCameraMatrix ( == cameraToWorldMatrix.inverse)
        1  0  0   0
        0  1  0  -1
        0  0 -1 -10
        0  0  0   1

Scene 화면의 카메라 객체를 움직이지 않는 한 저 값은 C# 스크립트에서 편집 상태의 shader에 언제나 그대로 넘어가게 됩니다. 반면, _WorldSpaceCameraPos 내장 변숫값은 shader에 Scene 화면의 사용자 조작에 따른 카메라 값을 반영하고 있는 것입니다.




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







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

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

비밀번호

댓글 작성자
 




... 76  77  78  79  80  81  82  83  84  85  86  87  88  89  [90]  ...
NoWriterDateCnt.TitleFile(s)
11388정성태12/6/201714522개발 환경 구성: 338. WSL 또는 Ubuntu에 닷넷 코어 설치 [3]
11387정성태12/6/201715065오류 유형: 439. 이벤트 로그 - Data Sharing Service 서비스의 %%3239247874 오류 메시지
11386정성태12/5/201710991오류 유형: 438. Hyper-V - '...' failed to add device 'Virtual CD/DVD Disk'
11385정성태12/5/201722584VC++: 121. DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++) [16]파일 다운로드1
11384정성태12/5/201712936오류 유형: 437. Visual C++ - Cannot open include file: 'SDKDDKVer.h'
11383정성태12/4/201715933디버깅 기술: 110. 비동기 코드 실행 중 예외로 인한 ASP.NET 프로세스 비정상 종료 현상 [1]
11382정성태12/4/201714722오류 유형: 436. System.Data.SqlClient.SqlException (0x80131904): Connection Timeout Expired 예외 발생 시 "[Pre-Login] initialization=48; handshake=1944;" 값의 의미
11381정성태11/30/201711231.NET Framework: 702. 한글이 포함된 바이트 배열을 나눈 경우 한글이 깨지지 않도록 다시 조합하는 방법(두 번째 이야기)파일 다운로드1
11380정성태11/30/201711355디버깅 기술: 109. windbg - (x64에서의 인자 값 추적을 이용한) Thread.Abort 시 대상이 되는 스레드를 식별하는 방법
11379정성태11/30/201711721오류 유형: 435. System.Web.HttpException - Session state has created a session id, but cannot save it because the response was already flushed by the application.
11378정성태11/29/201713559.NET Framework: 701. 한글이 포함된 바이트 배열을 나눈 경우 한글이 깨지지 않도록 다시 조합하는 방법 [1]파일 다운로드1
11377정성태11/29/201712734.NET Framework: 700. CommonOpenFileDialog 사용 시 사용자가 선택한 파일 목록을 구하는 방법 [3]파일 다운로드1
11376정성태11/28/201716093VS.NET IDE: 123. Visual Studio 편집기의 \r\n (crlf) 개행을 \n으로 폴더 단위로 설정하는 방법
11375정성태11/28/201712536오류 유형: 434. Visual Studio로 ASP.NET 디버깅 중 System.Web.HttpException - Could not load type 오류
11374정성태11/27/201717137사물인터넷: 14. 라즈베리 파이 - (윈도우의 NT 서비스처럼) 부팅 시 시작하는 프로그램 설정 [1]
11373정성태11/27/201716188오류 유형: 433. Raspberry Pi/Windows 다중 플랫폼 지원 컴파일 관련 오류 기록
11372정성태11/25/201719438사물인터넷: 13. 윈도우즈 사용자를 위한 라즈베리 파이 제로 W 모델을 설정하는 방법 [4]
11371정성태11/25/201713365오류 유형: 432. Hyper-V 가상 스위치 생성 시 Failed to connect Ethernet switch port 0x80070002 오류 발생
11370정성태11/25/201712962오류 유형: 431. Hyper-V의 Virtual Switch 생성 시 "External network" 목록에 특정 네트워크 어댑터 항목이 없는 경우
11369정성태11/25/201715321사물인터넷: 12. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 키보드 및 마우스로 쓰는 방법 (절대 좌표, 상대 좌표, 휠) [1]
11368정성태11/25/201720345.NET Framework: 699. UDP 브로드캐스트 주소 255.255.255.255와 192.168.0.255의 차이점과 이를 고려한 C# UDP 서버/클라이언트 예제 [2]파일 다운로드1
11367정성태11/25/201720485개발 환경 구성: 337. 윈도우 운영체제의 route 명령어 사용법
11366정성태11/25/201712282오류 유형: 430. 이벤트 로그 - Cryptographic Services failed while processing the OnIdentity() call in the System Writer Object.
11365정성태11/25/201714556오류 유형: 429. 이벤트 로그 - User Policy could not be updated successfully
11364정성태11/24/201715667사물인터넷: 11. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스로 쓰는 방법 (절대 좌표) [2]
11363정성태11/23/201715369사물인터넷: 10. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스 + 키보드로 쓰는 방법 (두 번째 이야기)
... 76  77  78  79  80  81  82  83  84  85  86  87  88  89  [90]  ...