¿La mejor manera de enmascarar sprites 2D en XNA?

24

Actualmente estoy tratando de enmascarar algunos sprites. En lugar de explicarlo en palabras, he inventado algunas imágenes de ejemplo:

El área para enmascarar El área a enmascarar (en blanco)

El área para enmascarar, con la imagen para enmascarar Ahora, el sprite rojo que necesita ser recortado.

El resultado final El resultado final.

Ahora, sé que en XNA puedes hacer dos cosas para lograr esto:

  1. Usa el tampón de plantilla.
  2. Utiliza un sombreador de píxeles.

Intenté hacer un sombreador de píxeles, que esencialmente hizo esto:

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

Esto parece recortar las imágenes (aunque no es correcto una vez que la imagen comienza a moverse), pero mi problema es que las imágenes se mueven constantemente (no son estáticas), por lo que este recorte debe ser dinámico.

¿Hay alguna forma de alterar el código del sombreador para tener en cuenta su posición?


Alternativamente, he leído sobre el uso del Stencil Buffer, pero la mayoría de las muestras parecen depender del uso de un rendertarget, lo que realmente no quiero hacer. (Ya estoy usando 3 o 4 para el resto del juego, y agregar otro encima parece excesivo)

El único tutorial que he encontrado que no usa Rendertargets es uno del blog de Shawn Hargreaves aquí. Sin embargo, el problema con eso es que es para XNA 3.1 y no parece traducirse bien a XNA 4.0.

Me parece que el sombreador de píxeles es el camino a seguir, pero no estoy seguro de cómo obtener el posicionamiento correcto. Creo que tendría que cambiar mis coordenadas en pantalla (algo así como 500, 500) para estar entre 0 y 1 para las coordenadas del sombreador. Mi único problema es tratar de averiguar cómo usar correctamente las coordenadas transformadas.

¡Gracias de antemano por cualquier ayuda!

electroflame
fuente
Stencil parece ser el camino a seguir, sin embargo, también necesito saber cómo usarlos correctamente: P Esperaré una buena respuesta al respecto.
Gustavo Maciel
Je, la mayoría de los buenos tutoriales de Stencil son para XNA 3.1, y la mayoría de los 4.0 usan RenderTargets (lo cual me parece un desperdicio). He actualizado mi pregunta con un enlace a un antiguo tutorial 3.1 que parecía que podría funcionar, pero no en 4.0. ¿Quizás alguien sabe cómo traducirlo a 4.0 correctamente?
electroflame
Si usaras el sombreador de píxeles para lograrlo, creo que tendrías que desproyectar tu coordenada de píxeles a la pantalla / mundo (depende de lo que quieras hacer), y esto es un poco inútil (la plantilla no tendría este problema)
Gustavo Maciel
Esta pregunta es una excelente pregunta.
ClassicThunder

Respuestas:

11

Puede usar su enfoque de sombreador de píxeles, pero está incompleto.

También deberá agregarle algunos parámetros que informan al sombreador de píxeles donde desea ubicar su plantilla.

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

En su código C #, pasa variables al sombreador de píxeles antes de la SpriteBatch.Drawllamada de esta manera:

maskEffect.Parameters["MaskWidth"].SetValue(800);
maskEffect.Parameters["MaskHeight"].SetValue(600);
Jim
fuente
¡Gracias por la respuesta! Parece que funcionará, pero actualmente tengo un problema. Actualmente, se está recortando a la izquierda de la imagen (también con un borde recto). ¿Se supone que debo usar coordenadas de pantalla para las posiciones? ¿O ya se supone que los he convertido a 0 - 1?
electroflame
Veo el problema Debería haber compilado el sombreador de píxeles antes de compartirlo. : P En el maskCoordcálculo, una de las X debería haber sido una Y. Edité mi respuesta inicial con la corrección e incluí la configuración de la abrazadera UV que necesitará para su MaskTexture sampler.
Jim
1
Ahora funciona perfectamente, gracias! Lo único que debo mencionar es que si tienes tus sprites centrados (usando Ancho / 2 y Altura / 2 para el origen) necesitarás restar la mitad de tu Ancho o Altura para tus ubicaciones X e Y (que pasas a el sombreador) ¡Después de eso funciona perfectamente y es extremadamente rápido! ¡Gracias una tonelada!
electroflame
18

imagen de ejemplo

El primer paso es decirle a la tarjeta gráfica que necesitamos el búfer de la plantilla. Para hacer esto cuando crea GraphicsDeviceManager, configuramos PreferredDepthStencilFormat en DepthFormat.Depth24Stencil8 para que realmente haya una plantilla para escribir.

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

AlphaTestEffect se utiliza para establecer el sistema de coordenadas y filtrar los píxeles con alfa que pasan la prueba alfa. No vamos a establecer ningún filtro y establecer el sistema de coordenadas en el puerto de vista.

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

A continuación, debemos configurar dos DepthStencilStates. Estos estados dictan cuándo SpriteBatch se procesa en la plantilla y cuándo SpriteBatch se procesa en BackBuffer. Estamos principalmente interesados ​​en dos variables StencilFunction y StencilPass.

  • StencilFunction dicta cuándo SpriteBatch dibujará píxeles individuales y cuándo serán ignorados.
  • StencilPass dicta cuándo los píxeles dibujados y los píxeles afectan a la plantilla.

Para el primer DepthStencilState configuramos StencilFunction en CompareFunction. Esto hace que el StencilTest tenga éxito y cuando el StencilTest el SpriteBatch procesa ese píxel. StencilPass está establecido en StencilOperation. Reemplazar lo que significa que cuando el StencilTest tenga éxito, ese píxel se escribirá en el StencilBuffer con el valor del ReferenceStencil.

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

En resumen, el StencilTest siempre pasa, la imagen se dibuja en la pantalla normalmente y para los píxeles dibujados en la pantalla se almacena un valor de 1 en el StencilBuffer.

El segundo DepthStencilState es un poco más complicado. Esta vez solo queremos dibujar en la pantalla cuando el valor en StencilBuffer es. Para lograr esto, configuramos StencilFunction en CompareFunction.LessEqual y ReferenceStencil en 1. Esto significa que cuando el valor en el búfer de stencil es 1, el StencilTest tendrá éxito. Establecer StencilPass en StencilOperation. Keep hace que el StencilBuffer no se actualice. Esto nos permite dibujar varias veces usando la misma máscara.

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

En resumen, el StencilTest solo pasa cuando el StencilBuffer es menor que 1 (los píxeles alfa de la máscara) y no afecta al StencilBuffer.

Ahora que tenemos configurados nuestros DepthStencilStates. De hecho, podemos dibujar con una máscara. Simplemente dibuje la máscara con el primer DepthStencilState. Esto afectará tanto al BackBuffer como al StencilBuffer. Ahora que el búfer de plantilla tiene un valor de 0 donde la máscara tenía transparencia y 1 donde contenía color, podemos usar StencilBuffer para enmascarar imágenes posteriores.

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

El segundo SpriteBatch usa el segundo DepthStencilStates. No importa lo que dibuje, solo los píxeles en los que StencilBuffer esté configurado en 1 pasarán la prueba de la plantilla y se dibujarán en la pantalla.

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

A continuación se muestra la totalidad del código en el método Draw, no olvide establecer PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8 en el constructor del juego.

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();
Trueno clásico
fuente
1
+1 Este es uno de los ejemplos de StencilBuffer más completos que he visto para XNA 4.0. ¡Gracias por esto! La única razón por la que elegí la respuesta del sombreador de píxeles es porque fue más fácil de configurar (y en realidad más rápido de iniciar), pero esta es una respuesta perfectamente válida también y puede ser más fácil si ya tiene muchos sombreadores en su lugar. ¡Gracias! ¡Ahora tenemos una respuesta completa para ambas rutas!
electroflame
Esta es la mejor solución real para mí, y más que la primera
Mehdi Bugnard
¿Cómo podría usar eso si tuviera un modelo 3D? Creo que esto podría resolver mi pregunta gamedev.stackexchange.com/questions/93733/… , pero no sé cómo aplicarlo en modelos 3D. ¿Sabes si puedo hacer eso?
dimitris93
0

En la respuesta anterior , sucedieron algunas cosas raras cuando hice exactamente lo que se explica.

Lo único que necesitaba eran los dos 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,
};

Y los sorteos sin el 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();

Y todo estuvo bien como se esperaba.

Aseu
fuente