Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일
 

Unity - shader의 원근 투영(Perspective projection) 행렬(UNITY_MATRIX_P)을 수작업으로 구성

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

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

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

이제 마지막으로 투영 행렬을 구성해 볼 차례입니다. ^^




투영 행렬을 구성하는 요소는 Unity Inspector 창에 보이는 "Field of View", "Clipping Planes"와 게임 화면이 실행되는 윈도우의 "가로/세로" 비율이 됩니다. C# 스크립트에서 이에 대한 값은 각각 다음과 같이 구할 수 있습니다.

Camera camera = Camera.main;

float aspect = camera.aspect; // 또는 (float)camera.pixelWidth / camera.pixelHeight;

float fov = camera.fieldOfView;
float near = camera.nearClipPlane;
float far = camera.farClipPlane;

여기서 Field of View는 다음과 같이 Unity에서 카메라로부터 상하로 퍼져 나가는 시야각을 의미하며 화각 또는 FOV라고 줄여서 말하기도 합니다. (Unity 에디터의 기본값은 60도)

projection_matrix_1.png

near는 기본 값이 0.3인데 월드 좌표계 기준으로 카메라로부터 0.3 만큼 떨어진 "근-평면"의 위치를 의미합니다. Scene을 새로 생성하면 카메라의 기본 위치값이 (0, 1, -10) 좌표이므로 근-평면은 (0, 1, -9.7) 좌표에 위치하게 됩니다.

far는 기본 값이 1000이고 월드 좌표계 기준으로 카메라로부터 1000 만큼 떨어진 "원-평면"의 위치를 의미합니다. 마찬가지로 Scene의 기본 값 상태일 때 원-평면은 (0, 1, 990) 좌표에 위치하게 됩니다. 아래는 유니티 편집 화면에서의 근-평면과 원-평면의 모습을 보여줍니다.

projection_matrix_2.png

FOV와 Near, Far 값은 개발자가 제어할 수 있지만 화면 비율(aspect 값)은 딱히 조정할 수 없습니다. 이건 사용자가 게임을 실행할 때 보통 전체 화면으로 실행하기 때문에 윈도우의 width, height가 고정되거나 "창 모드"로 게임을 실행했을지라도 사용자가 임의로 변경하는 것이기 때문에 개발자 입장에서는 그때마다 aspect 비율을 투영 행렬에 잘 반영하면 됩니다.

이 값들로부터 투영 행렬을 만드는 것은 다음의 책을 보면 잘 나옵니다.

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

자세하게 수학적인 설명과 곁들여 설명하고 있으므로 그 부분에 대해서는 책을 참고하시고, 이 글에서는 해당 투영 행렬을 Unity shader에서 어떻게 구성할 수 있는지에 대한 내용만 알아보겠습니다.

우선, 책에 있는 프로젝션 변환 행렬 P는 다음과 같이 이뤄진다고 소개하고 있습니다.

${
P = \begin{bmatrix} \frac {2n} {r - l} & 0 & \frac {r + l} {r - l} & 0 \\ 0 & \frac {2n} {t - b} & \frac {t + b} {t - b} & 0 \\ 0 & 0 & \frac {-(f + n)} {f - n} & \frac {-2fn} {f - n} \\ 0 & 0 & -1 & 0 \end{bmatrix}
}$


하지만, 저 투영 행렬은 OpenGL의 관례를 따라 나타낸 것이고 각각의 플랫폼에 따른 shader 상의 투영 행렬은 다르다고 합니다. 일례로 (DirectX를 사용하는) 윈도우 데스크톱 환경의 경우에는 다음과 같은 투영 행렬이 사용된다고 설명합니다.

${
P = \begin{bmatrix} \frac {2n} {r - l} & 0 & \frac {r + l} {r - l} & 0 \\ 0 & \frac {-2n} {t - b} & -\frac {t + b} {t - b} & 0 \\ 0 & 0 & \frac {-f} {f - n} & \frac {-fn} {f - n} \\ 0 & 0 & -1 & 0 \end{bmatrix}
}$


수식에 사용된 변수의 의미는 다음과 같습니다.

l == 근-평면의 좌측 끝의 x 좌표
r == 근-평면의 우측 끝의 x 좌표
b == 근-평면의 하단 끝의 y 좌표
t == 근-평면의 상단 끝의 y 좌표
n == 원점으로부터 근-평면까지의 거리
f == 원점으로부터 원-평면까지의 거리

여기서 n과 f는 이미 Unity Inspector 창에서 Camera 객체의 값으로부터 설정된 바로 그 값입니다. 그리고 나머지 left, right, top, bottom의 값은 FOV에 지정된 각을 이용해 탄젠트 삼각함수로 구할 수 있습니다. 가령 top 값은 근-평면 하단 끝의 y 좌표이므로 x축 기준으로 y-z 평면으로 봤을 때,

projection_matrix_3.png

직각 삼각형의 빗변과 밑변의 각도는 Field of View 60도에서 절반인 30도이고 밑변의 길이가 0.3임을 알고 있으므로 top과 bottom은 다음의 공식으로 알아낼 수 있습니다.

top = tan(30°) * near
    = tan(DegreeToRadian(30°)) * near
bottom = -top;

top과 bottom은 쌍을 이루니 당연히 bottom은 -top이 됩니다. 반면 left와 right의 경우에는 FOV를 이용할 수 없습니다. FOV는 시야의 상하각이기 때문인데, 대신 aspect 값이 있으므로 이를 이용해 top을 aspect와 곱해 구할 수 있습니다.

float right = (top * aspect);
float left = -right;

실제로 Unity C# 스크립트로부터 camera.projectionMatrix와 GL.GetGPUProjectionMatrix(camera.projectionMatrix, true)로 구한 행렬의 값은 다음과 같은 식으로 나옵니다. (Unity 기본 Scene 상태를 가정합니다.)

aspect  1.323475        // 사용자의 환경에 따라 변경
tangentFov 0.5773503    // 기본 Scene 상태인 경우 
near 0.3                // 기본 Scene 상태인 경우 
far 1000                // 기본 Scene 상태인 경우 

top 0.1732051
bottom -0.1732051
right   0.2292326
left    -0.2292326

이렇게 해서 소스가 준비되었군요. ^^




이제 Unity C# 스크립트에서 실제 사용하고 있는 행렬을 보겠습니다.

camera.projectionMatrix
1.30871 0.00000  0.00000  0.00000
0.00000 1.73205  0.00000  0.00000
0.00000 0.00000 -1.00060 -0.60018
0.00000 0.00000 -1.00000  0.00000

GL.GetGPUProjectionMatrix(camera.projectionMatrix, true)
1.30871 0.00000 0.00000 0.00000
0.00000 -1.73205 0.00000 0.00000
0.00000 0.00000 0.00030 0.30009
0.00000 0.00000 -1.00000 0.00000

그런데 대충 봐도, (r+l) / (r-l)을 나타내는 projectionMatrix[0,2]의 값이 0인 것을 보면 완전히 똑같은 것은 아닌 것 같습니다. shader에서도 이렇게 쓰고 있는지 다음과 같은 코드로 테스트할 수 있습니다.

v2f vert(appdata v)
{
    float4 pos;

    v2f o;

    float4x4 m = UNITY_MATRIX_P;
    float4x4 projectionMatrix;

    projectionMatrix[0] = float4(m[0].x, 0,      0,      0);
    projectionMatrix[1] = float4(0,      m[1].y, 0,      0);
    projectionMatrix[2] = float4(0,      0,      m[2].z, m[2].w);
    projectionMatrix[3] = float4(0,      0,      -1,     0);

    pos = mul(unity_ObjectToWorld, v.vertex);
    pos = mul(UNITY_MATRIX_V, pos);
    pos = mul(projectionMatrix, pos);

    o.vertex = pos;

    return o;
}

camera.projectionMatrix 및 GL.GetGPUProjectionMatrix의 구조와 동일한 위치의 값만 설정했는데 정상적으로 투영 행렬로 동작하는 것을 확인할 수 있으며 즉, Unity shader에서 사용하는 투영 행렬은 다음의 2개로 정해집니다.

[OpenGL을 따르는 투영 행렬]

${
P = \begin{bmatrix} \frac {2n} {r - l} & 0 & 0 & 0 \\ 0 & \frac {2n} {t - b} & 0 & 0 \\ 0 & 0 & \frac {-(f + n)} {f - n} & \frac {-2fn} {f - n} \\ 0 & 0 & -1 & 0 \end{bmatrix}
}$

[실행 환경에 맞는 투영 행렬 - 아래는 DirectX를 사용하는 윈도우 환경에서의 투영 행렬]

${
P = \begin{bmatrix} \frac {2n} {r - l} & 0 & 0 & 0 \\ 0 & \frac {-2n} {t - b} & 0 & 0 \\ 0 & 0 & \frac {-f} {f - n} & \frac {-fn} {f - n} \\ 0 & 0 & -1 & 0 \end{bmatrix}
}$


그럼 끝났군요. ^^ 재료를 이용해 다음과 같이 투영 행렬을 구할 수 있습니다.

Matrix4x4 myProjection1 = new Matrix4x4();
myProjection1[0, 0] = (2 * near) / (right - left);
myProjection1[1, 1] = (2 * near) / (top - bottom);
myProjection1[2, 2] = -(far + near) / (far - near);
myProjection1[2, 3] = -(2 * far * near) / (far - near);
myProjection1[3, 2] = -1;

Matrix4x4 myProjection2 = new Matrix4x4();
myProjection2[0, 0] = (2 * near) / (right - left);
myProjection2[1, 1] = -(2 * near) / (top - bottom);
myProjection2[2, 2] = -far / (far - near);
myProjection2[2, 3] = -(far * near) / (far - near);
myProjection2[3, 2] = -1;

바로 저 2개의 행렬들은 Unity C# 스크립트에서 다음의 코드로 대응합니다.

myProjection1 == camera.worldToCameraMatrix
myProjection2 == GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);

또한 우리가 구한 l, r, b, t, n, f의 값들이 실제로 camera.worldToCameraMatrix.decomposeProjection 값들과 동일합니다.

camera.projectionMatrix.decomposeProjection
		left	-0.2292326
		right	0.2292326
		bottom	-0.1732051
		top	    0.1732051	
		zNear	0.3	
		zFar	1000.134	

GL.GetGPUProjectionMatrix(camera.projectionMatrix, true).decomposeProjection
		left	0.2293702	
		right	-0.2293702	
		bottom	-0.1733091	
		top	    0.1733091	
		zNear	-0.3001801	
		zFar	0.3




다음 단계로 Unity shader에서 사용하는 투영 행렬이 camera.projectionMatrix 인지, GL.GetGPUProjectionMatrix 반환 값인지는 다음과 같은 코드로 쉽게 확인할 수 있습니다.

using UnityEngine;

[ExecuteInEditMode]
public class SetMatrix : MonoBehaviour {

    void Start () {
    }

    void Update () {
        Camera camera = Camera.main;

        Matrix4x4 projectionMatrix = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
        Shader.SetGlobalMatrix("_projectionMatrix1", projectionMatrix);

        Shader.SetGlobalMatrix("_projectionMatrix2", camera.projectionMatrix);
    }
}

float4x4 _projectionMatrix;
float4x4 _projectionMatrix2;

v2f vert(appdata v)
{
    float4 pos;

    v2f o;

    pos = mul(unity_ObjectToWorld, v.vertex);
    pos = mul(UNITY_MATRIX_V, pos);

    pos = mul(_projectionMatrix1, pos);
    // pos = mul(_projectionMatrix2, pos);

    o.vertex = pos;

    return o;
}

실제로 실행해 보면 GL.GetGPUProjectionMatrix가 반환한 투영 행렬이 정상적으로 동작하는 것을 볼 수 있습니다. 약간 혼란스러운 점이 있다면, C# 스크립트에서 넘겨줄 때의 행렬 값이 다음과 같은 반면,

GL.GetGPUProjectionMatrix

0.97183  0.00000  0.00000 0.00000
0.00000 -1.73205  0.00000 0.00000
0.00000  0.00000  0.00030 0.30009
0.00000  0.00000 -1.00000 0.00000

"Visual Studio Graphics Analyzer"로 디버깅 환경의 Watch 창에서 GL.GetGPUProjectionMatrix의 값을 보면 다음과 같다는 것입니다.

_projectionMatrix1      float4x4
    _projectionMatrix1[0]   x = 0.971829400, y = 0.000000000, z = 0.000000000, w = 0.000000000
    _projectionMatrix1[1]   x = 0.000000000, y = -1.732051000, z = 0.000000000, w = 0.000000000
    _projectionMatrix1[2]   x = 0.000000000, y = 0.000000000, z = 0.000300050, w = -1.000000000
    _projectionMatrix1[3]   x = 0.000000000, y = 0.000000000, z = 0.300090000, w = 0.000000000

즉, 전치가 되어 있는데 이것은 아마도 Visual Studio의 디버거 창이 자동으로 전치를 해주는 것인지? 아니면 메모리 상의 값을 읽을 때 열/행우선을 잘못 판단한 것인지는 알 수 없으나 C# 스크립트 상에서의 디버거 값이 올바른 형식입니다.

정리해 보면, Unity에서 제공하는 투영 행렬은 2가지가 있습니다.

  • camera.projectionMatrix == OpenGL을 따르는 형식
  • GL.GetGPUProjectionMatrix == Unity 프로그램이 실행되는 환경에 부합하는 투영 행렬(일례로 위에서의 _projectionMatrix1 값은 DirectX 11을 사용하는 윈도우 환경에서 유효한 투영 행렬)

휴~~~ 이것으로 대충 정리가 되었군요. ^^ 행렬의 생성 규칙과 그 값의 decomposeProjection을 알게 되었으니 이제 나머지 단계는 unity shader에서 decomposeProjection에 해당하는 값을 어떻게 구하느냐에 달려 있습니다.




unity shader에서 근-평면/원-평면은 내장 변수를 통해 값을 구할 수 있습니다.

_ProjectionParams float4

x is 1.0 (or ?1.0 if currently rendering with a flipped projection matrix)
y is the camera’s near plane
z is the camera’s far plane
w is 1/FarPlane

float nearPlane = _ProjectionParams.y;
float farPlane = _ProjectionParams.z;

// 기본값인 경우 nearPlane == 0.3, farPlane == 1000

또한 aspect 비율도 _ScreenParams 내장 변수를 통해 구할 수 있습니다.

float width = _ScreenParams.x;
float height = _ScreenParams.y;

float aspect = width / height;

문제는, FOV 값인데 이것은 unity가 내장 변수로 제공하지 않습니다. 대신 기존 UNITY_MATRIX_P로부터 구해올 수는 있습니다. 이에 대한 계산이 재미있는데요. ^^ 이전에 행렬을 구하는 것에서 (0,0)의 값은 다음과 같이 이뤄집니다.

[0, 0] = (2 * near) / (right - left);

그런데 검색해 보면 다음의 소스 코드를 통해,

https://github.com/g-truc/glm/blob/master/glm/ext/matrix_clip_space.inl

template<typename T>
GLM_FUNC_QUALIFIER mat<4, 4, T, defaultp> perspectiveLH_NO(T fovy, T aspect, T zNear, T zFar)
{
    assert(abs(aspect - std::numeric_limits<T>::epsilon()) > static_cast<T>(0));

    T const tanHalfFovy = tan(fovy / static_cast<T>(2));

    mat<4, 4, T, defaultp> Result(static_cast<T>(0));
    Result[0][0] = static_cast<T>(1) / (aspect * tanHalfFovy);
    Result[1][1] = static_cast<T>(1) / (tanHalfFovy);
    Result[2][2] = (zFar + zNear) / (zFar - zNear);
    Result[2][3] = static_cast<T>(1);
    Result[3][2] = - (static_cast<T>(2) * zFar * zNear) / (zFar - zNear);
    return Result;
}

"1 / (aspect * tanHalfFovy) = UNITY_MATRIX_P[0].x"와 같다는 것을 알 수 있습니다. 따라서 각도는 다음과 같이 구할 수 있습니다.

aspect * tanHalfFovy * UNITY_MATRIX_P[0].x = 1
tanHalfFovy = 1 / (aspect * UNITY_MATRIX_P[0].x)

radian_FOV = arctan(1 / (aspect * UNITY_MATRIX_P[0].x))

자, 그럼 다 끝났군요. ^^ 이제 다음과 같이 UNITY_MATRIX_P를 재조립해 적용할 수 있습니다.

Shader "Unlit/NewUnlitShader"
{
    Properties
    {
    }
    SubShader
    {
        Tags{ "LightMode" = "ForwardBase" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma enable_d3d11_debug_symbols

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            float4x4 _projectionMatrix;

            v2f vert(appdata v)
            {
                float4 pos;

                v2f o;

                float nearPlane = _ProjectionParams.y;
                float farPlane = _ProjectionParams.z;
                
                float width = _ScreenParams.x;
                float height = _ScreenParams.y;
                float aspect = width / height;

                float4x4 m = UNITY_MATRIX_P;

                float halfFov = atan2(1, aspect * m[0].x);

                float top = tan(halfFov) * nearPlane;
                float bottom = -top;

                float right = (top * aspect);
                float left = -right;

                // DirectX를 사용하는 윈도우 환경에서의 투영 행렬
                float p00 = (2 * nearPlane) / (right - left);
                float p11 = -(2 * nearPlane) / (top - bottom);
                float p22 = -farPlane / (farPlane - nearPlane);
                float p23 = -(farPlane * nearPlane) / (farPlane - nearPlane);

                float4x4 projectionMatrix;
                projectionMatrix[0] = float4(p00, 0,   0,   0);
                projectionMatrix[1] = float4(0,   p11, 0,   0);
                projectionMatrix[2] = float4(0,   0,   p22, p23);
                projectionMatrix[3] = float4(0,   0,   -1,  0);

                pos = mul(unity_ObjectToWorld, v.vertex);
                pos = mul(UNITY_MATRIX_V, pos);
                pos = mul(projectionMatrix, pos);
                // pos = mul(UNITY_MATRIX_P, pos);

                o.vertex = pos;

                return o;
            }

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

            ENDCG
        }
    }
}




참고로 https://github.com/g-truc/glm/blob/master/glm/ext/matrix_clip_space.inl 소스 코드를 보면 알 수 있지만 동일한 값을 다음과 같이 다르게 구하고 있습니다.

projectionMatrix[0, 0] = (2 * near) / (right - left);
                       = 1.0 / (aspect * halfFov)

projectionMatrix[1, 1] = -(2 * near) / (top - bottom);
                       = -1.0 / halfFov;

projectionMatrix[2, 2] = -far / (far - near);
projectionMatrix[2, 3] = -(far * near) / (far - near);
projectionMatrix[3, 2] = -1;

2개의 공식이 왜 같은지는 다음과 같이 풀어 보면 쉽게 이해할 수 있습니다.

(2 * near) / (right - left)
 = (2 * near) / (right * 2) // 어차피 left == -right이므로.
 = near / right             // 공통 인수 2 약분
 = near / (top * aspect)    // right = top * aspect이므로.
 = near / (halfFov * near * aspect) // top = halfFov * near
 = 1 / (halfFov * aspect)   // 공통 인수 near 약분

-(2 * near) / (top - bottom)
 = -(2 * near) / (2 * top)  // 어차피 bottom == -top이므로.
 = -near / top              // 공통 인수 2 약분
 = -near / (halfFov * near) // top = halfFov * near
 = -1 / halfFov             // 공통 인수 near 약분

그러니까 결국, left, right, bottom, top을 구할 필요가 없었던 것입니다. 따라서 shader에서의 소스 코드는 다음과 같이 더 간단해집니다.

v2f vert(appdata v)
{
    float4 pos;

    v2f o;

    float nearPlane = _ProjectionParams.y;
    float farPlane = _ProjectionParams.z;

    float width = _ScreenParams.x;
    float height = _ScreenParams.y;
    float aspect = width / height;

    float4x4 m = UNITY_MATRIX_P;

    float halfFov = atan2(1, aspect * m[0].x);

    // DirectX를 사용하는 윈도우 환경에서의 투영 행렬
    float p00 = 1 / (tan(halfFov) * aspect);
    float p11 = -1 / tan(halfFov);
    float p22 = -farPlane / (farPlane - nearPlane);
    float p23 = -(farPlane * nearPlane) / (farPlane - nearPlane);

    float4x4 projectionMatrix;
    projectionMatrix[0] = float4(p00, 0,   0,   0);
    projectionMatrix[1] = float4(0,   p11, 0,   0);
    projectionMatrix[2] = float4(0,   0,   p22, p23);
    projectionMatrix[3] = float4(0,   0,   -1,  0);

    pos = mul(unity_ObjectToWorld, v.vertex);
    pos = mul(UNITY_MATRIX_V, pos);
    pos = mul(projectionMatrix, pos);
    // pos = mul(UNITY_MATRIX_P, pos);

    o.vertex = pos;

    return o;
}

그 외에 여유가 되시면 Unity가 아닌 실제 카메라 영상을 기준으로 한 다음의 설명도 읽어보시고. ^^

카메라 캘리브레이션 (Camera Calibration)
; http://darkpgmr.tistory.com/32?category=460965




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

[연관 글]





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

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

비밀번호

댓글 쓴 사람
 




... 16  [17]  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
11764정성태10/26/20182396개발 환경 구성: 419. MIT 라이선스로 무료 공개된 Detours API 후킹 라이브러리
11763정성태10/25/20181852사물인터넷: 53. New NodeMcu v3(ESP8266)의 https 통신
11762정성태10/25/20182538사물인터넷: 52. New NodeMcu v3(ESP8266)의 http 통신파일 다운로드1
11761정성태10/25/20182008Graphics: 26. 임의 축을 기반으로 3D 벡터 회전파일 다운로드1
11760정성태10/24/20181242개발 환경 구성: 418. Azure - Runbook 내에서 또 다른 Runbook 스크립트를 실행
11759정성태10/24/20181324개발 환경 구성: 417. Azure - Runbook에서 사용할 수 있는 다양한 메서드를 위한 부가 Module 추가
11758정성태6/7/20191862.NET Framework: 800. C# - Azure REST API 사용을 위한 인증 획득 [3]파일 다운로드1
11757정성태10/23/20181849개발 환경 구성: 416. Visual Studio 2017을 이용한 아두이노 프로그램 개발(및 디버깅)
11756정성태10/19/20181572오류 유형: 500. Visual Studio Code의 아두이노 프로그램 개발 시 인텔리센스가 안 된다면?
11755정성태10/19/20183255오류 유형: 499. Visual Studio Code extension for Arduino - #include errors detected. [1]
11754정성태10/19/20181930개발 환경 구성: 415. Visual Studio Code를 이용한 아두이노 프로그램 개발 - 새 프로젝트
11753정성태10/19/20185493개발 환경 구성: 414. Visual Studio Code를 이용한 아두이노 프로그램 개발
11752정성태10/18/20181234오류 유형: 498. SQL 서버 - Database source is not a supported version of SQL Server
11751정성태10/18/20181486오류 유형: 497. Visual Studio 실행 시 그래픽이 투명해진다거나, 깨진다면?
11750정성태10/18/20181387오류 유형: 496. 비주얼 스튜디오 - One or more projects in the solution were not loaded correctly.
11749정성태10/18/20181500개발 환경 구성: 413. 비주얼 스튜디오에서 작성한 프로그램을 빌드하는 가장 쉬운 방법
11748정성태10/18/20182023개발 환경 구성: 412. Arduino IDE를 Store App으로 설치한 경우 컴파일만 되고 배포가 안 되는 문제
11747정성태10/17/20181907.NET Framework: 799. C# - DLL에도 EXE처럼 Main 메서드를 넣어 실행할 수 있도록 만드는 방법파일 다운로드1
11746정성태10/15/20181401개발 환경 구성: 411. Bitvise SSH Client의 인증서 모드에서 자동 로그인 방법파일 다운로드1
11745정성태10/15/20181403오류 유형: 495. TFS 파일/폴더 삭제 - The item [...] could not be found in your workspace, or you do not have permission to access it.
11744정성태10/15/20181254개발 환경 구성: 410. msbuild로 .pubxml 설정에 따른 배포 파일을 만드는 방법
11743정성태10/15/20181173웹: 37. Bootstrap의 dl/dt/dd 조합에서 문자열이 잘리지 않도록 CSS 설정
11742정성태10/15/20181591스크립트: 13. 윈도우 배치(Batch) 스크립트에서 날짜/시간 문자열을 구하는 방법
11741정성태10/15/20181009Phone: 13. Android - LinearLayout 간략 설명
11740정성태10/15/20181549사물인터넷: 51. Synology NAS(DS216+II)를 이용한 원격 컴퓨터의 전원 스위치 제어
11739정성태10/15/20182333Windows: 151. 윈도우 10의 전원 관리가 "균형 조정(Balanced)"으로 바뀌는 문제
... 16  [17]  18  19  20  21  22  23  24  25  26  27  28  29  30  ...