Basics and Filtering
The host globals
When a script runs inside RPTS (RapidPipeline Twin Studio), a few names are already available without any import:
rpts- the RPTS scripting module, with helpers for selection, visibility, materials and more.rpd- the RapidPipeline data module, which provides the scene classes (Node,Mesh,Material, ...).get_asset()- returns the currently loaded asset, orNoneif nothing is loaded.
import rpts and import rpd are harmless if you prefer to be explicit, and rpts.get_asset() is the same call as get_asset(). The examples on this page use the explicit form.
Always check for None before using the asset, since a script can run while no model is loaded.
Getting the current asset
import rpts
asset = rpts.get_asset()
if asset is None:
print("No model currently loaded.")
else:
print(f"Asset has {asset.num_meshes} mesh(es) and {asset.num_materials} material(s).")
Keep in mind that an asset object is tied to the model that was loaded when you fetched it. Operations that replace the model (such as processing or undo) invalidate it, so fetch the asset again with rpts.get_asset() after such a change.
Walking the scene hierarchy
Every scene is a tree of Node objects. The top of the tree is asset.root_node, and each node can be used like a read-only list of its children:
node[i]returns the i-th direct child (aNode).len(node)returns the number of direct children.node.parentreturns the parent node, orNonefor the root.node.nameis the node's name, andnode.pathis its full hierarchical path, which is unique within the scene.
A small recursive helper collects a node and all of its descendants:
def walk(node):
# yields the node itself, then every node beneath it (depth-first)
yield node
for i in range(len(node)):
yield from walk(node[i])
To look up a node directly when you already know its path, use asset.node_at_path(path), which returns the node or None.
Nodes with and without a mesh
Not every node carries geometry. A node that holds a mesh has node.mesh_index set to the index of that mesh. node.mesh_index == -1 only means that the node has no mesh of its own; it does not tell you what kind of node it is:
for node in walk(asset.root_node):
if node.mesh_index != -1:
mesh = asset.mesh(node.mesh_index)
print(node.path, "has", mesh.num_triangles, "triangles")
If a node references a mesh, a relationship can be established between the node and the mesh’s material. This can be useful when assigning materials.
Filtering nodes
Instead of walking the hierarchy by hand and checking a condition yourself, you can describe what you're looking for with an rpd.NodeFilter and pass it to asset.query_nodes.
A NodeFilter always takes the same four keyword arguments:
target- arpd.NodeFilterTargetvalue saying what to match against:NODE_NAME,MATERIAL_NAME,MESH_NAME, orMETADATA_SUB_ATTRIB_TITLE.match_type- arpd.FilterMatchTypevalue saying how to match:REGEX,EXACT,CONTAINS,STARTS_WITH, orENDS_WITH.pattern- the string (or regular expression, whenmatch_typeisREGEX) to match with.case_sensitive- whether the comparison is case-sensitive.
asset.query_nodes(filters, query_mode) takes a list of NodeFilter objects and a rpd.QueryMode value, and returns the matching nodes (a sequence supporting len(), with .path/.name on each node):
rpd.QueryMode.MATCH_ANY- a node matches if any filter in the list matches (OR).rpd.QueryMode.MATCH_ALL- a node matches only if every filter in the list matches (AND).
Here is a single filter with MATCH_ANY, finding nodes whose name follows a prefix naming convention:
# this script is written for RPTS 0.9.8
import rpts
import rpd
asset = rpts.get_asset()
name_filter = rpd.NodeFilter(
target=rpd.NodeFilterTarget.NODE_NAME,
match_type=rpd.FilterMatchType.REGEX,
pattern=r"^(Proxy|Collision)_",
case_sensitive=False,
)
matches = asset.query_nodes(
[name_filter], rpd.QueryMode.MATCH_ANY
)
for node in matches:
print(node.path)
Passing more than one filter combines them according to query_mode. With MATCH_ALL, every filter must match the same node, which is how you express an AND condition - for example, nodes whose name contains "Handle" and that use a material whose name contains "Chrome":
# this script is written for RPTS 0.9.8
import rpts
import rpd
asset = rpts.get_asset()
filters = [
rpd.NodeFilter(
target=rpd.NodeFilterTarget.NODE_NAME,
match_type=rpd.FilterMatchType.CONTAINS,
pattern="Handle",
case_sensitive=False,
),
rpd.NodeFilter(
target=rpd.NodeFilterTarget.MATERIAL_NAME,
match_type=rpd.FilterMatchType.CONTAINS,
pattern="Chrome",
case_sensitive=False,
),
]
matches = asset.query_nodes(
filters, rpd.QueryMode.MATCH_ALL
)
for node in matches:
print(node.path)
Example: finding nodes by metadata attribute
The following script finds every node that carries a metadata sub-attribute with a given title, and prints their paths. It only reads the scene: nothing is selected, hidden, renamed or deleted, so it is safe to run on any model.
Replace "<metadata-key>" with the title of the metadata attribute you are interested in.
# this script is written for RPTS 0.9.8
# read-only: this script only reads the scene and never changes it
import rpts
import rpd
METADATA_KEY = "<metadata-key>"
def report_nodes_with_metadata(key):
asset = rpts.get_asset()
if asset is None:
print("No model currently loaded.")
return
metadata_filter = rpd.NodeFilter(
target=rpd.NodeFilterTarget.METADATA_SUB_ATTRIB_TITLE,
match_type=rpd.FilterMatchType.EXACT,
pattern=key,
case_sensitive=False,
)
matches = asset.query_nodes(
[metadata_filter], rpd.QueryMode.MATCH_ANY
)
for node in matches:
print(node.path)
print(f"Found {len(matches)} node(s). The scene was not changed.")
report_nodes_with_metadata(METADATA_KEY)
Metadata filters search metadata paths or attribute titles, not the stored attribute values. To filter by a metadata value, first query the relevant nodes and then inspect their metadata separately.
Next steps
Once you can find the nodes you care about, you can act on them. Selection and visibility shows how to select, hide and delete nodes. For the basics of materials, see Working with Materials.