Siggraph Presentation

This guide will be officially introduced at Siggraph 2023 - Houdini Hive on Wednesday, 9. of August 2023 at 11:00 AM PST.

Schemas

Important

This page only covers how to compile/install custom schemas, as we cover what schemas are in our schemas basic building blocks of USD section.

As there is a very well written documentation in the official docs, we only cover compilation(less) schema creation and installation here as a hands-on example and won't go into any customization details. You can also check out Colin's excellent Usd-Cook-Book example.

Table of Contents

  1. API Overview In-A-Nutshell
  2. What should I use it for?
  3. Resources
  4. Overview
  5. Generate Codeless Schema
    1. Edit 'GLOBAL' prim 'customData' dict
    2. Run usdGenSchema
    3. Add the generated pluginInfo.json director to 'PXR_PLUGINPATH_NAME' env var.
    4. Run your Usd (capable) application.
  6. Generate Compiled Schema
    1. Run usdGenSchema
    2. Compile schema
    3. Update environment variables.
    4. Run your Usd (capable) application.

TL;DR - Schema Creation In-A-Nutshell

  • Generating schemas in Usd is as easy as supplying a customized schema.usda file to the usdGenSchema commandline tool that ships with Usd. That's right, you don't need to code!
  • Custom schemas allow us to create custom prim types/properties/metadata (with fallback values) so that we don't have to repeatedly re-create it ourselves.
  • In OOP speak: It allows you to create your own subclasses that nicely fit into Usd and automatically generates all the Get<PropertyName>/Set<PropertyName> methods, so that it feels like you're using native USD classes.
  • We can also create codeless schemas, these don't need to be compiled, but we won't get our nice automatically generated getters and setters and schema C++/Python classes.

Tip

Codeless schemas are ideal for smaller studios or when you need to prototype a schema. The result only consists of a plugInfo.json and generatedSchema.usda file and is instantly created without any need for compiling.

Compiling against USD

Most DCCs ship with a customized USD build, where most vendors adhere to the VFX Reference Platform and only change USD with major version software releases. They do backport important production patches though from time to time. That's why we recommend using the USD build from the DCC instead of trying to self compile and link it to the DCC, as this guarantees the most stability. This does mean though, that you have to compile all plugins against each (major version) releases of each individual DCC.

What should I use it for?

Tip

We'll usually want to generate custom schemas, when we want to have a set of properties/metadata that should always exist (with a fallback value) on certain prims. A typical use case for creating an own typed/API schema is storing common render farm settings or shot related data.

Resources

Overview

For both examples we'll start of with the example schema that USD ships with in its official repo.

You can copy and paste the content into a file and then follow along or take the prepared files from here that ship with this repo.

Our guide focuses on working with Houdini, therefore we use the usdGenSchema that ships with Houdini. You can find it in your Houdini /bin directory.

$HFS/bin/usdGenSchema
# For example on Linux:
/opt/hfs19.5/bin/usdGenSchema

If you download/clone this repo, we ship with .bash scripts that automatically runs all the below steps for you.

You'll first need to cd to the root repo dir and then run ./setup.sh. Make sure that you edit the setup.sh file to point to your Houdini version. By default it will be the latest Houdini major release symlink, currently /opt/hfs19.5, that Houdini creates on install.

Then follow along the steps as mentioned below.

Codeless Schema

Codeless schemas allow us to generate schemas without any C++/Python bindings. This means your won't get fancy Schema.Get<PropertyName>/Schema.Set<PropertyName> getters and setters. On the upside you don't need to compile anything.

Tip

Codeless schemas are ideal for smaller studios or when you need to prototype a schema. The result only consists of a plugInfo.json and generatedSchema.usda file.

To enable codeless schema generation, we simply have to add bool skipCodeGeneration = true to the customData metadata dict on the global prim in our schema.usda template file.

over "GLOBAL" (
    customData = {
        bool skipCodeGeneration = true
    }
) {
}

Let's do this step by step for our example schema.

Step 1: Edit 'GLOBAL' prim 'customData' dict

Update the global prim custom data dict from:

over "GLOBAL" (
    customData = {
        string libraryName       = "usdSchemaExamples"
        string libraryPath       = "."
        string libraryPrefix     = "UsdSchemaExamples"
    }
) {
}
to:
over "GLOBAL" (
    customData = {
        string libraryName       = "usdSchemaExamples"
        string libraryPath       = "."
        string libraryPrefix     = "UsdSchemaExamples"
        bool skipCodeGeneration = true
    }
) {
}

Result | Click to expand content

#usda 1.0
(
    """ This file contains an example schemata for code generation using
        usdGenSchema.
    """
    subLayers = [
        @usd/schema.usda@
    ]
) 

over "GLOBAL" (
    customData = {
        string libraryName       = "usdSchemaExamples"
        string libraryPath       = "."
        string libraryPrefix     = "UsdSchemaExamples"
        bool skipCodeGeneration = true
    }
) {
}

class "SimplePrim" (
    doc = """An example of an untyped schema prim. Note that it does not 
    specify a typeName"""
    # IsA schemas should derive from </Typed>, which is defined in the sublayer
    # usd/lib/usd/schema.usda.
    inherits = </Typed>
    customData = {
        # Provide a different class name for the C++ and python schema classes.
        # This will be prefixed with libraryPrefix.
        # In this case, the class name becomes UsdSchemaExamplesSimple.
        string className = "Simple"
        }
    )  
{
    int intAttr = 0 (
        doc = "An integer attribute with fallback value of 0."
    )
    rel target (
        doc = """A relationship called target that could point to another prim
        or a property"""
    )
}

# Note that it does not specify a typeName.
class ComplexPrim "ComplexPrim" (
    doc = """An example of a untyped IsA schema prim"""
    # Inherits from </SimplePrim> defined in simple.usda.
    inherits = </SimplePrim>
    customData = {
        string className = "Complex"
    }
)  
{
    string complexString = "somethingComplex"
}
    
class "ParamsAPI" (
    inherits = </APISchemaBase>
)
{
    double params:mass (
        # Informs schema generator to create GetMassAttr() method
        # instead of GetParamsMassAttr() method
        customData = {
            string apiName = "mass"
        }
        doc = "Double value denoting mass"
    )
    double params:velocity (
        customData = {
            string apiName = "velocity"
        }
        doc = "Double value denoting velocity"
    )
    double params:volume (
        customData = {
            string apiName = "volume"
        }
        doc = "Double value denoting volume"
    )
}

Step 2: Run usdGenSchema

Next we need to generate the schema.

Make sure that you first sourced you Houdini environment by running $HFS/houdini_setup so that it find all the correct libraries and python interpreter.

usdGenSchema on Windows

On Windows you can also run hython usdGenSchema schema.usda dst to avoid having to source the env yourself.

Then run the following

cd /path/to/your/schema # In our case: .../VFX-UsdSurvivalGuide/files/plugins/schemas/codelessSchema
usdGenSchema schema.usda dst
Or if you use the helper bash scripts in this repo (after sourcing the `setup.sh` in the repo root):
cd ./files/plugins/schemas/codelessTypedSchema/
chmod +x build.sh # Add execute rights
source ./build.sh # Run usdGenSchema and source the env vars for the plugin path

Bug

Not sure if this is a bug, but the usdGenSchema in codeless mode currently outputs a wrong plugInfo.json file. (It leaves in the cmake @...@ string replacements).

The fix is simple, open the plugInfo.json file and replace:

...
    "LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", 
    "Name": "usdSchemaExamples", 
    "ResourcePath": "@PLUG_INFO_RESOURCE_PATH@", 
    "Root": "@PLUG_INFO_ROOT@", 
    "Type": "resource"
...

To:

...
    "LibraryPath": ".", 
    "Name": "usdSchemaExamples", 
    "ResourcePath": ".", 
    "Root": ".", 
    "Type": "resource"
...

Result | Click to expand content

# Portions of this file auto-generated by usdGenSchema.
# Edits will survive regeneration except for comments and
# changes to types with autoGenerated=true.
{
    "Plugins": [
        {
            "Info": {
                "Types": {
                    "UsdSchemaExamplesComplex": {
                        "alias": {
                            "UsdSchemaBase": "ComplexPrim"
                        }, 
                        "autoGenerated": true, 
                        "bases": [
                            "UsdSchemaExamplesSimple"
                        ], 
                        "schemaKind": "concreteTyped"
                    }, 
                    "UsdSchemaExamplesParamsAPI": {
                        "alias": {
                            "UsdSchemaBase": "ParamsAPI"
                        }, 
                        "autoGenerated": true, 
                        "bases": [
                            "UsdAPISchemaBase"
                        ], 
                        "schemaKind": "singleApplyAPI"
                    }, 
                    "UsdSchemaExamplesSimple": {
                        "alias": {
                            "UsdSchemaBase": "SimplePrim"
                        }, 
                        "autoGenerated": true, 
                        "bases": [
                            "UsdTyped"
                        ], 
                        "schemaKind": "abstractTyped"
                    }
                }
            }, 
            "LibraryPath": ".", 
            "Name": "usdSchemaExamples", 
            "ResourcePath": ".", 
            "Root": ".", 
            "Type": "resource"
        }
    ]
}

Step 3: Add the generated pluginInfo.json director to 'PXR_PLUGINPATH_NAME' env var.

Next we need to add the pluginInfo.json directory to the PXR_PLUGINPATH_NAME environment variable.

// Linux
export PXR_PLUGINPATH_NAME=/Enter/Path/To/dist:${PXR_PLUGINPATH_NAME}
// Windows
set PXR_PLUGINPATH_NAME=/Enter/Path/To/dist;%PXR_PLUGINPATH_NAME%
If you used the helper bash script, it is already done for us.

Step 4: Run your Usd (capable) application.

Yes, that's right! It was that easy. (Puts on sunglass, ah yeeaah! 😎)

If you run Houdini and then create a primitive, you can now choose the ComplexPrim as well as assign the ParamAPI API schema.

"test"

Or if you want to test it in Python:

from pxr import Usd, Sdf
### High Level ###
stage = Usd.Stage.CreateInMemory()
prim_path = Sdf.Path("/myCoolCustomPrim")
prim = stage.DefinePrim(prim_path, "ComplexPrim")
prim.AddAppliedSchema("ParamsAPI") # Returns: True
# AddAppliedSchema does not check if the schema actually exists, 
# you have to use this for codeless schemas.
### Low Level ###
from pxr import Sdf
layer = Sdf.Layer.CreateAnonymous()
prim_path = Sdf.Path("/myCoolCustomPrim")
prim_spec = Sdf.CreatePrimInLayer(layer, prim_path)
prim_spec.typeName = "ComplexPrim"
schemas = Sdf.TokenListOp.Create(
    prependedItems=["ParamsAPI"]
)
prim_spec.SetInfo("apiSchemas", schemas)

Compiled Schema

Compiled schemas allow us to generate schemas with any C++/Python bindings. This means we'll get Schema.Get<PropertyName>/Schema.Set<PropertyName> getters and setters automatically which gives our schema a very native Usd feeling. You can then also edit the C++ files to add custom features on top to manipulate the data generated by your schema. This is how many of the schemas that ship with USD do it.

usdGenSchema on Windows

Currently these instructions are only tested for Linux. We might add Windows support in the near future. (We use CMake, so in theory it should be possible to run the same steps in Windows too.)

Let's get started step by step for our example schema.

We also ship with a build.sh for running all the below steps in one go. Make sure you first run the setup.sh as described in the overview section and then navigate to the compiledSchema folder.

cd .../VFX-UsdSurvivalGuide/files/plugins/schemas/compiledSchema
chmod +x build.sh # Add execute rights
source ./build.sh # Run usdGenSchema and source the env vars for the plugin path

This will completely rebuild all directories and set the correct environment variables. You can then go straight to the last step to try it out.

Step 1: Run usdGenSchema

First we need to generate the schema.

Make sure that you first sourced you Houdini environment by running $HFS/houdini_setup so that it can find all the correct libraries and python interpreter.

usdGenSchema on Windows

On Windows you can also run hython usdGenSchema schema.usda dst to avoid having to source the env yourself.

Then run the following

cd /path/to/your/schema # In our case: ../VFX-UsdSurvivalGuide/files/plugins/schemas/compiledSchema
rm -R src
usdGenSchema schema.usda src

Currently usdGenSchema fails to generate the following files:

  • module.cpp
  • moduleDeps.cpp
  • __init__.py

We needs these for the Python bindings to work, so we supplied them in the VFX-UsdSurvivalGuide/files/plugins/schemas/compiledSchema/auxiliary folder of this repo. Simply copy them into the src folder after running usdGenSchema.

It does automatically detect the boost namespace, so the generated files will automatically work with Houdini's hboost namespace.

Important

If you adjust your own schemas, you will have edit the following in these files:

  • module.cpp: Per user define schema you need to add a line consisting of TF_WRAP(<SchemaClassName>);
  • moduleDeps.cpp: If you add C++ methods, you will need to declare any dependencies what your schemas have. This file also contains the namespace for C++/Python where the class modules will be accessible. We change RegisterLibrary(TfToken("usdSchemaExamples"), TfToken("pxr.UsdSchemaExamples"), reqs); to RegisterLibrary(TfToken("usdSchemaExamples"), TfToken("UsdSchemaExamples"), reqs); as we don't want to inject into the default pxr namespace for this demo.

Step 2: Compile schema

Next up we need to compile the schema. You can check out our asset resolver guide for more info on system requirements. In short you'll need a recent version of:

  • gcc (compiler)
  • cmake (build tool).

To compile, we first need to adjust our CMakeLists.txt file.

USD actually ships with a CMakeLists.txt file in the examples section. It uses some nice USD CMake convenience functions generate the make files.

We are not going to use that one though. Why? Since we are building against Houdini and to make things more explicit, we prefer showing how to explicitly define all headers/libraries ourselves. For that we provide the CMakeLists.txt file here.

Then run the following

# Clear build & install dirs
rm -R build
rm -R dist
# Build
cmake . -B build
cmake --build build --clean-first # make clean all
cmake --install build             # make install

Here is the content of the CMakeLists.txt file. We might make a CMake intro later, as it is pretty straight forward to setup once you know the basics.

CMakeLists.txt | Click to expand content

### Configuration ###
# Here we declare some custom variables that configure namings
set(USDSG_PROJECT_NAME UsdExamplesSchemas)

set(USDSG_EXAMPLESCHEMAS_USD_PLUGIN_NAME usdExampleSchemas)
set(USDSG_EXAMPLESCHEMAS_USD_CXX_CLASS_NAME UsdExampleSchemas)
set(USDSG_EXAMPLESCHEMAS_USD_PYTHON_MODULE_NAME UsdExampleSchemas)
set(USDSG_EXAMPLESCHEMAS_TARGET_LIB usdExampleSchemas)
set(USDSG_EXAMPLESCHEMAS_TARGET_PYTHON _${USDSG_EXAMPLESCHEMAS_TARGET_LIB})

# Arch
set(USDSG_ARCH_LIB_SUFFIX so)
# Houdini
set(USDSG_HOUDINI_ROOT $ENV{HFS})
set(USDSG_HOUDINI_LIB_DIR ${USDSG_HOUDINI_ROOT}/dsolib)
set(USDSG_HOUDINI_INCLUDE_DIR ${USDSG_HOUDINI_ROOT}/toolkit/include)
# Usd
set(USDSG_PXR_LIB_DIR ${USDSG_HOUDINI_ROOT}/dsolib)
set(USDSG_PXR_LIB_PREFIX "pxr_")
set(USDSG_PXR_INCLUDE_DIR ${USDSG_HOUDINI_INCLUDE_DIR})
# Python
set(USDSG_PYTHON_LIB_DIR ${USDSG_HOUDINI_ROOT}/python/lib)
set(USDSG_PYTHON_LIB python3.9)
set(USDSG_PYTHON_LIB_NUMBER python39)
set(USDSG_PYTHON_LIB_SITEPACKAGES ${USDSG_PYTHON_LIB_DIR}/${USDSG_PYTHON_LIB}/site-packages)
set(USDSG_PYTHON_INCLUDE_DIR ${USDSG_HOUDINI_INCLUDE_DIR}/${USDSG_PYTHON_LIB})
# Boost
set(USDSG_BOOST_NAMESPACE hboost)
set(USDSG_BOOST_INCLUDE_DIR "${USDSG_HOUDINI_INCLUDE_DIR}/${USDSG_BOOST_NAMESPACE}")
set(USDSG_BOOST_PYTHON_LIB ${USDSG_BOOST_NAMESPACE}_${USDSG_PYTHON_LIB_NUMBER})
# usdGenSchema plugInfo.json vars
set(PLUG_INFO_ROOT ".")
set(PLUG_INFO_LIBRARY_PATH "../lib/${USDSG_EXAMPLESCHEMAS_TARGET_LIB}.${USDSG_ARCH_LIB_SUFFIX}")
set(PLUG_INFO_RESOURCE_PATH ".")

### Init ###
cmake_minimum_required(VERSION 3.14 FATAL_ERROR)
project(${USDSG_PROJECT_NAME} VERSION 1.0.0 LANGUAGES CXX)

### CPP Settings ###
set(BUILD_SHARED_LIBS ON)
# Preprocessor Defines (Same as #define)
add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 HBOOST_ALL_NO_LIB BOOST_ALL_NO_LIB)
# This is the same as set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHBOOST_ALL_NO_LIB -D_GLIBCXX_USE_CXX11_ABI=0")
# Compiler Options
add_compile_options(-fPIC -Wno-deprecated -Wno-deprecated-declarations -Wno-changes-meaning) # -Zc:inline-

### Packages ### (Settings for all targets)
# Houdini
link_directories(${USDSG_HOUDINI_LIB_DIR})
# Usd (Already provided via Houdini)
# link_directories(${USDSG_PXR_LIB_DIR})
# Python (Already provided via Houdini)
# link_directories(${USDSG_PYTHON_LIB_DIR})

### CPP Settings ###
SET(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/dist" CACHE PATH "Default install dir " FORCE)

### Targets ###
## Target library > usdSchemaExamples ##
add_library(${USDSG_EXAMPLESCHEMAS_TARGET_LIB}
    SHARED
        src/complex.cpp
        src/paramsAPI.cpp
        src/simple.cpp
        src/tokens.cpp
)
# Libs
target_link_libraries(${USDSG_EXAMPLESCHEMAS_TARGET_LIB}
    ${USDSG_PXR_LIB_PREFIX}tf
    ${USDSG_PXR_LIB_PREFIX}vt
    ${USDSG_PXR_LIB_PREFIX}usd
    ${USDSG_PXR_LIB_PREFIX}sdf
)
# Headers
target_include_directories(${USDSG_EXAMPLESCHEMAS_TARGET_LIB}
    PUBLIC
    ${USDSG_BOOST_INCLUDE_DIR}
    ${USDSG_PYTHON_INCLUDE_DIR}
    ${USDSG_PXR_INCLUDE_DIR}
)
# Props
# Remove default "lib" prefix
set_target_properties(${USDSG_EXAMPLESCHEMAS_TARGET_LIB} PROPERTIES PREFIX "")
# Preprocessor Defines (Same as #define)
target_compile_definitions(${USDSG_EXAMPLESCHEMAS_TARGET_LIB}
    PRIVATE
        # USD Plugin Internal Namings
        MFB_PACKAGE_NAME=${USDSG_EXAMPLESCHEMAS_USD_PLUGIN_NAME}
)
# Install
configure_file(src/plugInfo.json plugInfo.json)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json DESTINATION resources)
install(FILES src/generatedSchema.usda DESTINATION resources)
install(TARGETS ${USDSG_EXAMPLESCHEMAS_TARGET_LIB} RUNTIME DESTINATION lib)

## Target library > usdSchemaExamples Python ##
add_library(${USDSG_EXAMPLESCHEMAS_TARGET_PYTHON}
    SHARED
        src/wrapComplex.cpp
        src/wrapParamsAPI.cpp
        src/wrapSimple.cpp
        src/wrapTokens.cpp
        src/module.cpp
        src/moduleDeps.cpp
)
# Libs
target_link_libraries(${USDSG_EXAMPLESCHEMAS_TARGET_PYTHON}
    ${USDSG_EXAMPLESCHEMAS_TARGET_LIB}
    ${USDSG_BOOST_PYTHON_LIB}
)
# Headers
target_include_directories(${USDSG_EXAMPLESCHEMAS_TARGET_PYTHON}
    PUBLIC
        ${USDSG_BOOST_INCLUDE_DIR}
        ${USDSG_PYTHON_INCLUDE_DIR}
        ${USDSG_PXR_INCLUDE_DIR}
)
# Props
# Remove default "lib" prefix
set_target_properties(${USDSG_EXAMPLESCHEMAS_TARGET_PYTHON} PROPERTIES PREFIX "")
# Preprocessor Defines (Same as #define)
target_compile_definitions(${USDSG_EXAMPLESCHEMAS_TARGET_PYTHON}
    PRIVATE
        # USD Plugin Internal Namings
        MFB_PACKAGE_NAME=${USDSG_EXAMPLESCHEMAS_USD_PLUGIN_NAME}
        MFB_PACKAGE_MODULE=${USDSG_EXAMPLESCHEMAS_USD_PYTHON_MODULE_NAME}
)
# Install
install(FILES src/__init__.py DESTINATION lib/python/${USDSG_EXAMPLESCHEMAS_USD_PYTHON_MODULE_NAME})
install(
    TARGETS ${USDSG_EXAMPLESCHEMAS_TARGET_PYTHON}
    DESTINATION lib/python/${USDSG_EXAMPLESCHEMAS_USD_PYTHON_MODULE_NAME}
)

# Status
message(STATUS "--- Usd Example Schemas Instructions Start ---")
message(NOTICE "To use the compiled files, set the following environment variables:")
message(NOTICE "export PYTHONPATH=${CMAKE_INSTALL_PREFIX}/lib/python:${USDSG_PYTHON_LIB_SITEPACKAGES}:$PYTHONPATH")
message(NOTICE "export PXR_PLUGINPATH_NAME=${CMAKE_INSTALL_PREFIX}/resources:$PXR_PLUGINPATH_NAME")
message(NOTICE "export LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib:${HFS}/python/lib:${HFS}/dsolib:$LD_LIBRARY_PATH")
message(STATUS "--- Usd Example Schemas Instructions End ---\n")

Step 3: Update environment variables.

Next we need to update our environment variables. The cmake output log actually has a message that shows what to set:

  • PXR_PLUGINPATH_NAME: The USD plugin search path variable.
  • PYTHONPATH: This is the standard Python search path variable.
  • LD_LIBRARY_PATH: This is the search path variable for how .so files are found on Linux.
// Linux
export PYTHONPATH=..../VFX-UsdSurvivalGuide/files/plugins/schemas/compiledSchema/dist/lib/python:/opt/hfs19.5/python/lib/python3.9/site-packages:$PYTHONPATH
export PXR_PLUGINPATH_NAME=.../VFX-UsdSurvivalGuide/files/plugins/schemas/compiledSchema/dist/resources:$PXR_PLUGINPATH_NAME
export LD_LIBRARY_PATH=.../VFX-UsdSurvivalGuide/files/plugins/schemas/compiledSchema/dist/lib:/python/lib:/dsolib:$LD_LIBRARY_PATH
// Windows
set PYTHONPATH=..../VFX-UsdSurvivalGuide/files/plugins/schemas/compiledSchema/dist/lib/python;/opt/hfs19.5/python/lib/python3.9/site-packages;%PYTHON_PATH%
set PXR_PLUGINPATH_NAME=.../VFX-UsdSurvivalGuide/files/plugins/schemas/compiledSchema/dist/resources;%PXR_PLUGINPATH_NAME%

For Windows, specifying the linked .dll search path is different. We'll add more info in the future.

Step 4: Run your Usd (capable) application.

If we now run Houdini and then create a primitive, you can now choose the ComplexPrim as well as assign the ParamAPI API schema.

""

Or if you want to test it in Python:

from pxr import Usd, Sdf
### High Level ###
stage = Usd.Stage.CreateInMemory()
prim_path = Sdf.Path("/myCoolCustomPrim")
prim = stage.DefinePrim(prim_path, "ComplexPrim")
prim.AddAppliedSchema("ParamsAPI") # Returns: True
# AddAppliedSchema does not check if the schema actually exists, 
# you have to use this for codeless schemas.
### Low Level ###
from pxr import Sdf
layer = Sdf.Layer.CreateAnonymous()
prim_path = Sdf.Path("/myCoolCustomPrim")
prim_spec = Sdf.CreatePrimInLayer(layer, prim_path)
prim_spec.typeName = "ComplexPrim"
schemas = Sdf.TokenListOp.Create(
    prependedItems=["ParamsAPI"]
)
prim_spec.SetInfo("apiSchemas", schemas)

### Python Classes ###
stage = Usd.Stage.CreateInMemory()
prim = stage.GetPrimAtPath("/prim")
print(prim.GetTypeName())
print(prim.GetPrimTypeInfo().GetSchemaType().pythonClass)

# Schema Classes
import UsdExampleSchemas as schemas
print(schemas.Complex)
print(schemas.ParamsAPI)
print(schemas.Simple)
print(schemas.Tokens)
# Schema Get/Set/Create methods
schemas.Complex.CreateIntAttrAttr()

As you can see we now get our nice Create<PropertyName>/Get<PropertyName>/Set<PropertyName> methods as well as full Python exposure to our C++ classes.