platform-game/Forráskód/modules/camera.c
Andras Schmelczer ba2d649de7
All checks were successful
Build & deploy web version / Build WebAssembly & deploy (push) Successful in 1m0s
Fix scaling on mobile
2026-08-10 08:16:56 +01:00

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