Il modo migliore per mascherare gli sprite 2D in XNA?


24

Attualmente sto cercando di mascherare alcuni sprite. Invece di spiegarlo a parole, ho inventato alcune immagini di esempio:

L'area da mascherare L'area da mascherare (in bianco)

L'area da mascherare, con l'immagine da mascherare Ora, lo sprite rosso che deve essere ritagliato.

Il risultato finale Il risultato finale

Ora, sono consapevole che in XNA puoi fare due cose per ottenere questo risultato:

  1. Utilizzare il buffer di stencil.
  2. Usa un Pixel Shader.

Ho provato a fare un pixel shader, che essenzialmente ha fatto questo:

float4 main(float2 texCoord : TEXCOORD0) : COLOR0
{
    float4 tex = tex2D(BaseTexture, texCoord);
    float4 bitMask = tex2D(MaskTexture, texCoord);

    if (bitMask.a > 0)
    { 
        return float4(tex.r, tex.g, tex.b, tex.a);
    }
    else
    {
        return float4(0, 0, 0, 0);
    }
}

Questo sembra ritagliare le immagini (anche se, non corretto una volta che l'immagine inizia a muoversi), ma il mio problema è che le immagini sono in costante movimento (non sono statiche), quindi questo ritaglio deve essere dinamico.

C'è un modo per modificare il codice shader per tener conto della sua posizione?


In alternativa, ho letto sull'uso dello Stencil Buffer, ma la maggior parte dei campioni sembra dipendere dall'uso di un rendertarget, cosa che non voglio davvero fare. (Sto già usando 3 o 4 per il resto del gioco e aggiungerne un altro sembra eccessivo)

L'unico tutorial che ho scoperto che non usa Rendertargets è quello del blog di Shawn Hargreaves qui. Il problema con quello, però, è che è per XNA 3.1 e non sembra tradursi bene in XNA 4.0.

Mi sembra che il pixel shader sia la strada da percorrere, ma non sono sicuro di come ottenere il posizionamento corretto. Credo che dovrei cambiare le mie coordinate sullo schermo (qualcosa come 500, 500) per essere tra 0 e 1 per le coordinate dello shader. Il mio unico problema è cercare di capire come usare correttamente le coordinate trasformate.

Grazie in anticipo per qualsiasi aiuto!


Lo stencil sembra essere la strada da percorrere, però, devo sapere anche come usarli correttamente: P aspetterò una buona risposta in merito.
Gustavo Maciel,

Heh, la maggior parte dei buoni tutorial di Stencil sono per XNA 3.1 e la maggior parte di quelli 4.0 usano RenderTargets (che mi sembra dispendioso). Ho aggiornato la mia domanda con un collegamento a un vecchio tutorial 3.1 che sembrava funzionare, ma non in 4.0. Forse qualcuno sa come tradurlo correttamente in 4.0?
electroflame

Se dovessi usare il pixel shader per realizzarlo, penso che dovresti non proiettare le tue coordinate pixel sullo schermo / mondo (dipende da cosa vuoi fare), e questo è un po 'dispendioso (lo stencil non avrebbe questo problema)
Gustavo Maciel

Questa domanda è un'ottima domanda.
ClassicThunder

Risposte:


11

Puoi utilizzare il tuo approccio pixel shader ma è incompleto.

Dovrai anche aggiungere alcuni parametri che informano il pixel shader dove desideri posizionare lo stencil.

sampler BaseTexture : register(s0);
sampler MaskTexture : register(s1) {
    addressU = Clamp;
    addressV = Clamp;
};

//All of these variables are pixel values
//Feel free to replace with float2 variables
float MaskLocationX;
float MaskLocationY;
float MaskWidth;
float MaskHeight;
float BaseTextureLocationX;  //This is where your texture is to be drawn
float BaseTextureLocationY;  //texCoord is different, it is the current pixel
float BaseTextureWidth;
float BaseTextureHeight;

float4 main(float2 texCoord : TEXCOORD0) : COLOR0
{
    //We need to calculate where in terms of percentage to sample from the MaskTexture
    float maskPixelX = texCoord.x * BaseTextureWidth + BaseTextureLocationX;
    float maskPixelY = texCoord.y * BaseTextureHeight + BaseTextureLocationY;
    float2 maskCoord = float2((maskPixelX - MaskLocationX) / MaskWidth, (maskPixelY - MaskLocationY) / MaskHeight);
    float4 bitMask = tex2D(MaskTexture, maskCoord);

    float4 tex = tex2D(BaseTexture, texCoord);

    //It is a good idea to avoid conditional statements in a pixel shader if you can use math instead.
    return tex * (bitMask.a);
    //Alternate calculation to invert the mask, you could make this a parameter too if you wanted
    //return tex * (1.0 - bitMask.a);
}

Nel tuo codice C # passi le variabili al pixel shader prima della SpriteBatch.Drawchiamata in questo modo:

maskEffect.Parameters["MaskWidth"].SetValue(800);
maskEffect.Parameters["MaskHeight"].SetValue(600);

Grazie per la risposta! Sembra che funzionerà, ma al momento sto riscontrando un problema. Attualmente, viene ritagliato a sinistra dell'immagine (anche con un bordo dritto). Dovrei usare le coordinate dello schermo per le posizioni? O dovrei già averli convertiti in 0 - 1?
electroflame

Vedo il problema. Avrei dovuto compilare il pixel shader prima di condividerlo. : P Nel maskCoordcalcolo una delle X avrebbe dovuto essere una Y. Ho modificato la mia risposta iniziale con la correzione e incluso le impostazioni del morsetto UV necessarie per il tuoMaskTexture sampler .
Jim,

1
Ora funziona perfettamente, grazie! L'unica cosa che dovrei menzionare è che se i tuoi sprite sono centrati (usando Larghezza / 2 e Altezza / 2 per l'origine) dovrai sottrarre metà della tua Larghezza o Altezza per le tue posizioni X e Y (in cui passi lo shader). Dopodiché funziona perfettamente ed è estremamente veloce! Grazie mille!
electroflame,

18

immagine di esempio

Il primo passo è dire alla scheda grafica che abbiamo bisogno del buffer di stencil. Per fare ciò quando crei GraphicsDeviceManager, impostiamo PreferredDepthStencilFormat su DepthFormat.Depth24Stencil8 in modo che ci sia effettivamente uno stencil su cui scrivere.

graphics = new GraphicsDeviceManager(this) {
    PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8
};

AlphaTestEffect viene utilizzato per impostare il sistema di coordinate e filtrare i pixel con alfa che superano il test alfa. Non imposteremo alcun filtro e il sistema di coordinate sulla porta di visualizzazione.

var m = Matrix.CreateOrthographicOffCenter(0,
    graphics.GraphicsDevice.PresentationParameters.BackBufferWidth,
    graphics.GraphicsDevice.PresentationParameters.BackBufferHeight,
    0, 0, 1
);
var a = new AlphaTestEffect(graphics.GraphicsDevice) {
    Projection = m
};

Successivamente è necessario impostare due DepthStencilStates. Questi stati determinano quando lo SpriteBatch esegue il rendering sullo stencil e quando lo SpriteBatch esegue il rendering sul BackBuffer. Siamo principalmente interessati a due variabili StencilFunction e StencilPass.

  • StencilFunction determina quando SpriteBatch disegna singoli pixel e quando verranno ignorati.
  • StencilPass determina quando i pixel disegnati hanno effetto sullo Stencil.

Per il primo DepthStencilState impostiamo StencilFunction su CompareFunction. Questo fa sì che lo StencilTest abbia esito positivo e quando lo StencilTest SpriteBatch esegue il rendering di quel pixel. StencilPass è impostato su StencilOperation. Sostituire significa che quando lo StencilTest ha esito positivo, quel pixel verrà scritto nello StencilBuffer con il valore di ReferenceStencil.

var s1 = new DepthStencilState {
    StencilEnable = true,
    StencilFunction = CompareFunction.Always,
    StencilPass = StencilOperation.Replace,
    ReferenceStencil = 1,
    DepthBufferEnable = false,
};

In breve, lo StencilTest passa sempre, l'immagine viene disegnata sullo schermo normalmente e per i pixel disegnati sullo schermo un valore 1 viene memorizzato nello StencilBuffer.

Il secondo DepthStencilState è leggermente più complicato. Questa volta vogliamo disegnare sullo schermo solo quando il valore in StencilBuffer è. Per ottenere ciò, impostiamo StencilFunction su CompareFunction.LessEqual e ReferenceStencil su 1. Ciò significa che quando il valore nel buffer dello stencil è 1, StencilTest avrà esito positivo. Impostazione di StencilPass su StencilOperation. Keep fa sì che StencilBuffer non si aggiorni. Questo ci consente di disegnare più volte utilizzando la stessa maschera.

var s2 = new DepthStencilState {
    StencilEnable = true,
    StencilFunction = CompareFunction.LessEqual,
    StencilPass = StencilOperation.Keep,
    ReferenceStencil = 1,
    DepthBufferEnable = false,
};

In breve, StencilTest passa solo quando StencilBuffer è inferiore a 1 (i pixel alfa della maschera) e non ha effetto su StencilBuffer.

Ora che abbiamo impostato i nostri DepthStencilStates. Possiamo effettivamente disegnare usando una maschera. Disegna semplicemente la maschera usando il primo DepthStencilState. Ciò avrà effetto sia sul BackBuffer che sullo StencilBuffer. Ora che il buffer dello stencil ha un valore di 0 in cui la maschera aveva la trasparenza e 1 in cui conteneva il colore, possiamo usare StencilBuffer per mascherare le immagini successive.

spriteBatch.Begin(SpriteSortMode.Immediate, null, null, s1, null, a);
spriteBatch.Draw(huh, Vector2.Zero, Color.White); //The mask                                   
spriteBatch.End();

Il secondo SpriteBatch utilizza il secondo DepthStencilStates. Indipendentemente da ciò che disegni, solo i pixel in cui StencilBuffer è impostato su 1 supereranno il test dello stencil e verranno disegnati sullo schermo.

spriteBatch.Begin(SpriteSortMode.Immediate, null, null, s2, null, a);            
spriteBatch.Draw(color, Vector2.Zero, Color.White); //The background
spriteBatch.End();

Di seguito è riportato l'intero codice nel metodo Draw, non dimenticare di impostare PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8 nel costruttore del gioco.

GraphicsDevice.Clear(ClearOptions.Target 
    | ClearOptions.Stencil, Color.Transparent, 0, 0);

var m = Matrix.CreateOrthographicOffCenter(0,
    graphics.GraphicsDevice.PresentationParameters.BackBufferWidth,
    graphics.GraphicsDevice.PresentationParameters.BackBufferHeight,
    0, 0, 1
);

var a = new AlphaTestEffect(graphics.GraphicsDevice) {
    Projection = m
};

var s1 = new DepthStencilState {
    StencilEnable = true,
    StencilFunction = CompareFunction.Always,
    StencilPass = StencilOperation.Replace,
    ReferenceStencil = 1,
    DepthBufferEnable = false,
};

var s2 = new DepthStencilState {
    StencilEnable = true,
    StencilFunction = CompareFunction.LessEqual,
    StencilPass = StencilOperation.Keep,
    ReferenceStencil = 1,
    DepthBufferEnable = false,
};

spriteBatch.Begin(SpriteSortMode.Immediate, null, null, s1, null, a);
spriteBatch.Draw(huh, Vector2.Zero, Color.White); //The mask                                   
spriteBatch.End();

spriteBatch.Begin(SpriteSortMode.Immediate, null, null, s2, null, a);            
spriteBatch.Draw(color, Vector2.Zero, Color.White); //The background
spriteBatch.End();

1
+1 Questo è uno degli esempi di StencilBuffer più completi che ho visto per XNA 4.0. Grazie per questo! L'unico motivo per cui ho scelto la risposta del pixel shader è perché era più facile da configurare (e in realtà più veloce da avviare), ma questa è anche una risposta perfettamente valida e potrebbe essere più semplice se hai già molti shader in atto. Grazie! Ora abbiamo una risposta completa per entrambe le rotte!
electroflame,

Questa è la migliore soluzione reale per me, e più della prima
Mehdi Bugnard,

Come potrei usarlo se avessi un modello 3D? Penso che questo possa risolvere la mia domanda gamedev.stackexchange.com/questions/93733/… , ma non so come applicarlo sui modelli 3D. Sai se posso farlo?
dimitris93,

0

Nella risposta sopra , sono successe alcune cose strane quando ho fatto esattamente come spiegato.

L'unica cosa di cui avevo bisogno era entrambe DepthStencilStates.

var s1 = new DepthStencilState {
    StencilEnable = true,
    StencilFunction = CompareFunction.Always,
    StencilPass = StencilOperation.Replace,
    ReferenceStencil = 1,
    DepthBufferEnable = false,
};

var s2 = new DepthStencilState {
    StencilEnable = true,
    StencilFunction = CompareFunction.LessEqual,
    StencilPass = StencilOperation.Keep,
    ReferenceStencil = 1,
    DepthBufferEnable = false,
};

E i sorteggi senza il AlphaTestEffect.

spriteBatch.Begin(SpriteSortMode.Immediate, null, null, s1, null, null);
spriteBatch.Draw(huh, Vector2.Zero, Color.White); //The mask                                   
spriteBatch.End();

spriteBatch.Begin(SpriteSortMode.Immediate, null, null, s2, null, null);            
spriteBatch.Draw(color, Vector2.Zero, Color.White); //The background
spriteBatch.End();

E tutto andava bene come previsto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.