Dynamic objects in Isometric Map Drawing algorythms

Started by
12 comments, last by JTippetts 11 years, 8 months ago
I just finished my Isometric drawing class that can draw 2D cubic tilemaps (its not a single plane with objects on top, its a lot of cubes piled, planes divided on what I call floors (no, Im not cloning minecraft)).

I can draw in diamond shape and in the staggered shape, as described in most algorithms out there..Whats holding me up now is something I didnt find in any isometric article/topic out there, how do I draw characters/objects on it in a optimal way?

What Im doing now, as one can imagine, is traversing my grid/map ( its a 3d array) and drawing the tiles in a order so alpha blending works correctly.

Well, things are a lot easier when you just have a plane with objects on top of it, but since my grid is 3D, I cant see any other way than having to draw all my objects at the same time Im drawing the map, with screws the cuteness of the code..

I cant imagine a good way to do this, things that I can imagine is:

#1# give a list of objects(sprites and its positions) to the iso drawer, each loop iteration on the map drawing algo checks the position of all objects against the current tile on the grid....This is the most inefficient thing I ever imagined (for each tile on the map, check all the objects..)

#2# each time a object moves, insert it ON THE GRID, this is so ugly..screws everything, the isodrawer now will change a lot since now its not more a map drawer, its the entire game drawer (except the HUD)(well, this is true for #1# too)...Second problem with this approach is that objects can move freely, not per tile movement, so an object can occupy 2 tiles (be between 2 tiles), dont know how Id handle it.

#3# Forget the " iso drawer "map drawing algo" ", transform it onto a " "depth list" composer ", the list consist on [2D position, uv, Z(depth)] of all tiles on the map, then insert ..somehow..the objects on that list, then draw the list...I cant say for sure if this is feasible, youd have all tiles Z values, than you can compute all objects Z values, now totally independent, (the trick here would compute the Z of the objects considering its position on the map (i.e. object at floor 2, row 4, col 1 : Z = ? ) and the tiles Z too(giving top floors tiles higher Zs, etc.).......

Im using XNA spritebatch for doing this, I think (not sure, I just started using xna) that it can draw sorted by Z order for you, so I wouldnt need to sort the list.

Any links, tips or ideas are welcomed ;D

"why dont you do it 3D?" 3D is good for ultra realistic graphics, for simpler stuff it looks terrible, because 2D art is owsome and cant be compared with 3D, they are two different fields, not the evolution of the other, because I prefer it, because Im fan of isometric 2D for long, because programming iso stuff is tricky and is forcing my mind a lot.....IMO


Heres a screen shot of what I current have:
isoSS_cubicTilemap.png
Advertisement
I fear you misunderstand exactly what "3D" is. 3D doesn't have to look terrible, good, ultra-realistic, or anything else. 3D is merely a set of mathematical transformations. An abstraction, if you will. You can accomplish in 3D exactly the visual appearance you have already established with your 2D, by using an orthographic transformation and your 2D graphics view-projected onto properly-shaped primitives. But it will have the added bonus of a Z-buffer which will greatly aid your efforts of populating the map with dynamic objects. It will certainly be at least an order of magnitude easier than hacking together some kind of god-awful convoluted draw algorithm, as you seem to be working on. On a platform with the proper hardware support, I can think of absolutely zero valid reasons to be doing something like this the "old school" way. This is coming from someone who has written many isometric games and prototypes, who loves the isometric look and feel more than any other game.

It's all about utilizing the tools that modern technology provides to overcome some of the hurdles that people have been struggling with for years. Feel free to search the extensive back-history of the old Isometric Games forum here at gamedev.net. You will find countless posts on this very issue. What you won't find is a solution that is simple and elegant.... unless you go 3D.
Well, by 3D I really meant 3D cubes, but if you talking about "billboards", 3d quads with a texture, than we are talking about the same thing..I dont see how it changes anything..??
I plan to migrate all that stuff to my sprite engine that Im working on on dx11, with is exactly a ortho projection and quads, see?..With I believe must be the same thing xna sprites are...

Would you enlight me on how youd do it?
I'm not talking about billboards, except maybe for the mobs. The world geometry should be cubes (or, rather, your shortened cubes). That way, the depth buffer will have the depth that it needs to clip your mobs after the world is drawn. Just take your 2D landscape sprites and project them onto shortened cubes, ramps, etc... Since the view never changes, your cube primitives only need to consist of the three visible faces.
Translating pixel art to textures so you can map to ortho projected cubes is that easy? and will look the same?
The steps to do it, as Im guessing:
Use nearest/point filtering when strecthing the original 2d image (on photoshop i.e.) to produce the texture map, and then sample it with point sampling onto a ortho projected cube..
I just cant believe it will look exactly like the original pixel art o_o is that what you saying?

Because if you just saying it will look good and 2D, its still not enough reason to consider it, the great deal with 2d iso games, is the 2d iso art..

Even if it do will look just like..

One problem already is to make that translation, you have to do it manually for all tiles, or you build a tool to do it for you, with will have to account for all possible shapes you have in your tilseset.

Forgetting the tiles and do the textures straight ahead is also a complete different matter, I mean, how would you draw a grass texture for a cube, so it can look a nice 2D grass after projected, and not just a texture on a 3D cube? Youd have to draw the texture accounting for how much it will be stretched on Y, not feasible either..Youd need a different artist, not a good 2D pixel artist.

Then that tool to make the conversion automatically seems the better solution, but building it seems as tricky as all the isometric old school stuff..

I dont think theres a direct translation from 2D to 3D as you make it look, unless Im dont get what you mean yet, I remain with I said in the first post: 2D and 3D are two completely different art fields..
The question isn't about different art. Like I said, you can continue to use the same exact art you have. You were on the right track when you suggested to use nearest filtering and project it onto an ortho cube. That is exactly right. And yes, you can set up your mathematical abstraction so that the 3D version will look identical to the 2D. Here is a tile I made just now:

u16O3.png

I made a couple versions of it (lighter and darker). Then I took that tile and (1) hand-arranged several copies as if I were a 2D renderer, stacking blocks back to front and positioning the sprites just so to line up. Then (2) took the tile, mapped it onto a cube in Blender, set up an orthographic projection and a material with no texture filtering, then built a stack of blocks identical to the first. Can you tell me which one is the 2D version and which one is the 3D version?

3HdlV.png

I sure can't tell, and neither can the Gimp. Doing a subtraction operation between the two as layers results in a perfectly black bitmap. The two versions are pixel-perfect identical. However, the advantage of the 3D rendering (which is on the right, in case you were wondering) is this little goodie right here:

WaTlB.png

See that little beastie? That's the depth map of the 3D version. If this were in-game, that depth buffer would be used to clip incoming fragments. You can very easily draw your entire map, then go back and draw all your mobs, and the z-buffering will ensure that pixels clip correctly. It's so easy, I honest to God wonder why in this day and age anyone would ever bother with trying to rehash all the old techniques.

If you don't want to use the hardware for whatever reason, that's fine. You can implement your own z-buffer, build a simple 3D abstraction for your map layer, and do the depth testing yourself. It's relatively simple (though it is easier to just let the hardware handle it for you; that's what it's designed for). But revisiting 20+ years of old 2D techniques, hoping that things will turn out better this time, really isn't a solution. Nor is it even an interesting problem anymore. It's a very much solved problem, and all those sprite sorting and stacking tricks just get in the way of progress toward completing your game.

I don't mean to sound like some kind of crusty old know-it-all, but I have been doing isometric games now for almost 20 years. I've run the gamut of sprite sorting techniques, block tagging, you name it. Nothing beats the elegance of using the hardware correctly, in my opinion. To reinforce it, here are two images I've had kicking around for almost a decade now. They are from an old game I was working on, Golem. The first image was taken before I did a rewrite of the backend code to 3D. It was still using the original 2D code. The second image was from after the rewrite. You can see that visually, there is little difference. The game still looks the same. The differences were in the elimination of all the weird little edge cases that made the original 2D engine such a pain in the ass to work with. The code was made vastly simpler as well.

iiIZj.jpg

Iygnx.jpg

I was able to reuse all assets, I didn't have to make any artistic changes whatsoever. The game still looked 2D. The only difference was that the character could now walk through certain doorways or cross tile boundaries in certain ways, and not have to worry about being overdrawn in odd fashion by some other piece of the landscape.
Thats freaking nice..Im not trying to forcing myself onto old school stuff either, its just that accepting you can do the same on 3D never seemed possible to me.

When mapping the tiles to the cubes on blender, you didnt even need to stretch the tile/texture? you can just map directly?

One thing that holded me up on that is the tile spacing, when dealing with pixel art, you account the amount of pixels from one tile to another, and how much pixels you overlap(or not), with also means you can have different shaped iso tiles (mine have 2 horizontal px on the top and bottom corners, 2 px horizontal on the left right ones, on the top and bottom you must overlap by one pixel, on the left and right you overlap by too, I already saw many tilesets with different proportions but same perspective(like no overlapping at all, 3 px at top and bottom, etc.)) I never imagined this fitting on 3D stuff without losing the 2D appeal.

Can I assume irregular tiles like fancy curved slopes, like on this famous tileset:
iso_64x64_outside_both.png
Will also work fine? o_o look all the irregular, not straight cube stuff on these tiles.
Yeah, you can map all of those to cubes. It really is just like mapping them to billboards, only you use cubes instead so they have depth. Mapping it to cubes will give it the shape of a cube, though, so in the case of some of those ramps and such you can deform the cube somewhat, maybe even subdivide it one or two times, to give it a shape that more closely approximates the sprite it is meant to represent. The key word is approximate; you don't need to get crazy.

As far as mapping the sprite onto the geometry, there is no stretching or tiling. Here is how I do it in Goblinson Crusoe and all my other ventures.

1) Start with a cube in Blender.

2) Since only three faces (top, front-left, front-right) are visible, I delete the non-visible faces.

3) Set up an orthographic camera that is rotated 30 degrees on the X axis, 45 degrees on Z. This gives a viewpoint that mathematically implements the 2:1 tile ratio size of most "standard" isometric games. Hit Numpad-0 to go to camera view so I'm looking at the cube from the perspective it will be viewed in game.

4) Tab into Edit mode on the cube, press 'u' for UV unwrap, and select Project to View (bounds). What this does is assigns UV coordinates based on the current camera view. In effect, it flattens the cube as it is being viewed into a UV map. The result is something like this:

Jvggp.png

4) Export the cube geometry (or write down the verts and UVs and just enter them manually into a geometry array in your program, if you don't want to much with model loading.

5) Draw the cube with the tile set as the texture, with Nearest filtering.

You can see that the UVs are projected exactly as the cube is viewed. The only slightly tricky part here is using trigonometry to calculate how much to scale the unit-sized cube vertically to fit the desired tile size. This is something you can either calculate mathematically or by trial and error. You want to scale it so that the UVs line up with your tile exactly as so. Then you want to scale the cube itself, which will be drawn by your engine, so that it will fit the tile size in the orthographic projection.

You do have to think about the 3D math that underlies the abstraction, but as you gain a deeper understanding of 3D it becomes no problem. By setting up your projection carefully, you can draw the cube geometry such that it will cover exactly the same screen space as the 2D version, pixel for pixel.

There is one small caveat: you do want to avoid using too much floating point in your abstraction. I typically set my orthographic projection up on a 1:1 basis; that is, 1 pixel in screen space equals one unit in 3D space, and always clamp translations to integers. The reason for this is that floating point math can get a little bit "leaky", and you might sometimes get rasterization artifacts when you are trying to get pixel-perfect rendering of your sprite. You can also pad your sprites, duplicating pixels all the way around the shape, so that if the occasional off by one funky raster does take place, it won't be noticeable. However, if you are careful with your math, that shouldn't really be a problem.

Maybe if I get a few minutes at work, I'll pop together a demo.
Im having artifacts on XNA already when zooming (scaling) the camera/view matrix, not a 3D exclusive issue ;D
My WIP sprite engine on Dx11 is pixel perfect already, but I have a scale unscale thing going on so I can work with physics with a px not being a metter, so basically I have a to_Metter on sprite sizes, and a to_PX on my projection matrix (the inverse of the to_Metter).. I probably can just plug cubes instead of quads and it will work, and workout the camera position.

But never done anything in 3D with XNA yet (started messing with XNA last week)

How do you turn off all filtering on blender? I turned mipmaps off on the system tab, but it stills have some filtering going on (left model, right texture):
isoblenderss.png

From programming to modeling questions ;D

"Maybe if I get a few minutes at work, I'll pop together a demo."
._. * humiliation *
The key to eliminating filtering, whether it's in Blender or in a game, is to ensure that there is exactly 1:1 correspondence between the pixels being drawn and the region of the buffer they are being drawn to. Any change in scale or stretching will result in filtering of some sort, and you will have to plan accordingly. You can use nearest filtering, but there will still be filtering. This is actually the biggest hurdle when trying to do a pixel-perfect translation from 2D to 3D. Although, of course, you also have to worry about filtering if you do traditional 2D with scaling, so there's that.

In Blender, you can reduce filtering by going to the Texture->Image Sampling panel, turning off Mip Maps and interpolation, and cranking down the filter size as low as it will go. But you also have to ensure that your object is scaled relative to the viewpoint correctly, so that there is a 1:1 pixel mapping. Also, I notice that in your posted image, on the right, there is a bit of sloppiness in your tile. To be pixel-perfect, or as near to it as can be achieved, you need to ensure that your tiles are correct. A 2:1 tile ratio has cell lines that are exactly 2 pixels horizontally for every 1 pixel vertically; in your tile, there are a couple spots on the cell boundary that don't follow the pattern precisely, and this can have implications when drawing. That is a minor nitpick, though.

Getting the math right for pixel-perfect is the trickiest part of the process. You also have to ensure the quality of the asset creation process. I have found that, for best results, a 2:1 isometric tile hold approximately to this template:
fFjeP.png

That is zoomed, of course. You can see that all the edge lines hold to the 2:1 line ratio. In the image, the dark zones indicate areas where padding may be placed, in order to mitigate what filtering may take place. Extra padding around the outside may be necessary if the sprite is to be scaled. The padding between the top and the walls should match the top, and not the walls, since those rows of padding will be visible on tiles of the same level placed adjacent to one another, without tiles on top of them.

The main areas where you need to do some calculating are:

1) The orthographic projection matrix, and how it relates to the drawing size of the tile cubes. My personal method is to visualize each tile, as in the template above, as being some number of units in size. Each 2-pixel wide block in the cell lines denotes a unit, so a tile that is 64 pixels wide would denote a 16x16 unit cell. So if I draw my cubes as 16x16xH (height varies, depending), and use integer math for coordinates and locations, then I can ensure that tiles will be drawn on whole-pixel locations, and filtering is reduced. This requires special consideration in calculating the projection matrix.

My method, then, of calculating the orthographic matrix is to divide the screen horizontal resolution by the width, in pixels, of a tile. (64, in the case of the above magnified template). Then I multiply this value by the world size of a tile (16, in my system) and multiply that result by 1.41421356237 (The square root of 2). The reason for this is that, given a camera rotated 45 degrees around the vertical axis, you are viewing a tile not edge-wise, but diagonally. The distance between the corners of a square whose sides are 1x1 is, of course, the square root of 2.

With that calculated value, I multiply by the aspect ratio to get the orthographic height of the viewport, then use those values to set the ortho matrix. To illustrate, here is a bit of code to do just that in OpenGL:


void CIso::applyCamera()
{
// Get the viewport resolution
GLint viewport[4];
glGetIntegerv( GL_VIEWPORT, viewport );

// Set the projection matrix
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
float aspect=(float)viewport[3]/(float)viewport[2];
float screen_tiles=(float)viewport[2] / (float)tile_pixel_width_;
float ortho_width=screen_tiles*(float)tile_world_size_*1.414213562373f;
float ortho_height=ortho_width * aspect;
glOrtho(-ortho_width/2, ortho_width/2, -ortho_height/2, ortho_height/2, 0.0f, 1000.0f);

// Set the camera, pointed at (x_,z_), 30 degrees around X and 45 degrees around Z. 100 units (arbitrarily chosen, will probably need to be larger for a bigger map, to
// Prevent far-plane clipping
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0,0,-100);
glRotatef(30,1,0,0);
glRotatef(-45.0f,0,1,0);
glTranslatef(-x_,0, -z_);
}


That is working code from the demo that sets the projection matrix. The member tile_pixel_width_ of that class is set during initialization, to the width (in pixels) of a tile. 64, with the above template. The member tile_world_size_ is set by dividing the tile pixel width by 4, for a 16x16xH unit tile in the case of the above template.

2) Tile height. If you set up a 30 degree camera (the angle needed for a 2:1 projection) and view a cube, you will notice that the cube vertical faces are fore-shortened, due to the angle of the viewpoint. There needs to be careful consideration for the correspondence between the tile sprite and the cube primitive that is drawn. The height of the cube primitive needs to be scaled correctly to correspond with that foreshortening. This can take a little bit of trigonometry, but once you wrap your head around it it's not too complicated. In the case of my template tile above, the tile is a total of 64x48 pixels. (The rest is padding to bring it to 64x64.) That means 32 of the pixels are taken by the top of the tile, leaving 16 for the vertical faces. A bit of trig gives me the totally arbitrarily funky value of ~0.40824829f as the height to scale the unit-sized cube to in order to match the tile template.

With that in place, the rest is pretty simple. I put together a small demo in C++, using the latest RC of SFML 2.0. (I don't use XNA or C#.) But the code should be relatively explanatory. It's a quick, one-off demo meant to show the concepts, rather than a comprehensive prototype, but it works.

The framework:

main.cpp
[spoiler]

#include <iostream>
#include <SFML/Window.hpp>
#include <SFML/System.hpp>
#include <SFML/Graphics.hpp>
#include <cmath>
#include <sstream>
#include "app.h"
#include "iso.h"

int main()
{
CApp app;
app.setVideoMode(1024, 768, false, true);
CIso iso(64);
iso.create(8,8,8);
iso.setPosition(0,0);
iso.createTile("grasstile.png", TILE);
iso.createTile("dirttile.png", TILE);
iso.createTile("leftramptile.png", LEFTRAMP);
iso.createTile("rightramptile.png", RIGHTRAMP);
iso.setNode(0,0,0,0);
iso.setNode(1,0,0,0);
iso.setNode(0,0,1,0);
iso.setNode(1,0,1,0);
iso.setNode(0,1,0,0);
iso.setNode(0,1,2,1);
iso.setNode(0,0,2,1);
iso.setNode(2,0,0,1);
iso.setNode(0,0,3,2);
iso.setNode(3,0,0,3);
iso.setNode(1,1,0,3);
float x=0, z=0;
unsigned int curscreen=0;
while (app.isOpen())
{
sf::Event event;
float thistime=app.getTime();
if (app.pollEvent(event))
{
if (event.type == sf::Event::Closed)
{
app.close();
break;
}
if (event.type==sf::Event::KeyPressed)
{
if(event.key.code==sf::Keyboard::S)
{
std::stringstream filename;
filename << "screen" << curscreen++ << ".png";
app.screenshot(filename.str().c_str());
}
}
}
app.clear();
iso.render();
// Update the window
app.swap();
//float elapsed=app.getTime()-thistime;
float elapsed=app.getTime();
x=64+sin(elapsed)*64;
z=64+cos(elapsed)*64;
iso.setPosition(x,z);
}
return EXIT_SUCCESS;
}

[/spoiler]

app.h
[spoiler]

#ifndef APP_H
#define APP_H
#include <SFML/Window.hpp>
#include <SFML/System.hpp>
#include <SFML/Graphics.hpp>
#include <string>
#include <map>
class CApp
{
public:
CApp();
~CApp();
bool setVideoMode(int w, int h, bool fullscreen, bool vsync);
bool isOpen();
void clear();
void swap();
void close();
bool pollEvent(sf::Event &event);
void screenshot(const char *name);
float getTime();
private:
sf::Window window_;
sf::Clock clock_;
};
#endif

[/spoiler]

app.cpp
[spoiler]

#include "app.h"
#include <vector>
#include <GL/gl.h>
CApp::CApp() : window_(), clock_()
{
}
CApp::~CApp()
{
window_.close();
}
bool CApp::isOpen()
{
return window_.isOpen();
}
bool CApp::setVideoMode(int w, int h, bool fullscreen, bool vsync)
{
if(fullscreen)
{
sf::VideoMode mode(w,h);
if(!mode.isValid())
{
// Not a supported full-screen mode
return false;
}
}
window_.create(sf::VideoMode(w,h,32), "Isometric Test", (fullscreen==true) ? sf::Style::Fullscreen : sf::Style::Close);
if(vsync) window_.setVerticalSyncEnabled(true);
glEnable(GL_TEXTURE_2D);
glEnable(GL_DEPTH_TEST);
glEnable(GL_ALPHA_TEST);
glEnable(GL_BLEND);
glDepthFunc(GL_LEQUAL);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glAlphaFunc(GL_GREATER, 0.0);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);

return true;
}
void CApp::close()
{
window_.close();
}
void CApp::clear()
{
glClearColor(0.0f,0.0f,0.0f,0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void CApp::swap()
{
window_.display();
}
bool CApp::pollEvent(sf::Event &event)
{
return window_.pollEvent(event);
}
void CApp::screenshot(const char *name)
{
sf::Vector2u size=window_.getSize();
if(window_.setActive(true))
{
std::vector<sf::Uint8> pixels(size.x*size.y*4);
sf::Uint8 *pixelptr=&pixels[0];
glReadPixels(0,0,size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, pixelptr);
unsigned int pitch=size.x*4;
for(unsigned y=0; y<size.y/2; ++y)
{
std::swap_ranges(pixelptr+y*pitch, pixelptr+(y+1)*pitch, pixelptr+(size.y-y-1)*pitch);
}
sf::Image img;
img.create(size.x, size.y, pixelptr);
img.saveToFile(name);
}
}
float CApp::getTime()
{
return clock_.getElapsedTime().asSeconds();
}

[/spoiler]


And the isometric map code:

iso.h
[spoiler]

#ifndef ISO_H
#define ISO_H
#include <SFML/Graphics.hpp>
#include <GL/gl.h>
#include <vector>
enum ETileShapes
{
TILE,
LEFTRAMP,
RIGHTRAMP
};
struct STile
{
STile(std::string &texname, ETileShapes shape);
sf::Texture tex_;
sf::Vector3f *verts_;
sf::Vector2f *uvs_;
unsigned int *indices_;
unsigned int numverts_;
unsigned int numindices_;
};
class CIso
{
public:
CIso(unsigned int tile_pixel_width);
~CIso();
void create(unsigned int width, unsigned int height, unsigned int depth);
void setNode(unsigned int x, unsigned int y, unsigned int z, unsigned int tile);
void fill(unsigned int index);
void setPosition(unsigned x, unsigned z);
void render();
void createTile(const char *imgname, ETileShapes shape);
private:
std::vector<unsigned int> nodes_;
unsigned int width_, height_, depth_;
std::vector<STile> tiles_;
float x_, z_;
unsigned int tile_world_size_, tile_pixel_width_;
void applyCamera();
void renderNode(unsigned int x, unsigned int y, unsigned int z);

};
#endif

[/spoiler]

iso.cpp
[spoiler]

#include "iso.h"
#include <iostream>
static float ht=0.40824829f;
// Basic Shapes (ETileShapes)
// TILE
// Verts
sf::Vector3f tile_verts[7]=
{
sf::Vector3f(0,ht,0),
sf::Vector3f(0,ht,1),
sf::Vector3f(1,ht,1),
sf::Vector3f(1,ht,0),
sf::Vector3f(0,0,1),
sf::Vector3f(1,0,1),
sf::Vector3f(1,0,0)
};
// UVs
sf::Vector2f tile_uvs[7]=
{
sf::Vector2f(0.5,0),
sf::Vector2f(0, 0.25),
sf::Vector2f(0.5, 0.5),
sf::Vector2f(1,0.25),
sf::Vector2f(0,0.5),
sf::Vector2f(0.5,0.75),
sf::Vector2f(1,0.5)
};
// Indices
unsigned int tile_indices[18]=
{
0,1,2, 0,2,3, 1,4,2, 2,4,5, 3,2,6, 2,5,6
};
// LEFTRAMP
// Verts
sf::Vector3f left_verts[5]=
{
sf::Vector3f(0,ht,0),
sf::Vector3f(0,0,1),
sf::Vector3f(1,0,1),
sf::Vector3f(1,ht,0),
sf::Vector3f(1,0,0)
};
// UVs
sf::Vector2f left_uvs[5]=
{
sf::Vector2f(0.5,0),
sf::Vector2f(0,0.5),
sf::Vector2f(0.5,0.75),
sf::Vector2f(1,0.25),
sf::Vector2f(1,0.5)
};
// Indices
unsigned int left_indices[9]=
{
0,1,2, 0,2,3, 3,2,4
};
// RIGHTRAMP
// Verts
sf::Vector3f right_verts[5]=
{
sf::Vector3f(0,ht,0),
sf::Vector3f(0,ht,1),
sf::Vector3f(1,0,1),
sf::Vector3f(1,0,0),
sf::Vector3f(0,0,1)
};
// UVs
sf::Vector2f right_uvs[5]=
{
sf::Vector2f(0.5,0),
sf::Vector2f(0,0.25),
sf::Vector2f(0.5,0.75),
sf::Vector2f(1,0.5),
sf::Vector2f(0,0.5)
};
// Indices
unsigned int right_indices[9]=
{
0,1,2, 0,2,3, 1,4,2
};

STile::STile(std::string &texname, ETileShapes shape) : tex_(), verts_(0), uvs_(0), indices_(0), numverts_(0), numindices_(0)
{
tex_.loadFromFile(texname.c_str());
tex_.setSmooth(false);
switch(shape)
{
case TILE: numverts_=7; numindices_=18; verts_=tile_verts; uvs_=tile_uvs; indices_=tile_indices; break;
case LEFTRAMP: numverts_=5; numindices_=9; verts_=left_verts; uvs_=left_uvs; indices_=left_indices; break;
case RIGHTRAMP: numverts_=5; numindices_=9; verts_=right_verts; uvs_=right_uvs; indices_=right_indices; break;
default: break;
}
}

CIso::CIso(unsigned int tile_pixel_width) : tile_world_size_(tile_pixel_width/4), tile_pixel_width_(tile_pixel_width)
{
}
CIso::~CIso()
{
}
void CIso::applyCamera()
{
// Calculate the orthographic width/height for the screen projection based on the tile pixel size
// This is calculated as so:
// The World-space size of a tile is tile_pixel_width/4. This means that, for example, a tile whose pixel width
// is 64 represents a tile that is 16x16 units in world size. So to cross the tile, an actor has to move 16 steps.
// The number of tiles that are visible across the screen is calculated by dividing the screen resolution width by
// tile_pixel_width. Now, this represents the number of tiles visible across the screen, but those tiles are viewed at
// a 45 degree angle around the Y axis, so the actual number of tiles visible would be this result divided by the
// square root of 2. This gives you the total number of tiles across that the screen is. This result is then multiplied
// by the tile world size (16, in the case of a 64x64 tile sprite) to get the orthographic width of the screen.
GLint viewport[4];
glGetIntegerv( GL_VIEWPORT, viewport );
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
float aspect=(float)viewport[3]/(float)viewport[2];
float screen_tiles=(float)viewport[2] / (float)tile_pixel_width_;
float ortho_width=screen_tiles*(float)tile_world_size_*1.414213562373f;
float ortho_height=ortho_width * aspect;
glOrtho(-ortho_width/2,ortho_width/2,-ortho_height/2,ortho_height/2,0.0f, 1000.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0,0,-100);
glRotatef(30,1,0,0);
glRotatef(-45.0f,0,1,0);
glTranslatef(-x_,0, -z_);
}
void CIso::create(unsigned int width, unsigned int height, unsigned int depth)
{
width_=width;
height_=height;
depth_=depth;
nodes_.resize(width*height*depth);
fill(1000);
}
void CIso::setNode(unsigned int x, unsigned int y, unsigned int z, unsigned int tile)
{
unsigned int index=z*width_*height_ + y*width_ + x;
if(index>nodes_.size()-1) return;
nodes_[index]=tile;
}
void CIso::fill(unsigned int index)
{
for(unsigned int c=0; c<nodes_.size(); ++c) nodes_[c]=index;
}
void CIso::setPosition(unsigned int x, unsigned int z)
{
//unsigned int tx=(unsigned int)(x/64);
//unsigned int tz=(unsigned int)(z/64);
//x_=(float)tx*64.0f;
//z_=(float)tz*64.0f;
//unsigned int tx=(unsigned int)(x*16);
//unsigned int tz=(unsigned int)(z*16);
//x_=(float)tx/16.0f;
//z_=(float)tz/16.0f;
x_=(float)x;
z_=(float)z;
}
void CIso::createTile(const char *imgname, ETileShapes shape)
{
std::string s(imgname);
tiles_.push_back(STile(s, shape));
}
void CIso::renderNode(unsigned int x, unsigned int y, unsigned int z)
{
unsigned int index=z*width_*height_ + y*width_ + x;
//std::cout << index << std::endl;
if(index>=nodes_.size()) return;
unsigned int tile=nodes_[index];
if(tile>=tiles_.size()) return;
//std::cout << "render" <<std::endl;
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glTranslatef(x*tile_world_size_,y*tile_world_size_*0.40824829f,z*tile_world_size_);
glScalef(tile_world_size_, tile_world_size_, tile_world_size_);
STile *tile_ptr=&tiles_[tile];
if(!tile_ptr) return;
tile_ptr->tex_.bind();
glVertexPointer(3,GL_FLOAT,0,tile_ptr->verts_);
glTexCoordPointer(2,GL_FLOAT,0,tile_ptr->uvs_);
glDrawElements(GL_TRIANGLES, tile_ptr->numindices_, GL_UNSIGNED_INT, tile_ptr->indices_);
glPopMatrix();
}
void CIso::render()
{
applyCamera();
for(int x=width_-1; x>=0; --x)
{
for(int y=height_-1; y>=0; --y)
{
for(int z=depth_-1; z>=0; --z)
{
renderNode(x,y,z);
}
}
}
}

[/spoiler]

Here are the tiles used for the demo. (They should be in the root directory of the executable.)

dirttile.png
ai2f8.png

grasstile.png
iHKa4.png

leftramptile.png
ULcWw.png

rightramptile.png
39oFK.png

The demo sets up a tiny sample map, then enters a loop in which the camera scrolls around endlessly in a circle. Nothing fancy. The tile geometry units are hard-coded, but in a real game I'd use loadable geometry in case I needed a weird tile of some sort. Here is a shot of the demo in action:

mu0Xo.png

If you zoom in, you can see that the pixels are pretty close to pixel-perfect (or, at least they were on the source image; I really can't vouch for imgur's service after they're uploaded). Filtering artifacts might occur (very occasionally) but if you pad your sprites correctly, even on the rare times they happen, they are not that noticeable.

I didn't have time at work to hack in mob sprites, but depth testing IS working correctly, and the sprites would be correctly clipped. (You can tell depth buffering is working, as the tiles are drawn from front (top, nearest corner) to back (bottom, far corner), and yet the back tiles are correctly clipped by the front tiles.

This topic is closed to new replies.

Advertisement