Skip to main content
GameDev.net gamedev.net
🔒 Locked 🤖 Godot

Trying to dynamically render cards in game faster

Started by Oconzer Feb 2, 2015 at 9:35 PM 15 replies 5.3k views
Original Post
Oconzer
Oconzer

For my game that I'm developing I want to have my cards dynamically drawn so if there is any changes done to them it could be drawn on the card itself. I'm currently using a CPU method as I'm not sure if there is a way to do it via GPU as I'm new to drawing and textures.

What I have currently is that the cards are drawn to a RenderTarget at their originally designed size of 2100x1480 px. (I'll probably for game purposes have to scale that down to 1050x740 for its native size, but was designed that way to have 600 dpi for physical versions).

Anyway the way I want to do it is that after drawing everything to the RenderTarget I scale it down and store that scaled down texture to the card's texture (which will be 164 x 115 for the playing size, the 1050 x 740 is the zoomed in size that I want so players can view what the card does and stats). The issue that I'm facing is that the way I currently tried to do it was far too slow taking over 9 seconds to scale it down after drawing the original size to the RenderTarget.

The code that I'm currently using:


protected void DrawCardTest(decimal cardScale)
{
   startTime = DateTime.Now;
   // Set the device to the render target
   graphics.GraphicsDevice.SetRenderTarget(cardRenderTarget);

   graphics.GraphicsDevice.Clear(Color.Transparent);

   spriteBatch.Begin();
   Vector2 pos = Vector2.Zero;
   spriteBatch.Draw(cardDrawables.Texture, pos, cardDrawables.SourceRectangle("Blank Card Front"), Color.White, 0.0F, pos, new Vector2(1.0F, 1.0F), SpriteEffects.None, 0.0F);
   spriteBatch.End();

   // Reset the device to the back buffer so can grab the texture from the rendertarget
   graphics.GraphicsDevice.SetRenderTarget(null);
   colorArrayForTexture = new Color[cardDrawables.SourceRectangle("Blank Card Front").Width * cardDrawables.SourceRectangle("Blank Card Front").Height];
   cardRenderTarget.GetData(colorArrayForTexture);

   cardTest = new Texture2D(graphics.GraphicsDevice, (int)(cardRenderTarget.Width * (decimal)cardScale), (int)(cardRenderTarget.Height * (decimal)cardScale));

   Color[] arrayAfterScale = new Color[(int)(cardRenderTarget.Width * (decimal)cardScale) * (int)(cardRenderTarget.Height * (decimal)cardScale)];

   cardTest.SetData(ScaleTexture(colorArrayForTexture, cardRenderTarget.Width, cardRenderTarget.Height, (decimal)cardScale));
   endTime = DateTime.Now;
}

public Color[] ScaleTextureNew(Color[] textureData, int width, int height, decimal scaleAmount)
{
   #region ScaleTexture Variables and Setup
   decimal scaleX = scaleAmount;
   decimal scaleY = scaleAmount;

   int widthAfterScale = (int)(width * scaleX);
   int heightAfterScale = (int)(height * scaleY);

   //create a pixel array that will hold the scaled texture data
   Color[] scaledTextureData = new Color[widthAfterScale * heightAfterScale];

   ColorStorage colorToAverage = new ColorStorage();

   decimal widthStep = (decimal)width / (decimal)widthAfterScale;
   decimal heightStep = (decimal)height / (decimal)heightAfterScale;

   decimal widthTotal = widthStep;
   decimal heightTotal = heightStep;

   //check to see if width sample total or height sample total will be repeating
   if ((widthStep * widthAfterScale) != width) // this should equal out if not there is a repeating issue
      widthTotal += repeatingFix;
   if ((heightStep * heightAfterScale) != height) // this should equal out if not there is a repeating issue
      heightTotal += repeatingFix;

   decimal sampledWidth = 0.0M;
   decimal sampledHeight = 0.0M;

   decimal widthSampling = 0.0M;
   decimal heightSampling = 0.0M;

   decimal widthSamplingTotal = 0.0M;
   decimal heightSamplingTotal = 0.0M;

   decimal widthIncrement = 0.0M;
   decimal heightIncrement = 0.0M;

   decimal widthRemainder = 0.0M;
   decimal heightRemainder = 0.0M;

   decimal transparentPixelArea = 0.0M;
   decimal areaSampled = 1.0M;
   decimal totalAreaSampled = 0.0M;

   decimal currentHeight = 0.0M;
   decimal currentWidth = 0.0M;

   decimal alpha = 0.0M;

   if (widthStep > 1.0M)
      widthIncrement = 1.0M;
   else if (widthStep < 1.0M)
      widthIncrement = widthStep;

   if (heightStep > 1.0M)
      heightIncrement = 1.0M;
   else if (heightStep < 1.0M)
      heightIncrement = heightStep;

   Index widthIndexUnscaled = new Index(0);
   Index heightIndexUnscaled = new Index(0);

   Index widthIndexScaled = new Index(0);
   Index heightIndexScaled = new Index(0);

   Index widthIndexToUse;
   Index heightIndexToUse;

   int indexIntoArray = 0;

   decimal[] samplingArrayWidth; // use the bigger between width and widthafterscale
   if (width > widthAfterScale)
   {
      samplingArrayWidth = new decimal[width];
      widthIndexToUse = widthIndexUnscaled;
   }
   else
   {
      samplingArrayWidth = new decimal[widthAfterScale];
      widthIndexToUse = widthIndexScaled;
   }
   CalculateSampling(ref samplingArrayWidth, widthStep);

   decimal[] samplingArrayHeight; // use the bigger between width and widthafterscale
   if (height > heightAfterScale)
   {
      samplingArrayHeight = new decimal[height];
      heightIndexToUse = heightIndexUnscaled;
   }
   else
   {
      samplingArrayHeight = new decimal[heightAfterScale];
      heightIndexToUse = heightIndexScaled;
   }
   CalculateSampling(ref samplingArrayHeight, heightStep);

   bool doneScaling = false;
   #endregion

   //will iterate top left to right bottom
   while (!doneScaling)
   {
      while (sampledHeight < heightStep)
      {
         if (heightRemainder == 0.0M)
         {
            heightRemainder = heightStep - samplingArrayHeight[heightIndexToUse.Point];
            heightSampling = heightStep - heightRemainder;
         }
         else if (heightRemainder >= 1.0M)
         {
            heightRemainder -= samplingArrayHeight[heightIndexToUse.Point];
            heightSampling = 1.0M;
         }
         else
            heightSampling = heightRemainder;

         while (sampledWidth < widthStep)
         {
            if (widthRemainder == 0.0M)
            {
               widthRemainder = widthStep - samplingArrayWidth[widthIndexToUse.Point];
               widthSampling = widthStep - widthRemainder;
            }
            else if (widthRemainder >= 1.0M)
            {
               widthRemainder -= samplingArrayWidth[widthIndexToUse.Point];
               widthSampling = 1.0M;
            }
            else
               widthSampling = widthRemainder;

               areaSampled *= heightSampling * widthSampling;

               // check to see if the pixel is blank to correctly sample
               indexIntoArray = width * heightIndexUnscaled.Point + widthIndexUnscaled.Point; // so don't have to calculate index every time
               if (textureData[indexIntoArray].A == 0)
                  transparentPixelArea += areaSampled;

               alpha = textureData[indexIntoArray].A;
               colorToAverage.Alpha += alpha * areaSampled;
               alpha /= 255; //get the oppacity percentage to reverse the multiplyied alpha onto the colors

               if (alpha == 0.0M)
                  alpha = 1.0M; // this is so colors don't get created where there were none.

               // undivide the colors by alpha since its been done and needs to be corrected

               colorToAverage.Red += textureData[indexIntoArray].R * areaSampled / alpha;// *alpha * areaSampled;
               colorToAverage.Green += textureData[indexIntoArray].G * areaSampled / alpha;// *alpha * areaSampled;
               colorToAverage.Blue += textureData[indexIntoArray].B * areaSampled / alpha;// *alpha * areaSampled;

               //add the area sampled to total and reset area to sample
               totalAreaSampled += areaSampled;
               areaSampled = 1.0M;

               sampledWidth += widthSampling;
               widthSamplingTotal += widthSampling;
               widthIndexUnscaled.Point = (int)widthSamplingTotal; // have to do this here since counter has to change to sample properly
            }
            widthSampling = 0.0M;
            widthRemainder = 0.0M;
            sampledWidth = 0.0M;
            widthSamplingTotal = currentWidth;
            widthIndexUnscaled.Point = (int)widthSamplingTotal; // have to do this here since counter has to change to sample properly

            sampledHeight += heightSampling;
            heightSamplingTotal += heightSampling;
            heightIndexUnscaled.Point = (int)heightSamplingTotal; // have to do this here since counter has to change to sample properly
         }
         heightSampling = 0.0M;
         heightRemainder = 0.0M;
         sampledHeight = 0.0M;

         //calculate the scaled texture's value here
         scaledTextureData[heightIndexScaled.Point * widthAfterScale + widthIndexScaled.Point] = ColorAveraged(colorToAverage,
           (totalAreaSampled - transparentPixelArea), totalAreaSampled);

         //reset colorToAverage for next pass
         colorToAverage.Alpha = 0.0M;
         colorToAverage.Red = 0.0M;
         colorToAverage.Blue = 0.0M;
         colorToAverage.Green = 0.0M;

         totalAreaSampled = 0.0M;
         transparentPixelArea = 0.0M;

         if (widthTotal >= width) // at the right of the texture
         {
            if (heightTotal >= height) // at the bottom of the texture
               doneScaling = true;
            else
            {
               //increment height stuff for next line of texture
               currentHeight = heightTotal;
               heightSamplingTotal = currentHeight;
               heightIndexUnscaled.Point = (int)heightSamplingTotal; // have to do this here since counter has to change to sample properly
               heightTotal += heightStep;
               heightIndexScaled.Point++;

               //reset width stuff for next line of texture
               widthTotal = widthStep;

               if ((widthStep * widthAfterScale) != width) // this should equal out if not there is a repeating issue
                  widthTotal += repeatingFix;

               widthIndexScaled.Point = 0;
               widthIndexUnscaled.Point = 0;
            }
         }
         else
         {
            currentWidth = widthTotal;
            widthSamplingTotal = currentWidth;
            widthIndexUnscaled.Point = (int)widthSamplingTotal; // have to do this here since counter has to change to sample properly
            widthTotal += widthStep;
            widthIndexScaled.Point++;
            heightSamplingTotal = currentHeight;
            heightIndexUnscaled.Point = (int)heightSamplingTotal; // have to do this here since counter has to change to sample properly
         }
      }

      decimal widthToBlur;
      if (widthAfterScale > width) // only blur width if its been scaled up
         widthToBlur = ((decimal)widthAfterScale / (decimal)width) / 2.0M;
      else
         widthToBlur = 0.0M;

      decimal heightToBlur;
      if (heightAfterScale > height) // only blur height if its been scaled up
         heightToBlur = ((decimal)heightAfterScale / (decimal)height) / 2.0M;
      else
         heightToBlur = 0.0M;

      //if ((height < heightAfterScale) || (width < widthAfterScale)) //texture was scaled up so need to blur it
      // scaledTextureData = Blur(scaledTextureData, widthAfterScale, heightAfterScale,
      // widthToBlur, heightToBlur);
      return scaledTextureData;
   }

}

What the function is supposed to do. Example1: if scaling down a 4x4 image to a 2x2 image each pixel for the 2x2 image will be a composition of a 2x2 pixel sampling area of the original image.

Example2: if scaling down a 66x66 image to a 10x10 image each pixel for the 10x10 image will be a composition of a 6.6x6.6 pixel sampling area of the original image.

EDIT: Not sure how to do code for this site so it shows up more readable. :S

jms bc
jms bc

Yeah, pretty much impossible to read. Scaling with the draw call doesn't work well enough?

The Four Horsemen of Happiness have left.
frob
frob

Is there something that the built-in scaling and rotating code does not handle? Some reason you are trying to do all this yourself?

Seems rather burdensome to render it, copy the image, then attempt to manually scale the earlier rendering. Instead just draw it with the right parameters in the first place.

Oconzer
Oconzer

I want to be able to scale the texture down so it doesn't take up as much memory is the reason that I'm trying to scale the texture down from the render target and storing it in a texture. If I do not the memory taken by each card will be 2100x1480x4/1024/1024 = 11.856 MB per card and when my game is going to have 6 players able to play at once each with upto 100 cards that could not be duplicates, thats now 11.856 MB * 100 * 6 = 7113.6 MB

That is the reason that I need to be able to scale the texture down, not for being drawn but for storage reasons.

Also I don't have any clue how to format the code on this site to show up as code so its easily read.

Oconzer
Oconzer

Thanks braindigitalis as I couldn't figure out how to get the code to show up as code so it was less obnoxious.

Eklipse
Eklipse

You should just set your render target to the size you want it to be at. There is no difference really in drawing to a higher res render target then scaling it down after.

Then you don't have to do any work on the CPU.

But you say you want the texture at 1050 x 740 and 164 x 115. So you are going to have a minimum of the 1050 x 740 x 100 x 6 players or whatever your numbers were.

If you generate mips maps on those textures it will be a little more memory per card but you can then just render a quad at the 164x115 size with the full res texture to get the look of a small card.

The mips will help with performance since you don't need to read from the full res version to render the small size one.

You can also make sure your texture formats are using a compressed format. Also if you don't need alpha then it can be reduced as well and compressed better.

Oconzer
Oconzer

Not sure what you mean by the texture formats as I'm new to textures and drawing. Do you mean like the file format or something with when I declare the texture that will hold the image?

jHaskell
jHaskell

Surely not all 600 cards will be displayed all at the same time, which means you don't need to have the textures for all 600 cards loaded at the same time.

Load the textures when the card needs to be displayed. Unload them when the card is removed from play.

if it's some sort of fast paced action card game that needs to maintain 60fps, then you may need to implement some form of streaming pre-load of the next card in each players deck, but I find that unlikely.

phil_t
phil_t




You can also make sure your texture formats are using a compressed format. Also if you don't need alpha then it can be reduced as well and compressed better.

It's not possible to render to a compressed format in XNA, so I'm not sure how that's going to help anything. Certainly you should consider compressed formats for the base card textures though.




For my game that I'm developing I want to have my cards dynamically drawn so if there is any changes done to them it could be drawn on the card itself.

Are you sure you need to store the card images in a render target? What kinds of changes will be made to the "base card texture" during gameplay? Before resorting to using render targets (which have the memory issues, as you have found out, and also prevent you from using compressed formats), you should see if you can just redraw things from scratch each time.

Oconzer
Oconzer

Well for the cards they are being rendered to a render target then placed in a texture for storage. I planned on having the cards have stats changed based on effects on them from abilities or other cards, as well as showing any ability changes from temporary abilities being added to a card.

As for the game play its not action paced but more of TCG paced is the best way I can put it.

As for cards on the playing field if players aren't losing many there could be situations where there would be at least 400+ cards out on the field. Cards in the deck of course wouldn't need to have but the back of the card drawn, but cards in the scrapyard would need to be kept to be drawn in case a player has a card that allows them to do stuff with the scrapyard or if a player wants to see what all is in a scrapyard.

phil_t
phil_t




Well for the cards they are being rendered to a render target then placed in a texture for storage. I planned on having the cards have stats changed based on effects on them from abilities or other cards, as well as showing any ability changes from temporary abilities being added to a card.

Right, but I'm asking why you need to store their images. You obviously already have the logic to render what you need to the render target where you're currently storing their image. Can't you just use that logic to draw them directly to the back buffer? Does your draw code not support scaling for some reason? Or do you think it's faster to store them in a render target and just draw that image? It might be, but it might not be. Using cached images means less draw calls, but it also comes with limitations like no texture compression (which means more texture bandwidth required to draw the card).

If you really decide you need cached images, and for some reason you need to render your cards at 2100x1480 even though you store them smaller, then you only need a single 2100x1480 render target. You'll render to the 2100x1480 render target, and then render again to a 164x115 render target (or whatever size you decide you need per-card). The large render target would only be used as an intermediate step, and you don't care about preserving its contents.

jHaskell
jHaskell




Well for the cards they are being rendered to a render target then placed in a texture for storage. I planned on having the cards have stats changed based on effects on them from abilities or other cards, as well as showing any ability changes from temporary abilities being added to a card.

If I understand what you're saying there (and I may not be), don't do that. Each card should be a composition of elements that are rendered completely each and every frame. If your cards have a common border, that's one single texture that should be loaded and exist in memory in one single location. Honestly, the ONLY thing that should be unique for each card is the image for that card, and even then there are options for reusing textures to reduce total texture memory footprint Applying various color modifications to the same texture at draw time can provide you with a blue dragon, green dragon, white dragon, red dragon, etc all from the same texture. Not nearly as much variety for sure, but if you're having memory usage issues, these are the sort of resolutions you need to consider using. Stats, descriptions, abilities, effects, buffs, etc should NOT be part of a pre-rendered, stored texture. All this data should be stored as text, ints, bools, enums, etc for a fraction of the memory cost. Then you use either a single graphical font or a small handful of graphical fonts to render that data when and where it needs to be displayed on screen each frame.

There is no reason to prerender each individual card, especially when you have such a large number of 'unique' cards that then introduces such a large memory burden with that technique. Extract out all common visuals and compose each and every card visually, each and every frame. it's a card game, unless you want to run on extremely old hardware, the burden on the processor and gpu will be easily manageable, likely even minimal.

Oconzer
Oconzer

Well for the cards they are being rendered to a render target then placed in a texture for storage. I planned on having the cards have stats changed based on effects on them from abilities or other cards, as well as showing any ability changes from temporary abilities being added to a card.

Right, but I'm asking why you need to store their images. You obviously already have the logic to render what you need to the render target where you're currently storing their image. Can't you just use that logic to draw them directly to the back buffer? Does your draw code not support scaling for some reason? Or do you think it's faster to store them in a render target and just draw that image? It might be, but it might not be. Using cached images means less draw calls, but it also comes with limitations like no texture compression (which means more texture bandwidth required to draw the card).

If you really decide you need cached images, and for some reason you need to render your cards at 2100x1480 even though you store them smaller, then you only need a single 2100x1480 render target. You'll render to the 2100x1480 render target, and then render again to a 164x115 render target (or whatever size you decide you need per-card). The large render target would only be used as an intermediate step, and you don't care about preserving its contents.

OK. I may be explaining it in a way that is confusing people (one of my bad traits). Going to explain as best I can what I was doing.

I've got a single rendertarget that is 2100x1480 at the moment for my game for testing out drawing cards. I don't have multiple. The reason that I was going to render to that rendertarget then shrink it down to store it in a texture was because I thought it would save on having to redraw everything that each card needs every time the game needs to redraw the cards(which depending on the number of cards that need to be drawn based on number in play would become an issue from what I'm thinking), making the game perform better.

I've got a "blank" card front that is the common image that the cards use. I am setting up the code to draw all the stats, icons, artwork for the card (the image on the card that represents what the object that the card is for would look like) to the blank card after its been drawn to the render target.

I was going to have my game render the card at the time that the player draws it from their deck of cards, as well as if any changes happen to a card and then store the texture for the small (164x115 texture) card only. The reason for this is that I figured I'd just remake the zoomed card image (1050x740 texture) when the player zooms in on a card to be able to see what the card does because you can only zoom on one card.

While I'm thinking of it I may have thought of a way to do what I wanted to do originally. I should be able to draw everything about the card to the one 2100x1480 render target, then store that to a texture, then redraw that texture at the scaled size to a new render target that is of the zoomed or small card size and store that so I have my reduced in size card textures right?

phil_t
phil_t

While I'm thinking of it I may have thought of a way to do what I wanted to do originally. I should be able to draw everything about the card to the one 2100x1480 render target, then store that to a texture, then redraw that texture at the scaled size to a new render target that is of the zoomed or small card size and store that so I have my reduced in size card textures right?

Yes, that's what I explained in the 2nd paragraph of my last post (A render target is a texture by the way, you don't need to "store it to a texture").

Note that if this is on windows, you'll lose the contents of all your render targets when the device is lost. So you'll need to be able to regenerate all the card images on demand. This is with XNA/DX9, I'm not sure what the behavior of monogame is with OpenGL or DX11.

Due to all the complexity associated with this, and the memory issues, I (like other posters) would not recommend this approach as the default approach.


The reason that I was going to render to that rendertarget then shrink it down to store it in a texture was because I thought it would save on having to redraw everything that each card needs every time the game needs to redraw the cards(which depending on the number of cards that need to be drawn based on number in play would become an issue from what I'm thinking), making the game perform better.

That's an untested assumption. Games generally redraw everything every frame. If your rendering code is primitive though, and doesn't make any attempt at reducing draw calls and texture switches and the like, then it's possible redrawing cards from scratch every frame could be a bottleneck. A game like you described would generally do it this way though. Especially if the alternative was potentially keeping 100s of decent-sized render targets around.

Oconzer
Oconzer

Yes, that's what I explained in the 2nd paragraph of my last post (A render target is a texture by the way, you don't need to "store it to a texture")

Ok I didn't understand that. I thought texture was in the memory rather than video memory. Then would that apply to the color array that I use to grab the data from the render target (is it in video or normal memory) or would that work for keeping the cards data from being lost?

Because if that would work then all I have to do is after drawing to the scaled down rendertarget is get the data and then when I want to draw the card use a texture to set the data into and draw it.

Also I've got the draw calls reduced to the max frames that the user has set by doing a test for if the time has passed the next allowed frame time.

Not sure what you mean by texture switches, unless you mean the redrawing of the card if something changes to its stats / abilities.

(Sorry for not posting this sooner been busy).

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.