Melhor maneira de mascarar sprites 2D no XNA?

24

Atualmente, estou tentando mascarar alguns sprites. Em vez de explicá-lo em palavras, criei algumas fotos de exemplo:

A área a mascarar A área a mascarar (em branco)

A área a mascarar, com a imagem a mascarar Agora, o sprite vermelho que precisa ser cortado.

O resultado final O resultado final.

Agora, eu sei que no XNA você pode fazer duas coisas para fazer isso:

  1. Use o buffer de estêncil.
  2. Use um Pixel Shader.

Eu tentei fazer um pixel shader, que basicamente fez isso:

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

Isso parece cortar as imagens (embora não seja correto quando a imagem começa a se mover), mas meu problema é que as imagens estão em movimento constante (não são estáticas), portanto esse corte precisa ser dinâmico.

Existe uma maneira de alterar o código do sombreador para levar em consideração sua posição?


Como alternativa, eu li sobre o uso do Stencil Buffer, mas a maioria das amostras parece depender de um rendertarget, o que realmente não quero fazer. (Eu já estou usando 3 ou 4 para o resto do jogo, e adicionar outro em cima dele parece exagero)

O único tutorial que descobri que não usa os Rendertargets é o do blog de Shawn Hargreaves aqui. O problema com esse, no entanto, é que é para o XNA 3.1 e não parece ser bem traduzido para o XNA 4.0.

Parece-me que o pixel shader é o caminho a seguir, mas não tenho certeza de como obter o posicionamento correto. Acredito que precisaria alterar minhas coordenadas na tela (algo como 500, 500) para ficar entre 0 e 1 para as coordenadas do sombreador. Meu único problema é tentar descobrir como usar corretamente as coordenadas transformadas.

Agradecemos antecipadamente por qualquer ajuda!

electroflame
fonte
No entanto, o estêncil parece ser o caminho a seguir, também preciso saber usá-los adequadamente: P Estarei aguardando uma boa resposta sobre o assunto.
Gustavo Maciel
Heh, a maioria dos bons tutoriais do Stencil são para o XNA 3.1, e a maioria dos 4.0 usa RenderTargets (o que me parece um desperdício). Atualizei minha pergunta com um link para um antigo tutorial 3.1 que parecia funcionar, mas não no 4.0. Talvez alguém saiba como traduzi-lo para 4.0 corretamente?
electroflame
Se você usasse o pixel shader para realizá-lo, acho que teria que projetar sua coordenada de pixel para a tela / mundo (depende do que você deseja fazer), e isso é meio desperdício (o estêncil não teria esse problema)
Gustavo Maciel
Esta questão é uma excelente pergunta.
ClassicThunder

Respostas:

11

Você pode usar sua abordagem de pixel shader, mas ela está incompleta.

Você também precisará adicionar alguns parâmetros que informam ao pixel shader onde você deseja que seu estêncil esteja localizado.

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

No seu código C #, você passa variáveis ​​para o pixel shader antes da SpriteBatch.Drawchamada, da seguinte maneira:

maskEffect.Parameters["MaskWidth"].SetValue(800);
maskEffect.Parameters["MaskHeight"].SetValue(600);
Jim
fonte
Obrigado pela resposta! Parece que vai funcionar, mas atualmente estou tendo um problema. Atualmente, ele está sendo cortado à esquerda da imagem (com uma borda reta também). Eu deveria estar usando coordenadas de tela para as posições? Ou eu já deveria ter convertido para 0 - 1?
electroflame
Eu vejo o problema. Eu deveria ter compilado o pixel shader antes de compartilhá-lo. : P No maskCoordcálculo, um dos X deveria ser um Y. Editei minha resposta inicial com a correção e incluí as configurações da braçadeira UV que você precisará para o seu MaskTexture sampler.
Jim
1
Agora funciona perfeitamente, obrigado! A única coisa que devo mencionar é que, se você tiver seus sprites centralizados (usando Largura / 2 e Altura / 2 para a origem), precisará subtrair metade da Largura ou Altura para os locais X e Y (que você passar o shader). Depois disso, ele funciona perfeitamente e é extremamente rápido! Muito obrigado!
electroflame
18

imagem de exemplo

O primeiro passo é dizer à placa gráfica que precisamos do buffer de estêncil. Para fazer isso quando você cria GraphicsDeviceManager, definimos PreferredDepthStencilFormat como DepthFormat.Depth24Stencil8, para que haja realmente um estêncil no qual gravar.

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

O AlphaTestEffect é usado para definir o sistema de coordenadas e filtrar os pixels com alfa que passa no teste alfa. Não vamos definir nenhum filtro e definir o sistema de coordenadas para a porta de visualização.

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

Em seguida, precisamos configurar dois DepthStencilStates. Esses estados determinam quando o SpriteBatch é renderizado no estêncil e quando o SpriteBatch é renderizado no BackBuffer. Estamos interessados ​​principalmente em duas variáveis ​​StencilFunction e StencilPass.

  • StencilFunction determina quando o SpriteBatch irá desenhar pixels individuais e quando eles serão ignorados.
  • O StencilPass determina quando pixels desenhados pixels afetam o Stencil.

Para o primeiro DepthStencilState, definimos StencilFunction como CompareFunction. Isso faz com que o StencilTest tenha êxito e quando o StencilTest, o SpriteBatch, processa esse pixel. StencilPass está definido como StencilOperation. Substitua o significado de que, quando o StencilTest for bem-sucedido, o pixel será gravado no StencilBuffer pelo valor do ReferenceStencil.

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

Em resumo, o StencilTest sempre passa, a imagem é desenhada para a tela normalmente e, para os pixels desenhados na tela, um valor de 1 é armazenado no StencilBuffer.

O segundo DepthStencilState é um pouco mais complicado. Desta vez, queremos desenhar apenas na tela quando o valor no StencilBuffer for. Para isso, definimos StencilFunction como CompareFunction.LessEqual e ReferenceStencil como 1. Isso significa que quando o valor no buffer de estêncil for 1, o StencilTest será bem-sucedido. Definindo StencilPass como StencilOperation. Manter faz com que o StencilBuffer não atualize. Isso nos permite desenhar várias vezes usando a mesma máscara.

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

Em resumo, o StencilTest só passa quando o StencilBuffer é menor que 1 (os pixels alfa da máscara) e não afeta o StencilBuffer.

Agora que temos nossos DepthStencilStates configurados. Podemos realmente desenhar usando uma máscara. Simplesmente desenhe a máscara usando o primeiro DepthStencilState. Isso afetará o BackBuffer e o StencilBuffer. Agora que o buffer de estêncil tem um valor de 0 onde você mascara tinha transparência e 1 onde continha cor, podemos usar StencilBuffer para mascarar imagens posteriores.

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

O segundo SpriteBatch usa o segundo DepthStencilStates. Não importa o que você desenhe, apenas os pixels em que o StencilBuffer está definido como 1 passarão no teste de estêncil e serão atraídos para a tela.

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

Abaixo está a totalidade do código no método Draw, não se esqueça de definir PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8 no construtor do jogo.

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
fonte
1
+1 Este é um dos exemplos mais completos do StencilBuffer que eu já vi no XNA 4.0. Obrigado por isso! A única razão pela qual escolhi a resposta do pixel shader é porque era mais fácil de configurar (e realmente mais rápido de inicializar), mas essa também é uma resposta perfeitamente válida e pode ser mais fácil se você já tiver muitos shaders no lugar. Obrigado! Agora temos uma resposta completa para as duas rotas!
electroflame
Esta é a melhor solução real para mim, e mais do que a primeira
Mehdi Bugnard 17/10
Como eu poderia usar isso se eu tivesse um modelo 3D? Acho que isso poderia resolver minha pergunta gamedev.stackexchange.com/questions/93733/… , mas não sei como aplicá-lo em modelos 3D. Você sabe se eu posso fazer isso?
dimitris93
0

Na resposta acima , algumas coisas estranhas aconteceram quando eu fiz exatamente o que é explicado.

A única coisa que eu precisava era dos dois 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 os empates sem o 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 tudo estava bem como o esperado.

Aseu
fonte