lexus2k / ssd1306

Driver for SSD1306, SSD1331, SSD1351, IL9163, ILI9341, ST7735, PCD8544, Nokia 5110 displays running on Arduino/ESP32/Linux (Rasperry) platforms
MIT License
651 stars 125 forks source link

Render bitmap smoothly without using the sprites #121

Closed Darenn closed 3 years ago

Darenn commented 3 years ago

Hi Aleksei,

I read your arkanoid example (the old version I think?) and some of the other demo and I'm kind of stuck on something, having difficulties to draw without flickering/flashing image.

I'm creating a space invader clone, and there are 50 invaders to render in it (+player, 3 bullets and a UFO). At start I was using your SPRITE struct, calling erasingTrace() and draw() each time an invader moves. But it was very heavy in memory, and I could not display more than 15 invaders.

So I decided to not have one sprite per invader, I found a way to be able to calculate their positions at all times in a deterministic manner so that I don't need to store their position, and as they point all to the same image, I can just draw this bitmap on each invader calculated positions.

So basically I'm doing this :

static inline drawInvaders() {
  ssd1306_clearBlock(0, 0, 128, 50);
  for (uint_fast8_t i = 0; i < INVADERS_COUNT; ++i) {
    if (!invaders[i].isDead) {
      uint_fast8_t x = getPosX(i);
      uint_fast8_t y = getPosY(i);
       ssd1306_drawSpriteEx(x, y/8, sizeof(heartImage),  heartImage);
    }
  }
}

And I tried every drawing functions I could find in the library. But I always have this kind of flickering/flashing when drawing to the screen. I don't remember having it when using your SPRITE.

I think it's the same problem when I want to to draw something every frame. I would do something like this.

void loop() {
   clearScreen();
   updatePositions();
   drawNewThings();
   wait(32 - deltaTime ms) // to get fixed 30 fps
}

I think there's something I don't understand in the way it works, can you help please?

Another thing is that now my invaders are not rendered at the good position, I think it's because of this block system. I did not have the problem either when using your sprites.

My next step is to dive into your code or screen manual, but I admit I don't have a very good level and was using your library to avoid that :p.

Here is the repo if you want to take a look : https://github.com/Darenn/Game-Tiny

Best regards, thanks again for this great work.

krukhlis commented 3 years ago

And I tried every drawing functions I could find in the library. But I always have this kind of flickering/flashing when drawing to the screen. I don't remember having it when using your SPRITE.

I think it's the same problem when I want to to draw something every frame. I would do something like this.

The problem is in 1306 controllers. They don't have double buffers. So every frame you have to refresh the whole screen. And the function that causes visual glitches you've noticed is ClearScreen. Try not to clear screen and flickering will be gone( if you are using more or less usable controller). But obviously you will end up with a mess on the screen. What I recommend is instead of re-drawing whole screen every frame you should redraw only those areas where changes from previous frame should occure. So for each object that has moved or changed in specific frame you :

1) Clean the area currently occupied by the sprite 2) Draw the sprite at new position. Flickering will be almost gone( depending on your screen and MCU. This approach is applicable if there are only few sprites that change their position or shape every frame. If all sprites move and they cover >60% of screen -- this will not help.

lexus2k commented 3 years ago

@Darenn

Try to use NanoEngine, supported by the library. From one side it add some complexity to the source code, but it allows avoid large SRAM usage. It updates only small areas on the display, using very small screen buffer, and allows to avoid flickering. You can find some examples here: https://github.com/lexus2k/ssd1306/wiki/Using-NanoEngine-for-systems-with-low-resources (Reading keys with NanoEngine)

Darenn commented 3 years ago

Thanks for the explanation @krukhli, it's true that my invader are taking a lot of screen space and are all moving at the same time (good thing is that in my final design I want them to move row by row so that should solve the problem).

Thank @lexus2k for the link, this example might just be perfect (I thought I would not need the NanoEngine).

I will try later in the day and let you know with a video!

Darenn commented 3 years ago

I tried to compile the example "Readings keys", it's taking 83% of RAM in global variables, I really can't afford it unfortunately (unless I did smthg wrong while compiling?)

Also drawing my invaders only when they move is kind of making the same issue, it's not flashing but I can definitely see them disapear an reappear. Maybe because I'm trying to draw to much thing at the same time, or maybe because my screen is not great. It's just strange than I did not have the problem when using the SPRITE struct. Maybe I should come back to it... but its very heavy for a lot for game objects like that. Will keep looking into and let you know.

lexus2k commented 3 years ago

Hi @Darenn .

I need more details on your problem. I have just installed the newest version of Arduino IDE to my Windows 10 PC and compiled example below:

#include "ssd1306.h"
#include "nano_engine.h"

const uint8_t heartSprite[8] PROGMEM =
{
    0B00001110,
    0B00011111,
    0B00111111,
    0B01111110,
    0B01111110,
    0B00111101,
    0B00011001,
    0B00001110
};

NanoEngine8 engine;
NanoSprite<NanoEngine8, engine> sprite( {0, 0}, {8, 8}, heartSprite );

bool drawAll()
{
    engine.canvas.clear();
    engine.canvas.setMode(0);  // We want to draw non-transparent bitmap
    engine.canvas.setColor(RGB_COLOR8(255,0,0));  // draw with red color
    sprite.draw();
    return true;
}

void setup()
{
    /* Init SPI 96x64 RBG oled. 3 - RESET, 4 - CS (can be omitted, oled CS must be pulled down), 5 - D/C */
    ssd1331_96x64_spi_init(3, 4, 5);
    engine.begin();
    engine.drawCallback( drawAll );  // Set callback to draw parts, when NanoEngine8 asks
    engine.refresh();                // Makes engine to refresh whole display content at start-up
}

void loop()
{
    if (!engine.nextFrame()) return;
    // You will see horizontal flying heart
    sprite.moveBy( { 1, 0 } );
    engine.display();                // refresh display content
}

Arduino Nano with Atmega328p gives

Sketch uses 5692 bytes (17%) of program storage space. Maximum is 32256 bytes.
Global variables use 589 bytes (28%) of dynamic memory, leaving 1459 bytes for local variables. Maximum is 2048 bytes.

589 bytes (28%) is good value, since it includes standard Wire library memory consumption and some other Arduino things.

Darenn commented 3 years ago

Thanks for your time trying it out! Oh sorry, I'm using the AT Tiny 85 that only have 512 bytes of RAM.

lexus2k commented 3 years ago

Then, there is still some choice for you. Find UserSettings.h header file and disable all, you don't need there. Next check again all variables created in your project.

Darenn commented 3 years ago

Hi I commented out all the the #define and compile this example again. I gain 2% of ram on the AT Tiny. Maybe I'm wrong somewhere?

Sketch uses 3640 bytes (44%) of program storage space. Maximum is 8192 bytes. Global variables use 415 bytes (81%) of dynamic memory, leaving 97 bytes for local variables. Maximum is 512 bytes. Low memory available, stability problems may occur.

If It can be any help, I can compile and run perfectly your Arkanoid first version on my at tiny85, but not the second one that is taking too much memory.

Darenn commented 3 years ago

I just tried again using the SPRITE struct and its method (eraseTrace() and draw()) and it works fine.

I will send you both codes with a video of what its doing.

Darenn commented 3 years ago

So here's the two versions of my draw function, called only when the invaders move.

Version using a SPRITE struct inside my Invader Struct

void drawInvader(Invader* i, uint_fast8_t index, uint_fast8_t strafeCounter, uint_fast8_t diveCounter) {
  uint_fast8_t x = getPosX(index); // As I said before I have function that calculates the position, so I don't have to store them
  uint_fast8_t y = getPosY(index);
  i->sprite.x = x;
  i->sprite.y = y;
  i->sprite.eraseTrace();
  i->sprite.draw(); without sprite
}

And here what it gives in video, a stable display where you see the hearts moving. https://user-images.githubusercontent.com/11272715/104728827-c7b0c280-5737-11eb-938c-f39d9181974d.mp4

Here's the version to be able to not store a sprite per invader:

void drawInvader(Invader* i, uint_fast8_t index) {
  uint_fast8_t x = getPosX(index); 
  uint_fast8_t y = getPosY(index);
  ssd1306_drawSpriteEx(x, y/8, sizeof(heartImage),  heartImage);
}

void drawInvaders() {
  ssd1306_clearBlock(0, 0, 128, 50);
  for (uint_fast8_t i = 0; i < INVADERS_COUNT; ++i) {
    if (!invaders[i].isDead) {
      drawInvader(&invaders[i], i, strafeCounter, diveCounter);
    }
  }
}

As you can see I use the clearBlock() function to erase the area with the invaders, and drawSpriteEx() to draw the bitmap. And here's the result in video, we can see the hearts disapearthen reapear quickly. https://user-images.githubusercontent.com/11272715/104729117-3d1c9300-5738-11eb-8bc1-0e9feba3538c.mp4

I suspect that sprite.eraseTrace() is maybe doing some clever stuff behind the scene, like not sending tons of dat and just erasing what's necessary, so that we don't see anything problem. And that clearblock all the pixels might be a heavy operation that gives that effect, but I admit I don't really know how to do otherwise.

Thanks again for your help!

Darenn commented 3 years ago

I created a clearRect function inspired by fillRect to precisely clear the zone where the invaders move. With around 20 invaders the bad effect is not happening, with 20 more it's happening, so I guess it was just the amount of things cleared and drawn during one frame that was too much to handle for the screen, and that using the erase sprite worked well because it is more optimized. Thanks for your help, I will close the question but if you have any advice I'm all ears!