Inizializzazione 1750 Visite) DirectX 11
Il Device ed il DeviceContext sono gli oggetti principali di Direct3D11. Fino alla versione 10 erano un unico oggetto mentre ora sono stati suddivisi per competenze: il primo è dedicato alla creazione delle risorse, il secondo al loro utilizzo. Un’applicazione Direct3D inizia quando questi 2 oggetti sono creati e termina quando vengono distrutti.
Il Device è legato ad un controllo dotato di Handle, un codice univoco con cui Windows identifica un controllo come ad esempio una finestra; per tanto è necessario creare un Form ed inizializzare a partire da essa il nostro Device.
Oltre al Device è fondamentale un secondo oggetto: lo Swap Chain. Come spiegato nei precedenti articoli Direct3D prende dati in ingresso e genera ogni istante un’immagine da copiare su una superficie. Questa superficie deve essere inviata al monitor e questo legame è fatto tramite lo SwapChain. Questo può essere di 2 tipi: Windowed o FullScreen. Il primo racchiude l’intera visualizzazione all’interno della finestra lasciando il resto del desktop perfettamente visibile, l’altra invece prevede che l’intero schermo sia utilizzato come zona di visualizzazione nascondendo tutto ciò che c’è sotto. Dal punto di vista della creazione non cambia molto ma bisogna stare attenti a gestire gli eventi dell’utente. Ad esempio in modalità Windowed l’utente può ridimensionare a piacimento la finestra mentre in quella FullScreen potrebbe ridurre l’applicazione ad icona e poi riaprirla. Per evitare crash bisogna imparare a gestire le due situazioni.
SharpDX permette di creare un Device a partire da qualsiasi risorsa, ma fornisce anche un comodo oggetto che crea una Form perfettamente ottimizzata per il rendering
RenderForm form = new RenderForm();
Per creare un device occorre definirne le sue caratteristiche tramite la struttura SwapChainDescription
Device11 _device;
DeviceContext _deviceContext;
SwapChain _swapchain;
var desc = new SwapChainDescription()
{
BufferCount = 1
ModeDescription = new ModeDescription(Width, Height, new Rational(60, 1), Format.R8G8B8A8_UNorm),
IsWindowed = true,
OutputHandle = form.Handle,
SampleDescription = new SampleDescription(1, 0),
SwapEffect = SwapEffect.Discard,
Usage = Usage.RenderTargetOutput
};
Con lo SwapChainDescription si specificano i parametri di creazione come la risoluzione (1024 x 768), se in modalità finestra (IsWindowed =true) e diversi altri parametri che sono spiegati in dettaglio nella SDK e che saranno oggetti di futuri tutorial. In modalità windowed la risoluzione deve essere pari a quella della finestra (o meglio la client area che non include la barra della finestra). In modalità FullScreen dovrete utilizzare per forza una delle modalità supportate dalla scheda.
Passando la descrizione all’istruzione Device11.CreateWithSwapChain verrà creato sia il Device che lo SwapChain. Tra i parametri da passare i FeatureLevel supportati e la modalità (nella maggior parte dei casi Hardware ma è possibile utilizzare la modalità Reference per testare il sistema su schede video incompatibili ma in questo caso le performance saranno troppo basse per fare qualsiasi cosa).
FeatureLevel[] levels = new FeatureLevel[] { FeatureLevel.Level_11_0, FeatureLevel.Level_10_1, FeatureLevel.Level_10_0 };
DeviceCreationFlags flag = DeviceCreationFlags.None;
Device11.CreateWithSwapChain(SharpDX.Direct3D.DriverType.Hardware, flag, levels, desc, out _device, out _swapchain);
_deviceContext = Device.ImmediateContext;
Infine il metodo _swapchain.GetParent permette di ottenere un oggetto Factory per permettervi di controllare diversi eventi (ad esempio, abilitare o meno, la funzionalità Alt+Enter che permette di modificare la visualizzazione da finestra a FullScreen e viceversa).
var factory = _swapchain.GetParent<Factory>();
factory.MakeWindowAssociation(View.Handle, WindowAssociationFlags.IgnoreAll);
I 3 oggetti sono ora creati ma non è ancora sufficiente. Tornando alla Pipeline di Direct3D il rendering viene fatto su superfici da riversare sullo swap chain. Queste superfici sono i render target. Per creare un render target è sufficiente
var _backBufferTexture = SwapChain.GetBackBuffer<Texture2D>(0);
_backbufferView = new RenderTargetView(Device, _backBufferTexture);
_backBufferTexture.Dispose();
A supporto del Render Target va creato un secondo oggetto chiamato DepthStencil, responsabile della gestione della profondità. Questa superficie gestisce la profondità nella scena ed è necessario per scene 3D in cui un oggetto può apparire dietro ad un altro (ossia nel 99% dei casi).
var _zbufferTexture = new Texture2D(Device, new Texture2DDescription()
{
Format = Format.D16_UNorm,
ArraySize = 1,
MipLevels = 1,
Width = View.ClientSize.Width,
Height = View.ClientSize.Height,
SampleDescription = new SampleDescription(1, 0),
Usage = ResourceUsage.Default,
BindFlags = BindFlags.DepthStencil,
CpuAccessFlags = CpuAccessFlags.None,
OptionFlags = ResourceOptionFlags.None
});
_zbufferView = new DepthStencilView(Device, _zbufferTexture);
_zbufferTexture.Dispose();
Eseguite il Dispose degli oggetti Texture in modo da liberare al più presto la memoria occupata da questi oggetti.
Creato il DepthStencilView ed il RenderTargetView questi possono essere associati al DeviceContext descrivendo tramite l’oggetto viewport la dimensione degli stessi. Il numero 1 indica quanti render target utilizzare. Infatti è possibile utilizzare più render target contemporaneamente ma in questo tutorial tratteremo l'utilizzo di default.
DeviceContext.Rasterizer.SetViewport(0, 0, View.ClientSize.Width, View.ClientSize.Height);
DeviceContext.OutputMerger.SetTargets(_zbufferView, _backbufferView);
Ora finalmente la nostra applicazione può essere utilizzata. Ogni rendering prevede la pulizia dei RenderTarget, l’esecuzione dei shader e draw dei buffer e quindi il present dello swap chain.
Nel primo esempio mostrerò una semplice finestra blu. Per far questo si utilizzeranno le prime istruzioni base di Direct3D. La prima è il Clear che rende il render target del colore desiderato pulendolo del precedente rendering. La seconda è il Clear del DepthStencil che pulisce il backbuffer dai precedenti calcoli della profondità.
DeviceContext.ClearRenderTargetView(_backbufferView, color);
DeviceContext.ClearDepthStencilView(_zbufferView, DepthStencilClearFlags.Depth, 1.0F, 0);
Come vedete è il DeviceContext ad eseguire i metodi.
Infine lo SwapChain manda il render target a video
SwapChain.Present(0, PresentFlags.None);
In quest’istante il contenuto del back buffer verrà mostrato a video. Inserendo le 3 istruzioni in una funzione e chiamandola dal ciclo di gestione dei messaggi di windows si farà in modo che questa venga eseguita anche migliaia di volte al secondo, i cosiddetti fotogrammi al secondo o FPS (frame per second). Una scena complessa ovviamente richiederà più tempo ma saranno sufficienti 20-30 FPS per avere una buona fluidità (l’ideale sono 60 FPS). SharpDX ci viene incontro con il metodo RenderLoop.Run che gestisce per noi il ciclo dei messaggi.
In caso la finestra richieda di essere ridimensionata (a seguito di un resize) occorrerà fermare il rendering, distruggere i 2 target (tramite metodo Dispose), ridimensionare lo SwapChain tramite il suo metodo
SwapChain.ResizeBuffers(1, form.ClientSize.Width, form.ClientSize.Height, Format.R8G8B8A8_UNorm, SwapChainFlags.AllowModeSwitch);
E quindi ricreare i 2 target prima di procedere di nuovo al rendering.
Vi rimando al tutorial numero 2 della serie (Github o in fondo dal primo tutorial)