How can I use a Shader in XNA to color single pixels?
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));
}
}
}
}
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;
}
}
}
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.
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...
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.
精彩评论