All checks were successful
Build & deploy web version / Build WebAssembly & deploy (push) Successful in 1m0s
352 lines
15 KiB
C
352 lines
15 KiB
C
#include "camera.h"
|
|
|
|
#define focalLength 0.05 /**< gyújtótáv [m] */
|
|
#define sensorSize 0.07 /**< szenzor hosszabb oldalának a fele [m] */
|
|
#define renderObjectsPerCube 10 /**< max( (projectile + player + enemy) / m^3), erről később bővebben. */
|
|
|
|
vector3D cameraPosition; /**< A kamera térbeli helyvektora. */
|
|
float magnification, columns, rows; /**< Ennyi látszik a pályából [m]. */
|
|
bool watchBounds = false; /**< Feldarabolós rajzolás van-e? */
|
|
float leftBound, rightBound, upperBound, lowerBound; /**< Darabolás határai. */
|
|
int leftMostBound, rightMostBound, topBound, bottomBound;
|
|
renderObject*** renderMap = NULL; /**< Darabolás mátrixa. */
|
|
|
|
|
|
///A feldarabolós rajzoláshoz szükséges mátrixot szabadítja fel.
|
|
void freeRenderMap() {
|
|
if(renderMap != NULL) {
|
|
free(renderMap[0][0]);
|
|
free(renderMap[0]);
|
|
free(renderMap);
|
|
}
|
|
}
|
|
|
|
///A feldarabolós rajzoláshoz szükséges mátrixot mallocolja.
|
|
static void initRenderMap() {
|
|
renderMap = (renderObject***) malloc(sizeof(renderObject**));
|
|
renderMap[0] = (renderObject**) malloc(sizeof(renderObject*));
|
|
renderMap[0][0] = (renderObject*) malloc(sizeof(renderObject));
|
|
}
|
|
|
|
/** \brief Kiszámolja a kamera poziciója és a képernyő mérete alapján, hogy mennyi látszik a pályából.
|
|
*
|
|
* Elsősorban ezeket a változókat változtatja:
|
|
* columns, rows = képernyőn megjelenő oszlopok és sorok száma,
|
|
* magnification = megmondja egy méter a kamera "szenzorán" hány pixelnek felel meg (jó soknak)
|
|
*
|
|
* Hasonló háromszögek alapján kiszámolja mennyi látszik a pályából:
|
|
* sensor size
|
|
* |
|
|
* V
|
|
* ----
|
|
* \ |
|
|
* \ | <- focalLength
|
|
* \|
|
|
* |
|
|
* |\
|
|
* | \
|
|
* Z -> | \
|
|
* | \
|
|
* | \
|
|
* -------------
|
|
* columns
|
|
*
|
|
* Ezek alapján reallocolja a darabolós mátrixot.
|
|
*
|
|
*/
|
|
static void calculateRowsAndColumns() {
|
|
/* A szenzor hosszabb oldala mindig a képernyő hosszabb oldalára képződik le. Fekvő
|
|
* képernyőn ez a szélesség, állón viszont a magasság: egy telefon képernyőjén a
|
|
* szélességhez igazodva a pályának majdnem a kétszerese férne ki függőlegesen, amitől
|
|
* a kockák aprók lennének, a játékos feje fölött pedig végtelen üres ég maradna.
|
|
* Így viszont fekvőn és állón is ugyanakkorák a kockák, csak más alakú részt látunk. */
|
|
float longerScreenSide = larger(screen -> w, screen -> h);
|
|
magnification = longerScreenSide / 2 / sensorSize;
|
|
|
|
float longerSideInMeters = 2 * cameraPosition.z * sensorSize / focalLength;
|
|
//hogy a négyzet négyzet is maradjon
|
|
columns = screen -> w / longerScreenSide * longerSideInMeters;
|
|
rows = screen -> h / longerScreenSide * longerSideInMeters;
|
|
|
|
if(renderMap == NULL) initRenderMap();
|
|
|
|
int xSize = (int)columns + 4; //a nagyobb testek átlóghatnak a képernyő határán
|
|
int ySize = (int)rows + 4;
|
|
int iSize = renderObjectsPerCube;
|
|
|
|
renderObject* innerTmp = (renderObject*) realloc(renderMap[0][0], xSize * ySize * iSize * sizeof(renderObject));
|
|
renderObject** outerTmp = (renderObject**) realloc(renderMap[0], xSize * ySize * sizeof(renderObject*));
|
|
renderMap = (renderObject***) realloc(renderMap, xSize * sizeof(renderObject**));
|
|
|
|
renderMap[0] = outerTmp;
|
|
|
|
int x;
|
|
for(x = 1; x < xSize; x++)
|
|
renderMap[x] = renderMap[0] + x * ySize;
|
|
|
|
renderMap[0][0] = innerTmp;
|
|
|
|
renderObject empty;
|
|
empty.doesExist = false;
|
|
|
|
int y, i = 0;
|
|
int j;
|
|
for(x = 0; x < xSize; x++)
|
|
for(y = 0; y < ySize; y++) {
|
|
renderMap[x][y] = renderMap[0][0] + i++ * iSize;
|
|
for(j = 0; j < iSize; j++) renderMap[x][y][j] = empty;
|
|
}
|
|
}
|
|
|
|
///A képernyő méretének változása után újraszámolja a látható területet és átméretezi a darabolós mátrixot.
|
|
void refreshCameraForScreen() {
|
|
calculateRowsAndColumns();
|
|
}
|
|
|
|
///A játékost követi a kamerával, nem hagyja, hogy a pálya alá lásson.
|
|
void followPlayerWithCamera() {
|
|
renderObject player = getPlayerRenderObject();
|
|
cameraPosition.x = (2 * player.position.x + player.size.x) / 2;
|
|
cameraPosition.y = (player.position.y < rows / 2 ? rows / 2 : player.position.y) + 1;
|
|
}
|
|
|
|
///A menüben használt kamerát állítja be, visszaadja a columns-t és rows-t is.
|
|
void menuCamera(int* getColumns, int* getRows) {
|
|
cameraPosition.z = cameraDeltaZDistanceStart;
|
|
calculateRowsAndColumns();
|
|
*getColumns = columns;
|
|
*getRows = rows;
|
|
cameraPosition.x = columns / 2;
|
|
cameraPosition.y = rows / 2;
|
|
}
|
|
|
|
///Visszaadja, hogy hány oszlop látszik a pályából.
|
|
int getColumns() {
|
|
return columns;
|
|
}
|
|
|
|
///Visszaadja, hogy hány sor látszik a pályából.
|
|
int getRows() {
|
|
return rows;
|
|
}
|
|
|
|
/** \brief A kamera dollyzását végzi, a kamerát mozgatja
|
|
* A dolly állását staticban tárolja, ezért új dolly eseteén először egy restart = true-val kell meghívni,
|
|
* \param ms eltelt idő
|
|
* \param kell-e újrakezdés?
|
|
* \return elérte-e a végállást?
|
|
* Minden frameban újraszámolja a rows&coulmns-t,
|
|
*/
|
|
bool cameraDollyZ(int deltaTime, bool restart) {
|
|
static int timePassed;
|
|
if(restart) timePassed = 0;
|
|
|
|
bool done = true;
|
|
|
|
timePassed += deltaTime;
|
|
|
|
float t = (float)(cameraDollyTime - timePassed) / cameraDollyTime;
|
|
float cube = t*t*t;
|
|
|
|
float currentZ = cameraDeltaZDistanceEnd + (cameraDeltaZDistanceStart - cameraDeltaZDistanceEnd) * cube;
|
|
|
|
if(currentZ <= cameraDeltaZDistanceEnd) {
|
|
currentZ = cameraDeltaZDistanceEnd;
|
|
done = false;
|
|
}
|
|
|
|
cameraPosition.z = currentZ;
|
|
calculateRowsAndColumns();
|
|
return done;
|
|
}
|
|
|
|
/** \brief Térbeli pontból síkbelit képez.
|
|
* A 3D-s pont koordinátái és a kamera alapján kiszámolja, hogy a kamera 2D-s szenzorán hol jelenik meg a pont.
|
|
* Geometriailag leírva, a két ponton (point, cameraPosition) átmenő egyenes egyenletének, és a 'szenzor' vízszintesének metszéspontja
|
|
* a keresett pontunk. A lent és a linken is látható egyenletet először a tér x-z, majd az y-z vetületére alkalmazzuk,
|
|
* és megkapjuk a térbeli pontunk megfelelő síkbeli leképezést.
|
|
* Ez interaktívan ábrázolva így néz ki: https://www.desmos.com/calculator/8qjvkptjmn
|
|
* \param Térvektor típusú pont
|
|
* \return Síkvektor típusú pont
|
|
* A visszaadott pont koordinátái törtszámok: egy pixelen belüli helyet is meg tudnak adni.
|
|
* Erre azért van szükség, mert a kamera folyamatosan mozog, és ha itt pixelre kerekítenénk,
|
|
* akkor a testek élei egymástól függetlenül, más-más képkockán ugranának át a szomszéd pixelre.
|
|
* Ez a függvény igazából egy camera obscura modellje.
|
|
*/
|
|
vector2D get2DPoint(vector3D point) {
|
|
//Ha watchBounds = true, csak egy megadott kockán belül adja vissza az értéket (rajzol), ez a feldarabolás miatt fontos
|
|
if(watchBounds) {
|
|
putInsideIntervalF(leftBound, &point.x, rightBound);
|
|
putInsideIntervalF(lowerBound, &point.y, upperBound);
|
|
}
|
|
|
|
/* A metszéspont képlete kibontva összevonható, hiszen zDistFromHole = zDist - focalLength:
|
|
* (kamera * (zDist - f) + f * pont) / zDist - kamera == f * (pont - kamera) / zDist
|
|
* A két alak matematikailag azonos, de a bal oldali a pálya közepén (kamera ~1000 m-nél)
|
|
* egy ~20000 nagyságrendű számból vonna ki majdnem ugyanakkorát, és a maradék néhány
|
|
* tizedből épp a lényeg veszne el: a float pontossága ott már csak ezred méter, amit a
|
|
* magnification több tized pixelnyi hibává nagyít. A jobb oldali alak előbb végzi el a
|
|
* kivonást (két közeli szám különbsége hibamentes), így a leképezés a pálya szélén is
|
|
* pixeltört pontos marad. */
|
|
float zDist = point.z - cameraPosition.z;
|
|
float dx = focalLength * (point.x - cameraPosition.x) / zDist;
|
|
float dy = focalLength * (point.y - cameraPosition.y) / zDist;
|
|
|
|
//Megfelelően tükrözzük a kapot pontot, és a méterben kapott értéket átváltjuk pixelekre
|
|
vector2D resultPoint = {
|
|
screen -> w / 2.0f - magnification * dx,
|
|
screen -> h / 2.0f + magnification * dy
|
|
};
|
|
|
|
return resultPoint;
|
|
}
|
|
|
|
/** \brief Lerajzol egy négyszöget
|
|
* A bemenetként kapott 4 pont alkotta négyszöget rajzolja ki.
|
|
* \param síkvektor a csúcshoz
|
|
* \param síkvektor b csúcshoz
|
|
* \param síkvektor c csúcshoz
|
|
* \param síkvektor d csúcshoz
|
|
* \param szín négyszög színe
|
|
* Ezt egyszer kitöltéssel csinálja, egyszer pedig anti-aliasing poligonnal,
|
|
* hogy a rajz széle ne legyen recegős.
|
|
*/
|
|
static void drawTetragon(vector2D a, vector2D b, vector2D c, vector2D d, color color) {
|
|
float xCoords[4] = {a.x, b.x, c.x, d.x};
|
|
float yCoords[4] = {a.y, b.y, c.y, d.y};
|
|
|
|
filledPolygonRGBA(screen, xCoords, yCoords, 4, color.r, color.g, color.b, color.a);
|
|
aapolygonRGBA(screen, xCoords, yCoords, 4, color.r, color.g, color.b, color.a);
|
|
}
|
|
|
|
/** \brief Lerajzol egy téglatestet
|
|
* A kamera modul lényege, a rajzolást végzi el az eddigi függvények segítségével.
|
|
* \param test téglatest adatai
|
|
* \param bool melyik oldalakat kell rajzolni?
|
|
* Végez egy plusz ellenőrést is, nem csak a paraméterben megadott tömb alapján nem rajzol oldalakat,
|
|
* azokat se rajzolja le, amiket a helyzetük miatt biztosan nem láthat a kamera.
|
|
*/
|
|
void drawCuboid(renderObject object, bool sides[]) {
|
|
vector2D vertexes[8];
|
|
//A téglatestet bejárva megkeressük a csúcsait
|
|
int i;
|
|
for(i = 0; i < 8; i++) {
|
|
vertexes[i] = get2DPoint(object.position);
|
|
switch(i % 4) {
|
|
case 0:
|
|
object.position.x += object.size.x;
|
|
break;
|
|
case 1:
|
|
object.position.y += object.size.y;
|
|
break;
|
|
case 2:
|
|
object.position.x -= object.size.x;
|
|
break;
|
|
case 3:
|
|
object.position.y -= object.size.y;
|
|
}
|
|
if(i == 3) object.position.z += object.size.z;
|
|
}
|
|
|
|
//alja
|
|
if(sides[0] && object.position.y > cameraPosition.y) drawTetragon(vertexes[0], vertexes[4], vertexes[5], vertexes[1], object.colors.dark);
|
|
//bal oldali lap
|
|
if(sides[1] && object.position.x > cameraPosition.x) drawTetragon(vertexes[0], vertexes[3], vertexes[7], vertexes[4], object.colors.dark);
|
|
//jobb oldali lap
|
|
if(sides[2] && object.position.x + object.size.x < cameraPosition.x) drawTetragon(vertexes[1], vertexes[2], vertexes[6], vertexes[5], object.colors.dark);
|
|
//teteje
|
|
if(sides[3] && object.position.y + object.size.y < cameraPosition.y) drawTetragon(vertexes[3], vertexes[7], vertexes[6], vertexes[2], object.colors.light);
|
|
//eleje
|
|
if(sides[4]) drawTetragon(vertexes[4], vertexes[5], vertexes[6], vertexes[7], object.colors.normal);
|
|
}
|
|
|
|
/// Az egy m^3-ben lévő testeket rajzolja le
|
|
static void drawPlace(int x, int* y, int direction, int bound) {
|
|
bool sides[5] = {true, true, true, true, true};
|
|
|
|
renderObject toDraw = getBlockRenderObject(x, y, direction, bound, sides);
|
|
if(toDraw.doesExist) {
|
|
watchBounds = false;
|
|
drawCuboid(toDraw, sides);
|
|
watchBounds = true;
|
|
}
|
|
else {
|
|
int i = 0;
|
|
toDraw = renderMap[x - leftMostBound][*y - bottomBound][i];
|
|
while(toDraw.doesExist && i < renderObjectsPerCube) {
|
|
drawCuboid(toDraw, sides);
|
|
i++;
|
|
toDraw = renderMap[x - leftMostBound][*y - bottomBound][i];
|
|
}
|
|
|
|
renderObject player = getPlayerRenderObject();
|
|
|
|
if(
|
|
(x == (int)floor(player.position.x) || x == (int)floor(player.position.x + player.size.x)) &&
|
|
(*y == (int)floor(player.position.y) || *y == (int)floor(player.position.y + player.size.y))
|
|
)
|
|
drawCuboid(player, sides);
|
|
}
|
|
}
|
|
|
|
///Az egy oszlopban lévő testeket rajzolja le, a részecskéket mozgatja közvetlen
|
|
static void drawY(int deltaTime, int x) {
|
|
int y;
|
|
|
|
for(y = topBound; y > cameraPosition.y; y--) {
|
|
upperBound = y + 1;
|
|
lowerBound = y;
|
|
drawPlace(x, &y, -1, (int)cameraPosition.y);
|
|
}
|
|
for(y = bottomBound; y <= cameraPosition.y; y++) {
|
|
upperBound = y + 1;
|
|
lowerBound = y;
|
|
drawPlace(x, &y, 1, (int)cameraPosition.y + 1);
|
|
}
|
|
watchBounds = false;
|
|
drawParticles(x, bottomBound, topBound, deltaTime);
|
|
watchBounds = true;
|
|
}
|
|
|
|
/** \brief Lerajzolja a teljes látható játékteret.
|
|
* A megfelelő rajzoláshoz figyelni kell a testek takarására, nem mindegy ezért, hogy milyen sorrendben rajzoljuk őket.
|
|
* A részecskéknél ezzel nincs probléma, azokat bármilyen sorrendben rajzolhatjuk. Ők a játékteret kitakarják, egymásnál viszont nem látszik,
|
|
* hogy melyik takrja melyiket, ezért azokat elég a játéktér után bármilyen sorrendben lerajzolni.
|
|
* A térképet alkotó kockáknál már nem ilyen egyszeű a helyzet. Azokat 4 irányból kell rajzolni, bal lentről, jobb lentről, bal fentről, illetve jobb fentről középre.
|
|
* Ha ebben a négy lépésben rajzoljuk őket (ezek között a sorrend nem számít) akkor mindig a legfesőket rajzoljuk utoljára, és így valósnak néz ki az eredmény.
|
|
* A játékos, a lövedékek, illetve az ellenségek viszont sajnos nincsenek egy négyzethálóhoz kötve. Így gyakran megesik, hogy az egyik felüket a pálya
|
|
* egyik kockája előtt, a másikat pedig az után, de még a kövektező előtt kéne rajzolni. Ezt nem tudjuk megcsinálni, hatalmas erőforrásba kerülne ez a majdnem pixelről
|
|
* pixelre történő szkennelés. A pályát továbbra is m^3-ént szeretnénk lerajzolni. Hogy ez megfelelően nézzen ki, minden köbméterben csak az abban a kockában lévő dolgokat
|
|
* szeretnénk megrajzolni. Ehhez fel kell "darabolnunk" a játékost, a lövedékeket, illetve az ellenségeket is. A darabolás azt jelenti, hogy egy mátrix minden cellájába
|
|
* beírjuk, hogy az adott kockában melyik, milyen tulajdonsággal rendelkező testek lógnak be. Rajzolásnál pedig a testeknek mindig csak az adott kockabeli részét rajzoljuk le.
|
|
* Mivel a kockákat jó sorrendben rajzoljuk, így a bennük levő "testekrészeket" is jó sorrendbe fogjuk rajzolni. A problémát megoldottuk.
|
|
* \param ms eltelt idő
|
|
* Ez a függvény elvégzi a részecskék, a pálya, illetve minden más test rajzolását is. A részecskék mozgatása innen van meghívva.
|
|
*/
|
|
void drawMap(int deltaTime) {
|
|
leftMostBound = (int)(cameraPosition.x - (float)columns / 2) - 1;
|
|
rightMostBound = (int)(cameraPosition.x + (float)columns / 2) + 1;
|
|
topBound = (int)(cameraPosition.y + (float)rows / 2 + 1);
|
|
bottomBound = (int)(cameraPosition.y - (float)rows / 2 - 1);
|
|
|
|
int y, x;
|
|
|
|
for(x = leftMostBound; x < rightMostBound; x++)
|
|
for(y = bottomBound; y < topBound; y++)
|
|
getFloatersRenderObject(x, y, renderMap[x - leftMostBound][y - bottomBound], renderObjectsPerCube);
|
|
|
|
float slicer = cameraPosition.x;
|
|
watchBounds = true;
|
|
for(x = leftMostBound; x <= slicer; x++) {
|
|
leftBound = x - 0.005;
|
|
rightBound = x + 1 + 0.005;
|
|
drawY(deltaTime, x);
|
|
}
|
|
for(x = rightMostBound; x > slicer - 1; x--) {
|
|
leftBound = x - 0.005;
|
|
rightBound = x + 1 + 0.005;
|
|
drawY(deltaTime, x);
|
|
}
|
|
|
|
watchBounds = false;
|
|
}
|