2D Water Reflection

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 :slight_smile: (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 after gfx.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):
image

This is how it looks in game:
water-reflection

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;
}
24 Likes

Really cool effect! The alternate scanlines is a great idea.

1 Like

This is awesome! I love learning about all the things that make games interesting to people.

1 Like

I made a few tweaks to allow placing smaller pools throughout the screen. It only works on x-coordinates divisible by 8.

reflectionpools

#include <stdio.h>
#include <stdlib.h>

#include "pd_api.h"

PlaydateAPI *pd;

const short rowStride = 52;

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, int x, int width)
{
	for (int i = x; i < x + width; ++i)
	{
		row[i] <<= distance;
		if (i < x + width - 1)
		{
			row[i] |= (row[i + 1] >> (8 - distance));
		}
	}
}

void shiftRowRight(uint8_t *row, short distance, int x, int width)
{
	for (int i = x + width - 1; i >= x; --i)
	{
		row[i] >>= distance;
		if (i > x)
		{
			row[i] |= (row[i - 1] << (8 - distance));
		}
	}
}

void setRowBlack(uint8_t *row, int x, int width)
{
	for (int i = x; i < x + width; ++i)
	{
		row[i] = 0x00;
	}
}

static int renderwaterreflection(lua_State *L)
{
	int x = pd->lua->getArgInt(1) / 8;
	int y = pd->lua->getArgInt(2);
	int width = pd->lua->getArgInt(4) / 8;
	int height = pd->lua->getArgInt(5);
	int sy = y - pd->lua->getArgInt(3) - height;

	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 < height; i++)
	{
		uint8_t *sourceRowStart = frameBuffer + ((sy + i) * rowStride);
		uint8_t *destRowStart = frameBuffer + ((y - 1 + height - i) * rowStride);

		if (i % 2 == 0)
		{
			setRowBlack(destRowStart, x, width);
		}
		else
		{
			memcpy(destRowStart + x, sourceRowStart + x, width);

			int shift = arrayOfShifts[(i + (cycleCount / stepLength)) % 22];
			if (shift > 0)
			{
				shiftRowLeft(destRowStart, shift, x, width);
			}
			else if (shift < 0)
			{
				shiftRowRight(destRowStart, -shift, x, width);
			}
		}
	}

	pd->graphics->markUpdatedRows(y, y + height);

	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;
}
2 Likes

So cool! I figured the downside of horizontal scan lines will be that it doesn't look as good for movement on the x-axis, but it actually looks surprisingly good. Very cool!

This looks awesome! The scanlines/dithering seems great for performance without sacrificing style.
For reference, here's a devlog about the water effects in Kingdom.

1 Like