An example project that uses the output of the edgewalker.
Algorithm
The Edgewalker tool starts at a group of points, and walks along edges to neighboring points to create an edge group or polyline.
A progression of snapshots of the algorithm. A frame is skipped between each stage for demonstration purposes.
While the look can seem similar to the Find Shortest Path SOP or perhaps Edge Network, those nodes don’t avoid current edges and have many merges or splits.
For the above video I took the attribute created upon creating an edge called path_time. I capture a rest position after the final solve, then set the Y position to this attribute. After that I use a Clip SOP (as opposed to Blast) to smoothly remove any path geometry ahead of a certain amount and then return the rest position. I then pushed points away from the center based on their path_time attribute as well, giving some depth and layering to the final shape. Finally, instancing some spheres at corners and carve positions for some extra life.
Controls
The controls are split up into three main groups that don’t much affect the others.
One of the most powerful tools in this is the direction attribute, which lets you use a vector point attribute to guide the neighbor preference algorithm. This allows for very nice swirls and parallel pathing that can look natural and interesting.
The three tabs of controls.
Setup
The setup involves a few basic things, like creating the initial starting group, creating attributes, but most importantly doing some heavy calculations in parallel before the solver, which has to process things linearly do avoid racing problems.
Network for the setup. After this it pipes into the Solver node.
Other things can be calculated once here instead of at every walk iteration. You can gather the neighbors once and also sort that list once to be easily looked up during the walk loop. If a certain neighbor is unavailable the neighbor list doesn’t need to be recalculated, you just need to choose a different index.
// Parametersstringdir_type=chs("../dir_type");stringdir_vec_style=chs("../dir_vec_style");stringdir_attr=chs("../dir_attr");intnorm_dir=chi("../norm_dir");intrev_dir=chi("../rev_dir");vectordir_vec=chv("../dir_vec");// Pre-process the direction vectorvectordir;if(dir_type=="vec"){dir=dir_vec;}else{dir=point(0,dir_attr,@ptnum);if(norm_dir){dir=normalize(dir);}if(rev_dir){dir*=-1;}}if(dir_vec_style=="local"){dir*=@N;}/* Compute dot product between the direction to each neighbor
and the current point's direction attribute */intnbs[]=i[]@neighborhood;// Neighborhoodfloatvalues[];// Array to store the values to rank neighbors byforeach(intnbpt;nbs){vectornb_pos=point(0,"P",nbpt);floatdot=dot(normalize(nb_pos-@P),dir);push(values,dot);}/* Sort the neighborhood by dot product values via argsort
Note: argsort would normally return an ascending list so
we have to reverse it in order to get highest values first.*/i[]@neighborhood=reorder(nbs,reverse(argsort(values)));
VEX code for the 'Sort By Direction' node.
Solver
The heart of the walk algorithm is to choose from a sorted list of candidate points and add the edge between them to an edge group. If there are no candidate points, start with a new point in a list of open points.
A few point attributes are necessary to solve this. One is a group of “active” points, pretty standard in most solver/growth algorithms. The other is a “closed” group. This evolved from a “processed” state that other algorithms have because I wanted points with only one connected edge to remain open to be closed later on by a path that finds it.
Solver network with a bypass branch if there are no more paths to create or consider.
The walk has to be done in a non-parallel fashion since two points competing for the same point need to be able to find a resolution. One will take it, and the other won’t see it as an option anymore. I’ve been thinking about ways to make it parallel again though, at least for the most part since most points are not next to each other.
// Parametersstringpath_closing=chs("../../../../path_closing");intmerging=chi("../../../../merging");intpt=chi("../loop_meta/point");// Which point to processintnbs[]=point(0,"neighborhood",pt);// Gather candidate pointsintcandidates[];intactive_candidates[];intinactive_candidates[];foreach(intnbpt;nbs){intclosed=inpointgroup(0,"closed",nbpt);intactive=inpointgroup(0,"active",nbpt);intconnected=inedgegroup(0,"edges",pt,nbpt);intno_closing=(path_closing=="no_closing");intofflimits=inpointgroup(0,"offlimits",nbpt);if(!(closed&!merging)&&!connected&&!(active&&no_closing)&&!offlimits){if(path_closing=="no_pref"||no_closing){push(candidates,nbpt);}else{if(active){push(active_candidates,nbpt);}else{push(inactive_candidates,nbpt);}}}}if(path_closing=="close"){if(len(active_candidates)>0){candidates=active_candidates;}else{candidates=inactive_candidates;}}elseif(path_closing=="avoid"){if(len(inactive_candidates)>0){candidates=inactive_candidates;}else{candidates=active_candidates;}}/* A candidate list is stored at the detail for efficient access since
it's only used once in the next node and then discarded */setdetailattrib(0,"candidates",candidates);
VEX code for the 'Get Candidates' node, which generates a list of which neighbors can be connected to.
// Parametersstringpre="../../../../";stringnew_path_behavior=chs(pre+"new_path_behavior");stringdeadend_behavior=chs(pre+"deadend_behavior");stringpath_closing=chs(pre+"path_closing");intdo_time_attr=chi(pre+"do_time_attr");stringtime_attr=chs(pre+"time_attr");intdo_dist_attr=chi(pre+"do_dist_attr");stringdist_attr=chs(pre+"dist_attr");floatseed=ch(pre+"seed");floatdop_time=chf("doptime");floatdop_time_delta=chf("doptimedelta");intpt=chi("../loop_meta/point");// Which point to processintnext=-1;// Initialize next point indexintcandidates[]=detail(0,"candidates");intdeadend=0;if(len(candidates)==0){deadend=1;}// If all neighbors are unmatchable, deadendif(!deadend){next=candidates[0];// Choose the first element in the candidate list// Apply Path Time attribute to the new pointif(do_time_attr&&point(0,time_attr,next)<0){setpointattrib(0,time_attr,next,dop_time+dop_time_delta);}// Apply Path Distance attribute to the new pointif(do_time_attr&&point(0,dist_attr,next)<0){floatdist=distance(vector(point(0,"P",pt)),vector(point(0,"P",next)));setpointattrib(0,dist_attr,next,dist);}setedgegroup(0,"edges",pt,next,1);// Connect the pointssetdetailattrib(0,"next_pt",next);}// If next point is an active pointif(inpointgroup(0,"active",next)){setpointgroup(0,"closed",next,1);setpointgroup(0,"active",next,0);setpointgroup(0,"closed",pt,1);deadend=1;// Path is now closed}// Otherwiseelse{setpointgroup(0,"active",next,1);// Set next point to activeif(new_path_behavior=="one"){// Prevent branching if appropriatesetpointgroup(0,"active",pt,0);// Set self to inactive}if(path_closing=="no_closing"){setpointgroup(0,"closed",pt,1);// Set self to closed}}// If the next point is a closed pointif(inpointgroup(0,"closed",next)){setpointgroup(0,"closed",pt,1);// Set self to closeddeadend=1;}// Deadend Behaviorif(deadend){if(deadend_behavior=="new"){intopen[]=detail(1,"open_pts");// Available points to start atintnum_open=len(open);if(num_open>0){intnew=open[floor(rand(pt*14.23421+seed)*num_open)];// Select random indexsetpointgroup(0,"active",new,1);// Set selected point to active}}setpointgroup(0,"active",pt,0);// Set self to inactivesetpointgroup(0,"closed",pt,1);// Set self to closed}if(do_time_attr&&point(0,time_attr,pt)<0){setpointattrib(0,time_attr,pt,dop_time);}
VEX code for the Walk node. This connects points to the next best point available.
stringpath_closing=chs("../../../../path_closing");intpts[];push(pts,chi("../loop_meta/point"));intnext_pt=detail(0,"next_pt");if(next_pt!=0){push(pts,next_pt);}foreach(intpt;pts){intconnections=0;intnbs[]=point(0,"neighborhood",pt);foreach(intnbpt;nbs){if(inedgegroup(0,"edges",pt,nbpt)){// If connectedconnections++;}if(connections>1){// If more than one connectionsetpointgroup(0,"closed",pt,1);setpointgroup(0,"active",pt,0);break;}else{if(path_closing!="no_closing"){setpointgroup(0,"closed",pt,0);}}}if(inpointgroup(0,"closed",pt)){setpointgroup(0,"active",pt,0);}}
VEX code for the Set Closed Group node. This code polishes up any incorrect point groups.
Output
The final output of the solver is not lines, it’s actually an edge group. This lets you receive your geometry unharmed, if that’s what you need. Mostly though, the edge group is used to create polylines out of each path.
Later, temporary attributes and groups are cleaned up, unless otherwise decided.