How to: Draw a Sprite Over a Model
In this example, we are drawing an animated sprite representing an explosion over the current screen position of a 3D model. This example presumes you already have a model, a camera, and an animated sprite loaded. See How to: Animate a Sprite for an example of the AnimatedTexture class.
To draw a sprite over a model
-
In your Update method, handle input to move your camera, then call UpdateFrame on the AnimatedTexture.
protected override void Update( GameTime gameTime ) { // TODO: Add your update logic here GamePadState PlayerOne = GamePad.GetState( PlayerIndex.One ); // Move the camera using thumbsticks MoveCamera( PlayerOne ); // Start or stop the animated sprite using buttons if (PlayerOne.Buttons.A == ButtonState.Pressed) explosion.Play(); if (PlayerOne.Buttons.B == ButtonState.Pressed) explosion.Stop(); // Update the animated sprite explosion.UpdateFrame( (float)gameTime.ElapsedGameTime.TotalSeconds ); -
Use CreateMerged to create a BoundingSphere that contains all the BoundingSphere values for each ModelMesh in the Model. Then use Viewport.Project to find the centerpoint of that sphere - that is the center of the model in screen coordinates.
// Create a total bounding sphere for the mesh BoundingSphere totalbounds = new BoundingSphere(); foreach (ModelMesh mesh in Ring.Meshes) { totalbounds = BoundingSphere.CreateMerged( totalbounds, mesh.BoundingSphere ); } // Project the center of the 3D object to the screen, and center the // sprite there Vector3 center = graphics.GraphicsDevice.Viewport.Project( totalbounds.Center, projectionMatrix, Camera1.ViewMatrix, Matrix.Identity ); explosionpos.X = center.X; explosionpos.Y = center.Y; -
Take the BoundingSphere for the model and use that to create a BoundingBox with CreateFromSphere. Then find the corner of the box that is farthest from the center using Project, and use that to scale the sprite appropriately.
// Create a bounding box from the bounding sphere, and find the corner // that is farthest away from the center using Project BoundingBox extents = BoundingBox.CreateFromSphere( totalbounds ); float maxdistance = 0; float distance; Vector3 screencorner; foreach (Vector3 corner in extents.GetCorners()) { screencorner = graphics.GraphicsDevice.Viewport.Project( corner, projectionMatrix, Camera1.ViewMatrix, Matrix.Identity ); distance = Vector3.Distance( screencorner, center ); if (distance > maxdistance) maxdistance = distance; } // Scale the sprite using the two points (the sprite is // 75 pixels square) explosion.Scale = maxdistance / 75; base.Update( gameTime ); } -
In your Draw method, draw the Model normally, then draw the animated sprite using the position calculated in Update.
protected override void Draw( GameTime gameTime ) { graphics.GraphicsDevice.Clear( Color.CornflowerBlue ); //Draw the model, a model can have multiple meshes, so loop foreach (ModelMesh mesh in Ring.Meshes) { //This is where the mesh orientation is set, as well as our camera and projection foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = Matrix.Identity * Matrix.CreateRotationY( RingRotation ) * Matrix.CreateTranslation( RingPosition ); effect.View = Camera1.ViewMatrix; effect.Projection = projectionMatrix; } //Draw the mesh, will use the effects set above. mesh.Draw(); } // Draw the sprite over the 3D object batch.Begin( SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred, SaveStateMode.SaveState ); explosion.DrawFrame( batch, explosionpos ); batch.End(); base.Draw( gameTime ); }
The Complete Example
using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; Model Ring; float RingRotation; Vector3 RingPosition; SampleArcBallCamera Camera1; Matrix projectionMatrix; SpriteBatch batch; AnimatedTexture explosion; Vector2 explosionpos; public Game1() { graphics = new GraphicsDeviceManager( this ); content = new ContentManager( Services ); Camera1 = new SampleArcBallCamera( SampleArcBallCameraMode.Free ); Camera1.Target = new Vector3( 0, 0, 0 ); Camera1.Distance = 100f; Camera1.OrbitRight( MathHelper.PiOver4 ); Camera1.OrbitUp( MathHelper.PiOver4 ); projectionMatrix = Matrix.CreatePerspectiveFieldOfView( MathHelper.PiOver4, 4.0f / 3.0f, 1.0f, 10000f ); RingPosition = Vector3.Zero; RingRotation = 0.0f; explosion = new AnimatedTexture( new Vector2( 75 ), 0, 1.0f, 1.0f ); } protected override void Initialize() { base.Initialize(); } protected override void LoadGraphicsContent( bool loadAllContent ) { if (loadAllContent) { Ring = content.Load<Model>( "ring16b" ); batch = new SpriteBatch( graphics.GraphicsDevice ); explosion.Load( graphics.GraphicsDevice, content, "explosion", 10, 3 ); explosion.Pause(); } graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; graphics.GraphicsDevice.RenderState.CullMode = CullMode.None; } protected override void UnloadGraphicsContent( bool unloadAllContent ) { if (unloadAllContent == true) { content.Unload(); } } protected override void Update( GameTime gameTime ) { // Allows the default game to exit on Xbox 360 and Windows if (GamePad.GetState( PlayerIndex.One ).Buttons.Back == ButtonState.Pressed) this.Exit(); // TODO: Add your update logic here GamePadState PlayerOne = GamePad.GetState( PlayerIndex.One ); // Move the camera using thumbsticks MoveCamera( PlayerOne ); // Start or stop the animated sprite using buttons if (PlayerOne.Buttons.A == ButtonState.Pressed) explosion.Play(); if (PlayerOne.Buttons.B == ButtonState.Pressed) explosion.Stop(); // Update the animated sprite explosion.UpdateFrame( (float)gameTime.ElapsedGameTime.TotalSeconds ); // Create a total bounding sphere for the mesh BoundingSphere totalbounds = new BoundingSphere(); foreach (ModelMesh mesh in Ring.Meshes) { totalbounds = BoundingSphere.CreateMerged( totalbounds, mesh.BoundingSphere ); } // Project the center of the 3D object to the screen, and center the // sprite there Vector3 center = graphics.GraphicsDevice.Viewport.Project( totalbounds.Center, projectionMatrix, Camera1.ViewMatrix, Matrix.Identity ); explosionpos.X = center.X; explosionpos.Y = center.Y; // Create a bounding box from the bounding sphere, and find the corner // that is farthest away from the center using Project BoundingBox extents = BoundingBox.CreateFromSphere( totalbounds ); float maxdistance = 0; float distance; Vector3 screencorner; foreach (Vector3 corner in extents.GetCorners()) { screencorner = graphics.GraphicsDevice.Viewport.Project( corner, projectionMatrix, Camera1.ViewMatrix, Matrix.Identity ); distance = Vector3.Distance( screencorner, center ); if (distance > maxdistance) maxdistance = distance; } // Scale the sprite using the two points (the sprite is // 75 pixels square) explosion.Scale = maxdistance / 75; base.Update( gameTime ); } private void MoveCamera( GamePadState Player ) { Camera1.OrbitUp( Player.ThumbSticks.Right.Y / 4 ); Camera1.OrbitRight( Player.ThumbSticks.Right.X / 4 ); Camera1.Distance -= Player.ThumbSticks.Left.Y; Vector3 newTarget = Camera1.Target; newTarget.X += Player.ThumbSticks.Left.X; Camera1.Target = newTarget; } protected override void Draw( GameTime gameTime ) { graphics.GraphicsDevice.Clear( Color.CornflowerBlue ); //Draw the model, a model can have multiple meshes, so loop foreach (ModelMesh mesh in Ring.Meshes) { //This is where the mesh orientation is set, as well as our camera and projection foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = Matrix.Identity * Matrix.CreateRotationY( RingRotation ) * Matrix.CreateTranslation( RingPosition ); effect.View = Camera1.ViewMatrix; effect.Projection = projectionMatrix; } //Draw the mesh, will use the effects set above. mesh.Draw(); } // Draw the sprite over the 3D object batch.Begin( SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred, SaveStateMode.SaveState ); explosion.DrawFrame( batch, explosionpos ); batch.End(); base.Draw( gameTime ); } } public class AnimatedTexture { private int framecount; private Texture2D myTexture; private float TimePerFrame; private int Frame; private float TotalElapsed; private bool Paused; public float Rotation, Scale, Depth; public Vector2 Origin; public AnimatedTexture( Vector2 Origin, float Rotation, float Scale, float Depth ) { this.Origin = Origin; this.Rotation = Rotation; this.Scale = Scale; this.Depth = Depth; } public void Load( GraphicsDevice device, ContentManager content, string asset, int FrameCount, int FramesPerSec ) { framecount = FrameCount; myTexture = content.Load<Texture2D>( asset ); TimePerFrame = (float)1 / FramesPerSec; Frame = 0; TotalElapsed = 0; Paused = false; } // class AnimatedTexture public void UpdateFrame( float elapsed ) { if (Paused) return; TotalElapsed += elapsed; if (TotalElapsed > TimePerFrame) { Frame++; // Keep the Frame between 0 and the total frames, minus one Frame = Frame % (framecount - 1); TotalElapsed -= TimePerFrame; } } // class AnimatedTexture public void DrawFrame( SpriteBatch Batch, Vector2 screenpos ) { DrawFrame( Batch, Frame, screenpos ); } public void DrawFrame( SpriteBatch Batch, int Frame, Vector2 screenpos ) { int FrameWidth = myTexture.Width / framecount; Rectangle sourcerect = new Rectangle( FrameWidth * Frame, 0, FrameWidth, myTexture.Height ); Batch.Draw( myTexture, screenpos, sourcerect, Color.White, Rotation, Origin, Scale, SpriteEffects.None, Depth ); } public bool IsPaused { get { return Paused; } } public void Reset() { Frame = 0; TotalElapsed = 0f; } public void Stop() { Pause(); Reset(); } public void Play() { Paused = false; } public void Pause() { Paused = true; } }