Лучший способ маскировать 2D спрайты в XNA?

24

В настоящее время я пытаюсь замаскировать некоторые спрайты. Вместо того, чтобы объяснить это словами, я составил несколько иллюстраций:

Площадь для маскировки Область для маскировки (белого цвета)

Область для маскировки, с изображением для маскировки Теперь красный спрайт, который нужно обрезать.

Конечный результат Конечный результат.

Теперь я знаю, что в XNA вы можете сделать две вещи для достижения этой цели:

  1. Используйте трафаретный буфер.
  2. Используйте пиксельный шейдер.

Я попытался сделать пиксельный шейдер, который по сути сделал это:

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

Кажется, что это обрезает изображения (хотя и не правильно, как только изображение начинает двигаться), но моя проблема в том, что изображения постоянно движутся (они не статичны), поэтому обрезка должна быть динамичной.

Есть ли способ, которым я мог бы изменить код шейдера, чтобы учесть его положение?


Кроме того, я читал об использовании буфера трафарета, но большинство примеров, похоже, зависят от цели рендеринга, чего я действительно не хочу делать. (Я уже использую 3 или 4 для остальной части игры, и добавление еще одного сверху кажется излишним)

Единственное учебное пособие, которое я нашел, но не использует Rendertargets, - это одно из блога Шона Харгривза здесь. Проблема с этим, однако, в том, что он для XNA 3.1, и, похоже, не очень хорошо подходит для XNA 4.0.

Мне кажется, что пиксельный шейдер - это путь, но я не уверен, как правильно расположить его. Я считаю, что мне придется изменить мои экранные координаты (что-то вроде 500, 500), чтобы быть между 0 и 1 для координат шейдера. Моя единственная проблема - попытаться понять, как правильно использовать преобразованные координаты.

Заранее благодарю за любую помощь!

электроогневая
источник
Трафарет, кажется, является подходящим вариантом, хотя мне нужно знать, как правильно их использовать: P Я буду ждать хорошего ответа по этому вопросу.
Густаво Масиэль
Хех, большинство хороших учебников по трафарету предназначено для XNA 3.1, а большинство из 4.0 использует RenderTargets (что мне кажется расточительным). Я обновил свой вопрос ссылкой на старый учебник 3.1, который, казалось, мог работать, но не в 4.0. Может кто знает, как правильно перевести его на 4.0?
электропламин
Если бы вы использовали пиксельный шейдер для этого, я думаю, вам придется отменить проекцию вашей пиксельной координаты на экран / мир (зависит от того, что вы хотите сделать), и это довольно расточительно (у трафарета такой проблемы не будет)
Густаво Масиэль
Этот вопрос отличный вопрос.
ClassicThunder,

Ответы:

11

Вы можете использовать свой подход пиксельных шейдеров, но он неполон.

Вам также нужно будет добавить в него некоторые параметры, которые сообщат пиксельному шейдеру, где вы хотите разместить трафарет.

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

В вашем коде C # вы передаете переменные в пиксельный шейдер до SpriteBatch.Drawвызова следующим образом:

maskEffect.Parameters["MaskWidth"].SetValue(800);
maskEffect.Parameters["MaskHeight"].SetValue(600);
Джим
источник
Спасибо за ответ! Кажется, что это будет работать, но в настоящее время у меня есть проблема с этим. В настоящее время он обрезается слева от изображения (также с прямым краем). Я должен использовать координаты экрана для позиций? Или я уже должен был конвертировать их в 0 - 1?
электрокамин
Я вижу проблему. Я должен был скомпилировать пиксельный шейдер, прежде чем делиться им. : P В maskCoordрасчете один из X должен был быть Y. Я отредактировал свой первоначальный ответ с исправлением и включил настройки УФ-зажима, которые вам понадобятся MaskTexture sampler.
Джим
1
Теперь это работает отлично, спасибо! Единственное, что я должен упомянуть, это то, что если ваши спрайты центрированы (используя Width / 2 и Height / 2 для начала координат), вам нужно будет вычесть половину вашей Width или Height для ваших X и Y местоположений (которые вы передаете в шейдер). После этого он работает отлично и очень быстро! Благодаря тонну!
электропламин
18

пример изображения

Первый шаг - сообщить графической карте, что нам нужен буфер трафарета. Чтобы сделать это при создании GraphicsDeviceManager, мы устанавливаем для PreferredDepthStencilFormat значение DepthFormat.Depth24Stencil8, поэтому на самом деле существует трафарет для записи.

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

AlphaTestEffect используется для установки системы координат и фильтрации пикселей с альфа-каналом, которые проходят альфа-тестирование. Мы не собираемся устанавливать какие-либо фильтры и устанавливать систему координат для порта просмотра.

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

Далее нам нужно настроить два DepthStencilStates. Эти состояния определяют, когда SpriteBatch рендерится по трафарету и когда SpriteBatch рендерится в BackBuffer. Нас в первую очередь интересуют две переменные StencilFunction и StencilPass.

  • StencilFunction определяет, когда SpriteBatch будет рисовать отдельные пиксели и когда они будут игнорироваться.
  • StencilPass определяет, когда пиксели пикселя влияют на трафарет.

Для первого DepthStencilState мы устанавливаем StencilFunction в CompareFunction. Это приводит к успешному выполнению StencilTest, и когда StencilTest SpriteBatch отображает этот пиксель. StencilPass установлен в StencilOperation. Замените значение, означающее, что при успешном выполнении StencilTest этот пиксель будет записан в StencilBuffer со значением ReferenceStencil.

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

Таким образом, StencilTest всегда проходит, изображение отображается на экране в обычном режиме, а для пикселей, отображаемых на экране, значение 1 сохраняется в StencilBuffer.

Второй DepthStencilState немного сложнее. На этот раз мы хотим рисовать только на экране, когда значение в StencilBuffer равно. Чтобы достичь этого, мы устанавливаем StencilFunction в CompareFunction.LessEqual и ReferenceStencil в 1. Это означает, что когда значение в буфере трафарета равно 1, StencilTest будет успешным. Установка StencilPass в StencilOperation. Keep заставляет StencilBuffer не обновляться. Это позволяет нам рисовать несколько раз, используя одну и ту же маску.

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

Таким образом, StencilTest проходит только тогда, когда StencilBuffer меньше 1 (альфа-пиксели от маски) и не влияет на StencilBuffer.

Теперь, когда у нас настроены DepthStencilStates. Мы можем рисовать с помощью маски. Просто нарисуйте маску, используя первый DepthStencilState. Это повлияет как на BackBuffer, так и на StencilBuffer. Теперь, когда буфер трафарета имеет значение 0, где вы маскируете прозрачность, и 1, где оно содержит цвет, мы можем использовать StencilBuffer для маскировки последующих изображений.

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

Второй SpriteBatch использует второй DepthStencilStates. Независимо от того, что вы рисуете, только пиксели, для которых StencilBuffer установлен в 1, пройдут проверку трафарета и будут отображены на экране.

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

Ниже приведен полный код метода Draw, не забудьте установить PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8 в конструкторе игры.

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
источник
1
+1 Это один из самых полных примеров StencilBuffer, которые я видел для XNA 4.0. Спасибо за это! Единственная причина, по которой я выбрал ответ на пиксельный шейдер, заключается в том, что его было проще настроить (и на самом деле быстрее загружать), но это также совершенно правильный ответ, который может быть проще, если у вас уже есть много шейдеров. Благодарность! Теперь у нас есть полный ответ для обоих маршрутов!
электропламин
Это лучшее реальное решение для меня, и больше, чем первое
Mehdi Bugnard
Как я могу использовать это, если у меня была 3D-модель? Я думаю, что это может решить мой вопрос gamedev.stackexchange.com/questions/93733/… , но я не знаю, как применить его на 3D-моделях. Ты знаешь, смогу ли я это сделать?
dimitris93
0

В ответе выше некоторые странные вещи произошли, когда я сделал именно так, как объяснено.

Единственное, что мне было нужно, это оба 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,
};

И ничья без 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();

И все было хорошо, как и ожидалось.

АГЭУ
источник