Underwater effect, decals, and LZ4 compression

A little zoo of topics here, and what they have in common is me avoiding gameplay programming (which comes next), and that they’re again nice-to-haves rather than essentials. But they were all fun to do, so that’s that. Here’s a video that shows the underwater effect and the decals:

Underwater effect

Recently I refactored sprite rendering to use variants with multiple configuration variables, one of them being the type of the pass being regular, shadow or occluded. The occluded pass is like the regular pass, but we draw in areas that fail the Z test, and make this pass semi-transparent. So far so good, and what does this have to do with the underwater effect?

Starting point: creatures float over water. Not good!

Well, previously my sprites have been holier-than-thou, as they all seem to be walking on top of any body of water, like the sea or some lakes/rivers in the level maps. Clearly that does not look very nice, and does not give the impression that we’re inside that liquid. So, after a few experiments, I thought I’d reuse (the programmer’s wet dream) the occluded pass to do that. How? A bit of background info first: In my rendering pipeline, sprites do use depth/Z values, so I can have order in rendering, characters appearing behind trees, etc.

First we need to detect when we’re on tiles with liquid. On those tiles, push the Z value further back: more for deep liquid, less for shallow liquid. Push the sprites so that part of the sprites is underground. Important: the position on screen is identical, I’m just pushing the Z value so that part of the sprite will fail the Z test

Change Z values when in liquid tiles, so half the sprite will fail the depth test

Now sprites are successfully hiding in the liquid, and so far it’s already looking good! Some short creatures might be completely underwater though, so you won’t be able to see them. So we can enhance the visualization, and here is where we consider the occlusion pass.

Since there’s going to be a part of the sprite that fails the Z test, this will pass the occluded pass test (which renders what fails Z test), so it really “just works” as long as no settings are changed (I had to hack things to show how it would look without the occluded pass running). The sprite portion that’s underwater is naturally rendered with transparency.

The “occlusion rendering” pass automatically renders the missing parts with transparency

And now for the fun of it, because we know that things get distorted underwater and that water is never perfectly still, especially as adventurers and monsters plow through it, we can add a little bit of distortion in the occluded area. So in the pixel shader, if the occluded area represents area-in-liquid (we already have used this info in the vertex shader to push the Z back), then we just apply some periodic distortion in the sampling of the horizontal texture coordinate. And here’s the result!

When rendering occluded parts while in a liquid, mess up the u texture coordinates for a shimmer/distortion effect

Finally, just to spice things up slightly more, items dropped underwater are slightly visible, and the level of visibility depends on the depth. So, in shallow liquid are generally faint, but in deeper water they are even more faint. Maybe I should make them completely invisible in deep water, but let’s see, maybe later.

A key on the ground (top-left), in shallow water (top middle) and deep water (bottom middle)

Decals

One common effect in games is decals: images that are used as “stickers” in the game world. Examples could be footprints, blood spatter, scorch marks, etc. While these have certain challenges in order to implement in 3D, it’s far easier to do in 2D, as it’s just another sprite splatted in the world. And typically, decals can fade out after a while. So, since I’ve got the relevant rendering machinery already, I wanted to add support for decals, for any purpose that I see fit later on.

What should have been a walk in the part, turned out to be a pain thanks to Unity’s bad rendering debugging facilities. So, given the way I’ve structured the code, adding decals should have been a walk in the park. It’s another sprite pass, and I implement it as a persistent particle system, where decals have a lifetime of 5-10 seconds, after which they fade out. So, I added a few blood spatter sprites, wrote a basic shader and hooked the systems up, and lo and behold, nothing to be seen. Long story short and a few hours later, the problem was that the Compute buffer (that I’m using to send instancing data) was set up on C# side to be 3 uints per element, and in the shader I had a StructuredBuffer<uint> that I was addressing by buffer[i*3+0], buffer[i*3+1] and buffer[i*3+2]. I was expecting that the memory would be aliased but that was not the case. And no errors of course did not help (“Hey, you’re binding a uint3 buffer to an incompatible uint shader buffer!”). Anyway, long story short, that was it, and now we have decals. I hooked them up with damage, so that when a creature gets damaged it spawns a small blood spatter, and when it gets killed it spawns a big one. Yay for proof of concept, more to come when needed.

Killing the ghost results in blood pool. Ok, maybe this needs changing. But hey, decals work!

LZ4 compression for save files

Save files can get large, due to the large number of 2D arrays for various purposes: lots of layers per map, world map data, some other heavyweight caches, entities, etc. The finished game should have hundreds of cities and dungeons. I’m not going to go too deep into this rabbit hole for now, as it’s eventually loading needs to become asynchronous, but to begin with, I wanted to reduce the file size.

The starting test case is 7 multi-level dungeons , so about 15-25 levels altogether, plus world map. Size on disk is 21MB, it takes about 3.2 seconds to save and 3.5 seconds to load (in Play in Editor, not final build). So I got a simple LZ4 implementation for C++ and put it in the C++ plugin. The plugin now has 3 more functions:

  • Save an array of bytes to a file using LZ4
  • Get the number of uncompressed bytes needed by an LZ4 file on disk
  • Decompress an LZ4 file to a preallocated array of bytes

The LZ4 bytes store as a first value an integer with how many bytes will we need. The reason I did that was because the plugin functions work with preallocated memory. So C# can query the correct size, allocate the byte array and send the array to be populated in C#. Still it’s far from optimal due to some possibly unnecessary copies, but hey, it works.

The timings for the LZ4 version are pretty much the same, but the file size is now 5MB instead of 21MB. Yay!

More autotiles: transitions between floor types

Smooth transitions on the ground layer, as an extra rendering pass

This is just a quick demo of a new visual feature: overlapping tiles for ground layer transitions. The problem before was that the transitions between the floor tiles were sharp and square. A square tile would either have e.g. a grass or dungeon floor tile. This was problematic when we have a non-full-square blocker tile occupying a border tile, such as these grass wall tiles, and the problem effect is shown here. What happens is that it looks like the grass border is gray, when it actually isn’t. To counter the problem, a new pass is needed, that can ensure smooth transitions.

For this new pass, we need autotiles of the type “rug” (so, just 16 of them)

Autotile tool: left side: “canvas” + connections, right side: candidate tiles

I’ve developed a small Python tool that allows me to easily do that from an input set of files, so I’m just loading up some tiles from an Oryx tileset and place them appropriately:

Autotile tool: 16 clicks later (click-select tile on right, click-place tile on left)

The layout can be then exported and embedded in a texture atlas, to be used by the game. So, in game, before we apply the layer, the background tiles look like this:

In-game base map layer with grass tiles and dungeon tiles

The new autotile layer is rendered after the background and before any other passes. My map tiles have zone IDs, so grass would be the outer zone (id == 1) and dungeon floor would be an inner zone (id == 2). If we are in an inner zone and we’re on the boundary with an outer zone that has overlap tileset, we set the appropriate bits in a bitmask, that we’re going to use to read the appropriate autotile. This results in the bottom image:

Adding the grass overgrowth autotiles

Now we can add the rest of the passes, resulting in a nicely smooth transition:

https://i.imgur.com/syA2EVp.png
Smooth transitions between blocking and non-blocking layers

And for reference, here’s the same view without the new overlap pass:

As above, but without using the new smooth overlap layer. Notice the gray square-y border in the grass, because of the underlying gray tiles

Autotiles, instancing and object clusters

new approach: using instancing
New approach: using instancing
attempted approach: using autotiling
Attempted approach: using autotiling
original approach: a sprite per cell
Original approach: using nothing

The problem

Some of the blocking tiles in the game are things like a tree, a cactus, etc. Occasionally, I want to use these tiles to represent blocked cells in an outdoors map. But, if I just put one such sprite per cell, the result looks poor (see bottom image above, “original approach”). So I thought, “ok, let’s try to create an autotile version of the trees”.

In the meantime, I’ve developed some helper tool to assist with creating autotiles (rug/fence/blob) from a selection of input tiles:

Autotile tool: blob

… So I hacked a bit of that code away, to automatically place sprites that respect the edge restrictions, so effectively automatically creating the autotile blob from any single sprite. Example output:

Autotile tool: blob, automatic placement based on edges

While I was super happy initially, I soon realized that it would only work under very specific circumstances (symmetric sprites, placed appropriately at particular spots), and in order to cover all scenarios , I would need to automatically create a lot more sprites. So, after seeing a lot of restrictions, I wanted to go for plan B, and reuse some code that I already have for the overworld. That code uses Poisson disk sampling to create instances of things to populate the overworld.

Sprite shader refactoring

The problem was that that shader was restricted for the overworld vegetation, so I needed to generalise. I took a hard look of the miscellaneous shaders that I’m using for sprites (anything that uses texture atlases) and I noticed ones for the following:

  • GUI
  • Static objects
  • Moving objects
  • Moving object shadows
  • Moving objects occluded areas
  • Vegetation normal
  • Vegetation shadows
  • Vegetation decal normal
  • [Future] static object decals
  • [Future] moving object decals

So, lots of combinations. So I delved in Unity’s multi_compile pragma and custom, manual shader variants, and I came up with the following scheme, to have 3 different shader variant axes for sprites:

  • Orientation: Standing or decal
  • Sprite type: Static, moving or “splat”
  • Render type: Regular, shadow or occluded

GUI is still its own thing, but all the rest can be expressed with one value per “axis” above. While Unity nicely allows keywords to configure the multi_compile option, such configuration cannot change blend settings, z settings and core things like that. So, variants based on Render type (regular, shadow, occluded) are all different shader files, that define some defines and include the common shader code. The rest of the variants are just expressed with #ifdef. Here’s how the “Regular” render type variant shader looks like:

Shader "Sprite/TextureAtlasSpriteRegular"
{
	Properties
	{
		g_TextureAtlasSprites("TextureAtlasSprites", 2DArray) = "white" {}
		g_TextureAtlasConstants("TextureAtlasConstants", Vector) = (32,32,1,0)
		g_RealTime("Real time", int) = 0
		g_RenderingMoveSpeed("Rendering move speed", float) = 1
	}
		SubShader
		{
			Tags { "Queue" = "AlphaTest" "RenderType" = "Opaque" }
			LOD 100

			AlphaToMask On

			Pass
			{
				CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag
				#pragma target 4.5

				#define VARIANT_REGULAR
				#pragma multi_compile_local VARIANT_ORIENTATION_DECAL VARIANT_ORIENTATION_STANDING
				#pragma multi_compile_local VARIANT_SPRITETYPE_STATIC VARIANT_SPRITETYPE_MOVING VARIANT_SPRITETYPE_SPLAT

				#pragma multi_compile_instancing
				//#pragma instancing_options procedural:setup

				#include "UnityCG.cginc"

				#include "Assets/Shaders/common.cginc"
				#include "Assets/Shaders/sprite.cginc"
				#include "Assets/Shaders/noise/random.cginc"

				// We don't need this, as we don't have gameobjects and materials for each
				UNITY_INSTANCING_BUFFER_START(Props)
				UNITY_INSTANCING_BUFFER_END(Props)

				
				#include "Assets/Shaders/Sprite/TextureAtlasSprite_common.cginc"

				ENDCG
			}
		}
}

So, now all the sprite code for all the variants is in a single source file, which is super convenient for editing. This approach now allows easy proper shadows for any object (static or moving) among other things.

Benefits of the new system: everything has proper shadows! fountain, chest, character, door.

As this was a hell of a tangent, to solve the original problem, I wrote a pseudo-autotile algorithm class called “Splat” where, if I’ve specified it, instead of autotiling it creates an instance buffer and renders that with the Splat render variant (which includes shadows). This results in the first image shown on the page, where we have nice randomized trees including shadows. And, even though I’m not showing it here, we can use a variety of tree types, which is very, very convenient (with autotiling that would be near impossible).