Der beste Weg, um 2D-Sprites in XNA zu maskieren?

24

Ich versuche gerade, einige Sprites zu maskieren. Anstatt es in Worten zu erklären, habe ich ein paar Beispielbilder zusammengestellt:

Der zu maskierende Bereich Der zu maskierende Bereich (in Weiß)

Der zu maskierende Bereich und das zu maskierende Bild Nun das rote Sprite, das abgeschnitten werden muss.

Das Endergebnis Das Endergebnis.

Nun ist mir bewusst, dass Sie in XNA zwei Dinge tun können, um dies zu erreichen:

  1. Verwenden Sie den Stencil Buffer.
  2. Verwenden Sie einen Pixel Shader.

Ich habe versucht, einen Pixel-Shader zu erstellen, der im Wesentlichen Folgendes ausführt:

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

Dies scheint die Bilder zu beschneiden (wenn auch nicht korrekt, sobald sich das Bild zu bewegen beginnt), aber mein Problem ist, dass sich die Bilder ständig bewegen (sie sind nicht statisch), so dass dieses Beschneiden dynamisch sein muss.

Kann ich den Shader-Code ändern, um seine Position zu berücksichtigen?


Alternativ habe ich über die Verwendung des Stencil Buffer gelesen, aber die meisten Beispiele scheinen von der Verwendung eines Rendertargets abhängig zu sein, was ich wirklich nicht tun möchte. (Ich benutze bereits 3 oder 4 für den Rest des Spiels und das Hinzufügen eines weiteren erscheint übertrieben)

Das einzige Tutorial, das ich gefunden habe und das Rendertargets nicht verwendet, stammt von Shawn Hargreaves 'Blog hier. Das Problem dabei ist jedoch, dass es für XNA 3.1 ist und sich nicht gut auf XNA 4.0 übertragen lässt.

Mir scheint, der Pixel-Shader ist der richtige Weg, aber ich bin mir nicht sicher, wie ich die richtige Positionierung erreichen soll. Ich glaube, ich müsste meine Bildschirmkoordinaten (etwa 500, 500) ändern, um zwischen 0 und 1 für die Shader-Koordinaten zu liegen. Mein einziges Problem ist, herauszufinden, wie man die transformierten Koordinaten richtig verwendet.

Vielen Dank im Voraus für jede Hilfe!

Elektroflamme
quelle
Schablone scheint der richtige Weg zu sein, aber ich muss auch wissen, wie man sie richtig einsetzt: P Ich werde auf eine gute Antwort in dieser Angelegenheit warten.
Gustavo Maciel
Heh, die meisten guten Stencil-Tutorials sind für XNA 3.1, und die meisten 4.0-Tutorials verwenden RenderTargets (was mir verschwenderisch erscheint). Ich habe meine Frage mit einem Link zu einem alten 3.1-Tutorial aktualisiert, das anscheinend funktionieren könnte, aber nicht in 4.0. Vielleicht weiß jemand, wie man es richtig in 4.0 übersetzt?
Electroflame
Wenn Sie Pixel - Shader es zu erreichen verwenden würde, würde ich denken Sie unproject haben Ihre Pixel auf Bildschirm / Weltkoordinaten (hängt davon ab , was Sie tun wollen), und dies ist ein bisschen verschwenderisch (Schablone würde dieses Problem nicht haben)
Gustavo Maciel
Diese Frage ist eine ausgezeichnete Frage.
ClassicThunder

Antworten:

11

Sie können Ihren Pixel-Shader-Ansatz verwenden, der jedoch unvollständig ist.

Sie müssen auch einige Parameter hinzufügen, die den Pixel-Shader darüber informieren, wo sich Ihre Schablone befinden soll.

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

In Ihrem C # -Code übergeben Sie vor dem SpriteBatch.DrawAufruf Variablen wie folgt an den Pixel-Shader :

maskEffect.Parameters["MaskWidth"].SetValue(800);
maskEffect.Parameters["MaskHeight"].SetValue(600);
Jim
quelle
Danke für die Antwort! Das scheint zu funktionieren, aber momentan habe ich ein Problem damit. Derzeit wird es links im Bild abgeschnitten (auch mit einer geraden Kante). Soll ich Bildschirmkoordinaten für die Positionen verwenden? Oder soll ich sie schon auf 0 - 1 konvertiert haben?
Electroflame
Ich sehe das Problem. Ich hätte den Pixel-Shader kompilieren sollen, bevor ich ihn weitergebe. : P In der maskCoordBerechnung sollte eines der X ein Y gewesen sein. Ich habe meine ursprüngliche Antwort mit dem Fix bearbeitet und die UV-Klemmeinstellungen eingefügt, die Sie für Ihr X benötigen MaskTexture sampler.
Jim
1
Jetzt funktioniert es einwandfrei, danke! Das einzige, was ich erwähnen sollte, ist, dass Sie, wenn Sie Ihre Sprites zentriert haben (mit Width / 2 und Height / 2 als Ursprung), die Hälfte Ihrer Width oder Height für Ihre X- und Y-Positionen abziehen müssen (an die Sie übergeben werden) der Shader). Danach funktioniert es einwandfrei und ist extrem schnell! Danke vielmals!
Electroflame
18

Beispiel Bild

Der erste Schritt besteht darin, der Grafikkarte mitzuteilen, dass wir den Schablonenpuffer benötigen. Um dies zu tun, wenn Sie GraphicsDeviceManager erstellen, setzen wir PreferredDepthStencilFormat auf DepthFormat.Depth24Stencil8, sodass tatsächlich eine Schablone zum Schreiben vorhanden ist.

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

Der AlphaTestEffect wird verwendet, um das Koordinatensystem festzulegen und die Pixel mit Alpha zu filtern, die den Alpha-Test bestehen. Wir werden keine Filter setzen und das Koordinatensystem auf den Ansichtsport setzen.

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

Als nächstes müssen wir zwei DepthStencilStates einrichten. Diese Zustände bestimmen, wann der SpriteBatch in die Schablone und wann der SpriteBatch in den BackBuffer gerendert wird. Wir interessieren uns hauptsächlich für zwei Variablen StencilFunction und StencilPass.

  • StencilFunction bestimmt, wann der SpriteBatch einzelne Pixel zeichnet und wann sie ignoriert werden.
  • StencilPass bestimmt, wann gezeichnete Pixel die Schablone beeinflussen.

Für den ersten DepthStencilState setzen wir StencilFunction auf CompareFunction. Dies bewirkt, dass der StencilTest erfolgreich ist und wenn der StencilTest, den SpriteBatch ausführt, dieses Pixel gerendert wird. StencilPass ist auf StencilOperation eingestellt. Ersetzen bedeutet, dass bei erfolgreichem StencilTest dieses Pixel mit dem Wert von ReferenceStencil in den StencilBuffer geschrieben wird.

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

Zusammenfassend besteht der StencilTest immer, das Bild wird normal auf den Bildschirm gezeichnet und für auf den Bildschirm gezeichnete Pixel wird der Wert 1 im StencilBuffer gespeichert.

Der zweite DepthStencilState ist etwas komplizierter. Dieses Mal möchten wir nur dann auf dem Bildschirm zeichnen, wenn der Wert im StencilBuffer ist. Um dies zu erreichen, setzen wir die StencilFunction auf CompareFunction.LessEqual und die ReferenceStencil auf 1. Dies bedeutet, dass der StencilTest erfolgreich ist, wenn der Wert im Schablonenpuffer 1 ist. StencilPass auf StencilOperation setzen. Behalten bewirkt, dass der StencilBuffer nicht aktualisiert wird. Auf diese Weise können wir mit derselben Maske mehrere Male zeichnen.

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

Zusammenfassend lässt sich sagen, dass der StencilTest nur erfolgreich ist, wenn der StencilBuffer kleiner als 1 ist (Alpha-Pixel von der Maske), und den StencilBuffer nicht beeinflusst.

Jetzt haben wir unsere DepthStencilStates eingerichtet. Wir können tatsächlich mit einer Maske zeichnen. Zeichnen Sie einfach die Maske mit dem ersten DepthStencilState. Dies wirkt sich sowohl auf den BackBuffer als auch auf den StencilBuffer aus. Nachdem der Schablonenpuffer den Wert 0 hat, bei dem Sie Transparenz maskiert haben, und 1, bei dem es Farbe enthielt, können wir StencilBuffer verwenden, um spätere Bilder zu maskieren.

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

Der zweite SpriteBatch verwendet die zweiten DepthStencilStates. Unabhängig davon, was Sie zeichnen, bestehen nur die Pixel, bei denen der StencilBuffer auf 1 gesetzt ist, den Schablonentest und werden auf den Bildschirm gezeichnet.

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

Unten finden Sie den gesamten Code in der Draw-Methode. Vergessen Sie nicht, PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8 im Spielkonstruktor festzulegen.

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
quelle
1
+1 Dies ist eines der umfassendsten StencilBuffer-Beispiele, die ich für XNA 4.0 gesehen habe. Danke dafür! Der einzige Grund, warum ich mich für die Pixel-Shader-Antwort entschieden habe, ist, dass sie einfacher einzurichten war (und tatsächlich schneller zu booten war), aber dies ist auch eine absolut gültige Antwort und möglicherweise einfacher, wenn bereits viele Shader vorhanden sind. Vielen Dank! Jetzt haben wir für beide Routen eine vollständige Antwort!
Electroflame
Dies ist die beste echte Lösung für mich und mehr als die erste
Mehdi Bugnard
Wie könnte ich das nutzen, wenn ich ein 3D-Modell hätte? Ich denke, das könnte meine Frage lösen gamedev.stackexchange.com/questions/93733/… , aber ich weiß nicht, wie ich es auf 3D-Modelle anwenden soll. Weißt du, ob ich das kann?
dimitris93
0

In der obigen Antwort passierten einige seltsame Dinge, als ich genau das tat, was erklärt wurde.

Das einzige, was ich brauchte, war die beiden 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,
};

Und die Unentschieden ohne die 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();

Und alles war gut wie erwartet.

Aseu
quelle