Глава 22. Изменение размеров экрана

При смене размеров экрана надо переустановить камеру так, чтобы сцена оптимально заполнила новые размеры. Например, в нашем случае желаемый размер сцены, скажем, 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. Включаем, разблокируем, подключаем, разрешаем.

Компиляция и запуск:


Leave a Reply

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