Глава 14. 3D координаты и Game Subjects

3D subject-ам понадобятся 3D координаты включая углы ориентации, или углы Эйлера – тангаж, рысканье, вращение (pitch, yaw, roll) в авиационной терминологии или курс, дифферент, крен (heading, attitude, bank) в морской. Мы начнем с углов Эйлера в градусах и в радианах и матрицы вращений (rotation Matrix). Назовем этот класс Coords.

Windows

1. Запускаем VS. Открываем C:\CPP\a998engine\p_windows\p_windows.sln.


2. Под xEngine добавим новый header файл Coords.h

Location: C:\CPP\engine

Код:

#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. Под xEngine добавим новый C++ файл Coords.cpp

Location: C:\CPP\engine

Код:

#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 класс для начала включим 3Д координаты, 3Д скорость (в данный момент нас в основном интересуют угловые скорости), modelMatrix для рендринга, и ссылку на вовлеченные DrawJobs (это 2 переменные: djStartN и djTotalN)

4. Под xEngine добавим новый header файл GameSubj.h

Location: C:\CPP\engine

Код:

#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. Под xEngine добавим новый C++ файл GameSubj.cpp

Location: C:\CPP\engine

Код:

#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;
}

У нас тут пока 2 функции :

  • buildModelMatrix() которая строит ownModelMatrix из 3Д координат для рендринга и
  • moveSubj() которая (на данный момент) просто изменяет 3Д матрицу субъекта в зависимости от заданных угловых скоростей и вызывает пересчет углов Эйлера из полученной матрицы. Это функции setRotationMatrix() и matrixToEulerRd().

Нужно добавить в TheGame.h массив (vector) 3D gameSubjs.

6. Заменим TheGame.h код на:

#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);
};


В качесиве простой 3Д модели вTheGame.cpp мы возьмем наш текстурированный прямоугольник. Команда glEnable(GL_CULL_FACE) проинструктирует OpenGL рендрить только ту сторону, которая обращена к нам лицом, и игнорировать обратную (изнаночную) сторону. OpenGL рассматривает поверхность как “обращенную к нам лицом” если вершины идут против часовой стрелки (counter-clockwise order).

В функции getReady() мы создадим 1GameSubj с двумя VBO и двумя соответствующими DrawJobs.

Функция drawFrame() будет сканировать массив gameSubjs (состоящий из 1 субъекта), для каждого (пока одного) субъекта, вызывать его функцию pGS->moveSubj() которая повернет его координаты на скорости вращения, потом построит MVP матрицу для рендринга и исполнит вовлеченные DrawJobs.

7. Откроем TheGame.cpp и заменим код на:

#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. Компиляция и запуск:

Картинка мягко вращается по всем трем осям, значит наши 3D функции работают правильно.


Кватернионы / Quaternions

Хотя они последнее время и “в тренде”, я так и не вижу в чем они могут заменить матрицы, даже в сложных 3D вычислениях по трем осям. Просто альтернативная форма представления 3D вращений. Или я что-то упустил? Поправьте меня если неправ.


Android

9. Пере-запускаем VS. Открываем C:\CPP\a998engine\p_android\p_android.sln.

Под xEngine добавим Existing Item,

из C:\CPP\engine,

Выбираем

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

Add



10. Включаем, разблокируем, подключаем, разрешаем.

Компиляция и запуск. Норм.


Leave a Reply

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