Houdini学习笔记028_梯度能用来干嘛?

这一篇我们来学习下图所示图案的制作方法。

该图案可分为两个部分,本质上是相同的。可以看作是雨滴落在一块起伏的表面上,沿着高度梯度向下或向上流淌。当然,自然界中肯定是向下(重力方向)流淌的。

参考教程链接点这里。原教程虽然很详细,但不熟悉代码的人看起来可能一头雾水,自己动手时不知从何做起。为了能让VEX初学者容易理解,我尽量会解释每段代码的意思,以及这样书写的缘由。
下面我们从头开始讲起——
首先还是创建一个起伏的平面,使用的是mountain节点。

高度梯度属性我们直接用measure节点来创建,Element type默认为Primitives,Measure选择Gradient,位置P的分量选择 Y Component。输出的属性名称为gradient(你也可以自行命名为其他名称)。

放大后是这样的,这个gradient属性是一个矢量,起点位于面上,因为是primitive属性。为什么不设为点属性呢?等介绍VEX中的xyzdist函数时你就知道了。

然后还需要有雨滴,用scatter节点撒点来表示。测试时数量可以少一点,如10个。

以上是准备工作。下面用Point Wrangle节点来书写VEX脚本。
写VEX之前我们先进行简单的分析,我们的最终目的是得到每个点按照梯度流淌的曲线。而每个点当前所在的面(primitive)都是有一个gradient属性的,这个属性可以确定当下流淌的方向,我们再设定一个移动距离就可以得到下一时刻点的位置。不断重复该过程就可以得到一系列的点坐标,然后连成一条曲线。
现在摆在我们面前的有两个问题:
1、每个点所在的高度不同,到达局部最低点所需的步数也不一样,循环条件怎么给?
2、每步移动距离是多少?给一个确定的值还是变化的值?如果变化,怎么变?
结合实际情况,“水滴”向局部最低点流动时,gradient属性的y分量肯定是逐渐趋向于0的。也就是与上一个点的P.y差值逐渐趋向于0。我们可以设置一个容差值tolerance,当满足条件ΔP.y<tolerance时,循环终止。使用的是while语句。(原教程中使用的是总路径长度超过一定数值作为判断循环终止的条件,这里我用的是不同的思路)
至于每步移动的距离,我们暂且给一个可调节的确定值(step)。
创建Point Wrangle节点,将scatter节点连接到其第一个(0号)端口,因为我们是要对scatter的点进行操作。然后将measure节点连接到第二个端口,因为后面要借助表面的gradient属性来计算。

定义一个浮点型变量delta来记录连续两个点之间的y方向位置分量的差值,当其小于tolerance值时,循环结束。delta的初始值大于tolerance即可。

初始的点位置是位于表面上的,但每次移动一段距离后是否还位于表面上则不确定。可以用xyzdist函数将其重新投射到表面上。xyzdist函数的作用是获取点到模型表面最近距离,这里还有一个作用是返回最近面的编号primnum,从而便于获取其gradient属性作为下一步移动的方向。

xyzdist函数写法如下,第一个参数是输入端口编号,这里为1。第二个参数是点的坐标,第三个参数可以返回距离最近面的编号,第四个参数返回的是最近距离投射点在面上的uv值。这里分别用pos0、hitprim和hituv来表示,记得变量要初始化,并注意其数据类型。

然后将投射点的坐标作为新的pos0,写法为
pos0 = primuv(1, "P", hitprim, hituv);
xyzdist函数一般和primuv函数搭配使用。
然后获取投射面的gradient矢量,同样是用primuv函数。
vector dir = primuv(1, "gradient", hitprim, hituv);
这里可以用normalize函数对dir进行归一化,还有一点要注意的是gradient的方向默认是由低处指向高处的,要往下流动的话应该取负值。移动后新的点坐标为:
vector pos1 = pos0 + dir * step;
并重新计算两点间的delta值作为下一步的判断条件:
delta = pos0[1] - pos1[1];
为了避免陷入死循环,我们可以用一个变量count来记录循环的次数。当超过一定次数则自动跳出循环。
count += 1;
if(count > chi("counts")){
break;
}
同时为了让点接近局部最低点时步长逐渐收敛,可以给每步移动的距离乘上一个衰减系数falloff,每循环一次:
step *= falloff;
到此位置,全部脚本如下所示——

然后就是每步添加新的点,用的是addpoint函数,前面已用过多次。循环之前先把scatter的点算进去:
int pts[ ] = array();
int ptid = addpoint(0, pos0);
append(pts, ptid);
pts[ ]数组是为了后面画线用,此时得到的结果如下——

当前循环结束前别忘了把新的点坐标重新设为pos0,即:
pos0 = pos1;
然后再进行下一次循环。
最后两句VEX(画线,移除原来scatter的点):
addprim(0, "polyline", pts);
removepoint(0, @ptnum);
添加smooth节点,让曲线更加平滑。

后续上色可以用foreach primitive节点,对每条曲线分别设置。根据点编号添加属性(还是用Attribute Wrangle节点):
@ratio = (float) @ptnum / (@numpt - 1);

Color节点的类型选择Ramp from Attribute,属性即刚刚设置的ratio(也可以直接根据y方向的点坐标值P.y来着色)。

最终结果如下——

想要得到由低处往高处流动的曲线,只要将dir的方向负号去掉即可。另外,由低处往高处运动时delta值为负,也要反转一下,或者用abs函数取绝对值。

本篇分享就到这里,感谢各位的阅读,下回见~