When screen size changed, we need to re-position the camera to fit new screen size optimally. For example, in our case optimal scene/"stage" size would be, let's say, 500x300 (to fit our spinning box into the screen). Our camera's view angle is 30 degrees.
so, ? / 250 = cosine(15) / sine(15) = cotangent(15)
? = 250 * cotangent(15) = 250 * 3.73 = 923
which is almost the same as our hard-coded 1000 (line 69 in TheGame.cpp) and doesn't explain why we don't fit screen on Android.
First, we don't count screen resolution, and second:
Start VS, open C:\CPP\a997modeler\p_windows\p_windows.sln, run the project, play with window size (from tall and narrow to short and wide). You will note (it was a surprise to me) that our 1000 camera distance defines only VERTICAL fit, not horizontal one. But we don't need to fit 500 size units vertically, vertically we need just 300, so
cameraDistanceV (vertical) = 150 * cotangent(15)
For horizontal fit we need to count also screen aspect ratio, which is screen width / height, so
cameraDistanceH (horizontal) = 250 * cotangent(15) / screenAspectRatio
For example, my S20 phone screen size is 1080 X 2400, so
screenAspectRatio = 1080 / 2400 = 0.45
cotangent(15) = 3.73, so:
cameraDistanceV = 150 * cotangent(15) = 150 * 3.73 = 560
cameraDistanceH = 250 * cotangent(15) / screenAspectRatio = 250 * 3.73 / 0.45 = 2072
For our camera we'll pick the greater value, 2072, which looks quite reasonable. From SUCH distance it surely will fit.
Now, if we flip the phone horizontally, screen size will switch to 2400 X 1080. so
screenAspectRatio = 2400 / 1080 = 2.22, so:
cameraDistanceV remains the same, 560, when horizontal one
cameraDistanceH = 250 * cotangent(15) / screenAspectRatio = 250 * 3.73 / 2.22 = 420, so this time we'll pick the greater vertical value, 560, which is logical, now fitment will be defined by screen vertical size limit.
NOW we have adaptive camera positioning.
Implementation:
- Let's call new variable screenAspectRatio.
Open TheGame.h and replace the code by:
#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);
};
In TheGame::getReady(), TheGame::drawFrame() and in TheGame::onScreenResize() we have camera related changes. Also we'll move camera-related calculations from TheGame to the Camera class.
2. Open TheGame.cpp and replace the code by:
#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. In Camera class we have new variables and functionality.
Open Camera.h and replace the code by:
#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. Under xEngine add new C++ file, name: Camera.cpp
Location: C:\CPP\engine\
Code:
#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. Build and run.
Looks good.
Android
6. Close and re-start VS. Open C:\CPP\a997modeler\p_android\p_android.sln.
7. Under xEngine add existing item Camera.cpp from C:\CPP\engine
Add screen resize handling to myglPollEvents().
8. Open myplatform.cpp and replace code by:
#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. Switch on, unlock, plug in, allow.
Build and run: