DirectX 11 - Tutoriel 5 : Application de textures

RasterTek

Traducteur

Jérôme Marsaguet

Ce tutoriel va vous expliquer comment appliquer une texture dans DirectX 11. Cela nous permet d'ajouter du réalisme à nos scènes en appliquant des photos et autres images sur les faces des polygones.

Commentez

- Titre : DirectX 11 - Tutoriel 5 : Application de textures

- Auteur : RasterTek

- Traducteur : Jérôme Marsaguet

- Parution : 2 juillet 2014

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 RasterTek. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- II. Introduction

- III. Texture.vs

- IV. Texture.ps

- V. Textureclass.h

- VI. Textureclass.cpp

- VII. Modelclass.h

- VIII. Modelclass.cpp

- IX. Textureshaderclass.h

- X. Textureshaderclass.cpp

- XI. Graphicsclass.h

- XII. Graphicsclass.cpp

- XIII. Résumé

- XIV. Exercices à faire

- XV. Code source

- XVI. Remerciements

- Navigation

Navigation

Tutoriel précédent : tampons, shaders et HLSL

Sommaire

Tutoriel suivant : lumière diffuse

II - Introduction

Dans ce tutoriel, nous prendrons pour exemple cette image :

Que nous appliquerons au polygone du tutoriel précédent afin d'obtenir ceci :

Nous utiliserons le format DDS pour nos textures. C'est le format Direct Draw Surface que DirectX utilise. L'outil utilisé pour créer des fichiers .dds est livré avec le SDK de DirectX. L'outil se nomme DirectX Texture Tool, il est situé dans DirectX Utilities. Vous pouvez créer des textures de toutes tailles et tous formats, puis couper et coller votre image vers un autre format de texture et l'enregistrer comme un fichier .dds. Il est très simple à utiliser.

Avant de rentrer dans le code, nous devrions voir comment l'application de texture fonctionne. Pour lier des pixels de l'image dds au polygone, nous utilisons ce qu'on appelle le « système de coordonnées Texel ». Ce système convertit la valeur entière du pixel vers une valeur flottante entre 0.0f et 1.0f. Par exemple, pour une texture de 256 pixels de largeur, le premier pixel est à 0.0f, le 256e pixel à 1.0f, et le 128e pixel (au centre) à 0.5f.

Dans le système de coordonnées Texel, la valeur selon la largeur est nommée « U » et la valeur selon la hauteur est nommée « V ». La largeur va de 0.0 (à gauche) à 1.0 (à droite). La hauteur va de 0.0 (en haut) à 1.0 (en bas). Par exemple, le coin supérieur gauche sera noté U 0.0, V 0.0 et le coin inférieur droit sera noté U 1.0, V 1.0. Le schéma ci-dessous illustre ce système :

Maintenant que nous avons les bases de l'application de textures sur les polygones, nous pouvons regarder notre cadre de travail actualisé pour ce tutoriel :

Les modifications apportées depuis le tutoriel précédent concernent la nouvelle classe TextureClass au sein de la classe ModelClass, et TextureShaderClass qui remplace ColorShaderClass. Nous commencerons par regarder le code des nouveaux shaders de texture HLSL.

III - Texture.vs

Les vertex shaders sont similaires aux précédents, exceptées les parties intégrant l'application de texture.

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : texture.vs
////////////////////////////////////////////////////////////////////////////////

////////////////////////
// VARIABLES GLOBALES //
////////////////////////
cbuffer MatrixBuffer
{
matrix worldMatrix;
matrix viewMatrix;
matrix projectionMatrix;
};

Dans notre type, nous utilisons des coordonnées de texture à la place de la couleur. Puisque les coordonnées de texture nécessitent deux flottants U et V, nous utilisons le type float2. Pour les vertex et pixel shaders, la sémantique utilisée pour les coordonnées de texture est TEXCOORD0. Vous pouvez mettre n'importe quel nombre à la place de ce zéro final afin d'indiquer avec quel ensemble de coordonnées vous travaillez, étant donné que vous êtes autorisé à travailler avec plusieurs coordonnées de texture.

//////////////////////////
// DÉFINITION DES TYPES //
//////////////////////////
struct VertexInputType
{
float4 position : POSITION;
float2 tex : TEXCOORD0;
};

struct PixelInputType
{
float4 position : SV_POSITION;
float2 tex : TEXCOORD0;
};

////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
PixelInputType TextureVertexShader(VertexInputType input)
{
PixelInputType output;

// Change la quatrième composante du vecteur pour avoir des calculs matriciels corrects.
input.position.w = 1.0f;

// Calcule la position du sommet selon les matrices monde, de vue et de projection.
output.position = mul(input.position, worldMatrix);
output.position = mul(output.position, viewMatrix);
output.position = mul(output.position, projectionMatrix);

La seule différence dans le vertex shader de texture par rapport au vertex shader de couleur du tutoriel précédent est que, au lieu de faire une copie de la couleur du sommet en entrée, nous prenons les coordonnées de la texture et les transmettons au pixel shader.

// Stocke les coordonnées de la texture pour le pixel shader.
output.tex = input.tex;

return output;
}

IV - Texture.ps

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : texture.ps
////////////////////////////////////////////////////////////////////////////////

Le pixel shader de texture a deux variables globales. La première Texture2D shaderTexture est la ressource de texture qui sera utilisée pour le rendu de la texture sur le modèle. La deuxième SamplerState SampleType, l'état de l'échantillonneur, nous permet de modifier la façon dont les pixels sont écrits dans la face du polygone lorsqu'elle est réduite. Par exemple, si le polygone est vraiment très loin et ne représente que 8 pixels sur l'écran, nous utilisons l'état de l'échantillonneur pour déterminer quels pixels ou quelle combinaison de pixels de la texture originale seront effectivement dessinés. La texture originale peut faire 256x256 pixels, donc décider quels pixels seront dessinés est vraiment important afin de veiller à ce que la texture ressemble toujours à quelque chose, même sur de très petites faces. Nous définirons aussi l'état de l'échantillonneur dans la classe TextureShaderClass et l'attacherons au pointeur de ressources afin que ce pixel shader puisse l'utiliser pour déterminer quels pixels seront dessinés.

////////////////////////
// VARIABLES GLOBALES //
////////////////////////
Texture2D shaderTexture;
SamplerState SampleType;

Le type PixelInputType du pixel shader de texture est également modifié pour mettre les coordonnées de texture à la place des valeurs de couleur.

//////////////////////////
// DÉFINITION DES TYPES //
//////////////////////////
struct PixelInputType
{
float4 position : SV_POSITION;
float2 tex : TEXCOORD0;
};

Le pixel shader a été modifié de sorte qu'il utilise dorénavant le code de la fonction d'échantillonnage HLSL. Cette fonction utilise l'état de l'échantillonneur défini ci-dessus et les coordonnées de texture du pixel afin de déterminer et retourner la valeur du pixel correspondant aux coordonnées UV sur la face du polygone.

////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
float4 TexturePixelShader(PixelInputType input) : SV_TARGET
{
float4 textureColor;

// Évalue la couleur du pixel à partir de la texture en utilisant l'échantillonneur aux coordonnées désignées.
textureColor = shaderTexture.Sample(SampleType, input.tex);

return textureColor;
}

V - Textureclass.h

La classe TextureClass encapsule le chargement, le déchargement, et l'accès à une ressource de texture unique. Un objet de cette classe doit être instancié chaque fois que nous avons besoin d'une texture.

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : textureclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _TEXTURECLASS_H_
#define _TEXTURECLASS_H_

////////////////
// INCLUSIONS //
////////////////
#include <d3d11.h>
#include <d3dx11tex.h>

////////////////////////////////////////////////////////////////////////////////
// Nom de la classe : TextureClass
////////////////////////////////////////////////////////////////////////////////
class TextureClass
{
public:
TextureClass();
TextureClass(const TextureClass&);
~TextureClass();

Les deux premières méthodes chargeront une texture à partir du nom d'un fichier donné et la déchargeront quand elle ne sera plus nécessaire.

bool Initialize(ID3D11Device*, WCHAR*);
void Shutdown();

La méthode GetTexture renvoie un pointeur vers la texture de sorte qu'elle puisse être utilisée pour le rendu par les shaders.

ID3D11ShaderResourceView* GetTexture();

private:
// C'est la ressource de texture privée.
ID3D11ShaderResourceView* m_texture;
};

#endif

VI - Textureclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : textureclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "textureclass.h"

Le constructeur de la classe initialise à zéro le pointeur de la texture du shader.

TextureClass::TextureClass()
{
m_texture = 0;
}

TextureClass::TextureClass(const TextureClass& other)
{
}

TextureClass::~TextureClass()
{
}

La méthode Initialize prend en paramètre le dispositif Direct3D et le nom du fichier de la texture. Elle charge le fichier dans la variable ressource du shader nommée m_texture. La texture peut maintenant être utilisée pour effectuer le rendu.

bool TextureClass::Initialize(ID3D11Device* device, WCHAR* filename)
{
HRESULT result;

// Charge la texture.
result = D3DX11CreateShaderResourceViewFromFile(device, filename, NULL, NULL, &m_texture, NULL);
if(FAILED(result))
{
return false;
}

return true;
}

La méthode Shutdown libère la ressource de la texture si elle a été chargée et met le pointeur à la valeur nulle.

void TextureClass::Shutdown()
{
// Libère la ressource de la texture.
if(m_texture)
{
m_texture->Release();
m_texture = 0;
}

return;
}

La méthode GetTexture est appelée par d'autres objets qui ont besoin d'accéder à la ressource de la texture du shader afin qu'ils puissent l'utiliser pour le rendu.

ID3D11ShaderResourceView* TextureClass::GetTexture()
{
return m_texture;
}

VII - Modelclass.h

La classe ModelClass a changé de façon à permettre l'application de texture.

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : modelclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _MODELCLASS_H_
#define _MODELCLASS_H_

//////////////
// INCLUDES //
//////////////
#include <d3d11.h>
#include <d3dx10math.h>

L'en-tête de la classe TextureClass est maintenant inclus.

/////////////////////////////
// INCLUSIONS DE MA CLASSE //
/////////////////////////////
#include "textureclass.h" // NOUVEAU

////////////////////////////////////////////////////////////////////////////////
// Nom de la classe : ModelClass
////////////////////////////////////////////////////////////////////////////////
class ModelClass
{
private:

Le type VertexType remplace la composante couleur (verte, utilisée dans le tutoriel précédent) par les coordonnées de texture.

struct VertexType // MODIFIÉ
{
D3DXVECTOR3 position;
D3DXVECTOR2 texture;
};

public:
ModelClass();
ModelClass(const ModelClass&);
~ModelClass();

bool Initialize(ID3D11Device*, WCHAR*);
void Shutdown();
void Render(ID3D11DeviceContext*);

int GetIndexCount();

La classe ModelClass a également une méthode GetTexture afin qu'elle puisse passer sa propre ressource de texture de shaders qui dessinera le modèle.

ID3D11ShaderResourceView* GetTexture();

private:
bool InitializeBuffers(ID3D11Device*);
void ShutdownBuffers();
void RenderBuffers(ID3D11DeviceContext*);

La classe ModelClass possède deux méthodes privées LoadTexture et ReleaseTexture pour le chargement et la libération de la texture qui sera utilisée pour le rendu de ce modèle.

bool LoadTexture(ID3D11Device*, WCHAR*); // NOUVEAU
void ReleaseTexture(); // NOUVEAU

private:
ID3D11Buffer *m_vertexBuffer, *m_indexBuffer;
int m_vertexCount, m_indexCount;

La variable m_Texture est utilisée pour le chargement, la libération, et l'accès à la texture du modèle.

TextureClass* m_Texture; // NOUVEAU
};

#endif

VIII - Modelclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : modelclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "modelclass.h"

ModelClass::ModelClass()
{
m_vertexBuffer = 0;
m_indexBuffer = 0;

Le constructeur de la classe initialise maintenant à la valeur nulle le nouvel objet texture.

m_Texture = 0; // NOUVEAU
}

ModelClass::ModelClass(const ModelClass& other)
{
}

ModelClass::~ModelClass()
{
}

La méthode Initialize prend maintenant en entrée le nom du fichier .dds de la texture que le modèle utilisera

bool ModelClass::Initialize(ID3D11Device* device, WCHAR* textureFilename) //  MODIFIÉ
{
bool result;

// Initialise les tampons de sommets et d'indices qui contiennent la géométrie du triangle.
result = InitializeBuffers(device);
if(!result)
{
return false;
}

… et appelle maintenant une nouvelle fonction privée qui va charger la texture.

// NOUVEAU - charge la texture pour ce modèle.
result = LoadTexture(device, textureFilename);
if(!result)
{
return false;
}

return true;
}

void ModelClass::Shutdown()
{

La fonction Shutdown appelle maintenant la nouvelle fonction privée pour libérer l'objet texture chargé lors de l'initialisation.

// // NOUVEAU - libère la texture du modèle.
ReleaseTexture();

// Libère les tampons de sommets et d'indices.
ShutdownBuffers();

return;
}

void ModelClass::Render(ID3D11DeviceContext* deviceContext)
{
// Place les tampons de sommets et d'indices sur le pipeline graphique pour les préparer à être dessinés.
RenderBuffers(deviceContext);

return;
}

int ModelClass::GetIndexCount()
{
return m_indexCount;
}

La méthode GetTexture retourne la texture du modèle. Le shader de texture aura besoin d'y accéder pour effectuer le rendu du modèle.

ID3D11ShaderResourceView* ModelClass::GetTexture() // NOUVEAU
{
return m_Texture->GetTexture();
}

bool ModelClass::InitializeBuffers(ID3D11Device* device)
{
VertexType* vertices;
unsigned long* indices;
D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc;
D3D11_SUBRESOURCE_DATA vertexData, indexData;
HRESULT result;

// Fixe la taille du tableau de sommets.
m_vertexCount = 3;

// Fixe la taille du tableau d'indices.
m_indexCount = 3;

// Crée le tableau de sommets.
vertices = new VertexType[m_vertexCount];
if(!vertices)
{
return false;
}

// Crée le tableau d'indices.
indices = new unsigned long[m_indexCount];
if(!indices)
{
return false;
}

Le tableau des sommets a maintenant une composante de texture à la place de la composante de couleur. Le vecteur texture a toujours U en premier et V en second. Par exemple, la première coordonnée de texture est celle en bas à gauche du triangle correspondant à U 0.0, V 1.0. Référez-vous à pour savoir quelles coordonnées utiliser. Notez que vous pouvez modifier ces coordonnées pour lier n'importe quelle partie de la texture à n'importe quelle partie de la face du polygone. Dans ce tutoriel, je fais juste une correspondance directe pour des raisons de simplicité.

// Charge les données dans le tableau de sommets.
vertices[0].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f);  // En bas à gauche.
vertices[0].texture = D3DXVECTOR2(0.0f, 1.0f); //  MODIFIÉ

vertices[1].position = D3DXVECTOR3(0.0f, 1.0f, 0.0f);  // En haut au milieu.
vertices[1].texture = D3DXVECTOR2(0.5f, 0.0f); //  MODIFIÉ

vertices[2].position = D3DXVECTOR3(1.0f, -1.0f, 0.0f);  // En bas à droite.
vertices[2].texture = D3DXVECTOR2(1.0f, 1.0f); //  MODIFIÉ

// Charge les données dans le tableau d'indices.
indices[0] = 0;  // En bas à gauche.
indices[1] = 1;  // En haut au milieu.
indices[2] = 2;  // En bas à droite.

// Met en place la description du tampon de sommets.
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
vertexBufferDesc.ByteWidth = sizeof(VertexType) * m_vertexCount;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexBufferDesc.CPUAccessFlags = 0;
vertexBufferDesc.MiscFlags = 0;
vertexBufferDesc.StructureByteStride = 0;

// Donne à la structure de sous-ressources un pointeur vers le tableau des sommets.
vertexData.pSysMem = vertices;
vertexData.SysMemPitch = 0;
vertexData.SysMemSlicePitch = 0;

// Crée le tampon des sommets.
result = device->CreateBuffer(&vertexBufferDesc, &vertexData, &m_vertexBuffer);
if(FAILED(result))
{
return false;
}

// Met en place la description du tampon statique des indices.
indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
indexBufferDesc.ByteWidth = sizeof(unsigned long) * m_indexCount;
indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
indexBufferDesc.CPUAccessFlags = 0;
indexBufferDesc.MiscFlags = 0;
indexBufferDesc.StructureByteStride = 0;

// Donne à la structure de sous-ressources un pointeur vers le tableau des indices.
indexData.pSysMem = indices;

// Crée le tampon des indices.
result = device->CreateBuffer(&indexBufferDesc, &indexData, &m_indexBuffer);
if(FAILED(result))
{
return false;
}

// Libère les tableaux, maintenant que les tampons ont été créés et remplis.
delete [] vertices;
vertices = 0;

delete [] indices;
indices = 0;

return true;
}

void ModelClass::ShutdownBuffers()
{
// Libère le tampon d'indices.
if(m_indexBuffer)
{
m_indexBuffer->Release();
m_indexBuffer = 0;
}

// Libère le tampon de sommets.
if(m_vertexBuffer)
{
m_vertexBuffer->Release();
m_vertexBuffer = 0;
}

return;
}

void ModelClass::RenderBuffers(ID3D11DeviceContext* deviceContext)
{
unsigned int stride;
unsigned int offset;

// Règle le pas et l'offset du tampon.
stride = sizeof(VertexType);
offset = 0;

// Active le tampon des sommets dans l'assembleur d'entrées pour effectuer son rendu.
deviceContext->IASetVertexBuffers(0, 1, &m_vertexBuffer, &stride, &offset);

// Active le tampon des indices dans l'assembleur d'entrées pour effectuer son rendu.
deviceContext->IASetIndexBuffer(m_indexBuffer, DXGI_FORMAT_R32_UINT, 0);

// Définit le type de primitive qui doit être rendue depuis ce tampon de sommets, ici un triangle.
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

return;
}

LoadTexture est une nouvelle méthode privée qui va créer l'objet texture et l'initialiser avec le nom du fichier d'entrée fourni. Cette fonction est appelée lors de l'initialisation.

bool ModelClass::LoadTexture(ID3D11Device* device, WCHAR* filename) // NOUVEAU
{
bool result;

// Crée l'objet texture.
m_Texture = new TextureClass;
if(!m_Texture)
{
return false;
}

// Initialise l'objet texture.
result = m_Texture->Initialize(device, filename);
if(!result)
{
return false;
}

return true;
}

La méthode ReleaseTexture libérera l'objet texture créé et chargé dans la méthode LoadTexture.

void ModelClass::ReleaseTexture() // NOUVEAU
{
// Libère l'objet texture.
if(m_Texture)
{
m_Texture->Shutdown();
delete m_Texture;
m_Texture = 0;
}

return;
}

IX - Textureshaderclass.h

La classe TextureShaderClass est juste une version mise à jour de ColorShaderClass du tutoriel précédent. Cette classe sera utilisée pour dessiner les modèles 3D en utilisant les vertex et pixel shaders.

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : textureshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _TEXTURESHADERCLASS_H_
#define _TEXTURESHADERCLASS_H_

////////////////
// INCLUSIONS //
////////////////
#include <d3d11.h>
#include <d3dx10math.h>
#include <d3dx11async.h>
#include <fstream>
using namespace std;

////////////////////////////////////////////////////////////////////////////////
// Nom de la classe : TextureShaderClass
////////////////////////////////////////////////////////////////////////////////
class TextureShaderClass
{
private:
struct MatrixBufferType
{
D3DXMATRIX world;
D3DXMATRIX view;
D3DXMATRIX projection;
};

public:
TextureShaderClass();
TextureShaderClass(const TextureShaderClass&);
~TextureShaderClass();

bool Initialize(ID3D11Device*, HWND);
void Shutdown();
bool Render(ID3D11DeviceContext*, int, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, ID3D11ShaderResourceView*);

private:
bool InitializeShader(ID3D11Device*, HWND, WCHAR*, WCHAR*);
void ShutdownShader();
void OutputShaderErrorMessage(ID3D10Blob*, HWND, WCHAR*);

bool SetShaderParameters(ID3D11DeviceContext*, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, ID3D11ShaderResourceView*);
void RenderShader(ID3D11DeviceContext*, int);

private:
ID3D11VertexShader* m_vertexShader;
ID3D11PixelShader* m_pixelShader;
ID3D11InputLayout* m_layout;
ID3D11Buffer* m_matrixBuffer;

Une nouvelle variable privée est introduite : le pointeur vers l'état de l'échantillonneur. Ce pointeur sera utilisé pour l'interface avec le shader de texture.

ID3D11SamplerState* m_sampleState;
};

#endif

X - Textureshaderclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : textureshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "textureshaderclass.h"

TextureShaderClass::TextureShaderClass()
{
m_vertexShader = 0;
m_pixelShader = 0;
m_layout = 0;
m_matrixBuffer = 0;

Le nouveau pointeur sur l'état de l'échantillonneur est mis à la valeur nulle dans le constructeur de la classe.

m_sampleState = 0;
}

TextureShaderClass::TextureShaderClass(const TextureShaderClass& other)
{
}

TextureShaderClass::~TextureShaderClass()
{
}

bool TextureShaderClass::Initialize(ID3D11Device* device, HWND hwnd)
{
bool result;

Les nouveaux fichiers HLSL texture.vs et texture.ps sont chargés pour ce shader.

// Initialise les vertex et pixel shaders.
result = InitializeShader(device, hwnd, L"../Engine/texture.vs", L"../Engine/texture.ps");
if(!result)
{
return false;
}

return true;
}

La méthode Shutdown appelle la fonction de libération des variables shaders.

void TextureShaderClass::Shutdown()
{
// Ferme les vertex et pixel shaders ainsi que les objets associés.
ShutdownShader();

return;
}

La méthode Render prend désormais un nouveau paramètre nommé texture représentant le pointeur vers la ressource de texture. Celui-ci est envoyé dans la méthode SetShaderParameters afin qu'elle soit définie dans le shader et utilisée pour le rendu.

bool TextureShaderClass::Render(ID3D11DeviceContext* deviceContext, int indexCount, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix,
D3DXMATRIX projectionMatrix, ID3D11ShaderResourceView* texture)
{
bool result;

// Définit les paramètres du shader qui seront utilisés pour le rendu.
result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix, texture);
if(!result)
{
return false;
}

// Effectue le rendu des tampons avec le shader.
RenderShader(deviceContext, indexCount);

return true;
}

La méthode InitializeShader met en place le shader de texture.

bool TextureShaderClass::InitializeShader(ID3D11Device* device, HWND hwnd, WCHAR* vsFilename, WCHAR* psFilename)
{
HRESULT result;
ID3D10Blob* errorMessage;
ID3D10Blob* vertexShaderBuffer;
ID3D10Blob* pixelShaderBuffer;
D3D11_INPUT_ELEMENT_DESC polygonLayout[2];
unsigned int numElements;
D3D11_BUFFER_DESC matrixBufferDesc;

Nous avons une nouvelle variable qui contient la description de l'échantillonneur de la texture et qui sera configurée dans cette méthode.

D3D11_SAMPLER_DESC samplerDesc;

// Initialise à zéro les pointeurs que la méthode va utiliser.
errorMessage = 0;
vertexShaderBuffer = 0;
pixelShaderBuffer = 0;

Charge les nouveaux vertex et pixel shaders de texture.

// Compile le code du vertex shader.
result = D3DX11CompileFromFile(vsFilename, NULL, NULL, "TextureVertexShader", "vs_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL,
&vertexShaderBuffer, &errorMessage, NULL);
if(FAILED(result))
{
// Si la compilation a échoué, le shader doit avoir écrit un message d'erreur.
if(errorMessage)
{
OutputShaderErrorMessage(errorMessage, hwnd, vsFilename);
}
// Si le message d'erreur a été laissé vide, c'est qu'il n'a pas pu trouver le fichier.
else
{
MessageBox(hwnd, vsFilename, L"Missing Shader File", MB_OK);
}

return false;
}

// Compile le code du pixel shader.
result = D3DX11CompileFromFile(psFilename, NULL, NULL, "TexturePixelShader", "ps_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL,
&pixelShaderBuffer, &errorMessage, NULL);
if(FAILED(result))
{
// Si la compilation a échoué, le shader doit avoir écrit un message d'erreur.
if(errorMessage)
{
OutputShaderErrorMessage(errorMessage, hwnd, psFilename);
}
// Si le message d'erreur a été laissé vide, c'est qu'il n'a pas pu trouver le fichier.
else
{
MessageBox(hwnd, psFilename, L"Missing Shader File", MB_OK);
}

return false;
}

// Crée le vertex shader à partir du tampon.
result = device->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), NULL, &m_vertexShader);
if(FAILED(result))
{
return false;
}

// Crée le pixel shader à partir du tampon.
result = device->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), pixelShaderBuffer->GetBufferSize(), NULL, &m_pixelShader);
if(FAILED(result))
{
return false;
}

La disposition d'entrée a changé, nous avons maintenant un élément de texture à la place de la couleur. Le premier élément de la position reste inchangé, mais les champs SemanticName et Format du second élément ont été modifiés en TexCoord et DXGI_FORMAT_R32G32_FLOAT. Ces deux modifications sont alors en accord avec la disposition de notre nouveau VertexType à la fois dans la définition de la classe ModelClass et dans les alias des fichiers de shader.

// Crée la description de la disposition d'entrée du vertex.
// Cette disposition doit être la même que celle de la structure VextexType du shader.
polygonLayout[0].SemanticName = "POSITION";
polygonLayout[0].SemanticIndex = 0;
polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;
polygonLayout[0].InputSlot = 0;
polygonLayout[0].AlignedByteOffset = 0;
polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
polygonLayout[0].InstanceDataStepRate = 0;

polygonLayout[1].SemanticName = "TEXCOORD";
polygonLayout[1].SemanticIndex = 0;
polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT;
polygonLayout[1].InputSlot = 0;
polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
polygonLayout[1].InstanceDataStepRate = 0;

// Obtient le nombre d'éléments de la disposition des données.
numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);

// Crée la disposition d'entrée du vertex.
result = device->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(),
&m_layout);
if(FAILED(result))
{
return false;
}

// Libère les vertex et pixel shaders, car nous n'en avons plus besoin.
vertexShaderBuffer->Release();
vertexShaderBuffer = 0;

pixelShaderBuffer->Release();
pixelShaderBuffer = 0;

// Met en place la description du tampon constant de matrice dynamique situé dans le vertex shader.
matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
matrixBufferDesc.ByteWidth = sizeof(MatrixBufferType);
matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
matrixBufferDesc.MiscFlags = 0;
matrixBufferDesc.StructureByteStride = 0;

// Crée le pointeur vers le tampon constant afin de pouvoir y accéder depuis cette classe.
result = device->CreateBuffer(&matrixBufferDesc, NULL, &m_matrixBuffer);
if(FAILED(result))
{
return false;
}

La description de l'état de l'échantillonneur de la texture est configurée ici et peut ensuite être transmise au pixel shader. L'élément le plus important est le champ Filtre qui détermine la méthode utilisée pour garder et combiner les pixels servant à créer l'apparence finale de la texture sur la face du polygone. Dans cet exemple, j'utilise D3D11_FILTER_MIN_MAG_MIP_LINEAR qui est plus coûteux en termes de traitement, mais donne le meilleur résultat visuel. Il demande à l'échantillonneur d'effectuer une interpolation linéaire pour le rétrécissement, le grossissement et l'échantillonnage de MIP maps.

Les champs AddressU et AddressV sont mis à Wrap, ce qui garantit que les coordonnées restent entre 0.0f et 1.0f. Celles qui se trouvent en dehors sont ramenées dans cet intervalle. Tous les autres paramètres sont définis par défaut.

// Crée une description de l'état de l'échantillonneur de la texture.
samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.MipLODBias = 0.0f;
samplerDesc.MaxAnisotropy = 1;
samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
samplerDesc.BorderColor[0] = 0;
samplerDesc.BorderColor[1] = 0;
samplerDesc.BorderColor[2] = 0;
samplerDesc.BorderColor[3] = 0;
samplerDesc.MinLOD = 0;
samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

// Crée l'état de l'échantillonneur de la texture.
result = device->CreateSamplerState(&samplerDesc, &m_sampleState);
if(FAILED(result))
{
return false;
}

return true;
}

La méthode ShutdownShader libère toutes les variables utilisées dans la classe.

void TextureShaderClass::ShutdownShader()
{

Elle libère maintenant le nouvel état de l'échantillonneur créé lors de l'initialisation.

// Libère l'état de l'échantillonneur.
if(m_sampleState)
{
m_sampleState->Release();
m_sampleState = 0;
}

// Libère le tampon de matrice constant.
if(m_matrixBuffer)
{
m_matrixBuffer->Release();
m_matrixBuffer = 0;
}

// Libère la disposition.
if(m_layout)
{
m_layout->Release();
m_layout = 0;
}

// Libère le pixel shader.
if(m_pixelShader)
{
m_pixelShader->Release();
m_pixelShader = 0;
}

// Libère le vertex shader.
if(m_vertexShader)
{
m_vertexShader->Release();
m_vertexShader = 0;
}

return;
}

La méthode OutputShaderErrorMessage écrit les erreurs dans un fichier texte si le shader HLSL n'a pas pu être chargé.

void TextureShaderClass::OutputShaderErrorMessage(ID3D10Blob* errorMessage, HWND hwnd, WCHAR* shaderFilename)
{
char* compileErrors;
unsigned long bufferSize, i;
ofstream fout;

// Récupère le pointeur vers le message d'erreur du tampon.
compileErrors = (char*)(errorMessage->GetBufferPointer());

// Obtient la longueur du message.
bufferSize = errorMessage->GetBufferSize();

// Ouvre un fichier pour y écrire le message d'erreur.
fout.open("shader-error.txt");

// Écrit le message d'erreur.
for(i=0; i<bufferSize; i++)
{
fout << compileErrors[i];
}

// Ferme le fichier.
fout.close();

// Libère le message d'erreur.
errorMessage->Release();
errorMessage = 0;

// Affiche un message à l'écran pour avertir l'utilisateur qu'il doit vérifier le contenu du fichier pour y trouver les erreurs de compilation.
MessageBox(hwnd, L"Error compiling shader.  Check shader-error.txt for message.", shaderFilename, MB_OK);

return;
}

La méthode SetShaderParameters prend maintenant un pointeur vers une ressource de texture pour le transmettre au shader. Notez que la texture doit être fixée avant le rendu du tampon.

bool TextureShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix,
D3DXMATRIX projectionMatrix, ID3D11ShaderResourceView* texture)
{
HRESULT result;
D3D11_MAPPED_SUBRESOURCE mappedResource;
MatrixBufferType* dataPtr;
unsigned int bufferNumber;

// Transpose les matrices afin de les préparer pour le shader.
D3DXMatrixTranspose(&worldMatrix, &worldMatrix);
D3DXMatrixTranspose(&viewMatrix, &viewMatrix);
D3DXMatrixTranspose(&projectionMatrix, &projectionMatrix);

// Pose un verrou sur le tampon constant afin que l'on puisse écrire dedans.
result = deviceContext->Map(m_matrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
if(FAILED(result))
{
return false;
}

// Récupère le pointeur des données dans le tampon constant.
dataPtr = (MatrixBufferType*)mappedResource.pData;

// Copie la matrice dans le tampon constant.
dataPtr->world = worldMatrix;
dataPtr->view = viewMatrix;
dataPtr->projection = projectionMatrix;

// Déverrouille le tampon constant.
deviceContext->Unmap(m_matrixBuffer, 0);

// Définit la position du tampon constant dans le vertex shader.
bufferNumber = 0;

// Fixe le tampon constant contenant les données à jour dans le vertex shader.
deviceContext->VSSetConstantBuffers(bufferNumber, 1, &m_matrixBuffer);

La méthode SetShaderParameters a été modifiée pour intégrer la texture dans le pixel shader.

// Définit la texture dans le pixel shader.
deviceContext->PSSetShaderResources(0, 1, &texture);

return true;
}

La méthode RenderShader appelle le shader pour effectuer le rendu des polygones.

void TextureShaderClass::RenderShader(ID3D11DeviceContext* deviceContext, int indexCount)
{
// Définit la disposition des données en entrée du vertex.
deviceContext->IASetInputLayout(m_layout);

// Définit les vertex et pixel shaders qui seront utilisés pour le rendu du triangle.
deviceContext->VSSetShader(m_vertexShader, NULL, 0);
deviceContext->PSSetShader(m_pixelShader, NULL, 0);

La méthode RenderShader intègre maintenant l'état de l'échantillonneur dans le pixel shader avant le rendu.

// Définit l'état de l'échantillonneur dans le pixel shader.
deviceContext->PSSetSamplers(0, 1, &m_sampleState);

// Effectue le rendu du triangle.
deviceContext->DrawIndexed(indexCount, 0, 0);

return;
}

XI - Graphicsclass.h

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_

/////////////////////////////
// INCLUSIONS DE LA CLASSE //
/////////////////////////////
#include "d3dclass.h"
#include "cameraclass.h"
#include "modelclass.h"

La classe GraphicsClass comprend maintenant le fichier d'en-tête de la classe TextureShaderClass tandis que celui de la classe ColorShaderClass a été supprimé.

#include "textureshaderclass.h" // NOUVEAU

////////////////////////
// VARIABLES GLOBALES //
////////////////////////
const bool FULL_SCREEN = true;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;

////////////////////////////////////////////////////////////////////////////////
// Nom de la classe : GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
GraphicsClass();
GraphicsClass(const GraphicsClass&);
~GraphicsClass();

bool Initialize(int, int, HWND);
void Shutdown();
bool Frame();

private:
bool Render();

private:
D3DClass* m_D3D;
CameraClass* m_Camera;
ModelClass* m_Model;

Nous avons ajouté un nouvel objet TextureShaderClassprivé.

A new TextureShaderClass private object has been added.

TextureShaderClass* m_TextureShader; // NOUVEAU
};

#endif

XII - Graphicsclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : graphicsclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "graphicsclass.h"

La variable m_TextureShader est définie à la valeur nulle dans le constructeur.

GraphicsClass::GraphicsClass()
{
m_D3D = 0;
m_Camera = 0;
m_Model = 0;
m_TextureShader = 0; // NOUVEAU
}

GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}

GraphicsClass::~GraphicsClass()
{
}

bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
bool result;

// Crée l'objet Direct3D.
m_D3D = new D3DClass;
if(!m_D3D)
{
return false;
}

// Initialise l'objet Direct3D.
result = m_D3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
if(!result)
{
MessageBox(hwnd, L"Could not initialize Direct3D.", L"Error", MB_OK);
return false;
}

// Crée l'objet Camera.
m_Camera = new CameraClass;
if(!m_Camera)
{
return false;
}

// Crée l'objet Model.
m_Model = new ModelClass;
if(!m_Model)
{
return false;
}

La méthode ModelClass::initialize prend désormais le nom de la texture utilisée pour le rendu du modèle.

// Initialise l'objet Model.
result = m_Model->Initialize(m_D3D->GetDevice(), L"../Engine/data/seafloor.dds"); // MODIFIÉ
if(!result)
{
MessageBox(hwnd, L"Could not initialize the model object.", L"Error", MB_OK);
return false;
}

Le nouvel objet TextureShaderClass est créé et initialisé.

// NOUVEAU - crée l'objet shader de texture.
m_TextureShader = new TextureShaderClass;
if(!m_TextureShader)
{
return false;
}

// NOUVEAU - initialise l'objet shader de texture.
result = m_TextureShader->Initialize(m_D3D->GetDevice(), hwnd);
if(!result)
{
MessageBox(hwnd, L"Could not initialize the texture shader object.", L"Error", MB_OK);
return false;
}

return true;
}

void GraphicsClass::Shutdown()
{

L'objet TextureShaderClass est également libéré dans la méthode Shutdown.

// NOUVEAU - libère l'objet shader de texture.
if(m_TextureShader)
{
m_TextureShader->Shutdown();
delete m_TextureShader;
m_TextureShader = 0;
}

// Libère l'objet Model.
if(m_Model)
{
m_Model->Shutdown();
delete m_Model;
m_Model = 0;
}

// Libère l'objet Caméra.
if(m_Camera)
{
delete m_Camera;
m_Camera = 0;
}

// Libère l'objet Direct3D.
if(m_D3D)
{
m_D3D->Shutdown();
delete m_D3D;
m_D3D = 0;
}

return;
}

bool GraphicsClass::Frame()
{
bool result;

// Effectue le rendu de la scène.
result = Render();
if(!result)
{
return false;
}

return true;
}

bool GraphicsClass::Render()
{
D3DXMATRIX viewMatrix, projectionMatrix, worldMatrix;
bool result;

// Efface les tampons pour débuter la scène.
m_D3D->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

// Génère la matrice de vue à partir de la position de la caméra.
m_Camera->Render();

// Récupère les matrices monde, de vue et de projection depuis les objets Caméra et D3D.
m_Camera->GetViewMatrix(viewMatrix);
m_D3D->GetProjectionMatrix(projectionMatrix);
m_D3D->GetWorldMatrix(worldMatrix);

// Place les tampons des sommets et d'indices du modèle dans le pipeline graphique pour les préparer à être rendus.
m_Model->Render(m_D3D->GetDevice());

Le shader de texture est maintenant appelé à la place du shader de couleur pour effectuer le rendu du modèle. Notez qu'il prend aussi le pointeur de la ressource de texture du modèle de sorte que le shader texture y ait accès.

// NOUVEAU - effectue le rendu du modèle avec le shader de texture.
result = m_TextureShader->Render(m_D3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix,
m_Model->GetTexture());
if(!result)
{
return false;
}

// Affiche la scène rendue à l'écran.
m_D3D->EndScene();

return true;
}

XIII - Résumé

Vous devriez maintenant savoir charger une texture, la lier à un polygone, et effectuer son rendu avec un shader.

XIV - Exercices à faire

1. Recompilez le code et assurez-vous que la texture liée au triangle apparaisse à l'écran. Ceci fait, pressez la touche « Echap » pour quitter le programme ;

2. Créez votre propre fichier .dds et placez-le dans le même dossier que le fichier seafloor.dds. Dans la méthode GraphicsClass::Initialize, changez le nom du modèle pour utiliser votre texture, puis recompilez et exécutez de nouveau votre programme ;

3. Modifiez le code afin de créer deux triangles formant un carré. Liez la texture entière à ce carré de sorte qu'elle s'affiche correctement ;

4. Faites varier la distance avec la caméra afin d'observer l'effet du filtre MIN_MAG_MIP_LINEAR ;

5. Essayez d'autres filtres et bougez la caméra afin de voir les différents résultats.

XV - Code source

- Projet Visual Studio 2010 : dx11tut05.zip

- Source seule : dx11src05.zip

- Exécutable seul : dx11exe05.zip

XVI - Remerciements

Cet article est une traduction autorisée de l'article paru sur RasterTek.

Navigation

Tutoriel précédent : tampons, shaders et HLSL

Sommaire

Tutoriel suivant : lumière diffuse