Isometric View of “Voxel-Style” Terrain

Suppose you have an arrangement of data for a terrain set up like this: terrain[layer][cart-x][cart-y]. Each location (ex. terrain[0][0][0] for the top left corner of the lowest layer) in the array simply stores -1 for air, 0 for water or 1 for dirt. How should I loop over this data to produce a "camera?" For example, given a focal point, world, and screen size, iterate over only the visible blocks and call the block-render function.

I have some semi-working Javascript, but I have several issues;

How can I find the minimal number of tiles to draw so that the screen edges are covered, while keeping the position centered on screen? I've gotten this done but it's messy.

When the layer depth varies, how do I properly account for that as far as offsets are concerned? Currently, I can make an offset calculation for a depth. But it doesn't carry to other depths. Like say, you are viewing an area where there is terrain at layer 15. The camera is offset by a certain amount to restore the focal point to center. But if you move to an area where the height is only 14, then you'll start to see a drop off toward one edge. This also happens when there is something much lower on the "forward" side.

How do I prevent drawing blocks that are completely obscured? This one is very important and should be seen as the priority of these, but I don't think I can move on to it yet.

Here's what I've made so far:

View working (hacky) example on Codepen

(Showing only what I think are the important parts here)

function screenPos(x,y,z)
{
var op = {
x: (x-y)*th,
y: ((x+y)*th/2)-(z*td)
};
return op;
}

function drawTerrain(position, world, ctx, minElev, maxElev) {
minElev = (minElev===undefined? position.z : minElev);
maxElev = (maxElev===undefined? position.z+1 : maxElev);

var cen = {
a: position.x + position.y,
b: position.x - position.y
},

max = { //Use testSize instead of canvas size so we can see overdraw
a: Math.floor(testSize.y/th),
b: Math.floor(testSize.x/ts)
},

startPos = {
x: ((cen.a - max.a) + (cen.b - max.b))/2,
y: ((cen.a - max.a) - (cen.b - max.b))/2
},

offset = screenPos(startPos.x, startPos.y);

//account for testSize offset
offset.x-=50;
offset.y-=50;

//regular offsets
offset.y += th/2;
offset.y -= (th)*minElev;

ctx.textAlign = "center";
ctx.textBaseline = "middle";

for(var z = minElev-1; z < maxElev; z++) {
for(var a = cen.a - max.a -1; a < cen.a + max.a +3; a++) {
for(var b = cen.b - max.b -1; b < cen.b + max.b+2; b++) {
if ((b&1) != (a&1)) continue;

var x = (a+b)/2,
y = (a-b)/2;

drawTile(x, y, z, offset, world, ctx);

i++;
}
}
}
}


Some thoughts on possible solutions: I think that at least one step of conversion between x/y and a/b could be removed here, as it's gotten all convoluted while trying to make things work. There are lots of things like that, I realize. The Codepen page shows exactly how thrown-together the current solution is. The only thing I'm interested in is fixing and optimizing the main loop and any methods/data necessary to drive it.

P.S. I didn't feel the need to include the drawTile function on this page as it has very little conditional logic, only basic stuff, and calls to the same screenPos function. That said, you can see it at the Codepen Example

Here's how most people start out in it for "basic" terrain height maps ...

Create my volume (voxel array). Generate a Y value for each x,z im interesting (terrain heightmap generation). Loop through a column of voxels from 0 to terrain height and set their values as "inside the volume" (on).

Generate a mesh based on examination of the volume by looping through each voxel and asking "is the voxel on each face resulting in a sign switch".

For example ... Lets take the most basic way of handling voxel data and assume all voxels are boolean true or false where true is "something" and false is "nothing".

I have a height map value of say ... 3 so using 1 for true and 0 for false my data for the current column would be ...

111000...

This tells my mesh generator that at point x,3,z I will need to render "something".

taking this a step further we can then do the check on all sides of our "volume of space for a voxel and its neighbours" ...

Assuming a voxel is a cube (which makes sense when storing as a voxel[][][]) you can do something like ...

var xyz = (1,1,1);

if !(y + 1) { top face visible }
if !(y - 1) { bottom face visible }
if !(x - 1) { left face visible }
if !(x + 1) { right face visible }


...

You get the idea ...

Most people use float values for their voxel data as this allows more precision in the mesh generation as you are looking for a point at which the data evaluates to 0 for a given "cell" where a cell is a chunk of 27 voxels and you are evaluting the center voxel.

for example ...

if the center voxel = 0.5 and the voxel above = -0.4 the sum of the two values is -0.1 which means that the surface you want to render is -0.1 from the top of the voxel space for your center voxel. Meshing algorithms like Marching cubes, or Dual Contouring work this way.

• SMH! Cube! I've realized problem #2 is caused by my "cubes" being slightly squashed... The simple examination of how to check face visibility is extremely helpful. Thank you. – Danie Clawson Oct 10 '14 at 23:07
• the slight squashing could all be done on the the gpu as part of a custom isometric shader for simplicity if you set your view and projection matrices up correctly (seen people do this before). That way your cube / voxel code is all unified grids (making life much simpler) – War Oct 11 '14 at 9:44