将 2D L 系统转换为线条的最快方法是什么



我正在构建一个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-系统示例

我想不出任何方法可以(显著地)提高性能。我想是关于multihtreading的,但你现在需要在每个线程的开头定位,但之后你需要扩展前一个字符。

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

编辑:我已经研究了答案,这就是我的想法,这提高了性能,但一个缺点是我的程序将使用更多的ram(我被限制在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:与下面发布的代码相比,它仍然很慢

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

我已经实现了一个L系统来生成和绘制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

CCD_ 2是递归函数。它被调用了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;
}

一般来说,优化应用程序性能的第一步是对代码进行概要分析,以查看花在哪里的时间最多。如果没有这一步骤,优化代码可能会浪费大量精力,而实际上对性能几乎没有影响。

然而,在这种特殊的情况下,我希望简化您的代码,以便更容易地了解发生了什么,从而更容易地解释性能评测的结果。

可以简化您的递归函数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;
}
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?回避现在还没有完成吗

相关内容

最新更新