วิธีที่ดีที่สุดในการปิดบังสไปรต์ 2D ใน XNA


24

ฉันกำลังพยายามปกปิดผีสางบ้าง แทนที่จะอธิบายเป็นคำฉันได้สร้างภาพตัวอย่าง:

นำมาพอกบริเวณที่ บริเวณที่สวมหน้ากาก (สีขาว)

พื้นที่ที่จะมาส์กโดยมีรูปภาพเพื่อมาส์ก ทีนี้สไปรท์สีแดงที่ต้องถูกครอบตัด

ผลสุดท้าย ผลสุดท้าย

ตอนนี้ฉันทราบแล้วว่าใน XNA คุณสามารถทำสองสิ่งเพื่อบรรลุเป้าหมายนี้:

  1. ใช้ Buffer ฉลุ
  2. ใช้ Pixel Shader

ฉันได้ลองทำพิกเซลเชดเดอร์ซึ่งทำสิ่งนี้เป็นหลัก:

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

ดูเหมือนว่าจะครอบตัดรูปภาพ (ถึงแม้ว่าจะไม่ถูกต้องเมื่อรูปภาพเริ่มเคลื่อนไหว) แต่ปัญหาของฉันคือภาพเคลื่อนไหวตลอดเวลา (ไม่คงที่) ดังนั้นการครอบตัดนี้ต้องเป็นแบบไดนามิก

มีวิธีที่ฉันสามารถเปลี่ยนรหัส shader เพื่อพิจารณาว่าเป็นตำแหน่งหรือไม่


อีกวิธีหนึ่งคือฉันได้อ่านเกี่ยวกับการใช้ฉลุบัฟเฟอร์ แต่ตัวอย่างส่วนใหญ่ดูเหมือนจะใช้การเรนเดอร์เป้าหมายซึ่งฉันไม่ต้องการทำ (ฉันใช้เกม 3 หรือ 4 เกมที่เหลืออยู่แล้วและเพิ่มเกมอื่นที่ด้านบนของเกม)

กวดวิชาเดียวที่ฉันพบว่าไม่ได้ใช้ Rendertargets เป็นหนึ่งในบล็อกของ Shawn Hargreaves ที่นี่ ปัญหาของอันนั้นก็คือสำหรับ XNA 3.1 และดูเหมือนจะแปลได้ไม่ดีนักกับ XNA 4.0

สำหรับฉันแล้วดูเหมือนว่า pixel shader เป็นวิธีที่จะไป แต่ฉันไม่แน่ใจว่าจะทำให้การวางตำแหน่งถูกต้องได้อย่างไร ฉันเชื่อว่าฉันจะต้องเปลี่ยนพิกัดบนหน้าจอของฉัน (เช่น 500, 500) เพื่อให้อยู่ระหว่าง 0 ถึง 1 สำหรับพิกัด shader ปัญหาเดียวของฉันคือพยายามหาวิธีใช้พิกัดที่แปลงแล้วอย่างถูกต้อง

ขอบคุณล่วงหน้าสำหรับความช่วยเหลือใด ๆ !


ลายฉลุดูเหมือนจะเป็นวิธีที่จะไป แต่ฉันจำเป็นต้องรู้วิธีการใช้อย่างถูกต้องเช่นกัน: P ฉันจะรอคำตอบที่ดีเกี่ยวกับเรื่องนี้
Gustavo Maciel

เฮ้บทเรียนสอนการฉลุที่ดีส่วนใหญ่ใช้สำหรับ XNA 3.1 และ 4.0 ส่วนใหญ่ใช้ RenderTargets (ซึ่งดูเหมือนว่าสิ้นเปลืองสำหรับฉัน) ฉันได้อัปเดตคำถามของฉันพร้อมลิงก์ไปยังบทช่วยสอน 3.1 ที่ดูเหมือนว่าใช้งานได้ แต่ไม่ใช่ใน 4.0 อาจมีคนรู้วิธีการแปลเป็น 4.0 อย่างถูกต้อง?
electroflame

หากคุณใช้ Pixel Shader เพื่อทำให้สำเร็จฉันคิดว่าคุณต้องยกเลิกการประสานงานพิกเซลของหน้าจอ / โลก (ขึ้นอยู่กับสิ่งที่คุณต้องการจะทำ) และนี่เป็นสิ่งที่สิ้นเปลือง (ลายฉลุจะไม่มีปัญหานี้)
Gustavo Maciel

คำถามนี้เป็นคำถามที่ยอดเยี่ยม
ClassicThunder

คำตอบ:


11

คุณสามารถใช้วิธี Pixel Shader แต่ไม่สมบูรณ์

นอกจากนี้คุณยังจะต้องเพิ่มพารามิเตอร์บางอย่างเพื่อแจ้ง pixel shader ที่คุณต้องการให้ stencil ของคุณอยู่

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 อยู่แล้ว?
electroflame

ฉันเห็นปัญหา ฉันควรรวบรวม pixel shader ก่อนที่จะแชร์ : P ในmaskCoordหนึ่งในการคำนวณของ X ที่ควรจะได้รับวายผมได้แก้ไขคำตอบของฉันเริ่มต้นด้วยการแก้ไขและรวมถึงการตั้งค่าการยึด UV MaskTexture samplerคุณจะต้องสำหรับคุณ
Jim

1
ตอนนี้มันทำงานได้อย่างสมบูรณ์ขอบคุณ! สิ่งเดียวที่ฉันควรพูดถึงคือถ้าคุณมีสไปรต์อยู่กึ่งกลาง (โดยใช้ความกว้าง / 2 และความสูง / 2 สำหรับแหล่งกำเนิด) คุณจะต้องลบความกว้างหรือความสูงครึ่งหนึ่งสำหรับตำแหน่ง X และ Y ของคุณ (ที่คุณผ่านเข้าไป shader) หลังจากนั้นก็ทำงานได้อย่างสมบูรณ์และรวดเร็วมาก! ขอบคุณตัน!
electroflame

18

ภาพตัวอย่าง

ขั้นตอนแรกคือการบอกกราฟิกการ์ดว่าเราต้องการบัฟเฟอร์ลายฉลุ ในการทำเช่นนี้เมื่อคุณสร้าง GraphicsDeviceManager เราได้ทำการตั้งค่า PreferredDepthStencilFormat เป็น DepthFormat.Depth24Stencil8 ดังนั้นจึงมี stencil สำหรับเขียน

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 แสดงผลเป็น stencil และเมื่อ 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 ซึ่งหมายความว่าเมื่อค่าในบัฟเฟอร์ stencil เท่ากับ 1 theStarTestTest จะประสบความสำเร็จ การตั้งค่า StencilPass เป็น TencOperation Keep ทำให้ SignatureBuffer ไม่อัปเดต สิ่งนี้ทำให้เราสามารถวาดได้หลายครั้งโดยใช้มาสก์เดียวกัน

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

กล่าวโดยสรุปเครื่องมือตรวจสอบ StencilTest จะส่งผ่านเฉพาะเมื่อ StencilBuffer น้อยกว่า 1 (พิกเซลอัลฟาจากมาสก์) และจะไม่มีผลกับเครื่องมือนี้

ตอนนี้เราได้ตั้งค่า DepthStencilStates ของเราแล้ว จริงๆแล้วเราสามารถวาดโดยใช้หน้ากาก เพียงวาดหน้ากากโดยใช้ DepthStencilState ตัวแรก สิ่งนี้จะมีผลกับทั้ง BackBuffer และ StencilBuffer ตอนนี้บัฟเฟอร์ stencil มีค่าเป็น 0 ซึ่งคุณ mask มีความโปร่งใสและ 1 ซึ่งมีสีที่เราสามารถใช้ StencilBuffer เพื่อปกปิดรูปภาพในภายหลัง

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

SpriteBatch ตัวที่สองใช้ DepthStencilStates ตัวที่สอง ไม่ว่าคุณจะวาดอะไรมีเพียงพิกเซลที่กำหนดให้ TencelBuffer เป็น 1 เท่านั้นที่จะผ่านการทดสอบ stencil และวาดลงบนหน้าจอ

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

ด้านล่างนี้คือรหัสทั้งหมดในวิธีการวาดอย่าลืมตั้งค่า PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8 ใน Constructor ของเกม

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

1
+1 นี่คือหนึ่งในตัวอย่างเครื่องมือที่สมบูรณ์ที่สุดที่ฉันเคยเห็นสำหรับ XNA 4.0 ขอบคุณสำหรับสิ่งนี้! เหตุผลเดียวที่ฉันเลือกคำตอบ pixel shader ก็คือมันง่ายกว่าในการตั้งค่า (และบูตได้เร็วกว่าจริง ๆ ) แต่นี่เป็นคำตอบที่ถูกต้องสมบูรณ์เช่นกันและอาจจะง่ายขึ้นหากคุณมีเฉดสีมากมาย ขอบคุณ! ตอนนี้เรามีคำตอบที่สมบูรณ์สำหรับทั้งสองเส้นทาง!
electroflame

นี่คือทางออกที่แท้จริงที่ดีที่สุดสำหรับฉันและมากกว่าครั้งแรก
Mehdi Bugnard

ฉันจะใช้มันอย่างไรถ้าฉันมีโมเดล 3 มิติ? ฉันคิดว่านี่สามารถแก้คำถามของฉันได้ที่gamedev.stackexchange.com/questions/93733/แต่ฉันไม่รู้วิธีใช้กับโมเดล 3 มิติ คุณรู้หรือไม่ว่าฉันสามารถทำเช่นนั้นได้?
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();

และทุกอย่างก็ดีเหมือนที่คาดไว้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.