Overview

CAD Converter Service Extension - [omni.services.convert.cad]

About

The CAD Converter Service is a service for batch conversion of CAD files to USD.

SAMPLE REQUEST:

Format: “setting name” : default value

Request arguments:

"import_path": ""

Full path to the CAD File to convert.

"output_path": ""

Full path to the ouptut usd file.

"converter_option": { "bInstancing": true }

Converter options to use. Refer to omni.kit.converter.hoops_core, omni.kit.converter.jt_core, and omni.kit.converter.dgn_core for configuration options. This takes precendence over the config_path option below.

"config_path": ""

Full path to converter config file. It will be deprecated in the future and use of converter_option is preferred. Refer to omni.kit.converter.hoops_core, omni.kit.converter.jt_core, and omni.kit.converter.dgn_core for configuration options.

Sample Input JSON:

{
  "import_path": "/ANCHOR.sldprt",
  "output_path": "/tmp/testing/ANCHOR.usd",
  "converter_options": { "bInstancing": true },
  "config_path": "/sample_config.json",
}

To use the DGN Converter, the parameter must be set to “DGN Converter”. This is shown with the following request body example:

Sample Input JSON for DGN:

{
  "import_path": "/tmp/input_file.dgn",
  "output_path": "/tmp/output_file.usd",
  "converter_options" : { "bInstancing": true },
  "config_path": "/tmp/sample_config.json",
}

1.0 How to set your extension up as a Kit Service

This section goes over how to build and register a converter extension with the CAD Converter Service Extension in the Omniversion Application USD Composer for testing.

An example of setting up an extension as a Kit Service can be found at https://docs.omniverse.nvidia.com/services/latest/services/services_convert.html.

This section assumes you have already developed a converter backend with Python bindings and module with the naming convention of omni.connect.{file format} (e.g., "omni.connect.foo") available for interfacing with your converter extension as well as a helper class for accessing this interface. Registering with the Service extension also requires a method to be called for creating conversion tasks for your extension. This section also assumes you have this method (e.g., create_converter_task) to call your converter backend to begin conversion.

This section is divided into what extensions to add as dependencies to gain access to important Python modules and necessary modifications to your extension including inheritance and imported methods with their required parameters.

1.1 Adding the Common Converter and CAD Service extensions as a depedency

This sub-section adds the service extension as a dependency to your extension. This allows your kit extension to look for and load the service extension’s python module and import important classes and variables. This is done by adding a dependency to an extension’s extension.toml file. All kit extensions need this file to define its name, dependencies, version number, author names, etc.

  1. Navigate to your extension.toml - you can place your extension file here: {repository_root}/source/extensions/{extension name}/config/extension.toml

  2. Modify your dependencies section to add the Common Converter and CAD Service extensions

[dependencies]
"omni.kit.converter.common" = { version = "500.0.0"}
"omni.services.convert.cad" = { version = "500.0.0", optional = true }

1.2 Setting up your extension to register and un-register with the service extension

Now that you added the Converter Common extension in your extension.toml file, you will have to import the module omni.kit.converter.common. You do not need to import the entire module, only the class ICadCoreExtBase and method initialize_connect_sdk. The former is a base class (from omni.kit.converter.common/python/cad_core_ext_base.py) that we can inherit from that handles registering and un-registering extensions with the CAD Service extension. It adds variables such as "FILTER_DATA" and "OPTIONS_CLS" necessary for the CAD Service extension to determine whether a converter extension can process the incoming conversion task.

To inherit from the class, add the class name to your class like below:

class FOOConverter(ICadCoreExtBase):

Within this class, you need to add your filters ("FILTER_DATA"), options ("OPTIONS_CLS"), and the service title ("SERVICE_TITLE"). The first two will be discussed later in subsections 1.3 and 1.4.

FILTER_DATA = FOO_CORE_FILTER_DATA
OPTIONS_CLS = FOOOptions
SERVICE_TITLE = "FOO Converter"

The methods on_startup and on_shutdown are required and must be defined. You can create global instances and call the base class’s _on_startup and _on_shutdown.

def on_startup(self, ext_id):
    """
    Initialize the FOO Converter and/or registers the service
    """
    global _global_instance
    _global_instance = weakref.ref(self)
    super()._on_startup(ext_id)

def on_shutdown(self):
    """
    Uninitialize the FOO Converter and/or un-registers the service
    """
    global _global_instance
    _global_instance = None
    super()._on_shutdown()

The method "initialize_connect_sdk" is responsible for setting up variables for the Connect SDK libraries which are essential for processing USD data. This can be called at the beginning of your converter task function "create_converter_task".

  1. Here is the full example of a file to define your extension : {repository_root}/source/extensions/omni.kit.converter.my_core/python/impl/extension.py

import weakref
from typing import Optional

import carb
import omni.ext
from omni.kit.converter.common import ICadCoreExtBase, initialize_connect_sdk

from .filters import FOO_CONVERTER_SUPPORTED_FORMATS, FOO_CORE_FILTER_DATA
from .helper import FOOConverterCoreHelper
from .options import FOOOptions

__all__ = [
    "FOOConverter",
    "get_instance",
    "FOO_CONVERTER_SUPPORTED_FORMATS",
    "FOO_CORE_FILTER_DATA",
    "FOOConverterCoreHelper",
    "FOOOptions",
]

_global_instance = None


class FOOConverter(ICadCoreExtBase):
    """
    FOO Core Converter Extension
    """

    FILTER_DATA = FOO_CORE_FILTER_DATA
    OPTIONS_CLS = FOOOptions
    SERVICE_TITLE = "FOO Converter"

    def on_startup(self, ext_id):
        """
        Initialize the FOO Converter and/or registers the service
        """
        global _global_instance
        _global_instance = weakref.ref(self)
        super()._on_startup(ext_id)

    def on_shutdown(self):
        """
        Uninitialize the FOO Converter and/or un-registers the service
        """
        global _global_instance
        _global_instance = None
        super()._on_shutdown()

    def create_converter_task(self, input_path: str, output_path: str, file_format_args: dict[str, str]):
        """
        This method is passed on to the ConverterRegistry within omni.services.convert.cad to be called when a conversion task is requested
        """
        initialize_connect_sdk(self.get_ext_name())
        _helper = FOOConverterCoreHelper()
        return _helper._create_import_task(input_path, output_path, file_format_args)


def get_instance() -> Optional[FOOConverter]:
    """
    If available, returns the weakref pointer
    """
    global _global_instance
    if _global_instance and _global_instance():
        return _global_instance()

1.3 Add filters for file format conversion

CAD converter extension filters list what formats ("FOO_CONVERTER_SUPPORTED_FORMATS") are supported by a converter. The filter data ("FOO_CORE_FILTER_DATA") contains the name of the converter extension denoted by “CAD Converter - {name of converter}”, the filter regular expressions, and the filter descriptions.

  1. Create a file under your extension : {repository_root}/source/extensions/omni.kit.converter.my_core/python/impl/filters.py

from omni.kit.converter.common import ConverterFilterData

# list of supported file formats by omni.kit.convert.my_core
FOO_CONVERTER_SUPPORTED_FORMATS = ["(.*\\.FOO$)"]

FOO_CORE_FILTER_DATA = [
    ConverterFilterData("CAD Converter - FOO", ["(.*\\.FOO$)"], ["FOO Files (*.FOO)"]),
]

1.4 Add options

The options class adds configurations to be updated based on JSON configuration file. You will need the Python module dataclasses. This modeule contains dataclass which is a decorator for creating user-defined classes, in this case a Python class (e.g., FOOOptions) based on your specifications class from your Python bindings (e.g., omni.connect.foo.Spec).

You can also import typing for Dict class. This is optional. The DGN and HOOPS converter extensions possess the parseArgs method from their respective backend’s specification class (called by super().parseArgs(args)). Both extensions convert JSON formatted strings into Python dictionaries containing the options as strings. The documentation assumes your extension handles parsing of arguments for options.

  1. Create a file under your extension : {repository_root}/source/extensions/omni.kit.converter.my_core/python/impl/options.py

from dataclasses import dataclass
from typing import Dict

import omni.connect.foo # this name is based on the Python module of your converter

__all__ = ["FOOOptions"]


@dataclass
class FOOOptions(omni.connect.foo.Spec):
    def __init__(self):
        omni.connect.foo.Spec.__init__(self)

    # optional - this depends
    def parse(self, args: dict[str, str]):
        super().parseArgs(args)

1.5 Updating your Helper Class

This subsection goes over using the Converter Common extension as previously mentioned in the section. This subsection assumes you already possess a helper class for calling the Python bindings interface to your converter backend.

Reference omni.kit.converter.*_core extensions’ helper classes (omni.kit.converter.FOO_core/python/impl/helper.py) for examples of helper class implementation.

Import the following classes and methods from the Converter Common extension:

  • ConverterStatus - a tuple containing the error code (0 if successful) and the error message

  • OmniClientWrapper - used to check whether a folder exists

  • OmniUrl - creates a URL valid for Nucleus

  • ProgressLogConsumer - optional; used for parsing the converter logs for progress in CAD Converter GUI extension; see omni.kit.converter.common/python/progress_log_consumer.py

  • run_scene_opt - runs the scene optimizer extension as a post-process to generate UVs for meshes, remove hidden prims

from omni.kit.converter.common import ConverterStatus, OmniClientWrapper, OmniUrl, ProgressLogConsumer, run_scene_opt

To review how the Service extension sends requests for conversion tasks, your extension registers itself with the Service extension in subsection 1.2 and provides it with a conversion task function, e.g., create_converter_task. This method should call a function from your helper class to use the interface to convert a CAD file. Let’s call this _create_import_task.

In your _create_import_task method, use ConverterStatus to return the error code and error message to the Service extension. The content of the string is dependent on the error code value. Note that the example code below is from omni.kit.converter.jt_core which returns an integer to denote a success or the error type. The extensions omni.kit.converter.dgn_core and omni.kit.converter.hoops_core return success flags with status strings.

Here are examples of using ConverterStatus:

# example if the input path is invalid
if not input_file_url.exists:
  return "", ConverterStatus(-1, "Input file does not exist!")

res, local_file_url = await input_file_url.get_local_file_async()

# confirm local file exists; else return
if res != omni.client.Result.OK:
    omni.log.error(f"Could not get local file: {local_file_url}. Reason: {res}")
    return "", ConverterStatus(error_code=error_code, error_msg=error_msg)
result = await self._convert_obj_async(str(local_file_url), output_destination_url, file_format_args)

message = ""
if result[0] != 0:
    message = result[1]
    omni.log.warn(message)
else:
    omni.log.info(message)

return str(output_destination_url), ConverterStatus(result[0], message)

In your _create_import_task method, use OmniClientWrapper to check whether a folder exists and to create a folder in Nucleus.

Here is an example of using OmniClientWrapper:

output_directory = temp_output_folder or Path(host_dir)
if not await OmniClientWrapper.exists(str(output_directory)):
    await OmniClientWrapper.create_folder(str(output_directory))

In your _create_import_task method, use OmniUrl to create valid URLs.

async def _create_import_task(self, input_path: str, output_path: str, file_format_args: dict[str, str]):
    ...
    input_file_url = OmniUrl(input_path)
    export_url = OmniUrl(output_path)

The class ProgressLogConsumer parses output from the converter backends. It reads standard output, and the CAD Converters provided updates using “*step*”, “*prog*”, and “*Begin*”. “*step*” denotes a new stage of the conversion whether it is, for example, the “Reading CAD” stage or “Exporting to USD” stage. “*prog*” provides updates on the conversion of the file. DGN, HOOPS, and JT provide this information to update the progress bar for the GUI extension. “*Begin*” denotes the beginning of a new step in the converter. To use ProgressLogConsumer, you can inherit from the class like below:

class DgnConverterCoreHelper(ProgressLogConsumer):

In your _create_import_task method, use run_scene_opt to generate UVs for meshes in USD or remove hidden prims.

# Run Scene Optimizer
run_scene_opt(output_path, options.bOptimize, options.bConvertHidden)

1.6 Building the extension

  1. Navigate to your repository’s root folder via terminal and run the following command.

./repo.bat build -xrd
  1. This will generate builds of your extension for both release and debug configurations located at {repository_root}/_build/{platform}/{config}/exts.

2.0 How to use a Service

This section covers how to use your extension in an Omniverse Application and running requests through OpenAPI. The section is divided into how to enable your service extension, test requests against your extension in OpenAPI, and testing ypur extension in a CAD Container (if applicable).

2.1 Enabling your Kit Service on USD Composer

Follow these instructions to set up the Service extension and other converter extensions in Composer:

  1. Ensure you have built the CAD Converter extensions.

  2. Open Composer using Kit 106 (e.g., version 2024.1.0).

  3. Open the Extensions Manager window.

  4. Click the three-horizontal bar icon to access Settings (refer to the screenshot below):

    Extensions Manager Settings

  5. Add the path to the built Service extension’s containing folder and the folders of other converters (e.g., {repository_root}/_build/{platform}/{config}/exts).

  6. Enable the Service extension first, and then enable the converters.

2.2 Testing your Kit Service through OpenAPI

  1. Once the Service extension is enabled, navigate to the local host link in a web browser. The link should scroll you down to the CAD section.

http://localhost:8111/docs#/cad/asset_convert_convert_cad_process_post
  1. Select “Try it out” and apply the following settings:

    • import_path: Path to the file to be converted.

    • output_path: Path to the usd output file.

    • converter_options: Dictionary representing the converter options to use.

    • config_path: Path to the JSON config file (optional and soon to be deprecated).

{
 "import_path": "C:/Github/cad-converter-service-ext/test_data/1-box-1.SLDPRT",
 "output_path": "C:/Github/cad-converter-service-ext/test_data/1-box-1.usd",
 "converter_options": { "bInstancing": true },
 "config_path": "C:/Github/hoops-exchange-cad-converter/data/Omni_CADConverterConfig_Annotated_Simplified.json",
}
  1. Once you have updated the settings, select “Post”. Running CAD Services

2.3 Running the CAD Converter service extension on a CAD Container

This section assumes the user has access to Omniverse NVIDIA GPU Cloud (NGC) Catalog (https://catalog.ngc.nvidia.com/) and an team’s pool of CAD Container images. To view a team’s pool of images, the user needs to login at https://ngc.nvidia.com/signin and select their team.

This section also assumes that you already have docker installed. Docker install link: https://docs.docker.com/engine/install/ubuntu/

For NGC need to run docker login nvcr.io With username = $oauthtoken And API Key coming from one generated here: https://ngc.nvidia.com/setup/api-key

Run the container with the following: docker run -it -v {path_to_host_directory_containing_files}:{path_on_container_to_access_files} --env "ACCEPT_EULA=Y" -p {port}:{port} {link_to_image} /startup.sh

Example {link_to_image}: nvcr.io/{my_ngc_team}/omniverse-cad-converter:0.1.8-kit.105.0-alpha.1

Once you the container is up go to and you should see the OpenAPI docs: http://localhost:8011/docs. The last lines to show up when the container is ready are:

[Info] [omni.kit.app.plugin] app ready
app ready
[Info] [omni.kit.app._impl] python GC: gc.enable()

The Open API docs page should look like the below image: Open API docs

Scroll down to the GET request and click on it. You will a button “Try it out”. Click on it to see another blue button “Execute”. Click on “Execute” to see an HTML response with code 200 under “Responses” to see the CAD Converters loaded with the container. They should show as “CAD Converter;DGN Converter;JT Converter”.

NOTE: If the container has been closed (e.g., docker process was killed), then the user will see a response code of “undocumented” and a response body of “Failed to fetch.”.

Scroll down to the POST request and click on it. You will see a JSON request body:

{
  "import_path": "string", (full path to CAD file)
  "output_path": "string", (path to output file)
  "converter_options": "dict" (Dictionary representing the converter options to use)
  "config_path": "string" (Path to the JSON config file [optional and soon to be deprecated])
}

3.0 How to make an Omniverse TAAS and Farm setup

This section covers how you can create and test ov-kit-taas based containers on Stage-A, for internal development and QA testing. It provides examples on how to create job definitions with the CAD Converter service extension.

3.2 Creating, Modifying, and Publishing a Job Definition

  1. Creating and mofifying a Job Definition

In your extension you will need to create a .kit file to add your extension (e.g., "--enable omni.services.convert.foo").

Create a folder named "job_definitions" under your {repository_root}/source/extensions/{extension.name} directory.

As an example, see the CAD Service extension’s (omni.services.convert.cad) file job_definitions/job.cad_converter.kit.

Here is an example of a job definition .kit file:

args = [
    "--enable omni.services.convert.cad",
    "--enable omni.kit.converter.foo_core",
]
  1. Publishing a Job Definition

You can publish a container manually or as part of the CI/CD. To publish your repository’s job definitions, run the following commands:

  • Linux: ./repo.sh services job-definition publish

  • Windows: repo.bat services job-definition publish

You can specify the extensions in your {repository_root}/repo.toml file on which extensions you wish to publish job definitions for. You can also define the remote URL here. The remote URL is where the job will be deployed to.

[repo_services_tools_job_definitions_publish]
extensions = [
    "omni.services.convert.cad",
]
remote = "https://ov-taas-deploy.sc-paas.nvidia.com/release/dashboard/definition/cad-converter"

You can publish job definitions only one extension:

  • Linux: ./repo.sh job_def_publish --ext {extension.name}

  • Windows: repo.bat job_def_publish --ext {extension.name}

This can also be done by supplying the .kit file of the job definition

./repo.sh services job-definition publish --file ./source/exts/omni.services.convert.cad/job_definitions/job.cad_converter.kit

This results in a job definition that is ready to be deployed on an Omniverse Farm instance.

3.3 Container Deployment

  1. Go to the CAD Converter Service job definition deployment, https://ov-taas-deploy.sc-paas.nvidia.com/release/dashboard/definition/cad-converter. This URL was defined during publishing in subsection 3.2.

  2. Click the “Log In” button. You do not need to enter credentials. Here you will see job definitions published in the previous subsection.

  3. Click the “Deploy” button.

  4. For Deploy Target, select Stage-A.

  5. Select container image:

  • Click on the drop-down menu and select the desired container image, OR

  • From NGC (https://registry.ngc.nvidia.com/orgs/nvidian/teams/omniverse/containers/ov-kit-taas/tags) copy and paste the custom image tag.

    • If you do not have access to NGC, go to https://stage-a.us-east-1.nv-ov.farm/queue/management/jobs/load to access the latest version and use the image for the "container" key for "cad-converter".

  • If all looks good, click the Deploy button. You should see a confirmation page.

3.4 Testing

You need a local kit.exe and files example_submit_farm_job.py and heads_list.txt.

  • heads_list.txt - list of CAD files to convert

  • example_submit_farm_job.py - Python script to submit to Stage-A

Use a local kit file to submit the job by executing the Python script example_submit_farm_job.py.

  • Monitor the tasks here (https://stage-a.us-east-1.nv-ov.farm/queue/management/dashboard/tasks?sortBy=submittedAt&sortOrder=desc). CLI Args:

--output-dir= {output directory for converted USD files}
--heads-list= {list of CAD files to convert}
--farm-url= {TAAS container staging path} # https://stage-a.us-east-1.nv-ov.farm/ (use this for Stage-A)

Example:

C:\Gitlab\cad-converter-service-ext\_build\windows-x86_64\release\kit\kit.exe --/log/outputStreamLevel=Info --enable omni.client --enable omni.kit.pip_archive --exec "./example_submit_farm_job.py --output-dir=omniverse://kit-test-content.ov.nvidia.com/Projects/Converters_Test_Models/outputs --heads-list=./heads_list.txt --farm-url=https://stage-a.us-east-1.nv-ov.farm/"

Check output-dir for results once the task status is finished.

4.0 How to use the NVIDIA Cloud Functions (NVCF) Service (Coming Soon)

Our team is working on the deployment of NVCF CAD Converter services. These involve cloud functions - serverless API developed to allow batch processing of CAD files on GPUs to assist in the scaling of users’ workloads.

Users can also map their container images to NVCF. This can also be done through the NGC website. Users can create functions, define the function name, provide the transport protocol (e.g., HTTP), port, inference endpoint, and health path. Additionally, users can specify environment variables and alternative start up scripts.

When users create their cloud functions, they can see their status (e.g., INACTIVE) and deploy a specified version of their function. Each function can have defined backends (e.g., GFN), GPU (L40G), and number of instances. Deployment of functions can take a few minutes, and their status changes to “ACTIVE” once available.

Please see the NVIDIA documentation’s user guide for further details: https://docs.nvidia.com/cloud-functions/user-guide/latest/cloud-function/functions.html This involves the publishing of a container to NGC (as described in subsection 3.3 above) and testing including through the use of Python scripts through Command Line Interface (CLI).

4.1 Example Testing through CLI

  1. You need NGC SDK for CLI testing: pip install ngcsdk

  2. You need to create a “Run Key” on NVCF to test your deployed functions. You can do so by clicking “Generate Run Key” and confirming to invalidate any existing key.

  3. Click the function name and a side panel will open to display “Function ID” and “Version ID”.

  4. You can then use your script by changing the variables below with “Run Key”, “Function ID” and “Version ID” to test your deployment.

STARFLEET_API_KEY = (
    "nvapi-{your_run_key}"
)
function_id = "{your_fn_id}"
function_version_id = "{your_fn_ver_id}"

5.0 Licensing Terms of Use and Third-Party Notices

The omni.services.convert.cad and related CAD converter Extensions are Omniverse Core Extensions. Do not redistribute or sublicense without express permission or agreement. Please read the Omniverse License Agreements for detailed license information.