Глава 18. Camera view

В прошлой главе мы использовали прямую проекцию нашего кубика на плоскость экрана (screen surface). Для более реалистичного изображения понадобится Camera. OpenGL ES не предлагает встроенного объекта Камеры, значит надо создать самим.

Windows

1. Запускаем VS. Открываем C:\CPP\a997modeler\p_windows\p_windows.sln.


Для наглядности давайте сперва упростим движение кубика.

2. Откроем TheGame.cpp и заменим угловые скорости (строка 26) на

pGS->ownSpeed.setDegrees(0,1,0);

Компиляция и запуск. Теперь кубик вращается просто вокруг оси Y. Выглядит забавно, не так ли?


3. Под xEngine добавм новый header file Camera.h

Location: C:\CPP\engine

Код:

#pragma once
#include "Coords.h"
#include "linmath.h"

class Camera
{
public:
	Coords ownCoords;
	mat4x4 lookAtMatrix;
};

Теперь надо задать позицию и ориентацию камеры. В итоге нам надо будет расчитать матрицу lookAtMatrix чтобы потом считать матрицу MVP (Model-View-Projection) для рендринга.

Размеры кубика – 100x200x400 и позиция – 0x0x0. Расположим камеру на некотором удалении от кубика ближе к нам, на небольшой высоте чтобы смотреть слегка сверху. Позиция 0x200x1000 подойдет.

Направление взгляда (View direction) будет направлением от позиции камеры к позиции кубика, в нашем случае 0x-200x-1000.

Чтобы задать view matrix нужны: положение камеры (camera position), направление взгляда (view direction), который мы только что вычислили, и вектор камеры вверх (camera UP vector), которого у нас пока нет, но его можно посчитать из направления взгляда. Из направления взгляда мы вычислим углы ориентации камеры – рысканье и тангаж (yaw and pitch), которые в нашем случае 180 и 11.3 градусов. По ним мы можем построить rotationMatrix камеры, и применим ее к вертикальному вектору 0x1x0. И тогда мы сможем вызвать функцию mat4x4_look_at() для расчета матрицы lookAtMatrix для камеры.

Расчеты yaw и pitch мы поместим в utils.


4. Откроем utils.h и заменим код на:

#pragma once
#include <string>
#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);


5. Откроем utils.cpp и заменим код на:

#include "utils.h"
#include "platform.h"

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; 
}


В TheGame.cpp вместо прямой матрицы mProjection мы зададим mProjection перспективы и mViewProjection, что есть результат умножения mProjection на lookAtMatrix камеры.

Вызов для расчета матрицы mProjection перспективы такой:

mat4x4_perspective(mProjection, 3.14f / 6.0f, (float)screenSize[0]/screenSize[1], 700.f, 1300.f);

Параметры:

  • mProjection – куда записать результирующую матрицу
  • 3.14f / 6.0f – угол зрения, в этом примере – 30 градусов в радианах
  • (float)screenSize[0]/screenSize[1] – screen size ratio
  • 700.f, 1300.f – ближний и дальний планы

6. Откроем 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"

extern std::string filesRoot;

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.type.assign("box");
    vs.whl[0] = 100;
    vs.whl[1] = 200;
    vs.whl[2] = 400;

    Material mt;
    //define material - flat red
    mt.shaderN = Shader::spN_flat_ucolor;
    mt.primitiveType = GL_TRIANGLES;
    mt.uColor.setRGBA(255, 0, 0,255); //red
    pMB->useMaterial(&mt);

    pMB->buildBoxFace("front", &vs);
    pMB->buildBoxFace("back", &vs);
    pMB->buildBoxFace("top", &vs);
    pMB->buildBoxFace("bottom", &vs);
    pMB->buildBoxFace("left", &vs);

    mt.uColor.setRGBA(0, 0, 255,255); pMB->useMaterial(&mt); //blue
    pMB->buildBoxFace("right", &vs);

    pMB->buildDrawJobs(gameSubjs);

    delete pMB;

    //===== set up camera
    v3set(mainCamera.ownCoords.pos, 0, 200, 1000); //set position
    float cameraDir[3];
    v3set(cameraDir, 0, -200, -1000); //set direction vector
    float cameraYawDg = v3yawDg(cameraDir);
    float cameraPitchDg = v3pitchDg(cameraDir);
    mylog("cameraYaw=%f, cameraPitch=%f\n", cameraYawDg, cameraPitchDg);

    mainCamera.ownCoords.setDegrees(cameraPitchDg, cameraYawDg, 0);
    float cameraUp[4] = { 0,1,0,0 }; //y - up
    mat4x4_mul_vec4plus(cameraUp, *mainCamera.ownCoords.getRotationMatrix(), cameraUp, 0);

    mat4x4_look_at(mainCamera.lookAtMatrix, mainCamera.ownCoords.pos, pGS->ownCoords.pos, cameraUp);

    return 1;
}
int TheGame::drawFrame() {
    myPollEvents();

    //glClearColor(0.0, 0.0, 0.5, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    mat4x4 mProjection, mViewProjection, mMVP;
    //mat4x4_ortho(mProjection, -(float)screenSize[0] / 2, (float)screenSize[0] / 2, -(float)screenSize[1] / 2, (float)screenSize[1] / 2, 100.f, 500.f);
    mat4x4_perspective(mProjection, 3.14f / 6.0f, (float)screenSize[0] / screenSize[1], 700.f, 1300.f);
    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);
        //render subject
        for (int i = 0; i < pGS->djTotalN; i++) {
            DrawJob* pDJ = DrawJob::drawJobs.at(pGS->djStartN + i);
            pDJ->execute((float*)mMVP, 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;
    screenRatio = (float)width / (float)height;
    glViewport(0, 0, width, height);
    mylog(" screen size %d x %d\n", width, height);
    return 1;
}
int TheGame::run() {
    getReady();
    while (!bExitGame) {
        drawFrame();
    }
    cleanUp();
    return 1;
}


TheGame.h тоже слегка изменен, добавлен объект mainCamera.

7. Заменим TheGame.h код на:

#pragma once
#include <vector>
#include "GameSubj.h"
#include "Camera.h"

class TheGame
{
public:
	int screenSize[2];
	float screenRatio;
	bool bExitGame;
	Camera mainCamera;

	//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);
};


8. Компиляция и запуск. Результат:

Гораздо лучше, не правда ли?


9. Правда присутствует “эффект трапеции”

Keystone (trapezoid) effect

Всем кто фотографировал в городе, этот эффект хорошо знаком. Это “перспективное искажение”, “заваливание” вертикальных линий:

Маленькая хитрость поможет это исправить. В матрице mViewProjection мы можем указать, что мы не хотим чтобы экранные координаты зависели от вертикальной (Y) компоненты. Это позиция [1][3] в матрице. Просто установим ее в ноль.

Это строка 83 в TheGame.cpp.

До:

После:


Теперь – на

Андроиде

10. Пере-запускаем VS. Открываем C:\CPP\a997modeler\p_android\p_android.sln.


11. Под xEngine добавим Existing Item

из C:\CPP\engine

Camera.h

Add.


12. Включаем, разблокируем, подключаем, разрешаем.

Rebuild solution, запуск.

Не вполне вписывается в экран, но работает!


Leave a Reply

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