Глава 24. Синхронизация

Сейчас на обоих моих платформах (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 – правильный выбор.


Leave a Reply

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