Houdini·电线杆工具

本次练习实现电线杆生成工具。通过创建曲线在曲线路径上生成电线杆。主要特性有:

🔹 在自定义曲线路径上生成电线杆,可选等距或按原曲线节点生成

🔹 电线杆的横梁上有电线串联,可定义电线数量、受重力影响程度、连接模式

🔹 电线杆的末尾处可进行断线垂坠模拟

🔹 电线杆的倾斜旋转程度可控,可整体随机

🔹 电线杆的各个部件(横梁、杆体、装饰)可分别替换为外部模组,可根据替换模组列表中对各部件进行整体随机

Base

电线杆的主体为横梁,横梁上有电线支点,用于生成串联电线。Base由一横一竖两根直线构成,需要为后续生成提供的信息有:

对以上信息分组,以备后用。

生成路径

使用curve创建路径。

工具支持两种生成路径方式,一是在curve上的节点处生成电线杆,二是在curve路径上等距生成电线杆。前者就不需要对curve处理了,后者添加resamplemaximum segment length,length为x即表示每隔x单位长度生成一个电线杆。同理如果想设定具体电线杆数量,就勾选下面的maximun segments选项。

电线杆朝向

这里设定电线杆的初始朝向与相邻两个线段的半程向量相切,这样可以最大程度避免电线间隔过小。

使用polyframe添加N,在attribute wranggle中添加如下代码:

@N = normalize(@N);
int nei[] = neighbours(0,@ptnum);
if(nei[1]!=0){
    vector next = point(0,'N',nei[1]);
    @N = next + @N;
}

N会从初始的沿路径方向修改为与两节相邻线段的半程向量相切。

之后可以选择对电线杆进行旋转角度随机。随即旋转代码如下(这里直接修改法线向量):

@N.y += rand(@ptnum * 100)*clamp(rand(@ptnum*125*ch("rotate_intensity")) * ch("rotate_intensity"),-ch("slope_intensity"),ch("slope_intensity"));
@N.y -= rand(@ptnum * 20.9)*clamp(rand(@ptnum*215*ch("rotate_intensity"))* ch("rotate_intensity"),-ch("slope_intensity"),ch("slope_intensity"));
@N.z += rand(@ptnum * 10.3)*clamp(rand(@ptnum *332* ch("rotate_intensity"))* ch("rotate_intensity"),-ch("slope_intensity"),ch("slope_intensity"));

@N = normalize(@N);

电线生成

电线生成这里,起初参考了一些教程。比如之前制作scifi建筑的教程,里面使用grid+ray从高处向下覆盖电线杆所在区域进行投影生成电线,还有不少用物理解算模拟电缆的工具展示…不过好像都不太适用电线杆工具。电线杆间的电线串联还算是比较规律的(当然也可以很乱,这个有机会再拓展吧),此外也没有太大必要对所有电线做物理模拟。

对base和路径使用copytopoint,获得一条路上的电线杆base,之后针对base中的电线生成点group添加add节点,可以根据横梁上的生成点数量连接成线段。

然后删去多余的点面,只留下电线部分。使用add生成的线段是没有根据点分割成prim的,而后续电线垂坠计算是针对相邻电线杆之间的电线段进行,因此需要将一整条完整的电线根据电线杆的数量分割成段,这里使用carve节点即可。

电线的垂坠计算需要对每一段电线段进行位移计算,模拟电线因为老化和重力影响而产生的松懈下坠效果,简单来说就是计算电线段中各点的y坐标。此外对随机seed进行了一点点处理,可以模拟老化电线曲曲折折的样子。代码如下:

i@seed = detail(1,"iteration");
if(@ptnum == 0 ||@ptnum == @numpt-1)
    @P.y-=0;
else{
    float a = float(@ptnum)/@numpt*2-1;
    a=1-a*a;
    int mode = ch("curvemode");
    float seedmode = max(mode*@ptnum,1);
    @P.y-=a*(ch("intensity") + ch("mess")*rand(seedmode*@seed*100));
}

断裂处垂坠

模拟头尾断裂处首先需要在头尾节点处沿法线方向对线段进行一截延伸。头尾两排的节点不难获得,直接groupbyrange,然后沿法线方向或法线相反方向对点进行随机位移,作为延伸线段的尾部。然后同上用add连接成线段。

setpointgroup(1,"head",@ptnum,0,"set");
//@N = -@N;  //起点那端需要对法线取反
float offset = ch("offset");
float seed = ch("snapping_Seed");
@P += offset * fit01(rand(seed*@ptnum),0.4,1) * @N;

接下来对末尾延伸线段进行垂坠模拟,这里不太好像前面的串联电线一样作伪计算,用物理模拟更方便。这里选用vellum制作该效果。

首先依然是对线段进行resample。这里需要注意——务必按maximum segment length均匀分段。因为线段长度随机,如果按固定段数分段,最后模拟出来的效果会跟现实相反(长线段理应垂坠程度更高,但分段长度会影响模拟效果)。然后问题它就出现了——resample后因为每个线段的点数不一样,无法根据简单的groupbyrange获取延伸线段头部point组(模拟时需要该数据组作为固定根节点)。

我也不知道有没有更简单的解决办法,总之最后我是这样做的——在分段前添加一个point属性,值为$PT(point number),这样会为原始point添加一个索引值(都是整数),resample后,新增点的属性值会在原有点的$PT值间插值(都是小数)。然后根据小数点后是否为0区分出哪些是原有的节点,以此达成获取初始根节点的目的。

之后设定root组,选用vellumhair模式,vellumBend属性的stiffness拉满,这样比较符合电缆这种非柔软线段的特性,点击play对延伸线段进行重力模拟。

模型随机替换

这里希望可以不限量地添加模块然后在整个列表中为电线杆随机模块。思路是在工具UI中添加object list,使用时往list中添加模块路径,然后通过按钮更新list中的模块到工具中,再从列表中进行随机或者指定模块操作。

具体操作为:

①切到Prameters Tab,新建一个folder,模式选择multiple input(list)

②在folder中加入需要的模块,之后在面板中点击那两个加/减号按钮即可以自定义的这些模块为一个单位去增删。变量名后的#,用于表示编号

③切到Scripts Tab,左下方Event Handler选定PythonModule——这个Module名是固定的,必须叫PythonModule。在右侧可以写函数,然后在UI param中调用。以本次需要实现的模型替换功能为例,代码如下:

# create module list and link to dest switch node ---------------------------------------------------------------------------------------------
def createList(t, dest):
    ts = "_" + t 
    
    k = hou.node('./AlfxModuleList' + ts)
    if k != None:
        # print 'update success (^ω<)'
        k.destroy()    
    # else:
        # print 'create list (^ω<)'
    
    n = hou.node(".")
    w = n #.parent()
    
    p = w.createNode('subnet',"AlfxModuleList" + ts)
    c = hou.node("./merge"+ts)
    s = hou.node(dest)
    
    # get total modules in current list
    # then update range parameter here
    objnum = hou.parm(n.path()+"/numobj"+ts).eval()
    
    for x in range(0,objnum):
       m = p.createNode('object_merge',"AlfxModule" + ts)
       # objnode = hou.node(hou.parm(n.path()+"/objpath"+str(x)).eval())
       m.moveToGoodPosition()
       hou.parm(m.path()+"/objpath1").set(hou.parm(n.path()+"/objpath"+str(x+1)+ts).eval())     
       
       o = p.createNode('output',"output"+str(x))
       o.setInput(0,m)
       onode = hou.parm(p.path()+"/output"+str(x))
       s.setInput(x+1,p,x)
       c.setInput(x+1,p,x)

Houdini的python还蛮好写的,主要就是根据层级关系获取节点,之后各种变量名可以通过鼠标悬浮在对象上查看(遇到Nontype报错基本是变量名没对上,注意仔细检查)。

④添加完函数,之后就是在UI操作中调用。这里用到按钮来调用上面写的这个函数,所有参数的信息面板上都有一个callback script,选择python模式,调用语法为——

hou.pwd().hdaModule().createList('pole','./PoleSwitch') # “hou.pwd().hdaModule().”后接函数

以上可参考文档

⑤其他: