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;
}

我在项目中遇到过一个问题:写了一个批量修改点位置的函数,忘了加 &,结果调了半天,点纹丝不动。你想想看,这得多郁闷。

注意:VEX里函数参数用 & 表示引用传递,但只支持 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的灵魂所在。你写的代码不是孤立的,它要跟场景里的几何体打交道。nearpointprimuv 是我用得最多的两个函数。

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基础语法(下) 函数定义与调用 数组(Array)操作 字符串处理 常用几何函数 核心要点 • 返回值类型 + 函数名 + 参数 • 值传递 vs 引用传递(&) • void 无返回值函数 • 先定义后调用 核心要点 • 动态数组,无需预分配 • push/pop/insert/removeindex • len/sort/reverse • for vs foreach 遍历 核心要点 • sprintf 格式化拼接 • find/substr/replace • split 分割字符串 • upper/lower 大小写 核心要点 • nearpoint 找最近点 • primuv 面片UV采样 • point/prim/vertex 读取 • setpointattrib 写入 💡 核心思想:封装逻辑 → 批量处理 → 与场景交互

这一章的内容,说白了就是让你从「只会写单行表达式」进化到「能写完整的逻辑模块」。函数帮你封装,数组帮你批量,字符串帮你处理元数据,几何函数帮你跟场景对话。这四个东西组合起来,你就能做出很酷的程序化工具了。

嗯,我记得刚开始学VEX时,觉得 primuv 特别抽象,不知道它有什么用。直到有一次做屋顶瓦片生成,需要在曲面上一片一片地放置瓦片,用 primuv 配合循环,几行代码就搞定了。从那以后,我就爱上了这个函数。

你想想看,如果没有这些几何函数,你要手动计算每个点在曲面上的位置,那得多麻烦?VEX把这些底层计算都封装好了,你只需要调用就行。这就是Houdini的魅力所在——用代码解放双手。

专注资料整理