Condividi tramite


Aggiunta di contenuto visivo all'esempio Marble Maze

Il presente documento descrive come il gioco Marble Maze utilizzi Direct3D e Direct2D nell'ambiente dell'app Universal Windows Platform (UWP) in modo da poter apprendere i modelli e adattarli quando si lavora con contenuti di gioco personalizzati. Per scoprire come i componenti del gioco visivo si inseriscono nella struttura complessiva dell'applicazione di Marble Maze, consulta 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. Usare 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 e visualizzati correttamente nel gioco.
  4. Integrare shader di vertice e di pixel che migliorano la qualità visiva delle risorse di gioco.
  5. Integrare la logica del gioco, come l'animazione e l'input dell'utente.

Ci siamo inoltre concentrati prima sull'aggiunta di asset 3D e poi sui 2D. Ad esempio, ci siamo concentrati sulla logica fondamentale 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 abbiamo apportato modifiche ai modelli mesh e marble, abbiamo dovuto modificare anche parte del codice shader che supporta tali modelli.

Nota

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

  Ecco alcuni dei punti chiave trattati in questo documento quando lavori con DirectX e contenuti visivi di giochi, vale a dire quando inizializzi le librerie grafiche DirectX, carichi le risorse della scena, aggiorni ed esegui il rendering della scena:

  • In genere, l'aggiunta di contenuti di gioco, comporta molti passaggi. Anche questi passaggi spesso richiedono iterazione. Gli sviluppatori di giochi spesso si concentrano prima sull'aggiunta di contenuti di gioco 3D e poi sull'aggiunta di contenuti 2D.
  • Raggiungi più clienti e offri a tutti un'esperienza eccezionale supportando la più ampia gamma possibile di hardware grafico.
  • Separa in modo netto i formati in fase di progettazione e in fase di esecuzione. Struttura le tue risorse in fase di progettazione per massimizzare la flessibilità e consentire iterazioni rapide sui contenuti. Formatta e comprimi le tue risorse per caricarle ed eseguirne il rendering nel modo più efficiente possibile in fase di esecuzione.
  • Puoi creare i dispositivi Direct3D e Direct2D in un'app UWP in modo molto simile a quanto fai in un'app desktop Windows classica. Una differenza importante è il modo in cui la catena di scambio è associata alla finestra di output.
  • Quando progetti il gioco, assicurati che il formato mesh scelto supporti gli scenari chiave. Ad esempio, se il tuo gioco richiede collisioni, assicurati di poter ottenere i dati sulle collisioni dalle mesh.
  • Separa la logica del gioco da quella del rendering aggiornando tutti gli oggetti della scena prima di eseguirne il rendering.
  • In genere si disegnano gli oggetti della scena 3D e quindi tutti gli oggetti 2D che appaiono davanti alla scena.
  • Sincronizza il disegno con lo spazio vuoto verticale per garantire che il gioco non perda tempo a disegnare fotogrammi che non verranno mai effettivamente visualizzati sul display. Uno spazio vuoto verticale è il tempo che intercorre tra il momento in cui un fotogramma termina di essere visualizzato sul monitor e l'inizio del fotogramma successivo.

Introduzione alla grafica DirectX

Quando abbiamo pianificato il gioco Marble Maze UWP (Universal Windows Platform), abbiamo scelto C++ e Direct3D 11.1 perché sono scelte eccellenti per la creazione di 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ò aiutarti a raggiungere più clienti in modo più efficiente perché non devi riscrivere il codice per ciascuna delle versioni precedenti di DirectX.

Marble Maze utilizza Direct3D 11.1 per eseguire il rendering degli asset di gioco 3D, vale a dire la biglia e il labirinto. Marble Maze utilizza anche Direct2D, DirectWrite e Windows Imaging Component (WIC) per disegnare gli asset di gioco 2D, come i menu e il timer.

Lo sviluppo di giochi richiede pianificazione. Se non hai esperienza con la grafica DirectX, ti consigliamo di leggere DirectX: come iniziare per acquisire familiarità con i concetti di base della creazione di un gioco UWP DirectX. Mentre leggi questo documento e lavori sul codice sorgente di Marble Maze, puoi fare riferimento alle risorse che seguono per informazioni più approfondite sulla grafica DirectX:

  • Grafica Direct3D 11: descrive Direct3D 11, una potente API grafica 3D con accelerazione hardware per il rendering della geometria 3D sulla piattaforma Windows.
  • Direct2D: descrive Direct2D, un'API grafica 2D con accelerazione hardware che fornisce prestazioni elevate e rendering di alta qualità per geometria 2D, bitmap e testo.
  • DirectWrite: descrive DirectWrite, che supporta il rendering del testo di alta qualità.
  • Componente di imaging di Windows: descrive WIC, una piattaforma estensibile che fornisce API di basso livello per immagini digitali.

Livelli di funzionalità

Direct3D 11 introduce un paradigma denominato livelli di funzionalità. Un livello di funzionalità è un insieme ben definito di funzionalità GPU. Utilizza i livelli di funzionalità per far sì che il gioco venga eseguito su 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ù ampia gamma di hardware possibile e di ridimensionare i contenuti di gioco in modo che i tuoi clienti che dispongono di computer di fascia alta o di fascia bassa possano tutti godere di un'ottima esperienza. Per ulteriori informazioni sui livelli di funzionalità, consulta Direct3D 11 su hardware di livello inferiore.

Inizializzazione di Direct3D e Direct2D

Il dispositivo rappresenta la scheda video. Puoi creare i dispositivi Direct3D e Direct2D in un'app UWP in modo molto simile a quanto fai in un'app desktop Windows classica. La differenza principale è il modo in cui colleghi la catena di scambio Direct3D al sistema a finestre.

La classe DeviceResources è la base per la gestione di Direct3D e Direct2D. Questa classe gestisce l'infrastruttura generale, non le risorse specifiche del gioco. Marble Maze definisce la classe MarbleMazeMain per gestire risorse specifiche del gioco, che ha un riferimento a un oggetto DeviceResources per dargli 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 classe DeviceResources separa questa funzionalità in modo che possa rispondere più facilmente quando l'ambiente cambia. Ad esempio, chiama il metodo CreateWindowSizeDependentResources quando cambia la dimensione della finestra.

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 il punto di partenza per la creazione di risorse grafiche. Marble Maze specifica D2D1_FACTORY_TYPE_SINGLE_THREADED perché esegue tutti i disegni 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 definisce D3D11CreateDevice per creare l'oggetto dispositivo che rappresenta la scheda video 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 nell'array featureLevels. Direct3D esamina l'elenco in ordine e fornisce all'app il primo livello di funzionalità disponibile. Pertanto le voci dell'array D3D_FEATURE_LEVEL sono elencate dal più alto al più basso in modo che l'app ottenga il livello di funzionalità più alto disponibile. Il metodo DeviceResources::CreateDeviceResources ottiene il dispositivo Direct3D 11.1 eseguendo 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 crea quindi il dispositivo Direct2D. Direct2D utilizza l'infrastruttura grafica Microsoft DirectX (DXGI) per interagire con Direct3D. DXGI consente la condivisione delle superfici di memoria video tra i runtime grafici. Marble Maze utilizza il dispositivo DXGI sottostante dal dispositivo Direct3D per creare il dispositivo Direct2D dalla factory 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 ulteriori informazioni su DXGI e sull'interoperabilità tra Direct2D e Direct3D, consulta Panoramica di DXGI e Panoramica dell'interoperabilità Direct2D e Direct3D.

Associazione di Direct3D alla vista

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. L'aspetto importante che distingue un'app UWP DirectX da un'app desktop è il modo in cui la catena di scambio è associata alla finestra di output. La catena di scambio è responsabile della visualizzazione del buffer di cui il dispositivo esegue il rendering sul monitor. La struttura dell'applicazione Marble Maze descrive la differenza tra il sistema a finestre di un'app UWP e desktop. Poiché un'app UWP non funziona con oggetti HWND, Marble Maze deve utilizzare il metodo IDXGIFactory2::CreateSwapChainForCoreWindow per associare l'output del dispositivo alla vista. L'esempio seguente mostra 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 energetico, operazione importante su dispositivi alimentati a batteria come laptop e tablet, il metodo DeviceResources::CreateWindowSizeDependentResources chiama il metodo IDXGIDevice1::SetMaximumFrameLatency per garantire che il rendering del gioco venga eseguito solo dopo lo spazio vuoto verticale. La sincronizzazione con lo spazio vuoto verticale è descritta in maggiore dettaglio 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 un modo che funziona per la maggior parte dei giochi.

Nota

Il termine vista ha un significato diverso in Windows Runtime rispetto a Direct3D. In Windows Runtime, una vista si riferisce alla raccolta di impostazioni dell'interfaccia utente per un'app, inclusi l'area di visualizzazione e i comportamenti di input, oltre al thread utilizzato per l'elaborazione. Puoi specificare la configurazione e le impostazioni necessarie quando crei una vista. Il processo di impostazione della visualizzazione dell'app è descritto in struttura dell'applicazione Marble Maze. In Direct3D, il termine vista ha molteplici significati. Una vista delle risorse definisce le risorse secondarie a cui una risorsa può accedere. Ad esempio, quando un oggetto texture è associato a una vista delle risorse dello shader, quest'ultimo può successivamente accedere alla texture. Un vantaggio della visualizzazione delle risorse è che puoi interpretare i dati in modi diversi in fasi diverse della pipeline di rendering. Per ulteriori informazioni sulle visualizzazioni delle risorse, consulta Visualizzazioni delle risorse. Se utilizzato nel contesto di una trasformazione della vista o di una matrice di trasformazione della vista, la suddetta si riferisce alla posizione e all'orientamento della telecamera. La trasformazione della vista riposiziona gli oggetti nel mondo attorno alla posizione e all'orientamento della telecamera. Per ulteriori informazioni sulle trasformazioni della vista, vedere Trasformazione della vista (Direct3D 9). Il modo in cui Marble Maze utilizza le visualizzazioni delle risorse e della matrice è descritto in maggior dettaglio in questo argomento.

 

Caricamento delle risorse della scena

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

Per garantire un'app reattiva, Marble Maze carica le risorse della scena in modo asincrono o in background. Man mano che le risorse vengono caricate in background, il gioco può rispondere agli eventi della finestra. Questo processo è spiegato più dettagliatamente nella sezione Caricamento delle risorse di gioco in background di questa guida.

Caricamento della sovrapposizione 2D e dell'interfaccia utente

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

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

L'interfaccia utente (UI) è costituita da componenti 2D, come menu e head-up display (HUD), che appaiono 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 i punteggi più alti.
  • Un timer che esegue il conto alla rovescia di tre secondi prima dell'inizio del gioco.
  • Un timer che tiene traccia del tempo di gioco trascorso.
  • Una tabella che elenca i tempi di arrivo più veloci.
  • Testo che dice In pausa quando il gioco è in pausa.

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

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 tipi che derivano da ElementBase che implementano comportamenti specifici del gioco. Ad esempio, HighScoreTable definisce il comportamento della tabella dei punteggi più alti. Per ulteriori informazioni su questi tipi, fai riferimento al codice sorgente.

Nota

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

 

Caricamento di shader

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

Gli shader sono l'unità fondamentale della programmazione GPU nei giochi odierni. Quasi tutta l'elaborazione grafica 3D avviene tramite shader, che si tratti di trasformazione del modello e illuminazione della scena o di elaborazione di geometrie più complesse, dal rivestimento dei personaggi alla tassellazione. Per ulteriori informazioni sul modello di programmazione degli shader, consulta HLSL.

Marble Maze utilizza shader di vertice e di pixel. Uno shader di vertice opera sempre su un vertice di input e produce un vertice come output. Lo shader di pixel accetta valori numerici, dati di texture, valori interpolati per vertice e altri dati per produrre un colore pixel come output. Poiché uno shader trasforma un elemento alla volta, l'hardware grafico che fornisce più pipeline di 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 uno shader di vertice e uno shader di pixel dopo aver caricato la sovrapposizione. Le versioni in fase di progettazione di questi shader sono definite in BasicVertexShader.hlsl e BasicPixelShader.hlsl, rispettivamente. Marble Maze applica questi shader sia alla biglia sia al labirinto durante la fase di rendering.

Il progetto Marble Maze include sia la versione .hlsl (il formato in fase di progettazione) sia la versione .cso (il formato in fase di esecuzione) dei file shader. In fase di compilazione, Visual Studio usa il compilatore di effetti fxc.exe per compilare il file sorgente .hlsl in uno shader binario .cso. Per ulteriori informazioni sullo strumento di compilazione degli effetti, consulta Strumento di compilazione degli effetti.

Lo shader di vertice utilizza il modello, la vista e le matrici di proiezione forniti per trasformare la geometria di input. I dati di posizione dalla 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 allo shader di pixel di eseguire calcoli di illuminazione. Il vettore normale alla superficie viene trasformato nello spazio globale, che viene utilizzato anche dallo shader di pixel per l'illuminazione. Le coordinate della texture vengono passate invariate allo shader di pixel.

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;
}

Lo shader di pixel riceve l'output dello shader di vertice come input. Questo shader esegue calcoli di illuminazione per imitare un faretto dai bordi morbidi che si libra sopra il labirinto ed è allineato con la posizione della biglia. L'illuminazione è più forte per le superfici che puntano direttamente verso la luce. La componente diffusa si assottiglia fino a zero quando la normale alla superficie diventa perpendicolare alla luce, e il termine ambientale diminuisce quando la normale punta lontano dalla luce. I punti più vicini alla biglia (e quindi più vicini al centro del punto luminoso) vengono illuminati con maggiore intensità. Tuttavia, l'illuminazione è modulata nei punti sotto la biglia per simulare un'ombra leggera. In un ambiente reale, un oggetto come la biglia bianca rifletterebbe diffusamente i punti luce sugli altri oggetti nella scena. Questo è approssimato per le superfici che sono in vista della metà lucida della biglia. I fattori di illuminazione aggiuntivi sono nell'angolo e distanza relativi della biglia. 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);
}

Avviso

Lo shader di pixel compilato contiene 32 istruzioni aritmetiche e 1 istruzione texture. Questo shader dovrebbe funzionare bene su computer desktop o tablet con prestazioni più elevate. Tuttavia, alcuni computer potrebbero non essere in grado di elaborare questo shader e fornire comunque una frequenza fotogrammi interattiva. Considera l'hardware tipico del tuo pubblico target e progetta i tuoi shader per soddisfare le capacità di quell'hardware.

 

Il metodo MarbleMazeMain::LoadDeferredResources utilizza il metodo BasicLoader::LoadShader per caricare gli shader. L'esempio seguente carica lo shader di vertice. Il formato di runtime per questo shader è BasicVertexShader.cso. La variabile membro m_vertexShader è un oggetto ID3D11VertexShader.

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 input assembler (IA). Uno dei compiti della fase IA è rendere gli shader più efficienti utilizzando valori generati dal sistema, operazione nota anche come semantica, per elaborare solo quelle primitive o vertici che non sono già state elaborate.

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

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

Confronta il layout di input con la struttura sVSInput definita dallo shader di vertice, come mostrato nell'esempio seguente. La struttura sVSInput definisce gli elementi POSITION, NORMAL e TEXCOORD0. Il runtime DirectX associa 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 Semantica descrive ciascuna delle semantiche disponibili in maggiore dettaglio.

Nota

In un layout è possibile specificare componenti aggiuntivi che non vengono utilizzati per consentire a più shader di condividere lo stesso layout. Ad esempio, l'elemento TANGENT non viene utilizzato dallo shader. Puoi utilizzare l'elemento TANGENT se vuoi sperimentare tecniche come la mappatura normale. Utilizzando la mappatura normale, nota anche come mappatura dei rilievi, è possibile creare l'effetto dei rilievi sulle superfici degli oggetti. Per ulteriori informazioni sulla mappatura dei rilievi, consulta Mappatura dei rilievi (Direct3D 9).

 

Per ulteriori informazioni sulla fase di assemblaggio dell'input, consulta Fase Input-Assembler e Nozioni di base sulla fase Input-Assembler.

Il processo di utilizzo degli shader di vertice e di pixel 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 puoi utilizzare per passare i dati agli shader. Marble Maze utilizza un buffer costante per contenere la vista del modello (o del mondo) e le matrici di proiezione per l'oggetto della scena attiva.

L'esempio seguente mostra come il metodo MarbleMazeMain::LoadDeferredResources crea un buffer costante che successivamente conterrà i dati della matrice. Nell'esempio viene creata una struttura D3D11_BUFFER_DESC che utilizza il flag D3D11_BIND_CONSTANT_BUFFER per specificare l'utilizzo come buffer costante. In questo esempio viene quindi passata la 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 aggiorna successivamente gli oggetti ConstantBuffer, uno per il labirinto e uno per la biglia. Il metodo MarbleMazeMain::Render associa quindi ciascun oggetto ConstantBuffer al buffer costante prima che venga eseguito il rendering di ciascun oggetto. L'esempio seguente mostra 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 il modo in cui i buffer costanti vengono mappati al codice dello shader, confronta la struttura ConstantBuffer in MarbleMazeMain.h con il buffer costante ConstantBuffer definito dallo shader di vertice 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 sono memorizzati nel registro 0. Il metodo MarbleMazeMain::Render specifica il registro 0 quando attiva il buffer costante. Questo processo è descritto più dettagliatamente più avanti in questo documento.

Per ulteriori informazioni sui buffer costanti, consulta Introduzione ai buffer in Direct3D 11. Per ulteriori informazioni sulla parola chiave registro, consulta registro.

Caricamento delle mesh

Marble Maze utilizza SDK-Mesh come formato di runtime perché questo formato fornisce un modo semplice per caricare i dati mesh per le applicazioni di esempio. Per l'utilizzo in produzione, dovresti utilizzare un formato mesh che soddisfi i requisiti specifici del gioco.

Il metodo MarbleMazeMain::LoadDeferredResources carica i dati mesh dopo aver caricato gli shader di vertice e di pixel. Una mesh è una raccolta di dati sui vertici che spesso include informazioni quali posizioni, dati normali, colori, materiali e coordinate della trama. Le mesh vengono generalmente create nel software di creazione 3D e mantenute in file separati dal codice dell'applicazione. La biglia e il labirinto sono due esempi di maglie utilizzate nel gioco.

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

Importante

Marble Maze utilizza il formato SDK-Mesh e fornisce la classe SDKMesh solo a scopo illustrativo. Sebbene il formato SDK-Mesh sia utile per l'apprendimento e per la creazione di prototipi, è un formato molto semplice che potrebbe non soddisfare i requisiti di sviluppo della maggior parte di giochi. Ti consigliamo di utilizzare un formato mesh che soddisfi i requisiti specifici del gioco.

 

L'esempio seguente mostra come il metodo MarbleMazeMain::LoadDeferredResources utilizza il metodo SDKMesh::Create per caricare i dati mesh per il labirinto e per la biglia.

// 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 dei dati sulle collisioni

Sebbene questa sezione non si concentri su come Marble Maze implementa la simulazione fisica tra la biglia e il labirinto, si noti che la geometria della mesh per il sistema fisico viene letta quando le suddette vengono caricate.

// 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 carichi i dati sulle collisioni dipende in gran parte dal formato runtime utilizzato. Per ulteriori informazioni su come Marble Maze carica la geometria di collisione da un file SDK-Mesh, consulta il metodo MarbleMazeMain::ExtractTrianglesFromMesh nel codice sorgente.

Aggiornamento dello stato del gioco

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

La struttura dell'applicazione Marble Maze descrive il ciclo principale del gioco. L'aggiornamento della scena, che fa parte del ciclo di gioco, avviene dopo l'elaborazione degli eventi e degli 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à fornita e aggiorna il testo che verrà visualizzato successivamente.

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 (GameState, archiviato in m_gameState). Quando il gioco è nello stato attivo (GameState::InGameActive), Marble Maze aggiorna la telecamera per seguire la biglia, la parte della matrice di visualizzazione dei buffer costanti e la simulazione fisica.

L'esempio seguente mostra come il metodo MarbleMazeMain::Update aggiorna la posizione della telecamera. Marble Maze utilizza la variabile m_resetCamera per segnalare che la telecamera deve essere ripristinata per essere posizionata direttamente sopra la biglia. La telecamera viene ripristinata quando inizia il gioco o quando la biglia cade nel labirinto. Quando il menu principale o la schermata di visualizzazione del punteggio più alto sono attivi, la telecamera viene impostata su una posizione costante. Altrimenti, Marble Maze utilizza il parametro timeDelta per interpolare la posizione della telecamera tra la sua posizione corrente e quella di destinazione. La posizione target è leggermente sopra e davanti alla biglia. L'utilizzo del tempo del fotogramma trascorso consente alla telecamera di seguire, o inseguire, gradualmente 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. La matrice del modello del labirinto, o del mondo, rimane sempre la matrice identità. Fatta eccezione per la diagonale principale, i cui elementi sono tutti uno, 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 legge l'input dell'utente e simula il movimento della biglia, consulta Aggiunta di input e interattività all'esempio Marble Maze.

Rendering della scena

Quando viene eseguito il rendering di una scena, questi passaggi sono generalmente inclusi.

  1. Imposta il buffer dello stencil di profondità della destinazione di rendering corrente.
  2. Cancella le viste di rendering e stencil.
  3. Prepara gli shader di vertice e di pixel per il disegno.
  4. Esegui il rendering degli oggetti 3D nella scena.
  5. Esegui il rendering di qualsiasi oggetto 2D che desideri appaia davanti alla scena.
  6. Presenta l'immagine renderizzata al monitor.

Il metodo MarbleMazeMain::Render associa le viste della destinazione di rendering e dello stencil di profondità, cancella tali viste, disegna la scena e quindi disegna la sovrapposizione.

Preparazione delle destinazioni di rendering

Prima di eseguire il rendering della scena, è necessario impostare il buffer dello stencil di profondità del target di rendering corrente. Se non è garantito che la scena venga disegnata su ogni pixel dello schermo, cancella anche le viste rendering e stencil. Marble Maze cancella le viste di rendering e stencil su ogni fotogramma per garantire che non siano presenti artefatti visibili dal fotogramma precedente.

L'esempio seguente mostra come il metodo MarbleMazeMain::Render chiama il metodo ID3D11DeviceContext::OMSetRenderTargets per impostare la destinazione di rendering e il buffer dello stencil di profondità 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 della trama fornito da Direct3D 10 e versioni successive. Per ulteriori informazioni sulle visualizzazioni delle texture, consulta Viste delle texture (Direct3D 10). Il metodo OMSetRenderTargets prepara la fase di fusione dell'output della pipeline Direct3D. Per ulteriori informazioni sulla fase di unione dell'output, consulta Fase di unione dell'output.

Preparazione dei vertex e dei pixel shader

Prima di eseguire il rendering degli oggetti della scena, eseguire i seguenti passaggi per preparare gli shader di vertice e di pixel per il disegno:

  1. Imposta il layout di input dello shader come layout corrente.
  2. Imposta gli shader di vertice e di pixel come shader correnti.
  3. Aggiorna eventuali buffer costanti con i dati che devi passare agli shader.

Importante

Marble Maze utilizza una coppia di shader di vertice e di pixel per tutti gli oggetti 3D. Se il tuo gioco utilizza più di una coppia di shader, devi eseguire questi passaggi ogni volta che disegni oggetti che utilizzano shader diversi. Per ridurre il sovraccarico associato alla modifica dello stato dello shader, ti consigliamo di raggruppare le chiamate di rendering per tutti gli oggetti che utilizzano gli stessi shader.

 

La sezione Caricamento degli shader in questo documento descrive come viene creato il layout di input quando viene creato lo shader di vertice. L'esempio seguente mostra come il metodo MarbleMazeMain::Render utilizza 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 utilizza i metodi ID3D11DeviceContext::VSSetShader e ID3D11DeviceContext::PSSetShader per impostare rispettivamente gli shader di vertice e 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 visualizzazione e le matrici di proiezione per il labirinto. Il metodo UpdateSubresource copia i dati della matrice dalla memoria della CPU alla memoria della GPU. Ricordiamo che i componenti del modello e della vista della struttura ConstantBuffer vengono aggiornati nel metodo MarbleMazeMain::Update. Il metodo MarbleMazeMain::Render chiama quindi i metodi 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 la biglia da sottoporre a rendering.

Rendering del labirinto e della biglia

Dopo aver attivato gli shader correnti, puoi 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 ne sconsigliamo l'uso in un gioco di qualità di produzione. Tuttavia, tieni presente che il metodo SDKMesh::RenderMesh, chiamato da SDKMesh::Render, utilizza i metodi ID3D11DeviceContext::IASetVertexBuffers e ID3D11DeviceContext::IASetIndexBuffer per impostare i buffer di vertici e indici correnti che definiscono la mesh e ID3D11DeviceContext::DrawIndexed per disegnare i buffer. Per ulteriori informazioni su come utilizzare i buffer dei vertici e degli indici, consulta Introduzione ai buffer in Direct3D 11.

Disegno dell'interfaccia utente e della sovrapposizione

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

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

// 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 utilizza un oggetto ID2D1DeviceContext per disegnare gli elementi dell'interfaccia utente. Questo metodo imposta lo stato di disegno, disegna tutti gli elementi dell'interfaccia utente attivi e quindi ripristina lo stato di disegno precedente.

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 utilizza una tecnica simile per disegnare la bitmap di sovrapposizione.

Presentazione della scena

Dopo aver disegnato tutti gli oggetti della scena 2D e 3D, Marble Maze presenta l'immagine renderizzata sul monitor. Sincronizza il disegno sullo spazio vuoto verticale per garantire che non venga sprecato tempo disegnando cornici che non verranno mai effettivamente visualizzate sul display. Marble Maze gestisce anche le modifiche al dispositivo quando presenta la scena.

Dopo che il metodo MarbleMazeMain::Render ritorna, il ciclo del gioco chiama il metodo DX::DeviceResources::Present per inviare l'immagine renderizzata al monitor o al display. Il metodo DX::DeviceResources::Present chiama IDXGISwapChain::Present per eseguire l'operazione presente, come mostrato 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 di IDXGISwapChain::Present, 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 spazio vuoto verticale.

Il metodo IDXGISwapChain::Present restituisce un codice di errore che indica che il dispositivo è stato rimosso o ha comunque avuto un guasto. 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

Consulta Aggiunta di input e interattività all'esempio Marble Maze per informazioni su alcune delle pratiche chiave da tenere a mente quando lavori con dispositivi di input. Questo documento illustra il modo in cui Marble Maze supporta il tocco, l'accelerometro, i controller di gioco e l'input del mouse.