Сейчас на обоих моих платформах (Android и PC) кубик делает 1 оборот примерно за 4 секунды. Это 360 кадров (скорость вращения установлена на 1 градус за кадр). Это значит 360/4=90 кадров в секунду (frames per second, FPS). Понятно, что на других устройствах эта цифра может быть другой. Кроме того, на скорость могут влиять фоновые процессы и другие факторы. В основном, сколько и чего мы рендрим, насколько плотно занят наш экран.
Синхронизация нужна чтобы держать FPS скорость постоянной и предсказуемой. Выше FPS – мягче анимация. Ниже FPS – больше времени на более сложный рендринг.
“Золотая середина” – 30 FPS. На всякий случай, 24 (как вполне комфортное) было кино-стандартом целый век.
1000/30 дает нам 33 миллисекунды на рендринг кадра. Не так чтоб много (для сложного кадра), но мы поствраемся вписаться.
Имплементация:
1. Запускаем VS, открываем C:\CPP\a997modeler\p_windows\p_windows.sln.
В TheGame.cpp, когда очередной кадр готов, перед тем как выдать его на экран, мы будем проверять системное время и ждать пока не пройдет 33 миллисекунды от предыдущего кадра.
Для этого нам понадобится
системное время в миллисекундах.
MS последовательно совершенствует свою способность делать простые вещи сложными. Библиотека chrono – очередное достижение, поэтому современное решение будет выглядеть так:
auto currentTime = std::chrono::system_clock::now().time_since_epoch();
return std::chrono::duration_cast<std::chrono::milliseconds>(currentTime).count();
На стороне TheGame потребуется несколько новых переменных.
2. Заменим TheGame.h код на:
#pragma once
#include <vector>
#include "GameSubj.h"
#include "Camera.h"
class TheGame
{
public:
int screenSize[2];
float screenAspectRatio = 1;
//synchronization
long long int lastFrameMillis = 0;
int targetFPS = 30;
int millisPerFrame = 1000 / targetFPS;
bool bExitGame;
Camera mainCamera;
float dirToMainLight[4] = { 1,1,1,0 };
//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 код на:
#include "TheGame.h"
#include "platform.h"
#include "utils.h"
#include "linmath.h"
#include "Texture.h"
#include "Shader.h"
#include "DrawJob.h"
#include "ModelBuilder.h"
#include "TexCoords.h"
extern std::string filesRoot;
extern float degrees2radians;
std::vector<GameSubj*> TheGame::gameSubjs;
int TheGame::getReady() {
bExitGame = false;
Shader::loadShaders();
glEnable(GL_CULL_FACE);
//=== create box ========================
GameSubj* pGS = new GameSubj();
gameSubjs.push_back(pGS);
pGS->name.assign("box1");
pGS->ownCoords.setPosition(0, 0, 0);
pGS->ownCoords.setDegrees(0, 0, 0);
pGS->ownSpeed.setDegrees(0,3,0);
ModelBuilder* pMB = new ModelBuilder();
pMB->useSubjN(gameSubjs.size() - 1);
//define VirtualShape
VirtualShape vs;
vs.setShapeType("box-tank");
vs.whl[0] = 60;
vs.whl[1] = 160;
vs.whl[2] = 390;
vs.setExt(20);
vs.extD = 0;
vs.extF = 0; //to make front face "flat"
vs.sectionsR = 2;
Material mt;
//define material - flat red
mt.shaderN = Shader::spN_phong_ucolor;
mt.primitiveType = GL_TRIANGLES;
mt.uColor.setRGBA(255, 0, 0,255); //red
pMB->useMaterial(&mt);
pMB->buildBoxFace(pMB,"front v", &vs);
pMB->buildBoxFace(pMB, "back v", &vs);
pMB->buildBoxFace(pMB, "top", &vs);
pMB->buildBoxFace(pMB, "bottom", &vs);
pMB->buildBoxFace(pMB, "left all", &vs);
mt.uColor.clear(); // set to zero;
mt.uTex0 = Texture::loadTexture(filesRoot + "/dt/sample_img.png");
mt.shaderN = Shader::spN_phong_tex;
pMB->useMaterial(&mt);
TexCoords tc;
tc.set(mt.uTex0, 11, 12, 256, 128, "h"); //flip horizontally
pMB->buildBoxFace(pMB, "right all", &vs, &tc);
pMB->buildDrawJobs(gameSubjs);
delete pMB;
//===== set up camera
mainCamera.ownCoords.setDegrees(15, 180, 0); //set camera angles/orientation
mainCamera.viewRangeDg = 30;
mainCamera.stageSize[0] = 500;
mainCamera.stageSize[1] = 375;
memcpy(mainCamera.lookAtPoint, pGS->ownCoords.pos, sizeof(float) * 3);
mainCamera.onScreenResize();
//===== set up light
v3set(dirToMainLight, -1, 1, 1);
vec3_norm(dirToMainLight, dirToMainLight);
return 1;
}
int TheGame::drawFrame() {
myPollEvents();
//glClearColor(0.0, 0.0, 0.5, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
//calculate halfVector
float dirToCamera[4] = { 0,0,-1,0 }; //-z
mat4x4_mul_vec4plus(dirToCamera, *mainCamera.ownCoords.getRotationMatrix(), dirToCamera, 0);
float uHalfVector[4] = { 0,0,0,0 };
for (int i = 0; i < 3; i++)
uHalfVector[i] = (dirToCamera[i] + dirToMainLight[i]) / 2;
vec3_norm(uHalfVector, uHalfVector);
mat4x4 mProjection, mViewProjection, mMVP, mMV4x4;
//mat4x4_ortho(mProjection, -(float)screenSize[0] / 2, (float)screenSize[0] / 2, -(float)screenSize[1] / 2, (float)screenSize[1] / 2, 100.f, 500.f);
float nearClip = mainCamera.focusDistance - 250;
float farClip = mainCamera.focusDistance + 250;
mat4x4_perspective(mProjection, mainCamera.viewRangeDg * degrees2radians, screenAspectRatio, nearClip, farClip);
mat4x4_mul(mViewProjection, mProjection, mainCamera.lookAtMatrix);
//mViewProjection[1][3] = 0; //keystone effect
//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, mViewProjection, pGS->ownModelMatrix);
//build Model-View (rotation) matrix for normals
mat4x4_mul(mMV4x4, mainCamera.lookAtMatrix, (vec4*)pGS->ownCoords.getRotationMatrix());
//convert to 3x3 matrix
float mMV3x3[3][3];
for (int y = 0; y < 3; y++)
for (int x = 0; x < 3; x++)
mMV3x3[y][x] = mMV4x4[y][x];
//render subject
for (int i = 0; i < pGS->djTotalN; i++) {
DrawJob* pDJ = DrawJob::drawJobs.at(pGS->djStartN + i);
pDJ->execute((float*)mMVP, *mMV3x3, dirToMainLight, uHalfVector, NULL);
}
}
//synchronization
while (1) {
long long int currentMillis = getSystemMillis();
long long int millisSinceLastFrame = currentMillis - lastFrameMillis;
if (millisSinceLastFrame >= millisPerFrame) {
lastFrameMillis = currentMillis;
break;
}
}
mySwapBuffers();
return 1;
}
int TheGame::cleanUp() {
int itemsN = gameSubjs.size();
//delete all UISubjs
for (int i = 0; i < itemsN; i++) {
GameSubj* pGS = gameSubjs.at(i);
delete pGS;
}
gameSubjs.clear();
//clear all other classes
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;
screenAspectRatio = (float)width / height;
glViewport(0, 0, width, height);
mainCamera.onScreenResize();
mylog(" screen size %d x %d\n", width, height);
return 1;
}
int TheGame::run() {
getReady();
while (!bExitGame) {
drawFrame();
}
cleanUp();
return 1;
}
- Поскольку FPS теперь в 3 раза ниже (30 вместо 90 ранее), можно увеличить скорость вращения кубика (строка 28).
Функцию getSystemMillis() мы расположим в наборе utils.
4. Заменим utils.h код на:
#pragma once
#include <string>
#include <vector>
#include "linmath.h"
int checkGLerrors(std::string ref);
void mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3);
void v3set(float* vOut, float x, float y, float z);
void v3copy(float* vOut, float* vIn);
float v3pitchRd(float* vIn);
float v3yawRd(float* vIn);
float v3pitchDg(float* vIn);
float v3yawDg(float* vIn);
long long int getSystemMillis();
long long int getSystemNanos();
int getRandom(int fromN, int toN);
float getRandom(float fromN, float toN);
std::vector<std::string> splitString(std::string inString, std::string delimiter);
std::string trimString(std::string inString);
bool fileExists(const char* filePath);
std::string getFullPath(std::string filePath);
std::string getInAppPath(std::string filePath);
int makeDirs(std::string filePath);
- Пользуясь поводом, я добавип еще несколько полезных функций, так что getSystemMillis() – не единственное изменение.
5. Заменим utils.cpp код на:
#include "utils.h"
#include "platform.h"
#include <chrono>
#include <stdlib.h> /* srand, rand */
#include <sys/stat.h> //if fileExists
#include <time.h> //for srand()
extern std::string filesRoot;
extern float radians2degrees;
int checkGLerrors(std::string ref) {
//can be used after any GL call
int res = glGetError();
if (res == 0)
return 0;
std::string errCode;
switch (res) {
//case GL_NO_ERROR: errCode = "GL_NO_ERROR"; break;
case GL_INVALID_ENUM: errCode = "GL_INVALID_ENUM"; break;
case GL_INVALID_VALUE: errCode = "GL_INVALID_VALUE"; break;
case GL_INVALID_OPERATION: errCode = "GL_INVALID_OPERATION"; break;
case GL_INVALID_FRAMEBUFFER_OPERATION: errCode = "GL_INVALID_FRAMEBUFFER_OPERATION"; break;
case GL_OUT_OF_MEMORY: errCode = "GL_OUT_OF_MEMORY"; break;
default: errCode = "??"; break;
}
mylog("GL ERROR %d-%s in %s\n", res, errCode.c_str(), ref.c_str());
return -1;
}
void mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3) {
vec4 v2;
if (vOut == vIn) {
memcpy(&v2, vIn, sizeof(vec4));
vIn = v2;
}
vIn[3] = (float)v3;
mat4x4_mul_vec4(vOut, M, vIn);
}
void v3set(float* vOut, float x, float y, float z) {
vOut[0] = x;
vOut[1] = y;
vOut[2] = z;
}
void v3copy(float* vOut, float* vIn) {
for (int i = 0; i < 3; i++)
vOut[i] = vIn[i];
}
float v3yawRd(float* vIn) {
return atan2f(vIn[0], vIn[2]);
}
float v3pitchRd(float* vIn) {
return -atan2f(vIn[1], sqrtf(vIn[0] * vIn[0] + vIn[2] * vIn[2]));
}
float v3pitchDg(float* vIn) {
return v3pitchRd(vIn) * radians2degrees;
}
float v3yawDg(float* vIn) {
return v3yawRd(vIn) * radians2degrees;
}
long long int getSystemMillis() {
auto currentTime = std::chrono::system_clock::now().time_since_epoch();
return std::chrono::duration_cast<std::chrono::milliseconds>(currentTime).count();
}
long long int getSystemNanos() {
auto currentTime = std::chrono::system_clock::now().time_since_epoch();
return std::chrono::duration_cast<std::chrono::nanoseconds>(currentTime).count();
}
int randomCallN = 0;
int getRandom() {
if (randomCallN % 1000 == 0)
srand((unsigned int)getSystemNanos()); //re-initialize random seed:
randomCallN++;
return rand();
}
int getRandom(int fromN, int toN) {
int randomN = getRandom();
int range = toN - fromN + 1;
return (fromN + randomN % range);
}
float getRandom(float fromN, float toN) {
int randomN = getRandom();
float range = toN - fromN;
return (fromN + (float)randomN / RAND_MAX * range);
}
std::vector<std::string> splitString(std::string inString, std::string delimiter) {
std::vector<std::string> outStrings;
int delimiterSize = delimiter.size();
outStrings.clear();
while (inString.size() > 0) {
int delimiterPosition = inString.find(delimiter);
if (delimiterPosition == 0) {
inString = inString.substr(delimiterSize, inString.size() - delimiterSize);
continue;
}
if (delimiterPosition == std::string::npos) {
//last element
outStrings.push_back(trimString(inString));
break;
}
std::string outString = inString.substr(0, delimiterPosition);
outStrings.push_back(trimString(outString));
int startAt = delimiterPosition + delimiterSize;
inString = inString.substr(startAt, inString.size() - startAt);
}
return outStrings;
}
std::string trimString(std::string inString) {
//Remove leading and trailing spaces
int startsAt = inString.find_first_not_of(" ");
if (startsAt == std::string::npos)
return "";
int endsAt = inString.find_last_not_of(" ") + 1;
return inString.substr(startsAt, endsAt - startsAt);
}
bool fileExists(const char* filePath) {
struct stat info;
if (stat(filePath, &info) == 0)
return true;
else
return false;
}
std::string getFullPath(std::string filePath) {
if (filePath.find(filesRoot) == 0)
return filePath;
else
return (filesRoot + filePath);
}
std::string getInAppPath(std::string filePath) {
std::string inAppPath(filePath);
if (inAppPath.find(filesRoot) == 0) {
int startsAt = filesRoot.size();
inAppPath = inAppPath.substr(startsAt, inAppPath.size() - startsAt);
}
if (inAppPath.find(".") != std::string::npos) {
//cut off file name
int endsAt = inAppPath.find_last_of("/");
inAppPath = inAppPath.substr(0, endsAt + 1);
}
return inAppPath;
}
int makeDirs(std::string filePath) {
filePath = getFullPath(filePath);
std::string inAppPath = getInAppPath(filePath);
std::vector<std::string> path = splitString(inAppPath, "/");
int pathSize = path.size();
filePath.assign(filesRoot);
for (int i = 0; i < pathSize; i++) {
filePath.append("/" + path.at(i));
if (fileExists(filePath.c_str())) {
continue;
}
//create dir
myMkDir(filePath.c_str());
mylog("Folder %d: %s created.\n", i, filePath.c_str());
}
return 1;
}
Еще добавлены:
- getRandom() – скоро нам понядобятся случайные числа. В C++ есть функция rand(), которая генерирует псевдо-случайные integer в диапазоне от 0 до RAND_MAX. Для удобства мы завернем ее в пару других функций, возвращающих случайный int или float в заданном диапазоне.
- Также добавлены getFullPath() и getInAppPath() из FileLoader.
- Еще пара функций для работы со строками: splitString(…) – разбить строку и trimString(…)– убрать лишние пробелы.
- И пара – для работы с файлами: проверка на наличие файла fileExists(…) и создание каталогов makeDirs(…).
6. Компиляция и запуск.
Скорость вращения выглядит так же. Значит, поставленная цель достигнута.
На Андроиде тоже проверено, анимация такая же гладкая, значит 30 FPS – правильный выбор.