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