Isometric Sprites with Depth Buffer
amarillion

This is mostly just a demo of isometric depth buffering, because I managed to solve most problems myself. But there are two small question at the end.

Continuing work on Usagi, I was trying to implement depth buffering, so that the sprites could be drawn in any order. This would simplify the drawing code a lot!

I started by creating a small sample program. Here is the initial state without depth buffer test. As you can see, the sprites are drawn in the wrong order on purpose.

{"name":"612292","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/2\/d\/2d3b2f6bc68a4f6ccbb19aebfc42fbc6.png","w":640,"h":508,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/2\/d\/2d3b2f6bc68a4f6ccbb19aebfc42fbc6"}612292

Then I create a depth buffer, enable the depth buffering test, and translate to a given z coordinate before drawing each sprite:

    ALLEGRO_TRANSFORM transform;
    al_identity_transform(&transform);
    al_translate_transform_3d(&transform, 0, 0, z);
    al_use_transform (&transform);
    al_draw_bitmap (sprite, x, y, 0);

This works partially, but there is a rendering problem. The transparent parts of the sprites are drawn on the depth buffer, so the transparent part of a front sprite will still mask the sprite behind it:

{"name":"612291","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/0\/d\/0d4efffa85e7887b7b95274c716b84b0.png","w":640,"h":508,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/0\/d\/0d4efffa85e7887b7b95274c716b84b0"}612291

I was stuck here for a while, but then I read that if I use the discard statement in a pixel shader, nothing will be written to the depth buffer. So I could solve the problem with a custom shader:

#SelectExpand
1 uniform sampler2D al_tex 2 uniform bool al_use_tex 3 varying vec4 varying_color; 4 varying vec2 varying_texcoord; 5 void main() 6 { 7 if (al_use_tex) { 8 vec4 p = texture2D(al_tex, varying_texcoord); 9 if (p.a == 0.0) { 10 discard; 11 } 12 gl_FragColor = varying_color * p; 13 } 14 else 15 gl_FragColor = varying_color; 16 }

Here is the default allegro shader, for reference:

   uniform sampler2D al_tex
   uniform bool al_use_tex
   varying vec4 varying_color;
   varying vec2 varying_texcoord;
   void main()
   {
     if (al_use_tex) {
       gl_FragColor = varying_color * texture2D(al_tex, varying_texcoord);
     }
     else
       gl_FragColor = varying_color;
   }

Then I finally end up with the correct drawing (Full source code is attached):

{"name":"612294","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/9\/8\/988df5b6319a23d9e379b18f3cf6bd4a.png","w":640,"h":508,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/9\/8\/988df5b6319a23d9e379b18f3cf6bd4a"}612294

So my questions are:

1. is there any reason not to use the discard test, all the time? I could make this shader part of my engine code and make it the default everywhere. Perhaps it should even be added to the allegro code.

2. I found that in order to make it work, I had to use al_set_new_bitmap_depth before creating my buffer bitmap, which depends on an unstable API. What is the thought behind this being marked unstable, and how likely is this to change in the future?

#define ALLEGRO_UNSTABLE
...
al_set_new_bitmap_depth(16);

Mark Oates

1. is there any reason not to use the discard test, all the time? I could make this shader part of my engine code and make it the default everywhere. Perhaps it should even be added to the allegro code.

I'm not an expert, but in this case the discard would not account for translucent pixels, anti-aliasing on text or sprites, etc. If you were to include a p.a < 1.0 block the shader, it would need to account for blending modes and/or shaders that could change between the objects that are being rendered. All that can only ultimately be solved by rendering back-to-front.

I don't suggest making this as a default shader because this territory is where a graphics engine splits off into different solutions that very depending on the limitations/specs needed by the engine. For your Usagi engine, the shader is a great solution. It might even work well for Labyrinth of Lore, actually since at this time there were no "requirements" for translucent pixels in sprites (but there are requirements for different shaders on sprites đŸ¤”).

Anyway, I did a little bit of research for this topic during the KrampusHack (due to a need for rendering billboarded sprites that also have transparent pixels). The general consensus seems to be that these faces should be rendered back-to-front of the camera, in a separate pass, which may seem a bit odd because it seems like that's what the depth buffer solves for in the first place. The technique ensures that "behind" pixels are rendered before they are potentially depth-checked against pixels that might be "in-front" that would otherwise block them from being rendered.

A similar technique is used in Labyrinth of Lore's "TileMapWaterMesh", which (will) render a transparent layer over the existing "TileMapMesh". The overlayed water mesh layer needs to be rendered separately, after the TileMapMesh, so as not to prevent the pixels from the TileMapMesh from being blocked out by the transparent layer.

In the case of ordering objects back-to-front, the technique should not a difficult one. Given a master list of in-world-entities:

std::vector<Entity::Base*> all_entities;

A collection is extracted, sorted and rendered. It might look like this:

// Billboarded sprites are picked out
std::vector<Entity::Base*> billboarded_sprites = Rendering::BillboardSelector(all_entities).select();

// Sprites are sorted using a dot-product distance function
std::vector<Entity::Base*> sorted_sprites = Rendering::DistanceToCameraSorter(camera, billboarded_sprites).sort();

// Render
for (auto &entity : sorted_sprites)
{
   Rendering::EntityRenderer(entity).render();
}

That's the sketch of what was planned for Labyrinth of Lore, but that feature was actually spec'd for after the hack, since the existing artifacts were negligible and no entity shaders were slotted for the hack version.

l j

As far as I'm aware 3d engines draw all opaque objects using the depth buffer but will disable the depth buffer and use back to front rendering for transparent objects in a second pass.

Transparent objects that can cross each other require more work to draw properly though.

Both discard and branches can slow down the GPU quite a bit and discard will prevent some optimizations. On the other hand sorting a large number of sprites could impact the CPU.

I think sorting might actually be the fastest for most 2d games.

DarĂ­o B.

A common basic solution that most 3D engines do* is separating the materials into three categories: opaque, translucent and punch-through.

Opaque would be the first result you got (depth write on any pixel).
Punch-through would be your second result (write only pixels that are above a certain alpha threshold).
Translucent would be no depth writing whatsoever. This normally required careful placement from the artists and sorting from back to front.

Unless you have a really big number of entities, I'd recommend the sorting approach for a first implementation without a depth buffer altogether. You could speed it up with some better data structures considering the fixed perspective, but better tackle that later if it becomes a problem.

*: Without getting into order-independent transparency implementations, which are another different kind of headache you probably shouldn't get into yet.

amarillion

Well, the Usagi engine currently uses the sorting approach, without depth buffer. And it works. But I want to experiment with rendering certain occluded sprites, so that you can sometimes see through.

Also, there are some complications with sorting. There are blocks that are so big that they can be at the same time in front and behind something. I solved that by splitting the blocks in two parts, and drawing them twice. But that means keeping two references to a sprite in the sorted sprite list, which is a bit of a pain.

Anyway, I see now that there are enough reasons that the discard shader cannot be the default, so I'll forget about that.

Does anybody know about my 2nd question, about the use of ALLEGRO_UNSTABLE al_set_new_bitmap_depth(16); ?

DarĂ­o B.

There are blocks that are so big that they can be at the same time in front and behind something. I solved that by splitting the blocks in two parts, and drawing them twice. But that means keeping two references to a sprite in the sorted sprite list, which is a bit of a pain.

That sounds about right. If you really wanted it to only be one sprite, you'd need its depth information in another channel/image at least combined with using the depth buffer. This is one approach I've used in the past (inspired by this guy's video), and I believe it is currently being used in Pillars of Eternity so the backgrounds (which tend to be a few large pre-rendered/painted over images) have occlusion with the 3D characters.

EDIT: How Pillars of Eternity's rendering works. You can see an example of the depth buffer information embedded into the background around the 1 minute mark.

Mark Oates

Does anybody know about my 2nd question, about the use of ALLEGRO_UNSTABLE al_set_new_bitmap_depth(16); ?

I don't. Last I asked Elias about it, I think it just needed to kinda "sit around" for a while to see what side effects might get reported. Either that, or additional eyes/testing of this feature the non-major platforms. But I really love this feature. It's worked flawlessly for me on Windows/Mac as much as I've used it, and I think the naming of the functions is good too.

That sounds about right. If you really wanted it to only be one sprite, you'd need its depth information in another channel/image at least combined with using the depth buffer. This is one approach I've used in the past (inspired by this guy's video [www.youtube.com])

Oh yea! I did something similar when messing around with shadow mapping (video). The "custom depth buffer" bitmap is shown on the top right and represents depth information for the light source's view perspective onto the scene. It's combined with the camera perspective at render time to determine if a shadow should be rendered or not.

RmBeer2

I remember seeing in the Allegro source code that it really already supports 3 coordinates, so it is easily possible to add a feature that allows the use of the Z coordinate without using a transformation.

amarillion

Thanks for sharing those videos. Is there an easy way to examine/display the contents of the depth buffer for debugging purposes, like you're doing in that video, Mark? I guess I'll have to look into the opengl-specific functions for that?

Mark Oates

Is there an easy way to examine/display the contents of the depth buffer for debugging purposes, like you're doing in that video, Mark? I guess I'll have to look into the opengl-specific functions for that?

In the shader, you are able to access the depth buffer pixel value from the .a member (in the sense of "rgba") of the gl_FragCoord. You might use this to draw an image you could use to help you debug.

The "DepthDarken" shader in Labyrinth of Lore darkens the scene if it's further away from the camera. Essentially what's happening is the depth buffer is being drawn on top of scene (with some scaling and range capping), and it's intended to emulate light from a torch carried by the player. It looks like the image posted on the KrampusHack log:

{"name":"612300","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/b\/a\/bad4a64898e533fa00142dd877746d1f.jpg","w":960,"h":551,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/b\/a\/bad4a64898e533fa00142dd877746d1f"}612300

This file is the quintessence for that shader, highlighted is the fragment shader source: DepthDarken shader

Specifically, if you look at line 81, it shows pulling out depth_value from gl_FragCoord and then multiplying the fragment color for that pixel by that depth value, here.

The values contained in .a, though, I'm not sure what the ranges are. I think it could be 0..1, but in the example above I noticed some colors were blowing at "near" ranges, so it's possible the ranges could be something other than 0..1.

Thread #618021. Printed from Allegro.cc