Convertir l’infrastructure de rendu
Résumé
- Partie 1 : Initialiser Direct3D 11
- Partie 2 : Convertir l’infrastructure de rendu
- Partie 3 : Porter la boucle du jeu
Montre comment convertir une infrastructure de rendu simple de Direct3D 9 en Direct3D 11, notamment comment transférer des mémoires tampons géométriques, comment compiler et charger des programmes de nuanceur HLSL et comment implémenter la chaîne de rendu dans Direct3D 11. Partie 2 du port d’une application Direct3D 9 simple vers DirectX 11 et plateforme Windows universelle (UWP) pas à pas.
Convertir des effets en nuanceurs HLSL
L’exemple suivant est une technique D3DX simple, écrite pour l’API Effets héritées, pour la transformation de vertex matériel et les données de couleur directe.
Code nuanceur Direct3D 9
// Global variables
matrix g_mWorld; // world matrix for object
matrix g_View; // view matrix
matrix g_Projection; // projection matrix
// Shader pipeline structures
struct VS_OUTPUT
{
float4 Position : POSITION; // vertex position
float4 Color : COLOR0; // vertex diffuse color
};
struct PS_OUTPUT
{
float4 RGBColor : COLOR0; // Pixel color
};
// Vertex shader
VS_OUTPUT RenderSceneVS(float3 vPos : POSITION,
float3 vColor : COLOR0)
{
VS_OUTPUT Output;
float4 pos = float4(vPos, 1.0f);
// Transform the position from object space to homogeneous projection space
pos = mul(pos, g_mWorld);
pos = mul(pos, g_View);
pos = mul(pos, g_Projection);
Output.Position = pos;
// Just pass through the color data
Output.Color = float4(vColor, 1.0f);
return Output;
}
// Pixel shader
PS_OUTPUT RenderScenePS(VS_OUTPUT In)
{
PS_OUTPUT Output;
Output.RGBColor = In.Color;
return Output;
}
// Technique
technique RenderSceneSimple
{
pass P0
{
VertexShader = compile vs_2_0 RenderSceneVS();
PixelShader = compile ps_2_0 RenderScenePS();
}
}
Dans Direct3D 11, nous pouvons toujours utiliser nos nuanceurs HLSL. Nous mettons chaque nuanceur dans son propre fichier HLSL afin que Visual Studio les compile dans des fichiers distincts et ultérieur, nous les chargerons en tant que ressources Direct3D distinctes. Nous définissons le niveau cible sur le modèle de nuanceur 4 niveau 9_1 (/4_0_level_9_1), car ces nuanceurs sont écrits pour les GPU DirectX 9.1.
Lorsque nous avons défini la disposition d’entrée, nous avons assuré qu’elle représentait la même structure de données que celle que nous utilisons pour stocker les données par vertex dans la mémoire système et dans la mémoire GPU. De même, la sortie d’un nuanceur de vertex doit correspondre à la structure utilisée comme entrée au nuanceur de pixels. Les règles ne sont pas les mêmes que le passage de données d’une fonction à une autre en C++ ; vous pouvez omettre les variables inutilisées à la fin de la structure. Mais l’ordre ne peut pas être réorganisé et vous ne pouvez pas ignorer le contenu au milieu de la structure de données.
Notez que les règles de Direct3D 9 pour lier les nuanceurs de vertex aux nuanceurs de pixels étaient plus souples que les règles dans Direct3D 11. L’arrangement Direct3D 9 était flexible, mais inefficace.
Il est possible que vos fichiers HLSL utilisent une syntaxe plus ancienne pour la sémantique du nuanceur, par exemple COLOR au lieu de SV_TARGET. Si c’est le cas, vous devez activer le mode de compatibilité HLSL (/Option du compilateur Gec) ou mettre à jour la sémantique du nuanceur vers la syntaxe actuelle. Le nuanceur de vertex de cet exemple a été mis à jour avec la syntaxe actuelle.
Voici notre nuanceur de vertex de transformation matérielle, cette fois défini dans son propre fichier.
Notez que les nuanceurs de vertex sont nécessaires pour générer la sémantique SV_POSITION valeur système. Cette sémantique résout les données de position de vertex pour coordonner les valeurs où x est compris entre -1 et 1, y est compris entre -1 et 1, z est divisé par la valeur de coordonnées homogènes d’origine (z/w), et w est 1 divisé par la valeur w d’origine (1/w).
Nuanceur de vertex HLSL (niveau de fonctionnalité 9.1)
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix mWorld; // world matrix for object
matrix View; // view matrix
matrix Projection; // projection matrix
};
struct VS_INPUT
{
float3 vPos : POSITION;
float3 vColor : COLOR0;
};
struct VS_OUTPUT
{
float4 Position : SV_POSITION; // Vertex shaders must output SV_POSITION
float4 Color : COLOR0;
};
VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
VS_OUTPUT Output;
float4 pos = float4(input.vPos, 1.0f);
// Transform the position from object space to homogeneous projection space
pos = mul(pos, mWorld);
pos = mul(pos, View);
pos = mul(pos, Projection);
Output.Position = pos;
// Just pass through the color data
Output.Color = float4(input.vColor, 1.0f);
return Output;
}
C’est tout ce dont nous avons besoin pour notre nuanceur de pixels pass-through. Même si nous l’appelons un pass-through, il obtient en fait des données de couleur interpolées correctes pour chaque pixel. Notez que la sémantique de valeur système SV_TARGET est appliquée à la sortie de la valeur de couleur par notre nuanceur de pixels, comme requis par l’API.
Notez que le niveau de nuanceur 9_x nuanceurs de pixels ne peut pas lire à partir de la sémantique SV_POSITION valeur système. Les nuanceurs de pixels model 4.0 (et versions ultérieures) peuvent utiliser SV_POSITION pour récupérer l’emplacement du pixel à l’écran, où x est compris entre 0 et la largeur de la cible de rendu et y est compris entre 0 et la hauteur de la cible de rendu (chaque décalage de 0,5).
La plupart des nuanceurs de pixels sont beaucoup plus complexes qu’une traversée ; Notez que les niveaux de fonctionnalités Direct3D supérieurs permettent un nombre beaucoup plus élevé de calculs par programme de nuanceur.
Nuanceur de pixels HLSL (niveau de fonctionnalité 9.1)
struct PS_INPUT
{
float4 Position : SV_POSITION; // interpolated vertex position (system value)
float4 Color : COLOR0; // interpolated diffuse color
};
struct PS_OUTPUT
{
float4 RGBColor : SV_TARGET; // pixel color (your PS computes this system value)
};
PS_OUTPUT main(PS_INPUT In)
{
PS_OUTPUT Output;
Output.RGBColor = In.Color;
return Output;
}
Compiler et charger des nuanceurs
Les jeux Direct3D 9 utilisent souvent la bibliothèque Effets comme moyen pratique d’implémenter des pipelines programmables. Les effets peuvent être compilés au moment de l’exécution à l’aide de la méthode de fonction D3DXCreateEffectFromFile.
Chargement d’un effet dans Direct3D 9
// Turn off preshader optimization to keep calculations on the GPU
DWORD dwShaderFlags = D3DXSHADER_NO_PRESHADER;
// Only enable debug info when compiling for a debug target
#if defined (DEBUG) || defined (_DEBUG)
dwShaderFlags |= D3DXSHADER_DEBUG;
#endif
D3DXCreateEffectFromFile(
m_pd3dDevice,
L"CubeShaders.fx",
NULL,
NULL,
dwShaderFlags,
NULL,
&m_pEffect,
NULL
);
Direct3D 11 fonctionne avec les programmes de nuanceur en tant que ressources binaires. Les nuanceurs sont compilés lorsque le projet est généré, puis traités comme des ressources. Par conséquent, notre exemple chargera le bytecode du nuanceur dans la mémoire système, utilisez l’interface d’appareil Direct3D pour créer une ressource Direct3D pour chaque nuanceur et pointez vers les ressources du nuanceur Direct3D lorsque nous avons configuré chaque trame.
Chargement d’une ressource de nuanceur dans Direct3D 11
// BasicReaderWriter is a tested file loader used in SDK samples.
BasicReaderWriter^ readerWriter = ref new BasicReaderWriter();
// Load vertex shader:
Platform::Array<byte>^ vertexShaderData =
readerWriter->ReadData("CubeVertexShader.cso");
// This call allocates a device resource, validates the vertex shader
// with the device feature level, and stores the vertex shader bits in
// graphics memory.
m_d3dDevice->CreateVertexShader(
vertexShaderData->Data,
vertexShaderData->Length,
nullptr,
&m_vertexShader
);
Pour inclure le bytecode du nuanceur dans votre package d’application compilé, ajoutez simplement le fichier HLSL au projet Visual Studio. Visual Studio utilise l’outil Effect-Compiler (FXC) pour compiler des fichiers HLSL dans des objets de nuanceur compilés (. Fichiers CSO) et les inclure dans le package d’application.
Notez que veillez à définir le niveau de fonctionnalité cible approprié pour le compilateur HLSL : cliquez avec le bouton droit sur le fichier source HLSL dans Visual Studio, sélectionnez Propriétés et modifiez le paramètre de modèle de nuanceur sous compilateur HLSL -> Général. Direct3D vérifie cette propriété par rapport aux fonctionnalités matérielles lorsque votre application crée la ressource de nuanceur Direct3D.
Il s’agit d’un bon endroit pour créer la disposition d’entrée, qui correspond à la déclaration de flux de vertex dans Direct3D 9. La structure de données par vertex doit correspondre à ce que le nuanceur de vertex utilise ; dans Direct3D 11, nous avons plus de contrôle sur la disposition d’entrée ; nous pouvons définir la taille du tableau et la longueur de bits des vecteurs à virgule flottante et spécifier la sémantique du nuanceur de vertex. Nous créons une structure D3D11_INPUT_ELEMENT_DESC et l’utilisons pour informer Direct3D de l’apparence des données par vertex. Nous avons attendu jusqu’à ce que nous ayons chargé le nuanceur de vertex pour définir la disposition d’entrée, car l’API valide la disposition d’entrée par rapport à la ressource de nuanceur de vertex. Si la disposition d’entrée n’est pas compatible, Direct3D lève une exception.
Les données par vertex doivent être stockées dans des types compatibles dans la mémoire système. Les types de données DirectXMath peuvent vous aider ; par exemple, DXGI_FORMAT_R32G32B32_FLOAT correspond à XMFLOAT3.
Notez que les mémoires tampons constantes utilisent une disposition d’entrée fixe qui s’aligne sur quatre nombres à virgule flottante à la fois. XMFLOAT4 (et ses dérivés) sont recommandés pour les données de mémoire tampon constantes.
Définition de la disposition d’entrée dans Direct3D 11
// Create input layout:
const D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
Créer des ressources geometry
Dans Direct3D 9, nous avons stocké des ressources geometry en créant des mémoires tampons sur l’appareil Direct3D, en verrouillant la mémoire et en copiant des données de la mémoire processeur vers la mémoire GPU.
Direct3D 9
// Create vertex buffer:
VOID* pVertices;
// In Direct3D 9 we create the buffer, lock it, and copy the data from
// system memory to graphics memory.
m_pd3dDevice->CreateVertexBuffer(
sizeof(CubeVertices),
0,
D3DFVF_XYZ | D3DFVF_DIFFUSE,
D3DPOOL_MANAGED,
&pVertexBuffer,
NULL);
pVertexBuffer->Lock(
0,
sizeof(CubeVertices),
&pVertices,
0);
memcpy(pVertices, CubeVertices, sizeof(CubeVertices));
pVertexBuffer->Unlock();
DirectX 11 suit un processus plus simple. L’API copie automatiquement les données de la mémoire système vers le GPU. Nous pouvons utiliser des pointeurs intelligents COM pour faciliter la programmation.
DirectX 11
D3D11_SUBRESOURCE_DATA vertexBufferData = {0};
vertexBufferData.pSysMem = CubeVertices;
vertexBufferData.SysMemPitch = 0;
vertexBufferData.SysMemSlicePitch = 0;
CD3D11_BUFFER_DESC vertexBufferDesc(
sizeof(CubeVertices),
D3D11_BIND_VERTEX_BUFFER);
// This call allocates a device resource for the vertex buffer and copies
// in the data.
m_d3dDevice->CreateBuffer(
&vertexBufferDesc,
&vertexBufferData,
&m_vertexBuffer
);
Implémenter la chaîne de rendu
Les jeux Direct3D 9 utilisent souvent une chaîne de rendu basée sur des effets. Ce type de chaîne de rendu configure l’objet d’effet, lui fournit les ressources dont il a besoin et lui permet de restituer chaque passage.
Chaîne de rendu Direct3D 9
// Clear the render target and the z-buffer.
m_pd3dDevice->Clear(
0, NULL,
D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_ARGB(0, 45, 50, 170),
1.0f, 0
);
// Set the effect technique
m_pEffect->SetTechnique("RenderSceneSimple");
// Rotate the cube 1 degree per frame.
D3DXMATRIX world;
D3DXMatrixRotationY(&world, D3DXToRadian(m_frameCount++));
// Set the matrices up using traditional functions.
m_pEffect->SetMatrix("g_mWorld", &world);
m_pEffect->SetMatrix("g_View", &m_view);
m_pEffect->SetMatrix("g_Projection", &m_projection);
// Render the scene using the Effects library.
if(SUCCEEDED(m_pd3dDevice->BeginScene()))
{
// Begin rendering effect passes.
UINT passes = 0;
m_pEffect->Begin(&passes, 0);
for (UINT i = 0; i < passes; i++)
{
m_pEffect->BeginPass(i);
// Send vertex data to the pipeline.
m_pd3dDevice->SetFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE);
m_pd3dDevice->SetStreamSource(
0, pVertexBuffer,
0, sizeof(VertexPositionColor)
);
m_pd3dDevice->SetIndices(pIndexBuffer);
// Draw the cube.
m_pd3dDevice->DrawIndexedPrimitive(
D3DPT_TRIANGLELIST,
0, 0, 8, 0, 12
);
m_pEffect->EndPass();
}
m_pEffect->End();
// End drawing.
m_pd3dDevice->EndScene();
}
// Present frame:
// Show the frame on the primary surface.
m_pd3dDevice->Present(NULL, NULL, NULL, NULL);
La chaîne de rendu DirectX 11 effectue toujours les mêmes tâches, mais les passes de rendu doivent être implémentées différemment. Au lieu de placer les spécificités dans les fichiers FX et de laisser les techniques de rendu plus ou moins opaques dans notre code C++, nous allons configurer tout notre rendu en C++.
Voici comment notre chaîne de rendu ressemblera. Nous devons fournir la disposition d’entrée que nous avons créée après le chargement du nuanceur de vertex, fournir chacun des objets de nuanceur et spécifier les mémoires tampons constantes pour chaque nuanceur à utiliser. Cet exemple n’inclut pas plusieurs passes de rendu, mais si nous avons effectué une chaîne de rendu similaire pour chaque passe, en modifiant la configuration en fonction des besoins.
Chaîne de rendu Direct3D 11
// Clear the back buffer.
const float midnightBlue[] = { 0.098f, 0.098f, 0.439f, 1.000f };
m_d3dContext->ClearRenderTargetView(
m_renderTargetView.Get(),
midnightBlue
);
// Set the render target. This starts the drawing operation.
m_d3dContext->OMSetRenderTargets(
1, // number of render target views for this drawing operation.
m_renderTargetView.GetAddressOf(),
nullptr
);
// Rotate the cube 1 degree per frame.
XMStoreFloat4x4(
&m_constantBufferData.model,
XMMatrixTranspose(XMMatrixRotationY(m_frameCount++ * XM_PI / 180.f))
);
// Copy the updated constant buffer from system memory to video memory.
m_d3dContext->UpdateSubresource(
m_constantBuffer.Get(),
0, // update the 0th subresource
NULL, // use the whole destination
&m_constantBufferData,
0, // default pitch
0 // default pitch
);
// Send vertex data to the Input Assembler stage.
UINT stride = sizeof(VertexPositionColor);
UINT offset = 0;
m_d3dContext->IASetVertexBuffers(
0, // start with the first vertex buffer
1, // one vertex buffer
m_vertexBuffer.GetAddressOf(),
&stride,
&offset
);
m_d3dContext->IASetIndexBuffer(
m_indexBuffer.Get(),
DXGI_FORMAT_R16_UINT,
0 // no offset
);
m_d3dContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_d3dContext->IASetInputLayout(m_inputLayout.Get());
// Set the vertex shader.
m_d3dContext->VSSetShader(
m_vertexShader.Get(),
nullptr,
0
);
// Set the vertex shader constant buffer data.
m_d3dContext->VSSetConstantBuffers(
0, // register 0
1, // one constant buffer
m_constantBuffer.GetAddressOf()
);
// Set the pixel shader.
m_d3dContext->PSSetShader(
m_pixelShader.Get(),
nullptr,
0
);
// Draw the cube.
m_d3dContext->DrawIndexed(
m_indexCount,
0, // start with index 0
0 // start with vertex 0
);
La chaîne d’échange fait partie de l’infrastructure graphique. Nous utilisons donc notre chaîne d’échange DXGI pour présenter l’image terminée. DXGI bloque l’appel jusqu’au prochain vsync ; puis il retourne, et notre boucle de jeu peut passer à l’itération suivante.
Présentation d’un cadre à l’écran à l’aide de DirectX 11
m_swapChain->Present(1, 0);
La chaîne de rendu que nous venons de créer sera appelée à partir d’une boucle de jeu implémentée dans la méthode IFrameworkView ::Run . Ceci est illustré dans la partie 3 : Fenêtre d’affichage et boucle de jeu.