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