# Fake 3D effect in SFML - follow up 2

Based on my previous question, I have implemented all the recommendations received. In addition, i have implemented new features and completed road geometries.

Here is a summary of the improvements:

• Rewritten the Colors class again instead of old class which is used hierarchy design.
• Added fake fog effect to the scene (no Opengl used)

I would like to know how can I improve it further.

#define _USE_MATH_DEFINES
#include <SFML/Graphics.hpp>
#include <vector>
#include <array>
#include <memory>
#include <cmath>
#include <random>
#include <stdexcept>
#include <iostream>

#ifndef M_PI
#define M_PI 3.141592653589793238462643383
#endif

#ifndef M_E
#define M_E 2.71828182845904523536
#endif

namespace
{
float increase(float start, float increment, float max)
{
auto result = start + increment;

while (result >= max)
result -= max;

while (result < 0)
result += max;

return result;
}

float limit(float value, float min, float max)
{
return std::max(min, std::min(value, max));
}

{
return projectedRoadWidth / std::max(6u, 2 * lanes);
}

{
return projectedRoadWidth / std::max(32u, 8 * lanes);
}

float exponentialFog(float distance, float density)
{
return static_cast<float>(1 / std::pow(M_E, (distance * distance * density)));
}

float easeIn(float a, float b, float percent)
{
return a + (b - a) * std::pow(percent, 2.f);
}

float easeOut(float a, float b, float percent)
{
return a + (b - a) * (1 - std::pow(1.f - percent, 2.f));
}

float easeInOut(float a, float b, float percent)
{
return a + (b - a) * (static_cast<float>(-std::cos(percent * M_PI) / 2.f) + 0.5f);
}

float interpolate(float a, float b, float percent)
{
return a + (b - a)*percent;
}

float percentRemaining(float n, float total)
{
return (static_cast<int>(n) % static_cast<int>(total)) / total;
}
}

class Polygon final : public sf::Drawable, public sf::Transformable, sf::NonCopyable
{
public:

void setVertices(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, sf::Color color)
{
mVertices[0].position = sf::Vector2f(x1, y1);
mVertices[1].position = sf::Vector2f(x2, y2);
mVertices[2].position = sf::Vector2f(x3, y3);
mVertices[3].position = sf::Vector2f(x4, y4);

mVertices[0].color = mVertices[1].color = mVertices[2].color = mVertices[3].color = color;
}

private:
void draw(sf::RenderTarget& target, sf::RenderStates states) const override
{
states.transform *= getTransform();

target.draw(mVertices, states);
}

private:
sf::VertexArray mVertices;
};

struct Point
{
struct Screen
{
float x{};
float y{};
float w{};

}screen{};

sf::Vector3f world{};
sf::Vector3f camera{};

void project(float cameraX, float cameraY, float cameraZ, float cameraDepth, float width, float height, float roadWidth)
{
camera.x = world.x - cameraX;
camera.y = world.y - cameraY;
camera.z = world.z - cameraZ;

auto scale = cameraDepth / camera.z;
screen.x = width / 2.f + scale * camera.x  * width / 2.f;
screen.y = height / 2.f - scale * camera.y  * height / 2.f;
screen.w = scale * roadWidth * width / 2.f;
}
};

class Colors
{
struct ColorsData
{
sf::Color grass{};
sf::Color rumble{};
sf::Color lanes{};
};

using ColorsContainer = std::vector<ColorsData>;

public:
enum Type
{
Light,
Dark,
Start,
Finish,
TypeCount
};

public:
Colors(Type type = Colors::Light)
: mType(type)
, mData(Colors::TypeCount)
{
mData[Colors::Light].road = { 100, 100, 100 };
mData[Colors::Light].grass = { 16, 170, 16 };
mData[Colors::Light].rumble = { 85, 85 , 85 };
mData[Colors::Light].lanes = sf::Color::White;

mData[Colors::Dark].road = { 100, 100, 100 };
mData[Colors::Dark].grass = { 0, 154, 0 };
mData[Colors::Dark].rumble = { 187,187, 187 };
mData[Colors::Dark].lanes = { 100, 100, 100 };

mData[Colors::Start].grass = { 16, 170, 16 };
mData[Colors::Start].rumble = sf::Color::White;
mData[Colors::Start].lanes = sf::Color::White;

mData[Colors::Finish].grass = { 16, 170, 16 };
mData[Colors::Finish].rumble = {};
mData[Colors::Finish].lanes = {};
}

void setType(Type type) { mType = type;}

sf::Color getGrass() const { return mData[mType].grass;}
sf::Color getRumble() const { return mData[mType].rumble;}
sf::Color getLane() const { return mData[mType].lanes;}

private:
Type mType;
ColorsContainer mData;
};

class Segment final : public sf::Drawable, public sf::Transformable, sf::NonCopyable
{
public:
using Ptr = std::unique_ptr<Segment>;

public:
void setCurve(float i) { mCurve = i; }
float getCurve() const { return mCurve; }

Point&  getPoint1() { return mPoint1; }
Point&  getPoint2() { return mPoint2; }
const Point&  getPoint1() const { return mPoint1; }
const Point&  getPoint2() const { return mPoint2; }

void setSegmentColors(Colors::Type c) { mColors.setType(c); }

void setIndex(std::size_t i) { mIndex = i; }
std::size_t getIndex() const { return mIndex; }

void setGrounds(float width, float fog)
{
auto lanes = 3u;

// Landscape
mLandscape.setSize({ width, mPoint1.screen.y - mPoint2.screen.y });
mLandscape.setPosition(0, mPoint2.screen.y);
mLandscape.setFillColor(mColors.getGrass());

// Rumble sides
auto rumbleWidth1 = rumbleWidth(mPoint1.screen.w, lanes);
auto rumbleWidth2 = rumbleWidth(mPoint2.screen.w, lanes);

mRumbleSide1.setVertices(mPoint1.screen.x - mPoint1.screen.w - rumbleWidth1, mPoint1.screen.y,
mPoint1.screen.x - mPoint1.screen.w, mPoint1.screen.y,
mPoint2.screen.x - mPoint2.screen.w, mPoint2.screen.y,
mPoint2.screen.x - mPoint2.screen.w - rumbleWidth2, mPoint2.screen.y, mColors.getRumble());

mRumbleSide2.setVertices(mPoint1.screen.x + mPoint1.screen.w + rumbleWidth1, mPoint1.screen.y,
mPoint1.screen.x + mPoint1.screen.w, mPoint1.screen.y,
mPoint2.screen.x + mPoint2.screen.w, mPoint2.screen.y,
mPoint2.screen.x + mPoint2.screen.w + rumbleWidth2, mPoint2.screen.y, mColors.getRumble());

mPoint1.screen.x + mPoint1.screen.w, mPoint1.screen.y,
mPoint2.screen.x + mPoint2.screen.w, mPoint2.screen.y,

// Lanes
auto laneMarkerWidth1 = laneMarkerWidth(mPoint1.screen.w, lanes);
auto laneMarkerWidth2 = laneMarkerWidth(mPoint2.screen.w, lanes);
auto lanew1 = mPoint1.screen.w * 2 / lanes;
auto lanew2 = mPoint2.screen.w * 2 / lanes;
auto lanex1 = mPoint1.screen.x - mPoint1.screen.w + lanew1;
auto lanex2 = mPoint2.screen.x - mPoint2.screen.w + lanew2;

for (auto lane = 1u; lane < lanes; lanex1 += lanew1 + 1, lanex2 += lanew2 + 1, lane++)
{
if (lane == 1)
mLanes1.setVertices(lanex1 - laneMarkerWidth1 / 2, mPoint1.screen.y,
lanex1 + laneMarkerWidth1 / 2, mPoint1.screen.y,
lanex2 + laneMarkerWidth2 / 2, mPoint2.screen.y,
lanex2 - laneMarkerWidth2 / 2, mPoint2.screen.y, mColors.getLane());
else
mLanes2.setVertices(lanex1 - laneMarkerWidth1 / 2, mPoint1.screen.y,
lanex1 + laneMarkerWidth1 / 2, mPoint1.screen.y,
lanex2 + laneMarkerWidth2 / 2, mPoint2.screen.y,
lanex2 - laneMarkerWidth2 / 2, mPoint2.screen.y, mColors.getLane());
}

// Fog effect
mFog.setSize({ width, mPoint1.screen.y - mPoint2.screen.y });
mFog.setPosition(0, mPoint1.screen.y);
mFog.setFillColor(sf::Color(255, 255, 255, 255 - static_cast<unsigned char>(fog * 255)));
}

private:
void draw(sf::RenderTarget& target, sf::RenderStates states) const override
{
states.transform *= getTransform();
target.draw(mLandscape, states);
target.draw(mRumbleSide1, states);
target.draw(mRumbleSide2, states);
target.draw(mLanes1, states);
target.draw(mLanes2, states);
target.draw(mFog, states);
}

private:
Point mPoint1{};
Point mPoint2{};
Polygon mRumbleSide1{};
Polygon mRumbleSide2{};
Polygon mLanes1{};
Polygon mLanes2{};
sf::RectangleShape mLandscape{};
sf::RectangleShape mFog{};
Colors mColors{};
std::size_t mIndex{};
float mCurve{};
};

{
struct Length
{
const float shorty = 25.f;
const float  medium = 50.f;
const float  longy = 100.f;
} length;

struct Curve
{
const float easy = 2.f;
const float  medium = 4.f;
const float  hard = 6.f;
} curve;

struct  Hill
{
const float none = 0.f;
const float  low = 20.f;
const float medium = 40.f;
const float  high = 60.f;
}hill;

class Game
{
using SegmentContainer = std::vector<Segment::Ptr>;

public:
Game()
: mWindow(sf::VideoMode(640, 480), "test")
, mSegments()
, mSegmentLength(200.f)
, mPlayerX(0.f)
, mCameraDepth(1 / std::atan((100.f / 2.f)))
, mCameraHeight(1000.f)
, mPosition(0.f)
, mRumbleLength(3u)
, mTrackLength(0.f)
, mSpeed(0.f)
{

// setup start and finish of road
mSegments[mSegments[static_cast<std::size_t>(std::floor(mPlayerZ / mSegmentLength)) % mSegments.size()]->getIndex() + 2]->setSegmentColors(Colors::Start);
mSegments[mSegments[static_cast<std::size_t>(std::floor(mPlayerZ / mSegmentLength)) % mSegments.size()]->getIndex() + 3]->setSegmentColors(Colors::Start);

for (auto n = 0u; n < mRumbleLength; n++)
{
mSegments[mSegments.size() - 1 - n]->setSegmentColors(Colors::Finish);
}

mTrackLength = mSegments.size() * mSegmentLength;
}

void run()
{
sf::Clock clock;
auto timeSinceLastUpdate = sf::Time::Zero;

while (mWindow.isOpen())
{
auto elapsedTime = clock.restart();
timeSinceLastUpdate += elapsedTime;

while (timeSinceLastUpdate > TimePerFrame)
{
timeSinceLastUpdate -= TimePerFrame;

processEvents();
update(TimePerFrame);
}

render();
}
}

private:
void processEvents()
{
sf::Event event;

while (mWindow.pollEvent(event))
{
if (event.type == sf::Event::Closed)
mWindow.close();
}
}

void update(sf::Time TimePerFrame)
{
auto dt = TimePerFrame.asSeconds();
auto step = 1 / 60.f;
auto maxSpeed = mSegmentLength / step;
auto accel = maxSpeed / 5.f;
auto breaking = -maxSpeed;
auto decel = -maxSpeed / 5.f;
auto offRoadDecel = -maxSpeed / 2.f;
auto offRoadLimit = maxSpeed / 4.f;
auto centrifugal = 0.3f;

const auto& playerSegment = *mSegments[static_cast<std::size_t>(std::floor((mPosition + mPlayerZ) / mSegmentLength)) % mSegments.size()];
auto speedPercent = mSpeed / maxSpeed;
auto dx = dt * speedPercent;

if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
mPlayerX -= dx;

if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
mPlayerX += dx;

mPlayerX -= (dx * speedPercent * playerSegment.getCurve() * centrifugal);

if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up))
mSpeed += accel * dt;
else
mSpeed += decel * dt;

if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down))
mSpeed += breaking * dt;

if (((mPlayerX < -1.f) || (mPlayerX > 1.f)) && (mSpeed > offRoadLimit))

mPlayerX = limit(mPlayerX, -2.f, 2.f);
mSpeed = limit(mSpeed, 0, maxSpeed);

mPosition = increase(mPosition, dt * mSpeed, mTrackLength);
}

void render()
{
auto width = 640.f;
auto height = 480.f;
auto drawDistance = 500u;
auto fogDensity = 5.f;

const auto& baseSegment = *mSegments[static_cast<std::size_t>(std::floor(mPosition / mSegmentLength)) % mSegments.size()];
auto basePercent = percentRemaining(mPosition, mSegmentLength);

const auto& playerSegment = *mSegments[static_cast<std::size_t>(std::floor((mPosition + mPlayerZ) / mSegmentLength)) % mSegments.size()];
auto playerPercent = percentRemaining(mPosition + mPlayerZ, mSegmentLength);
auto playerY = interpolate(playerSegment.getPoint1().world.y, playerSegment.getPoint2().world.y, playerPercent);

auto x = 0.f;
auto dx = -(baseSegment.getCurve() * basePercent);
auto maxy = height;

mWindow.clear();

for (auto n = 0u; n < drawDistance; ++n)
{
auto& segment = *mSegments[(baseSegment.getIndex() + n) % mSegments.size()];

bool looped = segment.getIndex() < baseSegment.getIndex();
auto fog = exponentialFog(n / (drawDistance * 1.f), fogDensity);

auto camX = mPlayerX * roadWidth;
auto camY = playerY + mCameraHeight;
auto camZ = mPosition - (looped ? mTrackLength : 0.f);

auto& point1 = segment.getPoint1();
auto& point2 = segment.getPoint2();

x += dx;
dx += segment.getCurve();

if ((point1.camera.z <= mCameraDepth) || (point2.screen.y >= maxy || point2.screen.y >= point1.screen.y))
continue;

segment.setGrounds(width, fog);

mWindow.draw(segment);

maxy = point2.screen.y;
}

mWindow.display();
}

{
auto n = mSegments.size();

auto segment = std::make_unique<Segment>();

segment->setIndex(n);
segment->setCurve(curve);

segment->getPoint1().world.y = lastY();
segment->getPoint2().world.y = y;

segment->getPoint1().world.z = n * mSegmentLength;
segment->getPoint2().world.z = (n + 1) * mSegmentLength;

if (static_cast<std::size_t>(std::floor(n / mRumbleLength)) % 2)
segment->setSegmentColors(Colors::Light);
else
segment->setSegmentColors(Colors::Dark);

mSegments.push_back(std::move(segment));
}

{
auto startY = lastY();
auto endY = startY + (y * mSegmentLength);
auto total = enter + hold + leave;

for (auto n = 0.f; n < enter; ++n)
addSegment(easeIn(0, curve, n / enter), easeInOut(startY, endY, n / total));

for (auto n = 0.f; n < hold; ++n)
addSegment(curve, easeInOut(startY, endY, (enter + n) / total));

for (auto n = 0.f; n < leave; ++n)
addSegment(easeInOut(curve, 0, n / leave), easeInOut(startY, endY, (enter + hold + n) / total));
}

{
auto num = (n == 0) ? road.length.medium : n;
}

void  addHill(float n = 0, float h = 0)
{
auto num = (n == 0) ? road.length.medium : n;
auto height = (h == 0) ? road.hill.medium : h;

}

void  addCurve(float n = 0, float c = 0, float h = 0)
{
auto num = (n == 0) ? road.length.medium : n;
auto curve = (c == 0) ? road.curve.medium : c;
auto height = (h == 0) ? road.hill.none : h;

}

{
auto num = (n == 0) ? 200 : n;
}

{
}

void addLowRollingHills(float n = 0, float h = 0)
{
auto num = (n == 0) ? road.length.shorty : n;
auto height = (h == 0) ? road.hill.low : h;

}

float lastY()
{
return (mSegments.size() == 0) ? 0 : mSegments[mSegments.size() - 1]->getPoint2().world.y;
}

private:
sf::RenderWindow mWindow;

const static sf::Time TimePerFrame;

SegmentContainer mSegments;
std::size_t mRumbleLength;
float mSegmentLength;
float mPlayerX;
float mCameraHeight;
float mPlayerZ;
float mPosition;
float mTrackLength;
float mSpeed;
};

const sf::Time Game::TimePerFrame = sf::seconds(1 / 60.f);

int main()
{
try
{
Game game;
game.run();
}
catch (std::exception& e)
{
std::cout << "\nEXCEPTION: " << e.what() << std::endl;
}
}


Output: