Chapter 14. 3D Coordinates and Game Subjects

3D subject will require 3D coordinates including orientation angles. We will need Euler angles (yaw, pitch, and roll in aviation terminology or heading, attitude and bank in naval). We'll start with Euler angles in degrees and in radians and a rotation Matrix. We'll call this class Coords.

Windows

1. Start VS. Open C:\CPP\a998engine\p_windows\p_windows.sln.


2. Under xEngine add new header file Coords.h

Location: C:\CPP\engine

Code:

#pragma once
#include "linmath.h"

class Coords
{
private:
	float eulerDg[3] = { 0,0,0 }; //Euler angles (yaw, pitch, and roll) in degrees
	float eulerRd[3] = { 0,0,0 }; //Euler angles in radians
	mat4x4 rotationMatrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
	//mat4x4 rotationMatrix = { {1,0,0,0}, {0,1,0,0}, {0,0,1,0}, {0,0,0,1} };
public:
	float pos[4] = { 0,0,0,0 }; //x,y,z position + 4-th element for compatibility with 3D 4x4 matrices math
public:
	void setDegrees(float ax, float ay, float az) { setDegrees(this, ax, ay, az); };
	static void setDegrees(Coords* pC, float ax, float ay, float az);
	float getRd(int i) { return eulerRd[i]; }; //get angle in radians
	//float getDg(int i) { return eulerDg[i]; }; //get angle in degrees
	void setPosition(float kx, float ky, float kz) { setPosition(this, kx, ky, kz); };
	static void setPosition(Coords* pC, float kx, float ky, float kz);
	mat4x4* getRotationMatrix() { return &rotationMatrix; };
	void setRotationMatrix(mat4x4 m) { setRotationMatrix(this, m); };
	static void setRotationMatrix(Coords* pC, mat4x4 m);
	static void eulerRdToMatrix(mat4x4 rotationMatrix, float* eulerRd);
	static void matrixToEulerRd(float* eulerRd, mat4x4 m);
};


3. Under xEngine add new C++ file Coords.cpp

Location: C:\CPP\engine

Code:

#include "Coords.h"
#include "platform.h"
#include <string>

float PI = 3.141592f;
float degrees2radians = PI / 180.0f;
float radians2degrees = 180.0f / PI;

void Coords::setDegrees(Coords* pC, float ax, float ay, float az) {
	if (pC->eulerDg[0] == ax && pC->eulerDg[1] == ay && pC->eulerDg[2] == az)
		return;
	pC->eulerDg[0] = ax;
	pC->eulerDg[1] = ay;
	pC->eulerDg[2] = az;
	//convert to radians
	pC->eulerRd[0] = pC->eulerDg[0] * degrees2radians;
	pC->eulerRd[1] = pC->eulerDg[1] * degrees2radians;
	pC->eulerRd[2] = pC->eulerDg[2] * degrees2radians;
	//re-build rotation matrix
	eulerRdToMatrix(pC->rotationMatrix, pC->eulerRd);
}
void Coords::eulerRdToMatrix(mat4x4 rotationMatrix, float* eulerRd){
	//builds rotation matrix from Euler angles (in radians)
	mat4x4_identity(rotationMatrix);
	//rotation order: Z-X-Y
	float a = eulerRd[1];
	if (a != 0)
		mat4x4_rotate_Y(rotationMatrix, rotationMatrix, a);
	a = eulerRd[0];
	if (a != 0)
		mat4x4_rotate_X(rotationMatrix, rotationMatrix, a);
	a = eulerRd[2];
	if (a != 0)
		mat4x4_rotate_Z(rotationMatrix, rotationMatrix, a);
}
void Coords::setPosition(Coords* pC, float kx, float ky, float kz) {
	pC->pos[0] = kx;
	pC->pos[1] = ky;
	pC->pos[2] = kz;
}
void Coords::setRotationMatrix(Coords* pC, mat4x4 m) {
	memcpy(pC->rotationMatrix, m, sizeof(pC->rotationMatrix));
	//update Euler angles
	matrixToEulerRd(pC->eulerRd, pC->rotationMatrix);

	pC->eulerDg[0] = pC->eulerRd[0] * radians2degrees;
	pC->eulerDg[1] = pC->eulerRd[1] * radians2degrees;
	pC->eulerDg[2] = pC->eulerRd[2] * radians2degrees;
}
void Coords::matrixToEulerRd(float* eulerRd, mat4x4 m) {
	//calculates Euler angles (in radians) from matrix
	float yaw, pitch, roll;

	if (m[1][2] > 0.998 || m[1][2] < -0.998) { // singularity at south or north pole
		yaw = atan2f(-m[2][0], m[0][0]);
		roll = 0;
	}
	else {
		yaw = atan2f(-m[0][2], m[2][2]);
		roll = atan2f(-m[1][0], m[1][1]);
	}
	pitch = asinf(m[1][2]);

	eulerRd[0] = pitch;
	eulerRd[1] = yaw;
	eulerRd[2] = roll;
}


GameSubj class for beginning will include coordinates, speed (at this point we're mostly interested in rotation speed), modelMatrix for rendering, and a reference to involved DrawJobs (it's 2 variables: djStartN and djTotalN)

4. Under xEngine add new header file GameSubj.h

Location: C:\CPP\engine

Code:

#pragma once
#include "Coords.h"
#include "Material.h"
#include <string>

class GameSubj
{
public:
	std::string name;
	Coords ownCoords;
	Coords ownSpeed;
	float scale[3] = { 1,1,1 };
	int djStartN = 0; //first DJ N in DJs array (DrawJob::drawJobs)
	int djTotalN = 0; //number of DJs
	mat4x4 ownModelMatrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };

	Material* pAltMaterial = NULL;

public:
	virtual ~GameSubj();
	void buildModelMatrix() { buildModelMatrix(this); };
	static void buildModelMatrix(GameSubj* pGS);
	virtual int moveSubj() { return moveSubj(this); };
	static int moveSubj(GameSubj* pGS);
};


5. Under xEngine add new C++ file GameSubj.cpp

Location: C:\CPP\engine

Code:

#include "GameSubj.h"
#include "platform.h"

GameSubj::~GameSubj() {
    if (pAltMaterial != NULL)
        delete pAltMaterial;
}
void GameSubj::buildModelMatrix(GameSubj* pGS) {
    mat4x4_translate(pGS->ownModelMatrix, pGS->ownCoords.pos[0], pGS->ownCoords.pos[1], pGS->ownCoords.pos[2]);
    //rotation order: Z-X-Y
    mat4x4_mul(pGS->ownModelMatrix, pGS->ownModelMatrix, *(pGS->ownCoords.getRotationMatrix()));

    if (pGS->scale[0] != 1 || pGS->scale[1] != 1 || pGS->scale[2] != 1)
        mat4x4_scale_aniso(pGS->ownModelMatrix, pGS->ownModelMatrix, pGS->scale[0], pGS->scale[1], pGS->scale[2]);
}
int GameSubj::moveSubj(GameSubj* pGS) {
    if (pGS->ownSpeed.getRd(0) != 0 || pGS->ownSpeed.getRd(1) != 0 || pGS->ownSpeed.getRd(2) != 0) {
        //apply angle speed
        mat4x4 newRotationMatrix;
        mat4x4_mul(newRotationMatrix, *(pGS->ownCoords.getRotationMatrix()), *(pGS->ownSpeed.getRotationMatrix()));
        pGS->ownCoords.setRotationMatrix(newRotationMatrix);
    }
    return 1;
}

We have 2 functions here so far:

buildModelMatrix() which builds ownModelMatrix for rendering from given coordinates and
moveSubj() which at this point applies rotation speed to model's coordinates (in matrix form) and forces to recalculate model's Euler angles from resulting matrix by calling setRotationMatrix() and matrixToEulerRd() functions.


In TheGame.h we'll need an array (vector) of 3D gameSubjs.

6. So, replace TheGame.h code by:

#pragma once
#include <vector>
#include "GameSubj.h"

class TheGame
{
public:
	int screenSize[2];
	float screenRatio;
	bool bExitGame;

	//static arrays (vectors) of active GameSubjs
	static std::vector<GameSubj*> gameSubjs;
public:
	int run();
	int getReady();
	int drawFrame();
	int cleanUp();
	int onScreenResize(int width, int height);
};


In TheGame.cpp we will use our textured rectangle as a simple 3D model. The command glEnable(GL_CULL_FACE) will instruct OpenGL to render only side, that facing us and to ignore another one. OpenGL considers surface "facing us" if vertices are going in counter-clockwise order.

In the getReady() function we will create a GameSubj, a VBO and corresponding DrawJob.

In the drawFrame() we will scan gameSubjs array (consisting of 1 subject), for each (only 1 at this point) subject we'll call pGS->moveSubj() function which will apply rotation speed to angle coordinates, then it will build MVP matrix and execute involved DrawJobs.

7. Open TheGame.cpp and replace code by:

#include "TheGame.h"
#include "platform.h"
#include "linmath.h"
#include "Texture.h"
#include "Shader.h"
#include "DrawJob.h"

extern std::string filesRoot;

//static array (vector) for loaded gameSubjs
std::vector<GameSubj*> TheGame::gameSubjs;

static const struct
{
    float x, y, z, tu, tv;
} frontVertices[4] =
{
    { -0.5f,  0.5f, 0.f, 0.f, 0.f }, //top-left
    { -0.5f, -0.5f, 0.f, 0.f, 1.f }, //bottom-left
    {  0.5f,  0.5f, 0.f, 1.f, 0.f }, //top-right
    {  0.5f, -0.5f, 0.f, 1.f, 1.f }  //bottom-right
};

int TheGame::getReady() {
    bExitGame = false;
    Shader::loadShaders();
    glEnable(GL_CULL_FACE);

    GameSubj* pGS = new GameSubj();
    gameSubjs.push_back(pGS);
    pGS->djStartN = DrawJob::drawJobs.size();

    pGS->name.assign("img1");
    //pGS->ownCoords.setPosition(-50, 50, 0);
    pGS->ownSpeed.setDegrees(1, 2, 3);
    pGS->scale[0] = 400;
    pGS->scale[1] = pGS->scale[0] / 2;

    //face DrawJob
    //build VBO
    unsigned int VBOid = DrawJob::newBufferId();
    glBindBuffer(GL_ARRAY_BUFFER, VBOid);
    glBufferData(GL_ARRAY_BUFFER, sizeof(frontVertices), frontVertices, GL_STATIC_DRAW);
    int stride = sizeof(float) * 5;
    //add DrawJob
    DrawJob* pDJ = new DrawJob();
    pDJ->pointsN = 4; //number of vertices
    //define material
    pDJ->mt.shaderN = Shader::spN_flat_tex;
    pDJ->mt.primitiveType = GL_TRIANGLE_STRIP;
    pDJ->mt.uTex0 = Texture::loadTexture(filesRoot + "/dt/sample_img.png");
    //attributes references
    AttribRef* pAR;
    pAR = &pDJ->aPos; pAR->offset = 0;                 pAR->glVBOid = VBOid; pAR->stride = stride;
    pAR = &pDJ->aTuv; pAR->offset = sizeof(float) * 3; pAR->glVBOid = VBOid; pAR->stride = stride;
    //create and fill vertex attributes array (VAO)
    pDJ->buildVAO();
    pGS->djTotalN = DrawJob::drawJobs.size() - pGS->djStartN;

    return 1;
}
int TheGame::drawFrame() {
    myPollEvents();

    //glClearColor(0.0, 0.0, 0.5, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    mat4x4 mProjection, mMVP;
    mat4x4_ortho(mProjection, -(float)screenSize[0] / 2, (float)screenSize[0] / 2, -(float)screenSize[1] / 2, (float)screenSize[1] / 2, 200.f, -200.f);

    //scan subjects
    int subjsN = gameSubjs.size();
    for (int subjN = 0; subjN < subjsN; subjN++) {
        GameSubj* pGS = gameSubjs.at(subjN);
        //behavior - apply rotation speed
        pGS->moveSubj();
        //prepare subject for rendering
        pGS->buildModelMatrix(pGS);
        //build MVP matrix for given subject
        mat4x4_mul(mMVP, mProjection, pGS->ownModelMatrix);
        //render subject
        for (int i = 0; i < pGS->djTotalN; i++) {
            DrawJob* pDJ = DrawJob::drawJobs.at(pGS->djStartN + i);
            pDJ->execute((float*)mMVP, NULL);
        }
    }
    mySwapBuffers();
    return 1;
}
int TheGame::cleanUp() {
    Texture::cleanUp();
    Shader::cleanUp();
    DrawJob::cleanUp();
    return 1;
}
int TheGame::onScreenResize(int width, int height) {
    if (screenSize[0] == width && screenSize[1] == height)
        return 0;
    screenSize[0] = width;
    screenSize[1] = height;
    screenRatio = (float)width / (float)height;
    glViewport(0, 0, width, height);
    mylog(" screen size %d x %d\n", width, height);
    return 1;
}
int TheGame::run() {
    getReady();
    while (!bExitGame) {
        drawFrame();
    }
    cleanUp();
    return 1;
}


8. Build and run:

The image is spinning smoothly in all 3 dimensions, so our 3D functions are working properly.


Quaternions
Although they are in vogue lately, I still don't see where they can replace matrices, even in complex 3D calculations involving all 3 axes. Just an alternative way to represent 3D rotation. Or am I missing something? Please correct me if I am wrong.

  • Clarification: Eventually I did find them quite handy and rewrote this class accordingly.


Android

9. Re-start VS. Open C:\CPP\a998engine\p_android\p_android.sln.

Under xEngine add Existing Item,

Go to C:\CPP\engine,

Pick

  • Coords.cpp
  • Coords.h
  • GameSubj.cpp
  • GameSubj.h

Add



10. Switch on, unlock, plug in Android, allow.

Build and run. Cool.


Leave a Reply

Your email address will not be published. Required fields are marked *