Taking a screenshot & saving a PNG?

How could I save a specific portion of the screen as a PNG from code (not using the snippet tool)?

YES! You can use rendertarget.saveaspng ā€¦

so you would just draw your normal screen stuff on a rendertarget, then draw THAT render-target to one of the size you need, using a stencil rect to ā€œcropā€ the first stencil rect to fit the second one.

I found this code that takes a screencap of the entire screen:

    public void TakeScreenCap()
        {
            
            int w = GraphicsDevice.PresentationParameters.BackBufferWidth;
            int h = GraphicsDevice.PresentationParameters.BackBufferHeight;

            //force a frame to be drawn (otherwise back buffer is empty) 
            Draw(new GameTime());

            //pull the picture from the buffer 
            int[] backBuffer = new int[w * h];
            GraphicsDevice.GetBackBufferData(backBuffer);

            //copy into a texture 
            Texture2D texture = new Texture2D(GraphicsDevice, w, h, false, GraphicsDevice.PresentationParameters.BackBufferFormat);
            texture.SetData(backBuffer);

            //save to disk 
            Stream stream = File.OpenWrite("Test.png");

            texture.SaveAsPng(stream, w, h);
            stream.Dispose();

            texture.Dispose();
        }

How could I modify this (probably using a rect) defined as:

Rect r = new Rect(LeftMapOffset, TopMapOffset, Map.Width, Map.Height);

so it just takes a screen cap of the map bit (using the above Rect r):

1 Like

Heres what I would do:

Instead of drawing everything to the back-buffer, switch to drawing on a render-target.
You can then draw that render-target to the screen to see your gameā€¦
But MORE important, you can take that renderTarget, and draw it on another smaller renderTarget.
-One that has the SIZE you need for your screen shotā€¦

Then you draw you big render target on your small render target like so (pseudo code)

_graphics.setrendertarget(_renderTarget2);
spriteBatch.Draw( _renderTarget , vector2.zero, stencilrect, color.White )

ā€¦Then call save png on your _rendertarget2ā€¦ Because it already HAS the size you want, you wont even have to modify the code you have.

Okay, but Iā€™m not following you on how to create the first Texture2D that is of the portion of the screen that I want.

You get that by using a stencil rectangle, just like you would if animating a sprite using a sprite sheetā€¦

A stencil rectangle cuts out a rectangular section of a texture, and draw that only, discarding the rest.

That is how you can have a hundred sprites on one big texture, and only draw a selection of them.

your Draw method has several overload options to choose from, and some of them include accepting a stencil rectā€¦ Just declare ANY rectangle you want, and pass that into the draw overlodā€¦

1 declare 2 new renderTargetsā€¦ one the size of your screen, and one the size you want to screen-grabā€¦
2 declare a new rectangle the size and location you want to grab off the screenā€¦
-something like stencilRect = 100,100,1820,980 would grab everything minus a border of 100.

3 inside draw() simply set current render target to the bigger of the 2 you just made.
and draw game normally.

4 your whole screen is now on a render-targetā€¦ You then draw this whole rendertarget to the screen, like any textureā€¦ switch current render target back to null, ie back buffer, and draw the rendertargetā€¦
Now your game is displayed normally, BUT you have it all stored in one textureā€¦

5 when you want to take a screen shot, SET your current render-target to be the smaller one of your 2, and draw the bigger one on IT, using the stencil rectā€¦ like so:

graphics.setrendertarget(small_RT);
spriteBatch.Draw(big_RT, vector2.zero, stencilRect, color.white)

How do I set current render target?

Also, your method seems to require this to be a global:

 RenderTarget2D WholeScreen = new RenderTarget2D(GraphicsDevice, Map.Width, Map.Height);

But, I canā€™t declare it as a global. It has to be declared inside of Draw(). So, Iā€™m completely confused.

I donā€™t see the problem of grabbing the backbuffer. I just need to cut a rect out of Texture2D texture in the code above. That would solve my problem simply. Iā€™m having a lot of trouble trying to implement your pseudo-code.

yea sorry for the sloppy pseudo code.

Here is some practically copy pasted code from my current little project, that definitely works:


declarations:
Rectangle stencil_rect = new Rectangle(100,100,200,200);
RenderTarget2D big_RT;
RenderTarget2D small_RT;

in LOAD CONTENT:
 big_RT = new RenderTarget2D(_graphics.GraphicsDevice, screen_width, screen_height,false, SurfaceFormat.Color,DepthFormat.Depth24,0,RenderTargetUsage.PreserveContents);

small_RT = new RenderTarget2D(_graphics.GraphicsDevice, stencil_rect.Width, stencil_rect.Height, false, SurfaceFormat.Color, DepthFormat.Depth24, 0, RenderTargetUsage.PreserveContents);
           
IN DRAW:
// first draw all your stuff, but to a rendertarget.
GraphicsDevice.SetRenderTarget(big_RT);
spritebatch.Begin()
//draw game HERE as per usual
spritebatch.End();

//get a designated section of said rendertarget, by drawing it using the stencil ON a seperate Rendertarget...
GraphicsDevice.SetRenderTarget(small_RT);
spritebatch.Begin()
spritebatch.Draw(big_RT, vector2.Zero, stencil_rect, color.white.);
spritebatch.End();

// here you just draw the big render-target to the screen, so the player can see the game.
GraphicsDevice.SetRenderTarget(null);
spritebatch.Begin()
spritebatch.Draw(big_RT, vector2.Zero, null, color.white.);
spritebatch.End();

Then of coarse, youā€™ll need to do the small_RT.save bit, but that should be a breezeā€¦ Make sure to save the texture after it has been drawn.

Having your stuff on one render-target makes a lot of effects much easier too, from certain shaders to camera shake, and stuff like screen shots, fading to black, mass-scaling, whatever you want, now you just need to manipulate ONE sprite.

Thanks! Iā€™m going to mark that as the solution. However, while you were doing that, I found some code to crop my existing backbuffer Texture so Iā€™m going to post that here, too, for anybody who needs it later:

  public void TakeScreenCap()
        {
            Texture2D JustMap = new Texture2D(GraphicsDevice, Map.Width, Map.Height);
            Microsoft.Xna.Framework.Rectangle MapRect = new Microsoft.Xna.Framework.Rectangle(LeftMapOffset, TopMapOffset, Map.Width, Map.Height);

            string slug = ScenarioFileName + CurrentGameTime.ToString("hhmm");

            int w = GraphicsDevice.PresentationParameters.BackBufferWidth;
            int h = GraphicsDevice.PresentationParameters.BackBufferHeight;

            //force a frame to be drawn (otherwise back buffer is empty) 
            Draw(new GameTime());

            //pull the picture from the buffer 
            int[] backBuffer = new int[w * h];
            GraphicsDevice.GetBackBufferData(backBuffer);

            //copy into a texture 
            Texture2D texture = new Texture2D(GraphicsDevice, w, h, false, GraphicsDevice.PresentationParameters.BackBufferFormat);
            texture.SetData(backBuffer);

            JustMap = Crop(texture, MapRect);

            //save to disk 
            Stream stream = File.OpenWrite(slug + ".png");

            JustMap.SaveAsPng(stream, Map.Width, Map.Height);
            stream.Dispose();

            texture.Dispose();
        }


        public static Texture2D Crop(Texture2D image, Rectangle source)
        {
            var graphics = image.GraphicsDevice;
            var ret = new RenderTarget2D(graphics, source.Width, source.Height);
            var sb = new SpriteBatch(graphics);

            graphics.SetRenderTarget(ret); // draw to image
            graphics.Clear(new Color(0, 0, 0, 0));

            sb.Begin();
            sb.Draw(image, Vector2.Zero, source, Color.White);
            sb.End();

            graphics.SetRenderTarget(null); // set back to main window

            return (Texture2D)ret;
        }

Amazing, I actually asked around before I gave my answer, so you must have found some ancient wizard stuff there.

Yeah, it was ancient crop code from Stack Overflow.

1 Like

You are pulling data from GPU to CPU, then get them from CPU to GPU (exactly same data). Then create new buffer!!! on GPU, then do GPU to GPU (that in your case still require RTV bind and drawcall from CPU) and then pull from GPU to CPU once again to finally save it. This is one of the most absurd, worst solution, Iā€™ve seen in my entire life. Congratulation. Actually for good measure you also allocate new spritebatch.

How to do it properly: Right after you get to CPU from backbuffer you save the damn data and be done with it. Also if you plan to do that repeatedly you donā€™t heap allocate array for that data pull everytimeā€¦

I donā€™t get how that your ā€œsolutionā€ can seem or feel reasonable. It is borderline heresy. For how to save the data: check SaveAsPng function in source code, thatā€™s all you need to do, basic programmer newbie approach, you read existing code. Students of first semester CS are capable of doing that, so called doctorate is definitely able to do it as well.

Edit: Data are row-major, all you need to know to crop it (not to mention there maybe is overload of that function to get given data area in the first place).

Also note: I ofc donā€™t care about what you do in your own project, thatā€™s all perfectly fine. I am worried about ppl that will come looking for solution and will consider this to be a good idea.

1 Like

I donā€™t quite understand what you are saying, itā€™s probably well over my head.

But what do you think of my approach as listed above? the render target stuff.

Yours is fine.

1 Like

You seem to have a lot of anger. I like this forum and I donate money to MonoGame because I like the community and itā€™s helpful. You are the exception.

If you have a better solution, please post the code and enlighten everybody.

You can read my ā€˜so called doctorateā€™ thesis here.

That code is right there:

Check SaveAsPng then go to platform you use and you will see exact code you need to see.

Funny you brought that up, because I believe it was this topic that made you donate:

Anyway, it doesnā€™t matter, what matters is how much you are derailing people looking for help with your ā€œsolutionsā€. Basically pattern is: Ask about basics, get reasonable answer, then instead come with your own, absolutely wrong horrible approach.

Itā€™s fine to not know, itā€™s fine to ask, but if we donā€™t know we have to be aware of not knowing.

1 Like