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. Включаем, разблокируем, подключаем, разрешаем.
Компиляция и запуск. Норм.