Teo-CD
cad775f88e
Implement the basic raycaster, checking every block intersection and the block linked to that position. Split the screen uniformally along the width. Add data needed for camera maths in the Player class Specify constraints on the raycast parameters. Render the screen using the raycasts. Call the renderer in the main loop.
246 lines
7.1 KiB
C++
246 lines
7.1 KiB
C++
//
|
|
// Created by trotfunky on 27/05/19.
|
|
//
|
|
|
|
#include "World.h"
|
|
#include <cmath>
|
|
|
|
|
|
World::World(int w, int h, sf::Color groundColor, sf::Color ceilingColor, std::vector<BlockType> worldMap) : w(w), h(h),
|
|
groundColor(groundColor), ceilingColor(ceilingColor), map(std::move(worldMap)), player(0,0,0)
|
|
{
|
|
map.resize(w*h,BlockType::WALL);
|
|
}
|
|
|
|
int World::getW() const
|
|
{
|
|
return w;
|
|
}
|
|
|
|
int World::getH() const
|
|
{
|
|
return h;
|
|
}
|
|
|
|
BlockType World::getBlock(float x, float y) const
|
|
{
|
|
return(map.at(static_cast<int>(x)+w* static_cast<int>(y)));
|
|
}
|
|
|
|
void World::setBlock(BlockType block, int x, int y, int width, int height)
|
|
{
|
|
for(int i = 0;i<height;i++)
|
|
{
|
|
for(int j = 0;j<width;j++)
|
|
{
|
|
if(x+j<w && y+i < h)
|
|
{
|
|
map.at((y+i)*w+x+j) = block;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& ostream, World const& world)
|
|
{
|
|
for(int i = 0;i<world.w*world.h;i++)
|
|
{
|
|
if(i%world.w == 0)
|
|
{
|
|
ostream << std::endl;
|
|
}
|
|
switch(world.getBlock(i%world.w,i/world.w))
|
|
{
|
|
case BlockType::AIR:
|
|
{
|
|
if(static_cast<int>(world.player.x) == i%world.w && static_cast<int>(world.player.y) == i/world.h)
|
|
{
|
|
ostream << "P";
|
|
}
|
|
else
|
|
{
|
|
ostream << " ";
|
|
}
|
|
break;
|
|
}
|
|
case BlockType::WALL:
|
|
{
|
|
ostream << "W";
|
|
break;
|
|
}
|
|
case BlockType::DOOR:
|
|
{
|
|
ostream << "D";
|
|
break;
|
|
}
|
|
case BlockType::WINDOW:
|
|
{
|
|
ostream << "W";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return(ostream);
|
|
}
|
|
|
|
void World::fillColumn(sf::RenderWindow& window, int column, float scale, sf::Color wallColor) const
|
|
{
|
|
float columnHeight = window.getSize().y*scale;
|
|
sf::RectangleShape pixelColumn(sf::Vector2f(1,columnHeight));
|
|
pixelColumn.setPosition(column,(window.getSize().y-columnHeight)/2.0);
|
|
pixelColumn.setFillColor(wallColor);
|
|
|
|
window.draw(pixelColumn);
|
|
}
|
|
|
|
float World::castRay(float originX, float originY, float orientation) const
|
|
{
|
|
/*
|
|
* Reference used for ray intersection computations :
|
|
* https://web.archive.org/web/20220628034315/https://yunes.informatique.univ-paris-diderot.fr/wp-content/uploads/cours/INFOGRAPHIE/08-Raycasting.pdf
|
|
* The logic is as follows :
|
|
* - This computes one set of point per edge crossings (horizontal/vertical)
|
|
* - The origin not being confined to the grid, offsets are computed to
|
|
* align the intersections properly
|
|
* - The intersections are at multiples of the tangent of the relevant
|
|
* angle for the axis of interest, and simply on successive edges of
|
|
* the grid for the other one
|
|
* - Depending on the orientation, signs must be taken into account
|
|
* to work 360°
|
|
*/
|
|
/* Offsets to get back on the grid from the ray's origin. */
|
|
float hOffsetX;
|
|
float hOffsetY;
|
|
float vOffsetX;
|
|
float vOffsetY;
|
|
|
|
/* Signs controlling the direction of travel. */
|
|
float hDir;
|
|
float vDir;
|
|
/* Need offset for rounding in the right direction ? */
|
|
float hRound;
|
|
float vRound;
|
|
|
|
float rads = orientation * deg_to_rad;
|
|
/* Used for vertical intersections. */
|
|
float rads_offset = (90 - orientation) * deg_to_rad;
|
|
|
|
/* Tangents used for the different axes. */
|
|
float hTan = tanf(rads);
|
|
float vTan = tanf(rads_offset);
|
|
|
|
/* Check if cos > 0 for horizontal hits formulas. */
|
|
if (orientation < 90 || orientation > 270) {
|
|
hOffsetX = ceilf(originY) - originY;
|
|
hOffsetY = ceilf(originY);
|
|
hDir = +1;
|
|
hRound = 0;
|
|
} else {
|
|
hOffsetX = originY - floorf(originY);
|
|
hOffsetY = floorf(originY);
|
|
hDir = -1;
|
|
hRound = -1;
|
|
}
|
|
hTan *= hDir;
|
|
hOffsetX *= hTan;
|
|
|
|
/* Check if sin > 0 for vertical hits formulas. */
|
|
if (orientation < 180) {
|
|
vOffsetX = ceilf(originX);
|
|
vOffsetY = ceilf(originX) - originX;
|
|
vDir = +1;
|
|
vRound = 0;
|
|
} else {
|
|
vOffsetX = floorf(originX);
|
|
vOffsetY = originX - floorf(originX);
|
|
vDir = -1;
|
|
vRound = -1;
|
|
}
|
|
vTan *= vDir;
|
|
vOffsetY *= vTan;
|
|
|
|
/*
|
|
* Now we have all the constants and deltas to work with, cast the ray.
|
|
* Generated points follow the formulas :
|
|
* - h-intersect : (originX + hOffsetX + hTan*i, hOffsetY + hDir*i)
|
|
* - v-intersect : (vOffsetX + vDir*i, originY + vOffsetY + vTan*i)
|
|
*/
|
|
int i = 0;
|
|
float hCheckX = originX + hOffsetX;
|
|
float hCheckY = hOffsetY;
|
|
/* Bounds + sanity check. */
|
|
while (hCheckX >= 0 && hCheckX <= w && hCheckY >= 0 && hCheckY <= h && i < h) {
|
|
if (getBlock(floorf(hCheckX), floorf(hCheckY) + hRound) == BlockType::WALL) {
|
|
break;
|
|
}
|
|
|
|
hCheckX += hTan;
|
|
hCheckY += hDir;
|
|
i++;
|
|
}
|
|
|
|
i = 0;
|
|
float vCheckX = vOffsetX;
|
|
float vCheckY = originY + vOffsetY;
|
|
|
|
/* Bounds + sanity check. */
|
|
while (vCheckX >= 0 && vCheckX < w && vCheckY >= 0 && vCheckY < h && i < w) {
|
|
if (getBlock(floorf(vCheckX) + vRound, floorf(vCheckY)) == BlockType::WALL) {
|
|
break;
|
|
}
|
|
|
|
vCheckX += vDir;
|
|
vCheckY += vTan;
|
|
i++;
|
|
}
|
|
|
|
/*
|
|
* We may or may not have hit something. Check which coordinates are closest
|
|
* and use those for computing the apparent size on screen.
|
|
*/
|
|
float hDist = sqrtf((originX - hCheckX)*(originX - hCheckX) +
|
|
(originY - hCheckY)*(originY - hCheckY));
|
|
float vDist = sqrtf((originX - vCheckX)*(originX - vCheckX) +
|
|
(originY - vCheckY)*(originY - vCheckY));
|
|
float finalDist = hDist > vDist ? vDist : hDist;
|
|
|
|
/* 2 Is wall height in meters. */
|
|
return player.focalLength*2/finalDist;
|
|
}
|
|
|
|
void World::render(sf::RenderWindow& window) const
|
|
{
|
|
/*
|
|
* Draw ground and sky planes through half of the screen, as the walls
|
|
* will get drawn over them.
|
|
* This doesn't work if we support textures/levels.
|
|
*/
|
|
sf::RectangleShape ground = sf::RectangleShape(sf::Vector2f(window.getSize().x,window.getSize().y/2.0));
|
|
ground.setFillColor(groundColor);
|
|
ground.setPosition(0,window.getSize().y/2.0);
|
|
|
|
sf::RectangleShape ceiling = sf::RectangleShape(sf::Vector2f(window.getSize().x,window.getSize().y/2.0));
|
|
ceiling.setFillColor(ceilingColor);
|
|
|
|
window.draw(ground);
|
|
window.draw(ceiling);
|
|
|
|
/*
|
|
* Throw rays and draw walls over the ceiling and ground.
|
|
* Only throws in the plane, which doesn't work for levels/3D.
|
|
*/
|
|
for(int i = 0;i<window.getSize().x;i++)
|
|
{
|
|
float deltaAngle = (player.fov/window.getSize().x) * (i-window.getSize().x/2.0);
|
|
float rayAngle = player.orientation + deltaAngle;
|
|
if (rayAngle < 0) {
|
|
rayAngle += 360;
|
|
} else if (rayAngle > 360) {
|
|
rayAngle -= 360;
|
|
}
|
|
float obstacleScale = castRay(player.x, player.y, rayAngle)/player.sensorSize;
|
|
fillColumn(window, i, obstacleScale);
|
|
}
|
|
}
|
|
|
|
}
|