Create a Blender Add-On using the REST API#
Note
This tutorial is targeted at more technical users.
To be able to follow this tutorial, you should have a basic understanding of the Python programming language, RESTful APIs and Blender.
This tutorial will guide you through building a Blender add-on that communicates with an external application using REST APIs.
A Blender add-on consists of code that adds new functionality to the Blender software. It can be used to extend the capabilities of Blender or to integrate it with other software.
We will specifically focus on the RTX Remix REST API, but the process should be the same for any other REST API.
Requirements#
Before you start building a Blender Add-On, you will need to make sure the following requirements are installed:
RTX Remix Toolkit ( version 2024.4.0 or greater)
Optional:
A code editor of your choice may work better than Blender’s built-in Scripting window:
Version-controlling your code using a tool like Git is a good idea.
Completed Tutorial Code#
If you need to access the code produced in this tutorial, you can download it by clicking on the following link:
Understanding the Architecture#
At a very high level, the architecture of an RTX Remix Blender Add-On will be as follows:
The Add-On will be built using the Blender Python API
Blender Add-Ons are created using Python scripts.
The Add-On’s script defines the behavior of the Add-On, including the creation of UI, communication with the REST API, and more.
Panels are the UI components that are displayed in the Blender interface.
Operators are the actions that are triggered when a user interacts with a panel.
The Add-On will communicate with the RTX Remix Toolkit using the REST API
A REST API is a way for different programs to communicate with each-other without requiring explicit connections between them. Think of it like a set of rules for how to request and send information.
The RTX Remix Toolkit REST API allows any external application to get the selected assets or textures, start an ingestion process, replace a reference, and more.
All of these actions are defined in the REST API as
endpoints
.
Using the RTX API, the Add-On will be able to execute any of the functions, and get information from any of the endpoints defined in the REST API
NOTE: The Add-On will require the RTX Remix Toolkit to be open for the Add-On to be able to send HTTP requests to the REST API server that is created when the Toolkit starts up.
Getting Started#
Setting up your Code Editor#
Open your preferred Code Editor
Create a new Project/Directory for your Add-On
Create a new Python script called
rtx_remix_addon.py
[OPTIONAL] Opening/Closing the Blender Console Window#
Since we will be building a Blender Add-On, opening the Blender console could give us a better idea of what is going on during our script’s execution.
Select the
Window
menuClick the
Toggle System Console
entry
That will open the console in a separate window, making it easier to see what is going on.
To close the console, simply repeat the same steps.
Blender Add-On File Structure#
In order to keep the code organized and make sure the Add-On can be extended easily, we will separate the code into multiple modules.
To read more about Python modules packages, see this article.
Even though we only plan on building a single panel with multiple operators, we will use the following file structure to ensure the Add-On is well organized and extendable:
rtx_remix_addon/
blender_manifest.toml
__init__.py
operators/
__init__.py
operator1.py
operator2.py
...
panels/
__init__.py
panel1.py
panel2.py
...
properties/
__init__.py
property1.py
property2.py
...
utils/
__init__.py
utils1.py
utils2.py
...
Given the Add-On we want to start building, and the file structure described above, we should have the following file structure:
rtx_remix_addon/
blender_manifest.toml
__init__.py
operators/
__init__.py
import_assets_operator.py
panels/
__init__.py
viewport_panel.py
If not done already, you should create this file structure in your Code Editor.
Building a Blender Add-On#
The best starting point to build a Blender Add-On is to start with Blender’s own tutorial.
This tutorial will be focused on an Add-On that works with the RTX Remix Toolkit.
rtx_remix_addon/blender_manifest.py
#
Since Blender 4.2, Add-Ons need to define a blender_manifest.toml
file in the same directory as the Add-On in order
to be recognized by the Add-On manager.
id = "rtx_remix_addon"
name = "RTX Remix Toolkit Add-On"
maintainer = "YOUR NAME <you@email.com>"
type = "Add-On"
version = "1.0.0"
schema_version = "1.0.0"
blender_version_min = "4.2.0"
tagline = "An Add-On allowing Blender to perform actions within the RTX Remix Toolkit"
license = ["SPDX:GPL-3.0-or-later"]
rtx_remix_addon/__init__.py
#
The top-level __init__.py
file will import the submodules and ensure they are registered when the Add-On is
enabled.
Don’t worry if some methods are undefined at this moment, we will be defining them in the following steps.
from .operators import register as register_operators
from .operators import unregister as unregister_operators
from .panels import register as register_panels
from .panels import unregister as unregister_panels
def register():
register_operators()
register_panels()
def unregister():
unregister_panels()
unregister_operators()
if __name__ == "__main__":
register()
rtx_remix_addon/operators/__init__.py
#
__init__.py
files within the sub-folders will import the various panels, operators, or other classes defined within
their package. This will make sure that the module is available to the top-level __init__.py
file.
Furthermore, the register
and unregister
functions should register the various classes defined in the package in
Blender using the Blender Python API module.
Once again, don’t worry if some classes are undefined at this moment, we will be defining them in the following steps.
import bpy
from .import_assets_operator import REMIX_OT_ImportAssets
def register():
bpy.utils.register_class(REMIX_OT_ImportAssets)
def unregister():
bpy.utils.unregister_class(REMIX_OT_ImportAssets)
if __name__ == "__main__":
register()
rtx_remix_addon/operators/import_assets_operator.py
#
This class defines a basic operator which is a function that can be called from Blender’s interface. The execute
method defines what happens when the operator is called. In this case, it just reports a message and signals to Blender
that its job is done.
import bpy
class REMIX_OT_ImportAssets(bpy.types.Operator):
bl_idname = "rtx_remix.import_selected_assets"
bl_label = "Import Selected Assets"
bl_description = "Import the assets selected in the RTX Remix Toolkit"
bl_options = {"REGISTER", "UNDO"} # Enable undo for the operator.
def execute(self, context):
self.report({"INFO"}, "Operator executed")
return {"FINISHED"}
rtx_remix_addon/panels/__init__.py
#
The __init__.py
files within this package will have the same purpose as the __init__.py
files within the operators
package.
import bpy
from .viewport_panel import REMIX_PT_ViewportPanel
def register():
bpy.utils.register_class(REMIX_PT_ViewportPanel)
def unregister():
bpy.utils.unregister_class(REMIX_PT_ViewportPanel)
if __name__ == "__main__":
register()
rtx_remix_addon/panels/viewport_panel.py
#
This class defines a panel in Blender’s interface. The draw
method defines what appears in the panel. Here, it adds a
button to call our Import Assets operator.
import bpy
from ..operators import REMIX_OT_ImportAssets
class REMIX_PT_ViewportPanel(bpy.types.Panel):
bl_label = "RTX Remix"
bl_idname = "REMIX_PT_ViewportPanel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "RTX Remix"
def draw(self, context):
self.layout.operator(REMIX_OT_ImportAssets.bl_idname)
We have a basic Add-On!#
At this point, you could skip ahead to the Installing the Blender Add-On section and start testing your Add-On in Blender.
However, the Add-On is currently quite limited in its capabilities, so the next section of this tutorial will focus on expanding its functionality by utilizing the RTX Remix Toolkit REST API.
[OPTIONAL] Git Repository Structure#
If you are using Git, the rtx_remix_addon
folder should be a subdirectory in your Git repository like such:
Blender-RTX-Remix/
.gitignore
LICENSE.md
README.md
rtx_remix_addon/
blender_manifest.toml
__init__.py
operators/
__init__.py
import_assets_operator.py
panels/
__init__.py
viewport_panel.py
Adding Remix Functionality using the REST API#
Now that we have a well organized Add-On with the necessary files, let’s add some actual functionality using the RTX Remix Toolkit REST API.
As described in the Understanding the Architecture section, we now need the Add-On to send HTTP requests to the REST API
To begin, we will start by writing a simple wrapper around the requests
module to send the HTTP requests to the REST
API.
Prim Utils#
Although used later in this tutorial, we will now create a utility class to work with prims, or more specifically, prim paths in this instance.
Create a rtx_remix_addon/utils/prims.py
file and start by importing the required modules.
Import the required modules#
import re
The re
module will be used to execute regular expressions on prim paths
received from the REST API.
Create the utility class#
class PrimsUtils:
Since the PrimsUtils
doesn’t need any attributes, we will leave it empty.
Create a utility method to get a root mesh path from a referenced mesh#
@staticmethod
def get_mesh_path(prim_path: str) -> str | None:
"""
Get the root mesh path from a refrence prim path using regular expressions.
Examples:
/RootNode/meshes/mesh_HASH/mesh -> /RootNode/meshes/mesh_HASH
Args:
prim_path: The prim path to get the mesh path from
Returns:
The mesh path if the prim path matches the pattern, otherwise None
"""
mesh_pattern = r"^(.*mesh_[A-Z0-9]{16}(?:_[0-9])*)\/.*$"
pattern_match = re.match(mesh_pattern, prim_path)
if pattern_match:
return pattern_match.group(1)
return None
The get_mesh_path
method defined above will take a prim path as an argument. It will return the base mesh path if the
prim path matches a specific pattern. Otherwise, it will return None
. For more details, refer to the example in the
docstring.
That’s the only prim utility method we need for now.
REST API Utils#
We will now write a utility class to wrap HTTP requests into easy-to-use Python methods.
To create the various utility methods, the RTX Remix REST API documentation can be used to get information on the possible query parameters, body contents, return values, etc.
Create a rtx_remix_addon/utils/api.py
file and start by importing the required modules.
Import the required modules#
import requests
import json
from http import HTTPMethod
from pathlib import Path
The requests
module will be used to send HTTP requests to the REST API. The json
module will be used to convert
data to JSON format. The http
module will be used to define the HTTP method to use for the request. Lastly, the
pathlib
module will be used to manipulate file paths.
Create the utility class#
class APIUtils:
def __init__(self, base_url: str = "http://127.0.0.1:8011"):
self.base_url = base_url
The RTX Remix REST API is available at http://127.0.0.1:8011
by default but can be changed. The base_url
variable will therefore be used to define the base URL of the REST API and make sure the code doesn’t break if the URL
changes.
If the URL changing is a problem, it could even be exposed as a parameter in the Blender Add-On UI for the user to set.
Create a method to send the request#
def _send_request(
self,
method: HTTPMethod,
endpoint: str,
data: dict = None,
params: dict = None,
version: str = "1.0"
) -> dict:
"""
Sends a request to the RTX Remix REST API and returns the response as a dictionary.
Args:
method: The HTTP method to use for the request (e.g. GET, POST, PUT, DELETE).
endpoint: The endpoint to send the request to.
data: The data to send in the request body.
params: The query parameters to send with the request.
version: The version of the API to use.
"""
url = f"{self.base_url}{endpoint}"
headers = {
"Content-Type": "application/json",
"Accept": f"application/lightspeed.remix.service+json; version={version}",
}
response = requests.request(
method, url, headers=headers, data=json.dumps(data), params=params
)
response.raise_for_status()
return response.json()
Here we defined a protected method that will send the HTTP request to the REST API and return the response as a dictionary.
If an error occurs (the response code is not 2XX), it will also raise an exception.
Create a utility method to encode strings to URLs#
def _encode_string(self, string: str) -> str:
"""
Encode a string to be used in a URL.
"""
return requests.utils.quote(string, safe='')
Since we might need to build URLs dynamically in the future, we will create a utility method to encode strings in a way that is safe for use in URLs. This will make it easier to generate URLs and keep the URL-building logic in one place.
Create a utility method to get the status of the RTX Remix API#
def get_status(self) -> bool:
"""
Get the status of the RTX Remix API.
"""
try:
self._send_request(HTTPMethod.GET, "/status")
return True
except Exception:
return False
This simple method will send a GET request to the /status
endpoint of the RTX Remix API and return True if the
request is successful and False if it is not.
We need to catch any exceptions that might occur during the request, since if the API is not available, we will most likely encounter a connection error, whereas if the API is in a non-functional state but available, we may simply get a non-2XX response.
Create a utility method to get the opened project#
def get_opened_project(self) -> str | None:
"""
Get the currently opened project. Will return None if no project is opened.
"""
try:
response = self._send_request(HTTPMethod.GET, "/stagecraft/project")
return response.get("layer_id")
except requests.exceptions.HTTPError as e:
# The REST API is expected to return a 404 if no project is opened
if e.response.status_code == 404:
return None
else:
raise
This method will send a GET request to the /stagecraft/project
endpoint of the RTX Remix API and return the project
data if the request is successful and None if it is not.
The endpoint in question is expected to return a 404 response if no project is opened, so we need to handle that case.
Lastly, for any other error, we will re-raise the exception since we don’t want to handle it here.
Create a utility method to get assets from the currently opened project#
def get_assets(self, **kwargs) -> list[str]:
"""
Get the assets in the currently opened project.
Args:
**kwargs: Additional query parameters to send with the request.
Possible Query Parameters:
- asset_hashes: list[str] - A list of specific asset hashes to get
- asset_types: list[str] - A list of specific asset types to get
- selection: bool - Whether to return only selected assets or all assets
- filter_session_assets: bool - Whether to filter out assets defined on the session layer or not
- layer_identifier: str - Look for assets that exists or not on a given layer
- exists: bool - Whether to look for assets that exists or not on `layer_identifier`
"""
params = {k: v for k, v in kwargs.items() if v is not None}
response = self._send_request(HTTPMethod.GET, "/stagecraft/assets", params=params)
return response.get("asset_paths")
This method will send a GET request to the /stagecraft/assets
endpoint of the RTX Remix API and return the assets
data (a list of Prim Paths).
This method also allows you to add extra query parameters to the request. These parameters are passed in the kwargs
argument. We don’t define all the individual query parameters in this method because we only want to send the parameters
that you set when calling the method.
The downside of this approach is that invalid query parameters could be passed to the method. Luckily for us, invalid query parameters will be silently ignored.
Create a utility method to get referenced file paths for a given asset#
def get_file_paths(self, asset_path: str) -> list[str]:
"""
Get the referenced absolute file paths for the given asset path.
Args:
asset_path: The asset path to get the referenced file paths for
"""
encoded_path = self._encode_string(asset_path)
response = self._send_request(HTTPMethod.GET, f"/stagecraft/assets/{encoded_path}/file-paths")
reference_paths = response.get("reference_paths")
# Build absolute paths from the reference paths
file_paths = []
for _, path in reference_paths:
asset_path, layer_id = path
file_paths.append(str(Path(layer_id).parent / asset_path))
return file_paths
This method will do a bit more than the previous utility method. It will send a GET request to the
/stagecraft/assets/{asset_path}/file-paths
endpoint of the RTX Remix API, where {asset_path}
is one asset_path
received from the get_assets
method.
The method will then process the response received from the REST API and return a list of absolute file paths for the asset references.
Importing the Selected Assets#
Now that we have a utility method to get information from the RTX Remix REST API, let’s write a proper execute
method
for the REMIX_OT_ImportSelectedAssets
operator defined in the rtx_remix_addon/operators/import_assets_operator.py
file.
This will be a multi-step process. We will need to:
Make sure the REST API is available
Make sure a project is opened
Get the assets selected in the viewport
Get the referenced file paths for the selected assets
Import the references in the Blender scene
Import the required modules#
import re
from ..utils.api import APIUtils
The APIUtils
class will be used to send HTTP requests to the REST API in an easy-to-use way. The re
module will
be used to extract the base mesh Prim Path using regular expressions
later on.
Initialize the utility class#
def __init__(self):
self._api = APIUtils()
def __del__(self):
self._api = None
When the Operator class is initialized (__init__
), we will create an instance of the APIUtils
class and store it
in the _api
attribute. This will allow us to use the same instance throughout the class.
We will also delete the _api
attribute when the Operator class is deleted (__del__
).
Implement the execute
method#
def execute(self, context):
ASSETS_SCALE = 0.1
CREATE_COLLECTION = True
# Make sure the API is running
if not self._api.get_status():
self.report({"ERROR"}, "The REST API is not running")
return {"CANCELLED"}
# Make sure a project is opened
if not self._api.get_opened_project():
self.report({"ERROR"}, "No project is opened")
return {"CANCELLED"}
try:
# Get the assets selected in the RTX Remix viewport
assets = self._api.get_assets(selection=True, asset_types="models")
# Get the mesh paths for the selected assets
meshes = [PrimsUtils.get_mesh_path(asset) for asset in assets if PrimsUtils.get_mesh_path(asset)]
# Get the referenced file paths for the selected assets
file_paths = set()
for mesh in meshes:
file_paths = file_paths.union(self._api.get_file_paths(mesh))
# Import the selected assets
for file_path in file_paths:
# Assets coming from Remix are always in USD format
bpy.ops.wm.usd_import(
filepath=file_path,
create_collection=CREATE_COLLECTION,
scale=ASSETS_SCALE,
import_visible_only=False,
relative_path=False,
)
except Exception as e:
self.report({"ERROR"}, str(e))
return {"CANCELLED"}
return {"FINISHED"}
NOTE: You will need to import the PrimsUtils in order for the contents of the method to be executed correctly:
from ..utils.prims import PrimsUtils
Going back to the steps previously described:
Make sure the REST API is available
if not self._api.get_status(): self.report({"ERROR"}, "The REST API is not running") return {"CANCELLED"}
Here we ensure that the REST API is running. If not, we will display an error message and cancel the operation.
Make sure a project is opened
if not self._api.get_opened_project(): self.report({"ERROR"}, "No project is opened") return {"CANCELLED"}
Here we ensure that a project is opened. If not, we will display an error message and cancel the operation.
Get the assets selected in the viewport
assets = self._api.get_assets(selection=True, asset_types="models") # /RootNode/meshes/mesh_HASH/mesh -> /RootNode/meshes/mesh_HASH mesh_pattern = r"^(.*mesh_[A-Z0-9]{16}(?:_[0-9])*)\/.*$" meshes = [] for asset in assets: pattern_match = re.match(mesh_pattern, asset) if pattern_match: meshes.append(pattern_match.group(1))
Here we get the model assets selected in the viewport and store them in the
assets
variable. Note that we are using theselection=True
andasset_types="models"
kwargs to specify that we only want to get selected model assets.We then use the
re
module to extract the base mesh Prim Path from the asset path. For example, if the asset path is/RootNode/meshes/mesh_HASH/mesh
, the base mesh Prim Path will be/RootNode/meshes/mesh_HASH
. Note that we only add the mesh when a match is found. Meshes should always match, but it in the case they don’t, we want to avoid raising an error.To learn more about regular expressions and the
re
module, refer to this tutorial.Get the referenced file paths for the selected assets
file_paths = set() for mesh in meshes: file_paths = file_paths.union(self._api.get_file_paths(mesh))
Here we get the referenced file paths for the selected assets and store them in the
file_paths
variable. Note that we are using theset
data structure to remove duplicate file paths.Import the references in the Blender scene
for file_path in file_paths: # Assets coming from Remix are always in USD format bpy.ops.wm.usd_import( filepath=file_path, create_collection=CREATE_COLLECTION, scale=ASSETS_SCALE, import_visible_only=False, relative_path=False, )
Here we import the referenced mesh files in the Blender scene. We set
import_visible_only
toFalse
to make sure that all meshes, not just the visible ones, are imported.
Installing the Blender Add-On#
Zip up the Add-On’s content (
rtx_remix_addon
in this case)In Blender, select the
Edit
menuClick the
Preferences
entryIn the preferences window, click the
Add-ons
tabClick the
⌄
button at the top right of the panelClick the
Install from Disk
entrySelect the zip you created in step 1
The Add-On is now enabled and should be visible in Blender’s Viewport Sidebar!
Updating the Blender Add-On#
If you have made changes to the Add-On, update it in Blender by simply following the same installation steps again.
Installing the Add-On on top of the existing Add-On will simply update it in Blender.
Conclusion#
Congratulations! You have successfully created a Blender Add-On that sends HTTP requests to the RTX Remix REST API. You can modify this code to suit your specific needs, such as changing the URLs or adding additional functionality.
Bonus Features#
Now that you have a functional Blender Add-On that can import assets from the RTX Remix REST API, you can add additional functionality to it.
The following sections won’t be as detailed as the rest of the tutorial, but they will help you to add more functionality to the Add-On.
Adding parameters to the Blender Add-On#
Notice how in the Implement the execute
method section there are two variables
at the top of the method:
ASSETS_SCALE = 0.1
CREATE_COLLECTION = True
Exposing those to the user is a great way to add more functionality to the Add-On.
To do so, we will need to do 2 things:
Define a property variable that can be set by the user
Display the properties in the UI
Defining a Blender Property#
Referring back to the Blender Add-On File Structure section, we will now add a property class to the Add-On.
Create a rtx_remix_addon/properties/import_assets_properties.py
file and start by importing the required modules.
import bpy
We can then create some typed class variables to define our properties.
assets_scale: bpy.props.FloatProperty(
name="Assets Scale",
description="Scale factor for imported assets",
default=1.0,
min=0.0001,
max=10000.0
)
create_collection: bpy.props.BoolProperty(
name="Create Collection",
description="Create a new collection for imported assets",
default=True
)
We will also need to register the properties just like we registered the panel and operator in the
rtx_remix_addon/operators/__init__.py
file.
Create a rtx_remix_addon/properties/__init__.py
file and register the modules.
import bpy
from .import_assets_properties import REMIX_PG_ImportAssetsProperties
def register():
bpy.utils.register_class(REMIX_PG_ImportAssetsProperties)
bpy.types.Scene.remix_import_props = bpy.props.PointerProperty(type=REMIX_PG_ImportAssetsProperties)
def unregister():
del bpy.types.Scene.remix_import_props
bpy.utils.unregister_class(REMIX_PG_ImportAssetsProperties)
if __name__ == "__main__":
register()
Note that we also update the scene’s custom properties when we register and unregister the properties.
We can now use the properties in place of the values we previously defined like so:
props = context.scene.remix_import_props
bpy.ops.wm.usd_import(
filepath=file_path,
create_collection=props.create_collection,
scale=props.assets_scale,
import_visible_only=False,
relative_path=False,
)
Displaying the properties in the UI#
Since the operator is only responsible for defining an action, we need to display the properties in the panel.
To do so we will modify the draw
method of the REMIX_PT_ViewportPanel
(rtx_remix_addon/panels/viewport_panel.py
)
class we defined earlier.
After slightly refactoring the existing code, we will get the following methods:
def draw(self, context):
layout = self.layout
# Create a column to organize the layout
col = layout.column()
# Setup the Import Assets operator
self._setup_import_assets(context, col)
def _setup_import_assets(self, context, col):
box = col.box()
box_col = box.column()
# Add a label
box_col.label(text="Import Assets")
# Add the operator button
box_col.operator(REMIX_OT_ImportAssets.bl_idname, text="Import")
# Add a separator
box_col.separator()
# Add the operator properties
props = context.scene.remix_import_props
box_col.prop(props, "create_collection")
box_col.prop(props, "assets_scale")
The draw
method now calls a protected method we defined below called __setup_import_assets
. This newly
defined method will display the operator we previously built as well as the properties we just defined.
You may also notice that some boxes and separators were added and the operator button’s display text was changed.
For more information on how to build layouts in Blender, refer to the Blender API Documentation
The result#
We now have a couple of parameters displayed in the UI that affect the behavior of the Import Selected Assets operator.
Appending an asset in the RTX Remix Toolkit from Blender#
To replace an asset in the RTX Remix Toolkit from Blender, you will need to add a new operator
REMIX_OT_ExportAssets
and add it to your existing panel REMIX_PT_ViewportPanel
. You can use the
Building a Blender Add-On instructions as a baseline to create this new operator.
From there, you will need to implement the execute
method of the operator, but the method will need to call some
utility methods to carry out its functionality, so you can start adding the required utility methods in the APIUtils
class (rtx_remix_addon/utils/api.py
).
Building the utility methods#
You will need the following utility methods:
get_output_path
- To get the current project’s default output path for ingested assetsingest_models
- To ingest the Blender assets into the RTX Remix Toolkitreplace_file_path
- To replace the file path of an asset in the RTX Remix Toolkit with the newly ingested asset
Starting with the get_output_path
method, all we need is to request the path from the REST API and return it.
def get_output_path(self) -> str:
"""
Get the default ingested assets output path for the currently opened project.
"""
response = self._send_request(HTTPMethod.GET, "/stagecraft/assets/default-directory")
return response.get("asset_path")
The ingest_models
method is slightly more complex. It will get a list of input paths and an output path as arguments
and return the output paths of the ingested assets when the ingestion is complete.
We also added optional arguments to set the executor and usd extension. Note that the arguments are not required to ingest the assets.
The complexity of the method comes from the data built and received. Whilst a lot of options are available, we only need to send the input files and the output directory for the ingestion to succeed.
We will also utilize the ingestion dataflows to get the output paths when the ingestion is complete.
def ingest_models(
self, input_paths: list[str], output_path: str, executor: int = 1, usd_extension: str = "usd"
) -> list[str]:
"""
Ingest a Blender mesh.
Args:
input_paths: A list of absolute file paths to the meshes to ingest
output_path: The output path where the ingested assets will be stored
Returns:
A list of absolute file paths to the ingested assets
"""
if executor not in [0, 1]:
raise ValueError(f"Unsupported executor: {executor}.")
if usd_extension not in ["usd", "usda", "usdc"]:
raise ValueError(f"Unsupported USD extension: {usd_extension}.")
# Prepare the data for the API request
data = {
"executor": executor,
"context_plugin": {
"data": {
"input_files": input_paths,
"output_directory": output_path,
"output_usd_extension": usd_extension,
},
}
}
# Send the request
response = self._send_request(HTTPMethod.POST, "/ingestcraft/mass-validator/queue/model", data=data)
# Get the output paths when the ingestion is complete
output_paths = []
for completed_task in response.get("completed_schemas"):
dataflows = completed_task.get("context_plugin").get("data").get("data_flows")
for dataflow in dataflows:
if dataflow.get("channel") != "ingestion_output":
continue
output_paths.extend(dataflow.get("output_data"))
return output_paths
Lastly, we need to build the append_file_path
method. This method will take a Prim Path, and an Asset File Path, and
will append the asset file path to the asset in the RTX Remix Toolkit.
def append_file_path(self, prim_path: str, asset_file_path: str):
"""
Replace an asset reference file path with a new one.
"""
encoded_path = self._encode_string(prim_path)
data = {
"asset_file_path": asset_file_path
}
self._send_request(HTTPMethod.POST, f"/stagecraft/assets/{encoded_path}/file-paths", data=data)
Creating the properties#
Since we want the Export Assets operator to have the same level of functionality as the other operators, we will create some properties for it too.
Create a rtx_remix_addon/properties/export_assets_properties.py
and add the following properties:
import bpy
class REMIX_PG_ExportAssetsProperties(bpy.types.PropertyGroup):
assets_scale: bpy.props.FloatProperty(
name="Assets Scale",
description="Scale factor for exported assets",
default=1.0,
min=0.0001,
max=10000.0
)
export_target: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Export Target",
description="The target object to export",
)
ingestion_executor: bpy.props.EnumProperty(
name="Ingestion Executor",
description="Choose the RTX Remix ingestion executor",
items=[
("0", "Current Process", "Execute ingestion in the current process"),
("1", "External Process", "Execute ingestion in an external process")
],
default="1"
)
usd_extension: bpy.props.EnumProperty(
name="USD Extension",
description="Choose the USD file extension",
items=[
("usd", "USD", "Universal Scene Description"),
("usda", "USDA", "USD ASCII"),
("usdc", "USDC", "USD Crate (binary)")
],
default="usd"
)
The assets scale property will be used to scale the assets when exporting them using Blender. The export target property will be used to specify the target object to export.
The ingestion executor property will be used to specify the ingestion executor during the ingestion. The USD extension property will be used to specify the file extension for the ingested assets.
We will also need to make sure the Property Group is registered in the rtx_remix_addon/properties/__init__.py
file.
import bpy
from .export_assets_properties import REMIX_PG_ExportAssetsProperties
from .import_assets_properties import REMIX_PG_ImportAssetsProperties
def register():
bpy.utils.register_class(REMIX_PG_ExportAssetsProperties)
bpy.utils.register_class(REMIX_PG_ImportAssetsProperties)
bpy.types.Scene.remix_export_props = bpy.props.PointerProperty(type=REMIX_PG_ExportAssetsProperties)
bpy.types.Scene.remix_import_props = bpy.props.PointerProperty(type=REMIX_PG_ImportAssetsProperties)
def unregister():
del bpy.types.Scene.remix_export_props
del bpy.types.Scene.remix_import_props
bpy.utils.unregister_class(REMIX_PG_ExportAssetsProperties)
bpy.utils.unregister_class(REMIX_PG_ImportAssetsProperties)
if __name__ == "__main__":
register()
Creating the operator#
Now that we have all the utility methods, we can create the operator. Following the same steps we followed in the
previous section, we will create a rtx_remix_addon/operators/export_assets_operator.py
file, and start implementing
its content.
We will start by importing the required dependencies.
The pathlib
and tempfile
modules will be used to create temporary directories and files, while the APIUtils
and
PrimsUtils
modules will be used to interact with the RTX Remix Toolkit and the prim paths returned by the REST API.
import bpy
from pathlib import Path
from tempfile import TemporaryDirectory
from ..utils.api import APIUtils
from ..utils.prims import PrimsUtils
We can then define our REMIX_OT_ExportAssets
operator class.
class REMIX_OT_ExportAssets(bpy.types.Operator):
bl_idname = "rtx_remix.export_assets"
bl_label = "Export Assets"
bl_description = (
"Export the selected assets to the RTX Remix Toolkit and append the reference to the "
"selected meshes"
)
def __init__(self):
self._api = APIUtils()
def __del__(self):
self._api = None
Just like we did with the REMIX_OT_ImportAssets
operator, we will now implement the execute
method.
The method may look daunting but the comments should make it easier to understand.
Similarly to the
execute
method of theREMIX_OT_ImportAssets
operator, we start by validating the inputs and the state the of the API.We then get the RTX Remix Toolkit selection and make sure we have a target asset to append our exported assets to.
The next step is then to get the output path for the ingestion process.
After that’s done, we select the export target in Blender to make sure we export the correct assets.
We then use the
bpy.ops.file.pack_all()
operator to collect all the textures for the exported asset.Once that’s done, we setup a temporary directory and use the the export target’s name to create a temporary file path.
Next, we export the target object to an OBJ file in the temporary directory, and ingest it using the
APIUtils
’singest_models
method.Finally, we append the absolute file paths of the ingested assets to the target mesh using
APIUtils
’sappend_file_path
method.
def execute(self, context):
props = context.scene.remix_export_props
# Make sure an export target is selected
if not props.export_target:
self.report({"ERROR"}, "An export target must be selected")
return {"CANCELLED"}
# Make sure the API is running
if not self._api.get_status():
self.report({"ERROR"}, "The REST API is not running")
return {"CANCELLED"}
# Make sure a project is opened
if not self._api.get_opened_project():
self.report({"ERROR"}, "No project is opened")
return {"CANCELLED"}
try:
# Get the assets selected in the RTX Remix viewport
assets = self._api.get_assets(selection=True, asset_types="models")
# Get the mesh paths for the selected assets
meshes = [PrimsUtils.get_mesh_path(asset) for asset in assets if PrimsUtils.get_mesh_path(asset)]
if not meshes:
self.report({"ERROR"}, "No asset selected in RTX Remix")
return {"CANCELLED"}
# If multiple meshes are selected in Remix, use the first one
target_mesh = meshes[0]
# Get the ingestion output path
output_path = self._api.get_output_path()
# Selected the Export Target
bpy.ops.object.select_all(action="DESELECT")
props.export_target.select_set(True)
context.view_layer.objects.active = props.export_target
# Pack all textures
bpy.ops.file.pack_all()
# Setup a temporarary directory
with TemporaryDirectory() as temp_dir:
# Build a temporary file path
temp_file = str((Path(temp_dir) / props.export_target.name).with_suffix(".obj"))
# Export the target Object
bpy.ops.wm.obj_export(
filepath=temp_file,
global_scale=props.assets_scale,
export_pbr_extensions=True,
path_mode="COPY",
export_triangulated_mesh=True,
)
# Ingest the exported model
ingested_paths = self._api.ingest_models(
[temp_file],
output_path,
executor=int(props.ingestion_executor),
usd_extension=props.usd_extension
)
# Append the references to the target mesh
for ingested_path in ingested_paths:
self._api.append_file_path(target_mesh, ingested_path)
except Exception as e:
self.report({"ERROR"}, str(e))
return {"CANCELLED"}
return {"FINISHED"}
The last step before the add-on is completed is to register the newly added operator
(in rtx_remix_addon/operators/__init__.py
) and to add it to our panel (in rtx_remix_addon/panels/viewport_panel.py
).
import bpy
from .export_assets_operator import REMIX_OT_ExportAssets
from .import_assets_operator import REMIX_OT_ImportAssets
def register():
bpy.utils.register_class(REMIX_OT_ExportAssets)
bpy.utils.register_class(REMIX_OT_ImportAssets)
def unregister():
bpy.utils.unregister_class(REMIX_OT_ExportAssets)
bpy.utils.unregister_class(REMIX_OT_ImportAssets)
if __name__ == "__main__":
register()
Adding the operator and properties to the panel#
The final panel implementation should look like so:
import bpy
from ..operators import REMIX_OT_ExportAssets
from ..operators import REMIX_OT_ImportAssets
class REMIX_PT_ViewportPanel(bpy.types.Panel):
bl_label = "RTX Remix"
bl_idname = "REMIX_PT_ViewportPanel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "RTX Remix"
def draw(self, context):
layout = self.layout
# Create a column to organize the layout
col = layout.column()
# Setup the Import Assets operator
self._setup_import_assets(context, col)
# Add a separator
col.separator()
# Setup the Export operator
self._setup_export_assets(context, col)
def _setup_import_assets(self, context, col):
box = col.box()
box_col = box.column()
# Add a label
box_col.label(text="Import Assets")
# Add the operator button
box_col.operator(REMIX_OT_ImportAssets.bl_idname, text="Import")
# Add a separator
box_col.separator()
# Add the operator properties
props = context.scene.remix_import_props
box_col.prop(props, "create_collection")
box_col.prop(props, "assets_scale")
def _setup_export_assets(self, context, col):
box = col.box()
box_col = box.column()
# Add a label
box_col.label(text="Export Assets")
# Add the operator button
box_col.operator(REMIX_OT_ExportAssets.bl_idname, text="Export")
# Add a separator
box_col.separator()
# Add the operator properties
props = context.scene.remix_export_props
box_col.prop(props, "export_target")
box_col.prop(props, "ingestion_executor")
box_col.prop(props, "usd_extension")
box_col.prop(props, "assets_scale")
The result#
You should now see your assets in the RTX Remix Toolkit!
Need to leave feedback about the RTX Remix Documentation? Click here