Dienstag, 10. März 2015

Part 2A: The Graphics Engine

This is Part 2A of my tutorial series about how to write an emulator.

In this part we will set up some graphics where the emulator can "draw to".

After reading Part 2 you should have created a basic WinPhone & MonoGame project, which we will extend now.

Note: If you downloaded the source from Part 2, you need to move some static methods to the new created GFXEngine.cs file (which is also there, but empty).

The Graphics-Engine needs to draw a texture on the screen, which is scaled to the screen size or an appropriate format (4:3 etc). There needs to be a method which can put all the graphics data from the emulator (intended to be a 1-dimensional Int32 array in the size consoleScreenWidth*consoleScreenHeight) onto this texture.

The texture itself will be the size of the console screen, but scaled to the size of the device (phone) screen. If the emulator changes, it should create a new texture in the appropriate size and reset the scaling factor.

There's more to that: A console can have more pixels than there will be seen. These must be covered or be outside of the device screen.

After this tutorial, you have a texture to draw on, which is scaled to the right size and in portrait mode, rotated about 90 degrees.

It will be filled with test pixels - you don't need to do that, it's only that you see something.

A little note: I am talking about UNSIGNED integers here. When I write Int32, you can assume it's meant UInt32. Unsigned means that there can be only positive numbers, not negative ones. This leaves one bit more for the value itself. Otherwise that bit would be used to determine if it's a positive or negative number.

Implementing

Ok then, first remove all the SpriteBatch-related code in Game1.cs.
We will implement that into our graphics-engine.

Now, lets create a new class called GFXEngine.cs.

public class GFXEngine
{
     // the graphics device from the engine.
     private GraphicsDevice graphicsDevice;

     // a SpriteBatch to draw sprites/textures.
     SpriteBatch spriteBatch;

     // the texture to draw on.
     Texture2D texture;

     // the device screen resolution is stored here.
     public int DeviceScreenWidth { get; private set; }
     public int DeviceScreenHeight { get; private set; }

     // Get device screen resolution
     // You need to call that before the game starts/outside of the game thread.
     public void ActualizeScreenResolution() {}

     // use this as constructor, it's called in a "safe region".
     public void LoadContent(GraphicsDevice gfxDevice) {}

     // the update call from the game engine.
     public void Update(GameTime gameTime) {}

    //the draw call from the game engine.
    public void Draw(GameTime gameTime) {}
}


That class will be extended later. First, we implement it into the engine.

Create an instance of the class in the Static.cs - class.

public class Static
{
      public static GFXEngine GFX = new GFXEngine();
      ...
}

And now call the methods in Game1.cs.
Call Static.GFX.ActualizeScreenResolution(); in the Constructor of Game1.cs.
Call Static.GFX.LoadContent(GraphicsDevice); in the LoadContent-method of Game1.cs.
Call Static.GFX.Update(gameTime); in the Update-method of Game1.cs.
Call Static.GFX.Draw(gameTime); in the Draw-method of Game1.cs.

We have  implemented our GFXEngine into the game and will now extend the class to our needs.

Extending

The MonoGame-screen is in portrait-mode. That means, we need to rotate our texture 90 degrees.
If you managed to run it in "real" landscape mode, you can skip the rotation part and just use 0 at the right position.

Add an enum, the PI-Constant and the RGBAToInt32-methods to your Static.cs-file:

public static class Static
{
    ...
    public enum EColorShift
    {
        RED = 0,
        GREEN = 8,
        BLUE = 16,
        ALPHA = 24
    }

    public const double PI = 3.14159265359;
    ....
    public static UInt32 RGBAToInt32i(int R, int G, int B, int A = 255)
    {
        return RGBAToInt32b((byte)R, (byte)G, (byte)B, (byte)A);
    }

    public static UInt32 RGBAToInt32b(byte R, byte G, byte B, byte A = 255)
    {
        UInt32 c= (UInt32)(R << (int)EColorShift.RED);
        c += (UInt32)(G << (int)EColorShift.GREEN);
        c += (UInt32)(B << (int)EColorShift.BLUE);
        c += (UInt32)(A << (int)EColorShift.ALPHA);
        return c;
    }

    // additional method to get color values back.
    public static byte ColorFromInt32(UInt32 value, EColorShift color)
    {
        // value (bit-)shift right by color, then get last 8 bits (0xFF = 255 = 8 bits all up)
        // e.g: 0xAABBCC & 0xFF = 0xAABBCC & 0x0000FF = 0xCC

        return (byte)((value >> (int)color) & 0xFF);
    }
}

The enum defines the right values for bitshifting later.
If you have another format than RGBA (e.g. AGBR) then you have to change only these values.

We need PI to get the correct radian angle. (I tried with 0.5 and 1.0 but it did'nt work.)

The RGBAToInt32-methods are the main functions to create the color-values for the console screen array. (And the ColorFromInt32-method does the exact opposite - getting color values back from Int32)

It simply pushes all values (ranging from 0-255 / 0x00-0xFF) into an Int32 variable by bitshifting.

Bitshifting works like that: You have x which is 4 bits "wide", the last one is up (1): 0001 => x=1
Shift Left 2 (x = x << 2) means shift all bits two to the left: 0001=>0100 (<--) => x=4
Shift Right 1 (x = x >> 1) means shift all bits one to the right: 0100=>0010 (-->) => x=2

E.g: R=0x11, G=0x22, B=0x33 and A=0xFF will give this hex value into the Int32-variable: 0xFF332211 because A is shifted 24 bits to the left, then B shifted 16 bits to the left and G shifted 8 bits to the left. R remained "where it was".

This methods are used by the emulators and have nothing to do with the graphics engine internals.
(But they are set up to correspond to the texture data format used by the graphics engine.)
The graphics engine must react to this functions, not otherwise.

That's why this methods are in the Static-class and not in the GFXEngine class.

Now, lets extend the GFXEngine-class. First we need some variables and properties:

public class GFXEngine
{
...
      private Vector2 textureOrigin;
      private Vector2 deviceHalfScreenSize;
      private Rectangle consoleRectangle;
      private float scaleFactor = 1.0f;
      // and the rotation for the portrait-to-landscape-hack:
      private const float consoleScreenRotation = (float)(90 * Static.PI / 180);

      public DeviceScreenWidthForConsole { get { return DeviceScreenHeight; }}
      public DeviceScreenHeightForConsole { get { return DeviceScreenWidth; }}
...
}

We could compute the most of them in each frame, but that's not necessary and like that, it saves some performance.

textureOrigin is the position on the texture from where it is accessed from outside (rotated / positioned). It's half the texture size.
deviceHalfScreenSize is the half of the device screen size. No need to compute that in each frame.
consoleRectangle is the rectangle on the texture which will actually be drawn. That is for the cutoff lines.
scaleFactor is used to scale the texture to device screen size. There is no independent scale factor for x and y. It's the same for both.
consoleScreenRotation is the rotation of the texture. It needs to be rotated 90 degrees because it's in portrait mode. If you managed to use real landscape mode, just use 0 here.
DeviceScreenWidthForConsole and DeviceScreenHeightForConsole return the "real" width and height for the console screen. Because it's in portrait mode, this values return the "other" value instead of the "right" one. (Height = DeviceWidth and vice versa.)

ActualizeScreenResolution

Extend the ActualizeScreenResolution()-method:
public void ActualizeScreenResolution()
{
      DeviceScreenWidth = (int)Application.Current.Host.Content.ActualWidth;
      DeviceScreenHeight = (int)Application.Current.Host.Content.ActualHeight;
      deviceHalfScreenSize = new Vector2(DeviceScreenWidth * 0.5, DeviceScreenHeight * 0.5);
}

(It's better to multiplicate instead of dividing (* 0.5 instead of / 2))

This method gets the device screen width and height. If you call that Application.xxx stuff inside the game, there will be an asyncrounous error. The method is called once at startup.

LoadContent

This is somewhat our constructor. Stuff will be created and initialized here for the first time.
public void LoadContent(GraphicsDevice gfxDev)
{
     graphicsDevice = gfxDev;
     spriteBatch = new SpriteBatch(graphicsDevice);

     // create a first texture so there is one. (...and it's used for testing.)
     ResizeTexture(140,100); // for the exact same test image like mine, use size 34x24

     // build the test image.
     BuildTestImage();
}

The methods ResizeTexture and BuildTestImage will follow later. You don't need BuildTestImage, it's only here to see something because otherwise, the texture would be transparent.

Draw

Lets extend the Draw-method.
public void Draw(GameTime gameTime)
{
     // we only need the samplerstate here.
     // needed for pixel-perfect scaling without blur and stuff.
      spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend,
                                    SamplerState.PointClamp,
                                    DephtStencilState.Default,
                                    RasterizerState.CullCounterClockwise);

     // now draw the texture with all given values.
     spriteBatch.Draw(texture,
                           deviceHalfScreenSize,
                           consoleRectangle,
                           Color.White,
                           consoleScreenRotation,
                           textureOrigin,
                           scaleFactor,       // bad that there is no scaling for x / y individually.
                           SpriteEffects.None,
                           0.0f);

     spriteBatch.End();
}

We need to modify the SamplerState in SpriteBatch.Begin so there is no blurry image. All other values are left default. Then we give all the values which are computed in ResizeTexture to SpriteBatch.Draw. SpriteBatch.End must be called to end the draw call.

ResizeTexture

Create the method ResizeTexture(int,int,int,int):
public void ResizeTexture(int width, int height, int cutOffTopLines = 0, int cutOffBottomLines = 0)
{
        // first, create the new texture with given width and height.
        texture = new Texture2D(graphicsDevice, width, height);

        // hcalc is the height minus the cutoff.
        // it's stored in consoleRectangle and used for scaling and origin.
        float hcalc = height - cutOffTopLines - cutOffBottomLines;
        // origin needs to be calculated using hcalc instead of height.
        textureOrigin = new Vector2(width * 0.5f, hcalc * 0.5f);
        // define the shown rectangle on the texture.
        consoleRectangle = new Rectangle(0, cutOffTopLines, width, (int)hcalc);
        // now calculate the scale factor.
        scaleFactor = (float)DeviceScreenHeightForConsole / hcalc;
        // maybe it's a widescreen. (not possible, but why not calculate it?)
        if(width * scaleFactor > DeviceScreenWidthForConsole)
               scaleFactor = (float)DeviceScreenWidthForConsole / width;
}

Everything is explained in the comments above.

This is the basic "graphics engine". All we need to do now, are some methods to put the given array from the console to the texture. For that, we set up some helper functions to modify textures.

"Interface" Methods

I call those "Interface Methods" because this are the methods which the emulators need to interface with the graphics engine.

Create these four methods in GFXEngine.cs:
public class GFXEngine
{
    // set the data for the main (onscreen) texture.
    public void SetMainTexData<T>(T[] data) where T:struct
    {
        ArrayToTexture(data, texture);
    }

    // get the data from the main (onscreen) texture.
    public T[] GetMainTexData<T>() where T:struct
    {
        return TextureToArray<T>(texture);
    }

    // retrieves all colors from a texture in an 1-dimensional {T} array.
    public static T TextureToArray<T>(Texture2D tex) where T:struct
    {
        T[] colors1D = new T[tex.Width * tex.Height];
        tex.GetData(colors1D);
        return colors1D;
    }

    // set an 1-dimensional {T} array to a texture.
    public static void ArrayToTexture<T>(T [] cols, Texture2D tex) where T:struct
    {
         tex.SetData(cols);
    }
}

The T stuff means simply that you can get/put any data type you wish, as long as it's accepted by the texture code itself. You can either use an Xna.Color-Array or an Int32-Array or something else there. ...where T:struct defines the T-variables as not-nullable.

To create an array with the right size, just use the TextureToArray-method.

Int32[] ar = TextureToArray<Int32>(myTex);



Testimage

Last but not least, the test image:

public void BuildTestImage()
{
   Microsoft.Xna.Framework.Color[] c = GetMainTexData<Microsoft.Xna.Framework.Color>();

  // go through all the pixels and color them.
  for(int i=0; i<c.Length; i++)
  {
       // color every second pixel different.
       if(i % 2 == 1)
             c[i] = Microsoft.Xna.Framework.Color.Red;
       else
             c[i] = Microsoft.Xna.Framework.Color.Green;

      // color the first four lines.
      if(i < texture.Width * 4)
         c[i] = Microsoft.Xna.Framework.Color.Yellow;
      // color the first 2 lines.
      if(i < texture.Width * 2)
         c[i] = Microsoft.Xna.Framework.Color.Magenta;

      // color the last two lines
      if(i > (texture.Height - 2) * texture.Width)
         c[i] = Microsoft.Xna.Framework.Color.Magenta;
  }

    // set the new data to the texture.
    SetMainTexData(c);
}

You will see an image like this:


On the left and right there is the background which is actually in the color "Cornflowerblue".
If you have a widescreen image (e.g. 200x100px), the background will be visible on the top and bottom.

The first two lines (magenta, top and bottom) are used to test the cutoff. You can use ResizeTexture(w, h, 2,2) to test that. The yellow line indicates the first two lines of the "real visible" screen. Then, every pixel is either red or green. In the bottom of the image, you should see a green or red pixel in the magenta lines. That one indicates if the image is drawn from left to right.


Download

Download the Source for this Part:
Emu_02_GraphicsEngine.zip from Mirror 1 (OneDrive)
Emu_02_GraphicsEngine.zip from Mirror 2 (homeserver)


That's it with this part. You should now have a "graphics engine" which your emulators can use.

In Part 2B [TBA] we will create the "Sound Engine"/Sound Interface for the emulators to use.

Part 2B: The Sound Engine [TBA]

You can skip the whole Part 2x stuff and just go to Part 3 if you do that on another Engine/Platform.

Keine Kommentare:

Kommentar veröffentlichen