1
\$\begingroup\$

I'm looking to create an effect like this in a game I'm making:

Camera Aperture Closing

I would describe it as a camera aperture closing.

I have the following requirements:

  1. I need to be able to reverse the fade (open the aperture instead of close it)
  2. The hole should close/open towards/from a given center point.

I can't really think of a great way to do this. I'm so used to SpriteBatch and Texture2D that I don't know how I could draw an ellipse style effect.

My current thoughts would be to make a few Texture2D frames and then animated them on the screen but that seems crazy.

If I just wanted to create pseudo-code to do it, it would probably look something like this (coming from a GDI/GDI+ background):

Call: Create_Relevant_Ellipse_Bounds;
Call: Set_Graphics_Exclusion_Clip_From_Ellipse;
Call: Fill_BackColor_With_Black_Using_Clip;
Call: Update_Ellipse_Bounds;
Call: Wait_For_Next_Draw;
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

A point p is inside a circle at c with radius r when

(p.x - c.x)^2 + (p.y - c.y)^2 < r^2

is true. Using this equation you can color any points inside a circle one color (such as "transparent") and anything outside the circle a different color (such as "black"). You can even fade the edges of the circle, if you are so inclined.

You can write a custom shader to do this, and apply it to a full-screen quad. Set up shader constants defining the center of the circle and the current radius, and draw your quad. Every frame, decrease the radius by some factor to animate the circle getting smaller.

SV_Position contains the fragment position in D3D10 and up, but it's not available in D3D9 (which is what XNA is based on). Instead you can use the VPOS semantic (more information here). This will give you the position of p for the above equation. c and r come from your shader constants.

You can use the step function to return 0 or 1 based on which side of the equation is greater. This will allow you to avoid an actual branch in the shader, so you can compute the alpha component of your final color like this:

// alpha will be 1 if (r * r) is greater, meaning c is outside the radius.
// Ptherwise it will be zero.
float alpha = step(pow(p.x - c.x, 2) + pow(p.y - c.y, 2), r * r)

return (0,0,0,alpha);

If you didn't have SV_Position or VPOS available, you could always just put texture coordinates in the quad vertices. These will be interpolated across the face of the quad during rasterization, and can be used as an approximate coordinate system for specifying the position and radius of the camera aperture.

\$\endgroup\$
5
  • \$\begingroup\$ Wouldn't I have to do return (0,0,0,alpha*255); ? So it'd be black or completely transparent? \$\endgroup\$
    – test
    Commented Oct 21, 2014 at 15:20
  • \$\begingroup\$ Also, what is the x variable in x.y? Should that be (p.y - p.x)? \$\endgroup\$
    – test
    Commented Oct 21, 2014 at 15:32
  • 1
    \$\begingroup\$ In shaders, color component values are generally represented as being in the range 0 to 1. The "x.y" is a typo and should be "c.y" instead; I'll fix it. \$\endgroup\$
    – user1430
    Commented Oct 21, 2014 at 15:42
  • \$\begingroup\$ I know you mentioned the VPOS semantic but every quad example for XNA doesn't use this semantic and it isn't supported for vertex shaders. Every example I'm seeing uses a vertex shader and pixel shader to do this. I don't see how I could use the VPOS semantic (I'm not sure it's needed?) \$\endgroup\$
    – test
    Commented Oct 21, 2014 at 16:30
  • 1
    \$\begingroup\$ You need it in the pixel shader, where it should be supported. If you don't want to use it, you can just use the texture coordinates - just write the transformed positions of the vertices as output from the vertex shader, and use those as the coordinate system in the pixel shader. \$\endgroup\$
    – user1430
    Commented Oct 21, 2014 at 16:33
1
\$\begingroup\$

Thanks @JoshPetrie for getting me on the right track. I know XNA is basically dead but I could not find a good sample to do something that should be simple. Below I've posted all relevant code to perform an aperture close or open effect.

Before you run, add a sprite called Background to your project. The result of the code should be an aperture closing and opening over the center of your background sprite.

Game1.cs

public class Game1 : Microsoft.Xna.Framework.Game
{

    #region fields

    GraphicsDeviceManager graphics;
    Effect apertureEffect;
    QuadRenderer quadRenderer;
    SpriteBatch spriteBatch;
    Texture2D backgroundSprite;

    // flag to indicate direction of aperture effect
    private bool open = true;

    #endregion

    #region constructors

    public Game1()
    {
        this.graphics = new GraphicsDeviceManager(this);

        base.Content.RootDirectory = "Content";
    }

    #endregion

    #region methods

    protected override void LoadContent()
    {
        this.spriteBatch = new SpriteBatch(base.GraphicsDevice);
        this.backgroundSprite = base.Content.Load<Texture2D>("Background");
        this.apertureEffect = base.Content.Load<Effect>("Aperture");
        this.quadRenderer = new QuadRenderer(base.GraphicsDevice);

        // center screen for circle start
        float x = base.GraphicsDevice.Viewport.Width / 2f;
        float y = base.GraphicsDevice.Viewport.Height / 2f;

        this.apertureEffect.Parameters["c"].SetValue(new float[] { x, y });
        this.apertureEffect.Parameters["r"].SetValue(10f);

    }

    protected override void Update(GameTime gameTime)
    {
        base.Update(gameTime);

        float radius = this.apertureEffect.Parameters["r"].GetValueSingle();

        if (this.open && radius > (base.GraphicsDevice.Viewport.Height / 2f))
            this.open = false;
        else if (!this.open && radius <= 0f)
            this.open = true;

        // adjust increment for "shutter speed"
        float increment = 1.0f;
        if (!this.open)
            increment = -increment;

        this.apertureEffect.Parameters["r"].SetValue(radius + increment);
    }

    protected override void Draw(GameTime gameTime)
    {
        base.GraphicsDevice.Clear(Color.CornflowerBlue);

        // render any background sprite to showcase transparency
        // if no background sprite is rendered you will only see black
        this.spriteBatch.Begin();
        this.spriteBatch.Draw(this.backgroundSprite,
                              new Rectangle(0, 0,
                                            base.GraphicsDevice.Viewport.Width,
                                            base.GraphicsDevice.Viewport.Height),
                              Color.White);
        this.spriteBatch.End();

        // render the full-screen quad
        this.quadRenderer.Render(this.apertureEffect);

        base.Draw(gameTime);
    }

    #endregion

}

QuadRenderer.cs

internal sealed class QuadRenderer
{

    #region fields

    private VertexPositionTexture[] triangles;
    private GraphicsDevice device;
    private short[] indexData = new short[] { 0, 1, 2, 2, 3, 0 };

    #endregion

    #region constructors

    public QuadRenderer(GraphicsDevice device)
    {
        this.device = device;

        // texture coordinates semantic not used or needed
        this.triangles = new VertexPositionTexture[] 
                         { 
                           new VertexPositionTexture(new Vector3(1, -1, 0), 
                                                     Vector2.Zero),
                           new VertexPositionTexture(new Vector3(-1, -1, 0), 
                                                     Vector2.Zero),
                           new VertexPositionTexture(new Vector3(-1, 1, 0), 
                                                     Vector2.Zero),
                           new VertexPositionTexture(new Vector3(1, 1, 0), 
                                                     Vector2.Zero) 
                         };
    }

    #endregion

    #region methods

    public void Render(Effect effect)
    {
        foreach (EffectPass p in effect.CurrentTechnique.Passes)
            p.Apply();

        this.Render();
    }

    private void Render()
    {
        this.device.DrawUserIndexedPrimitives(PrimitiveType.TriangleList,
                                              this.triangles, 0, 4,
                                              this.indexData, 0, 2);
    }

    #endregion

}

Aperture.fx

// center point of circle
float2 c;

// radius of circle
float r;

float4 ShadeVertex(float3 pos : POSITION0) : POSITION0
{
    return float4(pos, 1);
}

float4 ShadePixel(float2 p : VPOS) : COLOR0
{
    float alpha = step(r * r, pow(p.x - c.x, 2) + pow(p.y - c.y, 2));

    // black or transparent
    return float4(0, 0, 0, alpha);
}

technique Simple  
{
    pass FirstPass  
    {
        VertexShader = compile vs_3_0 ShadeVertex();  
        PixelShader = compile ps_3_0 ShadePixel();  
    }
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .