Condividi tramite


Aggiunta di contenuto visivo all'esempio Marble Maze

Questo documento descrive come il gioco Marble Maze usa Direct3D e Direct2D nell'ambiente dell'app UWP (Universal Windows Platform) in modo da poter apprendere i modelli e adattarli quando si lavora con contenuti di gioco personalizzati. Per informazioni sul modo in cui i componenti visivi del gioco rientrano nella struttura complessiva dell'applicazione di Marble Maze, vedere struttura dell'applicazione Marble Maze.

Questi passaggi di base sono stati seguiti durante lo sviluppo degli aspetti visivi di Marble Maze:

  1. Creare un framework di base che inizializza gli ambienti Direct3D e Direct2D.
  2. Usa programmi di modifica di immagini e modelli per progettare gli asset 2D e 3D visualizzati nel gioco.
  3. Assicurarsi che gli asset 2D e 3D vengano caricati correttamente e visualizzati nel gioco.
  4. Integrare vertex shader e pixel shader che migliorano la qualità visiva degli asset del gioco.
  5. Integrare la logica del gioco, ad esempio l'animazione e l'input dell'utente.

Ci siamo anche concentrati prima sull'aggiunta di asset 3D e poi su asset 2D. Ad esempio, ci siamo concentrati sulla logica principale del gioco prima di aggiungere il sistema di menu e il timer.

È anche necessario scorrere alcuni di questi passaggi più volte durante il processo di sviluppo. Ad esempio, quando sono state apportate modifiche ai modelli mesh e marble, è stato necessario modificare anche parte del codice shader che supporta tali modelli.

Annotazioni

Il codice di esempio corrispondente a questo documento è disponibile nell'esempio di gioco DirectX Marble Maze .

  Ecco alcuni dei punti chiave illustrati in questo documento quando si lavora con DirectX e il contenuto del gioco visivo, vale a dire quando si inizializzano le librerie grafiche DirectX, si caricano le risorse della scena e si aggiorna e si esegue il rendering della scena:

  • L'aggiunta di contenuto del gioco comporta in genere molti passaggi. Questi passaggi richiedono spesso anche l'iterazione. Gli sviluppatori di giochi si concentrano spesso sull'aggiunta di contenuti di gioco 3D e poi sull'aggiunta di contenuto 2D.
  • Raggiungere più clienti e offrire loro tutta un'esperienza eccezionale supportando la più grande gamma di hardware grafico possibile.
  • Separare chiaramente i formati di fase di progettazione da quelli di esecuzione. Strutturare gli asset in fase di progettazione per ottimizzare la flessibilità e abilitare iterazioni rapide sul contenuto. Formattare e comprimere gli asset per caricare ed eseguire il rendering nel modo più efficiente possibile in fase di esecuzione.
  • Crei i dispositivi Direct3D e Direct2D in un'app UWP come fai in un'app desktop di Windows classica. Una differenza importante è il modo in cui la catena di scambio è associata alla finestra di output.
  • Quando si progetta il gioco, assicurarsi che il formato mesh scelto supporti gli scenari chiave. Ad esempio, se il tuo gioco richiede collisioni, assicurati di poter ottenere dati sulle collisioni dalle tue mesh.
  • Separare la logica del gioco dalla logica di rendering aggiornando prima tutti gli oggetti della scena e poi eseguendo il rendering.
  • Si disegnano generalmente gli oggetti della scena 3D, e poi tutti gli oggetti 2D che appaiono davanti alla scena.
  • Sincronizza il disegno con lo spazio verticale vuoto per assicurarti che il gioco non impiega tempo a disegnare fotogrammi che non verranno mai effettivamente visualizzati sullo schermo. Un vuoto verticale è il tempo tra il momento in cui un fotogramma finisce di disegnarsi sul monitor e l'inizio del disegno del fotogramma successivo.

Introduzione alla grafica DirectX

Quando abbiamo pianificato il gioco UWP (Marble Maze Universal Windows Platform), abbiamo scelto C++ e Direct3D 11.1 perché sono scelte eccellenti per creare giochi 3D che richiedono il massimo controllo sul rendering e prestazioni elevate. DirectX 11.1 supporta l'hardware da DirectX 9 a DirectX 11 e pertanto può aiutare a raggiungere più clienti in modo più efficiente perché non è necessario riscrivere il codice per ognuna delle versioni precedenti di DirectX.

Marble Maze usa Direct3D 11.1 per eseguire il rendering degli asset del gioco 3D, ovvero la biglia e il labirinto. Marble Maze usa anche Direct2D, DirectWrite e Windows Imaging Component (WIC) per disegnare gli asset del gioco 2D, ad esempio i menu e il timer.

Lo sviluppo di giochi richiede la pianificazione. Se non hai familiarità con la grafica DirectX, ti consigliamo di leggere DirectX: Introduzione per acquisire familiarità con i concetti di base della creazione di un gioco DirectX UWP. Durante la lettura di questo documento e l'uso del codice sorgente di Marble Maze, è possibile fare riferimento alle risorse seguenti per informazioni più approfondite sulla grafica DirectX:

  • Direct3D 11 Graphics: descrive Direct3D 11, un'API grafica 3D potente e accelerata dall'hardware per il rendering della geometria 3D nella piattaforma Windows.
  • Direct2D: descrive Direct2D, un'API grafica 2D con accelerazione hardware che offre prestazioni elevate e rendering di alta qualità per geometria 2D, bitmap e testo.
  • DirectWrite: Descrive DirectWrite, che supporta il rendering di testo di alta qualità.
  • Windows Imaging Component: descrive WIC, una piattaforma estendibile che fornisce API di basso livello per le immagini digitali.

Livelli di funzionalità

Direct3D 11 introduce un paradigma denominato livelli di funzionalità . Un livello di funzionalità è un set ben definito di funzionalità GPU. Usa i livelli di funzionalità per impostare come destinazione il tuo gioco per l'esecuzione nelle versioni precedenti dell'hardware Direct3D. Marble Maze supporta il livello di funzionalità 9.1 perché non richiede funzionalità avanzate dai livelli superiori. Ti consigliamo di supportare la più grande gamma di hardware possibile e ridimensionare il contenuto del gioco in modo che i tuoi clienti che dispongono di computer di fascia alta o bassa abbiano un'esperienza ottimale. Per altre informazioni sui livelli di funzionalità, vedere Direct3D 11 on Downlevel Hardware.

Inizializzazione di Direct3D e Direct2D

Un dispositivo rappresenta l'adattatore video. Crei i dispositivi Direct3D e Direct2D in un'app UWP come fai in un'app desktop di Windows classica. La differenza principale è il modo in cui si connette la catena di scambio Direct3D al sistema di gestione delle finestre.

La classe DeviceResources è una base per la gestione di Direct3D e Direct2D. Questa classe gestisce l'infrastruttura generale, non gli asset specifici del gioco. Marble Maze definisce la classe MarbleMazeMain per gestire asset specifici del gioco, che ha un riferimento a un oggetto DeviceResources per fornire l'accesso a Direct3D e Direct2D.

Durante l'inizializzazione, il costruttore DeviceResources crea risorse indipendenti dal dispositivo e i dispositivi Direct3D e Direct2D.

// Initialize the Direct3D resources required to run. 
DX::DeviceResources::DeviceResources() :
    m_screenViewport(),
    m_d3dFeatureLevel(D3D_FEATURE_LEVEL_9_1),
    m_d3dRenderTargetSize(),
    m_outputSize(),
    m_logicalSize(),
    m_nativeOrientation(DisplayOrientations::None),
    m_currentOrientation(DisplayOrientations::None),
    m_dpi(-1.0f),
    m_deviceNotify(nullptr)
{
    CreateDeviceIndependentResources();
    CreateDeviceResources();
}

La DeviceResources classe separa questa funzionalità in modo che possa rispondere più facilmente quando l'ambiente cambia. Ad esempio, chiama il metodo CreateWindowSizeDependentResources quando le dimensioni della finestra cambiano.

Inizializzazione delle factory Direct2D, DirectWrite e WIC

Il metodo DeviceResources::CreateDeviceIndependentResources crea le factory per Direct2D, DirectWrite e WIC. Nella grafica DirectX, le factory sono i punti di partenza per la creazione di risorse grafiche. Marble Maze specifica D2D1_FACTORY_TYPE_SINGLE_THREADED perché tutto il disegno viene eseguito sul thread principale.

// These are the resources required independent of hardware. 
void DX::DeviceResources::CreateDeviceIndependentResources()
{
    // Initialize Direct2D resources.
    D2D1_FACTORY_OPTIONS options;
    ZeroMemory(&options, sizeof(D2D1_FACTORY_OPTIONS));

#if defined(_DEBUG)
    // If the project is in a debug build, enable Direct2D debugging via SDK Layers.
    options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif

    // Initialize the Direct2D Factory.
    DX::ThrowIfFailed(
        D2D1CreateFactory(
            D2D1_FACTORY_TYPE_SINGLE_THREADED,
            __uuidof(ID2D1Factory2),
            &options,
            &m_d2dFactory
            )
        );

    // Initialize the DirectWrite Factory.
    DX::ThrowIfFailed(
        DWriteCreateFactory(
            DWRITE_FACTORY_TYPE_SHARED,
            __uuidof(IDWriteFactory2),
            &m_dwriteFactory
            )
        );

    // Initialize the Windows Imaging Component (WIC) Factory.
    DX::ThrowIfFailed(
        CoCreateInstance(
            CLSID_WICImagingFactory2,
            nullptr,
            CLSCTX_INPROC_SERVER,
            IID_PPV_ARGS(&m_wicFactory)
            )
        );
}

Creazione dei dispositivi Direct3D e Direct2D

Il metodo DeviceResources::CreateDeviceResources chiama D3D11CreateDevice per creare l'oggetto dispositivo che rappresenta la scheda di visualizzazione Direct3D. Poiché Marble Maze supporta il livello di funzionalità 9.1 e versioni successive, il metodo DeviceResources::CreateDeviceResources specifica i livelli da 9.1 a 11.1 nella matrice featureLevels. Direct3D scorre la lista in ordine e fornisce all'app il primo livello di funzionalità che è disponibile. Di conseguenza, le voci dell'array D3D_FEATURE_LEVEL sono elencate dal più alto al più basso per consentire all'app di raggiungere il livello di funzionalità più alto disponibile. Il metodo DeviceResources::CreateDeviceResources ottiene il dispositivo Direct3D 11.1 effettuando una query sul dispositivo Direct3D 11 restituito da D3D11CreateDevice.

// This flag adds support for surfaces with a different color channel ordering
// than the API default. It is required for compatibility with Direct2D.
UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;

#if defined(_DEBUG)
    if (DX::SdkLayersAvailable())
    {
        // If the project is in a debug build, enable debugging via SDK Layers 
        // with this flag.
        creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
    }
#endif

// This array defines the set of DirectX hardware feature levels this app will support.
// Note the ordering should be preserved.
// Don't forget to declare your application's minimum required feature level in its
// description.  All applications are assumed to support 9.1 unless otherwise stated.
D3D_FEATURE_LEVEL featureLevels[] =
{
    D3D_FEATURE_LEVEL_11_1,
    D3D_FEATURE_LEVEL_11_0,
    D3D_FEATURE_LEVEL_10_1,
    D3D_FEATURE_LEVEL_10_0,
    D3D_FEATURE_LEVEL_9_3,
    D3D_FEATURE_LEVEL_9_2,
    D3D_FEATURE_LEVEL_9_1
};

// Create the Direct3D 11 API device object and a corresponding context.
ComPtr<ID3D11Device> device;
ComPtr<ID3D11DeviceContext> context;

HRESULT hr = D3D11CreateDevice(
    nullptr,                    // Specify nullptr to use the default adapter.
    D3D_DRIVER_TYPE_HARDWARE,   // Create a device using the hardware graphics driver.
    0,                          // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
    creationFlags,              // Set debug and Direct2D compatibility flags.
    featureLevels,              // List of feature levels this app can support.
    ARRAYSIZE(featureLevels),   // Size of the list above.
    D3D11_SDK_VERSION,          // Always set this to D3D11_SDK_VERSION for UWP apps.
    &device,                    // Returns the Direct3D device created.
    &m_d3dFeatureLevel,         // Returns feature level of device created.
    &context                    // Returns the device immediate context.
    );

if (FAILED(hr))
{
    // If the initialization fails, fall back to the WARP device.
    // For more information on WARP, see:
    // https://go.microsoft.com/fwlink/?LinkId=286690
    DX::ThrowIfFailed(
        D3D11CreateDevice(
            nullptr,
            D3D_DRIVER_TYPE_WARP, // Create a WARP device instead of a hardware device.
            0,
            creationFlags,
            featureLevels,
            ARRAYSIZE(featureLevels),
            D3D11_SDK_VERSION,
            &device,
            &m_d3dFeatureLevel,
            &context
            )
        );
}

// Store pointers to the Direct3D 11.1 API device and immediate context.
DX::ThrowIfFailed(
    device.As(&m_d3dDevice)
    );

DX::ThrowIfFailed(
    context.As(&m_d3dContext)
    );

Il metodo DeviceResources::CreateDeviceResources quindi crea il dispositivo Direct2D. Direct2D usa Microsoft DirectX Graphics Infrastructure (DXGI) per interagire con Direct3D. DXGI consente la condivisione delle superfici di memoria video tra runtime grafici. Marble Maze utilizza il dispositivo DXGI sottostante del dispositivo Direct3D per creare il dispositivo Direct2D dalla fabbrica Direct2D.

// Create the Direct2D device object and a corresponding context.
ComPtr<IDXGIDevice3> dxgiDevice;
DX::ThrowIfFailed(
    m_d3dDevice.As(&dxgiDevice)
    );

DX::ThrowIfFailed(
    m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice)
    );

DX::ThrowIfFailed(
    m_d2dDevice->CreateDeviceContext(
        D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
        &m_d2dContext
        )
    );

Per altre informazioni su DXGI e l'interoperabilità tra Direct2D e Direct3D, vedere panoramica di DXGI e Direct2D e Direct3D Interoperability Overview.

Associare Direct3D alla visualizzazione

Il metodo DeviceResources::CreateWindowSizeDependentResources crea le risorse grafiche che dipendono da una determinata dimensione della finestra, ad esempio la catena di scambio e le destinazioni di rendering Direct3D e Direct2D. Un modo importante per cui un'app UWP DirectX differisce da un'app desktop è il modo in cui la catena di scambio è associata alla finestra di output. Una catena di scambio è responsabile della visualizzazione del buffer su cui il dispositivo esegue il rendering sul monitor. La struttura dell'applicazione 'Marble Maze' descrive come il sistema di gestione delle finestre per un'app UWP differisce da un'app desktop. Poiché un'app UWP non funziona con oggetti HWND, Marble Maze deve usare il metodo IDXGIFactory2::CreateSwapChainForCoreWindow per associare l'output del dispositivo alla visualizzazione. L'esempio seguente illustra la parte del metodo DeviceResources::CreateWindowSizeDependentResources che crea la catena di scambio.

// Obtain the final swap chain for this window from the DXGI factory.
DX::ThrowIfFailed(
    dxgiFactory->CreateSwapChainForCoreWindow(
        m_d3dDevice.Get(),
        reinterpret_cast<IUnknown*>(m_window.Get()),
        &swapChainDesc,
        nullptr,
        &m_swapChain
        )
    );

Per ridurre al minimo il consumo di energia, che è importante eseguire su dispositivi a batteria come portatili e tablet, il metodo DeviceResources::CreateWindowSizeDependentResources chiama il metodo IDXGIDevice1::SetMaximumFrameLatency per garantire che il rendering del gioco venga eseguito solo dopo il vuoto verticale. La sincronizzazione con lo spazio verticale è descritta in modo più dettagliato nella sezione Presentazione della scena in questo documento.

// Ensure that DXGI does not queue more than one frame at a time. This both 
// reduces latency and ensures that the application will only render after each
// VSync, minimizing power consumption.
DX::ThrowIfFailed(
    dxgiDevice->SetMaximumFrameLatency(1)
    );

Il metodo DeviceResources::CreateWindowSizeDependentResources inizializza le risorse grafiche in modo che funzioni per la maggior parte dei giochi.

Annotazioni

Il termine visualizzazione ha un significato diverso in Windows Runtime rispetto a quello in Direct3D. In Windows Runtime, una visualizzazione fa riferimento alla raccolta di impostazioni dell'interfaccia utente per un'app, tra cui l'area di visualizzazione e i comportamenti di input, oltre al thread usato per l'elaborazione. Tu specifichi la configurazione e le impostazioni necessarie quando crei una vista. Il processo di impostazione della vista dell'app è descritto in struttura dell'applicazione Marble Maze. In Direct3D il termine view ha più significati. Una visualizzazione risorse definisce le sottorisorse a cui una risorsa può accedere. Ad esempio, quando un oggetto texture è associato a una vista risorsa shader, tale shader può successivamente accedere alla texture. Uno dei vantaggi di una visualizzazione risorse è che è possibile interpretare i dati in modi diversi in diverse fasi della pipeline di rendering. Per ulteriori informazioni sulle viste delle risorse, vedere Viste delle Risorse. Quando si utilizza nel contesto di una trasformazione della visuale o di una matrice di trasformazione della visuale, "visuale" si riferisce alla posizione e all'orientamento della fotocamera. Una trasformazione di visualizzazione sposta gli oggetti nel mondo intorno alla posizione e all'orientamento della fotocamera. Per altre informazioni sulle trasformazioni di visualizzazione, vedere View Transform (Direct3D 9). Il modo in cui Marble Maze usa le visualizzazioni delle risorse e delle matrici viene descritto in modo più dettagliato in questo argomento.

 

Caricamento delle risorse della scena

Marble Maze usa la classe BasicLoader, dichiarata in BasicLoader.h, per caricare texture e shader. Marble Maze usa la classe SDKMesh per caricare le mesh 3D per il labirinto e la biglia.

Per garantire un'app reattiva, Marble Maze carica le risorse della scena in modo asincrono o in background. Man mano che gli asset si caricano in background, il gioco può rispondere agli eventi della finestra. In questa guida, questo processo è illustrato in modo più dettagliato in Caricamento degli asset del gioco in background.

Caricamento dell'overlay 2D e dell'interfaccia utente

In Marble Maze la sovrimpressione è l'immagine visualizzata nella parte superiore dello schermo. La sovrimpressione viene sempre visualizzata davanti alla scena. In Marble Maze la sovrimpressione contiene il logo di Windows e la stringa di testo esempio di gioco DirectX Marble Maze. La gestione della sovrimpressione viene eseguita dalla classe SampleOverlay, definita in SampleOverlay.h. Anche se usiamo la sovrapposizione come parte degli esempi Direct3D, puoi adattare questo codice per visualizzare qualsiasi immagine che appare di fronte alla tua scena.

Un aspetto importante della sovrimpressione è che, poiché il suo contenuto non cambia, la classe SampleOverlay disegna o memorizza nella cache il proprio contenuto in un oggetto ID2D1Bitmap1 durante l'inizializzazione. In fase di disegno, la classe SampleOverlay deve disegnare solo la bitmap sullo schermo. In questo modo, non è necessario eseguire routine costose come il disegno di testo per ogni fotogramma.

L'interfaccia utente è costituita da componenti 2D, come i menu e i visori a sovrimpressione (HUD), visualizzati davanti alla scena. Marble Maze definisce gli elementi dell'interfaccia utente seguenti:

  • Voci di menu che consentono all'utente di avviare il gioco o visualizzare punteggi elevati.
  • Timer che fa il conto alla rovescia per tre secondi prima dell'inizio del gioco.
  • Timer che tiene traccia del tempo di gioco trascorso.
  • Tabella che elenca i tempi di fine più rapidi.
  • Testo che indica Sospeso quando il gioco viene sospeso.

Marble Maze definisce elementi dell'interfaccia utente specifici del gioco in UserInterface.h. Marble Maze definisce la classe ElementBase come tipo di base per tutti gli elementi dell'interfaccia utente. La classe ElementBase definisce attributi quali dimensioni, posizione, allineamento e visibilità di un elemento dell'interfaccia utente. Controlla anche il modo in cui gli elementi vengono aggiornati e sottoposti a rendering.

class ElementBase
{
public:
    virtual void Initialize() { }
    virtual void Update(float timeTotal, float timeDelta) { }
    virtual void Render() { }

    void SetAlignment(AlignType horizontal, AlignType vertical);
    virtual void SetContainer(const D2D1_RECT_F& container);
    void SetVisible(bool visible);

    D2D1_RECT_F GetBounds();

    bool IsVisible() const { return m_visible; }

protected:
    ElementBase();

    virtual void CalculateSize() { }

    Alignment       m_alignment;
    D2D1_RECT_F     m_container;
    D2D1_SIZE_F     m_size;
    bool            m_visible;
};

Fornendo una classe base comune per gli elementi dell'interfaccia utente, la classe UserInterface, che gestisce l'interfaccia utente, deve contenere solo una raccolta di oggetti ElementBase, che semplifica la gestione dell'interfaccia utente e fornisce un gestore dell'interfaccia utente riutilizzabile. Marble Maze definisce i tipi che derivano da ElementBase che implementano comportamenti specifici del gioco. Ad esempio, HighScoreTable definisce il comportamento per la tabella dei punteggi elevati. Per altre info su questi tipi, vedere il codice sorgente.

Annotazioni

Poiché XAML consente di creare più facilmente interfacce utente complesse, come quelle disponibili nei giochi di simulazione e strategia, valutare se usare XAML per definire l'interfaccia utente. Per informazioni su come sviluppare un'interfaccia utente in XAML in un gioco UWP DirectX, vedi Estendere l'esempio di gioco, che fa riferimento all'esempio di gioco di tiro 3D DirectX.

 

Caricamento di shader

Marble Maze usa il metodo BasicLoader::LoadShader per caricare uno shader da un file.

Gli shader sono oggi l'unità fondamentale della programmazione GPU nei giochi. Quasi tutte le elaborazioni grafiche 3D vengono guidate tramite shader, sia che si tratti di trasformazione del modello che di illuminazione della scena, o di un'elaborazione geometrica più complessa, dalla skinning dei caratteri alla tassellatura. Per altre informazioni sul modello di programmazione dello shader, vedere HLSL.

Marble Maze usa vertex shader e pixel shader. Un vertex shader opera sempre su un vertice di input e produce un vertice come output. Un pixel shader prende valori numerici, dati di texture, valori interpolati per vertice e altri dati per produrre un colore del pixel come risultato. Poiché uno shader trasforma un elemento alla volta, l'hardware grafico che fornisce più pipeline shader può elaborare set di elementi in parallelo. Il numero di pipeline parallele disponibili per la GPU può essere notevolmente maggiore del numero disponibile per la CPU. Pertanto, anche gli shader di base possono migliorare notevolmente la velocità effettiva.

Il metodo MarbleMazeMain::LoadDeferredResources carica un vertex shader e un pixel shader dopo il caricamento della sovrimpressione. Le versioni in fase di progettazione di questi shader sono definite rispettivamente in BasicVertexShader.hlsl e BasicPixelShader.hlsl. Marble Maze applica questi shader sia alla palla che al labirinto durante la fase di rendering.

Il progetto Marble Maze include le versioni .hlsl (formato di progettazione) e .cso (formato di runtime) dei file shader. Al momento della compilazione, Visual Studio usa il compilatore di effetti fxc.exe per trasformare il tuo file sorgente .hlsl in uno shader binario .cso. Per altre informazioni sullo strumento del compilatore di effetti, vedere Effect-Compiler Tool.

Il vertex shader usa il modello, la visualizzazione e le matrici di proiezione forniti per trasformare la geometria di input. I dati di posizione della geometria di input vengono trasformati e restituiti due volte: una volta nello spazio dello schermo, necessario per il rendering e di nuovo nello spazio globale per consentire al pixel shader di eseguire calcoli di illuminazione. Il vettore normale della superficie viene trasformato nello spazio globale, che viene usato anche dal pixel shader per l'illuminazione. Le coordinate della trama vengono passate senza modifiche al pixel shader.

sPSInput main(sVSInput input)
{
    sPSInput output;
    float4 temp = float4(input.pos, 1.0f);
    temp = mul(temp, model);
    output.worldPos = temp.xyz / temp.w;
    temp = mul(temp, view);
    temp = mul(temp, projection);
    output.pos = temp;
    output.tex = input.tex;
    output.norm = mul(float4(input.norm, 0.0f), model).xyz;
    return output;
}

Il pixel shader riceve come input l'output del vertex shader. Questo shader esegue calcoli di illuminazione per imitare un riflettore a bordi morbidi che sovrasta il labirinto ed è allineato con la posizione della biglia. L'illuminazione è più forte per le superfici che puntano direttamente verso la luce. Il componente diffuso si riduce a zero man mano che la normale della superficie diventa perpendicolare alla luce, e il termine ambientale diminuisce man mano che la normale si allontana dalla luce. I punti più vicini alla biglia (e quindi più vicini al centro del riflettore) sono illuminati più intensamente. Tuttavia, l'illuminazione è modulata per i punti sotto il marmo per simulare un'ombra morbida. In un ambiente reale, un oggetto come il marmo bianco rifletterebbe in maniera diffusa la luce su altri oggetti della scena. Questo è approssimato per le superfici esposte alla parte luminosa del marmo. I fattori di illuminazione aggiuntivi sono in un angolo relativo e a una distanza rispetto al marmo. Il colore del pixel risultante è una composizione della trama campionata con il risultato dei calcoli di illuminazione.

float4 main(sPSInput input) : SV_TARGET
{
    float3 lightDirection = float3(0, 0, -1);
    float3 ambientColor = float3(0.43, 0.31, 0.24);
    float3 lightColor = 1 - ambientColor;
    float spotRadius = 50;

    // Basic ambient (Ka) and diffuse (Kd) lighting from above.
    float3 N = normalize(input.norm);
    float NdotL = dot(N, lightDirection);
    float Ka = saturate(NdotL + 1);
    float Kd = saturate(NdotL);

    // Spotlight.
    float3 vec = input.worldPos - marblePosition;
    float dist2D = sqrt(dot(vec.xy, vec.xy));
    Kd = Kd * saturate(spotRadius / dist2D);

    // Shadowing from ball.
    if (input.worldPos.z > marblePosition.z)
        Kd = Kd * saturate(dist2D / (marbleRadius * 1.5));

    // Diffuse reflection of light off ball.
    float dist3D = sqrt(dot(vec, vec));
    float3 V = normalize(vec);
    Kd += saturate(dot(-V, N)) * saturate(dot(V, lightDirection))
        * saturate(marbleRadius / dist3D);

    // Final composite.
    float4 diffuseTexture = Texture.Sample(Sampler, input.tex);
    float3 color = diffuseTexture.rgb * ((ambientColor * Ka) + (lightColor * Kd));
    return float4(color * lightStrength, diffuseTexture.a);
}

Avvertimento

Il pixel shader compilato contiene 32 istruzioni aritmetiche e 1 istruzione di texture. Questo shader dovrebbe funzionare bene nei computer desktop o nei tablet con prestazioni superiori. Tuttavia, alcuni computer potrebbero non essere in grado di elaborare questo shader e fornire comunque una frequenza dei fotogrammi sufficiente per l'interattività. Prendere in considerazione l'hardware tipico del pubblico di destinazione e progettare gli shader per soddisfare le funzionalità di tale hardware.

 

Il metodo MarbleMazeMain::LoadDeferredResources usa il metodo BasicLoader::LoadShader per caricare gli shader. Nell'esempio seguente viene caricato il vertex shader. Il formato di runtime per questo shader è BasicVertexShader.cso. La variabile membro m_vertexShader è un ID3D11VertexShader oggetto.

BasicLoader^ loader = ref new BasicLoader(m_deviceResources->GetD3DDevice());

D3D11_INPUT_ELEMENT_DESC layoutDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 32, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
m_vertexStride = 44; // must set this to match the size of layoutDesc above

Platform::String^ vertexShaderName = L"BasicVertexShader.cso";
loader->LoadShader(
    vertexShaderName,
    layoutDesc,
    ARRAYSIZE(layoutDesc),
    &m_vertexShader,
    &m_inputLayout
    );

La variabile membro m_inputLayout è un oggetto ID3D11InputLayout. L'oggetto input-layout incapsula lo stato di input della fase dell'assembler di input (IA). Un processo della fase IA consiste nel rendere gli shader più efficienti usando valori generati dal sistema, noti anche come semantica , per elaborare solo le primitive o i vertici che non sono già stati elaborati.

Usare il metodo ID3D11Device::CreateInputLayout per creare un layout di input da una matrice di descrizioni degli elementi di input. L'array contiene uno o più elementi di input; ogni elemento di input descrive un elemento di dati del vertice da un buffer di vertici. L'intero set di descrizioni degli elementi di input descrive tutti gli elementi dei dati dei vertici di tutti i vertex buffer che verranno associati alla fase IA.

layoutDesc nel frammento di codice precedente mostra la descrizione del layout usata da Marble Maze. La descrizione del layout descrive un buffer dei vertici che contiene quattro elementi di dati dei vertici. Le parti importanti di ciascun elemento dell'array sono il nome semantico, il formato dati e l'offset dei byte. Ad esempio, l'elemento POSITION specifica la posizione del vertice nello spazio degli oggetti. Inizia con un offset di byte 0 e contiene tre componenti a virgola mobile, per un totale di 12 byte. L'elemento NORMAL specifica il vettore normale. Inizia all'offset di byte 12 perché viene visualizzato direttamente dopo POSITION nel layout, che richiede 12 byte. L'elemento NORMAL contiene un intero senza segno a quattro componenti a 32 bit.

Confronta il layout di input con la struttura di sVSInput definita dal vertex shader, come illustrato nell'esempio seguente. La struttura di sVSInput definisce gli elementi POSITION, NORMALe TEXCOORD0. Il runtime DirectX effettua il mapping di ogni elemento nel layout alla struttura di input definita dallo shader.

struct sVSInput
{
    float3 pos : POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
};

struct sPSInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
};

sPSInput main(sVSInput input)
{
    sPSInput output;
    float4 temp = float4(input.pos, 1.0f);
    temp = mul(temp, model);
    output.worldPos = temp.xyz / temp.w;
    temp = mul(temp, view);
    temp = mul(temp, projection);
    output.pos = temp;
    output.tex = input.tex;
    output.norm = mul(float4(input.norm, 0.0f), model).xyz;
    return output;
}

Il documento Semantics descrive in modo più dettagliato ciascuna delle semantiche disponibili.

Annotazioni

In un layout è possibile specificare componenti aggiuntivi che non vengono usati per consentire a più shader di condividere lo stesso layout. Ad esempio, l'elemento TANGENT non viene utilizzato dallo shader. È possibile usare l'elemento TANGENT se si desidera sperimentare tecniche come la mappatura normale. Usando il mapping normale, noto anche come mapping d'urto, è possibile creare l'effetto di urto sulle superfici degli oggetti. Per ulteriori informazioni sul bump mapping, vedere Bump Mapping (Direct3D 9).

 

Per ulteriori informazioni sulla fase di input dell'assemblaggio, consultare Input-Assembler Fase e Introduzione alla Fase Input-Assembler.

Il processo di utilizzo dei vertex shader e dei pixel shader per eseguire il rendering della scena è descritto nella sezione Rendering della scena più avanti in questo documento.

Creazione del buffer costante

Il buffer Direct3D raggruppa una raccolta di dati. Un buffer costante è un tipo di buffer che è possibile usare per passare i dati agli shader. Marble Maze usa un buffer costante per contenere la visualizzazione del modello (o del mondo) e le matrici di proiezione per l'oggetto attivo nella scena.

L'esempio seguente mostra come il metodo MarbleMazeMain::LoadDeferredResources crea un buffer costante che conterrà successivamente i dati della matrice. Nell'esempio viene creata una struttura D3D11_BUFFER_DESC che utilizza il flag D3D11_BIND_CONSTANT_BUFFER per specificarne l'uso come buffer costante. Questo esempio passa quindi tale struttura al metodo ID3D11Device::CreateBuffer. La variabile m_constantBuffer è un oggetto ID3D11Buffer.

// Create the constant buffer for updating model and camera data.
D3D11_BUFFER_DESC constantBufferDesc = {0};

// Multiple of 16 bytes
constantBufferDesc.ByteWidth = ((sizeof(ConstantBuffer) + 15) / 16) * 16;

constantBufferDesc.Usage               = D3D11_USAGE_DEFAULT;
constantBufferDesc.BindFlags           = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDesc.CPUAccessFlags      = 0;
constantBufferDesc.MiscFlags           = 0;

// This will not be used as a structured buffer, so this parameter is ignored.
constantBufferDesc.StructureByteStride = 0;

DX::ThrowIfFailed(
    m_deviceResources->GetD3DDevice()->CreateBuffer(
        &constantBufferDesc,
        nullptr,    // leave the buffer uninitialized
        &m_constantBuffer
        )
    );

Il metodo MarbleMazeMain::Update successivamente aggiorna oggetti ConstantBuffer, uno per il labirinto e uno per la biglia. Il metodo MarbleMazeMain::Render associa quindi ogni oggetto ConstantBuffer al buffer costante prima che venga eseguito il rendering di ogni oggetto. L'esempio seguente illustra la struttura ConstantBuffer, che si trova in MarbleMazeMain.h.

// Describes the constant buffer that draws the meshes.
struct ConstantBuffer
{
    XMFLOAT4X4 model;
    XMFLOAT4X4 view;
    XMFLOAT4X4 projection;

    XMFLOAT3 marblePosition;
    float marbleRadius;
    float lightStrength;
};

Per comprendere meglio come i buffer costanti vengono mappati al codice shader, confronta la struttura ConstantBuffer in MarbleMazeMain.h al buffer costante ConstantBuffer definito dal vertex shader in BasicVertexShader.hlsl:

cbuffer ConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
    float3 marblePosition;
    float marbleRadius;
    float lightStrength;
};

Il layout della struttura ConstantBuffer corrisponde all'oggetto cbuffer. La variabile cbuffer specifica il registro b0, il che significa che i dati del buffer costante vengono archiviati nel registro 0. Il metodo MarbleMazeMain::Render specifica il registro 0 quando attiva il buffer costante. Questo processo viene descritto in modo più dettagliato più avanti in questo documento.

Per altre informazioni sui buffer costanti, vedere Introduction to Buffers in Direct3D 11. Per ulteriori informazioni sulla parola chiave "register", vedere la registrazione .

Caricamento maglie

Marble Maze usa SDK-Mesh come formato di runtime perché questo formato offre un modo di base per caricare dati mesh per applicazioni di esempio. Per l'uso in produzione, è consigliabile usare un formato mesh che soddisfi i requisiti specifici del gioco.

Il metodo MarbleMazeMain::LoadDeferredResources carica i dati mesh dopo il caricamento dei vertex shader e dei pixel shader. Una mesh è una raccolta di dati sui vertici che spesso includono informazioni quali posizioni, dati normali, colori, materiali e coordinate di trama. Le mesh vengono in genere create nel software di creazione 3D e gestite in file separati dal codice dell'applicazione. La biglia e il labirinto sono due esempi di mesh usate dal gioco.

Marble Maze utilizza la classe SDKMesh per gestire le mesh. Questa classe viene dichiarata in SDKMesh.h. SDKMesh fornisce metodi per caricare, eseguire il rendering e distruggere i dati mesh.

Importante

Marble Maze usa il formato SDK-Mesh e fornisce la classe SDKMesh solo per l'illustrazione. Anche se il formato SDK-Mesh è utile per l'apprendimento e per la creazione di prototipi, è un formato molto semplice che potrebbe non soddisfare i requisiti della maggior parte dello sviluppo di giochi. Ti consigliamo di usare un formato mesh che soddisfi i requisiti specifici del gioco.

 

L'esempio seguente illustra come il metodo MarbleMazeMain::LoadDeferredResources usa il metodo SDKMesh::Create per caricare i dati mesh per il labirinto e per la palla.

// Load the meshes.
DX::ThrowIfFailed(
    m_mazeMesh.Create(
        m_deviceResources->GetD3DDevice(),
        L"Media\\Models\\maze1.sdkmesh",
        false
        )
    );

DX::ThrowIfFailed(
    m_marbleMesh.Create(
        m_deviceResources->GetD3DDevice(),
        L"Media\\Models\\marble2.sdkmesh",
        false
        )
    );

Caricamento di dati di collisione

Anche se questa sezione non si concentra sul modo in cui Marble Maze implementa la simulazione fisica tra la biglia e il labirinto, si noti che la geometria mesh per il sistema di fisica viene letta quando vengono caricate le mesh.

// Extract mesh geometry for the physics system.
DX::ThrowIfFailed(
    ExtractTrianglesFromMesh(
        m_mazeMesh,
        "Mesh_walls",
        m_collision.m_wallTriList
        )
    );

DX::ThrowIfFailed(
    ExtractTrianglesFromMesh(
        m_mazeMesh,
        "Mesh_Floor",
        m_collision.m_groundTriList
        )
    );

DX::ThrowIfFailed(
    ExtractTrianglesFromMesh(
        m_mazeMesh,
        "Mesh_floorSides",
        m_collision.m_floorTriList
        )
    );

m_physics.SetCollision(&m_collision);
float radius = m_marbleMesh.GetMeshBoundingBoxExtents(0).x / 2;
m_physics.SetRadius(radius);

Il modo in cui si caricano i dati di collisione dipende in gran parte dal formato di runtime usato. Per altre informazioni sul modo in cui Marble Maze carica la geometria di collisione da un file di SDK-Mesh, vedere il metodo MarbleMazeMain::ExtractTrianglesFromMesh nel codice sorgente.

Aggiornamento dello stato del gioco

Marble Maze separa la logica di gioco dalla logica di rendering aggiornando prima tutti gli oggetti della scena prima di eseguire il rendering.

struttura dell'applicazione Marble Maze descrive il ciclo principale del gioco. L'aggiornamento della scena, che fa parte del ciclo del gioco, si verifica dopo l'elaborazione di eventi e input di Windows e prima del rendering della scena. Il metodo MarbleMazeMain::Update gestisce l'aggiornamento dell'interfaccia utente e del gioco.

Aggiornamento dell'interfaccia utente

Il metodo MarbleMazeMain::Update chiama il metodo UserInterface::Update per aggiornare lo stato dell'interfaccia utente.

UserInterface::GetInstance().Update(
    static_cast<float>(m_timer.GetTotalSeconds()), 
    static_cast<float>(m_timer.GetElapsedSeconds()));

Il metodo UserInterface::Update aggiorna ogni elemento nella raccolta dell'interfaccia utente.

void UserInterface::Update(float timeTotal, float timeDelta)
{
    for (auto iter = m_elements.begin(); iter != m_elements.end(); ++iter)
    {
        (*iter)->Update(timeTotal, timeDelta);
    }
}

Le classi che derivano da ElementBase (definite in UserInterface.h) implementano il metodo Update per eseguire comportamenti specifici. Ad esempio, il metodo StopwatchTimer::Update aggiorna il tempo trascorso in base alla quantità specificata e aggiorna il testo visualizzato in un secondo momento.

void StopwatchTimer::Update(float timeTotal, float timeDelta)
{
    if (m_active)
    {
        m_elapsedTime += timeDelta;

        WCHAR buffer[16];
        GetFormattedTime(buffer);
        SetText(buffer);
    }

    TextElement::Update(timeTotal, timeDelta);
}

Aggiornamento della scena

Il metodo MarbleMazeMain::Update aggiorna il gioco in base allo stato corrente della macchina a stati, ovvero il GameState, che è archiviato in m_gameState. Quando il gioco si trova nello stato attivo (GameState::InGameActive), Marble Maze aggiorna la fotocamera per seguire la biglia, modifica la parte relativa alla matrice di visualizzazione dei buffer costanti e aggiorna la simulazione fisica.

L'esempio seguente mostra come il metodo MarbleMazeMain::Update aggiorna la posizione della fotocamera. Marble Maze usa la variabile m_resetCamera per contrassegnare che la fotocamera deve essere reimpostata in modo che si trovi direttamente sopra la biglia. La videocamera viene reimpostata quando il gioco inizia o quando la biglia cade attraverso il labirinto. Quando il menu principale o lo schermo di visualizzazione del punteggio elevato è attivo, la fotocamera viene impostata in una posizione costante. In caso contrario, Marble Maze usa il parametro timeDelta per interpolare la posizione della fotocamera tra le posizioni correnti e di destinazione. La posizione di destinazione è leggermente sopra e di fronte alla biglia. L'uso del tempo trascorso consente alla fotocamera di seguire gradualmente, o inseguire, la biglia.

static float eyeDistance = 200.0f;
static XMFLOAT3A eyePosition = XMFLOAT3A(0, 0, 0);

// Gradually move the camera above the marble.
XMFLOAT3A targetEyePosition;
XMStoreFloat3A(
    &targetEyePosition, 
    XMLoadFloat3A(&marblePosition) - (XMLoadFloat3A(&g) * eyeDistance));

if (m_resetCamera)
{
    eyePosition = targetEyePosition;
    m_resetCamera = false;
}
else
{
    XMStoreFloat3A(
        &eyePosition, 
        XMLoadFloat3A(&eyePosition) 
            + ((XMLoadFloat3A(&targetEyePosition) - XMLoadFloat3A(&eyePosition)) 
                * min(1, static_cast<float>(m_timer.GetElapsedSeconds()) * 8)
            )
    );
}

// Look at the marble. 
if ((m_gameState == GameState::MainMenu) || (m_gameState == GameState::HighScoreDisplay))
{
    // Override camera position for menus.
    XMStoreFloat3A(
        &eyePosition, 
        XMLoadFloat3A(&marblePosition) + XMVectorSet(75.0f, -150.0f, -75.0f, 0.0f));

    m_camera->SetViewParameters(
        eyePosition, 
        marblePosition, 
        XMFLOAT3(0.0f, 0.0f, -1.0f));
}
else
{
    m_camera->SetViewParameters(eyePosition, marblePosition, XMFLOAT3(0.0f, 1.0f, 0.0f));
}

L'esempio seguente mostra come il metodo MarbleMazeMain::Update aggiorna i buffer costanti per la biglia e il labirinto. Il modello o il mondo del labirinto rimane sempre la matrice di identità. Ad eccezione della diagonale principale, i cui elementi sono tutti uni, la matrice identità è una matrice quadrata composta da zeri. La matrice del modello della biglia si basa sulla matrice di posizione moltiplicata per la matrice di rotazione.

// Update the model matrices based on the simulation.
XMStoreFloat4x4(&m_mazeConstantBufferData.model, XMMatrixIdentity());

XMStoreFloat4x4(
    &m_marbleConstantBufferData.model, 
    XMMatrixTranspose(
        XMMatrixMultiply(
            marbleRotationMatrix, 
            XMMatrixTranslationFromVector(XMLoadFloat3A(&marblePosition))
        )
    )
);

// Update the view matrix based on the camera.
XMFLOAT4X4 view;
m_camera->GetViewMatrix(&view);
m_mazeConstantBufferData.view = view;
m_marbleConstantBufferData.view = view;

Per informazioni su come il metodo MarbleMazeMain::Update elabora l'input dell'utente e simula il movimento della sfera, consultare Aggiunta di input e interattività all'esempio di Marble Maze.

Rendering della scena

Quando viene eseguito il rendering di una scena, questi passaggi vengono in genere inclusi.

  1. Impostare il buffer depth-stencil dell'attuale destinazione di rendering.
  2. Cancellare le visualizzazioni di rendering e stencil.
  3. Preparare i vertex e i pixel shader per il disegno.
  4. Renderizzare gli oggetti 3D nella scena.
  5. Eseguire il rendering di qualsiasi oggetto 2D che si desidera visualizzare davanti alla scena.
  6. Presentare l'immagine di cui è stato eseguito il rendering al monitor.

Il metodo MarbleMazeMain::Render associa i target di rendering e le view di stencil di profondità, cancella tali view, disegna la scena e quindi disegna l'overlay.

Preparazione dei target di rendering

Prima di eseguire il rendering della scena, è necessario impostare il buffer depth-stencil di destinazione di rendering corrente. Se non è garantito che la scena disegni su ogni pixel sullo schermo, è necessario cancellare anche le viste di rendering e stencil. Marble Maze cancella le visualizzazioni di rendering e stencil in ogni fotogramma per assicurarsi che non siano presenti artefatti visibili dal frame precedente.

L'esempio seguente illustra come il metodo MarbleMazeMain::Render chiama il metodo ID3D11DeviceContext::OMSetRenderTargets per impostare la destinazione di rendering e il buffer depth-stencil come quelli correnti.

auto context = m_deviceResources->GetD3DDeviceContext();

// Reset the viewport to target the whole screen.
auto viewport = m_deviceResources->GetScreenViewport();
context->RSSetViewports(1, &viewport);

// Reset render targets to the screen.
ID3D11RenderTargetView *const targets[1] = 
    { m_deviceResources->GetBackBufferRenderTargetView() };

context->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());

// Clear the back buffer and depth stencil view.
context->ClearRenderTargetView(
    m_deviceResources->GetBackBufferRenderTargetView(), 
    DirectX::Colors::Black);

context->ClearDepthStencilView(
    m_deviceResources->GetDepthStencilView(), 
    D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 
    1.0f, 
    0);

Le interfacce ID3D11RenderTargetView e ID3D11DepthStencilView supportano il meccanismo di visualizzazione trama fornito da Direct3D 10 e versioni successive. Per ulteriori informazioni sulle visualizzazioni delle texture, vedere Visualizzazioni delle Texture (Direct3D 10). Il metodo OMSetRenderTargets prepara la fase di unione dell'output della pipeline Direct3D. Per ulteriori informazioni sulla fase di unione dell'output, vedi Output-Merger Fase.

Preparazione dei vertex shader e dei pixel shader

Prima di eseguire il rendering degli oggetti scena, seguire questa procedura per preparare i vertex shader e i pixel shader per il disegno:

  1. Impostare il layout di input dello shader come layout corrente.
  2. Imposta i vertex shader e i pixel shader come shader attuali.
  3. Aggiornare eventuali buffer costanti con i dati che è necessario passare agli shader.

Importante

Marble Maze usa una coppia di vertex shader e pixel shader per tutti gli oggetti 3D. Se il gioco usa più di una coppia di shader, devi eseguire questi passaggi ogni volta che disegnare oggetti che usano shader diversi. Per ridurre l'overhead associato alla modifica dello stato dello shader, è consigliabile raggruppare le chiamate di rendering per tutti gli oggetti che usano gli stessi shader.

 

La sezione Caricamento degli shader in questo documento descrive come viene creato il layout di input quando viene creato il vertex shader. L'esempio seguente illustra come il metodo MarbleMazeMain::Render usa il metodo ID3D11DeviceContext::IASetInputLayout per impostare questo layout come layout corrente.

m_deviceResources->GetD3DDeviceContext()->IASetInputLayout(m_inputLayout.Get());

L'esempio seguente mostra come il metodo MarbleMazeMain::Render usa i metodi ID3D11DeviceContext::VSSetShader e ID3D11DeviceContext::PSSetShader per impostare rispettivamente gli shader di vertice e gli shader di pixel come shader correnti.

// Set the vertex shader stage state.
m_deviceResources->GetD3DDeviceContext()->VSSetShader(
    m_vertexShader.Get(),   // use this vertex shader
    nullptr,                // don't use shader linkage
    0);                     // don't use shader linkage

m_deviceResources->GetD3DDeviceContext()->PSSetShader(
    m_pixelShader.Get(),    // use this pixel shader
    nullptr,                // don't use shader linkage
    0);                     // don't use shader linkage

m_deviceResources->GetD3DDeviceContext()->PSSetSamplers(
    0,                          // starting at the first sampler slot
    1,                          // set one sampler binding
    m_sampler.GetAddressOf());  // to use this sampler

Dopo che MarbleMazeMain::Render imposta gli shader e il relativo layout di input, usa il metodo ID3D11DeviceContext::UpdateSubresource per aggiornare il buffer costante con il modello, la vista e la matrice di proiezione per il labirinto. Il metodo UpdateSubresource copia i dati della matrice dalla memoria CPU alla memoria GPU. Ricorda che i componenti della visualizzazione e del modello della struttura ConstantBuffer vengono aggiornati nel metodo MarbleMazeMain::Update. Il metodo MarbleMazeMain::Render chiama quindi il metodo ID3D11DeviceContext::VSSetConstantBuffers e ID3D11DeviceContext::PSSetConstantBuffers per impostare questo buffer costante come quello corrente.

// Update the constant buffer with the new data.
m_deviceResources->GetD3DDeviceContext()->UpdateSubresource(
    m_constantBuffer.Get(),
    0,
    nullptr,
    &m_mazeConstantBufferData,
    0,
    0);

m_deviceResources->GetD3DDeviceContext()->VSSetConstantBuffers(
    0,                                  // starting at the first constant buffer slot
    1,                                  // set one constant buffer binding
    m_constantBuffer.GetAddressOf());   // to use this buffer

m_deviceResources->GetD3DDeviceContext()->PSSetConstantBuffers(
    0,                                  // starting at the first constant buffer slot
    1,                                  // set one constant buffer binding
    m_constantBuffer.GetAddressOf());   // to use this buffer

Il metodo MarbleMazeMain::Render esegue passaggi simili per preparare il rendering della biglia.

Rendering del labirinto e della biglia

Dopo aver attivato gli shader correnti, è possibile disegnare gli oggetti della scena. Il metodo MarbleMazeMain::Render chiama il metodo SDKMesh::Render per eseguire il rendering della mesh del labirinto.

m_mazeMesh.Render(
    m_deviceResources->GetD3DDeviceContext(), 
    0, 
    INVALID_SAMPLER_SLOT, 
    INVALID_SAMPLER_SLOT);

Il metodo MarbleMazeMain::Render esegue passaggi simili per eseguire il rendering della biglia.

Come accennato in precedenza in questo documento, la classe SDKMesh viene fornita a scopo dimostrativo, ma non è consigliabile usarla in un gioco di qualità di produzione. Si noti tuttavia che il metodo SDKMesh::RenderMesh, chiamato da SDKMesh::Render, usa i metodi ID3D11DeviceContext::IASetVertexBuffers e ID3D11DeviceContext::IASetIndexBuffer per impostare i buffer dei vertici e degli indici correnti che definiscono la mesh, e il metodo ID3D11DeviceContext::DrawIndexed per disegnare i buffer. Per ulteriori informazioni su come lavorare con i buffer di vertici e indici, vedere Introduction to Buffers in Direct3D 11.

Disegno dell'interfaccia utente e sovrapposizione

Dopo aver disegnato gli oggetti della scena 3D, Marble Maze disegna gli elementi 2D dell'interfaccia utente che appaiono davanti alla scena.

Il metodo MarbleMazeMain::Render termina disegnando l'interfaccia utente e la sovrimpressione.

// Draw the user interface and the overlay.
UserInterface::GetInstance().Render(m_deviceResources->GetOrientationTransform2D());

m_deviceResources->GetD3DDeviceContext()->BeginEventInt(L"Render Overlay", 0);
m_sampleOverlay->Render();
m_deviceResources->GetD3DDeviceContext()->EndEvent();

Il metodo UserInterface::Render usa un oggetto ID2D1DeviceContext per disegnare gli elementi dell'interfaccia utente. Questo metodo imposta lo stato del disegno, disegna tutti gli elementi dell'interfaccia utente attivi e quindi ripristina lo stato precedente del disegno.

void UserInterface::Render(D2D1::Matrix3x2F orientation2D)
{
    m_d2dContext->SaveDrawingState(m_stateBlock.Get());
    m_d2dContext->BeginDraw();
    m_d2dContext->SetTransform(orientation2D);

    m_d2dContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);

    for (auto iter = m_elements.begin(); iter != m_elements.end(); ++iter)
    {
        if ((*iter)->IsVisible())
            (*iter)->Render();
    }

    // We ignore D2DERR_RECREATE_TARGET here. This error indicates that the device
    // is lost. It will be handled during the next call to Present.
    HRESULT hr = m_d2dContext->EndDraw();
    if (hr != D2DERR_RECREATE_TARGET)
    {
        DX::ThrowIfFailed(hr);
    }

    m_d2dContext->RestoreDrawingState(m_stateBlock.Get());
}

Il metodo SampleOverlay::Render usa una tecnica simile per disegnare la bitmap di sovrimpressione.

Presentazione della scena

Dopo aver disegnato tutti gli oggetti della scena 2D e 3D, Marble Maze presenta l'immagine sottoposta a rendering al monitor. Sincronizza il disegno con lo spazio vuoto verticale per garantire che il tempo non venga impiegato per disegnare fotogrammi che non verranno mai effettivamente visualizzati sullo schermo. Marble Maze gestisce anche le modifiche dei dispositivi quando presenta la scena.

Al termine del metodo MarbleMazeMain::Render, il ciclo di gioco chiama il metodo DX::DeviceResources::Present per inviare l'immagine sottoposta a rendering al monitor o al display. Il metodo DX::DeviceResources::Present chiama IDXGISwapChain::Present per eseguire l'operazione di presentazione, come illustrato nell'esempio seguente:

// The first argument instructs DXGI to block until VSync, putting the application
// to sleep until the next VSync. This ensures we don't waste any cycles rendering
// frames that will never be displayed to the screen.
HRESULT hr = m_swapChain->Present(1, 0);

In questo esempio, m_swapChain è un oggetto IDXGISwapChain1. L'inizializzazione di questo oggetto è descritta nella sezione Inizializzazione di Direct3D e direct2D in questo documento.

Il primo parametro per IDXGISwapChain::P resent, SyncInterval, specifica il numero di spazi vuoti verticali da attendere prima di presentare il frame. Marble Maze specifica 1 in modo che attenda fino al successivo vuoto verticale.

Il metodo IDXGISwapChain::Present restituisce un codice di errore che indica che il dispositivo è stato rimosso o ha incontrato un altro tipo di errore. In questo caso Marble Maze reinizializza il dispositivo.

// If the device was removed either by a disconnection or a driver upgrade, we
// must recreate all device resources.
if (hr == DXGI_ERROR_DEVICE_REMOVED)
{
    HandleDeviceLost();
}
else
{
    DX::ThrowIfFailed(hr);
}

Passaggi successivi

Leggere L'aggiunta di input e interattività all'esempio Marble Maze per informazioni su alcune delle procedure principali da tenere presenti quando si lavora con i dispositivi di input. Questo documento illustra in che modo Marble Maze supporta il tocco, l'accelerometro, i controller di gioco e l'input del mouse.