При смене размеров экрана надо переустановить камеру так, чтобы сцена оптимально заполнила новые размеры. Например, в нашем случае желаемый размер сцены, скажем, 500×300 (чтобы кубик вписывался в экран). Угол зрения камеры у нас 30 градусов.
значит, ? / 250 = косинус(15) / синус(15) = котангенс(15)
? = 250 * котангенс(15) = 250 * 3.73 = 923
что несильно отличается от нашей 1000 (строка 69 в TheGame.cpp) и совсем не объясняет почему не вписываемся в экран Андроида.
Во-первых, мы не учитываем разрешение экрана, а во-вторых:
Запустим VS, откроем C:\CPP\a997modeler\p_windows\p_windows.sln, запустим программу (зеленая стрелка), поиграем с размерами окна (от высокого узкого к низкому широкому). Вы увидите (для меня было неожиданностью) что наша 1000 (дистанция камеры) определяет заполнение только по ВЕРТИКАЛИ, не по горизонтали. Кроме того, нам не надо вписывать 500 единиц вертикально, по вертикали достаточно 300, значит
cameraDistanceV (для вертикали) = 150 * котангенс(15)
Для горизонтали же нужно также учесть screen aspect ratio, соотношение ширины экрана к высоте, значит
cameraDistanceH (для горизонтали) = 250 * котангенс(15) / screenAspectRatio
Например, на моем S20 телефоне размер экрана 1080 X 2400, значит
screenAspectRatio = 1080 / 2400 = 0.45
котангенс(15) = 3.73, значит:
cameraDistanceV = 150 * котангенс(15) = 150 * 3.73 = 560
cameraDistanceH = 250 * котангенс(15) / screenAspectRatio = 250 * 3.73 / 0.45 = 2072
Для камеры выбираем бОльшее значение, 2072, что выглядит разумно. С ТАКОГО расстояния – точно впишется.
Теперь, если развернуть телефон горизонтально, размер экрана станет 2400 X 1080. значит
screenAspectRatio = 2400 / 1080 = 2.22, значит:
cameraDistanceV останется той же, 560, в то время как горизонтальная
cameraDistanceH = 250 * котангенс(15) / screenAspectRatio = 250 * 3.73 / 2.22 = 420, так что теперь бОльшим значением будет вертикальное, 560, что логично, поскольку теперь заполнение ограничено размером экрана по вертикали.
Вот ТЕПЕРЬ у нас адаптивное позиционирование камеры.
Имплементация:
1. Нужна новая переменная screenAspectRatio.
Заменим TheGame.h код на:
#pragma once
#include <vector>
#include "GameSubj.h"
#include "Camera.h"
class TheGame
{
public:
int screenSize[2];
float screenAspectRatio = 1;
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);
};
В функцияхTheGame::getReady(), TheGame::drawFrame() и в TheGame::onScreenResize() у нас изменения, связанные с позиционированием камеры. Связанные с этим вычисления лучше перенесем из TheGame в класс Camera.
2. Заменим 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,1,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_flat_tex;
pMB->useMaterial(&mt);
TexCoords tc;
tc.set(mt.uTex0, 11, 12, 256, 128, "180");
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);
}
}
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;
}
3. В классе Camera – новые переменные и функционал.
Заменим Camera.h код на:
#pragma once
#include "Coords.h"
#include "linmath.h"
class Camera
{
public:
Coords ownCoords;
mat4x4 lookAtMatrix;
float lookAtPoint[3] = { 0,0,0 };
float focusDistance = 100;
float viewRangeDg = 30;
float stageSize[2] = { 500, 375 };
public:
float pickDistance() { return pickDistance(this); };
static float pickDistance(Camera* pCam);
void setCameraPosition() { setCameraPosition(this); };
static void setCameraPosition(Camera* pCam);
void buildLookAtMatrix() { buildLookAtMatrix(this); };
static void buildLookAtMatrix(Camera* pCam);
void onScreenResize() { onScreenResize(this); };
static void onScreenResize(Camera* pCam);
};
4. Под xEngine добавим новый C++ file Camera.cpp
Location: C:\CPP\engine\
Код:
#include "Camera.h"
#include "TheGame.h"
#include "utils.h"
extern TheGame theGame;
extern float degrees2radians;
float Camera::pickDistance(Camera* pCam) {
float cotangentA = 1.0f / tanf(degrees2radians * pCam->viewRangeDg / 2);
float cameraDistanceV = pCam->stageSize[1] / 2 * cotangentA;
float cameraDistanceH = pCam->stageSize[0] / 2 * cotangentA / theGame.screenAspectRatio;
pCam->focusDistance = fmax(cameraDistanceV, cameraDistanceH);
return pCam->focusDistance;
}
void Camera::setCameraPosition(Camera* pCam) {
v3set(pCam->ownCoords.pos, 0, 0, -pCam->focusDistance);
mat4x4_mul_vec4plus(pCam->ownCoords.pos, *pCam->ownCoords.getRotationMatrix(), pCam->ownCoords.pos, 1);
for (int i = 0; i < 3; i++)
pCam->ownCoords.pos[i] += pCam->lookAtPoint[i];
}
void Camera::buildLookAtMatrix(Camera* pCam) {
float cameraUp[4] = { 0,1,0,0 }; //y - up
mat4x4_mul_vec4plus(cameraUp, *pCam->ownCoords.getRotationMatrix(), cameraUp, 0);
mat4x4_look_at(pCam->lookAtMatrix, pCam->ownCoords.pos, pCam->lookAtPoint, cameraUp);
}
void Camera::onScreenResize(Camera* pCam) {
pCam->pickDistance();
pCam->setCameraPosition();
pCam->buildLookAtMatrix();
}
5. Компиляция и запуск.
Выглядит неплохо.
Android
6. Пере-запускаем VS. Открываем C:\CPP\a997modeler\p_android\p_android.sln.
7. Под xEngine добавим existing item Camera.cpp из C:\CPP\engine
Добавим реакцию на изменение размеров экрана в myglPollEvents().
8. Заменим myplatform.cpp код на:
#include <android/log.h>
#include "stdio.h"
#include "TheGame.h"
#include <sys/stat.h> //mkdir for Android
extern struct android_app* androidApp;
extern const ASensor* accelerometerSensor;
extern ASensorEventQueue* sensorEventQueue;
extern EGLDisplay androidDisplay;
extern EGLSurface androidSurface;
extern TheGame theGame;
void mylog(const char* _Format, ...) {
#ifdef _DEBUG
char outStr[1024];
va_list _ArgList;
va_start(_ArgList, _Format);
vsprintf(outStr, _Format, _ArgList);
__android_log_print(ANDROID_LOG_INFO, "mylog", outStr, NULL);
va_end(_ArgList);
#endif
};
void mySwapBuffers() {
eglSwapBuffers(androidDisplay, androidSurface);
}
void myPollEvents() {
// Read all pending events.
int ident;
int events;
struct android_poll_source* source;
// If not animating, we will block forever waiting for events.
// If animating, we loop until all events are read, then continue
// to draw the next frame of animation.
while ((ident = ALooper_pollAll(0, NULL, &events,
(void**)&source)) >= 0) {
// Process this event.
if (source != NULL) {
source->process(androidApp, source);
}
// If a sensor has data, process it now.
if (ident == LOOPER_ID_USER) {
if (accelerometerSensor != NULL) {
ASensorEvent event;
while (ASensorEventQueue_getEvents(sensorEventQueue,
&event, 1) > 0) {
//LOGI("accelerometer: x=%f y=%f z=%f",
// event.acceleration.x, event.acceleration.y,
// event.acceleration.z);
}
}
}
// Check if we are exiting.
if (androidApp->destroyRequested != 0) {
theGame.bExitGame = true;
break;
}
}
//check screen size
EGLint w, h;
eglQuerySurface(androidDisplay, androidSurface, EGL_WIDTH, &w);
eglQuerySurface(androidDisplay, androidSurface, EGL_HEIGHT, &h);
theGame.onScreenResize(w, h);
}
int myFopen_s(FILE** pFile, const char* filePath, const char* mode) {
*pFile = fopen(filePath, mode);
if (*pFile == NULL) {
mylog("ERROR: can't open file %s\n", filePath);
return -1;
}
return 1;
}
int myMkDir(const char* outPath) {
struct stat info;
if (stat(outPath, &info) == 0)
return 0; //exists already
int status = mkdir(outPath, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
if (status == 0)
return 1; //Successfully created
mylog("ERROR creating, status=%d, errno: %s.\n", status, std::strerror(errno));
return -1;
}
void myStrcpy_s(char* dst, int maxSize, const char* src) {
strcpy(dst, src);
//fill tail by zeros
int strLen = strlen(dst);
if (strLen < maxSize)
for (int i = strLen; i < maxSize; i++)
dst[i] = 0;
}
9. Включаем, разблокируем, подключаем, разрешаем.
Компиляция и запуск: