5
\$\begingroup\$

I was reading about making fake 3D effects or so-called "mode 7" on 2D contexts. I have made a small program to demonstrate the fake 3D effect by using SFML. How can I improve it?

#include <SFML/Graphics.hpp>
#include <array>
#include <cmath>

struct P
{
    struct Screen
    {
        float x, y, w, scale;
    }screen;
    sf::Vector3f world, camera;
};

struct Color
{
    virtual ~Color() {}

};

struct Light : public Color
{
    sf::Color road = sf::Color(100, 100, 100), grass = sf::Color(0, 100, 0), rumble = sf::Color(100, 0, 0), lane = sf::Color::White;
}light;

struct Dark : public Color
{
    sf::Color road = sf::Color(100, 100, 100), grass = sf::Color(0, 150, 0), rumble = sf::Color::White;
}dark;

struct Segment
{
    P p1, p2;
    int index;
    Color* color;
};

class Polygon : public sf::Drawable, public sf::Transformable
{
public:
    Polygon(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, sf::Color color)
    {
        m_vertices.setPrimitiveType(sf::Quads);
        m_vertices.resize(4);

        m_vertices[0].position = sf::Vector2f(x1, y1);
        m_vertices[1].position = sf::Vector2f(x2, y2);
        m_vertices[2].position = sf::Vector2f(x3, y3);
        m_vertices[3].position = sf::Vector2f(x4, y4);
        m_vertices[0].color = m_vertices[1].color = m_vertices[2].color = m_vertices[3].color = color;

    }
private:

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

        target.draw(m_vertices, states);
    }

    sf::VertexArray m_vertices;
};

void project(P& p, float cameraX, float cameraY, float cameraZ, float cameraDepth, float width, float height, float roadWidth)
{
    p.camera.x = p.world.x - cameraX;
    p.camera.y = p.world.y - cameraY;
    p.camera.z = p.world.z - cameraZ;
    p.screen.scale = cameraDepth / p.camera.z;
    p.screen.x = std::round((width / 2) + (p.screen.scale * p.camera.x  * width / 2));
    p.screen.y = std::round((height / 2) - (p.screen.scale * p.camera.y  * height / 2));
    p.screen.w = std::round((p.screen.scale * roadWidth   * width / 2));
}

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

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

void renderSegment(sf::RenderTarget& target, float width, int lanes, float x1, float y1, float w1, float x2, float y2, float w2, Color* c)
{
    sf::RectangleShape rect(sf::Vector2f(width, y1 - y2));
    rect.setPosition(0, y2);

    float r1 = rumbleWidth(w1, lanes),
    r2 = rumbleWidth(w2, lanes),
    l1 = laneMarkerWidth(w1, lanes),
    l2 = laneMarkerWidth(w2, lanes),
    lanew1, lanew2, lanex1, lanex2, lane;

    Light* p = dynamic_cast<Light*>(c);
    if (p)
    {
        rect.setFillColor(p->grass);
        Polygon polygon(x1 - w1 - r1, y1, x1 - w1, y1, x2 - w2, y2, x2 - w2 - r2, y2, p->rumble);
        Polygon polygon1( x1 + w1 + r1, y1, x1 + w1, y1, x2 + w2, y2, x2 + w2 + r2, y2, p->rumble);
        Polygon polygon2( x1 - w1, y1, x1 + w1, y1, x2 + w2, y2, x2 - w2, y2, p->road);
        target.draw(rect);
        target.draw(polygon);
        target.draw(polygon1);
        target.draw(polygon2);
    }
    else
    {
        Dark* d = dynamic_cast<Dark*>(c);
        if (d)
        {
            rect.setFillColor(d->grass);
            Polygon polygon(x1 - w1 - r1, y1, x1 - w1, y1, x2 - w2, y2, x2 - w2 - r2, y2, d->rumble);
            Polygon polygon1(x1 + w1 + r1, y1, x1 + w1, y1, x2 + w2, y2, x2 + w2 + r2, y2, d->rumble);
            Polygon polygon2(x1 - w1, y1, x1 + w1, y1, x2 + w2, y2, x2 - w2, y2, d->road);
            target.draw(rect);
            target.draw(polygon);
            target.draw(polygon1);
            target.draw(polygon2);
        }
    }

    lanew1 = w1 * 2 / lanes;
    lanew2 = w2 * 2 / lanes;
    lanex1 = x1 - w1 + lanew1;
    lanex2 = x2 - w2 + lanew2;

    for (lane = 1; lane < lanes; lanex1 += lanew1, lanex2 += lanew2, lane++)
    {
        Light* p = dynamic_cast<Light*>(c);
        if (p)
        {
            Polygon polygon(lanex1 - l1 / 2, y1, lanex1 + l1 / 2, y1, lanex2 + l2 / 2, y2, lanex2 - l2 / 2, y2, p->lane);
            target.draw(polygon);
        }
    }

}

int main()
{
    sf::RenderWindow window(sf::VideoMode(800, 600), "test");

    float roadWidth = 2000;                    
    float segmentLength = 200;
    float width = 800;                    
    float height = 600;                     
    int drawDistance = 300;                    
    float playerX = 0;                      
    float playerZ = 0;                    
    float cameraHeight = 1000;                  
    int fieldOfView = 100;
    float cameraDepth = 1 / std::tan((fieldOfView / 2) * 3.141592653589793238462643383f / 180);
    float position = 0;                      
    int rumbleLength = 3;                       
    int lanes = 3;                     
    std::array<Segment, 500> segments;

    for (auto i = 0u; i < segments.size(); ++i)
    {
        Segment s;
        s.index = i;
        s.p1.world.z = i * segmentLength;
        s.p2.world.z = (i + 1) * segmentLength;

        if (static_cast<std::size_t>(std::floor(i / rumbleLength)) % 2)
            s.color = &light;
        else
            s.color = &dark;

        segments[i] = s;
    }

    const Segment& baseSegment = segments[static_cast<std::size_t>(std::floor(position / segmentLength)) % segments.size()];


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

        window.clear();

        for (int n = 0; n < drawDistance; n++)
        {
            Segment& s= segments[(baseSegment.index + n) % segments.size()];

            float camX = playerX * roadWidth;
            float camY = cameraHeight;
            float camZ = position;
            project(s.p1, camX, camY, camZ, cameraDepth, width, height, roadWidth);
            project(s.p2, camX, camY, camZ, cameraDepth, width, height, roadWidth);

            renderSegment(window, width, lanes, s.p1.screen.x, s.p1.screen.y, s.p1.screen.w, s.p2.screen.x, s.p2.screen.y, s.p2.screen.w, s.color);
        }

        window.display();
    }
}

Output:

enter image description here

\$\endgroup\$
2
\$\begingroup\$

Looking pretty cool! :)

Think about meaningful names

There are a few issues with naming though. What exactly is a struct P? And when you instantiate it inside Segment, can you think of better names than p1 and p2? The renderSegment function is also heavy on single letter + number names, such as w1,l1,r1, etc. That function is by far the most intimidating piece in your code. Better names would make it a lot easier to read and understand.

One declaration per line

Something like this is completely unnecessary:

struct Light : public Color
{
    sf::Color road = sf::Color(100, 100, 100), grass = sf::Color(0, 100, 0), rumble = sf::Color(100, 0, 0), lane = sf::Color::White;
}light;

It doesn't matter if you're going to have to repeat sf::Color for each declaration, that's still better than an absurdly compound statement like the above. Lines with multiple declaration will demand slightly more mental effort from the reader to identify everything in there, this can lead to overlooking important things when you are tired. Not to mention the need for horizontal scroll.

 struct Light : public Color
 {
     sf::Color road   { 100, 100, 100    };
     sf::Color grass  { 0,   100, 0      }; 
     sf::Color rumble { 100, 0,   0      };
     sf::Color lane   { sf::Color::White };
 } light;

Good comments are good

Some of your function are a little more involving, such as project(). It would be nice to have a summary comment at the top of the function describing the high-level algorithm is it implementing, why is the rounding necessary, etc. Links to external sites/tutorials with more step-by-step descriptions are always welcome.

You should do the same with rumbleWidth and laneMarkerWidth to explain what is going one in them. If you provide a good comment for each, you can even leave the magic numbers used inside.

dynamic_cast == bad class hierarchy design

Using dynamic_cast at runtime to infer the underlaying type of an interface class is a clear indication that the class hierarchy is deficient. Doing this defeats the very purpose of an interface/base-class, which is to hide the implementation under a common type. renderSegment is meant to be a pure virtual member of the base class (Color) that each subclass implements to draw itself. No need for dynamic_casting once you do that.

Try M_PI before defining your own

Don't redefine the constant PI on your own, risking using less precision than the actual supported. <cmath> defines the macro constant M_PI. You can test that it is defined, then if not, provide a fallback:

#ifndef M_PI
    #define M_PI 3.141592653589793238462643383
#endif 
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.