Recent

Author Topic: Sprites processing with standard Lazarus tools  (Read 1822 times)

_N_

  • New Member
  • *
  • Posts: 30
Sprites processing with standard Lazarus tools
« on: September 20, 2021, 01:42:08 pm »
Hello all.
I am writing piece of software that works with sprites. It should be able to use external resources that are stored in png images (I cannot embed them). Also, I don't need all of them, but only some subset (subset of sprites in every png). So for now my plan is the following:
1. Ask use for the path to original sprites
2. Pick needed sprites, process them, and save near the application
3. Load processed sprites at runtime (no modifications at this step)
But I have problem with step 2 (I tried to do the processing from original resources at runtime, but it was very slow (I also do repainting), so decided to spend a few megabytes of RAM to speed up everything and do processing beforehand and only load result at runtime).
What's needed for a sprite file:
1. Get several first sprites from png image (I know sizes for each file).
2. Duplicate them with flips to get "full row".
3. Copy the row several times, but repainting some colors (I have color mapping what should be replaced).
4. Save all this into a png file that will be used further on.
I don't want to use OpenGL or similar and also would like not to depend on an external library (but still consider it as an option if using standard mechanism is problematic).
For now I have:
Code: Pascal  [Select][+][-]
  1.   PNG := TPortableNetworkGraphic.Create();
  2.   Sprites := TImageList.Create(nil);
  3.   try
  4.     PNG.LoadFromFile(SourceFile);
  5.  
  6.     Sprites.Width := PNG.Width div 5;
  7.     Sprites.Height := getHeightByName(SpriteName);
  8.     // Sprites.BkColor := clBlack;
  9.     // Sprites.DrawingStyle := dsTransparent;
  10.  
  11.     for p := 1 to 10 do begin
  12.       Sprites.AddSliced(PNG, 5, 1); // Quick way to extract needed sprites from png
  13.  
  14.       // HERE I NEED TO ADD FLIPPED/ROTATED EXISTING SPRITES
  15.  
  16.       // Repaint sprites
  17.       for i := 0 to ROW_SPRITES_COUNT - 1 do begin
  18.         SpriteIndex := p * ROW_SPRITES_COUNT + i;
  19.  
  20.         SpriteBitmap := TBitmap.Create();
  21.         try
  22.           Sprites.GetBitmap(SpriteIndex, SpriteBitmap);
  23.           RepaintSprite(SpriteBitmap, ORIGINAL_COLOR, DESIRED_COLOR);
  24.           Sprites.Replace(SpriteIndex, SpriteBitmap, nil);
  25.         finally
  26.           SpriteBitmap.Free();
  27.         end;
  28.       end;
  29.     end;
  30.  
  31.     // Save image list to png file
  32.  
  33.     Bitmap := TBitmap.Create();
  34.     Bitmap.Height := Sprites.Height;
  35.     Bitmap.Width := Sprites.Width * Sprites.Count;
  36.     for i:=0 to (Sprites.Count - 1) do
  37.       Sprites.Draw(Bitmap.Canvas, i * Sprites.Width, 0 , i);
  38.     try
  39.       // THIS LOSE TRANSPARENCY WHICH IS NEEDED
  40.       // I have to use png imgae instead bitmap here but I can get only bitmap from image list, what to do?
  41.       Bitmap.SaveToFile(DestFile);
  42.     finally
  43.       Bitmap.Free();
  44.     end;
  45.  
  46.   finally
  47.     Sprites.Free();
  48.     PNG.Free();
  49.   end;      
  50.  
  51. // --------------------------------------------
  52.  
  53. procedure RepaintSprite
  54.   if (ColorFrom = ColorTo) then Exit;
  55.   Bitmap.BeginUpdate();
  56.   for y:=0 to (Bitmap.Height - 1) do begin
  57.     for x:=0 to (Bitmap.Width - 1) do begin
  58.       // I know that it is possible to read whole row,  but I read somewhere
  59.       // that it may bring trouble on different platforms, is it true?
  60.       PixelColor := Bitmap.Canvas.Pixels[x, y];
  61.       for i:=0 to SHADES_NUMBER do begin
  62.         if (PixelColor = SPRITES_COLORS[ColorFrom][i]) then begin
  63.           Bitmap.Canvas.Pixels[x, y] := SPRITES_COLORS[ColorTo][i];
  64.           break;
  65.         end;
  66.       end;
  67.     end;
  68.   end;
  69.   Bitmap.EndUpdate();  
  70.  

To sum up:
1. How to copy & flip sprites and and them into the same imagelist (or maybe I do not need image list at all here, but then I have to read sprites from original png myself, but I don't know how to copy an area and keep transparency...)?
2. Repaint. First, how to correctly replace (or maybe add after repaint) in imagelist. Second, but not critical, optimization. Pixel access is very slow, but can I safely use faster method (and keep cross platform support)? At least I can repaint first, so after flip I don't need to repaint again.
3. How to save back to png file. How to get sprite from imagelist keeping transparency and draw on resulting png in memory?

I do not need compatibility with Delphi, so using Lazarus specific classes are completely ok, but I need to support cross platform compilation.

I don't have much experience with images processing in Lazarus or Pascal, so sorry for several, probably silly questions (for people who know)... I googled, but there are different problems with different solutions... And people use different approaches: TBitmap, TImage, TPicture, TCanvas, Masks, etc (actually it should be sort of the same, but again if you know details about implementations). I got somewhat lost. Code above is what I was able to achieve for now...

Thank you in advance.

Mr.Madguy

  • Hero Member
  • *****
  • Posts: 724
Re: Sprites processing with standard Lazarus tools
« Reply #1 on: September 22, 2021, 09:17:42 am »
Unfortunately it sounds like "I need advanced 2D processing tools, but I don't want to use them". Some things, you ask for, like copying parts of bitmaps and even flipping them (not 100% sure about TCanvas, but raw GDI allows it), are possible. But other things, like color mapping, aren't possible. At least if you don't use 8bit palette, but 16/32bit target without palette. FPC doesn't seem to support 8bit bitmaps though. You can always try memory bitmaps, like DIBs from GDI. But if you want cross-platform solution, then may be you should try BGRABitmap.

Overall advanced 2D processing requires something like DirectDraw. But once DirectDraw is obsoleted and 2D is now considered to be subset of 3D - 3D libraries should be used instead.
07.10.2021 - Major bug is fixed in main project, that was causing random crashes in 64bit version.
My project still requires full Delphi 2009 support to be ported to Lazarus.
It's time to finally do it, because Delphi 2009 is 12 years old.

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 9600
  • FPC developer.
Re: Sprites processing with standard Lazarus tools
« Reply #2 on: September 22, 2021, 10:49:59 am »
Any  Bitmap.Canvas.Pixels[x, y] is slow.

Try to find or create a dedicated copy procedure that optimizes this.

Mr.Madguy

  • Hero Member
  • *****
  • Posts: 724
Re: Sprites processing with standard Lazarus tools
« Reply #3 on: September 22, 2021, 11:47:24 am »
Pure Canvas can actually be used.

Sprite flipping can be achieved via code like this:
Code: Pascal  [Select][+][-]
  1.   Canvas.CopyRect(ClientRect, Picture.Bitmap.Canvas, TRect.Create(0, Picture.Height, Picture.Width, 0));
  2.  

Repainting some colors is harder task, but can actually be achieved via using masked bitmap. I won't provide full working example, but here is template:
Code: Pascal  [Select][+][-]
  1. //This two are done every time, you need to change repaint color
  2. BackdropBitmap.Canvas.Brush.Color := MyColor;
  3. BackdropBitmap.Canvas.FillRect(TRect.Create(0, 0, BackdropBitmap.Width, BackdropBitmap.Height));//Not sure, if 1x1 pixel bitmap can be used here for optimization purposes
  4. //Load predefined MaskBitmap here
  5. BackdropBitmap.MaskHandle := MaskBitmap.ReleaseHandle;//You need to do it only once
  6. //You may destroy MaskBitmap after that
  7. Canvas.Draw(0, 0, MySprite);
  8. Canvas.Draw(0, 0, BackdropBitmap);
  9. //Just another way to get mask bitmap, if one color should be changed to another
  10. MySprite.Mask(ColorToChange);
  11. //Ehhh. Mask is reversed here - you need to manually reverse it.
  12. BackdropBitmap.MaskHandle := MySprite.ReleaseMaskHandle;
  13.  
« Last Edit: September 22, 2021, 12:26:05 pm by Mr.Madguy »
07.10.2021 - Major bug is fixed in main project, that was causing random crashes in 64bit version.
My project still requires full Delphi 2009 support to be ported to Lazarus.
It's time to finally do it, because Delphi 2009 is 12 years old.

wp

  • Hero Member
  • *****
  • Posts: 8905
Re: Sprites processing with standard Lazarus tools
« Reply #4 on: September 22, 2021, 11:53:14 am »
There is a demo in folder examples/sprites of the Lazarus installation.
Mainly Lazarus trunk / fpc 3.2.0 / all 32-bit on Win-10, but many more...

Mr.Madguy

  • Hero Member
  • *****
  • Posts: 724
Re: Sprites processing with standard Lazarus tools
« Reply #5 on: September 22, 2021, 01:16:53 pm »
Here is example. Every time you click image - new random color and direction are generated. Sorry for a little bit messy code. No time to optimize it.
« Last Edit: September 22, 2021, 01:33:23 pm by Mr.Madguy »
07.10.2021 - Major bug is fixed in main project, that was causing random crashes in 64bit version.
My project still requires full Delphi 2009 support to be ported to Lazarus.
It's time to finally do it, because Delphi 2009 is 12 years old.

_N_

  • New Member
  • *
  • Posts: 30
Re: Sprites processing with standard Lazarus tools
« Reply #6 on: September 24, 2021, 07:49:34 am »
Thank you, all, for the help.
I am still trying to solve the issue.

Mr.Madguy, thank you for the sample project. I got some ideas from it, but unfortunately it doesn't work under Linux (I see Mario on blue background or just a random color that fills whole image).
CopyRect does the work with flips, thanks!

wp, you are right, I forgot to look at samples when posting this, sorry, but now I know that the samples don't help with my issue.

marcov, thanks, I understand that it migth work, but it requires knowledge about some internals (that I don't have) and also a subject to get issues between platforms...

So, the code I have now (it doesn't do completely what I need and very slow because of repaint, but maybe someone find it, or parts of it, useful):
Code: Pascal  [Select][+][-]
  1.   SpritesImage := TPortableNetworkGraphic.Create();
  2.   ResultImage := TPortableNetworkGraphic.Create();
  3.   //ResultImage.PixelFormat := pf32bit;
  4.   try
  5.     SpritesImage.LoadFromFile(SourceFile);
  6.  
  7.     SpriteWidth := SpritesImage.Width div UNITS_SPRITES_PER_ROW;
  8.     SpriteHeight := SpriteData.SpriteHeight;
  9.     ResultImage.Width := SpriteWidth * SPRITES_PER_PLAYER_UNIT;
  10.     ResultImage.Height := SpriteHeight * MAX_PLAYER_NUMBER;
  11.  
  12.     // For copying and flipping CopyRect is used, but it loses transparency! Not resolved yet, but if one doesn't need transparency works great!
  13.     // Copy original 5 sprites
  14.     SrcRect := TRect.Create(0, 0, SpritesImage.Width, SpriteHeight);
  15.     DestRect := TRect.Create(0, 0, SpritesImage.Width, SpriteHeight);
  16.     ResultImage.Canvas.CopyRect(DestRect, SpritesImage.Canvas, SrcRect);
  17.     // Add missing sprites to the sequence
  18.     // It should: 1 2 3 4 5 -> 1 2 3 4 5 4' 3' 2'
  19.     for i := 1 to 3 do begin
  20.       SrcIndex := UNITS_SPRITES_PER_ROW - i;
  21.       DestIndex := UNITS_SPRITES_PER_ROW + i;
  22.       // Reverse destination x axis to get horizontal flip effect
  23.       SrcRect := TRect.Create((SrcIndex - 1) * SpriteWidth, 0, SrcIndex * SpriteWidth, SpriteHeight);
  24.       DestRect := TRect.Create(DestIndex * SpriteWidth, 0, (DestIndex - 1) * SpriteWidth, SpriteHeight);
  25.       ResultImage.Canvas.CopyRect(DestRect, SpritesImage.Canvas, SrcRect);
  26.     end;
  27.  
  28.     // Duplicate first row
  29.     SrcRect := TRect.Create(0, 0, ResultImage.Width, SpriteHeight);
  30.     for c := 2 to 8 do begin
  31.       DestRect := TRect.Create(0, c * SpriteHeight, ResultImage.Width, (c + 1) * SpriteHeight);
  32.       ResultImage.Canvas.CopyRect(DestRect, ResultImage.Canvas, SrcRect);
  33.     end;
  34.  
  35.     // Approach with Canvas.Pixels is extremely slow (~10 sec per image which is ~700x700 px on old 2HGz Celeron), but safe unlike ScanLine
  36.     // Also I don't use Begin/EndUpdate as using them causes just a black color over all image (even calling EndUpdate right after BeginUpdate! Have no idea why).
  37.     // Repaint each row for corresponding color
  38.     // First row already has correct color
  39.     for c := 2 to 8 do begin
  40.       for y := c * SpriteHeight to (c + 1) * SpriteHeight do begin
  41.         for x := 0 to ResultImage.Width do begin
  42.           PixelColor := ResultImage.Canvas.Pixels[x, y];
  43.           for i := 0 to COLOR_SHADES_NUMBER do begin
  44.             if (PixelColor = SPRITES_COLORS[1][i]) then begin
  45.               ResultImage.Canvas.Pixels[x, y] := SPRITES_COLORS[c][i];
  46.               break;
  47.             end;
  48.           end;
  49.         end;
  50.       end;
  51.     end;
  52.  
  53.     //ResultImage.TransparentColor := clBlack;
  54.     ResultImage.SaveToFile(ResultFileName + '.png']));
  55.   finally
  56.     SpritesImage.Free();
  57.     ResultImage.Free();
  58.   end;                                    
  59.  

Looking at BGRABitmap now... seems like a real option to go with.

Mr.Madguy

  • Hero Member
  • *****
  • Posts: 724
Re: Sprites processing with standard Lazarus tools
« Reply #7 on: September 25, 2021, 06:34:16 am »
Unfortunately I won't be able to test my project on Linux till Monday. Masked transparency - is capricious thing. But if transparent bitmaps and image lists (with Masked = true) work on Linux, then there should be way to make it work there.
« Last Edit: September 25, 2021, 07:00:44 am by Mr.Madguy »
07.10.2021 - Major bug is fixed in main project, that was causing random crashes in 64bit version.
My project still requires full Delphi 2009 support to be ported to Lazarus.
It's time to finally do it, because Delphi 2009 is 12 years old.

_N_

  • New Member
  • *
  • Posts: 30
Re: Sprites processing with standard Lazarus tools
« Reply #8 on: September 25, 2021, 02:12:00 pm »
Thank you for the response. I've installed BGRABitmap and will probably use it, so no need in fixing pure Linux example (only for curiosity).
However, I got a new issue with BGRABitmap. I cannot CopyRect from the same image. I was able to copy from the source image, but duplicating of the first row doesn't work and I don't know why:
Code: Pascal  [Select][+][-]
  1. const
  2.   SPRITES_PER_ROW = 5;
  3.   SPRITES_PER_UNIT = 8;
  4. var
  5.   SpritesImage: TBGRABitmap;
  6.   ResultImage: TBGRABitmap;
  7.   SpriteWidth: Integer;
  8.   SpriteHeight: Integer;
  9.   SrcRect: TRect;
  10.   DestRect: TRect;
  11.   i: Integer;
  12.   c: integer;
  13. begin
  14.   SpritesImage := TBGRABitmap.Create(SourceFile);
  15.   SpriteWidth := SpritesImage.Width div SPRITES_PER_ROW;
  16.   SpriteHeight := SpriteData.SpriteHeight;
  17.   ResultImage := TBGRABitmap.Create(SpriteWidth * SPRITES_PER_UNIT, SpriteHeight * MAX_PLAYER_NUMBER);
  18.   try
  19.     // Copy original 5 sprites
  20.     SrcRect := TRect.Create(0, 0, SpritesImage.Width, SpriteHeight);
  21.     DestRect := TRect.Create(0, 0, SpritesImage.Width, SpriteHeight);
  22.     ResultImage.Canvas.CopyRect(DestRect, SpritesImage.Canvas, SrcRect);
  23.     // Add missing sprites to the sequence
  24.     // It should: 1 2 3 4 5 -> 1 2 3 4 5 4' 3' 2'
  25.     for i := 1 to 3 do begin
  26.       SrcIndex := SPRITES_PER_ROW - i;
  27.       DestIndex := SPRITES_PER_ROW + i;
  28.       // Reverse destination x axis to get horizontal flip effect
  29.       SrcRect := TRect.Create((SrcIndex - 1) * SpriteWidth, 0, SrcIndex * SpriteWidth, SpriteHeight);
  30.       DestRect := TRect.Create(DestIndex * SpriteWidth, 0, (DestIndex - 1) * SpriteWidth, SpriteHeight);
  31.       ResultImage.Canvas.CopyRect(DestRect, SpritesImage.Canvas, SrcRect);
  32.     end;
  33.  
  34.    // THE CODE BELOW DOES NOTHING, WHY?
  35.  
  36.     // Duplicate first row for each color
  37.     SrcRect := TRect.Create(0, 0, ResultImage.Width, SpriteHeight);
  38.     for c := 1 to 7 do begin
  39.       DestRect := TRect.Create(0, c * SpriteHeight, ResultImage.Width, (c + 1) * SpriteHeight);
  40.       ResultImage.Canvas.CopyRect(DestRect, ResultImage.Canvas, SrcRect);
  41.     end;    
  42.  
  43.    ResultImage.SaveToFile(DestName + '.png']));    
  44.   finally
  45.     SpritesImage.Free();
  46.     ResultImage.Free();
  47.  
  48.   end;
  49. end;
  50.  

As a workaround, I may create a new BGRABitmap and copy into it, but I'd like to know why it is not possible to copy from the same BGRABitmap (or maybe I just miss something)...

winni

  • Hero Member
  • *****
  • Posts: 2714
Re: Sprites processing with standard Lazarus tools
« Reply #9 on: September 25, 2021, 03:21:22 pm »
Hi!

Wprking with BGRAbitmap needs a minimum of knowledge of the abilities and the syntax. It is not just another canvas.

Some things you need are
Code: Pascal  [Select][+][-]
  1.  
  2. BGRAdest.putimage (x,y, BGRAsource,TDrawMode);

The most used  TDrawMode are dmSet or dmDrawWithTransparency.

If you need a part of a BGRAbitmap you do:
Code: Pascal  [Select][+][-]
  1. part : TBGRAbitmap;
  2. R : TRect;
  3. ...
  4. part := TBGRAbitmap.create;
  5. BGRAreplace (part, SourceBGRA.getPart(R));

Have a look in the source of the BGRAbitmap.
And read the tutorial:

https://wiki.lazarus.freepascal.org/BGRABitmap_tutorial


Winni

And read about the component TBGRASspriteAnimation:

 https://wiki.freepascal.org/BGRASpriteAnimation
« Last Edit: September 25, 2021, 03:24:45 pm by winni »

_N_

  • New Member
  • *
  • Posts: 30
Re: Sprites processing with standard Lazarus tools
« Reply #10 on: October 16, 2021, 09:32:56 am »
Thank you, Winni for your answer and sorry for the delay.
I had read the tutorial, but it says nothing about the functions you showed. I employed PutImage that worked for some tasks, but what is still unknown is how to copy a part of a BGRA image into another part of another image (for now even without resampling).
As about reading source, it's not that easy and time consuming. I feel like it should be done if one wants to be a guru for that lib or really needs to clarify how a procedure behaves. Instead, I just used autocompletion in IDE which could give some relevant to the problem procedures and the google interesting ones.
Also, want to say a note about speed (maybe it will be useful for others). BGRA lib is really fast. It converts a hundred of images (with colors replacing!) in a moment, so now I am in doubt if I need the cache at all.

 

TinyPortal © 2005-2018