전체 글 (99)

  • 2022.03.18
  • 2022.03.18
  • 2022.03.17
  • 2022.03.16
  • 2022.03.15
  • 2022.03.02
  • 2022.02.23
  • 2022.02.23
  • 2022.02.18
  • 2022.02.10
  • 2022.02.03
  • 2022.01.27
  • 03
    18

    C 스타일의 캐스팅은 (type-id)(e-pression) 문법이다.

    C++스타일은 캐스팅은 <type-id>(e-pression) 문법

     

    const 객체에 대한 포인터를 non-const객체에 대한 포인터로

    바꾸는 캐스트와 기본 클래스와 파생 클래스로 바꿔주는 캐스트는

    동작 자체가 굉장히 다르기 때문에, (C는 무지성 캐스팅을 함)

    C++ 스타일 캐스팅으로 명확하게 해서, 세부사항을 고려할 수 있다. 

     

    ->C 스타일은 어떤 타입이든 다른 타입으로 바꿔준다.

    ->C++스타일은 명확하게 캐스팅이 가능하다. 

    ->static_cast, const_cast, dynamic_cast, reinterpret_cast

     

    1. const_cast

    const_cast는 상수 성을 없애고 부여하는 데 사용한다.

    const int a = 0;
    int b = const_cast<int&>(a);

    상수 성을 제거해서 받았다.

     

    2. reinterpret_cast

    포인터 타입 -> 포인터 타입

    int* -> char*, Aclass* -> Unrelated_Class*

    비트단위로 재해석하면서 모든 포인터 타입을

    강제 형변환 하기 때문에 특정 목적이나 관계가 명확할 때

    쓰는 것이 바람직하다. (주소가 유효한지 검사 안 한다.)

     

    변환 결과가 컴파일러에 따라 다르게 정의돼있어,

    다른 곳에 소스 이식하기 불편함.

     

    3. static_cast

    기본적인 캐스트 연산자이다.

    우리가 일반적으로 생각하기에 구조체를 int나 double 형태로 형 변환하지 못하고

    float타입을 포인터 타입으로 변환하지 못하는 그 캐스팅 연산자이다. 

    이름이 static인 이유는 컴파일 단계에서 형변환 타입 체크를 하기 때문이다.

    dynamic_cast은 런타임에 타입체크를 한다. 

     

    4. dynamic_cast

    동적으로 다운 캐스팅 시에 사용하는 캐스팅 연산자이다.

    기본 클래스 객체에 대한 포인터나 참조자를

    파생 클래스, 형제 클래스 타입으로 변환해준다.

    런타임에 다형성을 사용해 애매한 타입 캐스팅을 시도할 때

    엉뚱한 결과가 넘어가지 않게 런타임 오류를 방지한다.  

    캐스팅 실패는 NULL, 예외(참조자)를 보고 판별 가능.

     

    5. static_cast VS dynamic_cast

    정적 형 변환, 동적형 변환으로서 앞에서 얘기했듯,

    컴파일 단계에서 검사하느냐, 런타임에서 검사하느냐의 차이이다.

     

    변환 대상 타입이 바뀔 일이 없고 명확하다면 static을 쓰고, 

    변환 대상이 애매모호하다면 dynamic_cast를 사용하는 것이 좋다.

     

    http://egloos.zum.com/sweeper/v/1907485

     

    [C++] static_cast, dynamic_cast, const_cast, reinterpret_cast

    아래 내용은 MEC++ 2장을 토대로 개인 견해와 예제 코드를 곁들여 작성되었다. -------------------------------------------------------------------------------- 1. C 스타일 캐스트와 C++ 캐스트 간략 소개 캐스트(cast,

    egloos.zum.com

     

    COMMENT
     
    03
    18

    이번에는 법선매핑, 노말 맵을 다이렉트x에 적용한다.

    노말 맵은 픽셀에 사용할 법선 정보를 담고 있다.

    평평한 면의 법선
    울퉁불퉁한 노말맵을 적용한 면

    노말 매핑을 사용하면 버텍스의 노말(법선) 정보가 아닌
    텍스쳐의 노말(법선)정보를 사용하므로, 텍스쳐의 공간을 사용해야 한다.

    법선맵에 관한 내용은 이전에도 포스팅했다
    https://dlemrcnd.tistory.com/m/37

    셰이더 프로그래밍 - AMD RenderMonkey 사용해서 쉐이더 공부를 해보자5 (법선 매핑)

    <본 포스팅은 강좌가 아닙니다.> 조명을 계산할때 각 정점의 법선(노말) 정보를 가져와서 조명효과를 결정했다. 박스면의 법선을 생각해보자면 한 면은 아래와 같은 모습일 것이다. 만약 벽돌처

    dlemrcnd.tistory.com


    이때는, TBN 행렬을 렌더 몽키의 자체적인 레이아웃에서
    만들어 볼 수 있었는데, 직접 다이렉트에서 구현해보려니
    이 접선 공간, 탄젠트 스페이스에 대해서 알아야 할 필요성을 느꼈다.


    접선 공간(Tangent Space)

    접선 공간은 텍스쳐 위의 좌표계라고도 한다.
    법선벡터(Normal), 접선벡터(Tangent), 바이노멀(Binormal)
    벡터 축으로 하는 공간이다.
    이러한 벡터들로 한 픽셀이 어떤 기울기를 갖고 있는지 알 수 있다.

    법선(Normal) : 면에서 수직인 벡터
    접선(Tangent) : 정점 표면의 정보
    종법선(Bi-Normal) : 접선과 법선의 외적 된 결과, 노말의 수직인 벡터

    이러한 공간을 사용하는 이유는 각 정점마다 좌표계가 모두 다르기 때문이다.
    이러한 기준으로 노말 정보를 담은 게 탄젠트 공간 노말 맵이라 한다.

    라이팅 연산을 위해서 벡터와 벡터끼리 공간을 맞춰줘야 한다.
    빛 방향이 월드 공간에 있다면 노말 방향도 월드 공간에 있어야 한다.
    위에서 말했듯이 우리의 텍스쳐인 법 선 맵 (노말 맵)은 탄젠트 공간에 있음

    변환을 하기 위해서는 TBN(tangent, binormal, normal) 행렬을 만들어야 한다.

    기준 행렬( Row major matrix ) - d3d
    | Tx Ty Tz |
    | Bx By Bz |
    | Nx Ny Nz |

    열기 준 행렬 ( Column major matrix ) - open gl
    | Tx Bx Nx |
    | Ty By Ny |
    | Tz Bz Nz |


    세 점으로 법선, 종법선, 접선을 구하는 함수를 구현했다.

    CreateTangentSpace

    void KBoxObj::CreateTangentSpace(KVector3* v1, KVector3* v2, KVector3* v3, KVector2* uv1, KVector2* uv2, KVector2* uv3, KVector3* normal, KVector3* tangent, KVector3* binormal)
    {
    	KVector3 vEdge1 = *v2 - *v1;
    	KVector3 vEdge2 = *v3 - *v1;
    
    	float vEdge1_U = uv2->x - uv1->x;
    	float vEdge1_V = uv2->y - uv1->y;
    	float vEdge2_U = uv3->x- uv1->x;
    	float vEdge2_V = uv3->y - uv1->y;
    
    
    	float fDenominator = vEdge1_U * vEdge2_V - vEdge2_U * vEdge1_V;
    
    	if (fDenominator < 0.0001f && fDenominator>-0.0001f)
    	{
    		*tangent =KVector3(1.0f, 0.0f, 0.0f);
    		*binormal= KVector3(0.0f, 1.0f, 0.0f);
    		*normal= KVector3(0.0f, 0.0f, 1.0f);
    	}
    	else
    	{
    		//계산
    		float fScale = 1.0f / fDenominator;
    
    		KVector3 T;
    		KVector3 B;
    		KVector3 N;
    		T = KVector3((vEdge2_V * vEdge1.x - vEdge1_V * vEdge2.x) * fScale,
    			(vEdge2_V * vEdge1.y - vEdge1_V * vEdge2.y) * fScale,
    			(vEdge2_V * vEdge1.z - vEdge1_V * vEdge2.z) * fScale);
    		B = KVector3((-vEdge2_U * vEdge1.x + vEdge1_U * vEdge2.x) * fScale,
    			(-vEdge2_U * vEdge1.y + vEdge1_U * vEdge2.y) * fScale,
    			(-vEdge2_U * vEdge1.z + vEdge1_U * vEdge2.z) * fScale);
                
    		D3DXVec3Cross( &N, &T, &B );
    	
    		float fScale2 = 1.0f / ((T.x * B.y * N.z - T.z * B.y * N.x) +
    								(B.x * N.y * T.z - B.z * N.y * T.x) + 
    								(N.x * T.y * B.z - N.z * T.y * B.x));
    		KVector3 vTemp;
    		(*tangent).x = D3DXVec3Cross(&vTemp, &B, &N)->x * fScale2; 
    		(*tangent).y = -(D3DXVec3Cross(&vTemp, &N, &T)->x * fScale2);
    		(*tangent).z = D3DXVec3Cross(&vTemp, &T, &B)->x * fScale2;
    		D3DXVec3Normalize(&(*tangent), &(*tangent));
    
    		(*normal).x = -(D3DXVec3Cross(&vTemp, &B, &N)->y * fScale2); 
    		(*normal).y = D3DXVec3Cross(&vTemp, &N, &T)->y * fScale2;
    		(*normal).z = -(D3DXVec3Cross(&vTemp, &T, &B)->y * fScale2);
    		D3DXVec3Normalize(&(*normal), &(*normal));
            
    		(*binormal).x = D3DXVec3Cross(&vTemp, &B, &N)->z * fScale2;
    		(*binormal).y = -(D3DXVec3Cross(&vTemp, &N, &T)->z * fScale2);
    		(*binormal).z = D3DXVec3Cross(&vTemp, &T, &B)->z * fScale2;
    		D3DXVec3Normalize(&(*binormal), &(*binormal));
    	}
    
    }


    세 점의 벡터를 얻어와서 가장자리의 벡터를 구한다.
    텍스쳐 위의 좌표이기 때문에 UV값도 가져온다.
    점과 점 사이의 간격이 너무 좁은 예외 사항 처리를 하고,
    탄젠트와 바이 노말은 UV의 좌표에서 수직 관계이어서 이를 이용한다.
    탄젠트와 바이 노말을 구했으면 외적 결과로 노말 값을 얻는다. 이런 함수로
    어떤 면이든 3점을 얻어서 TBN 행렬을 만들 수 있도록 해준다.

    만든 TBN을 버텍스 버퍼에 담아 보내주고
    PS에서 TBN행렬을 제작한다. 버퍼로 행렬만 넘겨줘도 되지만,
    빠른 구현을 위해 픽셸셰이더에서 계산했다.

    PSShader

    cbuffer CBuf
    {
    	matrix g_matWorld : packoffset(c0);
    	matrix g_matView : packoffset(c4);
    	matrix g_matProj : packoffset(c8);
    	matrix g_matNormal : packoffset(c12);
    	float4 g_lightPos : packoffset(c16); //라이트 방향
    	float4 g_lightColor : packoffset(c17); //라이트 색상
    	float4 g_camPos : packoffset(c18); //카메라 방향
    	float4 g_value : packoffset(c19); //기타 시간 값등
    };
    struct VS_OUTPUT
    {
    	float4 p : SV_POSITION;
    	float2 t : TEXCOORD0;
    	float3 mLightDir : TEXCOORD1; //방향
    	float3 mViewDir : TEXCOORD2; //방향
    	float3 mT        : TEXCOORD3;
    	float3 mB        : TEXCOORD4;
    	float3 mN        : TEXCOORD5;
    };
    
    Texture2D		g_txDiffuse : register(t0);
    Texture2D		g_txSpecular : register(t1);
    Texture2D		g_txNormal : register(t2);
    SamplerState	g_Sample : register(s0);
    float4 PS(VS_OUTPUT Input) : SV_TARGET
    {
    	//텍스쳐에서 노말, 법선 좌표 구해옴
       float3 tangentNormal = g_txNormal.Sample(g_Sample, Input.t).xyz;
       tangentNormal = normalize(tangentNormal * 2 - 1);
    
       float3x3 TBN = float3x3(normalize(Input.mT), normalize(Input.mB),normalize(Input.mN));
       TBN = transpose(TBN);
       float3 worldNormal = mul(TBN, tangentNormal);
       //디퓨즈 텍스쳐
       float4 albedo = g_txDiffuse.Sample(g_Sample, Input.t);
       float3 lightDir = normalize(Input.mLightDir);
       float3 diffuse = saturate(dot(worldNormal, -lightDir));
       diffuse = g_lightColor.rgb*albedo.rgb * diffuse;
    
       float3 specular = 0;
    
       if (diffuse.x > 0.0f)
       {
    	  float3 reflection = reflect(lightDir, worldNormal);
    	  float3 viewDir = normalize(Input.mViewDir);
    
    	  specular = saturate(dot(reflection, -viewDir));
    	  specular = pow(specular,20.0f);
    
    	  //스페큘러 텍스쳐
    	  float4 specularInten = g_txSpecular.Sample(g_Sample, Input.t);
    	  specular *= specularInten.rgb* g_lightColor;
       }
    
       float3 ambient = float3(0.1f, 0.1f, 0.1f) * albedo;
    
       return float4(ambient + diffuse + specular,1);
    }

    결과


    COMMENT
     
    03
    17

    이전 포스팅에서는 렌더몽키 환경에서

    난반사광, 정반사광 적용을 했었다.

    복습할겸, 다이렉트X11 환경에서 적용해보았다.

     

    VS_0.hlsl

    cbuffer CBuf
    {
    	matrix g_matWorld : packoffset(c0);
    	matrix g_matView : packoffset(c4);
    	matrix g_matProj : packoffset(c8);
    	matrix g_matNormal : packoffset(c12);
    	float4 g_lightPos : packoffset(c16); //라이트 방향
    	float4 g_lightColor : packoffset(c17); //라이트 방향
    	float4 g_camPos : packoffset(c18); //카메라 방향
    	float4 g_value : packoffset(c19); //기타 시간 값등
    };
    struct VS_INPUT 
    {
       float3 mPosition : POSITION;
       float3 mNormal    : NORMAL;
       float4 mColor	: COLOR;
       float2 mUV		: TEXTURE;
    };
    struct VS_OUTPUT
    {
    	float4 mPosition : SV_POSITION;
    	float3 mNormal : NORMAL;
    	float4 mColor : COLOR0;
    	float2 mUV : TEXCOORD0;		//uv
    	float3 mDiffuse : TEXCOORD1; //디퓨즈
    	float3 mViewDir : TEXCOORD2; //방향
    	float3 mReflection : TEXCOORD3; //반사
    };
    
    VS_OUTPUT VS(VS_INPUT Input) 
    {
    	//난반사광의 계산 동일한 계산을 PS VS 둘다 할 수 있음. 
       //하지만 픽셀단위 계산보다 정점단위 계산이 더 연산량이 적음
    	VS_OUTPUT Output = (VS_OUTPUT)0;
    	Output.mPosition = float4(Input.mPosition, 1.0f);
    	Output.mPosition = mul(Output.mPosition, g_matWorld);
    
    	//월드 행렬 곱함, 월드 공간에서의 위치여서 여기서 광원의 위치를 뺀다.
    	float3 lightDir = Output.mPosition.xyz - g_lightPos.xyz;
    	lightDir = normalize(lightDir);
    	//
    	float3 viewDir = Output.mPosition.xyz - g_camPos.xyz;
    	Output.mViewDir = viewDir;
    	//
    
    	Output.mPosition = mul(Output.mPosition, g_matView);
    	Output.mPosition = mul(Output.mPosition, g_matProj);
    
    	float3 worldNormal = mul(Input.mNormal, (float3x3)g_matWorld);
    	worldNormal = normalize(worldNormal);
    	//내적함수 dot()사용
    	Output.mDiffuse = dot(-lightDir, worldNormal);
    	Output.mReflection = reflect(lightDir, worldNormal);
    
    	Output.mNormal = Input.mNormal;
    	Output.mColor = Input.mColor;
    	Output.mUV = Input.mUV;
    	return  Output;
    }

     

    PS_0.hlsl

    Texture2D		g_txDiffuse : register(t0);
    Texture2D		g_txSpecular : register(t1);
    Texture2D		g_txNormal : register(t2);
    SamplerState	g_Sample : register(s0);
    
    struct VS_OUTPUT
    {
    	float4 mPosition : SV_POSITION;
    	float3 mNormal : NORMAL;
    	float4 mColor : COLOR0;
    	float2 mUV : TEXCOORD0;		//uv
    	float3 mDiffuse : TEXCOORD1; //디퓨즈
    	float3 mViewDir : TEXCOORD2; //방향
    	float3 mReflection : TEXCOORD3; //반사
    };
    
    float4 PS(VS_OUTPUT Input) : SV_TARGET
    {	
    	float3 diffuse = saturate(Input.mDiffuse);
    	//보간기를 거치는 동안 값이 흐트러질수도 있음 다시 정규화
    	float3 reflection = normalize(Input.mReflection);
    	float3 viewDir = normalize(Input.mViewDir);
    	float3 specular = 0;
    	//난사빛(빛이 닿아야)이 있어야 정반사광이 존재함
    	if (diffuse.x > 0.0f)
    	{
    	   specular = saturate(dot(reflection, -viewDir));
    	   //20번 거듭제곱
    	   specular = pow(specular,20.0f);
    	}
    
    	float3 ambient = float3(0.1f, 0.1f, 0.1f);
    
    	return float4(ambient + diffuse + specular,1);
    }

     

    퐁모델 : 반사광과 카메라벡터 각도의 코사인 값을 구하고, 그 결과를 여러번 거듭제곱함.

    다만 코사인 연산은 내적 연산보다 무거워서 내적연산으로 대체한다. -1 다른방향 , 0 직각 , 1 같은 방향


    내적공식으로 코사인 값은 둘의 내적값에 두 벡터의 길이를 곱한 결과를 나누면 코사인 값이 나온다.

     

     

    COMMENT
     
    03
    16
    //int형 스왑 함수
    void num_swap(int &a, int &b)
    {
    	int temp;
        temp = a;
        a  = b;
        b= temp;
    }
    //float형 스왑 함수
    void num_swap(float &a, float &b)
    {
    	float temp;
        temp = a;
        a = b;
        b = temp;
    }

     

    위 둘 함수는 매개 변수만 다르고 작동원리는 같다. 

    이럴 때 함수나 클래스를 개별적으로 다시 작성하지 않고

    다른 수많은 자료형에서 작동하도록 해주는 틀으로서 템플릿을 사용한다.

     

    template <typename T>
    T num_swap(T &a, T &b)
    {
        T temp;
        temp = a;
        a = b;
        b = temp;	
    }
    
    // 사용할때는
    num_swap<int>(1,2);

     

    템플릿 동작 과정

    문법 오류 검사, 기계어로 번역하는 작업을 하는 과정, 컴파일에서 템플릿은

    무시하고 지나간다. 왜냐하면 초기에 자료형이 정해져 있지 않기 때문이다.

    그러다가 함수를 호출하면 자료형에 맞는 일반 함수를 찾다가 

    없다면 템플릿 함수를 참조한다. 템플릿으로 원하는 자료형으로 사용 가능하게 하는 

    일련의 과정이 있다.

     

    클래스 템플릿

    위에서 함수를 템플릿으로 만들었듯이, 클래스 또한 템플릿으로 

    만들 수 있다. 클래스를 템플릿 변수에 따라 생성할 수 있다.

    컨테이너 용도로 많이 씀 

     

    템플릿 선언문 class typename 차이

    template<class T>
    class Object;
    
    template<typename T>
    class Object;

     

    개인적으로 궁금했던 것인데, 기능상 차이는 없고

    typename이 중첩타입을 명시할때만 사용할때 사용하는 특성이 있다.

    템플릿의 템플릿을 구현할때 키워드가 구분되어야 함으로 두가지를 혼용해서

    쓰기도 하는 것 같다. 

    template <template <typename> class    T> class C { }; // valid!
    
    template <template <typename> typename T> class C { }; // invalid!

     

    https://stackoverflow.com/questions/2023977/difference-of-keywords-typename-and-class-in-templates

     

    Difference of keywords 'typename' and 'class' in templates?

    For templates I have seen both declarations: template < typename T > template < class T > What's the difference? And what exactly do those keywords mean in the following example (ta...

    stackoverflow.com

     

    typename이 더 직관적으로 보이기 때문에 더 선호된다고 한다.

    COMMENT
     
    03
    15
    COMMENT
     
    03
    02

    이전에 FBX 파일을 읽어와 다이렉트 환경에서 렌더링을 했었다.

    FBX는 광범위한 속성을 담을 수 있는 포맷으로

    특히 애니메이션이 있을때 유용하다.

     

    하지만 Obj 확장자는 매쉬, 텍스쳐, 메터리얼 3가지만 담는

    단순한 구조이다. 그래서 외부 라이브러리 없이도

    간단히 파싱해서 사용하기 좋다고 판단했다.

     

    OBJ (wavefront file format)

     

    아무 obj 파일 하나를 열어서 봐보자.

     

    # Blender3D v249 OBJ File: untitled.blend
    # www.blender3d.org
    mtllib cube.mtl
    v 1.000000 -1.000000 -1.000000
    v 1.000000 -1.000000 1.000000
    v -1.000000 -1.000000 1.000000
    v -1.000000 -1.000000 -1.000000
    v 1.000000 1.000000 -1.000000
    v 0.999999 1.000000 1.000001
    v -1.000000 1.000000 1.000000
    v -1.000000 1.000000 -1.000000
    vt 0.748573 0.750412
    vt 0.749279 0.501284
    vt 0.999110 0.501077
    vt 0.999455 0.750380
    vt 0.250471 0.500702
    vt 0.249682 0.749677
    vt 0.001085 0.750380
    vt 0.001517 0.499994
    vt 0.499422 0.500239
    vt 0.500149 0.750166
    vt 0.748355 0.998230
    vt 0.500193 0.998728
    vt 0.498993 0.250415
    vt 0.748953 0.250920
    vn 0.000000 0.000000 -1.000000
    vn -1.000000 -0.000000 -0.000000
    vn -0.000000 -0.000000 1.000000
    vn -0.000001 0.000000 1.000000
    vn 1.000000 -0.000000 0.000000
    vn 1.000000 0.000000 0.000001
    vn 0.000000 1.000000 -0.000000
    vn -0.000000 -1.000000 0.000000
    usemtl Material_ray.png
    s off
    f 5/1/1 1/2/1 4/3/1
    f 5/1/1 4/3/1 8/4/1
    f 3/5/2 7/6/2 8/7/2
    f 3/5/2 8/7/2 4/8/2
    f 2/9/3 6/10/3 3/5/3
    f 6/10/4 7/6/4 3/5/4
    f 1/2/5 5/1/5 2/9/5
    f 5/1/6 6/10/6 2/9/6
    f 5/1/7 8/11/7 6/10/7
    f 8/11/7 7/12/7 6/10/7
    f 1/2/8 2/9/8 3/13/8
    f 1/2/8 3/13/8 4/14/8

     

    v : 버텍스 로컬좌표

    vt : 텍스쳐 로컬좌표

    vn : 노말값 로컬좌표

    f : (v, vt, vn) 인덱스

     

    위 구조로 되어있으니, 이 데이터들을 파일입출력으로 읽어와

    다이렉트 버퍼들을 넘겨주면 된다.

    본 포스팅은 간단하게 버텍스 버퍼로만 렌더링 해보겠다.

     

    1. 파일 불러오기

     

    FILE* fp = nullptr;
    	_tfopen_s(&fp, objfile.c_str(), _T("rt"));
    	if (fp == NULL)
    	{
    		//파일이 없음
    		return false;
    	}

     

    2. 파일의 데이터 배열에 저장

     

    //실패인 경우 0 리턴 EOF은 -1
    	//한줄을 읽어옴
    	while (_fgetts(buffer, _countof(buffer), fp)!=0)
    	{
    		TCHAR type[6] = { 0, };
    		TCHAR value[256] = { 0, };
    
    		_stscanf_s(buffer, _T("%s"), type,(unsigned int)_countof(type));
    		//문자열이 같은 지 체크하는 함수 strcmp 와 같음
    		if (_tcscmp(type, L"v") == 0)
    		{
    			KVector3 vertex;
    			_stscanf_s(buffer, _T("%s %f %f %f\n"), value, (unsigned int)_countof(value),
    				&vertex.x, &vertex.y, &vertex.z);
    			m_vlist.push_back(vertex);
    		}
    		else if (_tcscmp(type, L"vt") == 0)
    		{
    			KVector2 uv;
    			_stscanf_s(buffer, _T("%s %f %f \n"), value, (unsigned int)_countof(value),
    				&uv.x, &uv.y);
    			m_vtlist.push_back(uv);
    		}
    		else if (_tcscmp(type, L"vn") == 0) {
    			KVector3 normal;
    			_stscanf_s(buffer, _T("%s %f %f %f\n"), value, (unsigned int)_countof(value),
    				&normal.x, &normal.y, &normal.z);
    			m_vnlist.push_back(normal);
    		}
    	
    		else if (_tcscmp(type, L"f") == 0) {
    			std::string vertex1, vertex2, vertex3;
    			unsigned int vertexIndex[3], uvIndex[3], normalIndex[3];
    			int matches = _stscanf_s(buffer, _T("%s %d/%d/%d %d/%d/%d %d/%d/%d %d/%d/%d\n"),
    				value, (unsigned int)_countof(type),
    				&vertexIndex[0], &uvIndex[0], &normalIndex[0],
    				&vertexIndex[1], &uvIndex[1], &normalIndex[1],
    				&vertexIndex[2], &uvIndex[2], &normalIndex[2]);
    			if (matches != 10) {
    				printf("File can't be read by our simple parser \n");
    				continue;
    			}
    			m_vertexindex.push_back(vertexIndex[0]);
    			m_vertexindex.push_back(vertexIndex[1]);
    			m_vertexindex.push_back(vertexIndex[2]);
    			m_uvindex.push_back(uvIndex[0]);
    			m_uvindex.push_back(uvIndex[1]);
    			m_uvindex.push_back(uvIndex[2]);
    			m_normalindex.push_back(normalIndex[0]);
    			m_normalindex.push_back(normalIndex[1]);
    			m_normalindex.push_back(normalIndex[2]);
    		}
    	}
    	
    
    	fclose(fp);
    	return true;
    }

     

     

    길지만 간단하다. 한줄 한줄 읽는 반복문안에서

    먼저 앞 알파벳을 읽어 오고

    v, vt, vn, f일때 구별해서 각 배열에 넣어줄 뿐이다.

     

    3. 버텍스 데이터 채우기

     

    	for (unsigned int index = 0; index < m_vertexindex.size(); index++)
    	{
    		unsigned int vertexindex = m_vertexindex[index];
    		unsigned int uvindex = m_uvindex[index];
    		unsigned int normalindex = m_normalindex[index];
    
    		PNCT_VERTEX pnct;
    		pnct.pos = m_vlist[vertexindex-1];
    		pnct.tex = m_vtlist[uvindex - 1];
    		pnct.normal = m_vnlist[normalindex - 1];
    		m_VertexList.push_back(pnct);
    	}
    	return true;

     

    최종으로 버텍스버퍼로 넘어갈 배열에 넣어준다.

    간단히 렌더링하기 위해 인덱스 순서대로, 해당 좌표를 넣어준다.

    바람직한 방법은 아니다. 출력이 되는지 확인하기 위해 사용했다.

     

    4. 결과

     

     

    만약에 출력된 오브젝트의 텍스쳐와 좌표가 거울로 본 것 처럼 반대로 되어있다면

    모델링 프로그램에서 Export할때, 방향을 제대로 설정했는지 확인한다.

    Direct는 왼손좌표계를 사용하고 대부분의 모델링 프로그램은

    오른손좌표계를 사용함으로 좌표계를 변환해야한다.

     

    COMMENT
     
    02
    23

     


    버퍼

     

    버퍼란 Shader 프로그래밍에서 GPU 자원(ID3D11Buffer : ID3D11Resource)를 뜻한다.

    버텍스 버퍼, 인덱스 버퍼, 상수 버퍼 등 이에 해당된다.

    셰이더와 프로그램이 연동되는 변수라고 생각하면 편하다.

     

    상수 버퍼

     

    상수버퍼는 말 그대로 상수를 데이터를 보내주는 버퍼로,

    앞서 얘기한 월드 변환 행렬, 뷰 변환 행렬, 투영 변환 행렬을 셰이더로 넘겨주어

    각 정점에 곱할 수 있게 해 준다. 

     

    1. 상수버퍼에 넣어줄 구조체

    struct CB_DATA
    {
    	KMatrix  matWorld;
    	KMatrix  matView;
    	KMatrix  matProj;
    	KMatrix  matNormal;
    	KVector4 vLightDir;
    	KVector4 vValue; // 시간 값 xyzw
    };

     

    상수 버퍼 구조체를 이용해서 넘기면 편하게 받을 수 있다.

    채워진 구조체 자체를 보낼 수 있기 때문이다. 

     

    2. 상수 버퍼 생성

    HRESULT KObject::CreateConstantBuffer()
    {
        HRESULT hr = S_OK;
        D3D11_BUFFER_DESC bd;
        ZeroMemory(&bd, sizeof(D3D11_BUFFER_DESC));
        bd.ByteWidth = sizeof(CB_DATA);
        bd.Usage = D3D11_USAGE_DEFAULT;
        bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
        D3D11_SUBRESOURCE_DATA data;
        ZeroMemory(&data, sizeof(D3D11_SUBRESOURCE_DATA));
        data.pSysMem = &m_cbData;
        hr = g_pd3dDevice->CreateBuffer(&bd, &data, &m_pConstantBuffer);
        if (FAILED(hr)) return hr;
        return hr;
    }

     

    Direct 디바이스로 버퍼를 만드는 함수 CreateBuffer()로

    m_pConstantBuffer주소에 버퍼를 만든다. 

    필요한 D3D11_BUFFER_DESC, D3D11_SUBRESOURCE_DATA 구조 포인터를 기입한다.  

     

    3. 상수버퍼의 갱신 및 셰이더 상수 버퍼 설정

     

    자체적으로 구현된 렌더 파이프라인에 넣어주면 된다. 

    리소스, 상수버퍼와 셰이더의 변수와 연동한다. 

     

       pContext->UpdateSubresource(
            m_pConstantBuffer.Get(), 0, NULL, &m_cbData, 0, 0);
    
        pContext->VSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());
        pContext->PSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());

     

    0, 1 숫자는 0번째 슬롯에 1개를 보내겠다는 의미로 

    Buffers <- s가 붙었다는 것은 여러 개를 보낼 수 있다는 뜻이다.

    자세한 함수 설명은 msdn을 참조..

     

    4. 셰이더

     

    //HLSL->c언와 유사하다.
    //정점쉐이더는 반드시 float4:SV_POSITION(레지스터)로 반환한다.
    //정점버퍼의 정점1개마다 호출된다.
    //POSITION(시멘틱:의미구조)
    
    cbuffer CBuf
    {
    	matrix g_matWorld : packoffset(c0);
    	matrix g_matView : packoffset(c4);
    	matrix g_matProj : packoffset(c8);
    	float4 vLightDir : packoffset(c12);
    	float4	vValue : packoffset(c13); 
    };
    
    struct VS_OUTPUT
    {
    	float4 p : SV_POSITION;
    	float3 n : NORMAL;
    	float4 c : COLOR0;
    	float2 t : TEXCOORD0;
    };
    
    VS_OUTPUT VS(float3 p: POSITION, float3 n : NORMAL, float4 c : COLOR, float2 t : TEXTURE) 
    {
    	VS_OUTPUT pOut = (VS_OUTPUT)0;
    	float4 vLocal = float4(p,1.0f);
    	float4 vWorld = mul(vLocal, g_matWorld);
    	float4 vView = mul(vWorld, g_matView);
    	float4 vProj = mul(vView, g_matProj);
    	pOut.p = vProj;
    	pOut.n = n;
    	pOut.c = c;
    	pOut.t = t;
    	return pOut;
    }

     

    상단에 있는 것이 상수버퍼 구조체이다. 

    셰이더에서 상수버퍼는 레지스터 단위로 할당이 되어야 한다. 

    float4가 기준이다. 행렬인 경우 (Matrix)는 float4 4개가 있는 것과 같으므로

    4개씩 늘어나고 float4는 1개씩 자리를 이동하는 모습이다.

     

    결과 - 공간 변환 (박스 출력)

     

     

    결과 - 상수버퍼로 시간을 전달, cos 값으로 정점 이동

     

    COMMENT
     
    02
    23

    상수 버퍼를 사용해서 셰이더에 원하는 어떤 변수든 넘겨줄 수 있지만

    가장 큰 이점은 각 정점을 3D 공간으로 변환해주는 행렬을

    넘겨줄 수 있는 것이라 생각한다.

    그래서 그 공간 변환부터 정리해볼까 한다.

     

    3D 렌더링 파이프라인

     

    공간 변환 

    출처&amp;amp;nbsp;https://www.flipcode.com/archives/Geometry_Skinning_Blending_and_Vertex_Lighting-Using_Programmable_Vertex_Shaders_and_DirectX_80.shtml

     

    한 정점이 렌더링이 되기까지의 렌더 파이프라인이다. 

    정점이 입력되면, 월드 변환, 뷰 변환, 투영 변환, 뷰포트 변환을

    거쳐 화면으로 출력이 된다.  이러한 파이프라인을 타는 이유는

    3D로 계산된 물체가 화면에 출력되는 결과는 결국 2D이기 때문이다. 

     

    로컬 좌표계, 모델 좌표계

     

    박스 하나를 띄운다고 해보자. 박스는 정점 8개로 이루어져있다. 

    박스의 각 정점들, 본인 고유 물체의 좌표를 로컬좌표계라고 한다.

     

     

    월드 변환

     

    로컬 좌표계 기준으로 박스 하나의 물체를 구성했다. 

    기존과 같은 박스를 하나 더 출력한다 했을 때, 물체는 겹치게 될 것이다.

    그래서 월드좌표계의 기준으로 정점을 변환한다. 월드 변환 행렬

    Scale, Translation, Rotation

    https://dlemrcnd.tistory.com/2?category=515796 

     

    DirectX11 3D - 이동 행렬, 신축 행렬(스케일), 회전 행렬

    1. 이동 행렬 (Translation Matrix) (x,y,z) 위치를 (tx, ty, yz)만큼 이동 _41, _42, _43의 행렬 요소를 곱한다. x+tx, y+ty, z+tz 다이렉트X에서 제공하는 함수는 다음과 같다. D3DXMATRIX matMatrix; D3DXMatr..

    dlemrcnd.tistory.com

    Translation matrix 이동행렬로 (월드 기준) 물체를 이동한다. 

     

    뷰 변환

     

    우리가 화면에서 물체를 보는 것은 카메라 방향이다. 

    화면에 나오는 물체는 카메라 방향 카메라 기준으로 변환한다.

    D3DXMatrixLookAtLH() 함수를 사용하면, 카메라 변환 행렬을 구할 수 있다. 

    D3DXMATRIX* D3DXMatrixLookAtLH(
        _Inout_  D3DXMATRIX *pOut,  // 카메라 변환 행렬
        _In_     const D3DXVECTOR3 *pEye,   // 카메라 위치
        _In_     const D3DXVECTOR3 *pAt,    // 카메라가 바라보는 지점
        _In_     const D3DXVECTOR3 *pUp     // 카메라의 상향 벡터
        );

     

    투영 변환

    위까지는 3D 좌표계이지만, 디스플레이에서는 2D 화면이다.

    https://www.labri.fr/perso/nrougier/teaching/opengl/

    투영 변환은 절단된 사각뿔 모양의 프러스텀 뷰 볼륨(P) 또는 평행뷰볼륨(O)

    를 사용해서, 시야의 near 가까운 면은 far 먼 면보다 작기 때문에

    카메라에 가까운 개체를 확장하는 인간의 원근감을 유사하게 구현한다. 

    옆에서 본 프러스텀 뷰 볼륨

    시야각이 90도인 옆으로 본 프러스텀 뷰 볼륨이 있다.

    원근법에서 카메라 룩 방향 Z축(카메라 깊이)에 반비례해서 작게 보여야한다.

    x,y축을 z로 모두 나누면. z/y의 값이 1이 되는 부분이 있다. 

    카메라 시야 범위 안 0~1로 카메라 밖에 있으면 보이지 않는 컬링이 된다.

    원근감이 적용된 장면은 Left, Top Right, Bottom 즉, 정규화된 장치 좌표계(NDC)공간에

    놓는 것이 투영 변환이다.

     

    실제 2차원의 투영은 레스터화 단계에서 원근 나눗셈을 할때 된다. 

    xyz 축 중에 z축을 제거하면 2D가 된다.

    하지만 z축을 제거하면 깊이 정보가 소실되어, z버퍼가 필요하다. 

    그래서 뎁스스텐실 작업을 따로 해주는 것이다. (깊이 버퍼용 텍스쳐 적용)

     

    투영엔 두 종류가 있는데 일반적으로 원근 투영을 사용한다.

    역시 행렬을 구해주는 함수가 Direct11 sdk에 포함되어 있다.

    D3DXMATRIX* D3DXMatrixPerspectiveFovLH(
      _Inout_  D3DXMATRIX *pOut,  // 투영 변환 행렬
      _In_     FLOAT fovy,  // 시야각
      _In_     FLOAT Aspect, // 종횡비
      _In_     FLOAT zn,  // 가까운 평면의 z 값
      _In_     FLOAT zf   // 먼 평면의 z 값
    );

     

    화면좌표 변환

     

    최종적으로 투영 변환 후에 윈도우의 어디쯤 출력한지 지정한다.

    ID3D11DeviceContext::RSSetViewports로 RS에서 알아서 행렬이 곱해진다고

    이해를 했다. 화면 크기를 윈도우 크기에 맞게 출력이 되어야 하기 때문에

    RS에서 레스터라이져 단계에서 다렉 화면 크기를 설정해준다.

    COMMENT
     
    02
    18

    FMOD API 

     

    언리얼, 유니티, 크라이엔진 등 많은 게임 엔진에서 탑재해 사용하는 사운드 라이브러리이다.

     

    출력 가능한 사운드 형식은

    ALFF, ASF, ASX, DLS, FLAC, FSB, IT, M3U, MID, MOD, MP2, MP3, OGG, WAV, WMA 등등..

    그냥 알고 있는 사운드 확장자는 다 지원한다고 생각해도 무방하다. 

     

    WINAPI이나 다이렉트에서도 사운드 관련 라이브러리를 제공하지만

    사용에 있어서 매우 편한 FMOD API를 사용해보도록 하겠다. 

    회원가입만 하면 무료로 사용 가능하다. 

     

    https://www.fmod.com/

     

    FMOD

    The sonic universe of Creaks We talked to the creative team responsible for the audio of Creaks, the latest game from renowned Czech game developer, Amanita Design. Visit blog

    www.fmod.com

     

     

    FMOD ENGINE 2.02 버전을 받으면 

    \FMOD Studio API Windows\api\core 경로에 포함 파일과

    라이브러리 파일을 본인 프로젝트에 사용하면 된다.

     

    참고로 라이브러리 파일의 dll, lib 파일이 있는데 

    dll은 동적 라이브러리, lib은 정적 라이브러리로

    dll은 실행파일에 lib은 프로젝트에 연결해주면 된다.

     

    1. 환경 구성

     

    헤더 파일과 라이브러리를 연결해준다.

    #include "fmod.hpp"
    #include "fmod_errors.h"
    #pragma comment(lib, "fmod_vc.lib")

     

    FMOD를 사용하기 위해서 3가지 인터페이스가 필요하다.

    FMOD::System*	m_pSystem = nullptr;
    FMOD::Sound*	m_pSound = nullptr;
    FMOD::Channel*	m_pChannel = nullptr;

     

    system은 다이렉트 디바이스와 유사하다. 사운드 시스템이다.

    sound은 사운드 포인터로 실제 사운드 메모리이다.

    channel은 최대 32 채널을 갖고 중복적으로 실행이 되기 위해서 필요하다. 오디오 채널이다.

     

    1. 시스템 생성

     

    bool KSoundManager::Init()
    {
    	FMOD_RESULT ret;
    	//FMOD 시스템 디바이스 생성
    	ret = FMOD::System_Create(&m_pSystem);
    	if (ret != FMOD_OK)
    	{
    		return false;
    	}
    	//FMOD 시스템 초기화 채널 32채널을 갖고 사운드 시스템 초기화
    	//배경음악도 있고 이펙트도 있음, 중복적으로 실행이 되는것 
    	//그 카운터가 32개의 채널이다. 
    	ret = m_pSystem->init(32, FMOD_INIT_NORMAL, 0);
    	if (ret != FMOD_OK)
    	{
    		return false;
    	}
    	return true;
    }

     

    FMOD::System_Create() 함수로 시스템을 생성했으면 사운드를 적재해 플레이할 수 있다. 

    사운드 객체는 사운드 파일과 1:1대 응이 되기 때문에, Sound 클래스와 Sound 매니저(싱글톤)를 구현해

    구현하였다. 하나의 FMOD시스템이 여러 FMOD 사운드와 FMOD 채널을 사용한다고 생각하면 된다. 

     

    2. 시스템 업데이트

     

    //프레임에서 반드시 업데이트를 호출시켜야함
    	m_pSystem->update();
    	return true;

     

    음악은 계속해서 나와야 한다. 그래서 자체적으로 구현한

    프레임 함수나 업데이트 함수에서(프로그램 루틴)

    FMOD 시스템의 update()가 계속 호출하게 해야 한다. 

     

    3. 사운드 로드

     

    KSound* sound = new KSound(m_pSystem, m_index, FileName);
    std::string mutiname = to_wm(filename);
    	ret = m_pSystem->createSound(mutiname.c_str(),
    		FMOD_DEFAULT, 0, &sound->m_pSound);
    	if (ret != FMOD_OK)
    	{
    		sound->Release();
    		return nullptr;
    	}
        //createSound 사운드 생성 완료 후 리스트에 추가
    	m_SoundList.insert(std::make_pair(m_index++, sound));
    	return sound;
    }

     

    생성했던 시스템 포인터로 createSound() 함수 호출해 사운드 메모리를 얻는다.

    멀티 바이트 문자로 파일 경로를 받아와야 한다. 본인은 유니코드로 구현되어

    자체 변환 함수로 바꿔주었다.

     

    4. 사운드 실행

     

    void KSound::SoundPlay(bool bloop)
    {
    	if (m_pChannel != nullptr)
    	{
    		m_pChannel->isPlaying(&m_bPlay);
    	}
    	if (m_bPlay == false)
    	{
    		//채널은 플레이 사운드에서 반환이됨
    		//FMOD API 에서 플레이 되는 사운드의 제어를 담당함 
    		FMOD_RESULT ret = m_pSystem->playSound(m_pSound,
    			nullptr, false, &m_pChannel);
    		if (ret == FMOD_OK)
    		{
    			//m_pChannel->setVolume(0.5f);
    			if (bloop)
    				m_pChannel->setMode(FMOD_LOOP_NORMAL);
    			else
    				m_pChannel->setMode(FMOD_LOOP_OFF);
    		}
    	}
    }

     

    사운드 포인터를 얻었다면, m_pSystem의 playSound() 함수로 사운드 포인터

    를 넘겨서 플레이할 수 있다. m_channel은 기본적으로 FMOD가 관리하는데

     

    우리는 channel 포인터로 정지, 볼륨, 루프 옵션 등을 조절할 수 있다. 

    위 함수에서 bool 값으로 플레이하는 사운드를 loop 할 것인지를 조절해주고 있다.

     

    또한 sound 포인터로 전체 길이를 받을 수도 있는 getlength()나

    channel 포인터로 현재 플레이 초를 받는 getPosition이 있으니

    자세한 것은 Document를 참조하도록 하자. 

     

    https://www.fmod.com/resources/documentation-api?version=2.02&page=core-guide.html#initialization-simple-start-up-with-no-configuration-necessary 

     

    FMOD - Core API Guide

    3. Core API Guide The FMOD Core API is a programmer API that is intended to cover the basics / primitives of sound. This includes concepts such as 'Channels', 'Sounds', 'DSP', 'ChannelGroups', 'Sound Groups', 'Recording' and concepts for 3D Sound and occlu

    www.fmod.com

     

    5. 시스템 해제

    m_pSystem->close();
    	m_pSystem->release();

     

    시스템을 종료할때, FMOD 시스템을 끄고 포인터를 해제해준다. 

     

    전체 코드 

     

    더보기

    KSoundManager.cpp

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    #include "KSoundManager.h"
    //인덱스로 사운드 포인터 반환
    KSound* KSoundManager::GetSound(int index)
    {
        auto iter = m_SoundList.find(index);
        if (iter != m_SoundList.end())
        {
            return (*iter).second;
        }
        return nullptr;
    }
    //사운드 메모리 적재하고 사운드 생성, 리스트 추가
    KSound* KSoundManager::LoadSound(std::wstring filename)
    {
        FMOD_RESULT ret;
     
        TCHAR szFileName[MAX_PATH] = { 0, };
        TCHAR Drive[MAX_PATH] = { 0, };
        TCHAR Dir[MAX_PATH] = { 0, };
        TCHAR FileName[MAX_PATH] = { 0, };
        TCHAR FileExt[MAX_PATH] = { 0, };
        std::wstring fullpathname = filename;
        _tsplitpath_s(fullpathname.c_str(), Drive, Dir, FileName, FileExt);
     
        //중복 방지 처리
        for (auto data : m_SoundList)
        {
            if (data.second->m_name == FileName)
            {
                return data.second;
            }
        }
     
        //동적 사운드 메모리 로드 생성자
        KSound* sound = new KSound(m_pSystem, m_index, FileName);
     
        //createSound 사운드 생성
        std::string mutiname = to_wm(filename);
        ret = m_pSystem->createSound(mutiname.c_str(),
            FMOD_DEFAULT, 0&sound->m_pSound);
        if (ret != FMOD_OK)
        {
            sound->Release();
            return nullptr;
        }
        //createSound 사운드 생성 완료 후 리스트에 추가
        m_SoundList.insert(std::make_pair(m_index++, sound));
        return sound;
    }
     
    bool KSoundManager::Init()
    {
        FMOD_RESULT ret;
        //FMOD 시스템 디바이스 생성
        ret = FMOD::System_Create(&m_pSystem);
        if (ret != FMOD_OK)
        {
            return false;
        }
        //FMOD 시스템 초기화 채널 32채널을 갖고 사운드 시스템 초기화
        //배경음악도 있고 이펙트도 있음, 중복적으로 실행이 되는것 
        //그 카운터가 32개의 채널이다. 
        ret = m_pSystem->init(32, FMOD_INIT_NORMAL, 0);
        if (ret != FMOD_OK)
        {
            return false;
        }
        return true;
    }
     
    bool KSoundManager::Frame()
    {
        //프레임에서 반드시 업데이트를 호출시켜야함
        m_pSystem->update();
        return true;
    }
     
    bool KSoundManager::Render()
    {
        return true;
    }
     
    bool KSoundManager::Release()
    {
        for (auto data : m_SoundList)
        {
            data.second->Release();
            delete data.second;
        }
        m_SoundList.clear();
     
        m_pSystem->close();
        m_pSystem->release();
        return true;
    }
     
    KSoundManager::KSoundManager()
    {
        m_index = 0;
        m_SoundList.clear();
        m_pSystem = nullptr;
    }
     
    KSoundManager::~KSoundManager()
    {
     
    }
     
    cs

     

    KSound.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    #include "KSound.h"
    //loop 플레이 사운드
    void KSound::SoundPlay(bool bloop)
    {
        if (m_pChannel != nullptr)
        {
            m_pChannel->isPlaying(&m_bPlay);
        }
        if (m_bPlay == false)
        {
            //채널은 플레이 사운드에서 반환이됨
            //FMOD API 에서 플레이 되는 사운드의 제어를 담당함 
            FMOD_RESULT ret = m_pSystem->playSound(m_pSound,
                nullptr, false&m_pChannel);
            if (ret == FMOD_OK)
            {
                //m_pChannel->setVolume(0.5f);
                if (bloop)
                    m_pChannel->setMode(FMOD_LOOP_NORMAL);
                else
                    m_pChannel->setMode(FMOD_LOOP_OFF);
            }
        }
    }
     
    //채널을 임시적으로 만들어 한번만 사운드
    void KSound::SoundPlay_Once()
    {
        FMOD::Channel* pChannel = nullptr;
        // 채널은 플레이 되는 사운드의 제어를 담당.
        FMOD_RESULT    ret = m_pSystem->playSound(
            m_pSound, nullptr, false&pChannel);
        if (ret == FMOD_OK)
        {
        }
    }
     
    //사운드 끄기
    void KSound::SoundStop()
    {
        if (m_pChannel != nullptr)
        {
            m_pChannel->stop();
        }
    }
     
    //사운드 멈춤 실행중일때는 멈추고 멈춘상태에서는 재실행
    void KSound::SoundPaused()
    {
        m_pChannel->isPlaying(&m_bPlay);
        (m_bPlay) ? m_bPlay = false : m_bPlay = true;
        m_pChannel->setPaused(m_bPlay);
    }
     
    //볼륨 관련
    void KSound::SoundVolumeDown(float vol)
    {
        m_fVolume -= vol;
        SoundVolume(m_fVolume);
    }
    void KSound::SoundVolumeUp(float vol)
    {
        m_fVolume += vol;
        SoundVolume(m_fVolume);
    }
    void KSound::SoundVolume(float vol)
    {
        if (m_pChannel != nullptr)
        {
            m_fVolume = max(0.0f, m_fVolume);
            m_fVolume = min(1.0f, m_fVolume);
            m_pChannel->setVolume(m_fVolume);
        }
    }
    //------------------------------------
    bool KSound::Init()
    {
        return true;
    }
     
    bool KSound::Frame()
    {
        if (m_pSound != nullptr || m_pChannel != nullptr) return true;
     
        //전체 길이
        m_pSound->getLength(&m_size, FMOD_TIMEUNIT_MS);
        //플레이 위치
        m_pChannel->getPosition(&m_pos, FMOD_TIMEUNIT_MS);
        return true;
    }
     
    bool KSound::Render()
    {
        return true;
    }
     
    bool KSound::Release()
    {
        if (m_pSound)
        {
            m_pSound->release();
            m_pSound = nullptr;
        }
        return true;
    }
     
     
    KSound::KSound()
    {
     
    }
     
    KSound::KSound(FMOD::System* system, int index, std::wstring name)
    {
     
        m_pSystem = system;
        m_id = index;
        m_name = name;
        m_bPlay = false;
        m_fVolume = 1.0f;
        m_size = 0;
        m_pos = 0;
    }
     
    KSound::~KSound()
    {
     
    }
     
    cs

     

    COMMENT
     
    02
    10

    이전 포스팅에서

    ODBC 라이브러리로 MS Access의 데이터베이스에 접근을 해서

    비주얼 스튜디오에서 사용을 했었다.

     

     

    온라인 서비스를 하는데 서버와 클라이언트가 있듯이

    데이터베이스도 마찬가지다.

    MySQL, Oracle, MS SQL Server 등과 같은 관계형 DBMS

    서버에 데이터베이스를 탑재해 사용한다. 

     

    요즘은 아마존이나 구글의 파이어베이스, Azure 등

    클라우드 서비스로 전환하는 추세이지만, 

     

    MS SQL Server로 DB서버를 만들어보려고 한다.

    https://www.microsoft.com/en-us/evalcenter/evaluate-sql-server-2016

     

    Try SQL Server 2016 with SP2 on Microsoft Evaluation Software

    Evaluations  |  180 days 5 |  Last Visited:

    www.microsoft.com

     

    설치 과정은 생략하겠다. 

    MS SQL Server Management Studio (SSMS)도 같이 설치해준다.

    https://docs.microsoft.com/ko-kr/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-ver15 

     

    SSMS(SQL Server Management Studio) 다운로드 - SQL Server Management Studio (SSMS)

    최신 버전의 SSMS(SQL Server Management Studio)를 다운로드합니다.

    docs.microsoft.com

     

    비쥬얼 스튜디오에서도 특정 데이터베이스 연결, 조회 처리 모두 가능하다.

    굳이 SSMS를 사용하는 이유는, SSMS는 DB에 중점적이기 때문이다.

    서버용 비주얼 스튜디오라고 생각하면 편하다.

    SQL Server, Azure SQL Database, Azure SQL Managed Instance 등 적용할 수 있다.

     

    설치가 완료되면 ODBC 관리자에서 SQL Server 드라이버를 확인 할 수 있다.

     

    SQL Sever 드라이버 확인

     

    ODBC로 접속하는 방법은 여러 방법 있다.

     

    저번 포스팅에서는DSN 파일 경로로 SQLConnect()함수를 사용해 연결했었다.

    SQLConnect() 함수를 호출하기 위해서 HDBC 핸들. DNS 이름, 아이디, 비밀번호가 필요하다.

     정해진 데이터베이스 계정과 연동하여 프로그램을 사용하면

    이 방법이 좋지만, 독립적인 방향의 프로그래밍이 되겠지만, 

     

    SQLDriverConnect() 함수를 사용한다면 대화 상자로 계정과 데이터베이스

    정보를 입력해, 데이터베이스 서버에 연결할 수 있다.

    이번에는 대화상자로 접속하는 방식을 사용했다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SQLRETURN SQLDriverConnect(  
         SQLHDBC         ConnectionHandle,  // 연결 핸들
         SQLHWND         WindowHandle,  // 윈도우 핸들 (창 유무)
         SQLCHAR *       InConnectionString,  // 연결 문자열
         SQLSMALLINT     StringLength1,  // 연결 문자열 사이즈
         SQLCHAR *       OutConnectionString,  // 완료된 연결 문자열 
         SQLSMALLINT     BufferLength, // 완료된 문자열 사이즈
         SQLSMALLINT *   StringLength2Ptr,// 총 문자 수
         SQLUSMALLINT    DriverCompletion);// 메시지 플래그
    cs

     

    GetDesktopWindow() 함수는 현재 표시된 윈도우 화면 핸들을 반환하는 것이다.

    2번째 파라미터에 윈도우 핸들 값을 넣어주면 된다. 

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    case 3://ms access 대화상자 버젼
            {
                HWND hWnd = GetDesktopWindow();
                SQLSMALLINT len;
                ret = SQLDriverConnect(handle_dbc, hWnd,
                    (SQLWCHAR*)L"Driver={Microsoft Access Driver (*.mdb, *.accdb)}", SQL_NTS,
                    (SQLWCHAR*)inCon, _countof(inCon),
                    &len, SQL_DRIVER_PROMPT);
            }break;
    cs

     

    아래 사진과 같은 창이 뜨게 되고  SQL Server 데이터베이스 연결할 수 있다.

     

    사전에 만든 GameDB에 접근한 모습

     

    COMMENT
     
    02
    03

    열 조회

    이전 포스팅에서는 직접 변수 타입을 지정해 테이블의

    열 타입과 바인딩해서 쿼리문을 사용했다.

     

    변수 선언 후 테이블 열 정보와 바인딩 SQLBindCol()

     

    확장성을 위해  초기에 테이블을 파싱을 해서 어떤 열 정보를 갖고 있는지

    파악 후, 열의 타입을 알아내 변수를 바인딩하는 작업이 바람직하다.

    그러기 위해서 SQLDescribeCol 함수가 있다.

     

    그전에 열 정보를 얻기 위해서는 해당 열의 인덱스가 필요하므로

    SQLNumResultCols() 함수로 열의 개수를 반환받는다.

     

    1
    2
    3
    SQLRETURN SQLNumResultCols(  
         SQLHSTMT        StatementHandle, //명령핸들
         SQLSMALLINT *   ColumnCountPtr); // 반환 버퍼 포인터
    cs

     

    총 열의 개수를 알아냈으니 열 인덱스에 맞게 열의 정보를 얻어오면 된다. 

     

    1
    2
    3
    4
    5
    6
    7
    8
    //열 총 사이즈
    SQLNumResultCols(handle_stmt, &총 열수);
     
    for (int 열위치 = 1; 열위치<= 총 열수; 열위치++)
        {
            SQLDescribeCol(열위치)
        }
     
    cs

     

    위 방식으로 조회하면 된다. (의사 코드)

     

    SQLDescribeCol

     

    Select문에 의해 생성된 결과로 표시된 열에 대한 결과, 정보를 반환한다.

    중요한 것은 이 SQLDescribeCol를 호출하기 전에 SQLPrepare(), 

    또는 SQLExecDirect() 함수를 호출해야 된다는 것이다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    cㅊSQLRETURN SQLDescribeCol(  
          SQLHSTMT       StatementHandle, //명령문 핸들
          SQLUSMALLINT   ColumnNumber, //1부터 시작하는 데이터의 열수
          SQLCHAR *      ColumnName, //null로 끝나는 열 이름 버퍼
          SQLSMALLINT    BufferLength,//버퍼, 문자의 길이
          SQLSMALLINT *  NameLengthPtr,//문자 수를 반환할 버퍼의 포인터
          SQLSMALLINT *  DataTypePtr,  //데이터 형식 반환 포인터
          SQLULEN *      ColumnSizePtr, // 열 크기 포인터
          SQLSMALLINT *  DecimalDigitsPtr, //열의 소수 자릿수 포인터
          SQLSMALLINT *  NullablePtr);  //출력 열이 Null 허용 여부 
    cs

     

    명령 핸들과 포인터를 매개변수로 열에 대한 여러 정보를 반환해준다.

    이러한 정보와 갖은 내용의 구조체를 만들어서

    그 구조체를 vector로 저장하도록 했다.

     

     

    데이터베이스의 인덱스는 0이 아닌 1부터 시작한다. 반복문도

    이 점을 유념해서 사용해야 한다. 

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    bool KODBC::Execute_TableSet(const TCHAR* tablename)
    {
        Table_Info table;
        table.table_name = tablename;
        std::wstring statement = L"select * from ";
        statement += tablename;
     
        SQLRETURN ret = SQLExecDirect(handle_stmt, (SQLTCHAR*)statement.c_str(), SQL_NTS);
        if (ret != SQL_SUCCESS)
        {
            Check();
            return false;
        }
     
        //결과집합의 콜 열수를 반환함
        SQLNumResultCols(handle_stmt, &table.table_col_num);
        //데이터 베이스는 행이 0이 아닌 1부터 시작함
        for (int icol = 1; icol <= table.table_col_num; icol++)
        {
            //열 정보 구조체 생성
            Table_ColumnInfo column;
            //행 수 
            column.col_num = icol;
            //열 이름 만큼 사이즈
            int col_size = _countof(column.col_name);
     
            /*SQLDescribeCol 결과 집합의 한 열에 대해 결과 설명자-
            열 이름, 형식, 열 크기, 10 진수 숫자 및 null 허용 여부를 반환 합니다. */
            SQLDescribeCol(handle_stmt,
                icol,
                column.col_name,
                col_size,
                &column.col_name_ptr,
                &column.col_data_type,// 데이터형
                &column.col_size_ptr,
                &column.col_decimal_ptr, // 10진수 자리수
                &column.col_nullable);// NULL 허용여부
            table.table_data.push_back(column);
        }
    }
    cs

     

    이렇게 해서 ODBC의 SQLDescribeCol() 를 사용해

    모든 데이터베이스의 테이블의 열의 정보를 알아내는 방법을 알아보았다.

    왜 이 함수를 사용하냐면,

     

    Select와 같은 sql 쿼리문 명령어를 사용할 때 필요한 변수 타입 바인딩을

    사전에 알아낸 열의 정보를 활용해 바인딩을 한다면,

    추후에 변경사항이 있어도 맞는 변수 타입으로 바인딩해주는,

    유지보수성이 있는 프로그래밍이 가능하다.    

     

    아래는 C타입 데이터 타입과 SQL 데이터 타입 비교표이다. 

     

    SQL 타입 ID
    SQL 데이터 타입 C 타입 ID ODBC C typedef C 데이터 타입
    SQL_CHAR CHAR(n) SQL_C_CHAR SQLCHAR * unsigned char *
    SQL_VARCHAR VARCHAR(n) SQL_C_CHAR
    SQL_WCHAR WCHAR(n) SQL_C_WCHAR SQLWCHAR * wchar_t *
    SQL_WVARCHAR VARWCHAR(n) SQL_C_WCHAR
             
        SQL_C_TCHAR SQLTCHAR *  
             
    SQL_BIGINT BIGINT SQL_C_SBIGINT SQLBIGINT _ _int64
    SQL_INTEGER INTEGER SQL_C_SLONG SQLINTEGER long int
    SQL_SMALLINT SMALLINT SQL_C_SSHORT SQLSMALLINT short int
    SQL_TINYINT TINYINT SQL_C_UTINYINT SQLCHAR unsigned char
             
    SQL_REAL REAL SQL_C_FLOAT SQLREAL float
    SQL_FLOAT FLOAT(p) SQL_C_DOUBLE SQLFLOAT double
    SQL_DOUBLE DOUBLE SQLDOUBLE
             
    SQL_BINARY BINARY(n) SQL_C_BINARY SQLCHAR * unsigned char *
    SQL_VARBINARY VARBINARY(n) SQL_C_BINARY
             
    SQL_TYPE_DATE DATE SQL_C_TYPE_DATE SQL_DATE_STRUCT struct tagDATE_STRUCT
    SQL_TYPE_TIME TIME(p) SQL_C_TYPE_TIME SQL_TIME_STRUCT struct tagTIME_STRUCT
    SQL_TYPE_TIMESTAMP TIMESTAMP(p) SQL_C_TYPE_TIMESTAMP SQL_TIMESTAMP_STRUCT struct tagTIMESTAMP_STRUCT

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    }
            //변수 타입 바인딩
            SQLLEN lTemp = 0;
            TCHAR szData[100][21= { 0, };
            int   iData[100];
            Table_Record record_data;
            //모든 열만큼 돌면서 변수 바인딩
            for (int iBind = 0; iBind < table.table_data.size(); iBind++)
            {
                switch (table.table_data[iBind].col_data_type)
                {
                case SQL_WCHAR://문자열 타입일때,
                case SQL_WVARCHAR: {
                    Table_Field data;
                    data.field_data_type = SQL_UNICODE;
                    ret = SQLBindCol(handle_stmt, iBind + 1,
                        SQL_UNICODE,
                        szData[iBind],
                        sizeof(szData[iBind]),
                        &lTemp);
                    if (ret != SQL_SUCCESS)
                    {
                        Check();
                        return false;
                    }
                    record_data.record.push_back(data);
                }break;
                case SQL_INTEGER: {
                    Table_Field data;
                    data.field_data_type = SQL_INTEGER;
                    ret = SQLBindCol(handle_stmt, iBind + 1,
                        SQL_INTEGER,
                        &iData[iBind],
                        0,
                        &lTemp);
                    if (ret != SQL_SUCCESS)
                    {
                        Check();
                        return false;
                    }
                    record_data.record.push_back(data);
                }break;
                case SQL_REAL: {
                    Table_Field data;
                    data.field_data_type = SQL_C_FLOAT;
                    ret = SQLBindCol(handle_stmt, iBind + 1,
                        SQL_C_FLOAT,
                        &iData[iBind],
                        0,
                        &lTemp);
                    if (ret != SQL_SUCCESS)
                    {
                        Check();
                        return false;
                    }
                    record_data.record.push_back(data);
                };
                }
            
            }
            std::cout << "" << std::endl;
    }
    cs

     

    문자, 정수, 실수 타입를 스위치문을 사용해

    맞는 타입으로 바인딩 후에 벡터에 저장하는 모습이다. 

    COMMENT
     
    01
    27

    ODBC 관리자

    ODBC 관리자

    ODBC 관리자에 해당 사용할 DBMS 드라이버가 있다면

    ODBC로 데이터베이스를 불러온 준비가 되었다.

     

     

    본인은 MS사의 Access를 사용할 것이므로 MS 드라이버를 다운로드하였다.

    설치 방법은 바로 이전 포스팅 참고하면 된다.

     

    ODBC 핸들 종류 및 할당 및 해제

    ODBC핸들에는 종류가 4가지가 있다.

     

    핸들 종류

     

    환경 핸들, 연결(접속) 핸들, 명령 핸들, 설명 핸들이 그것이다.

     

    할당 함수

    1
    2
    3
    4
    SQLRETURN SQLAllocHandle(  
          SQLSMALLINT   HandleType,//할당하고자 핸들 타입
          SQLHANDLE     InputHandle,//생성할 부모 핸들 지정
          SQLHANDLE *   OutputHandlePtr);// 생성할 핸들의 주소
    cs

     

    파라미터에 부모 핸들 지정이 있는데, 

    환경 핸들을 만들고 환경 핸들 갖고 접속 핸들을 만들고

    접속핸들을 가지고 접속에 성공하면 명령 핸들을 상속한다.

    상속관계를 가지고 있다.

     

    반환 값이 SQLRETURN 인데, 성공하면 SQL_SUCCESS를 반환한다. 

    SQL_ERROR 또는 SQL_SUCCESS_WITH_INFO 플래그가 뜬다면

    에러 처리를 하기 위한. SQLError() 함수가 있다.

     

    해제 함수

    1
    2
    3
    SQLRETURN SQLFreeHandle(  
         SQLSMALLINT   HandleType, //해제할 핸들 ㅏ입
         SQLHANDLE     Handle);  // 해제 
    cs

     


    환경 핸들, 연결(접속) 핸들, 명령 핸들을 사용해서

    MS Access 파일인 User_DB.accdb 파일을 불러와보자.

    파일

    1. ODBC 헤더 파일, 라이브러리를 추가한다.

    ODBC Core functions을 하는 헤더와 라이브러리 추가

    #include <sql.h>, #include <sqlext.h>

    링크 : obdc32.lib

    라이브러리는 기본으로 프로젝트에 링크가 되어있다.

     

    2. 환경 핸들 할당

    핸들 선언

    우선 환경 핸들을 SQLAllocHandle()함수로 할당한다.

     

     

    그런 후에 환경 버전 처리를 하는 SQLSetEnvAttr()함수를 호출한다.

     

     

    3. 연결 핸들 할당 및 연결 시도 

    환경 핸들을 부모로 두어 접속 핸들 할당한다.

     

     

    그런 다음 접속 시도인 SQLDriverConnect()함수를 호출한다.

    SQLDriverConnect()를 하기 위해서 드라이버 경로를 입력해줘야 한다.

     

    하드코딩

     

    위 방법처럼 직접 드라이버를 입력하는 건 좋지 않다.

    .dsn 파일의 규격의 텍스트 파일을 읽어서 연결해야 한다. 

     

    아래는 해당 프로젝트 안에 있는. dsn 파일을 읽어온 코드이다.

     

    4. 명령 핸들 할당 및 명령

    드라이버의 연결이 완료가 되었다면 명령 핸들을 할당해서 명령을 한다.

    연결이 되었다면 SQLAllocHandle 함수로 명령 핸들 할당한다. 

     

     

    명령문(쿼리문)부터 보자

     

    위 사진은 유저 테이블 안에 모든 값을 조회해달라는 쿼리문이다.

    SQLExecDirect 바로 명령어 처리하는 함수이다.

    1
    2
    3
    4
    SQLRETURN SQLExecDirect(  
         SQLHSTMT     StatementHandle, // 명령 핸들
         SQLCHAR *    StatementText,  // 실행할 SQL문
         SQLINTEGER   TextLength);  // SQL문의 문자열 
    cs

     

    Select문, Update문, Insert문, Delete문 모두 가능하다.

    다루지는 않겠다. 

    Select 문으로 그 값을 개발자가 조회해야 하는데,

    조회된 값을 얻어오기 위해서는

    c언어 변수와 테이블 변수와 연동을 해야 한다.  (Bind)

     

    SQLBindCol - 변수 Binding

    1
    2
    3
    4
    5
    6
    7
    SQLRETURN SQLBindCol(  
          SQLHSTMT       StatementHandle, //명령 핸들 
          SQLUSMALLINT   ColumnNumber,  //바인딩될 컬럼의 번호
          SQLSMALLINT    TargetType,  //바인딩 되는 변수의 데이터 타입
          SQLPOINTER     TargetValuePtr,  // 결과값 저장할 버퍼
          SQLLEN         BufferLength,  //버퍼의 길이
          SQLLEN *       StrLen_or_IndPtr);  //컬럼의 길이나 상태를 리턴
    cs

     

    반복문으로 결과 집합에서 다음 데이터 행 집합을 가져오고

    바인딩된 모든 열에 대한 데이터를 반환하는 함수 호출.

    SQLFetch() 함수 - 바인딩된 모든 열 데이터 반환

    1
    2
    SQLRETURN SQLFetch(  
         SQLHSTMT     StatementHandle);  
    cs

     

    데이터 SQL_NO_DATA 데이터가 없을 때까지 커서를 통해서 조회를 한다.

    시작 하기 전의 커서 상태 블록 커서는 결과 집합의 시작 부분 앞에 배치 됩니다. 새 행 집합의 첫 번째 행이 결과 집합의 시작 앞에 있으면 Sqlfetch 는 SQL_NO_DATA을 반환 합니다.
    종료 후의 커서 상태 블록 커서는 결과 집합의 끝 뒤에 배치 됩니다. 새 행 집합의 첫 번째 행이 결과 집합의 끝 뒤에 있으면 Sqlfetch 는 SQL_NO_DATA을 반환 합니다.

    SQL_NO_DATA는 데이터가 커서의 위치에서 데이터가 없을때 뜬다.

    while (SQLFetch(handle_HSTMT) != SQL_NO_DATA)로 

    커서 위치에 데이터가 없을 때까지 출력하도록 한다.

    만약 커서를 다 사용했다면 SQLCloseCursor(명령핸들); 로 커서를 초기화한다.

     

     

     

    결과 

    성공적으로 DB에 있는 데이터를 Select로 전체 조회를 했다. 

     

    전체 코드

    더보기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    #define _CRT_SECURE_NO_WARNINGS
    #include <windows.h>
    #include <tchar.h>
    #include <string>
    #include <sql.h>
    #include <sqlext.h>
    #include <iostream>
    //SQLHANDLE = void* 임 이름만 다름
    SQLHENV handle_HENV; // 환경핸들
    SQLHDBC handle_HDBC; // 접속핸들
    SQLHSTMT handle_HSTMT; // 명령핸들
    void Check();
    void main()
    {
        setlocale(LC_ALL, "");
        if (SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &handle_HENV) != SQL_SUCCESS)
        {
            return;
        }
        //환경 설정 환경 버젼 선택
        if (SQLSetEnvAttr(handle_HENV, SQL_ATTR_ODBC_VERSION,
             (SQLPOINTER)SQL_OV_ODBC3_80, SQL_IS_INTEGER) != SQL_SUCCESS)//버젼처리
        {
            return;
        }
        //환경 핸들 갖고 접속핸들 만듬 (환경핸들이 부모)
        if (SQLAllocHandle(SQL_HANDLE_DBC, handle_HENV, &handle_HDBC) != SQL_SUCCESS)
        {
            return;
        }
     
        //접속을 해야함 유니코드 버젼으로 하겠다. 버젼선택 mdb, accdb 파일을 읽어온다.
        //파일 경로 스트링
        SQLWCHAR dir[MAX_PATH] = { 0, };
        GetCurrentDirectory(MAX_PATH, dir);
        std::wstring dbpath = dir;
        dbpath += L"\\User_DB.dsn";
     
        TCHAR inCon[256= { 0, };
        TCHAR outCon[256= { 0, };
     
        //연결 Connect. _countof 문자열의 길이
        //원래는 .dsn 파일의 규격의 텍스트 파일을 읽어서 연결해야한다. 
        _stprintf(inCon, _T("FileDsn=%s"), dbpath.c_str());
        
        SQLSMALLINT cbOut_Len;
        SQLRETURN ret = SQLDriverConnect(handle_HDBC, NULL, inCon,_countof(inCon),
                        outCon, _countof(outCon), &cbOut_Len, SQL_DRIVER_NOPROMPT);
     
        //접속이 성공하면 success가 뜨는데 완벽한 성공, 두번째는 부족한대 성공 
        if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO)
        {
            Check();
            return;
        }
     
        if (SQLAllocHandle(SQL_HANDLE_STMT, handle_HDBC, &handle_HSTMT) != SQL_SUCCESS)
        {
            return;
        }
     
        SQLLEN lid = SQL_NTS;
        SQLLEN lname = SQL_NTS;
        SQLLEN llevel = SQL_NTS;
        int user_id = 0;
        int user_level= 0;
        TCHAR user_name[20= { 0, };
     
        //결과를 저장 바인드
        //첫번째 필드로 바인드 각각의 레코드를 반환해준다. 그중에 1번 필드
        //명령 핸들에 사용자 c++ 변수를 바인딩해줌 메모리 연결
        ret = SQLBindCol(handle_HSTMT, 1, SQL_C_ULONG, &user_id, 0&lid);
        ret = SQLBindCol(handle_HSTMT, 2, SQL_UNICODE, user_name, sizeof(user_name), &lname);
        ret = SQLBindCol(handle_HSTMT, 3, SQL_C_ULONG, &user_level, 0&llevel);
     
        //쿼리문 SQL문 demogame 테이블에서 모든 값을 출력
        TCHAR sql[MAX_PATH] = L"select * from user_table";
        ret = SQLExecDirect(handle_HSTMT, (SQLTCHAR*)&sql, SQL_NTS);
     
        // 행을 반환할 때 바인딩된 각 열에 대한 데이터를 
        // 해당 열에 바인딩된 버퍼에 넣는다
        while (SQLFetch(handle_HSTMT) != SQL_NO_DATA)
        {
            std::wcout << L"번호 : " << user_id << L"\t이름 : " <<
                user_name << L"\t레벨 : " << user_level<< std::endl;
        }
        SQLCloseCursor(handle_HSTMT);
     
        SQLFreeHandle(SQL_HANDLE_STMT, handle_HSTMT);
        SQLDisconnect(handle_HDBC);
        SQLFreeHandle(SQL_HANDLE_DBC, handle_HDBC);
        SQLFreeHandle(SQL_HANDLE_ENV, handle_HENV);
    }
    void Check()
    {
        SQLTCHAR szSQLState[SQL_SQLSTATE_SIZE + 1];
        SQLTCHAR errorBuffer[SQL_MAX_MESSAGE_LENGTH + 1];
        SQLINTEGER iSQLCode;
        SQLSMALLINT length;
        SQLError(handle_HENV, handle_HDBC,
            handle_HSTMT,
            szSQLState,
            &iSQLCode,
            errorBuffer,
            sizeof(errorBuffer),
            &length);
        MessageBox(NULL, errorBuffer, szSQLState, MB_OK);
    }
    cs

     

    COMMENT
     
    1 2 3 4 5 6 ··· 9