【问题标题】:What's the fastest way to convert a 2D L-system to lines将 2D L 系统转换为线的最快方法是什么
【发布时间】:2021-06-09 12:57:05
【问题描述】:

我正在构建一个 3D 图形引擎,并且我想绘制 2D L 系统。但是我注意到,一旦增加迭代次数,这会变得很慢。我正在寻找一种方法将我的 L 系统快速扩展为 vector<Line>,其中 Line 是一个包含 2 个点的类。这是我当前的代码:

// 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 系统示例

我想不出任何方法来提高性能(显着)。我虽然关于多线程,但你现在需要将你的位置放在每个线程的开头,但是你需要扩展前一个字符。

有没有更有效的算法来完成这项任务?或者一种实现这一点的方法,以便我可以使用多线程?

编辑:我已经查看了答案,这就是我想出的,这提高了性能,但一个缺点是我的程序将使用更多的内存(而且我被限制为 2GB,这是很多但仍然.) 一种解决方案是使用队列,但这会降低性能。

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();
}

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

编辑 3:https://github.com/Robin-Dillen/3DEngine 所有代码

编辑 4:有一个奇怪的错误,循环没有结束

    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());
    } 

【问题讨论】:

    标签: c++ multithreading graphics turtle-graphics


    【解决方案1】:

    我已经实现了一个 Lsystem 来生成和绘制一个 Sierpinski (https://en.wikipedia.org/wiki/L-system#Example_5:_Sierpinski_triangle) 这与您正在做的非常相似。我以直接的方式实施,没有任何技巧。这是对迭代深度为 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 是递归函数。它被调用了 11 次,平均执行时间为 22 毫秒。

    draw 是获取最终生成的字符串并将其绘制到屏幕上的函数。这被调用一次,需要 250 毫秒。

    由此得出的结论是,递归函数不需要优化,因为 50% 的应用时间都被绘图占用了。

    在您的问题中,您没有提供时间分析数据,甚至没有提供“相当慢”的意思。我会说,如果您的代码需要超过 100 毫秒来生成(而不是绘制)最终字符串,那么您就会遇到一个问题,这是由标准算法的执行不力引起的。但是,如果您抱怨的缓慢是由于绘制线条造成的,那么您的问题很可能是图形库选择不当 - 一些图形库甚至可以比其他库快数百倍地完成简单的事情,例如绘制线条。

    我邀请你看看我的代码 https://github.com/JamesBremner/Lindenmayer/blob/main/main.cpp

    如果您只想解析字符串并将线条保存到向量中,那么事情会变得更快,因为不涉及图形库。

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

    这里是代码

    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;
    }
    

    【讨论】:

    • 首先,感谢您的回答。其次,我将给出一些时间指示: 生成线:2.47564s 绘制线:1.10817s 经过时间 写入:0.0180279s 其他一些东西……总共花了 4.06407s 这是 9 次迭代,我想尝试 11 次,但是我可能会花一分钟多的时间。我会查看你的代码,这样我就可以尝试找出哪里出错了。
    • 我通读了代码,发现了一些差异。当我使用 Line 生成包含 2 个点的类时(std::list&lt;Line&gt;)。您只是在扩展字符串。由于我正在从头开始构建 3d 引擎(输出被写入 .bmp 文件),我有点需要这样做(我可以更改它,但如果没有必要我不会)这意味着我的扩展功能需要更多时间,但我的绘图功能应该需要更少的时间。不过,您的代码看起来更加整洁和简单。 PS:我想如果我将if else 块更改为开关,我会得到一点提升。
    • "你只是在扩展字符串。"不寻常地使用“just”这个词,请参阅我的答案以获取用于将线条提取到向量的代码和配置文件。
    • 你的回答帮助我改进了我的代码,但它仍然比你的慢很多。我真的不明白为什么它与你相比如此缓慢......(谈论扩展功能)。我也考虑不转换为对象,但这不是一个选项,因为我根据线条的位置定义图像的宽度和高度。
    • 请注意,这样做,提取到包含 2 个点的线,远非最佳。每行的起点与前一行的终点相同。它消耗两倍于所需的内存并涉及额外的计算。
    【解决方案2】:

    一般而言,优化应用程序性能的第一步是分析代码,以查看花费最多时间的确切位置。如果没有这一步,可能会浪费大量精力来优化实际上对性能影响不大的代码。

    但是,在这种特殊情况下,我希望简化您的代码,以便更容易看到正在发生的事情,从而更容易解释性能分析的结果。

    您的递归函数 expand 可以简化为

    1. 将所有这些参数移出签名。无需将这么多副本放在堆栈上的相同事物上!
    2. 递归函数应该做的第一件事是检查递归是否完成。在这种情况下,请检查深度。
    3. 如果需要进一步递归,第二件事是准备下一次调用。在这种情况下,从当前字符串生成下一个字符串。
    4. 终于可以调用递归函数了。

    下面我将发布实现 Lindenmayer 的原始 L 系统的代码,用于对藻类的生长进行建模。这比您所做的要简单得多,但希望能展示将递归代码重新组织成“标准”风格的递归的方法和好处。

    Is there a more efficient algorithm to do this task?
    

    我对此表示怀疑。我怀疑你可以改进你的实现,但是如果不分析你的代码就很难知道。

    一种实现这一点的方法,以便我可以使用多线程?

    递归算法不适合多线程。

    这是实现类似递归算法的简单代码

    #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;
    }
    

    【讨论】:

      【解决方案3】:
         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))};
      

      如果不进行分析,就很难知道这有多重要。然而:

      1. 为什么不将角度存储为弧度,而不是一遍又一遍地将其转换为弧度?
      2. 如果在其他地方使用弧度会出现问题,请至少进行一次转换并存储在本地
      3. 最好添加一个 Line2D 构造函数,该构造函数将 Turtle 引用作为参数并进行自己的计算。
      4. 是否需要重新计算 T.pos?回避现在不是完成了吗?

      【讨论】:

      • 我将减少从度数转换为弧度的次数。其次,将海龟交给 Line2D 也是一个好主意,这样它就可以自己计算了。最后,T.pos的计算是必要的,我们只在最后的递归步骤中移动。
      猜你喜欢
      • 1970-01-01
      • 2015-05-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多