Développer des jeux en 3D sous Windows 8 avec DirectX – Vertex haut en couleur

Après la création d’une belle pyramide dans l’article précédent, nous allons pouvoir ajouter quelques couleurs à notre objet pour plus de convivialité :)

Je vous rassure tout de suite, cet article ne sera pas aussi long que les précédents, après tout un ajout de couleur ne doit pas être si compliqué … allons voir ça de plus près !

Si vous avez bien suivi les précédents articles, vous pouvez reprendre directement le projet là où nous nous sommes arrêté. Sinon je vous suggère de récupérer directement ce projet :)

On se dirige vers le header de notre objet : MyFirst3DObject.h.

Nous allons créer une nouvelle structure qui définira les vertices de notre objet, cette structure va contenir la position et la couleur d’un vertex:

struct CustomVertex
{
	DirectX::XMFLOAT3 position; //position du vertex
	DirectX::XMFLOAT3 color; //couleur du vertex
};

Nous allons à présent nous rendre dans le fichier MyFirst3DObject.cpp

Nous allons rajouter une ligne de plus dans le tableau de description de vertex pour informer les shaders que nous renseignons à nos vertices une couleur:

//définition des données que nous renseignerons à nos vertices
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 },        
};

Nous passons ensuite à la redéfinition de nos vertices:

Anciennement définis comme ceci:

XMFLOAT3 vertices[] =
{
    XMFLOAT3(-0.5f,0.0f, 0.0f), //bas gauche
    XMFLOAT3( 0.5f, 0.0f, 0.0f), //bas droit
    XMFLOAT3( 0.0f, 0.5f, 0.0f), //haut milieu
    XMFLOAT3( 0.0f, 0.0f, 0.5f), //bas milieu avant
};

Voici à présent la nouvelle définition:

CustomVertex vertices[] =
{
    {XMFLOAT3(-0.5f,0.0f, 0.0f), XMFLOAT3(0.0f,1.0f, 0.0f)}, //bas gauche
    {XMFLOAT3( 0.5f, 0.0f, 0.0f), XMFLOAT3(0.0f,0.0f, 1.0f)}, //bas droit
    {XMFLOAT3( 0.0f, 0.5f, 0.0f), XMFLOAT3(1.0f,0.0f, 0.0f)}, //haut milieu
    {XMFLOAT3( 0.0f, 0.0f, 0.5f), XMFLOAT3(1.0f,1.0f, 1.0f)}, //bas milieu avant
};

Dans l’ordre de haut en bas, nous aurons du vert, du bleu, du rouge et du blanc.

Il vous reste à modifier cette ligne dans la méthode Render() (le numéro de la ligne devrait correspondre):

UINT stride = sizeof(XMFLOAT3);

en ceci:

UINT stride = sizeof(CustomVertex);

Nous avons terminé pour ce fichier, il ne nous reste plus qu’à modifier les fichiers SimpleVertexShader.hlsl et SimplePixelShader.hlsl ;)

Commençons par le plus court: SimplePixelShader.hlsl.

Le fichier présente une structure et une méthode main. Pour la structure, il faut simplement rajouter la prise en compte de la couleur:

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float4 color : COLOR;
};

Pour la méthode main, au lieu de retourner toujours la même couleur, nous allons retourner la couleur définie dans le Vertex

float4 main(PixelShaderInput input) : SV_TARGET
{
    return input.color;
}

Et s’en est fini pour ce fichier. Nous pouvons terminer par le fichier SimpleVertexShader.hlsl.
Nous allons modifier la structure d’entrée et de sortie du VertexShader en y ajoutant notre couleur:

//structure contenant la position de notre vertex
struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 color : COLOR;
};

//structure contenant la position transformée en 3D
struct VertexShaderOutput
{
    float4 pos : SV_POSITION;
    float4 color : COLOR;
};

Enfin nous allons ajouter cette couleur dans le main de notre fichier juste avant notre return:

VertexShaderOutput main(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 pos = float4(input.pos, 1.0f);

    pos = mul(pos,model); //transformation des coordonnées du vertex vers l'espace 3D
    pos = mul(pos, view); //espace 3D vers l'espace caméra
    pos = mul(pos, projection); //espace caméra vers l'écran 
    output.pos = pos; //ajout de la position du vertex
    output.color = float4(input.color, 1.0f); //ajout de la couleur du vertex

    return output;
}

Vous pouvez à présent admirer votre travail ;)

Code source du projet : MyFirstApp3D – Pyramide colorée.zip

2  

Développer des jeux en 3D sous Windows 8 avec DirectX – Premier objet 3D

Lors des précédents articles, nous avons vu comment préparer notre environnement sous DirectX et comment créer un premier triangle qui est la base de tout objet 3D.

Dans cet article, nous allons continuer sur le même projet que précédemment et voir comment développer notre premier objet 3D.

Nous allons procéder comme suit:

  • Modification de notre objet
  • Modification du fichier hlsl
  • Modification de nos vertices et indices
  • Création du troisième buffer (ConstantBuffer)
  • Rotation de notre objet
  • Rendu de l’objet

Si vous ne l’avez pas déjà, je vous propose de télécharger MyFirstApp3D.zip, qui vous permettra de suivre correctement cet article.

Modification de notre objet

Rendez-vous dans le header de notre objet MyFirst3DObject pour y ajouter quelques lignes.

Maintenant que nous voulons faire un objet en 3D, il va falloir travailler avec les matrices de model, vue et projection.

Ces trois matrices vont nous servir à faciliter les différentes transformations que nous allons lui affecter, notamment sa rotation que nous verrons un peu plus tard dans ce même article.

Pour faire simple, les sommets de notre objet vont, un après l’autre, passer par ces trois matrices pour être transformés et affichés à l’écran. La matrice model va transformer les coordonnées de notre sommet en coordonnées de notre espace 3D. La matrice view va ensuite transformer notre espace 3D vers l’espace que notre caméra pourra visualiser. Et enfin la matrice projection va transformer le résultat précédent pour être affiché à notre écran.

Nous allons déclarer ces trois matrices dans une structure que nous ajouterons dans le header de notre objet. Cette structure sera utilisée par un Constant Buffer.

Le constant buffer va se charger de mettre à jour les données relatives aux transformations de notre objet pour recharger les données de notre model et de notre caméra.

Voici le code de notre header :

#pragma once

#include "Direct3DBase.h"

//déclaration de la structure de notre ConstantBuffer
struct ConstantBuffer
{
    DirectX::XMMATRIX model;
    DirectX::XMMATRIX view;
    DirectX::XMMATRIX projection;
};

ref class MyFirst3DObject sealed : public Direct3DBase
{
public:
    MyFirst3DObject(void);
    ~MyFirst3DObject(void);
    virtual void CreateDeviceResources() override;
    virtual void CreateWindowSizeDependentResources() override;
    void Update(float timeTotal, float timeDelta);
    virtual void Render() override;

private:

    Microsoft::WRL::ComPtr<ID3D11VertexShader> m_vertexShader;
    Microsoft::WRL::ComPtr<ID3D11InputLayout> m_inputLayout;
    Microsoft::WRL::ComPtr<ID3D11PixelShader> m_pixelShader;

    Microsoft::WRL::ComPtr<ID3D11Buffer> m_vertexBuffer;
    Microsoft::WRL::ComPtr<ID3D11Buffer> m_indexBuffer;
    int m_indexCount;

    Microsoft::WRL::ComPtr<ID3D11Buffer> m_constantBuffer;

    ConstantBuffer m_constantBufferData; //données contenues dans le Constant Buffer
};

Nous avons aussi rajouté la déclaration du constant buffer ainsi que la déclaration des données qui seront contenues dans celui-ci.
Modification du fichier hlsl

Nous allons devoir mettre à jour le fichier SimpleVertexShader.hlsl. Notre triangle ne possédait que deux coordonnées: x et y. Si nous voulons travailler en 3D, il nous faut intégrer la troisième coordonnée (la profondeur), z.

Voici le fichier SimpleVertexShader.hlsl modifié et commenté:

cbuffer constantBuffer : register( b0 ) //déclaration du constantbuffer
{
    matrix model;
    matrix view;
    matrix projection;
};

//structure contenant la position de notre vertex en entrée (3 coordonnées non transformées)
struct VertexShaderInput
{
    float3 pos : POSITION;
};

//structure contenant la position transformée par les matrices
struct VertexShaderOutput
{
    float4 pos : SV_POSITION;
};

VertexShaderOutput main(VertexShaderInput input)
{
VertexShaderOutput output;

    float4 pos = float4(input.pos, 1.0f);

    pos = mul(pos,model); //transformation des coordonnées du vertex vers l'espace 3D
    pos = mul(pos, view); //espace 3D vers l'espace caméra
    pos = mul(pos, projection); //espace caméra vers l'écran
    output.pos = pos;

    return output;
}

Nous allons par la même occasion, modifier la définition de nos vertices dans la méthode CreateDeviceResources() de notre classe MyFirst3DObject comme ceci :

const D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

Nous avons simplement remplacé le format DXGI_FORMAT_R32G32_FLOAT par le format DXGI_FORMAT_R32G32B32_FLOAT qui admet trois float au lieu de deux.

Modification de nos vertices et indices

Nous allons rajouter un quatrième point à notre objet, celui-ci se transformera donc en une pyramide à base triangulaire:

La face arrière de notre pyramide n’est autre que notre ancien triangle, et le point se situant à l’avant de la pyramide est le sommet que nous allons rajouter.

Nous nous retrouvons dans la méthode CreateDeviceResources() pour réécrire notre tableau de vertices:

XMFLOAT3 vertices[] =
{
    XMFLOAT3(-0.5f,0.0f, 0.0f), //bas gauche
    XMFLOAT3( 0.5f, 0.0f, 0.0f), //bas droit
    XMFLOAT3( 0.0f, 0.5f, 0.0f), //haut milieu
    XMFLOAT3( 0.0f, 0.0f, 0.5f), //bas milieu avant
};

Nous pouvons à présent nous occuper des indices. Lors de la conception de notre triangle, nous n’avions que 3 indices car il n’y avait qu’une seule face à afficher. A présent, notre pyramide se compose de 4 faces, nous allons donc créer 4 fois plus d’indices. (Vu que notre topologie de conception est triangleList)

Voici le code pour les indices avec une topologie TriangleList:

unsigned short indices[] =
{
    0,1,2, //face arrière
    1,3,2, //face de droite
    3,0,2, //face de gauche
    0,3,1, //face du dessous
};

Si nous utilisons une topologie TriangleStrip, la définition des indices se présenterait comme ceci :

//topologie TriangleStrip
unsigned short indices[] =
{
   0,1,2, //face arrière
   3, //face de droite
   0, //face de gauche
   1, //face du dessous
};

Je pense que vous vous rendez compte des optimisations possibles .. :)

Création du troisième buffer (ConstantBuffer)

Nous allons passer à la création du ConstantBuffer. L’ensemble se passe dans le fichier MyFirst3DObject.cpp, à la suite de la création de notre IndexBuffer. La création du ConstantBuffer utilise la même procédure que l’indexBuffer. Nous donnons la description du buffer, ici D3D11_BIND_CONSTANT_BUFFER avec sa taille puis nous donnons l’objet dans lequel stocker le buffer.

DX::ThrowIfFailed(
    m_d3dDevice->CreateBuffer(
        &CD3D11_BUFFER_DESC(sizeof(ConstantBuffer), D3D11_BIND_CONSTANT_BUFFER),
        nullptr,
        &m_constantBuffer
        )
    );

Nous allons maintenant définir comment notre objet doit être vu. Cette définition va essentiellement dépendre de la taille de la fenêtre de notre application, nous allons donc nous placer dans la méthode adaptée: CreateWindowSizeDependentResources() sous la première ligne déjà présente.

Nous allons affecter à notre caméra un angle de 70 degrés, lui fournir le ratio de notre fenêtre, la plus petite et la plus grande distance avec laquelle un objet peut être affiché.

m_constantBufferData.projection = XMMatrixTranspose(XMMatrixPerspectiveFovRH(
    70.0f,
    m_renderTargetSize.Width / m_renderTargetSize.Height,
    0.01f,
    100.0f
    ));

Rotation de notre objet

Enfin pour terminer, nous allons faire tourner notre objet sur l’axe Y afin d’observer notre travail :)

Dans la méthode Update, nous allons positionner la caméra de notre scène, puis nous affecterons à notre model une rotation.

XMVECTOR eye = XMVectorSet(0.0f, 0.7f, 1.5f, 0.0f);
XMVECTOR at = XMVectorSet(0.0f, -0.1f, 0.0f, 0.0f);
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

m_constantBufferData.view = XMMatrixTranspose(XMMatrixLookAtRH(eye, at, up));
m_constantBufferData.model = XMMatrixTranspose(XMMatrixRotationY(timeTotal));

Les trois premières lignes correspondent à la caméra, dans l’ordre:

  • sa position,
  • le point qu’elle regarde,
  • le haut de l’espace 3D (pour que la caméra sache où est le haut et donc où est le bas :) )

Les deux dernières lignes correspondent à la création de la caméra avec les attributs précédemment créés et à la rotation de notre model sur l’axe Y (en augmentant l’angle en fonction du temps total du jeu).

Rendu de l’objet

Il ne nous reste plus qu’à afficher le tout !

Pour cela, il faut simplement ajouter le chargement du constantBuffer dans la méthode Render() et modifier la taille de l’ensemble des vertices (de XMFLOAT2 à XMFLOAT3).

Voici l’ensemble de la méthode Render() commentée:

void MyFirst3DObject::Render() 
{
	//création d'une couleur blanche
	const float white[] = { 1.0f, 1.0f, 1.0f, 1.000f };

	//on nettoie l'écran avec la couleur définie ci-dessus
    m_d3dContext->ClearRenderTargetView(
        m_renderTargetView.Get(),
        white
        );

	m_d3dContext->ClearDepthStencilView(
        m_depthStencilView.Get(),
        D3D11_CLEAR_DEPTH,
        1.0f,
        0
        );

	//établit la cible de rendu avec l'écran précédemment nettoyé
	m_d3dContext->OMSetRenderTargets(
        1,
        m_renderTargetView.GetAddressOf(),
        m_depthStencilView.Get()
        );

	//charge les données de contantBufferData vers constantBuffer
	m_d3dContext->UpdateSubresource(
		m_constantBuffer.Get(),
		0,
		NULL,
		&m_constantBufferData,
		0,
		0
		);

	//on place l'ensemble des vertices sur l'écran
	UINT stride = sizeof(XMFLOAT3);
    UINT offset = 0;
    m_d3dContext->IASetVertexBuffers(
        0,
        1,
        m_vertexBuffer.GetAddressOf(),
        &stride,
        &offset
        );

	//on charge les indices 
    m_d3dContext->IASetIndexBuffer(
        m_indexBuffer.Get(),
        DXGI_FORMAT_R16_UINT,
        0
        );

	//on indique la topologie à utiliser
	m_d3dContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	//on charge l'InputLayout
	m_d3dContext->IASetInputLayout(m_inputLayout.Get());

    //on charge le VertexShader
    m_d3dContext->VSSetShader(
        m_vertexShader.Get(),
        nullptr,
        0
        );

	//on charge le ConstantBuffer
	m_d3dContext->VSSetConstantBuffers(
		0,
		1,
		m_constantBuffer.GetAddressOf()
		);

	//on charge le PixelShader
    m_d3dContext->PSSetShader(
        m_pixelShader.Get(),
        nullptr,
        0
        );

    //Enfin nous dessinons le tout
    m_d3dContext->DrawIndexed(
        m_indexCount,
        0,
        0
        );
}

Si vous exécutez votre code, vous obtiendrez une belle pyramide toute bleue (voir l’article précédent pour la couleur).

Comme pour les précédents articles, vous pouvez retrouver l’ensemble du code source ici :) .

0  

Développer des jeux en 3D sous Windows 8 avec DirectX – Premier triangle

Suite de mon premier article sur le développement de jeux en 3D, nous allons à présent découvrir comment créer un triangle dans un univers 3D. Ce dernier nous permettra d’approcher les bases de la 3D, de son initialisation à son rendu visuel.

Nous reprenons le projet créé dans l’introduction, si besoin voici ma version :  MyFirstApp3D.zip.

Pour plus de confort, nous allons enlever le cube de départ.

Commencez par le fichier MyFirstApp3D.cpp, nous allons supprimer toutes les lignes qui contiennent « m_renderer » correspondant à ce fameux cube. Au total vous devriez avoir supprimé 6 lignes, ci-dessous l’ensemble du fichier après suppression :

#include "pch.h"
#include "MyFirstApp3D.h"
#include "BasicTimer.h"

using namespace Windows::ApplicationModel;
using namespace Windows::ApplicationModel::Core;
using namespace Windows::ApplicationModel::Activation;
using namespace Windows::UI::Core;
using namespace Windows::System;
using namespace Windows::Foundation;
using namespace Windows::Graphics::Display;

MyFirstApp3D::MyFirstApp3D() :
    m_windowClosed(false)
{
}

void MyFirstApp3D::Initialize(CoreApplicationView^ applicationView)
{
    applicationView->Activated +=
        ref new TypedEventHandler(this, &MyFirstApp3D::OnActivated);

    CoreApplication::Suspending +=
        ref new EventHandler(this, &MyFirstApp3D::OnSuspending);

    CoreApplication::Resuming +=
        ref new EventHandler(this, &MyFirstApp3D::OnResuming);

}

void MyFirstApp3D::SetWindow(CoreWindow^ window)
{
    window->SizeChanged += 
        ref new TypedEventHandler(this, &MyFirstApp3D::OnWindowSizeChanged);

    window->Closed += 
        ref new TypedEventHandler(this, &MyFirstApp3D::OnWindowClosed);

    window->PointerCursor = ref new CoreCursor(CoreCursorType::Arrow, 0);

}

void MyFirstApp3D::Load(Platform::String^ entryPoint)
{
}

void MyFirstApp3D::Run()
{
    BasicTimer^ timer = ref new BasicTimer();

    while (!m_windowClosed)
    {
        timer->Update();
        CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
    }
}

void MyFirstApp3D::Uninitialize()
{
}

void MyFirstApp3D::OnWindowSizeChanged(CoreWindow^ sender, WindowSizeChangedEventArgs^ args)
{
}

void MyFirstApp3D::OnWindowClosed(CoreWindow^ sender, CoreWindowEventArgs^ args)
{
    m_windowClosed = true;
}

void MyFirstApp3D::OnActivated(CoreApplicationView^ applicationView, IActivatedEventArgs^ args)
{
    CoreWindow::GetForCurrentThread()->Activate();
}

void MyFirstApp3D::OnSuspending(Platform::Object^ sender, SuspendingEventArgs^ args)
{
    // Save application state after requesting a deferral. Holding a deferral
    // indicates that the application is busy performing suspending operations.
    // Be aware that a deferral may not be held indefinitely. After about five
    // seconds, the application will be forced to exit.
     SuspendingDeferral^ deferral = args->SuspendingOperation->GetDeferral();

     // Insert your code here

     deferral->Complete();
}

void MyFirstApp3D::OnResuming(Platform::Object^ sender, Platform::Object^ args)
{
}

IFrameworkView^ Direct3DApplicationSource::CreateView()
{
    return ref new MyFirstApp3D();
}

[Platform::MTAThread]
int main(Platform::Array^)
{
    auto direct3DApplicationSource = ref new Direct3DApplicationSource();
    CoreApplication::Run(direct3DApplicationSource);
    return 0;
}

Nous passons ensuite au second fichier, MyFirstApp3D.h contenant une seule ligne à supprimer:

CubeRenderer^ m_renderer;

A présent, nous allons pouvoir commencer à créer une nouvelle classe qui contiendra notre objet 3D. Pour ce projet, j’ai choisi d’appeler ma classe « MyFirst3DObject », vous pouvez bien sûr choisir un autre nom du moment que celui-ci n’est pas déjà utilisé ;)

Vous vous retrouvez normalement sur le .h de votre nouvelle classe. Nous allons la faire hériter de la classe Direct3DBase permettant de manipuler des objets 3D.

#include "Direct3DBase.h"

ref class MyFirst3DObject sealed : public Direct3DBase

Après avoir hérité de Direct3DBase, nous allons alors implémenter les différentes méthodes nécessaires au bon fonctionnement de notre objet 3D:

virtual void CreateDeviceResources() override;
virtual void CreateWindowSizeDependentResources() override;
void Update(float timeTotal, float timeDelta);
virtual void Render() override;

Nous passons ensuite au fichier MyFirst3DObject.cpp pour y implémenter les méthodes déclarées précédemment dans le header de notre classe:

void MyFirst3DObject::CreateDeviceResources()
{
    Direct3DBase::CreateDeviceResources();
}

void MyFirst3DObject::CreateWindowSizeDependentResources()
{
    Direct3DBase::CreateWindowSizeDependentResources();
}

void MyFirst3DObject::Update(float timeTotal, float timeDelta)
{
}

void MyFirst3DObject::Render() 
{
}

Nous retournons ensuite dans MyFirstApp3D.h pour déclarer notre tout nouvel objet :)
On remplace l’include CubeRenderer.h par MyFirst3DObject.h comme ceci:

#include "MyFirst3DObject.h"

Puis nous déclarons notre objet dans la partie private comme ceci:

private:
	MyFirst3DObject^ m_3DObject;

Vous avez peut-être remarqué le signe ^ après le type de notre objet?
Il est nécessaire d’utiliser ce signe ainsi que « ref new » lorsque nous utilisons des objets appartenant à Windows Runtime. Il sert à manager automatiquement la durée de vie de vos objets Windows Runtime permettant de diminuer les fuites de mémoire.

Enfin, nous passons à MyFirstApp3D.cpp où nous allons instancier et gérer notre objet.
Si vous avez bien suivi mon introduction dans mon premier article vous devriez sans aucun problème réussir à compléter ce fichier. Sinon, voici un petit cours de rattrapage :

Nous commençons par la méthode Initialize pour instancier notre objet (celle-là était facile je vous l’accorde :) )

m_3DObject = ref new MyFirst3DObject();

On continue par initialiser notre objet cette fois-ci dans la méthode SetWindow.
Une fois que notre fenêtre est créée et qu’elle a la bonne dimension, nous allons pouvoir donner à notre objet les informations dont il a besoin pour être correctement initialisé.

Nous initialisons notre objet avec sa propre méthode Initialize en lui passant en argument la fenêtre du thread actif (avec CoreWindow::GetForCurrentThread())

window->PointerCursor = ref new CoreCursor(CoreCursorType::Arrow, 0);

m_3DObject->Initialize(CoreWindow::GetForCurrentThread());

Enfin, nous nous rendons dans la méthode Run pour mettre à jour et afficher notre objet.

void MyFirstApp3D::Run()
{
    BasicTimer^ timer = ref new BasicTimer();

    while (!m_windowClosed)
    {
        timer->Update();
        CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
		m_3DObject->Update(timer->Total, timer->Delta);
		m_3DObject->Render();
		m_3DObject->Present();

	}
}

Maintenant que la structure de notre projet est posée, nous allons enfin pouvoir passer à la création de notre premier triangle.

L’ensemble de sa création se passera dans la classe MyFirst3DObject. Nous allons commencer par préparer l’ossature de notre triangle avec ses vertices et indices, nous lui donnerons une couleur et pour terminer nous l’afficherons.

Commençons par définir ce qu’est un vertex (des vertices):

Un « vertex » se traduit par un « sommet » en français, ce dernier étant un point d’une figure géométrique. Ainsi, une ligne se compose de deux sommets, et un triangle de trois!

On continue par définir ce qu’est un index (des indices):

Un indice va correspondre à un numéro donné à un sommet (Cet indice est défini lorsque chaque sommet est lui-même défini: le premier sommet défini obtiendra l’indice 0). L’indice va ensuite servir à déterminer l’ordre dans lequel chaque sommet doit être dessiné.

Un petit schéma pour expliquer tout ça :

Nous avons un triangle composé de trois vertices chacun ayant des coordonnées.

Le premier point se trouve aux coordonnées -0,5 sur l’axe des abscisses et 0,0 sur l’axe des ordonnées. Etant le premier, il se voit attribuer l’indice 0 et ainsi de suite jusqu’au dernier point.

Maintenant que les présentations sont faites, nous allons pouvoir commencer à rentrer un peu plus dans le code :)

Rendez-vous dans la méthode que nous avons implémenté un peu plus tôt: CreateDeviceResources().

Cette méthode contient déjà une ligne de code qui appelle CreateDeviceRessources de l’objet Direct3DBase.

Cette dernière méthode va déclarer les versions de DirectX que votre application supporte mais va aussi initialiser les objets m_d3dDevice et m_d3dContext.

Ces deux objets sont respectivement (et très globalement) l’objet gérant votre écran et l’objet gérant les éléments à afficher sur ce dernier.

Avant de continuer, je pense qu’il serait bien de se concentrer un peu plus en détail sur le Device et le Context (ou plutôt DeviceContext).

Comme je l’ai dit, le Device va gérer votre écran. En fait, cet objet est utilisé pour créer des ressources et définir les capacités techniques qu’offre votre écran.

Le Device va contenir un à plusieurs DeviceContext. Le premier est ce qu’on appelle le Context Immédiat (ou Immediate Context en anglais), il va afficher directement à l’écran ce qu’on lui fournit. Un Device ne peut avoir qu’un seul Immediate Context. Il existe aussi le Context Différé (ou Deferred Context), celui-ci est utilisé principalement pour faire du multithreading.

Nous allons charger un fichier déjà présent dans la solution de base, il s’agit de SimpleVertexShader.hlsl.

Avant de le charger, nous allons modifier ce dernier. (Actuellement, ce fichier est configuré pour fonctionner avec un univers 3D, le but de cet article étant de voir comment concevoir un premier triangle, nous n’allons pas directement travailler dans cet univers).

Voici le fichier SimpleVertexShader.hlsl une fois modifié (Il vous suffit de remplacer l’ensemble du contenu de votre fichier par le code ci-dessous):

//structure contenant la position en entrée
//possédant deux coordonnées x et y
//pos contiendra les coordonnées des Vertices que nous enverrons
struct VertexShaderInput
{
    float2 pos : POSITION;
};

//structure contenant la position transformée en 3D (ajout de la profondeur (0.5f)) 
struct VertexShaderOutput
{
    float4 pos : SV_POSITION;
};

VertexShaderOutput main(VertexShaderInput input)
{
    VertexShaderOutput output;

    output.pos = float4(input.pos, 0.5f, 1.0f);

    return output;
}

Nous pouvons à présent le charger à la suite de Direct3DBase::CreateDeviceResources(); puis créer notre VertexShader :

//création de la tâche asynchrone dédiée à la lecture du fichier SimpleVertexShader
auto loadVSTask = DX::ReadDataAsync("SimpleVertexShader.cso");

//lancement de la tâche asynchrone loadVSTask
loadVSTask.then([this](DX::ByteArray ba){
    //exécution du code suivant lorsque le chargement du fichier est terminé
    auto bytecodeVS = ba.data;

    //création du vertexShader avec le fichier précédemment chargé
    DX::ThrowIfFailed(
        m_d3dDevice->CreateVertexShader(
        bytecodeVS->Data,
        bytecodeVS->Length,
        nullptr,
        &m_vertexShader
        )
    );
});

Nous pouvons rajouter par la même occasion dans le header de notre classe la déclaration de l’objet m_vertexShader qui va contenir notre VertexShader.

Microsoft::WRL::ComPtr<ID3D11VertexShader> m_vertexShader;

La tâche du VertexShader consiste entre autres à bien positionner, colorer les sommets de la scène 3D sur votre écran (transformation des positions 3D en position 2D).

Regardons de plus près la méthode qui construit notre VertexShader.

La méthode CreateVertexShader nécessite 4 arguments:

  • Le premier correspond aux données compilées du shader
  • Le second correspond à la taille de ses données
  • Le troisième correspond à un objet de type ID3D11ClassLinkage, ce troisième argument ne nous intéresse pas pour le moment, nous pouvons le laisser à null.
  • Le dernier correspond quant à lui à l’objet qui contiendra le VertexShader.

Nous allons rajouter la création de l’InputLayout juste après la création du VertexShader.

Un InputLayout va globalement contenir une définition des données qui peuvent être fournies pour dessiner les vertices, pour cet exemple nous allons simplement dire que nous fournirons une position 2D des vertices.

Voici l’ensemble du code commenté concernant le fichier SimpleVertexShader de sa lecture à l’implémentation de l’InputLayout:

//création de la tâche asynchrone dédiée à la lecture du fichier SimpleVertexShader
auto loadVSTask = DX::ReadDataAsync("SimpleVertexShader.cso");

//lancement de la tâche asynchrone loadVSTask
loadVSTask.then([this](DX::ByteArray ba){
    //exécution du code suivant lorsque le chargement du fichier est terminé
    auto bytecodeVS = ba.data;

    //création du vertexShader avec le fichier précédemment chargé
    DX::ThrowIfFailed(
        m_d3dDevice->CreateVertexShader(
            bytecodeVS->Data,
            bytecodeVS->Length,
            nullptr,
            &m_vertexShader
            )
        );

//définition des données que nous renseignerons à nos vertices
const D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

//création de l'inputLayout avec le tableau de définition précédemment créé
DX::ThrowIfFailed(
    m_d3dDevice->CreateInputLayout(
        vertexDesc, //tableau de définition
        ARRAYSIZE(vertexDesc),//taille du tableau
        bytecodeVS->Data,//données compilées du shader
        bytecodeVS->Length,//taille des données compilées
        &m_inputLayout//objet de sortie
        )
    );
});

A nouveau, nous rajoutons la déclaration de l’objet qui contiendra l’InputLayout dans le header de la classe.

Microsoft::WRL::ComPtr<ID3D11InputLayout> m_inputLayout;

Nous allons à présent charger le fichier SimplePixelShader.hlsl, même procédure que précédemment, nous apportons quelques modifications avant de le charger:

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    //au lieu de récupérer les données renseignées, nous affectons manuellement une couleur
    //les trois premiers arguments correspondent aux couleur rouge, vert et bleu
    //pour obtenir du bleu, il faut donc renseigner dans l'ordre 0, 0, 1
    return float4(0.0f,0.0f,1.0f,1.0f);
}

Toujours dans la même méthode (CreateDeviceResources()), nous allons ajouter à la suite de notre code le chargement du fichier SimplePixelShader ainsi que la création de notre PixelShader:

//création de la tâche asynchrone dédiée à la lecture du fichier SimplePixelShader
auto loadPSTask = DX::ReadDataAsync("SimplePixelShader.cso");

//lancement de la tâche asynchrone loadPSTask
auto createPSTask = loadPSTask.then([this](DX::ByteArray ba) {
    auto bytecodePS = ba.data;
    DX::ThrowIfFailed(
        m_d3dDevice->CreatePixelShader(
        bytecodePS->Data,//données compilées du shader
        bytecodePS->Length,//taille des données compilées
        nullptr, //classe de linkage (non utilisé pour le moment)
        &m_pixelShader //objet de sortie
        )
    );
});

Et nous ajoutons dans le header de notre classe la déclaration de m_pixelShader:

 Microsoft::WRL::ComPtr<ID3D11PixelShader> m_pixelShader;

Le PixelShader quant à lui va s’occuper du rendu de chaque pixel de vos objets, il va entre autres choses réaliser des calculs pour colorer, éclairer vos pixels.

Maintenant que les shaders sont créés, il nous reste à définir les propriétés de l’objet que nous voulons créer ! Vous vous souvenez du petit schéma que je vous ai montré un peu plus haut ? On va s’en inspirer pour créer notre triangle :) Pour rappel, les coordonnées des sommets de ce triangle étaient:

  • 0 : {-0.5;0.0}
  • 1 : {  0.0;0.5}
  • 2 : {  0.5;0.0}

Nous allons le retranscrire en code, toujours dans la même méthode, à la suite de la création de notre PixelShader:

XMFLOAT2 vertices[] =
{
    XMFLOAT2(-0.5f,0.0f),
    XMFLOAT2( 0.0f, 0.5f),
    XMFLOAT2( 0.5f, 0.0f),
};

Pas très compliqué cette déclaration, on notera quand même le type utilisé pour déclarer ce tableau XMFLOAT2.
Ce type est utilisé pour combiner deux float en une seule variable, il existe aussi XMFLOAT3 et bien d’autres.
Pour pouvoir l’utiliser, nous devons utiliser le namespace DirectX.
Il faut tout simplement le déclarer en haut de notre fichier juste après l’include du header:

using namespace DirectX;

Nous allons ensuite créer le VertexBuffer directement sous la déclaration de nos vertices :

D3D11_SUBRESOURCE_DATA vertexBufferData = {0};
vertexBufferData.pSysMem = vertices; //enregistrement des vertices pour la création du VertexBuffer
DX::ThrowIfFailed(
    m_d3dDevice->CreateBuffer(
        //description du contenu du buffer, on fourni la taille du tableau de vertices et on indique que nous voulons faire un VertexBuffer
        &CD3D11_BUFFER_DESC(sizeof(vertices), D3D11_BIND_VERTEX_BUFFER),
        &vertexBufferData,//on donne le tableau de vertices précédemment enregistré
        &m_vertexBuffer //et on stocke le tout dans m_vertexBuffer
    )
);

Encore une fois on déclare m_vertexBuffer dans notre header :

Microsoft::WRL::ComPtr<ID3D11Buffer> m_vertexBuffer;

Le VertexBuffer va contenir un tableau de l’ensemble des vertices de votre espace 3D. Au lieu d’envoyer ce tableau à chaque fois que nous voulons un rendu 3D, nous allons l’envoyer dès le début à la carte graphique pour alléger le transfert de données. Ainsi la carte graphique aura en mémoire tous les vertices dont elle à besoin pour créer un rendu de votre scène.

Nous allons enfin terminer l’initialisation de notre scène avec les indices :) (à  la suite de la déclaration de notre VertexBuffer)

Vu que nous n’avons que 3 sommets, la déclaration des indices sera assez rapide:

unsigned short indices[] =
{
    0,1,2,
};

Nous gardons ensuite en mémoire le nombre d’indices créés pour une future utilisation et nous créons l’indexBuffer de la même manière que le VertexBuffer:

m_indexCount = ARRAYSIZE(indices);
D3D11_SUBRESOURCE_DATA indexBufferData = {0};
    indexBufferData.pSysMem = indices; //enregistrement des indices pour la création de l'IndexBuffer
    DX::ThrowIfFailed(
        m_d3dDevice->CreateBuffer(
            //description du contenu du buffer, on fourni la taille du tableau d'indices et on indique que nous voulons faire un IndexBuffer
            &CD3D11_BUFFER_DESC(sizeof(indices), D3D11_BIND_INDEX_BUFFER),
            &indexBufferData,//on donne le tableau d'indices précédemment enregistré
            &m_indexBuffer //et on stocke le tout dans m_indexBuffer
            )
        );

Et on déclare m_indexBuffer ainsi que m_indexCount dans le header de la classe:

Microsoft::WRL::ComPtr<ID3D11Buffer> m_indexBuffer;
int m_indexCount;

L’IndexBuffer va optimiser votre application. Dans notre cas, il ne va pas être grandement utile mais prenons l’exemple d’un simple carré. Etant donné qu’on ne peut créer que des triangles, un carré sera composé de deux triangles. Un triangle possède 3 sommets, le carré contiendra alors 6 sommets. Je pense que vous voyez où je veux en venir. Plus vos objets seront complexes et plus ils contiendront beaucoup de sommets souvent inutiles. L’IndexBuffer va être là pour parer à ce genre de phénomène. Il va fusionner les sommets se trouvant aux mêmes positions et optimisera donc le nombre de sommet à afficher. Le carré aura donc 4 sommets comme il se doit.

Si vous êtes arrivé jusque-là, je vous garantis que vous avez fait le plus dur! Bravo ;)

Il ne nous reste plus qu’à ajouter le tout dans la méthode render() pour afficher notre triangle:

void MyFirst3DObject::Render()
{
    //création d'une couleur blanche
    const float white[] = { 1.0f, 1.0f, 1.0f, 1.000f };

    //on nettoie l'écran avec la couleur définie ci-dessus
    m_d3dContext->ClearRenderTargetView(
        m_renderTargetView.Get(),
        white
    );

    m_d3dContext->ClearDepthStencilView(
        m_depthStencilView.Get(),
        D3D11_CLEAR_DEPTH,
        1.0f,
        0
    );

    //établit la cible de rendu avec l'écran précédemment nettoyé
    m_d3dContext->OMSetRenderTargets(
        1,
        m_renderTargetView.GetAddressOf(),
        m_depthStencilView.Get()
    );

    //on place l'ensemble des vertices sur l'écran
    UINT stride = sizeof(XMFLOAT2);
    UINT offset = 0;
    m_d3dContext->IASetVertexBuffers(
        0,
        1,
        m_vertexBuffer.GetAddressOf(),
        &stride,
        &offset
    );

    //on charge les indices
    m_d3dContext->IASetIndexBuffer(
        m_indexBuffer.Get(),
        DXGI_FORMAT_R16_UINT,
        0
    );

    //on indique la topologie à utiliser
    m_d3dContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

    //on charge l'InputLayout
    m_d3dContext->IASetInputLayout(m_inputLayout.Get());

    //on charge le VertexShader
    m_d3dContext->VSSetShader(
        m_vertexShader.Get(),
        nullptr,
        0
    );

    //on charge le PixelShader
    m_d3dContext->PSSetShader(
        m_pixelShader.Get(),
        nullptr,
        0
    );

    //Enfin nous dessinons le tout
    m_d3dContext->DrawIndexed(
        m_indexCount,
        0,
        0
    );
}

Je pense que les commentaires parlent d’eux-mêmes, je reviendrais toutefois sur la topologie utilisée.

Ce type de topologie va aider à interpréter d’une certaine façon les différents vertex que nous fournissons. Il existe différentes topologies énumérées ici.
La topologie Triangle List permet de créer un triangle à chaque trois vertex.
On aura par exemple une autre topologie, Triangle Strip, qui permet, une fois le premier triangle de créé, d’ajouter un triangle à chaque fois que nous ajoutons un vertex.

A présent, vous pouvez enfin exécuter votre code et admirer.. un seul triangle sur un fond uni:

Je vous mets à disposition le code source de l’objet créé dans cet article, vous pouvez aussi récupérer l’ensemble du projet de cet article ici.

#pragma once

#include "Direct3DBase.h"

ref class MyFirst3DObject sealed : public Direct3DBase
{
public:
    MyFirst3DObject(void);
    ~MyFirst3DObject(void);
    virtual void CreateDeviceResources() override;
    virtual void CreateWindowSizeDependentResources() override;
    void Update(float timeTotal, float timeDelta);
    virtual void Render() override;

private:
    Microsoft::WRL::ComPtr<ID3D11VertexShader> m_vertexShader;
    Microsoft::WRL::ComPtr<ID3D11InputLayout> m_inputLayout;
    Microsoft::WRL::ComPtr<ID3D11PixelShader> m_pixelShader;

    Microsoft::WRL::ComPtr<ID3D11Buffer> m_vertexBuffer;
    Microsoft::WRL::ComPtr<ID3D11Buffer> m_indexBuffer;
    int m_indexCount;
};


#include "pch.h"
#include "MyFirst3DObject.h"
using namespace DirectX;

MyFirst3DObject::MyFirst3DObject(void)
{
}

MyFirst3DObject::~MyFirst3DObject(void)
{
}

void MyFirst3DObject::CreateDeviceResources()
{
	Direct3DBase::CreateDeviceResources();

	//création de la tâche asynchrone dédiée à la lecture du fichier SimpleVertexShader	
	auto loadVSTask = DX::ReadDataAsync("SimpleVertexShader.cso");

	//lancement de la tâche asynchrone loadVSTask	
	loadVSTask.then([this](DX::ByteArray ba){
		//exécution du code suivant lorsque le chargement du fichier est terminé		
		auto bytecodeVS = ba.data;

		//création du vertexShader avec le fichier précédemment chargé
		DX::ThrowIfFailed(
			m_d3dDevice->CreateVertexShader(
				bytecodeVS->Data,
				bytecodeVS->Length,
				nullptr,
				&m_vertexShader
				)
			);

		//définition des données que nous renseignerons à nos vertices
		const D3D11_INPUT_ELEMENT_DESC vertexDesc[] = 
        {
            { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0,  D3D11_INPUT_PER_VERTEX_DATA, 0 },
        };

		//création de l'inputLayout avec le tableau de définition précédemment créé
        DX::ThrowIfFailed(
            m_d3dDevice->CreateInputLayout(
                vertexDesc, //tableau de définition
                ARRAYSIZE(vertexDesc),//taille du tableau
                bytecodeVS->Data,//données compilées du shader
                bytecodeVS->Length,//taille des données compilées
                &m_inputLayout//objet de sortie
                )
            );
	});

	//création de la tâche asynchrone dédiée à la lecture du fichier SimplePixelShader	
	auto loadPSTask = DX::ReadDataAsync("SimplePixelShader.cso");

	//lancement de la tâche asynchrone loadPSTask
	 auto createPSTask = loadPSTask.then([this](DX::ByteArray ba) {
        auto bytecodePS = ba.data;
        DX::ThrowIfFailed(
            m_d3dDevice->CreatePixelShader(
                bytecodePS->Data,//données compilées du shader
                bytecodePS->Length,//taille des données compilées
                nullptr,
                &m_pixelShader //objet de sortie
                )
            );
    });

	 XMFLOAT2 vertices[] =
	 {
		XMFLOAT2(-0.5f,0.0f),
        XMFLOAT2( 0.0f,  0.5f),
        XMFLOAT2( 0.5f, 0.0f),
	 };

	 D3D11_SUBRESOURCE_DATA vertexBufferData = {0};
        vertexBufferData.pSysMem = vertices; //enregistrement des vertices pour la création du VertexBuffer
        DX::ThrowIfFailed(
            m_d3dDevice->CreateBuffer(
				//description du contenu du buffer, on fourni la taille du tableau de vertices et on indique que nous voulons faire un VertexBuffer
                &CD3D11_BUFFER_DESC(sizeof(vertices), D3D11_BIND_VERTEX_BUFFER),
                &vertexBufferData,//on donne le tableau de vertices précédemment enregistré
                &m_vertexBuffer //et on stocke le tout dans m_vertexBuffer
                )
            );

	 unsigned short indices[] =
	 {
		 0,1,2,
	 };

	 m_indexCount = ARRAYSIZE(indices);
	 D3D11_SUBRESOURCE_DATA indexBufferData = {0};
        indexBufferData.pSysMem = indices; //enregistrement des indices pour la création de l'IndexBuffer
        DX::ThrowIfFailed(
            m_d3dDevice->CreateBuffer(
				//description du contenu du buffer, on fourni la taille du tableau d'indices et on indique que nous voulons faire un IndexBuffer
                &CD3D11_BUFFER_DESC(sizeof(indices), D3D11_BIND_INDEX_BUFFER),
                &indexBufferData,//on donne le tableau d'indices précédemment enregistré
                &m_indexBuffer //et on stocke le tout dans m_indexBuffer
                )
            );

}

void MyFirst3DObject::CreateWindowSizeDependentResources()
{
	Direct3DBase::CreateWindowSizeDependentResources();
}

void MyFirst3DObject::Update(float timeTotal, float timeDelta)
{
}

void MyFirst3DObject::Render() 
{
	//création d'une couleur blanche
	const float white[] = { 1.0f, 1.0f, 1.0f, 1.000f };

	//on nettoie l'écran avec la couleur définie ci-dessus
    m_d3dContext->ClearRenderTargetView(
        m_renderTargetView.Get(),
        white
        );

	m_d3dContext->ClearDepthStencilView(
        m_depthStencilView.Get(),
        D3D11_CLEAR_DEPTH,
        1.0f,
        0
        );

	//établit la cible de rendu avec l'écran précédemment nettoyé
	m_d3dContext->OMSetRenderTargets(
        1,
        m_renderTargetView.GetAddressOf(),
        m_depthStencilView.Get()
        );

	//on place l'ensemble des vertices sur l'écran
	UINT stride = sizeof(XMFLOAT2);
    UINT offset = 0;
    m_d3dContext->IASetVertexBuffers(
        0,
        1,
        m_vertexBuffer.GetAddressOf(),
        &stride,
        &offset
        );

	//on charge les indices 
    m_d3dContext->IASetIndexBuffer(
        m_indexBuffer.Get(),
        DXGI_FORMAT_R16_UINT,
        0
        );

	//on indique la topologie à utiliser
    m_d3dContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	//on charge l'InputLayout
	m_d3dContext->IASetInputLayout(m_inputLayout.Get());

    //on charge le VertexShader
    m_d3dContext->VSSetShader(
        m_vertexShader.Get(),
        nullptr,
        0
        );

	//on charge le PixelShader
    m_d3dContext->PSSetShader(
        m_pixelShader.Get(),
        nullptr,
        0
        );

    //Enfin nous dessinons le tout
    m_d3dContext->DrawIndexed(
        m_indexCount,
        0,
        0
        );
}


Maintenant que vous savez comment intégrer un tout premier triangle dans une application Windows 8, vous êtes prêt à attaquer de la vraie 3D avec mon prochain articleÂ

3  

Développer des jeux en 3D sous Windows 8 avec DirectX – Introduction

Avec la sortie récente de la version RTM (Release To Manufacturing) de Windows 8, j’ai décidé de rédiger une série d’articles concernant le développement de jeux en 3D de style Windows Metro avec Direct X en C++.

Cet article est composé essentiellement de théorie concernant le cycle de vie et la structure d’un projet Direct3D App. En guise d’exemple, nous allons prendre un nouveau projet qui servira de fil rouge tout au long des articles de cette série. Ce projet est réalisé sous Microsoft Windows 8 Release Preview avec Microsoft Visual Studio Professional 2012 RC.

  1. Cycle de vie

    Pour commencer, le cycle de vie d’une application correspond tout simplement aux différents états dans laquelle celle-ci passe tout au long de son existence, de son démarrage à sa fermeture.

    Sous Windows 8, une application peut se trouver dans trois états distincts : NotRunning, Running et Supsended.

    Lors de son démarrage, l’application passe de l’état NotRunning à Running. Entre ces deux états, l’application va recevoir l’évènement Activated.

    L’application passe dans l’état Suspended lorsque l’utilisateur la quitte ou lorsque l’utilisateur passe une autre application au premier plan. L’évènement Suspending est alors appelé juste avant de passer dans l’état Suspended. (En règle générale, il est vivement conseillé de sauvegarder les informations de l’application lorsque l’évènement Suspending est appelé pour éviter de perdre toutes données importantes si l’application est quittée).

    Si l’utilisateur repasse l’application au premier plan, celle-ci reçoit l’évènement Resuming et revient dans l’état Running sinon l’application est quittée et passe alors à l’état NotRunning.

    Voici un petit schéma récapitulatif issue du site msdn:

  2. Création du projet Direct3D App

    Après un peu de théorie, nous allons passer à un tout petit peu de pratique avec la création d’un nouveau projet de type Direct3D App que nous allons nommer MyFirstApp3D.

    Vous vous retrouvez avec un nouveau projet ainsi que 4 fichiers déjà ouverts:

    CubeRenderer.h, CubeRenderer.cpp, MyFirstApp3D.h et MyFirstApp3D.cpp

    CubeRenderer est un objet héritant de Direct3DBase permettant de créer, mettre à jour et afficher un Cube en 3D.

    MyFirstApp3D est un objet héritant de IFrameworkView et va quant à lui gérer l’ensemble des ressources requises au bon fonctionnement de l’application.Il va aussi gérer la logique globale de votre jeu.

  3. Structure du fichier principal

    Nous allons nous intéresser à la structure de la classe MyFirstApp3D:

    Cette classe est constituée de 5 méthodes issues de IFrameworkView:

    • Initialize: appelée au lancement de l’application pour initialiser la logique du jeu, les trois évènements qui utilisent la vue sont également initialisés, à savoir Activated, Suspending et Resuming.
    • SetWindow : appelée après la méthode Initialize pour enregistrer l’ensemble des évènements se rattachant à la fenêtre de votre jeu (redimensionnement de la fenêtre, mouvement de la souris, appuie sur une touche du clavier, etc…). La méthode Initialize de notre objet Direct3DBase est aussi appelée pour qu’il récupère les paramètres de la fenêtre une fois celle-ci redimensionnée.
    • Load : appelée après la méthode SetWindow pour charger les ressources utiles à votre jeu.
    • Run : Cette méthode contient la boucle principale de votre jeu. Elle peut elle-même être décomposée en trois parties:
      • La gestion des évènements pouvant survenir dans votre jeu
      • La mise à jour des objets de votre jeu (La méthode Update de notre objet Direct3DBase est appelée)
      • L’affichage de vos objets (Les méthodes Render et Present de notre objet Direct3DBase sont appelées dans cette partie)
    • Unitinialize : Cette méthode est appelée lorsque votre jeu se ferme afin de libérer les ressources précédemment chargées dans la méthode Load.

A présent, si vous lancez votre application (avec la touche F5) vous y découvrirez un très beau cube qui tourne sur lui-même :) . Le problème c’est qu’à ce stade, nous ne savons pas vraiment pourquoi et comment celui-ci a été dessiné, ni comment il tourne, etc…

Je vous invite donc à suivre mon second article concernant la création d’un premier triangle qui constitue la base de tout objet 3D :)

1  

TimeToMove, L’application WindowsPhone 7

Pour un projet de fin de cours .NET, nous avons eu le choix entre la création d’une application de flux RSS et la création de notre propre application. Grégoire et moi avons opté pour le second choix :)

Après une centaine d’heure de développement et recherche, nous avons pu mettre sur pied TimeToMove. L’application peut facilement récupérer l’ensemble des évènements se déroulant tout près de votre position. A chacun de vos déplacements, TimeToMove rafraîchira automatiquement les évènements proche de votre position tel que les prochains concerts, les manifestations, les promotions, ou même les évènements humanitaires comme les dons du sang!
Vous pouvez ajouter vos propres évènements (comme l’organisation de votre anniversaire, d’une fête totalement improvisée, etc..), ou rechercher des évènements proche d’un lieu où vous allez bientôt vous rendre.

Voici à quoi ressemble TimeToMove :

Par la même occasion, nous participons à un concours proposé par le Site du Zéro dans la catégorie « Géolocalisation », le but est de comptabiliser un maximum de « J’aime/Like » Facebook sur la page de TimeToMove comme le montre l’image suivante :

Merci d’avance pour tous vos J’aime :)

0  

Réagir sur une interface de manière Asynchrone

Lorsque votre projet utilise une base de données, vous allez faire diverses requêtes lors du lancement de votre application. Cependant, si vos appels sont en temps réel, vous voudriez certainement que votre interface réagisse instantanément pour afficher le résultat de vos requêtes.

Si votre base de données est en local, il n’y aura pas beaucoup de problèmes, en quelques millisecondes le résultat sera affiché. Par contre si votre base de données commence à être volumineuse, ou si celle-ci se trouve sur un serveur distant, vous allez patienter de plus en plus longtemps sans pour autant pouvoir continuer à utiliser votre interface. Votre application va attendre que la base de données réponde pour pouvoir afficher les informations demandées.

Pour contourner ce problème, nous pouvons utiliser des méthodes asynchrones, cependant lorsque vous allez utiliser ce type de méthodes, votre recherche va retourner un résultat sur un Thread différent de celui de votre interface,  qui ne pourra pas récupérer les informations nécessaire à l’affichage du résultat !

Mon article vous présente une des solution possible pour vous aider à contourner ce problème.

Pour cet article, j’ai créé un nouveau projet mais rien ne vous empêche de continuer le votre ;)

Je commence par faire une petite interface qui nous permettra de simuler un accès à une base de données :

J’ai créé un simple bouton possédant une méthode dans le code behind, et un textBlock qui me permettra de visualiser le résultat Asynchrone.

A présent, le reste de mon article se passera dans le code behind de mon interface ( à noter que si vous travaillez en MVVM, le code behind que je présente se trouvera dans le ViewModel de votre View ;) )

Nous commençons par créer la méthode qui sera appelée de façon Asynchrone :

private string AMethod(int duration)
{
     Thread.Sleep(duration);

     return duration.ToString();
}

Ici,  j’ai créé une simple méthode qui récupère un entier, celle-ci attend pendant un nombre de milliseconde correspondant à l’entier donné en argument, puis on renvoit sous forme de chaîne de caractère le temps attendu.

Pour que cette méthode puisse être appelée sous forme Asynchrone, nous utilisons un délégué de celle-ci :

private delegate string AsynchroneMethod(int duration);

Pour plus d’information sur les délégués de méthode, je vous invite à vous rendre sur cette page :

http://msdn.microsoft.com/fr-fr/library/system.delegate(v=vs.95).aspx

Nous allons maintenant créer la méthode qui modifiera notre interface :

public void changeTextBlock(string msg)
{
   textBlock1.Text = msg;
}

Ici, rien de nouveau, je récupère une chaîne de caractère, et je l’envoie à mon textBlock précédemment créé.

Nous pouvons passer à l’appel de notre méthode Asynchrone.

On se place dans la méthode behind du bouton de notre interface :

private void button1_Click(object sender, RoutedEventArgs e)
{

    AsynchroneMethod AM = new AsynchroneMethod(AMethod);

    IAsyncResult result = AM.BeginInvoke(3000, new AsyncCallback(CallbackMethod),null);

    //3000 = parametre pour la méthode appelée (nombre de millisecondes à attendre)
    //new AsyncCallback(CallbackMethod) = méthode à appeler lorsque la méthode asynchrone à terminée de s'exécuter
    //null = Async State information, aucune information n'est nécessaire
}

Ici nous instancions notre méthode Asynchrone de la même manière qu’une classe normale.  Ensuite nous utilisons IAsyncResult pour appeler notre méthode asynchrone.

Comme écrit dans les commentaires, on utilise la méthode BeginInvoke de notre delegate, à laquelle nous passons en argument le nombre de milliseconde, puis en second argument une nouvelle instance permettant d’appeler une méthode lorsque la méthode asynchrone est terminée.

Passons à la création de la méthode CallBackMethod:

private void CallbackMethod(IAsyncResult ar)
{
    // Récupération du delegate
    AsyncResult result = (AsyncResult)ar;
    AsynchroneMethod AM = (AsynchroneMethod)result.AsyncDelegate;

    // Appel de EndInvoke pour récupérer le résultat de AMethod
    string returnValue = AM.EndInvoke(ar);
    changeTextBlock(returnValue);
}

Ici nous récupérons en argument le résultat de la méthode asynchrone grâce à l’objet IAsyncResult.
On le stocke dans un objet AsyncResult, puis nous récupérons la chaîne de caractère grâce à EndInvoke(ar).
Et pour finir nous appelons la méthode qui modifie l’interface.

Si vous exécutez le code à ce stade, vous obtiendrez l’erreur suivante :

Cette erreur est dû au fait que le thread asynchrone n’est pas le même que le thread de l’interface. Nous devons utiliser le Dispatcher de l’interface pour pouvoir lui envoyer nos variables.

Remplacer la ligne

changeTextBlock(returnValue);

Par celle ci :

Dispatcher.Invoke(new ChangeTextBlock(changeTextBlock), "La méthode s'est exécutée en "+ returnValue + " milliseconds"); 
//appel de la méthode qui intéragit avec l'interface utilisateur

Note: En MVVM, vous n’aurez pas la possibilité d’utiliser Dispatcher. Vous pouvez néanmoins l’utiliser de cette manière: « Dispatcher.CurrentDispatcher » avec « using System.Windows.Threading; »

Pour nous permettre de faire une instance de notre méthode, il nous faut à nouveau faire appel à un delegate :

public delegate void ChangeTextBlock(string message); 
//obligatoire pour en faire appel dans le Dispatcher

A présent, si vous exécutez votre application, votre méthode va bel et bien être appelée, puis lorsque celle-ci sera terminée appellera la méthode CallBack :)

Si vous avez des questions, tout se passe ci-dessous ;)

0  

SplashScreen, écran de chargement

Lorsque votre projet commence à demander beaucoup de temps de chargement, il se peut que vous ayez besoin d’informer l’utilisateur que votre application est en cours de chargement.

On appel dans ce cas une image qui va servir à faire patienter l’utilisateur pendant que votre application réuni les ressources nécessaires, celle-ci s’appelle un SplashScreen.

Il suffit d’ajouter à votre solution l’image que vous voulez afficher lors du chargement, puis dans les propriétés de l’image, modifier l’action de génération en SplashScreen.

Je vous conseille de prendre une image au format .png pour jouer avec la transparence pour un meilleur rendu ;)

Vous pouvez exécuter votre application, celle-ci affichera d’abord votre SplashScreen, puis votre application.

Note: Si votre application charge rapidement les ressources nécessaires, vous ne verrez pratiquement pas votre SplashScreen, voir pas du tout, si c’est le cas vous n’avez donc pas besoin de cette petite astuce … ;)

0  

Bouton personnalisé avec Microsoft Expression Blend

Création  et conception du template

Création du bouton avec template personnalisé

Pour commencer à créer un template personnalisé, nous avons besoin de créer le bouton qui sera associé au template voulu.

Ouvrez un nouveau projet WPF dans Microsoft Expression Blend

Puis sur votre MainWindow, créer votre bouton

Nous allons à présent créer notre template qui sera directement lié au bouton, faite un clic droit sur celui-ci en mode graphique ou directement dans l’arbre visuel de votre projet, sélectionnez Modifier le modèle, puis Créer un élément vide…

Indiquez le nom de votre Template, puis sélectionnez l’emplacement de celui-ci. Je vous conseille de créer un nouveau Dictionnaire de ressource si vous n’en avez pas déjà un. Ainsi vous pourrez directement regrouper et retrouver plus facilement vos différents styles de votre projet.

On se retrouve avec un nouveau fichier (ici StyleRessource.xaml) qui contient une Grid totalement vide avec les mêmes dimensions que le bouton précédemment créé.

Création du design du template

Nous allons commencer par créer la forme globale de notre bouton. Un border avec un cornerRadius de 10.

On définit une couleur unie pour son background pour faciliter la suite de la création de notre template.

On enlève le borderThickness, et on enlève toutes les marges mises automatiquement par le logiciel.

Nous nous retrouvons avec un border aux bords arrondis prenant l’ensemble de la Grid de notre template.


Pour ajouter un effet de profondeur et relief, on ajoute un dégradé en haut et en bas du border en noir (alpha : 40%)

On ajoute un dégradé noir – blanc – noir

Noir : alpha 50% position 0

Blanc : alpha 0% position 50%

Noir alpha 50% position 100%

De même que le border précédent, on supprime le borderThickness, les marges ajoutées et le cornerRadius de 10


Nous allons maintenant ajouter le texte et son effet de relief

Pour un meilleur rendu, on le centre en vertical et horizontal avec les propriétés

HorizontalAlignement et VerticalAlignement, on enlève les marges, et met le texte en Gras et de taille 11pt.


Pour rajouter un effet de relief, on ajoute un effet  DropShadowEffect. Celui-ci donnera une ombre blanche au texte.


Enfin pour se rapprocher du style Windows, nous rajoutons un Border avec dégradé blanc sur la moitié haute du bouton.

Nous modifions le Background du border avec un dégradé :

Blanc : position 25% alpha 50%

Blanc : position 75% alpha 0%

On enlève le BorderThickness et on arrondi les bords avec un CornerRadius de 10

On assigne Stretch pour l’horizontalAlignment ainsi que le verticalAlignment, et on enlève les marges

Il ne nous reste plus qu’à modifier le comportement du contrôle


Création du comportement du Template

Liaison de donnée

Nous allons commencer par modifier le texte du Bouton par celui qui sera rentré lors de la création du bouton :

Cliquez sur le bouton situé à côté du champ Text :

Sélectionnez Liaison de modèle, puis Content

Le contenu du textBlock sera automatiquement modifié par le contenu passé dans la propriété Content de notre Bouton.

Animations

OnMouseOver

Nous allons définir le comportement de notre contrôle lorsque la souris passe au-dessus de celui-ci.

Pour ce faire, nous nous rendons dans l’onglet Déclencheurs pour créer un nouvel évènement par rapport à la propriété isMouseOver :

Pour créer notre comportement, nous n’avons qu’à modifier les propriétés voulues lors de l’enregistrement de l’animation

Pour cette propriété, nous allons simplement mettre le pourcentage de l’apha du  background du dernier border à 0%

IsPressed

On ajoute une nouvelle condition de propriété, celle de IsPressed

Nous allons tout d’abord modifier les propriétés du deuxième Border qui contient le dégradé Noir-Blanc-Noir comme suit :

Noir : position 0% alpha 90%

Noir :position 100% alpha 30%


Pour cette même propriété, nous allons modifier l’effet du texte :

Enfin il faut inverser les conditions de ces deux propriétés.

Pour faire ceci, il faut modifier directement dans le code de notre template :

Trouvez la ligne <ControlTemplate.Triggers> qui se trouve vers la fin du fichier.

Et modifiez les false en true :

<Trigger Property= »IsMouseOver » Value= »True »>

<Trigger Property= »IsPressed » Value= »True »>

Vous pouvez executer avec la touche F5 !

3  

WPF dans une application XNA

Dans cet article, nous allons apprendre comment insérer des contrôles WPF à l’intérieur d’un jeu XNA.

Je ne pense pas que cette technique est approuvée par les puristes… mais pour obtenir le résultat désiré en très peu de temps sans passer par un moteur gérant des menus et autres, je pense qu’elle sera appréciée par plus d’un :)

L’exemple qui suit se base sur un projet vierge, mais cette technique fonctionne très bien sur des projets déjà avancés.

  1. Inclusion d’un contrôle WPF dans une application XNA.

Nous allons commencer par ajouter les références utilisées par les contrôles WPF :

-System.Windows.Forms

-WindowsFormsIntegration

-System.Xaml

Ainsi que le namespace requis pour nos contrôles , ajoutez la ligne suivante au début du fichier Game1.cs:

using System.Windows.Forms.Integration;

Voici le résultat:

 Jusque là rien de bien passionnant je vous l’accorde.

Nous allons continuer par l’ajout de l’objet qui contiendra notre contrôle WPF : ElementHost.

Nous ajoutons donc cette ligne au début de notre classe Game1 :

ElementHost elementHost = new ElementHost() ;

A partir de maintenant, si vous essayez de compiler, Visual Studio vous balancera une belle erreur expliquant que le thread n’est pas dans le mode adéquate.

Ceci est le principal point embêtant de cette méthode…En effet, pour intégrer des contrôles WPF dans une application XNA, vous êtes obligé de passer votre application en [STAThread].

Pour plus d’information, je vous invite à lire ceci : http://msdn.microsoft.com/fr-fr/library/ms182351(v=vs.80).aspx

Nous allons donc ajouter cette ligne dans le fichier Program.cs de notre projet (où se trouve l’entrée de notre programme).

[STAThread]
static void Main(string[] args)
{
    using (Game1 game = new Game1())
    {
        game.Run();
    }
}

A présent, nous pouvons à nouveau compiler normalement et passer à l’ajout de nos contrôles WPF !

Pour ce faire, aller dans Projet => Ajouter un nouvel élément… puis sélectionnez dans WPF, Contrôle utilisateur (WPF), enfin nommez-le.

Nous nous retrouvons avec une page xaml contenant un UserControl :)

Nous avons les mêmes fonctionnalités qu’un projet WPF banale, la fameuse Boîte à outils, avec notre interface en deux parties (interface et code).

Je vais créer un rapide contrôle pour passer à la suite du tutoriel :

Une fois celle-ci faites, nous pouvons l’ajouter à notre application XNA dans le fichier Game1.cs.

Il nous suffit d’instancier le contrôle précédemment créé à la suite de notre ElementHost, pour moi ce sera :

MonContrôle moncontrole = new MonContrôle() ;

Notre Contrôle est ajouté ! Le problème … il ne s’affiche pas :(

Nous allons utiliser l’elementHost cité un peu plus haut pour résoudre ce petit souci.

Avant toute chose, il nous faut ajouter une autre référence, System.Drawing, qui va nous permettre de  « dessiner » notre contrôle dans notre application.  Enfin ajoutez ces lignes au début du fichier:

using System.Drawing;
using Color = Microsoft.Xna.Framework.Color;

Cette dernière ligne sert d’alias pour éviter les ambiguïtés entre l’objet Color du Framework XNA et celui de System.Drawing. Vous aurez certainement besoin d’ajouter d’autre alias à la suite de ce tutoriel ;)

Nous pouvons à présent ajouter ces trois lignes dans la méthode LoadContent()

protected override void LoadContent()
{
     // Create a new SpriteBatch, which can be used to draw textures.
     spriteBatch = new SpriteBatch(GraphicsDevice);

     elementHost.Location = new System.Drawing.Point(0, 0);
     elementHost.Size = new Size(300, 300);
     elementHost.Child = moncontrole;

     // TODO: use this.Content to load your game content here
}

La première correspond à la position du coin haut gauche de notre contrôle dans notre application.

La deuxième correspond à sa taille, et la troisième correspond au contrôle à ajouter.

Cependant, tout cela ne permet pas d’afficher notre contrôle … :) il permet seulement de le charger dans l’application, mais ne vous inquiétez pas, on y est presque :)

Nous ajoutons l’utilisation du namespace System.Windows.Forms  et de l’alias ButtonState :

using System.Windows.Forms;
using ButtonState = Microsoft.Xna.Framework.Input.ButtonState;

Nous allons donc enfin afficher nos contrôles avec cette ligne dans la fonction Update(GameTime gameTime) :

Control.FromHandle(Window.Handle).Controls.Add(elementHost);

Nous voilà avec un contrôle WPF dans notre application XNA !

Dans la suite de ce tutoriel nous allons apprendre à utiliser nos contrôles WPF pour avoir une interaction dans notre application.

II.                  Interaction d’un contrôle WPF avec une application XNA.

 

Tout d’abord, je vais créer un petit sprite pour la suite du tutoriel :

J’ai ajouté un alias :

using Rectangle = Microsoft.Xna.Framework.Rectangle ;

Puis au début de la classe, j’ai ajouté deux objets :

Texture2D textRec ;
Rectangle rec = new Rectangle(400,200,30,30) ;

Ensuite dans LoadContent(), j’ai chargé la texture GameThumbnail.png .

Il suffit de faire un drag&drop de celle-ci :

Et d’ajouter la ligne suivante dans LoadContent()  :

textRec = Content.Load<Texture2D>("GameThumbnail") ;

Pour finir je la dessine dans la fonction Draw(GameTime gameTime):

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin() ;
spriteBatch.Draw(textRec, rec, Color.White) ;
spriteBatch.End();

Et voici le résultat :

Une fois notre texture ajoutée, nous allons pouvoir commencer à la faire bouger.

Certains me diront que c’est très simple en XNA, on vérifie l’appui d’une certaine touche et on déplace notre texture… oui oui mais le but ici est de le faire avec WPF ! ;)

Voici la procédure à ajouter dans la fonction Update(GameTime gameTime):

if (moncontrole.Haut.IsPressed)
    rec.Y--;

Puis…. C’est tout :) Et oui il suffit juste de ces deux lignes pour interagir avec notre contrôle !

Il nous reste à faire la même chose pour les trois autres contrôles et ce sera terminé ;)

Nous pouvons tout aussi bien avoir une interaction avec les boutons de notre contrôle WPF ainsi qu’une interaction avec le clavier dans XNA , dans ce cas il nous faut ajouter l’alias suivant :

using Keys = Microsoft.Xna.Framework.Input.Keys;

Ensuite dans la fonction Update(GameTime gameTime) :

if (moncontrole.Haut.IsPressed || Keyboard.GetState().IsKeyDown(Keys.Up))
   rec.Y--;

if (moncontrole.Bas.IsPressed || Keyboard.GetState().IsKeyDown(Keys.Down))
   rec.Y++;

if (moncontrole.Gauche.IsPressed || Keyboard.GetState().IsKeyDown(Keys.Left))
   rec.X--;

if (moncontrole.Droit.IsPressed || Keyboard.GetState().IsKeyDown(Keys.Right))
   rec.X++;

Ce tutoriel touche à sa fin, j’espère qu’il vous aura appris quelque chose et n’hésitez pas à me poser des questions si vous en avez ;)

A bientôt !

1  

Tutoriel WPF

Je vais vous présenter ici un tutoriel qui va vous permettre de découvrir les bases de la programmation en WPF. Quelques notions de .NET sont souhaitables ainsi qu’une première approche de Visual Studio, version 2010 préférable.

Si vous et votre Visual Studio êtes prêts, alors commençons. :)

I.            Création d’un nouveau projet :

Ouvrez Visual Studio 2010, puis allez sur Fichiers->Nouveau->Projet.

Enfin sélectionnez Application WPF et donnez-lui bien entendu en nom.Â

Après avoir validé, vous découvrez l’interface de développement de votre application que nous allons brièvement décrire :

  1. Ceci est l’interface principale en WPF, c’est ici que nous verrons l’ensemble de l’application évolué au niveau de son affichage. On peut y « drag & drop » tout un tas de contrôles pour modeler notre interface à notre guise. Cette interface à un lien direct avec l’interface numéro 2 qui est son code XAML.

Lors de l’ajout d’un nouveau contrôle, le code est automatiquement créé dans l’interface 2.

  1. Cette section contient le code de l’interface graphique de notre application. Lorsqu’on modifie quelque chose dans cette section, l’affichage dans la section 1 est directement modifié.
  2. Ce bloc est le couteau suisse du WPF, il contient  tous les contrôles existants de ce langage.
  3. Cette partie va nous permettre d’afficher tous les fichiers contenus dans notre projet.
  4. Cette section va afficher les propriétés de l’élément sélectionné.

Si jamais, les blocs 3, 4 et 5 ne sont pas affichés par défaut, voici comment les récupérer.Â

Dans la barre d’outils de Visual Studio allez sur Affichage et sélectionnez Explorateur de solutions, Boîte à outils, Fenêtres Propriétés.

Nous pouvons enfin passer au code.

II.            L’interface graphique :

Pour accéder à l’interface, il faut ouvrir le fichier MainWindow.xaml à partir de l’Explorateur de solutions

Vous l’aurez sans doute remarqué, nous avons déjà un contrôle existant, une Grid.

La Grid est un contrôle organisé en cellules grâce à des colonnes et des lignes. Il peut contenir l’ensemble des contrôles WPF et ceci plusieurs fois.

Pour le moment ce contrôle est vide, il ne contient aucune propriété, nous allons donc arranger cela. :)

Premièrement, on va séparer la Grid en deux.

Pour ce faire, quand on est sur la Grid on clique sur le bandeau bleu horizontal qui se trouve au-dessus de celle-ci, puis on place le curseur à la position souhaitée.

Voici le résultat de l’affichage, ainsi que le code correspondant :

Notez la présence de petites * dans le code, celles-ci correspondent à la proportion de chaque colonne.

On les trouve exclusivement dans les Grid.

Nous allons maintenant nous occuper de la partie gauche de notre application.

Ajoutons tout d’abord un StackPanel en drag&drop et ajustons-le via le code :

  • Nous allons définir le couple hauteur/largeur de ce contrôle par rapport à sa cellule en assignat la valeur Stretch aux propriétés HorizontalAlignment et VerticalAlignment.

Stretch sert à remplir la place disponible autour du contrôle.

Contrairement à une Grid qui permet comme son nom l’indique de créer une grille pour y placer des éléments à divers endroits, le StackPanel permet uniquement de placer des éléments enfants sur une seule ligne ou une seule colonne.

La valeur par défaut de cette Orientation est Vertical.

Nous allons maintenant placer en drag&drop dans notre StackPanel un TextBox qui va permettre la saisie de texte.

Un TextBox est un contrôle qui permet d’afficher et surtout d’éditer du texte.

Grâce aux propriétés, nous allons maintenant mettre en forme ce TextBox en définissant ses dimensions ainsi que des Margin, sans oublier de mettre sa VerticalScrollBarVisibility à Auto pour permettre l’affichage d’une scrollbar verticale si le texte saisi est trop long.Â

A présent nous allons nous occuper de la partie qui va servir à mettre en forme notre texte.

Nous allons donc commencer par mettre en place un Border. Ce Border va permettre d’apporter un petit plus niveau design grâce à sa propriété CornerRadius qui permet d’arrondir les angles de la Border selon la valeur qu’on lui assigne.

Voici à quoi ressemble notre Border de base, ainsi que son code :

Une fois son CornerRadius mis à 20, voici le résultat :

Bien entendu, si vous le souhaitez, il est possible d’assigner un CornerRadius individuel à chaque angle de notre Border :

La première valeur est attribuée à l’angle haut gauche, la deuxième à l’angle haut droit, la troisième à l’angle bas droit et la dernière à l’angle bas gauche.

Afin de placer correctement de la manière la plus propre nos futurs boutons, nous allons avoir besoin d’un contrôle que je vous ai présenté plus tôt et qui s’organise en cellules…

Vous l’aurez bien sûr deviné, il s’agit d’une Grid. Nous allons la placer à l’intérieur de notre joli Border.

Passons maintenant à la mise en place de nos boutons.

Nous allons ici utiliser 2 types de bouton, les RadioButton et les CheckBox.

Notez les propriétés Grid.Row et Grid.Column qui définissent la position de chaque bouton parmi les différentes cellules de la Grid.

Occupons-nous maintenant du contenu de chacun de nos checkBox et de leur mise en page grâce à leurs propriétés :

  • Dans la catégorie Commun, Content permet de définir le texte à afficher.
  • Dans la catégorie Texte, nous pouvons définir le style de Content en changeant la police, la taille, l’alignement, etc…
  • Dans la catégorie Mise en page, Width et Height assignent les dimensions à nos boutons. En assignant la valeur Center à HorizontalAlignment et VerticalAlignment et 0 à Margin, nous pouvons centrer chaque bouton dans la cellule qui la contient.

Et voici le résultat… :)

Notez le positionnement un peu particulier de notre dernier CheckBox nommé Encrypt, qui semble être à cheval sur les 2 colonnes…

En fait, grâce à la propriété attachée Grid.ColumnSpan, nous pouvons faire fusionner plusieurs colonnes ensemble pour en obtenir une unique.

Avant d’en finir avec la partie gauche de notre application, il nous reste encore un contrôle à placer, un Button.

Celui-ci va prendre place en dehors de notre Border, juste en dessous de celui-ci dans le StackPanel.

Occupons-nous de ses propriétés.

En appliquant la valeur Auto à ses dimensions Height et Width et avec un HorizontalAlignment défini sur Center, nous obtenons un bouton dont la taille s’adapte à la longueur de son Content et qui se positionne automatiquement au centre du StackPanel.

Après la partie gauche, passons à la partie droite de notre application qui va servir à afficher le rendu dans un TextBlock.

Un TextBlock, par rapport à un TextBox, ne permet pas d’éditer du texte, seul l’affichage de ce dernier est possible.

Contrairement au TextBox qui possède cette propriété, le TextBlock ne gère pas la VerticalScrollBarVisibility et ne peut donc pas afficher de lui-même une ScrollBar pour permettre le défilement du texte si celui-ci le remplit.

Mais rassurez-vous, il existe une solution.Â

Il suffit juste d’utiliser le contrôle ScrollViewer puis de mettre notre TextBlock dedans. Voyons ça tout de suite.

Dans la boîte à outils, sélectionnez le contrôle ScrollViewer et placez-le dans la partie droite de votre application.

Appliquez-lui ensuite un Margin pour laisser suffisamment de place en dessous pour y ajouter encore 2 autres contrôles que nous découvrirons juste après le TextBlock.

Toujours en drag&drop, ajoutons le TextBlock dans le ScrollViewer, sans oublier d’ajuster ses propriétés pour qu’il remplisse le ScrollViewer.


Nous avons presque terminé notre interface, il ne reste plus qu’à ajouter 2 contrôles.

Il s’agit d’un Slider et d’un Label qui serviront à modifier et afficher la taille de la police du TextBlock.

Voilà, notre interface est enfin terminée.

III.            Gestion de l’interface :

Dans cette partie nous allons voir comment interagir avec notre interface. Nous allons pour cela travailler dans le fichier MainWindow.xaml.cs, mais gardez MainWindow.xaml ouvert, vous verrez pourquoi.Â

Voici le code initial contenu dans MainWindow.xaml.cs, auto-généré par Visual Studio à la création du projet :

Voici comment nous allons nous organiser :

  1. Boutons de modification de la casse :

Tout d’abord, pour permettre la désactivation d’un RadioButton quand l’autre est activé il va être néceassaire de leur définir un GroupName dans MainWindow.xaml :

Ensuite, toujours dans MainWindow.xaml sélectionnez le RadioButton « A » puis allez sur l’onglet Evènements dans ses propriétés.

Double-cliquez sur Checked :

De cette manière, vous allez générer une méthode vide dans le codebehind qui sera appelée lorsque ce RadioButton sera activé :

Maintenant nous pouvons ajouter le code qui va permettre à ce RadioButton de mettre tout le texte en MAJUSCULES :

Comme vous pouvez le constater, il n’y a rien de bien compliqué ici. :)

Procédez de la même manière pour le second RadioButton « a », mais cette fois sa méthode devra permettre de mettre le texte en minuscules :

  1. Bouton Gras :

Notre bouton « G » étant un CheckBox, il n’est pas nécessaire de lui attribuer un GroupName, chaque CheckBox étant indépendant des autres.

Comme expliqué juste avant, dans ses propriétés, dans l’onglet Evènements double-cliquez sur Checked, puis allez dans le code behind (MainWindow.xaml.cs pour rappel ), voilà à quoi devrait ressembler votre méthode :

Avant de passer au bouton suivant, attardons-nous un peu sur notre CheckBox.

Du fait que les CheckBox ne voient pas leur état modifié par l’activation d’une autre CheckBox comme c’est le cas pour les RadioButton, nous devons nous-même gérer ce changement.

Pour remédier à cela, retournez dans MainWindow.xaml et de nouveau dans les Evènements de cette CheckBox « G », cherchez la méthode Unchecked, tout en bas de la liste, double-cliquez dessus et revenez sur le code behind :

Vous y verrez une nouvelle méthode générée, dans laquelle vous allez implémenter le code permettant de remettre le poids du texte en normal :

  1. Bouton Italique :

De la même manière que pour le bouton « G » ci-dessus, double-cliquez sur les Evènements Checked et Unchecked et définissez ces méthodes ainsi :

  1. Bouton Encrypt :

Encore une fois, vous voici devant un CheckBox. Pas de différence avec nos 2 précédents CheckBox quant aux Evènements à sélectionner.

La partie intéressante ici se trouve dans le code behind.Â

Pour ce tutoriel, j’ai choisi de faire simple et d’utiliser un algorithme connu et facilement implémentable, le chiffre de César qui est chiffrement par décalage.

Pour plus de détails si vous le souhaitez, Chiffrement par décallage.

Mais voici un exemple concret de ce cryptage basique :

(source : Wikipédia)

Passons maintenant au code :

  1. Bouton Vider texte :

Ici vous avez un simple Button, qui ne nécessite pas la gestion de différents états et aucun besoin non plus de lui définir un GroupName.

Le seul Evènement qu’il va falloir gérer et le Click, quoi de plus normal pour un simple bouton, n’est-ce pas ? ^^

Dans ses Propriétés, toujours dans l’onglet Evènements, double-cliquez sur Click (c’est le premier de la liste), puis allez dans le code behind pour vous occuper de sa méthode qui vous allez le voir est très très compliquée !! :p

La voici :

Alors ? Pas trop difficile ?Â

  1. Le Slider de zoom avec son Label :

C’est presque fini, voici la dernière ligne droite, le zoom.

Pour ces 2 derniers contrôles nous n’allons pas procéder de la même manière que pour nos boutons. Cette fois tout va se passer dans le fichier MainWindow.xaml, il n’est plus question de code behind…

Nous allons utiliser le Binding.

« Le Binding ? C’est quoi ? »

Bonne question, voyons ça tout de suite.

Un petit schéma pour commencer :p

Explications :

WPF permet d’une manière simple et puissante de mettre à jour automatiquement des données entre le modèle et l’interface utilisateur, c’est ce qu’on appelle le DataBinding. A chaque modification d’une donnée dans le modèle, celle-ci est automatiquement envoyée à l’interface et vice versa. C’est la méthode la plus appréciée pour envoyer des informations à l’interface utilisateur.

Pour mettre en place ce Binding, à partir du code XAML, il faut utiliser {Binding} dans la propriété de l’objet cible.

Voyons comment mettre en place ce Binding dans notre code.

Commençons par définir quelle propriété cible de notre TextBlock va devoir être modifiée par notre Slider… Il s’agit de la propriété FontSize.

Ensuite il nous faut déterminer la propriété du Slider à récupérer. Ça doit sûrement être Value.Â

Pour permettre une utilisation correcte du Slider il va falloir lui définir un Minimum et un Maximum pour encadrer Value.

Enfin, il ne nous reste plus qu’à implémenter notre Binding. De quelle manière ? En utilisant la propriété ElementName de Binding qui va récupérer le nom de l’élément à utiliser comme source, ce qui correspond dans notre cas au Name de notre Slider.

Mais ce n’est pas tout, après avoir récupéré le nom de notre Slider, il ne reste plus qu’à récupérer sa propriété Value avec la propriété Path du Binding.

Ce qui nous donne dans le code :

Maintenant que le Binding est appliqué sur le TextBlock, faisons la même chose pour notre Label !

La procédure est exactement la même que pour le TextBlock, à la différence près que la propriété cible est ici Content.

Et nous obtenons :

Petit bonus pour notre Slider :

Je pense que vous serez d’accord avec moi, une taille de police à virgule, comme par exemple 12,93738, ce n’est pas très jolie, alors occupons-nous de ce petit détail.Â

Pour n’obtenir que des valeurs entières, il faut donc limiter la Value du Slider à des valeurs en Integer et non plus en Double comme c’est le cas pour le moment.

Pour ce faire, dans le code XAML du Slider il est nécessaire d’assigner la valeur True à IsSnapToTickEnabled, comme suit :

Notre tutoriel touche enfin à sa fin. Si vous souhaitez laisser un commentaire ou poser une question à propos de ce tutoriel, tout est prévu ci-dessous. :)

A bientôt.

co-écrit par Silvestre Thomas
3