4. VEX语法核心(中):运算符、表达式与语句

好,咱们接着聊VEX语法。上一节我们把变量和数据类型捋了一遍,这一节要啃的,是真正让数据“动起来”的东西——运算符、表达式和语句。

说实话,我刚开始学VEX那会儿,觉得运算符不就是加减乘除嘛,有啥好学的?结果在项目里写了一个复杂的点云处理逻辑,死活跑不对,最后发现是位运算的优先级搞错了。嗯,从那以后我再也不敢小看这些基础符号了。

4.1 算术运算符:不只是加减乘除

VEX的算术运算符跟C语言几乎一模一样。常用的有这些:

运算符 含义 示例
+ 加法 a + b
- 减法 a - b
* 乘法 a * b
/ 除法 a / b
% 取模(求余数) a % b
** 幂运算 a ** b

这里有个坑,我踩过好几次——整数除法。在VEX里,如果你写 5 / 2,结果不是2.5,而是2。因为两个整数相除,结果还是整数,小数部分直接截断。想要浮点数结果,得写成 5.0 / 2 或者 float(5) / 2

注意:VEX的取模运算符 % 只能用于整数。如果你对浮点数取模,编译器会报错。我习惯先转成整数再操作。

幂运算 ** 在Houdini里特别常用。比如做衰减效果时,我经常写 pow(1 - dist, 2),其实等价于 (1 - dist) ** 2。我个人更喜欢用 **,写起来更直观。

4.2 比较运算符:判断的基石

比较运算符返回的是整数值——1表示真,0表示假。VEX里没有专门的布尔类型,这一点跟C语言一样。

运算符 含义 示例
== 等于 a == b
!= 不等于 a != b
< 小于 a < b
> 大于 a > b
<= 小于等于 a <= b
>= 大于等于 a >= b

你可能会问:为什么不用 = 做比较?因为 = 是赋值运算符。我见过新手把 if (a = 5) 写在条件里,结果永远为真,因为赋值表达式返回的是被赋的值(5),而5是非零值,被视为真。这个bug找起来特别隐蔽。

小技巧:比较浮点数时,别直接用 ==。浮点数有精度问题,0.1 + 0.2 可能不等于 0.3。我一般写成 abs(a - b) < 1e-6

4.3 逻辑运算符:组合条件

逻辑运算符用来组合多个比较结果。VEX支持三个:

  • && —— 逻辑与(AND),两边都为真才为真
  • || —— 逻辑或(OR),一边为真即为真
  • ! —— 逻辑非(NOT),取反

举个例子:

// 只处理半径在0.5到1.5之间的点
if (dist > 0.5 && dist < 1.5) {
    @Cd = {1, 0, 0};
}

这里有个性能相关的点——短路求值。VEX从左到右计算逻辑表达式,一旦结果确定,后面的就不算了。比如 a && b,如果a是假,b根本不会执行。我在项目里经常利用这个特性来避免空指针访问:

if (ptnum != -1 && point(0, "P", ptnum).y > 0) {
    // 安全操作
}

如果ptnum是-1,后面的 point() 就不会调用,避免了报错。

4.4 位运算:底层操作的利器

位运算直接操作整数的二进制位。说实话,在Houdini日常工作中用得不多,但一旦用上,效率提升是质的飞跃。

运算符 含义 示例
& 按位与 a & b
| 按位或 a | b
^ 按位异或 a ^ b
~ 按位取反 ~a
<< 左移 a << 2
>> 右移 a >> 2

我曾经用位运算做过一个“属性标签系统”。每个点用一个整数存储多个开关状态,比如第0位表示“是否被选中”,第1位表示“是否被冻结”,第2位表示“是否可见”。用 &| 来读写,比用多个float属性快得多。

// 设置第2位为1(可见)
int flags = @flags;
flags |= (1 << 2);
@flags = flags;

// 检查第0位是否为1(选中)
if (@flags & 1) {
    // 处理选中逻辑
}
核心要点:位运算适合做“紧凑存储”。如果你有多个布尔值要存,别用多个float,用一个int就够了。内存占用小,访问速度快。

4.5 表达式与语句:代码的组织方式

表达式是“有值的代码片段”。比如 a + b 是一个表达式,它的值是a和b的和。语句是“执行一个动作的代码单元”,以分号结尾。

VEX里常见的语句类型:

  • 表达式语句@P.y += 0.1;
  • 声明语句float radius = 1.5;
  • 控制流语句ifforwhile
  • 返回语句return value;

这里有个容易混淆的地方——赋值是表达式,不是语句。在VEX里,a = 5 是一个表达式,它的值是5。加上分号变成 a = 5; 才是一条语句。这意味着你可以这样写:

int b = (a = 5) + 3;  // a被赋值为5,b被赋值为8

我个人不太推荐这种写法,可读性太差。但在某些紧凑的循环里,偶尔会用一下。

4.6 运算符优先级:别让括号救你

运算符优先级决定了表达式的计算顺序。VEX的优先级跟C语言基本一致:

  1. 括号 ()
  2. 一元运算符 ! ~ -
  3. 乘除取模 * / %
  4. 加减 + -
  5. 移位 << >>
  6. 比较 < > == !=
  7. 按位与 &
  8. 按位异或 ^
  9. 按位或 |
  10. 逻辑与 &&
  11. 逻辑或 ||
  12. 赋值 = += -=

我的建议是:拿不准就加括号。别觉得丢人,代码是给人读的,不是给编译器读的。我在项目里见过一行代码写了七八个运算符,不加括号,结果维护的人(包括我自己)看了半天才搞明白意图。

个人习惯:我写复杂表达式时,会刻意用括号分组。比如 if ((a > 0) && (b < 10))if (a > 0 && b < 10) 更清晰。虽然结果一样,但读代码的人不用去查优先级表。

4.7 复合赋值运算符:少写点代码

VEX支持C语言风格的复合赋值运算符:

运算符 等价于
a += b a = a + b
a -= b a = a - b
a *= b a = a * b
a /= b a = a / b
a %= b a = a % b
a &= b a = a & b
a |= b a = a | b
a <<= b a = a << b
a >>= b a = a >> b

这些运算符不仅少打字,还有一个好处——变量只被求值一次。如果你写 arr[i++] = arr[i++] + 1,副作用会执行两次,结果不可预测。但写成 arr[i++] += 1,副作用只执行一次,行为是确定的。

4.8 知识体系总览

下面这张图是我自己整理的VEX运算符知识结构,你可以把它当作一个快速索引:

VEX运算符 算术运算符 + - * / % ** 比较运算符 == != < > <= >= 逻辑运算符 && || ! 位运算符 & | ^ ~ << >> 赋值运算符 = += -= *= /= 整数除法陷阱 浮点数比较精度 复合赋值副作用 短路求值特性 紧凑存储应用 核心原则:拿不准就加括号,代码可读性优先 表达式 = 有值 | 语句 = 有分号

这张图把运算符分成了五大类,每个类别下面都标注了常见的陷阱和应用场景。你可以把它当作一个快速参考,写代码时瞄一眼,能避免不少低级错误。

好了,运算符这块就聊到这儿。记住一句话:运算符是VEX的“动词”,变量是“名词”,把它们组合好,才能写出漂亮的“句子”。下一节我们会深入控制流,看看怎么用这些运算符写出真正的逻辑判断和循环。


公众号:蓝海资料掘金营,微信deep3321