用于 VR 的 Python API v2(自 2020.1 起)

用于 VR 的 Python API 遵循的概念是将设备操作(例如,按下按钮)连接到通过 Python 脚本实现的实际功能。API 不受平台限制,这意味着对于不同的 VR 控制器仅需编写一次脚本。唯一的例外是输入设备上的按钮。一个设备提供的按钮可能会与另一个设备不同。因此,在 Python 脚本中使用相应界面时应牢记这一点。

在 VRED 中,VR 中的功能(例如,传送)被称为设备交互,类型为 vrdDeviceInteraction。这些交互包含多个 vrdDeviceAction 类型的设备操作,可反映按下按钮等操作。以传送为例,有一个设备操作用于激活目标弧,另一个用于执行传送,再有一个用于取消激活目标弧。

图 1 - 活动交互组包含多个设备交互,设备交互包含多个设备操作

在 VRED 中,还存在交互组概念。每个设备交互是交互组的一部分,且一次仅有一个组处于活动状态。当此类组处于活动状态时,其包含的所有交互也处于活动状态,反之亦然。划分不同的组便于快速更改用户交互的上下文,例如当在 VR 中选择不同的工具时。请注意,每个设备操作(例如,按下左侧控制器的按钮 A)仅可在交互组中发生一次。这可以防止同时触发多个函数,以免不同交互之间出现一些负面影响。

要更好地了解交互组、设备交互和设备操作之间的关系,请参见图 1。

API 还可用于访问表示输入设备的 vrdVRDevice 类型对象。这些对象提供了一些附加功能来读取和管理每个设备的状态。

使用界面

VR Python 界面的起点是类 vrDeviceService。此服务允许创建、访问和管理设备交互和 VR 输入设备。

创建新设备交互

vrDeviceService 提供了方法 createInteraction,可将新交互的名称视作参数。默认情况下,所有可用交互组均支持此交互。例如,对于交互组“Locomotion”,它包含一些 VRED 的默认交互(例如,传送和指针)。如果要在此组中添加新交互,请务必知悉某些按钮和相应操作已被默认交互使用且不再可用。以下代码行展示了如何创建交互。

myInteraction = vrDeviceService.createInteraction("MyInteraction")

该交互使用服务创建,名称为“MyInteraction”且在当前可用的所有交互组中均受支持。

vrdDeviceInteraction 对象允许使用方法 createControllerAction 创建设备操作。此方法将字符串视作参数,用于描述实际操作。此字符串的格式为 <Side>-<Element>-<Event>(例如,“left-trigger-pressed”)。有关参数的更多信息,请参见 VRED 随附的 Python 文档(“帮助”>“Python 文档”)。此方法返回的 vrdDeviceAction 提供了名为“triggered”的信号,可用于将操作连接到 Python 脚本中的函数,如以下示例所示。

def myFunction():
    print("My function is called")

myInteraction = vrDeviceService.createInteraction("MyInteraction")
myAction = myInteraction.createControllerAction("any-xa-pressed")
myAction.signal().triggered.connect(myFunction)

首先,实现函数并显示已调用此函数。设备操作由设备交互对象创建而成,如果在任何控制器上按下 X/A 按钮,将发送一个信号。最后一行将信号“triggered”(它属于设备操作的信号对象)连接到应调用的实际函数。

如果设备操作已在使用,则需要将交互添加到其他交互组并激活此组。

def myFunction(action, device):
    print("My function is called")

myInteraction = vrDeviceService.createInteraction("MyInteraction")
myInteraction.setSupportedInteractionGroups(["MyGroup"])
vrDeviceService.setActiveInteractionGroup("MyGroup")
myAction = myInteraction.createControllerAction("any-touchpad-pressed")
myAction.signal().triggered.connect(myFunction)

此示例还使用方法 setSupportedInteractionGroups 设置了交互的受支持交互组。然后,使用服务的方法 setActiveInteractionGroup 激活组。这样,可以再次指定已在组“Locomotion”中使用的触控板。请注意,在活动组中多次使用设备操作会导致未定义的行为。VRED 的默认交互组“Locomotion”中已使用以下操作:

单元 事件
任意 触控板 已按下
任意 触控板 未按下
任意 触控板 已触摸
任意 触控板 未触摸
任意 自定义触发器 已按下
任意 自定义触发器 未按下
任意 自定义触发器 已触摸
任意 自定义触发器 未触摸
任意 菜单 已按下
任意 菜单 已释放
任意 拇指 已按下
任意 拇指 未按下
任意 拇指 已触摸
任意 拇指 未触摸
任意 YB 已按下
任意 YB 已释放

vrdDeviceInteraction 对象可提供更多功能来管理设备操作及其所属的交互组。有关详细信息,请参见 VRED 的 Python 文档(“帮助”>“Python 文档”)。VRED 示例还包含脚本 vr/customInteraction.py,展示了如何实现新的自定义设备交互。

建议将需要使用一些默认操作的复杂交互放入自定义交互组,根据需要激活此组。可以使用菜单条目等执行此操作。有关详细信息,请参见创建 xR 主页菜单条目。此外,还可以使用虚拟按钮在控制器的触控板上重新排列默认操作。有关详细信息,请参见在触控板上设置虚拟按钮

连接到默认设备交互

默认交互(例如,传送和指针)提供了一些基本功能,用于导航场景中的对象或与这些对象进行交互。通过将函数连接到默认交互的操作,可以扩展此功能。要实现此操作,首先应从 vrDeviceService 获取默认交互。

def printNodeName(action, device):
    node = device.pick().getNode()
    print(node.getName())

pointer = vrDeviceService.getInteraction("Pointer")
pointerExecute = pointer.getControllerAction("execute")
pointerExecute.signal().triggered.connect(printNodeName)

示例使用指针的“执行”操作,当用户通过定位光线“单击”场景中的对象时将触发此操作。指针还具有“准备”和“中止”操作,仅可激活或取消激活目标光线。可以使用 vrDeviceService.getInteractions 查询可用的不同交互。此外,每个交互还提供方法 getControllerActions 来获取与 VR 输入设备相关的所有可用操作。脚本 vr/connectToDeviceActionSignal.py 包含另一个简单示例,展示了如何连接到默认交互。

使用控制器和跟踪器

VR 的输入设备由类 vrdVRDevice 表示。此类可用于访问按钮、位置、触摸反馈和设备可视化。有关 vrdVRDevice 的所有方法的详细说明,请参见 VRED 随附的 Python 文档(“帮助”>“Python 文档”)。

获取 VR 设备

可采用多种方法获取设备对象。第一个是 vrDeviceService 方法 getVRDevice

leftController = vrDeviceService.getVRDevice("left-controller")

这将返回表示左侧控制器的对象。字符串参数是设备的名称。左侧控制器的名称始终为“left-controller”,右侧控制器的名称始终为“right-controller”。这与硬件无关,适用于 VRED 支持的所有控制器。请注意,一旦连接相应的控制器,将立即应用所做更改。当控制器的左右手习惯发生变化时,VRED 会将“原来”左侧控制器的所有设置应用于“新的”左侧控制器。

getVRDevice 方法也适用于 HTC Vive 跟踪器。

tracker = vrDeviceService.getVRDevice("tracker-1")

跟踪器的名称始终为“tracker-”后加一个编号。请注意,此编号将根据跟踪器的连接顺序而发生变化。这可能会在某些应用程序中引入问题,因此,也可以使用方法 vrDeviceService.getVRDeviceBySerialNumber 通过相关序列号识别设备。对于某些设备,设备上保存的序列号会与设备上输出的序列号不同。在这种情况下,API 支持通过 vrdVRDevice 方法 getSerialNumber 读取序列号,如果已连接设备,这将仅返回有效值。示例脚本 vr/printAllDeviceSerialNumbers.py 将输出所有序列号。获取特定序列号的一种简便方法是,运行示例脚本时一次仅连接一个设备。

方法 getVRDevicegetVRDeviceBySerialNumber 始终返回对象,即使没有连接任何设备。此对象的设置在连接设备之前便已确定,一旦连接相应的设备将立即应用这些设置。

请注意,应将 getVRDevicegetVRDeviceBySerialNumber 用于相同的设备类型,因为当进行连接或重新连接时某些设备的名称可能会改变(例如,跟踪器的编号或 HTC Vive 控制器的左右侧)。否则,两个不同的对象可能表示相同的物理设备,这可能会导致不必要的行为。

如果需要所有设备,vrDeviceService 提供了方法 getConnectedVRDevices,它将返回当前连接的所有设备。

将几何体附加到 VR 设备

vrdVRDevice 类提供了方法 getNode,它将返回用于表示设备变换的节点。此节点不应该用于直接添加子节点,因为不会在新场景中清除此节点,但可以用于添加父约束,如以下示例所示。

deviceNode = myDevice.getNode()
constraint = vrConstraintService,createParentConstraint([deviceNode], myNode, False)

在触控板上设置虚拟按钮

某些控制器(例如,HTC Vive 控制器)的触控板可以拆分为不同区域以模拟更多按钮。类 vrdVRDevice 提供了方法 addVirtualButton,它涉及两个参数:一个类型为 vrdVirtualTouchpadButton 的对象和一个它所属的相应非虚拟按钮的字符串。

padTop = vrdVirtualTouchpadButton("padtop", 0.0, 1.0, 270.0, 90.0)
padBottom = vrdVirtualTouchpadButton("padbottom", 0.0, 1.0, 90.0, 270.0)

在此示例中,首先创建两个 vrdVirtualTouchpadButton 对象。构造函数的第一个参数为新按钮赋予名称,第二个和第三个参数描述按钮所处的半径,此示例中使用触控板的完整半径。第四个和第五个参数描述触控板上按钮所处的角度。在这两种情况中,第一个按钮将占据触控板的上半部分,第二个按钮将占据触控板的下半部分。图 2 展示了两个按钮的布局。请注意,如果存在重叠的按钮,则仅其中一个按钮将发送信号。不建议创建重叠的按钮。

图 2:圆形触控板上虚拟按钮的布局。一个按钮位于顶部(蓝色),另一个按钮位于底部(橙色)。

实例化虚拟按钮后,可以将这些按钮添加到控制器中。

controller.addVirtualButton(self.padTop, "Touchpad")
controller.addVirtualButton(self.padBottom, "Touchpad")

方法 vrdVRDevice.addVirtualButton 涉及两个参数,第一个参数是 vrdVirtualTouchpadButton 对象,第二个参数是虚拟按钮所处实际按钮的名称。在 VRED 中,当前这仅适用于 HTC Vive 控制器的圆形触控板。

创建设备操作时,新添加的虚拟按钮现在可像常规按钮一样使用。

myAction = myInteraction.createControllerAction("left-padtop-pressed")

有关如何使用虚拟按钮的更多示例,请参见脚本 vr/virtualControllerButtons.pyvr/groupTeleport

VR 协作会话

使用 VRED Python API 来管理协作会话。这不仅需要调整常规设置,还需要处理用户和在参与者之间发送 Python 命令。

常规会话管理

vrSessionService 可用于访问在 VRED 的“协作”用户界面中也可用的函数。其中包括加入或退出会话、检查连接、上传场景等。

除了设置,此服务还可用于实现一些逻辑以在协作会话中进行交互。为此,它提供了 addSyncNodesyncNode 等方法,用于在所有用户之间同步节点的变换和可见性。当多个用户同时操纵场景中的对象时,这可能会有所帮助。

会话用户

vrSessionService 还可用于访问参与会话的其他用户。为此,可使用方法 getUsergetUsersgetRemoteUsers。它们将分别返回类型为 vrdSessionUser 的一个对象和对象列表。此类可用于访问基本用户属性(例如,ID、名称、条件和要使用的 HMD)。也可用于访问构成用户头像的节点,其中包括各个部分的跟踪矩阵。

将 Python 命令发送给其他用户

会话服务提供了相关方法以将 Python 代码发送给会话中的任何用户,随后将在接收方本地执行此代码。

可直接使用 vrSessionService 提供的方法 sendPython 将 Python 代码快速发送给每个用户。如果使用此方法,会话中的每个用户会收到相同的 Python 代码并执行此代码。当需要发送多个常规代码(无用户特定部分)时,这尤其有用。

vrSessionService,sendPython("print('Hello')")

请注意,如果在这些字符串中使用变量,接收方也需要存在这些变量,因为代码执行方式如同接收用户将代码键入其本地 VRED 控制台。

如果需要发送用户特定代码(例如,包含用户名的欢迎消息),也可以仅将命令发送给选定用户。

users = vrSessionService.getRemoteUsers()
for user in users:
    name = user.getName()
    user.sendPython("print('Hello {0}')").format`(name)`)

第一步是使用 vrSessionService.getRemoteUsers 获取当前连接到场景的所有用户。这将返回 vrdSessionUser 对象列表,每个对象表示一个用户。此示例将遍历用户,并针对每个用户对象调用方法 getName 以获取名称。然后,将调用方法 sendPython 以发送一些 Python 代码。此方法将 Python 代码视作字符串。

发送 Python 代码时可能并非所有用户均存在,因此该服务还提供了信号,当用户加入或退出会话时将发送此信号。可以将函数连接到这些信号。

def userArrived(user):
    print(user.getUserName() + " has arrived")

def userLeaves(user):
    print(user.getUserName() + " has left")
vrSessionService.userArrives.connect(userArrived)
vrSessionService.userLeaves.connect(userLeaves)

在此示例中,实现了两个函数,它们将 vrdSessionUser 类型的对象视作参数,然后连接到相应信号。

有关 vrSessionServicevrdSessionUser 的所有方法的详细说明,请参见 VRED 的 Python 文档(“帮助”>“Python 文档”)。

创建 xR 主页菜单条目

xR 主页菜单可以通过自定义条目进行扩展。为此,可使用类 vrImmersiveUIService 创建新工具。以下示例展示了如何将简单的下压按钮添加到菜单以及如何将函数连接到其单击信号。

def hasClicked():
    print("click")

tool = vrImmersiveUiService.createTool("MyTool")

tool.setText("My Tool")

icon = QtGui.QIcon()
icon.addFile("myToolIcon.png")
tool.setIcon(icon)

tool.signal().clicked.connect(hasClicked)

vrdImmersiveTool 使用沉浸式 UI 服务创建。此工具需要一些配置,即设置将在 xR 主页菜单中显示的文本和图标。此工具随附了不同的信号,但在此情况下,仅需要单击信号并将此信号连接到函数,以表明其运行正常。

菜单中的这些附加工具并不仅限于简单下压按钮。还可以添加切换按钮、包含 Web 内容的子菜单或包含整个 Qt 控件的子菜单。示例脚本 vr/customMenuButton.py 中展示了更多高级功能。

VRED 随附的示例

VR 示例更详细地展示了如何使用 Python API。也可在 VRED 的 Python API v2 文档中找到这些示例。