tags:

views:

603

answers:

4

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

alt text

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));
                }
            }
        }
    }
A: 

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.

LaZe
I'm not a shader expert, but I don't think you can manipulate the underlying textures in a vertex or pixel shader. Therefore you'd have to modify the textures in the game code, which would be something like Texture2D.SetData(). However, if you have a 800x600 texture defined based on your array values, you don't even need a pixel shader. Just use Spritebatch to draw that texture to the screen every time.
Venesectrix
You can render to a texture while reading from another texture. So it's not modifying the texture, but generating a new texture each frame. That's why I would use two textures and swap them.
LaZe
I'd be interested (and maybe the poster would be as well) to see how this is done. Do you have any links to examples you can post? Thanks!
Venesectrix
Thanks for posting the code! I have a few more questions. Is SetTexture a member function of a class? I couldn't find any XNA documentation on that function as you have used it. Also, if you look at his update function, it references and updates adjacent pixels. I thought a pixel shader was only able to return the value of one pixel at a time. How would the code in his pixel shader be able to compute the value of a nearby pixel and set that specific value when it is supposed to be returning the value for the current pixel it is processing?
Venesectrix
Argh, the copy-paste monster did it... It was simply a wrapper method for setting a texture on the Device. Have edited the post now.
LaZe
Thanks, that was the path I was actually going down. Seems to work a bit faster. The only issue I am having now is that a black outline is being drawn around the edges of my pixels. Do you know what is causing that behavior?
George
Looks like an off-by-one error. Are your graphics being clipped on the right/bottom as well? If so you've either got to add a pixel to your mask texture coordinates or ensure you're calculating the texture coordinates properly.
Ron Warholic
I've checked over everything, and as I'm doing a 1x1 pixel, I don't see where anything could be off. My 1x1 pixels, once they are combined into a group, such as the little mound of dirt showing in the graphic above, once combined, makes the border go away except for on the left.
George
This could be caused by your texture coordinates. Try to displace them a half pixels. (For example 0.5f/800.0f)
LaZe
Check your `GetGrainColor(x, y)` method to ensure you're not off by one calculating the appropriate color for your grain. As a side note you shouldn't be calling `Draw()` for each pixel, instead create your texture in memory, fill it with your `SetPixels` method, then upload the entire thing at once to a texture.
Ron Warholic
+2  A: 

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;
                    }
    }
}
Venesectrix
Could not agree more. To make GPU stuff work fast, the way to go is to let the CPU upload your data as a texture. Using an indexed color texture is probably the fastest scenario, but 32 bit colors will probably perform just fine. Shaders are fun to write but they probably don't add value in this scenario.
jdv
A: 

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...

Peter
A: 

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.

Danny Varod