进入影视特效和游戏CG行业后,会经常听到一个词:Pipeline(管线)。管线的本质并不是程序员的专利,它是一套让重复劳动变得可复用、可维护、可追踪的工作方法。对于 Houdini 艺术家来说,学会 Python 最大的价值,不是从美术转行去写系统,而是能把自己“每天都要手工重复做的事”交给代码去执行:建节点、接输出、批量设置路径、整理命名、检查场景规范。
这套教程会从“为什么学”“如何跑起来”开始,讲到 Python 基础语法、Houdini 的hou节点模型,再通过批量命名检查、批量创建 File Cache、把脚本封装成工具架按钮三个完整案例,带你走一遍“面向 Houdini 艺术家的 Python 自动化”全流程。无论你是刚开始接触 Python 的新人,还是在项目中做过一些 VEX、想系统补足脚本能力的 TA,都可以按章节逐步跟练。
1. 为什么 Houdini 艺术家要系统学 Python
1.1 让重复工作交给代码,把时间留给创意
Houdini 老用户应该都有这类经历:一个角色、一辆车或一个地形资产,在进入光照环节前要整理命名、创建输出 Null、统一缓存路径;一个镜头里的几十个资产,命名前缀可能来自不同组员,有人用TERRAIN_,有人用Terrain_,还有人直接叫heightfield1。如果这些整理动作全部靠手工在节点编辑器里完成,不仅慢,而且很容易漏。
Python 在 Houdini 里的角色,就是给你一个“可以在你的场景里执行批处理操作”的入口。它不像 VEX 那样作用于每一个点、每一个面,而是在更高的层级工作:可以是“遍历/obj下所有 Geometry 节点”,可以是“读取当前选中节点的参数”,也可以是“在/out创建 ROP 并设置输出路径”。
对于一个 VFX 镜头来说,算力浪费可以靠渲染农场解决,流程混乱却会成倍消耗人力。Python 帮艺术家解决的问题,本质上不是“算得快”,而是“不用人来重复做重复事”。
1.2 Python、VEX 与 HOM:先分清分工
很多新手会把 Python 和 VEX 混在一起,看到 Houdini 里能写表达式、能写 VEX、还能写 Python,于是不知道应该重点学哪个。这里做一个简化理解:
- VEX:运行在 Houdini 的 SOP、DOP 内部,适合做几何、物理数据的逐点、逐面、逐体处理。
- HScript 表达式:适合用来驱动参数、写简单的按钮回调,但它能做的事情有限。
- Python(HOM,Houdini Object Model):控制场景层级、节点网络、参数、文件、渲染和资产流程,适合做项目级自动化工具。
这三者互相有交叉,但在实际工作中定位不同。下面用一个表格区分:
| 应用方向 | 推荐工具 | 案例 |
|---|---|---|
| 点位置偏移、属性计算 | VEX | 让所有点的@Cd根据高度变化 |
| 参数动态变化 | HScript / VEX 表达式 | 让tx随时间跳动 |
| 批量创建、改名、连节点 | Python | 一键搭建多个缓存节点 |
| 批量检查文件和贴图路径 | Python | 扫描场景中失效的贴图路径 |
| 驱动 HDA 自动化逻辑 | Python | 在 HDA 内部按钮上执行批量操作 |
对想要进入 TA(Technical Artist)方向的 Houdini 艺术家来说,Python 是更接近“工具开发”的一门技能,而 VEX 更接近“效果开发”。两者并不冲突,但从管线自动化的角度,Python 往往才是流程侧的主角。
1.3 学 Python 后能解决哪些典型问题
通常掌握 Houdini Python 后,你可以把下面这些工作变成“选中节点 → 执行脚本 → 完成”:
- 批量创建多个输出 Null,统一命名。
- 扫描整张镜头的节点树,检查是否存在空节点、未连接节点、不合理命名。
- 批量设置缓存文件的输出目录,并为不同对象生成符合命名规范的
.bgeo序列。 - 将多个 SOP 节点的最终状态一键导出成 Alembic。
- 定时清理场景中无用的参考文件和贴图节点。
- 把常用的操作封装进 HDA,让普通艺术家不需要打开 Python Shell 也能使用。
我在项目中更看重的,并不是艺术家能写出多漂亮的算法,而是遇到重复流程时,他愿不愿意停下来想一想:“这套动作能不能写成一个小工具,让组里所有人都用?”这个习惯一旦形成,工作效率的提升会非常明显。
2. 环境准备:写 Python 之前先选对运行环境
2.1 使用 Houdini 内置 Python,不一定需要单独安装
先说一个常见误区。很多初学者看到“Python 教程”,第一反应是先下载一个 Python,然后配置环境变量,再考虑要不要装 PyCharm 或 VS Code。这个流程用于普通的软件开发没问题,但如果你的目标只是在 Houdini 里写自动化脚本,那么Houdini 安装包里已经内置了 Python 解释器,你不需要额外安装任何 Python,也不需要配置系统环境变量。
需要理解的是,Houdini 使用的 Python 环境是一套被“绑定”在 Houdini 软件内部的解释器,它能加载hou这个核心模块。如果你单独安装一个纯 Python,然后用命令行python去执行import hou,通常会报错:
ModuleNotFoundError: No module named 'hou'hou模块并不是一个独立的开源包,而是 Houdini 内置的功能模块。所以最省力的方式,就是在 Houdini 中直接运行 Python 脚本。
当然,这不意味着外部 Python 完全没有用。如果你以后要开发独立的资产库管理工具、爬虫脚本或 Web 服务,它们可能会放到公司共享服务器上跑,那时你再用外部 Python 也不迟。但在 Houdini 自动化入门阶段,请先把精力集中在 Houdini 自带环境里。
2.2 三个日常写 Python 的入口
Houdini 中常用的 Python 入口有三个,建议从第一天起就熟悉它们。
第一个是Python Shell。在 Houdini 菜单栏点击Windows → Python Shell,底部会出现一个命令行窗口。它的体验和普通 Python 终端很相似,适合快速测试单行代码:
import hou node = hou.node("/obj") print(node)第二个是Python Source Editor。在Windows → Python Source Editor打开,这里可以粘贴一段完整的 Python 代码,点击运行。它比 Shell 更适合做脚本开发,因为你可以编写多行函数,而不必受单行输入限制。
第三个是Houdini 工具架工具(Shelf Tool)。当你创建并编辑一个工具时,可以把 Python 代码放到工具的脚本区域。这样以后只要点击工具架上的按钮,脚本就会执行。正式做小组工具时,这种方式对普通艺术家最友好。
如果觉得 Houdini 自带的编辑器功能不够强,你完全可以在自己的 VS Code、PyCharm 中按“纯 Python 代码”的语法高亮去编写脚本,然后复制粘贴到 Houdini 的 Python Source Editor 中运行。不过要注意,此时代码里仍然包含import hou,必须回到 Houdini 环境运行,外部解释器执行会失败。
2.3 无 UI 环境下用 hython 执行脚本
Houdini 自动化不只发生在图形界面里。在批处理、验证 Hip 文件、跑渲染农场时,我们有时需要在没有 UI 的服务器或命令行环境中执行脚本。这时会用到一个叫hython的工具,它是 Houdini 提供的 Python 解释器,通常可以从开始菜单的 Houdini Command Line Tools 中启动。
在命令行中输入:
hython -c "import hou; print(hou.version())"如果返回 Houdini 版本号,说明命令行环境可以正常工作。也可以用hython直接运行一个脚本文件:
hython myscript.py在hython模式下,hou模块依然可以使用,但很多 UI 方法不可用,例如hou.ui.selectNode()、hou.ui.displayMessage()。所以脚本里如果涉及用户交互,最好先判断当前是否有 UI:
import hou if hou.isUIAvailable(): selected = hou.ui.selectNode(title="请选择一个节点") else: selected = None print("当前环境没有 UI,无法弹出节点选择窗口")这种写法在工具进入批量处理流程后非常重要,否则脚本在命令行模式下很容易因为弹窗报错而中断。
2.4 Python 2 与 Python 3 兼容性提醒
Houdini 从某个版本开始从 Python 2 迁移到 Python 3,目前大多数新项目已经采用 Python 3。但 VFX 行业经常有长期维护的旧项目、旧插件、旧 HDA,里面可能仍然保留 Python 2 语法痕迹。因此看到报错时,先确认当前 Houdini 版本使用的 Python 大版本,再判断是代码逻辑问题还是语法兼容问题。
常见的区别主要有:
# Python 3 print("hello") # Python 2 中也可以写成 print "hello"如果你在自己的 Houdini 版本里运行print "hello"报语法错误,说明当前环境是 Python 3。写脚本时,建议统一使用 Python 3 的print()函数写法,并尽量避免unicode、long这类跨版本差异较大的类型。
3. Python 核心语法:面向 Houdini 的速学
这一节不会像大学教材那样把 Python 的所有语法都展开,而是聚焦 Houdini 艺术家日常写脚本最常用的核心内容。你可以先了解一遍,再回到自己的场景中改造。
3.1 变量、列表与字典
变量用于保存数据。在 Houdini 脚本中,最常见的“数据”就是节点对象、字符串路径、浮点数参数值。
import hou my_node = hou.node("/obj/geo1") node_name = my_node.name() print(node_name)列表和字典是两种非常常用的数据结构。
列表适合保存一组同类型数据,例如多个节点:
all_children = hou.node("/obj").children() for child in all_children: print(child.path())字典适合保存“键值对”数据,例如一个节点的多个参数值:
info = { "name": "geo1", "type": "geo", "count": 100 } print(info["name"])在编写批量处理脚本时,经常会把“节点列表”和“参数映射表”组合在一起使用。例如先获取所有需要处理的节点,再用字典维护每个节点的自定义配置。
3.2 条件、循环与列表推导
条件判断用来根据节点类型或参数值决定是否处理:
for child in hou.node("/obj").children(): if child.type().name() == "geo": print("这是一个 Geometry 容器:", child.path()) else: print("跳过:", child.path())循环几乎是所有批量操作的基础。配合continue、break可以精准控制遍历过程:
for child in hou.node("/obj").children(): if not child.isLockedHDA(): child.moveToGoodPosition()列表推导是一种更简洁地生成列表的写法。下面的代码等价于遍历后筛选:
geo_nodes = [n for n in hou.node("/obj").children() if n.type().name() == "geo"]这种写法在脚本中很常见,含义是:遍历/obj的子节点,只保留类型名是"geo"的节点。第一次看到时可以把它拆成普通循环来理解,熟练后能很大程度缩短代码。
3.3 函数:让自己少写重复代码
如果一段逻辑要在多个地方使用,就应该封装成函数。函数让脚本结构清晰,也方便以后移动到工具架、HDA 中继续复用。
import hou def get_geo_nodes(): """获取 /obj 下所有 Geometry 容器节点。""" obj = hou.node("/obj") if obj is None: return [] return [child for child in obj.children() if child.type().name() == "geo"] geo_nodes = get_geo_nodes() for node in geo_nodes: print(node.name())上面这段代码中:
def get_geo_nodes():表示定义一个函数。"""..."""是文档字符串,用来描述函数作用。return返回结果。
定义函数时还可以接收参数。例如你想写一个能批量修改任意容器下所有节点名字前缀的函数:
def add_prefix(parent_path, prefix): parent = hou.node(parent_path) if parent is None: print("节点不存在:", parent_path) return for child in parent.children(): new_name = prefix + "_" + child.name() child.setName(new_name, unique_name=True)函数把“操作对象”和“操作范围”都暴露出来,脚本的通用性会高很多。
3.4 文件路径与异常处理
处理缓存路径时,推荐使用os.path.join而不是直接拼接字符串。比如在不同操作系统上,路径分隔符可能不一样,直接拼字符串容易出错:
import os output_dir = "$HIP/geo/cache" output_file = os.path.join(output_dir, "terrain.$F4.bgeo.sc") print(output_file)异常处理用于捕获脚本运行时的错误。在没有异常处理时,脚本可能因为一个节点不存在就直接中断:
import hou try: node = hou.node("/obj/geo_missing") node.parm("tx").set(1) except AttributeError: print("节点不存在,请检查路径")对于工具脚本来说,用户不一定是代码作者,他们看到的应该是清晰的提示信息,而不是满屏红色 Traceback。所以要在关键操作处做好异常处理。
4. 理解hou模块:操纵 Houdini 的钥匙
4.1 从hou.node开始遍历场景
hou模块最核心的入口就是hou.node()。它接收一个路径字符串,返回对应的节点对象。如果路径不存在,则返回None,并不会直接抛出异常。所以很多脚本需要先做空值判断。
import hou obj = hou.node("/obj") print(obj)拿到节点对象后,可以用.children()获取它的直接子节点,用.parent()获取父节点,用.path()获取完整路径。
geo = hou.node("/obj/geo1") for child in geo.children(): print(child.path())要注意children()默认只返回当前层级直接子节点,不会递归到底层。如果你希望遍历某个 Geometry 容器内部的所有嵌套节点,需要自己写递归,或者根据流程控制遍历深度。
4.2 筛选节点并读取参数
场景复杂后,节点类型非常多,直接遍历容易把不相干的节点也带进来。通常先用.type().name()判断节点类型:
for child in geo.children(): node_type = child.type().name() if node_type == "filecache": print("缓存节点:", child.path()) elif node_type == "null": print("输出 Null:", child.path())读取参数用.parm()和.eval():
file_parm = geo.parm("file") if file_parm is not None: value = file_parm.eval() print(value).parm()返回的是一个参数对象。如果参数名不存在,它会返回None。因此在设置参数前,最好先判断参数是否存在,或者用hou.parm相关方法捕获异常。例如想修改多个参数,可以使用节点上的.setParms():
node = hou.node("/obj/geo1/box1") node.setParms({ "tx": 2.0, "ty": 3.0, "tz": 0.0 })参数名必须和实际节点中的内部名称一致,不能写成界面上的中文标签。例如 Houdini 界面可能显示“平移 X”,但脚本参数名仍然是tx。如果你不确定某个参数名,可以在节点上右键查看参数表达式,或把鼠标悬停在参数上方查看帮助提示。
4.3 创建节点并设置参数
创建节点最常见的方式是使用父节点的createNode()方法。第一个参数是节点类型名,第二个参数是给新节点起的名称。
import hou obj = hou.node("/obj") geo = obj.createNode("geo", "MY_ASSET") sphere = geo.createNode("sphere", "SPHERE_SOURCE") null = geo.createNode("null", "OUT") print(geo.path())如果场景里已经存在同名节点,Houdini 会自动在名字后面加数字编号。你也可以在创建时使用exact_name=True来让createNode严格使用指定名称,但这要求该名字没有被占用。
4.4 连接节点与场景布局
创建节点后,通常需要把节点连接到一起。setInput(0, source_node)表示把源节点的输出连接到目标节点的第一个输入。
null.setInput(0, sphere)也可以用.setNextInput()让 Houdini 自动寻找下一个空闲输入。
新节点创建后往往堆叠在一起,位置不好看。Houdini 提供了moveToGoodPosition()和父节点上的layoutChildren():
geo.layoutChildren()这一步对艺术家非常友好。脚本生成的节点,如果布局混乱,使用体验会大打折扣。
再组合一个完整示例。下面的代码会在/obj下创建一层 Geometry 节点,在其中创建一个 Box、一个 Null,并把 Null 连接到 Box 后面:
import hou def create_box_asset(asset_name): obj = hou.node("/obj") geo = obj.createNode("geo", asset_name) box = geo.createNode("box", "BOX_SOURCE") out_null = geo.createNode("null", "OUT") out_null.setInput(0, box) box.moveToGoodPosition() out_null.moveToGoodPosition() geo.layoutChildren() return geo created = create_box_asset("MY_BOX_ASSET") print("创建成功:", created.path())运行这段代码后,你会看到/obj下多了一个 MY_BOX_ASSET 的 Geometry 节点,内部结构是 Box 连接到 Null。虽然功能简单,但已经包含了“创建节点、连接节点、布局节点、返回结果”的所有核心知识点,是后续复杂工具的最小单元。
5. 实战案例:批量规范检查与命名整理
5.1 需求背景
项目中的资产数量往往很多。不同艺术家创建的 Geometry 节点,命名风格可能差异很大。上灯光、上材质、做渲染前,TD 通常需要统一整理命名。人工一个个看节点名,不仅效率低,还容易出现“改错节点、改漏输出”的问题。
下面这个工具脚本,可以用来扫描/obj下所有 Geometry 容器节点,检查其内部节点命名是否符合规范。这里以一个非常简化的规范为例:
- Geometry 容器名统一以
AST_开头; - 容器内部的核心输出 Null 命名为
OUT_; - 若名称不符合规范,脚本会打印出相关信息,而不是直接修改。
先修改信息再执行,是最安全的做法。因为脚本一旦直接改名,连锁反应可能影响材质、动画、渲染引用。
5.2 编写检查脚本
import hou def check_geo_naming(prefix="AST_"): obj = hou.node("/obj") if obj is None: print("/obj 节点不存在") return geo_nodes = [n for n in obj.children() if n.type().name() == "geo"] errors = [] for geo in geo_nodes: geo_name = geo.name() if not geo_name.startswith(prefix): errors.append("容器名称不符合规范: {}".format(geo.path())) for child in geo.children(): if child.type().name() == "null" and not child.name().startswith("OUT"): errors.append("Null 命名不规范: {}".format(child.path())) if errors: print("发现 %d 个命名问题:" % len(errors)) for msg in errors: print(" -", msg) else: print("检查通过,所有命名符合当前规范") check_geo_naming("AST_")在这个脚本中,我把检查逻辑封装成一个函数,前缀prefix作为参数传入,方便以后改成其他前缀。脚本运行后,如果有命名问题,需要人工确认后再修改。这种“先报告、后处理”的方式对于团队工具非常重要。
5.3 增加一键修复功能
如果你希望脚本直接把不符合规则的节点改掉,可以在检查之后增加一个修复函数:
def fix_geo_naming(prefix="AST_"): obj = hou.node("/obj") if obj is None: return for geo in [n for n in obj.children() if n.type().name() == "geo"]: if not geo.name().startswith(prefix): new_name = prefix + geo.name() geo.setName(new_name, unique_name=True) print("已修改:", geo.path()) for geo in obj.children(): if geo.type().name() == "geo": for child in geo.children(): if child.type().name() == "null" and not child.name().startswith("OUT"): child.setName("OUT_" + child.name(), unique_name=True) print("已修改:", child.path())请注意,修改名称可能会让某些引用改名的外部脚本失效。在正式生产环境中,一键改名工具建议限定在镜头内部块使用,并且最好先确认该节点不是被多处引用的共享资产。
6. 实战案例:批量创建 File Cache 并统一缓存路径
6.1 应用场景
Houdini 项目中,为了提升视口交互速度和渲染效率,常用的做法是把耗时的模拟结果或复杂节点缓存成.bgeo文件。Houdini 提供了 File Cache 节点来帮助完成这个操作。
但当一个场景里有多个需要缓存的 SOP 节点时,手动逐个创建 File Cache 节点、逐个选择缓存目录、逐个设置文件名,是一件非常繁琐的事。更麻烦的是,如果路径命名不统一,后期查找缓存、写渲染脚本时也会出现各种问题。
因此,这个实战案例的目标是:选择多个 SOP 节点,批量给它们创建 File Cache 缓存节点,并自动设置统一的输出路径。
6.2 编写脚本
import hou import os # 缓存文件输出根目录,可以使用 $HIP、$JOB 等 Houdini 变量 OUTPUT_ROOT = "$HIP/geo/cache" # File Cache 节点类型名 FILE_CACHE_TYPE = "filecache" def create_cache_nodes_for_selected_sops(): selected = hou.selectedNodes() if not selected: print("请先在节点编辑器中选择需要缓存的 SOP 节点") return # 筛选出 SOP 类别的节点 sop_nodes = [n for n in selected if n.type().category().name() == "Sop"] if not sop_nodes: print("当前选中的节点不是 SOP 节点") return # 验证 File Cache 节点类型在当前版本