Rendering to the Screen, Simple Sprites
Now that we have the ability to startup and render with Direct3D let’s get to rendering something! I am going to keep these tutorial sets geared towards creating a full engine, so everything will be as object oriented as possible to make it easy to add to and use later. To start out, let’s create a file that will contain all of the code for objects our engine will render. I am calling mine HMObject.cs, but you can use whatever you want. Make sure you create this in the 3D Engine project and not the demo one
Step 1: Creating the basic object class
Start the file out by adding in the proper using and namespace code:
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace HMEngine { // Whatever your main namespace is }
Since we will be creating more than one kind of object, let’s create a class that holds all of the things each object will need to use so we can inherit from it later.
public class HMObject { protected Vector3 myPosition = new Vector3(0, 0, 0); // We will use these for protected Vector3 myRotation = new Vector3(0, 0, 0); // building our transformation protected Vector3 myScaling = new Vector3(1, 1, 1); // matrices in the camera later public virtual void ReloadResources(Device myDevice){} // This will be used for device resets public virtual void Render(Device myDevice){} // Basic render call for every object }
We will not be using those vectors until step 3, but I thought we should add them now since we will end up doing so later anyway.
Step 2: Rendering a simple sprite
The first useful object to start creating a game with is probably a sprite. Now I know sprites are only 2D and you are probably thinking “This is a 3D engine!! We don’t need sprites!”, but you would be surprised at what they might come in handy for later. That said I should inform you that DirectX already has a Sprite class that is very easy to use, but to cut down on our game coding time later, we will write a custom Sprite class to use in our games that will take care of many repetitive things for us. Here’s what my custom sprite class looks like:
public class HMSprite : HMObject { private Sprite mySprite; // Sprite object that does the rendering private Texture myTexture; // Texture object used for rendering private string myPath; // Path to the texture file so we can use on resets public HMSprite(string imagePath, Vector3 location, int width, int height, Device myDevice) { myPosition = location; // Keep location in inherited vector mySprite = new Sprite(myDevice); // Create a new sprite object myScaling = new Vector3(width, height, 1); // Keep size in inherited vector myPath = imagePath; // Store the texture’s path myTexture = TextureLoader.FromFile(myDevice, myPath); // Load our texture } // This will be called if the device resets and resources had to be acquired again public override void ReloadResources(Device myDevice) { myTexture = TextureLoader.FromFile(myDevice, myPath); } public override void Render(Device myDevice) { // Here we find the center of the texture dimensions Vector3 texCenter = new Vector3( myTexture.GetSurfaceLevel(0).Description.Width/2, myTexture.GetSurfaceLevel(0).Description.Height/2, 0 ); mySprite.Transform = Matrix.Transformation( new Vector3(), // Scaling Center, blank scales from the top corner new Quaternion(), // Scaling Rotation, blank to scale down and right (normally) // Scale ratio: newSize/texSize, since we stored half the size in texCenter we use texCenter*2 new Vector3(myScaling.X/(texCenter.X*2), myScaling.Y/(texCenter.Y*2),0), texCenter, // Rotation Center new Quaternion(), // Rotation amount new Vector3() // Translation amount (location) ); mySprite.Begin(SpriteFlags.AlphaBlend); // Start drawing with out sprite, and use Transparency // The parameters for the following .Draw call are: // myTexture - the Texture object to draw // new Vector3 - center to draw from, we are positioning from top left so use an empty vector // myLocation - the screen offset to draw at // Color.White - Color to modulate the texture with, this value keeps original colors mySprite.Draw(myTexture, new Vector3(), myPosition, Color.White.ToArgb()); mySprite.End(); // All done drawing } }
You will also need to add references to Microsoft.DirectX and Microsoft.DirectX.Direct3D to the demo project, and you need to add a reference to Miscrosoft.DirectX.Direct3DX as well since the Sprite class we are using is in that library.
In our sprite code, the first thing we did was create some storage spots for the sprites size, location, texture, and the actual sprite object DirectX will use to draw with. The constructor takes a path to the image we are using for our texture, the location in 3D space of the sprite (most of them will be at <X, Y, 0>), the width and height of the sprite we want to draw, and the device we will use to create the texture and sprite objects.
The path can be passed as relative or absolute in the filesystem. If you just want to pass something like “image1.jpg” to the constructor, then you will have to place the image file in the same directory that the .exe will be running from (in VC# this will be in the /bin folder of the Debug or Release sections of the demo application. You can use any standard image format you would like (.bmp, .gif, .jpg, .tga) and they can contain transparent sections as well.
Step 3: Adding a Simple Scene Graph
Now that we have an object to render, we need a good method to actually render it that we can extend for a more complicated use later. For this task we will create a very simple Scene class that we will add on to later when more complicated tasks come around (like frustum culling).
Start out by creating a new code file with the normal using and namespace statements and a using for System.Collections as well, and call it something similar to mine: HMScene. Here is our very, very simple scene class.
public class HMScene { ArrayList WorldObjects = new ArrayList(); public void AddObject(HMObject newObject) { WorldObjects.Add(newObject); } public void RemoveObject(HMObject oldObject) { WorldObjects.Remove(oldObject); } public void ReloadResources(Device myDevice) { foreach(HMObject o in WorldObjects) { o.ReloadResources(myDevice); } } public void Render(Device myDevice) { foreach(HMObject o in WorldObjects) { o.Render(myDevice); } } }
Ok, so we have objects and a scene, let’s add a scene object to our main system class and get it rendering. In the main system class with the rest of its instance variables (at the top) add a new scene variable and a way to access it from the outside:
private HMScene myScene = new HMScene(); public HMScene MyScene { get { return myScene; } } public Device MyDevice { get { return myDevice; } }
I also made the device available from outside the system. Now, add the call to render the scene into the main Render function between the Clear and Present calls, like this:
myDevice.BeginScene(); myScene.Render(myDevice); myDevice.EndScene();
That’s it for the engine code. Now all we have to do is add a sprite object to the scene in the demo code and watch it go. Here’s how you do that. In the Main() function of the demo add this:
HMSprite sprite1 = new HMSprite(”hmglogo.tga”, new Vector3(), 320, 240, demo1.MyDevice); demo1.MyScene.AddObject(sprite1);