전체 글 (99)

  • 2022.05.11
  • 2022.05.03
  • 2022.04.27
  • 2022.04.26
  • 2022.04.22
  • 2022.04.13
  • 2022.04.11
  • 2022.04.09
  • 2022.04.04
  • 2022.03.25
  • 2022.03.22
  • 2022.03.21
  • 05
    11

    UPROPERTY()

    UPROPERTY()는 변수에 붙는 리플렉션 매크로다.

    매크로에 인자값을 넣어서 용도에 맞게 사용이 가능,

    인자 값 순서는 상관이 없지만, 규칙이 존재함

    1. 변수 공개 & 수정 권한

    VisibleDefaultsOnly // 인스펙터 창에서 보이기만 하고 수정 불가능

    VisibleInstanceOnly//월드 배치에서만

    VisibleAnywhere//둘다

     

    EditDefautOnly // 인스펙터 창에서 보이고 수정도 가능 에디터에서만

    EditInstanceOnly// 월드 배치에서만

    EditAnywhere// 둘다

     

    2. 블루 프린트 공개 & 수정 권한

    BlueprintReadOnly 블루프린트에서 해당 변수를 읽기가 가능합니다.
    BlueprintReadWrite 블루프린트에서 해당 변수를 읽기 & 쓰기가 가능합니다.
    BlueprintGetter 해당 변수에 접근 할 수 있는 함수를 지정하고 블루프린트는 해당 함수를 통해 변수에 접근합니다.
    BlueprintSetter 해당 변수에 수정 할 수 있는 함수를 지정하고 블루프린트는 해당 함수를 통해 변수에 수정합니다.

     

    3. Category

    디테일 패널의 항목 이름 명칭

     

    4. Meta

    Meta 키워드는 Metadata 지정자라고 하며 에디터 관련 다양한 기능을 구현가능  
    private으로 되어 있어도 "meta = (AllowPrivateAccess = "true")"명령어를 사용하여 에디터에서 보이게 할 수 있음
    이외에도 다양한 Metadata 명령어가 있음

    https://docs.unrealengine.com/ko/Programming/UnrealArchitecture/Reference/Metadata/index.html

     

    UFUNCTION()

    위는 변수용 매크로이며, UFUNCTION 함수용 매크로이다.

     

    1. Blueprintcallable

    c++에서 작성한 함수를 블루프린트에서 사용할 수 있게 하는 키워드, 재정의는 불가

     

    2. BlueprintPure

    값 하나 반환하는 함수에 사용하는 BlueprintPure 키워드, 아마도 매개변수 전용인듯

    UPARAM 매크로는 레퍼런스를 인자로 받아 올 수잇는 키워드이다.

     

    3. BlueprintImplementableEvent

    c++이 블루프린트 함수를 사용하고 싶을때 쓰는 매크로

    c++에서 블루프린트에서 사용할 함수 원형을 적어줘야함

     

    4. BlueprintNativeEvent

    c++에서 가상함수를 만드는 매크로

    c++에서 함수원형을 작성해야함

    COMMENT
     
    05
    03

    Github는 100mb 이상 파일과 최대 2G 파일을 지원하지 않는다.

    그래서 LFS를 사용, 분할 업로드를 통해 해결하는데,

    언리얼 프로젝트를 진행하니까 Github의 불편함을 느끼고

    마이크로소프트 사의 Azure Dev Ops를 사용하게 되었다. 

     

    https://app.vssps.visualstudio.com/_signedin?realm=dev.azure.com&protocol=wsfederation&reply_to=https%3A%2F%2Fdev.azure.com%2F 

     

    Azure DevOps Services | 로그인

    현재 사용자 환경에서 Microsoft Internet Explorer의 보안 강화 구성이 사용하도록 설정되어 있습니다. 이 보안 강화 수준으로 인해 웹 통합 환경이 정상적으로 표시되거나 작동할 수 없습니다. 작업을

    app.vssps.visualstudio.com

     

    우선 Azure Dev Ops를 켜서 새 레포지를 만든다.

     

    프로젝트 세팅에서 Github Connections으로

    기존 레포지와 연결할 수 있다. 

    연동이 되는 것이 아닌데, 왜 연결하는지는 아직 모르겠음, 

    아마도 소스코드같은 것 연동하는데 쓰이는 것 같다.

     

     

    Azure DevOps Clone

     

    비주얼스튜디오를 켜고 깃허브 창을 켠다. 

    복제를 누르면 Azure DevOps 같은 온라인 리포지토리를 복사할 수 있게 된다.

    아래 오른쪽 사진 Azure DevOps의 레포 지를 복사해온 모습이다.

     

     

    100mb가 넘어가는 언리얼의 리소스를 용량 제한 없이 올릴 수 있는 모습이다.

    프로젝트의 초기 세팅에 Azure로 변경하여, 앞으로 푸시하는데

    용량에 한에서 아무 걱정없이 할 수 있을 것 같다.

     

    참고로 언리얼에 필수적으로 필요한 파일들은

    Config, Content, Script, exe 만 필요하고 나머지는 Generate VS project file을 통해 생성하면 된다.

     

    .gitignore

    # Visual Studio 2015 user specific files
    .vs/
    
    # Compiled Object files
    *.slo
    *.lo
    *.o
    *.obj
    
    # Precompiled Headers
    *.gch
    *.pch
    
    # Compiled Dynamic libraries
    *.so
    *.dylib
    *.dll
    
    # Fortran module files
    *.mod
    
    # Compiled Static libraries
    *.lai
    *.la
    *.a
    *.lib
    
    # Executables
    *.exe
    *.out
    *.app
    *.ipa
    
    # These project files can be generated by the engine
    *.xcodeproj
    *.xcworkspace
    *.sln
    *.suo
    *.opensdf
    *.sdf
    *.VC.db
    *.VC.opendb
    
    # Precompiled Assets
    SourceArt/**/*.png
    SourceArt/**/*.tga
    
    # Binary Files
    Binaries/*
    Plugins/*/Binaries/*
    
    # Builds
    Build/*
    
    # Whitelist PakBlacklist-<BuildConfiguration>.txt files
    !Build/*/
    Build/*/**
    !Build/*/PakBlacklist*.txt
    
    # Don't ignore icon files in Build
    !Build/**/*.ico
    
    # Built data for maps
    *_BuiltData.uasset
    
    # Configuration files generated by the Editor
    Saved/*
    
    # Compiled source files for the engine to use
    Intermediate/*
    Plugins/*/Intermediate/*
    
    # Cache files for the editor to use
    DerivedDataCache/*
    COMMENT
     
    04
    27

    3D 정점은 로컬 -> 월드 -> 뷰 -> 투영 -> 화면 좌표로 최종 변환이 된다.

    마우스 피킹에서는 화면 좌표 -> 투영 - > 뷰 -> 월드  역순으로 돌아와야 한다.

    화면 좌표계에서 있는 마우스 클릭, 그 위치에서 월드 공간에서의 오브젝트를 체킹 하는 것이다.

     

    참고로, 투영 정점 -> 화면 좌표(스크린 좌표)로 변환될 때 뷰포트 행렬이 사용된다.

    뷰포트 행렬

    더보기

    뷰포트 행렬은 화면에 벗어나는 선분이나 오브젝트를 클리핑 하는 영역을 의미한다.

    뷰포트 행렬은 -1~1 범위를 0~1로 바꾸는 작업과 같음

    0~1로 된 것을 이제 화면 좌표계가

    0~화면 최대 크기(width)  0~화면 최대 높이(height)

    로 되어야 하기 때문에, 뷰포트 가로 및 세로를 크기를 곱하는 작업이다.

    뷰포트 행렬의 공식은 다음과 같다.

     

     

    화면 좌표를 투영 좌표로 변환

    화면 좌표 -> 투영 - > 뷰 -> 월드  역순으로 돌아와야 한다.

    우선 화면 좌표에서 투영 좌표로 변환하기 위해서

    위에 뷰포트 행렬의 역행렬을 사용하면 되지만, 

    공식이 있다. 

    vProj.x = (((2.0f * ptCursor.x - 2.0f * g_rtClient.left) / g_rtClient.right) - 1);
    vProj.y = -(((2.0f * ptCursor.y - 2.0f * g_rtClient.top) / g_rtClient.bottom) - 1);
    vProj.z = 1.0f;

    뷰 포트 영역이 윈도우의 전체 클라이언트와 동일하다면 뷰 포트의 시작 시점은 X = Y = 0 이 된다.

     

    투영 좌표를 뷰 좌표로 변환

    투영 좌표를 구했으면 투영 행렬의 역행렬을 곱해 뷰 정점으로 변환해야 한다.

    다만, 투영 행렬은 _11, _22 성분만이 정점에 영향을 미쳐서 

    방금 구한 투영 좌표에 카메라의 투영 행렬 _11, _22 성분만 나누면 같은 결과가 나온다.

    vView.x = vProj.x / m_pCamera->m_matProj._11;
    vView.y = vProj.y / m_pCamera->m_matProj._22;
    vView.z = vProj.z;

     

    뷰 좌표에서 월드 좌표로 변환

    카메라 뷰 행렬의 역행렬을 곱하면 된다.

    카메라에서 피킹을 하는 것이기 때문에, 뷰 좌표계에서

    레이의 시작은 무조건 (뷰 좌표에서) 원점인 0,0,0이다.

    //뷰좌표계에서는 시작은 무조건 0,0,0
    KMatrix matInverse;
    D3DKMatrixInverse(&matInverse, nullptr, &m_pCamera->m_matView);
    
    //카메라의 월드 좌표의 레이를 만든다.
    TRay ray;
    ray.direction.x = vView.x * matInverse._11 + vView.y * matInverse._21 + vView.z * matInverse._31;
    ray.direction.y = vView.x * matInverse._12 + vView.y * matInverse._22 + vView.z * matInverse._32;
    ray.direction.z = vView.x * matInverse._13 + vView.y * matInverse._23 + vView.z * matInverse._33;
    
    ray.position.x = matInverse._41;
    ray.position.y = matInverse._42;
    ray.position.z = matInverse._43;

    위에서 직접 정점을 행렬로 곱한 코드이다. 다이렉트 내장 함수 써도 무방하다.

     

    D3DXVec3Normalize(&ray.direction, &ray.direction);

    이제 레이는 월드 좌표의 정점이 되었다.

    정규화를 하고, 이 레이로 박스 AABB, OBB, 구, 평면 등 교점 계산을 해

    충돌 처리, 마우스 피킹을 하면 된다.  

     

    레이와 면의 충돌 체크를 하면, 모든 오브젝트를 검출할 수 있지만,

    많은 연산양을 요구해서 보통 콜라이더, 바운딩 박스와 같이 박스로 많이

    하기도 한다. AABB는 축이 회전이 되지 않는 고정된 박스이고 OBB는 회전되는 박스이다.

     

    박스와 레이의 충돌

    레이와 AABB박스의 교점을 4개를 구한다.

    (Y에 대해서 최소 Y값 최대 Y값, X에 대해서 최소 X값 최대 X값)

    위의 사진에서 충돌 났을 때와 안 났을 때와 차이는

    왼쪽은 MinX < MaxY 일 때 충돌이 안되고

    오른쪽은 (MinY <MaxX && MaxY> MinX)일 때 충돌임

     

     

    하지만 효율을 위해 vMax의 xyz값 중 가장 최소 거리 값,

    vMin의 xyz값 중 가장 최대 거리 값을 찾아 vMax의 최소 거리 값이 크면 충돌이다.

     

    결과

    마우스 피킹

    바운딩박스(박스 콜라이더)를 사용해서

    오브젝트를 검출해서 선택된 오브젝트를 출력하는 모습

     

    COMMENT
     
    04
    26

    기본적으로 맵에 있는 텍스쳐 한 장, 멀티 텍스쳐를 적용할 4개의 텍스쳐를 준비한다.

    왜 4개로 레이어를 구성하냐면, 한 개의 텍스쳐에서

    RGBA값으로 각각의 텍스쳐의 비율을 담아 놓기 때문이다.

    그 텍스쳐는 멀티 마스크 텍스쳐 역활을 한다.  

    추가적으로 텍스쳐를 붙이고 싶다면, 추가적인 마스크 텍스쳐를 만들면 된다.

     

    마스크 텍스쳐에 따라 적용되는 멀티 텍스쳐링

     

    맵툴에 적용하기 위해 브러시를 사용해서 마스크 텍스쳐를 수정해야 한다.

    여러 방법이 있겠지만, Compute Shader를 사용하면 GPU를 사용해

    브러시 연산을 빠르게 처리할 수 있다. (구조체 상수버퍼로 넘겨서 연산)

    또한, 마스크 텍스쳐도 원본 이미지, 복사될 이미지를 받아

    수정을 한뒤에 다시 UNMAP하여, 누적되게 텍스쳐에 그릴 수 있게 구현한다.

     

    KMapSprite.cpp

    bool KMapSprite::Init(ID3D11DeviceContext* pContext, KMap* pMap)
    {
    	if (pMap == nullptr)return false;
    	if (pContext == nullptr)return false;
    	//브러쉬 버퍼
    	m_pMap = pMap;
    	m_pContext = pContext;
    	m_Pickbuffer.fRadius = 20.0f;
    	m_Pickbuffer.iIndex = 0;
    	m_Pickbuffer.vPickPos = KVector3(0, 0, 0);
    	m_Pickbuffer.vRect[0] = KVector3(-0, 0, 0);
    	m_Pickbuffer.vRect[1] = KVector3(0, 0, 0);
    	m_Pickbuffer.vRect[2] = KVector3(0, -0, 0);
    	m_Pickbuffer.vRect[3] = KVector3(-0, 0, 0);
    	//
    	//Structed Buffer, SRV 생성 : cs에 보내는 버퍼
    	CreateStructuredBuffer(g_pd3dDevice, sizeof(PICKBUFFER), 1, &m_Pickbuffer, m_pPickBuffer.GetAddressOf());
    	CreateBufferSRV(g_pd3dDevice, m_pPickBuffer.Get(), m_pPickBufferSRV.GetAddressOf());
    	//unorder access view 생성 : SRV 순서와 상관없이 다른 리소스에 대한 읽기 및 쓰기 허용
    	CreateBufferUAV(g_pd3dDevice, m_pMap->m_BoxCollision.size.x, m_pMap->m_BoxCollision.size.z, m_pResultUAV.GetAddressOf());
    	//CS 쉐이더 생성
    
    	KShader* pCS = g_ShaderManager.CreateComputeShader(L"../../data/shader/CS_Terrian.hlsl");
    	m_pCS = pCS->m_pComputeShader;
    
    	//기존텍스쳐가 아닌 맵의 텍스쳐를 복사되어지는 텍스쳐로 바꾼다.
    	pMap->m_pMapAlphaResultSRV = m_pTextureCopySRV.Get();
    	return true;
    }

     

    맵 스프라이팅 클래스의 초기화 함수이다. 

    1. CreateStructedBuffer()와 CreateBufferSRV()는 CS에 보낼 구조체 버퍼를 만드는 함수로

    구조체의 사이즈 맞게 버퍼를 생성

    2. CreateBufferUAV() Unorder Access View 버퍼를 생성하는 함수로 

    순서와 상관없이 다른 리소스에 대한 읽기 및 쓰기를 허용하는 버퍼 생성,

    3. ComputeShader를 생성한다.

     

    KMapSprite::CreateBufferUAV()

    HRESULT KMapSprite::CreateBufferUAV(ID3D11Device* pDevice, int iWidth, int iHeight, ID3D11UnorderedAccessView** ppUAVOut)
    {
    	HRESULT hr = S_OK;
    	//before dispatch
    	D3D11_TEXTURE2D_DESC textureDesc;
    	ZeroMemory(&textureDesc, sizeof(textureDesc));
    	textureDesc.Width = iWidth;
    	textureDesc.Height = iHeight;
    	textureDesc.MipLevels = 1;
    	textureDesc.ArraySize = 1;
    	textureDesc.SampleDesc.Count = 1;
    	textureDesc.SampleDesc.Quality = 0;
    	textureDesc.Usage = D3D11_USAGE_DEFAULT;
    	textureDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE;
    	textureDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; DXGI_FORMAT_R32G32B32A32_FLOAT;
    	hr = pDevice->CreateTexture2D(&textureDesc, NULL, m_pTexture.GetAddressOf());
    
    	D3D11_UNORDERED_ACCESS_VIEW_DESC viewDescUAV;
    	ZeroMemory(&viewDescUAV, sizeof(viewDescUAV));
    	viewDescUAV.Format = textureDesc.Format;
    	viewDescUAV.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D;
    	viewDescUAV.Texture2D.MipSlice = 0;
    	hr = pDevice->CreateUnorderedAccessView(m_pTexture.Get(), &viewDescUAV, ppUAVOut);
    
    	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
    	ZeroMemory(&srvDesc, sizeof(srvDesc));
    	srvDesc.Format = textureDesc.Format;
    	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
    	srvDesc.Texture2D.MipLevels = 1;
    	hr = pDevice->CreateShaderResourceView(m_pTexture.Get(), &srvDesc, m_pTextureSRV.GetAddressOf());
    
    	// 복사본
    	hr = pDevice->CreateTexture2D(&textureDesc, NULL, m_pTextureCopy.GetAddressOf());
    	hr = pDevice->CreateShaderResourceView(m_pTextureCopy.Get(), &srvDesc, m_pTextureCopySRV.GetAddressOf());
    	return hr;
    }

     

    텍스쳐의 BindFlags를 D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE로 설정한다.

    CreateUnorderedAccessView()로 m_pTexture를 UnorderedAccessView 버퍼로 만들며,

    이 텍스쳐가 수정이 되면 복사되어 m_pTextureCopy를 얻고 이를 가지고 맵이 이 텍스쳐를 사용하면 된다.

    pMap->m_pMapAlphaResultSRV = m_pTextureCopySRV.Get();

     

    그러면 마우스가 눌렀을 때 컴퓨트 쉐이더를 수행하여 브러시에 맞게 그려지게 하면 된다.

    매프레임 마다 호출되는 Frame()함수이다.

    bool KMapSprite::Frame()
    {
    	if (g_InputData.bMouseState[0])
    	{
    		m_pContext->UpdateSubresource(m_pPickBuffer.Get(), 0, NULL, &m_Pickbuffer, 0, 0);
    		//맵의 텍스쳐를 복사해온다.
    		ID3D11ShaderResourceView* aRViews[3] = { m_pMap->m_pTexture_Diffuse->m_pSRVTexture.Get(), m_pTextureCopySRV.Get(), m_pPickBufferSRV.Get()};
    		RunComputeShader(m_pContext, m_pCS.Get(), 3, aRViews, NULL, NULL, 0,
    			m_pResultUAV.GetAddressOf(),
    			m_pMap->m_BoxCollision.size.x/32, m_pMap->m_BoxCollision.size.z / 32, 1);
    
    		m_pContext->CopyResource(m_pTextureCopy.Get(), m_pTexture.Get());
    	}
    	return true;
    }

     

    아래는 ComputeShader 수행 함수이다.

    void KMapSprite::RunComputeShader(ID3D11DeviceContext* pContext, ID3D11ComputeShader* pComputeShader, UINT nNumViews, ID3D11ShaderResourceView** pShaderResourceViews, ID3D11Buffer* pCBCS, void* pCSData, DWORD dwNumDataBytes, ID3D11UnorderedAccessView** pUnorderedAccessView, UINT X, UINT Y, UINT Z)
    {
    	pContext->CSSetShader(pComputeShader, NULL, 0);
    	pContext->CSSetShaderResources(0, nNumViews, pShaderResourceViews);
    	pContext->CSSetUnorderedAccessViews(0, 1, pUnorderedAccessView, NULL);
    	if (pCBCS)
    	{
    		D3D11_MAPPED_SUBRESOURCE MappedResource;
    		pContext->Map(pCBCS, 0, D3D11_MAP_WRITE_DISCARD, 0, &MappedResource);
    		memcpy(MappedResource.pData, pCSData, dwNumDataBytes);
    		pContext->Unmap(pCBCS, 0);
    		ID3D11Buffer* ppCB[1] = { pCBCS };
    		pContext->CSSetConstantBuffers(0, 1, ppCB);
    	}
    
    	pContext->Dispatch(X, Y, Z);
    
    	pContext->CSSetShader(NULL, NULL, 0);
    
    	//다시 널로 바꿔주는 작업
    	ID3D11UnorderedAccessView* ppUAViewNULL[1] = { NULL };
    	pContext->CSSetUnorderedAccessViews(0, 1, ppUAViewNULL, NULL);
    
    	ID3D11ShaderResourceView* ppSRVNULL[2] = { NULL, NULL };
    	pContext->CSSetShaderResources(0, 2, ppSRVNULL);
    
    	ID3D11Buffer* ppCBNULL[1] = { NULL };
    	pContext->CSSetConstantBuffers(0, 1, ppCBNULL);
    }

    기존 텍스쳐 복사 텍스쳐, 구조체 상수 버퍼, UAV 버퍼를 설정하고

    Map은 인터락, 크리티컬 섹션처럼 공유 자원의 독점, 안전하게 수행할 수 있다.

     

    CS_Terrian.hlsl

    Texture2D<float4>		InputMap : register(t0);
    Texture2D<float4>		CopyMap : register(t1);
    RWTexture2D<float4>		OutputMap : register(u0);
    
    struct CBuf_Brush
    {
    	float3 vPickPos;
    	float3 vRect[4];
    	float  g_fRadius;
    	int    iIndex;
    };
    StructuredBuffer<CBuf_Brush> Buffer0 : register(t2);
    // Group size
    #define size_x 32
    #define size_y 32
    [numthreads(size_x, size_y, 1)]
    void CS(uint3 GroupID : SV_GroupID, uint3 DispatchThreadID : SV_DispatchThreadID, uint3 GroupThreadID : SV_GroupThreadID, uint GroupIndex : SV_GroupIndex)
    {
    	int3 texturelocation = int3(0, 0, 0);
    	// 0 ~ 1024, 1024
    	texturelocation.x = GroupID.x * size_x + GroupThreadID.x; // u
    	texturelocation.y = GroupID.y * size_y + GroupThreadID.y; // v
    	//texturelocation.x = DispatchThreadID.x; //위랑 같음
    	//texturelocation.y = DispatchThreadID.y;
    
    	float4 Color = InputMap.Load(texturelocation);
    	// 0 ~1 
    	float2 uv = float2(texturelocation.x / 1280.0f, //현재 지형 크기 1280 고정
    		texturelocation.y / 1280.0f);
    	// vRect[0]   ~   vRect[1]  
    	float1 width = (Buffer0[0].vRect[1].x - Buffer0[0].vRect[0].x) / 2.0f;
    	//
    	// vRect[3]   ~   vRect[2]  
    	float1 height = (Buffer0[0].vRect[0].y - Buffer0[0].vRect[3].y) / 2.0f;
    	float3 vPos = float3((uv.x * 2 - 1.0f) * width,
    		-(uv.y * 2 - 1.0f) * height,
    		0.0f);
    
    	float fRadius = distance(vPos.xyz, Buffer0[0].vPickPos.xyz);
    	//텍스쳐 복사한것
    	float4 fAlpha = CopyMap.Load(texturelocation);
    	float fDot = 1.0f - (fRadius / Buffer0[0].g_fRadius);
    
    	//4개의 텍스쳐의 알파값을 저장한 텍스쳐
    	switch (Buffer0[0].iIndex)
    	{
    		case 0: fAlpha.x = max(fAlpha.x, fDot); break;
    		case 1: fAlpha.y = max(fAlpha.y, fDot); break;
    		case 2: fAlpha.z = max(fAlpha.z, fDot); break;
    		case 3: fAlpha.w = max(fAlpha.w, fDot); break;
    	}
    	OutputMap[texturelocation.xy] = float4(fAlpha.xyzw);
    }

     

    상수 버퍼로 받아온 브러시의 형태에 따라, 원 영역을 계산하고

    브러쉬의 iIndex에 따라 스위치 조건문으로 각 색 영역에 알파 값을 넣는다.

    그리고 최종적으로 완성된 출력된 리소스를 쓴다. 

     

    VSPS_Terrain.hlsl

      float4 albedo = g_txDiffuse.Sample(g_Sample, Input.t * 4); //알베도 기본 색상 텍스쳐
       float4 mapMask = g_txMapMask.Sample(g_Sample, Input.t ); // 맵 마스크 작업
    
       float4 subTexture1 = g_txSubTex1.Sample(g_Sample, Input.t * 4); // 맵 서브 텍스쳐
       float4 subTexture2 = g_txSubTex2.Sample(g_Sample, Input.t * 4); // 맵 서브 텍스쳐
       float4 subTexture3 = g_txSubTex3.Sample(g_Sample, Input.t * 4); // 맵 서브 텍스쳐
       float4 subTexture4 = g_txSubTex4.Sample(g_Sample, Input.t * 4); // 맵 서브 텍스쳐
       
       albedo = lerp(albedo, subTexture1, mapMask.r); // 첫번째 레이어 작업
       albedo = lerp(albedo, subTexture2, mapMask.g); // 두번째 레이어 작업
       albedo = lerp(albedo, subTexture3, mapMask.b); // 세번째 레이어 작업
       albedo = lerp(albedo, subTexture4, mapMask.a); // 네번째 레이어 작업

     

    맵의 쉐이더의 한 부분이다. 

    각각 텍스쳐를 불러오고, 마스크 텍스쳐에 적용된 RGBA 값을 활용해

    기본 텍스쳐 알베도 위에 색을 Lerp() 내장함수로 보간한다. 

     

    결과

     

    왼쪽 지형 최종 텍스쳐, 오른쪽 마스크 텍스쳐

     

    COMMENT
     
    04
    22

    알파 테스팅과 알파 블랜딩은 알파채널을 이용해서 투명하게 만들어준다.

     

    알파 블랜딩은 알파맵, 알파 채널의 비율에 따라서 투명하게 만든다.

    알파 테스팅은 50%의 알파 채널 이하는 투명도가 없는 것으로 한다.

    극단적으로 투명인지 아닌지 판별하는 작업이다. 

     

    알파 소팅 (Alpha Sorting)

    반투명 오브젝트 끼리 렌더링 할때는 Zbuffer의 문제가 생긴다.

    렌더링할때, 가려진 오브젝트는 렌더링하지않는다. (Culling)

    하지만 반투명, 투명은 뒤에 있는 오브젝트가 그려져야한다.

     

     

    위는 본인 다이렉트 엔진에서 나무잎을 그리는 사진으로 

    알파값 뒤에 오브젝트가 가려져서 컬링이 되고 있는 모습이다.

     

    출처 :&nbsp;https://www.slideshare.net/jpcorp/5-10351002

     

    알파 값이 있는 것은 Zbuffer문제가 생기고 

    알파소팅의 헛점이라고 한다.  

     

    알파 테스팅

    이를 해결하는 Zbuffer의 ZWriteZRead나 알파테스팅 여러 꼼수가 있지만, 

    알파 테스팅을 사용하면 알파 블렌딩의 앞뒤 판정이 쉽게 해결된다. 

    픽셀셰이더에서 알파맵의 알파를 뺀 최종 알파값을

    0.5f 절반보다 작다면 그냥 그 픽셀을 날려버린다. 

    알파 값이 0.5f 절반이라면 CutOff 해버림

    알파 테스팅은 50%의 알파 채널 이하는 투명도가 없는 것으로 취급하는거다.

    discard는 픽셀을 아예 버리는것이다. 

    결과

     

    나무잎이 풍성하게 잘 보이는 모습이다.

    픽셀쉐이더 사용해서 알파테스팅을 했는데,

    DirectX의 Output Merger Stage OM statge에서 Blend State의 설정에 따라

    알파테스팅이 가능하다.

    AlphatoCoverageEnable은 Direcx10부터 정식으로 생긴 기능으로

    멀티 샘플링에서 렌더타겟에 픽셀값을 설정할때의 알파값을 사용할지 설정한다.

     

    HRESULT KState::CreateBlendState()
    {
        HRESULT hr = S_OK;
        D3D11_BLEND_DESC bd;
        ZeroMemory(&bd, sizeof(D3D11_BLEND_DESC));
    
        bd.AlphaToCoverageEnable = false;
        bd.IndependentBlendEnable = true;
        bd.RenderTarget[0].BlendEnable = TRUE;
        bd.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
        bd.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
        bd.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
        //// A 연산 저장
        bd.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
        bd.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
        bd.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
        bd.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
        hr = g_pd3dDevice->CreateBlendState(&bd, &g_pBlendState);
        if (FAILED(hr))
        {
            return hr;
        }
    COMMENT
     
    04
    13

    1. 입방체 환경 매핑

    환경 매핑이란 물체에 비친 주변 환경을 물체의 표면에 나타내는 것이다.

    유니티에 있는 Skybox shader들

    유니티에서는 입방체 6면체 (6개의 상하좌우 전후)가

    필요한 큐브 맵(Cubemap)이나

    한 장의 텍스쳐(Panoramic) HDR 파일을 사용하는

    쉐이더가 있음을 확인할 수 있다.

     

    2. 큐브 텍스쳐

    다이렉트 X11에 큐브 맵을 적용하는 방법은 3가지가 있다.

     

    1. 텍스쳐를 배열로 넘겨서 한번 렌더링 하는 법

    2. 6개의 면을 하나씩 렌더링 하는 법

    3. CubeMap으로 렌더링 하는 법

     

    이 포스팅에서는 3번만 다룬다. 

    Cubemap은 6개의 각 면이 텍스처 배열과 같이 저장되어 있는 타입이다.

    텍스쳐를 만들 때, 다이렉트 툴킷 (DirectXTK)의 CreateDDSTextureFromFile을

    사용했었다. 참고로, 다이렉트 툴킷은 큐브 맵을 완벽히 지원하지 않는다.

    (예를 들어 밉맵 버전을 지원하지 않는다, 압축 포맷 지원도 다른 걸로 알고 있음)

    완벽히 지원하는 건 다이렉트 텍스쳐 라이브러리를 사용하면 된다.

    본인은 다이렉트 툴킷을 사용한다. 

     

        HRESULT hr = DirectX::CreateWICTextureFromFile(
            g_pd3dDevice,
            filename.c_str(),
            m_pResourceTexture.GetAddressOf(),
            m_pSRVTexture.GetAddressOf());
        if (FAILED(hr))
        {
            hr = DirectX::CreateDDSTextureFromFile(
                g_pd3dDevice,
                filename.c_str(),
                m_pResourceTexture.GetAddressOf(),
                m_pSRVTexture.GetAddressOf());
        }
        if (FAILED(hr))
        {
            return false;
        }

     

    이렇게 함수를 통해 파일로 텍스쳐를 만들었었다.

    CubeMap 파일을 불러와서 쉐이더에 넘겨주면 된다.

     

    2. 입방체 큐브 맵 파일

    큐브 맵 파일이 있어야 한다.

    DDS파일의 큐브 맵을 인터넷을 다운로드하거나,

    아니면 직접 제작할 수 있다.

    우선 상하좌우 전후 각 6개의 텍스쳐가 있어야 한다. 

    https://opengameart.org/content/miramar-skybox

     

    Miramar skybox

    Miramar skybox, made by hipshot, 6* 1024*1024 TGA images.

    opengameart.org

     

    위에 링크에는. tga파일의 6개 파일의 무료 소스 텍스쳐가 있다.

     

    위에서 받은 텍스쳐 6개로 디렉트 툴킷 유틸리티인 DxTex로 제작 가능하다.

    DirectXTK 다운로드하면 기본적으로 Utilities 폴더에 있다.

     

     

    프로그램을 켰으면 새 텍스쳐를 만든다.

    Type은 Cubemap Texture로 포맷은 1-bit

    알파 값을 가진 Four CC 4-bit : DXT1으로 진행한다. 

     

     

    View 탭에서 여러 방향의 큐브 맵 면을 선택할 수 있다.

    선택해서 해당 위치에 맞는 텍스쳐를 넣으면 된다.

    아래는 편하게 텍스쳐 넣으려고 만든 그림이다..

    아티스트마다, 좌표의 차이가 있는 것 같다. 

    큐브맵 좌표에 따른 면

     

    TextureCube		g_txSky : register(t0);
    SamplerState	g_Sample : register(s0);
    
    struct VS_INPUT
    {
    	float3 p : POSITION;
    	float3 n : NORMAL;
    	float4 c : COLOR;
    	float3 t : TEXCOORD;
    };
    struct VS_OUTPUT
    {
    	float4 p : SV_POSITION;
    	float3 n : NORMAL;
    	float4 c : COLOR0;
    	float3 t : TEXCOORD;	// float3임
    };
    
    VS_OUTPUT VS(VS_INPUT vIn) 
    {
    	VS_OUTPUT vOut = (VS_OUTPUT)0;
    	vOut.p			= mul( float4(vIn.p,1.0f), g_matWorld );
    	vOut.p			= mul(vOut.p, g_matView );
    	vOut.p			= mul(vOut.p, g_matProj );
    	vOut.p			= vOut.p.xyww;
    
    	vOut.c			= vIn.c;
    	vOut.t			= normalize(vIn.p.xyz); 
    	vOut.n			= normalize(mul(vIn.n, (float3x3)g_matWorld));	
        return vOut;
    }
    
    
    float4 PS(VS_OUTPUT input) : SV_TARGET
    {	
    	//텍스처에서 t좌표에 해당하는 컬러값(픽셀) 반환
    	return g_txSky.Sample(g_Sample, input.t);
    }

     

    큐브 맵은 Texture2D가 아닌 TextureCube 타입의 텍스쳐 리소스이다. 

    일반적으로 UV 값이 float2 값이었지만, 큐브 맵은 3차원 공간의 텍스쳐이기 때문에,

    float3의 Texcoord를 갖는다. 

    박스 하나를 상속받아 텍스쳐를 붙이고,

    뷰 행렬의 위치를 원점에 두어, 움직임이 없게 하고

    크기를 시야보다 크게 해서 매핑을 하면 된다.  배경에 큐브 맵이 붙는다. 

    결과

     

    3. 반사 벡터, 큐브 맵

    동적 큐브 맵(실시간 계산)과 정적 큐브 맵(저장된 이미지)이 있는데,

    동적 큐브 맵은 계속해서 텍스쳐를 찍어내는 방식이다. 연산량이 많다.

    정적 큐브 맵은 위에서 적용한 큐브 맵 텍스쳐로

    물체의 반사되는 이미지로 적용하면 된다. 

    이전에 포스팅에 렌더 몽키로 구현했었다.

    https://dlemrcnd.tistory.com/39?category=525775 

     

    셰이더 프로그래밍 - AMD RenderMonkey 사용해서 쉐이더 공부를 해보자6 (환경 매핑, 입방체 매핑)

    환경매핑이란, 주위의 환경을 미리 텍스처 안에 저장하고 실시간으로 그 텍스처를 입히는 것이다. 간접광을 재현하며 거울같은 표면을 표현하는 기법이다. 유니티에서 hdri를 적용하거나 스카

    dlemrcnd.tistory.com

     

    렌더 몽키가 9 버전이라 버전이 달라서 함수의 차이가 있다.

     

    정점 노말과 시선 벡터를 사용하여 반사 벡터를 계산한다.

    반사 벡터 자체가 큐브 맵의 3차원 텍스처 좌표가 된다. 

     

    6번 슬롯에 큐브 맵 텍스쳐 SRV를 넘긴다.

     

    4. 환경 매핑 쉐이더

    Texture2D		g_txDiffuse : register(t0);
    Texture2D		g_txSpecular : register(t1);
    Texture2D		g_txNormal : register(t2);
    Texture2D		g_txShadow  : register(t3);
    TextureCube	    g_txCubeMap : register(t6); //환경매핑
    
    SamplerState	g_Sample : register(s0);
    SamplerState	 g_SamplerClamp : register(s1);
    float4 PS(VS_OUTPUT Input) : SV_TARGET
    {
    	//텍스쳐에서 노말, 법선 좌표 구해옴
    	..
       //----------------------------------------------------------------------
       float4 albedo = g_txDiffuse.Sample(g_Sample, Input.t); //알베도 기본 색상 텍스쳐
       //쉐도우
       ..
       //방향 벡터 정규화
       ..
       //디퓨즈 반사, 난반사
       ..
       //스페큘러 반사, 정반사
       ..
       //환경 매핑
       float3 env_coord = reflect(viewDir, worldNormal);//환경 매핑을 위한 텍스처 주소 반사벡터
       float4 env = g_txCubeMap.Sample(g_Sample, env_coord);
    
       float4 final = float4(ambient + diffuse + specular + (env*0.35f), 1);
    
       return final;
    }

     

    정점에서 반사 벡터를 구해서, 넘겨줘도 되지만, 

    픽셀 단위로 반사 벡터를 만들면 좀 더 깔끔한 결과를 얻을 수 있다.

     

    결과

     

     

    극단적으로 오브젝트에 환경 매핑이 붙은 모습이다. 

    현재까지 ambient, diffuse, specular, environment

    4개의 텍스쳐를 불러와 오브젝트에 입힐 수 있도록 적용했다.

    COMMENT
     
    04
    11

    https://dlemrcnd.tistory.com/85?category=525778 

     

    DirectX11 3D - FBX SDK, FBX Importer, FBX 파일 불러오기, 3ds Max Exporter Plugin, Assimp (1)

    1. 3ds Max Exporter Plugin 게임에서는 FBX, OBJ 같은 확장자를 가진 파일을 직접 탑재해서 구동하지 않는다. FBX, OBJ, ASE 같은 확장자의 모델링 된 파일을 가지고 해석하는 과정, 컨버팅 하는 작업은 미리

    dlemrcnd.tistory.com

    이전 포스팅과 이어짐.

     

    이전 포스팅에서 NodeProcess() 함수로 N트리 구조의 FBX 트리를 재귀 함수로 돌아서

    매쉬 타입 오브젝트를 저장해, ParseMesh() 함수로 그 매쉬의 정보를 가져오는 작업을 했다.

     

    NodeProcess()

    void KFbxLoader::NodeProcess(KFBXObj* pParentObj, FbxNode* pNode)
    {
    	KFBXObj* fbx = nullptr;
    	if (pNode!=nullptr)
    	{
    		fbx = new KFBXObj;
    		fbx->m_pFbx_ThisNode = pNode;
    		fbx->m_pFbx_ParentNode = pNode->GetParent();
    		fbx->m_pFbx_ParentObj = pParentObj;
    		fbx->m_iIndex = m_FBXTreeList.size();
    		fbx->m_ObjName = to_mw(pNode->GetName());
    		m_FBXTreeList.push_back(fbx); // obj 검색 데이터를 넣을때,
    		m_pFbxNodeMap.insert(std::make_pair(pNode, fbx->m_iIndex));
    		m_pFbxObjMap.insert(std::make_pair(fbx->m_ObjName, fbx));
    		//이름으로 바로 인덱스 접근할 수 있으면 끝.
    	}
    	// 카메라나 라이트, 헬퍼 오브젝트는 메시타입이 아님
    	FbxMesh* pMesh = pNode->GetMesh();
    	if (pMesh)
    	{
    		//매쉬 타입만 라이트, 카메라 제외
    		m_MeshList.push_back(fbx);
    	}
    	int iNumChild = pNode->GetChildCount();
    	for (int iNode = 0; iNode < iNumChild; iNode++)
    	{
    		FbxNode* child = pNode->GetChild(iNode);
    		NodeProcess(fbx, child);
    	}
    }

     

    위 m_MeshList에 오브젝트 KFBXObj 객체를 저장한다.

    이제 m_MeshList에서 하나 씩 빼내서 해석하면 된다. 

     

    ParseMesh()

    더보기
    void KFbxLoader::ParseMesh(KFBXObj* pObject)
    {
    	//현재 노드의 매쉬를 만듬, 버텍스 PNCT를 채워줘야함
    	FbxMesh* pFbxMesh = pObject->m_pFbx_ThisNode->GetMesh();
    
    	pObject->m_bSkinned = ParseMeshSkinning(pFbxMesh, pObject);
    	if (pFbxMesh)
    	{
    		//기하 행렬(FBX 위치 버텍스에서 -> 초기 정점 로컬 위치로 변환)
    		FbxAMatrix  mat_Geo;
    		FbxVector4	t = pObject->m_pFbx_ThisNode->GetGeometricTranslation(FbxNode::eSourcePivot);
    		FbxVector4	r = pObject->m_pFbx_ThisNode->GetGeometricRotation(FbxNode::eSourcePivot);
    		FbxVector4	s = pObject->m_pFbx_ThisNode->GetGeometricScaling(FbxNode::eSourcePivot);
    
    		mat_Geo.SetT(t);
    		mat_Geo.SetR(r);
    		mat_Geo.SetS(s);
    
    		//노말 행렬, 기하행렬의 역행렬의 전치
    		//노말 매트릭스
    		FbxAMatrix normalMatrix = mat_Geo;
    		normalMatrix = normalMatrix.Inverse();
    		normalMatrix = normalMatrix.Transpose();
    
    
    		std::vector<FbxLayerElementMaterial*>	 MaterialSet; // 매터리얼
    		std::vector<FbxLayerElementUV*>			 UVSet; // UV
    		std::vector<FbxLayerElementVertexColor*> VertexColorSet;//
    
    		//// 노말맵을 위한 노말, 바이노말, 탄젠트
    		std::vector<FbxLayerElementNormal*>		 NormalSet;  
    		std::vector<FbxLayerElementBinormal*>    BinormalSet;
    		std::vector<FbxLayerElementTangent*>     TangentSet; 
    
    		int iLayerCount = pFbxMesh->GetLayerCount(); // 레이어 ( 1번에 랜더링, 여러번에 걸쳐서 랜더링 개념)
    
    		for (int iLayer = 0; iLayer < iLayerCount; iLayer++)
    		{
    			FbxLayer* pFbxLayer = pFbxMesh->GetLayer(iLayer);// 레이어에 UV 정보가 있음 필수적임
    
    			//매터리얼, UV, 버텍스 컬러
    			if (pFbxLayer->GetMaterials() != nullptr)
    			{
    				MaterialSet.push_back(pFbxLayer->GetMaterials());
    			}
    			if (pFbxLayer->GetUVs() != nullptr)
    			{
    				UVSet.push_back(pFbxLayer->GetUVs());
    			}
    			if (pFbxLayer->GetVertexColors() != nullptr)
    			{
    				VertexColorSet.push_back(pFbxLayer->GetVertexColors());
    			}
    			//노말값
    			if (pFbxLayer->GetNormals() != nullptr)
    			{
    				NormalSet.push_back(pFbxLayer->GetNormals());
    			}
    
    			if (pFbxLayer->GetBinormals() != nullptr) 
    			{
    				BinormalSet.push_back(pFbxLayer->GetBinormals());
    			}
    			if (pFbxLayer->GetTangents() != nullptr)
    			{
    				TangentSet.push_back(pFbxLayer->GetTangents());
    			}
    		}
    
    		//매터리얼 개수 만큼 돌면서 읽어옴 
    		//현재는 저장된 텍스쳐의 이름만 가져옴
    		int iNumMtrl = pObject->m_pFbx_ThisNode->GetMaterialCount();
    		for (int iMtrl = 0; iMtrl < iNumMtrl; iMtrl++)
    		{
    			FbxSurfaceMaterial* pSurface = pObject->m_pFbx_ThisNode->GetMaterial(iMtrl);
    			if (pSurface)
    			{
    				//메터리얼의 텍스쳐 이름을 가져와서 리스트 추가 및 SRV 생성
    				std::wstring strFbxPath = L"../../data/model/";
    				std::wstring strFBXTexName = to_mw(ParseMaterial(pSurface));
    				std::wstring strTexDefault1 = L"../../data/model/Default_Diffuse.jpg";
    				std::wstring strTexDefault2 = L"../../data/model/Default_Specular.jpg";
    				std::wstring strTexDefault3 = L"../../data/model/T_Pack_01_N.jpg";
    				if (!strFBXTexName.empty())
    				{
    					strFbxPath += strFBXTexName;
    					//자동으로 텍스쳐를 만드는데, 실패할 경우
    					KTexture* pTex1 = g_TextureMananger.Load(strFbxPath);
    					KTexture* pTex2 = g_TextureMananger.Load(strTexDefault2);
    					KTexture* pTex3 = g_TextureMananger.Load(strTexDefault3);
    					if (pTex1 != nullptr)
    					{
    						pObject->m_pTexture_Diffuse = pTex1;
    					}
    					else
    					{
    						KTexture* pNoDir = g_TextureMananger.Load(strTexDefault1);
    						pObject->m_pTexture_Diffuse = pNoDir;
    					}
    					
    					pObject->m_pTexture_Specular = pTex2;
    					pObject->m_pTexture_Normal = pTex3;
    				}
    			}
    		}
    		//개수가 1보다 많다면 매터리얼 수만큼 배열 할당해주고 
    		// 그외에는 무조건 하나로 할당해준다.
    		if (iNumMtrl > 0)
    		{
    			pObject->m_pSubBTList.resize(iNumMtrl);
    			pObject->m_pSubVertexList.resize(iNumMtrl);
    			pObject->m_pSubIWVertexList.resize(iNumMtrl);
    		}
    		else
    		{
    			pObject->m_pSubBTList.resize(1);
    			pObject->m_pSubVertexList.resize(1);
    			pObject->m_pSubIWVertexList.resize(1);
    		}
    		//----------------------------------------------------------
    		// 폴리곤, 면 개수 만큼 돌면서 위치를 저장
    		// 삼각형, 사각형
    		int iCurpolyIndex = 0; // 증가되는 폴리곤 인덱스
    		int iNumPolyCount = pFbxMesh->GetPolygonCount(); //폴리곤 수
    		FbxVector4* pVertexPositions = pFbxMesh->GetControlPoints(); //정점 위치 
    		int iNumFace = 0;
    		for (int iPoly = 0; iPoly < iNumPolyCount; iPoly++)
    		{
    			int iPolySize = pFbxMesh->GetPolygonSize(iPoly); //4또는 3 삼각형이나 사각형이냐
    			iNumFace = iPolySize - 2; // 한면 구하는 계산
    
    			int iSubMtrl = 0;
    			//서브 매터리얼 
    			if (iNumMtrl >= 1 && MaterialSet[0] != nullptr)
    			{
    				iSubMtrl = GetSubMaterialIndex(iPoly, MaterialSet[0]);
    			}
    			//면 4 - 2는 2개의 트라이앵글
    			for (int iFace = 0; iFace < iNumFace; iFace++)
    			{
    				int VertexIndex[3] = { 0, iFace + 2, iFace + 1 };
    				int CornerIndex[3];
    				CornerIndex[0] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[0]);
    				CornerIndex[1] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[1]);
    				CornerIndex[2] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[2]);
    				for (int iIndex = 0; iIndex < 3; iIndex++)
    				{
    					PNCT_VERTEX pnct_vertex;
    					BT_VERTEX bt_vertex;
    					// Max(x,z,y) ->(dx)x,y,z    
    					FbxVector4 v = pVertexPositions[CornerIndex[iIndex]];
    					v = mat_Geo.MultT(v); // 로컬 좌표로 행렬 곱 
    					pnct_vertex.pos.x = v.mData[0];
    					pnct_vertex.pos.y = v.mData[2];
    					pnct_vertex.pos.z = v.mData[1];
    
    					// UV
    					int u[3];
    					u[0] = pFbxMesh->GetTextureUVIndex(iPoly, VertexIndex[0]);
    					u[1] = pFbxMesh->GetTextureUVIndex(iPoly, VertexIndex[1]);
    					u[2] = pFbxMesh->GetTextureUVIndex(iPoly, VertexIndex[2]);
    					//UV 리스트에 값이 있다면
    					if (UVSet.size() > 0)
    					{
    						FbxLayerElementUV* pUVSet = UVSet[0];
    						FbxVector2 uv;
    						ReadTextureCoord(
    							pFbxMesh,
    							pUVSet,
    							CornerIndex[iIndex],
    							u[iIndex],
    							uv);
    						pnct_vertex.tex.x = uv.mData[0];
    						pnct_vertex.tex.y = 1.0f - uv.mData[1];
    					}
    					//----------------------------------------------------------
    					//버텍스 컬러 값이 있다면
    					FbxColor color = FbxColor(1, 1, 1, 1);
    					if (VertexColorSet.size() > 0)
    					{
    						color = ReadColor(pFbxMesh,
    							VertexColorSet.size(),
    							VertexColorSet[0],
    							CornerIndex[iIndex],
    							iCurpolyIndex + VertexIndex[iIndex]);
    					}
    					pnct_vertex.color.x = color.mRed;
    					pnct_vertex.color.y = color.mGreen;
    					pnct_vertex.color.z = color.mBlue;
    					pnct_vertex.color.w = pObject->m_iIndex; //버텍스 컬러 값에 인덱스 저장
    					//----------------------------------------------------------
    					//노말값이 있다면
    					if (NormalSet.size() > 0)
    					{
    						FbxVector4 normal = ReadNormal(pFbxMesh,
    							CornerIndex[iIndex],
    							iCurpolyIndex + VertexIndex[iIndex]);
    						normal = normalMatrix.MultT(normal);
    						pnct_vertex.normal.x = normal.mData[0]; // x
    						pnct_vertex.normal.y = normal.mData[2]; // z
    						pnct_vertex.normal.z = normal.mData[1]; // y
    					}
    
    					if (BinormalSet.size() > 0)
    					{
    						FbxVector4 binormal = ReadBinormal(pFbxMesh,
    							CornerIndex[iIndex],
    							iCurpolyIndex + VertexIndex[iIndex]);
    						binormal = normalMatrix.MultT(binormal);
    						bt_vertex.binormal.x = binormal.mData[0]; // x
    						bt_vertex.binormal.y = binormal.mData[2]; // z
    						bt_vertex.binormal.z = binormal.mData[1]; // y
    					}
    
    					if (TangentSet.size() > 0)
    					{
    						FbxVector4 tangent = ReadTangent(pFbxMesh,
    							CornerIndex[iIndex],
    							iCurpolyIndex + VertexIndex[iIndex]);
    						tangent = normalMatrix.MultT(tangent);
    						bt_vertex.tangent.x = tangent.mData[0]; // x
    						bt_vertex.tangent.y = tangent.mData[2]; // z
    						bt_vertex.tangent.z = tangent.mData[1]; // y
    					}
    					//애니메이션을 위한 가중치
    					//캐릭터 애니메이션이 아닌 오브젝트도 스키닝화 시킨다.
    					IW_VERTEX iwVertex;
    					if (pObject->m_bSkinned) //캐릭터일 경우
    					{
    						KWeight* weight = &pObject->m_WeightList[CornerIndex[iIndex]];
    						for (int i = 0; i < 4; i++)
    						{
    							iwVertex.i[i] = weight->Index[i];
    							iwVertex.w[i] = weight->Weight[i];
    						}
    					}
    					else//오브젝트 애니메이션인 경우
    					{
    						// 일반오브젝트 에니메이션을 스키닝 케릭터 화 작업.
    						iwVertex.i[0] = pObject->m_iIndex;
    						iwVertex.w[0] = 1.0f;
    					}
    					pObject->m_pSubVertexList[iSubMtrl].push_back(pnct_vertex);
    					pObject->m_pSubIWVertexList[iSubMtrl].push_back(iwVertex);
    					pObject->m_pSubBTList[iSubMtrl].push_back(bt_vertex);
    				}
    			}
    			iCurpolyIndex += iPolySize;
    		}
    
    	}
    }

     

    오브젝트의 버텍스 위치, 버텍스 컬리, 매터리얼, UV값, 법선값, 접선 값, 종법선 값 등

    이 한 함수에서 정보를 빼내 온다. 

     

    여기서 한 가지 문제점이 있다. FBX 파일에 따라서, 내 경우에는 대부분의 파일이

    탄젠트 공간의 정보가 빠져있었다. 

     

    FBX를 Export 할 때, Tangent 공간을 같이 익스포트 하지 않는 이상,

    GetBinormal, GetTangents

    Binormal(종법선), Tangent(접선) 정보가 저장돼있지 않기 때문에 FBX 매쉬에

    정보를 꺼내올 수 없었던 것이다.

    Blender Export 창

     

    위는 Blender에서 Tangent Space를 같이 익스포트 하는 방법이다.

    하지만, 모든 파일을 내가 직접 제작할 거면 굳이 FBX 파일을 사용하는

    이유가 없기 때문에, 

     

    트라이앵글 3점을 이용해서 탄젠트 공간을 구하는 함수를 구현했었다.

    아래는 그 함수의 내용이 있는 포스팅이다.

     

    https://dlemrcnd.tistory.com/80?category=525778 

     

    DirectX11 - 법선매핑 (Normal Mapping) 접선 공간, 접선 (Tangent), 종법선 (Binormal)

    이번에는 법선매핑, 노말 맵을 다이렉트x에 적용한다. 노말 맵은 픽셀에 사용할 법선 정보를 담고 있다. 노말 매핑을 사용하면 버텍스의 노말(법선) 정보가 아닌 텍스쳐의 노말(법선)정보를 사용

    dlemrcnd.tistory.com

     

    하지만 이 방법은 비효율적이라고 할 수 있다.

    왜냐하면 FBX SDK에는 이미 구현되어 있다..!

     

    int iLayerCount = pFbxMesh->GetLayerCount();
    // 레이어 ( 1번에 랜더링, 여러번에 걸쳐서 랜더링 개념)
    
    if (iLayerCount == 0 || pFbxMesh->GetLayer(0)->GetNormals() == nullptr)
    {
    	pFbxMesh->InitNormals();
    	pFbxMesh->GenerateNormals();
    }
    //노말 탄젠트 바이노말 없을때 생성해준다.
    if (iLayerCount == 0 || pFbxMesh->GetLayer(0)->GetTangents() == nullptr || 
    	pFbxMesh->GetLayer(0)->GetBinormals() == nullptr)
    {
    	pFbxMesh->InitTangents();
    	pFbxMesh->InitBinormals();
    	pFbxMesh->CreateElementBinormal();
    	pFbxMesh->GenerateTangentsData(0, true, false);
    }

     

    레이어 단위로 정보를 얻기 전에

    GetNormal(), GetTangent(), GetBinomal을 호출한다.

    nullptr, 즉, 결과 없을 경우에 정보가 없는 경우이기 때문에,

    새로이 노말, 탄젠트, 바이 노말을 생성한다. 

    InitNormals(), InitTangents(), InitBinormals()은 정점 개수만큼

    배열의 메모리를 할당하는 함수이다. 

     

    GenerateNormals() 함수로 혹시 없을 노말 값을 생성하고, 

    GenerateTagentsData()로 탄젠트, 바이 노말을 값을 생성한다.

    bool GenerateTangentsData(int pUVSetLayerIndex,
    		bool pOverwrite=false,
    		bool pIgnoreTangentFlip = false);

    pOverwrite에 true값을 준 것은, init함수로 할당될때, 0000값이 들어가서

    덮어쓰겠다는 의미이다. 

     

    결과

    노말맵 적용

    정점에 탄젠트 공간을 만들어 노말 맵이 잘 적용된 모습이다.

    이제 어떤 파일이든 노말값, 탄젠트 공간 정보가 없어도

    생성해서 쉐이더에 잘 적용할 수 있다.  

     

    COMMENT
     
    04
    09

    게임에서의 빛 연산, 그림자 연산의 완벽한 연산 알고리즘은 아직 개발되지 않았다. 

    특히 3D 모델링을 해봤다면, VRay나 Cycles 같은 렌더러에서

    한 장의 이미지를 렌더링 하기 위해서 한 시간을 기다렸던 경험이 있을지도 모른다. 

    출처 :&nbsp;https://www.chaos.com/vray/sketchup/free-trial

    레이 트레이싱 방식은 픽셀 하나하나 마다 통과하는 광선으로 계산한다. 요구 연산량이

    상당하기 때문에, 실시간 렌더링이 불가능하며, 현재 게임에서 쓰이는 레이 트레이싱은

    모든 픽셀을 계산하지 않고, AI연산으로 그 많은 연산을 커버한다고 한다. 

    (레이 트레이싱 전용 가속 장치 RT 코어 탑재된 RTX 20 시리즈부터 지원함)

     

    - Shadow Mapping

    그림자란 광원의 경로 상에서 불투명한 물체가 있을 때, 빛의 직전성 때문에,

    물체에 빛이 통과하지 못하여 생기는 어두운 부분을 말한다.

    출처 :https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

    왼쪽 사진에서는 그림자가 없을 때의 모습인데, 3D 공간에서 

    그림자가 없으면 물체가 떠있는지 바닥에 있는지 구별하기가 어렵다. 

    최소한 3D 공간라면, 원형 그림자이라도 깔아놔야 한다. 

    원형 평면 그림자

    게임에서는 그림자를 흉내내기 위한 기법이 여러 가지 있지만, 

    대표적으로 평면 투영 쉐도우, 투영 쉐도우, 깊이 맵 쉐도우가 있다. 

     

    1. 평면 투영 쉐도우 (Planar Projected Shadow)

    그림자를 만들 오브젝트를 빛 방향에서 바닥으로 납작하게 눌러서 바닥에 그려주는 방식이다.

    고려할 점은 평면 투영 행렬을 구해서 곱한다는 것이다.

    (D3DXMatrixShadow() - 평면 투영 행렬 구하는 Direct함수) 

    오브젝트를 직접 평면 투영 행렬을 월드 행렬로 사용하여 그림자를 구현한다.

    구현한 평면 투영 쉐도우

     

    오브젝트를 바닥으로 짓누른 모양으로 한번 더 렌더링 하기 때문에,

    와이어프레임으로 보면 똑같은 매쉬로 그려지는 것을 볼 수 있다.  

    단점으로는, 평면이 아니라면 그림자가 드리워질 수 없다.

     

    2. 투영 텍스쳐 쉐도우 (Projection Shadow) 

    위는 오직 평면 지형에서만 그림자가 그려진다.,

    울퉁불퉁한 지형에 그림자를 그리기 위해 투영 텍스쳐 쉐도우를 사용한다.

    광원 위치에서 바라본 장면, 빈 텍스쳐에 그 장면을 텍스쳐로 저장한다.

    텍스쳐를 생성해, 빈 텍스쳐에 렌더 타깃 RenderTargetView()을 그려서 리소스에 전달해야 한다.

    위 그림의 왼쪽 아래에 있는 텍스쳐가 렌더 타깃의 모습이다. 

    지형이 울퉁불퉁하더라도, 텍스쳐 좌표에 입히기 때문에, 

    지형에 맞게 뿌려진다. 

    단, 자기 그림자를 그릴 수 없다.  쉐도우 여부만 판정하고,

    깊이의 정보가 없기 때문이다.

     

    3. 깊이 맵 쉐도우 (Depth Map Shadow)

    이전에, 렌더 몽키를 이용해 구현한 적이 있다.

    https://dlemrcnd.tistory.com/41?category=525775 

     

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

    <본 포스팅은 강좌가 아닙니다.> 이전 다렉 프로젝트할때 뎁스맵 쉐도우를 적용했었다. 그러면서 3가지의 그림자 기법인 평면쉐도우, 프로젝션 쉐도우, 뎁스맵 쉐도우 대해 알고있었다. 1. 평면

    dlemrcnd.tistory.com

     

    전방향 쉐도우(Omnidirectional Shadow Maps), 계단식 쉐도우(Cascaded Shadow)등

    기초가 되는 그림자 기법이며 자기 그림자인, Self-Shadow가 가능하다.

     

    과정은 다음과 같다. 

    1. 광원 위치에서 바라보는 뷰 및 투영 행렬 연산

    2. 깊이 맵 저장할 텍스쳐 생성

    3. 깊이 맵 텍스쳐에 깊이 값 렌더링

    4. 오브젝트 렌더링 시 깊이 값 이용해 그림자 판정

     

    VSPS_DepthShadow.hlsl

    더보기
    cbuffer CBuf : register(b0)
    {
    	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);		//기타 시간 값등
    };
    cbuffer cbDataShadow: register(b2)
    {
    	matrix g_matShadow	: packoffset(c0);
    };
    struct VS_INPUT
    {
    	float3 p : POSITION;
    	float3 n : NORMAL;
    	float4 c	: COLOR;
    	float2 t	: TEXCOORD;
    
    	float3 mTangent	: TANGENT;
    	float3 mBinormal : BINORMAL;
    };
    struct VS_OUTPUT
    {
    	float4 p : SV_POSITION;
    	float2 t : TEXCOORD0;
    	float4 c : COLOR0;
    	float3 mLightDir : TEXCOORD1; //방향
    	float3 mViewDir  : TEXCOORD2; //방향
    	float4 mShadow	 : TEXCOORD3; //뎁스 맵 쉐도우 추가
    	float3 mT        : TEXCOORD4;
    	float3 mB        : TEXCOORD5;
    	float3 mN        : TEXCOORD6;
    };
    
    VS_OUTPUT VS(VS_INPUT Input)
    {
    	//난반사광의 계산 동일한 계산을 PS VS 둘다 할 수 있음. 
       //하지만 픽셀단위 계산보다 정점단위 계산이 더 연산량이 적음
    	VS_OUTPUT Output = (VS_OUTPUT)0;
    	float4 vLocal = float4(Input.p, 1.0f);
    	float4 vWorld = mul(vLocal, g_matWorld);
    
    	//라이트 방향 월드 행렬 곱함, 월드 공간에서의 위치여서 여기서 광원의 위치를 뺀다.
    	float3 lightDir = vWorld.xyz - g_lightPos.xyz;
    	Output.mLightDir = normalize(lightDir);
    	//보는 방향
    	float3 viewDir = vWorld.xyz - g_camPos.xyz;
    	Output.mViewDir = normalize(viewDir);
    	//쉐도우 행렬곱
    	Output.mShadow = mul(vWorld, g_matShadow);
    
    	float4 vView = mul(vWorld, g_matView);
    	float4 vProj = mul(vView, g_matProj);
    	float3 worldTangent = mul(Input.mTangent, (float3x3)g_matWorld);
    	float3 worldBinormal = mul(Input.mBinormal, (float3x3)g_matWorld);
    	float3 worldNormal = mul(Input.n, (float3x3)g_matWorld);
    	Output.p = vProj;
    	Output.t = Input.t;
        //깊이 연산, 거리만큼 0~1 0이면 쉐도우가 없고 1이면 쉐도우가 있는 것
    	float depth1 = vProj.z * 1.0f / (1000.0f - 1.0f) + -1.0f / (1000.0f - 1.0f);
    	Output.c = float4(depth1, depth1, depth1, 1);
    	Output.mT = normalize(worldTangent);
    	Output.mB = normalize(worldBinormal);
    	Output.mN = normalize(worldNormal);
    
    	return  Output;
    }
    Texture2D		g_txDiffuse : register(t0);
    Texture2D		g_txSpecular : register(t1);
    Texture2D		g_txNormal : register(t2);
    Texture2D		g_txShadow  : register(t3);
    SamplerState	g_Sample : register(s0);
    SamplerState	 g_SamplerClamp : register(s1);;
    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 vShadowProj;
       vShadowProj.xy = Input.mShadow.xy / Input.mShadow.w;
       float shadow = g_txShadow.Sample(g_SamplerClamp, vShadowProj.xy);
       float depth = Input.mShadow.z * 1.0f / (1000.0f - 1.0f) + -1.0f / (1000.0f - 1.0f);
       if (shadow + 0.005f <= depth)
       {
    	   albedo = albedo * float4(0.5f
    		   , 0.5f, 0.5f, 1.0f);
       }
       //디퓨즈 텍스쳐
       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.05f, 0.05f, 0.05f) * albedo;
       return float4(ambient + diffuse + specular, 1);
    }
    float4 PSDepth(VS_OUTPUT Input) : SV_TARGET
    {
    	return Input.c;
    }

     

     

    깊이 맵에서, 멀리 있으면 흰색, 가까울수록 검은색으로 연산해서

    텍스쳐의 배경은 흰색으로 해야 한다. 

    깊이 맵 텍스쳐

     

    깊이 맵을 만들 때, 고려해야 할 것이 깊이 바이어스(편향치)이다.

    저장 시에 약간의 실수 값의 오차가 발생되어서 나타난다.

     

    https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

     

    광원의 각도로 텍스쳐를 저장해서 기울어진 깊이 텍스쳐가 되었기 때문이다. 

     

     

    이 문제를 깊이 바이어스, 깊이 값을 오프셋함으로써 문제를 해결할 수 있다.

     

    결과

    COMMENT
     
    04
    04

    1. 3ds Max Exporter Plugin

    게임에서는 FBX, OBJ 같은 확장자를 가진 파일을 직접 탑재해서 구동하지 않는다.

    FBX, OBJ, ASE 같은 확장자의 모델링 된 파일을 가지고 해석하는 과정, 컨버팅 하는

    작업은 미리 끝난, 해석된 정보를 갖고 있는 파일을 탑재한다. 

     

    그런 파일들을 편하게 만들기 위해서는 MAX SDK를 사용해서

    자체적으로 맥스 익스포터 (3ds Max Exporter Plugin) 제작을 한다.

    대용량 데이터 저장하는 xml이나 json, 또는 본인만의 방식으로 저장한다.

    이렇게 제작하는 하는 사람, 제작하는 회사마다 저장하는 방식이 다르다면

    저장된 방식을 알아야 읽어드릴 수 있다. 

     

    2. Assimp

    가장 많이 쓰이는 Importing 라이브러리이다.  Open Asset Import Library라는 의미로,

    거의 모든 모델의 데이터를 Assimp가 생성한 데이터 구조로 불러오고 보낼 수가 있다.

    https://github.com/begla/Intrinsic/issues/14

     

    Use Assimp instead of FBX SDK · Issue #14 · begla/Intrinsic

    While the license of the engine was addressed in #8, the dependence of the FBX SDK means that the engine (or games using the engine) cannot be packaged by Linux distributions, as the license of FBX...

    github.com

     

    Assimp을 사용함으로써 이점은 무료 라이선스다양한 모델 형식을 지원한다는 점,

    또한 오픈 소스여서 지원하는 문서도 다양하다.

     

    반면에, 이번에 사용할 FBX SDK는 라이선스도 제한적이며, 공식문서도 불친절하다고 한다.

    그럼에도 불구하고 내가 FBX SDK를 사용하려는 이유는

    게임은 대부분 FBX 파일 형식을 사용하는데, Assimp는 FBX 파일 형식에서

    기능이 완전하지 않고 FBX SDK가 좀더 FBX에 전문적이기 때문이다. 

    예를 들어,  FBX 애니메이션에서 정점별 애니메이션을 지원하지 않는다고 한다.

     

     

    3. FBX SDK

    https://dlemrcnd.tistory.com/76

     

    DirectX11 3D - .Obj 확장자 파일 다이렉트 X 환경에서 렌더링하기

    이전에 FBX 파일을 읽어와 다이렉트 환경에서 렌더링을 했었다. FBX는 광범위한 속성을 담을 수 있는 포맷으로 특히 애니메이션이 있을때 유용하다. 하지만 Obj 확장자는 매쉬, 텍스쳐, 메터리얼 3

    dlemrcnd.tistory.com

     

    이전 글에서는 Obj 확장자 파일을 파일 입출력으로 읽어와 렌더 했다. 

    FBX SDK 사용하여 직접 파일 입출력을 하지 않아도, 메모리에 다 올려준다.

     

    Direct Toolkit의 텍스쳐 로더 (WICTextureLoader)처럼 PNG, JPG 포맷에 규칙을 몰라도

    라이브러리에서 다 해결해주는 것처럼 말이다.

     

    FBX SDK 환경구성을 위한 링크이다. 2020.2.1 버전으로 사용한다.

    설치

    https://www.autodesk.com/developer-network/platform-technologies/fbx-sdk-2020-0

    공식문서

    https://help.autodesk.com/view/FBX/2020/ENU/

     

    출처 :https://download.autodesk.com/us/fbx/20112/fbx_sdk_help/index.html?url=WS8e4c2438b09b7f9c-5fe157071197ccd9b09-7ffe.htm,topicNumber=d0e4377

     

    FBX 파일을 FBX Importer가 FBX Scene에 불러와서 트리를 구성한다. 

    FBX Scene은 N개의 트리로 이루워져 있다. (트리의 자식의 개수가 N개수)

     

    씬에는 트리 구조에는 모델의 정보를 가지고 있고

    전 순회해서 원하는 정보를 가져오면 된다. 그 정보를 가지고

    본인 엔진에 맞는 렌더링 데이터로 바꾸면 된다.

     

    대략적인 순서는 다음과 같다.

    1. FBX Manager, FBX Scene, FBX Importer 생성

    2. FBX Importer에게 불러올 파일명을 전달 (초기화)

    3. FBX Importer에게 생성한 FBX Scene을 전달, (씬에다가 모델을 풀어놓음)

    4. Rootnode부터 순회하면서 원하는 정보를 얻는다. (Vertex, Normal, Tangent..)

     

     환경설정 : <fbxsdk.h>, libfbxsdk-md.lib;, libxml2-md.lib;, zlib-md.lib;

    KFbxLoader.h

    #include <fbxsdk.h>//헤더 추가
    class KFbxLoader
    {
    public:
    	FbxManager*				m_pFbxManager;
    	FbxImporter*			m_pFbxImporter;
    	FbxScene*				m_pFbxScene;
    .
    .

    fbxsdk.h 를 추가하고 FBX Manager, FBX Scene, FBX Importer 포인터 변수 선언

    KFbxLoader.cpp

    bool KFbxLoader::Init()
    {
    	m_pFbxManager = FbxManager::Create(); 
    	m_pFbxImporter = FbxImporter::Create(m_pFbxManager, "");
    	m_pFbxScene = FbxScene::Create(m_pFbxManager, "");
    	return true;
    }
    ..
    bool KFbxLoader::Release()
    {
    	m_pFbxScene->Destroy();
    	m_pFbxImporter->Destroy();
    	m_pFbxManager->Destroy();
    	return true;
    }

    1번 순서처럼, Manager를 생성하고 매니저로 임포터와 씬을 생성한다. 

    해제는 그 반대의 순서로 해제해준다.

     

    bool KFbxLoader::Load(std::wstring filename)
    {
    	//파일 - > 임포터 -> 씬 (트리) 해석 -> 매쉬 -> 정보 가져오기 pnct -> 출력
    
    	std::string temp = to_wm(filename);
    	bool bRet = m_pFbxImporter->Initialize(temp.c_str()); //파일명 넘김
    	bRet = m_pFbxImporter->Import(m_pFbxScene);
    	FbxAxisSystem::MayaZUp.ConvertScene(m_pFbxScene); //마야 Z축 버젼 사용
    
    	if (bRet)
    	{
    		//fbx는 트리 구조로 이어져있음
    		//재귀호출로 전 순회 가능, N 트리여서 자식 수를 알아야함
    		//N트리 : 자식 개수가 N개임
    		m_pRootNode = m_pFbxScene->GetRootNode();
    
    		//전 순회해서 씬에 저장되어 있는 트리구조에서 오브젝트를 찾아옴
    		NodeProcess(nullptr, m_pRootNode);
    
    		//N트리에서 찾아낸 오브젝트를 해석함
    		for (int iobj = 0; iobj < m_ObjectList.size(); iobj++)
    		{
    			ParseMesh(m_ObjectList[iobj]);
    		}
    		return true;
    	}
    	return false;
    }

     

    Fbx Importer에게 파일명을 넘긴다. 그리고 그걸 FbxScene에서 넘겨주는 모습이다.

    결과가 bRet에 저장해서 잘 불러왔으면 씬에 있는 모델의 정보를 가져온다.

    트리 구조이기 때문에, 루트 노드부터 재귀 함수를 통해 오브젝트 개수와 종류를 알아온다.

    NodeProcess가 그 함수이다.  

     

    void KFbxLoader::NodeProcess(FbxNode* pParentNode, FbxNode* pNode)
    {
    	// 카메라나 라이트 등 매쉬가 아니라면 리턴
    	/*if (pNode->GetCamera() || pNode->GetLight())
    	{
    		return;
    	}*/
    	//매쉬타입 이니까 매쉬를 얻어옴
    	FbxMesh* pMesh = pNode->GetMesh();
    	if (pMesh)
    	{
    		KFBXObj* fbx = new KFBXObj;
    		fbx->m_pFbx_ParentNode = pParentNode;
    		fbx->m_pFbx_ThisNode = pNode;
    		m_ObjectList.push_back(fbx);
    	}
    
    	int iNumChild = pNode->GetChildCount();
    	for (int iNode = 0; iNode < iNumChild; iNode++)
    	{
    		FbxNode* child = pNode->GetChild(iNode);
    		NodeProcess(pNode, child);
    	}
    }

     

    카메라, 라이트 들도 예외처리를 해야 하지만,

    우선 카메라와 라이트가 없는 파일이라는 전제로 진행한다.

    매쉬 타입이라면, 오브젝트를 생성하고 오브젝트 리스트에 저장한다.

    N트리라고 했듯이, 자식의 N개이기 때문에, GetChildCount() 함수를 통해

    노드의 자식수를 알 수 있다. 재귀 함수로 모든 노드를 탐색한다. 

     

    //N트리에서 찾아낸 오브젝트를 해석함
    		for (int iobj = 0; iobj < m_ObjectList.size(); iobj++)
    		{
    			ParseMesh(m_ObjectList[iobj]);
    		}
    		return true;

     

    사실 FBX 파일의 PNCT(position, normal, color, texture) 이 외에도 애니메이션 등등..

    너무 정보가 방대하고, 각각 해줘야 할 처리가 다르기 때문에

    우선 Vertex 위치만 하고 나중에 천천히 정리해보겠다.

     

    위의 함수 NodeProcess로 모든 노드를 탐색해서 얻은 오브젝트를 

    해석하는 함수 PraseMesh로 이제 본인이 원하는 정보를 얻어오면 된다.

     

    	//현재 노드의 매쉬를 만듬, 버텍스 PNCT를 채워줘야함
    	FbxMesh* pFbxMesh = pObject->m_pFbx_ThisNode->GetMesh();

     

    아까 NodeProcess에서 저장한 매쉬를 가져온다.

    FBXMesh는 Vertex, Edge, Polygon 등 다양한 정보를 담고 있다.

     

    	// 폴리곤, 면 개수 만큼 돌면서 위치를 저장
    		// 삼각형, 사각형
    		int iCurpolyIndex = 0; // 증가되는 폴리곤 인덱스
    		int iNumPolyCount = pFbxMesh->GetPolygonCount(); //폴리곤 수
    		FbxVector4* pVertexPositions = pFbxMesh->GetControlPoints(); //정점 위치 
    		int iNumFace = 0;
    		for (int iPoly = 0; iPoly < iNumPolyCount; iPoly++)
    		{
    			int iPolySize = pFbxMesh->GetPolygonSize(iPoly); //4또는 3 삼각형이나 사각형이냐
    			iNumFace = iPolySize - 2; // 한면
    			for (int iFace = 0; iFace < iNumFace; iFace++)
    			{
    				int VertexIndex[3] = { 0, iFace + 2, iFace + 1 };
    				int CornerIndex[3];
    				CornerIndex[0] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[0]);
    				CornerIndex[1] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[1]);
    				CornerIndex[2] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[2]);
    				for (int iIndex = 0; iIndex < 3; iIndex++)
    				{
    					PNCT_VERTEX Vertex;
    					// Max(x,z,y) ->(dx)x,y,z    
    					FbxVector4 v = pVertexPositions[CornerIndex[iIndex]];
    					v = mat_Geo.MultT(v); // 로컬 좌표로 행렬 곱 
    					Vertex.pos.x = v.mData[0];
    					Vertex.pos.y = v.mData[2];
    					Vertex.pos.z = v.mData[1];

     

    FbxMesh 함수, GetPolygonCount(), GetControlPoints(), GetPolygonSize, GetPolygonVertex()등

    여러 함수로 원하는 정보를 파싱 할 수 있다. 폴리곤은 한 벽을 의미한다. 트라이 앵글로 이루어진 면은 

     폴리곤 하나당 페이스가 두 개 있다고 할 수 있다. 

    iNumFace = iPolySize - 2;으로 만약에 폴리 사이즈가 3이면 삼각형으로 면이 하나 있다는 것이다.

    폴리 사이즈가 4개라면 면이 2개라는 뜻, 우리의 Primitive는 삼각형이기 때문이다.

     

     

    맥스는 오른손 좌표계, DirectX는 왼손 좌표계여서, 

    두 번째 하고 세 번째 인덱스가 바뀌어 있다. 또한 맥스는 X, Z, Y로 저장되어 있다. 

    그리고 mData가 0번이 x축 1번 z 축 2번 y 축이여서 순서를 바꿔서 대입한다.

     

    이거는 텍스쳐 축, UV 축도 마찬가지다. U는 같지만 V는 다르다.

    왼쪽 하단이 원점이다.

     

    이런 식으로 Mesh의 정보를 꺼내 주는 내장 함수를 통해

    원하는 UV값, 노말, 탄젠트, 바이 노말, 애니메이션 등

    렌더링 할 수 있는 데이터로 변환해야 한다.

     

    결과

     

    아래는 FBX SDK를 이용한 탄젠트 공간 생성 포스팅이다.

    https://dlemrcnd.tistory.com/87

     

    DirectX11 3D - FBX SDK, FBX Importer, FBX 파일 불러오기 탄젠트 공간 (2)

    https://dlemrcnd.tistory.com/85?category=525778 DirectX11 3D - FBX SDK, FBX Importer, FBX 파일 불러오기, 3ds Max Exporter Plugin, Assimp (1) 1. 3ds Max Exporter Plugin 게임에서는 FBX, OBJ 같은 확장..

    dlemrcnd.tistory.com

    COMMENT
     
    03
    25

    3D Rotation Matrix

    회전 행렬은 회전 변환 관계를 나타내기 위한 행렬이다. 

    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

    회전 행렬 변환

     

    이전 포스팅에서 회전 행렬에 대해서 올렸었다.

    위 4X4 행렬을 표현하기 위해서는 16개의 원소가 필요하다.

    축과 회전 각도를 활용하여 간략하게 사용하기 위해

    Fixed Angle, Euler Angle, Quaternion을 사용한다.

    이를 가지고 행렬로 변환시켜 최종적으로 월드 행렬을 만든다.

     

    Fixed Angles Rotation (고정 각 회전)

    회전을 표현하는 여러 방법 중 하나다. 

    고정된 축 (Roll, Pitch, Yaw)을 가지고 회전을 시킨다. 

    https://www.youtube.com/watch?v=9Nxx7UK1JRo 

     

    행렬 공식

    XYZ 순서대로 회전을 했지만 우변을 보면 순서가 바뀐 것(ZYX)을 볼 수 있다.

    제일 먼저 실행할 회전의 회전행열을 제일 뒤쪽에 두고 앞에서 곱해주는 방식이다.

    이를 Pre-Multiplication 규칙이라 하며 이것을 지키며 회전한다.

     

    Euler Angles Rotation (오일러 각 회전)

    고정축 회전과 다르게 Post-Multiplication 규칙을 지키며 회전한다. 

    회전의 기준이 되는 축이 따로 있지 않고, 이전에 회전한 축 기준으로 회전한다.

    기준 축이 회전 후에 새롭게 형성된다. 

    행렬공식

    Post-Multiplication 규칙 덕분에 순서대로 곱해진다. 그래서 훨씬 직관적이다.

    X축 회전을 롤(roll), Y축 회전을 피치(pitch),

    Z 축 회전을 요(Yaw)라고 표기하는 X-Y-Z 좌표인 요, 피치, 롤 방식이 있다.

    출처 : https://hoodymong.tistory.com/3

    계속 생성되는 새로운 축, 각 축을 독립적으로 평가하기 때문에,

    행렬을 서로 곱하면서 짐벌락이 나타나게 된다. 

     

    짐벌락 : 두 축이 겹쳐서 나머지 한축의 자유도가 상실함 

     

    xyz, xzy, yzx, yxz.. 어떤 순서대로 하든 짐벌락은 존재한다.

    세 축이 동시에 업데이트되면 짐벌락을 해소할 수 있다.

    사원수(쿼터니언 Quaternion)이 이를 해결한다.

     

    사원수(쿼터니언 Quaternion)

    19세기 윌리암 해밀턴에 의해서 만들어 짐

    실수 4개를 갖아 행렬에 비해 저장공간 및 연산이 적고 보간을 할 때

    좀 더 매끄러운 애니메이션이 가능하다.

    세 축이 동시에 업데이트되면서 짐벌락을 해소할 수 있다.

     

    회전 표현을 실수 4개, (실수부 1개 + 허수부 3개)로 표현한다.

    실수(Real Number)허수(Imaginary Number) 성분을 갖는 걸

    복소수(Complex Number)라고 한다.

    사원수의 연산은 결합 법칙, 분배 법칙이 성립하지만 교환 법칙은 성립 안됨.

     

    임의의 정점 v를 사원 수로 회전하기 위해서는 공액 켤레 곱셈(Conjugate Product)

    을 사용해야 한다. 사원수는 정점과 바로 곱하지 못해서 정점 v를 사원수화(p)를 해야 함

    단위 사원수와 공액 사원수를 앞뒤로 곱해 서서 결과를 얻을 수 있다. 

    임의의 점 v(1,1,1) 60도 회전 공식 적용 예시

     

    셰이더는 역행렬이나 사원수를 지원하지 않는다. 레지스터가 제한적이기 때문이다.

    그래서 사원수 -> 행렬, 행렬 -> 사원수로 변환하는 법을 알아야한다. 

     

    사원수 행렬의 변환은 행렬에 대한 정점의 곱으로 변환하면 된다.

    아래는 유도된 사원수-> 행렬 변환 공식이다.

    행우선 방식

     

    COMMENT
     
    03
    22

    Terrian(지형) 렌더링

    지형 렌더링에서는 높이 맵을 사용 한다.

     

    1. 높이 맵

    높이 맵이란 지형의 높이를 나타내는 y값을 텍스쳐로 저장한 텍스쳐를 뜻한다.

    Byte로 저장된 이미지는 0~255로 표현하는데,

    그림판 RGB

    그림판에서 흰색을 표현할 때 최댓값 255를 가지는 것을 

    볼 수가 있다. 흰색일수록 높은 높이값을 갖는다는 의미다. 

    Height Map

    그렇다면 위 높이 맵을 보면, 가운데는 평지고,

    바깥쪽은 높은 고지를 이룬다는 점을 미루어 짐작할 수 있겠다. 

    하지만 이런 높이 맵은 지형의 기울기에 따라, 폴리곤의 증가나 감소가 어렵다.

    이는 쿼트 트리, 옥트리, BSP 공간분할 기법을 이용해 LOD 같은 기술을

    사용해 보완하기도 한다.

     

    2. 텍스쳐 접근 및 제한

    CPU나 GPU가 리소스에 접근할 수 있는지는 개발자가 직접 D3D11_USAGE

    사용해서 지정해야 한다. 

    typedef 
    enum D3D11_USAGE
        {
            D3D11_USAGE_DEFAULT	= 0,
            D3D11_USAGE_IMMUTABLE	= 1,
            D3D11_USAGE_DYNAMIC	= 2,
            D3D11_USAGE_STAGING	= 3
        } 	D3D11_USAGE;

     

    일반적으로 GPU가 접근할 수 있다. (D3D11_USAGE_DEFAULT)

    높이 맵 텍스쳐를 읽어와서 높이 맵 정보를 토대로

    높이 맵을 적용하기 위해(Vertex생성) CPU가 접근해야 한다.

     

    리소스 사용 방법 DEFAULT DYNAMIC IMMUTABLE STAGING
    GPU 읽기 가능 허용1 가능 허용 1,2
    GPU 쓰기 허용1     허용 1,2
    CPU 읽기       허용 1,2
    CPU 쓰기   가능   허용 1,2

    허용은 예외 사항을 갖고 있다는 뜻이다. 

     

    CPU는 ID3D11DeviceContext::Map으로 접근할 수 있다.

    GPU는 CopySubresourceResource나 CopyResource, UpdateSubresource로 접근 가능하다.

     

    3. 높이 맵 적용

    높이 맵 텍스쳐를 로드해 STAGING 권한을 갖고

    ID3D11DeviceContext::Map API를 통해 높이 정보를 받아 오면 된다.

    Map은 크리티컬 섹션처럼 공유 자원의 독점을 보장해준다.

    Unmap 하기 전까지 아무도 접근을 못한다. 

     

    HRESULT hr;
    	ID3D11ShaderResourceView* pSRV = nullptr;
    	wrl::ComPtr<ID3D11Resource> pTexture;
    	size_t maxsize = 0;
    	if (FAILED(hr = CreateWICTextureFromFileEx(g_pd3dDevice,
    		heightmap.c_str(),
    		maxsize, 
    		D3D11_USAGE_STAGING,
    		NULL,
    		D3D11_CPU_ACCESS_WRITE|D3D11_CPU_ACCESS_READ,
    		NULL,
    		WIC_LOADER_DEFAULT,
    		pTexture.GetAddressOf(), nullptr)))
    	{
    		return false;
    	}

    WICTextureLoader DXToolkit으로 텍스쳐를 로드한다.

    CPU Access Flag를 D3D11_USAGE_STAGING

    misc Flags를 D3D11_CPU_ACCESS_WRITE|D3D11_CPU_ACCESS_READ로

    CPU가 접근해서 쓰고 읽기 가능하게 한다.

     

    ID3D11Texture2D* pTexture2D = NULL;
    	if (FAILED(pTexture->QueryInterface(__uuidof(ID3D11Texture2D), (LPVOID*)&pTexture2D)))
    	{
    		return false;
    	}

    그런 후에 인터페이스를 얻기 위해 사용하는 QueryInterface 함수를 사용한다.

    위에서 얻는 텍스쳐 리소스를 ID3D11Texture2D* 포인터 변수에 가상 포인터를 넣어준다.

     

    D3D11_TEXTURE2D_DESC desc;
    	pTexture2D->GetDesc(&desc);

    이제 이 pTexture2D 변수로 파일의 정보를 얻을 수 있다.

     

    	m_HeightList.resize(desc.Height * desc.Width);
    
    	if (pTexture2D)
    	{
    		D3D11_MAPPED_SUBRESOURCE MappedFaceDest;
    		//크리티칼 섹션처럼 unmap 하기전까지 접근 못함
    		if (SUCCEEDED(m_pContext->Map((ID3D11Resource*)pTexture2D, 
    			D3D11CalcSubresource(0, 0, 1), D3D11_MAP_READ, 0, &MappedFaceDest)))
    		{
    			UCHAR* pTexels = (UCHAR*)MappedFaceDest.pData;
    			PNCT_VERTEX	v;
    			for (UINT row = 0; row < desc.Height; row++)
    			{
    				UINT rowStart = row * MappedFaceDest.RowPitch;
    				for (UINT col = 0; col < desc.Width; col++)
    				{
    					UINT colStart = col * 4;
    					UINT byte_height = pTexels[rowStart + colStart + 0];
    					//byte에 저장할수있는 최대값은 0~255
    					//따라서 높이를 조절하려면 나눗셈
    					m_HeightList[row * desc.Width + col] = (static_cast<float>(byte_height)/8.0f)-4.0f;	/// DWORD이므로 pitch/4	
    				}
    			}
    			m_pContext->Unmap(pTexture2D, D3D11CalcSubresource(0, 0, 1)); 
    		}
    	}
    
    	m_num_row = desc.Height;
    	m_num_col = desc.Width;
    
    	if (pTexture2D) pTexture2D->Release();

     

    HRESULT Map(
      [in]            ID3D11Resource           *pResource,//읽을 리소스 포인터
      [in]            UINT                     Subresource,//인덱스 번호
      [in]            D3D11_MAP                MapType,//읽기 쓰기 권한
      [in]            UINT                     MapFlags,//CPU 수행 작업 플래그
      [out, optional] D3D11_MAPPED_SUBRESOURCE *pMappedResource//깊이,크기,사이즈 조회 가능
    );


    Map함수를 호출하여 텍스쳐 정보에 액세스 할 수 있는

    D3D11_MAPPED_SUBRESOURCE 포인터를 받아온다.

    참고로 2번째 인자는 D3D11SubCalcResource 함수로 계산되는데

    인덱스 번호를 지정하는 것으로 [리소스의 밉맵 레벨 X 리소스 번호 + 서브 리소스 번호]

    로 지정이 된다.

    UINT rowStart = row * MappedFaceDest.RowPitch;
    for (UINT col = 0; col < desc.Width; col++)
    {
    	UINT colStart = col * 4;
    	UINT byte_height = pTexels[rowStart + colStart + 0];
    //byte에 저장할수있는 최대값은 0~255
    //따라서 높이를 조절하려면 나눗셈
    	m_HeightList[row * desc.Width + col] = (static_cast<float>(byte_height)/8.0f)-4.0f;
    }

    나머지는 파싱 해서 높이 값 정보 m_HeightList에 저장을 한다. 

    오브젝트 위치에 y축 0에 맞추고 싶어서 적당히 나눠준 모습이다.

     

    https://docs.microsoft.com/en-us/windows/win32/api/d3d11/ns-d3d11-d3d11_mapped_subresource

     

    D3D11_MAPPED_SUBRESOURCE (d3d11.h) - Win32 apps

    Provides access to subresource data.

    docs.microsoft.com

     

    이제 버텍스를 찍는 함수에서 Y값을 높이맵 정보를 저장한 HeightLIst로 만들어 낸다. 

     

    나머지는 저번에 노말 맵 적용하면서 퐁 셰이딩 적용했기 때문에, 

    오브젝트와 마찬가지로 노말, 바이 노말, 탄젠트 값을 버텍스 정보에 넘겨주었다.

    나중에 Flat Shading, Gouraud Shading, Phong Shading에 대해서도 정리해야겠다.

     

    4. 결과

    COMMENT
     
    03
    21

    기존 템플릿은 인자의 타입, 이름, 갯수를 명시하고 

    그대로만 사용했었다. 따라서, 인자의 수가 다르면

    새로운 템플릿을 만들어야 했는데, C++ 11에서는 이러한 불편함을

    해소하고자, 가변 인자 템플릿을 도입했다. 

     

    #include <iostream>
    
    template <typename T>
    void print(T arg) {
    	std::cout << arg << std::endl;
    }
    
    template <typename T, typename... Types>
    void print(T arg, Types... args) {
    	std::cout << arg << ", ";
    	print(args...);
    }
    
    int main() {
    	print(1, 3.1, "abc");
    }

    typename ... 다음에 온 "Types"은 Parameter Pack이라고 한다.

    그리고 print 함수 파라미터에서의 Types... args를 Function Parameter Pack이라고 한다.

    0개 이상의 템플릿이나 함수 인자를 받는 다는 뜻이다.

     

    하나의 함수 파라미터 팩으로, 여러 인자를 받는 다는 건

    재귀적으로 동작한다는 걸 의미함.

    print(1, 3.1, "abc");

    위 함수가 동작하게 되면

    첫번째 인자 int형으로 첫번째 print가 호출이 된다.

    파라미터 팩이 없는 함수의 우선순위가 높기때문이다.

    따라서 기본 템플릿 print 함수 순서가 가변인자 템플릿 보다

    아래에 있다면 오류가 컴파일러는 함수를 찾지 못해 오류가 난다.

     

    sizeof...는 가변 길이 변수의 개수, 즉, 전체 인자의 개수를 리턴한다. 

    이 사이즈는 마지막에 인자가 없을때까지 재귀적으로 함수를 돈다.

    COMMENT
     
    1 2 3 4 5 ··· 9