La meilleure façon de masquer les sprites 2D dans XNA?

24

J'essaie actuellement de masquer certains sprites. Plutôt que de l'expliquer par des mots, j'ai composé quelques exemples d'images:

La zone à masquer La zone à masquer (en blanc)

La zone à masquer, avec l'image à masquer Maintenant, le sprite rouge qui doit être recadré.

Le résultat final Le résultat final.

Maintenant, je sais que dans XNA, vous pouvez faire deux choses pour y parvenir:

  1. Utilisez le tampon de pochoir.
  2. Utilisez un Pixel Shader.

J'ai essayé de faire un pixel shader, qui a essentiellement fait ceci:

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

Cela semble recadrer les images (bien que ce ne soit pas correct une fois que l'image commence à bouger), mais mon problème est que les images se déplacent constamment (elles ne sont pas statiques), donc ce recadrage doit être dynamique.

Existe-t-il un moyen de modifier le code du shader pour prendre en compte sa position?


Alternativement, j'ai lu sur l'utilisation du tampon de gabarit, mais la plupart des exemples semblent dépendre de l'utilisation d'un rendu cible, ce que je ne veux vraiment pas faire. (J'utilise déjà 3 ou 4 pour le reste du jeu, et en ajouter un autre par dessus semble exagéré)

Le seul tutoriel que j'ai trouvé qui n'utilise pas Rendertargets est celui du blog de Shawn Hargreaves ici. Le problème avec celui-ci, cependant, c'est que c'est pour XNA 3.1, et ne semble pas bien se traduire par XNA 4.0.

Il me semble que le pixel shader est la voie à suivre, mais je ne sais pas comment obtenir le bon positionnement. Je pense que je devrais changer mes coordonnées à l'écran (quelque chose comme 500, 500) pour qu'elles soient comprises entre 0 et 1 pour les coordonnées du shader. Mon seul problème est d'essayer de savoir comment utiliser correctement les coordonnées transformées.

Merci d'avance pour votre aide!

électroflame
la source
Le pochoir semble être la voie à suivre, cependant, je dois aussi savoir comment les utiliser correctement: P J'attendrai une bonne réponse à ce sujet.
Gustavo Maciel
Hé, la plupart des bons tutoriels Stencil sont pour XNA 3.1, et la plupart des 4.0 utilisent RenderTargets (ce qui me semble inutile). J'ai mis à jour ma question avec un lien vers un ancien tutoriel 3.1 qui semblait fonctionner, mais pas en 4.0. Peut-être que quelqu'un sait comment le traduire correctement en 4.0?
electroflame
Si vous utilisiez un pixel shader pour l'accomplir, je pense que vous devriez dé-projeter vos coordonnées de pixel à l'écran / monde (cela dépend de ce que vous voulez faire), et c'est un peu inutile (le pochoir n'aurait pas ce problème)
Gustavo Maciel
Cette question est une excellente question.
ClassicThunder

Réponses:

11

Vous pouvez utiliser votre approche pixel shader mais elle est incomplète.

Vous devrez également lui ajouter des paramètres qui informent le pixel shader où vous souhaitez placer votre pochoir.

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

Dans votre code C #, vous passez des variables au pixel shader avant l' SpriteBatch.Drawappel comme ceci:

maskEffect.Parameters["MaskWidth"].SetValue(800);
maskEffect.Parameters["MaskHeight"].SetValue(600);
Jim
la source
Merci pour la réponse! Cela semble fonctionner, mais actuellement j'ai un problème avec ça. Actuellement, il est rogné à gauche de l'image (avec un bord droit également). Suis-je censé utiliser les coordonnées d'écran pour les positions? Ou suis-je déjà censé les avoir convertis en 0 - 1?
électroflame
Je vois le problème. J'aurais dû compiler le pixel shader avant de le partager. : P Dans le maskCoordcalcul, l'un des X aurait dû être un Y. J'ai modifié ma réponse initiale avec le correctif et inclus les paramètres de la pince UV dont vous aurez besoin pour votre MaskTexture sampler.
Jim
1
Maintenant, cela fonctionne parfaitement, merci! La seule chose que je dois mentionner est que si vous avez vos sprites centrés (en utilisant Largeur / 2 et Hauteur / 2 pour l'origine), vous devrez soustraire la moitié de votre Largeur ou Hauteur pour vos emplacements X et Y (que vous passez dans le shader). Après cela, il fonctionne parfaitement et est extrêmement rapide! Merci beaucoup!
electroflame
18

exemple d'image

La première étape consiste à indiquer à la carte graphique que nous avons besoin du tampon de gabarit. Pour ce faire, lorsque vous créez GraphicsDeviceManager, nous définissons PreferredDepthStencilFormat sur DepthFormat.Depth24Stencil8 pour qu'il y ait en fait un gabarit sur lequel écrire.

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

L'AlphaTestEffect est utilisé pour définir le système de coordonnées et filtrer les pixels avec alpha qui réussissent le test alpha. Nous n'allons pas définir de filtres et définir le système de coordonnées sur le port d'affichage.

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

Ensuite, nous devons configurer deux DepthStencilStates. Ces états dictent le moment où le SpriteBatch effectue le rendu vers le gabarit et le moment où le SpriteBatch effectue le rendu vers le BackBuffer. Nous sommes principalement intéressés par deux variables StencilFunction et StencilPass.

  • StencilFunction dicte quand le SpriteBatch dessinera des pixels individuels et quand ils seront ignorés.
  • StencilPass dicte quand les pixels dessinés les pixels affectent le pochoir.

Pour le premier DepthStencilState, nous définissons StencilFunction sur CompareFunction. Cela provoque le StencilTest à réussir et lorsque le StencilTest le SpriteBatch rend ce pixel. StencilPass est défini sur StencilOperation. Remplacez ce qui signifie que lorsque le StencilTest réussit, ce pixel sera écrit dans le StencilBuffer avec la valeur du ReferenceStencil.

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

En résumé, le StencilTest passe toujours, l'image est dessinée à l'écran normalement et pour les pixels dessinés à l'écran, une valeur de 1 est stockée dans le StencilBuffer.

Le deuxième DepthStencilState est légèrement plus compliqué. Cette fois, nous voulons uniquement dessiner à l'écran lorsque la valeur dans le StencilBuffer est. Pour ce faire, nous définissons StencilFunction sur CompareFunction.LessEqual et ReferenceStencil sur 1. Cela signifie que lorsque la valeur dans le tampon de stencil est 1, StencilTest réussit. Définition de StencilPass sur StencilOperation. Keep empêche le StencilBuffer de se mettre à jour. Cela nous permet de dessiner plusieurs fois en utilisant le même masque.

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

En résumé, le StencilTest ne passe que lorsque le StencilBuffer est inférieur à 1 (les pixels alpha du masque) et n'affecte pas le StencilBuffer.

Maintenant que nous avons configuré nos DepthStencilStates. Nous pouvons réellement dessiner en utilisant un masque. Dessinez simplement le masque à l'aide du premier DepthStencilState. Cela affectera à la fois le BackBuffer et le StencilBuffer. Maintenant que le tampon de gabarit a une valeur de 0 où votre masque avait de la transparence et 1 où il contenait de la couleur, nous pouvons utiliser StencilBuffer pour masquer les images ultérieures.

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

Le deuxième SpriteBatch utilise le second DepthStencilStates. Peu importe ce que vous dessinez, seuls les pixels où le StencilBuffer est défini sur 1 passeront le test du pochoir et seront dessinés à l'écran.

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

Vous trouverez ci-dessous l'intégralité du code de la méthode Draw, n'oubliez pas de définir PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8 dans le constructeur du jeu.

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();
ClassicThunder
la source
1
+1 C'est l'un des exemples de StencilBuffer les plus complets que j'ai vus pour XNA 4.0. Merci pour cela! La seule raison pour laquelle j'ai choisi la réponse pixel shader est parce qu'elle était plus facile à configurer (et en fait plus rapide à démarrer), mais c'est également une réponse parfaitement valide et peut être plus facile si vous avez déjà beaucoup de shaders en place. Merci! Maintenant, nous avons une réponse complète pour les deux itinéraires!
electroflame
C'est la meilleure vraie solution pour moi, et plus que la première
Mehdi Bugnard
Comment pourrais-je l'utiliser si j'avais un modèle 3D? Je pense que cela pourrait résoudre ma question gamedev.stackexchange.com/questions/93733/… , mais je ne sais pas comment l'appliquer sur des modèles 3D. Savez-vous si je peux faire ça?
dimitris93
0

Dans la réponse ci - dessus , des choses étranges se sont produites lorsque j'ai fait exactement ce qui est expliqué.

La seule chose dont j'avais besoin était les deux 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,
};

Et les tirages sans 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();

Et tout allait bien comme prévu.

Aseu
la source