OpenUSD PrimHints 权威指南:用 uiHints 控制 Prim 在 UI 中的显示分组与条件展示
【免费下载链接】OpenUSDUniversal Scene Description项目地址: https://gitcode.com/GitHub_Trending/ope/OpenUSD
PrimHints 是 OpenUSD(Universal Scene Description)中一类面向 Prim 级别的 UI 提示(UI Hints)机制,用于告诉 DCC 工具或应用程序:Prim 下的属性应当如何按"显示分组(display groups)"组织、哪些分组默认展开或折叠、以及哪些分组在什么条件下才显示。本文将以 docs/user_guides/schemas/usdUI/PrimHints.md 为骨架,结合仓库中pxr/usd/usdUI的源码实现与测试用例,系统讲解 PrimHints 的字段语义、USDA 编写方式、Python API 用法,以及与ObjectHints、PropertyHints的协作关系。读完本文,你将能够为任意 Prim 编写一套可被工具直接消费的 UI 呈现规范。
图注:一个 mock 场景浏览器中应用 PrimHints 后的 UI 效果示意,其中 "Controller" 分组默认展开,"Widget Settings" 分组默认折叠(详见下文示例)。
PrimHints 的定位:UI Hints 体系中的 Prim 级入口
在 OpenUSD 的 UsdUI 域中,UI Hints 是一组"类 schema"(schema-like)的元数据约定,它们统一存放在对象的uiHints字典元数据(metadata)中,用于描述一个对象在 UI 中的呈现方式。UI Hints 按作用对象划分为四层,见 docs/user_guides/schemas/usdUI/overview.md:
- ObjectHints:适用于任何 Prim 或属性(Property)的通用提示,如用户可见的
displayName、是否在 UI 中隐藏的hidden。 - PrimHints(本文主题):Prim 级提示,例如
displayGroupsExpanded(哪些显示分组默认展开)与displayGroupsShownIf(哪些显示分组在什么条件下才显示)。 - PropertyHints:属性级提示,如属性归属的
displayGroup、控制属性是否显示的shownIf表达式。 - AttributeHints:属性值级提示,如枚举值标签
valueLabels及其顺序valueLabelsOrder。
从源码看,PrimHints 对应 C++ 类UsdUIPrimHints,定义于 pxr/usd/usdUI/primHints.h,其继承自UsdUIObjectHints(pxr/usd/usdUI/objectHints.h),即 Prim 天然同时具备 ObjectHints 的能力。UsdUIPrimHints的注释明确指出:它是"schema-like"包装器,解释UsdPrim实例上uiHints字典中的字段并提供便捷 API,并非正式 schema,不派生自UsdSchemaBase。
文档 PrimHints.md 开头也强调:该文件不是自动生成的(文件头部注释注明 "This file isnotgenerated, as UIHints are not defined in schema.usda")。也就是说,displayGroupsExpanded、displayGroupsShownIf这类 PrimHints 字段并未像普通 schema 属性那样定义在 pxr/usd/usdUI/schema.usda 中,而是由UsdUIPrimHints等 API 按约定解释uiHints字典键值。字典键的 token 定义在 pxr/usd/usdUI/objectHints.h 的USDUI_HINT_KEYS宏中,包括uiHints、displayName、displayGroup、hidden、shownIf、valueLabels、valueLabelsOrder、displayGroupsExpanded、displayGroupsShownIf。
一个直观的最小示例
PrimHints 文档给出了如下示例:一个名为ControllerA的 Prim,拥有多个属性,并分属两个显示分组 "Controller" 和 "Widget Settings"。Prim 级uiHints指出:DCC 工具初始应完整展开 "Controller" 分组、不展开 "Widget Settings" 分组;且 "Widget Settings" 仅在表达式widgetReadOnlyMode == 0求值为真时才显示。
def "ControllerA" ( uiHints = { # UI hints from ObjectHints string displayName = "ControllerA" bool hidden = 0 dictionary displayGroupsExpanded = { bool "Controller" = 1 bool "Widget Settings" = 0 } dictionary displayGroupsShownIf = { string "Widget Settings" = "widgetReadOnlyMode == 0" } } ) { float controlValue = 1.0 ( uiHints = { string displayName = "value" string displayGroup = "Controller" } ) bool controlIsOffset = false ( uiHints = { string displayName = "is offset" string displayGroup = "Controller" } ) bool showWidget = true ( uiHints = { string displayName = "show" string displayGroup = "Widget Settings" } ) color3f[] widgetColor = (1.0, 0.5, 0.5) ( uiHints = { string displayName = "color" string displayGroup = "Widget Settings" } ) float widgetSize = 10.0 ( uiHints = { string displayName = "size" string displayGroup = "Widget Settings" } ) bool widgetReadOnlyMode = 0 }注意:displayName与hidden属于 ObjectHints(在uiHints字典内以注释标明来源);displayGroup属于 PropertyHints,标注每个属性归入哪个显示分组。PrimHints 的两组字典则控制这些分组在 UI 中的整体呈现。文档同时给出了该 Prim 在场景浏览器中的 mock 效果图(uihints-primhints.svg),其中widgetReadOnlyMode为false(即 0)时,"Widget Settings" 分组可见。
PrimHints 字段详解
displayGroupsExpanded
USD 类型:dictionary
以显示分组名称为键的字典,指示各显示分组在 UI 中默认是展开(1/true)还是折叠(0/false)。未在字典中列出的分组,工具可自行决定默认状态(见下文源码默认值分析)。
对应 C++ API 位于 pxr/usd/usdUI/primHints.h:
VtDictionary GetDisplayGroupsExpanded() const:返回整个展开字典;bool SetDisplayGroupsExpanded(const VtDictionary& expanded):整体写入展开字典;bool GetDisplayGroupExpanded(const std::string& group) const:查询单个分组是否默认展开;bool SetDisplayGroupExpanded(const std::string& group, bool expanded):设置单个分组的展开状态。
从 pxr/usd/usdUI/primHints.cpp 的实现可以看到几个关键细节:
- 读取通过
_prim.GetMetadataByDictKey(UsdUIHintKeys->UIHints, UsdUIHintKeys->DisplayGroupsExpanded, &dict)从uiHints字典中按键取子字典;若未authoring,返回空字典。 - 写入校验:
SetDisplayGroupsExpanded会遍历字典所有条目,若存在非bool值则通过TF_CODING_ERROR报错并拒绝写入(测试用例 testUsdUIHints.py 中也验证了传非 bool 值的错误路径)。 - 单分组查询默认值:
GetDisplayGroupExpanded使用VtDictionaryGet<bool>(expandedDict, group, VtDefault = false),即未authoring 的分组默认按false(折叠)处理。测试同样断言了GetDisplayGroupExpanded('non-existent')返回False(见 testUsdUIHints.py)。 - 嵌套分组的扁平化存储:
SetDisplayGroupExpanded特意不使用SetMetadataByDictKey直接写单键,而是"先读整字典、改条目、再整字典写回"。源码注释解释得很清楚:因为分组名本身可能含冒号分隔符(如"A:B:C"),直接按字典键写入会把它们变成嵌套子字典;而我们希望所有分组条目平铺在displayGroupsExpanded顶层:
dictionary displayGroupsExpanded = { bool "A" = 1 bool "A:B" = 1 bool "A:B:C" = 1 }而不是:
dictionary displayGroupsExpanded = { dictionary A = { dictionary B = { bool C = 1 } } }- 组合语义:该字段是字典值,其合成值(composed value)是所有相关编辑目标(edit targets)中条目级(per-entry)覆盖合并的结果,而非整个字典整体覆盖。这一语义同样适用于
displayGroupsShownIf,在头文件注释中明确说明。
displayGroupsShownIf
USD 类型:dictionary
以显示分组名称为键的字典,键对应的值是基于SdfBooleanExpression(pxr/usd/sdf/booleanExpression.h)的表达式字符串。表达式求值为真时,对应显示分组才在 UI 中显示。
对应 C++ API(pxr/usd/usdUI/primHints.h):
VtDictionary GetDisplayGroupsShownIf() const;bool SetDisplayGroupsShownIf(const VtDictionary& shownIf);std::string GetDisplayGroupShownIf(const std::string& group) const;bool SetDisplayGroupShownIf(const std::string& group, const std::string& shownIf)。
primHints.cpp 中SetDisplayGroupsShownIf的写入校验要求所有值必须是std::string,否则TF_CODING_ERROR拒绝写入。测试用例 testUsdUIHints.py 中同样覆盖了"给DisplayGroupsShownIf传 int 值报错"的场景。
与displayGroupsExpanded不同,GetDisplayGroupShownIf/SetDisplayGroupShownIf的单分组读写使用_MakeKeyPath(UsdUIHintKeys->DisplayGroupsShownIf, TfToken(group))拼接键路径(_MakeKeyPath以命名空间分隔符:连接,见 pxr/usd/usdUI/objectHints.h),即直接按displayGroupsShownIf:<group>字典键读写。测试验证:未设置的分组GetDisplayGroupShownIf返回空字符串''。
显示分组(Display Groups)与条件表达式的完整语义
PrimHints 并非孤立存在,它与显示分组机制、布尔表达式机制紧密耦合。以下语义出自 overview.md 的 "Display Groups and Property Order" 与 "Working With Conditional UI Hints" 两节,是理解 PrimHints 两字段的前提。
分组归属与嵌套
- 属性通过 PropertyHints 的
displayGroup声明自己归属的显示分组(PropertyHints.md),一个属性只能属于一个分组。 - 分组可嵌套:分组名中使用
:分隔符,例如"GroupA:NestedGroup"表示NestedGroup是GroupA的子分组。 - 分组是可选的,但能帮助工具把相关属性聚合展示、提升操作效率。
Python API 设置分组归属:
property = prim.GetProperty("myProperty") hints = UsdUI.PropertyHints(property) hints.SetDisplayGroup("Custom Properties")分组呈现由 Prim 控制
Prim 通过displayGroupsExpanded和displayGroupsShownIf控制分组在 UI 中的呈现,这正是 PrimHints 的核心职责。一个更丰富的综合示例(来自 overview.md):
def "TreeA" ( uiHints = { string displayName = "Tree template" dictionary displayGroupsExpanded = { bool "Trunk settings" = 1 bool "Body settings" = 1 bool "Body settings:Branch settings" = 0 bool "Body settings:Leaf settings" = 1 } dictionary displayGroupsShownIf = { string "Body settings:Leaf settings" = "trunkSize != 1" } } ) { color3f trunkColor = (0.6, 0.3, 0.0) ( uiHints = { string displayName = "color" string displayGroup = "Trunk settings" } ) int trunkSize = 2 ( uiHints = { string displayName = "size" string displayGroup = "Trunk settings" dictionary valueLabels = { int huge = 3 int sapling = 1 int standard = 2 } token[] valueLabelsOrder = ["sapling", "standard", "huge"] } ) float bodyRadius = 5.0 ( uiHints = { string displayName = "size" string displayGroup = "Body settings" } ) float branchDensity = 1.0 ( uiHints = { string displayName = "density" string displayGroup = "Body settings:Branch settings" } ) float branchLength = 5.0 ( uiHints = { string displayName = "length" string displayGroup = "Body settings:Branch settings" } ) color3f leafColor = (0.4, 0.7, 0.25) ( uiHints = { string displayName = "color" string displayGroup = "Body settings:Leaf settings" } ) float leafComplexity = 1.0 ( uiHints = { string displayName = "complexity" string displayGroup = "Body settings:Leaf settings" string shownIf = "leafStyle == 1" } ) int leafStyle = 1 ( uiHints = { string displayName = "style" string displayGroup = "Body settings:Leaf settings" dictionary valueLabels = { int acute = 1 int obtuse = 2 int truncate = 3 } token[] valueLabelsOrder = ["acute", "obtuse", "truncate"] } ) string tempNotes = "" ( uiHints = { bool hidden = 1 } ) reorder properties = ["trunkColor", "trunkSize", "bodyRadius", "branchDensity", "branchLength", "leafColor", "leafStyle", "leafComplexity"] }这个例子展示了 UI Hints 四层协同:ObjectHints(displayName、hidden)、PropertyHints(displayGroup、shownIf)、AttributeHints(valueLabels、valueLabelsOrder)与 PrimHints(displayGroupsExpanded、displayGroupsShownIf)。其中"Body settings:Leaf settings"分组同时受displayGroupsShownIf(表达式trunkSize != 1)和组内属性leafComplexity的shownIf(leafStyle == 1)双重条件控制。该示例的 UI mock 见 uihints-example-mock.svg。
属性顺序对分组的影响
Prim 通过reorder properties或Usd.Prim.SetPropertyOrder()控制属性在 UI 中的顺序。带显示分组时,工具应"按属性首次引用分组的位置排列分组,并按 Prim 属性顺序排列组内属性"。例如:
def "PropertyOrderPrimWithDisplayGroups" ( uiHints = { string displayName = "Example" dictionary displayGroupsExpanded = { bool "Group A" = 1 bool "Group B" = 1 } } ) { reorder properties = ["attribute4", "attribute2", "attribute1", "attribute3"] int attribute1 = 1 ( uiHints = { string displayGroup = "Group B" } ) int attribute2 = 2 int attribute3 = 3 ( uiHints = { string displayGroup = "Group B" } ) int attribute4 = 4 ( uiHints = { string displayGroup = "Group A" } ) }其 UI 呈现顺序的 mock 图见 uihints-propertyorder.svg。另外注意:旧的displayGroupOrderPrim 元数据字段已废弃,不应与属性顺序/分组相关 UI Hints 混用。
布尔表达式语法与求值语义
displayGroupsShownIf的值(以及 PropertyHints 的shownIf)是布尔表达式字符串,求值基于SdfBooleanExpression。表达式通常用于测试包含该 Prim 的某个属性的合成值(resolved value)。支持的操作符(见 overview.md 与 booleanExpression.h 中的运算符枚举文档):
| 操作符 | 含义 |
|---|---|
== | 等于 |
!= | 不等于 |
< | 小于 |
<= | 小于等于 |
> | 大于 |
>= | 大于等于 |
&& | 逻辑与 |
\|\| | 逻辑或 |
! | 一元逻辑非 |
表达式支持一元!与括号分组。例如!(status == "active" || level > 5)表示:仅当status不等于"active"且level小于等于 5 时才显示。
条件表达式示例(overview.md):
def "PrimUsingExpressions" ( uiHints = { dictionary displayGroupsShownIf = { string "Deformation parameters" = "materialHardness <= 2.0" } } ) { float bendAmount = 0.0 ( uiHints = { string displayGroup = "Deformation parameters" string displayName = "Bend amount" } ) float bendDirection = 0.0 ( uiHints = { string displayGroup = "Deformation parameters" string displayName = "Bend direction" } ) float fractureAmount = 0.0 ( uiHints = { string displayGroup = "Deformation parameters" string displayName = "Fracture amount" string shownIf = "isFractured == true" } ) float materialHardness = 10.0 bool isFractured = false }这里"Deformation parameters"分组的显示条件是materialHardness <= 2.0,而组内fractureAmount属性还要满足isFractured == true才会显示。
visibility 判定规则:对象级hidden提示始终与shownIf一起参与判定。即属性在 UI 中可见,当且仅当shownIf表达式求值为真且hidden不为真。这一规则同样适用于分组:displayGroupsShownIf表达式为假,或分组(Prim)的hidden为真,都会导致分组不可见。
用 API 读写 PrimHints(C++ / Python)
C++ API
构造:UsdUIPrimHints hints(prim);,读取/写入见上文字段对应 API。要点:
- 未authoring 时,
GetDisplayGroupsExpanded()返回空VtDictionary,GetDisplayGroupExpanded(group)返回false(默认折叠),GetDisplayGroupShownIf(group)返回空字符串。 - 写入字典时,
displayGroupsExpanded要求全部bool值、displayGroupsShownIf要求全部string值,否则报TF_CODING_ERROR。 - 字典字段的合成值按条目覆盖(per-entry override)合并,跨编辑目标时不是整体替换。
Python API
UI Hints 的 Python 绑定在 pxr/usd/usdUI 目录的wrap*文件中(如 wrapPrimHints.cpp)。典型用法:
from pxr import Usd, UsdUI stage = Usd.Stage.CreateInMemory() prim = stage.DefinePrim("/MyPrim") hints = UsdUI.PrimHints(prim) hints.SetDisplayGroupsExpanded({"Controller": True, "Widget Settings": False}) hints.SetDisplayGroupShownIf("Widget Settings", "widgetReadOnlyMode == 0") print(hints.GetDisplayGroupExpanded("Controller")) # True print(hints.GetDisplayGroupShownIf("Widget Settings")) # "widgetReadOnlyMode == 0"推荐使用 API 而非直接读写uiHints元数据:API 在未authoring 时提供合理的回退值。overview.md 中给出的例子:对一个uiHints为空的 Prim,UsdUI.ObjectHints(prim).GetDisplayName()返回空字符串,而直接prim.GetMetadata("uiHints").get("displayName")返回None。只有自定义 UI hint 键等场景才需要直接访问uiHints字典。
兼容性与迁移注意
overview.md 明确指出:displayName、hidden、displayGroup过去是独立的元数据字段(通过UsdObject/UsdProperty提供),现已废弃。UI Hints API 在uiHints字典中未authoring 对应值时,会回退查找这些旧字段以保持向后兼容;但新内容不应再author 这些独立字段,应统一使用 UI Hints API 或uiHints字典。这一回退行为在 objectHints.h 与 propertyHints.h 的注释中被标注为"临时的,将在未来版本移除"。
测试验证:从 testUsdUIHints.py 看行为契约
仓库测试 pxr/usd/usdUI/testenv/testUsdUIHints.py 直接覆盖了 PrimHints 的关键行为契约:
GetDisplayGroupsExpanded()返回与写入一致的字典;GetDisplayGroupExpanded(k)对每个键返回对应值;对不存在的分组'non-existent'返回False(默认折叠)。GetDisplayGroupsShownIf()/GetDisplayGroupShownIf(k)同理,不存在的分组返回空字符串''。- 向
SetDisplayGroupsShownIf传入非字符串值(如{'group': 5})会触发错误拒绝写入。 - 对
group:subgroup这类嵌套分组名,SetDisplayGroupExpanded('group', True)后GetDisplayGroupExpanded('group')与GetDisplayGroupExpanded('group:subgroup')均独立生效,且整字典按平铺结构读取——印证了上文"嵌套分组名平铺存储"的实现细节。 - 未authoring 的 Prim,其 PrimHints 相关读取均返回空字典/
False/空字符串,验证了默认回退行为。
测试同时覆盖了 ObjectHints(displayName、hidden)与 PropertyHints(displayGroup、shownIf)的回退与读写,可用于在改动 UI Hints 行为后做回归验证。
小结
PrimHints 通过displayGroupsExpanded与displayGroupsShownIf两个字典字段,把"显示分组如何呈现"的决定权交给数据本身:前者声明各分组默认展开/折叠状态,后者用SdfBooleanExpression表达式条件化控制分组可见性。它与 ObjectHints(displayName/hidden)、PropertyHints(displayGroup/shownIf)、AttributeHints(valueLabels/valueLabelsOrder)共同构成完整的 UI Hints 体系,全部承载在uiHints字典元数据中,由UsdUIPrimHints等类 schema API 解释读写。
需要强调的边界:UI Hints 本质是"建议"(suggestions),最终呈现方式由消费它的工具或应用程序决定(overview.md 明确说明)。因此,在 DCC 工具、资产浏览器或节点编辑器中消费这些提示时,应把 PrimHints 作为默认布局的输入,同时允许用户覆盖。本文所有字段语义、默认值(未设置分组默认折叠、未设置表达式默认显示)、类型校验与兼容回退行为,均可在 primHints.h、primHints.cpp 与 testUsdUIHints.py 中找到直接依据。
【免费下载链接】OpenUSDUniversal Scene Description项目地址: https://gitcode.com/GitHub_Trending/ope/OpenUSD
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考