Building An Isometric Game Using Horde3D (Part 3)

Published August 03, 2011
Advertisement
Okay, so after I published the previous post (link) I did some digging around, asked on the Horde3D forums, etc... whereupon I was directed to a method, updateQueues, which was responsible for the actual sorting of geometry based on the order attribute in the pipeline DrawGeometry element. You know, for the asshole pixels. Turns out, the sort-key is calculated as the fabs() of the distance from the eyepoint, meaning that if an object is behind the eyepoint it will be fabs'ed and sorted in with the objects in front of the eye-point. This has repercussions for the way I was doing the camera, ie setting the camera location itself to be the look-at point, and using a negative Near plane.

So, to resolve the issue, I needed to make a change to the camera:

[source]
-- Isometric camera
IsometricCamera=class(function(self, name, pipeline, nodescreensize)
self.cam=h3dAddCameraNode(H3DRootNode, name, pipeline)

h3dSetNodeParamI( self.cam, H3DCamera.ViewportXI, 0 )
h3dSetNodeParamI( self.cam, H3DCamera.ViewportYI, 0 )
h3dSetNodeParamI( self.cam, H3DCamera.ViewportWidthI, config.screenwidth )
h3dSetNodeParamI( self.cam, H3DCamera.ViewportHeightI, config.screenheight )

h3dSetNodeParamI( self.cam, H3DCamera.OrthoI, 1)

local screennodes_x = config.screenwidth / nodescreensize
local screennodes_y = config.screenheight / nodescreensize

h3dSetNodeParamF( self.cam, H3DCamera.LeftPlaneF, 0, -screennodes_x/2)
h3dSetNodeParamF( self.cam, H3DCamera.RightPlaneF, 0, screennodes_x/2)
h3dSetNodeParamF( self.cam, H3DCamera.BottomPlaneF, 0, -screennodes_y/2)
h3dSetNodeParamF( self.cam, H3DCamera.TopPlaneF, 0, screennodes_y/2)
h3dSetNodeParamF( self.cam, H3DCamera.NearPlaneF, 0, 0.0)
h3dSetNodeParamF( self.cam, H3DCamera.FarPlaneF, 0, 30.0)


end)

function IsometricCamera:SetPosition(x,z)
self.x,self.z=x,z
local y=5
local yoff=y*1.225
h3dSetNodeTransform( self.cam, x+yoff, y, z+yoff, -30.0 ,45, 0, 1, 1, 1 )
end
[/source]

I've changed the Near plane to 0, and added a y offset (currently magic-numbered to 5). Now, doing it this way means that the center-point on the screen will no longer be the same as the camera node's position, so I need to offset the incoming (x,z) coordinate pair sent to SetPosition to account for the y offset. In order to calculate the magic number of the offset, I had to go back to trigonometry. Assuming a 30-60-90 triangle (due to the 30-degree azimuth), a translation of 1 unit along the up axis corresponds to a translation of 2*cos(30) units along the horizontal axis, or ~1.7321. Now, since there is also a 45 degree spin around the up axis, this length represents the distance to move along the view-direction vector, (1,0,1). So solving the Pythagorean theorem, 2*a^2=c^2 (with the legs of the triangle being of equal length) we arrive at the constant ~1.225. So every 1 unit the camera translates in the up direction, we need to translate the camera X and Z positions by 1.225 (approx) in order to keep the view centered.

So now the camera sits above the geometry instead of right in the middle of it, and we can verify that it works correctly:

screen1693.jpg

No more sorting artifacts. At least so far, anyway. But our work is not yet done, since we are only testing with a solid ground and some walls. Goblinson Crusoe allows the use of decorative decals on the ground and walls, and with these I can foresee some problems. But before we worry about wall decals, let's get ground decals in there. GroundDecals are just another step in the pipeline, executed just after the Ground step and using the TRANSLUCENT context. Other than that, the materials are the same. Entities of type GroundDecal need to have their material class set to GroundDecal. This system of using contexts and classes was, at first, a little awkward-seeming for me, but it is proving to have merit, for it easily allows you to customize the draw order in just the fashion that we are attempting.

Let's throw in some ground decals and see what we get.

screen7852.jpg

Now, that looks about right. However, when you are working with a general 3D engine, there are some assumptions that you can not really make, that you can make if working with a custom-built isometric engine, especially when dealing with partially-transparent sprites--a fact that was proven in the previous post. In particular, you can not make any assumptions about order of sorting for partially-transparent sprites that are instanced at the exact same location. For example, what if we were to instance two different decals at the same location, in order to provide a complex layering effect on the ground? In a roll-your-own, you can structure it such that the order in which they are inserted is the order in which they would be sorted for drawing. However, given that we don't know the underlying architecture of the spatial graph in Horde3D, we can't make that assumption. Let's do a test. We will load up 2 different types of decals, a red one and a green one. Each time a decal is selected to be placed, we'll insert a red one first, and then a green one on top. Then we'll turn it loose and see what we get. In the interest of this experiment, I implemented some logic code to move the camera around in a circle around the origin, since there can often be issues of what is called "Z-fighting" that might not crop up if the camera is in one location, but may crop up in another location.

screen3880.jpg

Yep, just as I suspected, you can't count on insertion order when the geometry is sorted. Some of the green decals are drawn before the red, and some after; worse, it changes as the camera moves, creating a hideous flickering effect.

There really is no good way around this. If it isn't a sorting problem, it will be a z-fighting problem, in which pixels are drawn from two co-planar polygons in a weird hashed-up sort of pattern, as mathematical imprecision prefers one polygon over the other at different stages of the rasterization. Fact is, general 3D engines simply do not like co-planar polygons. Floating-point math is just too imprecise. There are hacks you can do to mitigate it somewhat. One hack would be to add in additional DrawGeometry stages in the pipeline, for different categories of decals, then code to ensure that one decal of a given stage does not coincide with another decal of the same stage. This might be useful, for example, in implementing terrain transitions as a layer, atop which can be placed terrain decorations. The drawback to this, of course, is the increasing number of sorted-draw routines, and the increase in general overhead required to implement another layer or pass. This can have an effect on the speed of rendering.
There really is no good way around this. If it isn't a sorting problem, it will be a z-fighting problem, in which pixels are drawn from two co-planar polygons in a weird hashed-up sort of pattern, as mathematical imprecision prefers one polygon over the other at different stages of the rasterization. Fact is, general 3D engines simply do not like co-planar polygons. Floating-point math is just too imprecise. There are hacks you can do to mitigate it somewhat. One hack would be to add in additional DrawGeometry stages in the pipeline, for different categories of decals, then code to ensure that one decal of a given stage does not coincide with another decal of the same stage. This might be useful, for example, in implementing terrain transitions as a layer, atop which can be placed terrain decorations. The drawback to this, of course, is the increasing number of sorted-draw routines, and the increase in general overhead required to implement another layer or pass. This can have an effect on the speed of rendering.

I suspect that the same is going to occur with wall decals. Wall decals are going to be further complicated by the fact that if I do the decals in a pass separate from the walls themselves, then they will be subject to the same asshole pixel trickery as unsorted geometry, due to the fact that a decal might be drawn on a wall behind an existing wall. To prove it, let's create a wall decal.

(Note: Creating these additional primitive resources is fairly easy. I just create a new texture, then edit an existing .scene.xml and .material.xml to create a new resource. This process could easily be scripted using a tool to automatically generate resources from a list of textures.)

Here is what we get with wall decals:

screen6003.jpg

See those damned pixels?

screen6003zoom.jpg

It's not too bad, but it is there: just a bit of halo around the fore-ground walls where they are not being properly blended with the decal. Again, there are hacks that could mitigate this, but finding a perfect solution is difficult. One hack would be to remove any differentiation between Wall and WallDecal, and have them all be drawn in the same geometry pass. However, this will incur problems in that the decals are coplanar with the walls. This could be mitigated by using a different geometry primitive for the decals, one in which the wall polygon itself is offset just a little bit from the plane of the wall, ensuring that it would always be sorted in front of its corresponding wall. Similarly to using many different passes for floor decals, you could create many different versions of the wall geometry, with increasing offsets, for different layers of wall decal geometry. This could quickly become absurd, of course, but for a limited number of possible layers, it represents one possible solution. Or you could go the simple route and use the same geometry, but add the offset when placing the primitive. Either way, this creates the situation where the decal sits just above the surface it is applied to, enough that it will always be sorted in front of the wall and drawn correctly

Now, though using different geometry primitives or offsets might solve the sorting problem, you may still be faced with possible Z-fighting problems on layers that share the same geometry offset. You can help to prevent this by disabling Z-write when drawing decals, ensuring that subsequent decals don't stink up the Z-buffer with their offset depths.

All of this adds constraints to the system, constraints that are not quite so pronounced in a roll-your-own isometric scene structure. For instance, Goblinson Crusoe allows arbitrary placement and stacking of any number of floor and wall decals, with never an occurrence of Z-sorting or Z-fighting artifacts, due to the inherently layered design and the rigidness of the custom drawing order. Of course, this level of flexibility might not necessarily be required. Much of the purpose of decal decorating is to add character to an otherwise unremakable and highly regular piece of wall. This kind of decoration can usually be done in a single layer, without requiring complex stacking of decals. The typical use-case in GC is to only add a single layer of decoration. The occurrence of multiple layers is quite rare. So the constraints introduced by sorting and z-fighting might not be all that limiting.

The question is, though: Is it really worth it?

In Goblinson Crusoe, with a map full of goodies and a double handful of baddies running around tossing fireballs and AoE heal spells, drawing map and mini-map and GUI, performing logic, etc... I get around 157 FPS. Let's see what I get with this new system:

screen18678.jpg

Around 32 FPS. While that is not bad (considering how crap my computer is) I haven't even added characters, logic updating, or GUI rendering yet. Those are going to bring their own performance penalties.

So I guess the conclusion I'm coming to here is that for an anti-aliased/blended sprite view, then going with out-of-the-box Horde3D might not be the way to go. To be fair, I have tried this same experiment with both Ogre3D and Irrlicht, with similar conclusions. A general-case 3D engine is great for that general-case, in which case the tradeoffs are easier to swallow, but there are assumptions that you can make about an isometric viewpoint that allow you to make some exceptional speed optimizations that you just can not make with a general scene structure. If you want to disallow older crap hardware such as mine, then using a general 3D scene graph is fine, but be aware that some people will complain if your 2D-looking game doesn't run well on their hardware. I also don't like the tradeoffs I have to make with the decal system in order to get it to work right.

My next project is to dive into the guts of Horde3D scene management and see how difficult it would be to override the default scene manager and implement my own...
3 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement