afficher dans plusieurs vues avec DirectX :
CreateAdditionalSwapChain
(niveau : moyen)


Table des matières

Introduction
Architecture
IRenderer / CRenderer
CSwapChain
CSwapChainDX8
CRendererDX8
Utilisation
Référence
Mise à jour : redimensionnement des fenêtres
Mise à jour : DirectX 9


Introduction

Le but de cet article est d' afficher une scène visualisée par plusieurs caméras différentes dans 4 vues indépendantes, comme sur cette image :

Cela est évidemment utile dès que l'on doit programmer des outils : éditeur de niveaux, d'effets spéciaux, d'animations, etc...
Avec DirectX 8, une fois l'objet principal créé par un appel à Direct3DCreate8 (reportez-vous à la documentation et aux samples du SDK si vous ne savez pas de quoi il s'agit), il serait possible de demander un nouveau device pour chacune des vues lors de son initialisation. Cette option présente les inconvénients suivants :

- la documentation dit que DirectX est optimisé pour le rendu avec un seul device (cas des jeux, et le plus fréquent), et pas avec 4.
- pour pouvoir afficher la scène voulue, chaque device doit posséder les objets (index et vertex buffers), les textures et les matériaux correspondants. En admettant qu'il soit possible (je ne l'ai pas vérifié) de partager ces ressources entre les devices, il faudrait également s'assurer que les render states sont identiques, c'est à dire les maintenir synchronisés. Beaucoup de travail dont on souhaite si possible se passer !

La "bonne" méthode consiste à utiliser la fonction CreateAdditionalSwapChain, comme indiqué dans la documentation (cf Using Direct3D / Rendering / Presenting a scene / Multiple views in windowed mode). Celle-ci n'est malheureusement pas très bavarde sur le sujet, et le seul sample (DxTex) utilisant à l'heure actuelle cette fonction n'affiche qu'un quad déjà projeté dans le repère de l'écran (screen space), sans zbuffer. Je n'ai pas non plus trouvé beaucoup d'informations sur internet, mise à part la page citée en référence ; d'où cet article et le programme de démonstration (version DirectX 8, 329 Ko - version DirectX 9, 412 Ko) qui l'accompagne.


Architecture

L'exemple fourni est une application MFC très simple (une MDI pour être précis, Multiple Document Interface), mais cela n'a aucune importance vis à vis du problème qui nous intéresse. Les fichiers importants sont situés dans le répertoire \rendering, qui comporte un sous-répertoire \dx8. Le premier contient les classes de base qui ne dépendent pas de l'API utilisée pour le rendu (= elles seraient identiques avec OpenGL), le second contient les classes dérivées spécifiques à DirectX 8.

Ces classes sont les suivantes :
- IRenderer << CRenderer << CRendererDX8
- CSwapChain << CSwapChainDX8

La classe IRenderer et ses dérivées assurent l'affichage dans l'application. La classe CSwapChain et sa dérivée gèrent les différentes "swap chains" ; mais qu'est-ce qu'une swap chain dans DirectX ? Ce terme désigne une série (chaîne circulaire) de buffers, constituée d'1 ou plusieurs back buffers, et de 0 ou 1 zbuffer (et/ou éventuellement du stencil buffer qui va avec, puisque zbuffer et stencil partagent le même espace sur les cartes actuelles). La scène est rendue dans chacun des back buffers à tour de rôle (en utilisant toujours le même zbuffer), puis copiée dans le front buffer pour devenir visible à l'écran.

Par défaut le device contient déjà 1 swap chain, créée au moment de l'appel à CreateDevice, ce qui lui permet d'afficher en plein écran ou dans une vue. Le principe du rendu dans plusieurs vues est de créer une swap chain supplémentaire pour chaque vue à l'aide de CreateAdditionalSwapChain, de diriger l'affichage vers la chaîne adéquate lorsqu'on doit rafraîchir une vue, et de copier son contenu dans la fenêtre correspondante. Ce sont ces différentes étapes qui vont être expliquées par la suite.

NB : le device possédant déjà une swap chain, il n'est pas nécessaire d'en créer une pour la 1ère vue. C'est cependant ce que je fais, pour simplifier et uniformiser la gestion des swap chains. Afin de ne pas gaspiller inutilement de la mémoire, ma swap chain associée au device ne fait que 16*16 pixels, cf la création du renderer dans MultiDx.cpp.


IRenderer / CRenderer

Pour les besoins de cet article, j'ai réduit le renderer au minimum vital afin de ne pas compliquer inutilement la compréhension des sources ; c'est pourquoi il n'y a par exemple pas de gestion des textures. Partant de cette base, j'ai fait les modifications suivantes :

- dans la classe virtuelle IRenderer, ajout des méthodes :
    virtual u32               CreateSwapChain     (const HWND hWnd,const u32 u32Flags=_WINDOWED_|_ZBUFFER_,
                                                   const u32 u32Width=0,const u32 u32Height=0) = 0;
    virtual void              ReleaseSwapChain    (const u32 u32Chain) = 0;
    virtual bool              SelectSwapChain     (const u32 u32Chain) = 0;
    virtual bool              AddSwapChain        (CSwapChain* pSwapChain) = 0;
    virtual bool              RemoveSwapChain     (CSwapChain* pSwapChain) = 0;

    virtual CSwapChain*       GetSwapChain        (const u32 u32Chain) = 0;
    virtual CSwapChain*       GetCurrentChain     (void) const = 0;

- dans la classe CRenderer, ajout des méthodes :
    virtual bool              AddSwapChain        (CSwapChain* pSwapChain);
    virtual bool              RemoveSwapChain     (CSwapChain* pSwapChain);

    virtual CSwapChain*       GetSwapChain        (const u32 u32Chain);
    virtual CSwapChain*       GetCurrentChain     (void) const;
et des membres :
  // typedefs

  public:

    typedef std::list< CSwapChain* >              listChain;
    typedef listChain::iterator                   iterChain;

  // protected data

  protected:

    listChain                 m_SwapChains;
    u32                       m_u32CurrentChain;
    CSwapChain*               m_pCurrentChain;

On voit que :

- CRenderer stocke et gère les swap chains du device dans une liste STL. Elle possède également la notion de "chaîne courante".
- plusieurs méthodes ne sont pas implémentées dans CRenderer : CreateSwapChain, ReleaseSwapChain, SelectSwapChain. Leur code dépend de l'API graphique utilisée, et se trouve donc dans CRendererDX8.


CSwapChain

Le but de cette classe est uniquement de stocker les caractéristiques d'une swap chain : handle, mode fenêtré ou non, possède un zbuffer ou pas, handle de la fenêtre associée, dimensions, couleur du clear (pas indispensable). Son header est le suivant :

//--------------------------------------------------------------------------------------------------------------------//
//                                                SWAP CHAIN CLASS                                                    //
//                                                (MULTIPLE VIEWS)                                                    //
// 22/05/02, Mythos                                                                                                   //
//--------------------------------------------------------------------------------------------------------------------//

#ifndef   _MYTHOS_SWAPCHAIN_H_
#define   _MYTHOS_SWAPCHAIN_H_
#pragma    once

//----------------------------------------------- INCLUDES -----------------------------------------------------------//

#include  "Maths/VectMat.h"

#include  "Global/Defines.h"
#include  "Memory/SmartPtr.h"

//----------------------------------------------- CLASSES ------------------------------------------------------------//

//--------------------------------------------------------------------------------------------------------------------//
//                                                CSwapChain                                                          //
//--------------------------------------------------------------------------------------------------------------------//

namespace Mythos
{
  class CSwapChain
  {
  // public methods

  public:

    // constructors & destructor

                              CSwapChain          (u32 u32Handle,bool boWindowed,bool boZBuffer,HWND hWnd,u32 u32Width,u32 u32Height);
    virtual                  ~CSwapChain          (void);

    // get/set

    static u32                GetFreeHandle       (void);
    u32                       GetHandle           (void) const;

    bool                      IsWindowed          (void) const;
    bool                      HasZBuffer          (void) const;

    HWND                      GetHwnd             (void) const;
    u32                       GetWidth            (void) const;
    u32                       GetHeight           (void) const;

    CVect3D                   GetClearColor       (void) const;
    void                      SetClearColor       (const CVect3D& v3Color);

  // protected data

  protected:

    static u32                m_u32NextHandle;
    u32                       m_u32Handle;

    bool                      m_boWindowed;
    bool                      m_boZBuffer;

    HWND                      m_hWnd;
    u32                       m_u32Width,m_u32Height;

    CVect3D                   m_v3ClearColor;
  };

  // smart ptr

  _SMART_POINTER_(CSwapChain)
}

//----------------------------------------------- INLINES ------------------------------------------------------------//

#ifndef   _DEBUG
#include  "SwapChain.inl"
#endif

//--------------------------------------------------------------------------------------------------------------------//

#endif // _MYTHOS_SWAPCHAIN_H_

NB : la fonction GetHandle retourne le handle de la swap chain considérée, la méthode statique GetFreeHandle retourne le prochain numéro libre et l'incrémente, cf CSwapChain.inl.


CSwapChainDX8

La classe CSwapChainDX8 est également très simple : elle ajoute les méthodes et membres suivants, qui vont permettre de manipuler une swap chain créée avec DirectX 8 :

    // get/set

    IDirect3DSwapChain8*      GetInterface        (void) const;
    void                      SetInterface        (IDirect3DSwapChain8* pInterface);
    IDirect3DSurface8*        GetBackBuffer       (void) const;
    void                      SetBackBuffer       (IDirect3DSurface8* pBackBuffer);
    IDirect3DSurface8*        GetZStencil         (void) const;
    void                      SetZStencil         (IDirect3DSurface8* pZStencil);

  // protected data

  protected:

    IDirect3DSwapChain8*      m_pInterface;
    IDirect3DSurface8*        m_pBackBuffer;
    IDirect3DSurface8*        m_pZStencil;

m_pInterface pointe la swap chain DX8, m_pBackBuffer pointe son back buffer (dans mon application chaque swap chain n'a qu'un back buffer, le triple buffering n'est pas intéressant en mode fenêtré et consomme beaucoup de mémoire vidéo), m_pZStencil pointe l'éventuel zbuffer (et/ou son stencil) ou est NULL. Ainsi certaines vues qui n'en auraient pas besoin (exemple : browser de textures) peuvent ne pas avoir de zbuffer, ce qui économise la mémoire vidéo.


CRendererDX8

Qu'est-ce qui change dans le renderer DirectX, par rapport à la version "classique" (avec une seule vue) ? Peu de choses en fait, 3 nouvelles fonctions apparaissent et 2 sont modifiées (3 si on compte le Clear dont la couleur dépend de la swap chain utilisée).

- CreateSwapChain : cette méthode est de loin la plus longue, c'est celle qui crée une nouvelle swap chain pour le device. La chaîne contient 1 back buffer, et un zbuffer ou pas en fonction des flags. Son format est hardcodé (D3DFMT_D16), il faudra modifier le code si un stencil buffer s'avère nécessaire.

//----------------------------------------------- CreateSwapChain ----------------------------------------------------//
// create additional swap chain
// 21/05/02, Mythos
// in : window handle,flags(=_WINDOWED_|_ZBUFFER_),width(=0) & height(=0)
// out: chain handle, 0==error
// rem: if width==height==0 the client area of the window is used
//--------------------------------------------------------------------------------------------------------------------//

u32 Mythos::CRendererDX8::CreateSwapChain(const HWND hWnd,const u32 u32Flags,const u32 u32Width,const u32 u32Height)
  {
  bool boWindowed =(u32Flags & _WINDOWED_) != 0;
  bool boZBuffer  =(u32Flags & _ZBUFFER_ ) != 0;
  u32  u32Width2  = u32Width;
  u32  u32Height2 = u32Height;

  if(boWindowed)
    {
    RECT rect;
    if(!GetClientRect(hWnd,&rect)) return 0;
    if(!u32Width)  u32Width2  = rect.right;
    if(!u32Height) u32Height2 = rect.bottom;
    }

  if(!u32Width2 || !u32Height2) return 0;

  // back buffer

  if(!m_pD3D | !m_pDevice) return 0;

  D3DDISPLAYMODE d3ddm;
  if(FAILED(m_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT,&d3ddm))) return 0;

  D3DPRESENT_PARAMETERS d3dpp; 
  ZeroMemory(&d3dpp,sizeof(d3dpp));
  d3dpp.BackBufferWidth  = u32Width2;
  d3dpp.BackBufferHeight = u32Height2;
  d3dpp.BackBufferFormat = d3ddm.Format;
  d3dpp.BackBufferCount  = 1;
  d3dpp.MultiSampleType  = D3DMULTISAMPLE_NONE;
  d3dpp.SwapEffect       = D3DSWAPEFFECT_DISCARD;
  d3dpp.hDeviceWindow    = hWnd;
  d3dpp.Windowed         = boWindowed;
  d3dpp.EnableAutoDepthStencil = false;
  d3dpp.Flags            = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;

  IDirect3DSwapChain8* pInterface;
  HRESULT hrErr = m_pDevice->CreateAdditionalSwapChain(&d3dpp,&pInterface);
  if(FAILED(hrErr))
    {
    TCHAR szError[1024];
    D3DXGetErrorString(hrErr,szError,1024);
    _ASSERT_(false,"CreateAdditionalSwapChain failed");
    _ASSERT_(false,szError);
    return 0;
    }

  // Z & stencil

  IDirect3DSurface8* pZStencil = NULL;

  if(boZBuffer)
    {
    hrErr = m_pDevice->CreateDepthStencilSurface(u32Width2,u32Height2,D3DFMT_D16,D3DMULTISAMPLE_NONE,&pZStencil);
    if(FAILED(hrErr))
      {
      TCHAR szError[1024];
      D3DXGetErrorString(hrErr,szError,1024);
      _ASSERT_(false,"CreateDepthStencilSurface failed");
      _ASSERT_(false,szError);
      pInterface->Release();
      return 0;
      }
    }

  //

  CSwapChainDX8* pSwapChain = new CSwapChainDX8(CSwapChain::GetFreeHandle(),boWindowed,boZBuffer,hWnd,u32Width2,u32Height2);
  if(!pSwapChain)
    {
    pInterface->Release();
    if(pZStencil) pZStencil->Release();
    return 0;
    }

  IDirect3DSurface8* pBackBuffer;
  pInterface->GetBackBuffer(0,D3DBACKBUFFER_TYPE_MONO,&pBackBuffer);

  AddSwapChain(pSwapChain);
  pSwapChain->SetInterface (pInterface);
  pSwapChain->SetBackBuffer(pBackBuffer);
  pSwapChain->SetZStencil  (pZStencil);

  return pSwapChain->GetHandle();
  }

La création du back buffer est assez similaire à celle du device. Le zbuffer pourrait être créé par l'intermédiaire du champ EnableAutoDepthStencil, mais 1) il n'y a pas toujours besoin d'un zbuffer, 2) il faut pouvoir récupérer son adresse et l'interface IDirect3DSwapChain8 ne possède pas de méthode GetDepthStencilSurface. C'est pourquoi le zbuffer est créé à part.

Si tout s'est correctement passé, un objet de type CSwapChainDX8 est créé et ajouté à la liste des swap chains. Ses variables sont initialisées, et son handle est retourné pour indiquer le succès de l'opération.


- ReleaseSwapChain : cette méthode est beaucoup plus simple : elle retire de la liste la swap chain demandée (via la fonction RemoveSwapChain de CRenderer), et la détruit. Ceci a pour effet de libérer les buffers alloués par DirectX, grâce aux Release placés dans le destructeur de CSwapChainDX8.

//----------------------------------------------- ReleaseSwapChain ---------------------------------------------------//
// release additional swap chain
// 21/05/02, Mythos
// in : chain handle
// out:
//--------------------------------------------------------------------------------------------------------------------//

void Mythos::CRendererDX8::ReleaseSwapChain(const u32 u32Chain)
  {
  CSwapChainDX8* pSwapChain = reinterpret_cast<CSwapChainDX8*>(GetSwapChain(u32Chain));
  if(!pSwapChain) return;

  RemoveSwapChain(pSwapChain);
  delete pSwapChain;
  }


- SelectSwapChain : cette méthode active la swap chain passée en paramètre, c'est à dire qu'elle dirige toutes les futures opérations du device vers le back buffer et le zbuffer correspondants. Elle utilise pour cela la fonction SetRenderTarget.

//----------------------------------------------- SelectSwapChain ----------------------------------------------------//
// select as rendering target
// 21/05/02, Mythos
// in : chain handle
// out: OK?
//--------------------------------------------------------------------------------------------------------------------//

bool Mythos::CRendererDX8::SelectSwapChain(const u32 u32Chain)
  {
  if(!m_pDevice) return false;
  if((u32Chain == m_u32CurrentChain) && m_pCurrentChain) return true;

  CSwapChainDX8* pSwapChain = reinterpret_cast<CSwapChainDX8*>(GetSwapChain(u32Chain));
  if(!pSwapChain) return false;

  IDirect3DSurface8* pBackBuffer = pSwapChain->GetBackBuffer();
  IDirect3DSurface8* pZStencil   = pSwapChain->GetZStencil();
  if(!pBackBuffer) return false;

  if(FAILED(m_pDevice->SetRenderTarget(pBackBuffer,pZStencil))) return false;

  m_pCurrentChain   = pSwapChain;
  m_u32CurrentChain = u32Chain;
  return true;
  }


- Create : après la création du device, quelques lignes sont insérées dans ma méthode Create afin de faire apparaître sa swap chain initiale dans la liste du renderer, avec le handle 0. Cette swap chain n'est pas utilisée dans l'exemple, mais pourrait l'être dans une autre application.

  // swap chain 0

  CSwapChainDX8* pSwapChain = new CSwapChainDX8(0,boWindowed,boZBuffer,hWnd,u32Width2,u32Height2);
  if(!pSwapChain) return false;
  AddSwapChain(pSwapChain);

  IDirect3DSurface8* pBackBuffer;
  IDirect3DSurface8* pZStencil;
  m_pDevice->GetBackBuffer(0,D3DBACKBUFFER_TYPE_MONO,&pBackBuffer);
  m_pDevice->GetDepthStencilSurface(&pZStencil);

  pSwapChain->SetInterface (NULL);
  pSwapChain->SetBackBuffer(pBackBuffer);
  pSwapChain->SetZStencil  (pZStencil);

  SelectSwapChain(0); 


- Swap : la méthode qui rend visible à l'écran l'image tracée est modifiée. Selon que l'on veut copier le contenu de la swap chain initiale du device ou d'une swap chain additionnelle, il faut appeler la méthode Present du device ou de la chaîne souhaitée. Le prototype est le même dans les deux cas, d'où le code :

//----------------------------------------------- Swap ---------------------------------------------------------------//
// swap front & back buffers
// 20/05/02, Mythos
// in : source(=NULL) & dest(=NULL) rects,nb vbls(=0) to wait (TBI)
// out: OK?
//--------------------------------------------------------------------------------------------------------------------//

bool Mythos::CRendererDX8::Swap(const RECT* pSrcRect,const RECT* pDestRect,const u32 u32Vbls)
  {
  if(!m_pDevice) return false;
  if(!m_u32CurrentChain) return SUCCEEDED(m_pDevice->Present(pSrcRect,pDestRect,NULL,NULL));

  CSwapChainDX8* pSwapChain = reinterpret_cast<CSwapChainDX8*>(m_pCurrentChain);
  if(!pSwapChain) return false;
  IDirect3DSwapChain8* pInterface = pSwapChain->GetInterface();
  if(!pInterface) return false;
  return SUCCEEDED(pInterface->Present(pSrcRect,pDestRect,NULL,NULL));

  UNREFERENCED_PARAMETER(u32Vbls);
  }


Utilisation

Le renderer (donc son device) est créé dans MultiDx.cpp, pendant l'initialisation de l'application (InitInstance). Symétriquement, il est détruit dans ExitInstance. Toutes les vues de tous les documents (car vous pouvez en créer d'autres par la commande File\New, dans la limite de la mémoire vidéo disponible, et les "cascader" ou les "tiler") utilisent ce renderer commun, auquel elles peuvent accéder via la variable globale theApp. Lorsqu'elle a du temps de libre (fonction OnIdle), l'application demande à toutes les vues du document actif de se rafraîchir, et au document de faire tourner la théière d'un angle donné. Lorsque le programme n'a plus la main (m_boActivated est à false), cette portion de code n'est plus exécutée afin de libérer le CPU pour les autres processus.

En ce qui concerne les swap chains, tout est évidemment géré dans la classe CMultiDxView, dont chaque vue de l'exemple fourni est une instance. Elle possède une variable m_u32Renderer qui contient le handle de la swap chain qui lui correspond (ou 0 si la chaîne n'existe pas), et deux méthodes DrawFrame et CreateRenderer.

CreateRenderer est appelée à l'initialisation de la vue depuis OnInitialUpdate, comme son nom l'indique plus ou moins elle est chargée de demander au renderer de créer une nouvelle swap chain pour la vue. Cette chaîne sera bien sûr utilisée dans DrawFrame, et détruite par le destructeur de CMultiDxView.

DrawFrame est appelée par OnEraseBkgnd (par exemple si une autre application est déplacée et que cela nécessite de redessiner la vue), et dans OnUpdate par le message envoyé lorsque l'application demande à ses vues de se rafraîchir.

Maintenant que nous avons vu le fonctionnement général de l'ensemble, il nous reste à regarder de plus près le code des deux méthodes principales :

void CMultiDxView::DrawFrame()
  {
  if(!m_u32Renderer || !theApp.m_pRenderer) return;

  Mythos::IRenderer* pRenderer = theApp.m_pRenderer;
  if(!pRenderer->SelectSwapChain(m_u32Renderer))
    {
    _ASSERT_(false,"DrawFrame : SelectSwapChain failed");
    return;
    }

  // rotate mesh

  Mythos::CMat4x4 m4Y;
  m4Y.RotationY(GetDocument()->m_fAngleY);
  pRenderer->SetModel2World(m4Y);

  // camera

  Mythos::CMat4x4 m4W2C;
  Mythos::CMat4x4 m4Rot = Mythos::CMat4x4::Identity;
  m4W2C.Translation(Mythos::CVect3D(0.f,0.f,-5.f));

  switch(m_u32Renderer)
    {
    case 1 :
             break;
    case 2 : m4Rot.RotationY(-_PI_/2.f);
             break;
    case 3 : m4Rot.RotationX(-_PI_/2.f);
             break;
    case 4 : m4Rot.RotationXYZ(Mythos::CVect3D(-_PI_/4.f,_PI_/6.f,_PI_/4.f));
             break;
    }

  m4W2C *= m4Rot;
  pRenderer->SetWorld2View(m4W2C);

  // draw

  _ASSERT_(pRenderer->Clear(),     "DrawFrame : Clear failed");
  _ASSERT_(pRenderer->BeginScene(),"DrawFrame : BeginScene failed");

  pRenderer->DrawTeapot();

  _ASSERT_(pRenderer->EndScene(),  "DrawFrame : EndScene failed");
  _ASSERT_(pRenderer->Swap(),      "DrawFrame : Swap failed");
  }

//

bool CMultiDxView::CreateRenderer()
  {
  Mythos::IRenderer* pRenderer = theApp.m_pRenderer;
  if(!pRenderer) return false;

  CFrameWnd* pFrame = GetParentFrame();                     // CChildFrame
  if(!pFrame) return false;
  RECT Rect;
  pFrame->GetClientRect(&Rect);
  Rect.right  /= 2;
  Rect.bottom /= 2,

  m_u32Renderer = pRenderer->CreateSwapChain(m_hWnd,Mythos::IRenderer::_WINDOWED_ | Mythos::IRenderer::_ZBUFFER_,Rect.right,Rect.bottom);
  if(!m_u32Renderer) return false;

  if(!pRenderer->SelectSwapChain(m_u32Renderer)) return false;

  Mythos::CVect3D v3Red   (.5f,0.f,0.f);
  Mythos::CVect3D v3Green (0.f,.5f,0.f);
  Mythos::CVect3D v3Blue  (0.f,0.f,.5f);
  Mythos::CVect3D v3Yellow(.5f,.5f,0.f);
  Mythos::CVect3D v3Color;

  static DWORD dwColor = 0;
  switch(dwColor)
    {
    case 0  : v3Color = v3Red;    break;
    case 1  : v3Color = v3Green;  break;
    case 2  : v3Color = v3Blue;   break;
    case 3  : v3Color = v3Yellow; break;
    default : v3Color.Set(0.f);   break;
    }
  dwColor++;
  dwColor &= 3;

  pRenderer->SetClearColor(v3Color);
  return true;
  }

DrawFrame commence par vérifier qu'une swap chain existe bien pour la vue, et la sélectionne pour le rendu. Les matrices model->world et world->view sont ensuite calculées de façon à avoir une caméra différente pour chaque vue (cf l'instruction switch). L'affichage en lui-même n'est pas différent de ce que l'on rencontre habituellement lorsque l'on n'a qu'une vue : Clear, BeginScene, tracé, EndScene, Swap. Pour information, la théière est construite dans CRendererDX8::Create par un appel à D3DXCreateTeapot.

CreateRenderer demande au renderer de créer une swap chain correspondant aux dimensions de la vue (il s'agit des dimensions initiales, le redimensionnement de la vue n'est pas géré : si vous modifiez sa taille, le rendu est toujours effectué dans 1/4 d'écran, mais étiré au moment de la copie pour couvrir toute la fenêtre, car c'est ce qui est défini dans le Swap par les paramètres passés à la méthode Present). CreateRenderer sélectionne ensuite la nouvelle swap chain, et lui affecte une couleur d'effacement en fonction du "numéro" de la vue.

En résumé : une fois la swap chain créée, il suffit de la sélectionner au début de DrawFrame, et tout l'affichage se déroule ensuite comme d'habitude.


Référence

La page qui a quasiment été ma seule source d'information pour cet article.
La façon de faire de Robert diffère de la mienne : la swap chain initiale de son device couvre tout l'écran, et le zbuffer est partagé par toutes les vues.
Mon code peut paraître bien long comparé au sien : c'est parce qu'il s'insère dans les classes si possible réutilisables d'un renderer réel, ce qui nécessite obligatoirement un peu plus de lignes. En espérant que cela pourra vous être utile un jour :)


Mise à jour : redimensionnement des fenêtres

Le premier programme (version DirectX 8) fourni avec cet article ne gére pas le redimensionnement des fenêtres, ce qui présente deux inconvénients majeurs :

- les buffers (back buffers et z-buffers) des 4 swap chains ne sont jamais redimensionnés, ils gardent leurs tailles d'origine. Si une vue est agrandie, le rendu est donc fait dans une fenêtre plus petite que la vue, et le résultat est zoomé par DirectX afin qu'il couvre toute la vue, ce qui produit un affichage texélisé et flou.

- la matrice de projection n'est jamais mise à jour, ce qui signifie que la caméra "voit" toujours la même portion de la scène (ici la théière), et l'affiche à l'écran sans tenir compte du ratio de la vue. Le ratio de départ hauteur/largeur est de 4/3, s'il n'est pas préservé lorsque la vue est redimensionnée la scène est "étirée" horizontalement ou verticalement.

Ces deux défauts sont corrigés dans la version DirectX 9, de la façon suivante :

- pour redimensionner les buffers, il suffit de recréer la swap chain avec de nouvelles dimensions. C'est ce que fait la méthode ResizeSwapChain qui a été ajoutée à la classe du renderer. Si elle n'arrive pas à créer les nouveaux buffers les anciens sont conservés. Le handle de la swap chain demeure inchangé.
Le code de cette fonction ressemble beaucoup à celui de CreateSwapChain, idéalement on devrait éviter la duplication en créant une nouvelle fonction contenant les lignes communes.
ResizeSwapChain est appelée depuis la méthode MFC CMultiDxView::OnSize de la vue.

- le ratio de chaque vue est pris en compte dans la méthode CMultiDxView::DrawFrame afin de calculer une matrice de projection qui ne déforme pas la théière. Le code qui se charge de cette tâche est simplement :
  RECT rect;
  GetClientRect(&rect);
  pRenderer->SetPerspective(_DEG2RAD_(60.f),1.f,1000.f,(float)rect.right/rect.bottom);
Notez que le ratio largeur/hauteur est passé à SetPerspective en tant que dernier paramètre.
Cette fonction préserve la largeur de la scène, c'est à dire que la théière couvre toujours le même pourcentage de la vue en largeur, tandis que la hauteur s'adapte. Il est évidemment possible de faire le contraire en modifiant SetPerspective.


Mise à jour : DirectX 9

La conversion du sample et plus particulièrement des swap chains de DirectX 8 à DirectX 9 ne pose pas de problème. Voici les quelques modifications effectuées :

- les prototypes de certaines fonctions de DirectX ont changé, souvent pour leur ajouter un ou plusieurs paramètres : GetBackBuffer, D3DXFillTexture (paramètres const), CreateDepthStencilSurface, Present (pour une swap chain).
- certains render states ont disparu, et d'autres sont apparus.
- DXGetErrorString9 remplace D3DXGetErrorString. Pour l'utiliser, il faut inclure la librairie dxerr9.
- le filtrage des textures n'est plus géré par des SetTextureStageState, mais avec la nouvelle fonction SetSamplerState.
- enfin, SetRenderTarget ne permet plus de spécifier le depth/stencil buffer à utiliser, il faut appeler pour cela SetDepthStencilSurface.


haut de la page