다음을 통해 공유


DirectX 게임에 리소스 로드

대부분의 게임은 일부 시점에서 로컬 스토리지 또는 다른 데이터 스트림에서 리소스 및 자산(예: 셰이더, 텍스처, 미리 정의된 메시 또는 기타 그래픽 데이터)을 로드합니다. 여기서는 DirectX C/C++ UWP(유니버설 Windows 플랫폼) 게임에 사용하기 위해 이러한 파일을 로딩할 때 고려해야 할 부분에 대해 심도 있게 다룹니다.

예를 들어, 게임에서 다각형 개체의 메시는 다른 도구를 사용하여 생성되고 특정 형식으로 내보낼 수 있습니다. 텍스처의 경우도 마찬가지입니다. 미압축 플랫 비트맵은 대부분의 도구에서 일반적으로 작성되고 대부분의 그래픽 API에서 이해할 수 있지만 게임에서 사용하기에는 매우 비효율적일 수 있습니다. 여기서는 Direct3D에서 사용할 메시(모델), 텍스처(비트맵), 컴파일된 셰이더 개체라는 세 가지 유형의 그래픽 리소스를 로드하기 위한 기본 단계를 안내합니다.

알아야 하는 작업

기술

  • 병렬 패턴 라이브러리(ppltasks.h)

필수 조건

  • 기본 Windows 런타임 이해
  • 비동기 작업 이해
  • 3차원 그래픽 프로그래밍의 기본 개념을 이해합니다.

이 샘플에는 리소스 로드 및 관리를 위한 세 개의 코드 파일도 포함되어 있습니다. 이 토픽 전체에서 이러한 파일에 정의된 코드 개체가 표시됩니다.

  • BasicLoader.h/.cpp
  • BasicReaderWriter.h/.cpp
  • DDSTextureLoader.h/.cpp

이러한 샘플에 대한 전체 코드는 다음 링크에서 찾을 수 있습니다.

항목 설명

BasicLoader의 전체 코드

그래픽 메시 개체를 메모리로 변환하고 로드하는 클래스 및 메서드에 대한 전체 코드입니다.

BasicReaderWriter의 전체 코드

일반적으로 이진 데이터 파일을 읽고 쓰기 위한 클래스 및 메서드에 대한 전체 코드입니다. BasicLoader 클래스에서 사용됩니다.

DDSTextureLoader에 대한 전체 코드

메모리에서 DDS 텍스처를 로드하는 클래스 및 메서드에 대한 전체 코드입니다.

 

지침

비동기 로딩

비동기 로드는 PPL(병렬 패턴 라이브러리)의 작업 템플릿을 사용하여 처리됩니다. 작업에는 메서드 호출 다음에 비동기 호출이 완료된 후 결과를 처리하는 람다가 포함되며 일반적으로 다음 형식을 따릅니다.

task<generic return type>(async code to execute).then((parameters for lambda){ lambda code contents });;

.then() 구문을 사용하여 작업을 함께 연결할 수 있으므로 한 작업이 완료되면 이전 작업의 결과에 따라 다른 비동기 작업을 실행할 수 있습니다. 이러한 방식으로 플레이어가 거의 보이지 않는 방식으로 별도의 스레드에서 복잡한 자산을 로드, 변환, 관리할 수 있습니다.

자세한 내용은 C++에서 비동기 프로그래밍을 참조하세요.

이제 비동기 파일 로드 메서드인 ReadDataAsync를 선언하고 생성하기 위한 기본 구조를 살펴보겠습니다.

#include <ppltasks.h>

// ...
concurrency::task<Platform::Array<byte>^> ReadDataAsync(
        _In_ Platform::String^ filename);

// ...

using concurrency;

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename
    )
{
    return task<StorageFile^>(m_location->GetFileAsync(filename)).then([=](StorageFile^ file)
    {
        return FileIO::ReadBufferAsync(file);
    }).then([=](IBuffer^ buffer)
    {
        auto fileData = ref new Platform::Array<byte>(buffer->Length);
        DataReader::FromBuffer(buffer)->ReadBytes(fileData);
        return fileData;
    });
}

이 코드에서는 코드가 위에서 정의한 ReadDataAsync 메서드를 호출하면 파일 시스템에서 버퍼를 읽는 작업이 생성됩니다. 완료되면 연결된 작업은 버퍼를 가져와 해당 버퍼의 바이트를 정적 DataReader 형식을 사용하여 배열로 스트리밍합니다.

m_basicReaderWriter = ref new BasicReaderWriter();

// ...
return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
    {
      // Perform some operation with the data when the async load completes.          
    });

ReadDataAsync에 대한 호출은 다음과 같습니다. 완료되면 코드는 제공된 파일에서 읽은 바이트 배열을 받습니다. ReadDataAsync 자체는 작업으로 정의되므로 바이트 배열이 반환될 때 람다를 사용하여 해당 바이트 데이터를 사용할 수 있는 DirectX 함수에 전달하는 등의 특정 작업을 수행할 수 있습니다.

게임이 충분히 간단한 경우, 사용자가 게임을 시작할 때 다음과 같은 방법으로 리소스를 로드합니다. IFrameworkView::Run 구현의 호출 시퀀스의 특정 지점에서 기본 게임 루프를 시작하기 전에 이 작업을 수행할 수 있습니다. 다시 말하지만, 게임이 더 빨리 시작될 수 있도록 리소스 로드 메서드를 비동기 호출하므로 플레이어가 초기 상호 작용에 참여하기 전 로드가 완료될 때까지 기다릴 필요가 없습니다.

그러나 모든 비동기 로딩이 완료될 때까지는 게임을 제대로 시작하지 않으려 합니다. 특정 필드와 같이 로드가 완료된 경우 신호를 보낼 수 있는 몇 가지 메서드를 생성하고 로드 방법에서 람다를 사용하여 완료 시 해당 신호를 설정합니다. 로드된 리소스를 사용하는 구성 요소를 시작하기 전 변수를 확인합니다.

다음은 BasicLoader.cpp로 정의된 비동기 메서드를 사용하여 게임 시작 시 셰이더, 메시, 텍스처를 로드하는 예제입니다. 이 예제는 모든 로딩 메서드가 완료되면 게임 개체 m_loadingComplete에 특정 필드를 설정합니다.

void ResourceLoading::CreateDeviceResources()
{
    // DirectXBase is a common sample class that implements a basic view provider. 
    
    DirectXBase::CreateDeviceResources(); 

    // ...

    // This flag will keep track of whether or not all application
    // resources have been loaded.  Until all resources are loaded,
    // only the sample overlay will be drawn on the screen.
    m_loadingComplete = false;

    // Create a BasicLoader, and use it to asynchronously load all
    // application resources.  When an output value becomes non-null,
    // this indicates that the asynchronous operation has completed.
    BasicLoader^ loader = ref new BasicLoader(m_d3dDevice.Get());

    auto loadVertexShaderTask = loader->LoadShaderAsync(
        "SimpleVertexShader.cso",
        nullptr,
        0,
        &m_vertexShader,
        &m_inputLayout
        );

    auto loadPixelShaderTask = loader->LoadShaderAsync(
        "SimplePixelShader.cso",
        &m_pixelShader
        );

    auto loadTextureTask = loader->LoadTextureAsync(
        "reftexture.dds",
        nullptr,
        &m_textureSRV
        );

    auto loadMeshTask = loader->LoadMeshAsync(
        "refmesh.vbo",
        &m_vertexBuffer,
        &m_indexBuffer,
        nullptr,
        &m_indexCount
        );

    // The && operator can be used to create a single task that represents
    // a group of multiple tasks. The new task's completed handler will only
    // be called once all associated tasks have completed. In this case, the
    // new task represents a task to load various assets from the package.
    (loadVertexShaderTask && loadPixelShaderTask && loadTextureTask && loadMeshTask).then([=]()
    {
        m_loadingComplete = true;
    });

    // Create constant buffers and other graphics device-specific resources here.
}

이 작업은 모든 작업이 완료되었을 때만 로딩 완료 플래그를 설정하는 람다가 트리거되도록 && 연산자를 사용하여 집계되었습니다. 플래그가 여러 개 있는 경우 경합 조건이 발생할 수 있습니다. 예를 들어, 람다가 두 플래그를 동일한 값으로 순차적으로 설정하는 경우 두 번째 플래그가 설정되기 전 확인하는 경우에만 다른 스레드가 첫 번째 플래그 집합을 볼 수 있습니다.

리소스 파일을 비동기 로드하는 방법을 살펴보았습니다. 동기 파일 로드는 훨씬 더 간단하며 BasicReaderWriter의 전체 코드BasicLoader의 전체 코드에서 예제를 찾을 수 있습니다.

물론 다양한 리소스 및 자산 유형은 그래픽 파이프라인에서 사용할 준비가 되기 전 추가 처리 또는 변환이 필요한 경우가 많습니다. 메시, 텍스처, 셰이더의 세 가지 특정 리소스 유형을 살펴보겠습니다.

메시 로드

메시는 게임 내 코드에 의해 절차에 따라 생성되거나 다른 앱(예: 3DStudio MAX 또는 Alias WaveFront) 또는 도구에서 파일로 내보내는 꼭짓점 데이터입니다. 이러한 메시는 큐브와 구와 같은 간단한 기본 형식부터 자동차와 주택 및 문자에 이르기까지 게임의 모델을 나타냅니다. 형식에 따라 색 및 애니메이션 데이터도 포함되는 경우가 많습니다. 꼭짓점 데이터만 포함하는 메시에 집중하겠습니다.

메시를 올바르게 로드하려면 메시에 대한 파일의 데이터 형식을 알아야 합니다. 위의 간단한 BasicReaderWriter 형식은 간단히 바이트 스트림으로 데이터를 읽습니다. 바이트 데이터는 메시를 나타내며 다른 애플리케이션에서 내보낸 특정 메시 형식보다 훨씬 적습니다. 메시 데이터를 메모리로 가져올 때 변환을 수행해야 합니다.

(항상 내부 표현과 가까운 형식으로 자산 데이터를 패키지하려고 시도해야 합니다. 이렇게 하면 리소스 사용률이 줄어들고 시간이 절약됩니다.)

메시의 파일에서 바이트 데이터를 가져오겠습니다. 예제의 형식은 파일이 .vbo 접미사가 있는 샘플별 형식이라고 가정합니다. 다시 말하지만, 이 형식은 OpenGL의 VBO 형식과 동일하지 않습니다. 각 꼭짓점 자체는 obj2vbo 변환기 도구의 코드에 정의된 구조체인 BasicVertex 형식에 매핑됩니다. .vbo 파일의 꼭짓점 데이터 레이아웃은 다음과 같습니다.

  • 데이터 스트림의 처음 32비트(4바이트)에는 메시의 꼭짓점 수(numVertices)가 포함되어 uint32 값으로 표시됩니다.
  • 데이터 스트림의 다음 32비트(4바이트)에는 uint32 값으로 표시되는 메시(numIndices)의 인덱스 수가 포함됩니다.
  • 그 뒤 후속 (numVertices * sizeof(BasicVertex)) 비트에는 꼭짓점 데이터가 들어 있습니다.
  • 데이터의 마지막 (numIndices * 16) 비트에는 unit16 값의 시퀀스로 나타낸 인덱스 데이터가 들어 있습니다.

요점은 로드한 메시 데이터의 비트 수준 레이아웃을 알고 있다는 것입니다. 또한 endian-ness와 일치해야 합니다. 모든 Windows 8 플랫폼은 little-endian입니다.

이 예제에서는 LoadMeshAsync 메서드에서 CreateMesh 메서드를 호출하여 이 비트 수준 해석을 수행합니다.

task<void> BasicLoader::LoadMeshAsync(
    _In_ Platform::String^ filename,
    _Out_ ID3D11Buffer** vertexBuffer,
    _Out_ ID3D11Buffer** indexBuffer,
    _Out_opt_ uint32* vertexCount,
    _Out_opt_ uint32* indexCount
    )
{
    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ meshData)
    {
        CreateMesh(
            meshData->Data,
            vertexBuffer,
            indexBuffer,
            vertexCount,
            indexCount,
            filename
            );
    });
}

CreateMesh는 파일에서 로드한 바이트 데이터를 해석한 다음 ID3D11Device::CreateBuffer에 꼭짓점 및 인덱스 목록을 각각 전달하고 D3D11_BIND_VERTEX_BUFFER 또는 D3D11_BIND_INDEX_BUFFER를 지정하여 메시에 대한 꼭짓점 버퍼와 인덱스 버퍼를 만듭니다. BasicLoader에서 사용되는 코드는 다음과 같습니다.

void BasicLoader::CreateMesh(
    _In_ byte* meshData,
    _Out_ ID3D11Buffer** vertexBuffer,
    _Out_ ID3D11Buffer** indexBuffer,
    _Out_opt_ uint32* vertexCount,
    _Out_opt_ uint32* indexCount,
    _In_opt_ Platform::String^ debugName
    )
{
    // The first 4 bytes of the BasicMesh format define the number of vertices in the mesh.
    uint32 numVertices = *reinterpret_cast<uint32*>(meshData);

    // The following 4 bytes define the number of indices in the mesh.
    uint32 numIndices = *reinterpret_cast<uint32*>(meshData + sizeof(uint32));

    // The next segment of the BasicMesh format contains the vertices of the mesh.
    BasicVertex* vertices = reinterpret_cast<BasicVertex*>(meshData + sizeof(uint32) * 2);

    // The last segment of the BasicMesh format contains the indices of the mesh.
    uint16* indices = reinterpret_cast<uint16*>(meshData + sizeof(uint32) * 2 + sizeof(BasicVertex) * numVertices);

    // Create the vertex and index buffers with the mesh data.

    D3D11_SUBRESOURCE_DATA vertexBufferData = {0};
    vertexBufferData.pSysMem = vertices;
    vertexBufferData.SysMemPitch = 0;
    vertexBufferData.SysMemSlicePitch = 0;
    CD3D11_BUFFER_DESC vertexBufferDesc(numVertices * sizeof(BasicVertex), D3D11_BIND_VERTEX_BUFFER);

    m_d3dDevice->CreateBuffer(
            &vertexBufferDesc,
            &vertexBufferData,
            vertexBuffer
            );
    
    D3D11_SUBRESOURCE_DATA indexBufferData = {0};
    indexBufferData.pSysMem = indices;
    indexBufferData.SysMemPitch = 0;
    indexBufferData.SysMemSlicePitch = 0;
    CD3D11_BUFFER_DESC indexBufferDesc(numIndices * sizeof(uint16), D3D11_BIND_INDEX_BUFFER);
    
    m_d3dDevice->CreateBuffer(
            &indexBufferDesc,
            &indexBufferData,
            indexBuffer
            );
  
    if (vertexCount != nullptr)
    {
        *vertexCount = numVertices;
    }
    if (indexCount != nullptr)
    {
        *indexCount = numIndices;
    }
}

일반적으로 게임에서 사용하는 모든 메시에 대해 꼭짓점/인덱스 버퍼 쌍을 만듭니다. 메시를 로드하는 위치와 시기는 사용자에게 달려 있습니다. 메시가 많은 경우, 미리 정의된 특정 로드 상태와 같이 게임의 특정 지점에서 디스크에서 일부만 로드할 수 있습니다. 지형 데이터와 같은 대형 메시의 경우 캐시에서 꼭짓점을 스트리밍할 수 있지만 이 토픽의 범위가 아니라 더 복잡한 프로시저입니다.

다시 말하지만, 꼭짓점 데이터 형식을 알아야 합니다! 모델을 만드는 데 사용되는 도구에서 꼭짓점 데이터를 나타내는 방법에는 여러 가지가 있습니다. 또한 삼각형 목록 및 스트립 등 Direct3D에 대한 꼭짓점 데이터의 입력 레이아웃을 나타내는 여러 가지 방법이 있습니다. 꼭짓점 데이터에 대한 자세한 내용은 Direct3D 11의 버퍼 소개기본 형식을 참조하세요.

다음으로, 텍스처 로드를 살펴보겠습니다.

텍스처 로드

게임에서 가장 일반적인 자산과 디스크 및 메모리에 있는 파일의 대부분을 구성하는 자산은 텍스처입니다. 메시와 마찬가지로 텍스처는 다양한 형식으로 제공되며, 이를 로드할 때 Direct3D에서 사용 가능한 형식으로 변환합니다. 텍스처는 다양한 형식으로 제공되며 다양한 효과를 생성하는 데 사용됩니다. 텍스처에 대한 MIP 수준을 사용하여 거리 개체의 모양과 성능을 향상시킬 수 있습니다. 먼지와 조명 맵은 기본 질감에 효과와 세부 사항을 레이어하는 데 사용됩니다. 그리고 일반 맵은 픽셀별 조명 계산에 사용됩니다. 최신 게임에서 일반적인 장면에는 잠재적으로 수천 개의 개별 텍스처가 있을 수 있으며, 코드는 모든 텍스처를 효과적으로 관리해야 합니다.

메시와 마찬가지로 메모리 사용을 효율적으로 하는 데 사용되는 여러 가지 특정 형식이 있습니다. 텍스처는 GPU(및 시스템) 메모리의 상당 부분을 쉽게 사용할 수 있으므로 종종 특정 방식으로 압축됩니다. 게임의 텍스처에 압축을 사용할 필요는 없으며 Direct3D 셰이더에 이해할 수 있는 형식의 데이터(예: Texture2D 비트맵)를 제공하는 한 원하는 압축/압축 해제 알고리즘을 사용할 수 있습니다.

Direct3D는 모든 DXT 형식이 플레이어의 그래픽 하드웨어에서 지원되지 않을 수 있지만 DXT 텍스처 압축 알고리즘을 지원합니다. DDS 파일은 DXT 텍스처(및 기타 텍스처 압축 형식)를 포함하며 접미사가 .dds입니다.

DDS 파일은 다음 정보를 포함하는 바이너리 파일입니다.

  • 4개의 문자 코드 값 'DDS'(0x20534444)를 포함하는 DWORD(매직 넘버)입니다.

  • 파일의 데이터에 대한 설명입니다.

    데이터는 DDS_HEADER를 사용하여 헤더 설명으로 설명됩니다. 픽셀 형식은 DDS_PIXELFORMAT을 사용하여 정의됩니다. DDS_HEADERDDS_PIXELFORMAT 구조는 사용되지 않는 DDSURFACEDESC2, DDSCAPS2 및 DDPIXELFORMAT DirectDraw 7 구조를 대체합니다. DDS_HEADER는 DDSURFACEDESC2 및 DDSCAPS2의 이진 값입니다. DDS_PIXELFORMAT은 DDPIXELFORMAT의 이진 값입니다.

    DWORD               dwMagic;
    DDS_HEADER          header;
    

    DDS_PIXELFORMAT에서 dwFlags의 값이 DDPF_FOURCC로 설정되고 dwFourCC이 “DX10”으로 설정되는 경우 부동 소수점 형식, sRGB 형식 등, RGB 픽셀 형식으로 표현할 수 없는 DXGI 형식 또는 텍스처 배열을 수용하기 위해 추가 DDS_HEADER_DXT10 구조가 표시됩니다. DDS_HEADER_DXT10 구조가 있는 경우 전체 데이터 설명은 다음과 같이 나타납니다.

    DWORD               dwMagic;
    DDS_HEADER          header;
    DDS_HEADER_DXT10    header10;
    
  • 기본 표면 데이터를 포함하는 바이트 배열에 대한 포인터입니다.

    BYTE bdata[]
    
  • mipmap 수준, 큐브 맵의 얼굴, 볼륨 텍스처의 깊이와 같은 남은 표면을 포함하는 바이트 배열에 대한 포인터입니다. 텍스처, 큐브 맵 또는 볼륨 텍스처에 대한 DDS 파일 레이아웃에 대한 자세한 내용은 다음 링크를 따르세요.

    BYTE bdata2[]
    

많은 도구가 DDS 형식으로 내보냅니다. 텍스처를 이 형식으로 내보낼 도구가 없는 경우, 텍스처를 생성하는 것이 좋습니다. DDS 형식 및 코드에서 DDS로 작업하는 방법에 대한 자세한 내용은 DDS 프로그래밍 가이드를 참조하세요. 이 예제에서는 DDS를 사용합니다.

다른 리소스 종류와 마찬가지로 파일의 데이터를 바이트 스트림으로 읽습니다. 로드 작업이 완료되면 람다 호출은 코드( CreateTexture 메서드)를 실행하여 바이트 스트림을 Direct3D에서 사용할 수 있는 형식으로 처리합니다.

task<void> BasicLoader::LoadTextureAsync(
    _In_ Platform::String^ filename,
    _Out_opt_ ID3D11Texture2D** texture,
    _Out_opt_ ID3D11ShaderResourceView** textureView
    )
{
    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ textureData)
    {
        CreateTexture(
            GetExtension(filename) == "dds",
            textureData->Data,
            textureData->Length,
            texture,
            textureView,
            filename
            );
    });
}

이전 코드 조각에서 람다는 파일 이름에 확장명 'dds'가 있는지 확인하기 위해 검사합니다. 이 경우 DDS 텍스처라고 가정합니다. 그렇지 않은 경우, WIC(Windows 이미징 구성 요소) API를 사용하여 형식을 검색하고 데이터를 비트맵으로 디코딩합니다. 어느 쪽이든 결과는 Texture2D 비트맵(또는 오류)입니다.

void BasicLoader::CreateTexture(
    _In_ bool decodeAsDDS,
    _In_reads_bytes_(dataSize) byte* data,
    _In_ uint32 dataSize,
    _Out_opt_ ID3D11Texture2D** texture,
    _Out_opt_ ID3D11ShaderResourceView** textureView,
    _In_opt_ Platform::String^ debugName
    )
{
    ComPtr<ID3D11ShaderResourceView> shaderResourceView;
    ComPtr<ID3D11Texture2D> texture2D;

    if (decodeAsDDS)
    {
        ComPtr<ID3D11Resource> resource;

        if (textureView == nullptr)
        {
            CreateDDSTextureFromMemory(
                m_d3dDevice.Get(),
                data,
                dataSize,
                &resource,
                nullptr
                );
        }
        else
        {
            CreateDDSTextureFromMemory(
                m_d3dDevice.Get(),
                data,
                dataSize,
                &resource,
                &shaderResourceView
                );
        }

        resource.As(&texture2D);
    }
    else
    {
        if (m_wicFactory.Get() == nullptr)
        {
            // A WIC factory object is required in order to load texture
            // assets stored in non-DDS formats.  If BasicLoader was not
            // initialized with one, create one as needed.
            CoCreateInstance(
                    CLSID_WICImagingFactory,
                    nullptr,
                    CLSCTX_INPROC_SERVER,
                    IID_PPV_ARGS(&m_wicFactory));
        }

        ComPtr<IWICStream> stream;
        m_wicFactory->CreateStream(&stream);

        stream->InitializeFromMemory(
                data,
                dataSize);

        ComPtr<IWICBitmapDecoder> bitmapDecoder;
        m_wicFactory->CreateDecoderFromStream(
                stream.Get(),
                nullptr,
                WICDecodeMetadataCacheOnDemand,
                &bitmapDecoder);

        ComPtr<IWICBitmapFrameDecode> bitmapFrame;
        bitmapDecoder->GetFrame(0, &bitmapFrame);

        ComPtr<IWICFormatConverter> formatConverter;
        m_wicFactory->CreateFormatConverter(&formatConverter);

        formatConverter->Initialize(
                bitmapFrame.Get(),
                GUID_WICPixelFormat32bppPBGRA,
                WICBitmapDitherTypeNone,
                nullptr,
                0.0,
                WICBitmapPaletteTypeCustom);

        uint32 width;
        uint32 height;
        bitmapFrame->GetSize(&width, &height);

        std::unique_ptr<byte[]> bitmapPixels(new byte[width * height * 4]);
        formatConverter->CopyPixels(
                nullptr,
                width * 4,
                width * height * 4,
                bitmapPixels.get());

        D3D11_SUBRESOURCE_DATA initialData;
        ZeroMemory(&initialData, sizeof(initialData));
        initialData.pSysMem = bitmapPixels.get();
        initialData.SysMemPitch = width * 4;
        initialData.SysMemSlicePitch = 0;

        CD3D11_TEXTURE2D_DESC textureDesc(
            DXGI_FORMAT_B8G8R8A8_UNORM,
            width,
            height,
            1,
            1
            );

        m_d3dDevice->CreateTexture2D(
                &textureDesc,
                &initialData,
                &texture2D);

        if (textureView != nullptr)
        {
            CD3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc(
                texture2D.Get(),
                D3D11_SRV_DIMENSION_TEXTURE2D
                );

            m_d3dDevice->CreateShaderResourceView(
                    texture2D.Get(),
                    &shaderResourceViewDesc,
                    &shaderResourceView);
        }
    }


    if (texture != nullptr)
    {
        *texture = texture2D.Detach();
    }
    if (textureView != nullptr)
    {
        *textureView = shaderResourceView.Detach();
    }
}

이 코드가 완료되면 이미지 파일에서 로드된 Texture2D가 메모리에 있습니다. 메시와 마찬가지로 게임과 특정 장면에서 메시가 많이 있을 것입니다. 게임 또는 레벨이 시작될 경우 모든 텍스처를 로드하지 않고 장면당 또는 레벨별로 정기적으로 액세스하는 텍스처에 대한 캐시를 만드는 것이 좋습니다.

(다음 위 샘플에서 호출된 CreateDDSTextureFromMemory 메서드는 DDSTextureLoader에 대한 전체 코드에서 탐색할 수 있습니다.)

또한 개별 텍스처 또는 질감 '스킨'은 특정 메시 다각형 또는 표면에 매핑할 수 있습니다. 이 매핑 데이터는 일반적으로 모델 및 텍스처를 생성하는 데 사용되는 아티스트 또는 디자이너 도구에서 내보냅니다. 조각 음영 수행 시 올바른 텍스처를 해당 표면에 매핑하므로 내보낸 데이터를 로드할 때도 이 정보를 캡처해야 합니다.

셰이더 로드

셰이더는 메모리에 로드되고 그래픽 파이프라인의 특정 단계에서 호출되는 HLSL(High Level Shader Language) 파일로 컴파일됩니다. 가장 일반적이고 필수적인 셰이더는 꼭짓점 및 픽셀 셰이더로 메시의 개별 꼭짓점과 장면 뷰포트의 픽셀을 각각 처리합니다. HLSL 코드는 기하 도형을 변환하고, 조명 효과와 질감을 적용하며, 렌더링된 장면에서 후처리를 수행하기 위해 실행됩니다.

Direct3D 게임에는 다양한 셰이더가 많이 있을 수 있으며, 각 셰이더는 별도의 CSO(컴파일된 셰이더 개체, .cso) 파일로 컴파일됩니다. 일반적으로 동적으로 로드해야 하는 경우가 많지 않으며, 대부분의 경우 게임이 시작될 때 또는 수준별로 로드할 수 있습니다(예: 비 효과에 대한 셰이더).

BasicLoader 클래스의 코드는 꼭짓점, 기하 도형, 픽셀 및 헐 셰이더를 포함하여 다양한 셰이더에 대한 여러 오버로드를 제공합니다. 아래 코드에서는 픽셀 셰이더를 예제로 다룹니다. (BasicLoader에 대한 전체 코드에서 전체 코드를 검토할 수 있습니다.)

concurrency::task<void> LoadShaderAsync(
    _In_ Platform::String^ filename,
    _Out_ ID3D11PixelShader** shader
    );

// ...

task<void> BasicLoader::LoadShaderAsync(
    _In_ Platform::String^ filename,
    _Out_ ID3D11PixelShader** shader
    )
{
    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
    {
        
       m_d3dDevice->CreatePixelShader(
                bytecode->Data,
                bytecode->Length,
                nullptr,
                shader);
    });
}

이 예제에서는 BasicReaderWriter 인스턴스(m_basicReaderWriter)를 사용하여 제공된 .cso(컴파일된 셰이더 개체) 파일에서 바이트 스트림으로 읽습니다. 해당 작업이 완료되면 람다는 파일에서 로드된 바이트 데이터를 사용하여 ID3D11Device::CreatePixelShader를 호출합니다. 콜백은 로드가 성공했음을 나타내는 일부 플래그를 설정해야 하며, 코드는 셰이더를 실행하기 전에 이 플래그를 확인해야 합니다.

꼭짓점 셰이더는 좀 더 복잡합니다. 꼭짓점 셰이더의 경우 꼭짓점 데이터를 정의하는 별도의 입력 레이아웃 역시 로드합니다. 다음 코드를 사용하여 사용자 지정 꼭짓점 입력 레이아웃과 더불어 꼭짓점 셰이더를 비동기적으로 로드할 수 있습니다. 메시에서 로드하는 꼭짓점 정보를 이 입력 레이아웃으로 올바로 나타낼 수 있어야 합니다.

꼭짓점 셰이더를 로드하기 전에 입력 레이아웃을 만들어 보겠습니다.

void BasicLoader::CreateInputLayout(
    _In_reads_bytes_(bytecodeSize) byte* bytecode,
    _In_ uint32 bytecodeSize,
    _In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC* layoutDesc,
    _In_ uint32 layoutDescNumElements,
    _Out_ ID3D11InputLayout** layout
    )
{
    if (layoutDesc == nullptr)
    {
        // If no input layout is specified, use the BasicVertex layout.
        const D3D11_INPUT_ELEMENT_DESC basicVertexLayoutDesc[] =
        {
            { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,  D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "NORMAL",   0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,    0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        };

        m_d3dDevice->CreateInputLayout(
                basicVertexLayoutDesc,
                ARRAYSIZE(basicVertexLayoutDesc),
                bytecode,
                bytecodeSize,
                layout);
    }
    else
    {
        m_d3dDevice->CreateInputLayout(
                layoutDesc,
                layoutDescNumElements,
                bytecode,
                bytecodeSize,
                layout);
    }
}

이 특정 레이아웃에서 각 꼭짓점은 꼭짓점 셰이더에서 처리되는 다음과 같은 데이터를 가지고 있습니다.

  • 모델의 좌표 공간에서 32비트 부동 소수점 값의 3가지로 표현되는 3D 좌표 위치(x, y, z)입니다.
  • 꼭짓점에 대한 일반 벡터로 32비트 부동 소수점 값으로도 표시됩니다.
  • 32비트 부동 값 쌍으로 표현되는 변환된 2D 텍스처 좌표 값(u, v)입니다.

이러한 꼭짓점별 입력 요소를 HLSL 의미 체계라고 하며 컴파일된 셰이더 개체와 데이터를 전달하는 데 사용되는 정의된 레지스터 집합입니다. 파이프라인은 로드한 메시의 모든 꼭짓점마다 꼭짓점 셰이더를 한 번 실행합니다. 의미 체계는 실행 시 꼭짓점 셰이더에 대한 입력 및 출력을 정의하고, 셰이더의 HLSL 코드에서 꼭짓점별 계산에 대한 이 데이터를 제공합니다.

이제 꼭짓점 셰이더 개체를 로드합니다.

concurrency::task<void> LoadShaderAsync(
        _In_ Platform::String^ filename,
        _In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC layoutDesc[],
        _In_ uint32 layoutDescNumElements,
        _Out_ ID3D11VertexShader** shader,
        _Out_opt_ ID3D11InputLayout** layout
        );

// ...

task<void> BasicLoader::LoadShaderAsync(
    _In_ Platform::String^ filename,
    _In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC layoutDesc[],
    _In_ uint32 layoutDescNumElements,
    _Out_ ID3D11VertexShader** shader,
    _Out_opt_ ID3D11InputLayout** layout
    )
{
    // This method assumes that the lifetime of input arguments may be shorter
    // than the duration of this task.  In order to ensure accurate results, a
    // copy of all arguments passed by pointer must be made.  The method then
    // ensures that the lifetime of the copied data exceeds that of the task.

    // Create copies of the layoutDesc array as well as the SemanticName strings,
    // both of which are pointers to data whose lifetimes may be shorter than that
    // of this method's task.
    shared_ptr<vector<D3D11_INPUT_ELEMENT_DESC>> layoutDescCopy;
    shared_ptr<vector<string>> layoutDescSemanticNamesCopy;
    if (layoutDesc != nullptr)
    {
        layoutDescCopy.reset(
            new vector<D3D11_INPUT_ELEMENT_DESC>(
                layoutDesc,
                layoutDesc + layoutDescNumElements
                )
            );

        layoutDescSemanticNamesCopy.reset(
            new vector<string>(layoutDescNumElements)
            );

        for (uint32 i = 0; i < layoutDescNumElements; i++)
        {
            layoutDescSemanticNamesCopy->at(i).assign(layoutDesc[i].SemanticName);
        }
    }

    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
    {
       m_d3dDevice->CreateVertexShader(
                bytecode->Data,
                bytecode->Length,
                nullptr,
                shader);

        if (layout != nullptr)
        {
            if (layoutDesc != nullptr)
            {
                // Reassign the SemanticName elements of the layoutDesc array copy to point
                // to the corresponding copied strings. Performing the assignment inside the
                // lambda body ensures that the lambda will take a reference to the shared_ptr
                // that holds the data.  This will guarantee that the data is still valid when
                // CreateInputLayout is called.
                for (uint32 i = 0; i < layoutDescNumElements; i++)
                {
                    layoutDescCopy->at(i).SemanticName = layoutDescSemanticNamesCopy->at(i).c_str();
                }
            }

            CreateInputLayout(
                bytecode->Data,
                bytecode->Length,
                layoutDesc == nullptr ? nullptr : layoutDescCopy->data(),
                layoutDescNumElements,
                layout);   
        }
    });
}

이 코드에서 꼭짓점 셰이더의 CSO 파일에 대한 바이트 데이터를 읽은 후에는 ID3D11Device::CreateVertexShader를 호출하여 꼭짓점 셰이더를 만듭니다. 그런 다음 동일한 람다에서 셰이더에 대한 입력 레이아웃을 생성합니다.

헐 및 지오메트리 셰이더와 같은 다른 셰이더 형식에도 특정 구성이 필요할 수 있습니다. 다양한 셰이더 로드 방법에 대한 전체 코드는 BasicLoader용 전체 코드Direct3D 리소스 로드 샘플에서 제공됩니다.

설명

이 시점에서 메시, 텍스처 및 컴파일된 셰이더와 같은 일반적인 게임 리소스 및 자산을 비동기적으로 로드하기 위한 메서드를 파악하고 수정할 수 있어야 합니다.