1
0
Fork 0
Toy-Raytracer/World.cpp
trotFunky f7e1b3eab9 World: Fix wrong block type being returned in castRay
The GetBlock() call has specific offsets that are angle and axis dependent to get the correct
square on the grid from the coordinates of a hit.
This is something that I forgot when introducing the RaycastResult struct, and just used
the first GetBlock() as a common call, returning wrong types in some cases.

As with the hit coordinates, save the type of block hit and use that depending on
the closest hit later, rather than a common and wrong GetBlock().
2024-02-05 23:04:19 +00:00

384 lines
10 KiB
C++

//
// Created by trotfunky on 27/05/19.
//
#include "World.h"
#include <cmath>
#ifdef IMGUI
#include <imgui.h>
#include <imgui-SFML.h>
#endif
#include "FrameTracing.h"
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)
{
map.resize(w*h,BlockType::WALL);
}
int World::getW() const
{
return w;
}
int World::getH() const
{
return h;
}
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
{
return map[x + w*y];
}
BlockType World::getBlock(float x, float y) const
{
return map[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);
}
RaycastResult 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°
* - 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.
*/
FTrace_Scope;
/* 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;
BlockType hHitBlock = BlockType::AIR;
/* Bounds + sanity check. */
while (hCheckX >= 0 && hCheckX <= static_cast<float>(w) &&
hCheckY >= 0 && hCheckY <= static_cast<float>(h) && i < h) {
hHitBlock = getBlock(floorf(hCheckX), floorf(hCheckY) + hRound);
if (hHitBlock == BlockType::WALL) {
break;
}
hCheckX += hTan;
hCheckY += hDir;
i++;
}
i = 0;
float vCheckX = vOffsetX;
float vCheckY = originY + vOffsetY;
BlockType vHitBlock = BlockType::AIR;
/* Bounds + sanity check. */
while (vCheckX >= 0 && vCheckX < static_cast<float>(w) &&
vCheckY >= 0 && vCheckY < static_cast<float>(h) && i < w) {
vHitBlock = getBlock(floorf(vCheckX) + vRound, floorf(vCheckY));
if (vHitBlock == 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));
RaycastResult result{};
if (hDist > vDist) {
result.distance = vDist;
result.hitX = vCheckX;
result.hitY = vCheckY;
result.hitBlock = vHitBlock;
} else {
result.distance = hDist;
result.hitX = hCheckX;
result.hitY = hCheckY;
result.hitBlock = hHitBlock;
}
return result;
}
void World::fillColumn(sf::RenderWindow& window, unsigned int column,
float scale, sf::Color fillColor)
{
FTrace_Scope;
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)
{
FTrace_Scope;
float windowX = static_cast<float>(window.getSize().x);
float windowY = static_cast<float>(window.getSize().y);
/*
* 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);
sf::RectangleShape ceiling = sf::RectangleShape(sf::Vector2f(windowX,windowY/2.0f));
ceiling.setFillColor(ceilingColor);
window.draw(ground);
window.draw(ceiling);
const float worldToCamera = (player.focalLength*2)/player.sensorSize;
/*
* 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).distance *
cosf(deltaAngle*deg_to_rad)
);
/* 2 Is wall height in meters. */
fillColumn(window, i, obstacleScale);
}
}
void World::step(const float& stepTime) {
FTrace_Scope;
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);
}
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
}