2D Tile Based World Problem

Started by
17 comments, last by Trienco 11 years, 7 months ago

If you render a triangle and then use glScale to zoom as is suggested by some, OpenGL is still rendering the triangle, even though you can't see it. Or so I'm told.


As far as I know, everything that you can't see on screen is implicitly outside of Clip Space, and therefore not rendered ... But maybe there's something that I don't know about.

In either case, here's a complete zoom example written in C, using SDL for basic OpenGL context and input handling:

[source lang="cpp"]/*
* zoomdemo.c
*
* Zooming example by Goran Milovanovic, for Lance42.
*
* Arrow keys move the little square around.
* W key zooms in; S key zooms out.
* ESC to quit (or just close the window).
*
* Compile command used: gcc -Wall -o zoomdemo -lGL -lSDL zoomdemo.c
*
*/

#include "SDL/SDL.h"
#include "GL/gl.h"

SDL_Surface* createDisplay(short width, short height){

SDL_Init(SDL_INIT_EVERYTHING);

SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

SDL_Surface* screen = SDL_SetVideoMode(width, height, 32, SDL_OPENGL);

return screen;

}

void processEvents(char* keyboard){

static SDL_Event event;

while ( SDL_PollEvent(&event) ){

switch (event.type){

case SDL_KEYDOWN:
keyboard[ event.key.keysym.sym ] = 1;
break;

case SDL_KEYUP:
keyboard[ event.key.keysym.sym ] = 0;
break;

case SDL_QUIT:
exit(0);
}
}
}


void glInit(SDL_Surface* screen){

// Set clear color (black)
glClearColor( 0, 0, 0, 1 );

// Set projection matrix
glMatrixMode( GL_PROJECTION );
glLoadIdentity();

float aspect_ratio = (float)screen->w / screen->h;
float X = 10;
float Y = X / aspect_ratio;

glOrtho( -X, X, -Y, Y, 1.0, -1.0 );

// Set modelview matrix
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();


}

void glZoom(float scale){

glMatrixMode(GL_PROJECTION);
glScalef(1 + scale, 1 + scale, 1);
}


typedef struct Vec2{
float x;
float y;
} Vec2;

typedef struct Square{
Vec2 v_coords[4];
Vec2 pos;
} Square;

void squareInit(Square* square){

Vec2* v = square->v_coords;

v[0].x = -1; v[0].y = 1; // 0-----1
v[1].x = 1; v[1].y = 1; // | |
v[2].x = 1; v[2].y = -1; // | |
v[3].x = -1; v[3].y = -1; // 3-----2

square->pos.x = 0;
square->pos.y = 0;

}

void squareDraw(Square* square){

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(square->pos.x, square->pos.y, 0);

glEnableClientState(GL_VERTEX_ARRAY);

glVertexPointer(2, GL_FLOAT, 0, square->v_coords);

glDrawArrays(GL_QUADS, 0, 4);

glDisableClientState(GL_VERTEX_ARRAY);
}



int main(int argc, char* argv[]){

SDL_Surface* screen = createDisplay(640, 480);

glInit(screen);

char keyboard[322];
memset(keyboard, 0, sizeof(keyboard));


Square square;
squareInit(&square);


while (1){

processEvents(keyboard);

if (keyboard[SDLK_ESCAPE])
break;

glZoom((keyboard[SDLK_w] - keyboard[SDLK_s]) * 0.1f); // Incremental zoom


// Move the square
square.pos.x += (keyboard[SDLK_RIGHT] - keyboard[SDLK_LEFT]) * 0.25;
square.pos.y += (keyboard[SDLK_UP] - keyboard[SDLK_DOWN]) * 0.25;


glClear(GL_COLOR_BUFFER_BIT); // Clear the color buffer, so we can draw to a clean slate.

squareDraw(&square); // Draw whatever you want to draw.

SDL_GL_SwapBuffers(); // Update the screen with the newly rendered image.


SDL_Delay(16); // Close enough to 60 fps.

}

return 0;
}
[/source]

I'm doing it the "right way" as Trienco rightly pointed out: My glOrtho call creates a symmetric projection, and the zooming is done by simply scaling the projection matrix.

If you have any questions, feel free to ask.

+---------------------------------------------------------------------+

| Game Dev video tutorials -> http://www.youtube.com/goranmilovano | +---------------------------------------------------------------------+
Advertisement
Thank you for the example. I will definitely use it to check my work and make sure I'm doing it correctly!

Lance...

I'm doing it the "right way" as Trienco rightly pointed out: My glOrtho call creates a symmetric projection, and the zooming is done by simply scaling the projection matrix.


Actually no, because I pointed out that zooming doesn't belong in the modelview matrix at all. Scaling to zoom can make objects close to the camera suddenly disappear if they "grow" beyond the near clipping plane.

If you want to zoom the way an actual camera does it, you simply use this and stay away from the modelview matrix:

glOrtho( -X/scaleFactor, X/scaleFactor, -Y/scaleFactor, Y/scaleFactor, 1.0, -1.0 );

This will make sure that zooming while standing right in front of a wall doesn't turn into a wall hack.


As for rendering invisible geometry. OpenGL will render everything you throw at it, because it can't tell if something is visible without first transforming all vertices and performing clipping. That only saves you half the work (probably more, as rasterizing and shading are usually the expensive parts).

If you don't want something drawn, don't send it to OpenGL. Culling should be pretty simple when dealing with 2D tiles, even with zooming (it's just a factor after all).
f@dzhttp://festini.device-zero.de

Actually no, because I pointed out that zooming doesn't belong in the modelview matrix at all. Scaling to zoom can make objects close to the camera suddenly disappear if they "grow" beyond the near clipping plane.


Did you read my code?

I'm scaling the projection matrix, not the modelview matrix.

+---------------------------------------------------------------------+

| Game Dev video tutorials -> http://www.youtube.com/goranmilovano | +---------------------------------------------------------------------+

As for rendering invisible geometry. OpenGL will render everything you throw at it, because it can't tell if something is visible without first transforming all vertices and performing clipping. That only saves you half the work (probably more, as rasterizing and shading are usually the expensive parts).

If you don't want something drawn, don't send it to OpenGL. Culling should be pretty simple when dealing with 2D tiles, even with zooming (it's just a factor after all).


I was hoping that OpenGL had optimized ways of figuring out what not to bother rendering, but if I have to figure that out myself I can. Thanks. =D
What I ended up doing which seems to work:


public void UpdateCamera(Vector controlInput, double elapsedTime)
{
double scaleOffset = (_scale - 1) * -96;
worldOffset += (controlInput * elapsedTime * 256);
Gl.glMatrixMode(Gl.GL_PROJECTION);
Gl.glLoadIdentity();
Gl.glOrtho(-640 + worldOffset.X + scaleOffset, 640 + worldOffset.X - scaleOffset, -360 + worldOffset.Y + scaleOffset, 360 + worldOffset.Y - scaleOffset, -100, 100);
Gl.glMatrixMode(Gl.GL_MODELVIEW);
Gl.glLoadIdentity();/* */
}


This seems to work well. The worldOffset is a vector containing the an X and Y offset putting the camera over the proper place in the world. The scaleOffset is what does the scaling magic. The screen resolution is hard coded for testing purposes, and the hard coded 96 and 256 numbers come from the texture sizes and player movement speed, respectively. Have I done this completely wonky?

Did you read my code?

I'm scaling the projection matrix, not the modelview matrix.


My bad, I was just quickly scrolling back up and saw your first post, where it was the model view.


Have I done this completely wonky?
[/quote]

Well, I don't think adding a scale offset instead of multiplying with a scale factor makes much sense, especially since I would expect that to screw up your aspect ratio.

Let me dig out some old code..

void zoomBy( float factor )
{
scroll += ( factor < 1.f ? mousePos : Vector3(worldWindow.w/2, worldWindow.h/2,0) ) * invZoom*(1.f-factor);
invZoom *= factor;
}

void zoomIn() { zoomBy(1.25f); }
void zoomOut() { zoomBy(.8f); }

//When setting up the frame for rendering...

glViewport(worldWindow.x, worldWindow.y + (screenHeight-worldWindow.h), worldWindow.w, worldWindow.h);

glOrtho(scroll.x, scroll.x + (worldWindow.w*invZoom), scroll.y + (worldWindow.h*invZoom), scroll.y, -1000000, 1000000);


This is probably confusing more than helping. It will always zoom towards the location of the mouse pointer, but when zooming out it uses the center of the view. I find it a lot more convenient than zooming towards the center of the screen and it even lets you quickly zip all over the map without actually having to scroll, just by zooming in and out.

It's also (ab)using the projection to scroll, though one could argue that the "camera position" should be in the modelview, not the projection. The reason is simply so I won't have to consider the current zoom factor when scrolling (you will want to scroll faster when zoomed out and slower when zoomed in).

Note that zooming back out would be easier if the projection was centered around the origin, so I wouldn't have to correct the scroll offset. But since I need a function that will zoom towards/from any arbitrary point anyway, it wouldn't really make much difference and I find it very tedious to think of the screen as going from -width/2 to +width/2 instead of 0 to width.

In this case the "worldWindow" is the area where the world is drawn. I set the viewport to leave out the area at the bottom where the GUI stuff will go. Unless you want to limit the viewport, you can pretend that worldWindow.x/worldWindow.y are 0 and worldWindow.w/worldWindow.h are your screen or window size. So basically looking at my old code it's not even required to have your view centered around the origin.
f@dzhttp://festini.device-zero.de
@Lance42

For your purposes, where you just want to zoom in and out (as a camera typically would), my example represents a simpler, and arguably more elegant solution.

You should really take the time to understand it. The key thing to note: I'm not making multiple calls to glOrtho. I just call it once to initialize the matrix, and the zoom is simply a scaling operation implemented via glScalef.

Ultimately, it's your decision, but I would prefer the zoom function that is both easier to implement, and easier to understand.

@Trienco

Well, from what I understand, he's not trying to shift the zoom point.

However, if he wanted to do that, I would recommend doing it like this:


// Pseudo code

delta = mouse.worldPosition - camera.position
camera.position = mouse.worldPosition
camera.zoom(scale)
delta *= scale
camera.position -= delta


I think that would be cleaner (no need to "dirty" the projection matrix with modelview data).

+---------------------------------------------------------------------+

| Game Dev video tutorials -> http://www.youtube.com/goranmilovano | +---------------------------------------------------------------------+
Well, looking back I can now say that decision was made purely for the sake of simplicity and keeping things intuitive. This way the entire "view setup" is in the ortho projection by simply specifying the chunk of the world you want to see (in convenient world coordinates).

Doing it that way is more straight forward than creating a projection at a fixed location and then "shifting the world into place" and when dealing with numbers, I think the easier you can picture it, the better. Of course it doesn't transfer well to perspective projection, so if there are any plans to switch, it's better put in the modelview from the start.

In fact, the modelview isn't used at all, since for a simple 2D tile engine, immediate mode is more than fast enough, even when zooming out a lot. Trying to group tiles into chunks just to use vertex buffers would have been a waste of time, making things more complicated without any real benefit.
f@dzhttp://festini.device-zero.de

This topic is closed to new replies.

Advertisement