Opengles2 (Racing) Game Development


I'm personally very interested in your progress here Whynodd, so I hope you'll continue to share details - and maybe sources - in your quest to nail the HelloGLES(1,2)World ..
 
torpor said:
I'm personally very interested in your progress here Whynodd, so I hope you'll continue to share details - and maybe sources - in your quest to nail the HelloGLES(1,2)World ..

A small experiment, its easy to pass data (here the varying) from shader to shader:
2yxezo4.jpg

Code:
const char* pszFragShader = "\
  precision mediump float;\
  varying vec4 coordinates;\
  void main (void)\
    {\
      float dis;\
      dis = distance(coordinates,vec4(0.0,0.0,0.0,1.0));\
      dis = (sin(20.0*dis)+1.0)/2.0;\
      gl_FragColor = vec4(1.0-dis,1.0-dis, 1.0-dis, 1.0);\
    }";

const char* pszVertShader = "\
  attribute   vec4    vPosition;\
  varying vec4 coordinates;\
  void main(void)\
  {\
    coordinates = vPosition;\
    gl_Position = vPosition;\
  }";

Right now, I'm porting my game "engine" project (trial racing game) that i have started a long time ago. Today I learned a bit about Makefiles and manually wrote my first Makefiles to compile the project on the Pandora. Kept the project on ice until my Pandora arrived. More info about that (and some pictures) is here: German forum
Its a long term project so don't expect playable results in 3 days :p . At the moment, its not much more than an empty hull, it can load obj-files and display them on my desktop. Most of it is a collection of stubs. I thought it would be better to design all that as an UML model first to generate stubs and headers. Since I got my Pandora some time ago, i'm ready to integrate the ES2-thingies that I find out with my Hello Triangle testbed.
Soon I'll show how my engine loads, displays, lights and textures some obj-models on the Pandora. Sure, it works already on my desktop but that's immediate mode opengl and SDL. I dont want to use SDL on the Pandora, so I have to do the event handling.

By the way, this is my workflow:
- Samba share from Pandora as net drive on my win-desktop
- Extend utils + devextend (nand overlay with devtools) on Pandora
- File editing on my desktop in the samba net drive
- remote compiling on Pandora per ssh. 3 seconds to compile and link after small edits. So: Go for Bollocks style!
Man, it took a lot of steps to get all this working. For example, the Pandora has to constantly ping my wlan-router to keep the connection alive and responsive. At least this problem is sorted out after the next firmware patch.
 
Last edited by a moderator:
Very nice! I get a bit dizzy looking at that screenshot, but I'm really looking forward to watching the progress on your project!
 
Progress with my game project!!

Today I learned how to do a little threading in c++ because the kernel interface (In the wiki) for the game buttons blocks the process when nothing happens. In short: There were many pitfalls (tried fork() first, now I use pthreads). Now I have a class that handles input in a sperate infinite thread and a gameloop running nicely side by side.

For anyone who is interested in some details, this is in short how I did it:

My gameloop:
Code:
int Game::start()
{
  //initialisation stuff//
  ...
  KeyInput keyInput; // instance of a class that cares about input
  pthread_t keyInputThread;
  pthread_create( &keyInputThread, NULL, processInputThread, &keyInput); // after this, the key handler is infinitely running
  while (gamestate.getrunning())
  {
    // do a timestep here 
    scene.draw(); // and now draw the scene
    if (keyInput.getkeyQuit())
    {
      gamestate.setrunning(false);
    }
  }
  return 1;
}

And an indirect static function as a caller for the key handler. pthread_create can not call class members directly when they are not static.
Code:
void* Game::processInputThread(void* obj)
{
	KeyInput* myObj = reinterpret_cast<KeyInput*>(obj);
	myObj->processInput(); // processInput is finally the infinite loop that handles input
	return 0;
}

Btw: Am I the first one who makes a Pandora original game for openGLES2 (without one of the open source game engines) with such low level methods?
 
Mind if I write another status report on my game project? Ok, here it is:

- now shows an empty window, everything is prepared for graphics (yay!)
- My own button event system in a seperate thread works great. Its nice to have a system that allows me to ask the state of the game buttons wherever I want in my code.
- Doublebuffer swap (from the Hello Triangle) implemented

Next todos are:
- Writing something like a shader/material library system, gluing it together with my obj-loader
- Re-invent the matrix stuff from "normal" OpenGL like glTranslate, glPushMatrix etc.. ooouuuchh fun.
- Let a shader do the matrix stuff and calculate a simple directional lighting. When its fast, I'll do per pixel lighting and not per vertex.
- display a bunch of 3D models
- flying around with a camera
(obj-loader and a flying camera is already running "in the dark", just can't see it because the program draws nothing)

Tadaa, more interesting code to play with:

Finding the device file for the game buttons (abxy, start, select and so on). KeyInput is the class that I can ask from anywhere in the code about the status of buttons.
Code:
int KeyInput::findGPIOkeys()
{
	char name[100];
	GPIOkeyHandle=0; // its an int
	for (int i = 0; i<10; i++)
	{
		sprintf(name, "/dev/input/event%i", i);
		GPIOkeyHandle = open(name, O_RDONLY);
		if (GPIOkeyHandle < 0) break; /* no more devices */
		ioctl(GPIOkeyHandle, EVIOCGNAME(sizeof(name)), name);
		if (strcmp(name, "gpio-keys") == 0)
		{
			return 1; // found the buttons!
		}
		close(GPIOkeyHandle); /* we don't need this device */
	}
	GPIOkeyHandle=0;
	return 0; // did not find the buttons
}

The infinite button event munching loop. I hate case cascades. One time key down events are not yet implemented. This is the loop that runs in a seperate thread.
Code:
void KeyInput::processInput()
{
	#ifdef PANDORA
	while(1) // immer weiter schön die Tasten einlesen
	{

		struct input_event ev[64];
		int rd, ret;
		int fd = -1;
		fd_set fdset;

		// Der kommende Block sucht sich das input device aus, das etwas zu sagen hat
		FD_ZERO(&fdset);
		if (GPIOkeyHandle!= -1)
		{
			FD_SET(GPIOkeyHandle, &fdset);
		}
		ret = select(GPIOkeyHandle+1, &fdset, NULL, NULL, NULL);
		if (ret == -1)
		{
			printf("Error at select\n");
			return; // hier vielleicht mit fehler returnen und im aufrufenden Game behandeln
		}
		if (GPIOkeyHandle!= -1 && FD_ISSET(GPIOkeyHandle, &fdset))
		{
			fd = GPIOkeyHandle;
		}


		rd = read(fd, ev, sizeof(ev)); // blocks here if no buttons are pressed
		if (rd < (int) sizeof(ev[0]))
		{
			printf("Error reading GPIO-keys event\n");
			return;
		}

		// Jetzt die gelesenen Events verarbeiten
		for (int i = 0; i < rd / sizeof(ev[0]); i++)
		{
			switch (ev[i].type)
			{
				case EV_SYN: //Wird wohl bei jedem Tastendruck ausgelöst. K.a. was das ist.
				break;
				case EV_KEY:
					//set_key(ev[i].code, ev[i].value); // Überbleibsel aus op_test_inputs.c, http://pandorawiki.org/Kernel_interface
					switch (ev[i].code)
					{
						case KEY_RIGHTSHIFT: // L
						break;
						case KEY_RIGHTCTRL:  // R
						break;
						case KEY_LEFTALT:    // Start
						  this->keyPause=ev[i].value;
						break;
						case KEY_LEFTCTRL:   // Select
							this->keyQuit=ev[i].value;
						break;
						case KEY_MENU:       // Menu
						break;
						case KEY_LEFT:       // Steuerkreuz
							this->keyLeft=ev[i].value;
						break;
						case KEY_RIGHT:
							this->keyRight=ev[i].value;
						break;
						case KEY_UP:
							this->keyUp=ev[i].value;
						break;
						case KEY_DOWN:
							this->keyDown=ev[i].value;
						break;
						case KEY_PAGEUP:     // Y
						break;
						case KEY_PAGEDOWN:   // X
						break;
						case KEY_HOME:       // A
						break;
						case KEY_END:        // B
						break;
						default:
						break;
					}
				break;
					/* fallthrough */
				default:
					printf("unexpected event: type %i, code %d\n", ev[i].type, ev[i].code);
				break;
			}
		}
	}
	#endif
}
 
Edit: changed something, now 2 seperate classes. Makes more sense. All my classes have access to MatrixStacks. The usage is similar to the normal opengl matrix stacks. Pushing and popping works. Nice.
The UML-Modeltool BOUML (get it here) is REALLY helpful if you know the quirks of it (sometimes bouml refuses to generate an aggregation exactly with the types that I want)
Its fun: I can rewire inheritances, dependencies, compositions and aggregations on the fly, change the structure of my project and then I generate the code out of it. Only the bodies of functions need to be filled in.

Code speaks for itself. I'm reimplementing the opengl matrix stacks. At least I started with it, many methods are missing (push, pop, rotate, translate etc.) - should not be too difficult.

Declaration Matrix4x4:
Code:
class Matrix4x4
{
    public:
    Matrix4x4();

    //belegt die Matrix mit der Identity
    void identity();

    //Setzt projectionMatrix auf eine Projektionsmatrix mit den angegebenen Parametern
    void perspective(float fov, float aspect, float near, float far);

    float matrix[16];


};

Declaration MatrixStacks:
Code:
class MatrixStacks
{
  public:
    int pushProjection();

    int pushModelview();

    int pushTexture();

    int popProjection();

    int popModelview();

    int popTexture();

    //Löscht den Matrixstack und behält die aktuelle Matrix bei
    void clearProjection();

    //Löscht den Matrixstack und behält die aktuelle Matrix bei
    void clearModelview();

    //Löscht den Matrixstack und behält die aktuelle Matrix bei
    void clearTexture();

    static Matrix4x4 projectionMatrix;

    static Matrix4x4 textureMatrix;

    static Matrix4x4 modelviewMatrix;


  private:
    static vector<Matrix4x4> textureStack;

    static vector<Matrix4x4> modelviewStack;

    static vector<Matrix4x4> projectionStack;
};

I think that I am on the right track.
Next: Something threedimensional will appear on my screen, finally.

Btw: Can a mod change the thread title to something similar to "OpenGLES2 (racing)game development, progress and useful code"? - Thanks, Gruso!
 
Transformation matrix stack stuff nearly done. Only need to implement equivalents of glRotate (and glScale).
I have drawn the (2D)triangle (from the helloTriangle) and moved it around the screen with the game controls to test if everything in my game framework is ok. It is - the input has no lag and it is very precise, proves that the seperate input thread works well. Next todos are: Material/Shader library and a proper shader, that make use of my projection and modelview matrices. Whooooo. I want to see how the Pandora deals with complex scenes, multitexturing and lighting.

While coding, I try to encapsulate complex code sections so I don't have to worry about it and can concentrate on the logic. The game framework consists of approx. 50 classes. Half of them are empty stubs and will be filled in later.

No interesting code to post today. If someone is interested in my obj-loader, I could post that when its ready. Just ask.
 
Why don't you use an already implemented and hopefully well-tested library for your transformations?
For instance: http://glm.g-truc.net/
 
Laurent said:
Why don't you use an already implemented and hopefully well-tested library for your transformations?
For instance: http://glm.g-truc.net/
Hmm, code bloat: Hundreds of header files and I only need a small fraction of it. It would take a week for me to understand and integrate such a library but only 3 days to code it by myself. Also I want to compile as fast as possible. This is also the reason why I decide against a physics engine like Bullet. Hard to integrate (at least for me), hard to understand and bloaty. Found a good tutorial that explains physics integration in a nutshell (Physics integration)
I know what you think. "Is this guy mad? He wants to code EVERYTHING?" I think this approach pays off.

Licenses: I don't know much about all those OS-licenses. If I code everything on my own, I don't have to worry about licenses of included libraries.
Eventually in 1 year or 2, I may want to earn some money with that engine.
 
Last edited by a moderator:
Oh I won't blame you for your approach, I'd do the same :) I just wanted to point you to something that might ease your dev.

BTW the MIT license is one of the least restrictive license, it doesn't require you to release source code.
 
Today I worked a little on obj and mtl loading. I have prepared a Material class that will manage shaders, colors etc. It goes all together well.

I'm interested in your obj-loader code!
Here it is. If you want to use it, you shurely figure out how it works. This is work in progress but it already loads obj-files (made with blender) and puts the data into memory. I will have to restructure the data blobs after loading because obj-files do not provide the triangles in continuous form, it references to vertices that are defined further up in the file. Forget that, its what glDrawElements is for.
Read about obj-files then you know how they are structured.
One drawback: The obj-format only has one uv coordinate per vertex. I need at least two (for a texture and a lightmap) so that means that I have to modify blenders obj export skript to include all uv sets and all texture filenames. Again: FUUUn.

Code:
...
vector<Vector3> vertices;
vector<Vector3> normals;
vector<Vector2> texcoords;
vector<UintVector3> vertexAssociations;
vector<UintVector3> texcoordAssociations;
vector<UintVector3> normalAssociations;
vector<string> materialAssociations;
...

bool GraphicObject::loadOBJ(string filename)
{
  ifstream file;
  file.open(filename.c_str(), ios_base::in);
  if (file.is_open())
  {
    vertices.clear(); // alles erstmal löschen
    normals.clear();
    texcoords.clear();
    vertexAssociations.clear();
    normalAssociations.clear();
    texcoordAssociations.clear();

	string element;
	string currentMaterial="none";
	while (!file.eof())
	{
		file >> element;
		if (element=="v") //Vertexkoordinaten
		{
			double x,y,z;
			file >> x;
			file >> y;
			file >> z;
			//std::cout << "Vertices: " << x << " " << y << " " << z << std::endl;
			vertices.push_back(Vector3(x,y,z));
		}
		if (element=="vn") //Normalen
		{
			double x,y,z;
			file >> x;
			file >> y;
			file >> z;
			//std::cout << "Normalen: " << x << " " << y << " " << z << std::endl;
			normals.push_back(Vector3(x,y,z));
		}
		if (element=="vt") //Texturkoordinaten
		{
			double x,y;
			file >> x;
			file >> y;
			//std::cout << "Texturkoordinaten: " << x << " " << y << std::endl;
			texcoords.push_back(Vector2(x,y));
		}
		if (element=="usemtl")
		{
                        file >> currentMaterial;
                        tracer.systemInfo("GraphicObject","loadOBJ","Material jetzt: "+ currentMaterial);
                }
		if (element=="f") //Zuordunugen zu den Koordinaten und Normalen
		{
			unsigned int vertexIndex[3];
			unsigned int texcoordIndex[3];
			unsigned int normalIndex[3];
			for (int i=0; i<3; i++) // alle 3 Parts ablaufen (TRIangle)
			{
				file >> element;
				vector<string> parts;
				stringSplit(element,"/",parts);
				vertexIndex[i]=atoi(parts[0].c_str());
				texcoordIndex[i]=atoi(parts[1].c_str());
				normalIndex[i]=atoi(parts[2].c_str());
			}
			vertexAssociations.push_back(UintVector3(vertexIndex[0]-1,vertexIndex[1]-1,vertexIndex[2]-1));
			texcoordAssociations.push_back(UintVector3(texcoordIndex[0]-1,texcoordIndex[1]-1,texcoordIndex[2]-1));
			normalAssociations.push_back(UintVector3(normalIndex[0]-1,normalIndex[1]-1,normalIndex[2]-1));
			materialAssociations.push_back(currentMaterial);
		}
        }
    file.close();
    return 1;
  }
  else
  {
  	tracer.error("GraphicObject","loadOBJ","OBJ " + filename + " konnte nicht geladen werden");
	return 0;
  }
}

void GraphicObject::stringSplit(string str, string delim, vector<string> & results)
{
	int cutAt;
	while( (cutAt = str.find_first_of(delim)) != str.npos )
	{
		if(cutAt > 0)
		{
			results.push_back(str.substr(0,cutAt));
		}
		else
		{
			results.push_back("0");
		}
		str = str.substr(cutAt+1);
	}
	if(str.length() > 0)
	{
		results.push_back(str);
	}
}

In normal OpenGL, you could draw that stuff like this - sloooowly ;)
Code:
for (int i=0; i<vertexAssociations.size(); i++)
{
        materials[materialAssociations[i]]->applyMaterial();
          
        glBegin(GL_TRIANGLES);  
        glNormal3f( normals[normalAssociations[i].first].x, normals[normalAssociations[i].first].y, normals[normalAssociations[i].first].z);             
        glVertex3f( vertices[vertexAssociations[i].first].x, vertices[vertexAssociations[i].first].y, vertices[vertexAssociations[i].first].z);
          	
    	glNormal3f( normals[normalAssociations[i].second].x, normals[normalAssociations[i].second].y, normals[normalAssociations[i].second].z);
        glVertex3f( vertices[vertexAssociations[i].second].x, vertices[vertexAssociations[i].second].y, vertices[vertexAssociations[i].second].z);
      			
        glNormal3f( normals[normalAssociations[i].third].x, normals[normalAssociations[i].third].y, normals[normalAssociations[i].third].z); 
      	glVertex3f( vertices[vertexAssociations[i].third].x, vertices[vertexAssociations[i].third].y, vertices[vertexAssociations[i].third].z);  	
    	glEnd();
}

Edit: One } too much.
 
Sweet, good to understand .. thanks for sharing!

(I figure a piece here, a piece there, and eventually we'll have your engine!)

:p
 
Ok, I've implemented modelview rotations, hacked in a basic shader that uses the modelview+projection matrices and displays the naked geometry from the mockup screenshot on page 1 with some colors (calculated somehow from the normal vector so I can distinguish the triangles from their color). Ok, 3D and passing values to shaders works now and I can fly around in the scene that is displayed in false colours.

--- Forget the following, found the error (next post..) ---

BUT: I ran into some problems.
- When I disable fsaa, then the depth buffer behaves differently, kind of inversed or off. Don't know why fsaa influences the depth buffer or z-values. When fsaa is on: Depth test works. Maybe a bug in the driver?

- near and far planes don't behave like intended: Near value seems to change my field of view and near values smaller than 0.4 do not work (nothing is displayed). The far value also doesn't work. I can look VERY far, far farer than far.
I have reimplemented glFrustum exactly like this. glFrustum in my framework (note the column mayor order, that order works perfectly with my modelview matrices):

Code:
void Matrix4x4::frustum(float left, float right, float bottom, float top, float near, float far)
{
  // Vergleiche mit http://wiki.delphigl.com/index.php/glFrustum
	matrix[ 0] = 2.0f*near/(right-left);
	matrix[ 4] = 0.0f;
	matrix[ 8] = 0.0f;
	matrix[12] = 0.0f;

	matrix[ 1] = 0.0f;
	matrix[ 5] = 2.0f*near/(top-bottom);
	matrix[ 9] = 0.0f;
	matrix[13] = 0.0f;

	matrix[ 2] = (right+left)/(right-left);
	matrix[ 6] = (top+bottom)/(top-bottom);
	matrix[10] = -(far+near)/(far-near);
	matrix[14] = -1.0f;

	matrix[3] = 0.0f;
	matrix[7] = 0.0f;
	matrix[11] = -(2.0f*far*near)/(far-near);
	matrix[15] = 0.0f;
}

And the call to it in my camera class:
Code:
float ratio = (float)preferences.resolutionX / (float)preferences.resolutionY;
float leftRight = 2* tan(preferences.viewingAngle/2) * preferences.zNear;
float topBottom = leftRight/ratio;
matrixStacks.projectionMatrix.identity();
matrixStacks.projectionMatrix.frustum(-leftRight, leftRight, -topBottom, topBottom, (float)preferences.zNear, (float)preferences.zFar);

In the shader:
Code:
gl_Position = projectionMatrix * modelviewMatrix * vPosition;\
both matrices are 4x4

I browsed through the documentation in the Khronos SDK. They pass only one matrix, a premultiplied modelviewProjection matrix, to the vertex shader in their examples. I wonder if there is a fundamental difference between their matrix multiplication in cpu and my matrix multiplication on the graphics hardware in a shader. I think not. Is there a hidden fnord-alike command, that changes how the hardware handles depth buffer and clipping? Sure, there is something wrong, but I have no clue where to look.

Any ideas?
 
RAAAAH, sometimes problems just vanish, if you WRITE about them.
Found the error. My frustum function was wrong. Its funny that I had something 3D on the screen instead of garbage. This one is correct:

Code:
void Matrix4x4::frustum(float left, float right, float bottom, float top, float near, float far)
{
  // Vergleiche mit http://wiki.delphigl.com/index.php/glFrustum
	matrix[ 0] = 2.0f*near/(right-left);
	matrix[ 4] = 0.0f;
	matrix[ 8] = (right+left)/(right-left);
	matrix[12] = 0.0f;

	matrix[ 1] = 0.0f;
	matrix[ 5] = 2.0f*near/(top-bottom);
	matrix[ 9] = (top+bottom)/(top-bottom);
	matrix[13] = 0.0f;

	matrix[ 2] = 0.0;
	matrix[ 6] = 0.0;
	matrix[10] = -(far+near)/(far-near);
	matrix[14] = -(2.0f*far*near)/(far-near);

	matrix[3] = 0.0f;
	matrix[7] = 0.0f;
	matrix[11] = -1.0;
	matrix[15] = 0.0f;
}

Tomorrow: Screenshots. I'm amazed at how many triangles the Pandora can handle.

Edit: Now for something extreme: I've loaded the sponza atrium model (this without textures and lighting) on the pandora (41000 faces, approx 80000 tris), 4x fsaa, 640x480. 3-4 frames/sec. And my framework is far from optimized...
 
Sponza Atrium:
21mt175.png

Note that the columns and curvatures are blocky. I could smooth them in blender but I was too tired yesterday. My framework feeds the normals per vertex to the hardware, so smooth surfaces are no problem.
I've thrown several obj files that I exported from blender. Load a blender scene, mark all objects, export as obj, set some export parameters, and load it in the framework, thats all. Tried some simple and some complex scenes. All of them worked.
As I said, no lighting and textures yet, that is my task for today. The colors are interpreted normals in the screenshot.

I'd say that I make progress.
 
Back
Top