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。
% 只能用于整数。如果你对浮点数取模,编译器会报错。我习惯先转成整数再操作。
幂运算 ** 在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) {
// 处理选中逻辑
}
4.5 表达式与语句:代码的组织方式
表达式是“有值的代码片段”。比如 a + b 是一个表达式,它的值是a和b的和。语句是“执行一个动作的代码单元”,以分号结尾。
VEX里常见的语句类型:
- 表达式语句:
@P.y += 0.1; - 声明语句:
float radius = 1.5; - 控制流语句:
if、for、while等 - 返回语句:
return value;
这里有个容易混淆的地方——赋值是表达式,不是语句。在VEX里,a = 5 是一个表达式,它的值是5。加上分号变成 a = 5; 才是一条语句。这意味着你可以这样写:
int b = (a = 5) + 3; // a被赋值为5,b被赋值为8
我个人不太推荐这种写法,可读性太差。但在某些紧凑的循环里,偶尔会用一下。
4.6 运算符优先级:别让括号救你
运算符优先级决定了表达式的计算顺序。VEX的优先级跟C语言基本一致:
- 括号
() - 一元运算符
!~- - 乘除取模
*/% - 加减
+- - 移位
<<>> - 比较
<>==!= - 按位与
& - 按位异或
^ - 按位或
| - 逻辑与
&& - 逻辑或
|| - 赋值
=+=-=等
我的建议是:拿不准就加括号。别觉得丢人,代码是给人读的,不是给编译器读的。我在项目里见过一行代码写了七八个运算符,不加括号,结果维护的人(包括我自己)看了半天才搞明白意图。
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的“动词”,变量是“名词”,把它们组合好,才能写出漂亮的“句子”。下一节我们会深入控制流,看看怎么用这些运算符写出真正的逻辑判断和循环。
公众号:蓝海资料掘金营,微信deep3321