HubbleCommand.github.io

Personal blog, project documentation, and ramblings

Godot - Directional Sprites


tags: gamedev - godot

I really liked the game BoltGun; a pixel retro look with modern lighting, effects, and framerate.

I stumbled upon this cool video, and was intruiged as to how it was implemented.

Implementation

The starting point is the existing Sprite types to ensure an easy interoperability with GOdot’s existing systems. SpriteBase3D is the base class of the two 3D sprite types: Sprite3D and AnimatedSprite3D . The 2 2D Sprite types, Sprite2D and AnimatedSprite2D , however, both inherit directly from Node2D .

Using Cameras

The most basic implementation would do something like checking the rotation of the Sprite to a target camera, then changing the sprite based on that. However, this doesn’t work great: if there are multiple cameras (i.e. mirrors, portals, camera systems), then only one could ever be correct. This especially won’t work very well in a multiplayer environment: you can determine the local player’s camera, but would create some very tight coupling between every entity and the player. I hate coupling things that don’t need to, so I wanted to avoid this solution at all costs.

Here are some resources I found that do just that:

A more interesting and novel approach using AnimationTree from Jon Topielski.

Using Shaders

As I really wanted to avoid doing anything like that, I didn’t. Ideally, I wanted a solution that would work regardless of camera: some way to make it so that every camera would do the same thing when looking at this node. I started looking into the C++ implementations of the 3D sprite classes, but realized that it would take a lot of time to get something working, and was about to give up due to time constraints.

Then, I stumbled upon this Godot proposal. Calinou also did some work on it here. Someone suggested a shader, and I realized what a fool I was for not realizing that myself: it’s exactly the way to get each camera to get the correct representation!

I also found some other neat shaders, although none really did what I wanted, or just didn’t work

I don’t feel to bad for using as a starting point. There was a LOT more to add, and many issues to fix, but after a while I got it working with Godot’s existing Sprite3D node parameters.

Matrices Overview

Before going furthur, if you don’t know much about matrices, I highly recommend reading the following articles. Matrices are the basis of shaders, after all.

Funny issue with rads

Radians are usually the go-to when dealing with angles.

However, in my case, I had to use degrees. Not out of preference, but out of necessity. When using radians, there would be flickers between frames close to the angular “borders”.

My guess is that due to the successive divisions with floats, that precision loss was the issue with the small fractional values that are intrinsic with radians. Degrees, on the other hand, have a much greater range at two degrees of magnitude larger.

Going back to 2D

Ironically, getting directional sprites in 2D is much more painful.

The shader itself wasn’t too bad. However, I realized I had a problem once I looked into 2D lighting.

2D lighting doesn’t work the same at all as in 3D. In 3D space, you can change the ALPHA in fragment(), which changes how shadows are cast / light is blocked.

2D, however, requires LightOccluder2D nodes to cast shadows.

I first look into using a mix of fragment()’s SHADOW_VERTEX or light()’s SHADOW_MODULATE, although I believe the former only works on the shadow of the pixel, not on shadows cast. Looking at this discussion, it would appear that I am correct on the behavior of SHADOW_VERTEX. SHADOW_MODULATE’s description reads as follows:

Multiply shadows cast at this point by this color. However, this works like other modulation: it only changes the color. There doesn’t seem to be a way to change the shadows cast through the shader in this manner.

The logical next step would be to apply a shader to the LightOccluder2D node. However, this wont’t work either. LightOccluder2D doesn’t direcectly cast a shadow, it’s OccluderPolygon2D does. In practice, a shader applied to a LightOccluder will only change the editor / debug representation of the LightOccluder2D, but not the OccluderPolygon which casts the shadow.

Looking into the code, the issue seems to be that the of the is simply registered in the renderer. Looking at the source code,

void OccluderPolygon2D::set_polygon(const Vector<Vector2> &p_polygon) {
	polygon = p_polygon;
	rect_cache_dirty = true;
	RS::get_singleton()->canvas_occluder_polygon_set_shape(occ_polygon, p_polygon, closed);
	emit_changed();
}

Looking furthur down, renderer_canvas_cull.cpp’s canvas_occluder_polygon_set_shape and renderer_canvas_cull.cpp’s occluder_polygon_set_shape confirm my hypothesis.

Even when looking at 2D SDF, there was no apparent hope, although the following on SDF are neat:

I tried using 2D Meshes, as you can apply a shader to it that will affect it’s geometry. A MeshInstance2D with a QuadMesh will have it’s mesh’s height reduced to an eight with the following shader:

shader_type canvas_item;

void vertex() {
	VERTEX.y /= 8.0;
}

However, MeshInstance2Ds don’t cast shadows, the only things that do cast shadows are LightOccluder2D.

This means that the shader approach won’t really work, unfortunately.

The last ditch effort would be to rotate the LightOccluder2D in a script, however the question becomes where to rotate it to. Always keep it vertical? Well, the Sprite won’t always be vertical in world space, but in canvas space. And the only way to do that in a script is to keep a reference to the currently active camera, which has the issues described above.

	get_viewport().get_camera_2d()

The only other options would be to make a new 2D lighting system… or make major changes to the existing one.

Custom lighting system in Godot

It would appear that this is just not worth it, or to just do the script approach. The other thing is that the systems in 2D are usually very different than in 3D. Cameras are, in general, fixed to a certain direction, meaning that the major problem stated above with the Script approach doesn’t apply here.

“Workaround” for 2D shadows

Calling this a workaround seems disingenuous, as it really isn’t that much of a fix.

Just use an appropriately-shaped LightOccluder2D that provides an appropriate shadow cast for all angles of the sprite.

Now, this only works for sprites with little topographical varience. This will work for say, a human, but less likely for a long bus.

This also only works if your 2D world does not change orientation, i.e. the camera rotation always stays the same. This is very common for most 2D games, but it is a strict limitation.