Chapter 22. Screen resize

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:

  1. 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:


Leave a Reply

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