Najlepszy sposób maskowania duszków 2D w XNA?

24

Obecnie próbuję zamaskować niektóre duszki. Zamiast wyjaśniać to słowami, stworzyłem kilka przykładowych zdjęć:

Obszar do maskowania Obszar do maskowania (w kolorze białym)

Obszar do maskowania wraz z obrazem do maskowania Teraz czerwony duszek, który należy przyciąć.

Wynik końcowy Wynik końcowy.

Teraz wiem, że w XNA możesz zrobić dwie rzeczy, aby to osiągnąć:

  1. Użyj bufora szablonów.
  2. Użyj modułu Pixel Shader.

Próbowałem zrobić moduł cieniujący piksele, który zasadniczo to zrobił:

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

To wydaje się przycinać obrazy (choć nie jest poprawne, gdy obraz zaczyna się poruszać), ale moim problemem jest to, że obrazy ciągle się poruszają (nie są statyczne), więc to przycinanie musi być dynamiczne.

Czy mogę zmienić kod modułu cieniującego, aby uwzględnić jego pozycję?


Ewentualnie czytałem o używaniu bufora szablonów, ale większość próbek wydaje się zależeć od użycia rendertargetu, czego tak naprawdę nie chcę robić. (Używam już 3 lub 4 do końca gry, a dodanie kolejnej na górze wydaje się przesadą)

Jedyny samouczek, który nie używa Rendertargets, to jeden z bloga Shawna Hargreavesa tutaj. Problem polega na tym, że dotyczy XNA 3.1 i wydaje się, że nie przekłada się to dobrze na XNA 4.0.

Wydaje mi się, że cieniowanie pikseli jest właściwą drogą, ale nie jestem pewien, jak prawidłowo ustawić pozycję. Wydaje mi się, że musiałbym zmienić współrzędne na ekranie (np. 500, 500), aby były w zakresie od 0 do 1 dla współrzędnych modułu cieniującego. Moim jedynym problemem jest próba ustalenia, jak poprawnie używać przekształconych współrzędnych.

Z góry dziękuję za wszelką pomoc!

płomień elektryczny
źródło
Wydaje się, że wzornik jest właściwą drogą, ale muszę też wiedzieć, jak właściwie z nich korzystać: P Będę czekał na dobrą odpowiedź w tej sprawie.
Gustavo Maciel
Heh, większość dobrych samouczków z szablonami dotyczy XNA 3.1, a większość z 4.0 używa RenderTargets (co wydaje mi się marnotrawstwem). Zaktualizowałem swoje pytanie linkiem do starego samouczka 3.1, który wydawał się działać, ale nie w 4.0. Może ktoś wie, jak poprawnie przetłumaczyć to na 4.0?
electroflame
Jeśli użyjesz do tego modułu cieniującego piksele, myślę, że musiałbyś odsunąć współrzędną pikseli na ekran / świat (zależy od tego, co chcesz zrobić), a to jest trochę marnotrawne (szablon nie miałby tego problemu)
Gustavo Maciel
To pytanie jest doskonałe.
ClassicThunder

Odpowiedzi:

11

Możesz użyć metody cieniowania pikseli, ale jest ona niekompletna.

Będziesz także musiał dodać kilka parametrów, które informują moduł cieniujący pikseli, gdzie chcesz umieścić szablon.

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

W kodzie C # przekazujesz zmienne do modułu cieniującego piksele przed SpriteBatch.Drawwywołaniem w następujący sposób:

maskEffect.Parameters["MaskWidth"].SetValue(800);
maskEffect.Parameters["MaskHeight"].SetValue(600);
Jim
źródło
Dziękuję za Twoją odpowiedź! Wygląda na to, że zadziała, ale obecnie mam z tym problem. Obecnie jest przycinany po lewej stronie obrazu (również z prostą krawędzią). Czy powinienem używać współrzędnych ekranu dla pozycji? A może mam już przekonwertować je na 0 - 1?
płomieniówka
Widzę problem. Powinienem był skompilować moduł cieniujący piksele przed udostępnieniem. : P W maskCoordobliczeniach jeden z X powinien być literą Y. Zredagowałem moją wstępną odpowiedź z poprawką i uwzględniłem ustawienia klamry UV, których będziesz potrzebować MaskTexture sampler.
Jim,
1
Teraz działa idealnie, dziękuję! Jedyną rzeczą, o której powinienem wspomnieć jest to, że jeśli masz sprity wyśrodkowane (używając Szerokość / 2 i Wysokość / 2 jako początek), będziesz musiał odjąć połowę swojej Szerokość lub Wysokość dla lokalizacji X i Y (do których przechodzisz) moduł cieniujący). Potem działa idealnie i jest niezwykle szybki! Wielkie dzięki!
płomieniówka elektryczna
18

przykładowy obraz

Pierwszym krokiem jest poinformowanie karty graficznej, że potrzebujemy bufora szablonu. Aby to zrobić podczas tworzenia GraphicsDeviceManager, ustawiamy PreferredDepthStencilFormat na DepthFormat.Depth24Stencil8, aby w rzeczywistości istniał szablon do pisania.

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

AlphaTestEffect służy do ustawiania układu współrzędnych i filtrowania pikseli za pomocą alfa, które przejdą test alfa. Nie zamierzamy ustawiać żadnych filtrów ani ustawiać układu współrzędnych na port widoku.

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

Następnie musimy skonfigurować dwa DepthStencilStates. Te stany określają, kiedy SpriteBatch renderuje na szablonie, a kiedy SpriteBatch renderuje na BackBuffer. Interesują nas przede wszystkim dwie zmienne StencilFunction i StencilPass.

  • Funkcja StencilFunction określa, kiedy SpriteBatch będzie rysował poszczególne piksele, a kiedy będą ignorowane.
  • StencilPass określa, kiedy narysowane piksele wpływają na szablon.

Dla pierwszego DepthStencilState ustawiamy StencilFunction na CompareFunction. Powoduje to, że StencilTest się powiódł, a gdy StencilTest SpriteBatch renderuje ten piksel. StencilPass jest ustawiony na StencilOperation. Zamień, co oznacza, że ​​gdy test StencilTest zakończy się powodzeniem, piksel zostanie zapisany w StencilBuffer o wartości ReferenceStencil.

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

Podsumowując, StencilTest zawsze przechodzi, obraz jest rysowany na ekranie normalnie, a dla pikseli rysowanych na ekranie wartość 1 jest przechowywana w StencilBuffer.

Drugi DepthStencilState jest nieco bardziej skomplikowany. Tym razem chcemy rysować na ekranie tylko wtedy, gdy wartość w StencilBuffer wynosi. Aby to osiągnąć, ustawiamy StencilFunction na CompareFunction.LessEqual i ReferenceStencil na 1. Oznacza to, że gdy wartość w buforze szablonu wynosi 1, test StencilTest się powiedzie. Ustawienie StencilPass na StencilOperation. Zachowaj powoduje, że StencilBuffer nie aktualizuje się. To pozwala nam rysować wiele razy przy użyciu tej samej maski.

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

Podsumowując, StencilTest przechodzi tylko wtedy, gdy StencilBuffer ma mniej niż 1 (piksele alfa z maski) i nie wpływa na StencilBuffer.

Teraz, gdy mamy skonfigurowane nasze DepthStencilStates. Możemy rysować za pomocą maski. Po prostu narysuj maskę za pomocą pierwszego DepthStencilState. To wpłynie zarówno na BackBuffer, jak i StencilBuffer. Teraz, gdy bufor szablonu ma wartość 0, w której maska ​​ma przezroczystość, a 1, w której zawiera kolor, możemy użyć StencilBuffer do maskowania późniejszych obrazów.

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

Drugi SpriteBatch używa drugiego DepthStencilStates. Bez względu na to, co narysujesz, tylko piksele, w których StencilBuffer jest ustawiony na 1, przejdą test szablonu i zostaną narysowane na ekranie.

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

Poniżej znajduje się całość kodu w metodzie Draw, nie zapomnij ustawić PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8 w konstruktorze gry.

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
źródło
1
+1 Jest to jeden z najbardziej kompletnych przykładów StencilBuffer, jakie widziałem dla XNA 4.0. Dzięki za to! Jedynym powodem, dla którego wybrałem odpowiedź modułu cieniującego piksele, jest to, że łatwiej było ją skonfigurować (a właściwie szybciej uruchomić), ale jest to również poprawna odpowiedź i może być łatwiejsza, jeśli masz już wiele modułów cieniujących. Dzięki! Teraz mamy pełną odpowiedź na obie trasy!
płomieniówka elektryczna
Jest to dla mnie najlepsze prawdziwe rozwiązanie, i więcej niż pierwsze
Mehdi Bugnard
Jak mogę tego użyć, gdybym miał model 3D? Myślę, że to może rozwiązać moje pytanie gamedev.stackexchange.com/questions/93733/... , ale nie wiem, jak zastosować je do modeli 3D. Czy wiesz, czy mogę to zrobić?
dimitris93
0

W powyższej odpowiedzi zdarzyły się dziwne rzeczy, kiedy zrobiłem dokładnie to, co zostało wyjaśnione.

Jedyne, czego potrzebowałem, to jedno i drugie 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,
};

I losowania bez 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();

I wszystko było w porządku, zgodnie z oczekiwaniami.

Aseu
źródło