Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Większość gier w pewnym momencie ładuje zasoby i dane (takie jak shadery, tekstury, wstępnie zdefiniowane siatki lub inne dane graficzne) z przechowywania lokalnego lub innego strumienia danych. W tym miejscu przedstawiono ogólny widok tego, co należy wziąć pod uwagę podczas ładowania tych plików do użycia w grze DirectX C/C++ Universal Windows Platform (UWP).
Na przykład siatki dla obiektów wielokątnych w grze mogły zostać utworzone za pomocą innego narzędzia i wyeksportowane do określonego formatu. To samo dotyczy tekstur i nie tylko: podczas gdy płaska, nieskompresowana mapa bitowa może być często zapisywana przez większość narzędzi i rozumiana przez większość interfejsów API graficznych, może być niezwykle nieefektywna do użycia w grze. W tym miejscu przeprowadzimy Cię przez podstawowe kroki ładowania trzech różnych typów zasobów graficznych do użycia z funkcją Direct3D: siatki (modele), tekstury (mapy bitowe) i skompilowane obiekty cieniowania.
Co musisz wiedzieć
Technologie
- Biblioteka wzorców równoległych (ppltasks.h)
Wymagania wstępne
- Omówienie podstawowego środowiska uruchomieniowego systemu Windows
- Omówienie zadań asynchronicznych
- Zapoznaj się z podstawowymi pojęciami programowania grafiki 3-W.
Ten przykład zawiera również trzy pliki kodu na potrzeby ładowania zasobów i zarządzania nimi. W tym temacie napotkasz obiekty kodu zdefiniowane w tych plikach.
- BasicLoader.h/.cpp
- BasicReaderWriter.h/.cpp
- DDSTextureLoader.h/.cpp
Pełny kod dla tych przykładów można znaleźć w poniższych linkach.
| Temat | Opis |
|---|---|
Kompletny kod dla klasy i metod, które konwertują i ładują obiekty siatki grafiki na pamięć. |
|
Kompletny kod dla BasicReaderWriter |
Kompletny kod dla klasy i metod odczytywania i zapisywania plików danych binarnych w ogóle. Używany przez klasę BasicLoader. |
Kompletny kod dla DDSTextureLoader |
Kompletny kod dla klasy i metody, która ładuje teksturę DDS z pamięci. |
Instrukcje
Ładowanie asynchroniczne
Ładowanie asynchroniczne jest obsługiwane przy użyciu szablonu zadania z biblioteki równoległych wzorców (PPL). Zadanie zawiera wywołanie metody, po którym następuje lambda przetwarzająca wyniki wywołania asynchronicznego po jego zakończeniu i zazwyczaj przyjmuje format:
task<generic return type>(async code to execute).then((parameters for lambda){ lambda code contents });.
Zadania można połączyć w łańcuch przy użyciu składni .then(), aby po zakończeniu jednej operacji można było uruchomić inną operację asynchroniczną, która zależy od wyników poprzedniej operacji. W ten sposób można ładować, konwertować i zarządzać złożonymi zasobami na osobnych wątkach w sposób, który wydaje się niemal niewidoczny dla odtwarzacza.
Aby uzyskać więcej informacji, przeczytaj Asynchroniczne programowanie w języku C++.
Teraz przyjrzyjmy się podstawowej strukturze deklarowania i tworzenia metody ładowania plików asynchronicznych 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;
});
}
W tym kodzie, gdy kod wywołuje metodę ReadDataAsync zdefiniowaną powyżej, zadanie jest tworzone w celu odczytania buforu z systemu plików. Po zakończeniu powiązane zadanie pobiera bufor i przesyła bajty z tego bufora strumieniowo do tablicy, używając statycznego typu 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.
});
Oto wywołanie do readDataAsync. Po zakończeniu kod otrzymuje tablicę bajtów odczytanych z podanego pliku. Ponieważ sam readDataAsync jest definiowany jako zadanie, można użyć wyrażenia lambda do wykonania określonej operacji, gdy zostanie zwrócona tablica bajtów, na przykład przekazanie tych danych bajtowych do funkcji DirectX, która może je wykorzystać.
Jeśli gra jest wystarczająco prosta, załaduj zasoby za pomocą metody podobnej do tej, gdy użytkownik rozpocznie grę. Możesz to zrobić przed rozpoczęciem pętli głównej gry w pewnym momencie w sekwencji wywołań implementacji IFrameworkView::Run. Ponownie wywołaj metody ładowania zasobów asynchronicznie, aby gra mogła rozpocząć się szybciej, a więc gracz nie musi czekać, aż ładowanie zakończy się przed rozpoczęciem interakcji.
Nie chcesz jednak uruchamiać gry właściwej, aż zakończy się całe ładowanie asynchroniczne! Utwórz metodę do sygnalizowania zakończenia ładowania, na przykład poprzez ustawienie określonego pola, i użyj wyrażeń lambda w swoich metodach ładowania, aby ustawić ten sygnał, gdy ładowanie zostanie zakończone. Przed uruchomieniem wszystkich składników korzystających z załadowanych zasobów sprawdź zmienną.
Oto przykład użycia metod asynchronicznych zdefiniowanych w BasicLoader.cpp do ładowania cieniowania, siatki i tekstury po uruchomieniu gry. Zwróć uwagę, że ustawia określone pole na obiekcie gry, m_loadingComplete, po zakończeniu wszystkich metod ładowania.
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.
}
Należy pamiętać, że zadania zostały zagregowane przy użyciu operatora &&, tak aby lambda ustawiająca flagę zakończenia ładowania była wyzwalana tylko wtedy, gdy wszystkie zadania zostaną ukończone. Należy pamiętać, że jeśli masz wiele flag, masz możliwość warunków wyścigu. Jeśli na przykład lambda ustawia dwie flagi sekwencyjnie na tę samą wartość, inny wątek może zobaczyć tylko pierwszą flagę ustawioną, jeśli sprawdza je przed ustawieniem drugiej flagi.
Pokazano, jak asynchronicznie ładować pliki zasobów. Synchroniczne ładowanie plików jest znacznie prostsze i można znaleźć ich przykłady w Kompletny kod dla BasicReaderWriter i Kompletny kod dla BasicLoader.
Oczywiście różne typy zasobów i aktywów często wymagają dodatkowego przetwarzania lub konwersji, zanim będą gotowe do użycia w potoku grafiki. Przyjrzyjmy się trzem konkretnym typom zasobów: siatkom, teksturom i shaderom.
Ładowanie siatek
Siatki to dane dotyczące wierzchołków, generowane proceduralnie przez kod w grze lub eksportowane do pliku z innego programu (na przykład 3DStudio MAX lub Alias WaveFront) lub narzędzia. Te siatki reprezentują modele w grze, od prostych pierwotnych, takich jak kostki i kule, po samochody i domy i postacie. Często zawierają one również dane kolorów i animacji w zależności od ich formatu. Skupimy się na siatkach, które zawierają tylko dane wierzchołkowe.
Aby poprawnie załadować siatkę, musisz znać format danych w pliku dla siatki. Nasz prosty typ BasicReaderWriter powyżej po prostu odczytuje dane jako strumień bajtów; nie wie, że dane bajtów reprezentują siatkę, a tym bardziej określony format siatki eksportowany przez inną aplikację! Konwersję należy wykonać podczas wprowadzania danych siatki do pamięci.
(Zawsze należy próbować pakować dane zasobów w formacie zbliżonym do reprezentacji wewnętrznej, jak to możliwe. Pozwoli to zmniejszyć wykorzystanie zasobów i zaoszczędzić czas.
Pobierzmy dane bajtów z pliku siatki. Format w przykładzie zakłada, że plik ma format specyficzny dla próbki z sufiksem .vbo. (Ponownie ten format nie jest taki sam jak format VBO openGL). Każdy wierzchołek jest mapowany na typ BasicVertex , który jest strukturą zdefiniowaną w kodzie narzędzia konwertera obj2vbo. Układ danych wierzchołka w pliku .vbo wygląda następująco:
- Pierwsze 32 bity (4 bajty) strumienia danych zawierają liczbę wierzchołków (numVertices) w siatce reprezentowanej jako wartość uint32.
- Następne 32 bity (4 bajty) strumienia danych zawierają liczbę indeksów w siatce (indeksy liczbowe), reprezentowane jako wartość uint32.
- Następnie kolejne (numVertices * sizeof(BasicVertex)) bity zawierają dane wierzchołka.
- Ostatnie (numIndices * 16) bity danych zawierają dane indeksów, reprezentowane jako sekwencja wartości uint16.
Sedno sprawy to znać układ na poziomie bitów załadowanych danych siatki. Upewnij się również, że jesteś zgodny z endian-ness. Wszystkie platformy systemu Windows 8 są mało kompleksowe.
W tym przykładzie wywołasz metodę CreateMesh z metody LoadMeshAsync , aby wykonać tę interpretację na poziomie bitu.
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 interpretuje dane bajtowe załadowane z pliku i tworzy bufor wierzchołków oraz bufor indeksów dla siatki, poprzez przekazanie kolejno listy wierzchołków i indeksów do ID3D11Device::CreateBuffer, specyfikując jako D3D11_BIND_VERTEX_BUFFER lub D3D11_BIND_INDEX_BUFFER. Oto kod używany w narzędziu 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;
}
}
Zazwyczaj tworzysz parę buforów wierzchołków i indeksów dla każdej siatki używanej w swojej grze. Gdzie i kiedy ładujesz siatki, jest do Ciebie. Jeśli masz wiele siatek, możesz chcieć załadować część z nich na dysku tylko w określonych momentach gry, takich jak podczas konkretnych, wstępnie zdefiniowanych stanów ładowania. W przypadku dużych siatek, takich jak dane terenu, można przesyłać strumieniowo wierzchołki z pamięci podręcznej, ale jest to bardziej złożona procedura, co nie jest objęte zakresem tego tematu.
Ponownie poznaj format danych wierzchołka! Istnieje wiele sposobów reprezentowania danych wierzchołków w narzędziach używanych do tworzenia modeli. Istnieje również wiele różnych sposobów reprezentowania układu wejściowego danych wierzchołka na Direct3D, takich jak listy trójkątów i paski. Aby uzyskać więcej informacji na temat danych wierzchołków, przeczytaj Introduction to Buffers in Direct3D 11 i Primitives.
Następnie przyjrzyjmy się ładowaniu tekstur.
Ładowanie tekstur
Najczęstszym zasobem w grze — i tym, który składa się z większości plików na dysku i w pamięci — są tekstury. Podobnie jak siatki, tekstury mogą występować w różnych formatach, a podczas ich ładowania konwertujesz je na format używany przez Direct3D. Tekstury są również dostępne w wielu różnych typach i są używane do tworzenia różnych efektów. Poziomy MIP tekstur mogą służyć do poprawy wyglądu i wydajności obiektów znajdujących się w oddali; mapy zabrudzeń i światła są używane do efektów warstwowych i szczegółów na teksturze bazowej; oraz mapy normalne są używane w obliczeniach oświetlenia na piksel. W nowoczesnej grze typowa scena może potencjalnie mieć tysiące pojedynczych tekstur, a twój kod musi skutecznie zarządzać nimi wszystkimi!
Podobnie jak siatki, istnieje wiele konkretnych formatów, które są używane do wydajnego użycia pamięci. Ponieważ tekstury mogą łatwo zużywać dużą część pamięci procesora GPU (i systemu), są one często kompresowane w jakiś sposób. Nie musisz używać kompresji na teksturach gry i możesz użyć dowolnych algorytmów kompresji/dekompresji, o ile dostarczasz shadery Direct3D z danymi w formacie zrozumiałym dla nich (na przykład Texture2D mapy bitowej).
Funkcja Direct3D zapewnia obsługę algorytmów kompresji teksturY DXT, chociaż każdy format DXT może nie być obsługiwany w sprzęcie graficznym odtwarzacza. Pliki DDS zawierają tekstury DXT (i inne formaty kompresji tekstury), a także są sufiksowane za pomocą .dds.
Plik DDS jest plikiem binarnym zawierającym następujące informacje:
DWORD (liczba magiczna) zawierająca czteroznaczną wartość kodu "DDS" (0x20534444).
Opis danych w pliku.
Dane są opisane za pomocą opisu nagłówka przy użyciu DDS_HEADER; format pikseli jest definiowany przy użyciu DDS_PIXELFORMAT. Należy pamiętać, że struktury DDS_HEADER i DDS_PIXELFORMAT zastępują przestarzałe struktury DDSURFACEDESC2, DDSCAPS2 i DDPIXELFORMAT DirectDraw 7. DDS_HEADER jest binarnym odpowiednikiem DDSURFACEDESC2 i DDSCAPS2. DDS_PIXELFORMAT jest binarnym odpowiednikiem formatu DDPIXELFORMAT.
DWORD dwMagic; DDS_HEADER header;Jeśli wartość dwFlags w DDS_PIXELFORMAT jest ustawiona na DDPF_FOURCC, a dwFourCC jest ustawiona na "DX10" dodatkowa struktura DDS_HEADER_DXT10 będzie obecna w celu uwzględnienia tablic tekstur lub formatów DXGI, które nie mogą być wyrażone jako format pikseli RGB, taki jak formaty zmiennoprzecinkowe, Formaty sRGB itp. Gdy struktura DDS_HEADER_DXT10 jest obecna, cały opis danych będzie wyglądać następująco.
DWORD dwMagic; DDS_HEADER header; DDS_HEADER_DXT10 header10;Wskaźnik do tablicy bajtów zawierającej główne dane powierzchni.
BYTE bdata[]Wskaźnik do tablicy bajtów, która zawiera pozostałe powierzchnie, takie jak poziomy mipmap, twarze w mapie sześciennej, głębokości w teksturze wolumetrycznej. Skorzystaj z poniższych linków, aby uzyskać więcej informacji na temat układu pliku DDS dla tekstury , mapy modułu lub tekstury woluminu .
BYTE bdata2[]
Wiele narzędzi eksportuje do formatu DDS. Jeśli nie masz narzędzia do eksportowania tekstury do tego formatu, rozważ utworzenie tej tekstury. Aby uzyskać więcej informacji na temat formatu DDS i sposobu pracy z nim w kodzie, przeczytaj Przewodnik programowania dla usługi DDS. W naszym przykładzie użyjemy usługi DDS.
Podobnie jak w przypadku innych typów zasobów, dane z pliku są odczytywane jako strumień bajtów. Po zakończeniu zadania ładowania wywołanie lambda uruchamia kod (metoda CreateTexture ), aby przetworzyć strumień bajtów w formacie, którego można użyć w trybie 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
);
});
}
W poprzednim fragmencie kodu lambda sprawdza, czy nazwa pliku ma rozszerzenie "dds". Jeśli tak, zakładasz, że jest to tekstura DDS. Jeśli tak nie jest, użyj interfejsów API składnika Windows Imaging (WIC), aby odnaleźć format i zdekodować dane jako mapę bitową. Tak czy inaczej, wynikiem jest Texture2D mapa bitowa (lub błąd).
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();
}
}
Po zakończeniu tego kodu masz Texture2D w pamięci załadowaną z pliku obrazu. Podobnie jak w przypadku siatek, prawdopodobnie masz ich wiele w swojej grze i w każdej konkretnej scenie. Zastanów się nad tworzeniem pamięci podręcznych dla często używanych tekstur dla każdej sceny lub poziomu, zamiast ładować je wszystkie przy uruchomieniu gry lub poziomu.
(Metodę CreateDDSTextureFromMemory, wywoływaną w powyższym przykładzie, można w pełni przeanalizować w kompletnym kodzie dlaDDSTextureLoader).
Ponadto poszczególne tekstury lub tekstury „skóry” można mapować na konkretne wielokąty lub powierzchnie siatki. Te dane mapowania są zwykle eksportowane przez narzędzie artysty lub projektanta używanego do tworzenia modelu i tekstur. Upewnij się, że przechwytujesz te informacje również podczas ładowania wyeksportowanych danych, ponieważ będą one używane do mapowania poprawnych tekstur na odpowiednie powierzchnie, gdy wykonujesz cieniowanie fragmentów.
Ładowanie shaderów
Shadery to skompilowane pliki języka HLSL (High Level Shader Language), które są ładowane do pamięci i wywoływane na określonych etapach potoku graficznego. Najbardziej typowe i podstawowe shadery to shadery wierzchołków i pikseli, które przetwarzają poszczególne wierzchołki siatki oraz piksele w widokach sceny. Kod HLSL jest wykonywany w celu przekształcenia geometrii, zastosowania efektów oświetlenia i tekstur oraz wykonania przetwarzania końcowego na renderowanej scenie.
Gra Direct3D może mieć wiele różnych shaderów, z których każdy jest skompilowany w osobnym pliku CSO ("Skompilowany Obiekt Shaderów", .cso). Zazwyczaj nie masz aż tylu, aby trzeba było je ładować dynamicznie, a w większości przypadków można je po prostu załadować podczas uruchamiania gry lub na poziomie poszczególnych etapów (na przykład shader dla efektów deszczu).
Kod w klasie BasicLoader zawiera szereg przeciążeń dla różnych cieni, w tym cieni wierzchołków, geometrii, pikseli i kadłuba. Poniższy kod obejmuje shadery pikseli jako przykład. (Pełny kod można przejrzeć w temacie Complete code for BasicLoader (Kompletny kod dla modułu 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);
});
}
W tym przykładzie używasz wystąpienia BasicReaderWriter (m_basicReaderWriter), aby odczytać plik obiektu skompilowanego shader'a (.cso) z dostarczonego materiału jako strumień bajtów. Po zakończeniu tego zadania lambda wywołuje ID3D11Device::CreatePixelShader z danymi bajtowymi załadowanymi z pliku. Twoje wywołanie zwrotne musi ustawić flagę, że ładowanie zakończyło się pomyślnie, a twój kod musi sprawdzić tę flagę przed uruchomieniem shadera.
Cieniowania wierzchołków są nieco bardziej złożone. W przypadku cieniowania wierzchołków należy również załadować oddzielny układ wejściowy, który definiuje dane wierzchołka. Poniższy kod może służyć do asynchronicznego ładowania cieniowania wierzchołków wraz z niestandardowym układem wejściowym wierzchołka. Upewnij się, że informacje o wierzchołkach, które ładujesz z siatek, mogą być poprawnie reprezentowane przez ten układ wejściowy!
Utwórzmy układ danych wejściowych przed załadowaniem cieniowania wierzchołków.
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);
}
}
W tym konkretnym układzie każdy wierzchołek ma następujące dane przetwarzane przez shader wierzchołków.
- Położenie współrzędnych 3D (x, y, z) w przestrzeni współrzędnej modelu, reprezentowane jako trio 32-bitowych wartości zmiennoprzecinkowych.
- Normalny wektor wierzchołka, również reprezentowany jako trzy 32-bitowe wartości zmiennoprzecinkowe.
- Przekształcona wartość współrzędnych tekstury 2D (u, v) , reprezentowana jako para 32-bitowych wartości zmiennoprzecinkowych.
Te elementy wejściowe dla wierzchołków są nazywane semantykami HLSLi są zestawem zdefiniowanych rejestrów używanych do przekazywania danych do i z skompilowanego obiektu cieniowania. Twój potok uruchamia shader wierzchołków raz dla każdego wierzchołka w załadowanej siatce. Semantyka definiuje dane wejściowe do (i wyjściowe z) shaderu wierzchołkowego podczas jego działania, dostarczając te dane do obliczeń na wierzchołkach w kodzie HLSL twojego modułu cieniującego.
Teraz załaduj obiekt cieniowania wierzchołków.
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);
}
});
}
W tym kodzie, po odczytaniu danych bajtów dla pliku CSO shadera wierzchołków, tworzysz shader wierzchołków, wywołując ID3D11Device::CreateVertexShader. Następnie utworzysz układ danych wejściowych dla shader'a w tej samej lambda.
Inne typy cieniowania, takie jak kadłub i cieniowania geometryczne, mogą również wymagać określonej konfiguracji. Kompletny kod dla różnych metod ładowania shaderów jest dostępny w Kompletny kod dla BasicLoader i w przykładzie ładowania zasobów Direct3D.
Uwagi
W tym momencie powinieneś rozumieć i być w stanie tworzyć lub modyfikować metody asynchronicznego ładowania typowych zasobów gier, takich jak siatki, tekstury i skompilowane cienie.
Tematy pokrewne
- przykład ładowania zasobów Direct3D
- Kompletny kod BasicLoader
- Kompletny kod dla BasicReaderWriter
- Kompletny kod dla DDSTextureLoader