简体   繁体   中英

What's the fastest way to convert a 2D L-system to lines

I'm building a 3D graphics engine, and I want to draw 2D L-systems. But I noticed that this gets quite slow, once you increase the number of iterations. I'm searching a way to rapidly expand my L-system into a vector<Line> , with Line a class containing 2 points. this is my current code:

// LParser::LSystem2D contains the L-system (replacement rules, angle increase, etc..)
// the turtle is a class I use to track the current angle and position as I generate lines
// Lines2D is a std::list of Lines (with lines a class containing 2 points and a color)
void expand(char c, const LParser::LSystem2D &ls2D, Turtle &T, Lines2D &L2D, const Color &line_color, int max_depth,
            int depth = 0) {
    const std::string str = ls2D.get_replacement(c);
    for (const auto &character: str) {
        if (character == '+' || character == '-') {
            T.angle += (-((character == '-') - 0.5) * 2) * ls2D.get_angle(); // adds or subtracts the angle
            continue;
        } else if (character == '(') {
            T.return_pos.push({T.pos, T.angle}); // if a bracket is opened the current position and angle is stored
            continue;
        } else if (character == ')') {
            T.pos = T.return_pos.top().first; // if a bracket is closed we return to the stored position and angle
            T.angle = T.return_pos.top().second;
            T.return_pos.pop();
            continue;
        } else if (max_depth > depth + 1) {
            expand(character, ls2D, T, L2D, line_color, max_depth, depth + 1); // recursive call
        } else {
            // max depth is reached, we add the line to Lines2D
            L2D.emplace_back(Line2D(
                    {T.pos, {T.pos.x + cos(toRadians(T.angle)), T.pos.y + sin(toRadians(T.angle))}, line_color}));
            T.pos = {T.pos.x + cos(toRadians(T.angle)), T.pos.y + sin(toRadians(T.angle))};
        };
    }
}

Lines2D gen_lines(const LParser::LSystem2D &ls2D, const Color &line_color) {
    std::string init = ls2D.get_initiator();
    Lines2D L2D;
    Turtle T;
    T.angle = ls2D.get_starting_angle();
    for (const auto &c:init) {
        if (c == '+' || c == '-') {
            T.angle += (-((c == '-') - 0.5) * 2) * ls2D.get_angle();
            continue;
        } else if (c == '(') {
            T.return_pos.push({T.pos, T.angle});
            continue;
        } else if (c == ')') {
            T.pos = T.return_pos.top().first;
            T.angle = T.return_pos.top().second;
            T.return_pos.pop();
            continue;
        }
        expand(c, ls2D, T, L2D, line_color, ls2D.get_nr_iterations());
    }
    return L2D;
}
Alphabet = {L, R, F}

Draw = {
       L -> 1,
       R -> 1,
       F -> 1
}

Rules = {
       L -> "+RF-LFL-FR+",
       R -> "-LF+RFR+FL-",
       F -> "F"
}

Initiator     = "L"
Angle         = 90
StartingAngle = 0
Iterations    = 4

L-system example

I couldn't think of any way to increase performance (significantly). I though about multihtreading but you would need to now your position at the beginning of every thread, but then you would need to expand al the previous character.

Is there a more efficient algorithm to do this task? Or a way to implement this so I could use multithreading?

EDIT: I've looked into the answers and this is what I came up with, this increased performance, but one drawback is that my program will use more ram(and I'm limited to 2GB, which is alot but still.) One solution is using a queue, but this decreases performance.

Lines2D LSystem2DParser::generateLines() {
    Lines2D lines;
    drawing = l_system2d.get_initiator();
    Timer T;
    expand();
    T.endTimer("end of expand: ");
    Timer T2;
    lines = convert();
    T2.endTimer("end of convert: ");
    return lines;
}

void LSystem2DParser::expand() {
    if (depth >= max_depth) {
        return;
    }
    std::string expansion;
    for (char c : drawing) {
        switch (c) {
            case '+':
            case '-':
            case '(':
            case ')':
                expansion += c;
                break;
            default:
                expansion += replacement_rules[c];
                break;
        }
    }
    drawing = expansion;

    depth++;
    expand();
}

Lines2D LSystem2DParser::convert() {
    Lines2D lines;
    double current_angle = toRadians(l_system2d.get_starting_angle());
    double x = 0, y = 0, xinc = 0, yinc = 0;
    std::stack<std::array<double, 3>> last_pos;
    for (char c: drawing){
        switch (c) {
            case('+'):
                current_angle += angle;
                xinc = cos(current_angle);
                yinc = sin(current_angle);
                break;
            case ('-'):
                xinc = cos(current_angle);
                yinc = sin(current_angle);
                break;
            case ('('):
                last_pos.push({x, y, current_angle});
                break;
            case (')'):
                x = last_pos.top()[0];
                y = last_pos.top()[1];
                current_angle = last_pos.top()[2];
                last_pos.pop();
                break;
            default:
                lines.emplace_back(Line2D(Point2D(x,y), Point2D(x+xinc, y+yinc), line_color));
                x += xinc;
                y += yinc;
                break;
        }
    }
    return Lines2D();
}

EDIT 2: It's still slow, in comparison to the code posted below

EDIT 3: https://github.com/Robin-Dillen/3DEngine all the code

EDIT 4: having a weird bug with a loop not ending

    for (std::_List_const_iterator<Point2D> point = ps.begin(); point != ps.end(); point++) {
        std::_List_const_iterator<Point2D> point2 = point++;
        img.draw_line(roundToInt(point->x * d + dx), roundToInt(point->y * d + dy), roundToInt(point2->x * d + dx),
                      roundToInt(point2->y * d + dy), line_color.convert());
    } 

Generally speaking, the first step in optimizing the performance of an application is to profile the code to see where exactly the most time is being spent. Without this step, a lot of effort can be wasted optimizing code that actually has little impact on performance.

However, in this particular case, I would look to simplifying your code so it is easier to see what is going on and so make it easier to interpret the results of performance profiling.

Your recursive function expand could be streamlined by

  1. Moving all those parameters out of the signature. There is no need to place so many copies on the same things on stack!
  2. The first thing a recursive function should do is check if recursion is complete. In this case, check the depth.
  3. The second thing, if further recursion is required, is perform the preparation of the next call. In this case, production of the next string from the current.
  4. Finally, the recursive function can be called.

Below I will post code that implements Lindenmayer's original L-system for modelling the growth of algae. This is much simpler that what you are doing, but hopefully showsthe the method and benefit of re-organizing recursive code into the "standard" style of doing recursion.

Is there a more efficient algorithm to do this task?

I doubt it. I suspect that you could improve your implementation, but it is hard to know without profiling your code.

a way to implement this so I could use multithreading?

Recursive algorithms are not good candidates for multithreading.

Here is simple code implementing a similar recursive algorithm

#include <iostream>
#include <map>

using namespace std;

class cL
{
public:
    cL()
        : myAlphabet("AB")
    {
    }

    void germinate(
        std::map< char, std::string>& rules,
        const std::string& axiom,
        int generations )
    {
        myRules = rules;
        myMaxDepth = generations;
        myDepth = 0;
        myPlant = axiom;
        grow();
    }

private:
    std::string myAlphabet;
    std::map< char, std::string> myRules;
    int myDepth;
    int myMaxDepth;
    std::string myPlant;

    std::string production( const char c )
    {

        if( (int)myAlphabet.find( c ) < 0 )
            throw std::runtime_error(
                "production character not in alphabet");
        auto it = myRules.find( c );
        if( it == myRules.end() )
            throw std::runtime_error(
                "production missing rule");
        return it->second;
    }

    /// recursive growth

    void grow()
    {
        // check for completion
        if( myDepth == myMaxDepth )
        {
            std::cout << myPlant << "\n";
            return;
        }

        // produce the next growth spurt
        std::string next;
        for( auto c : myPlant )
        {
            next += production( c );
        }
        myPlant = next;

        // recurse
        myDepth++;
        grow();
    }
};

int main()
{
    cL L;
    std::map< char, std::string> Rules;
    Rules.insert(std::make_pair('A',std::string("AB")));
    Rules.insert(std::make_pair('B',std::string("A")));
    for( int d = 2; d < 10; d++ )
    {
        L.germinate( Rules, "A", d );
    }
    return 0;
}
   L2D.emplace_back(Line2D(
            {T.pos, {T.pos.x + cos(toRadians(T.angle)), T.pos.y + sin(toRadians(T.angle))}, line_color}));
    T.pos = {T.pos.x + cos(toRadians(T.angle)), T.pos.y + sin(toRadians(T.angle))};

Without profiling it is hard to know how important this is. However:

  1. Why not store the angle in radians, instead of converting it to radians over and over?
  2. If using radians would be problematical somewhere else, at least do the conversion once and store in local
  3. Would be a good idea to add a Line2D constructor that takes a Turtle reference as a parameter and does its own calculations.
  4. Is the recalculation of T.pos needed? Isn't the recusion now complete?

I have implemented a Lsystem to generate and draw a Sierpinski ( https://en.wikipedia.org/wiki/L-system#Example_5:_Sierpinski_triangle ) This is very similar to what your are doing. I have implemented in a straightforward way with no tricks. Here is the result of time profiling the code for an iteration depth of 11.

raven::set::cRunWatch code timing profile
Calls           Mean (secs)     Total           Scope
       1        0.249976        0.249976        draw
      11        0.0220157       0.242172        grow

grow is the recursive function. It is called 11 times, with a mean execution time of 22 milliseconds.

draw is the function that takes the final string produced and draws it on the screen. This is called once and needs 250 msec.

The conclusion from this is that the recursive function does not require optimization, since 50% of the application time is used by the drawing.

In your question you do not provide time profiling data, nor even what you mean by "quite slow". I would say that if your code takes more than, say, 100 milliseconds to generate ( not draw ) the final string, then you have a problem which is being caused by a poor implementation of the standard algorithm. If, however, the slowness you complain of is dues to the drawing of the lines, then your problem is likely with a poor choice of graphics library - some graphics libraries do even simple things like drawing lines hundreds of time faster than others.

I invite you take a look at my code at https://github.com/JamesBremner/Lindenmayer/blob/main/main.cpp

If you just want to parse the string and save the lines to a vector, then things go even faster since no graphics library is involved.

raven::set::cRunWatch code timing profile
Calls           Mean (secs)     Total           Scope
      11        0.00229241      0.0252165       grow
       1        0.0066558       0.0066558       VectorLines

Here is the code

std::vector<std::pair<int,int> > 
VectorLines( const std::string& plant )
{
        raven::set::cRunWatch aWatcher("VectorLines");

        std::vector<std::pair<int,int> > vL;

        int x = 10;
        int y = 10;
        int xinc = 10;
        int yinc = 0;
        float angle = 0;

        for( auto c : plant )
        {
            switch( c )
            {
            case 'A':
            case 'B':
                break;;
            case '+':
                angle += 1;
                xinc = 5 * cos( angle );
                yinc = 5 * sin( angle );
                break;
            case '-':
                angle -= 1;
                xinc = 5 * cos( angle );
                yinc = 5 * sin( angle );
                break;
            }

            x += xinc;
            y += yinc;
            vL.push_back( std::pair<int,int>( x, y ) );
        }
    return vL;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM