1
0
Fork 0
Toy-Raytracer/World.cpp

363 lines
9.5 KiB
C++
Raw Normal View History

2019-05-27 13:50:49 +02:00
//
// Created by trotfunky on 27/05/19.
//
#include "World.h"
#include <cmath>
2019-05-27 13:50:49 +02:00
#ifdef IMGUI
#include <imgui.h>
#include <imgui-SFML.h>
#endif
2019-05-27 13:50:49 +02:00
World::World(int w, int h, sf::Color groundColor, sf::Color ceilingColor, std::vector<BlockType> worldMap) :
player(0,0,0), w(w), h(h), map(std::move(worldMap)),
groundColor(groundColor), ceilingColor(ceilingColor)
2019-05-27 13:50:49 +02:00
{
2024-01-28 22:08:35 +00:00
map.resize(w*h,BlockType::WALL);
2019-05-27 13:50:49 +02:00
}
int World::getW() const
{
2024-01-28 22:08:35 +00:00
return w;
2019-05-27 13:50:49 +02:00
}
int World::getH() const
{
2024-01-28 22:08:35 +00:00
return h;
2019-05-27 13:50:49 +02:00
}
void World::resizeWindow(const sf::FloatRect& resizedView) {
sf::RectangleShape defaultRectangle(sf::Vector2f(1,2));
defaultRectangle.setFillColor(Colors::Wall);
renderColumns.resize(static_cast<long>(resizedView.width), defaultRectangle);
}
BlockType World::getBlock(int x, int y) const
{
2024-01-28 22:08:35 +00:00
return map[x + w*y];
}
2019-05-27 13:50:49 +02:00
BlockType World::getBlock(float x, float y) const
{
return map[static_cast<int>(x) + w*static_cast<int>(y)];
2019-05-27 13:50:49 +02:00
}
void World::setBlock(BlockType block, int x, int y, int width, int height)
{
2024-01-28 22:08:35 +00:00
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;
}
}
}
2019-05-27 13:50:49 +02:00
}
std::ostream& operator<<(std::ostream& ostream, World const& world)
{
2024-01-28 22:08:35 +00:00
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);
2019-05-27 13:50:49 +02:00
}
float World::castRay(float originX, float originY, float orientation) const
{
2024-01-28 22:08:35 +00:00
/*
* 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°
* - Those formulas consider regular axes (x,y), however the world is
* built around left-handed axes (x,y), so the rendered world is
* mirrored. This also explains some weird signs for rotations.
*/
/* Offsets to get back on the grid from the ray's origin. */
float hOffsetX;
float hOffsetY;
float vOffsetX;
float vOffsetY;
2024-01-28 22:08:35 +00:00
/* Signs controlling the direction of travel. */
float hDir;
float vDir;
/* Need offset for rounding in the right direction ? */
float hRound;
float vRound;
2024-01-28 22:08:35 +00:00
float rads = orientation * deg_to_rad;
/* Used for vertical intersections. */
float rads_offset = (90 - orientation) * deg_to_rad;
2024-01-28 22:08:35 +00:00
/* Tangents used for the different axes. */
float hTan = tanf(rads);
float vTan = tanf(rads_offset);
2024-01-28 22:08:35 +00:00
/* 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;
2024-01-28 22:08:35 +00:00
/* 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;
2024-01-28 22:08:35 +00:00
/*
* 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 <= static_cast<float>(w) &&
hCheckY >= 0 && hCheckY <= static_cast<float>(h) && i < h) {
2024-01-28 22:08:35 +00:00
if (getBlock(floorf(hCheckX), floorf(hCheckY) + hRound) == BlockType::WALL) {
break;
}
2024-01-28 22:08:35 +00:00
hCheckX += hTan;
hCheckY += hDir;
i++;
}
2024-01-28 22:08:35 +00:00
i = 0;
float vCheckX = vOffsetX;
float vCheckY = originY + vOffsetY;
2024-01-28 22:08:35 +00:00
/* Bounds + sanity check. */
while (vCheckX >= 0 && vCheckX < static_cast<float>(w) &&
vCheckY >= 0 && vCheckY < static_cast<float>(h) && i < w) {
2024-01-28 22:08:35 +00:00
if (getBlock(floorf(vCheckX) + vRound, floorf(vCheckY)) == BlockType::WALL) {
break;
}
2024-01-28 22:08:35 +00:00
vCheckX += vDir;
vCheckY += vTan;
i++;
}
2024-01-28 22:08:35 +00:00
/*
* 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));
2024-01-28 22:08:35 +00:00
return hDist > vDist ? vDist : hDist;
}
void World::fillColumn(sf::RenderWindow& window, unsigned int column,
float scale, sf::Color fillColor)
{
float columnHeight = static_cast<float>(window.getSize().y)*scale;
sf::RectangleShape& pixelColumn = renderColumns[column];
if (pixelColumn.getSize().y != columnHeight) {
pixelColumn.setSize({1, columnHeight});
pixelColumn.setPosition(static_cast<float>(column),
(static_cast<float>(window.getSize().y) - columnHeight) / 2.0f);
}
if (pixelColumn.getFillColor() != fillColor)
pixelColumn.setFillColor(fillColor);
window.draw(pixelColumn);
}
void World::render(sf::RenderWindow& window)
{
float windowX = static_cast<float>(window.getSize().x);
float windowY = static_cast<float>(window.getSize().y);
2024-01-28 22:08:35 +00:00
/*
* 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(windowX,windowY/2.0f));
ground.setFillColor(groundColor);
ground.setPosition(0,windowY/2.0f);
2024-01-28 22:08:35 +00:00
sf::RectangleShape ceiling = sf::RectangleShape(sf::Vector2f(windowX,windowY/2.0f));
ceiling.setFillColor(ceilingColor);
2024-01-28 22:08:35 +00:00
window.draw(ground);
window.draw(ceiling);
const float worldToCamera = (player.focalLength*2)/player.sensorSize;
2024-01-28 22:08:35 +00:00
/*
* Throw rays and draw walls over the ceiling and ground.
* Only throws in the plane, which doesn't work for levels/3D.
*/
for(unsigned int i = 0 ; i < window.getSize().x ; i++)
{
float deltaAngle = (player.fov/windowX) * (static_cast<float>(i)-windowX/2.0f);
float rayAngle = player.orientation + deltaAngle;
if (rayAngle < 0) {
rayAngle += 360;
} else if (rayAngle > 360) {
rayAngle -= 360;
}
float obstacleScale = worldToCamera / (
castRay(player.x, player.y, rayAngle) *
cosf(deltaAngle*deg_to_rad)
);
2024-01-28 22:08:35 +00:00
/* 2 Is wall height in meters. */
fillColumn(window, i, obstacleScale);
}
}
void World::step(const float& stepTime) {
2024-01-28 22:08:35 +00:00
player.move(player.currentMoveSpeedX*stepTime,
player.currentMoveSpeedY*stepTime);
/* Undo last move if the player would end up in a wall. */
if (getBlock(player.x, player.y) != BlockType::AIR) {
player.move(-player.currentMoveSpeedX*stepTime,
-player.currentMoveSpeedY*stepTime);
}
2024-01-28 22:08:35 +00:00
player.rotate(player.currentRotationSpeed*stepTime);
#ifdef IMGUI
if (ImGui::Begin("MapEdit")) {
static int blockToPlace = 1;
/*
* Group all the select boxes together : makes it a big block in the
* Dear ImGui layout an allows adding things on the side.
*/
ImGui::BeginGroup();
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
ImGui::PushID(x + y * w);
if (x > 0)
ImGui::SameLine();
BlockType currentBlock = map[x + w * y];
ImVec4 hoverColor;
switch ((BlockType) blockToPlace) {
case BlockType::WALL:
hoverColor = (ImVec4) Colors::Wall;
break;
case BlockType::DOOR:
hoverColor = (ImVec4) Colors::Door;
break;
case BlockType::WINDOW:
hoverColor = (ImVec4) Colors::Window;
break;
default:
/* Default header color, it seems ? */
hoverColor = (ImVec4) ImColor(188, 120, 32);
break;
}
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, hoverColor);
ImVec4 blockColor;
switch (currentBlock) {
case BlockType::WALL:
blockColor = (ImVec4) Colors::Wall;
break;
case BlockType::DOOR:
blockColor = (ImVec4) Colors::Door;
break;
case BlockType::WINDOW:
blockColor = (ImVec4) Colors::Window;
break;
default:
blockColor = (ImVec4) ImColor(188, 120, 32);
break;
}
ImGui::PushStyleColor(ImGuiCol_Header, blockColor);
if (ImGui::Selectable("", currentBlock != BlockType::AIR, 0, ImVec2(10, 10))) {
map[x + w * y] =
currentBlock == (BlockType) blockToPlace ? BlockType::AIR : (BlockType) blockToPlace;
}
ImGui::PopStyleColor(2);
ImGui::PopID();
}
}
ImGui::EndGroup();
ImGui::SameLine();
ImGui::BeginGroup();
ImGui::Indent();
ImGui::Text("Place :");
ImGui::RadioButton("Wall", &blockToPlace, (int) BlockType::WALL);
ImGui::RadioButton("Door", &blockToPlace, (int) BlockType::DOOR);
ImGui::RadioButton("Window", &blockToPlace, (int) BlockType::WINDOW);
ImGui::Unindent();
ImGui::EndGroup();
}
ImGui::End();
#endif
}