BACK TO ARCHIVE
houdini crowd agents usd solaris pipeline dcc mechanics

Houdini 20 快速人群:Crowd MotionPaths 入门与 Agents/USD 的关键概念

04.10.2024 ADUCG RESEARCH

原文:https://www.andreaskj.com/fast-crowd-generation-in-houdini-20/

Houdini 20 引入了一套无需仿真(simulation)即可生成 Crowd 的新工具集:Crowd MotionPaths

这是一个用于在 SOPs 中直接对人群做动画的新系统,可以完全绕开传统的 crowd simulation / DOP 网络(当然在需要时,两者也可以配合使用)。它让艺术家能非常轻松地为“无需复杂人群仿真”的镜头快速搭建人群。

本文会带你了解这套新的 SOP Crowds 工具集,并解释 Agents(也就是 crowd 的“居民”)在 SOPs 与 LOPs/USD 中是如何工作的。

文中的渲染示例使用了 BIG/MEDIUM/SMALL 的角色资产;同时我也会说明如何用 Houdini 内置的双足测试角色来搭建你的测试 crowd,因此你不需要购买任何自定义角色也能完整跟做。

下面开始。

Agents

Agents 简介

Agent 是构成人群的角色/演员,也是你在创建 crowd 前需要首先搭建的内容。

在 Houdini SOPs 里,Agent 是一种特殊的 packed primitive。这一点非常关键:它意味着每个 agent 本质上就像“一个点”,里面打包了大量数据,需要时才解包(unpack)。因此在视口里操作 agent 非常快——在性能消耗不大的情况下,你可以显示成百上千个 agent。

Agent primitive 内通常包含:

  • rig(骨骼/绑定)
  • shape library(模型/网格库)
  • layers(层:例如武器/服装/道具变体)
  • animation clips(动画片段)
  • transform groups
  • 以及按需的 metadata 等

节点信息面板(中键节点)里显示的 Packed Agent。

搭建你的 Agents

在 Houdini 里搭建一个 Agent 非常直接:创建一个 Agent 节点,把角色连接进去即可。角色来源可以很多样:

  • FBX
  • Character Rig(传统 Houdini Rig)
  • Agent Definition Cache(可导出并在之后复用的特殊缓存格式)
  • 甚至是 USD

本文用 Houdini 内置的 Mocap Biped 做演示,确保所有人都能跟上;如果你有自己的已绑定 FBX 角色,也完全可以替换使用。需要注意的是你还需要动画 clips——视你的 rig 而定,你可能也能用 Mixamo 生成一些。

用 Houdini Mocap Biped 来搭建 agent 的步骤如下:

  1. /obj 里添加一个 Mocap Biped 1。参数基本保持默认即可,但 in-place animation 先不要开。后面我们会手动创建 in-place 动画,从而生成 locomotion joint。

Mocap Biped 1 设置

  1. 新建一个 geometry 节点,在里面创建 Agent SOP 并指向你的 Mocap Biped 1。然后开启 Convert to in-Place Animation,并把 Locomotion Node 设置为 Mocap Biped 1 的 hip joint。这样会“禁用 locomotion”(角色随移动产生的位移变化),但保留相关信息,便于后续做速度计算。

Agent 设置

  1. 最后设置 clip 的帧范围。默认 walk cycle 是 1–25。

设置 Clip Frame Range

到这里,你就拥有了一个带 walk cycle 的 Agent。如果你只需要它做这一段动作,你已经可以进入 crowd 的搭建流程。但在进入人群之前,我们还要先补齐一些关键能力。

这走路气势。

添加更多 Animation Clips

通常下一步是给 agent 挂更多动画 clips,这样你可以让人群的动作更丰富,并能根据镜头需求做适配。如果你在使用 FBX Character Rig,这里也可以加载你自制的动画。

因为我们使用的是内置 Mocap Biped,所以我们直接通过 Mocap Biped 节点来加载不同 animation clip。你会发现 Animation 下拉菜单里有不同选项:把这个节点复制 3 份,并在每个节点里选择不同动画。本文示例使用了 walkrunwaitzombie

复制并切换动画,得到 4 个不同动画的 Mocap Biped 节点

确保

Inplace Animation

仍然保持关闭。

接下来,为了把这些新 clips 加到 agent 上,我们需要创建 Agent Clip 节点。

Agent Clip 节点中,把 Locomotion Node(例如 Hips)设置得和 Agent 节点一致,并把 Character Rig 参数指向其中一个加载了其它动画的 Mocap Biped(我这里从 run 开始)。勾选 Convert to Inplace Animation 并为 clip 命名。

同时你还要为每个动画设置正确的 Start/End 帧范围。下图展示了 Mocap Biped 默认动画的对应范围。

你也可以通过把 Set Current Clip 设置为新 clip 的名称来预览该动画。

Agent Clip 节点设置

之后你就可以用同样方式继续添加剩余 clips:要么用多个 Agent Clip 节点串联,要么在一个节点里点 + 增加多个 clips。记得给每个 clip 起一个唯一名字。

需要注意:对于站立动画,不需要勾选

Convert to Inplace Animation

因为它没有 locomotion。如果硬转成 in-place,会看起来非常怪。

⚠️

另外一个提示:Mocap Biped 的动画并不是完美可循环(loopable),所以生产环境里你可能会用自家/外部动画库。如果你在复现示例时看到一些卡顿/跳帧,大概率就是这个原因。

绑定道具(Props)

很多时候只有角色还不够,你可能还需要给他们加剑、盾、帽子等道具。下面我会展示如何做。

小技巧:Time Dependency(时间依赖)优化

首先给一个重要的优化技巧。Houdini 有一个概念叫 Time Dependency(时间依赖)。我之前在 LOPs 里提过,在 SOPs 中同样存在。简单说,它会告诉 Houdini:从引入时间依赖的那个节点开始,需要每一帧都重新 cook 整条网络。

时钟图标代表 time dependency

在我们继续把 agent 网络变得更复杂之前,建议先临时去掉 time dependency。之后因为要处理动画,当然还得加回来;但在我们进行道具绑定与其它调整时,先移除它能让场景更流畅。

移除方法:加一个 Time Shift,并把帧设置为 $FSTART。注意这会采样该帧的动画,使结果暂时变成静态。

移除 time dependency

等你在本节做完所有调整后,需要在网络底部重新引入 time dependency,让动画 clips 正常工作:添加一个 Agent Edit,开启 Clip Time 并设为 $T。你会看到动画回来了,同时在该节点之后时钟图标也会再次出现。

请确保必须是

$T

因为 Agent Clip Time 使用的是“秒”,不是“帧”。

$T

会返回当前时间(秒)。

在网络底部重新引入 time dependency

使用 Agent Layers 添加道具

现在开始绑定 props。首先在 Agent Clip 节点之后添加 Agent Layer。该节点允许你为 agent 定义新的 layer。默认情况下你的 agent 会消失,这是因为节点默认创建了一个名为 layer1 的空 layer。这里我想用更程序化的方式来管理 props,所以可以直接点击 “x” 把这个 layer 删掉,让 agent 恢复显示。

点击这里删除默认 layer

接下来,你需要在该节点第二输入(Shape geometry)里接入一些几何体,这样 Agent LayerShapes 区域才会启用。我们先用一个 tube 做一把临时的“剑”。你可以把原始 agent 设为 template,以便把 tube 对齐到他的手上。

根据 agent 把 tube 放到正确位置

然后添加一个 Merge Packed,把变换好的 tube 接进去。严格来说不一定非要用它,但它很方便:会自动把输入 pack,并根据输入节点名称生成 name 属性。

用 Merge Packed 一次性加入多个 props

现在回到 Agent Layer,需要设置几个参数。首先启用 Shape Name Attribute 并设为 name,告诉节点如何分离输入几何。

然后展开 Shape Bindings,设置 Transform Name。这个参数决定你的 prop 要约束到哪个关节/transform——也就是说,prop 会跟随这里指定的关节动画。我这里选了 LThumb_To_LThumbEnd。最后在最底部,启用 Layer Name Attribute 并同样设为 name,这样每个带 name 的 primitive 会被拆成独立 layer(比如你输入了多把剑,就会各自成 layer)。

Agent Layer 参数

但你会遇到一个问题:查看 Agent Layer 输出时,prop 可能突然飞到空间中的其它位置。这是因为我们在 world space 中摆放 prop,但当它绑定到 joint 时,坐标空间变了。

别慌。Mikael Pettersén 在 Houdini 19 的 Hive Crowds 演讲中给过一段非常好用的 VEX:可以把 packed prop 从 world space 转到 joint 的坐标空间。把下面片段放进 Attribute Wrangle,并把 "LThumb_To_LThumbEnd" 替换为你在 Agent Layer 里约束的 joint 名称。Wrangle 第一输入接 packed prop,第二输入接 Agent Layer 之前的 agent。

int handId = agentrigfind(1, 0, "LThumb_To_LThumbEnd");
matrix hand_xform = agentworldtransform(1, 0, handId);
matrix xform = getpackedtransform(0, @ptnum);
xform *= invert(hand_xform);
setpackedtransform(0, @ptnum, xform);

完成后,prop 就能正确跟随动画了,你可以继续添加更多道具,并尝试绑定到不同关节。

为了预览 props,你可以加一个 Agent Edit,把 Current Layers 设为 default 加上你的 prop layer 名称(用空格分隔)。然后切到那个用 $T 恢复动画的 Agent Edit 节点,就能看到带所有动画的 prop 效果。

Agent 最终基础设置

Solaris 中的 Prop Transform 问题(请务必阅读)

这里给一个重要提示:按上面的方式绑定 props 后,进入 Solaris 时你可能拿不到正确的 transforms。原因是(我也不太清楚为什么)Solaris 似乎不会读取我们塞在 packedtransform 里的完整转换信息。

修复非常简单:在你截图中那个 convert_coordinate_space wrangle 后面加一个 Unpack,紧接着再加一个 Pack。这样做的原因是:它会丢弃 packedtransform,并把变换真正应用到点上。

记得把 PackUnpack 都设置为传递 name 属性。

Solaris transform 修复

MotionPath Crowds

准备完成后,我们终于可以进入人群生成部分,看看 Houdini 20 新增的 Crowd MotionPath 工具!

我们会先用并不算新的 Crowd Source 节点创建 crowd,然后再尝试 Houdini 20 新增的一系列 MotionPath Crowd 节点。

Crowd Source

Crowd Source 是 crowd 创建流程的核心:负责散布 agents,并设置随机起始参数(随机 agent state、clip time、heading 等)。你在迭代 crowd 时会经常回到这个节点调整。

把它放在你的 Agent 节点之后(如果你有多个 agents,可以先 merge,再接到 Crowd Source 第一输入)。该节点第二输入用于接入自定义的散布表面。

在 agent 创建之后添加 Crowd Source

注意:我使用了一个

Agent Edit

Current Clip 设置为 run,并把 current layers 设置为

default

以及

sword

(也就是我的 prop layer 名称)。这样默认 agents 会跑步并带剑。

Crowd Source 第一页主要是散布设置,有两种主要模式:FormationRandom

  • Random:类似普通 scatter,支持 relax iterations、用 density attribute 控制等。
  • Formation:适用于需要队列/阵型的场景,比如行军队伍。

Formation vs. Random 对比

Crowd Source 第二页更关键:你可以在这里做大量随机化,避免 crowd 看起来像复制粘贴。

我建议至少启用:

  • Randomize Clip Time(默认值通常够用)
  • Randomize Initial State(让 agents 初始动画随机——这里称为 states)
  • 如果你有多个 agent primitive,再启用 Randomize Agent Primitive

对于 Randomize Initial State,你可以点击 “+” 添加想用的动画 clips/states,并通过 weights 控制比例。

添加随机化后的 Crowd Source

这里先不用随机 layers——我会用 Crowd Assign Layers 来演示更高效的做法。

现在播放时间轴,你会看到 agents 的动画与时间点都随机化了,变化很丰富。当然还没有 locomotion,我们下一节解决。

随机化后的 crowd source 动画效果

Crowd Assign Layers

在进入 MotionPaths 前,最后讲一个关键点:如何随机分配 layers,让不是所有人都拿同一种 prop。最有效的方法是用 Crowd Assign Layers

在把 crowd 接入这个节点之前,建议先加一个 Agent Edit,把 Set Current Layers 设为 default。这样你就有了一个不带道具的“干净起点”。

Crowd Assign Layers 可以做很多事,但这里我们只演示在不同 props 之间随机切换:在 Layer Selection 部分点击 “+”,为每类 prop(以及一个“无 prop”的选项)添加一条,并填写对应 layer 名称。

节点会为每个 agent 随机选择指定 layer。你可以通过旁边的 Weight 控制某种道具出现的概率。

使用 Crowd Assign Layers 分配 layers

我的例子里加入了 default(无道具)、sword(剑),以及额外做的 ballpole(一根棍子顶着个球)。现在 crowd 会随机获得这些 layer。

如果需要更复杂的分配,你可以提高 Number of Assignments,这里先保持默认,你可以自行探索。

Crowd MotionPaths 基础

进入人群动画部分。MotionPath Crowds 是 Houdini 20 新增的一组工具,可在不做仿真的前提下快速搭 crowd。它既可以单独使用(本文就是),也可以与仿真 crowd 组合。

设置非常简单:把 Crowd MotionPath 接在我们之前的设置之后,它会根据当前动画 clips 的 locomotion 自动为 crowd 添加位移。

Crowd MotionPath 节点

直接添加 Crowd MotionPath 的效果

Crowd MotionPath 工具体系有个很优雅的设计:这一类节点通常都有两个输出——右侧输出 agents,另一个输出 curves(也就是 “MotionPath”)。

MotionPath 本质上就是 SOP curve,你可以像操作普通曲线一样对它做任何操作,然后再把它接回人群系统。

MotionPath 曲线上存储的点属性示例

它的工作原理是什么?

Crowd MotionPath 会基于当前动画 clips 的 locomotion 生成初始 curves。曲线的每个点包含诸如:agent 在世界空间的位置、cliploopsclipnamescliptimes 等数据;primitive attributes 上还包含 agentname,用于描述每条曲线属于哪个 agent。节点把这些数据分配给每个 agent,从而输出无需仿真的 crowd 动画。

这个工具集剩余的节点,主要用于“更安全/更容易”地编辑这些 MotionPath 曲线——因为你一般不想手动直接编辑它们(比如如果你没维护点间距,速度会出问题,效果会很怪)。下面我会覆盖一些常用节点,并说明适用场景。

⚠️

我偶尔遇到视口里 agent 局部消失的问题,出现得比较随机,可能是我机器的特例。如果你也遇到,最快的恢复方法是:把你正在查看的节点禁用/启用一次。

Crowd MotionPath 系列节点

Edit

Crowd MotionPath Edit 效果演示

首先是 Crowd MotionPath Edit。它是这一组里最简单的节点,通过自定义 viewer state 让你以更高效的方式编辑曲线。

你当然也可以不用这个节点、直接用 SOP 标准工具手动改曲线,但因为点间距与速度密切相关,手动改起来并不友好,容易破坏速度一致性。

用法:放下节点后在视口里按 Enter。你就可以移动曲线末端,并在曲线上任意位置左键添加控制点。节点会尽力在这些点之间插值出合理曲线,非常适合做艺术指导(art-directing)。

Follow

Crowd MotionPath Follow

如果你想用一条输入曲线来驱动 MotionPath crowd 的路径,或者希望以更“宏观”的方式设计他们的路径,可以用 Crowd MotionPath Follow

该节点允许你输入曲线,然后尽可能让 MotionPath 沿着该曲线移动;如果你输入多条曲线,它也会尝试相应地分割 MotionPath。

Avoid

Crowd MotionPath Avoid 也非常实用,几乎应该放进每个 MotionPath crowd。它主要做两件事:

  1. 计算 agents 何时可能相互穿插,并尝试让他们转向以避免相交
  2. 允许你在第三输入接入自定义 mesh,作为 agents 需要避让的障碍物

参数中有大量选项用于控制转向强度、彼此间距等。因为没有仿真,每次改参数都能立刻看到结果。

Trigger

Crowd MotionPath Trigger 中的 Bounding Region

Crowd MotionPath Trigger 提供了一套创建 crowd “触发器(triggers)”的工具,主要用途是在某些条件下触发动画状态切换(下一节会看到)。

你可以用 Type 下拉菜单定义 trigger 的方式:

  • Time:在特定时间触发
  • Bounding Region:绘制一个区域,进入区域时触发
  • Object Distance:agent 靠近某对象时触发
  • Object Raycast:agent 发出的 ray 命中对象时触发
  • Neighbour Distance:附近有其它 agents 时触发
  • Animation Clip:满足某些 animation clip 条件时触发

Transition

最后,Crowd MotionPath Transition 允许你的 agents 基于 trigger(或多个 triggers 组合)在不同动画之间平滑过渡(可查看节点里的 Combine Triggers 部分)。

下面这个例子里,我让 agents 在进入某个区域时,从跑步动画切换到摔倒动画。

Crowd MotionPath Evaluate

在进入 Solaris 渲染之前,最后一个需要讲的是 Crowd MotionPath Evaluate。它通常是 MotionPath 设置的最后一个节点:在指定时间(默认当前帧)对 crowd 进行 evaluate,并输出带 transforms 与 animation clips 的 agents。

我建议用 filecache 缓存该节点输出,这样后续可以直接从磁盘流式读取数据。

基础 SOP Crowd Import 概览

💡

注意:time dependency(节点旁的时钟图标)直到这个节点才会出现。

USD/Solaris 中的人群

下面进入渲染部分。我也借此机会解释一下:agents(更广义的 rigged characters)在 USD 中的工作方式。

USD 中的 Agents(UsdSkel)

UsdSkel 是 USD 中的一个 Schema(可以理解为类)与 API,用于基于 skeleton 高效地做几何形变(deform)。如果没有这些机制,你通常需要把变形后的网格点缓存并直接加载到内存,这会非常重,尤其是面对人群时。

本节会从 crowd/agents 在 Solaris 中的角度,概览这种动画数据的结构与组织方式。

💡

请注意:并非所有 render delegate 都已经支持 agent 的正确 instancing。以本文写作时(2024 年 4 月)为准:Karma 与 RenderMan 支持,但 V-Ray 等不支持。

基础 SOP Crowd Import 的 scenegraph 示例

当你在 Solaris 中使用 SOP Crowd Import(下一节会讲)导入 crowd 后,你会得到类似上图的 scenegraph。

每个 agent(上图中名为 agent1_0agent1_1 等)包含该 agent 对应的 SkelRoot 实例(你可以在 agentdefinitions/agent1/layers/ 路径看到它——这是所有可用 agent layer 变体的集合)。在 USD 中,所有可 skin 的 primitives/geometry 都必须位于 SkelRoot 容器之下。

在每个 SkelRoot 中,你还能看到一个 Skeleton primitive,它包含 rig 的 bind skeleton,存储 joint 层级、bind transforms 等重要属性。它与 mesh(其中包含权重与 skinning 类型信息)之间会形成关系。两者组合起来构成一个 agent。上图里,几何体本身是对 agentdefinitions/agent1/shapelibrary 中某个 mesh 的引用。

动画数据在哪里?它不是直接存储在 skeleton 上,而是存储在另一种 primitive:SkelAnimation 上,并且位于每个 agent 之下(因为每个 agent 的动画可能略有不同)。最顶层的 group(例如 agent1_0agent1_1 等)通过 skel:animationSource relationship 指向各自的动画源。SkelAnimation primitive 本身包含每个 joint 的旋转/缩放/位移的时间采样(timesampled)属性。把这些组合起来,就得到与 SOP 输出匹配的动画 agents。

这部分确实偏技术,但希望它能帮助你建立 USD 中 agents/crowds 的基本概念。如果你想深入阅读,推荐查看官方 USD 文档中的 UsdSkel Schema 页面:https://openusd.org/docs/api/usd_skel_page_front.html?ref=andreaskj.com。你并不需要完全理解这些才能渲染 crowd,但对 USD 如何与导出结果交互有一个概览,会非常有帮助。

使用 SOP Crowd Import 导入/导出

说完这些,下面说明如何把 crowd 加载到 Solaris 并导出为 USD。好消息是:Houdini 已经把大多数繁琐细节自动处理了。

Houdini 自带一个叫 SOP Crowd Import 的节点:只要把你的 crowd 路径填到 SOP Path,就基本完成了。

我也建议把 Cache Behaviour 设为 Always Cache All Frames,以确保获得时间采样(并避免 time dependency),并开启 Animation Save PathGeometry Save Path ——否则 USD ROP 可能会提示需要自动生成这些路径。

SOP Crowd Import 节点

总结

以上就是 Houdini 中的 MotionPath Crowds。希望这篇文章对你有帮助。Crowd 非常好玩,我在写作过程中也学到了不少。

欢迎在评论区分享你的想法,也欢迎告诉我你希望我未来覆盖哪些主题。

如果你觉得这篇文章有意思,想看更多内容,也可以订阅作者的 newsletter,把新文章直接推送到你的邮箱。

本文采用 Creative Commons BY-NC-ND 4.0 协议进行授权。

BY-NC-ND: 署名-非商业性使用-禁止演绎

End of Article