11
votes

I have a standard 800x600 window in my XNA project. My goal is to color each individual pixel based on a rectangle array which holds boolean values. Currently I am using a 1x1 Texture and drawing each sprite in my array.

I am very new to XNA and come from a GDI background, so I am doing what I would have done in GDI, but it doesn't scale very well. I have been told in another question to use a Shader, but after much research, I still haven't been able to find out how to accomplish this goal.

My application loops through the X and Y coordinates of my rectangular array, does calculations based on each value, and reassigns/moves the array around. At the end, I need to update my "Canvas" with the new values. A smaller sample of my array would look like:

0,0,0,0,0,0,0
0,0,0,0,0,0,0
0,0,0,0,0,0,0
1,1,1,1,1,1,1
1,1,1,1,1,1,1

How can I use a shader to color each pixel?

A very simplified version of the calculations would be:

        for (int y = _horizon; y >= 0; y--)  // _horizon is my ending point
        {
            for (int x = _width; x >= 0; x--) // _width is obviously my x length.
            {
                if (grains[x, y] > 0)
                {
                    if (grains[x, y + 1] == 0)
                    {
                        grains[x, y + 1] = grains[x, y];
                        grains[x, y] = 0;
                    }
                }
            }
        }

..each time the update method is called, the calculations are performed and in example of the above loop, an update may look like:

Initial:

0,0,0,1,0,0,0
0,0,0,0,0,0,0
0,0,0,0,0,0,0
1,1,1,0,1,1,1
1,1,1,1,1,1,1

First:

0,0,0,0,0,0,0
0,0,0,1,0,0,0
0,0,0,0,0,0,0
1,1,1,0,1,1,1
1,1,1,1,1,1,1

Second:

0,0,0,0,0,0,0
0,0,0,0,0,0,0
0,0,0,1,0,0,0
1,1,1,0,1,1,1
1,1,1,1,1,1,1

Final:

0,0,0,0,0,0,0
0,0,0,0,0,0,0
0,0,0,0,0,0,0
1,1,1,1,1,1,1
1,1,1,1,1,1,1

Update:

After applying the Render2DTarget code and placing my pixels, I end up with an unwanted border on my pixels, always to the left. How can I remove this?

alt text http://www.refuctored.com/borders.png

alt text http://www.refuctored.com/fallingdirt.png

The some of the code for applying the textures is:

    RenderTarget2D target;
    Texture2D texture;
    protected override void LoadContent()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);
        texture = Content.Load<Texture2D>("grain");
        _width = this.Window.ClientBounds.Width - 1;
        _height = this.Window.ClientBounds.Height - 1;
        target = new RenderTarget2D(this.GraphicsDevice,_width, _height, 1, SurfaceFormat.Color,RenderTargetUsage.PreserveContents);
     }

 protected override void Draw(GameTime gameTime)
    {
        this.GraphicsDevice.SetRenderTarget(0, target);
        this.GraphicsDevice.SetRenderTarget(0, null);
        this.GraphicsDevice.Clear(Color.SkyBlue);
        this.spriteBatch.Begin(SpriteBlendMode.None,SpriteSortMode.Deferred,SaveStateMode.None);
        SetPixels(texture);
        this.spriteBatch.End();
    }

 private void SetPixels(Texture2D texture)
    {
        for (int y = _grains.Height -1; y > 0; y--)
        {
            for (int x = _grains.Width-1; x > 0; x--)
            {
                if (_grains.GetGrain(x, y) >0)
                {
                    this.spriteBatch.Draw(texture, new Vector2(x,y),null, _grains.GetGrainColor(x, y));
                }
            }
        }
    }
4
Would you mind sharing the calculations you perform on your rectangular array? If you can do those calculations inside the shader it may not be too hard to figure out. I'm having trouble figuring out how to pass an array of size 800x600 to the shader because arrays are limited to 65536 indices (16-bit index), apparently; so if you could just perform the calculations inside the shader without having to pass the large array in each time it would be easier to figure out.Venesectrix
I've updated the question with some more supporting information for your inquiry.George Johnston
Actually, sorry, now that I think about it that approach isn't a good idea for a number of reasons. It requires that a texture represent the initial state of your array, the pixel shader can't modify the texture, it can only return different values, so each time it was called it would be like starting from the initial state, and it does all the calculations in the draw() function instead of the update() function, which is probably not a good idea.Venesectrix
Are you calling Draw once pr. pixel or am I missing something? If so, that is a really big performance killer. You should fill your texture using the SetData method.LaZe

4 Answers

2
votes

This method doesn't use pixel shaders, but if you're looking to use Texture2D's SetData method instead of making a call to SpriteBatch.Draw() for every pixel, you may find this useful. I used an array of uint instead of bool to represent your colors. If you can get away with an 8-bit color texture, you could may be able to speed this up by changing the texture format.

public class Game1 : Microsoft.Xna.Framework.Game
{
    // Set width, height
    const int WIDTH = 800;
    const int HEIGHT = 600;

    // Used to randomly fill in initial data, not necessary
    Random rand;

    // Graphics and spritebatch
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    // Texture you will regenerate each call to update
    Texture2D texture;

    // Data array you perform calculations on
    uint[] data;

    // Colors are represented in the texture as 0xAARRGGBB where:
    // AA = alpha
    // RR = red
    // GG = green
    // BB = blue

    // Set the first color to red
    const uint COLOR0 = 0xFFFF0000;

    // Set the second color to blue
    const uint COLOR1 = 0xFF0000FF;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";

        // Set width, height
        graphics.PreferredBackBufferWidth = WIDTH;
        graphics.PreferredBackBufferHeight = HEIGHT;
    }

    protected override void Initialize()
    {
        base.Initialize();

        // Seed random, initialize array with random picks of the 2 colors
        rand = new Random((int)DateTime.Now.Ticks);
        data = new uint[WIDTH * HEIGHT];
        loadInitialData();
    }

    protected override void LoadContent()
    {
        // Create a new SpriteBatch, which can be used to draw textures.
        spriteBatch = new SpriteBatch(GraphicsDevice);

        // Create a new texture
        texture = new Texture2D(GraphicsDevice, WIDTH, HEIGHT);
    }

    protected override void Update(GameTime gameTime)
    {
        // Allows the game to exit
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
            this.Exit();

        // Run-time error without this
        // Complains you can't modify a texture that has been set on the device
        GraphicsDevice.Textures[0] = null;

        // Do the calculations
        updateData();

        // Update the texture for the next time it is drawn to the screen
        texture.SetData(data);

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        // Draw the texture once
        spriteBatch.Begin();
        spriteBatch.Draw(texture, Vector2.Zero, Color.Purple);
        spriteBatch.End();

        base.Draw(gameTime);
    }

    private void loadInitialData()
    {
        // Don't know where the initial data comes from
        // Just populate the array with a random selection of the two colors
        for (int i = 0; i < WIDTH; i++)
            for (int j = 0; j < HEIGHT; j++)
                data[i * HEIGHT + j] = rand.Next(2) == 0 ? COLOR0 : COLOR1;

    }

    private void updateData()
    {
        // Rough approximation of calculations
        for(int y = HEIGHT - 1; y >= 0; y--)
            for (int x = WIDTH - 1; x >= 0; x--)
                if (data[x * HEIGHT + y] == COLOR1)
                    if (y + 1 < HEIGHT && data[x * HEIGHT + (y + 1)] == COLOR0)
                    {
                        data[x * HEIGHT + (y + 1)] = data[x * HEIGHT + y];
                        data[x * HEIGHT + y] = COLOR0;
                    }
    }
}
0
votes

How about this...

Create two textures (800x600)

Initialize one of them to the initial values.

For each frame you render one texture to the other while updating the values in a pixelshader.

After rendering the resulting texture to the screen, you swap them, so they are ready for your next frame.

Edit:

You will need two instances of RenderTarget2D and create them with RenderTargetUsage.PreserveContents. You could start with SurfaceFormat.Color and use black for 0 and white for 1. (You may also be able to find a 8 bit format to save video memory.)

new RenderTarget2D(_device, 800, 600, 1, SurfaceFormat.Color, RenderTargetUsage.PreserveContents);

You assign them to the rendertaget like this:

_device.SetRenderTarget(0, myRenderTarget);

You use a RenderTarget2D as a texture, like this:

_device.Textures[0] = myRenderTarget.GetTexture();

Hope that helps... I can dig out more from my engine, so just ask.

0
votes

What you're trying to do (draw pixel by pixel) is what DirectX does. XNA is a layer that is built on top of DirectX so that you don't have to draw pixel by pixel. If that's really what you want to do then you should probably be learning DirectX instead of XNA. You would probably find it much easier...

0
votes

Add a billboard to your scene (directly in front of the camera, so that it takes up exactly 800x600 pixels).

Bind the binary array as a texture.

In the fragment (/pixel) shader calculate the color of the fragment using the 2D position of the fragment and the texture.