Charger des ressources dans votre jeu DirectX
La plupart des jeux, à un moment donné, chargent des ressources et des ressources (telles que les nuanceurs, les textures, les maillages prédéfinis ou d’autres données graphiques) à partir du stockage local ou d’un autre flux de données. Ici, nous vous présentons une vue générale de ce que vous devez prendre en compte lors du chargement de ces fichiers à utiliser dans votre jeu directX C/C++ plateforme Windows universelle (UWP).
Par exemple, les maillages des objets polygonaux de votre jeu ont peut-être été créés avec un autre outil et exportés dans un format spécifique. La même chose est vraie pour les textures, et plus encore : alors qu’une bitmap plate et non compressée peut être couramment écrite par la plupart des outils et comprise par la plupart des API graphiques, elle peut être extrêmement inefficace pour une utilisation dans votre jeu. Ici, nous vous guidons tout au long des étapes de base pour charger trois types de ressources graphiques différents à utiliser avec Direct3D : maillages (modèles), textures (bitmaps) et objets nuanceurs compilés.
Bon à savoir
Technologies
- Bibliothèque de modèles parallèles (ppltasks.h)
Prérequis
- Comprendre le Windows Runtime de base
- Comprendre les tâches asynchrones
- Comprendre les concepts de base de la programmation graphique 3D.
Cet exemple inclut également trois fichiers de code pour le chargement et la gestion des ressources. Vous rencontrerez les objets de code définis dans ces fichiers dans cette rubrique.
- BasicLoader.h/.cpp
- BasicReaderWriter.h/.cpp
- DDSTextureLoader.h/.cpp
Le code complet de ces exemples se trouve dans les liens suivants.
Sujet | Description |
---|---|
Code complet pour une classe et des méthodes qui convertissent et chargent des objets de maillage graphique en mémoire. |
|
Code complet pour une classe et des méthodes de lecture et d’écriture de fichiers de données binaires en général. Utilisé par la classe BasicLoader. |
|
Code complet pour une classe et une méthode qui charge une texture DDS à partir de la mémoire. |
Instructions
Chargement asynchrone
Le chargement asynchrone est géré à l’aide du modèle de tâche de la bibliothèque de modèles parallèles (PPL). Une tâche contient un appel de méthode suivi d’un lambda qui traite les résultats de l’appel asynchrone une fois qu’il est terminé, et suit généralement le format de :
task<generic return type>(async code to execute).then((parameters for lambda){ lambda code contents });
.
Les tâches peuvent être chaînées à l’aide de la syntaxe .then(), de sorte que lorsque l’une des opérations se termine, une autre opération asynchrone qui dépend des résultats de l’opération précédente peut être exécutée. De cette façon, vous pouvez charger, convertir et gérer des ressources complexes sur des threads distincts d’une manière qui apparaît presque invisible pour le lecteur.
Pour plus d’informations, lisez Programmation asynchrone en C++.
Examinons maintenant la structure de base pour déclarer et créer une méthode de chargement de fichiers asynchrone, 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;
});
}
Dans ce code, lorsque votre code appelle la méthode ReadDataAsync définie ci-dessus, une tâche est créée pour lire une mémoire tampon à partir du système de fichiers. Une fois l’opération terminée, une tâche chaînée prend la mémoire tampon et diffuse les octets de cette mémoire tampon dans un tableau à l’aide du typeDataReader statique.
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.
});
Voici l’appel que vous effectuez à ReadDataAsync. Une fois l’opération terminée, votre code reçoit un tableau d’octets lus à partir du fichier fourni. Étant donné que ReadDataAsync lui-même est défini comme une tâche, vous pouvez utiliser une expression lambda pour effectuer une opération spécifique lorsque le tableau d’octets est retourné, par exemple en passant ces données d’octet à une fonction DirectX qui peut l’utiliser.
Si votre jeu est suffisamment simple, chargez vos ressources avec une méthode comme celle-ci lorsque l’utilisateur démarre le jeu. Vous pouvez le faire avant de démarrer la boucle de jeu principale à partir d’un point dans la séquence d’appel de votre implémentation IFrameworkView::Run. Là encore, vous appelez de façon asynchrone vos méthodes de chargement de ressources afin que le jeu puisse démarrer plus rapidement et que le joueur n’ait pas besoin d’attendre que le chargement se termine avant d’engager des interactions anticipées.
Toutefois, vous ne souhaitez pas démarrer le jeu correctement tant que tout le chargement asynchrone n’est pas terminé ! Créez une méthode de signalisation lorsque le chargement est terminé, tel qu’un champ spécifique, et utilisez les lambdas sur votre ou vos méthodes de chargement pour définir ce signal lorsque vous avez terminé. Vérifiez la variable avant de démarrer les composants qui utilisent ces ressources chargées.
Voici un exemple utilisant les méthodes asynchrones définies dans BasicLoader.cpp pour charger des nuanceurs, un maillage et une texture au démarrage du jeu. Notez qu’il définit un champ spécifique sur l’objet de jeu, m_loadingComplete, lorsque toutes les méthodes de chargement se terminent.
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.
}
Notez que les tâches ont été agrégées à l’aide de l’opérateur && afin que l’expression lambda qui définit l’indicateur complet de chargement soit déclenchée uniquement lorsque toutes les tâches se terminent. Notez que si vous avez plusieurs drapeaux, vous avez la possibilité de conditions de concurrence. Par exemple, si l’expression lambda définit deux indicateurs séquentiellement sur la même valeur, un autre thread peut uniquement voir le premier indicateur défini s’il les examine avant que le deuxième indicateur ne soit défini.
Vous avez vu comment charger des fichiers de ressources de manière asynchrone. Les chargements de fichiers synchrones sont beaucoup plus simples, et vous pouvez trouver des exemples d’entre eux dans Code complet pour BasicReaderWriter et Code complet pour BasicLoader.
Bien sûr, différents types de ressources nécessitent souvent un traitement ou une conversion supplémentaires avant qu’ils ne soient prêts à être utilisés dans votre pipeline graphique. Examinons trois types de ressources spécifiques : maillages, textures et nuanceurs.
Chargement de maillages
Les maillages sont des données de vertex, générées de manière procédurale par du code au sein de votre jeu ou exportées vers un fichier à partir d’une autre application (comme 3DStudio MAX ou Alias WaveFront) ou un outil. Ces maillages représentent les modèles de votre jeu, des primitives simples comme des cubes et des sphères aux voitures et aux maisons et personnages. Ils contiennent souvent des données de couleur et d’animation, en fonction de leur format. Nous allons nous concentrer sur les maillages qui contiennent uniquement des données de vertex.
Pour charger correctement un maillage, vous devez connaître le format des données dans le fichier pour le maillage. Notre simple type BasicReaderWriter ci-dessus lit simplement les données dans un flux d’octets ; il ne sait pas que les données d’octet représentent un maillage, beaucoup moins un format de maillage spécifique tel qu’exporté par une autre application ! Vous devez effectuer la conversion lorsque vous apportez les données de maillage dans la mémoire.
(Vous devez toujours essayer de empaqueter les données de ressources dans un format aussi proche que possible de la représentation interne. Cela permet de réduire l’utilisation des ressources et de gagner du temps.)
Obtenons les données d’octet à partir du fichier du maillage. Le format de l’exemple suppose que le fichier est un suffixe de format spécifique à l’exemple avec .vbo. (Là encore, ce format n’est pas identique au format VBO d’OpenGL.) Chaque vertex lui-même est mappé au type BasicVertex, qui est un struct défini dans le code de l’outil de convertisseur obj2vbo. La disposition des données de vertex dans le fichier .vbo ressemble à ceci :
- Les 32 premiers bits (4 octets) du flux de données contiennent le nombre de vertex (numVertices) dans le maillage, représentés sous forme de valeur uint32.
- Les 32 bits suivants (4 octets) du flux de données contiennent le nombre d’index dans le maillage (numIndices), représentés sous la forme d’une valeur uint32.
- Après cela, les bits suivants (numVertices * sizeof(BasicVertex)) contiennent les données de vertex.
- Les derniers bits (numIndices * 16) de données contiennent les données d’index, représentées sous la forme d’une séquence de valeurs uint16.
Le point est le suivant : connaître la disposition au niveau du bit des données de maillage que vous avez chargées. En outre, assurez-vous que vous êtes cohérent avec endian-ness. Toutes les plateformes Windows 8 sont en mode Little Endian.
Dans l’exemple, vous appelez une méthode, CreateMesh, à partir de la méthode LoadMeshAsync pour effectuer cette interprétation au niveau du bit.
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 interprète les données d’octet chargées à partir du fichier et crée une mémoire tampon vertex et une mémoire tampon d’index pour le maillage en passant respectivement les listes de vertex et d’index à ID3D11Device::CreateBuffer et en spécifiant D3D11_BIND_VERTEX_BUFFER ou D3D11_BIND_INDEX_BUFFER. Voici le code utilisé dans 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;
}
}
Vous créez généralement une paire de tampons de vertex/index pour chaque maillage que vous utilisez dans votre jeu. L’emplacement et le moment où vous chargez les maillages vous revient. Si vous disposez d’un grand nombre de maillages, vous ne souhaiterez peut-être charger certains éléments du disque qu’à des points spécifiques du jeu, par exemple pendant des états de chargement prédéfinis spécifiques. Pour les maillages volumineux, comme les données de terrain, vous pouvez diffuser en continu les vertex à partir d’un cache, mais cette procédure est plus complexe et ne fait pas partie de l’étendue de cette rubrique.
Là encore, connaissez votre format de données vertex ! Il existe de très nombreuses façons de représenter des données vertex entre les outils utilisés pour créer des modèles. Il existe également de nombreuses façons de représenter le layout d’entrée des données vertex vers Direct3D, telles que les listes de triangles et les bandes. Pour plus d’informations sur les données vertex, consultez Présentation des mémoires tampons dans Direct3D 11 et Primitives.
Examinons ensuite le chargement des textures.
Chargement des textures
Les ressources les plus courantes dans un jeu, et celles qui composent la plupart des fichiers sur le disque et en mémoire, sont les textures. Comme les maillages, les textures peuvent être fournies dans un large éventail de formats et vous les convertissez en un format que Direct3D peut utiliser lorsque vous les chargez. Les textures sont également présentes dans un large éventail de types et sont utilisées pour créer différents effets. Les niveaux MIP pour les textures peuvent être utilisés pour améliorer l’apparence et les performances des objets de distance ; les cartes de saleté et de lumière sont utilisées pour calquer des effets et des détails au-dessus d’une texture de base ; et les cartes normales sont utilisées dans les calculs d’éclairage par pixel. Dans un jeu moderne, une scène typique peut potentiellement avoir des milliers de textures individuelles, et votre code doit les gérer efficacement tous !
De plus, comme les maillages, il existe un certain nombre de formats spécifiques utilisés pour rendre l’utilisation de la mémoire efficace. Étant donné que les textures peuvent facilement consommer une grande partie de la mémoire GPU (et système), elles sont souvent compressées d’une certaine manière. Vous n’êtes pas obligé d’utiliser la compression sur les textures de votre jeu, et vous pouvez utiliser n’importe quel algorithme de compression/décompression souhaité tant que vous fournissez les nuanceurs Direct3D avec des données dans un format qu’il peut comprendre (comme une image bitmap Texture2D).
Direct3D prend en charge les algorithmes de compression de texture DXT, bien que chaque format DXT ne soit pas pris en charge dans le matériel graphique du lecteur. Les fichiers DDS contiennent également des textures DXT (et d’autres formats de compression de texture) et ont pour suffixes .dds.
Un fichier DSS est un fichier binaire qui contient les informations suivantes :
DWORD (nombre magique) contenant la valeur de code de quatre caractères « DDS » (0x20534444).
Une description des données dans le fichier.
Les données sont décrites avec une description d’en-tête à l’aide de DDS_HEADER; le format de pixel est défini à l’aide de DDS_PIXELFORMAT. Notez que les structures DDS_HEADER et DDS_PIXELFORMAT remplacent les structures DDSURFACEDESC2, DDSCAPS2 et DDPIXELFORMAT DirectDraw 7 qui sont déconseillées. DDS_HEADER est l’équivalent binaire de DDSURFACEDESC2 et de DDSCAPS2. DDS_PIXELFORMAT est l’équivalent binaire de DDPIXELFORMAT.
DWORD dwMagic; DDS_HEADER header;
Si la valeur de dwFlags dans DDS_PIXELFORMAT est définie sur DDPF_FOURCC et dwFourCC est définie sur « DX10 », une structure de DDS_HEADER_DXT10 supplémentaire sera présente pour prendre en charge les tableaux de textures ou les formats DXGI qui ne peuvent pas être exprimés sous forme de format de pixel RVB tel que des formats à virgule flottante, des formats sRVB, etc. Lorsque la structure DDS_HEADER_DXT10 est présente, la description complète des données ressemble à ceci.
DWORD dwMagic; DDS_HEADER header; DDS_HEADER_DXT10 header10;
Pointeur vers un tableau d’octets qui contient les données de surface principale.
BYTE bdata[]
Pointeur vers un tableau d’octets qui contient les surfaces restantes telles que : niveaux mipmap, visages dans une carte de cube, profondeurs dans une texture de volume. Pour plus d’informations sur le layout de fichier DDS pour une texture, une carte de cube ou une texture de volume, suivez ces liens.
BYTE bdata2[]
De nombreux outils exportent au format DDS. Si vous n’avez pas d’outil pour exporter votre texture dans ce format, envisagez d’en créer un. Pour plus d’informations sur le format DDS et sur son utilisation dans votre code, lisez le Guide de programmation pour DDS. Nous allons utiliser DSS dans notre exemple.
Comme avec d’autres types de ressources, vous lisez les données d’un fichier sous la forme d’un flux d’octets. Une fois votre tâche de chargement terminée, l’appel lambda exécute le code (la méthode CreateTexture) pour traiter le flux d’octets dans un format que Direct3D peut utiliser.
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
);
});
}
Dans l’extrait de code précédent, la fonction lambda vérifie si le nom de fichier a une extension « dds ». Si c’est le cas, vous supposez qu’il s’agit d’une texture DDS. Si ce n’est pas le cas, utilisez les API WIC (Composant Imagerie Windows) pour découvrir le format et décoder les données en tant qu’image bitmap. Dans les deux cas, le résultat est une bitmap Texture2D (ou une erreur).
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();
}
}
Une fois ce code terminé, vous disposez d’une texture2D en mémoire, chargée à partir d’un fichier image. Comme avec les maillages, vous avez probablement beaucoup d’entre eux dans votre jeu et dans n’importe quelle scène donnée. Envisagez de créer des caches pour les textures régulièrement sollicitées par scène ou par niveau, plutôt que de les charger toutes au démarrage du jeu ou du niveau.
(La méthode CreateDDSTextureFromMemory appelée dans l’exemple ci-dessus peut être explorée dans le code complet pour DDSTextureLoader.)
De plus, des textures individuelles ou des « skins » de texture peuvent être mappées à des polygones ou surfaces de maillage spécifiques. Ces données de mappage sont généralement exportées par l’outil avec lequel un artiste ou un concepteur crée le modèle et les textures. Assurez-vous de capturer également ces informations lorsque vous chargez les données exportées, car vous les utiliserez mapper les textures appropriées aux surfaces correspondantes lorsque vous effectuez l’ombrage de fragments.
Chargement de nuanceurs
Les nuanceurs sont des fichiers HLSL (High Level Shader Language) compilés qui sont chargés en mémoire et appelés à des stades spécifiques du pipeline graphique. Les nuanceurs les plus courants et essentiels sont les nuanceurs de vertex et de pixels, qui traitent les vertex individuels de votre maillage et les pixels dans les fenêtres d’affichage de la scène, respectivement. Le code HLSL est exécuté pour transformer la géométrie, appliquer des effets d’éclairage et des textures, et effectuer un post-traitement sur la scène rendue.
Un jeu Direct3D peut avoir un certain nombre de nuanceurs différents, chacun compilé dans un fichier CSO distinct (objet shader compilé, .cso). Normalement, vous n’avez pas tant de personnes que vous devez les charger dynamiquement, et dans la plupart des cas, vous pouvez simplement les charger lorsque le jeu démarre, ou sur une base par niveau (par exemple, un nuanceur pour les effets de pluie).
Le code de la classe BasicLoader fournit un certain nombre de surcharges pour différents nuanceurs, notamment les nuanceurs de vertex, de géométries, de pixels et d’enveloppes. Le code ci-dessous couvre les nuanceurs de pixels comme exemple. (Vous pouvez consulter le code complet dans Code complet pour 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);
});
}
Dans cet exemple, vous utilisez l’instance de BasicReaderWriter (m_basicReaderWriter) pour lire dans le fichier d’objet nuanceur compilé fourni (.cso) en tant que flux d’octets. Une fois cette tâche terminée, l’expression lambda appelle ID3D11Device::CreatePixelShader avec les données d’octet chargées à partir du fichier. Votre rappel doit définir un indicateur de la réussite de la charge, et votre code doit contrôler cet indicateur avant d’exécuter le nuanceur.
Les nuanceurs de Vertex sont plus complexes. Pour un nuanceur de vertex, vous chargez également une disposition d’entrée distincte qui définit les données vertex. Le code suivant peut être utilisé pour charger de manière asynchrone un nuanceur de vertex avec une disposition d’entrée de vertex personnalisée. Assurez-vous que les informations de vertex que vous chargez à partir de vos maillages peuvent être correctement représentées par ce layout d’entrée !
Nous allons créer le layout d’entrée avant de charger le nuanceur de vertex.
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);
}
}
Dans cette disposition particulière, chaque vertex a les données suivantes traitées par le nuanceur de vertex :
- Position de coordonnées 3D (x, y, z) dans l’espace de coordonnées du modèle, représentée sous la forme d’un trio de valeurs à virgule flottante 32 bits.
- Vecteur normal pour le vertex, également représenté sous forme de trois valeurs à virgule flottante 32 bits.
- Valeur de coordonnée de texture 2D transformée (u, v), représentée sous la forme d’une paire de valeurs flottantes 32 bits.
Ces éléments d’entrée par vertex sont appelés sémantiques HLSL et sont un ensemble de registres définis utilisés pour transmettre des données à et à partir de votre objet nuanceur compilé. Votre pipeline exécute le nuanceur de vertex une fois pour chaque vertex dans le maillage que vous avez chargé. La sémantique définit l’entrée vers (et la sortie à partir) du nuanceur de vertex lors de son exécution et fournissez ces données pour vos calculs par vertex dans le code HLSL de votre nuanceur.
À présent, chargez l’objet nuanceur de vertex.
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);
}
});
}
Dans ce code, une fois que vous avez lu les données d’octet pour le fichier CSO du nuanceur de vertex, vous créez le nuanceur de vertex en appelant ID3D11Device::CreateVertexShader. Après cela, vous créez votre layout d’entrée pour le nuanceur dans le même lambda.
D’autres types de nuanceurs, tels que les nuanceurs de coque et de géométrie, peuvent également nécessiter une configuration spécifique. Le code complet d’une variété de méthodes de chargement de nuanceur est fourni dans le code complet pour BasicLoader et dans l’exemple de chargement de ressources Direct3D.
Notes
À ce stade, vous devez comprendre et être en mesure de créer ou de modifier des méthodes pour charger de manière asynchrone des ressources de jeu courantes, telles que des maillages, des textures et des nuanceurs compilés.
Rubriques connexes
- Exemple de chargement de ressource Direct3D
- Code complet pour BasicLoader
- Code complet pour BasicReaderWriter
- Code complet pour DDSTextureLoader