4. VEX基础语法(下):函数定义与调用、数组操作、字符串处理、常用几何函数
好,咱们接着聊VEX。上一章我们把变量、循环、条件判断这些基本功过了一遍。这一章要讲的东西,才是真正让你从「写小脚本」迈向「写工具」的关键。
说白了,函数、数组、字符串、几何查询——这四个东西,你玩熟了,Houdini里百分之八十的程序化逻辑你都能搞定。我当年刚接触VEX时,觉得数组操作特别绕,后来在做一个城市生成项目时,被逼着啃透了,才发现这东西真香。
4.1 函数定义与调用:把你的逻辑封装起来
写VEX跟写其他语言一样,重复代码多了,你就该考虑封装成函数。我个人习惯是:只要一段逻辑出现两次以上,立马写成函数。
4.1.1 函数的基本结构
// 返回类型 函数名(参数列表)
float myFunction(float a, int b)
{
float result = a * b;
return result;
}
调用起来很简单:
float val = myFunction(3.14, 2);
嗯,这里要注意:VEX里函数必须先定义后调用。不像C++可以声明在前定义在后。你如果写了个函数在下面,上面调用它,编译器会报错。
4.1.2 参数传递的坑
VEX的参数默认是值传递。什么意思?就是你在函数里改了参数,外面不会变。但如果你想让函数修改外部变量,可以用 export 关键字:
void modifyPoint(vector &pos, float offset)
{
pos.y += offset;
}
我在项目中遇到过一个问题:写了一个批量修改点位置的函数,忘了加 &,结果调了半天,点纹丝不动。你想想看,这得多郁闷。
& 表示引用传递,但只支持 export 上下文。在Wrangle节点里,你通常用 @P 直接操作属性,不太需要这个。但在函数内部,如果你要修改传入的数组或几何体句柄,就得小心了。
4.1.3 函数的返回值与void
如果函数不需要返回值,用 void:
void logMessage(string msg)
{
printf("LOG: %s\n", msg);
}
我个人建议:能返回值的尽量返回值,少用void。这样代码可读性更好,也方便调试。
4.2 数组(Array)操作:批量数据的灵魂
数组在VEX里太常用了。你处理一群点、一群面、一群颜色,本质上都是在操作数组。
4.2.1 数组的声明与初始化
// 声明一个空数组
int points[] = {};
// 声明并初始化
float values[] = {1.0, 2.5, 3.7, 4.2};
// 声明一个二维数组(数组的数组)
int grid[][] = {};
VEX的数组是动态的,你不需要提前指定大小。这一点跟Python有点像,但性能上比Python好得多。
4.2.2 常用数组操作函数
| 函数 | 作用 | 示例 |
|---|---|---|
len() |
获取数组长度 | int n = len(arr); |
push() |
在末尾添加元素 | push(arr, 42); |
pop() |
移除并返回末尾元素 | int last = pop(arr); |
insert() |
在指定位置插入 | insert(arr, 0, 99); |
removeindex() |
移除指定索引的元素 | removeindex(arr, 2); |
sort() |
排序(默认升序) | sort(arr); |
reverse() |
反转数组 | reverse(arr); |
push() 动态收集符合条件的点序号,最后统一处理。比如先遍历所有点,把高度大于10的点序号push到一个数组里,然后批量修改它们的颜色。这样比边遍历边修改要安全得多。
4.2.3 数组遍历的几种写法
// 方法1:for循环(最常用)
for(int i = 0; i < len(arr); i++)
{
printf("arr[%d] = %f\n", i, arr[i]);
}
// 方法2:foreach(更简洁)
foreach(float val; arr)
{
printf("value = %f\n", val);
}
// 方法3:带索引的foreach
foreach(int idx; float val; arr)
{
printf("index %d: %f\n", idx, val);
}
我个人更推荐 foreach,代码更干净。但如果你需要修改数组元素,还是得用 for 循环加索引。
4.3 字符串处理:不只是文字
字符串在VEX里经常被用来做属性名拼接、路径处理、调试输出。别小看它,用好了能省不少事。
4.3.1 字符串拼接与格式化
// 拼接
string name = "point_" + itoa(@ptnum);
// 格式化(推荐)
string info = sprintf("Point %d has position (%g, %g, %g)",
@ptnum, @P.x, @P.y, @P.z);
sprintf 的格式符跟C语言一样:%d 整数,%g 浮点数,%s 字符串。我习惯用 %g 而不是 %f,因为 %g 会自动去掉多余的零,输出更干净。
4.3.2 常用字符串函数
// 获取长度
int len = strlen("hello"); // 5
// 查找子串
int pos = find("hello world", "world"); // 6
// 截取子串
string sub = substr("hello world", 0, 5); // "hello"
// 替换
string newStr = replace("hello world", "world", "vex"); // "hello vex"
// 分割
string parts[] = split("a,b,c", ","); // {"a", "b", "c"}
// 转大小写
string upper = upper("hello"); // "HELLO"
string lower = lower("HELLO"); // "hello"
"floor_0_height"、"floor_1_height"。用 sprintf 加循环,几行代码就搞定了。
4.4 常用几何函数:与场景交互
这部分是VEX的灵魂所在。你写的代码不是孤立的,它要跟场景里的几何体打交道。nearpoint 和 primuv 是我用得最多的两个函数。
4.4.1 nearpoint:找最近的点
// 在指定几何体中,找距离某位置最近的点
int nearPt = nearpoint(1, @P);
// 如果没找到,返回 -1
if(nearPt != -1)
{
vector nearPos = point(1, "P", nearPt);
// 做点什么...
}
第二个参数是几何体的输入序号。0是当前节点,1是第二个输入,以此类推。我经常把原始模型连到第二个输入,然后用 nearpoint(1, @P) 找到当前点最近的原始模型上的点。
nearpoint,结果场景里有一万个点,每个点都要遍历所有点去找最近点,性能直接崩了。后来改用 pcfind(找指定半径内的点)加限制搜索范围,才解决问题。如果你要批量找最近点,记得考虑性能。
4.4.2 primuv:在面片上采样
这个函数太强了。它可以在一个面片的UV坐标上插值出任何属性值。
// 在面片0上,UV坐标(0.5, 0.5)处采样位置
vector pos = primuv(1, "P", 0, {0.5, 0.5});
// 采样颜色
vector color = primuv(1, "Cd", 0, {0.5, 0.5});
第三个参数是面片序号,第四个是UV坐标(二维向量)。返回值是插值后的属性值。
我举个例子你就明白了。假设你有一个地形网格,你想在地形表面撒点。用 primuv 可以精确控制每个点的位置:
// 在地形(第二个输入)上随机撒点
int primCount = nprimitives(1);
for(int i = 0; i < 100; i++)
{
int prim = int(rand(i * 100) * primCount);
float u = rand(i * 100 + 1);
float v = rand(i * 100 + 2);
vector pos = primuv(1, "P", prim, {u, v});
// 在pos位置创建点...
}
4.4.3 其他常用几何函数
| 函数 | 作用 | 示例 |
|---|---|---|
point() |
读取点的属性 | vector pos = point(0, "P", 10); |
setpointattrib() |
设置点的属性 | setpointattrib(0, "Cd", 10, {1,0,0}); |
prim() |
读取面片的属性 | int verts[] = prim(0, "vertices", 5); |
vertex() |
读取顶点的属性 | vector uv = vertex(0, "uv", 20); |
npoints() |
获取点的总数 | int total = npoints(0); |
nprimitives() |
获取面片的总数 | int total = nprimitives(0); |
4.5 本章知识体系
下面这张图帮你理清这一章的核心脉络:
这一章的内容,说白了就是让你从「只会写单行表达式」进化到「能写完整的逻辑模块」。函数帮你封装,数组帮你批量,字符串帮你处理元数据,几何函数帮你跟场景对话。这四个东西组合起来,你就能做出很酷的程序化工具了。
嗯,我记得刚开始学VEX时,觉得 primuv 特别抽象,不知道它有什么用。直到有一次做屋顶瓦片生成,需要在曲面上一片一片地放置瓦片,用 primuv 配合循环,几行代码就搞定了。从那以后,我就爱上了这个函数。
你想想看,如果没有这些几何函数,你要手动计算每个点在曲面上的位置,那得多麻烦?VEX把这些底层计算都封装好了,你只需要调用就行。这就是Houdini的魅力所在——用代码解放双手。