I was playing around a lot with a 2D water reflection à la Kingdom, one of my fave games. After a first rather dodgy attempt where I copied the current frame buffer, blurred and flipped it, and then rendered it onto the water with an alpha mask that led to amazing 10 fps (video on X, if somebody's curious), I now revamped it in C.
Curious about some suggestions on how to make it look better, but also leaving the code for inspiration if somebody cares!
So what are we doing here:
- Created function in C that is called at the very end of
playdate.update()
, especially aftergfx.sprite.update()
so that all sprites are already drawn. - This function takes 50 lines directly from the frame buffer, and copies every 2nd line in reverse order at the bottom of the screen where I want the reflection.
- I took every second line to darken the image without introduction dither flashing.
- The copy is a quick
memcpy()
.
- I then add some water swiggle by offsetting a full row of pixel by up to 4 pixels to the left or right, and rotating through a zig zag pattern of offsets.
- In the end I add some waves on top, but that's in Lua.
The operation is not for free, but on device around 1-2ms (that includes both the reflection in C plus adding imags on top in Lua):
This is how it looks in game:
The code:
#include <stdio.h>
#include <stdlib.h>
#include "pd_api.h"
PlaydateAPI *pd;
const short rowStride = 52;
const int rowBytes = 400 / 8;
const short arrayOfShifts[22] = {
0, 1, 2, 3, 2, 1,
0, -1, -2, -3, -2, -1,
0, 1, 2, 3, 4, 4, 3, 2, 1,
0};
short cycleCount = 0;
const short cycleLength = 22;
const short stepLength = 8;
void shiftRowLeft(uint8_t *row, short distance)
{
for (int i = 0; i < rowBytes; ++i)
{
row[i] <<= distance;
if (i < rowBytes - 1)
{
row[i] |= (row[i + 1] >> (8 - distance));
}
}
}
void shiftRowRight(uint8_t *row, short distance)
{
for (int i = rowBytes - 1; i >= 0; --i)
{
row[i] >>= distance;
if (i > 0)
{
row[i] |= (row[i - 1] << (8 - distance));
}
}
}
void setRowBlack(uint8_t *row)
{
for (int i = 0; i < rowBytes; ++i)
{
row[i] = 0x00;
}
}
static int renderwaterreflection(lua_State *L)
{
uint8_t *frameBuffer = pd->graphics->getFrame();
if (frameBuffer == NULL)
{
pd->system->error("Frame buffer is not available");
return 0;
}
cycleCount++;
if (cycleCount >= cycleLength * stepLength)
{
cycleCount = 0;
}
for (short i = 0; i < 50; i++)
{
uint8_t *sourceRowStart = frameBuffer + ((120 + i) * rowStride);
uint8_t *destRowStart = frameBuffer + ((239 - i) * rowStride);
if (i % 2 == 0)
{
setRowBlack(destRowStart);
}
else
{
memcpy(destRowStart, sourceRowStart, rowBytes);
int shift = arrayOfShifts[(i + (cycleCount / stepLength)) % 22];
if (shift > 0)
{
shiftRowLeft(destRowStart, shift);
}
else if (shift < 0)
{
shiftRowRight(destRowStart, -shift);
}
}
}
pd->graphics->markUpdatedRows(190, 240);
pd->lua->pushNil();
return 1;
}
#ifdef _WINDLL
__declspec(dllexport)
#endif
int eventHandler(PlaydateAPI *playdate, PDSystemEvent event, uint32_t arg)
{
pd = playdate;
if (event == kEventInitLua)
pd->lua->addFunction(renderwaterreflection, "c_renderwaterreflection", NULL);
return 0;
}