diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..56bf69e
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,106 @@
+---
+Language: Cpp
+
+BasedOnStyle: WebKit
+AlignAfterOpenBracket: AlwaysBreak
+AlignConsecutiveMacros: 'true'
+AlignConsecutiveDeclarations: 'true'
+AlignEscapedNewlines: Left
+AlignTrailingComments: 'true'
+AllowAllConstructorInitializersOnNextLine: 'false'
+AllowAllParametersOfDeclarationOnNextLine: 'false'
+AllowShortCaseLabelsOnASingleLine: 'true'
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: 'false'
+AlwaysBreakTemplateDeclarations: MultiLine
+BinPackArguments: 'false'
+BinPackParameters: 'false'
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterClass: 'true'
+ AfterEnum: 'true'
+ AfterFunction: 'true'
+ AfterStruct: 'true'
+ AfterUnion: 'true'
+BreakBeforeTernaryOperators: 'true'
+BreakConstructorInitializers: BeforeComma
+BreakInheritanceList: BeforeComma
+ColumnLimit: '100'
+CommentPragmas: '^\\.+'
+FixNamespaceComments: 'true'
+IncludeBlocks: Regroup
+IncludeCategories:
+
+ # Desired final ordering:
+ # 0. Glew must be included before any other GL header
+ # 1. Related header
+ # 2. All private headers
+ # 3/4/5. All public headers from this repository
+ # 6. UsdUfe headers
+ # 7. Pixar + USD headers
+ # 8. Autodesk + Qt headers
+ # 9. Other libraries' headers
+ # 10. C++ standard library headers
+ # 11. C system headers
+ # 12. Conditional includes
+
+ # 0. GL loaders must be included before any other GL header
+ # Negative priority puts it above the default IncludeIsMainRegex
+ - Regex: ''
+ Priority: -1
+
+ # 1. Related header
+ # Handled by the default IncludeIsMainRegex regex, and auto-assigned
+ # Priority 0
+
+ # 2. All private headers
+ - Regex: '^"'
+ Priority: 2
+
+ # 3. Headers from projects this repository that have dependencies on headers of 4 and/or 5
+ - Regex: '^<(MaxUsdObjects)/'
+ Priority: 3
+
+ # 4. Headers from projects this repository that have dependencies on headers of 4
+ - Regex: '^<(RenderDelegate|UFEUI|USDExport|USDImport)/'
+ Priority: 4
+
+ # 5. MaxUsd Headers
+ - Regex: '^<(MaxUsd)/'
+ Priority: 5
+
+ # 6. UsdUfe library headers
+ - Regex: '^$'
+ Priority: 10
+
+ # 11. C system headers
+ # angle brackets, no directory, end with ".h"
+ - Regex: '^<[A-Za-z0-9_-]+\.h>$'
+ Priority: 11
+
+ # 12. Conditional includes
+ # Not reordered by clang-format, we need to manually make sure these come last
+
+MaxEmptyLinesToKeep: '1'
+NamespaceIndentation: None
+UseTab: Never
+
+...
diff --git a/.clang-format-ignore b/.clang-format-ignore
new file mode 100644
index 0000000..e69de29
diff --git a/.clang-format-include b/.clang-format-include
new file mode 100644
index 0000000..aad3c80
--- /dev/null
+++ b/.clang-format-include
@@ -0,0 +1,8 @@
+\.c$
+\.cc$
+\.cpp$
+\.cxx$
+\.h$
+\.hh$
+\.hpp$
+\.hxx$
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1f1ad55
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+*.png filter=lfs diff=lfs merge=lfs -text
+*.max filter=lfs diff=lfs merge=lfs -text
+*.mui filter=lfs diff=lfs merge=lfs -text
+*.rc text working-tree-encoding=UTF-16LE-BOM eol=CRLF
+*.r text working-tree-encoding=UTF-16LE-BOM eol=CRLF
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.usdc filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..8d64b98
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,34 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: santosg87
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Steps to reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Attachments**
+If applicable, add screenshots, sample files, etc to help explain your problem.
+
+**Specs (if applicable):**
+ - OS & version [e.g. Windows 10]
+ - Compiler & version [e.g. MSVS VS 2029 (16.11.34)]
+ - 3ds Max version [e.g. 3ds Max 2025]
+ - 3ds USD commit SHA [e.g. dev at caa921c1]
+ - Pixar USD commit SHA [e.g. dev at b85ddac2]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/build-issue-report.md b/.github/ISSUE_TEMPLATE/build-issue-report.md
new file mode 100644
index 0000000..e7438ae
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/build-issue-report.md
@@ -0,0 +1,24 @@
+---
+name: Build issue report
+about: Before opening a new build issue, please review doc/build.md
+title: ''
+labels: help wanted
+assignees: ''
+
+---
+
+**Describe the issue**
+A description of what the issue is.
+
+**Build log**
+Please attach a build_log.txt
+
+**Specs:**
+ - OS & version [e.g. Windows 10]
+ - Compiler & version [e.g. MSVS VS 2029 (16.11.34)]
+ - 3ds Max version [e.g. 3ds Max 2025]
+ - 3ds USD commit SHA [e.g. dev at caa921c1]
+ - Pixar USD commit SHA [e.g. dev at b85ddac2]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..a27bf13
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you would like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you have considered**
+A clear and concise description of any alternative solutions or features you have considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/run-clang-format.py b/.github/run-clang-format.py
new file mode 100644
index 0000000..eb43c71
--- /dev/null
+++ b/.github/run-clang-format.py
@@ -0,0 +1,234 @@
+#!/usr/bin/env python
+
+'''Run clang-format on files in this repository
+
+By default, runs on all files, but may pass specific files.
+'''
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import argparse
+import inspect
+import fnmatch
+import io
+import os
+import re
+import stat
+import sys
+import platform
+import time
+
+from subprocess import check_call, check_output
+
+if sys.version_info[0] < 3:
+ # Python-2 check_output doesn't have encoding
+ def check_output(*args, **kwargs):
+ import subprocess
+ kwargs.pop('encoding')
+ return subprocess.check_output(*args, **kwargs)
+
+
+THIS_FILE = os.path.normpath(os.path.abspath(inspect.getsourcefile(lambda: None)))
+THIS_DIR = os.path.dirname(THIS_FILE)
+# THIS_DIR = REPO_ROOT/.github
+REPO_ROOT = os.path.dirname(THIS_DIR)
+
+UPDATE_INTERVAL = .2
+
+
+_last_update_len = 0
+_on_update_line = False
+
+
+def update_status(text):
+ global _last_update_len
+ global _on_update_line
+ sys.stdout.write('\r')
+ text_len = len(text)
+ extra_chars = _last_update_len - text_len
+ if extra_chars > 0:
+ text += (' ' * extra_chars)
+ sys.stdout.write(text)
+ _last_update_len = text_len
+ sys.stdout.flush()
+ _on_update_line = True
+
+
+def post_update_print(text):
+ global _on_update_line
+ if _on_update_line:
+ print()
+ print(text)
+ _on_update_line = False
+
+
+def regex_from_file(path, glob=False):
+ with io.open(path, 'r') as f:
+ patterns = f.read().splitlines()
+ # ignore comment lines
+ patterns = [x for x in patterns if x.strip() and not x.lstrip().startswith('#')]
+ if glob:
+ patterns = [fnmatch.translate(x) for x in patterns]
+ regex = '({})'.format('|'.join(patterns))
+ return re.compile(regex)
+
+if platform.system() == "Windows" and sys.version_info >= (3, 6):
+ import pathlib # Python 3.6 is required for pathlib.Path
+ def canonicalpath(path):
+ path = os.path.abspath(os.path.realpath(os.path.normpath(os.path.normcase(path))))
+ realpath = str(pathlib.Path(path).resolve()) # To get a properly cased path ie: from r'C:\WiNdOwS\SyStEm32\DeSkToP.iNi' get r'C:\Windows\System32\desktop.ini'
+ if len(path) == len(realpath): # This is to avoid following symbolic links, there is still the possibility that they could be equal.
+ path = realpath
+ if os.path.isabs(path) and path[0].upper() != path[0]:
+ path = path[0].upper() +path[1:] # path.capitalize()
+ path = path.replace('\\', '/')
+ return path
+else:
+ def canonicalpath(path):
+ path = os.path.abspath(os.path.realpath(os.path.normpath(os.path.normcase(path))))
+ return path.replace('\\', '/')
+
+def run_clang_format(paths=(), verbose=False, commit=None):
+ """Runs clang-format in-place on repo files
+
+ Returns
+ -------
+ List[str]
+ Files altered by clang-format
+ """
+ if not paths and not commit:
+ paths = [REPO_ROOT]
+
+ if commit:
+ check_call(['git', 'checkout', commit], cwd=REPO_ROOT)
+ text = check_output(
+ ['git', 'diff-tree', '--no-commit-id', '--name-only', '-r',
+ commit], cwd=REPO_ROOT, encoding=sys.stdout.encoding)
+ commit_paths = text.splitlines()
+ paths.extend(os.path.join(REPO_ROOT, p) for p in commit_paths)
+
+ files = set()
+ folders = set()
+
+ include_path = os.path.join(REPO_ROOT, '.clang-format-include')
+ include_regex = regex_from_file(include_path)
+
+ ignore_path = os.path.join(REPO_ROOT, '.clang-format-ignore')
+ ignore_regex = regex_from_file(ignore_path, glob=True)
+
+ # tried to parse .gitignore with regex_from_file, but it has
+ # too much special git syntax. Instead just using `git ls-files`
+ # as a filter...
+ git_files = check_output(['git', 'ls-files'], cwd=REPO_ROOT,
+ encoding=sys.stdout.encoding)
+ git_files = set(canonicalpath(x.strip()) for x in git_files.splitlines())
+
+ def print_path(p):
+ if p.startswith(REPO_ROOT):
+ p = os.path.relpath(p, REPO_ROOT)
+ return p
+
+ def passes_filter(path):
+ rel_path = os.path.relpath(path, REPO_ROOT)
+ match_path = os.path.join('.', rel_path)
+ # standardize on '/', because that's what's used in files,
+ # and output by `git ls-files`
+ match_path = match_path.replace('\\', '/')
+ if not include_regex.search(match_path):
+ return False
+ if ignore_regex.pattern != '()' and ignore_regex.search(match_path):
+ return False
+ return True
+
+ # parse the initial fed-in paths, which may be files or folders
+ for path in paths:
+ # Get the stat, so we only do one filesystem call, instead of
+ # two for os.path.isfile() + os.path.isdir()
+ try:
+ st_mode = os.stat(path).st_mode
+ if stat.S_ISDIR(st_mode):
+ folders.add(path)
+ elif stat.S_ISREG(st_mode):
+ if canonicalpath(path) in git_files:
+ files.add(path)
+ except Exception:
+ print("Given path did not exist: {}".format(path))
+ raise
+
+ for folder in folders:
+ # we already have list of potential files in git_files...
+ # filter to ones in this folder
+ folder = canonicalpath(folder) + '/'
+ files.update(x for x in git_files if x.startswith(folder))
+
+ # We apply filters even to fed-in paths... this is to aid
+ # in use of globs on command line
+ files = sorted(x for x in files if passes_filter(x))
+
+ clang_format_executable = os.environ.get('CLANG_FORMAT_EXECUTABLE',
+ 'clang-format')
+ if verbose:
+ print("Running clang-format on {} files...".format(len(files)))
+ last_update = time.time()
+
+ def print_path(p):
+ if p.startswith(REPO_ROOT):
+ p = os.path.relpath(p, REPO_ROOT)
+ return p
+
+ altered = []
+ for i, path in enumerate(files):
+ if verbose:
+ now = time.time()
+ if now - last_update > UPDATE_INTERVAL:
+ last_update = now
+ update_status('File {}/{} ({:.1f}%) - {}'.format(
+ i + 1, len(files), (i + 1) / len(files) * 100,
+ print_path(path)))
+ # Note - couldn't find a way to get clang-format to return whether
+ # or not a file was altered with '-i' - so checking ourselves
+ # Checking mtime is not foolproof, but is faster than reading file
+ # and comparing, and probably good enough
+ mtime_orig = os.path.getmtime(path)
+ check_call([clang_format_executable, '-i', path])
+ mtime_new = os.path.getmtime(path)
+ if mtime_new != mtime_orig:
+ post_update_print("File altered: {}".format(print_path(path)))
+ altered.append(path)
+ post_update_print("Done - altered {} files".format(len(altered)))
+ return altered
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ parser.add_argument('paths', nargs='*',
+ help='Paths to run clang-format on; defaults to all files in repo')
+ parser.add_argument('-v', '--verbose', action='store_true',
+ help='Enable more output (ie, progress messages)')
+ parser.add_argument('-c', '--commit',
+ help='Git commit / revision / branch; will first check out that commit,'
+ " then query it for it's list of affected files, to use as the files"
+ ' to run clang-format on; if PATHS are also manually given, they are'
+ ' appended')
+ return parser
+
+
+def main(raw_args=None):
+ parser = get_parser()
+ args = parser.parse_args(raw_args)
+ try:
+ altered = run_clang_format(paths=args.paths, verbose=args.verbose,
+ commit=args.commit)
+ except Exception:
+ import traceback
+ traceback.print_exc()
+ return 1
+ if altered:
+ return 1
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/.github/workflows/3dsmax-usd-new-issues.yml b/.github/workflows/3dsmax-usd-new-issues.yml
new file mode 100644
index 0000000..c70a5d5
--- /dev/null
+++ b/.github/workflows/3dsmax-usd-new-issues.yml
@@ -0,0 +1,15 @@
+name: Move Issues to Triage
+on:
+ issues:
+ types: [opened, reopened]
+
+jobs:
+ move-triage-card:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: alex-page/github-project-automation-plus@v0.9.0
+ # Feb 2024: Update v0.9.0 which uses node20 (node16 is out of support).
+ with:
+ project: Issue Triage
+ column: Needs triage
+ repo-token: ${{ secrets.REPO_ACCESS_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d2eb15c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,31 @@
+# Build and dependencies files:
+build/
+artifacts/
+package/
+src/packages
+# Ufe Runtime Icons, junctionned in from artifact. QT requirement.
+src/MaxUsdObjects/Icons/UfeRt
+
+# Development configuration files:
+src/.vs
+samples/.vs
+*.vcxproj.user
+
+# Generated files:
+*.pyc
+test-results/
+*.log
+resource.aps
+.env/
+.vscode/
+docs/out/
+.env*/
+.env
+src/**/GeneratedFiles/
+!src/**/GeneratedFiles/tfiles/*.ts
+.idea
+*_backup.max
+docs/html/
+SDK/
+src/Tests/Integration/SystemTestGup/
+devkit/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0618fb5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+![3ds Max USD](doc/images/header.png)
+# 3ds Max USD Plugin
+**3dsmax-usd** is a feature-rich plugin for 3ds Max that provides support for [OpenUSD](http://openusd.org/) as part of [AOUSD](https://aousd.org/). While the plugin provides traditional methods for import and export, the most important and exciting capability it provides is to load USD stages directly in the viewport for interactive editing without import/export.
+
+![3ds Max USD](doc/images/3dsmax-usd.png)
+
+The USD for 3ds Max extension lets you create, edit, work in, work with and collaborate on USD data, while enabling data to move between products (ie. Maya and 3ds Max). By enabling USD data to flow in and out of 3ds Max, you can take advantage of the following key benefits of USD: supporting DCC-agnostic pipelines/workflows and enabling non-linear collaboration. While not necessarily a replacement for the core 3ds Max referencing workflows (Object XRef and Scene XRef), the USD features can be used as a modern cross-DCC referencing pipeline that can enhance, and in many use-cases, replace existing referencing setups.
+
+The plugin comes with a complete API to allow extending the default import and export process and also to manipulate USD data directly via C++ and Python.
+
+
+## Features
+- Import and Export ASCII And Binary USD formats
+- Open a USD Stage directly for editing
+- USD Explorer
+- USD Cameras
+- USD Lights
+- 3ds Max Controller Support
+- MaterialX Material
+
+# Additional Information
+- [Building](#building)
+- [Coding Standards](doc/CodingGuidelines.md)
+- [Contributing](#contributions)
+- [Developer Documentation](#developer-documentation)
+- [Security](#security)
+- [Supported Versions](#versions)
+
+
+
+
+## Building
+Everything needed to build MaxUSD is provided in the form of source and a Devkit that needs to be installed. Unit tests are provided for all projects and can be optionally built and executed using google tests. Full details on how to build and test 3dsmax-usd can be found in the [build documentation](doc/build.md).
+
+## Contributions
+We welcome your contributions to enhance and extend the tool set. Please visit the [contributions](doc/CONTRIBUTING.md) page for further information.
+
+## Developer Documentation
+MaxUSD comes with extensive documentation and [samples](samples/readme.md). For more information on Maxscript exposure please reference the [online documentation](https://help.autodesk.com/view/MAXDEV/2025/ENU/?guid=MAXScript_USD_overview_html) or ask questions in the [discussion pages](https://github.com/Autodesk/3dsmax-usd/discussions). OpenUSD has a very active [forum](https://forum.aousd.org/) for specific USD questions or discussions.
+
+## Security
+We take security serious at Autodesk and the same goes for our open source contributions. Our guidelines are documented [here](SECURITY.md)
+
+## Versions
+3ds Max USD actively supports the following versions of 3ds Max.
+- 2022
+- 2023
+- 2024
+- 2025
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..4599a01
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,30 @@
+# Security
+
+### Reporting Security Issues
+The 3ds Max team and community take security bugs in 3dsmax-usd seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
+
+### Reporting a Security Vulnerability to Autodesk
+This vulnerability disclosure policy applies to any vulnerabilities you are considering reporting to Autodesk.
+We recommend reading this vulnerability disclosure policy fully before you report a vulnerability and always acting in compliance with it.
+We value those who take the time and effort to report security vulnerabilities according to this policy. However, we do not offer monetary rewards for vulnerability disclosures.
+If you believe you have found a security vulnerability relating to the Autodesk's systems or products, please submit a vulnerability report to [Autodesk HackerOne](https://hackerone.com/autodesk) program or psirt@autodesk.com. Otherwise, submit a report an incident at [Autodesk Trust Center](https://www.autodesk.com/trust/security).
+In your report please include details of:
+- The software package, website, IP or page where the vulnerability can be observed.
+- A brief description of the type of vulnerability, for example; "XSS vulnerability".
+- Steps to reproduce. These should be a benign, non-destructive, proof of concept. This helps to ensure that the report can be triaged quickly and accurately. It also reduces the likelihood of duplicate reports, or malicious exploitation of some vulnerabilities, such as sub-domain takeovers.
+
+The 3ds Max team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
+
+### Responsible Disclosure
+We appreciate the responsible disclosure of security vulnerabilities. Please allow us a reasonable amount of time to address the issue before making it public.
+
+### Supported Versions
+Only the last released version is supported.
+
+### Learning More About Security
+To learn more about Autodesk Security, please see the [Autodesk Trust Center](https://www.autodesk.com/trust/security).
+
+### Receiving Security Information From Autodesk
+Technical security information about our products and services is distributed through several channels.
+- Autodesk distributes information to customers about security vulnerabilities via https://autodesk.com and [Autodesk Trust Center](https://www.autodesk.com/trust/security).
+- Autodesk may issue release notes and security bulletins detailing security vulnerabilities, workarounds, remediations and any indicators of compromise to aid incident response teams.
\ No newline at end of file
diff --git a/additional_includes/MaxRestrictedSdk/2022/Graphics/InstanceRenderGeometry.h b/additional_includes/MaxRestrictedSdk/2022/Graphics/InstanceRenderGeometry.h
new file mode 100644
index 0000000..e7ff491
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2022/Graphics/InstanceRenderGeometry.h
@@ -0,0 +1,639 @@
+//
+// Copyright 2020 Autodesk, Inc. All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//
+//
+
+//Link with the optimesh.lib library to use this in your plugin.
+
+#pragma once
+
+#include "Graphics/IRenderGeometry.h"
+#include "Graphics/DrawContext.h"
+#include "Materials/Mtl.h"
+#include "quat.h"
+#include "Graphics/UpdateDisplayContext.h"
+#include "Graphics/UpdateNodeContext.h"
+#include "Graphics/MaterialRequiredStreams.h"
+#include "Graphics/VertexBufferHandle.h"
+#include "Graphics/IndexBufferHandle.h"
+#include "Noncopyable.h"
+#include "Graphics/MaterialConversionHelper.h"
+#include "export.h"
+
+namespace MaxSDK { namespace Graphics {
+
+///This macro converts from a color r,g,b in bytes (range 0 to 255) to a combined DWORD used internally by 3ds Max for storing Vertex Colors.
+#define INSTANCES_R8G8B8X8_DWORD(r,g,b)((DWORD)((((WORD)((BYTE)(b)) << 8)) | (((DWORD)(BYTE)(g)) << 16) | (((DWORD)(BYTE)(r)) << 24)));
+
+///Declaration for the private implementation of this class
+class InstanceRenderGeometryImpl;
+
+/** The struct InstanceData is used to pass instance data in different methods from the InstanceRenderGeometry class.
+It is used in InstanceRenderGeometry::CreateInstanceVertexBuffer to create the instance vertex buffer. This has to be called once only unless you want to reset all instance data with something else
+Or the number of instances has changed.
+It is also used in InstanceRenderGeometry::UpdateInstanceVertexBuffer to update the instance data which was changed. This is only used when there is animation on the instance data.
+Like updating the positions at different times.
+*/
+struct InstanceData : public MaxSDK::Util::Noncopyable
+{
+ /**numInstances : is the number of instances.
+ When creating the instance buffers with InstanceRenderGeometry::CreateInstanceVertexBuffer, this parameter should be non null.
+ When updating the instance buffers with InstanceRenderGeometry::UpdateInstanceVertexBuffer, this can be ignored as the number of instances should not have changed.
+ Please, keep in mind, that we currently have a limitation of 32768 instances.
+ If you want more instances than 32768, you will have to split the instances into several InstanceRenderGeometry.
+ */
+ size_t numInstances = 0;
+
+ /**If bTransformationsAreInWorldSpace is true, then all matrices/positions/orientation/scales used in that struct are in world space, so moving the node will not move the instances.
+ If bTransformationsAreInWorldSpace is false then all matrices/positions/orientation/scales are relative to the node's transfrom matrix, so moving the node will move all instances.
+ When creating the instance buffers with InstanceRenderGeometry::CreateInstanceVertexBuffer, this parameter should be set.
+ When updating the instance buffers with InstanceRenderGeometry::UpdateInstanceVertexBuffer, this parameter can be ingored as this should not change during update.
+ If so, then use InstanceRenderGeometry::CreateInstanceVertexBuffer to recreate the full buffer.
+ */
+ bool bTransformationsAreInWorldSpace = false;
+
+ /**pMatrices is an array of Matrix3 which is the transform matrix of each instance, this parameter can be null if you use the pPositions array instead but
+ either pMatrices or pPositions should be non null if you want the instances to be at different positions.
+ To set if these matrices are in world space or relative to the node's transform, please see bTransformationsAreInWorldSpace.
+ */
+ Matrix3* pMatrices = nullptr;
+
+ /**numMatrices is the number of elements in pMatrices, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numMatrices = 0;
+
+ /**pPositions is an array of Point3 which is the position of each instance, this parameter can be null if you use the pMatrices array instead at least one of those two pointers should be non null.
+ To set if these positions are in world space or relative to the node's transform, please see bTransformationsAreInWorldSpace.
+ Internally in the vertex buffer, this data will be converted to a Matrix3 with pos/orientation/scale combined when put in the instance vertex buffer.
+ When updating the instance buffers with InstanceRenderGeometry::UpdateInstanceVertexBuffer, if you are not using pMatrices, but rather position/orientation/scale,
+ always provide the 3 of them (position/orientation/scale) as any missing of those will be replaced by identity or null (for position)
+ as we are combining the 3 into a matrix. Even if only one of them is animated.
+ */
+ Point3* pPositions = nullptr;
+
+ /**numPositions is the number of elements in pPositions, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numPositions = 0;
+
+ /**pOrientationsAsPoint4 is an array of Point4 which are quaternions. This parameter can be null if the orientation of instances is not overriden, it will be an identity orientation in that case.
+ If you prefer to use the Quat class to provide orientations, please see pOrientationsAsQuat.
+ To set if these orientations are in world space or relative to the node's transform, please see bTransformationsAreInWorldSpace.
+ Internally in the vertex buffer, this data will be converted to a Matrix3 with pos/rot/scale combined when put in the instance vertex buffer.
+ When updating the instance buffers with InstanceRenderGeometry::UpdateInstanceVertexBuffer, if you are not using pMatrices, but rather position/orientation/scale,
+ always provide the 3 of them (position/orientation/scale) as any missing of those will be replaced by identity or null (for position)
+ as we are combining the 3 into a matrix. Even if only one of them is animated.
+ */
+ Point4* pOrientationsAsPoint4 = nullptr;
+
+ /**numOrientationsAsPoint4 is the number of elements in pOrientationsAsPoint4, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numOrientationsAsPoint4 = 0;
+
+
+ /**pOrientationsAsQuat is an array of Quat which are quaternions. This parameter can be null if the orientation of instances is not overriden, it will be an identity orientation in that case.
+ If you prefer to use the Point4 class to provide orientations instead of Quat, please see pOrientationsAsPoint4.
+ To set if these orientations are in world space or relative to the node's transform, please see bTransformationsAreInWorldSpace.
+ Internally in the vertex buffer, this data will be converted to a Matrix3 with pos/rot/scale combined when put in the instance vertex buffer.
+ When updating the instance buffers with InstanceRenderGeometry::UpdateInstanceVertexBuffer, if you are not using pMatrices, but rather position/orientation/scale,
+ always provide the 3 of them (position/orientation/scale) as any missing of those will be replaced by identity or null (for position)
+ as we are combining the 3 into a matrix. Even if only one of them is animated.
+ */
+ Quat* pOrientationsAsQuat = nullptr;
+
+ /**numOrientationsAsQuat is the number of elements in pOrientationsAsQuat, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numOrientationsAsQuat = 0;
+
+ /**pScales is an array of Point3 which is the scale of each instance. This parameter can be null if the scale of instances is not overriden, it will be an identity scale (1,1,1) used in that case.
+ To set if these scales are in world space or relative to the node's transform, please see bTransformationsAreInWorldSpace.
+ Internally in the vertex buffer, this data will be converted to a Matrix3 with pos/rot/scale combined when put in the instance vertex buffer.
+ When updating the instance buffers with InstanceRenderGeometry::UpdateInstanceVertexBuffer, if you are not using pMatrices, but rather position/orientation/scale,
+ always provide the 3 of them (position/orientation/scale) as any missing of those will be replaced by identity or null (for position)
+ as we are combining the 3 into a matrix. Even if only one of them is animated.
+ */
+ Point3* pScales = nullptr;
+
+ /**numScales is the number of elements in pScales, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numScales = 0;
+
+ /**pViewportMaterials is an array of BaseMaterialHandle which is the material of each instance. This parameter can be null if you want to use the original material from the node.
+ If you need to convert from a 3ds Max Mtl class to a BaseMaterialHandle, please see MaxSDK::Graphics::MaterialConversionHelper::ConvertMaxToNitrousMaterial
+ Warning : if you need to update the materials on instances such as remove/add some materials or change which material is applied to an instance, call CreateInstanceVertexBuffer instead of
+ UpdateInstanceVertexBuffer so we recreate the instance vertex buffer from scratch as we store the instance vertex buffer data per material which involves a reordering of the vertex buffer data.
+ */
+ BaseMaterialHandle* pViewportMaterials = nullptr;
+
+ /**materialStyle is used only when you set pViewportMaterials pointer. When compiling the materials, you can set which type of quality you want for the viewport shader.
+ An example is : MaterialConversionHelper::MaterialStyles::MaterialStyle_Simple is for a viewport in standard quality.
+ And MaterialConversionHelper::MaterialStyles::MaterialStyle_Realistic is for a viewport in high quality.
+ //This parameter is optional and by default is set to MaterialConversionHelper::MaterialStyles::MaterialStyle_Simple
+ */
+ MaterialConversionHelper::MaterialStyles materialStyle = MaterialConversionHelper::MaterialStyles::MaterialStyle_Simple;
+
+ /**numViewportMaterials is the number of elements in pViewportMaterials, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numViewportMaterials = 0;
+
+ /**pUVWMapChannel1 is an array of Point3 which is the UVW value of each instance in map channel 1, we override the UVWs from the whole geometry with a single UVW value per
+ instance on that map channel.
+ This parameter can be null if the UVWs in map channel 1 of each instance are not overriden.
+ */
+ Point3* pUVWMapChannel1 = nullptr;
+
+ /**numUVWMapChannel1 is the number of elements in pUVWMapChannel1, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numUVWMapChannel1 = 0;
+
+ /**pUVWMapChannel2 is an array of Point3 which is the UVW value of each instance in map channel 2, we override the UVWs from the whole geometry with a single UVW value per
+ instance on that map channel.
+ This parameter can be null if the UVWs in the map channel 2 of each instance are not overriden.
+ */
+ Point3* pUVWMapChannel2 = nullptr;
+
+ /**numUVWMapChannel2 is the number of elements in pUVWMapChannel2, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numUVWMapChannel2 = 0;
+
+ /**pUVWMapChannel3 is an array of Point3 which is the UVW value of each instance in map channel 3, we override the UVWs from the whole geometry with a single UVW value per
+ instance on that map channel.
+ This parameter can be null if the UVWs in the map channel 3 of each instance are not overriden.
+ */
+ Point3* pUVWMapChannel3 = nullptr;
+
+ /**numUVWMapChannel3 is the number of elements in pUVWMapChannel3, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numUVWMapChannel3 = 0;
+
+ /**pUVWMapChannel4 is an array of Point3 which is the UVW value of each instance in map channel 4, we override the UVWs from the whole geometry with a single UVW value per
+ instance on that map channel.
+ This parameter can be null if the UVWs in the map channel 4 of each instance are not overriden.
+ */
+ Point3* pUVWMapChannel4 = nullptr;
+
+ /**numUVWMapChannel4 is the number of elements in pUVWMapChannel4, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numUVWMapChannel4 = 0;
+
+ /**pUVWMapChannel5 is an array of Point3 which is the UVW value of each instance in map channel 5, we override the UVWs from the whole geometry with a single UVW value per
+ instance on that map channel.
+ This parameter can be null if the UVWs in the map channel 5 of each instance are not overriden.
+ */
+ Point3* pUVWMapChannel5 = nullptr;
+
+ /**numUVWMapChannel5 is the number of elements in pUVWMapChannel5, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numUVWMapChannel5 = 0;
+
+ /**pUVWMapChannel6 is an array of Point3 which is the UVW value of each instance in map channel 6, we override the UVWs from the whole geometry with a single UVW value per
+ instance on that map channel.
+ This parameter can be null if the UVWs in the map channel 6 of each instance are not overriden.
+ */
+ Point3* pUVWMapChannel6 = nullptr;
+
+ /**numUVWMapChannel6 is the number of elements in pUVWMapChannel6, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numUVWMapChannel6 = 0;
+
+ /**pUVWMapChannel7 is an array of Point3 which is the UVW value of each instance in map channel 7, we override the UVWs from the whole geometry with a single UVW value per
+ instance on that map channel.
+ This parameter can be null if the UVWs in the map channel 7 of each instance are not overriden.
+ */
+ Point3* pUVWMapChannel7 = nullptr;
+
+ /**numUVWMapChannel7 is the number of elements in pUVWMapChannel7, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numUVWMapChannel7 = 0;
+
+ /**pUVWMapChannel8 is an array of Point3 which is the UVW value of each instance in map channel 8, we override the UVWs from the whole geometry with a single UVW value per
+ instance on that map channel.
+ This parameter can be null if the UVWs in the map channel 8 of each instance are not overriden.
+ */
+ Point3* pUVWMapChannel8 = nullptr;
+
+ /**numUVWMapChannel8 is the number of elements in pUVWMapChannel8, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numUVWMapChannel8 = 0;
+
+ /**pColors is an array of AColor which is the rgba color of each instance. This will result in the viewport as a flat shading color applied on each instance (no lighting effect added).
+ AColor has r,g,b,a components usually with values from 0.0 to 1.0, though we don't do any check on this.
+ You can set a non zero alpha value to use transparency.
+ This parameter can be null if you don't want to use a color per instance.
+ Using a non-null pColors value with a non-null pViewportMaterials or pMaxMaterials is not advised as we will use only either the material or the colors but not both.
+ */
+ AColor* pColors = nullptr;
+
+ /**numColors is the number of elements in pColors, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numColors = 0;
+
+ /**pVertexColorsAsColor is an array of Color, i.e : an rgb color with no alpha. With r,g,b usually with values from 0.0 to 1.0. There is no alpha on vertex colors.
+ We override the Vertex Colors from the whole geometry with a single Vertex Color value per instance.
+ At this time, there is a limitation : Vertex Colors override will work only on map channels which have not been overriden when you show both in the viewport.
+ This parameter can be null if the Vertex Colors of each instance are not overriden.
+ This data will be converted when written in the vertex buffer to DWORD R8G8B8X8 color as this is what 3ds Max uses internally for Vertex Colors in vertex buffers.
+ If you prefer to use directly DWORD for Vertex Colors as it's faster, please see pVertexColorsAsDWORD.
+ */
+ Color* pVertexColorsAsColor = nullptr;
+
+ /**numVertexColorsAsColor is the number of elements in pVertexColorsAsColor, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numVertexColorsAsColor = 0;
+
+ /**pVertexColorsAsDWORD is an array of 32 bits DWORD R8G8B8X8, i.e : an rgb color with no alpha. With r,g,b usually with values from 0.0 to 1.0. There is no alpha on vertex colors.
+ We override the Vertex Colors from the whole geometry with a single Vertex Color value per instance.
+ At this time, there is a limitation : Vertex Colors override will work only on map channels which have not been overriden when you show both in the viewport.
+ This parameter can be null if the Vertex Colors of each instance are not overriden.
+ If you prefer to use Color for Vertex Colors, please see pVertexColorsAsColor. This will be slower as DWORD is the format that 3ds Max uses natively for the Vertex Color
+ in vertex buffers so we will convert from Color to DWORD.
+ As an helper, to convert from r,g,b being bytes, with values from 0 to 255, to 32 bits DWORD R8G8B8X8, please use the macro declared above INSTANCES_R8G8B8X8_DWORD.
+ */
+ DWORD* pVertexColorsAsDWORD = nullptr;
+
+ /**numVertexColorsAsDWORD is the number of elements in pVertexColorsAsDWORD, it can be less than numInstances, in that case, we will loop through the data when filling the vertex buffer.
+ */
+ size_t numVertexColorsAsDWORD = 0;
+};
+
+/** InstanceRenderGeometry is an extension of IRenderGeometry dealing with instancing.
+
+ Link with the optimesh.lib library to use this in your plugin.
+
+ You can replace any call using a IRenderGeometry by this class, so the geometry is filled the usual way (vertex buffers, stream etc.).
+ IRenderGeometry and InstanceRenderGeometry have identical methods related to the geometry except SetStreamRequirement where we fixed the typo on SetSteamRequirement.
+
+ And you can instantiate this geometry by using the methods below.
+ You can override matrices, positions, orientations, scales, material, UVs map channel (any up to 8), Vertex Colors or a color per instance.
+ The number of instances and the number of elements passed from an array to be applied on instances can be different.
+ Say, you have 100 instances. You are setting 100 positions for instances and 50 colors for instances, it's possible.
+ We will loop through the data that is lower than the number of instances, say if we have 50 colors, instance 51
+ will use color#0, ... Looping.
+
+ For instances to be visible, at least an array of matrices or positions must be provided to differentiate the instances positions visually.
+
+ About materials, there are different situation where this class can be used :
+ - You want to keep the original node's material. In that case don't set the members InstanceData::numColors or InstanceData::pColors and don't set InstanceData::numViewportMaterials
+ or InstanceData::pViewportMaterials and don't set InstanceData::numMaxMaterials or InstanceData::pMaxMaterials and we will use by default the node's material on all instances.
+ - You want the instances to use a flat color shading, use the members InstanceData::numColors and InstanceData::pColors and it will display a flat color per instance ignoring the original
+ node's material. Transparency can be applied on this flat colors shading.
+ - You want to use a (possibly) different material per instance ignoring the node's original material, then use the members InstanceData::numViewportMaterials
+ and InstanceData::pViewportMaterials or InstanceData::numMaxMaterials and InstanceData::pMaxMaterials.
+
+ Performance considerations in the InstanceData struct :
+ - Using InstanceData::pMatrices directly is faster than using InstanceData::pPositions/InstanceData::pOrientationsAsPoint4/InstanceData::pOrientationsAsQuat/InstanceData::pScales
+ as we will convert them into a Matrix3 in the vertex buffer by combining the position/orientation and scale. Though the conversion is not very expansive.
+
+ About InstanceData members pointers ownership : we never take the ownership of the arrays provided and we immediately build the instance vertex buffer from that data.
+ Except for the colors (InstanceData::pColors member) and materials where local copies of the arrays are done.
+
+ When overriding the materials per instance we do a for loop on each instance and apply the material on it before drawing it. So it's slower than using DrawInstances which we use with
+ either the original material or the color per instance override.
+ So changing the materials per instance often in an animation is not advised in term of performance.
+
+ an example of usage is :
+ \code
+ //From the header file :
+ class InstanceObject : public SimpleObject2, public MaxSDK::Graphics::IObjectDisplay2:
+ {
+ ...
+ //From MaxSDK::Graphics::IObjectDisplay2
+ virtual bool PrepareDisplay(const MaxSDK::Graphics::UpdateDisplayContext& prepareDisplayContext) override;
+ virtual bool UpdatePerNodeItems(const MaxSDK::Graphics::UpdateDisplayContext& updateDisplayContext, MaxSDK::Graphics::UpdateNodeContext& nodeContext, MaxSDK::Graphics::IRenderItemContainer& targetRenderItemContainer)override;
+
+ bool mInstanceDataWasUpdated = false;
+ MaxSDK::Graphics::InstanceRenderGeometry* mpShadedInstanceRenderGeometry = nullptr;
+ MaxSDK::Graphics::InstanceRenderGeometry* mpWireframeInstanceRenderGeometry= nullptr;
+ //Instance data stored
+ std::vector mInstancesPositions;
+ std::vector mInstancesScales;
+ std::vector mInstancesOrientations;
+ std::vector mInstancesColors;
+ std::vector mInstancesMaterials;
+ };
+
+ //From the cpp file
+ bool InstanceObject::PrepareDisplay(const MaxSDK::Graphics::UpdateDisplayContext& displayContext)
+ {
+ //Update instance data so it gets animated if the time has changed
+ const TimeValue t = displayContext.GetDisplayTime();
+ if (mLastTimeInstanceDataUpdated != t){
+ Instancing::UpdateInstanceMatricesData (mInstancesMatrixAndUVData , t);//This updates all position/orientation/scale from the matrices using current time
+ Instancing::UpdateInstancePositionData (mInstancesPositions , t);//This updates all positions using current time
+ Instancing::UpdateInstanceScaleData (mInstancesScales , t);//This updates all scales using current time
+ Instancing::UpdateInstanceOrientationData (mInstancesOrientations , t);//This updates all orientations using current time
+ Instancing::UpdateInstanceColorData (mInstancesColors , t);//This updates all colors using current time
+ Instancing::UpdateInstanceVertexColorData (mInstancesVertexColorsAsColor , t);//This updates all VertexColors using current time
+
+ mInstanceDataWasUpdated = true;
+ mLastTimeInstanceDataUpdated = t;
+ }
+
+ return true;
+ }
+
+ //Create the instance vertex buffer, this has to be called once per InstanceRenderGeometry
+ void InstanceObject::CreateInstanceData(InstanceRenderGeometry* pInstanceRenderGeometry)
+ {
+ DbgAssert(pInstanceRenderGeometry);
+ if (nullptr == pInstanceRenderGeometry){
+ return;
+ }
+
+ InstanceData data;
+
+ data.bTransformationsAreInWorldSpace = false;//Are relative to the node's TM
+ data.numInstances = mInstancesPositions.size();
+
+ data.pMatrices = mInstancesMatrixAndUVData.mMat.data(); //Give matrices with pos/orientation/scale for each instance
+ data.numMatrices = mInstancesMatrixAndUVData.mMat.size();
+
+ data.pUVWMapChannel1 = mInstancesMatrixAndUVData.mUV1.data();//Override map channel 1 for each instance
+ data.numUVWMapChannel1 = mInstancesMatrixAndUVData.mUV1.size();
+
+ data.pUVWMapChannel3 = mInstancesMatrixAndUVData.mUV3.data();//Override map channel 3 for each instance
+ data.numUVWMapChannel3 = mInstancesMatrixAndUVData.mUV3.size();
+
+ data.pVertexColorsAsColor = mInstancesVertexColorsAsColor.data();//Override VertexColors for each instance
+ data.numVertexColorsAsColor = mInstancesVertexColorsAsColor.size();
+
+ //For creation
+ pInstanceRenderGeometry->CreateInstanceVertexBuffer(data);
+ }
+
+ //Update the instance vertex buffer, this has to be called each time the instance data is updated
+ void InstanceObject::UpdateInstanceData(InstanceRenderGeometry* pInstanceRenderGeometry)
+ {
+ DbgAssert(pInstanceRenderGeometry);
+ if (nullptr == pInstanceRenderGeometry){
+ return;
+ }
+
+ InstanceData data;
+
+ //We are ignoring data.bTransformationsAreInWorldSpace and data.numInstances which should not have changed when doing an animation
+ //if so, use InstanceRenderGeometry::CreateInstanceVertexBuffer
+
+ data.pMatrices = mInstancesMatrixAndUVData.mMat.data();//Update position/orientation/scale from the matrices array
+ data.numMatrices = mInstancesMatrixAndUVData.mMat.size();
+
+ //Map channel 1 is not animated so ignored (it will be kept as it is from the InstanceRenderGeometry::CreateInstanceVertexBuffer call)
+ //No need to provide its data again
+
+ //Update map channel 3
+ data.pUVWMapChannel3 = mInstancesMatrixAndUVData.mUV3.data();
+ data.numUVWMapChannel3 = mInstancesMatrixAndUVData.mUV3.size();
+
+ //Update VertexColors
+ data.pVertexColorsAsColor = mInstancesVertexColorsAsColor.data();
+
+ //For updates
+ pInstanceRenderGeometry->UpdateInstanceVertexBuffer(data);
+ }
+
+ bool InstanceObject::UpdatePerNodeItems(const UpdateDisplayContext& updateDisplayContext, UpdateNodeContext& nodeContext, IRenderItemContainer& targetRenderItemContainer)
+ {
+ const TimeValue t = updateDisplayContext.GetDisplayTime();
+ const unsigned long requirementFlags = updateDisplayContext.GetRequiredComponents();
+ const bool bRequireSolidMesh = (requirementFlags & ObjectComponentSolidMesh) != 0;
+ const bool bRequireWireframe = (requirementFlags & ObjectComponentWireframe) != 0;
+
+ const MaterialRequiredStreams& materialRequiredStreams = updateDisplayContext.GetRequiredStreams();
+
+ const bool bNeedToRecreateGeometryVertexBuffers = (mLastMaterialRequiredStreams != materialRequiredStreams);
+ if (bNeedToRecreateGeometryVertexBuffers){
+ mLastMaterialRequiredStreams = materialRequiredStreams;
+ }
+
+ if (bRequireSolidMesh){
+ static const bool sbWireFrame = false;//For clarity
+
+ bool bInstanceVertexBufferWasCreated = false;
+ if((nullptr == mpShadedInstanceRenderGeometry) || bNeedToRecreateGeometryVertexBuffers){
+ mLastMaterialRequiredStreams = materialRequiredStreams;//store this material required stream
+ mpShadedInstanceRenderGeometry = new InstanceRenderGeometry;
+ CreateGeometry(mpShadedInstanceRenderGeometry, sbWireFrame, bForceAddingUVs, materialRequiredStreams);
+ CreateInstanceData(mpShadedInstanceRenderGeometry);
+ bInstanceVertexBufferWasCreated = true;
+ }
+
+ if (mInstanceDataWasUpdated && false == bInstanceVertexBufferWasCreated){
+ //Update instance data when there is an animation
+ UpdateInstanceData(mpShadedInstanceRenderGeometry);//Update instance vertex buffer
+ }
+
+ //Always add the render items
+ mpShadedInstanceRenderGeometry->GenerateInstances(sbWireFrame, updateDisplayContext, nodeContext, targetRenderItemContainer);
+ }
+
+ if (bRequireWireframe){
+ static const bool sbWireFrame = true;//For clarity
+
+ bool bInstanceVertexBufferWasCreated = false;
+
+ if ((nullptr == mpWireframeInstanceRenderGeometry) || bNeedToRecreateGeometryVertexBuffers){
+ mpWireframeInstanceRenderGeometry = new InstanceRenderGeometry;
+ CreateGeometry(mpWireframeInstanceRenderGeometry, sbWireFrame, bForceAddingUVs, materialRequiredStreams);
+ CreateInstanceData(mpWireframeInstanceRenderGeometry);
+ bInstanceVertexBufferWasCreated = true;
+ }
+
+ if (mInstanceDataWasUpdated && false == bInstanceVertexBufferWasCreated){
+ //Update instance data when there is an animation
+ UpdateInstanceData(mpWireframeInstanceRenderGeometry);//Update instance vertex buffer
+ }
+
+ //Always add the render items
+ mpWireframeInstanceRenderGeometry->GenerateInstances(sbWireFrame, updateDisplayContext, nodeContext, targetRenderItemContainer);
+ }
+
+ return true;
+ }
+
+ \endcode
+*/
+
+class InstanceRenderGeometry : public IRenderGeometry, public MaxSDK::Util::Noncopyable
+{
+public:
+ DllExport InstanceRenderGeometry();
+ DllExport virtual ~InstanceRenderGeometry();
+
+ /** Create the vertex buffer with instances data. We don't do any local copy of the data you pass (except colors and materials), we use it directly to build the vertex buffer for instances.
+ This does a full rebuild of the instance vertex buffer. So you should call this the first time you pass instance data or when the number of instances has changed.
+ \param[in] data : a const reference on InstanceData
+ */
+ DllExport virtual void CreateInstanceVertexBuffer(const InstanceData& data);
+
+ /** Update the instances data. We don't do any local copy of the data (except colors and materials), we use it directly to update the vertex buffer for instances.
+ Each time the data has been updated on your side, say the positions and colors of instances have been updated, then you should call this method to update the instance vertex buffer.
+ In the data parameter of this method, only what needs to be updated should be non-null pointers and non zero for the number of elements.
+ When calling that function, we update the data provided at the right place into the instance vertex buffer directly. We don't fully rebuild the vertex buffer.
+ Say you are updating only the positions but not the colors, you should do :
+ \code
+ Point3* my_updated_Point3_positions_array = initialized_somewhere_else;
+ InstanceData data;/by default the pColors and numcolors members will be 0.
+ data.pPositions = my_updated_Point3_positions_array;
+ data.numPositions = numPositions;//numPositions is the number of elements of my_updated_Point3_positions_array
+ MyInstanceRenderGeometry.UpdateInstanceVertexBuffer(data);//This will update only the positions and leave unchanged the others data set previously in a call to CreateInstanceVertexBuffer.
+ \endcode
+ \param[in] data : a const reference on InstanceData
+ */
+ DllExport virtual void UpdateInstanceVertexBuffer(const InstanceData& data);
+
+ /** Get if the matrices / positions / orientations / scales on instances are in world space or relative to the node's transform matrix.
+ \return true if they are in world space, false if they are relative to the node's transform.
+ */
+ DllExport virtual bool GetTransformationsAreInWorldSpace(void)const;
+
+ /** Generates the GeometryRenderItemHandle and adds it to the targetRenderItemContainer.
+ This is an helper function when you are in an INode's method UpdatePerNodeItems(const UpdateDisplayContext& updateDisplayContext, UpdateNodeContext& nodeContext, IRenderItemContainer& targetRenderItemContainer)
+ //so it creates and adds the instance render items to the container.
+ \param[in] if bWireframe, is true to generate the render items for a wireframe display, false means for a solid mesh display
+ \param[in] updateDisplayContext is an UpdateDisplayContext which has to be passed from UpdatePerNodeItems
+ \param[in] nodeContext is UpdateNodeContext which has to be passed from UpdatePerNodeItems
+ \param[in] targetRenderItemContainer is an IRenderItemContainer which has to be passed from UpdatePerNodeItems
+ */
+ DllExport virtual bool GenerateInstances(bool bWireframe, const UpdateDisplayContext& updateDisplayContext, UpdateNodeContext& nodeContext, IRenderItemContainer& targetRenderItemContainer);
+
+ /** Get the instance vertex buffer.
+ \return the instance vertex buffer.
+ */
+ DllExport virtual VertexBufferHandle GetInstanceVertexBuffer(void) const;
+
+ /** Get the instance stream format from vertex buffer.
+ \return the stream instance data from vertex buffer.
+ */
+ DllExport virtual const MaterialRequiredStreams& GetInstanceStream(void) const;
+
+ /** Get the geometry stream format from vertex buffer.
+ \return the geometry stream format from vertex buffer.
+ */
+ DllExport virtual const MaterialRequiredStreams& GetGeometryStream(void) const;
+
+ /** This function might be called multiple times in a frame. Inherited classes need to
+ use in the pipeline context to render the geometry. It's recommend to prepare geometry
+ data in another function, and only do rendering tasks in the Display() function.
+ Furthermore, sub-classes are not allowed to change current material parameters. So for
+ multiple material instance, you should use multiple render items to store them.
+ Note: the vertex buffers' format must match current stream requirement in pipeline context.
+ /param drawContext the context for display
+ /param start start primitive to render
+ /param count primitive count to render
+ /param lod current lod value from the adaptive degradation system
+ */
+ DllExport virtual void Display(DrawContext& drawContext, int start, int count, int lod) override;
+
+ /** Get the type of primitives in the geometry.
+ /return the geometry's primitive type
+ */
+ DllExport virtual PrimitiveType GetPrimitiveType() override;
+
+ /** Sets type of primitives in the geometry.
+ /param type the geometry's primitive type
+ */
+ DllExport virtual void SetPrimitiveType(PrimitiveType type) override;
+
+ /** Number of primitives the mesh represents.
+ /return geometry's primitive count
+ */
+ DllExport virtual size_t GetPrimitiveCount() override;
+
+ /** Set the number of primitives in the geometry.
+ \param[in] count the number of primitives
+ */
+ DllExport virtual void SetPrimitiveCount(size_t count);
+
+ /** Number of vertices in the mesh.
+ /return number of vertices in the mesh.
+ */
+ DllExport virtual size_t GetVertexCount() override;
+
+ /** Get start primitive of this geometry.
+ \return The index of the start primitive.
+ */
+ DllExport virtual int GetStartPrimitive() const override;
+
+ /** Set the start primitive offset for drawing.
+ \param[in] offset this offset will pass to Display() function
+ */
+ DllExport virtual void SetStartPrimitive(int offset);
+
+ /** Get the stream requirement of this render geometry.
+ To optimize performance, it's better to create a requirement-geometry mapping.
+ And make the render geometry read-only after created.
+ \return the stream requirement which this geometry built with.
+ */
+ DllExport virtual MaterialRequiredStreams& GetSteamRequirement()override;
+
+ /** Set the stream requirement of this render geometry.
+ \param[in] streamFormat the stream requirement which this geometry built with.
+ */
+ DllExport virtual void SetStreamRequirement(const MaterialRequiredStreams& streamFormat);
+
+ /** Get the vertex streams of this geometry
+ \return vertex streams of this geometry
+ */
+ DllExport virtual VertexBufferHandleArray& GetVertexBuffers()override;
+
+ /** Get index buffer of this geometry.
+ \return index buffer of this geometry. Might be invalid if the geometry doesn't need index buffer
+ */
+ DllExport virtual IndexBufferHandle& GetIndexBuffer()override;
+
+ /** Set index buffer of this geometry.
+ \param[in] indexBuffer index buffer of this geometry.
+ */
+ DllExport virtual void SetIndexBuffer(const IndexBufferHandle& indexBuffer);
+
+ /** Add a vertex buffer to this geometry.
+ \param[in] vertexBuffer : the vertex buffer to be added.
+ */
+ DllExport virtual void AddVertexBuffer(const VertexBufferHandle& vertexBuffer);
+
+ /** Remove the index-th geometry vertex buffer.
+ \param[in] index the index of the geometry vertex buffer to be removed
+ */
+ DllExport virtual void RemoveVertexBuffer(size_t index);
+
+ /** Get the number of geometry vertex buffers.
+ \return the number of geometry vertex buffers.
+ */
+ DllExport virtual size_t GetVertexBufferCount() const;
+
+ /** Get the index-th vertex buffer from the geometry.
+ \param[in] index the index of the geoemtry vertex buffer
+ \return the index-th geometry vertex buffer.
+ */
+ DllExport virtual VertexBufferHandle GetVertexBuffer(size_t index) const;
+
+ /** Retrieve an InterfaceID, is reserved for internal usage only.
+ */
+ DllExport virtual BaseInterface* GetInterface(Interface_ID id);
+
+private:
+ ///Private implementation of this class.
+ InstanceRenderGeometryImpl* mpImpl;
+};
+
+/** Generate the instance render item from a tuple mesh which is an InstanceRenderGeometry class.
+ This is an extension of the method above with more overridable data for instances. The method above could only
+ override the transform matrix in world space and UV from the map channel #1.
+ The InstanceRenderGeometry class lets you override more data per instance. For more details, please
+ see the header file maxsdk\include\Graphics\InstanceRenderGeometry.h
+
+ \param hInstanceRenderItem the instance render item handle which can replace original tuple mesh render item.
+ \param hTupleMeshHandle handle of a tuple mesh render item
+ \param pInstanceRenderGeometry : an InstanceRenderGeometry*
+ \return true if successful created the instance render item.
+*/
+ DllExport bool GenerateInstanceRenderItem(
+ RenderItemHandle& hInstanceRenderItem,
+ const RenderItemHandle& hTupleMeshHandle,
+ const InstanceRenderGeometry* pInstanceRenderGeometry);
+}
+
+} // namespace
diff --git a/additional_includes/MaxRestrictedSdk/2022/Graphics/MaterialConversionHelper.h b/additional_includes/MaxRestrictedSdk/2022/Graphics/MaterialConversionHelper.h
new file mode 100644
index 0000000..2a9ac1c
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2022/Graphics/MaterialConversionHelper.h
@@ -0,0 +1,104 @@
+//
+// Copyright 2020 Autodesk, Inc. All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//
+//
+
+#pragma once
+
+#include "Graphics/GraphicsExport.h"
+#include "Graphics/BaseMaterialHandle.h"
+#include "Materials/Mtl.h"
+#include "maxTypes.h"
+
+namespace MaxSDK { namespace Graphics {
+
+/**
+* MaterialConversionHelper is an helper class to convert from a 3ds Max material (Mtl class) into a Viewport material (BaseMaterialHandle class)
+* This viewport material (say BaseMaterialHandle physMatHandle )can be assigned with RenderItemHandle::SetCustomMaterial(physMatHandle) or if you get access to an
+* UpdateNodeContext& updateNodeContext which can be found in UpdatePerNodeItems, you can do nodeContext.GetRenderNode().SetSolidMaterial(physMatHandle);
+*
+* Examples of usage :
+*
+* //Create a the Nitrous equivalent of a physical material with a bitmap texture as its base color.
+* MSTR texturePath;
+* if (GetSpecDir(APP_MAP_DIR, _T("Maps"), texturePath)){
+* texturePath += TSTR(_T("/uv-grid.png")); //Use UV grid to get something
+* }
+* //Create a BitmapTex which holds the bitmap
+* BitmapTex *pBitmapTex = NewDefaultBitmapTex();
+* pBitmapTex->SetMapName(texturePath);
+* pBitmapTex->SetMtlFlag(MTL_TEX_DISPLAY_ENABLED, TRUE);//Active its display in the viewport
+* pBitmapTex->ActivateTexDisplay(TRUE); //activate it
+* pBitmapTex->NotifyDependents(FOREVER, PART_ALL, REFMSG_CHANGE);
+*
+* Interval valid;
+* valid.SetInfinite();
+*
+* //In realistic mode, like in high quality viewport
+* const MaterialConversionHelper::MaterialStyles matStyle = MaterialConversionHelper::MaterialStyles::MaterialStyle_Realistic;
+*
+* //Create a physical material and get its Nitrous material equivalent
+* BaseMaterialHandle physMatHandle ;
+* Mtl* pPhysicalMaterial = (Mtl*)GetCOREInterface()->CreateInstance(MATERIAL_CLASS_ID, PHYSICALMATERIAL_CLASS_ID);
+* pPhysicalMaterial->SetName(_T("PhysMat1"));
+ const int mapSlot = GetPrimaryMapSlot(pPhysicalMaterial);
+ pPhysicalMaterial->SetSubTexmap(mapSlot, pBitmapTex);
+* pPhysicalMaterial->Update(t, valid);
+* //debug if needed, put the created material to compact material editor slot #0
+* GetCOREInterface()->PutMtlToMtlEditor(pPhysicalMaterial, 0);
+* //Convert to Nitrous the physical material
+* physMatHandle = MaxSDK::Graphics::MaterialConversionHelper::ConvertMaxToNitrousMaterial(*pPhysicalMaterial, t, matStyle);
+*
+* //Another example :
+* //Create a physical material from a preset
+* BaseMaterialHandle physicalMaterialHandleFromGoldPreset;
+* const MSTR presetName (_T("Gold Polished")); //another example is const MSTR presetName (_T("Aluminium Matte"));
+* MaxSDK::Graphics::MaterialConversionHelper::GetNitrousMaterialFromPhysicalMaterialPreset(physicalMaterialHandleFromGoldPreset, presetName, t, matStyle);
+*/
+namespace MaterialConversionHelper
+{
+ /**
+ The enum MaterialStyles is the quality of the desired material from the viewport, when being in high quality viewport, you should use MaterialStyle_Realistic
+ to enableBump/Normal mapping.
+ */
+ enum class MaterialStyles
+ {
+ MaterialStyle_Default, ///The default from the material
+ MaterialStyle_Simple, ///Simple which is what is used in standard quality viewport
+ MaterialStyle_Realistic, ///Is with normal/bump mapping
+ MaterialStyle_Flat, ///No lighting used
+ MaterialStyle_HiddenLine, ///To show hidden lines
+ MaterialStyle_MaterialDecide, ///Let the material decide
+ MaterialStyle_Count,
+ MaterialStyleExt_FastShader = MaterialStyle_Count, ///Is the override material style with fast shader
+ MaterialStyleExt_UVChecker, ///Is the override material style with a UV checker map
+ MaterialStyleExt_RenderSetting, ///Is used internally
+ };
+
+ /** Convert a 3ds Max Mtl to a viewport BaseMaterialHandle
+ \param[in] mtl : the 3ds Max material.
+ \param[in] t : the time at which you want to conversion to happen (could be different from the current time).
+ \param[in] matStyle : the style used for the conversion. It is the quality of the desired material from the viewport, when being in high quality viewport, you should use MaterialStyle_Realistic
+ to enableBump/Normal mapping.
+ \return a BaseMaterialHandle which can be used with RenderItemHandle::SetCustomMaterial()
+ */
+ MaxGraphicsObjectAPI BaseMaterialHandle ConvertMaxToNitrousMaterial(Mtl& mtl, TimeValue t, MaterialStyles matStyle);
+
+
+ /** Create a viewport BaseMaterialHandle from a Physical material preset
+ \param[out] outBaseMaterialHandle : a BaseMaterialHandle which can be used with RenderItemHandle::SetCustomMaterial()
+ \param[in] presetName : the name used in the preset, you can find the preset names from en-US\Plugcfg\PhysicalMaterialTemplates.ini
+ ** BUT ** when there is a composite name like Polished Gold" it's usually the reverse order like "Gold Polished", in the .ini,
+ the actual name is what is after PhysicalTemplate_ActiveMaterial
+ \param[in] matStyle : the style used for the conversion. It is the quality of the desired material from the viewport, when being in high quality viewport, you should use MaterialStyle_Realistic
+ to enableBump/Normal mapping.
+ \return true if the preset name was found and the BaseMaterialHandle correctly filled.
+ */
+ MaxGraphicsObjectAPI bool GetNitrousMaterialFromPhysicalMaterialPreset( BaseMaterialHandle& outBaseMaterialHandle, const MSTR& presetName, TimeValue t, MaterialConversionHelper::MaterialStyles matStyle);
+}
+
+} } // namespace
diff --git a/additional_includes/MaxRestrictedSdk/2022/IMultipleOutputChannel/IMultiOutputConsumer.h b/additional_includes/MaxRestrictedSdk/2022/IMultipleOutputChannel/IMultiOutputConsumer.h
new file mode 100644
index 0000000..2cd5b93
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2022/IMultipleOutputChannel/IMultiOutputConsumer.h
@@ -0,0 +1,129 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#include "../../maxsdk/include/ifnpub.h"
+#define IMULTIOUTPUT_CONSUMER_INTERFACE Interface_ID( 0x6d4a30ed, 0x61024d74 )
+
+#define IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX ( -1 )
+
+// forward declarations
+class ReferenceTarget;
+class ReferenceMaker;
+
+//! \brief An interface for objects that reference other objects that implement IMultipleOutputChannels and acquire output channel data from those objects.
+/*! \sa Class IMultipleOutputChannels, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED
+ An object can acquire output channel data from another object by requesting the IMultipleOutputChannels interface from that object, and then calling
+ GetOutputChannelValue(int index) on that interface. This interface provides read/write access to the specification of which source objects implementing
+ IMultipleOutputChannels are being used and which output channel indices to use on those objects.
+ This interface is used by Schematic Material Editor to help create, modify, and display objects deriving from this interface.
+*/
+
+class IMultiOutputConsumer: public FPMixinInterface
+{
+public:
+
+ virtual int GetNumInputs() const = 0;
+ virtual bool SetOutputToInput( int input_index, ReferenceTarget* output_rtarg, int output_index ) = 0;
+ virtual bool GetOutputFromInput( int input_index, ReferenceTarget*& output_rtarg, int& output_index ) const = 0;
+ virtual bool CanAssignOutputToInput( int input_index, ReferenceTarget* output_rtarg, int output_index ) const = 0;
+ virtual MSTR GetInputName( int input_index ) const = 0;
+ virtual MSTR GetInputLocalizedName( int input_index ) const = 0;
+
+ FPInterfaceDesc* GetDesc(); // <-- must implement
+private:
+
+ ReferenceTarget* MXS_GetOutputRefTargetFromInput( int input_index )
+ {
+ ReferenceTarget* rtarg = 0x0;
+ int output_index = IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ if( GetOutputFromInput( input_index, rtarg, output_index ) )
+ {
+ return rtarg;
+ }
+ return 0x0;
+ }
+
+ int MXS_GetOutputIndexFromInput( int input_index )
+ {
+ ReferenceTarget* rtarg = 0x0;
+ int output_index = IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ if( GetOutputFromInput( input_index, rtarg, output_index ) )
+ {
+ return output_index;
+ }
+ return IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ }
+
+public:
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum {
+ imultioutputconsumer_getnuminputs,
+ imultioutputconsumer_setoutputtoinput,
+ imultioutputconsumer_getoutputrtargfrominput,
+ imultioutputconsumer_getoutputindexfrominput,
+ imultioutputconsumer_canassignoutputtoinput,
+ imultioutputconsumer_getinputname,
+ imultioutputconsumer_getinputlocalizedname
+ };
+
+ #pragma warning(push)
+ #pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP
+ RO_PROP_FN( imultioutputconsumer_getnuminputs, GetNumInputs, TYPE_INT );
+ FN_3( imultioutputconsumer_setoutputtoinput, TYPE_bool, SetOutputToInput, TYPE_INDEX, TYPE_REFTARG, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getoutputrtargfrominput, TYPE_REFTARG, MXS_GetOutputRefTargetFromInput, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getoutputindexfrominput, TYPE_INDEX, MXS_GetOutputIndexFromInput, TYPE_INDEX );
+ FN_3( imultioutputconsumer_canassignoutputtoinput, TYPE_bool, CanAssignOutputToInput, TYPE_INDEX, TYPE_REFTARG, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getinputname, TYPE_TSTR_BV, GetInputName, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getinputlocalizedname, TYPE_TSTR_BV, GetInputLocalizedName, TYPE_INDEX );
+ END_FUNCTION_MAP
+ #pragma warning(pop)
+};
+
+
+//-------------------------------------------------------------
+//! \brief Function Publishing descriptor for Mixin interface on IMultipleOutputChannels-derived classes
+/*! \sa Class IMultipleOutputChannels
+This interface needs to be manually added to the ClassDesc for IMultipleOutputChannels-derived objects using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+static FPInterfaceDesc imultioutput_consumer_interface(
+ IMULTIOUTPUT_CONSUMER_INTERFACE, _T("iMultiOutputConsumer"), 0, NULL, FP_MIXIN,
+
+ IMultiOutputConsumer::imultioutputconsumer_setoutputtoinput, _T("SetOutputToInput"), 0, TYPE_bool, 0, 3,
+ _T("input_index"), 0, TYPE_INDEX,
+ _T("output_rtarg"), 0, TYPE_REFTARG,
+ _T("output_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getoutputrtargfrominput, _T("GetOutputRefTargetFromInput"), 0, TYPE_REFTARG, 0, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getoutputindexfrominput, _T("GetOutputIndexFromInput"), 0, TYPE_INDEX, 0, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_canassignoutputtoinput, _T("CanAssignOutputToInput"), 0, TYPE_bool, 0, 3,
+ _T("input_index"), 0, TYPE_INDEX,
+ _T("output_rtarg"), 0, TYPE_REFTARG,
+ _T("output_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getinputname, _T("GetInputName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getinputlocalizedname, _T("GetInputLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ properties,
+
+ IMultiOutputConsumer::imultioutputconsumer_getnuminputs, FP_NO_FUNCTION, _T("numInputs"), FP_NO_REDRAW, TYPE_INT,
+
+ p_end
+);
\ No newline at end of file
diff --git a/additional_includes/MaxRestrictedSdk/2022/IMultipleOutputChannel/IMultipleOutputChannels.h b/additional_includes/MaxRestrictedSdk/2022/IMultipleOutputChannel/IMultipleOutputChannels.h
new file mode 100644
index 0000000..668bba2
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2022/IMultipleOutputChannel/IMultipleOutputChannels.h
@@ -0,0 +1,144 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#define IMULTIPLEOUTPUTCHANNELS_INTERFACE Interface_ID(0x43147cc9, 0x600e29ff)
+
+#include "../../maxsdk/include/strclass.h"
+#include "../../maxsdk/include/ifnpub.h"
+#include
+
+//! \brief This notification is sent to dependents when a IMultipleOutputChannels's output channel list changes.
+/*! It is sent by IMultipleOutputChannels-derived objects to tell dependents when the number or ordering
+of output channels changes, so those objects can keep pointing at the correct output channel.
+The PartID is a pointer to a Tab of MultiOutputChannelNumberChanged structure instances (defined in
+IMultipleOutputChannels.h) in which each element contains an old-to-new mapping. A new channel index
+of -1 implies the channel was removed. A old channel index of -1 implies the channel was added.
+IMultipleOutputChannelsConsumerWrapper-derived objects typically consume this notification.
+NOTE: If you send this message, the 'propagate' argument of NotifyDependents must be false.
+Otherwise, dependents of dependents think that their ref's output channel list is changing.*/
+#define REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED REFMSG_USER+0x13654850
+
+#define REFMSG_MULTIOUTPUT_CHANNEL_NEEDUPDATE REFMSG_USER+0x13654851
+
+//-------------------------------------------------------------
+//! \brief An interface for objects that that expose multiple output channels of various types that can be recognized by 3ds Max.
+/*! \sa Class IMultipleOutputChannelsConsumer, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED, imultioutput_interface
+This interface provides support for exposing multiple output channels. Each output channel has its own data type.
+
+This interface is primarily to support MetaSL and MR objects that have multiple outputs (for example, an XYZ and a derivative XYZ). Those objects will expose each output as a Texmap.
+
+Basic ParamBlock2 data types are supported by the interface, but not Tab data types. The interface could be expanded to handle Tab data types.
+
+Note: if you derive from this interface, see notification code REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED for information on how this notification should be sent or processed.
+
+Note: Classes that derive from this interface need to manually add the imultioutput_interface FPInterfaceDesc to their ClassDesc using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+#pragma warning(push)
+#pragma warning(disable:4100) // Unused parameters.
+
+class IMultipleOutputChannels: public FPMixinInterface
+{
+public:
+ //! \brief Returns the number of output channels the object exposes.
+ /*! The plugin implements this method to indicate the number of output channels it exposes.
+ \return The number of output channels the object exposes.
+ */
+ virtual int GetNumIMultipleOutputChannels() const = 0;
+
+ //! \brief Returns the localized name for the specified output channel.
+ /*! The plugin implements this method to provide the localized names of the output channels it exposes. These names will be used in the UI.
+ \param[in] index - The index of the output channel.
+ \return The localized name for the specified output channel.
+ */
+ virtual MSTR GetIMultipleOutputChannelLocalizedName( int index ) const = 0;
+
+ //! \brief Returns the non-localized name for the specified output channel.
+ /*! The plugin implements this method to provide the non-localized names of the output channels it exposes. These would typically be used in scripts to provide locale independence.
+ \param[in] index - The index of the output channel.
+ \return The non-localized name for the specified output channel.
+ */
+ virtual MSTR GetIMultipleOutputChannelName( int index ) const = 0;
+
+ //! \brief Returns the Parameter type for the specified output channel.
+ /*! The plugin implements this method to provide the parameter type of the output channels it exposes. The type can be used for parameter validation
+ between input and output channels.
+ \param[in] index - The index of the output channel.
+ \return The parameter type of the output channel.
+ Note: the data type will correspond to one of the data types supported by the ParamBlock2 system, tabs are not supported
+ */
+ virtual ParamType3 GetIMultipleOutputChannelType( int index) const =0;
+
+ //! \brief Data structure for REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED notifications
+ /*! A REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED notification sends a Tab*, which provides old-to-new output channel indexing.
+ An old index value of -1 means the output channel is new, a new output index of -1 means the old output channel was removed.
+ */
+ struct MultiOutputChannelNumberChanged
+ {
+ int oldIndex;
+ int newIndex;
+ };
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum { getnumoutputs, getlocalizedoutputname, getoutputname };
+ // FP-published symbolic enumerations
+
+#pragma warning(push)
+#pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP
+ RO_PROP_FN(getnumoutputs, GetNumIMultipleOutputChannels, TYPE_INT);
+ FN_1( getlocalizedoutputname, TYPE_TSTR_BV, MXS_GetIMultipleOutputChannelLocalizedName, TYPE_INDEX );
+ FN_1( getoutputname, TYPE_TSTR_BV, MXS_GetIMultipleOutputChannelName, TYPE_INDEX );
+ END_FUNCTION_MAP
+#pragma warning(pop)
+
+ FPInterfaceDesc* GetDesc() override; // <-- must implement
+
+protected:
+ // FPS exposure stub methods
+ void ValidateIMultipleOutputChannelIndexValue(int index) const
+ {
+ if (index < 0 || index >= GetNumIMultipleOutputChannels())
+ throw MAXException(_M("Invalid IMultiOutput channel index"));
+ }
+private:
+ MSTR MXS_GetIMultipleOutputChannelLocalizedName( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelLocalizedName(index);
+ }
+ MSTR MXS_GetIMultipleOutputChannelName( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelName(index);
+ }
+};
+
+
+#pragma warning(pop)
+//-------------------------------------------------------------
+//! \brief Function Publishing descriptor for Mixin interface on IMultipleOutputChannels-derived classes
+/*! \sa Class IMultipleOutputChannels
+This interface needs to be manually added to the ClassDesc for IMultipleOutputChannels-derived objects using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+
+*/
+static FPInterfaceDesc imultioutput_interface(
+ IMULTIPLEOUTPUTCHANNELS_INTERFACE, _T("iMultipleOutputChannels"), 0, nullptr, FP_MIXIN,
+ IMultipleOutputChannels::getlocalizedoutputname, _T("getIMultipleOutputChannelLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannels::getoutputname, _T("getIMultipleOutputChannelName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+
+ properties,
+ IMultipleOutputChannels::getnumoutputs, FP_NO_FUNCTION, _T("numIMultipleOutputChannels"),FP_NO_REDRAW,TYPE_INT,
+
+ p_end
+);
diff --git a/additional_includes/MaxRestrictedSdk/2022/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h b/additional_includes/MaxRestrictedSdk/2022/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h
new file mode 100644
index 0000000..1058b99
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2022/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h
@@ -0,0 +1,288 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#define IMULTIPLEOUTPUTCHANNELS_WITH_VALUES_INTERFACE Interface_ID(0x24280bd5, 0x22b1edca)
+
+#include "IMultipleOutputChannels.h"
+#include "../../maxsdk/include/paramtype.h"
+#include "../../maxsdk/include/strclass.h"
+#include "../../maxsdk/include/ifnpub.h"
+#include "../../maxsdk/include/interval.h"
+
+// forward declarations
+class Point3;
+class Point4;
+class Color;
+class AColor;
+class Mtl;
+class Texmap;
+class PBBitmap;
+class INode;
+class ReferenceTarget;
+class IParamBlock2;
+class Matrix3;
+namespace MaxSDK
+{
+ namespace AssetManagement
+ {
+ class AssetUser;
+ }
+}
+
+//-------------------------------------------------------------
+//! \brief An interface for objects that that expose multiple output channels of various types that can be recognized by 3ds Max.
+/*! \sa Class IMultipleOutputChannelsConsumer, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED, imultioutput_interface
+This interface provides support for exposing multiple output channels. Each output channel has its own data type.
+
+This interface is primarily to support MetaSL and MR objects that have multiple outputs (for example, an XYZ and a derivative XYZ). Those objects will expose each output as a Texmap.
+
+Basic ParamBlock2 data types are supported by the interface, but not Tab data types. The interface could be expanded to handle Tab data types.
+
+Note: if you derive from this interface, see notification code REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED for information on how this notification should be sent or processed.
+
+Note: Classes that derive from this interface need to manually add the imultioutput_with_values_interface FPInterfaceDesc to their ClassDesc using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+#pragma warning(push)
+#pragma warning(disable:4100) // Unused parameters.
+
+class IMultipleOutputChannelsWithChannelValues: public IMultipleOutputChannels
+{
+public:
+ //! \brief Indicates whether a REFMSG_CHANGED notification received from the object should be propagated from dependents using just the channel output value.
+ /*! The plugin implements this method to optimize REFMSG_CHANGE propagation. An object using an output channel value from this object will receive REFMSG_CHANGE notifications for
+ all changes to this object, even if that change does not affect the contents of the value retrieved from an output channel. The REFMSG_CHANGE notification does not need to
+ propagate from that object since its dependents are not affected by the change. If this method returns false, the object can return REF_STOP from its NotifyRefChanged if this object
+ is the target object.
+ \param[in] index - The index of the output channel.
+ \return false if a REFMSG_CHANGE notification from this object can be blocked from propagating from objects using just the specified output channel's data value.
+ */
+ virtual bool GetIMultipleOutputChannelValueChanged( int index) const { return true; }
+
+ //! \brief Returns the output data value for the specified output channel.
+ /*! The plugin implements this method to return the output channel's data value as an FPValue.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return The data value for the specified output channel as an FPValue.
+ */
+ virtual FPValue GetIMultipleOutputChannelValue( int index, TimeValue t, Interval& ivalid ) const = 0;
+
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, float& v, Interval& ivalid ) const { return false; } // TYPE_FLOAT
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, int& v, Interval& ivalid ) const { return false; } // TYPE_INT
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Color& v, Interval& ivalid ) const { return false; } // TYPE_RGBA
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Point3& v, Interval& ivalid ) const { return false; } // TYPE_POINT3
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, const MCHAR*& v, Interval& ivalid ) const { return false; } // TYPE_STRING
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, const MaxSDK::AssetManagement::AssetUser *& v, Interval& ivalid ) const { return false; } // TYPE_FILENAME
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Mtl*& v, Interval& ivalid ) const { return false; } // TYPE_MTL
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Texmap*& v, Interval& ivalid ) const { return false; } // TYPE_TEXMAP
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, PBBitmap*& v, Interval& ivalid ) const { return false; } // TYPE_BITMAP
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, INode*& v, Interval& ivalid ) const { return false; } // TYPE_INODE
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, ReferenceTarget*& v, Interval& ivalid ) const { return false; } // TYPE_REFTARG
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Matrix3& v, Interval& ivalid ) const { return false; } // TYPE_MATRIX3
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, IParamBlock2*& v, Interval& ivalid ) const { return false; } // TYPE_PBLOCK2
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Point4& v, Interval& ivalid ) const { return false; } // TYPE_POINT4
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, AColor& v, Interval& ivalid ) const { return false; } // TYPE_FRGBA
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum { getoutputtype = IMultipleOutputChannels::getoutputname+1, getoutputvalue };
+ // FP-published symbolic enumerations
+ enum
+ {
+ outputChannelType,
+ };
+#pragma warning(push)
+#pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP_PARENT(IMultipleOutputChannels)
+ FN_1( getoutputtype, TYPE_ENUM, MXS_GetIMultipleOutputChannelType, TYPE_INDEX );
+ FN_2( getoutputvalue, TYPE_FPVALUE_BV, MXS_GetIMultipleOutputChannelValue, TYPE_INDEX, TYPE_TIMEVALUE );
+ END_FUNCTION_MAP
+#pragma warning(pop)
+
+ FPInterfaceDesc* GetDesc(); // <-- must implement
+
+private:
+ DWORD MXS_GetIMultipleOutputChannelType( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelType(index);
+ }
+ FPValue MXS_GetIMultipleOutputChannelValue( int index, TimeValue t ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ Interval valid;
+ return GetIMultipleOutputChannelValue(index, t, valid);
+ }
+};
+
+
+#pragma warning(pop)
+//-------------------------------------------------------------
+//! \brief Function Publishing descriptor for Mixin interface on iMultipleOutputChannelsWithValues-derived classes
+/*! \sa Class IMultipleOutputChannels
+This interface needs to be manually added to the ClassDesc for iMultipleOutputChannelsWithValues-derived objects using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+
+*/
+static FPInterfaceDesc imultioutput_with_values_interface(
+ IMULTIPLEOUTPUTCHANNELS_WITH_VALUES_INTERFACE, _T("iMultipleOutputChannelsWithValues"), 0, NULL, FP_MIXIN,
+ IMultipleOutputChannels::getlocalizedoutputname, _T("getIMultipleOutputChannelLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannels::getoutputname, _T("getIMultipleOutputChannelName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannelsWithChannelValues::getoutputtype, _T("getIMultipleOutputChannelType"), 0, TYPE_ENUM, IMultipleOutputChannelsWithChannelValues::outputChannelType, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannelsWithChannelValues::getoutputvalue, _T("getIMultipleOutputChannelValue"), 0, TYPE_FPVALUE_BV, FP_NO_REDRAW, 2,
+ _T("index"), 0, TYPE_INDEX,
+ _T("time"), 0, TYPE_TIMEVALUE, f_keyArgDefault, 0,
+
+ properties,
+ IMultipleOutputChannels::getnumoutputs, FP_NO_FUNCTION, _T("numIMultipleOutputChannels"),FP_NO_REDRAW,TYPE_INT,
+
+ // symbolic enumerations
+ enums,
+ IMultipleOutputChannelsWithChannelValues::outputChannelType, 15,
+ _T("float"), TYPE_FLOAT,
+ _T("integer"), TYPE_INT,
+ _T("rgb"), TYPE_RGBA,
+ _T("point3"), TYPE_POINT3,
+ _T("string"), TYPE_STRING,
+ _T("filename"), TYPE_FILENAME,
+ _T("material"), TYPE_MTL,
+ _T("texturemap"), TYPE_TEXMAP,
+ _T("bitmap"), TYPE_BITMAP,
+ _T("node"), TYPE_INODE,
+ _T("maxObject"), TYPE_REFTARG,
+ _T("matrix3"), TYPE_MATRIX3,
+ _T("paramblock2"), TYPE_PBLOCK2,
+ _T("point4"), TYPE_POINT4,
+ _T("frgba"), TYPE_FRGBA,
+ p_end
+);
diff --git a/additional_includes/MaxRestrictedSdk/2022/Qt/QmaxRollupContainer.h b/additional_includes/MaxRestrictedSdk/2022/Qt/QmaxRollupContainer.h
new file mode 100644
index 0000000..ff3667e
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2022/Qt/QmaxRollupContainer.h
@@ -0,0 +1,98 @@
+#pragma once
+
+#include "CoreExport.h"
+
+#pragma warning(push)
+#pragma warning(disable: 4127 4251 4275 4512 4800 )
+#include
+#pragma warning(pop)
+
+
+namespace MaxSDK
+{
+ class QmaxRollup;
+ class QmaxRollupContainerPrivate;
+ class CoreExport QmaxRollupContainer : public QScrollArea
+ {
+ Q_OBJECT
+ public:
+ explicit QmaxRollupContainer( QWidget *parent = nullptr );
+
+ virtual ~QmaxRollupContainer();
+
+ void addRollup( QmaxRollup* rollup );
+ void insertRollup( QmaxRollup* rollup, int index );
+
+ int numRollups() const;
+ QmaxRollup* rollup( int index ) const;
+ QVector rollups() const;
+ int indexOf( QmaxRollup* rollup ) const;
+
+ /** \brief Resets the categories of the rollups back to the default state.
+ * Calls sortRollups() internally.
+ * \see sortRollups, QmaxRollup::category(), QmaxRollup::resetCategory()
+ */
+ void resetCategories();
+
+ QmaxRollup* takeRollup( int index );
+ void takeRollup( QmaxRollup* rollup );
+
+ /** \brief Sorts the rollups by category.
+ * Sorts the rollups in based on ascending category. Rollups with the
+ * same category will appear in the given order.
+ * \see QmaxRollup.category()
+ **/
+ void sortRollups();
+
+ virtual void openAllRollups();
+ virtual void closeAllRollups();
+
+ /** \brief Adds a QAction that get used within the right-click context
+ * menu of the rollup-container.
+ * The rollup container does not take ownership of the QAction. To
+ * remove an action, call removeRightClickAction() or simply delete
+ * the QAction on callers side.
+ * \see removeRightClickAction, QAction
+ */
+ virtual void addRightClickAction( QAction* action );
+
+ /** \brief Remove a QAction that from used within the right-click
+ * context menu of the rollup-container.
+ * \see addRightClickAction, QAction
+ */
+ virtual void removeRightClickAction( QAction* action );
+
+ QSize minimumSizeHint() const override;
+
+ bool canScroll() const;
+
+ signals:
+
+ public slots:
+
+ protected:
+
+ explicit QmaxRollupContainer( QmaxRollupContainerPrivate* d, QWidget *parent = nullptr );
+
+ void mouseMoveEvent( QMouseEvent *event ) override;
+ void mousePressEvent( QMouseEvent *event ) override;
+ void mouseReleaseEvent( QMouseEvent *event ) override;
+ void wheelEvent( QWheelEvent *event ) override;
+
+ void contextMenuEvent( QContextMenuEvent *event ) override;
+
+ void dragEnterEvent( QDragEnterEvent *event ) override;
+ void dragMoveEvent( QDragMoveEvent *event ) override;
+ void dragLeaveEvent( QDragLeaveEvent *event ) override;
+ void dropEvent( QDropEvent* event ) override;
+
+ Q_DECLARE_PRIVATE( QmaxRollupContainer );
+
+ private:
+ QmaxRollupContainerPrivate* d_ptr;
+ Q_DISABLE_COPY( QmaxRollupContainer );
+
+ friend class QmaxRollup;
+ };
+
+};
\ No newline at end of file
diff --git a/additional_includes/MaxRestrictedSdk/2023/IMultipleOutputChannel/IMultiOutputConsumer.h b/additional_includes/MaxRestrictedSdk/2023/IMultipleOutputChannel/IMultiOutputConsumer.h
new file mode 100644
index 0000000..2cd5b93
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2023/IMultipleOutputChannel/IMultiOutputConsumer.h
@@ -0,0 +1,129 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#include "../../maxsdk/include/ifnpub.h"
+#define IMULTIOUTPUT_CONSUMER_INTERFACE Interface_ID( 0x6d4a30ed, 0x61024d74 )
+
+#define IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX ( -1 )
+
+// forward declarations
+class ReferenceTarget;
+class ReferenceMaker;
+
+//! \brief An interface for objects that reference other objects that implement IMultipleOutputChannels and acquire output channel data from those objects.
+/*! \sa Class IMultipleOutputChannels, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED
+ An object can acquire output channel data from another object by requesting the IMultipleOutputChannels interface from that object, and then calling
+ GetOutputChannelValue(int index) on that interface. This interface provides read/write access to the specification of which source objects implementing
+ IMultipleOutputChannels are being used and which output channel indices to use on those objects.
+ This interface is used by Schematic Material Editor to help create, modify, and display objects deriving from this interface.
+*/
+
+class IMultiOutputConsumer: public FPMixinInterface
+{
+public:
+
+ virtual int GetNumInputs() const = 0;
+ virtual bool SetOutputToInput( int input_index, ReferenceTarget* output_rtarg, int output_index ) = 0;
+ virtual bool GetOutputFromInput( int input_index, ReferenceTarget*& output_rtarg, int& output_index ) const = 0;
+ virtual bool CanAssignOutputToInput( int input_index, ReferenceTarget* output_rtarg, int output_index ) const = 0;
+ virtual MSTR GetInputName( int input_index ) const = 0;
+ virtual MSTR GetInputLocalizedName( int input_index ) const = 0;
+
+ FPInterfaceDesc* GetDesc(); // <-- must implement
+private:
+
+ ReferenceTarget* MXS_GetOutputRefTargetFromInput( int input_index )
+ {
+ ReferenceTarget* rtarg = 0x0;
+ int output_index = IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ if( GetOutputFromInput( input_index, rtarg, output_index ) )
+ {
+ return rtarg;
+ }
+ return 0x0;
+ }
+
+ int MXS_GetOutputIndexFromInput( int input_index )
+ {
+ ReferenceTarget* rtarg = 0x0;
+ int output_index = IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ if( GetOutputFromInput( input_index, rtarg, output_index ) )
+ {
+ return output_index;
+ }
+ return IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ }
+
+public:
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum {
+ imultioutputconsumer_getnuminputs,
+ imultioutputconsumer_setoutputtoinput,
+ imultioutputconsumer_getoutputrtargfrominput,
+ imultioutputconsumer_getoutputindexfrominput,
+ imultioutputconsumer_canassignoutputtoinput,
+ imultioutputconsumer_getinputname,
+ imultioutputconsumer_getinputlocalizedname
+ };
+
+ #pragma warning(push)
+ #pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP
+ RO_PROP_FN( imultioutputconsumer_getnuminputs, GetNumInputs, TYPE_INT );
+ FN_3( imultioutputconsumer_setoutputtoinput, TYPE_bool, SetOutputToInput, TYPE_INDEX, TYPE_REFTARG, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getoutputrtargfrominput, TYPE_REFTARG, MXS_GetOutputRefTargetFromInput, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getoutputindexfrominput, TYPE_INDEX, MXS_GetOutputIndexFromInput, TYPE_INDEX );
+ FN_3( imultioutputconsumer_canassignoutputtoinput, TYPE_bool, CanAssignOutputToInput, TYPE_INDEX, TYPE_REFTARG, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getinputname, TYPE_TSTR_BV, GetInputName, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getinputlocalizedname, TYPE_TSTR_BV, GetInputLocalizedName, TYPE_INDEX );
+ END_FUNCTION_MAP
+ #pragma warning(pop)
+};
+
+
+//-------------------------------------------------------------
+//! \brief Function Publishing descriptor for Mixin interface on IMultipleOutputChannels-derived classes
+/*! \sa Class IMultipleOutputChannels
+This interface needs to be manually added to the ClassDesc for IMultipleOutputChannels-derived objects using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+static FPInterfaceDesc imultioutput_consumer_interface(
+ IMULTIOUTPUT_CONSUMER_INTERFACE, _T("iMultiOutputConsumer"), 0, NULL, FP_MIXIN,
+
+ IMultiOutputConsumer::imultioutputconsumer_setoutputtoinput, _T("SetOutputToInput"), 0, TYPE_bool, 0, 3,
+ _T("input_index"), 0, TYPE_INDEX,
+ _T("output_rtarg"), 0, TYPE_REFTARG,
+ _T("output_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getoutputrtargfrominput, _T("GetOutputRefTargetFromInput"), 0, TYPE_REFTARG, 0, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getoutputindexfrominput, _T("GetOutputIndexFromInput"), 0, TYPE_INDEX, 0, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_canassignoutputtoinput, _T("CanAssignOutputToInput"), 0, TYPE_bool, 0, 3,
+ _T("input_index"), 0, TYPE_INDEX,
+ _T("output_rtarg"), 0, TYPE_REFTARG,
+ _T("output_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getinputname, _T("GetInputName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getinputlocalizedname, _T("GetInputLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ properties,
+
+ IMultiOutputConsumer::imultioutputconsumer_getnuminputs, FP_NO_FUNCTION, _T("numInputs"), FP_NO_REDRAW, TYPE_INT,
+
+ p_end
+);
\ No newline at end of file
diff --git a/additional_includes/MaxRestrictedSdk/2023/IMultipleOutputChannel/IMultipleOutputChannels.h b/additional_includes/MaxRestrictedSdk/2023/IMultipleOutputChannel/IMultipleOutputChannels.h
new file mode 100644
index 0000000..668bba2
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2023/IMultipleOutputChannel/IMultipleOutputChannels.h
@@ -0,0 +1,144 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#define IMULTIPLEOUTPUTCHANNELS_INTERFACE Interface_ID(0x43147cc9, 0x600e29ff)
+
+#include "../../maxsdk/include/strclass.h"
+#include "../../maxsdk/include/ifnpub.h"
+#include
+
+//! \brief This notification is sent to dependents when a IMultipleOutputChannels's output channel list changes.
+/*! It is sent by IMultipleOutputChannels-derived objects to tell dependents when the number or ordering
+of output channels changes, so those objects can keep pointing at the correct output channel.
+The PartID is a pointer to a Tab of MultiOutputChannelNumberChanged structure instances (defined in
+IMultipleOutputChannels.h) in which each element contains an old-to-new mapping. A new channel index
+of -1 implies the channel was removed. A old channel index of -1 implies the channel was added.
+IMultipleOutputChannelsConsumerWrapper-derived objects typically consume this notification.
+NOTE: If you send this message, the 'propagate' argument of NotifyDependents must be false.
+Otherwise, dependents of dependents think that their ref's output channel list is changing.*/
+#define REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED REFMSG_USER+0x13654850
+
+#define REFMSG_MULTIOUTPUT_CHANNEL_NEEDUPDATE REFMSG_USER+0x13654851
+
+//-------------------------------------------------------------
+//! \brief An interface for objects that that expose multiple output channels of various types that can be recognized by 3ds Max.
+/*! \sa Class IMultipleOutputChannelsConsumer, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED, imultioutput_interface
+This interface provides support for exposing multiple output channels. Each output channel has its own data type.
+
+This interface is primarily to support MetaSL and MR objects that have multiple outputs (for example, an XYZ and a derivative XYZ). Those objects will expose each output as a Texmap.
+
+Basic ParamBlock2 data types are supported by the interface, but not Tab data types. The interface could be expanded to handle Tab data types.
+
+Note: if you derive from this interface, see notification code REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED for information on how this notification should be sent or processed.
+
+Note: Classes that derive from this interface need to manually add the imultioutput_interface FPInterfaceDesc to their ClassDesc using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+#pragma warning(push)
+#pragma warning(disable:4100) // Unused parameters.
+
+class IMultipleOutputChannels: public FPMixinInterface
+{
+public:
+ //! \brief Returns the number of output channels the object exposes.
+ /*! The plugin implements this method to indicate the number of output channels it exposes.
+ \return The number of output channels the object exposes.
+ */
+ virtual int GetNumIMultipleOutputChannels() const = 0;
+
+ //! \brief Returns the localized name for the specified output channel.
+ /*! The plugin implements this method to provide the localized names of the output channels it exposes. These names will be used in the UI.
+ \param[in] index - The index of the output channel.
+ \return The localized name for the specified output channel.
+ */
+ virtual MSTR GetIMultipleOutputChannelLocalizedName( int index ) const = 0;
+
+ //! \brief Returns the non-localized name for the specified output channel.
+ /*! The plugin implements this method to provide the non-localized names of the output channels it exposes. These would typically be used in scripts to provide locale independence.
+ \param[in] index - The index of the output channel.
+ \return The non-localized name for the specified output channel.
+ */
+ virtual MSTR GetIMultipleOutputChannelName( int index ) const = 0;
+
+ //! \brief Returns the Parameter type for the specified output channel.
+ /*! The plugin implements this method to provide the parameter type of the output channels it exposes. The type can be used for parameter validation
+ between input and output channels.
+ \param[in] index - The index of the output channel.
+ \return The parameter type of the output channel.
+ Note: the data type will correspond to one of the data types supported by the ParamBlock2 system, tabs are not supported
+ */
+ virtual ParamType3 GetIMultipleOutputChannelType( int index) const =0;
+
+ //! \brief Data structure for REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED notifications
+ /*! A REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED notification sends a Tab*, which provides old-to-new output channel indexing.
+ An old index value of -1 means the output channel is new, a new output index of -1 means the old output channel was removed.
+ */
+ struct MultiOutputChannelNumberChanged
+ {
+ int oldIndex;
+ int newIndex;
+ };
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum { getnumoutputs, getlocalizedoutputname, getoutputname };
+ // FP-published symbolic enumerations
+
+#pragma warning(push)
+#pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP
+ RO_PROP_FN(getnumoutputs, GetNumIMultipleOutputChannels, TYPE_INT);
+ FN_1( getlocalizedoutputname, TYPE_TSTR_BV, MXS_GetIMultipleOutputChannelLocalizedName, TYPE_INDEX );
+ FN_1( getoutputname, TYPE_TSTR_BV, MXS_GetIMultipleOutputChannelName, TYPE_INDEX );
+ END_FUNCTION_MAP
+#pragma warning(pop)
+
+ FPInterfaceDesc* GetDesc() override; // <-- must implement
+
+protected:
+ // FPS exposure stub methods
+ void ValidateIMultipleOutputChannelIndexValue(int index) const
+ {
+ if (index < 0 || index >= GetNumIMultipleOutputChannels())
+ throw MAXException(_M("Invalid IMultiOutput channel index"));
+ }
+private:
+ MSTR MXS_GetIMultipleOutputChannelLocalizedName( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelLocalizedName(index);
+ }
+ MSTR MXS_GetIMultipleOutputChannelName( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelName(index);
+ }
+};
+
+
+#pragma warning(pop)
+//-------------------------------------------------------------
+//! \brief Function Publishing descriptor for Mixin interface on IMultipleOutputChannels-derived classes
+/*! \sa Class IMultipleOutputChannels
+This interface needs to be manually added to the ClassDesc for IMultipleOutputChannels-derived objects using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+
+*/
+static FPInterfaceDesc imultioutput_interface(
+ IMULTIPLEOUTPUTCHANNELS_INTERFACE, _T("iMultipleOutputChannels"), 0, nullptr, FP_MIXIN,
+ IMultipleOutputChannels::getlocalizedoutputname, _T("getIMultipleOutputChannelLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannels::getoutputname, _T("getIMultipleOutputChannelName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+
+ properties,
+ IMultipleOutputChannels::getnumoutputs, FP_NO_FUNCTION, _T("numIMultipleOutputChannels"),FP_NO_REDRAW,TYPE_INT,
+
+ p_end
+);
diff --git a/additional_includes/MaxRestrictedSdk/2023/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h b/additional_includes/MaxRestrictedSdk/2023/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h
new file mode 100644
index 0000000..1058b99
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2023/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h
@@ -0,0 +1,288 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#define IMULTIPLEOUTPUTCHANNELS_WITH_VALUES_INTERFACE Interface_ID(0x24280bd5, 0x22b1edca)
+
+#include "IMultipleOutputChannels.h"
+#include "../../maxsdk/include/paramtype.h"
+#include "../../maxsdk/include/strclass.h"
+#include "../../maxsdk/include/ifnpub.h"
+#include "../../maxsdk/include/interval.h"
+
+// forward declarations
+class Point3;
+class Point4;
+class Color;
+class AColor;
+class Mtl;
+class Texmap;
+class PBBitmap;
+class INode;
+class ReferenceTarget;
+class IParamBlock2;
+class Matrix3;
+namespace MaxSDK
+{
+ namespace AssetManagement
+ {
+ class AssetUser;
+ }
+}
+
+//-------------------------------------------------------------
+//! \brief An interface for objects that that expose multiple output channels of various types that can be recognized by 3ds Max.
+/*! \sa Class IMultipleOutputChannelsConsumer, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED, imultioutput_interface
+This interface provides support for exposing multiple output channels. Each output channel has its own data type.
+
+This interface is primarily to support MetaSL and MR objects that have multiple outputs (for example, an XYZ and a derivative XYZ). Those objects will expose each output as a Texmap.
+
+Basic ParamBlock2 data types are supported by the interface, but not Tab data types. The interface could be expanded to handle Tab data types.
+
+Note: if you derive from this interface, see notification code REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED for information on how this notification should be sent or processed.
+
+Note: Classes that derive from this interface need to manually add the imultioutput_with_values_interface FPInterfaceDesc to their ClassDesc using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+#pragma warning(push)
+#pragma warning(disable:4100) // Unused parameters.
+
+class IMultipleOutputChannelsWithChannelValues: public IMultipleOutputChannels
+{
+public:
+ //! \brief Indicates whether a REFMSG_CHANGED notification received from the object should be propagated from dependents using just the channel output value.
+ /*! The plugin implements this method to optimize REFMSG_CHANGE propagation. An object using an output channel value from this object will receive REFMSG_CHANGE notifications for
+ all changes to this object, even if that change does not affect the contents of the value retrieved from an output channel. The REFMSG_CHANGE notification does not need to
+ propagate from that object since its dependents are not affected by the change. If this method returns false, the object can return REF_STOP from its NotifyRefChanged if this object
+ is the target object.
+ \param[in] index - The index of the output channel.
+ \return false if a REFMSG_CHANGE notification from this object can be blocked from propagating from objects using just the specified output channel's data value.
+ */
+ virtual bool GetIMultipleOutputChannelValueChanged( int index) const { return true; }
+
+ //! \brief Returns the output data value for the specified output channel.
+ /*! The plugin implements this method to return the output channel's data value as an FPValue.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return The data value for the specified output channel as an FPValue.
+ */
+ virtual FPValue GetIMultipleOutputChannelValue( int index, TimeValue t, Interval& ivalid ) const = 0;
+
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, float& v, Interval& ivalid ) const { return false; } // TYPE_FLOAT
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, int& v, Interval& ivalid ) const { return false; } // TYPE_INT
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Color& v, Interval& ivalid ) const { return false; } // TYPE_RGBA
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Point3& v, Interval& ivalid ) const { return false; } // TYPE_POINT3
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, const MCHAR*& v, Interval& ivalid ) const { return false; } // TYPE_STRING
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, const MaxSDK::AssetManagement::AssetUser *& v, Interval& ivalid ) const { return false; } // TYPE_FILENAME
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Mtl*& v, Interval& ivalid ) const { return false; } // TYPE_MTL
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Texmap*& v, Interval& ivalid ) const { return false; } // TYPE_TEXMAP
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, PBBitmap*& v, Interval& ivalid ) const { return false; } // TYPE_BITMAP
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, INode*& v, Interval& ivalid ) const { return false; } // TYPE_INODE
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, ReferenceTarget*& v, Interval& ivalid ) const { return false; } // TYPE_REFTARG
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Matrix3& v, Interval& ivalid ) const { return false; } // TYPE_MATRIX3
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, IParamBlock2*& v, Interval& ivalid ) const { return false; } // TYPE_PBLOCK2
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Point4& v, Interval& ivalid ) const { return false; } // TYPE_POINT4
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, AColor& v, Interval& ivalid ) const { return false; } // TYPE_FRGBA
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum { getoutputtype = IMultipleOutputChannels::getoutputname+1, getoutputvalue };
+ // FP-published symbolic enumerations
+ enum
+ {
+ outputChannelType,
+ };
+#pragma warning(push)
+#pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP_PARENT(IMultipleOutputChannels)
+ FN_1( getoutputtype, TYPE_ENUM, MXS_GetIMultipleOutputChannelType, TYPE_INDEX );
+ FN_2( getoutputvalue, TYPE_FPVALUE_BV, MXS_GetIMultipleOutputChannelValue, TYPE_INDEX, TYPE_TIMEVALUE );
+ END_FUNCTION_MAP
+#pragma warning(pop)
+
+ FPInterfaceDesc* GetDesc(); // <-- must implement
+
+private:
+ DWORD MXS_GetIMultipleOutputChannelType( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelType(index);
+ }
+ FPValue MXS_GetIMultipleOutputChannelValue( int index, TimeValue t ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ Interval valid;
+ return GetIMultipleOutputChannelValue(index, t, valid);
+ }
+};
+
+
+#pragma warning(pop)
+//-------------------------------------------------------------
+//! \brief Function Publishing descriptor for Mixin interface on iMultipleOutputChannelsWithValues-derived classes
+/*! \sa Class IMultipleOutputChannels
+This interface needs to be manually added to the ClassDesc for iMultipleOutputChannelsWithValues-derived objects using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+
+*/
+static FPInterfaceDesc imultioutput_with_values_interface(
+ IMULTIPLEOUTPUTCHANNELS_WITH_VALUES_INTERFACE, _T("iMultipleOutputChannelsWithValues"), 0, NULL, FP_MIXIN,
+ IMultipleOutputChannels::getlocalizedoutputname, _T("getIMultipleOutputChannelLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannels::getoutputname, _T("getIMultipleOutputChannelName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannelsWithChannelValues::getoutputtype, _T("getIMultipleOutputChannelType"), 0, TYPE_ENUM, IMultipleOutputChannelsWithChannelValues::outputChannelType, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannelsWithChannelValues::getoutputvalue, _T("getIMultipleOutputChannelValue"), 0, TYPE_FPVALUE_BV, FP_NO_REDRAW, 2,
+ _T("index"), 0, TYPE_INDEX,
+ _T("time"), 0, TYPE_TIMEVALUE, f_keyArgDefault, 0,
+
+ properties,
+ IMultipleOutputChannels::getnumoutputs, FP_NO_FUNCTION, _T("numIMultipleOutputChannels"),FP_NO_REDRAW,TYPE_INT,
+
+ // symbolic enumerations
+ enums,
+ IMultipleOutputChannelsWithChannelValues::outputChannelType, 15,
+ _T("float"), TYPE_FLOAT,
+ _T("integer"), TYPE_INT,
+ _T("rgb"), TYPE_RGBA,
+ _T("point3"), TYPE_POINT3,
+ _T("string"), TYPE_STRING,
+ _T("filename"), TYPE_FILENAME,
+ _T("material"), TYPE_MTL,
+ _T("texturemap"), TYPE_TEXMAP,
+ _T("bitmap"), TYPE_BITMAP,
+ _T("node"), TYPE_INODE,
+ _T("maxObject"), TYPE_REFTARG,
+ _T("matrix3"), TYPE_MATRIX3,
+ _T("paramblock2"), TYPE_PBLOCK2,
+ _T("point4"), TYPE_POINT4,
+ _T("frgba"), TYPE_FRGBA,
+ p_end
+);
diff --git a/additional_includes/MaxRestrictedSdk/2023/iparamb3.h b/additional_includes/MaxRestrictedSdk/2023/iparamb3.h
new file mode 100644
index 0000000..b869d38
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2023/iparamb3.h
@@ -0,0 +1,30 @@
+//**************************************************************************/
+// Copyright (c) 2008 Autodesk, Inc.
+// All rights reserved.
+//
+// These coded instructions, statements, and computer programs contain
+// unpublished proprietary information written by Autodesk, Inc., and are
+// protected by Federal copyright law. They may not be disclosed to third
+// parties or copied or duplicated in any form, in whole or in part, without
+// the prior written consent of Autodesk, Inc.
+//**************************************************************************/
+#pragma once
+
+#include
+
+// New types for IParamBlock3
+// Notice currently(R13) we don't need support for TYPE_BOOL2_TAB, TYPE_BOOL3_TAB, TYPE_BOOL4_TAB...
+// So these are not defined, but they are expected to be defined in the future if needed.
+// Should these parameter types be processed in RescaleParam? Currently are.
+enum ParamType3 {
+ TYPE_BOOL2 = TYPE_UNSPECIFIED + 1,
+ TYPE_BOOL3,
+ TYPE_BOOL4,
+ TYPE_INT2,
+ TYPE_INT3,
+ TYPE_INT4,
+ TYPE_INT2_TAB = TYPE_INT2 + TYPE_TAB,
+ TYPE_INT3_TAB = TYPE_INT3 + TYPE_TAB,
+ TYPE_INT4_TAB = TYPE_INT4 + TYPE_TAB,
+ TYPE_MAX_TYPE3,
+};
diff --git a/additional_includes/MaxRestrictedSdk/2024/IMultipleOutputChannel/IMultiOutputConsumer.h b/additional_includes/MaxRestrictedSdk/2024/IMultipleOutputChannel/IMultiOutputConsumer.h
new file mode 100644
index 0000000..32dc19a
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2024/IMultipleOutputChannel/IMultiOutputConsumer.h
@@ -0,0 +1,154 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#include "../../maxsdk/include/ifnpub.h"
+#include
+#define IMULTIOUTPUT_CONSUMER_INTERFACE Interface_ID( 0x6d4a30ed, 0x61024d74 )
+
+#define IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX ( -1 )
+
+#define REFMSG_MULTIOUTPUT_CONSUMER_NEEDUPDATE REFMSG_USER + 0x13654852
+
+// forward declarations
+class ReferenceTarget;
+class ReferenceMaker;
+
+//! \brief An interface for objects that reference other objects that implement IMultipleOutputChannels and acquire output channel data from those objects.
+/*! \sa Class IMultipleOutputChannels, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED
+ An object can acquire output channel data from another object by requesting the IMultipleOutputChannels interface from that object, and then calling
+ GetOutputChannelValue(int index) on that interface. This interface provides read/write access to the specification of which source objects implementing
+ IMultipleOutputChannels are being used and which output channel indices to use on those objects.
+ This interface is used by Schematic Material Editor to help create, modify, and display objects deriving from this interface.
+*/
+
+class IMultiOutputConsumer: public FPMixinInterface
+{
+public:
+
+ virtual int GetNumInputs() const = 0;
+ virtual bool SetOutputToInput( int input_index, ReferenceTarget* output_rtarg, int output_index ) = 0;
+ virtual bool GetOutputFromInput( int input_index, ReferenceTarget*& output_rtarg, int& output_index ) const = 0;
+ virtual bool CanAssignOutputToInput( int input_index, ReferenceTarget* output_rtarg, int output_index ) const = 0;
+ virtual MSTR GetInputName( int input_index ) const = 0;
+ virtual MSTR GetInputLocalizedName( int input_index ) const = 0;
+ virtual ParamType3 GetInputType( int input_index ) const = 0;
+
+ FPInterfaceDesc* GetDesc() override; // <-- must implement
+private:
+
+ ReferenceTarget* MXS_GetOutputRefTargetFromInput( int input_index )
+ {
+ ReferenceTarget* rtarg = nullptr;
+ int output_index = IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ if( GetOutputFromInput( input_index, rtarg, output_index ) )
+ {
+ return rtarg;
+ }
+ return nullptr;
+ }
+
+ int MXS_GetOutputIndexFromInput( int input_index )
+ {
+ ReferenceTarget* rtarg = nullptr;
+ int output_index = IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ if( GetOutputFromInput( input_index, rtarg, output_index ) )
+ {
+ return output_index;
+ }
+ return IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ }
+
+public:
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum {
+ imultioutputconsumer_getnuminputs,
+ imultioutputconsumer_setoutputtoinput,
+ imultioutputconsumer_getoutputrtargfrominput,
+ imultioutputconsumer_getoutputindexfrominput,
+ imultioutputconsumer_canassignoutputtoinput,
+ imultioutputconsumer_getinputname,
+ imultioutputconsumer_getinputlocalizedname
+ };
+
+ #pragma warning(push)
+ #pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP
+ RO_PROP_FN( imultioutputconsumer_getnuminputs, GetNumInputs, TYPE_INT );
+ FN_3( imultioutputconsumer_setoutputtoinput, TYPE_bool, SetOutputToInput, TYPE_INDEX, TYPE_REFTARG, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getoutputrtargfrominput, TYPE_REFTARG, MXS_GetOutputRefTargetFromInput, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getoutputindexfrominput, TYPE_INDEX, MXS_GetOutputIndexFromInput, TYPE_INDEX );
+ FN_3( imultioutputconsumer_canassignoutputtoinput, TYPE_bool, CanAssignOutputToInput, TYPE_INDEX, TYPE_REFTARG, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getinputname, TYPE_TSTR_BV, GetInputName, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getinputlocalizedname, TYPE_TSTR_BV, GetInputLocalizedName, TYPE_INDEX );
+ END_FUNCTION_MAP
+ #pragma warning(pop)
+};
+
+
+// ----------------------------------------------------------------------------
+/** \brief Function Publishing descriptor for Mixin interface on
+ * IMultipleOutputChannels-derived classes
+ *
+ * This interface needs to be manually added to the ClassDesc for
+ * IMultipleOutputChannels-derived objects using ClassDesc::AddInterface. This
+ * is typically performed in the ClassDesc::Create method.
+ *
+ * \see IMultipleOutputChannels
+ */
+static FPInterfaceDesc imultioutput_consumer_interface(
+ IMULTIOUTPUT_CONSUMER_INTERFACE, _T("iMultiOutputConsumer"), 0, nullptr, FP_MIXIN,
+
+ IMultiOutputConsumer::imultioutputconsumer_setoutputtoinput, _T("SetOutputToInput"), 0, TYPE_bool, 0, 3,
+ _T("input_index"), 0, TYPE_INDEX,
+ _T("output_rtarg"), 0, TYPE_REFTARG,
+ _T("output_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getoutputrtargfrominput, _T("GetOutputRefTargetFromInput"), 0, TYPE_REFTARG, 0, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getoutputindexfrominput, _T("GetOutputIndexFromInput"), 0, TYPE_INDEX, 0, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_canassignoutputtoinput, _T("CanAssignOutputToInput"), 0, TYPE_bool, 0, 3,
+ _T("input_index"), 0, TYPE_INDEX,
+ _T("output_rtarg"), 0, TYPE_REFTARG,
+ _T("output_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getinputname, _T("GetInputName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getinputlocalizedname, _T("GetInputLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ properties,
+
+ IMultiOutputConsumer::imultioutputconsumer_getnuminputs, FP_NO_FUNCTION, _T("numInputs"), FP_NO_REDRAW, TYPE_INT,
+
+ p_end
+);
+
+// ----------------------------------------------------------------------------
+/** Convenience helper function to retrieve the IMultiOutputConsumer interface
+ * from an ReferenceTarget.
+ * \param [in] rtarg The ReferenceTarget to be asked for the interface. Will be
+ * internally checked for nullptr, so there is no need to
+ * check outside.
+ * \returns A pointer to the IMultiOutputConsumer interface of the given
+ * ReferenceTarget, if it supports it, or a nullptr.
+ * \see IMultiOutputConsumer, imultioutput_consumer_interface
+ */
+template
+inline IMultiOutputConsumer* GetIMultiOutputConsumer(T* rtarg)
+{
+ return rtarg ? static_cast(rtarg->GetInterface(IMULTIOUTPUT_CONSUMER_INTERFACE)) : nullptr;
+}
\ No newline at end of file
diff --git a/additional_includes/MaxRestrictedSdk/2024/IMultipleOutputChannel/IMultipleOutputChannels.h b/additional_includes/MaxRestrictedSdk/2024/IMultipleOutputChannel/IMultipleOutputChannels.h
new file mode 100644
index 0000000..4e1ad28
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2024/IMultipleOutputChannel/IMultipleOutputChannels.h
@@ -0,0 +1,166 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#define IMULTIPLEOUTPUTCHANNELS_INTERFACE Interface_ID(0x43147cc9, 0x600e29ff)
+
+#include "../../maxsdk/include/strclass.h"
+#include "../../maxsdk/include/ifnpub.h"
+#include
+
+//! \brief This notification is sent to dependents when a IMultipleOutputChannels's output channel list changes.
+/*! It is sent by IMultipleOutputChannels-derived objects to tell dependents when the number or ordering
+of output channels changes, so those objects can keep pointing at the correct output channel.
+The PartID is a pointer to a Tab of MultiOutputChannelNumberChanged structure instances (defined in
+IMultipleOutputChannels.h) in which each element contains an old-to-new mapping. A new channel index
+of -1 implies the channel was removed. A old channel index of -1 implies the channel was added.
+IMultipleOutputChannelsConsumerWrapper-derived objects typically consume this notification.
+NOTE: If you send this message, the 'propagate' argument of NotifyDependents must be false.
+Otherwise, dependents of dependents think that their ref's output channel list is changing.*/
+#define REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED REFMSG_USER+0x13654850
+
+#define REFMSG_MULTIOUTPUT_CHANNEL_NEEDUPDATE REFMSG_USER+0x13654851
+
+//-------------------------------------------------------------
+//! \brief An interface for objects that that expose multiple output channels of various types that can be recognized by 3ds Max.
+/*! \sa Class IMultipleOutputChannelsConsumer, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED, imultioutput_interface
+This interface provides support for exposing multiple output channels. Each output channel has its own data type.
+
+This interface is primarily to support MetaSL and MR objects that have multiple outputs (for example, an XYZ and a derivative XYZ). Those objects will expose each output as a Texmap.
+
+Basic ParamBlock2 data types are supported by the interface, but not Tab data types. The interface could be expanded to handle Tab data types.
+
+Note: if you derive from this interface, see notification code REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED for information on how this notification should be sent or processed.
+
+Note: Classes that derive from this interface need to manually add the imultioutput_interface FPInterfaceDesc to their ClassDesc using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+#pragma warning(push)
+#pragma warning(disable:4100) // Unused parameters.
+
+class IMultipleOutputChannels: public FPMixinInterface
+{
+public:
+ //! \brief Returns the number of output channels the object exposes.
+ /*! The plugin implements this method to indicate the number of output channels it exposes.
+ \return The number of output channels the object exposes.
+ */
+ virtual int GetNumIMultipleOutputChannels() const = 0;
+
+ //! \brief Returns the localized name for the specified output channel.
+ /*! The plugin implements this method to provide the localized names of the output channels it exposes. These names will be used in the UI.
+ \param[in] index - The index of the output channel.
+ \return The localized name for the specified output channel.
+ */
+ virtual MSTR GetIMultipleOutputChannelLocalizedName( int index ) const = 0;
+
+ //! \brief Returns the non-localized name for the specified output channel.
+ /*! The plugin implements this method to provide the non-localized names of the output channels it exposes. These would typically be used in scripts to provide locale independence.
+ \param[in] index - The index of the output channel.
+ \return The non-localized name for the specified output channel.
+ */
+ virtual MSTR GetIMultipleOutputChannelName( int index ) const = 0;
+
+ //! \brief Returns the Parameter type for the specified output channel.
+ /*! The plugin implements this method to provide the parameter type of the output channels it exposes. The type can be used for parameter validation
+ between input and output channels.
+ \param[in] index - The index of the output channel.
+ \return The parameter type of the output channel.
+ Note: the data type will correspond to one of the data types supported by the ParamBlock2 system, tabs are not supported
+ */
+ virtual ParamType3 GetIMultipleOutputChannelType( int index) const =0;
+
+ //! \brief Data structure for REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED notifications
+ /*! A REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED notification sends a Tab*, which provides old-to-new output channel indexing.
+ An old index value of -1 means the output channel is new, a new output index of -1 means the old output channel was removed.
+ */
+ struct MultiOutputChannelNumberChanged
+ {
+ int oldIndex;
+ int newIndex;
+ };
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum { getnumoutputs, getlocalizedoutputname, getoutputname };
+ // FP-published symbolic enumerations
+
+#pragma warning(push)
+#pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP
+ RO_PROP_FN(getnumoutputs, GetNumIMultipleOutputChannels, TYPE_INT);
+ FN_1( getlocalizedoutputname, TYPE_TSTR_BV, MXS_GetIMultipleOutputChannelLocalizedName, TYPE_INDEX );
+ FN_1( getoutputname, TYPE_TSTR_BV, MXS_GetIMultipleOutputChannelName, TYPE_INDEX );
+ END_FUNCTION_MAP
+#pragma warning(pop)
+
+ FPInterfaceDesc* GetDesc() override; // <-- must implement
+
+protected:
+ // FPS exposure stub methods
+ void ValidateIMultipleOutputChannelIndexValue(int index) const
+ {
+ if (index < 0 || index >= GetNumIMultipleOutputChannels())
+ throw MAXException(_M("Invalid IMultiOutput channel index"));
+ }
+private:
+ MSTR MXS_GetIMultipleOutputChannelLocalizedName( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelLocalizedName(index);
+ }
+ MSTR MXS_GetIMultipleOutputChannelName( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelName(index);
+ }
+};
+#pragma warning(pop)
+
+// ----------------------------------------------------------------------------
+/** \brief Function Publishing descriptor for Mixin interface on
+ * IMultipleOutputChannels-derived classes.
+ *
+ * This interface needs to be manually added to the ClassDesc for
+ * IMultipleOutputChannels-derived objects using ClassDesc::AddInterface. This
+ * is typically performed in the ClassDesc::Create method.
+ *
+ * \see IMultipleOutputChannels
+ */
+
+static FPInterfaceDesc imultioutput_interface(
+ IMULTIPLEOUTPUTCHANNELS_INTERFACE, _T("iMultipleOutputChannels"), 0, nullptr, FP_MIXIN,
+ IMultipleOutputChannels::getlocalizedoutputname, _T("getIMultipleOutputChannelLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannels::getoutputname, _T("getIMultipleOutputChannelName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+
+ properties,
+ IMultipleOutputChannels::getnumoutputs, FP_NO_FUNCTION, _T("numIMultipleOutputChannels"),FP_NO_REDRAW,TYPE_INT,
+
+ p_end
+);
+
+
+// ----------------------------------------------------------------------------
+/** Convenience helper function to retrieve the IMultipleOutputChannels
+ * interface from an ReferenceTarget.
+ * \param [in] rtarg The ReferenceTarget to be asked for the interface. Will be
+ * internally checked for nullptr, so there is no need to
+ * check outside.
+ * \returns A pointer to the IMultipleOutputChannels interface of the given
+ * ReferenceTarget, if it supports it, or a nullptr.
+ * \see IMultipleOutputChannels, imultioutput_interface
+ */
+
+template
+inline IMultipleOutputChannels* GetIMultipleOutputChannels(T* rtarg)
+{
+ return rtarg ? static_cast(rtarg->GetInterface(IMULTIPLEOUTPUTCHANNELS_INTERFACE)) : nullptr;
+}
\ No newline at end of file
diff --git a/additional_includes/MaxRestrictedSdk/2024/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h b/additional_includes/MaxRestrictedSdk/2024/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h
new file mode 100644
index 0000000..1058b99
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2024/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h
@@ -0,0 +1,288 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#define IMULTIPLEOUTPUTCHANNELS_WITH_VALUES_INTERFACE Interface_ID(0x24280bd5, 0x22b1edca)
+
+#include "IMultipleOutputChannels.h"
+#include "../../maxsdk/include/paramtype.h"
+#include "../../maxsdk/include/strclass.h"
+#include "../../maxsdk/include/ifnpub.h"
+#include "../../maxsdk/include/interval.h"
+
+// forward declarations
+class Point3;
+class Point4;
+class Color;
+class AColor;
+class Mtl;
+class Texmap;
+class PBBitmap;
+class INode;
+class ReferenceTarget;
+class IParamBlock2;
+class Matrix3;
+namespace MaxSDK
+{
+ namespace AssetManagement
+ {
+ class AssetUser;
+ }
+}
+
+//-------------------------------------------------------------
+//! \brief An interface for objects that that expose multiple output channels of various types that can be recognized by 3ds Max.
+/*! \sa Class IMultipleOutputChannelsConsumer, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED, imultioutput_interface
+This interface provides support for exposing multiple output channels. Each output channel has its own data type.
+
+This interface is primarily to support MetaSL and MR objects that have multiple outputs (for example, an XYZ and a derivative XYZ). Those objects will expose each output as a Texmap.
+
+Basic ParamBlock2 data types are supported by the interface, but not Tab data types. The interface could be expanded to handle Tab data types.
+
+Note: if you derive from this interface, see notification code REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED for information on how this notification should be sent or processed.
+
+Note: Classes that derive from this interface need to manually add the imultioutput_with_values_interface FPInterfaceDesc to their ClassDesc using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+#pragma warning(push)
+#pragma warning(disable:4100) // Unused parameters.
+
+class IMultipleOutputChannelsWithChannelValues: public IMultipleOutputChannels
+{
+public:
+ //! \brief Indicates whether a REFMSG_CHANGED notification received from the object should be propagated from dependents using just the channel output value.
+ /*! The plugin implements this method to optimize REFMSG_CHANGE propagation. An object using an output channel value from this object will receive REFMSG_CHANGE notifications for
+ all changes to this object, even if that change does not affect the contents of the value retrieved from an output channel. The REFMSG_CHANGE notification does not need to
+ propagate from that object since its dependents are not affected by the change. If this method returns false, the object can return REF_STOP from its NotifyRefChanged if this object
+ is the target object.
+ \param[in] index - The index of the output channel.
+ \return false if a REFMSG_CHANGE notification from this object can be blocked from propagating from objects using just the specified output channel's data value.
+ */
+ virtual bool GetIMultipleOutputChannelValueChanged( int index) const { return true; }
+
+ //! \brief Returns the output data value for the specified output channel.
+ /*! The plugin implements this method to return the output channel's data value as an FPValue.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return The data value for the specified output channel as an FPValue.
+ */
+ virtual FPValue GetIMultipleOutputChannelValue( int index, TimeValue t, Interval& ivalid ) const = 0;
+
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, float& v, Interval& ivalid ) const { return false; } // TYPE_FLOAT
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, int& v, Interval& ivalid ) const { return false; } // TYPE_INT
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Color& v, Interval& ivalid ) const { return false; } // TYPE_RGBA
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Point3& v, Interval& ivalid ) const { return false; } // TYPE_POINT3
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, const MCHAR*& v, Interval& ivalid ) const { return false; } // TYPE_STRING
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, const MaxSDK::AssetManagement::AssetUser *& v, Interval& ivalid ) const { return false; } // TYPE_FILENAME
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Mtl*& v, Interval& ivalid ) const { return false; } // TYPE_MTL
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Texmap*& v, Interval& ivalid ) const { return false; } // TYPE_TEXMAP
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, PBBitmap*& v, Interval& ivalid ) const { return false; } // TYPE_BITMAP
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, INode*& v, Interval& ivalid ) const { return false; } // TYPE_INODE
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, ReferenceTarget*& v, Interval& ivalid ) const { return false; } // TYPE_REFTARG
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Matrix3& v, Interval& ivalid ) const { return false; } // TYPE_MATRIX3
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, IParamBlock2*& v, Interval& ivalid ) const { return false; } // TYPE_PBLOCK2
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Point4& v, Interval& ivalid ) const { return false; } // TYPE_POINT4
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, AColor& v, Interval& ivalid ) const { return false; } // TYPE_FRGBA
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum { getoutputtype = IMultipleOutputChannels::getoutputname+1, getoutputvalue };
+ // FP-published symbolic enumerations
+ enum
+ {
+ outputChannelType,
+ };
+#pragma warning(push)
+#pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP_PARENT(IMultipleOutputChannels)
+ FN_1( getoutputtype, TYPE_ENUM, MXS_GetIMultipleOutputChannelType, TYPE_INDEX );
+ FN_2( getoutputvalue, TYPE_FPVALUE_BV, MXS_GetIMultipleOutputChannelValue, TYPE_INDEX, TYPE_TIMEVALUE );
+ END_FUNCTION_MAP
+#pragma warning(pop)
+
+ FPInterfaceDesc* GetDesc(); // <-- must implement
+
+private:
+ DWORD MXS_GetIMultipleOutputChannelType( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelType(index);
+ }
+ FPValue MXS_GetIMultipleOutputChannelValue( int index, TimeValue t ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ Interval valid;
+ return GetIMultipleOutputChannelValue(index, t, valid);
+ }
+};
+
+
+#pragma warning(pop)
+//-------------------------------------------------------------
+//! \brief Function Publishing descriptor for Mixin interface on iMultipleOutputChannelsWithValues-derived classes
+/*! \sa Class IMultipleOutputChannels
+This interface needs to be manually added to the ClassDesc for iMultipleOutputChannelsWithValues-derived objects using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+
+*/
+static FPInterfaceDesc imultioutput_with_values_interface(
+ IMULTIPLEOUTPUTCHANNELS_WITH_VALUES_INTERFACE, _T("iMultipleOutputChannelsWithValues"), 0, NULL, FP_MIXIN,
+ IMultipleOutputChannels::getlocalizedoutputname, _T("getIMultipleOutputChannelLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannels::getoutputname, _T("getIMultipleOutputChannelName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannelsWithChannelValues::getoutputtype, _T("getIMultipleOutputChannelType"), 0, TYPE_ENUM, IMultipleOutputChannelsWithChannelValues::outputChannelType, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannelsWithChannelValues::getoutputvalue, _T("getIMultipleOutputChannelValue"), 0, TYPE_FPVALUE_BV, FP_NO_REDRAW, 2,
+ _T("index"), 0, TYPE_INDEX,
+ _T("time"), 0, TYPE_TIMEVALUE, f_keyArgDefault, 0,
+
+ properties,
+ IMultipleOutputChannels::getnumoutputs, FP_NO_FUNCTION, _T("numIMultipleOutputChannels"),FP_NO_REDRAW,TYPE_INT,
+
+ // symbolic enumerations
+ enums,
+ IMultipleOutputChannelsWithChannelValues::outputChannelType, 15,
+ _T("float"), TYPE_FLOAT,
+ _T("integer"), TYPE_INT,
+ _T("rgb"), TYPE_RGBA,
+ _T("point3"), TYPE_POINT3,
+ _T("string"), TYPE_STRING,
+ _T("filename"), TYPE_FILENAME,
+ _T("material"), TYPE_MTL,
+ _T("texturemap"), TYPE_TEXMAP,
+ _T("bitmap"), TYPE_BITMAP,
+ _T("node"), TYPE_INODE,
+ _T("maxObject"), TYPE_REFTARG,
+ _T("matrix3"), TYPE_MATRIX3,
+ _T("paramblock2"), TYPE_PBLOCK2,
+ _T("point4"), TYPE_POINT4,
+ _T("frgba"), TYPE_FRGBA,
+ p_end
+);
diff --git a/additional_includes/MaxRestrictedSdk/2024/iparamb3.h b/additional_includes/MaxRestrictedSdk/2024/iparamb3.h
new file mode 100644
index 0000000..b869d38
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2024/iparamb3.h
@@ -0,0 +1,30 @@
+//**************************************************************************/
+// Copyright (c) 2008 Autodesk, Inc.
+// All rights reserved.
+//
+// These coded instructions, statements, and computer programs contain
+// unpublished proprietary information written by Autodesk, Inc., and are
+// protected by Federal copyright law. They may not be disclosed to third
+// parties or copied or duplicated in any form, in whole or in part, without
+// the prior written consent of Autodesk, Inc.
+//**************************************************************************/
+#pragma once
+
+#include
+
+// New types for IParamBlock3
+// Notice currently(R13) we don't need support for TYPE_BOOL2_TAB, TYPE_BOOL3_TAB, TYPE_BOOL4_TAB...
+// So these are not defined, but they are expected to be defined in the future if needed.
+// Should these parameter types be processed in RescaleParam? Currently are.
+enum ParamType3 {
+ TYPE_BOOL2 = TYPE_UNSPECIFIED + 1,
+ TYPE_BOOL3,
+ TYPE_BOOL4,
+ TYPE_INT2,
+ TYPE_INT3,
+ TYPE_INT4,
+ TYPE_INT2_TAB = TYPE_INT2 + TYPE_TAB,
+ TYPE_INT3_TAB = TYPE_INT3 + TYPE_TAB,
+ TYPE_INT4_TAB = TYPE_INT4 + TYPE_TAB,
+ TYPE_MAX_TYPE3,
+};
diff --git a/additional_includes/MaxRestrictedSdk/2025/IMultipleOutputChannel/IMultiOutputConsumer.h b/additional_includes/MaxRestrictedSdk/2025/IMultipleOutputChannel/IMultiOutputConsumer.h
new file mode 100644
index 0000000..32dc19a
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2025/IMultipleOutputChannel/IMultiOutputConsumer.h
@@ -0,0 +1,154 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#include "../../maxsdk/include/ifnpub.h"
+#include
+#define IMULTIOUTPUT_CONSUMER_INTERFACE Interface_ID( 0x6d4a30ed, 0x61024d74 )
+
+#define IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX ( -1 )
+
+#define REFMSG_MULTIOUTPUT_CONSUMER_NEEDUPDATE REFMSG_USER + 0x13654852
+
+// forward declarations
+class ReferenceTarget;
+class ReferenceMaker;
+
+//! \brief An interface for objects that reference other objects that implement IMultipleOutputChannels and acquire output channel data from those objects.
+/*! \sa Class IMultipleOutputChannels, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED
+ An object can acquire output channel data from another object by requesting the IMultipleOutputChannels interface from that object, and then calling
+ GetOutputChannelValue(int index) on that interface. This interface provides read/write access to the specification of which source objects implementing
+ IMultipleOutputChannels are being used and which output channel indices to use on those objects.
+ This interface is used by Schematic Material Editor to help create, modify, and display objects deriving from this interface.
+*/
+
+class IMultiOutputConsumer: public FPMixinInterface
+{
+public:
+
+ virtual int GetNumInputs() const = 0;
+ virtual bool SetOutputToInput( int input_index, ReferenceTarget* output_rtarg, int output_index ) = 0;
+ virtual bool GetOutputFromInput( int input_index, ReferenceTarget*& output_rtarg, int& output_index ) const = 0;
+ virtual bool CanAssignOutputToInput( int input_index, ReferenceTarget* output_rtarg, int output_index ) const = 0;
+ virtual MSTR GetInputName( int input_index ) const = 0;
+ virtual MSTR GetInputLocalizedName( int input_index ) const = 0;
+ virtual ParamType3 GetInputType( int input_index ) const = 0;
+
+ FPInterfaceDesc* GetDesc() override; // <-- must implement
+private:
+
+ ReferenceTarget* MXS_GetOutputRefTargetFromInput( int input_index )
+ {
+ ReferenceTarget* rtarg = nullptr;
+ int output_index = IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ if( GetOutputFromInput( input_index, rtarg, output_index ) )
+ {
+ return rtarg;
+ }
+ return nullptr;
+ }
+
+ int MXS_GetOutputIndexFromInput( int input_index )
+ {
+ ReferenceTarget* rtarg = nullptr;
+ int output_index = IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ if( GetOutputFromInput( input_index, rtarg, output_index ) )
+ {
+ return output_index;
+ }
+ return IMULTIOUTPUT_CONSUMER_NO_OUTPUT_INDEX;
+ }
+
+public:
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum {
+ imultioutputconsumer_getnuminputs,
+ imultioutputconsumer_setoutputtoinput,
+ imultioutputconsumer_getoutputrtargfrominput,
+ imultioutputconsumer_getoutputindexfrominput,
+ imultioutputconsumer_canassignoutputtoinput,
+ imultioutputconsumer_getinputname,
+ imultioutputconsumer_getinputlocalizedname
+ };
+
+ #pragma warning(push)
+ #pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP
+ RO_PROP_FN( imultioutputconsumer_getnuminputs, GetNumInputs, TYPE_INT );
+ FN_3( imultioutputconsumer_setoutputtoinput, TYPE_bool, SetOutputToInput, TYPE_INDEX, TYPE_REFTARG, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getoutputrtargfrominput, TYPE_REFTARG, MXS_GetOutputRefTargetFromInput, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getoutputindexfrominput, TYPE_INDEX, MXS_GetOutputIndexFromInput, TYPE_INDEX );
+ FN_3( imultioutputconsumer_canassignoutputtoinput, TYPE_bool, CanAssignOutputToInput, TYPE_INDEX, TYPE_REFTARG, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getinputname, TYPE_TSTR_BV, GetInputName, TYPE_INDEX );
+ FN_1( imultioutputconsumer_getinputlocalizedname, TYPE_TSTR_BV, GetInputLocalizedName, TYPE_INDEX );
+ END_FUNCTION_MAP
+ #pragma warning(pop)
+};
+
+
+// ----------------------------------------------------------------------------
+/** \brief Function Publishing descriptor for Mixin interface on
+ * IMultipleOutputChannels-derived classes
+ *
+ * This interface needs to be manually added to the ClassDesc for
+ * IMultipleOutputChannels-derived objects using ClassDesc::AddInterface. This
+ * is typically performed in the ClassDesc::Create method.
+ *
+ * \see IMultipleOutputChannels
+ */
+static FPInterfaceDesc imultioutput_consumer_interface(
+ IMULTIOUTPUT_CONSUMER_INTERFACE, _T("iMultiOutputConsumer"), 0, nullptr, FP_MIXIN,
+
+ IMultiOutputConsumer::imultioutputconsumer_setoutputtoinput, _T("SetOutputToInput"), 0, TYPE_bool, 0, 3,
+ _T("input_index"), 0, TYPE_INDEX,
+ _T("output_rtarg"), 0, TYPE_REFTARG,
+ _T("output_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getoutputrtargfrominput, _T("GetOutputRefTargetFromInput"), 0, TYPE_REFTARG, 0, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getoutputindexfrominput, _T("GetOutputIndexFromInput"), 0, TYPE_INDEX, 0, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_canassignoutputtoinput, _T("CanAssignOutputToInput"), 0, TYPE_bool, 0, 3,
+ _T("input_index"), 0, TYPE_INDEX,
+ _T("output_rtarg"), 0, TYPE_REFTARG,
+ _T("output_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getinputname, _T("GetInputName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ IMultiOutputConsumer::imultioutputconsumer_getinputlocalizedname, _T("GetInputLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("input_index"), 0, TYPE_INDEX,
+
+ properties,
+
+ IMultiOutputConsumer::imultioutputconsumer_getnuminputs, FP_NO_FUNCTION, _T("numInputs"), FP_NO_REDRAW, TYPE_INT,
+
+ p_end
+);
+
+// ----------------------------------------------------------------------------
+/** Convenience helper function to retrieve the IMultiOutputConsumer interface
+ * from an ReferenceTarget.
+ * \param [in] rtarg The ReferenceTarget to be asked for the interface. Will be
+ * internally checked for nullptr, so there is no need to
+ * check outside.
+ * \returns A pointer to the IMultiOutputConsumer interface of the given
+ * ReferenceTarget, if it supports it, or a nullptr.
+ * \see IMultiOutputConsumer, imultioutput_consumer_interface
+ */
+template
+inline IMultiOutputConsumer* GetIMultiOutputConsumer(T* rtarg)
+{
+ return rtarg ? static_cast(rtarg->GetInterface(IMULTIOUTPUT_CONSUMER_INTERFACE)) : nullptr;
+}
\ No newline at end of file
diff --git a/additional_includes/MaxRestrictedSdk/2025/IMultipleOutputChannel/IMultipleOutputChannels.h b/additional_includes/MaxRestrictedSdk/2025/IMultipleOutputChannel/IMultipleOutputChannels.h
new file mode 100644
index 0000000..4e1ad28
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2025/IMultipleOutputChannel/IMultipleOutputChannels.h
@@ -0,0 +1,166 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#define IMULTIPLEOUTPUTCHANNELS_INTERFACE Interface_ID(0x43147cc9, 0x600e29ff)
+
+#include "../../maxsdk/include/strclass.h"
+#include "../../maxsdk/include/ifnpub.h"
+#include
+
+//! \brief This notification is sent to dependents when a IMultipleOutputChannels's output channel list changes.
+/*! It is sent by IMultipleOutputChannels-derived objects to tell dependents when the number or ordering
+of output channels changes, so those objects can keep pointing at the correct output channel.
+The PartID is a pointer to a Tab of MultiOutputChannelNumberChanged structure instances (defined in
+IMultipleOutputChannels.h) in which each element contains an old-to-new mapping. A new channel index
+of -1 implies the channel was removed. A old channel index of -1 implies the channel was added.
+IMultipleOutputChannelsConsumerWrapper-derived objects typically consume this notification.
+NOTE: If you send this message, the 'propagate' argument of NotifyDependents must be false.
+Otherwise, dependents of dependents think that their ref's output channel list is changing.*/
+#define REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED REFMSG_USER+0x13654850
+
+#define REFMSG_MULTIOUTPUT_CHANNEL_NEEDUPDATE REFMSG_USER+0x13654851
+
+//-------------------------------------------------------------
+//! \brief An interface for objects that that expose multiple output channels of various types that can be recognized by 3ds Max.
+/*! \sa Class IMultipleOutputChannelsConsumer, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED, imultioutput_interface
+This interface provides support for exposing multiple output channels. Each output channel has its own data type.
+
+This interface is primarily to support MetaSL and MR objects that have multiple outputs (for example, an XYZ and a derivative XYZ). Those objects will expose each output as a Texmap.
+
+Basic ParamBlock2 data types are supported by the interface, but not Tab data types. The interface could be expanded to handle Tab data types.
+
+Note: if you derive from this interface, see notification code REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED for information on how this notification should be sent or processed.
+
+Note: Classes that derive from this interface need to manually add the imultioutput_interface FPInterfaceDesc to their ClassDesc using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+#pragma warning(push)
+#pragma warning(disable:4100) // Unused parameters.
+
+class IMultipleOutputChannels: public FPMixinInterface
+{
+public:
+ //! \brief Returns the number of output channels the object exposes.
+ /*! The plugin implements this method to indicate the number of output channels it exposes.
+ \return The number of output channels the object exposes.
+ */
+ virtual int GetNumIMultipleOutputChannels() const = 0;
+
+ //! \brief Returns the localized name for the specified output channel.
+ /*! The plugin implements this method to provide the localized names of the output channels it exposes. These names will be used in the UI.
+ \param[in] index - The index of the output channel.
+ \return The localized name for the specified output channel.
+ */
+ virtual MSTR GetIMultipleOutputChannelLocalizedName( int index ) const = 0;
+
+ //! \brief Returns the non-localized name for the specified output channel.
+ /*! The plugin implements this method to provide the non-localized names of the output channels it exposes. These would typically be used in scripts to provide locale independence.
+ \param[in] index - The index of the output channel.
+ \return The non-localized name for the specified output channel.
+ */
+ virtual MSTR GetIMultipleOutputChannelName( int index ) const = 0;
+
+ //! \brief Returns the Parameter type for the specified output channel.
+ /*! The plugin implements this method to provide the parameter type of the output channels it exposes. The type can be used for parameter validation
+ between input and output channels.
+ \param[in] index - The index of the output channel.
+ \return The parameter type of the output channel.
+ Note: the data type will correspond to one of the data types supported by the ParamBlock2 system, tabs are not supported
+ */
+ virtual ParamType3 GetIMultipleOutputChannelType( int index) const =0;
+
+ //! \brief Data structure for REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED notifications
+ /*! A REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED notification sends a Tab*, which provides old-to-new output channel indexing.
+ An old index value of -1 means the output channel is new, a new output index of -1 means the old output channel was removed.
+ */
+ struct MultiOutputChannelNumberChanged
+ {
+ int oldIndex;
+ int newIndex;
+ };
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum { getnumoutputs, getlocalizedoutputname, getoutputname };
+ // FP-published symbolic enumerations
+
+#pragma warning(push)
+#pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP
+ RO_PROP_FN(getnumoutputs, GetNumIMultipleOutputChannels, TYPE_INT);
+ FN_1( getlocalizedoutputname, TYPE_TSTR_BV, MXS_GetIMultipleOutputChannelLocalizedName, TYPE_INDEX );
+ FN_1( getoutputname, TYPE_TSTR_BV, MXS_GetIMultipleOutputChannelName, TYPE_INDEX );
+ END_FUNCTION_MAP
+#pragma warning(pop)
+
+ FPInterfaceDesc* GetDesc() override; // <-- must implement
+
+protected:
+ // FPS exposure stub methods
+ void ValidateIMultipleOutputChannelIndexValue(int index) const
+ {
+ if (index < 0 || index >= GetNumIMultipleOutputChannels())
+ throw MAXException(_M("Invalid IMultiOutput channel index"));
+ }
+private:
+ MSTR MXS_GetIMultipleOutputChannelLocalizedName( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelLocalizedName(index);
+ }
+ MSTR MXS_GetIMultipleOutputChannelName( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelName(index);
+ }
+};
+#pragma warning(pop)
+
+// ----------------------------------------------------------------------------
+/** \brief Function Publishing descriptor for Mixin interface on
+ * IMultipleOutputChannels-derived classes.
+ *
+ * This interface needs to be manually added to the ClassDesc for
+ * IMultipleOutputChannels-derived objects using ClassDesc::AddInterface. This
+ * is typically performed in the ClassDesc::Create method.
+ *
+ * \see IMultipleOutputChannels
+ */
+
+static FPInterfaceDesc imultioutput_interface(
+ IMULTIPLEOUTPUTCHANNELS_INTERFACE, _T("iMultipleOutputChannels"), 0, nullptr, FP_MIXIN,
+ IMultipleOutputChannels::getlocalizedoutputname, _T("getIMultipleOutputChannelLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannels::getoutputname, _T("getIMultipleOutputChannelName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+
+ properties,
+ IMultipleOutputChannels::getnumoutputs, FP_NO_FUNCTION, _T("numIMultipleOutputChannels"),FP_NO_REDRAW,TYPE_INT,
+
+ p_end
+);
+
+
+// ----------------------------------------------------------------------------
+/** Convenience helper function to retrieve the IMultipleOutputChannels
+ * interface from an ReferenceTarget.
+ * \param [in] rtarg The ReferenceTarget to be asked for the interface. Will be
+ * internally checked for nullptr, so there is no need to
+ * check outside.
+ * \returns A pointer to the IMultipleOutputChannels interface of the given
+ * ReferenceTarget, if it supports it, or a nullptr.
+ * \see IMultipleOutputChannels, imultioutput_interface
+ */
+
+template
+inline IMultipleOutputChannels* GetIMultipleOutputChannels(T* rtarg)
+{
+ return rtarg ? static_cast(rtarg->GetInterface(IMULTIPLEOUTPUTCHANNELS_INTERFACE)) : nullptr;
+}
\ No newline at end of file
diff --git a/additional_includes/MaxRestrictedSdk/2025/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h b/additional_includes/MaxRestrictedSdk/2025/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h
new file mode 100644
index 0000000..1058b99
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2025/IMultipleOutputChannel/IMultipleOutputChannelsWithChannelValues.h
@@ -0,0 +1,288 @@
+//*********************************************************************/
+// Copyright (c) 2009-2020 Autodesk, Inc.
+// All rights reserved.
+//
+// Use of this software is subject to the terms of the Autodesk license
+// agreement provided at the time of installation or download, or which
+// otherwise accompanies this software in either electronic or hard copy form.
+//*********************************************************************/
+
+#pragma once
+
+#define IMULTIPLEOUTPUTCHANNELS_WITH_VALUES_INTERFACE Interface_ID(0x24280bd5, 0x22b1edca)
+
+#include "IMultipleOutputChannels.h"
+#include "../../maxsdk/include/paramtype.h"
+#include "../../maxsdk/include/strclass.h"
+#include "../../maxsdk/include/ifnpub.h"
+#include "../../maxsdk/include/interval.h"
+
+// forward declarations
+class Point3;
+class Point4;
+class Color;
+class AColor;
+class Mtl;
+class Texmap;
+class PBBitmap;
+class INode;
+class ReferenceTarget;
+class IParamBlock2;
+class Matrix3;
+namespace MaxSDK
+{
+ namespace AssetManagement
+ {
+ class AssetUser;
+ }
+}
+
+//-------------------------------------------------------------
+//! \brief An interface for objects that that expose multiple output channels of various types that can be recognized by 3ds Max.
+/*! \sa Class IMultipleOutputChannelsConsumer, REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED, imultioutput_interface
+This interface provides support for exposing multiple output channels. Each output channel has its own data type.
+
+This interface is primarily to support MetaSL and MR objects that have multiple outputs (for example, an XYZ and a derivative XYZ). Those objects will expose each output as a Texmap.
+
+Basic ParamBlock2 data types are supported by the interface, but not Tab data types. The interface could be expanded to handle Tab data types.
+
+Note: if you derive from this interface, see notification code REFMSG_MULTIOUTPUT_CHANNEL_NUMBER_CHANGED for information on how this notification should be sent or processed.
+
+Note: Classes that derive from this interface need to manually add the imultioutput_with_values_interface FPInterfaceDesc to their ClassDesc using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+*/
+#pragma warning(push)
+#pragma warning(disable:4100) // Unused parameters.
+
+class IMultipleOutputChannelsWithChannelValues: public IMultipleOutputChannels
+{
+public:
+ //! \brief Indicates whether a REFMSG_CHANGED notification received from the object should be propagated from dependents using just the channel output value.
+ /*! The plugin implements this method to optimize REFMSG_CHANGE propagation. An object using an output channel value from this object will receive REFMSG_CHANGE notifications for
+ all changes to this object, even if that change does not affect the contents of the value retrieved from an output channel. The REFMSG_CHANGE notification does not need to
+ propagate from that object since its dependents are not affected by the change. If this method returns false, the object can return REF_STOP from its NotifyRefChanged if this object
+ is the target object.
+ \param[in] index - The index of the output channel.
+ \return false if a REFMSG_CHANGE notification from this object can be blocked from propagating from objects using just the specified output channel's data value.
+ */
+ virtual bool GetIMultipleOutputChannelValueChanged( int index) const { return true; }
+
+ //! \brief Returns the output data value for the specified output channel.
+ /*! The plugin implements this method to return the output channel's data value as an FPValue.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return The data value for the specified output channel as an FPValue.
+ */
+ virtual FPValue GetIMultipleOutputChannelValue( int index, TimeValue t, Interval& ivalid ) const = 0;
+
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, float& v, Interval& ivalid ) const { return false; } // TYPE_FLOAT
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, int& v, Interval& ivalid ) const { return false; } // TYPE_INT
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Color& v, Interval& ivalid ) const { return false; } // TYPE_RGBA
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Point3& v, Interval& ivalid ) const { return false; } // TYPE_POINT3
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, const MCHAR*& v, Interval& ivalid ) const { return false; } // TYPE_STRING
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, const MaxSDK::AssetManagement::AssetUser *& v, Interval& ivalid ) const { return false; } // TYPE_FILENAME
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Mtl*& v, Interval& ivalid ) const { return false; } // TYPE_MTL
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Texmap*& v, Interval& ivalid ) const { return false; } // TYPE_TEXMAP
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, PBBitmap*& v, Interval& ivalid ) const { return false; } // TYPE_BITMAP
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, INode*& v, Interval& ivalid ) const { return false; } // TYPE_INODE
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, ReferenceTarget*& v, Interval& ivalid ) const { return false; } // TYPE_REFTARG
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Matrix3& v, Interval& ivalid ) const { return false; } // TYPE_MATRIX3
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, IParamBlock2*& v, Interval& ivalid ) const { return false; } // TYPE_PBLOCK2
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, Point4& v, Interval& ivalid ) const { return false; } // TYPE_POINT4
+ //! \brief Access the output data value for the specified output channel.
+ /*! The plugin implements this method to access the output channel's data value as a float.
+ \param[in] index - The index of the output channel.
+ \param[in] t The time at which to get the value.
+ \param[out] v - The value to retrieve is returned here.
+ \param[out] ivalid - The validity interval of the value retrieved is returned here.
+ \return true if the output value was written to v.
+ */
+ virtual bool GetIMultipleOutputChannelValue( int index, TimeValue t, AColor& v, Interval& ivalid ) const { return false; } // TYPE_FRGBA
+
+ // FPMixinInterface exposure
+ // FP-published function IDs
+ enum { getoutputtype = IMultipleOutputChannels::getoutputname+1, getoutputvalue };
+ // FP-published symbolic enumerations
+ enum
+ {
+ outputChannelType,
+ };
+#pragma warning(push)
+#pragma warning(disable:4238)
+ BEGIN_FUNCTION_MAP_PARENT(IMultipleOutputChannels)
+ FN_1( getoutputtype, TYPE_ENUM, MXS_GetIMultipleOutputChannelType, TYPE_INDEX );
+ FN_2( getoutputvalue, TYPE_FPVALUE_BV, MXS_GetIMultipleOutputChannelValue, TYPE_INDEX, TYPE_TIMEVALUE );
+ END_FUNCTION_MAP
+#pragma warning(pop)
+
+ FPInterfaceDesc* GetDesc(); // <-- must implement
+
+private:
+ DWORD MXS_GetIMultipleOutputChannelType( int index ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ return GetIMultipleOutputChannelType(index);
+ }
+ FPValue MXS_GetIMultipleOutputChannelValue( int index, TimeValue t ) const
+ {
+ ValidateIMultipleOutputChannelIndexValue(index);
+ Interval valid;
+ return GetIMultipleOutputChannelValue(index, t, valid);
+ }
+};
+
+
+#pragma warning(pop)
+//-------------------------------------------------------------
+//! \brief Function Publishing descriptor for Mixin interface on iMultipleOutputChannelsWithValues-derived classes
+/*! \sa Class IMultipleOutputChannels
+This interface needs to be manually added to the ClassDesc for iMultipleOutputChannelsWithValues-derived objects using ClassDesc::AddInterface. This is typically performed in the ClassDesc::Create method.
+
+*/
+static FPInterfaceDesc imultioutput_with_values_interface(
+ IMULTIPLEOUTPUTCHANNELS_WITH_VALUES_INTERFACE, _T("iMultipleOutputChannelsWithValues"), 0, NULL, FP_MIXIN,
+ IMultipleOutputChannels::getlocalizedoutputname, _T("getIMultipleOutputChannelLocalizedName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannels::getoutputname, _T("getIMultipleOutputChannelName"), 0, TYPE_TSTR_BV, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannelsWithChannelValues::getoutputtype, _T("getIMultipleOutputChannelType"), 0, TYPE_ENUM, IMultipleOutputChannelsWithChannelValues::outputChannelType, FP_NO_REDRAW, 1,
+ _T("index"), 0, TYPE_INDEX,
+ IMultipleOutputChannelsWithChannelValues::getoutputvalue, _T("getIMultipleOutputChannelValue"), 0, TYPE_FPVALUE_BV, FP_NO_REDRAW, 2,
+ _T("index"), 0, TYPE_INDEX,
+ _T("time"), 0, TYPE_TIMEVALUE, f_keyArgDefault, 0,
+
+ properties,
+ IMultipleOutputChannels::getnumoutputs, FP_NO_FUNCTION, _T("numIMultipleOutputChannels"),FP_NO_REDRAW,TYPE_INT,
+
+ // symbolic enumerations
+ enums,
+ IMultipleOutputChannelsWithChannelValues::outputChannelType, 15,
+ _T("float"), TYPE_FLOAT,
+ _T("integer"), TYPE_INT,
+ _T("rgb"), TYPE_RGBA,
+ _T("point3"), TYPE_POINT3,
+ _T("string"), TYPE_STRING,
+ _T("filename"), TYPE_FILENAME,
+ _T("material"), TYPE_MTL,
+ _T("texturemap"), TYPE_TEXMAP,
+ _T("bitmap"), TYPE_BITMAP,
+ _T("node"), TYPE_INODE,
+ _T("maxObject"), TYPE_REFTARG,
+ _T("matrix3"), TYPE_MATRIX3,
+ _T("paramblock2"), TYPE_PBLOCK2,
+ _T("point4"), TYPE_POINT4,
+ _T("frgba"), TYPE_FRGBA,
+ p_end
+);
diff --git a/additional_includes/MaxRestrictedSdk/2025/iparamb3.h b/additional_includes/MaxRestrictedSdk/2025/iparamb3.h
new file mode 100644
index 0000000..b869d38
--- /dev/null
+++ b/additional_includes/MaxRestrictedSdk/2025/iparamb3.h
@@ -0,0 +1,30 @@
+//**************************************************************************/
+// Copyright (c) 2008 Autodesk, Inc.
+// All rights reserved.
+//
+// These coded instructions, statements, and computer programs contain
+// unpublished proprietary information written by Autodesk, Inc., and are
+// protected by Federal copyright law. They may not be disclosed to third
+// parties or copied or duplicated in any form, in whole or in part, without
+// the prior written consent of Autodesk, Inc.
+//**************************************************************************/
+#pragma once
+
+#include
+
+// New types for IParamBlock3
+// Notice currently(R13) we don't need support for TYPE_BOOL2_TAB, TYPE_BOOL3_TAB, TYPE_BOOL4_TAB...
+// So these are not defined, but they are expected to be defined in the future if needed.
+// Should these parameter types be processed in RescaleParam? Currently are.
+enum ParamType3 {
+ TYPE_BOOL2 = TYPE_UNSPECIFIED + 1,
+ TYPE_BOOL3,
+ TYPE_BOOL4,
+ TYPE_INT2,
+ TYPE_INT3,
+ TYPE_INT4,
+ TYPE_INT2_TAB = TYPE_INT2 + TYPE_TAB,
+ TYPE_INT3_TAB = TYPE_INT3 + TYPE_TAB,
+ TYPE_INT4_TAB = TYPE_INT4 + TYPE_TAB,
+ TYPE_MAX_TYPE3,
+};
diff --git a/build-scripts/Prepare-BinPackage.ps1 b/build-scripts/Prepare-BinPackage.ps1
new file mode 100644
index 0000000..f2f45eb
--- /dev/null
+++ b/build-scripts/Prepare-BinPackage.ps1
@@ -0,0 +1,52 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+Param(
+ [Parameter(Mandatory=$true, HelpMessage="Source component folder")]
+ [String]$SourceFolder,
+ [Parameter(Mandatory=$true, HelpMessage="Destination component folder")]
+ [String]$DestinationFolder,
+ [Parameter(Mandatory=$true, HelpMessage="Max Target Version")]
+ [String]$TargetVersion,
+ [Parameter(Mandatory=$true, HelpMessage="Component version")]
+ [String]$ComponentVersion,
+ [Parameter(Mandatory=$true, HelpMessage="Build number")]
+ [String]$BuildNumber
+)
+
+# Remove test binaries which should not ship with the plugin.
+function RemoveTestBinaries {
+ $UnitTestExe = "$DestinationFolder/Contents/Bin/USD.Unit.test.exe"
+ if (Test-Path -Path $UnitTestExe) {
+ Remove-Item $UnitTestExe
+ }
+}
+
+Write-Host "Destination Folder: $DestinationFolder"
+
+# Early bail out on error
+$ErrorActionPreference = "Stop"
+
+robocopy $SourceFolder $DestinationFolder /SL /MIR /NFL /NDL /XF *.ilk *.iobj *.ipdb *.pdb *.test.* gmock*.dll gtest*.dll
+
+# Load the "PackageContents.xml" file to insert the build number into the
+# "Patch" segment of the package's semantic version:
+$PackageContentsFile = "$DestinationFolder/PackageContents.xml"
+[xml]$XmlDocument = Get-Content -Path $PackageContentsFile
+
+RemoveTestBinaries
+
+$XmlDocument.Save($PackageContentsFile)
diff --git a/build-scripts/Prepare-PackageContents.ps1 b/build-scripts/Prepare-PackageContents.ps1
new file mode 100644
index 0000000..18a5a8b
--- /dev/null
+++ b/build-scripts/Prepare-PackageContents.ps1
@@ -0,0 +1,205 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+Param(
+ [Parameter(Mandatory=$true, HelpMessage="Max Target Version")]
+ [String]$TargetVersion,
+ [Parameter(Mandatory=$false, HelpMessage="Artifacts file")]
+ [String]$ArtifactsXmlFile="",
+ [Parameter(Mandatory=$false, HelpMessage="Configuration")]
+ [String]$Config="Release",
+ [Parameter(Mandatory=$false, HelpMessage="Component folder")]
+ [String]$SourceFolder="",
+ [Parameter(Mandatory=$false, HelpMessage="Build Number")]
+ [String]$BuildNumber="0",
+ [Parameter(Mandatory=$false, HelpMessage="Prepare For Distribution")]
+ [Boolean]$Distrib=$false,
+ [Parameter(Mandatory=$true, HelpMessage="Component version")]
+ [String]$ComponentVersion
+)
+if ($SourceFolder.Equals("")) {
+ $SourceFolder = "$PSScriptRoot\..\build\bin\x64\$Config\usd-component-$TargetVersion"
+}
+if ($ArtifactsXmlFile.Equals("")) {
+ $ArtifactsXmlFile = "$PSScriptRoot\..\artifacts_$TargetVersion.xml"
+}
+
+# Early bail out on error
+$ErrorActionPreference = "Stop"
+$UnstableSdk = @("2026")
+
+if ($Distrib -And $UnstableSdk -contains $TargetVersion) {
+ [xml]$ArtifactsXmlDocument = Get-Content -Path $ArtifactsXmlFile
+ $MaxSdkDependency = $ArtifactsXmlDocument.SelectSingleNode("//dependencies/dependency[artifactId='maxsdk']")
+ $MaxSdkVersion = $MaxSdkDependency.version
+ $MaxSdkClassifier = $MaxSdkDependency.classifier
+ $SplitIdentifier = $MaxSdkClassifier.Split("-")
+ $MaxSdkIdentifier = $SplitIdentifier[0]
+} Else {
+ $MaxSdkIdentifier = ""
+}
+
+function GetSeriesMinMaxVersion {
+ Param(
+ [Parameter(Mandatory=$true, HelpMessage="Target Max Version (major) ex 2022")]
+ [String]$MaxMajorVersion,
+ [Parameter(Mandatory=$true, HelpMessage="Target MaxSDK version ex. 24.0.0")]
+ [String]$MaxSdkVersion,
+ [Parameter(Mandatory=$true, HelpMessage="MaxSDK classifier ex. H203-57.0")]
+ [String]$MaxSdkClassifier
+ )
+ if ($Distrib -And $UnstableSdk -contains $TargetVersion) {
+ $MaxBuildNumber = $MaxSdkClassifier.Split("-")[0]
+ $MaxBuildNumber = $MaxBuildNumber.substring(1)
+ $MaxSdkVersionInfo = $MaxSdkVersion.Split(".")
+ $MaxSdkUpdateVersion = $MaxSdkVersionInfo[1]
+ $MaxSdkHotFixVersion = $MaxSdkVersionInfo[2]
+ # YYYY.update.hotfix.build
+ return $MaxMajorVersion + "." + $MaxSdkUpdateVersion + "." + $MaxSdkHotFixVersion + "." + $MaxBuildNumber
+ }
+
+ return $MaxMajorVersion
+}
+
+# Load the "PackageContents.xml" file to insert the build number into the
+# "Patch" segment of the package's semantic version:
+$PackageContentsFile = "$SourceFolder/PackageContents.xml"
+[xml]$XmlDocument = Get-Content -Path $PackageContentsFile
+
+
+# Update both the "AppVersion" and "FriendlyVersion" attributes of the
+# "ApplicationPackage" root node:
+$AppVersion = $XmlDocument.SelectSingleNode("//ApplicationPackage/@AppVersion")
+$AppVersion.Value = "$ComponentVersion.$BuildNumber"
+
+$FriendlyVersion = $XmlDocument.SelectSingleNode("//ApplicationPackage/@FriendlyVersion")
+$FriendlyVersion.Value = "$ComponentVersion.$BuildNumber"
+
+if ($Distrib -And $UnstableSdk -contains $TargetVersion) {
+ $MinMaxVersion = GetSeriesMinMaxVersion -MaxMajorVersion $TargetVersion -MaxSdkVersion $MaxSdkVersion -MaxSdkClassifier $MaxSdkClassifier
+} Else {
+ $MinMaxVersion = $TargetVersion
+}
+
+# Update RuntimeRequirements to min=max=TargetVersion
+function UpdateMinMaxRuntimeRequirementBasedOnTargetVersion {
+ Param(
+ [Parameter(Mandatory=$true, HelpMessage="XmlDocument with the content of the 'PackageContents.xml' file")]
+ [xml]$XmlDocument,
+ [Parameter(Mandatory=$true, HelpMessage="Max Target Version for min/max runtime requirement")]
+ [String]$MinMaxVersion
+ )
+ $XmlDocument.SelectNodes("//RuntimeRequirements") | ForEach-Object {
+ $_.Attributes.RemoveNamedItem("SeriesMin")
+ $_.Attributes.RemoveNamedItem("SeriesMax")
+ $minAttr = $XmlDocument.CreateAttribute("SeriesMin")
+ $minAttr.Value = "$MinMaxVersion"
+ $maxAttr = $XmlDocument.CreateAttribute("SeriesMax")
+ $maxAttr.Value = "$MinMaxVersion"
+ $_.Attributes.SetNamedItem($minAttr)
+ $_.Attributes.SetNamedItem($maxAttr)
+ }
+}
+
+function AddMenus {
+ Param(
+ [Parameter(Mandatory=$true, HelpMessage="XmlDocument with the content of the 'PackageContents.xml' file")]
+ [xml]$XmlDocument,
+ [Parameter(Mandatory=$true, HelpMessage="Max Target Version for min/max runtime requirement")]
+ [String]$TargetVersion
+ )
+ $newMenuApixmlSnippet = @"
+
+
+
+
+"@
+ $legacyMenuApiXmlSnippet = @"
+
+
+
+
+"@
+
+ # Select the ApplicationPackage element
+ $applicationPackage = $XmlDocument.SelectSingleNode("ApplicationPackage")
+ # Create a new XML element from the snippet
+ $newElement = [System.Xml.XmlElement] $XmlDocument.CreateElement("Components")
+
+ if ($TargetVersion -ge 2025) {
+ $newElement.SetAttribute("Description", "menu parts")
+ $newElement.InnerXml = $newMenuApixmlSnippet
+ } else {
+ $newElement.SetAttribute("Description", "post-start-up scripts parts")
+ $newElement.InnerXml = $legacyMenuApiXmlSnippet
+ }
+ # Append the new element to the ApplicationPackage element
+ $applicationPackage.AppendChild($newElement)
+}
+
+
+AddMenus -XmlDocument $XmlDocument -TargetVersion $TargetVersion
+UpdateMinMaxRuntimeRequirementBasedOnTargetVersion -XmlDocument $XmlDocument -MinMaxVersion $MinMaxVersion
+
+
+# Update UpgradeCodes and AppName
+function UpdateAppNameAndUpgradeCodes {
+ Param(
+ [Parameter(Mandatory=$true, HelpMessage="XmlDocument with the content of the 'PackageContents.xml' file")]
+ [xml]$XmlDocument,
+ [Parameter(Mandatory=$true, HelpMessage="Max Target Version for min/max runtime requirement")]
+ [String]$MaxTargetVersion
+ )
+ $appPackageNode = $XmlDocument.SelectSingleNode("ApplicationPackage")
+ $upgradeCodeAttr = $appPackageNode.Attributes.RemoveNamedItem("UpgradeCode")
+ $upgradeCodeAttr.Value = $upgradeCodeAttr.Value.Replace("XXXX", "$MaxTargetVersion")
+ $appPackageNode.Attributes.SetNamedItem($upgradeCodeAttr)
+
+ $productCodeAttr = $appPackageNode.Attributes.RemoveNamedItem("ProductCode")
+ $productCodeAttr.Value = $productCodeAttr.Value.Replace("XXXX", "$MaxTargetVersion")
+ $appPackageNode.Attributes.SetNamedItem($productCodeAttr)
+
+ $currentNameValue = $appPackageNode.Attributes.GetNamedItem("Name").Value
+ $currentDescriptionValue = $appPackageNode.Attributes.GetNamedItem("Description").Value
+
+ $appName = "$currentNameValue $TargetVersion"
+ if ($Distrib -And $UnstableSdk -contains $TargetVersion) {
+ $appName = "$currentNameValue $TargetVersion-$MaxSdkIdentifier"
+ }
+
+ $descriptionName = "$currentDescriptionValue $TargetVersion"
+ if ($Distrib -And $UnstableSdk -contains $TargetVersion) {
+ $descriptionName = "$currentDescriptionValue $TargetVersion-$MaxSdkIdentifier"
+ }
+
+ $nameNodeAttr = $appPackageNode.Attributes.RemoveNamedItem("Name")
+ $nameNodeAttr.Value = $appName
+ $appPackageNode.Attributes.SetNamedItem($nameNodeAttr)
+
+ $descriptionNodeAttr = $appPackageNode.Attributes.RemoveNamedItem("Description")
+ $descriptionNodeAttr.Value = $descriptionName
+ $appPackageNode.Attributes.SetNamedItem($descriptionNodeAttr)
+}
+
+UpdateAppNameAndUpgradeCodes -XmlDocument $XmlDocument -MaxTargetVersion $TargetVersion
+
+# Update the "ProductCode" attribute of the "ApplicationPackage" root node:
+# For each build, the ProductCode would be newly generated
+$NewProductCode = '{'+[guid]::NewGuid().ToString()+'}'
+$ProductCode = $XmlDocument.SelectSingleNode("//ApplicationPackage/@ProductCode")
+$ProductCode.Value = $NewProductCode.ToUpper()
+
+$XmlDocument.Save($PackageContentsFile)
diff --git a/build-scripts/Prepare-VersionHeaders.ps1 b/build-scripts/Prepare-VersionHeaders.ps1
new file mode 100644
index 0000000..0077fb8
--- /dev/null
+++ b/build-scripts/Prepare-VersionHeaders.ps1
@@ -0,0 +1,49 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# NOTE: this is a temporary solution until there is a more standard
+# solution across component plugins from max-pipeline or devops team
+
+Param(
+ [Parameter(Mandatory=$true, HelpMessage="Source component folder")]
+ [String]$SourceFolder,
+ [Parameter(Mandatory=$true, HelpMessage="Build Number")]
+ [String]$BuildNumber="0",
+ [Parameter(Mandatory=$true, HelpMessage="Component version")]
+ [String]$ComponentVersion
+)
+
+$componentVersionHeaderFile = "$SourceFolder/USDComponentVersionNumber.h"
+$componentBuildNumberHeaderFile = "$SourceFolder/USDComponentBuildNumber.h"
+
+$componentVersions = $ComponentVersion.Split(".")
+Write-Host "$componentVersions"
+$componentVersionMajor = $componentVersions[0]
+$componentVersionMinor = $componentVersions[1]
+$componentVersionMicro = $componentVersions[2]
+
+$replaceVersionMajorRegex = '#define COMPONENT_VERSION_MAJOR.*'
+$replaceVersionMinorRegex = '#define COMPONENT_VERSION_MINOR.*'
+$replaceVersionMicroRegex = '#define COMPONENT_VERSION_MICRO.*'
+$replaceBuildNumberRegex = '#define VERSION_INT.*'
+
+Write-Host "Updating $componentVersionHeaderFile with version $componentVersionMajor $componentVersionMinor $componentVersionMicro"
+(Get-Content $componentVersionHeaderFile) -replace $replaceVersionMajorRegex, "#define COMPONENT_VERSION_MAJOR $componentVersionMajor" | Set-Content $componentVersionHeaderFile
+(Get-Content $componentVersionHeaderFile) -replace $replaceVersionMinorRegex, "#define COMPONENT_VERSION_MINOR $componentVersionMinor" | Set-Content $componentVersionHeaderFile
+(Get-Content $componentVersionHeaderFile) -replace $replaceVersionMicroRegex, "#define COMPONENT_VERSION_MICRO $componentVersionMicro" | Set-Content $componentVersionHeaderFile
+
+Write-Host "Updating $componentBuildNumberHeaderFile with version $BuildNumber"
+(Get-Content $componentBuildNumberHeaderFile) -replace $replaceBuildNumberRegex, "#define VERSION_INT $BuildNumber" | Set-Content $componentBuildNumberHeaderFile
diff --git a/build-scripts/build-solution.py b/build-scripts/build-solution.py
new file mode 100644
index 0000000..c1f1448
--- /dev/null
+++ b/build-scripts/build-solution.py
@@ -0,0 +1,153 @@
+#
+# Copyright 2024 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import os
+import subprocess
+import argparse
+
+# map to handle the dependencies location for the project
+# the key is the script parameter name
+# the value is a pair
+# 1) argument to pass to msbuild (the referenced property in vcxproj/props)
+# 2) the help text to display when help is invoked for the current script
+artifact_map = {'maxsdk':('MaxSDK', 'The path location for the \'MaxSDK\' folder.'),
+ 'qtinstall':('QtInstall', 'The Qt reference version from QtVsTools (aka \'Qt Installation\').'),
+ 'materialx':('MaterialXDir', 'The path location for the 3ds Max MaterialX material plugin folder.'),
+ 'googletest':('GoogleTestDir', 'The path location for the \'gtest\' folder.'),
+ 'pyopengl':('PyOpenGLDir', 'The path location for the \'OpenGL\' Python module (PyOpenGL).'),
+ 'maxusddevkit':('MaxUsdDevKit', 'The path location for the 3ds Max USD \'devkit\'.'),
+ 'spdlog':('SpdlogInc', 'The path location for \'spdlog\' include folder. If not provided, using the path from the \'devkit\' if the \'maxusddevkit\' option is provided.'),
+ 'python':('PythonLocation', 'The path location for the \'Python\' folder. If not provided, using the path from the \'devkit\' if the \'maxusddevkit\' option is provided.'),
+ 'pyside':('PySideDir', 'The path location for the \'PySide6\' Python module. If not provided, using the path from the \'devkit\' if the \'maxusddevkit\' option is provided.'),
+ 'shiboken':('PySideShibokenDir', 'The path location for the \'shiboken6\' Python module. If not provided, using the path from the \'devkit\' if the \'maxusddevkit\' option is provided.'),
+ 'ufeinc':('UfeInc', 'The path location for the \'Ufe\' include folder. If not provided, using the path from the \'devkit\' if the \'maxusddevkit\' option is provided.'),
+ 'ufelib':('UfeLib', ' The path location for the \'Ufe\' lib folder. If not provided, using the path from the \'devkit\' if the \'maxusddevkit\' option is provided.'),
+ 'usdufe':('UsdUfeDir', 'The path location for the \'UsdUfe\' folder. If not provided, using the path from the \'devkit\' if the \'maxusddevkit\' option is provided.'),
+ 'openusd':('PxrUsdRoot', 'The path location for the \'OpenUSD\' folder. If not provided, using the path from the \'devkit\' if the \'maxusddevkit\' option is provided.'),
+ 'tbb':('TBBDir', 'The path location for the \'TBB\' folder. If not provided, using the path from the \'OpenUSD\' if the \'openusd\' or \'maxusddevkit\' option is provided.'),
+ 'boostinc':('BoostInc', 'The path location for the \'Boost\' include folder. If not provided, using the path from the \'OpenUSD\' if the \'openusd\' or \'maxusddevkit\' option is provided.'),
+ 'boostlib':('BoostLib', ' The path location for the \'Boost\' lib folder. If not provided, using the path from the \'OpenUSD\' if the \'openusd\' or \'maxusddevkit\' option is provided.')}
+
+def parse_arguments() -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("configuration",
+ nargs='?',
+ type=str.lower,
+ choices=['release', 'hybrid'],
+ default='release',
+ help="The build configuration type.")
+ parser.add_argument("target", choices=[2022, 2023, 2024, 2025, 2026], help="The 3ds Max version to target.", type=int)
+ parser.add_argument("-b", "--build", help="The build number coming from the pipeline.", default=0, type=int)
+ parser.add_argument("-v", "--version", help="The 3ds Max USD component version being built.", default='0.0.0')
+ parser.add_argument("-w", "--warnaserror", help="Enable the compiler to treat all warnings as errors.", action='store_true')
+ parser.add_argument("-r", "--rebuild", help="Rebuild the project.", action='store_true')
+ parser.add_argument("-d", "--distrib", help="Prepare for redistribution. Write component version in source headers.", action='store_true')
+ parser.add_argument("-p", "--package", help="Prepare the package folder after build.", action='store_true')
+ # parse arguments to set the artifact specific paths if any
+ for arg, data in artifact_map.items() :
+ parser.add_argument(f"--{arg}", help=data[1])
+ return parser.parse_args()
+
+def get_script_folder() -> str :
+ return os.path.dirname(os.path.abspath(__file__))
+
+def build_command(args:argparse.Namespace) -> list:
+ # path to build-scripts
+ swd = get_script_folder()
+ # set up the build environment
+ cmd = [swd + "\\configure-vsdevcmd.bat"]
+ if args.target == 2022:
+ # 3ds Max 2022 is expected to be compiled with VS2017 and winsdk 10.0.17134.0
+ # Visual Studio 2019 can be used as long as VS2017 tooling is available
+ #cmd.append("2017")
+ #cmd.append("10.0.17134.0")
+ cmd.append("2019")
+ elif args.target == 2026:
+ cmd.append("2022")
+ else:
+ cmd.append("2019")
+
+ # append the build command
+ cmd.append("&&")
+
+ cmd.append('msbuild.exe')
+ cmd.append(swd + "\\..\\src\\usd-component.sln")
+
+ if args.rebuild:
+ cmd.append('/t:rebuild')
+ if args.warnaserror:
+ cmd.append('/warnaserror')
+ cmd.append(f'/p:Configuration={args.configuration}')
+ cmd.append(f'/p:Platform=x64')
+ if args.distrib:
+ cmd.append(f'/p:BuildType=jenkins')
+ cmd.append(f'/p:VersionTarget={args.target}')
+ cmd.append(f'/p:BuildNumber={args.build}')
+ cmd.append(f'/p:ComponentVersion={args.version}')
+
+ # parse the artifacts variable option list
+ args_dict = vars(args)
+ for arg, data in artifact_map.items() :
+ if args_dict[arg] is not None:
+ cmd.append(f'/p:{data[0]}={args_dict[arg]}')
+
+ return cmd
+
+def build_component(args:argparse.Namespace):
+ cmd = build_command(args)
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
+ print(f"Launching build process\n{proc.args}")
+ for raw_line in proc.stdout:
+ print(raw_line, end='')
+ proc.wait()
+ return proc.returncode == 0
+
+def package(config:str, target:int, version:str, build:int):
+ # path to build-scripts
+ swd = get_script_folder()
+ # set up the build environment
+ cmd = ['powershell',
+ '-ExecutionPolicy',
+ 'ByPass',
+ '-File',
+ swd + "\\Prepare-BinPackage.ps1",
+ '-SourceFolder',
+ f'{swd}\\..\\build\\bin\\x64\\{config}\\usd-component-{target}',
+ '-DestinationFolder',
+ f'{swd}\\..\\package\\3dsmax-usd-{target}',
+ '-ComponentVersion',
+ version,
+ '-TargetVersion',
+ str(target),
+ '-BuildNumber',
+ str(build)]
+
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
+ print(f"Launching packaging process\n{proc.args}")
+ for raw_line in proc.stdout:
+ print(raw_line, end='')
+ proc.wait()
+ return proc.returncode == 0
+
+def main():
+ args = parse_arguments()
+ if not build_component(args):
+ exit(1)
+ if args.package:
+ if not package(args.configuration, args.target, args.version, args.build):
+ exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/build-scripts/configure-vsdevcmd.bat b/build-scripts/configure-vsdevcmd.bat
new file mode 100644
index 0000000..766f268
--- /dev/null
+++ b/build-scripts/configure-vsdevcmd.bat
@@ -0,0 +1,137 @@
+::
+:: Copyright 2023 Autodesk
+::
+:: Licensed under the Apache License, Version 2.0 (the "License");
+:: you may not use this file except in compliance with the License.
+:: You may obtain a copy of the License at
+::
+:: http://www.apache.org/licenses/LICENSE-2.0
+::
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+::
+@echo off
+
+goto :ParseArgs
+
+:Usage
+ echo.
+ echo Usage:
+ echo %~nx0 ^ [winsdk_version]
+ echo.
+ echo vs_version Visual Studio version ^<2012^|2015^|2017^|2019^|latest^>
+ echo latest: Selects the latest available VS 2017 or greater
+ echo.
+ echo winsdk_version ^(optional^) The Windows SDK version to use
+ echo If unspecified, let Visual Studio configure the default version.
+ echo Ignored (not supported) for VS 2012.
+ echo.
+ echo Configures the developer command prompt for Visual Studio.
+ echo If installed, uses the latest compatible version of Build Tools for Visual Studio ^(minimal installation^).
+ echo Otherwise, uses the specified version of Visual Studio ^(full installation^).
+ echo.
+ echo Only supports Build Tools for VS 2017 or greater.
+ echo Build Tools for Visual Studio only supports toolset versions v140 or greater.
+ echo.
+ echo Returns non-zero on error.
+ echo.
+ echo Examples:
+ echo %~nx0 latest
+ echo %~nx0 2022
+ echo %~nx0 2019
+ echo %~nx0 2017 10.0.17134.0
+ echo.
+ exit /b 1
+
+:ParseArgs
+
+set VS_VERSION=%~1
+for %%v in ("" "2017" "2019" "2022" "latest") do (
+ if /I "%VS_VERSION%"=="%%~v" goto :Continue
+)
+echo ERROR: vs_version '%VS_VERSION%' is not supported
+goto :Usage
+
+:Continue
+
+if "%VS_VERSION%" == "" (
+ set VS_VERSION=latest
+)
+
+set WINSDK_VERSION=%~2
+
+REM Additional setup for VS 2017 or greater
+set "VSWHERE_LATEST_CMD="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -prerelease -latest"
+
+REM Select Visual Studio version
+if "%VS_VERSION%" GEQ "2015" goto :BuildToolsLatest
+goto :%VS_VERSION%
+
+:BuildToolsLatest
+REM Find latest installation of Build Tools for Visual Studio (2017 or greater) (tested with 2019)
+for /f "tokens=*" %%i in ('%VSWHERE_LATEST_CMD% -products Microsoft.VisualStudio.Product.BuildTools -property productPath') do (
+ set VSDEVCMD_PATH=%%~dpiVsDevCmd.bat
+)
+if not exist "%VSDEVCMD_PATH%" (
+ echo Build Tools for Visual Studio is not installed. Using full Visual Studio installation.
+ goto :%VS_VERSION%
+)
+for /f "tokens=*" %%i in ('%VSWHERE_LATEST_CMD% -products Microsoft.VisualStudio.Product.BuildTools -property installationVersion') do (
+ set VS_LATEST_VERSION=%%i
+)
+if "%VS_LATEST_VERSION:~0,2%" GEQ "16" (
+ if "%WINSDK_VERSION%" NEQ "" set "WINSDK_VERSION=-winsdk=%WINSDK_VERSION%"
+)
+call "%VSDEVCMD_PATH%" %WINSDK_VERSION%
+goto :End
+
+:latest
+REM Find latest installation of Microsoft Visual Studio 2017 or greater
+for /f "tokens=*" %%i in ('%VSWHERE_LATEST_CMD% -property installationPath') do set VS_PATH=%%i
+if not exist "%VS_PATH%" (
+ echo ERROR: Can't find latest installation of Visual Studio 2017 or greater
+ exit /b 2
+)
+for /f "tokens=*" %%i in ('%VSWHERE_LATEST_CMD% -property installationVersion') do (
+ set VS_LATEST_VERSION=%%i
+)
+if "%VS_LATEST_VERSION:~0,2%" GEQ "16" (
+ if "%WINSDK_VERSION%" NEQ "" set "WINSDK_VERSION=-winsdk=%WINSDK_VERSION%"
+)
+call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" amd64 %WINSDK_VERSION%
+goto :End
+
+:2022
+set VS_VERSION_RANGELO=17.0
+set VS_VERSION_RANGEHI=17.9
+goto :vswhere_vcvarsall
+
+:2019
+set VS_VERSION_RANGELO=16.0
+set VS_VERSION_RANGEHI=17.0
+goto :vswhere_vcvarsall
+
+:2017
+set VS_VERSION_RANGELO=15.0
+set VS_VERSION_RANGEHI=16.0
+goto :vswhere_vcvarsall
+
+:vswhere_vcvarsall
+REM Use vswhere to find Microsoft Visual Studio installation
+for /f "tokens=*" %%i in ('%VSWHERE_LATEST_CMD% -version ^[%VS_VERSION_RANGELO%^,%VS_VERSION_RANGEHI%^) -property installationPath') do set VS_PATH=%%i
+if not exist "%VS_PATH%" (
+ echo ERROR: Can't find Visual Studio 2017 installation
+ exit /b 2
+)
+call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" amd64 %WINSDK_VERSION%
+
+:End
+
+set vserror=%errorlevel%
+if "%vserror%" NEQ "0" (
+ echo ERROR: Can't configure developer command prompt for Visual Studio
+ exit /b %vserror%
+)
diff --git a/build-scripts/utility/outputSplineToCpp.ms b/build-scripts/utility/outputSplineToCpp.ms
new file mode 100644
index 0000000..91c44d0
--- /dev/null
+++ b/build-scripts/utility/outputSplineToCpp.ms
@@ -0,0 +1,38 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+-- This script is used to convert a spline object to CPP code.
+-- This is used to create the viewport icon for the USDStageObject.
+spl = selection[1]
+for s = 1 to (numsplines spl) do
+(
+ format "\n//Spline %\n" s
+ format "spline = baseShape.NewSpline();\n"
+ for k = 1 to (numknots spl s) do
+ (
+ p = (getKnotPoint spl s k)
+ format "p = Point3(%,%,0.0);\n" p[1] p[2]
+ inVec = (getInVec spl s k)
+ format "in = Point3(%,%,0.0);\n" inVec[1] inVec[2]
+ outVec = (getOutVec spl s k)
+ format "out = Point3(%,%,0.0);\n" outVec[1] outVec[2]
+ format "k.SetKnot(p);\n"
+ format "k.SetInVec(in);\n"
+ format "k.SetOutVec(out);\n"
+ format "spline->AddKnot(k);\n"
+ )
+ format "spline->SetClosed();\n"
+)
\ No newline at end of file
diff --git a/doc/CLA/AutodeskFormCorpContribAgmtForOpenSource.pdf b/doc/CLA/AutodeskFormCorpContribAgmtForOpenSource.pdf
new file mode 100644
index 0000000..996005d
Binary files /dev/null and b/doc/CLA/AutodeskFormCorpContribAgmtForOpenSource.pdf differ
diff --git a/doc/CLA/AutodeskFormIndContribAgmtForOpenSource.pdf b/doc/CLA/AutodeskFormIndContribAgmtForOpenSource.pdf
new file mode 100644
index 0000000..6fb99bc
Binary files /dev/null and b/doc/CLA/AutodeskFormIndContribAgmtForOpenSource.pdf differ
diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md
new file mode 100644
index 0000000..2981778
--- /dev/null
+++ b/doc/CONTRIBUTING.md
@@ -0,0 +1,36 @@
+# Contributing to USD For 3ds Max
+
+## Contributor License Agreement #
+Before contributing code to this project, we ask that you sign a Contributor License Agreement (CLA).
+
++ [AutodeskFormCorpContribAgmtForOpenSource.pdf](CLA/AutodeskFormCorpContribAgmtForOpenSource.pdf) please sign this one for corporate use
++ [AutodeskFormIndContribAgmtForOpenSource.pdf](CLA/AutodeskFormIndContribAgmtForOpenSource.pdf) please sign this one if you're an individual contributor
+
+The documents include instructions on where to send the completed forms to. Once a signed form has been received you will be able to submit pull requests.
+
+
+## Filing Issues
+
+### Suggestions
+
+The USD for 3ds Max project is meant to evolve with feedback - the project and its users greatly appreciate any thoughts on ways to improve the design or features. Please use the `enhancement` tag to specifically denote issues that are suggestions - this helps us triage and respond appropriately.
+
+### Bugs
+
+As with all pieces of software, you may end up running into bugs. Please submit bugs as regular issues on GitHub - 3ds Max developers are regularly monitoring issues and will prioritize and schedule fixes.
+
+The best bug reports include a detailed way to predictably reproduce the issue, and possibly even a working example that demonstrates the issue.
+
+## Contributing Code
+
+The USD for 3ds Max project accepts and greatly appreciates contributions. The project follows the [fork & pull](https://help.github.com/articles/using-pull-requests/#fork--pull) model for accepting contributions.
+
+It is highly recommended that an issue be logged on GitHub before any work is started. This will allow for early feedback from other developers and avoid multiple parallel efforts.
+
+### Pull Requests
+
+When contributing code, please also include appropriate tests as part of the pull request, and follow the same comment and coding style as the rest of the project. Take a look through the existing code for examples of the testing and style practices the project follows. Also read and adhere to the the [Coding guidelines](CodingGuidelines.md), many of which are enforced by clang-format checks.
+
+All development should happen against the [dev](https://github.com/Autodesk/3dsmax-usd/tree/dev) branch of the repository. Please make sure the base branch of your pull request is set to the dev branch when submitting your pull request.
+
+All pull requests require at least one approving code reviewer.
diff --git a/doc/CodingGuidelines.md b/doc/CodingGuidelines.md
new file mode 100644
index 0000000..449f71e
--- /dev/null
+++ b/doc/CodingGuidelines.md
@@ -0,0 +1,238 @@
+
+This document outlines coding guidelines for contributions to the [3dsmax-usd](https://github.com/autodesk/3dsmax-usd) project.
+
+# C++ Coding Guidelines
+
+Many of the C++ coding guidelines below are validated and enforced through the use of `clang-format` which is provided by the [LLVM project](https://github.com/llvm/llvm-project). Since the adjustments made by `clang-format` can vary from version to version, we standardize this project on a single `clang-format` version to ensure consistent results for all contributions made to 3dsmax-usd.
+
+| | Version | Source Code | Release |
+|:--------------------:|:-------:|:-----------:|:-------:|
+| `clang-format`/LLVM | 10.0.0 | [llvmorg-10.0.0 Tag](https://github.com/llvm/llvm-project/tree/llvmorg-10.0.0) | [LLVM 10.0.0](https://github.com/llvm/llvm-project/releases/tag/llvmorg-10.0.0) |
+
+## Foundation/Critical
+### License notice
+Every file should start with the Apache 2.0 licensing statement:
+```cpp
+// Licensed under the Apache License, Version 2.0 (the “License”);
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an “AS IS” BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+```
+
+### Copyright notice
+* Every file should contain at least one Copyright line at the top, which can be to Autodesk or the individual/company that contributed the code.
+* Multiple copyright lines are allowed, and if a significant new contribution is made to an existing file then an individual or company can append a new line to the copyright section.
+* The year the original contribution is made should be included but there is no requirement to update this every year.
+* There is no requirement that an Autodesk copyright line should be in all files. If an individual or company contributes new files they do not have to add both their name and Autodesk's name.
+* If existing code is being refactored or moved within the repo then the original copyright lines should be maintained. You should only append a new copyright line if significant new changes are added. For example, if some utility code is being moved from a plugin into a common area, and the work involved is only minor name changes or include path updates, then the original copyright should be maintained. In this case, there is no requirement to add a new copyright for the person that handled the refactoring.
+
+### #pragma once vs include guard
+The project makes use of `#pragma once` to avoid files being `#include’d` multiple times (which can cause duplication of definitions & compilation errors). While it is a non-standard and non-portable extension, it is widely supported. It is also a choice aligned with the 3ds Max SDK. The platform and compiler used (Windows/VS) fully support the preprocessor option.
+
+### Naming (file, type, variable, constant, function, namespace, macro, template parameters, schema names)
+**General Naming Rules**
+MaxUsd strives to use “camel case” naming. That is, each word is capitalized, except possibly the first word:
+* UpperCamelCase
+* lowerCamelCase
+
+While underscores in names (_) (e.g., as separator) are not strictly forbidden, they are strongly discouraged.
+Optimize for readability by selecting names that are clear to others (e.g., people on different teams.)
+Use names that describe the purpose or intent of the object. Do not worry about saving horizontal space. It is more important to make your code easily understandable by others. Minimize the use of abbreviations that would likely be unknown to someone outside your project (especially acronyms and initialisms).
+
+**File Names**
+Filenames should be UpperCamelCase and should not include underscores (_) or dashes (-).
+C++ files should end in .cpp and header files should end in .h.
+In general, make your filenames as specific as possible. For example:
+```
+StageData.cpp
+StageData.h
+```
+
+**Type Names**
+All type names (i.e., classes, structs, type aliases, enums, and type template parameters) should use UpperCamelCase, with no underscores. For example:
+```cpp
+class MaxUsdStageData;
+class ImportData;
+enum Roles;
+```
+
+**Variable Names**
+**Class/Struct Data Members**
+**Constant Names**
+All variables, data member and constant names, including function parameters and data members should use lowerCamelCase, with no underscores. For example:
+```cpp
+const INode* parentNode;
+const MSTR& rayDirection
+bool* drawRenderPurpose;
+```
+
+**Function/Method Names**
+All functions should be UpperCamelCase. This avoids inconsistencies with the 3ds Max SDK. For example:
+```cpp
+MSTR Name() const override;
+void RegisterExitCallback();
+```
+
+**Namespace Names**
+Namespace names should be UpperCamelCase. Top-level namespace names are based on the project name.
+```cpp
+namespace MaxUsd {}
+```
+
+**Enumerator Names**
+Enumerators (for both scoped and unscoped enums) should be UpperCamelCase.
+
+The enumeration name, `StringPolicy` is a type and therefore mixed case.
+```cpp
+enum class StringPolicy
+{
+ StringOptional,
+ StringMustHaveValue
+};
+```
+
+**Macro Names**
+In general, macros should be avoided (see [Modern C++](https://docs.google.com/document/d/1Jvbpfh2WNzHxGQtjqctZ1K1lnpaAtHOUwm0kmmEcxjY/edit#heading=h.ynbggnv41p3) ). However, if they are absolutely needed, macros should be all capitals, with words separated by underscores.
+```cpp
+#define ROUND(x) …
+#define PI_ROUNDED 3.0
+```
+
+### Documentation (class, method, variable, comments)
+* [Doxygen ](http://www.doxygen.nl/index.html) will be used to generate documentation from MaxUsd C++ sources.
+* Doxygen tags must be used to document classes, function parameters, function return values, and thrown exceptions.
+* The MaxUsd project does require the use of any Doxygen formatting style ( [Doxygen built-in formatting](http://www.doxygen.nl/manual/commands.html) )
+* Comments for users of classes and functions must be written in headers files. Comments in definition files are meant for contributors and maintainers.
+
+### Namespaces
+
+#### In header files (e.g. .h)
+
+* **Required:** to use fully qualified namespace names. Global scope using directives are not allowed. Inline code can use using directives in implementations, within a scope, when there is no other choice (e.g. when using macros, which are not namespaced).
+
+```cpp
+// In aFile.h
+inline PXR_NS::UsdPrim prim() const
+{
+ PXR_NAMESPACE_USING_DIRECTIVE
+ TF_VERIFY(item != nullptr);
+ return item->prim();
+}
+```
+
+#### In implementation files (e.g. .cpp)
+
+* **Recommended:** to use fully qualified namespace names, unless clarity or readability is degraded by use of explicit namespaces, in which case a using directive is acceptable.
+* **Recommended:** to use the existing namespace style, and not make gratuitous changes. If the file is using explicit namespaces, new code should follow this style, unless the changes are so significant that clarity or readability is degraded. If the file has one or more using directives, new code should follow this style.
+
+### Include directive
+For source files (.cpp) with an associated header file (.h) that resides in the same directory, it should be `#include`'d with double quotes and no path. This formatting should be followed regardless of with whether the associated header is public or private. For example:
+```cpp
+// In foobar.cpp
+#include "foobar.h"
+```
+
+All included public header files from outside and inside the project should be `#include`’d using angle brackets. For example:
+```cpp
+#include
+#include
+```
+
+Private project’s header files should be `#include`'d using double quotes, and a relative path. Private headers may live in the same directory or sub-directories, but they should never be included using "._" or ".._" as part of a relative path. For example:
+```cpp
+#include "privateUtils.h"
+#include "pvt/helperFunctions.h"
+```
+
+### Include order
+Headers should be included in the following order, with each section separated by a blank line and files sorted alphabetically:
+
+1. Related header
+2. All private headers
+3. All public headers from this repository (3dsmax-usd)
+4. UsdUfe library headers
+5. Pixar + USD headers
+6. Autodesk + 3ds Max headers
+7. Other libraries' headers
+8. C++ standard library headers
+9. C system headers
+10. Conditional includes
+
+```cpp
+#include "MaxSceneBuilder.h"
+
+#include "private/util.h"
+
+#include
+#include
+
+#include
+
+#include
+
+#include
+#include
+
+#include
+
+#ifdef IS_MAX2023_OR_GREATER
+#include
+#include
+#endif
+```
+
+### Conditional compilation (3ds Max, USD, UFE version)
+**3ds Max**
+* A series of `IS_MAX20XX_OR_GREATER` defines are available (see `MaxUsd/Utilities/MaxSupportUtils.h`)
+* `MAX_RELEASE` is the consistent macro to test 3ds Max version (see `maxsdk/include/plugapi.h` from the 3ds Max SDK)
+
+**UFE**
+* Each version of Ufe contains a features available define (in ufe.h) such as `UFE_V4_FEATURES_AVAILABLE` that can be used for conditional compilation on code depending on Ufe Version.
+
+**USD**
+* `PXR_VERSION` is the macro to test USD version (`PXR_MAJOR_VERSION` * 10000 + `PXR_MINOR_VERSION` * 100 + `PXR_PATCH_VERSION`)
+
+Respect the minimum supported version for 3ds Max and USD stated in [build.md](https://github.com/Autodesk/3dsmax-usd/dev/doc/build.md) .
+
+### std over boost
+Recent extensions to the C++ standard introduce many features previously only found in [boost](http://boost.org). To avoid introducing additional dependencies, developers should strive to use functionality in the C++ std over boost. If you encounter usage of boost in the code, consider converting this to the equivalent std mechanism.
+Our library currently has the following boost dependencies:
+* `boost::python`
+
+## Modern C++
+Our goal is to develop [3dsmax-usd](https://github.com/autodesk/3dsmax-usd) following modern C++ practices. We’ll follow the [C++ Core Guidelines](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) and pay attention to:
+* `using` (vs `typedef`) keyword
+* `virtual`, `override` and `final` keyword
+* `default` and `delete` keywords
+* `auto` keyword
+* initialization - `{}`
+* `nullptr` keyword
+* …
+
+## Diagnostic Facilities
+
+Developers are encouraged to use TF library diagnostic facilities in 3ds Max USD. Please follow below guideline for picking the correct facility: https://graphics.pixar.com/usd/docs/api/page_tf__diagnostic.html
+
+In MaxUsd, `DiagnosticDelegate` converts TF diagnostics into native 3ds Max infos, warnings, and errors. The environment flag `MAXUSD_SHOW_FULL_DIAGNOSTICS`, controls the granularity of TF error/warning/status messages being displayed in 3ds Max:
+
+e.g
+```
+[2024-05-23 12:06:53.623] [USDImport] [warning] Unexpected input type or mapped value - normal3f:(0, 0, 1):normal
+```
+vs
+```
+[2024-05-23 12:06:53.623] [USDImport] [warning] Unexpected input type or mapped value - normal3f:(0, 0, 1):normal -- Warning in usd_material_reader.output_max_material at line 463 of d:\usd-component-2024/contents/scripts//materials\usd_material_reader.py
+```
+
+# Coding guidelines for Python
+We are adopting the [PEP-8](https://www.python.org/dev/peps/pep-0008) style for Python Code with the following modification:
+* Mixed-case for variable and function names are allowed
+
+[Pylint](https://www.pylint.org/) is recommended for automation.
diff --git a/doc/LICENSE.md b/doc/LICENSE.md
new file mode 100644
index 0000000..11069ed
--- /dev/null
+++ b/doc/LICENSE.md
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/doc/build.md b/doc/build.md
new file mode 100644
index 0000000..580f39b
--- /dev/null
+++ b/doc/build.md
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+# Building
+
+## Getting and Building the Code
+### Download the source code
+
+Start by cloning the repository:
+```
+git clone https://github.com/Autodesk/3dsmax-usd.git
+cd 3dsmax-usd
+```
+##### Repository Layout
+| Location | Description |
+|--|--|
+| additional_includes | Additional include files required to build the component (from 3ds Max restricted SDK) |
+| samples | 3dsmax-usd sample plugins exposing its extensibility feature |
+| src | The source folder for the 3dsmax-usd component |
+
+### Dependencies
+Multiple dependencies are required in order to build the component. Take the time to first set up your build environment properly.
+* [System Prerequisites](#system-prerequisites)
+* [OpenUSD](#download-and-build-openusd)
+* [UFE](#universal-front-end-ufe)
+* [Python](#python-modules)
+* [MaterialX](#materialx-plugin-for-3ds-max)
+* [Other dependencies](#other-dependencies)
+
+#### 1. System Prerequisites
+
+Before building the project, consult the [3ds Max SDK requirements](https://help.autodesk.com/view/MAXDEV/2025/ENU/?guid=sdk_requirements) to ensure you use the recommended version of compiler, operating system and Qt version. You will need to need a copy of the [3ds Max SDK](https://aps.autodesk.com/developer/overview/3ds-max) headers and libraries as well (direct link to [3ds Max 2025](https://autodesk-adn-transfer.s3.us-west-2.amazonaws.com/ADN+Extranet/M%26E/Max/Autodesk+3ds+Max+2025/SDK_3dsMax2025.msi) and [3ds Max 2024](https://autodesk-adn-transfer.s3.us-west-2.amazonaws.com/ADN+Extranet/M%26E/Max/Autodesk+3ds+Max+2024/SDK_3dsMax2024.msi)).
+
+> For 3ds Max 2025, we suggest to use Microsoft Visual Studio 2019 (Community or better), version 16.10.4, C++ Platform Toolset v142, Windows Platform SDK 10.0.19041.0.
+
+Consult the [3ds Max Python API](https://help.autodesk.com/view/MAXDEV/2025/ENU/?guid=MAXDEV_Python_what_s_new_in_3ds_max_python_api_html) reference to learn more on the Python version to use.
+
+> The Microsoft Visual Studio extension 'Qt VS Tools' must be installed and properly configured with the Qt version used by 3ds Max. Make sure to reference the selected *version* name in '*Extensions->Qt VS Tools->Qt Versions*'.
+>
+> 3ds Max Qt versions can be found on the https://github.com/autodesk-forks/qt5/releases. These versions contain modifications to the Qt codebase which are specific to 3ds Max. No need to build your own version if you are not making changes to Qt.
+
+
+
+##### Simplified dependencies setup using the `devkit`
+Fetching all dependencies can be long and tedious. A `devkit` archive is made available inside the 3ds Max USD plugin installation. The `devkit` includes all the 3ds Max USD SDK files (includes and libs), the samples and the minimal dependencies required to compile the samples (or your extensibility plugin to the 3ds Max USD plugin). Additionally, the `devkit` contains additional dependencies required to compile the full 3ds Max USD plugin.
+
+The `devkit` can be found in the folder `Content` of an installed 3ds Max USD plugin (https://help.autodesk.com/view/3DSMAX/2025/ENU/?guid=GUID-1A33A64B-6829-4806-98FC-9B25E4ED47FC).
+
+> The `devkit` archive is available as a separate file for the initial release on GitHub (v0.9.0). It can be found along the [tag assets](https://github.com/Autodesk/3dsmax-usd/releases/tag/v0.9.0).
+
+The `devkit` includes:
+
+ - include files and libs for the 3ds Max USD SDK (the core MaxUsd library enabling third-party developers to extend the 3ds Max USD plugin);
+ - include files and libs for OpenUSD. The DLLs for OpenUSD are to be found in the installed 3ds Max USD plugin. The `Pixar_USD` folder from the `devkit` contains a Python script to copy those missing DLLs;
+ - include files and binaries (libs and dlls) for UFE and USDUFE;
+ - include files and libs for Python, PySide and Shiboken. The Python and PySide/Shiboken DLLs are not required to get a working 3ds Max USD component as they are provided by 3ds Max already.
+
+The `devkit` is required to build the 3ds Max USD plugin as it requires to link to Python, PySide/Shiboken, UFE and USDUFE and those dependencies cannot be recompiled on your own for compatibility reasons with 3ds max. You can either use OpenUSD from the `devkit` or rebuild your own as explained in the next section.
+
+> * Make sure to extract the `devkit` archive from the 3ds Max USD plugin to your development folder before trying to make use of the `devkit` .
+> * Once extracted, execute the Python script `copy_missing_DLLs_found_in_the_official_installation.py` found in the `Pixar_USD` folder to recreate a completed OpenUSD prebuilt library.
+
+#### 2. Download and Build OpenUSD
+
+ The `devkit` available inside the 3ds Max USD plugin installation provides a prebuild version of OpenUSD. It initially contains only the include files and libs for OpenUSD. The DLLs for OpenUSD are to be found in the installed 3ds Max USD plugin. To get a full and usable library, a Python script, to be found in the `Pixar_USD` folder from the `devkit`, must be executed to copy those missing DLLs back into the `devkit`.
+
+The OpenUSD library can also be rebuilt to fit with your needs. See OpenUSD's official github page for instructions on how to build USD: https://github.com/PixarAnimationStudios/OpenUSD . It is important the recommended `OpenUSD` commit ID or tag from the table below is used:
+
+> **note**: the OpenUSD library included in the 3ds Max USD plugin was compiled using those options : `--python --alembic --hdf --materialx`.
+
+| | ![](images/logo-horizontal-color.svg) | USD version used in 3ds Max | USD source for MaxUsd |
+|:------------: |:---------------: |:------------------------:|:-------------------------:|
+| CommitID/Tags | Officially supported: [v21.11](https://github.com/PixarAnimationStudios/OpenUSD/tree/v21.11), [v22.11](https://github.com/PixarAnimationStudios/OpenUSD/tree/v22.11), [v23.11](https://github.com/PixarAnimationStudios/OpenUSD/tree/v23.11)| 3ds Max 2022 = v21.11 3ds Max 2023 = v21.11 3ds Max 2024 = v22.11 3ds Max 2025 = v23.11 Codename Devi = v24.05 | [v21.11-MaxUsd-Public](https://github.com/autodesk-forks/USD/tree/v21.11-MaxUsd-Public) [v22.11-MaxUsd-Public](https://github.com/autodesk-forks/USD/tree/v22.11-MaxUsd-Public) [v23.11-MaxUsd-Public](https://github.com/autodesk-forks/USD/tree/v23.11-MaxUsd-Public) |
+
+The OpenUSD component has dependencies that are being reused to build the 3ds Max USD component (boost and TBB are dependencies to the 3ds Max USD) . Their source files are automatically fetched and built by the build script of OpenUSD. The table below reports on the various dependencies being used by the compiled version of OpenUSD found in the 3ds Max USD plugin.
+
+| Dependency | 3ds Max 2022 | 3ds Max 2024 | 3ds Max 2024 | 3ds Max 2025 |
+|:----------------:|:------------:|:------------:|:------------:|:------------:|
+| zlib | 1.2.13 | 1.2.13 | 1.2.13 | 1.2.13 |
+| boost | 1.70.0 | 1.70.0 | 1.76.0 | 1.81.0 |
+| TBB | tbb2018 (update 6)| tbb2018 (update 6)| tbb2019 (update 6)| tbb2020.3 |
+| HDF5 | 1.10.0 (patch 1)| 1.10.0 (patch 1)| 1.10.0 (patch 1)| 1.10.0 (patch 1) |
+| OpenEXR | 2.3.0| 2.3.0| 2.5.2| 3.1.11 |
+| Alembic | 1.7.10| 1.7.10| 1.7.10| 1.8.5 |
+| MaterialX | 1.38.0| 1.38.0| 1.38.4| 1.38.8 |
+| OpenSubDiv | 3.4.3| 3.4.3| 3.5.0| 3.5.1 |
+
+> :warning: Make sure that you don't have an older USD locations in your ```PATH``` and ```PYTHONPATH``` environment settings. ```PATH``` and ```PYTHONPATH``` are automatically adjusted inside the project to point to the correct USD location. See ```cmake/usd.cmake```.
+
+#### 3. Universal Front End (UFE)
+
+The Universal Front End (UFE) is a DCC-agnostic component that allows the 3ds Max USD component to browse and edit data in multiple data models. This allows 3ds Max to edit pipeline data such as USD, using UDSUFE. UFE is developed as a separate binary component, and therefore versioned separately from 3ds Max.
+
+The UFE component v5.3.0 is being used.
+The UFE component was originally developed by the Autodesk Maya team.
+Reference documentation - https://help.autodesk.com/view/MAYADEV/2025/ENU/?guid=MAYA_API_REF_ufe_ref_index_html
+
+To build the 3ds Max USD component, you will need to use the UFE/USDUFE headers and libraries included in the `devkit`.
+
+#### 4. Python Modules
+
+The project exposes some Python APIs which rely on Qt bindings that are possible through the use of PySide6 (or PySide2) and its specific Qt binding library, Shiboken.
+
+For 3ds Max 2025 and greater, PySide6 6.5.3 is required. For 3ds Max 2022 to 2024, PySide2 5.15.1 is required. PySide2/6 are compiled in a custom way for 3ds Max, as such you can get the pre-built binaries in the `devkit`.
+
+The 3ds Max USD Plugin also requires PyOpenGL to make use of the OpenUSD UsdView tool. The module should already present in your Python environment if you have built OpenUSD.
+
+PyOpenGL can be downloaded using Python PIP with the following command: `pip install --user PyOpenGL==3.1.5`
+
+> :warning: Make sure there are no PySide6/2 Python modules installed in your '*site-packages*' folders. You might run into issues otherwise, as it will create conflicts with the 3ds Max packages.
+
+#### 5. MaterialX plugin for 3ds Max (optional)
+
+In order to have support for MaterialX materials inside 3ds Max, you will need to hook up the MaterialX plugin for 3ds Max to the project. The MaterialX Plugin is installed alongside USD for Autodesk 3ds Max Plugin. Instructions for installation can be found here: https://help.autodesk.com/view/3DSMAX/2025/ENU/?guid=GUID-1A33A64B-6829-4806-98FC-9B25E4ED47FC
+
+Once the 3ds Max USD plugin is installed, you will find the `MaterialX for 3ds Max` plugin located in `Contents\MaterialX_plugin`.
+
+#### 6. Other Dependencies
+
+| Dependency | 3ds Max 2022 | 3ds Max 2023 | 3ds Max 2024 | 3ds Max 2025 | Link |
+|:----------------:|:------------:|:------------:|:------------:|:------------:|:---------------------------------------------:|
+| spdlog | 1.14.1 | 1.14.1 | 1.14.1 | 1.14.1 | https://github.com/gabime/spdlog |
+| gtest | 1.8.1 | 1.8.1 | 1.11.0 | 1.11.0 | https://github.com/google/googletest/releases |
+
+##### spdlog
+
+The project is only using the headers from this dependency. You can clone the git repository and locate the include folder (the include path is at the root of the repository).
+
+##### gtest
+GoogleTest is used to compile and execute the unit tests of the component. The library is not required to run the plugin inside 3ds Max. You need to build the dependency as it is not provided through the `devkit`.
+
+### Building
+#### General Information
+A Microsoft Visual Studio solution (`usd-component.sln`) for the 3ds Max USD component is provided in the `src\` folder. Make sure to fully read this section before trying to compile the component.
+
+The component can be built for multiple 3ds Max yearly releases (referred as a *target* version). The Visual Studio solution for the component only support one *target* build at a time.
+
+The solution creates a `build\bin\x64\\usd-component-\` folder in which an Autodesk Application Plugin folder structure is constructed. The build folder can be directly referenced in the `ADSK_APPLICATION_PLUGINS` environment variable for 3ds Max to find and load the compiled plugin. More general information on the plugin packaging can found in the [3ds Max Developer Help](https://help.autodesk.com/view/MAXDEV/2025/ENU/?guid=packaging_plugins).
+
+Edit the file `src\3dsmax.common.settings.props` to set the *target* version (i.e. `2025` to target 3ds Max 2025).
+
+Make sure the dependencies are prepared before launching the build. The next section is important to read to understand the expected structure.
+
+#### Plugin Dependency Setup
+Before starting to build the plugin, the component's dependencies location must be specified to the component's solution. A few options are possible (only use one of the options):
+
+* [_recommended option_] dependencies specific paths can be detailed in the props file `src\DependencyPathOverrides.props`; the paths specified using the props file will override the *defaults* from the component solution.
+
+* the component's Visual Studio solution expects a default directory structure has described below. If you are building the component for multiple _target_ version, this option might be preferable.
+ * `artifacts` folder at the root level
+ * non-*target*-dependent dependencies can be placed in the `artifacts` folder:
+ * `artifacts\spdlog` (the include folder for `spdlog` is placed inside this `spdlog` folder)
+ * for each *target* version, a *target* folder (i.e. : `artifacts\2025` is needed for the remaining dependencies:
+ * `artifacts\\_3dsmax-component-materialX`
+ * `artifacts\\gtest`
+ * `artifacts\\maxsdk`
+ * `artifacts\\Pixar_USD`
+ * `artifacts\\PyOpenGL`
+ * `artifacts\\PySide6\PySide6` (might be `PySide2` depending on the *target*)
+ * `artifacts\\PySide6\shiboken6`
+ * `artifacts\\PySide6\shiboken6_generator`
+ * `artifacts\\Python`
+ * `artifacts\\Qt`
+ * `artifacts\\ufe`
+ * `artifacts\\UsdUfe`
+
+* a Python build script (`build-scripts\build-solution.py`) is provided to launch the build from a command prompt. If not already described in `src\DependencyPathOverrides.props` or not using the default directory structure, the dependency paths can specified directly at the command prompt. This option might be easier to use if you are scripting your build procedure.
+
+The build script can be used by following these usage rules:
+
+ usage: build-solution.py [-h] [-b BUILD] [-v VERSION] [-w] [-r] [-d] [-p] [--maxsdk MAXSDK] [--qtinstall QTINSTALL]
+ [--materialx MATERIALX] [--googletest GOOGLETEST] [--pyopengl PYOPENGL]
+ [--maxusddevkit MAXUSDDEVKIT] [--spdlog SPDLOG] [--python PYTHON] [--pyside PYSIDE]
+ [--shiboken SHIBOKEN] [--ufeinc UFEINC] [--ufelib UFELIB] [--usdufe USDUFE]
+ [--openusd OPENUSD] [--tbb TBB] [--boostinc BOOSTINC] [--boostlib BOOSTLIB]
+ [{release,hybrid}] {2022,2023,2024,2025,2026}
+
+ positional arguments:
+ {release,hybrid} The build configuration type.
+ {2022,2023,2024,2025,2026}
+ The 3ds Max version to target.
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -b BUILD, --build BUILD
+ The build number coming from the pipeline.
+ -v VERSION, --version VERSION
+ The 3ds Max USD component version being built.
+ -w, --warnaserror Enable the compiler to treat all warnings as errors.
+ -r, --rebuild Rebuild the project.
+ -d, --distrib Prepare for redistribution. Write component version in source headers.
+ -p, --package Prepare the package folder after build.
+ --maxsdk MAXSDK The path location for the 'MaxSDK' folder.
+ --qtinstall QTINSTALL
+ The Qt reference version from QtVsTools (aka 'Qt Installation').
+ --materialx MATERIALX
+ The path location for the 3ds Max MaterialX material plugin folder.
+ --googletest GOOGLETEST
+ The path location for the 'gtest' folder.
+ --pyopengl PYOPENGL The path location for the 'OpenGL' Python module (PyOpenGL).
+ --maxusddevkit MAXUSDDEVKIT
+ The path location for the 3ds Max USD 'devkit'.
+ --spdlog SPDLOG The path location for 'spdlog' include folder. If not provided, using the path from the
+ 'devkit' if the 'maxusddevkit' option is provided.
+ --python PYTHON The path location for the 'Python' folder. If not provided, using the path from the 'devkit'
+ if the 'maxusddevkit' option is provided.
+ --pyside PYSIDE The path location for the 'PySide6' Python module. If not provided, using the path from the
+ 'devkit' if the 'maxusddevkit' option is provided.
+ --shiboken SHIBOKEN The path location for the 'shiboken6' Python module. If not provided, using the path from the
+ 'devkit' if the 'maxusddevkit' option is provided.
+ --ufeinc UFEINC The path location for the 'Ufe' include folder. If not provided, using the path from the
+ 'devkit' if the 'maxusddevkit' option is provided.
+ --ufelib UFELIB The path location for the 'Ufe' lib folder. If not provided, using the path from the 'devkit'
+ if the 'maxusddevkit' option is provided.
+ --usdufe USDUFE The path location for the 'UsdUfe' folder. If not provided, using the path from the 'devkit'
+ if the 'maxusddevkit' option is provided.
+ --openusd OPENUSD The path location for the 'OpenUSD' folder. If not provided, using the path from the 'devkit'
+ if the 'maxusddevkit' option is provided.
+ --tbb TBB The path location for the 'TBB' folder. If not provided, using the path from the 'OpenUSD' if
+ the 'openusd' or 'maxusddevkit' option is provided.
+ --boostinc BOOSTINC The path location for the 'Boost' include folder. If not provided, using the path from the
+ 'OpenUSD' if the 'openusd' or 'maxusddevkit' option is provided.
+ --boostlib BOOSTLIB The path location for the 'Boost' lib folder. If not provided, using the path from the
+ 'OpenUSD' if the 'openusd' or 'maxusddevkit' option is provided.
+
+For example, if you have:
+
+ - cloned the `3dsmax-usd` repository to `c:\dev\3dsmax-usd`
+ - you are targeting to build the plugin for 3ds Max 2025
+ - you have installed the Autodesk USD for 3ds Max 2025 application plugin, by default, to `C:\ProgramData\Autodesk\ApplicationPlugins\USD for 3ds Max 2025`
+ - extracted its `devkit` to `c:\dev\3dsmax-usd-devkit-2025` (the `devkit` archive would be found in `C:\ProgramData\Autodesk\ApplicationPlugins\USD for 3ds Max 2025\Contents`)
+ - execute the Python script `copy_missing_DLLs_found_in_the_official_installation.py` found in the `c:\dev\3dsmax-usd-devkit-2025\Pixar_USD` folder to recreate a completed OpenUSD prebuilt library
+ - installed the 3ds Max 2025 SDK, by default, to `C:\Program Files\Autodesk\3ds Max 2025 SDK`)
+ - Qt v6.5.3 is installed in `C:\Qt\6.5.3\msvc2019_64`
+ - PyOpenGL is installed in `C:\Users\myusername\AppData\Roaming\Python\Python311\site-packages`
+ - GoogleTest is installed in `c:\dev\googletest-distribution`
+
+You will have a command-line similar to the one below. Using the `devkit` reduces the amount of steps required to get to a state where the component can be built.
+
+ c:\dev\3dsmax-usd> \
+ python build-scripts\build-solution.py \
+ --maxusddevkit c:\dev\3dsmax-usd-devkit-2025 \
+ --googletest c:\dev\googletest-distribution \
+ --qtinstall c:\Qt\6.5.3\msvc2019_64 \
+ --maxsdk "c:\Program Files\Autodesk\3ds Max 2025 SDK\maxsdk" \
+ --materialx "c:\ProgramData\Autodesk\ApplicationPlugins\USD for 3ds Max 2025\Contents\MaterialX_plugin" \
+ --pyopengl C:\Users\myusername\AppData\Roaming\Python\Python311\site-packages -p release 2025
+
diff --git a/doc/images/3dsmax-usd.png b/doc/images/3dsmax-usd.png
new file mode 100644
index 0000000..3b4a51d
--- /dev/null
+++ b/doc/images/3dsmax-usd.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9569b7c1f5785142e0e77886e4c4a7d0534025d30a10f9a74543440464a14412
+size 1556605
diff --git a/doc/images/header.png b/doc/images/header.png
new file mode 100644
index 0000000..76fa28f
--- /dev/null
+++ b/doc/images/header.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81127bba8392babe8d398f8d8bf483acd011396922f9fbe8fd285bec11bd9f25
+size 830280
diff --git a/doc/images/logo-horizontal-color.svg b/doc/images/logo-horizontal-color.svg
new file mode 100644
index 0000000..f455c91
--- /dev/null
+++ b/doc/images/logo-horizontal-color.svg
@@ -0,0 +1,28 @@
+
diff --git a/doc/images/pxr.png b/doc/images/pxr.png
new file mode 100644
index 0000000..91da6aa
--- /dev/null
+++ b/doc/images/pxr.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3c9258783eae35fc623e014d5cf1343693c3c0f41ed54631d21ee257a4206b44
+size 4070
diff --git a/samples/3dsmaxUsdSample.common.settings.props b/samples/3dsmaxUsdSample.common.settings.props
new file mode 100644
index 0000000..d4b91e1
--- /dev/null
+++ b/samples/3dsmaxUsdSample.common.settings.props
@@ -0,0 +1,65 @@
+
+
+
+
+ true
+ false
+
+
+
+
+
+
+
+
+
+ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)\..\'))
+
+
+ 1
+ 2025
+ $(RepoRootDir)build\sample\
+ $(BuildDir)\bin\$(Platform)\$(Configuration)\
+ ApplicationPlugins\usd-component\Contents\bin\
+ $(RepoRootDir)src\
+ $(RepoRootDir)src\sample\$(PluginsRelDir)\
+ $(ApplicationPluginsDir)$(ProjectName)
+ $(BuildDir)\lib\$(Platform)\$(Configuration)\$(VersionTarget)\
+
+ $(RepoRootDir)artifacts\$(VersionTarget)\
+ $(SolutionDir)\..\
+ $(MaxUsdDevKit)\spdlog
+ $(Artifacts)..\spdlog
+ $(Artifacts)spdlog
+ $(Artifacts)maxsdk
+ $(MaxSDK)\lib\$(Platform)\Release
+ 6.5.3
+ 5.15.1
+
+
+ $(Artifacts)
+
+
+ $(Artifacts)$(VersionTarget)
+
+
+
+
+
+
+
+
+ $(BuildDir)\obj\$(Platform)\$(Configuration)\$(VersionTarget)\$(ProjectName)\
+ $(BuildDir)\lib\$(Platform)\$(Configuration)\$(VersionTarget)
+ $(IntDir)
+ $(ContentsDir)\bin\
+
+
+
+ SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_WCHAR_FILENAMES;FMT_HEADER_ONLY;%(PreprocessorDefinitions)
+ $(SpdLogInc);%(AdditionalIncludeDirectories)
+ stdcpp17
+
+
+
diff --git a/samples/3dsmaxUsdSample.settings.props b/samples/3dsmaxUsdSample.settings.props
new file mode 100644
index 0000000..3d55f56
--- /dev/null
+++ b/samples/3dsmaxUsdSample.settings.props
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+ $(MaxSdkInc);%(AdditionalIncludeDirectories)
+ MAX_$(VersionTarget);%(PreprocessorDefinitions)
+
+
+ $(MaxSdkLibRelease);%(AdditionalLibraryDirectories)
+
+
+
diff --git a/samples/GlTFMaterialWriterSample/Contents/RegisterPlugin.ms b/samples/GlTFMaterialWriterSample/Contents/RegisterPlugin.ms
new file mode 100644
index 0000000..e50e70e
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/Contents/RegisterPlugin.ms
@@ -0,0 +1,61 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+/*
+ * Registers the glTF Material Shader to USD Plug Registry
+ * This script is ran as part of the "post-start-up scripts parts"
+ * This is necessary to ensure that USD can register the plugin properly, and load the associated dll or python script when the time is right.
+ * Loading the USD plugin's DLL before the USD plugin object Load() is called can cause issues/crash.
+ * This script registers either the python or the c++ shader writer. Comment/uncomment the file accordingly to the one you want to load.
+ * Both could be registered at the same time without problem, but only one would ever be executed.
+ * For the purpose of this sample we demonstrate how to load a c++ and a python writer.
+*/
+(
+
+pyUsdPlug = python.import("pxr.Plug")
+
+function addMyMaxUsdPlugin =
+(
+ pysys = python.import("sys")
+ pyos = python.import("os")
+
+ scriptPath = getThisScriptFilename()
+ pluginPath = pathConfig.removePathLeaf scriptPath
+
+ -- note: either use the Python (default) or the C++ version of the plugin
+ -- You must comment/uncomment the respective maxscript code in the sections below
+
+ -- [section python - start]
+ -- Load the python prim reader
+ pluginPath = pluginPath + "\\python\\plugInfo.json"
+ -- The plugin script location must part of the Python path
+ pysys.path.insert 0 (pathConfig.removePathLeaf pluginPath)
+ -- [section python - end]
+
+ -- [section c++ - start]
+ -- Load the c++ prim reader
+ /**
+ * pluginPath = pluginPath + "\\plugInfo.json"
+ */
+ -- [section c++ - end]
+
+ plugRegistry = pyUsdPlug.Registry()
+ plugRegistry.RegisterPlugins pluginPath
+)
+
+addMyMaxUsdPlugin()
+
+)
\ No newline at end of file
diff --git a/samples/GlTFMaterialWriterSample/Contents/plugInfo.json b/samples/GlTFMaterialWriterSample/Contents/plugInfo.json
new file mode 100644
index 0000000..d4b8569
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/Contents/plugInfo.json
@@ -0,0 +1,28 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins": [
+ {
+ "Info": {
+ # Max specific token
+ "MaxUsd": {
+ "ShaderWriter": {
+ # Here we fill the array with the names of the materials supported by the plugin.
+ # In this sample, this is just the glTF material.
+ # To find the name you need to put here, you need to find the non localized name of the material, to do that :
+ # Create an object and assign the material you want to work with in this plugin to it. Select the object.
+ # Querry the class ID of this material in maxscript "classof $.material".
+ # Use the returned value (for glTF material it would be "glTFMaterial") in maxscript listener to call "glTFMaterial.nonLocalizedName"
+ "providesTranslator": [
+ "glTF Material"
+ ]
+ }
+ }
+ },
+ "Name": "glTFMaterialWriter",
+ "Type": "library",
+ "LibraryPath": "glTFMaterialWriter.dll"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/GlTFMaterialWriterSample/Contents/python/glTFShaderWriter.py b/samples/GlTFMaterialWriterSample/Contents/python/glTFShaderWriter.py
new file mode 100644
index 0000000..0ae601b
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/Contents/python/glTFShaderWriter.py
@@ -0,0 +1,60 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import maxUsd
+from pymxs import runtime as rt
+from pxr import UsdShade, Sdf
+import pymxs
+import traceback
+
+class glTFMaterialWriter(maxUsd.ShaderWriter):
+ # This python sample doesn't demonstrate animation, but an example can be found in the C++ sample.
+ def Write(self):
+ try:
+ # the 'GetMaterial()' method returns the anim handle on the material
+ # need to fetch the anim on the handle to get back the pymxs.Material
+ material = rt.GetAnimByHandle(self.GetMaterial())
+
+ # create the Shader prim
+ nodeShader = UsdShade.Shader.Define(self.GetUsdStage(), self.GetUsdPath())
+ nodeShader.CreateIdAttr("UsdPreviewSurface")
+ # assign the created Shader prim as the USD prim for the ShaderWriter
+ self.SetUsdPrim(nodeShader.GetPrim())
+ col = (material.baseColor.r/255, material.baseColor.g/255, material.baseColor.b/255)
+ inp = nodeShader.CreateInput("diffuseColor",Sdf.ValueTypeNames.Color3f)
+ inp.Set(col)
+
+ except Exception as e:
+ # Quite useful to debug errors in a Python callback
+ print('Write() - Error: %s' % str(e))
+ print(traceback.format_exc())
+
+ @classmethod
+ def CanExport(cls, exportArgs):
+ """
+ Static class method required to determine if the class is handling the
+ translation context required by the export job. The current class only
+ handles converting materials to 'UsdPreviewSurface' elements.
+ When this is called, we already know we're dealing with a glTF material,
+ we registered this writer specificaly for it.
+ """
+ if exportArgs.GetConvertMaterialsTo() == "UsdPreviewSurface":
+ return maxUsd.ShaderWriter.ContextSupport.Supported
+ elif "UsdPreviewSurface" not in exportArgs.GetAllMaterialConversions():
+ return maxUsd.ShaderWriter.ContextSupport.Fallback
+ return maxUsd.ShaderWriter.ContextSupport.Unsupported
+
+# Register the writer.
+maxUsd.ShaderWriter.Register(glTFMaterialWriter, "glTF Material")
\ No newline at end of file
diff --git a/samples/GlTFMaterialWriterSample/Contents/python/plugInfo.json b/samples/GlTFMaterialWriterSample/Contents/python/plugInfo.json
new file mode 100644
index 0000000..7f148ad
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/Contents/python/plugInfo.json
@@ -0,0 +1,30 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins": [
+ {
+ "Info": {
+ # Max specific token
+ "MaxUsd": {
+ "ShaderWriter": {
+ # Here we fill the array with the names of the materials supported by the plugin.
+ # In this sample, this is just the glTF material.
+ # To find the name you need to put here, you need to find the non localized name of the material, to do that :
+ # Create an object and assign the material you want to work with in this plugin to it. Select the object.
+ # Querry the class ID of this material in maxscript "classof $.material".
+ # Use the returned value (for glTF material it would be "glTFMaterial") in maxscript listener to call "glTFMaterial.nonLocalizedName"
+ "providesTranslator": [
+ "glTF Material"
+ ]
+ }
+ }
+ },
+ # This is the name of the python script that implements the shader writer.
+ # This file needs to be found at runtime by python, its path must be added
+ # to the python paths. See RegisterPlugin.ms for an exemple of how to do that.
+ "Name": "glTFShaderWriter",
+ "Type": "python"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/GlTFMaterialWriterSample/DLLEntry.h b/samples/GlTFMaterialWriterSample/DLLEntry.h
new file mode 100644
index 0000000..c4dfce4
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/DLLEntry.h
@@ -0,0 +1,18 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+#include
+extern HINSTANCE hInstance;
diff --git a/samples/GlTFMaterialWriterSample/DllEntry.cpp b/samples/GlTFMaterialWriterSample/DllEntry.cpp
new file mode 100644
index 0000000..046dabe
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/DllEntry.cpp
@@ -0,0 +1,33 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "DLLEntry.h"
+
+HINSTANCE hInstance;
+
+// This function is called by Windows when the DLL is loaded. This
+// function may also be called many times during time critical operations
+// like rendering. Therefore developers need to be careful what they
+// do inside this function. In the code below, note how after the DLL is
+// loaded the first time only a few statements are executed.
+BOOL WINAPI DllMain(HINSTANCE hinstDLL, ULONG fdwReason, LPVOID /*lpvReserved*/)
+{
+ if (fdwReason == DLL_PROCESS_ATTACH) {
+ // Hang on to this DLL's instance handle.
+ hInstance = hinstDLL;
+ DisableThreadLibraryCalls(hInstance);
+ }
+ return (TRUE);
+}
\ No newline at end of file
diff --git a/samples/GlTFMaterialWriterSample/GlTFMaterialWriter.cpp b/samples/GlTFMaterialWriterSample/GlTFMaterialWriter.cpp
new file mode 100644
index 0000000..87a4ade
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/GlTFMaterialWriter.cpp
@@ -0,0 +1,141 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "GlTFMaterialWriter.h"
+
+#include "Materials/Mtl.h"
+
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+
+// Macro for the pixar namespace "pxr::"
+// This is putting all of the code, until the close macro, into the pixar namespace.
+// This is needed for the Macros to compile and is required for all the Prim/Shader Writers
+PXR_NAMESPACE_OPEN_SCOPE
+
+// This macro registers the Shader Writer, it's adding the GlTFMaterialWriter class as a candidate
+// when trying to export a glTF material. It also verifies that the class we're registering
+// implements the CanExport() method and is a subclass of MaxUsdShaderWriter. The class_ID
+// represents the glTF material. It is also very important to set the project option "Remove
+// unreferenced code and data" to NO, not doing so could cause the Macro to be optimized out and the
+// Writer to never be properly registered.
+PXR_MAXUSD_REGISTER_SHADER_WRITER(Class_ID(0x38420192, 0x45fe4e1b), GlTFMaterialWriter);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+/*
+ * When this function is called we already know we are dealing with a glTF material,
+ * because we register this writer specifically against the glTF material Class_ID (see registration
+ * macro above). Which means the only thing we have to check is that we're trying to export to the
+ * desired format.
+ */
+MaxUsdShaderWriter::ContextSupport
+GlTFMaterialWriter::CanExport(const MaxUsd::USDSceneBuilderOptions& exportArgs)
+{
+ // GetConvertMaterialsTo returns the current target material being processed
+ // In this sample our target is UsdPreviewSurface.
+ if (exportArgs.GetConvertMaterialsTo() == UsdImagingTokens->UsdPreviewSurface) {
+ return ContextSupport::Supported;
+ }
+ // Only report as fallback if UsdPreviewSurface was not explicitly requested:
+ if (exportArgs.GetAllMaterialConversions().count(UsdImagingTokens->UsdPreviewSurface) == 0) {
+ return ContextSupport::Fallback;
+ }
+ return ContextSupport::Unsupported;
+}
+
+enum GlTFMaterialWriterCodes
+{
+ UnableToDefineShadeShader,
+ InvalidPrimForShadeShader,
+ MissingShadeShader
+};
+TF_REGISTRY_FUNCTION(TfEnum)
+{
+ TF_ADD_ENUM_NAME(UnableToDefineShadeShader, "Could not define UsdShadeShader");
+ TF_ADD_ENUM_NAME(InvalidPrimForShadeShader, "Could not get UsdPrim for UsdShadeShader");
+ TF_ADD_ENUM_NAME(MissingShadeShader, "Could not get UsdShadeShader schema for UsdPrim");
+};
+
+/*
+ * The Shader Writer constructor is expected to define the Shader prim.
+ * The Write method is then responsible for populating it's data.
+ */
+GlTFMaterialWriter::GlTFMaterialWriter(
+ Mtl* material,
+ const SdfPath& usdPath,
+ MaxUsdWriteJobContext& jobCtx)
+ : MaxUsdShaderWriter(material, usdPath, jobCtx)
+{
+ UsdShadeShader shaderSchema = UsdShadeShader::Define(GetUsdStage(), GetUsdPath());
+ if (!shaderSchema) {
+ TF_ERROR(UnableToDefineShadeShader, "at path '%s'\n", GetUsdPath().GetString().c_str());
+ return;
+ }
+
+ UsdAttribute idAttr = shaderSchema.CreateIdAttr(VtValue(UsdImagingTokens->UsdPreviewSurface));
+
+ usdPrim = shaderSchema.GetPrim();
+ if (!usdPrim.IsValid()) {
+ TF_ERROR(
+ InvalidPrimForShadeShader,
+ "at path '%s'\n",
+ shaderSchema.GetPath().GetString().c_str());
+ return;
+ }
+
+ // Surface Output
+ shaderSchema.CreateOutput(UsdShadeTokens->surface, SdfValueTypeNames->Token);
+}
+
+/*
+ * For the purpose of this sample, which is demonstrating the necessary parts needed to implement a
+ * Shader Writer, we will just export the base color of the glTF material. A similar approach can be
+ * taken for the other parameters.
+ */
+void GlTFMaterialWriter::Write()
+{
+ UsdShadeShader shaderSchema(usdPrim);
+ if (!shaderSchema) {
+ TF_ERROR(MissingShadeShader, "at path '%s'\n ", usdPrim.GetPath().GetString().c_str());
+ return;
+ }
+ const auto timeConfig = GetExportArgs().GetResolvedTimeConfig();
+ Mtl* mat = GetMaterial();
+ Interval valid = FOREVER;
+ auto pb = mat->GetParamBlock(0);
+ Point3 col;
+ const TimeValue startTime = timeConfig.GetStartTime();
+ const TimeValue endTime = timeConfig.GetEndTime();
+ // How much time we need to put for each Max's frame.
+ const int timeStep = timeConfig.GetTimeStep();
+ UsdShadeInput in
+ = shaderSchema.CreateInput(TfToken("diffuseColor"), SdfValueTypeNames->Color3f);
+
+ for (TimeValue timeVal = startTime; timeVal <= endTime;) {
+ pb->GetValueByName(_T("baseColor"), timeVal, col, valid);
+ UsdTimeCode usdTimeCode(double(timeVal) / double(GetTicksPerFrame()));
+ in.Set(GfVec3f(col[0], col[1], col[2]), usdTimeCode);
+ timeVal += timeStep;
+ }
+}
diff --git a/samples/GlTFMaterialWriterSample/GlTFMaterialWriter.h b/samples/GlTFMaterialWriterSample/GlTFMaterialWriter.h
new file mode 100644
index 0000000..223ab83
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/GlTFMaterialWriter.h
@@ -0,0 +1,32 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+#include
+
+#include
+
+/// Shader writer for exporting 3ds Max's material shading nodes to USD.
+class GlTFMaterialWriter : public pxr::MaxUsdShaderWriter
+{
+public:
+ GlTFMaterialWriter(
+ Mtl* material,
+ const pxr::SdfPath& usdPath,
+ pxr::MaxUsdWriteJobContext& jobCtx);
+
+ static ContextSupport CanExport(const MaxUsd::USDSceneBuilderOptions& exportArgs);
+ void Write() override;
+};
\ No newline at end of file
diff --git a/samples/GlTFMaterialWriterSample/GlTFMaterialWriterPlugin.vcxproj b/samples/GlTFMaterialWriterSample/GlTFMaterialWriterPlugin.vcxproj
new file mode 100644
index 0000000..1365553
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/GlTFMaterialWriterPlugin.vcxproj
@@ -0,0 +1,85 @@
+
+
+
+
+ Hybrid
+ x64
+
+
+ Release
+ x64
+
+
+
+ glTFMaterialWriterPlugin
+ 16.0
+ Win32Proj
+ {c8835a49-1acd-4952-8541-44e269ea9d6a}
+ DynamicLibrary
+ Unicode
+
+
+
+
+
+
+
+ <_ProjectFileVersion>10.0.30319.1
+ glTFMaterialWriter
+ .dll
+ $(ContentsDir)\Contents\
+
+
+
+ maxutil.lib;paramblk2.lib;geom.lib;%(AdditionalDependencies)
+ $(LibDir)\$(TargetName).lib
+
+
+ @echo Do not forget to update "$(OutDir)RegisterPlugin.ms" in order to load the C++ version of the plugin.
+
+
+ false
+ ../silence_usd_warnings.h
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ ..\%(Filename)%(Extension)
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/GlTFMaterialWriterSample/GlTFMaterialWriterPlugin.vcxproj.filters b/samples/GlTFMaterialWriterSample/GlTFMaterialWriterPlugin.vcxproj.filters
new file mode 100644
index 0000000..d8c1d5d
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/GlTFMaterialWriterPlugin.vcxproj.filters
@@ -0,0 +1,55 @@
+
+
+
+
+ {25315e12-ed5d-4ac2-b13c-ba8f290f8531}
+
+
+ {6f2f59da-a1f4-4d91-84ed-9453ed9aa3b7}
+
+
+ {895ead2c-9600-4cb7-bf86-f6e7212d472a}
+
+
+ {c84288c0-608c-4777-b0bd-330ac9d0e0aa}
+
+
+ {3aa8115e-cee5-47cc-a3dc-261479d40856}
+
+
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Source Files\Contents
+
+
+ Source Files\Contents
+
+
+ Source Files\python
+
+
+ Source Files\python
+
+
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/samples/GlTFMaterialWriterSample/PackageContents.xml b/samples/GlTFMaterialWriterSample/PackageContents.xml
new file mode 100644
index 0000000..70182a4
--- /dev/null
+++ b/samples/GlTFMaterialWriterSample/PackageContents.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/MaxUSDSamples.sln b/samples/MaxUSDSamples.sln
new file mode 100644
index 0000000..1105e99
--- /dev/null
+++ b/samples/MaxUSDSamples.sln
@@ -0,0 +1,69 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31624.102
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "glTFMaterialWriterPlugin", "GlTFMaterialWriterSample\GlTFMaterialWriterPlugin.vcxproj", "{C8835A49-1ACD-4952-8541-44E269EA9D6A}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SpherePrimWriterPlugin", "SpherePrimWriterSample\SpherePrimWriterPlugin.vcxproj", "{68768A54-4E41-4C50-92BC-3D1930ECB8B3}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UserDataExportChaserPlugin", "UserDataExportChaserSample\UserDataExportChaser.vcxproj", "{1EF31957-1C1A-4869-B52D-D733FE24F0A9}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SpherePrimReaderPlugin", "SpherePrimReaderSample\SpherePrimReaderPlugin.vcxproj", "{D472D226-4ACE-4A25-85B7-3263B3FFEE59}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TestSdk", "TestSdk\TestSdk.vcxproj", "{BCDDCE76-F91F-4035-933F-F486B72EE818}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UserDataImportChaserPlugin", "UserDataImportChaserSample\UserDataImportChaser.vcxproj", "{CAE8A112-333A-4CB5-81DD-9F9503D004A2}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Hybrid|x64 = Hybrid|x64
+ Hybrid|x86 = Hybrid|x86
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C8835A49-1ACD-4952-8541-44E269EA9D6A}.Hybrid|x64.ActiveCfg = Hybrid|x64
+ {C8835A49-1ACD-4952-8541-44E269EA9D6A}.Hybrid|x64.Build.0 = Hybrid|x64
+ {C8835A49-1ACD-4952-8541-44E269EA9D6A}.Hybrid|x86.ActiveCfg = Hybrid|x64
+ {C8835A49-1ACD-4952-8541-44E269EA9D6A}.Release|x64.ActiveCfg = Release|x64
+ {C8835A49-1ACD-4952-8541-44E269EA9D6A}.Release|x64.Build.0 = Release|x64
+ {C8835A49-1ACD-4952-8541-44E269EA9D6A}.Release|x86.ActiveCfg = Release|x64
+ {68768A54-4E41-4C50-92BC-3D1930ECB8B3}.Hybrid|x64.ActiveCfg = Hybrid|x64
+ {68768A54-4E41-4C50-92BC-3D1930ECB8B3}.Hybrid|x64.Build.0 = Hybrid|x64
+ {68768A54-4E41-4C50-92BC-3D1930ECB8B3}.Hybrid|x86.ActiveCfg = Hybrid|x64
+ {68768A54-4E41-4C50-92BC-3D1930ECB8B3}.Release|x64.ActiveCfg = Release|x64
+ {68768A54-4E41-4C50-92BC-3D1930ECB8B3}.Release|x64.Build.0 = Release|x64
+ {68768A54-4E41-4C50-92BC-3D1930ECB8B3}.Release|x86.ActiveCfg = Release|x64
+ {1EF31957-1C1A-4869-B52D-D733FE24F0A9}.Hybrid|x64.ActiveCfg = Hybrid|x64
+ {1EF31957-1C1A-4869-B52D-D733FE24F0A9}.Hybrid|x64.Build.0 = Hybrid|x64
+ {1EF31957-1C1A-4869-B52D-D733FE24F0A9}.Hybrid|x86.ActiveCfg = Hybrid|x64
+ {1EF31957-1C1A-4869-B52D-D733FE24F0A9}.Release|x64.ActiveCfg = Release|x64
+ {1EF31957-1C1A-4869-B52D-D733FE24F0A9}.Release|x64.Build.0 = Release|x64
+ {1EF31957-1C1A-4869-B52D-D733FE24F0A9}.Release|x86.ActiveCfg = Release|x64
+ {D472D226-4ACE-4A25-85B7-3263B3FFEE59}.Hybrid|x64.ActiveCfg = Hybrid|x64
+ {D472D226-4ACE-4A25-85B7-3263B3FFEE59}.Hybrid|x64.Build.0 = Hybrid|x64
+ {D472D226-4ACE-4A25-85B7-3263B3FFEE59}.Hybrid|x86.ActiveCfg = Hybrid|x64
+ {D472D226-4ACE-4A25-85B7-3263B3FFEE59}.Release|x64.ActiveCfg = Release|x64
+ {D472D226-4ACE-4A25-85B7-3263B3FFEE59}.Release|x64.Build.0 = Release|x64
+ {D472D226-4ACE-4A25-85B7-3263B3FFEE59}.Release|x86.ActiveCfg = Release|x64
+ {BCDDCE76-F91F-4035-933F-F486B72EE818}.Hybrid|x64.ActiveCfg = Hybrid|x64
+ {BCDDCE76-F91F-4035-933F-F486B72EE818}.Hybrid|x64.Build.0 = Hybrid|x64
+ {BCDDCE76-F91F-4035-933F-F486B72EE818}.Hybrid|x86.ActiveCfg = Hybrid|x64
+ {BCDDCE76-F91F-4035-933F-F486B72EE818}.Release|x64.ActiveCfg = Release|x64
+ {BCDDCE76-F91F-4035-933F-F486B72EE818}.Release|x64.Build.0 = Release|x64
+ {BCDDCE76-F91F-4035-933F-F486B72EE818}.Release|x86.ActiveCfg = Release|x64
+ {CAE8A112-333A-4CB5-81DD-9F9503D004A2}.Hybrid|x64.ActiveCfg = Hybrid|x64
+ {CAE8A112-333A-4CB5-81DD-9F9503D004A2}.Hybrid|x64.Build.0 = Hybrid|x64
+ {CAE8A112-333A-4CB5-81DD-9F9503D004A2}.Hybrid|x86.ActiveCfg = Hybrid|x64
+ {CAE8A112-333A-4CB5-81DD-9F9503D004A2}.Release|x64.ActiveCfg = Release|x64
+ {CAE8A112-333A-4CB5-81DD-9F9503D004A2}.Release|x64.Build.0 = Release|x64
+ {CAE8A112-333A-4CB5-81DD-9F9503D004A2}.Release|x86.ActiveCfg = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {7134C1FA-2164-4BB8-BA27-91494A5223AA}
+ EndGlobalSection
+EndGlobal
diff --git a/samples/SpherePrimReaderSample/Contents/RegisterPlugin.ms b/samples/SpherePrimReaderSample/Contents/RegisterPlugin.ms
new file mode 100644
index 0000000..8768eb9
--- /dev/null
+++ b/samples/SpherePrimReaderSample/Contents/RegisterPlugin.ms
@@ -0,0 +1,61 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+/*
+ * Registers the Sphere Prim Reader to USD Plug Registry
+ * This script is ran as part of the "post-start-up scripts parts"
+ * This is necessary to ensure that USD can register the plugin properly, and load the associated dll or python script when the time is right.
+ * Loading the USD plugin's DLL before the USD plugin object Load() is called can cause issues/crash.
+ * This script registers either the python or the c++ prim reader. Comment/uncomment the file accordingly to the one you want to load.
+ * Both could be registered at the same time without problem, but only one would ever be executed.
+ * For the purpose of this sample we demonstrate how to load a c++ and a python reader.
+*/
+(
+
+pyUsdPlug = python.import("pxr.Plug")
+
+function addMyMaxUsdPlugin =
+(
+ pysys = python.import("sys")
+ pyos = python.import("os")
+
+ scriptPath = getThisScriptFilename()
+ pluginPath = pathConfig.removePathLeaf scriptPath
+
+ -- note: either use the Python (default) or the C++ version of the plugin
+ -- You must comment/uncomment the respective maxscript code in the sections below
+
+ -- [section python - start]
+ -- Load the python prim reader
+ pluginPath = pluginPath + "\\python\\plugInfo.json"
+ -- The plugin script location must part of the Python path
+ pysys.path.insert 0 (pathConfig.removePathLeaf pluginPath)
+ -- [section python - end]
+
+ -- [section c++ - start]
+ -- Load the c++ prim reader
+ /**
+ * pluginPath = pluginPath + "\\plugInfo.json"
+ */
+ -- [section c++ - end]
+
+ plugRegistry = pyUsdPlug.Registry()
+ plugRegistry.RegisterPlugins pluginPath
+)
+
+addMyMaxUsdPlugin()
+
+)
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/Contents/plugInfo.json b/samples/SpherePrimReaderSample/Contents/plugInfo.json
new file mode 100644
index 0000000..ec9e3b4
--- /dev/null
+++ b/samples/SpherePrimReaderSample/Contents/plugInfo.json
@@ -0,0 +1,22 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins": [
+ {
+ "Info": {
+ # Max specific token
+ "MaxUsd": {
+ "PrimReader": {
+ "providesTranslator": [
+ "UsdGeomSphere"
+ ]
+ }
+ }
+ },
+ "Name": "SpherePrimReader",
+ "Type": "library",
+ "LibraryPath": "SpherePrimReader.dll"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/Contents/python/SpherePrimReader.py b/samples/SpherePrimReaderSample/Contents/python/SpherePrimReader.py
new file mode 100644
index 0000000..ee01685
--- /dev/null
+++ b/samples/SpherePrimReaderSample/Contents/python/SpherePrimReader.py
@@ -0,0 +1,71 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import maxUsd
+from pymxs import runtime as rt
+from pxr import UsdGeom
+from pxr import Gf as pyGf
+import pymxs
+import traceback
+
+class SphereReader(maxUsd.PrimReader):
+ '''
+ Prim reader for importing a USD native sphere into 3ds Max
+ '''
+
+ @classmethod
+ def CanImport(cls, args, prim):
+ '''
+ This method is responsible of telling the import process if it can import the current prim.
+ In the case of this sample, the PrimReaderRegistry already filters readers based on the
+ prim type they registered themselves with. Here, we want to make sure the default fallback
+ reader is superceeded by this one by returning 'Supported'.
+ '''
+ return maxUsd.PrimReader.ContextSupport.Supported
+
+ def Read(self):
+ try:
+ usdPrim = self.GetUsdPrim()
+ sphere = UsdGeom.Sphere(usdPrim)
+
+ # create sphere node and attach newly created node to its parents if any
+ parentHandle = self.GetJobContext().GetNodeHandle(usdPrim.GetPath().GetParentPath(), False)
+ if (parentHandle):
+ node = rt.sphere(name=usdPrim.GetName(), parent=rt.GetAnimByHandle(parentHandle))
+ else:
+ node = rt.sphere(name=usdPrim.GetName())
+
+ def RadiusSetter(value, usdTimeCode, maxFrame) -> bool:
+ node.radius = value
+ return True
+ maxUsd.TranslationUtils.ReadUsdAttribute(sphere.GetRadiusAttr(),
+ maxUsd.AnimatedAttributeHelper(RadiusSetter)(),
+ self.GetJobContext())
+
+ self.GetJobContext().RegisterCreatedNode(usdPrim.GetPath(), rt.GetHandleByAnim(node))
+ self.ReadXformable()
+
+ return True
+
+ except Exception as e:
+ # Quite useful to debug errors in a Python callback
+ print('Read() - Error: %s' % str(e))
+ print(traceback.format_exc())
+ return False
+
+# Register the reader.
+# First argument is the reader class, second argument is the USD type name handled by the reader
+maxUsd.PrimReader.Register(SphereReader, "UsdGeomSphere")
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/Contents/python/plugInfo.json b/samples/SpherePrimReaderSample/Contents/python/plugInfo.json
new file mode 100644
index 0000000..1b9fcf0
--- /dev/null
+++ b/samples/SpherePrimReaderSample/Contents/python/plugInfo.json
@@ -0,0 +1,24 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins": [
+ {
+ "Info": {
+ # Max specific token
+ "MaxUsd": {
+ "PrimReader": {
+ "providesTranslator": [
+ "UsdGeomSphere"
+ ]
+ }
+ }
+ },
+ # This is the name of the python script that implements the shader writer.
+ # This file needs to be found on runtime by python, it's path must be added
+ # to the python paths. See RegisterPlugin.ms for an exemple on how to do that.
+ "Name": "SpherePrimReader",
+ "Type": "python"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/DLLEntry.h b/samples/SpherePrimReaderSample/DLLEntry.h
new file mode 100644
index 0000000..c4dfce4
--- /dev/null
+++ b/samples/SpherePrimReaderSample/DLLEntry.h
@@ -0,0 +1,18 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+#include
+extern HINSTANCE hInstance;
diff --git a/samples/SpherePrimReaderSample/DllEntry.cpp b/samples/SpherePrimReaderSample/DllEntry.cpp
new file mode 100644
index 0000000..046dabe
--- /dev/null
+++ b/samples/SpherePrimReaderSample/DllEntry.cpp
@@ -0,0 +1,33 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "DLLEntry.h"
+
+HINSTANCE hInstance;
+
+// This function is called by Windows when the DLL is loaded. This
+// function may also be called many times during time critical operations
+// like rendering. Therefore developers need to be careful what they
+// do inside this function. In the code below, note how after the DLL is
+// loaded the first time only a few statements are executed.
+BOOL WINAPI DllMain(HINSTANCE hinstDLL, ULONG fdwReason, LPVOID /*lpvReserved*/)
+{
+ if (fdwReason == DLL_PROCESS_ATTACH) {
+ // Hang on to this DLL's instance handle.
+ hInstance = hinstDLL;
+ DisableThreadLibraryCalls(hInstance);
+ }
+ return (TRUE);
+}
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/PackageContents.xml b/samples/SpherePrimReaderSample/PackageContents.xml
new file mode 100644
index 0000000..b998dca
--- /dev/null
+++ b/samples/SpherePrimReaderSample/PackageContents.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/SpherePrimReader.cpp b/samples/SpherePrimReaderSample/SpherePrimReader.cpp
new file mode 100644
index 0000000..91bdd2f
--- /dev/null
+++ b/samples/SpherePrimReaderSample/SpherePrimReader.cpp
@@ -0,0 +1,113 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+// Macro for the pixar namespace "pxr::"
+PXR_NAMESPACE_USING_DIRECTIVE
+
+/// Prim reader for importing a USD native sphere.
+class SpherePrimReader : public MaxUsdPrimReader
+{
+public:
+ SpherePrimReader(const UsdPrim& prim, MaxUsdReadJobContext& jobCtx);
+
+ static MaxUsdPrimReader::ContextSupport
+ CanImport(const MaxUsd::MaxSceneBuilderOptions&, const UsdPrim&);
+ bool Read() override;
+};
+
+// This macro registers the Prim Reader, it's adding the SpherePrimReader class as a candidate when
+// trying to import a UsdGeomSphere prim. The CanImport() method is responsible for defining what
+// can be imported or not. It is also really important to set the project option "Remove
+// unreferenced code and data" to NO, this could cause the Macro to be optimized out and the Reader
+// to never be properly registered.
+
+TF_REGISTRY_FUNCTION_WITH_TAG(MaxUsdPrimReaderRegistry, UsdGeomSphere)
+{
+ MaxUsdPrimReaderRegistry::Register(
+ SpherePrimReader::CanImport, [](const UsdPrim& prim, MaxUsdReadJobContext& context) {
+ return std::make_shared(prim, context);
+ });
+}
+
+/*
+ * This Reader only deals with Sphere objects
+ */
+
+SpherePrimReader::SpherePrimReader(const UsdPrim& prim, MaxUsdReadJobContext& jobCtx)
+ : MaxUsdPrimReader(prim, jobCtx)
+{
+}
+
+MaxUsdPrimReader::ContextSupport
+SpherePrimReader::CanImport(const MaxUsd::MaxSceneBuilderOptions&, const UsdPrim&)
+{
+ // the primreader registry applies a first filter based on prim types
+ // this reader supports all UsdGeomSphere prims
+ return MaxUsdPrimReader::ContextSupport::Supported;
+}
+
+/*
+ * For this sample, we will demonstrate how to import
+ */
+bool SpherePrimReader::Read()
+{
+ const UsdPrim& usdPrim = GetUsdPrim();
+ if (!usdPrim) {
+ return false;
+ }
+ const UsdGeomSphere sphereSchema(usdPrim);
+ if (!sphereSchema) {
+ return false;
+ }
+
+ GeomObject* maxSphere = static_cast(
+ GetCOREInterface()->CreateInstance(GEOMOBJECT_CLASS_ID, Class_ID(SPHERE_CLASS_ID, 0)));
+ auto spherePb = maxSphere->GetParamBlock(0);
+
+ // animated attribute
+ if (!MaxUsdTranslatorUtil::ReadUsdAttribute(
+ sphereSchema.GetRadiusAttr(),
+ [&](const VtValue& radius, const UsdTimeCode&, const TimeValue time) {
+ spherePb->SetValueByName(
+ _T("radius"), static_cast(radius.Get()), time);
+ return true;
+ },
+ GetJobContext())) {
+ // unable to properly set the radius of the sphere
+ TF_WARN(
+ "Unable to properly set the radius on '%s'.", usdPrim.GetName().GetString().c_str());
+ return false;
+ }
+
+ INode* createdNode = MaxUsdTranslatorPrim::CreateAndRegisterNode(
+ usdPrim, maxSphere, usdPrim.GetName(), GetJobContext());
+
+ // read Xform attributes and convert those into 3ds Max transform values
+ MaxUsdTranslatorXformable::Read(usdPrim, createdNode, GetJobContext());
+
+ return true;
+}
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/SpherePrimReader.h b/samples/SpherePrimReaderSample/SpherePrimReader.h
new file mode 100644
index 0000000..c84a491
--- /dev/null
+++ b/samples/SpherePrimReaderSample/SpherePrimReader.h
@@ -0,0 +1,31 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include
+#include
+
+#include
+
+/// Prim reader for importing a USD native sphere.
+class SpherePrimReader : public pxr::MaxUsdPrimReader
+{
+public:
+ SpherePrimReader(const pxr::UsdPrim& prim, const MaxSceneBuilderOptions& options);
+ static ContextSupport
+ CanImport(const MaxSceneBuilderOptions& importArgs, const UsdPrim& importPrim);
+ bool Read(pxr::MaxUsdReadJobContext& context) override;
+};
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/SpherePrimReaderPlugin.filters b/samples/SpherePrimReaderSample/SpherePrimReaderPlugin.filters
new file mode 100644
index 0000000..19be970
--- /dev/null
+++ b/samples/SpherePrimReaderSample/SpherePrimReaderPlugin.filters
@@ -0,0 +1,54 @@
+
+
+
+
+ {25315e12-ed5d-4ac2-b13c-ba8f290f8531}
+
+
+ {6f2f59da-a1f4-4d91-84ed-9453ed9aa3b7}
+
+
+ {895ead2c-9600-4cb7-bf86-f6e7212d472a}
+
+
+ {c84288c0-608c-4777-b0bd-330ac9d0e0aa}
+
+
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Source Files\python
+
+
+ Source Files\python
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/SpherePrimReaderPlugin.vcxproj b/samples/SpherePrimReaderSample/SpherePrimReaderPlugin.vcxproj
new file mode 100644
index 0000000..e947d78
--- /dev/null
+++ b/samples/SpherePrimReaderSample/SpherePrimReaderPlugin.vcxproj
@@ -0,0 +1,85 @@
+
+
+
+
+ Hybrid
+ x64
+
+
+ Release
+ x64
+
+
+
+ SpherePrimReaderPlugin
+ 16.0
+ Win32Proj
+ {D472D226-4ACE-4A25-85B7-3263B3FFEE59}
+ DynamicLibrary
+ Unicode
+
+
+
+
+
+
+
+ <_ProjectFileVersion>10.0.30319.1
+ SpherePrimReader
+ .dll
+ $(ContentsDir)\Contents\
+
+
+
+ MNMath.lib;mesh.lib;maxutil.lib;paramblk2.lib;geom.lib;%(AdditionalDependencies)
+ $(LibDir)\$(TargetName).lib
+
+
+ false
+ ../silence_usd_warnings.h
+
+
+ @echo Do not forget to update "$(OutDir)RegisterPlugin.ms" in order to load the C++ version of the plugin.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ ..\%(Filename)%(Extension)
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/SpherePrimReaderSample/SpherePrimReaderPlugin.vcxproj.filters b/samples/SpherePrimReaderSample/SpherePrimReaderPlugin.vcxproj.filters
new file mode 100644
index 0000000..4d3e57f
--- /dev/null
+++ b/samples/SpherePrimReaderSample/SpherePrimReaderPlugin.vcxproj.filters
@@ -0,0 +1,48 @@
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+
+ Source Files\Contents
+
+
+ Source Files\python
+
+
+ Source Files\Contents
+
+
+ Source Files\python
+
+
+
+
+ {dd38d7f3-3c3d-435f-91f0-4942c731cd56}
+
+
+ {1c1443d9-57da-4918-a1e4-1458209a98af}
+
+
+ {0a465c0d-eacc-4ed1-a273-97598cf28a73}
+
+
+ {5cbeeec9-de6b-430c-a27a-03d0db1e3bd4}
+
+
+
+
+ Header Files
+
+
+
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/Contents/RegisterPlugin.ms b/samples/SpherePrimWriterSample/Contents/RegisterPlugin.ms
new file mode 100644
index 0000000..ce89364
--- /dev/null
+++ b/samples/SpherePrimWriterSample/Contents/RegisterPlugin.ms
@@ -0,0 +1,61 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+/*
+ * Registers the Sphere Prim Writer to USD Plug Registry
+ * This script is ran as part of the "post-start-up scripts parts"
+ * This is necessary to ensure that USD can register the plugin properly, and load the associated dll or python script when the time is right.
+ * Loading the USD plugin's DLL before the USD plugin object Load() is called can cause issues/crash.
+ * This script registers either the python or the c++ shader writer. Comment/uncomment the file accordingly to the one you want to load.
+ * Both could be registered at the same time without problem, but only one would ever be executed.
+ * For the purpose of this sample we demonstrate how to load a c++ and a python writer.
+*/
+(
+
+pyUsdPlug = python.import("pxr.Plug")
+
+function addMyMaxUsdPlugin =
+(
+ pysys = python.import("sys")
+ pyos = python.import("os")
+
+ scriptPath = getThisScriptFilename()
+ pluginPath = pathConfig.removePathLeaf scriptPath
+
+ -- note: either use the Python (default) or the C++ version of the plugin
+ -- You must comment/uncomment the respective maxscript code in the sections below
+
+ -- [section python - start]
+ -- Load the python prim reader
+ pluginPath = pluginPath + "\\python\\plugInfo.json"
+ -- The plugin script location must part of the Python path
+ pysys.path.insert 0 (pathConfig.removePathLeaf pluginPath)
+ -- [section python - end]
+
+ -- [section c++ - start]
+ -- Load the c++ prim reader
+ /**
+ * pluginPath = pluginPath + "\\plugInfo.json"
+ */
+ -- [section c++ - end]
+
+ plugRegistry = pyUsdPlug.Registry()
+ plugRegistry.RegisterPlugins pluginPath
+)
+
+addMyMaxUsdPlugin()
+
+)
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/Contents/plugInfo.json b/samples/SpherePrimWriterSample/Contents/plugInfo.json
new file mode 100644
index 0000000..9934829
--- /dev/null
+++ b/samples/SpherePrimWriterSample/Contents/plugInfo.json
@@ -0,0 +1,21 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins":[
+ {
+ "Info":{
+ # Max specific token
+ "MaxUsd":{
+ # This is left empty by design, the "CanConvert" method will be responsible
+ # of determining if this prim writer can be used.
+ # The PrimWriter key is all that is need for the plugin to be found and loaded on export.
+ "PrimWriter" : {}
+ }
+ },
+ "Name": "SpherePrimWriter",
+ "Type": "library",
+ "LibraryPath": "SpherePrimWriter.dll"
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/Contents/python/SpherePrimWriter.py b/samples/SpherePrimWriterSample/Contents/python/SpherePrimWriter.py
new file mode 100644
index 0000000..7f589ae
--- /dev/null
+++ b/samples/SpherePrimWriterSample/Contents/python/SpherePrimWriter.py
@@ -0,0 +1,105 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import maxUsd
+from pymxs import runtime as rt
+from pxr import UsdGeom
+from pxr import Gf as pyGf
+import pymxs
+import traceback
+
+EXPORT_AS_NATIVE_SPHERE = True
+
+# Prim writer for exporting Sphere object to USD native sphere.
+class SphereWriter(maxUsd.PrimWriter):
+
+ # This Writer only deals with Sphere objects, so we return a "Sphere" token because we want to convert the node to a native USD Sphere.
+ # For performance reasons, the export is done in two passes.
+ # The first pass creates all the prims inside a single SdfChangeBlock, the second pass populate each prim's attributes.
+ # It is not mandatory to implement this function, it is also possible to define the prim from the Write() method (which would override the prim's type created in the first pass),
+ # but you would lose the performance benefit. If not implemented, the base implementation returns Xform.
+ def GetPrimType(self):
+ if EXPORT_AS_NATIVE_SPHERE:
+ return "Sphere"
+ return "Mesh"
+
+ # For this sample, we will demonstrate how to export the Radius and DisplayColor attributes.
+ # We'll also demonstrate one way to handle animation.
+ def Write(self, prim, applyOffset, time):
+ try:
+ nodeHandle = self.GetNodeHandle()
+ stage = prim.GetStage()
+ opts = self.GetExportArgs()
+ node = rt.maxOps.getNodeByHandle(nodeHandle)
+
+ usdTime = time.GetUsdTime()
+ maxTime = time.GetMaxTime()
+
+ if EXPORT_AS_NATIVE_SPHERE:
+ # prim is already a Sphere, it was created for us from the type returned in GetPrimType()
+ spherePrim = UsdGeom.Sphere(prim)
+ radiusAttr = spherePrim.GetRadiusAttr()
+ extentAttr = spherePrim.CreateExtentAttr()
+ radius = None
+ with pymxs.attime(maxTime):
+ radius = node.radius
+ radiusAttr.Set(radius, usdTime)
+ extent = pyGf.Vec3f(radius, radius, radius)
+ extentAttr.Set([-extent, extent], usdTime)
+
+ # Alternatively, we could export the Sphere as a mesh, using the MeshConverter utility :
+ else:
+ spherePrim = maxUsd.MeshConverter.ConvertToUSDMesh(nodeHandle, stage, prim.GetPath(), opts, applyOffset, time)
+
+ if time.IsFirstFrame():
+ if EXPORT_AS_NATIVE_SPHERE:
+ print("Write a Sphere as a USD Sphere prim!")
+ else:
+ print("Write a Sphere as a USD Mesh prim!")
+
+ # Setup the wireColor as displayColor.
+ displayColorAttr = spherePrim.GetDisplayColorAttr()
+ r = node.wireColor.r / 255
+ g = node.wireColor.g / 255
+ b = node.wireColor.b / 255
+ displayColorAttr.Set([(r,g,b)])
+ return True
+
+ except Exception as e:
+ # Quite useful to debug errors in a Python callback
+ print('Write() - Error: %s' % str(e))
+ print(traceback.format_exc())
+ return False
+
+ def GetValidityInterval(self, timeFrame):
+ # The base implementation of GetValidityInterval() will return the object's validity interval.
+ # So the write() method would only be called when the object changes.
+ # for demonstration purposes, lets force the exporter to call the object's write every frame,
+ # by telling it that what we export at each frame is only valid at that exact frame.
+ return maxUsd.Interval(timeFrame,timeFrame)
+
+ # This method is responsible of telling the export process if it can export the current node's object.
+ # In the case of this sample, the only object type we want to handle is the Sphere.
+ @classmethod
+ def CanExport(cls, nodeHandle, exportArgs):
+ node = rt.maxOps.getNodeByHandle(nodeHandle)
+ if rt.classOf(node) == rt.Sphere:
+ return maxUsd.PrimWriter.ContextSupport.Supported
+ return maxUsd.PrimWriter.ContextSupport.Unsupported
+
+# Register the writer.
+# First argument is the class, second argument is the Writer name, which will be used as an ID internaly.
+maxUsd.PrimWriter.Register(SphereWriter, "SphereWriter")
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/Contents/python/plugInfo.json b/samples/SpherePrimWriterSample/Contents/python/plugInfo.json
new file mode 100644
index 0000000..8074de5
--- /dev/null
+++ b/samples/SpherePrimWriterSample/Contents/python/plugInfo.json
@@ -0,0 +1,23 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins": [
+ {
+ "Info": {
+ # Max specific token
+ "MaxUsd": {
+ # This is left empty by design, the "CanExport" method will be responsible
+ # of determining if this prim writer can be used.
+ # The PrimWriter key is all that is need for the plugin to be found and loaded on export.
+ "PrimWriter": {}
+ }
+ },
+ # This is the name of the python script that implements the shader writer.
+ # This file needs to be found on runtime by python, it's path must be added
+ # to the python paths. See RegisterPlugin.ms for an exemple on how to do that.
+ "Name": "SpherePrimWriter",
+ "Type": "python"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/DLLEntry.h b/samples/SpherePrimWriterSample/DLLEntry.h
new file mode 100644
index 0000000..c4dfce4
--- /dev/null
+++ b/samples/SpherePrimWriterSample/DLLEntry.h
@@ -0,0 +1,18 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+#include
+extern HINSTANCE hInstance;
diff --git a/samples/SpherePrimWriterSample/DllEntry.cpp b/samples/SpherePrimWriterSample/DllEntry.cpp
new file mode 100644
index 0000000..046dabe
--- /dev/null
+++ b/samples/SpherePrimWriterSample/DllEntry.cpp
@@ -0,0 +1,33 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "DLLEntry.h"
+
+HINSTANCE hInstance;
+
+// This function is called by Windows when the DLL is loaded. This
+// function may also be called many times during time critical operations
+// like rendering. Therefore developers need to be careful what they
+// do inside this function. In the code below, note how after the DLL is
+// loaded the first time only a few statements are executed.
+BOOL WINAPI DllMain(HINSTANCE hinstDLL, ULONG fdwReason, LPVOID /*lpvReserved*/)
+{
+ if (fdwReason == DLL_PROCESS_ATTACH) {
+ // Hang on to this DLL's instance handle.
+ hInstance = hinstDLL;
+ DisableThreadLibraryCalls(hInstance);
+ }
+ return (TRUE);
+}
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/PackageContents.xml b/samples/SpherePrimWriterSample/PackageContents.xml
new file mode 100644
index 0000000..58ed04b
--- /dev/null
+++ b/samples/SpherePrimWriterSample/PackageContents.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/SpherePrimWriter.cpp b/samples/SpherePrimWriterSample/SpherePrimWriter.cpp
new file mode 100644
index 0000000..824a841
--- /dev/null
+++ b/samples/SpherePrimWriterSample/SpherePrimWriter.cpp
@@ -0,0 +1,160 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "SpherePrimWriter.h"
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+
+// Macro for the pixar namespace "pxr::"
+// This is putting all of the code, until the close macro, into the pixar namespace.
+// This is needed for the Macros to compile and is required for all the Prim/Shader Writers
+PXR_NAMESPACE_OPEN_SCOPE
+
+// This macro registers the Prim Writer, it's adding the SpherePrimWriter class as a candidate when
+// trying to export an object. Unlike for shader writer, this doesn't contain information about the
+// supported classes. The CanExport() method is responsible for defining what can be exported or
+// not. It is also really important to set the project option "Remove unreferenced code and data" to
+// NO, this could cause the Macro to be optimized out and the Writer to never be properly
+// registered.
+PXR_MAXUSD_REGISTER_WRITER(SpherePrimWriter);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+// For demonstration purposes, provide an example to export using a native usd sphere, or as a usd
+// mesh.
+static bool EXPORT_AS_NATIVE_SPHERE = true;
+
+/*
+ * This method is responsible of telling the export process if it can export the current node's
+ * object. In the case of this sample, the only object type we want to handle is the Sphere.
+ */
+pxr::MaxUsdPrimWriter::ContextSupport
+SpherePrimWriter::CanExport(INode* node, const MaxUsd::USDSceneBuilderOptions&)
+{
+ // We simply pass TimeValue "0" here to EvalWorldStage() as it is not important at what time we
+ // evaluate the object, we are only looking at the object's type and we assume that it will not
+ // change over time.
+ const auto object = node->EvalWorldState(0).obj;
+ if (object->ClassID() == Class_ID(SPHERE_CLASS_ID, 0)) {
+ return ContextSupport::Supported;
+ }
+ return ContextSupport::Unsupported;
+}
+
+/*
+ * This Writer only deals with Sphere objects - we return a "Sphere" token if we want to convert the
+ * node to a native USD Sphere, or mesh, if not. For performance reasons, the export is done in two
+ * passes. The first pass creates all the prims inside a single SdfChangeBlock, the second pass
+ * populate each prim's attributes. It is not mandatory to implement this function, it is also
+ * possible to define the prim from the Write() method (which would override the prim's type created
+ * in the first pass), but you would lose the performance benefit. If not implemented, the base
+ * implementation returns Xform.
+ */
+pxr::TfToken SpherePrimWriter::GetPrimType()
+{
+ return EXPORT_AS_NATIVE_SPHERE ? pxr::TfToken("Sphere") : pxr::TfToken("Mesh");
+}
+
+SpherePrimWriter::SpherePrimWriter(const pxr::MaxUsdWriteJobContext& jobCtx, INode* node)
+ : MaxUsdPrimWriter(jobCtx, node)
+{
+}
+
+/*
+ * For this sample, we will demonstrate how to export the Radius and DisplayColor attributes.
+ * We'll also demonstrate one way to handle animation.
+ */
+bool SpherePrimWriter::Write(
+ pxr::UsdPrim& targetPrim,
+ bool applyOffsetTransform,
+ const MaxUsd::ExportTime& timeFrame)
+{
+ auto sourceNode = GetNode();
+
+ if (EXPORT_AS_NATIVE_SPHERE) {
+ // targetPrim is already a Sphere, it was created for us from the type returned in
+ // GetPrimType()
+ pxr::UsdGeomSphere spherePrim(targetPrim);
+ pxr::UsdAttribute radiusAttr = spherePrim.CreateRadiusAttr();
+ pxr::UsdAttribute extentAttr = spherePrim.CreateExtentAttr();
+ const auto spherePb
+ = sourceNode->EvalWorldState(timeFrame.GetMaxTime()).obj->GetParamBlock(0);
+ float radius = 0;
+ Interval inter = FOREVER;
+
+ // Get the value in Max time.
+ // We already know we're dealing with a Sphere object at this point, because we wouldn't be
+ // here otherwise. CanConvert must have passed for Write to be called.
+ spherePb->GetValueByName(_T("Radius"), timeFrame.GetMaxTime(), radius, inter);
+ // Set it at the equivalent usd time.
+ // When doing set on Attribute, the type must match exactly, otherwise it'll do nothing.
+ radiusAttr.Set(static_cast(radius), timeFrame.GetUsdTime());
+ // Set the extent Attribute, this is usd to compute the bounding box in USD
+ pxr::VtVec3fArray extent
+ = { pxr::GfVec3f(-radius, -radius, -radius), pxr::GfVec3f(radius, radius, radius) };
+ extentAttr.Set(extent, timeFrame.GetUsdTime());
+
+ if (timeFrame.IsFirstFrame()) {
+ pxr::UsdAttribute displayColorAttr = spherePrim.CreateDisplayColorAttr();
+ const COLORREF wireCol = sourceNode->GetWireColor();
+ pxr::VtVec3fArray colVec;
+ colVec.push_back(pxr::GfVec3f(
+ static_cast(GetRValue(wireCol)) / 255.f,
+ static_cast(GetGValue(wireCol)) / 255.f,
+ static_cast(GetBValue(wireCol)) / 255.f));
+ // When doing set on Attribute, the type must match exactly, otherwise it'll do nothing.
+ displayColorAttr.Set(colVec);
+ }
+ }
+ // Alternatively, we could export the Sphere as a mesh, using the MeshConverter utility :
+ else {
+ MaxUsd::MeshConverter converter;
+ converter.ConvertToUSDMesh(
+ sourceNode,
+ targetPrim.GetStage(),
+ targetPrim.GetPath(),
+ GetExportArgs().GetMeshConversionOptions(),
+ applyOffsetTransform,
+ GetExportArgs().GetResolvedTimeConfig().IsAnimated(),
+ timeFrame);
+ }
+ return true;
+}
+
+Interval SpherePrimWriter::GetValidityInterval(const TimeValue& time)
+{
+ // The base implementation of GetValidityInterval() will return the object's validity interval.
+ // In the sphere writer, we only export the radius, for demonstration purposes, lets make sure
+ // we only export the frames we really need, by telling the exporter that the exported sphere
+ // is valid as long as the radius doesnt change (i.e. we dont care about other properties that
+ // may change on the 3dsmax sphere)
+ const auto node = GetNode();
+ const auto spherePb = node->EvalWorldState(time).obj->GetParamBlock(0);
+
+ Interval radiusInterval = FOREVER;
+ float radius = 0;
+ spherePb->GetValueByName(_T("Radius"), time, radius, radiusInterval);
+
+ return radiusInterval;
+}
diff --git a/samples/SpherePrimWriterSample/SpherePrimWriter.h b/samples/SpherePrimWriterSample/SpherePrimWriter.h
new file mode 100644
index 0000000..c219b12
--- /dev/null
+++ b/samples/SpherePrimWriterSample/SpherePrimWriter.h
@@ -0,0 +1,36 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#pragma once
+#include
+
+#include
+
+/// Prim writer for exporting Sphere object to USD native sphere.
+class SpherePrimWriter : public pxr::MaxUsdPrimWriter
+{
+public:
+ SpherePrimWriter(const pxr::MaxUsdWriteJobContext& jobCtx, INode* node);
+
+ static ContextSupport CanExport(INode* node, const MaxUsd::USDSceneBuilderOptions& exportArgs);
+ pxr::TfToken GetPrimType() override;
+ bool Write(
+ pxr::UsdPrim& targetPrim,
+ bool applyOffsetTransform,
+ const MaxUsd::ExportTime& timeFrame) override;
+
+ virtual Interval GetValidityInterval(const TimeValue& time);
+};
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/SpherePrimWriterPlugin.filters b/samples/SpherePrimWriterSample/SpherePrimWriterPlugin.filters
new file mode 100644
index 0000000..bd83ef1
--- /dev/null
+++ b/samples/SpherePrimWriterSample/SpherePrimWriterPlugin.filters
@@ -0,0 +1,54 @@
+
+
+
+
+ {25315e12-ed5d-4ac2-b13c-ba8f290f8531}
+
+
+ {6f2f59da-a1f4-4d91-84ed-9453ed9aa3b7}
+
+
+ {895ead2c-9600-4cb7-bf86-f6e7212d472a}
+
+
+ {c84288c0-608c-4777-b0bd-330ac9d0e0aa}
+
+
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Source Files\python
+
+
+ Source Files\python
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/SpherePrimWriterPlugin.vcxproj b/samples/SpherePrimWriterSample/SpherePrimWriterPlugin.vcxproj
new file mode 100644
index 0000000..c1d1cf4
--- /dev/null
+++ b/samples/SpherePrimWriterSample/SpherePrimWriterPlugin.vcxproj
@@ -0,0 +1,85 @@
+
+
+
+
+ Hybrid
+ x64
+
+
+ Release
+ x64
+
+
+
+ SpherePrimWriterPlugin
+ 16.0
+ Win32Proj
+ {68768A54-4E41-4C50-92BC-3D1930ECB8B3}
+ DynamicLibrary
+ Unicode
+
+
+
+
+
+
+
+ <_ProjectFileVersion>10.0.30319.1
+ SpherePrimWriter
+ .dll
+ $(ContentsDir)\Contents\
+
+
+
+ MNMath.lib;mesh.lib;maxutil.lib;paramblk2.lib;geom.lib;%(AdditionalDependencies)
+ $(LibDir)\$(TargetName).lib
+
+
+ false
+ ../silence_usd_warnings.h
+
+
+ @echo Do not forget to update "$(OutDir)RegisterPlugin.ms" in order to load the C++ version of the plugin.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ ..\%(Filename)%(Extension)
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/SpherePrimWriterSample/SpherePrimWriterPlugin.vcxproj.filters b/samples/SpherePrimWriterSample/SpherePrimWriterPlugin.vcxproj.filters
new file mode 100644
index 0000000..ca17f11
--- /dev/null
+++ b/samples/SpherePrimWriterSample/SpherePrimWriterPlugin.vcxproj.filters
@@ -0,0 +1,52 @@
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Source Files\Contents
+
+
+ Source Files\Contents
+
+
+ Source Files\python
+
+
+ Source Files\python
+
+
+
+
+ {dd38d7f3-3c3d-435f-91f0-4942c731cd56}
+
+
+ {1c1443d9-57da-4918-a1e4-1458209a98af}
+
+
+ {0a465c0d-eacc-4ed1-a273-97598cf28a73}
+
+
+ {e225c4eb-e34d-4634-a702-f847f5d799e3}
+
+
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+
\ No newline at end of file
diff --git a/samples/USD.props b/samples/USD.props
new file mode 100644
index 0000000..9c663bd
--- /dev/null
+++ b/samples/USD.props
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+ $(MaxUsdDevKit)\Pixar_USD
+ $(Artifacts)\Pixar_USD
+ $(Artifacts)\Pixar_USD
+ $(Artifacts)\Pixar_USD\Release
+ $(PxrUsdRoot)\include
+ $(PxrUsdRoot)\lib
+ $(PxrUsdRoot)\include\boost-1_82
+ $(PxrUsdRoot)\include\boost-1_81
+ $(PxrUsdRoot)\include\boost-1_76
+ $(PxrUsdRoot)\include\boost-1_70
+ $(Pixar_USDLib)
+ $(MaxUsdDevKit)\Pixar_USD
+ $(Artifacts)\Python
+ $(PythonLocation)\Include
+ $(PythonLocation)\libs
+ $(RepoRootDir)\build\lib\$(Platform)\$(Configuration)\$(VersionTarget)\
+ $(SolutionDir)\..\maxusd\lib
+ $(SrcDir)
+ $(SolutionDir)\..\maxusd\include
+
+
+ $(PxrUsdRoot)\include\;$(VC_IncludePath);$(WindowsSDK_IncludePath)
+
+
+
+ USD_VERSION_21_11;%(PreprocessorDefinitions)
+
+ USD_VERSION_22_11;%(PreprocessorDefinitions)
+ USD_VERSION_23_08;%(PreprocessorDefinitions)
+
+
+ $(SolutionDir);$(BoostInc);$(PythonInc);$(Pixar_USDInc);$(MaxUSDIncludeFolder);%(AdditionalIncludeDirectories)
+ _SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING;NOMINMAX=1;TBB_SUPPRESS_DEPRECATED_MESSAGES;%(PreprocessorDefinitions)
+ BOOST_LIB_TOOLSET="vc142";%(PreprocessorDefinitions)
+ BOOST_LIB_TOOLSET="vc141";%(PreprocessorDefinitions)
+ BOOST_LINKING_PYTHON=1;TBB_USE_DEBUG=1;BOOST_DEBUG_PYTHON=1;%(PreprocessorDefinitions)
+ _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING;%(PreprocessorDefinitions)
+ 4003;%(DisableSpecificWarnings)
+ /experimental:external /external:I $(PxrUsdRoot)\include\ /external:W0 %(AdditionalOptions)
+ /Zc:__cplusplus
+
+
+
+ Disabled
+ ;_HYBRID;IS_HYBRID;SECURE_SCL=0;%(PreprocessorDefinitions)
+
+ Default
+
+
+ $(Pixar_USDLib);$(PythonLibs);$(MaxUSDLibFolder);%(AdditionalLibraryDirectories)
+ core.lib;maxutil.lib;maxUsd.lib;3dsmax_usd.lib;3dsmax_sdf.lib;3dsmax_tf.lib;3dsmax_ar.lib;3dsmax_arch.lib;3dsmax_garch.lib;3dsmax_gf.lib;3dsmax_glf.lib;3dsmax_hd.lib;3dsmax_hdSt.lib;3dsmax_hdx.lib;3dsmax_hf.lib;3dsmax_kind.lib;3dsmax_sdr.lib;3dsmax_ndr.lib;3dsmax_plug.lib;3dsmax_trace.lib;3dsmax_usdGeom.lib;3dsmax_usdImaging.lib;3dsmax_usdImagingGL.lib;3dsmax_usdLux.lib;3dsmax_vt.lib;3dsmax_work.lib;3dsmax_usdUtils.lib;3dsmax_usdShade.lib;3dsmax_usdUI.lib;%(AdditionalDependencies)
+
+
+
diff --git a/samples/UserDataExportChaserSample/Contents/RegisterPlugin.ms b/samples/UserDataExportChaserSample/Contents/RegisterPlugin.ms
new file mode 100644
index 0000000..40034da
--- /dev/null
+++ b/samples/UserDataExportChaserSample/Contents/RegisterPlugin.ms
@@ -0,0 +1,61 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+/*
+ * Registers the User Data Export Chaser to USD Plug Registry
+ * This script is ran as part of the "post-start-up scripts parts"
+ * This is necessary to ensure that USD can register the plugin properly, and load the associated dll or python script when the time is right.
+ * Loading the USD plugin's DLL before the USD plugin object Load() is called can cause issues/crash.
+ * This script registers either the python or the c++ export chaser. Comment/uncomment the file accordingly to the one you want to load.
+ * Both could be registered at the same time without problem, but only one would ever be executed.
+ * For the purpose of this sample we demonstrate how to load a c++ and a python chaser.
+*/
+(
+
+pyUsdPlug = python.import("pxr.Plug")
+
+function addMyMaxUsdPlugin =
+(
+ pysys = python.import("sys")
+ pyos = python.import("os")
+
+ scriptPath = getThisScriptFilename()
+ pluginPath = pathConfig.removePathLeaf scriptPath
+
+ -- note: either use the Python (default) or the C++ version of the plugin
+ -- You must comment/uncomment the respective maxscript code in the sections below
+
+ -- [section python - start]
+ -- Load the python prim reader
+ pluginPath = pluginPath + "\\python\\plugInfo.json"
+ -- The plugin script location must part of the Python path
+ pysys.path.insert 0 (pathConfig.removePathLeaf pluginPath)
+ -- [section python - end]
+
+ -- [section c++ - start]
+ -- Load the c++ prim reader
+ /**
+ * pluginPath = pluginPath + "\\plugInfo.json"
+ */
+ -- [section c++ - end]
+
+ plugRegistry = pyUsdPlug.Registry()
+ plugRegistry.RegisterPlugins pluginPath
+)
+
+addMyMaxUsdPlugin()
+
+)
\ No newline at end of file
diff --git a/samples/UserDataExportChaserSample/Contents/plugInfo.json b/samples/UserDataExportChaserSample/Contents/plugInfo.json
new file mode 100644
index 0000000..3d1ee85
--- /dev/null
+++ b/samples/UserDataExportChaserSample/Contents/plugInfo.json
@@ -0,0 +1,21 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins":[
+ {
+ "Info":{
+ # Max specific token
+ "MaxUsd":{
+ # The ExportChaser key is all that is need for the plugin to be found and loaded on export.
+ "ExportChaser" : {},
+ # The JobContextPlugin key is all that is needed for the context plugin to be found and loaded on export.
+ "JobContextPlugin": {}
+ }
+ },
+ "Name": "UserDataExportChaser",
+ "Type": "library",
+ "LibraryPath": "UserDataExportChaser.dll"
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/samples/UserDataExportChaserSample/Contents/python/UserDataExportChaser.py b/samples/UserDataExportChaserSample/Contents/python/UserDataExportChaser.py
new file mode 100644
index 0000000..db59e43
--- /dev/null
+++ b/samples/UserDataExportChaserSample/Contents/python/UserDataExportChaser.py
@@ -0,0 +1,187 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import maxUsd
+
+from pxr import Usd
+from pymxs import runtime as mxs
+
+import os, sys
+from enum import Enum
+import traceback
+
+# Export Chaser sample that adds Custom Data to the exported prims if the specified user
+# property or custom attribute are found in the corresponding Node. The sampled chaser
+# acts with default data or using the arguments that may have been passed to the chaser.
+
+# the supported data types by the UserDataExportChaserSample
+class PropertyType(Enum):
+ USER_PROP = 1
+ CUSTOM_DATA = 2
+
+
+class userDataExportChaserSample(maxUsd.ExportChaser):
+
+ default_property = { PropertyType.USER_PROP : ["myUserProperty"], PropertyType.CUSTOM_DATA : [] }
+
+ # constructor - can be customized for the Export Chaser requirements
+ # in the provided sample, it receives the stage and Prims to Node map, which are
+ # the minimal arguments for a functional chaser, and the chaser arguments, which are
+ # not mandatory but can be use to parametrize the chaser.
+ def __init__(self, factoryContext, *args, **kwargs):
+ super(userDataExportChaserSample, self).__init__(factoryContext, *args, **kwargs)
+
+ self.properties = userDataExportChaserSample.default_property
+
+ # retrieve the specified arguments to be used with the export chaser
+ # the chaser's arguments can be specified for different chasers
+ # the returned arguments are in the form of a dictionary
+ if 'UserData' in factoryContext.GetJobArgs().GetAllChaserArgs():
+ chaser_args = factoryContext.GetJobArgs().GetAllChaserArgs()['UserData']
+ for type, names in chaser_args.items():
+ if type =="user":
+ self.properties[PropertyType.USER_PROP] = []
+ for name in names.split(','):
+ self.properties[PropertyType.USER_PROP].append(name)
+ elif type == "custom":
+ self.properties[PropertyType.CUSTOM_DATA] = []
+ for name in names.split(','):
+ self.properties[PropertyType.CUSTOM_DATA].append(name)
+
+ # retrieve the export mapping dictionary for the stage
+ # the dictionary maps the resulting USD prim paths to their MAXScript NodeHandles origin
+ self.primsToNodeHandles = factoryContext.GetPrimsToNodeHandles()
+
+ # retrieve the USD stage being written to
+ self.stage = factoryContext.GetStage()
+
+ # retrieve the jobContextOptions the user may have modified through the UI
+ self.contextOptions = factoryContext.GetJobArgs().GetJobContextOptions("CustomDemoContext")
+
+ # processing that needs to run after the main 3ds Max USD export loop.
+ def PostExport(self):
+
+ # the jobContextOptions the user may have modified through the UI
+ if self.contextOptions.get('do_something_special', False):
+ # do something Special
+ pass
+
+ try:
+ for prim_path, node_handle in self.primsToNodeHandles.items():
+ node = mxs.maxOps.getNodeByHandle(node_handle)
+ prim = self.stage.GetPrimAtPath(prim_path)
+
+ for type, names in self.properties.items():
+ for name in names:
+ if type == PropertyType.USER_PROP:
+ prop = mxs.getUserProp(node, name)
+ # verify the property exists
+ if prop == None:
+ continue
+ if mxs.classOf(prop) == mxs.String:
+ # strip the quoting marks from the string
+ prop = prop[1:len(prop)-1]
+ prim.SetCustomDataByKey(name, prop)
+
+ elif type == PropertyType.CUSTOM_DATA:
+ if mxs.isProperty(node, name):
+ prop = mxs.getProperty(node, name)
+ prim.SetCustomDataByKey(name, prop)
+
+ except Exception as e:
+ # Quite useful to debug errors in a Python callback
+ print('Write() - Error: %s' % str(e))
+ print(traceback.format_exc())
+ return False
+
+ return True
+
+
+# Job Contexts allow to customize several options during a maxUsd export
+# - 'chaserNames'
+# - 'chaserArgs'
+# - 'convertMaterialsTo'
+#
+# This sample will add a "Custom Context Demo" option in the "PlugIn configuration" drop down in maxUsd exports
+def CustomExportJobContextSample():
+ # build a dictionary of the options to set using the context
+ extraArgs = {}
+
+ # The UserData chaser needs to be enabled in this job context
+ extraArgs['chaserNames'] = ['UserData']
+ extraArgs['chaserArgs'] = [['UserData', 'user', 'myUserFloatProperty,myUserProperty'], ['UserData', 'custom', 'inGame']]
+ return extraArgs
+
+
+
+maxUsd.ExportChaser.Register(userDataExportChaserSample, "UserData", "User Data Python DEMO", "Chaser to export user data along the exported USD prims")
+
+maxUsd.JobContextRegistry.RegisterExportJobContext("CustomDemoContext", "Custom Context Python DEMO", "Custom plug-in configuration", CustomExportJobContextSample)
+
+# Uncomment code below to export a scene using the export chaser sample
+# The code prepares the exporter options to
+# - export to a USD file in ASCII
+# - sets the ChaserNames to only use 'UserData'; the name of export chaser sample
+# - sets the arguments for the 'UserData' export chaser
+# - the list of user property ('user') to export as USD Custom Data; comma separated property names
+# - the list of custom attribute ('custom') to export as USD Custom Data; comma separated property names
+
+#opt = mxs.USDExporter.CreateOptions()
+#opt.FileFormat = mxs.Name("ascii")
+#opt.RootPrimPath = "/"
+#opt.ChaserNames = mxs.Array("UserData")
+#opt.AllChaserArgs = mxs.Array("UserData", "user", "myUserFloatProperty,myUserProperty", "UserData", "custom", "inGame")
+#mxs.USDExporter.ExportFile( "c:\\temp\\myscene_with_custom_data.usd",
+# exportOptions=opt)
+
+def showJobExportOptions(jobName, parentUI, contextOptions):
+ # 3dsMax 2025 and later is using PySide6, while 3dsMax 2024 and earlier is
+ # using PySide2 - so we need to check for both
+ try:
+ import PySide6.QtWidgets as QtWidgets
+ except ImportError:
+ try:
+ import PySide2.QtWidgets as QtWidgets
+ except ImportError as err:
+ print(f"Could not import QtWidgets from neither PySide6 nor PySide2 {err=}.")
+ raise err
+
+ try:
+ dlg = QtWidgets.QDialog(parentUI)
+ dlg.setWindowTitle(f"Export Options for {jobName}")
+ dlg.setModal(True)
+ dlg.setLayout(QtWidgets.QVBoxLayout())
+
+ checkBoxSpecial = QtWidgets.QCheckBox("Do something Special")
+ checkBoxSpecial.setChecked(contextOptions.get('do_something_special', False))
+ dlg.layout().addWidget(checkBoxSpecial)
+
+ # ...
+
+ buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
+ buttonBox.accepted.connect(dlg.accept)
+ buttonBox.rejected.connect(dlg.reject)
+ dlg.layout().addWidget(buttonBox, 0)
+
+ if dlg.exec() == QtWidgets.QDialog.Accepted:
+ contextOptions['do_something_special'] = checkBoxSpecial.isChecked()
+ #...
+
+ except Exception as err:
+ print(f"Unexpected {err=}, {type(err)=}")
+
+ return contextOptions
+
+maxUsd.JobContextRegistry.SetExportOptionsUI("CustomDemoContext", showJobExportOptions)
diff --git a/samples/UserDataExportChaserSample/Contents/python/plugInfo.json b/samples/UserDataExportChaserSample/Contents/python/plugInfo.json
new file mode 100644
index 0000000..fe0de14
--- /dev/null
+++ b/samples/UserDataExportChaserSample/Contents/python/plugInfo.json
@@ -0,0 +1,23 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins": [
+ {
+ "Info": {
+ # Max specific token
+ "MaxUsd": {
+ # The ExportChaser key is all that is needed for the plugin to be found and loaded on export.
+ "ExportChaser": {},
+ # The JobContextPlugin key is all that is needed for the context plugin to be found and loaded on export.
+ "JobContextPlugin": {}
+ }
+ },
+ # This is the name of the python script that implements the export chaser.
+ # This file needs to be found on runtime by python, it's path must be added
+ # to the python paths. See RegisterPlugin.ms for an exemple on how to do that.
+ "Name": "UserDataExportChaser",
+ "Type": "python"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/UserDataExportChaserSample/DLLEntry.h b/samples/UserDataExportChaserSample/DLLEntry.h
new file mode 100644
index 0000000..c4dfce4
--- /dev/null
+++ b/samples/UserDataExportChaserSample/DLLEntry.h
@@ -0,0 +1,18 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+#include
+extern HINSTANCE hInstance;
diff --git a/samples/UserDataExportChaserSample/DllEntry.cpp b/samples/UserDataExportChaserSample/DllEntry.cpp
new file mode 100644
index 0000000..046dabe
--- /dev/null
+++ b/samples/UserDataExportChaserSample/DllEntry.cpp
@@ -0,0 +1,33 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "DLLEntry.h"
+
+HINSTANCE hInstance;
+
+// This function is called by Windows when the DLL is loaded. This
+// function may also be called many times during time critical operations
+// like rendering. Therefore developers need to be careful what they
+// do inside this function. In the code below, note how after the DLL is
+// loaded the first time only a few statements are executed.
+BOOL WINAPI DllMain(HINSTANCE hinstDLL, ULONG fdwReason, LPVOID /*lpvReserved*/)
+{
+ if (fdwReason == DLL_PROCESS_ATTACH) {
+ // Hang on to this DLL's instance handle.
+ hInstance = hinstDLL;
+ DisableThreadLibraryCalls(hInstance);
+ }
+ return (TRUE);
+}
\ No newline at end of file
diff --git a/samples/UserDataExportChaserSample/ExportOptionsDialog.cpp b/samples/UserDataExportChaserSample/ExportOptionsDialog.cpp
new file mode 100644
index 0000000..309c491
--- /dev/null
+++ b/samples/UserDataExportChaserSample/ExportOptionsDialog.cpp
@@ -0,0 +1,94 @@
+//
+// Copyright 2024 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "ExportOptionsDialog.h"
+
+#include "UI_ExportOptionsDialog.h"
+
+#include
+using namespace std::string_literals;
+
+#include
+
+ExportOptionsDialog::ExportOptionsDialog(QWidget* parent /* = nullptr */)
+ : QDialog(parent)
+ , ui(new Ui::ExportOptionsDialog())
+{
+ ui->setupUi(this);
+}
+
+ExportOptionsDialog::~ExportOptionsDialog() = default;
+
+pxr::VtDictionary ExportOptionsDialog::ShowOptionsDialog(
+ const std::string& jobContext,
+ QWidget* parent,
+ const pxr::VtDictionary& options)
+{
+ auto dlg = new ExportOptionsDialog(parent);
+ if (!dlg) {
+ return options;
+ }
+
+ if (auto v = options.GetValueAtPath({ "Option A"s })) {
+ if (v->CanCast()) {
+ dlg->ui->radioButton_Option_A->setChecked(v->Get());
+ }
+ }
+
+ if (auto v = options.GetValueAtPath({ "Option B"s })) {
+ if (v->CanCast()) {
+ dlg->ui->radioButton_Option_B->setChecked(v->Get());
+ }
+ }
+
+ if (auto v = options.GetValueAtPath({ "Greeting"s, "Greet User"s })) {
+ if (v->CanCast()) {
+ dlg->ui->groupBox_Greeting->setChecked(v->Get());
+ }
+ }
+
+ if (auto v = options.GetValueAtPath({ "Greeting"s, "User Name"s })) {
+ if (v->CanCast()) {
+ dlg->ui->lineEdit_UserName->setText(QString::fromStdString(v->Get()));
+ }
+ }
+
+ if (auto v = options.GetValueAtPath({ "Greeting"s, "Formal"s })) {
+ if (v->CanCast()) {
+ dlg->ui->checkBox_FormalGreeting->setChecked(v->Get());
+ }
+ }
+
+ if (dlg->exec() != QDialog::Accepted) {
+ return options;
+ }
+
+ pxr::VtDictionary modified_options = options;
+ modified_options.SetValueAtPath(
+ { "Option A"s }, pxr::VtValue(dlg->ui->radioButton_Option_A->isChecked()));
+ modified_options.SetValueAtPath(
+ { "Option B"s }, pxr::VtValue(dlg->ui->radioButton_Option_B->isChecked()));
+
+ modified_options.SetValueAtPath(
+ { "Greeting"s, "Greet User"s }, pxr::VtValue(dlg->ui->groupBox_Greeting->isChecked()));
+ modified_options.SetValueAtPath(
+ { "Greeting"s, "User Name"s },
+ pxr::VtValue(dlg->ui->lineEdit_UserName->text().toStdString()));
+ modified_options.SetValueAtPath(
+ { "Greeting"s, "Formal"s }, pxr::VtValue(dlg->ui->checkBox_FormalGreeting->isChecked()));
+
+ return modified_options;
+}
diff --git a/samples/UserDataExportChaserSample/ExportOptionsDialog.h b/samples/UserDataExportChaserSample/ExportOptionsDialog.h
new file mode 100644
index 0000000..3f3ff23
--- /dev/null
+++ b/samples/UserDataExportChaserSample/ExportOptionsDialog.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+namespace Ui {
+class ExportOptionsDialog;
+}
+
+class ExportOptionsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ExportOptionsDialog(QWidget* parent = nullptr);
+ ~ExportOptionsDialog();
+
+ static pxr::VtDictionary ShowOptionsDialog(
+ const std::string& jobContext,
+ QWidget* parent,
+ const pxr::VtDictionary& options);
+
+private:
+ const std::unique_ptr ui;
+};
\ No newline at end of file
diff --git a/samples/UserDataExportChaserSample/ExportOptionsDialog.ui b/samples/UserDataExportChaserSample/ExportOptionsDialog.ui
new file mode 100644
index 0000000..d16111d
--- /dev/null
+++ b/samples/UserDataExportChaserSample/ExportOptionsDialog.ui
@@ -0,0 +1,132 @@
+
+
+ ExportOptionsDialog
+
+
+
+ 0
+ 0
+ 338
+ 225
+
+
+
+ Custom Context C++ DEMO Export Options
+
+
+ true
+
+
+
+
+
+ Additional Options
+
+
+
+
+
+ Option A
+
+
+ true
+
+
+
+
+
+
+ Option B
+
+
+
+
+
+
+
+
+
+ Greet the User on Export
+
+
+ true
+
+
+
+
+
+
+
+
+ Name:
+
+
+
+
+
+
+ Use Formal Greeting
+
+
+
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ ExportOptionsDialog
+ accept()
+
+
+ 390
+ 268
+
+
+ 39
+ 283
+
+
+
+
+ buttonBox
+ rejected()
+ ExportOptionsDialog
+ reject()
+
+
+ 480
+ 264
+
+
+ 482
+ 286
+
+
+
+
+
diff --git a/samples/UserDataExportChaserSample/PackageContents.xml b/samples/UserDataExportChaserSample/PackageContents.xml
new file mode 100644
index 0000000..0014284
--- /dev/null
+++ b/samples/UserDataExportChaserSample/PackageContents.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/UserDataExportChaserSample/SceneFiles/UserDataChaserSample.max b/samples/UserDataExportChaserSample/SceneFiles/UserDataChaserSample.max
new file mode 100644
index 0000000..a50a374
--- /dev/null
+++ b/samples/UserDataExportChaserSample/SceneFiles/UserDataChaserSample.max
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bd4ad6a0716f1df246b1efe8140c5933039cbc3710b11a598fec53d3cea9483
+size 692224
diff --git a/samples/UserDataExportChaserSample/UserDataExportChaser.cpp b/samples/UserDataExportChaserSample/UserDataExportChaser.cpp
new file mode 100644
index 0000000..3118a53
--- /dev/null
+++ b/samples/UserDataExportChaserSample/UserDataExportChaser.cpp
@@ -0,0 +1,320 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "ExportOptionsDialog.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+using namespace std::string_literals;
+
+// Data types supported by the UserDataExportChaserSample
+enum class PropertyType : int
+{
+ USER_PROP = 1, // "user"
+ CUSTOM_DATA = 2 // "custom"
+};
+
+// The UserDataExportChaserSample class
+// This Export Chaser sample adds Custom Data to the exported prims if the specified user
+// property or custom attribute are found in the corresponding Node. The sample chaser
+// acts with default data or using the arguments that may have been passed to the chaser.
+class UserDataExportChaserSample : public pxr::MaxUsdExportChaser
+{
+public:
+ // Constructor - can be customized for the Export Chaser requirements.
+ // In the provided sample it receives the stage and Prims to Node map, which are
+ // the minimal arguments for a functional chaser, and the chaser arguments, which are
+ // not mandatory but can be used to parameterize the chaser.
+ UserDataExportChaserSample(
+ pxr::UsdStagePtr stage,
+ const pxr::MaxUsdExportChaserRegistry::FactoryContext::PrimToNodeMap& primToNodeMap,
+ const MaxUsd::USDSceneBuilderOptions::ChaserArgs& args,
+ const pxr::VtDictionary& jobContextOptions);
+
+ // Processing that needs to run after the main 3ds Max USD export loop.
+ bool PostExport() override;
+
+private:
+ // The exported stage
+ pxr::UsdStagePtr stage;
+ // Maps full USD prim paths to INodes
+ const pxr::MaxUsdExportChaserRegistry::FactoryContext::PrimToNodeMap& primToNodeMap;
+ // potentially user-customized jobContext options.
+ const pxr::VtDictionary& jobContextOptions;
+
+ // sample specific parameters
+ // maps the data to be exported as Custom Data for the prims
+ typedef std::map> PropertyMap;
+ PropertyMap dataToExport;
+};
+
+// Add the using statement here to make the export chaser factory macro work
+PXR_NAMESPACE_USING_DIRECTIVE
+// Macro to register the Export Chaser. Defines a factory method for the chaser name. The 'ctx' will
+// be of type MaxUsdExportChaserRegistry::FactoryContext. The method should return a
+// MaxUsdExportChaser*. There are no guarantees about the lifetime of 'ctx'. It is also very
+// important to set the project option "Remove unreferenced code and data" to NO, not doing so could
+// cause the Macro to be optimized out and the Export Chaser to never be properly registered.
+PXR_MAXUSD_DEFINE_EXPORT_CHASER_FACTORY(
+ UserData,
+ "User Data C++ DEMO",
+ "Chaser to export user data along the exported USD prims",
+ ctx)
+{
+ // Fetching the export chaser parameters
+ // The chaser arguments are mapped using their registration chaser name (first arg of the macro)
+ MaxUsd::USDSceneBuilderOptions::ChaserArgs myArgs;
+ TfMapLookup(ctx.GetJobArgs().GetAllChaserArgs(), "UserData", &myArgs);
+ const pxr::VtDictionary& jobContextOptions
+ = ctx.GetJobArgs().GetJobContextOptions(TfToken("CustomDemoContext"));
+ return new UserDataExportChaserSample(
+ ctx.GetStage(), ctx.GetPrimToNodeMap(), myArgs, jobContextOptions);
+}
+
+UserDataExportChaserSample::UserDataExportChaserSample(
+ pxr::UsdStagePtr stage,
+ const pxr::MaxUsdExportChaserRegistry::FactoryContext::PrimToNodeMap& primToNodeMap,
+ const MaxUsd::USDSceneBuilderOptions::ChaserArgs& args,
+ const pxr::VtDictionary& jobContextOptions)
+ : stage(stage)
+ , primToNodeMap(primToNodeMap)
+ , jobContextOptions(jobContextOptions)
+{
+ // Default configuration
+ dataToExport
+ = { { PropertyType::USER_PROP, { "myUserProperty" } }, { PropertyType::CUSTOM_DATA, {} } };
+
+ // Parsing the specific export chaser arguments
+ boost::char_separator sep(",");
+ for (std::pair item : args) {
+ if (item.first == "user") {
+ dataToExport[PropertyType::USER_PROP].clear();
+ boost::tokenizer> tokens(item.second, sep);
+ for (auto name : tokens) {
+ dataToExport[PropertyType::USER_PROP].insert(name);
+ }
+ } else if (item.first == "custom") {
+ dataToExport[PropertyType::CUSTOM_DATA].clear();
+ boost::tokenizer> tokens(item.second, sep);
+ for (auto name : tokens) {
+ dataToExport[PropertyType::CUSTOM_DATA].insert(name);
+ }
+ } else {
+ TF_WARN(
+ "Wrong user data type ('%s') passed as argument to UserPropertyExportChaser",
+ item.first.c_str());
+ }
+ }
+
+ if (!jobContextOptions.empty()) {
+ if (auto greetUser = jobContextOptions.GetValueAtPath({ "Greeting"s, "Greet User"s })) {
+ if (greetUser->Get()) {
+ std::string username;
+ if (auto userNameV
+ = jobContextOptions.GetValueAtPath({ "Greeting"s, "User Name"s })) {
+ if (userNameV->CanCast()) {
+ username = userNameV->Get();
+ }
+ }
+ bool formal = false;
+ if (auto formalV = jobContextOptions.GetValueAtPath({ "Greeting"s, "Formal"s })) {
+ if (formalV->CanCast()) {
+ formal = formalV->Get();
+ }
+ }
+ std::string option;
+ if (auto optionAV = jobContextOptions.GetValueAtPath({ "Option A"s })) {
+ if (optionAV->CanCast() && optionAV->Get()) {
+ option = "Option A";
+ }
+ }
+ bool optionB = false;
+ if (auto optionBV = jobContextOptions.GetValueAtPath({ "Option B"s })) {
+ if (optionBV->CanCast() && optionBV->Get()) {
+ option = "Option B";
+ }
+ }
+
+ std::string greeting;
+ if (formal) {
+ time_t now = time(0);
+ tm* localtm = localtime(&now);
+ greeting = (localtm->tm_hour > 12) ? "Good Afternoon" : "Good Morning";
+ } else {
+ greeting = "Hi";
+ }
+ static constexpr const char* message = "{0} '{1}' - You have chosen {2}";
+ MaxUsd::Log::Info(message, greeting.c_str(), username.c_str(), option.c_str());
+ }
+ }
+ }
+}
+
+bool UserDataExportChaserSample::PostExport()
+{
+ // Cycle through the exported prim/node
+ for (std::pair exported : primToNodeMap) {
+ pxr::UsdPrim prim = stage->GetPrimAtPath(exported.first);
+ INode* node = exported.second;
+
+ // Are any of the specified user properties found in the Node
+ for (std::string propName : dataToExport[PropertyType::USER_PROP]) {
+ auto mxsPropName = MaxUsd::UsdStringToMaxString(propName);
+ if (node->UserPropExists(mxsPropName)) {
+ TSTR value;
+ node->GetUserPropString(mxsPropName, value);
+
+ if (_tcsicmp(value, _T("true")) == 0) {
+ prim.SetCustomDataByKey(pxr::TfToken(propName), pxr::VtValue(true));
+ } else if (_tcsicmp(value, _T("false")) == 0) {
+ prim.SetCustomDataByKey(pxr::TfToken(propName), pxr::VtValue(false));
+ } else {
+ // Note - to simplify the chaser sample implementation, the user property
+ // conversion to numerical values was not completed. It will be written as a
+ // simple string. The same chaser written in Python properly handles this
+ // situation however.
+ auto valueString = MaxUsd::MaxStringToUsdString(value);
+ // Strip the quoting marks from the string (if the property was originally a
+ // string)
+ if (*valueString.begin() == '\"' && *(valueString.end() - 1) == '\"') {
+ valueString.erase(valueString.begin());
+ valueString.erase(valueString.end() - 1);
+ }
+ prim.SetCustomDataByKey(pxr::TfToken(propName), pxr::VtValue(valueString));
+ }
+ }
+ }
+
+ // For the purpose of the sample, the chaser finds the custom attributes only on
+ // the base level, not on the modifiers or materials of the node
+ BaseObject* obj = node->GetObjectRef();
+ ICustAttribContainer* cc = obj->GetCustAttribContainer();
+ if (!cc) {
+ continue;
+ }
+ // Are any of the specified custom attributes found in the Node
+ for (std::string propName : dataToExport[PropertyType::CUSTOM_DATA]) {
+ auto mxsPropName = MaxUsd::UsdStringToMaxString(propName);
+ for (int i = 0; i < cc->GetNumCustAttribs(); i++) {
+ CustAttrib* ca = cc->GetCustAttrib(i);
+ if (!ca) {
+ continue;
+ }
+ MSCustAttrib* msCA = (MSCustAttrib*)ca->GetInterface(I_SCRIPTEDCUSTATTRIB);
+ if (!msCA) {
+ continue;
+ }
+
+ for (int j = 0; j < ca->NumParamBlocks(); ++j) {
+ IParamBlock2* pb2 = ca->GetParamBlock(j);
+ if (!pb2) {
+ continue;
+ }
+ ParamID paramId = MaxUsd::FindParamId(pb2, mxsPropName);
+ // If this specific attribute doesn't exist, we skip to look for it in the next
+ // PB
+ if (paramId == -1) {
+ continue;
+ }
+
+ // Found the attribute, extract the value and write the data
+ {
+ const ParamDef& paramDef = pb2->GetParamDef(paramId);
+ Interval iv = FOREVER;
+ bool hasFoundParam = false;
+ switch (paramDef.type) {
+ case TYPE_BOOL: {
+ int boolVal = 0;
+ hasFoundParam = pb2->GetValue(paramId, 0, boolVal, iv);
+ prim.SetCustomDataByKey(
+ pxr::TfToken(propName),
+ pxr::VtValue((boolVal == 0) ? false : true));
+ break;
+ }
+ case TYPE_INT: {
+ int strVal = false;
+ hasFoundParam = pb2->GetValue(paramId, 0, strVal, iv);
+ prim.SetCustomDataByKey(pxr::TfToken(propName), pxr::VtValue(strVal));
+ break;
+ }
+ case TYPE_FLOAT: {
+ float strVal = false;
+ hasFoundParam = pb2->GetValue(paramId, 0, strVal, iv);
+ prim.SetCustomDataByKey(pxr::TfToken(propName), pxr::VtValue(strVal));
+ break;
+ }
+ case TYPE_STRING: {
+ const wchar_t* strVal = L"";
+ hasFoundParam = pb2->GetValue(paramId, 0, strVal, iv);
+ prim.SetCustomDataByKey(
+ pxr::TfToken(propName),
+ pxr::VtValue(MaxUsd::MaxStringToUsdString(strVal)));
+ break;
+ }
+ default:
+ TF_WARN(
+ "Unsupported Custom Attribute type for '%s' in "
+ "UserPropertyExportChaser",
+ propName.c_str());
+ break;
+ }
+ // No need to search further, the attribute was found
+ break;
+ }
+ }
+ }
+ }
+ }
+ return true;
+}
+
+REGISTER_EXPORT_JOB_CONTEXT_FCT(
+ CustomDemoContext,
+ "Custom Context C++ DEMO",
+ "Custom plug-in configuration")
+{
+ VtDictionary extraArgs;
+ extraArgs[MaxUsdSceneBuilderOptionsTokens->chaserNames]
+ = VtValue(std::vector { VtValue(std::string("UserData")) });
+ VtValue chaserArgUserProp(
+ std::vector { VtValue(std::string("UserData")),
+ VtValue(std::string("user")),
+ VtValue(std::string("myUserFloatProperty,myUserProperty")) });
+ VtValue chaserArgCustomProp(std::vector { VtValue(std::string("UserData")),
+ VtValue(std::string("custom")),
+ VtValue(std::string("inGame")) });
+ extraArgs[MaxUsdSceneBuilderOptionsTokens->chaserArgs]
+ = VtValue(std::vector { chaserArgUserProp, chaserArgCustomProp });
+
+ return extraArgs;
+}
+
+TF_REGISTRY_FUNCTION(MaxUsdJobContextRegistry)
+{
+ MaxUsdJobContextRegistry::GetInstance().SetExportOptionsUI(
+ "CustomDemoContext", &ExportOptionsDialog::ShowOptionsDialog);
+}
diff --git a/samples/UserDataExportChaserSample/UserDataExportChaser.vcxproj b/samples/UserDataExportChaserSample/UserDataExportChaser.vcxproj
new file mode 100644
index 0000000..7d33617
--- /dev/null
+++ b/samples/UserDataExportChaserSample/UserDataExportChaser.vcxproj
@@ -0,0 +1,108 @@
+
+
+
+
+ Hybrid
+ x64
+
+
+ Release
+ x64
+
+
+
+ UserDataExportChaserPlugin
+ 16.0
+ QtVS_v302
+ {1EF31957-1C1A-4869-B52D-D733FE24F0A9}
+ DynamicLibrary
+ Unicode
+
+
+
+
+
+
+
+ <_ProjectFileVersion>10.0.30319.1
+ UserDataExportChaser
+ .dll
+ $(ContentsDir)\Contents\
+
+
+ $(QtArtifacts)\QtMsBuild
+
+
+
+
+
+
+
+
+ widgets
+ $(QtArtifacts)\Qt\$(QTVER)
+
+
+
+
+
+
+ $(QtInc);%(AdditionalIncludeDirectories)
+ false
+ ../silence_usd_warnings.h
+
+
+ $(QtLib);%(AdditionalLibraryDirectories)
+ maxscrpt.lib;maxutil.lib;paramblk2.lib;%(AdditionalDependencies)
+ $(LibDir)\$(TargetName).lib
+
+
+ @echo Do not forget to update "$(OutDir)RegisterPlugin.ms" in order to load the C++ version of the plugin.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ ..\%(Filename)%(Extension)
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/UserDataExportChaserSample/UserDataExportChaser.vcxproj.filters b/samples/UserDataExportChaserSample/UserDataExportChaser.vcxproj.filters
new file mode 100644
index 0000000..e50176a
--- /dev/null
+++ b/samples/UserDataExportChaserSample/UserDataExportChaser.vcxproj.filters
@@ -0,0 +1,61 @@
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ {736bd8fe-eeb6-4c2b-a0ab-dc1037725910}
+
+
+ {c3509517-bd52-40a7-ab6f-a09227ec306b}
+
+
+ {a6749ed4-ce14-4a10-9eeb-afdaff80a895}
+
+
+ {4524682a-81be-441f-85c9-08dc74790ba1}
+
+
+ {02e68e56-c893-4fba-9bf2-1729355bc0b9}
+
+
+
+
+ Header Files
+
+
+
+
+ Source Files\Contents
+
+
+ Source Files\Contents
+
+
+ Source Files\python
+
+
+ Source Files\python
+
+
+
+
+
+ Form Files
+
+
+
+
+ Header Files
+
+
+
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/CAMetaDataUtils.cpp b/samples/UserDataImportChaserSample/CAMetaDataUtils.cpp
new file mode 100644
index 0000000..97da605
--- /dev/null
+++ b/samples/UserDataImportChaserSample/CAMetaDataUtils.cpp
@@ -0,0 +1,59 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "CAMetaDataUtils.h"
+
+#include
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+enum CAMetaDataCodes
+{
+ UnableToCreateMetadataObject
+};
+TF_REGISTRY_FUNCTION(TfEnum)
+{
+ TF_ADD_ENUM_NAME(
+ UnableToCreateMetadataObject, "Unable to define built-in USD Metadata object.");
+};
+
+namespace caMetaData {
+
+IMetaDataManager::MetaDataID getOrDefineCABuiltInMetaData(
+ const std::vector>& cas)
+{
+ if (cas.empty()) {
+ return EmptyMetaDataID;
+ }
+
+ IMetaDataManager* maxMetaDataManager = IMetaDataManager::GetInstance();
+ Tab tabParams;
+
+ for (const auto& ca : cas) {
+ auto def = getCAMetaDataDef(ca.first, ca.second.key);
+ tabParams.Append(1, &def.usdMetaDataParamDef);
+ }
+
+ TSTR errMsg = nullptr;
+ IMetaDataManager::MetaDataID usdBuiltInMetaData
+ = maxMetaDataManager->CreateMetaDataDefinition(_T("USD"), _T("USD"), tabParams, &errMsg);
+ if (usdBuiltInMetaData == EmptyMetaDataID) {
+ TF_ERROR(UnableToCreateMetadataObject, MaxUsd::MaxStringToUsdString(errMsg).c_str());
+ }
+
+ return usdBuiltInMetaData;
+}
+
+} // namespace caMetaData
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/CAMetaDataUtils.h b/samples/UserDataImportChaserSample/CAMetaDataUtils.h
new file mode 100644
index 0000000..ab6c5a8
--- /dev/null
+++ b/samples/UserDataImportChaserSample/CAMetaDataUtils.h
@@ -0,0 +1,97 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include
+
+namespace caMetaData {
+
+enum UsdMetaDataTypeCA
+{
+ CA_BOOL,
+ CA_INT,
+ CA_STR
+};
+
+struct ParameterValue
+{
+ std::wstring key = L"";
+ int intValue = 0;
+ bool boolValue = false;
+ std::wstring strValue = L"";
+};
+
+struct UsdMetaDataDefCA
+{
+ UsdMetaDataTypeCA usdMetaData;
+ const wchar_t* usdMetaDataKey;
+ IMetaDataManager::ParamDescriptor usdMetaDataParamDef;
+};
+
+static UsdMetaDataDefCA createCABoolMetaDataDef(const std::wstring& key)
+{
+ IMetaDataManager::ParamDescriptor metaDataParamDef;
+ metaDataParamDef.m_name = key.c_str();
+ metaDataParamDef.m_dataType = static_cast(TYPE_BOOL);
+ metaDataParamDef.m_ctrlType = static_cast(TYPE_SINGLECHECKBOX);
+ metaDataParamDef.m_ctrlAlign = IMetaDataManager::ControlAlign::eAlignLeft;
+
+ return UsdMetaDataDefCA{ UsdMetaDataTypeCA::CA_BOOL, key.c_str(), metaDataParamDef };
+}
+
+static UsdMetaDataDefCA createCAIntMetaDataDef(const std::wstring& key)
+{
+ IMetaDataManager::ParamDescriptor metaDataParamDef;
+ metaDataParamDef.m_name = key.c_str();
+ metaDataParamDef.m_dataType = static_cast(TYPE_INT);
+ metaDataParamDef.m_ctrlType = TYPE_SLIDER;
+ metaDataParamDef.m_ctrlAlign = IMetaDataManager::ControlAlign::eAlignLeft;
+
+ return UsdMetaDataDefCA{ UsdMetaDataTypeCA::CA_INT, key.c_str(), metaDataParamDef };
+}
+
+
+static UsdMetaDataDefCA createCAStrMetaDataDef(const std::wstring& key)
+{
+ IMetaDataManager::ParamDescriptor metaDataParamDef;
+ metaDataParamDef.m_name = key.c_str();
+ metaDataParamDef.m_dataType = TYPE_STRING;
+ metaDataParamDef.m_ctrlType = TYPE_EDITBOX;
+ metaDataParamDef.m_ctrlAlign = IMetaDataManager::ControlAlign::eAlignLeft;
+
+ return UsdMetaDataDefCA{ UsdMetaDataTypeCA::CA_STR, key.c_str(), metaDataParamDef };
+}
+
+
+IMetaDataManager::MetaDataID getOrDefineCABuiltInMetaData(
+ const std::vector> &cas);
+
+static const UsdMetaDataDefCA getCAMetaDataDef(UsdMetaDataTypeCA type, const std::wstring& key)
+{
+ switch (type)
+ {
+ case CA_BOOL:
+ return createCABoolMetaDataDef(key);
+ case CA_INT:
+ return createCAIntMetaDataDef(key);
+ case CA_STR:
+ return createCAStrMetaDataDef(key);
+ default:
+ return createCABoolMetaDataDef(L"not_found");
+ }
+}
+
+} // namespace caMetaData
diff --git a/samples/UserDataImportChaserSample/Contents/RegisterPlugin.ms b/samples/UserDataImportChaserSample/Contents/RegisterPlugin.ms
new file mode 100644
index 0000000..272f1b8
--- /dev/null
+++ b/samples/UserDataImportChaserSample/Contents/RegisterPlugin.ms
@@ -0,0 +1,61 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+/*
+ * Registers the User Data Import Chaser to USD Plug Registry
+ * This script is ran as part of the "post-start-up scripts parts"
+ * This is necessary to ensure that USD can register the plugin properly, and load the associated dll or python script when the time is right.
+ * Loading the USD plugin's DLL before the USD plugin object Load() is called can cause issues/crash.
+ * This script registers either the python or the c++ import chaser. Comment/uncomment the file accordingly to the one you want to load.
+ * Both could be registered at the same time without problem, but only one would ever be executed.
+ * For the purpose of this sample we demonstrate how to load a c++ and a python chaser.
+*/
+(
+
+pyUsdPlug = python.import("pxr.Plug")
+
+function addMyMaxUsdPlugin =
+(
+ pysys = python.import("sys")
+ pyos = python.import("os")
+
+ scriptPath = getThisScriptFilename()
+ pluginPath = pathConfig.removePathLeaf scriptPath
+
+ -- note: either use the Python (default) or the C++ version of the plugin
+ -- You must comment/uncomment the respective maxscript code in the sections below
+
+ -- [section python - start]
+ -- Load the python prim reader
+ pluginPath = pluginPath + "\\python\\plugInfo.json"
+ -- The plugin script location must part of the Python path
+ pysys.path.insert 0 (pathConfig.removePathLeaf pluginPath)
+ -- [section python - end]
+
+ -- [section c++ - start]
+ -- Load the c++ prim reader
+ /**
+ * pluginPath = pluginPath + "\\plugInfo.json"
+ */
+ -- [section c++ - end]
+
+ plugRegistry = pyUsdPlug.Registry()
+ plugRegistry.RegisterPlugins pluginPath
+)
+
+addMyMaxUsdPlugin()
+
+)
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/Contents/plugInfo.json b/samples/UserDataImportChaserSample/Contents/plugInfo.json
new file mode 100644
index 0000000..6e2734d
--- /dev/null
+++ b/samples/UserDataImportChaserSample/Contents/plugInfo.json
@@ -0,0 +1,21 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins": [
+ {
+ "Info": {
+ # Max specific token
+ "MaxUsd": {
+ # The ImportChaser key is all that is need for the plugin to be found and loaded on import.
+ "ImportChaser": {},
+ # The JobContextPlugin key is all that is needed for the context plugin to be found and loaded on import.
+ "JobContextPlugin": {}
+ }
+ },
+ "Name": "UserDataImportChaser",
+ "Type": "library",
+ "LibraryPath": "UserDataImportChaser.dll"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/Contents/python/UserDataImportChaser.py b/samples/UserDataImportChaserSample/Contents/python/UserDataImportChaser.py
new file mode 100644
index 0000000..974691f
--- /dev/null
+++ b/samples/UserDataImportChaserSample/Contents/python/UserDataImportChaser.py
@@ -0,0 +1,147 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import maxUsd
+
+from pxr import Usd
+from pymxs import runtime as mxs
+
+import os, sys
+from enum import Enum
+import traceback
+
+# The UserDataImportChaserSample class
+# This Import Chaser sample reads customData from the imported prims and adds
+# user defined properties or custom attributes to the corresponding node
+# depending on the arguments. The sample chaser acts with default data or using
+# the arguments that may have been passed to the chaser.
+
+# the supported data types by the UserDataImportChaserSample
+class PropertyType(Enum):
+ USER_PROP = 1
+ CUSTOM_DATA = 2
+
+class userDataImportChaserSample(maxUsd.ImportChaser):
+ default_property = { PropertyType.USER_PROP : ["myUserProperty"], PropertyType.CUSTOM_DATA : [] }
+
+ # constructor - can be customized for the Import Chaser requirements
+ # the minimal arguments for a functional chaser, and the chaser arguments, which are
+ # not mandatory but can be use to parametrize the chaser.
+ def __init__(self, factoryContext, *args, **kwargs):
+ super(userDataImportChaserSample, self).__init__(factoryContext, *args, **kwargs)
+ self.properties = userDataImportChaserSample.default_property
+
+ # retrieve the specified arguments to be used with the import chaser
+ # the chaser's arguments can be specified for different chasers
+ # the returned arguments are in the form of a dictionary
+ if 'UserData' in factoryContext.GetJobArgs().GetAllChaserArgs():
+ chaser_args = factoryContext.GetJobArgs().GetAllChaserArgs()['UserData']
+ for type, names in chaser_args.items():
+ if type =="user":
+ self.properties[PropertyType.USER_PROP] = []
+ for name in names.split(','):
+ self.properties[PropertyType.USER_PROP].append(name)
+ elif type == "custom":
+ self.properties[PropertyType.CUSTOM_DATA] = []
+ for name in names.split(','):
+ self.properties[PropertyType.CUSTOM_DATA].append(name)
+
+ # retrieve the import mapping dictionary for the stage
+ # the dictionary maps the resulting USD prim paths to their MAXScript NodeHandles origin
+ self.primsToNodeHandles = factoryContext.GetPrimsToNodeHandles()
+
+ # retrieve the USD stage being written to
+ self.stage = factoryContext.GetStage()
+
+ # processing that needs to run after the main 3ds Max USD import loop.
+ def PostImport(self):
+ try:
+ for prim_path, node_handle in self.primsToNodeHandles.items():
+ node = mxs.maxOps.getNodeByHandle(node_handle)
+ prim = self.stage.GetPrimAtPath(prim_path)
+ addCustAttrib = False
+ params = ""
+ paramsUi = ""
+
+ if not prim:
+ print(f"Prim not found at path: {prim_path}")
+ return False
+
+ for type, names in self.properties.items():
+ customData = prim.GetCustomData()
+ for name in names:
+ val = customData.get(name)
+
+ if val != None:
+ if type == PropertyType.USER_PROP:
+ mxs.setUserProp(node, name, val)
+
+ elif type == PropertyType.CUSTOM_DATA:
+ if(isinstance(val, str)):
+ params += f'{name} type:#string ui:{name} default:"{val}" animateable:True\n'
+ paramsUi += f'edittext {name} "{name}" type:#string\n'
+ elif(isinstance(val, float)):
+ params += f'{name} type:#float ui:{name} default:{val} animateable:True\n'
+ paramsUi += f'spinner {name} "{name}" type:#float\n'
+ elif(isinstance(val, bool)):
+ params += f'{name} type:#boolean ui:{name} default:{val} animateable:True\n'
+ paramsUi += f'checkbox {name} "{name}" type:#boolean\n'
+ elif(isinstance(val, int)):
+ params += f'{name} type:#integer ui:{name} default:{val} animateable:True\n'
+ paramsUi += f'slider {name} "{name}" type:#integer\n'
+ addCustAttrib = True
+
+ if(addCustAttrib):
+ baseCA2=f'''attributes "USD"
+ (
+ parameters main rollout:params
+ (
+ {params}
+ )
+
+ rollout params "USD"
+ (
+ {paramsUi}
+ )
+ )'''
+ attr = mxs.execute(baseCA2)
+ mxs.custAttributes.add(node.baseObject, attr)
+
+ except Exception as e:
+ # Quite useful to debug errors in a Python callback
+ print('Write() - Error: %s' % str(e))
+ print(traceback.format_exc())
+ return False
+ return True
+
+# Job Contexts allow to customize several options during a maxUsd import
+# - 'chaserNames'
+# - 'chaserArgs'
+#
+# This sample will add a "Import User Data Python DEMO" option in the "PlugIn configuration" drop down in maxUsd imports
+def CustomImportJobContextSample():
+ # build a dictionary of the options to set using the context
+ extraArgs = {}
+
+ # The UserData chaser needs to be enabled in this job context
+ extraArgs['chaserNames'] = ['UserDataImport']
+
+ # The following arguments are based on the variables contained in the
+ # custom data of the prims found in: ./SceneFiles/UserDataChaserSample.usda
+ extraArgs['chaserArgs'] = [['UserData', 'user', 'myUserFloatProperty,myUserProperty'], ['UserData', 'custom', 'inGame,strVal']]
+ return extraArgs
+
+maxUsd.ImportChaser.Register(userDataImportChaserSample, "UserDataImport", "Import User Data Python DEMO", "Chaser that adds custom data from prims to their corresponding nodes")
+maxUsd.JobContextRegistry.RegisterImportJobContext("CustomImportContext", "Import User Data context Python DEMO", "Custom plug-in configuration", CustomImportJobContextSample)
diff --git a/samples/UserDataImportChaserSample/Contents/python/plugInfo.json b/samples/UserDataImportChaserSample/Contents/python/plugInfo.json
new file mode 100644
index 0000000..0bf0118
--- /dev/null
+++ b/samples/UserDataImportChaserSample/Contents/python/plugInfo.json
@@ -0,0 +1,23 @@
+{
+ # Comments are allowed and indicated by a hash at the start of a
+ # line or after spaces and tabs. They continue to the end of line.
+ # Blank lines are okay, too.
+ "Plugins": [
+ {
+ "Info": {
+ # Max specific token
+ "MaxUsd": {
+ # The ImportChaser key is all that is needed for the plugin to be found and loaded on import.
+ "ImportChaser": {},
+ # The JobContextPlugin key is all that is needed for the context plugin to be found and loaded on import.
+ "JobContextPlugin": {}
+ }
+ },
+ # This is the name of the python script that implements the import chaser.
+ # This file needs to be found on runtime by python, it's path must be added
+ # to the python paths. See RegisterPlugin.ms for an exemple on how to do that.
+ "Name": "UserDataImportChaser",
+ "Type": "python"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/DLLEntry.h b/samples/UserDataImportChaserSample/DLLEntry.h
new file mode 100644
index 0000000..14bf038
--- /dev/null
+++ b/samples/UserDataImportChaserSample/DLLEntry.h
@@ -0,0 +1,19 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include
+extern HINSTANCE hInstance;
diff --git a/samples/UserDataImportChaserSample/DllEntry.cpp b/samples/UserDataImportChaserSample/DllEntry.cpp
new file mode 100644
index 0000000..046dabe
--- /dev/null
+++ b/samples/UserDataImportChaserSample/DllEntry.cpp
@@ -0,0 +1,33 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "DLLEntry.h"
+
+HINSTANCE hInstance;
+
+// This function is called by Windows when the DLL is loaded. This
+// function may also be called many times during time critical operations
+// like rendering. Therefore developers need to be careful what they
+// do inside this function. In the code below, note how after the DLL is
+// loaded the first time only a few statements are executed.
+BOOL WINAPI DllMain(HINSTANCE hinstDLL, ULONG fdwReason, LPVOID /*lpvReserved*/)
+{
+ if (fdwReason == DLL_PROCESS_ATTACH) {
+ // Hang on to this DLL's instance handle.
+ hInstance = hinstDLL;
+ DisableThreadLibraryCalls(hInstance);
+ }
+ return (TRUE);
+}
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/ImportChaser.cpp b/samples/UserDataImportChaserSample/ImportChaser.cpp
new file mode 100644
index 0000000..3f0bcdd
--- /dev/null
+++ b/samples/UserDataImportChaserSample/ImportChaser.cpp
@@ -0,0 +1,101 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+// Add the using statement here to make the import chaser factory macro work
+PXR_NAMESPACE_USING_DIRECTIVE
+
+// The ImportChaserSample class that logs a simple message after an import
+class ImportChaserSample : public pxr::MaxUsdImportChaser
+{
+public:
+ // constructor - can be customized for the Import Chaser requirements.
+ ImportChaserSample(
+ Usd_PrimFlagsPredicate& returnPredicate,
+ const MaxUsdReadJobContext& context,
+ const fs::path& filename);
+
+ // processing that needs to run after the main 3ds Max USD import loop.
+ bool PostImport() override;
+
+private:
+ // the imported stage
+ const MaxUsdReadJobContext context;
+ const fs::path& filename;
+};
+
+// Macro to register the Import Chaser. Defines a factory method for the chaser name. The 'ctx' will
+// be of type MaxUsdImportChaserRegistry::FactoryContext. The method should return a
+// MaxUsdImportChaser*. There are no guarantees about the lifetime of 'ctx'. It is also very
+// important to set the project option "Remove unreferenced code and data" to NO, not doing so could
+// cause the Macro to be optimized out and the Import Chaser to never be properly registered.
+PXR_MAXUSD_DEFINE_IMPORT_CHASER_FACTORY(
+ ImportLog,
+ "Import Chaser C++ DEMO",
+ "Import chaser that logs a message ",
+ ctx)
+{
+ return new ImportChaserSample(
+ *(new Usd_PrimFlagsPredicate()), ctx.GetContext(), ctx.GetFilename());
+}
+
+ImportChaserSample::ImportChaserSample(
+ Usd_PrimFlagsPredicate& returnPredicate,
+ const MaxUsdReadJobContext& context,
+ const fs::path& filename)
+ : context(context)
+ , filename(filename)
+{
+}
+
+bool ImportChaserSample::PostImport()
+{
+ maxUsd::Log::Info("Stage imported successfully from '{0}'.\n", filename.string());
+ return true;
+}
+
+REGISTER_IMPORT_JOB_CONTEXT_FCT(
+ CustomImportContext,
+ "Custom Import Context",
+ "Custom import plug-in configuration")
+{
+ VtDictionary extraArgs;
+ extraArgs[MaxUsdSceneBuilderOptionsTokens->chaser]
+ = VtValue(std::vector { VtValue(std::string("ImportLog")) });
+ VtValue chaserArgUserProp(
+ std::vector { VtValue(std::string("ImportLog")),
+ VtValue(std::string("user")),
+ VtValue(std::string("myUserFloatProperty,myUserProperty")) });
+ VtValue chaserArgCustomProp(std::vector { VtValue(std::string("ImportLog")),
+ VtValue(std::string("custom")),
+ VtValue(std::string("inGame")) });
+ extraArgs[MaxUsdSceneBuilderOptionsTokens->chaserArgs]
+ = VtValue(std::vector { chaserArgUserProp, chaserArgCustomProp });
+
+ return extraArgs;
+}
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/PackageContents.xml b/samples/UserDataImportChaserSample/PackageContents.xml
new file mode 100644
index 0000000..39f04bd
--- /dev/null
+++ b/samples/UserDataImportChaserSample/PackageContents.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/SceneFiles/UserDataChaserSample.usda b/samples/UserDataImportChaserSample/SceneFiles/UserDataChaserSample.usda
new file mode 100644
index 0000000..84def23
--- /dev/null
+++ b/samples/UserDataImportChaserSample/SceneFiles/UserDataChaserSample.usda
@@ -0,0 +1,159 @@
+#usda 1.0
+(
+ customLayerData = {
+ string creator = "USD for Autodesk 3ds Max: v0.5.7.0"
+ }
+ defaultPrim = "root"
+ metersPerUnit = 0.0254
+ upAxis = "Z"
+)
+
+def Xform "root"
+{
+ def Mesh "Box001" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ customData = {
+ bool inGame = 1
+ string strVal = "Test string test ..."
+ string myUserFloatProperty = "42.0"
+ string myUserProperty = "allo"
+ }
+ )
+ {
+ float3[] extent = [(-24.820606, -24.820608, 0), (24.820606, 24.820608, 23.558146)]
+ int[] faceVertexCounts = [4, 4, 4, 4, 4, 4]
+ int[] faceVertexIndices = [0, 2, 3, 1, 4, 5, 7, 6, 0, 1, 5, 4, 1, 3, 7, 5, 3, 2, 6, 7, 2, 0, 4, 6]
+ rel material:binding =
+ point3f[] points = [(-24.820606, -24.820608, 0), (24.820606, -24.820608, 0), (-24.820606, 24.820608, 0), (24.820606, 24.820608, 0), (-24.820606, -24.820608, 23.558146), (24.820606, -24.820608, 23.558146), (-24.820606, 24.820608, 23.558146), (24.820606, 24.820608, 23.558146)]
+ color3f[] primvars:displayColor = [(0.88235295, 0.56078434, 0.34117648)]
+ float3[] primvars:normals = [(0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0)] (
+ interpolation = "faceVarying"
+ )
+ texCoord2f[] primvars:st = [(1, 0), (1, 1), (0, 1), (0, 0), (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1)] (
+ interpolation = "faceVarying"
+ )
+ uniform token subdivisionScheme = "none"
+ uniform token subsetFamily:materialBind:familyType = "partition"
+ matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (4.16454, -2.83188, 0, 1) )
+ uniform token[] xformOpOrder = ["xformOp:transform"]
+
+ def GeomSubset "_1_" (
+ customData = {
+ dictionary "3dsmax" = {
+ int matId = 1
+ }
+ }
+ )
+ {
+ uniform token elementType = "face"
+ uniform token familyName = "materialBind"
+ int[] indices = [1]
+ }
+
+ def GeomSubset "_2_" (
+ customData = {
+ dictionary "3dsmax" = {
+ int matId = 2
+ }
+ }
+ )
+ {
+ uniform token elementType = "face"
+ uniform token familyName = "materialBind"
+ int[] indices = [0]
+ }
+
+ def GeomSubset "_3_" (
+ customData = {
+ dictionary "3dsmax" = {
+ int matId = 3
+ }
+ }
+ )
+ {
+ uniform token elementType = "face"
+ uniform token familyName = "materialBind"
+ int[] indices = [5]
+ }
+
+ def GeomSubset "_4_" (
+ customData = {
+ dictionary "3dsmax" = {
+ int matId = 4
+ }
+ }
+ )
+ {
+ uniform token elementType = "face"
+ uniform token familyName = "materialBind"
+ int[] indices = [3]
+ }
+
+ def GeomSubset "_5_" (
+ customData = {
+ dictionary "3dsmax" = {
+ int matId = 5
+ }
+ }
+ )
+ {
+ uniform token elementType = "face"
+ uniform token familyName = "materialBind"
+ int[] indices = [2]
+ }
+
+ def GeomSubset "_6_" (
+ customData = {
+ dictionary "3dsmax" = {
+ int matId = 6
+ }
+ }
+ )
+ {
+ uniform token elementType = "face"
+ uniform token familyName = "materialBind"
+ int[] indices = [4]
+ }
+ }
+
+ def Mesh "Sphere001" (
+ customData = {
+ dictionary "3dsmax" = {
+ int matId = 2
+ }
+ }
+ )
+ {
+ float3[] extent = [(-16.736341, -16.736341, -16.736341), (16.736341, 16.736341, 16.736341)]
+ int[] faceVertexCounts = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
+ int[] faceVertexIndices = [0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 6, 0, 6, 7, 0, 7, 8, 0, 8, 9, 0, 9, 10, 0, 10, 11, 0, 11, 12, 0, 12, 13, 0, 13, 14, 0, 14, 15, 0, 15, 16, 0, 16, 17, 0, 17, 18, 0, 18, 19, 0, 19, 20, 0, 20, 21, 0, 21, 22, 0, 22, 23, 0, 23, 24, 0, 24, 25, 0, 25, 26, 0, 26, 27, 0, 27, 28, 0, 28, 29, 0, 29, 30, 0, 30, 31, 0, 31, 32, 0, 32, 1, 1, 33, 34, 2, 2, 34, 35, 3, 3, 35, 36, 4, 4, 36, 37, 5, 5, 37, 38, 6, 6, 38, 39, 7, 7, 39, 40, 8, 8, 40, 41, 9, 9, 41, 42, 10, 10, 42, 43, 11, 11, 43, 44, 12, 12, 44, 45, 13, 13, 45, 46, 14, 14, 46, 47, 15, 15, 47, 48, 16, 16, 48, 49, 17, 17, 49, 50, 18, 18, 50, 51, 19, 19, 51, 52, 20, 20, 52, 53, 21, 21, 53, 54, 22, 22, 54, 55, 23, 23, 55, 56, 24, 24, 56, 57, 25, 25, 57, 58, 26, 26, 58, 59, 27, 27, 59, 60, 28, 28, 60, 61, 29, 29, 61, 62, 30, 30, 62, 63, 31, 31, 63, 64, 32, 32, 64, 33, 1, 33, 65, 66, 34, 34, 66, 67, 35, 35, 67, 68, 36, 36, 68, 69, 37, 37, 69, 70, 38, 38, 70, 71, 39, 39, 71, 72, 40, 40, 72, 73, 41, 41, 73, 74, 42, 42, 74, 75, 43, 43, 75, 76, 44, 44, 76, 77, 45, 45, 77, 78, 46, 46, 78, 79, 47, 47, 79, 80, 48, 48, 80, 81, 49, 49, 81, 82, 50, 50, 82, 83, 51, 51, 83, 84, 52, 52, 84, 85, 53, 53, 85, 86, 54, 54, 86, 87, 55, 55, 87, 88, 56, 56, 88, 89, 57, 57, 89, 90, 58, 58, 90, 91, 59, 59, 91, 92, 60, 60, 92, 93, 61, 61, 93, 94, 62, 62, 94, 95, 63, 63, 95, 96, 64, 64, 96, 65, 33, 65, 97, 98, 66, 66, 98, 99, 67, 67, 99, 100, 68, 68, 100, 101, 69, 69, 101, 102, 70, 70, 102, 103, 71, 71, 103, 104, 72, 72, 104, 105, 73, 73, 105, 106, 74, 74, 106, 107, 75, 75, 107, 108, 76, 76, 108, 109, 77, 77, 109, 110, 78, 78, 110, 111, 79, 79, 111, 112, 80, 80, 112, 113, 81, 81, 113, 114, 82, 82, 114, 115, 83, 83, 115, 116, 84, 84, 116, 117, 85, 85, 117, 118, 86, 86, 118, 119, 87, 87, 119, 120, 88, 88, 120, 121, 89, 89, 121, 122, 90, 90, 122, 123, 91, 91, 123, 124, 92, 92, 124, 125, 93, 93, 125, 126, 94, 94, 126, 127, 95, 95, 127, 128, 96, 96, 128, 97, 65, 97, 129, 130, 98, 98, 130, 131, 99, 99, 131, 132, 100, 100, 132, 133, 101, 101, 133, 134, 102, 102, 134, 135, 103, 103, 135, 136, 104, 104, 136, 137, 105, 105, 137, 138, 106, 106, 138, 139, 107, 107, 139, 140, 108, 108, 140, 141, 109, 109, 141, 142, 110, 110, 142, 143, 111, 111, 143, 144, 112, 112, 144, 145, 113, 113, 145, 146, 114, 114, 146, 147, 115, 115, 147, 148, 116, 116, 148, 149, 117, 117, 149, 150, 118, 118, 150, 151, 119, 119, 151, 152, 120, 120, 152, 153, 121, 121, 153, 154, 122, 122, 154, 155, 123, 123, 155, 156, 124, 124, 156, 157, 125, 125, 157, 158, 126, 126, 158, 159, 127, 127, 159, 160, 128, 128, 160, 129, 97, 129, 161, 162, 130, 130, 162, 163, 131, 131, 163, 164, 132, 132, 164, 165, 133, 133, 165, 166, 134, 134, 166, 167, 135, 135, 167, 168, 136, 136, 168, 169, 137, 137, 169, 170, 138, 138, 170, 171, 139, 139, 171, 172, 140, 140, 172, 173, 141, 141, 173, 174, 142, 142, 174, 175, 143, 143, 175, 176, 144, 144, 176, 177, 145, 145, 177, 178, 146, 146, 178, 179, 147, 147, 179, 180, 148, 148, 180, 181, 149, 149, 181, 182, 150, 150, 182, 183, 151, 151, 183, 184, 152, 152, 184, 185, 153, 153, 185, 186, 154, 154, 186, 187, 155, 155, 187, 188, 156, 156, 188, 189, 157, 157, 189, 190, 158, 158, 190, 191, 159, 159, 191, 192, 160, 160, 192, 161, 129, 161, 193, 194, 162, 162, 194, 195, 163, 163, 195, 196, 164, 164, 196, 197, 165, 165, 197, 198, 166, 166, 198, 199, 167, 167, 199, 200, 168, 168, 200, 201, 169, 169, 201, 202, 170, 170, 202, 203, 171, 171, 203, 204, 172, 172, 204, 205, 173, 173, 205, 206, 174, 174, 206, 207, 175, 175, 207, 208, 176, 176, 208, 209, 177, 177, 209, 210, 178, 178, 210, 211, 179, 179, 211, 212, 180, 180, 212, 213, 181, 181, 213, 214, 182, 182, 214, 215, 183, 183, 215, 216, 184, 184, 216, 217, 185, 185, 217, 218, 186, 186, 218, 219, 187, 187, 219, 220, 188, 188, 220, 221, 189, 189, 221, 222, 190, 190, 222, 223, 191, 191, 223, 224, 192, 192, 224, 193, 161, 193, 225, 226, 194, 194, 226, 227, 195, 195, 227, 228, 196, 196, 228, 229, 197, 197, 229, 230, 198, 198, 230, 231, 199, 199, 231, 232, 200, 200, 232, 233, 201, 201, 233, 234, 202, 202, 234, 235, 203, 203, 235, 236, 204, 204, 236, 237, 205, 205, 237, 238, 206, 206, 238, 239, 207, 207, 239, 240, 208, 208, 240, 241, 209, 209, 241, 242, 210, 210, 242, 243, 211, 211, 243, 244, 212, 212, 244, 245, 213, 213, 245, 246, 214, 214, 246, 247, 215, 215, 247, 248, 216, 216, 248, 249, 217, 217, 249, 250, 218, 218, 250, 251, 219, 219, 251, 252, 220, 220, 252, 253, 221, 221, 253, 254, 222, 222, 254, 255, 223, 223, 255, 256, 224, 224, 256, 225, 193, 225, 257, 258, 226, 226, 258, 259, 227, 227, 259, 260, 228, 228, 260, 261, 229, 229, 261, 262, 230, 230, 262, 263, 231, 231, 263, 264, 232, 232, 264, 265, 233, 233, 265, 266, 234, 234, 266, 267, 235, 235, 267, 268, 236, 236, 268, 269, 237, 237, 269, 270, 238, 238, 270, 271, 239, 239, 271, 272, 240, 240, 272, 273, 241, 241, 273, 274, 242, 242, 274, 275, 243, 243, 275, 276, 244, 244, 276, 277, 245, 245, 277, 278, 246, 246, 278, 279, 247, 247, 279, 280, 248, 248, 280, 281, 249, 249, 281, 282, 250, 250, 282, 283, 251, 251, 283, 284, 252, 252, 284, 285, 253, 253, 285, 286, 254, 254, 286, 287, 255, 255, 287, 288, 256, 256, 288, 257, 225, 257, 289, 290, 258, 258, 290, 291, 259, 259, 291, 292, 260, 260, 292, 293, 261, 261, 293, 294, 262, 262, 294, 295, 263, 263, 295, 296, 264, 264, 296, 297, 265, 265, 297, 298, 266, 266, 298, 299, 267, 267, 299, 300, 268, 268, 300, 301, 269, 269, 301, 302, 270, 270, 302, 303, 271, 271, 303, 304, 272, 272, 304, 305, 273, 273, 305, 306, 274, 274, 306, 307, 275, 275, 307, 308, 276, 276, 308, 309, 277, 277, 309, 310, 278, 278, 310, 311, 279, 279, 311, 312, 280, 280, 312, 313, 281, 281, 313, 314, 282, 282, 314, 315, 283, 283, 315, 316, 284, 284, 316, 317, 285, 285, 317, 318, 286, 286, 318, 319, 287, 287, 319, 320, 288, 288, 320, 289, 257, 289, 321, 322, 290, 290, 322, 323, 291, 291, 323, 324, 292, 292, 324, 325, 293, 293, 325, 326, 294, 294, 326, 327, 295, 295, 327, 328, 296, 296, 328, 329, 297, 297, 329, 330, 298, 298, 330, 331, 299, 299, 331, 332, 300, 300, 332, 333, 301, 301, 333, 334, 302, 302, 334, 335, 303, 303, 335, 336, 304, 304, 336, 337, 305, 305, 337, 338, 306, 306, 338, 339, 307, 307, 339, 340, 308, 308, 340, 341, 309, 309, 341, 342, 310, 310, 342, 343, 311, 311, 343, 344, 312, 312, 344, 345, 313, 313, 345, 346, 314, 314, 346, 347, 315, 315, 347, 348, 316, 316, 348, 349, 317, 317, 349, 350, 318, 318, 350, 351, 319, 319, 351, 352, 320, 320, 352, 321, 289, 321, 353, 354, 322, 322, 354, 355, 323, 323, 355, 356, 324, 324, 356, 357, 325, 325, 357, 358, 326, 326, 358, 359, 327, 327, 359, 360, 328, 328, 360, 361, 329, 329, 361, 362, 330, 330, 362, 363, 331, 331, 363, 364, 332, 332, 364, 365, 333, 333, 365, 366, 334, 334, 366, 367, 335, 335, 367, 368, 336, 336, 368, 369, 337, 337, 369, 370, 338, 338, 370, 371, 339, 339, 371, 372, 340, 340, 372, 373, 341, 341, 373, 374, 342, 342, 374, 375, 343, 343, 375, 376, 344, 344, 376, 377, 345, 345, 377, 378, 346, 346, 378, 379, 347, 347, 379, 380, 348, 348, 380, 381, 349, 349, 381, 382, 350, 350, 382, 383, 351, 351, 383, 384, 352, 352, 384, 353, 321, 353, 385, 386, 354, 354, 386, 387, 355, 355, 387, 388, 356, 356, 388, 389, 357, 357, 389, 390, 358, 358, 390, 391, 359, 359, 391, 392, 360, 360, 392, 393, 361, 361, 393, 394, 362, 362, 394, 395, 363, 363, 395, 396, 364, 364, 396, 397, 365, 365, 397, 398, 366, 366, 398, 399, 367, 367, 399, 400, 368, 368, 400, 401, 369, 369, 401, 402, 370, 370, 402, 403, 371, 371, 403, 404, 372, 372, 404, 405, 373, 373, 405, 406, 374, 374, 406, 407, 375, 375, 407, 408, 376, 376, 408, 409, 377, 377, 409, 410, 378, 378, 410, 411, 379, 379, 411, 412, 380, 380, 412, 413, 381, 381, 413, 414, 382, 382, 414, 415, 383, 383, 415, 416, 384, 384, 416, 385, 353, 385, 417, 418, 386, 386, 418, 419, 387, 387, 419, 420, 388, 388, 420, 421, 389, 389, 421, 422, 390, 390, 422, 423, 391, 391, 423, 424, 392, 392, 424, 425, 393, 393, 425, 426, 394, 394, 426, 427, 395, 395, 427, 428, 396, 396, 428, 429, 397, 397, 429, 430, 398, 398, 430, 431, 399, 399, 431, 432, 400, 400, 432, 433, 401, 401, 433, 434, 402, 402, 434, 435, 403, 403, 435, 436, 404, 404, 436, 437, 405, 405, 437, 438, 406, 406, 438, 439, 407, 407, 439, 440, 408, 408, 440, 441, 409, 409, 441, 442, 410, 410, 442, 443, 411, 411, 443, 444, 412, 412, 444, 445, 413, 413, 445, 446, 414, 414, 446, 447, 415, 415, 447, 448, 416, 416, 448, 417, 385, 417, 449, 450, 418, 418, 450, 451, 419, 419, 451, 452, 420, 420, 452, 453, 421, 421, 453, 454, 422, 422, 454, 455, 423, 423, 455, 456, 424, 424, 456, 457, 425, 425, 457, 458, 426, 426, 458, 459, 427, 427, 459, 460, 428, 428, 460, 461, 429, 429, 461, 462, 430, 430, 462, 463, 431, 431, 463, 464, 432, 432, 464, 465, 433, 433, 465, 466, 434, 434, 466, 467, 435, 435, 467, 468, 436, 436, 468, 469, 437, 437, 469, 470, 438, 438, 470, 471, 439, 439, 471, 472, 440, 440, 472, 473, 441, 441, 473, 474, 442, 442, 474, 475, 443, 443, 475, 476, 444, 444, 476, 477, 445, 445, 477, 478, 446, 446, 478, 479, 447, 447, 479, 480, 448, 448, 480, 449, 417, 481, 450, 449, 481, 451, 450, 481, 452, 451, 481, 453, 452, 481, 454, 453, 481, 455, 454, 481, 456, 455, 481, 457, 456, 481, 458, 457, 481, 459, 458, 481, 460, 459, 481, 461, 460, 481, 462, 461, 481, 463, 462, 481, 464, 463, 481, 465, 464, 481, 466, 465, 481, 467, 466, 481, 468, 467, 481, 469, 468, 481, 470, 469, 481, 471, 470, 481, 472, 471, 481, 473, 472, 481, 474, 473, 481, 475, 474, 481, 476, 475, 481, 477, 476, 481, 478, 477, 481, 479, 478, 481, 480, 479, 481, 449, 480]
+ point3f[] points = [(0, 0, 16.736341), (-1.4272199e-7, 3.2650983, 16.414757), (-0.6369891, 3.2023604, 16.414757), (-1.249499, 3.0165577, 16.414757), (-1.8139913, 2.7148302, 16.414757), (-2.308773, 2.308773, 16.414757), (-2.7148302, 1.8139913, 16.414757), (-3.016558, 1.2494985, 16.414757), (-3.2023604, 0.63698834, 16.414757), (-3.2650983, -0.0000010639042, 16.414757), (-3.2023602, -0.6369904, 16.414757), (-3.0165567, -1.2495005, 16.414757), (-2.714829, -1.8139931, 16.414757), (-2.3087718, -2.3087747, 16.414757), (-1.8139895, -2.7148314, 16.414757), (-1.2494966, -3.0165586, 16.414757), (-0.63698626, -3.2023609, 16.414757), (0.0000031527761, -3.2650983, 16.414757), (0.63699245, -3.2023597, 16.414757), (1.2495024, -3.016556, 16.414757), (1.8139948, -2.7148278, 16.414757), (2.3087761, -2.3087702, 16.414757), (2.7148325, -1.8139877, 16.414757), (3.0165594, -1.2494947, 16.414757), (3.2023613, -0.63698417, 16.414757), (3.2650983, 0.0000052416485, 16.414757), (3.2023592, 0.6369945, 16.414757), (3.0165553, 1.2495043, 16.414757), (2.7148266, 1.8139966, 16.414757), (2.3087687, 2.3087776, 16.414757), (1.8139861, 2.7148337, 16.414757), (1.2494928, 3.01656, 16.414757), (0.63698214, 3.2023618, 16.414757), (-2.7995924e-7, 6.404721, 15.462363), (-1.2494991, 6.281656, 15.462363), (-2.4509802, 5.9171906, 15.462363), (-3.558272, 5.325331, 15.462363), (-4.5288215, 4.5288215, 15.462363), (-5.325331, 3.558272, 15.462363), (-5.917191, 2.4509795, 15.462363), (-6.2816563, 1.2494975, 15.462363), (-6.404721, -0.000002086923, 15.462363), (-6.2816553, -1.2495016, 15.462363), (-5.917189, -2.4509833, 15.462363), (-5.325329, -3.5582755, 15.462363), (-4.5288186, -4.5288243, 15.462363), (-3.5582685, -5.325333, 15.462363), (-2.4509757, -5.9171925, 15.462363), (-1.2494935, -6.2816567, 15.462363), (0.000006184393, -6.404721, 15.462363), (1.2495056, -6.2816544, 15.462363), (2.450987, -5.9171877, 15.462363), (3.5582788, -5.3253264, 15.462363), (4.528827, -4.5288157, 15.462363), (5.3253355, -3.558265, 15.462363), (5.917194, -2.4509718, 15.462363), (6.2816577, -1.2494894, 15.462363), (6.404721, 0.000010281864, 15.462363), (6.281654, 1.2495097, 15.462363), (5.9171863, 2.450991, 15.462363), (5.325324, 3.5582821, 15.462363), (4.5288124, 4.52883, 15.462363), (3.5582616, 5.325338, 15.462363), (2.4509683, 5.917196, 15.462363), (1.2494854, 6.2816586, 15.462363), (-4.064378e-7, 9.298213, 13.915759), (-1.8139914, 9.11955, 13.915759), (-3.5582716, 8.590429, 13.915759), (-5.16581, 7.731182, 13.915759), (-6.5748296, 6.5748296, 13.915759), (-7.731182, 5.16581, 13.915759), (-8.590429, 3.5582707, 13.915759), (-9.119551, 1.8139892, 13.915759), (-9.298213, -0.0000030297424, 13.915759), (-9.11955, -1.8139951, 13.915759), (-8.590426, -3.5582762, 13.915759), (-7.7311788, -5.165815, 13.915759), (-6.5748253, -6.574834, 13.915759), (-5.165805, -7.7311854, 13.915759), (-3.558265, -8.590432, 13.915759), (-1.8139833, -9.119552, 13.915759), (0.000008978347, -9.298213, 13.915759), (1.814001, -9.119549, 13.915759), (3.5582817, -8.590425, 13.915759), (5.16582, -7.7311754, 13.915759), (6.5748377, -6.574821, 13.915759), (7.731189, -5.1658, 13.915759), (8.590434, -3.5582595, 13.915759), (9.119553, -1.8139774, 13.915759), (9.298213, 0.000014926952, 13.915759), (9.119548, 1.8140068, 13.915759), (8.590423, 3.5582871, 13.915759), (7.731172, 5.165825, 13.915759), (6.5748167, 6.574842, 13.915759), (5.165795, 7.731192, 13.915759), (3.5582542, 8.590436, 13.915759), (1.8139715, 9.1195545, 13.915759), (-5.1729717e-7, 11.83438, 11.83438), (-2.308773, 11.606985, 11.83438), (-4.528821, 10.933542, 11.83438), (-6.5748286, 9.839928, 11.83438), (-8.368171, 8.368171, 11.83438), (-9.839928, 6.5748286, 11.83438), (-10.933542, 4.5288196, 11.83438), (-11.606986, 2.3087702, 11.83438), (-11.83438, -0.0000038561307, 11.83438), (-11.606985, -2.3087778, 11.83438), (-10.933539, -4.528826, 11.83438), (-9.839924, -6.5748353, 11.83438), (-8.368165, -8.3681755, 11.83438), (-6.5748224, -9.839932, 11.83438), (-4.5288124, -10.933545, 11.83438), (-2.3087628, -11.606988, 11.83438), (0.000011427267, -11.83438, 11.83438), (2.3087852, -11.606983, 11.83438), (4.5288334, -10.933537, 11.83438), (6.5748415, -9.839919, 11.83438), (8.368181, -8.368159, 11.83438), (9.839936, -6.574816, 11.83438), (10.933548, -4.5288053, 11.83438), (11.606989, -2.3087552, 11.83438), (11.83438, 0.000018998406, 11.83438), (11.606982, 2.3087926, 11.83438), (10.933534, 4.5288405, 11.83438), (9.839915, 6.5748477, 11.83438), (8.368155, 8.368187, 11.83438), (6.5748096, 9.839941, 11.83438), (4.5287986, 10.933551, 11.83438), (2.3087478, 11.606991, 11.83438), (-6.082772e-7, 13.91576, 9.298212), (-2.7148302, 13.648373, 9.298212), (-5.3253303, 12.856486, 9.298212), (-7.731181, 11.570532, 9.298212), (-9.839928, 9.839928, 9.298212), (-11.570532, 7.731181, 9.298212), (-12.856487, 5.325329, 9.298212), (-13.648373, 2.7148268, 9.298212), (-13.91576, -0.0000045343304, 9.298212), (-13.648372, -2.7148356, 9.298212), (-12.856483, -5.325337, 9.298212), (-11.570527, -7.731189, 9.298212), (-9.839922, -9.839934, 9.298212), (-7.731174, -11.570538, 9.298212), (-5.3253202, -12.85649, 9.298212), (-2.714818, -13.648375, 9.298212), (0.000013437046, -13.91576, 9.298212), (2.7148445, -13.64837, 9.298212), (5.325345, -12.85648, 9.298212), (7.7311964, -11.570522, 9.298212), (9.839941, -9.839915, 9.298212), (11.570542, -7.7311664, 9.298212), (12.856494, -5.325312, 9.298212), (13.648376, -2.7148092, 9.298212), (13.91576, 0.000022339764, 9.298212), (13.648368, 2.714853, 9.298212), (12.856477, 5.3253536, 9.298212), (11.570518, 7.7312036, 9.298212), (9.839909, 9.839947, 9.298212), (7.731159, 11.570547, 9.298212), (5.325304, 12.856497, 9.298212), (2.7148006, 13.648378, 9.298212), (-6.758814e-7, 15.462363, 6.4047203), (-3.0165575, 15.165257, 6.4047203), (-5.9171896, 14.285361, 6.4047203), (-8.590428, 12.856485, 6.4047203), (-10.933541, 10.933541, 6.4047203), (-12.856485, 8.590428, 6.4047203), (-14.285362, 5.9171877, 6.4047203), (-15.165258, 3.0165539, 6.4047203), (-15.462363, -0.000005038278, 6.4047203), (-15.1652565, -3.0165637, 6.4047203), (-14.285357, -5.917197, 6.4047203), (-12.856481, -8.590436, 6.4047203), (-10.933536, -10.933549, 6.4047203), (-8.59042, -12.856491, 6.4047203), (-5.9171786, -14.285366, 6.4047203), (-3.016544, -15.16526, 6.4047203), (0.000014930445, -15.462363, 6.4047203), (3.0165734, -15.165255, 6.4047203), (5.9172063, -14.285354, 6.4047203), (8.590445, -12.856475, 6.4047203), (10.933556, -10.933528, 6.4047203), (12.856497, -8.590411, 6.4047203), (14.28537, -5.9171696, 6.4047203), (15.165262, -3.016534, 6.4047203), (15.462363, 0.000024822613, 6.4047203), (15.165254, 3.016583, 6.4047203), (14.285351, 5.9172153, 6.4047203), (12.856469, 8.590453, 6.4047203), (10.93352, 10.933563, 6.4047203), (8.590404, 12.856503, 6.4047203), (5.9171605, 14.285374, 6.4047203), (3.0165246, 15.165265, 6.4047203), (-7.175118e-7, 16.414757, 3.2650988), (-3.2023602, 16.099352, 3.2650988), (-6.281655, 15.165258, 3.2650988), (-9.11955, 13.648372, 3.2650988), (-11.606986, 11.606986, 3.2650988), (-13.648372, 9.11955, 3.2650988), (-15.165259, 6.281653, 3.2650988), (-16.099352, 3.2023563, 3.2650988), (-16.414757, -0.000005348607, 3.2650988), (-16.09935, -3.2023668, 3.2650988), (-15.165255, -6.2816625, 3.2650988), (-13.648366, -9.119558, 3.2650988), (-11.606978, -11.606994, 3.2650988), (-9.11954, -13.648378, 3.2650988), (-6.281643, -15.165263, 3.2650988), (-3.202346, -16.099354, 3.2650988), (0.000015850075, -16.414757, 3.2650988), (3.202377, -16.099348, 3.2650988), (6.2816725, -15.165251, 3.2650988), (9.119567, -13.64836, 3.2650988), (11.607, -11.606971, 3.2650988), (13.648384, -9.119532, 3.2650988), (15.165267, -6.2816334, 3.2650988), (16.099356, -3.2023356, 3.2650988), (16.414757, 0.000026351545, 3.2650988), (16.099346, 3.2023873, 3.2650988), (15.165247, 6.2816825, 3.2650988), (13.648355, 9.1195755, 3.2650988), (11.606963, 11.607008, 3.2650988), (9.119523, 13.64839, 3.2650988), (6.281624, 15.165271, 3.2650988), (3.2023253, 16.09936, 3.2650988), (-7.315687e-7, 16.736341, 0.0000012635586), (-3.2650983, 16.414757, 0.0000012635586), (-6.40472, 15.462364, 0.0000012635586), (-9.298212, 13.91576, 0.0000012635586), (-11.83438, 11.83438, 0.0000012635586), (-13.91576, 9.298212, 0.0000012635586), (-15.462365, 6.404718, 0.0000012635586), (-16.414759, 3.2650943, 0.0000012635586), (-16.736341, -0.0000054533925, 0.0000012635586), (-16.414757, -3.265105, 0.0000012635586), (-15.462359, -6.404728, 0.0000012635586), (-13.915754, -9.298222, 0.0000012635586), (-11.834373, -11.834389, 0.0000012635586), (-9.298203, -13.915766, 0.0000012635586), (-6.404708, -15.462369, 0.0000012635586), (-3.2650838, -16.41476, 0.0000012635586), (0.000016160597, -16.736341, 0.0000012635586), (3.2651155, -16.414753, 0.0000012635586), (6.404738, -15.462356, 0.0000012635586), (9.29823, -13.915748, 0.0000012635586), (11.834395, -11.834366, 0.0000012635586), (13.915772, -9.298194, 0.0000012635586), (15.462373, -6.404698, 0.0000012635586), (16.414762, -3.265073, 0.0000012635586), (16.736341, 0.000026867803, 0.0000012635586), (16.414751, 3.265126, 0.0000012635586), (15.462352, 6.404748, 0.0000012635586), (13.915742, 9.29824, 0.0000012635586), (11.834357, 11.834403, 0.0000012635586), (9.298185, 13.915778, 0.0000012635586), (6.4046884, 15.462377, 0.0000012635586), (3.2650626, 16.414764, 0.0000012635586), (-7.175119e-7, 16.414759, -3.2650962), (-3.2023606, 16.099354, -3.2650962), (-6.281656, 15.16526, -3.2650962), (-9.119551, 13.648374, -3.2650962), (-11.606987, 11.606987, -3.2650962), (-13.648374, 9.119551, -3.2650962), (-15.165261, 6.281654, -3.2650962), (-16.099354, 3.2023566, -3.2650962), (-16.414759, -0.0000053486074, -3.2650962), (-16.099352, -3.2023673, -3.2650962), (-15.1652565, -6.2816634, -3.2650962), (-13.648368, -9.119559, -3.2650962), (-11.60698, -11.606995, -3.2650962), (-9.119542, -13.648379, -3.2650962), (-6.281644, -15.165265, -3.2650962), (-3.2023463, -16.099356, -3.2650962), (0.000015850077, -16.414759, -3.2650962), (3.2023776, -16.09935, -3.2650962), (6.2816734, -15.165253, -3.2650962), (9.119568, -13.648362, -3.2650962), (11.607001, -11.606973, -3.2650962), (13.648385, -9.119533, -3.2650962), (15.165269, -6.281634, -3.2650962), (16.099358, -3.2023358, -3.2650962), (16.414759, 0.000026351549, -3.2650962), (16.099348, 3.2023878, -3.2650962), (15.165248, 6.281683, -3.2650962), (13.648356, 9.119577, -3.2650962), (11.606964, 11.60701, -3.2650962), (9.119524, 13.648392, -3.2650962), (6.281625, 15.165273, -3.2650962), (3.2023256, 16.099361, -3.2650962), (-6.758814e-7, 15.462364, -6.404718), (-3.0165577, 15.165258, -6.404718), (-5.91719, 14.285362, -6.404718), (-8.590428, 12.856486, -6.404718), (-10.933542, 10.933542, -6.404718), (-12.856486, 8.590428, -6.404718), (-14.285363, 5.917188, -6.404718), (-15.165259, 3.0165539, -6.404718), (-15.462364, -0.000005038278, -6.404718), (-15.165257, -3.016564, -6.404718), (-14.285358, -5.917197, -6.404718), (-12.856482, -8.590437, -6.404718), (-10.933536, -10.93355, -6.404718), (-8.59042, -12.856492, -6.404718), (-5.917179, -14.285367, -6.404718), (-3.0165443, -15.165261, -6.404718), (0.000014930446, -15.462364, -6.404718), (3.0165734, -15.165256, -6.404718), (5.917207, -14.285355, -6.404718), (8.5904455, -12.856476, -6.404718), (10.933557, -10.933529, -6.404718), (12.856498, -8.590412, -6.404718), (14.285371, -5.9171696, -6.404718), (15.165263, -3.0165343, -6.404718), (15.462364, 0.000024822615, -6.404718), (15.165255, 3.0165832, -6.404718), (14.285351, 5.917216, -6.404718), (12.85647, 8.590453, -6.404718), (10.933521, 10.933563, -6.404718), (8.590404, 12.8565035, -6.404718), (5.917161, 14.285375, -6.404718), (3.0165248, 15.165266, -6.404718), (-6.082772e-7, 13.91576, -9.298212), (-2.7148302, 13.648373, -9.298212), (-5.3253303, 12.856486, -9.298212), (-7.731181, 11.570532, -9.298212), (-9.839928, 9.839928, -9.298212), (-11.570532, 7.731181, -9.298212), (-12.856487, 5.325329, -9.298212), (-13.648373, 2.7148268, -9.298212), (-13.91576, -0.0000045343304, -9.298212), (-13.648372, -2.7148356, -9.298212), (-12.856483, -5.325337, -9.298212), (-11.570527, -7.731189, -9.298212), (-9.839922, -9.839934, -9.298212), (-7.731174, -11.570538, -9.298212), (-5.3253202, -12.85649, -9.298212), (-2.714818, -13.648375, -9.298212), (0.000013437046, -13.91576, -9.298212), (2.7148445, -13.64837, -9.298212), (5.325345, -12.85648, -9.298212), (7.7311964, -11.570522, -9.298212), (9.839941, -9.839915, -9.298212), (11.570542, -7.7311664, -9.298212), (12.856494, -5.325312, -9.298212), (13.648376, -2.7148092, -9.298212), (13.91576, 0.000022339764, -9.298212), (13.648368, 2.714853, -9.298212), (12.856477, 5.3253536, -9.298212), (11.570518, 7.7312036, -9.298212), (9.839909, 9.839947, -9.298212), (7.731159, 11.570547, -9.298212), (5.325304, 12.856497, -9.298212), (2.7148006, 13.648378, -9.298212), (-5.1729717e-7, 11.83438, -11.83438), (-2.308773, 11.606985, -11.83438), (-4.528821, 10.933542, -11.83438), (-6.5748286, 9.839928, -11.83438), (-8.368171, 8.368171, -11.83438), (-9.839928, 6.5748286, -11.83438), (-10.933542, 4.5288196, -11.83438), (-11.606986, 2.3087702, -11.83438), (-11.83438, -0.0000038561307, -11.83438), (-11.606985, -2.3087778, -11.83438), (-10.933539, -4.528826, -11.83438), (-9.839924, -6.5748353, -11.83438), (-8.368165, -8.3681755, -11.83438), (-6.5748224, -9.839932, -11.83438), (-4.5288124, -10.933545, -11.83438), (-2.3087628, -11.606988, -11.83438), (0.000011427267, -11.83438, -11.83438), (2.3087852, -11.606983, -11.83438), (4.5288334, -10.933537, -11.83438), (6.5748415, -9.839919, -11.83438), (8.368181, -8.368159, -11.83438), (9.839936, -6.574816, -11.83438), (10.933548, -4.5288053, -11.83438), (11.606989, -2.3087552, -11.83438), (11.83438, 0.000018998406, -11.83438), (11.606982, 2.3087926, -11.83438), (10.933534, 4.5288405, -11.83438), (9.839915, 6.5748477, -11.83438), (8.368155, 8.368187, -11.83438), (6.5748096, 9.839941, -11.83438), (4.5287986, 10.933551, -11.83438), (2.3087478, 11.606991, -11.83438), (-4.0643775e-7, 9.298212, -13.91576), (-1.8139912, 9.119549, -13.91576), (-3.5582714, 8.590428, -13.91576), (-5.165809, 7.731181, -13.91576), (-6.5748286, 6.5748286, -13.91576), (-7.731181, 5.165809, -13.91576), (-8.590428, 3.5582702, -13.91576), (-9.11955, 1.8139889, -13.91576), (-9.298212, -0.0000030297422, -13.91576), (-9.119549, -1.8139949, -13.91576), (-8.5904255, -3.5582757, -13.91576), (-7.731178, -5.1658144, -13.91576), (-6.574825, -6.574833, -13.91576), (-5.1658044, -7.7311845, -13.91576), (-3.5582647, -8.590431, -13.91576), (-1.8139832, -9.119551, -13.91576), (0.000008978346, -9.298212, -13.91576), (1.8140007, -9.119548, -13.91576), (3.5582814, -8.590424, -13.91576), (5.165819, -7.7311745, -13.91576), (6.574837, -6.5748205, -13.91576), (7.731188, -5.1657996, -13.91576), (8.590433, -3.5582592, -13.91576), (9.119552, -1.8139772, -13.91576), (9.298212, 0.00001492695, -13.91576), (9.119547, 1.8140066, -13.91576), (8.590422, 3.558287, -13.91576), (7.731171, 5.1658244, -13.91576), (6.5748158, 6.5748415, -13.91576), (5.1657944, 7.731191, -13.91576), (3.558254, 8.590435, -13.91576), (1.8139714, 9.119554, -13.91576), (-2.7995912e-7, 6.404718, -15.462365), (-1.2494985, 6.281653, -15.462365), (-2.4509792, 5.917188, -15.462365), (-3.5582702, 5.325329, -15.462365), (-4.5288196, 4.5288196, -15.462365), (-5.325329, 3.5582702, -15.462365), (-5.917188, 2.4509785, -15.462365), (-6.2816534, 1.2494969, -15.462365), (-6.404718, -0.000002086922, -15.462365), (-6.2816525, -1.2495011, -15.462365), (-5.9171863, -2.4509823, -15.462365), (-5.3253264, -3.5582738, -15.462365), (-4.5288167, -4.5288224, -15.462365), (-3.5582669, -5.3253307, -15.462365), (-2.4509747, -5.91719, -15.462365), (-1.249493, -6.281654, -15.462365), (0.0000061843903, -6.404718, -15.462365), (1.249505, -6.2816515, -15.462365), (2.4509861, -5.917185, -15.462365), (3.5582771, -5.325324, -15.462365), (4.5288253, -4.528814, -15.462365), (5.325333, -3.5582635, -15.462365), (5.9171915, -2.450971, -15.462365), (6.281655, -1.2494888, -15.462365), (6.404718, 0.000010281859, -15.462365), (6.281651, 1.2495091, -15.462365), (5.9171834, 2.45099, -15.462365), (5.3253217, 3.5582807, -15.462365), (4.5288105, 4.528828, -15.462365), (3.55826, 5.3253355, -15.462365), (2.450967, 5.917193, -15.462365), (1.2494849, 6.281656, -15.462365), (-1.427218e-7, 3.2650943, -16.414759), (-0.6369883, 3.2023563, -16.414759), (-1.2494974, 3.0165539, -16.414759), (-1.813989, 2.7148268, -16.414759), (-2.3087702, 2.3087702, -16.414759), (-2.7148268, 1.813989, -16.414759), (-3.016554, 1.2494969, -16.414759), (-3.2023566, 0.6369875, -16.414759), (-3.2650943, -0.0000010639028, -16.414759), (-3.202356, -0.6369896, -16.414759), (-3.0165532, -1.249499, -16.414759), (-2.7148256, -1.8139908, -16.414759), (-2.308769, -2.3087718, -16.414759), (-1.8139873, -2.714828, -16.414759), (-1.249495, -3.0165548, -16.414759), (-0.6369855, -3.2023568, -16.414759), (0.0000031527723, -3.2650943, -16.414759), (0.6369917, -3.2023556, -16.414759), (1.2495009, -3.0165522, -16.414759), (1.8139925, -2.7148244, -16.414759), (2.3087733, -2.3087673, -16.414759), (2.7148292, -1.8139856, -16.414759), (3.0165555, -1.2494931, -16.414759), (3.2023573, -0.6369834, -16.414759), (3.2650943, 0.000005241642, -16.414759), (3.2023554, 0.6369937, -16.414759), (3.0165515, 1.2495028, -16.414759), (2.7148232, 1.8139943, -16.414759), (2.308766, 2.3087747, -16.414759), (1.8139838, 2.7148304, -16.414759), (1.2494912, 3.0165565, -16.414759), (0.63698137, 3.2023578, -16.414759), (0, 0, -16.736341)]
+ color3f[] primvars:displayColor = [(0.5411765, 0.03137255, 0.43137255)]
+ float3[] primvars:normals = [(1.2591131e-8, 1.2871766e-8, 1), (-1.2707872e-7, 0.20096879, 0.9795976), (-0.039207064, 0.19710727, 0.9795976), (-0.07690743, 0.18567102, 0.9795976), (-0.11165232, 0.16709946, 0.9795976), (-0.14210641, 0.14210641, 0.9795976), (-0.16709946, 0.11165232, 0.9795976), (-0.18567102, 0.076907404, 0.9795976), (-0.19710723, 0.039207, 0.9795976), (-0.20096883, -6.324106e-8, 0.97959757), (-0.19710727, -0.039207175, 0.9795976), (-0.18567094, -0.076907516, 0.9795976), (-0.16709942, -0.11165236, 0.97959757), (-0.14210634, -0.14210655, 0.97959757), (-0.11165216, -0.16709955, 0.9795976), (-0.07690728, -0.18567108, 0.97959757), (-0.039206892, -0.19710729, 0.97959757), (1.8793334e-7, -0.20096883, 0.97959757), (0.03920729, -0.19710723, 0.9795976), (0.07690765, -0.1856709, 0.9795976), (0.11165249, -0.16709933, 0.9795976), (0.14210661, -0.14210624, 0.9795976), (0.16709965, -0.11165208, 0.9795976), (0.18567112, -0.07690718, 0.9795976), (0.19710734, -0.039206777, 0.9795976), (0.20096883, 3.2157482e-7, 0.97959757), (0.1971072, 0.039207395, 0.97959757), (0.18567085, 0.07690778, 0.9795976), (0.16709925, 0.11165263, 0.9795976), (0.14210616, 0.14210668, 0.9795976), (0.111651994, 0.16709971, 0.97959757), (0.076907046, 0.18567115, 0.9795976), (0.039206747, 0.1971073, 0.9795976), (-2.6534158e-7, 0.38788694, 0.921707), (-0.07567297, 0.38043383, 0.921707), (-0.14843781, 0.3583608, 0.921707), (-0.21549846, 0.3225162, 0.921707), (-0.27427745, 0.27427745, 0.921707), (-0.3225162, 0.21549845, 0.921707), (-0.3583608, 0.14843778, 0.921707), (-0.38043383, 0.075672865, 0.921707), (-0.38788694, -1.2908511e-7, 0.921707), (-0.38043374, -0.07567316, 0.921707), (-0.35836074, -0.14843804, 0.921707), (-0.32251608, -0.21549855, 0.921707), (-0.27427727, -0.27427766, 0.921707), (-0.2154982, -0.3225163, 0.921707), (-0.14843756, -0.3583609, 0.921707), (-0.075672604, -0.38043386, 0.921707), (3.537888e-7, -0.38788694, 0.921707), (0.075673394, -0.3804337, 0.921707), (0.14843827, -0.35836065, 0.921707), (0.21549882, -0.32251593, 0.921707), (0.27427784, -0.2742771, 0.921707), (0.32251647, -0.21549799, 0.921707), (0.35836104, -0.14843734, 0.921707), (0.38043392, -0.0756724, 0.921707), (0.38788694, 5.6893066e-7, 0.921707), (0.3804337, 0.07567362, 0.921707), (0.35836056, 0.14843856, 0.921707), (0.32251573, 0.21549906, 0.921707), (0.2742769, 0.27427799, 0.921707), (0.21549784, 0.3225166, 0.921707), (0.14843716, 0.35836112, 0.921707), (0.07567234, 0.38043386, 0.921707), (-4.0952165e-7, 0.5597752, 0.8286446), (-0.109206654, 0.54901916, 0.8286445), (-0.21421656, 0.5171648, 0.8286446), (-0.31099445, 0.46543604, 0.8286446), (-0.3958208, 0.3958208, 0.8286446), (-0.46543607, 0.3109944, 0.8286445), (-0.5171649, 0.21421656, 0.8286445), (-0.5490193, 0.10920658, 0.8286445), (-0.5597751, -1.484815e-7, 0.8286445), (-0.54901916, -0.10920697, 0.8286446), (-0.5171647, -0.21421687, 0.8286446), (-0.46543586, -0.3109946, 0.8286446), (-0.3958206, -0.39582106, 0.8286446), (-0.3109941, -0.46543622, 0.8286446), (-0.21421619, -0.517165, 0.8286446), (-0.109206185, -0.54901934, 0.8286446), (5.0531617e-7, -0.5597752, 0.8286445), (0.1092073, -0.54901916, 0.8286446), (0.21421723, -0.5171645, 0.8286446), (0.31099498, -0.46543562, 0.8286446), (0.3958213, -0.39582032, 0.8286446), (0.46543646, -0.3109938, 0.8286446), (0.5171651, -0.21421584, 0.8286445), (0.5490194, -0.10920588, 0.8286446), (0.5597752, 7.855152e-7, 0.8286446), (0.54901904, 0.10920763, 0.8286446), (0.5171644, 0.2142176, 0.8286445), (0.4654354, 0.31099528, 0.8286446), (0.39582, 0.39582148, 0.8286446), (0.31099346, 0.4654366, 0.8286446), (0.21421559, 0.5171653, 0.8286445), (0.10920583, 0.5490194, 0.8286446), (-4.774545e-7, 0.7101414, 0.70405906), (-0.13854167, 0.69649625, 0.7040592), (-0.2717593, 0.65608513, 0.70405906), (-0.39453354, 0.590461, 0.70405906), (-0.5021459, 0.5021458, 0.704059), (-0.5904611, 0.3945335, 0.704059), (-0.65608513, 0.27175924, 0.70405906), (-0.69649637, 0.13854155, 0.70405906), (-0.7101415, -1.463554e-7, 0.70405906), (-0.6964962, -0.13854201, 0.70405906), (-0.656085, -0.27175966, 0.70405906), (-0.5904608, -0.3945338, 0.70405906), (-0.5021455, -0.5021461, 0.70405906), (-0.394533, -0.59046125, 0.70405906), (-0.2717588, -0.65608543, 0.70405906), (-0.1385411, -0.6964964, 0.70405906), (6.621982e-7, -0.7101414, 0.70405906), (0.13854246, -0.6964961, 0.704059), (0.2717601, -0.65608484, 0.70405906), (0.39453414, -0.5904605, 0.70405906), (0.5021464, -0.5021452, 0.70405906), (0.59046155, -0.39453268, 0.70405906), (0.65608555, -0.27175832, 0.70405906), (0.6964965, -0.13854067, 0.7040592), (0.7101414, 0.0000010436819, 0.70405906), (0.69649607, 0.13854288, 0.70405906), (0.6560846, 0.27176052, 0.70405906), (0.59046024, 0.39453456, 0.70405906), (0.5021449, 0.50214684, 0.70405906), (0.39453226, 0.5904618, 0.70405906), (0.271758, 0.6560857, 0.70405906), (0.13854063, 0.6964965, 0.70405906), (-5.311216e-7, 0.83333963, 0.5527614), (-0.16257648, 0.8173273, 0.5527614), (-0.31890523, 0.76990545, 0.5527614), (-0.46297878, 0.69289654, 0.5527614), (-0.5892601, 0.58926016, 0.5527613), (-0.69289654, 0.46297878, 0.5527613), (-0.76990545, 0.31890512, 0.5527614), (-0.8173273, 0.16257626, 0.5527614), (-0.8333397, -1.8985799e-7, 0.5527613), (-0.81732726, -0.16257685, 0.5527613), (-0.7699054, -0.31890565, 0.5527613), (-0.6928963, -0.46297908, 0.5527613), (-0.58925974, -0.5892605, 0.55276144), (-0.46297824, -0.6928969, 0.5527614), (-0.31890458, -0.7699057, 0.5527614), (-0.16257577, -0.8173274, 0.5527613), (7.738515e-7, -0.8333397, 0.5527614), (0.16257742, -0.81732714, 0.5527613), (0.31890613, -0.7699051, 0.5527613), (0.46297956, -0.692896, 0.5527614), (0.5892609, -0.5892593, 0.55276144), (0.6928972, -0.4629778, 0.5527614), (0.7699059, -0.3189041, 0.5527614), (0.81732756, -0.16257523, 0.5527614), (0.8333397, 0.0000012545046, 0.5527614), (0.817327, 0.16257784, 0.5527614), (0.76990485, 0.3189066, 0.5527613), (0.6928957, 0.4629801, 0.5527614), (0.589259, 0.58926123, 0.5527613), (0.46297735, 0.69289744, 0.5527614), (0.31890363, 0.76990616, 0.55276144), (0.16257519, 0.8173275, 0.55276144), (-6.2808493e-7, 0.92476565, 0.38053727), (-0.18041281, 0.90699655, 0.38053724), (-0.3538924, 0.854372, 0.38053724), (-0.5137723, 0.7689144, 0.38053727), (-0.653908, 0.653908, 0.38053727), (-0.7689144, 0.5137723, 0.38053724), (-0.8543721, 0.35389227, 0.38053718), (-0.90699655, 0.18041255, 0.38053718), (-0.92476565, -2.5989726e-7, 0.3805372), (-0.9069965, -0.18041328, 0.38053718), (-0.85437185, -0.35389283, 0.38053718), (-0.7689143, -0.51377255, 0.38053718), (-0.65390766, -0.65390843, 0.38053718), (-0.5137717, -0.7689148, 0.38053724), (-0.35389167, -0.8543723, 0.38053718), (-0.18041196, -0.90699667, 0.38053715), (8.4947897e-7, -0.92476565, 0.38053718), (0.18041381, -0.9069963, 0.38053715), (0.35389334, -0.8543716, 0.38053718), (0.5137732, -0.7689139, 0.38053727), (0.65390897, -0.65390724, 0.38053715), (0.7689152, -0.5137713, 0.38053718), (0.8543726, -0.35389113, 0.38053718), (0.90699685, -0.18041141, 0.38053715), (0.92476565, 0.0000013668669, 0.38053718), (0.90699625, 0.18041429, 0.38053718), (0.8543714, 0.35389397, 0.38053718), (0.7689135, 0.51377374, 0.38053718), (0.65390676, 0.65390927, 0.38053718), (0.5137708, 0.7689155, 0.38053727), (0.35389072, 0.8543728, 0.38053718), (0.18041132, 0.9069968, 0.38053727), (-6.695667e-7, 0.9810153, 0.19393012), (-0.19138657, 0.9621654, 0.19393015), (-0.37541825, 0.90634, 0.19393015), (-0.545023, 0.81568444, 0.1939301), (-0.6936826, 0.6936826, 0.19393009), (-0.81568444, 0.545023, 0.19393012), (-0.90634, 0.3754181, 0.19393016), (-0.9621655, 0.1913863, 0.19393021), (-0.9810153, -2.9143013e-7, 0.19393018), (-0.96216524, -0.19138706, 0.19393021), (-0.9063398, -0.3754187, 0.19393015), (-0.81568426, -0.5450232, 0.1939301), (-0.69368213, -0.69368297, 0.19393012), (-0.54502237, -0.81568485, 0.19393012), (-0.3754175, -0.9063403, 0.19393015), (-0.19138572, -0.9621656, 0.19393018), (9.609969e-7, -0.9810153, 0.19393009), (0.19138764, -0.9621652, 0.19393004), (0.37541926, -0.9063395, 0.19393007), (0.5450239, -0.8156838, 0.19393009), (0.69368345, -0.69368166, 0.1939301), (0.8156851, -0.54502183, 0.19393009), (0.90634054, -0.3754169, 0.19393012), (0.9621657, -0.19138512, 0.19393015), (0.9810153, 0.0000015101378, 0.19393009), (0.9621651, 0.1913882, 0.19392999), (0.9063393, 0.37541988, 0.19393003), (0.8156834, 0.5450245, 0.19393009), (0.6936813, 0.693684, 0.19393007), (0.5450213, 0.8156855, 0.19393003), (0.3754165, 0.9063408, 0.19393004), (0.19138505, 0.9621657, 0.19393004), (-6.5530895e-7, 1, 2.8669768e-7), (-0.19509028, 0.98078537, 2.9392538e-7), (-0.38268337, 0.92387956, 3.156084e-7), (-0.5555703, 0.8314696, 2.8187924e-7), (-0.70710677, 0.70710677, 2.4574084e-7), (-0.83146966, 0.5555703, 2.842885e-7), (-0.9238796, 0.38268325, 3.0838072e-7), (-0.98078537, 0.19509001, 2.9392538e-7), (-1, -2.9874383e-7, 2.9392538e-7), (-0.98078513, -0.1950908, 3.0356227e-7), (-0.9238793, -0.38268387, 3.1560833e-7), (-0.83146936, -0.5555706, 3.0838072e-7), (-0.70710635, -0.7071073, 3.0115305e-7), (-0.55556965, -0.8314701, 2.9633458e-7), (-0.38268262, -0.9238798, 2.9874383e-7), (-0.19508941, -0.9807854, 2.9151616e-7), (0.0000010359665, -0.99999994, 2.9151616e-7), (0.19509138, -0.98078513, 2.987438e-7), (0.3826844, -0.9238792, 3.156084e-7), (0.5555712, -0.8314689, 3.2042684e-7), (0.7071077, -0.7071059, 2.7947002e-7), (0.83147043, -0.5555691, 2.4815012e-7), (0.9238801, -0.38268206, 2.7224235e-7), (0.9807856, -0.19508882, 2.963346e-7), (0.99999994, 0.0000016117712, 2.9392538e-7), (0.980785, 0.19509204, 2.57787e-7), (0.92387897, 0.38268498, 2.4815012e-7), (0.8314686, 0.55557185, 3.1319917e-7), (0.70710546, 0.7071082, 3.4210984e-7), (0.55556864, 0.8314707, 3.2283606e-7), (0.38268155, 0.9238804, 3.156084e-7), (0.19508877, 0.98078567, 2.963346e-7), (-6.5993265e-7, 0.9810153, -0.19392991), (-0.19138654, 0.9621654, -0.19392997), (-0.37541825, 0.90634, -0.19392997), (-0.545023, 0.8156844, -0.19392993), (-0.6936826, 0.6936826, -0.19392991), (-0.8156844, 0.545023, -0.19392993), (-0.90634006, 0.37541813, -0.19392999), (-0.9621655, 0.19138631, -0.19393004), (-0.9810153, -3.0106423e-7, -0.19393004), (-0.96216536, -0.19138706, -0.19393003), (-0.9063398, -0.37541872, -0.19392996), (-0.8156842, -0.5450233, -0.19392996), (-0.69368213, -0.69368315, -0.19393001), (-0.54502237, -0.81568485, -0.19392997), (-0.37541753, -0.9063403, -0.19392999), (-0.19138567, -0.9621656, -0.19392999), (9.65814e-7, -0.9810153, -0.19392999), (0.19138762, -0.9621652, -0.1939299), (0.37541923, -0.90633965, -0.19392985), (0.5450239, -0.81568384, -0.1939299), (0.6936835, -0.69368166, -0.19392988), (0.8156851, -0.54502183, -0.19392991), (0.90634054, -0.3754169, -0.19392993), (0.96216565, -0.19138512, -0.19392997), (0.9810153, 0.0000015125466, -0.19392993), (0.9621651, 0.1913883, -0.19392985), (0.9063394, 0.37541983, -0.19392991), (0.8156834, 0.5450244, -0.19392993), (0.69368124, 0.693684, -0.19392991), (0.5450214, 0.8156855, -0.1939299), (0.37541652, 0.9063408, -0.1939299), (0.19138502, 0.9621657, -0.19392988), (-6.256784e-7, 0.9247656, -0.3805373), (-0.1804128, 0.9069965, -0.3805373), (-0.35389245, 0.8543721, -0.3805374), (-0.5137723, 0.7689144, -0.38053733), (-0.653908, 0.653908, -0.3805374), (-0.7689144, 0.5137723, -0.38053733), (-0.8543721, 0.3538923, -0.38053733), (-0.90699655, 0.18041252, -0.3805373), (-0.9247656, -2.6230367e-7, -0.38053727), (-0.9069965, -0.18041323, -0.38053727), (-0.85437185, -0.3538928, -0.3805373), (-0.7689143, -0.51377267, -0.3805374), (-0.65390754, -0.6539085, -0.38053733), (-0.5137717, -0.7689149, -0.3805373), (-0.3538917, -0.8543723, -0.3805373), (-0.18041196, -0.9069966, -0.38053727), (8.5910466e-7, -0.9247656, -0.38053727), (0.1804138, -0.90699625, -0.3805373), (0.35389334, -0.8543716, -0.38053727), (0.51377314, -0.76891387, -0.38053727), (0.65390885, -0.6539072, -0.38053727), (0.7689152, -0.5137713, -0.38053724), (0.85437256, -0.3538911, -0.38053718), (0.9069968, -0.18041138, -0.38053727), (0.9247656, 0.0000013668669, -0.38053727), (0.90699613, 0.1804144, -0.38053724), (0.8543714, 0.353894, -0.38053727), (0.76891357, 0.51377374, -0.3805374), (0.65390676, 0.65390927, -0.38053733), (0.5137708, 0.7689155, -0.38053733), (0.3538907, 0.8543727, -0.38053733), (0.18041132, 0.9069968, -0.38053733), (-5.275168e-7, 0.8333397, -0.5527614), (-0.16257645, 0.8173273, -0.5527613), (-0.31890523, 0.76990557, -0.5527613), (-0.46297878, 0.6928966, -0.55276126), (-0.58926016, 0.58926016, -0.55276126), (-0.6928966, 0.4629788, -0.5527613), (-0.76990557, 0.31890512, -0.5527614), (-0.8173273, 0.16257624, -0.5527613), (-0.8333397, -1.838498e-7, -0.55276126), (-0.81732726, -0.16257685, -0.55276126), (-0.7699053, -0.31890565, -0.5527613), (-0.69289637, -0.46297908, -0.5527614), (-0.5892597, -0.5892605, -0.5527613), (-0.46297827, -0.6928969, -0.5527614), (-0.31890455, -0.7699057, -0.5527613), (-0.16257578, -0.8173275, -0.5527613), (7.798596e-7, -0.8333397, -0.55276126), (0.1625774, -0.8173271, -0.5527613), (0.31890607, -0.7699051, -0.55276126), (0.46297956, -0.69289607, -0.55276144), (0.58926094, -0.5892594, -0.55276144), (0.6928972, -0.46297786, -0.5527614), (0.769906, -0.31890407, -0.5527613), (0.81732756, -0.16257523, -0.5527614), (0.83333963, 0.0000012545046, -0.5527614), (0.817327, 0.16257788, -0.5527613), (0.7699049, 0.31890664, -0.55276126), (0.69289577, 0.4629801, -0.55276126), (0.58925897, 0.5892613, -0.55276126), (0.46297738, 0.69289756, -0.5527613), (0.31890368, 0.7699061, -0.5527614), (0.16257517, 0.81732756, -0.5527614), (-4.774545e-7, 0.7101414, -0.70405906), (-0.13854167, 0.69649625, -0.7040592), (-0.2717593, 0.65608513, -0.70405906), (-0.3945335, 0.5904611, -0.70405906), (-0.5021458, 0.5021458, -0.704059), (-0.590461, 0.39453346, -0.70405906), (-0.65608513, 0.27175924, -0.70405906), (-0.69649637, 0.13854155, -0.70405906), (-0.7101415, -1.4755504e-7, -0.70405906), (-0.6964962, -0.13854203, -0.7040592), (-0.656085, -0.27175966, -0.70405906), (-0.5904608, -0.3945338, -0.70405906), (-0.5021455, -0.50214607, -0.70405906), (-0.3945331, -0.5904613, -0.70405906), (-0.2717588, -0.6560854, -0.70405906), (-0.13854109, -0.6964964, -0.704059), (6.6819644e-7, -0.7101415, -0.70405906), (0.13854247, -0.6964962, -0.704059), (0.27176014, -0.65608484, -0.70405906), (0.39453417, -0.5904605, -0.70405906), (0.5021465, -0.5021451, -0.70405906), (0.59046155, -0.39453268, -0.70405906), (0.65608555, -0.27175832, -0.70405906), (0.6964965, -0.13854067, -0.7040592), (0.7101415, 0.0000010376839, -0.70405906), (0.69649607, 0.13854288, -0.704059), (0.65608466, 0.27176052, -0.70405906), (0.59046036, 0.39453462, -0.70405906), (0.5021449, 0.50214684, -0.704059), (0.3945323, 0.59046185, -0.70405906), (0.27175796, 0.65608567, -0.70405906), (0.13854063, 0.6964965, -0.70405906), (-4.1191652e-7, 0.5597752, -0.8286446), (-0.10920663, 0.5490192, -0.8286445), (-0.21421659, 0.5171648, -0.8286445), (-0.31099445, 0.46543598, -0.8286446), (-0.3958208, 0.3958208, -0.8286446), (-0.46543607, 0.3109944, -0.8286445), (-0.5171648, 0.21421659, -0.8286446), (-0.5490192, 0.10920658, -0.8286445), (-0.5597752, -1.5566613e-7, -0.8286446), (-0.54901916, -0.109207004, -0.8286446), (-0.51716465, -0.21421692, -0.8286446), (-0.46543592, -0.31099463, -0.8286446), (-0.39582053, -0.39582106, -0.8286445), (-0.31099412, -0.46543622, -0.8286446), (-0.21421622, -0.51716495, -0.8286445), (-0.109206185, -0.54901934, -0.8286446), (5.113033e-7, -0.5597752, -0.8286445), (0.10920732, -0.5490191, -0.8286446), (0.21421728, -0.5171645, -0.8286446), (0.31099495, -0.46543565, -0.8286445), (0.3958213, -0.39582035, -0.8286446), (0.46543646, -0.31099382, -0.8286446), (0.5171651, -0.21421584, -0.8286445), (0.54901934, -0.10920586, -0.8286445), (0.55977523, 7.855152e-7, -0.8286445), (0.54901904, 0.10920763, -0.8286445), (0.5171644, 0.21421763, -0.8286446), (0.46543545, 0.3109953, -0.8286446), (0.39582005, 0.39582157, -0.8286446), (0.31099343, 0.4654366, -0.8286445), (0.21421559, 0.51716524, -0.8286445), (0.10920586, 0.5490194, -0.8286446), (-2.623535e-7, 0.38788685, -0.921707), (-0.075672925, 0.38043374, -0.921707), (-0.14843783, 0.3583608, -0.921707), (-0.2154984, 0.32251614, -0.921707), (-0.27427745, 0.27427742, -0.921707), (-0.32251617, 0.21549839, -0.921707), (-0.35836077, 0.1484378, -0.921707), (-0.38043374, 0.07567287, -0.921707), (-0.38788685, -1.3147556e-7, -0.921707), (-0.38043374, -0.07567319, -0.921707), (-0.35836068, -0.14843805, -0.921707), (-0.32251605, -0.21549855, -0.921707), (-0.27427727, -0.27427763, -0.921707), (-0.21549815, -0.32251623, -0.921707), (-0.14843756, -0.35836092, -0.921707), (-0.07567258, -0.38043383, -0.921707), (3.478126e-7, -0.38788685, -0.921707), (0.075673394, -0.3804336, -0.921707), (0.14843827, -0.35836056, -0.921707), (0.21549875, -0.3225159, -0.921707), (0.27427775, -0.2742771, -0.921707), (0.3225164, -0.21549797, -0.921707), (0.358361, -0.14843732, -0.921707), (0.38043386, -0.07567237, -0.921707), (0.3878869, 5.65345e-7, -0.921707), (0.38043362, 0.075673625, -0.92170703), (0.35836047, 0.14843854, -0.9217071), (0.32251573, 0.21549901, -0.921707), (0.27427688, 0.27427793, -0.921707), (0.21549778, 0.3225166, -0.921707), (0.14843714, 0.3583611, -0.921707), (0.07567236, 0.38043386, -0.921707), (-1.2334989e-7, 0.20096855, -0.9795976), (-0.039206997, 0.19710702, -0.9795976), (-0.07690731, 0.18567076, -0.9795977), (-0.11165216, 0.16709925, -0.9795977), (-0.1421062, 0.1421062, -0.9795977), (-0.16709925, 0.11165214, -0.9795976), (-0.18567078, 0.0769073, -0.9795976), (-0.19710703, 0.03920697, -0.9795977), (-0.2009686, -8.00954e-8, -0.9795977), (-0.19710702, -0.039207123, -0.9795977), (-0.18567075, -0.076907426, -0.9795976), (-0.16709922, -0.111652225, -0.9795976), (-0.14210616, -0.14210634, -0.9795977), (-0.11165204, -0.16709933, -0.9795976), (-0.076907165, -0.18567081, -0.9795976), (-0.03920683, -0.19710702, -0.9795976), (1.8345872e-7, -0.20096858, -0.9795976), (0.039207246, -0.19710696, -0.9795976), (0.07690752, -0.18567064, -0.9795976), (0.11165233, -0.16709912, -0.9795977), (0.1421064, -0.14210606, -0.9795977), (0.16709942, -0.11165194, -0.9795977), (0.18567084, -0.07690706, -0.9795976), (0.19710706, -0.039206725, -0.9795976), (0.20096858, 3.0994084e-7, -0.9795976), (0.19710696, 0.039207365, -0.9795977), (0.1856706, 0.076907665, -0.9795976), (0.16709906, 0.11165246, -0.9795976), (0.14210598, 0.14210652, -0.9795976), (0.111651845, 0.16709946, -0.9795976), (0.07690695, 0.1856709, -0.9795976), (0.039206695, 0.19710703, -0.9795976), (1.3451773e-8, -3.7418005e-8, -1)] (
+ interpolation = "vertex"
+ )
+ texCoord2f[] primvars:st = [(0, 1), (0.03125, 1), (0.0625, 1), (0.09375, 1), (0.125, 1), (0.15625, 1), (0.1875, 1), (0.21874999, 1), (0.24999999, 1), (0.28124997, 1), (0.31249997, 1), (0.34374997, 1), (0.375, 1), (0.40625, 1), (0.4375, 1), (0.46875003, 1), (0.50000006, 1), (0.53125006, 1), (0.56250006, 1), (0.59375006, 1), (0.62500006, 1), (0.6562501, 1), (0.6875001, 1), (0.7187501, 1), (0.7500001, 1), (0.7812501, 1), (0.8125002, 1), (0.8437502, 1), (0.8750002, 1), (0.9062502, 1), (0.9375002, 1), (0.96875024, 1), (0, 0.9375), (0.03125, 0.9375), (0.0625, 0.9375), (0.09375, 0.9375), (0.125, 0.9375), (0.15625, 0.9375), (0.1875, 0.9375), (0.21874999, 0.9375), (0.24999999, 0.9375), (0.28124997, 0.9375), (0.31249997, 0.9375), (0.34374997, 0.9375), (0.375, 0.9375), (0.40625, 0.9375), (0.4375, 0.9375), (0.46875003, 0.9375), (0.50000006, 0.9375), (0.53125006, 0.9375), (0.56250006, 0.9375), (0.59375006, 0.9375), (0.62500006, 0.9375), (0.6562501, 0.9375), (0.6875001, 0.9375), (0.7187501, 0.9375), (0.7500001, 0.9375), (0.7812501, 0.9375), (0.8125002, 0.9375), (0.8437502, 0.9375), (0.8750002, 0.9375), (0.9062502, 0.9375), (0.9375002, 0.9375), (0.96875024, 0.9375), (1.0000002, 0.9375), (0, 0.875), (0.03125, 0.875), (0.0625, 0.875), (0.09375, 0.875), (0.125, 0.875), (0.15625, 0.875), (0.1875, 0.875), (0.21874999, 0.875), (0.24999999, 0.875), (0.28124997, 0.875), (0.31249997, 0.875), (0.34374997, 0.875), (0.375, 0.875), (0.40625, 0.875), (0.4375, 0.875), (0.46875003, 0.875), (0.50000006, 0.875), (0.53125006, 0.875), (0.56250006, 0.875), (0.59375006, 0.875), (0.62500006, 0.875), (0.6562501, 0.875), (0.6875001, 0.875), (0.7187501, 0.875), (0.7500001, 0.875), (0.7812501, 0.875), (0.8125002, 0.875), (0.8437502, 0.875), (0.8750002, 0.875), (0.9062502, 0.875), (0.9375002, 0.875), (0.96875024, 0.875), (1.0000002, 0.875), (0, 0.8125), (0.03125, 0.8125), (0.0625, 0.8125), (0.09375, 0.8125), (0.125, 0.8125), (0.15625, 0.8125), (0.1875, 0.8125), (0.21874999, 0.8125), (0.24999999, 0.8125), (0.28124997, 0.8125), (0.31249997, 0.8125), (0.34374997, 0.8125), (0.375, 0.8125), (0.40625, 0.8125), (0.4375, 0.8125), (0.46875003, 0.8125), (0.50000006, 0.8125), (0.53125006, 0.8125), (0.56250006, 0.8125), (0.59375006, 0.8125), (0.62500006, 0.8125), (0.6562501, 0.8125), (0.6875001, 0.8125), (0.7187501, 0.8125), (0.7500001, 0.8125), (0.7812501, 0.8125), (0.8125002, 0.8125), (0.8437502, 0.8125), (0.8750002, 0.8125), (0.9062502, 0.8125), (0.9375002, 0.8125), (0.96875024, 0.8125), (1.0000002, 0.8125), (0, 0.75), (0.03125, 0.75), (0.0625, 0.75), (0.09375, 0.75), (0.125, 0.75), (0.15625, 0.75), (0.1875, 0.75), (0.21874999, 0.75), (0.24999999, 0.75), (0.28124997, 0.75), (0.31249997, 0.75), (0.34374997, 0.75), (0.375, 0.75), (0.40625, 0.75), (0.4375, 0.75), (0.46875003, 0.75), (0.50000006, 0.75), (0.53125006, 0.75), (0.56250006, 0.75), (0.59375006, 0.75), (0.62500006, 0.75), (0.6562501, 0.75), (0.6875001, 0.75), (0.7187501, 0.75), (0.7500001, 0.75), (0.7812501, 0.75), (0.8125002, 0.75), (0.8437502, 0.75), (0.8750002, 0.75), (0.9062502, 0.75), (0.9375002, 0.75), (0.96875024, 0.75), (1.0000002, 0.75), (0, 0.6875), (0.03125, 0.6875), (0.0625, 0.6875), (0.09375, 0.6875), (0.125, 0.6875), (0.15625, 0.6875), (0.1875, 0.6875), (0.21874999, 0.6875), (0.24999999, 0.6875), (0.28124997, 0.6875), (0.31249997, 0.6875), (0.34374997, 0.6875), (0.375, 0.6875), (0.40625, 0.6875), (0.4375, 0.6875), (0.46875003, 0.6875), (0.50000006, 0.6875), (0.53125006, 0.6875), (0.56250006, 0.6875), (0.59375006, 0.6875), (0.62500006, 0.6875), (0.6562501, 0.6875), (0.6875001, 0.6875), (0.7187501, 0.6875), (0.7500001, 0.6875), (0.7812501, 0.6875), (0.8125002, 0.6875), (0.8437502, 0.6875), (0.8750002, 0.6875), (0.9062502, 0.6875), (0.9375002, 0.6875), (0.96875024, 0.6875), (1.0000002, 0.6875), (0, 0.625), (0.03125, 0.625), (0.0625, 0.625), (0.09375, 0.625), (0.125, 0.625), (0.15625, 0.625), (0.1875, 0.625), (0.21874999, 0.625), (0.24999999, 0.625), (0.28124997, 0.625), (0.31249997, 0.625), (0.34374997, 0.625), (0.375, 0.625), (0.40625, 0.625), (0.4375, 0.625), (0.46875003, 0.625), (0.50000006, 0.625), (0.53125006, 0.625), (0.56250006, 0.625), (0.59375006, 0.625), (0.62500006, 0.625), (0.6562501, 0.625), (0.6875001, 0.625), (0.7187501, 0.625), (0.7500001, 0.625), (0.7812501, 0.625), (0.8125002, 0.625), (0.8437502, 0.625), (0.8750002, 0.625), (0.9062502, 0.625), (0.9375002, 0.625), (0.96875024, 0.625), (1.0000002, 0.625), (0, 0.5625), (0.03125, 0.5625), (0.0625, 0.5625), (0.09375, 0.5625), (0.125, 0.5625), (0.15625, 0.5625), (0.1875, 0.5625), (0.21874999, 0.5625), (0.24999999, 0.5625), (0.28124997, 0.5625), (0.31249997, 0.5625), (0.34374997, 0.5625), (0.375, 0.5625), (0.40625, 0.5625), (0.4375, 0.5625), (0.46875003, 0.5625), (0.50000006, 0.5625), (0.53125006, 0.5625), (0.56250006, 0.5625), (0.59375006, 0.5625), (0.62500006, 0.5625), (0.6562501, 0.5625), (0.6875001, 0.5625), (0.7187501, 0.5625), (0.7500001, 0.5625), (0.7812501, 0.5625), (0.8125002, 0.5625), (0.8437502, 0.5625), (0.8750002, 0.5625), (0.9062502, 0.5625), (0.9375002, 0.5625), (0.96875024, 0.5625), (1.0000002, 0.5625), (0, 0.5), (0.03125, 0.5), (0.0625, 0.5), (0.09375, 0.5), (0.125, 0.5), (0.15625, 0.5), (0.1875, 0.5), (0.21874999, 0.5), (0.24999999, 0.5), (0.28124997, 0.5), (0.31249997, 0.5), (0.34374997, 0.5), (0.375, 0.5), (0.40625, 0.5), (0.4375, 0.5), (0.46875003, 0.5), (0.50000006, 0.5), (0.53125006, 0.5), (0.56250006, 0.5), (0.59375006, 0.5), (0.62500006, 0.5), (0.6562501, 0.5), (0.6875001, 0.5), (0.7187501, 0.5), (0.7500001, 0.5), (0.7812501, 0.5), (0.8125002, 0.5), (0.8437502, 0.5), (0.8750002, 0.5), (0.9062502, 0.5), (0.9375002, 0.5), (0.96875024, 0.5), (1.0000002, 0.5), (0, 0.43750006), (0.03125, 0.43750006), (0.0625, 0.43750006), (0.09375, 0.43750006), (0.125, 0.43750006), (0.15625, 0.43750006), (0.1875, 0.43750006), (0.21874999, 0.43750006), (0.24999999, 0.43750006), (0.28124997, 0.43750006), (0.31249997, 0.43750006), (0.34374997, 0.43750006), (0.375, 0.43750006), (0.40625, 0.43750006), (0.4375, 0.43750006), (0.46875003, 0.43750006), (0.50000006, 0.43750006), (0.53125006, 0.43750006), (0.56250006, 0.43750006), (0.59375006, 0.43750006), (0.62500006, 0.43750006), (0.6562501, 0.43750006), (0.6875001, 0.43750006), (0.7187501, 0.43750006), (0.7500001, 0.43750006), (0.7812501, 0.43750006), (0.8125002, 0.43750006), (0.8437502, 0.43750006), (0.8750002, 0.43750006), (0.9062502, 0.43750006), (0.9375002, 0.43750006), (0.96875024, 0.43750006), (1.0000002, 0.43750006), (0, 0.37500006), (0.03125, 0.37500006), (0.0625, 0.37500006), (0.09375, 0.37500006), (0.125, 0.37500006), (0.15625, 0.37500006), (0.1875, 0.37500006), (0.21874999, 0.37500006), (0.24999999, 0.37500006), (0.28124997, 0.37500006), (0.31249997, 0.37500006), (0.34374997, 0.37500006), (0.375, 0.37500006), (0.40625, 0.37500006), (0.4375, 0.37500006), (0.46875003, 0.37500006), (0.50000006, 0.37500006), (0.53125006, 0.37500006), (0.56250006, 0.37500006), (0.59375006, 0.37500006), (0.62500006, 0.37500006), (0.6562501, 0.37500006), (0.6875001, 0.37500006), (0.7187501, 0.37500006), (0.7500001, 0.37500006), (0.7812501, 0.37500006), (0.8125002, 0.37500006), (0.8437502, 0.37500006), (0.8750002, 0.37500006), (0.9062502, 0.37500006), (0.9375002, 0.37500006), (0.96875024, 0.37500006), (1.0000002, 0.37500006), (0, 0.31250006), (0.03125, 0.31250006), (0.0625, 0.31250006), (0.09375, 0.31250006), (0.125, 0.31250006), (0.15625, 0.31250006), (0.1875, 0.31250006), (0.21874999, 0.31250006), (0.24999999, 0.31250006), (0.28124997, 0.31250006), (0.31249997, 0.31250006), (0.34374997, 0.31250006), (0.375, 0.31250006), (0.40625, 0.31250006), (0.4375, 0.31250006), (0.46875003, 0.31250006), (0.50000006, 0.31250006), (0.53125006, 0.31250006), (0.56250006, 0.31250006), (0.59375006, 0.31250006), (0.62500006, 0.31250006), (0.6562501, 0.31250006), (0.6875001, 0.31250006), (0.7187501, 0.31250006), (0.7500001, 0.31250006), (0.7812501, 0.31250006), (0.8125002, 0.31250006), (0.8437502, 0.31250006), (0.8750002, 0.31250006), (0.9062502, 0.31250006), (0.9375002, 0.31250006), (0.96875024, 0.31250006), (1.0000002, 0.31250006), (0, 0.25), (0.03125, 0.25), (0.0625, 0.25), (0.09375, 0.25), (0.125, 0.25), (0.15625, 0.25), (0.1875, 0.25), (0.21874999, 0.25), (0.24999999, 0.25), (0.28124997, 0.25), (0.31249997, 0.25), (0.34374997, 0.25), (0.375, 0.25), (0.40625, 0.25), (0.4375, 0.25), (0.46875003, 0.25), (0.50000006, 0.25), (0.53125006, 0.25), (0.56250006, 0.25), (0.59375006, 0.25), (0.62500006, 0.25), (0.6562501, 0.25), (0.6875001, 0.25), (0.7187501, 0.25), (0.7500001, 0.25), (0.7812501, 0.25), (0.8125002, 0.25), (0.8437502, 0.25), (0.8750002, 0.25), (0.9062502, 0.25), (0.9375002, 0.25), (0.96875024, 0.25), (1.0000002, 0.25), (0, 0.1875), (0.03125, 0.1875), (0.0625, 0.1875), (0.09375, 0.1875), (0.125, 0.1875), (0.15625, 0.1875), (0.1875, 0.1875), (0.21874999, 0.1875), (0.24999999, 0.1875), (0.28124997, 0.1875), (0.31249997, 0.1875), (0.34374997, 0.1875), (0.375, 0.1875), (0.40625, 0.1875), (0.4375, 0.1875), (0.46875003, 0.1875), (0.50000006, 0.1875), (0.53125006, 0.1875), (0.56250006, 0.1875), (0.59375006, 0.1875), (0.62500006, 0.1875), (0.6562501, 0.1875), (0.6875001, 0.1875), (0.7187501, 0.1875), (0.7500001, 0.1875), (0.7812501, 0.1875), (0.8125002, 0.1875), (0.8437502, 0.1875), (0.8750002, 0.1875), (0.9062502, 0.1875), (0.9375002, 0.1875), (0.96875024, 0.1875), (1.0000002, 0.1875), (0, 0.125), (0.03125, 0.125), (0.0625, 0.125), (0.09375, 0.125), (0.125, 0.125), (0.15625, 0.125), (0.1875, 0.125), (0.21874999, 0.125), (0.24999999, 0.125), (0.28124997, 0.125), (0.31249997, 0.125), (0.34374997, 0.125), (0.375, 0.125), (0.40625, 0.125), (0.4375, 0.125), (0.46875003, 0.125), (0.50000006, 0.125), (0.53125006, 0.125), (0.56250006, 0.125), (0.59375006, 0.125), (0.62500006, 0.125), (0.6562501, 0.125), (0.6875001, 0.125), (0.7187501, 0.125), (0.7500001, 0.125), (0.7812501, 0.125), (0.8125002, 0.125), (0.8437502, 0.125), (0.8750002, 0.125), (0.9062502, 0.125), (0.9375002, 0.125), (0.96875024, 0.125), (1.0000002, 0.125), (0, 0.06249994), (0.03125, 0.06249994), (0.0625, 0.06249994), (0.09375, 0.06249994), (0.125, 0.06249994), (0.15625, 0.06249994), (0.1875, 0.06249994), (0.21874999, 0.06249994), (0.24999999, 0.06249994), (0.28124997, 0.06249994), (0.31249997, 0.06249994), (0.34374997, 0.06249994), (0.375, 0.06249994), (0.40625, 0.06249994), (0.4375, 0.06249994), (0.46875003, 0.06249994), (0.50000006, 0.06249994), (0.53125006, 0.06249994), (0.56250006, 0.06249994), (0.59375006, 0.06249994), (0.62500006, 0.06249994), (0.6562501, 0.06249994), (0.6875001, 0.06249994), (0.7187501, 0.06249994), (0.7500001, 0.06249994), (0.7812501, 0.06249994), (0.8125002, 0.06249994), (0.8437502, 0.06249994), (0.8750002, 0.06249994), (0.9062502, 0.06249994), (0.9375002, 0.06249994), (0.96875024, 0.06249994), (1.0000002, 0.06249994), (0, -1.1920929e-7), (0.03125, -1.1920929e-7), (0.0625, -1.1920929e-7), (0.09375, -1.1920929e-7), (0.125, -1.1920929e-7), (0.15625, -1.1920929e-7), (0.1875, -1.1920929e-7), (0.21874999, -1.1920929e-7), (0.24999999, -1.1920929e-7), (0.28124997, -1.1920929e-7), (0.31249997, -1.1920929e-7), (0.34374997, -1.1920929e-7), (0.375, -1.1920929e-7), (0.40625, -1.1920929e-7), (0.4375, -1.1920929e-7), (0.46875003, -1.1920929e-7), (0.50000006, -1.1920929e-7), (0.53125006, -1.1920929e-7), (0.56250006, -1.1920929e-7), (0.59375006, -1.1920929e-7), (0.62500006, -1.1920929e-7), (0.6562501, -1.1920929e-7), (0.6875001, -1.1920929e-7), (0.7187501, -1.1920929e-7), (0.7500001, -1.1920929e-7), (0.7812501, -1.1920929e-7), (0.8125002, -1.1920929e-7), (0.8437502, -1.1920929e-7), (0.8750002, -1.1920929e-7), (0.9062502, -1.1920929e-7), (0.9375002, -1.1920929e-7), (0.96875024, -1.1920929e-7)] (
+ interpolation = "faceVarying"
+ )
+ int[] primvars:st:indices = [0, 32, 33, 1, 33, 34, 2, 34, 35, 3, 35, 36, 4, 36, 37, 5, 37, 38, 6, 38, 39, 7, 39, 40, 8, 40, 41, 9, 41, 42, 10, 42, 43, 11, 43, 44, 12, 44, 45, 13, 45, 46, 14, 46, 47, 15, 47, 48, 16, 48, 49, 17, 49, 50, 18, 50, 51, 19, 51, 52, 20, 52, 53, 21, 53, 54, 22, 54, 55, 23, 55, 56, 24, 56, 57, 25, 57, 58, 26, 58, 59, 27, 59, 60, 28, 60, 61, 29, 61, 62, 30, 62, 63, 31, 63, 64, 32, 65, 66, 33, 33, 66, 67, 34, 34, 67, 68, 35, 35, 68, 69, 36, 36, 69, 70, 37, 37, 70, 71, 38, 38, 71, 72, 39, 39, 72, 73, 40, 40, 73, 74, 41, 41, 74, 75, 42, 42, 75, 76, 43, 43, 76, 77, 44, 44, 77, 78, 45, 45, 78, 79, 46, 46, 79, 80, 47, 47, 80, 81, 48, 48, 81, 82, 49, 49, 82, 83, 50, 50, 83, 84, 51, 51, 84, 85, 52, 52, 85, 86, 53, 53, 86, 87, 54, 54, 87, 88, 55, 55, 88, 89, 56, 56, 89, 90, 57, 57, 90, 91, 58, 58, 91, 92, 59, 59, 92, 93, 60, 60, 93, 94, 61, 61, 94, 95, 62, 62, 95, 96, 63, 63, 96, 97, 64, 65, 98, 99, 66, 66, 99, 100, 67, 67, 100, 101, 68, 68, 101, 102, 69, 69, 102, 103, 70, 70, 103, 104, 71, 71, 104, 105, 72, 72, 105, 106, 73, 73, 106, 107, 74, 74, 107, 108, 75, 75, 108, 109, 76, 76, 109, 110, 77, 77, 110, 111, 78, 78, 111, 112, 79, 79, 112, 113, 80, 80, 113, 114, 81, 81, 114, 115, 82, 82, 115, 116, 83, 83, 116, 117, 84, 84, 117, 118, 85, 85, 118, 119, 86, 86, 119, 120, 87, 87, 120, 121, 88, 88, 121, 122, 89, 89, 122, 123, 90, 90, 123, 124, 91, 91, 124, 125, 92, 92, 125, 126, 93, 93, 126, 127, 94, 94, 127, 128, 95, 95, 128, 129, 96, 96, 129, 130, 97, 98, 131, 132, 99, 99, 132, 133, 100, 100, 133, 134, 101, 101, 134, 135, 102, 102, 135, 136, 103, 103, 136, 137, 104, 104, 137, 138, 105, 105, 138, 139, 106, 106, 139, 140, 107, 107, 140, 141, 108, 108, 141, 142, 109, 109, 142, 143, 110, 110, 143, 144, 111, 111, 144, 145, 112, 112, 145, 146, 113, 113, 146, 147, 114, 114, 147, 148, 115, 115, 148, 149, 116, 116, 149, 150, 117, 117, 150, 151, 118, 118, 151, 152, 119, 119, 152, 153, 120, 120, 153, 154, 121, 121, 154, 155, 122, 122, 155, 156, 123, 123, 156, 157, 124, 124, 157, 158, 125, 125, 158, 159, 126, 126, 159, 160, 127, 127, 160, 161, 128, 128, 161, 162, 129, 129, 162, 163, 130, 131, 164, 165, 132, 132, 165, 166, 133, 133, 166, 167, 134, 134, 167, 168, 135, 135, 168, 169, 136, 136, 169, 170, 137, 137, 170, 171, 138, 138, 171, 172, 139, 139, 172, 173, 140, 140, 173, 174, 141, 141, 174, 175, 142, 142, 175, 176, 143, 143, 176, 177, 144, 144, 177, 178, 145, 145, 178, 179, 146, 146, 179, 180, 147, 147, 180, 181, 148, 148, 181, 182, 149, 149, 182, 183, 150, 150, 183, 184, 151, 151, 184, 185, 152, 152, 185, 186, 153, 153, 186, 187, 154, 154, 187, 188, 155, 155, 188, 189, 156, 156, 189, 190, 157, 157, 190, 191, 158, 158, 191, 192, 159, 159, 192, 193, 160, 160, 193, 194, 161, 161, 194, 195, 162, 162, 195, 196, 163, 164, 197, 198, 165, 165, 198, 199, 166, 166, 199, 200, 167, 167, 200, 201, 168, 168, 201, 202, 169, 169, 202, 203, 170, 170, 203, 204, 171, 171, 204, 205, 172, 172, 205, 206, 173, 173, 206, 207, 174, 174, 207, 208, 175, 175, 208, 209, 176, 176, 209, 210, 177, 177, 210, 211, 178, 178, 211, 212, 179, 179, 212, 213, 180, 180, 213, 214, 181, 181, 214, 215, 182, 182, 215, 216, 183, 183, 216, 217, 184, 184, 217, 218, 185, 185, 218, 219, 186, 186, 219, 220, 187, 187, 220, 221, 188, 188, 221, 222, 189, 189, 222, 223, 190, 190, 223, 224, 191, 191, 224, 225, 192, 192, 225, 226, 193, 193, 226, 227, 194, 194, 227, 228, 195, 195, 228, 229, 196, 197, 230, 231, 198, 198, 231, 232, 199, 199, 232, 233, 200, 200, 233, 234, 201, 201, 234, 235, 202, 202, 235, 236, 203, 203, 236, 237, 204, 204, 237, 238, 205, 205, 238, 239, 206, 206, 239, 240, 207, 207, 240, 241, 208, 208, 241, 242, 209, 209, 242, 243, 210, 210, 243, 244, 211, 211, 244, 245, 212, 212, 245, 246, 213, 213, 246, 247, 214, 214, 247, 248, 215, 215, 248, 249, 216, 216, 249, 250, 217, 217, 250, 251, 218, 218, 251, 252, 219, 219, 252, 253, 220, 220, 253, 254, 221, 221, 254, 255, 222, 222, 255, 256, 223, 223, 256, 257, 224, 224, 257, 258, 225, 225, 258, 259, 226, 226, 259, 260, 227, 227, 260, 261, 228, 228, 261, 262, 229, 230, 263, 264, 231, 231, 264, 265, 232, 232, 265, 266, 233, 233, 266, 267, 234, 234, 267, 268, 235, 235, 268, 269, 236, 236, 269, 270, 237, 237, 270, 271, 238, 238, 271, 272, 239, 239, 272, 273, 240, 240, 273, 274, 241, 241, 274, 275, 242, 242, 275, 276, 243, 243, 276, 277, 244, 244, 277, 278, 245, 245, 278, 279, 246, 246, 279, 280, 247, 247, 280, 281, 248, 248, 281, 282, 249, 249, 282, 283, 250, 250, 283, 284, 251, 251, 284, 285, 252, 252, 285, 286, 253, 253, 286, 287, 254, 254, 287, 288, 255, 255, 288, 289, 256, 256, 289, 290, 257, 257, 290, 291, 258, 258, 291, 292, 259, 259, 292, 293, 260, 260, 293, 294, 261, 261, 294, 295, 262, 263, 296, 297, 264, 264, 297, 298, 265, 265, 298, 299, 266, 266, 299, 300, 267, 267, 300, 301, 268, 268, 301, 302, 269, 269, 302, 303, 270, 270, 303, 304, 271, 271, 304, 305, 272, 272, 305, 306, 273, 273, 306, 307, 274, 274, 307, 308, 275, 275, 308, 309, 276, 276, 309, 310, 277, 277, 310, 311, 278, 278, 311, 312, 279, 279, 312, 313, 280, 280, 313, 314, 281, 281, 314, 315, 282, 282, 315, 316, 283, 283, 316, 317, 284, 284, 317, 318, 285, 285, 318, 319, 286, 286, 319, 320, 287, 287, 320, 321, 288, 288, 321, 322, 289, 289, 322, 323, 290, 290, 323, 324, 291, 291, 324, 325, 292, 292, 325, 326, 293, 293, 326, 327, 294, 294, 327, 328, 295, 296, 329, 330, 297, 297, 330, 331, 298, 298, 331, 332, 299, 299, 332, 333, 300, 300, 333, 334, 301, 301, 334, 335, 302, 302, 335, 336, 303, 303, 336, 337, 304, 304, 337, 338, 305, 305, 338, 339, 306, 306, 339, 340, 307, 307, 340, 341, 308, 308, 341, 342, 309, 309, 342, 343, 310, 310, 343, 344, 311, 311, 344, 345, 312, 312, 345, 346, 313, 313, 346, 347, 314, 314, 347, 348, 315, 315, 348, 349, 316, 316, 349, 350, 317, 317, 350, 351, 318, 318, 351, 352, 319, 319, 352, 353, 320, 320, 353, 354, 321, 321, 354, 355, 322, 322, 355, 356, 323, 323, 356, 357, 324, 324, 357, 358, 325, 325, 358, 359, 326, 326, 359, 360, 327, 327, 360, 361, 328, 329, 362, 363, 330, 330, 363, 364, 331, 331, 364, 365, 332, 332, 365, 366, 333, 333, 366, 367, 334, 334, 367, 368, 335, 335, 368, 369, 336, 336, 369, 370, 337, 337, 370, 371, 338, 338, 371, 372, 339, 339, 372, 373, 340, 340, 373, 374, 341, 341, 374, 375, 342, 342, 375, 376, 343, 343, 376, 377, 344, 344, 377, 378, 345, 345, 378, 379, 346, 346, 379, 380, 347, 347, 380, 381, 348, 348, 381, 382, 349, 349, 382, 383, 350, 350, 383, 384, 351, 351, 384, 385, 352, 352, 385, 386, 353, 353, 386, 387, 354, 354, 387, 388, 355, 355, 388, 389, 356, 356, 389, 390, 357, 357, 390, 391, 358, 358, 391, 392, 359, 359, 392, 393, 360, 360, 393, 394, 361, 362, 395, 396, 363, 363, 396, 397, 364, 364, 397, 398, 365, 365, 398, 399, 366, 366, 399, 400, 367, 367, 400, 401, 368, 368, 401, 402, 369, 369, 402, 403, 370, 370, 403, 404, 371, 371, 404, 405, 372, 372, 405, 406, 373, 373, 406, 407, 374, 374, 407, 408, 375, 375, 408, 409, 376, 376, 409, 410, 377, 377, 410, 411, 378, 378, 411, 412, 379, 379, 412, 413, 380, 380, 413, 414, 381, 381, 414, 415, 382, 382, 415, 416, 383, 383, 416, 417, 384, 384, 417, 418, 385, 385, 418, 419, 386, 386, 419, 420, 387, 387, 420, 421, 388, 388, 421, 422, 389, 389, 422, 423, 390, 390, 423, 424, 391, 391, 424, 425, 392, 392, 425, 426, 393, 393, 426, 427, 394, 395, 428, 429, 396, 396, 429, 430, 397, 397, 430, 431, 398, 398, 431, 432, 399, 399, 432, 433, 400, 400, 433, 434, 401, 401, 434, 435, 402, 402, 435, 436, 403, 403, 436, 437, 404, 404, 437, 438, 405, 405, 438, 439, 406, 406, 439, 440, 407, 407, 440, 441, 408, 408, 441, 442, 409, 409, 442, 443, 410, 410, 443, 444, 411, 411, 444, 445, 412, 412, 445, 446, 413, 413, 446, 447, 414, 414, 447, 448, 415, 415, 448, 449, 416, 416, 449, 450, 417, 417, 450, 451, 418, 418, 451, 452, 419, 419, 452, 453, 420, 420, 453, 454, 421, 421, 454, 455, 422, 422, 455, 456, 423, 423, 456, 457, 424, 424, 457, 458, 425, 425, 458, 459, 426, 426, 459, 460, 427, 428, 461, 462, 429, 429, 462, 463, 430, 430, 463, 464, 431, 431, 464, 465, 432, 432, 465, 466, 433, 433, 466, 467, 434, 434, 467, 468, 435, 435, 468, 469, 436, 436, 469, 470, 437, 437, 470, 471, 438, 438, 471, 472, 439, 439, 472, 473, 440, 440, 473, 474, 441, 441, 474, 475, 442, 442, 475, 476, 443, 443, 476, 477, 444, 444, 477, 478, 445, 445, 478, 479, 446, 446, 479, 480, 447, 447, 480, 481, 448, 448, 481, 482, 449, 449, 482, 483, 450, 450, 483, 484, 451, 451, 484, 485, 452, 452, 485, 486, 453, 453, 486, 487, 454, 454, 487, 488, 455, 455, 488, 489, 456, 456, 489, 490, 457, 457, 490, 491, 458, 458, 491, 492, 459, 459, 492, 493, 460, 461, 494, 495, 462, 462, 495, 496, 463, 463, 496, 497, 464, 464, 497, 498, 465, 465, 498, 499, 466, 466, 499, 500, 467, 467, 500, 501, 468, 468, 501, 502, 469, 469, 502, 503, 470, 470, 503, 504, 471, 471, 504, 505, 472, 472, 505, 506, 473, 473, 506, 507, 474, 474, 507, 508, 475, 475, 508, 509, 476, 476, 509, 510, 477, 477, 510, 511, 478, 478, 511, 512, 479, 479, 512, 513, 480, 480, 513, 514, 481, 481, 514, 515, 482, 482, 515, 516, 483, 483, 516, 517, 484, 484, 517, 518, 485, 485, 518, 519, 486, 486, 519, 520, 487, 487, 520, 521, 488, 488, 521, 522, 489, 489, 522, 523, 490, 490, 523, 524, 491, 491, 524, 525, 492, 492, 525, 526, 493, 527, 495, 494, 528, 496, 495, 529, 497, 496, 530, 498, 497, 531, 499, 498, 532, 500, 499, 533, 501, 500, 534, 502, 501, 535, 503, 502, 536, 504, 503, 537, 505, 504, 538, 506, 505, 539, 507, 506, 540, 508, 507, 541, 509, 508, 542, 510, 509, 543, 511, 510, 544, 512, 511, 545, 513, 512, 546, 514, 513, 547, 515, 514, 548, 516, 515, 549, 517, 516, 550, 518, 517, 551, 519, 518, 552, 520, 519, 553, 521, 520, 554, 522, 521, 555, 523, 522, 556, 524, 523, 557, 525, 524, 558, 526, 525]
+ uniform token subdivisionScheme = "none"
+ matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (-27.6058, 8.03715, 0, 1) )
+ uniform token[] xformOpOrder = ["xformOp:transform"]
+ }
+
+ def "mtl"
+ {
+ def Material "Material__24"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Material__24"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.23921569, 0.23529412, 0.45490196)
+ token outputs:surface
+ }
+ }
+ }
+}
+
diff --git a/samples/UserDataImportChaserSample/UserDataImportChaser.cpp b/samples/UserDataImportChaserSample/UserDataImportChaser.cpp
new file mode 100644
index 0000000..917c8c2
--- /dev/null
+++ b/samples/UserDataImportChaserSample/UserDataImportChaser.cpp
@@ -0,0 +1,284 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "CAMetaDataUtils.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+enum UserDataImportChaserCodes
+{
+ UnableToAddMetadata,
+ UnavailableParamBlock
+};
+TF_REGISTRY_FUNCTION(TfEnum)
+{
+ TF_ADD_ENUM_NAME(UnableToAddMetadata, "Unable to add USD Metadata to object.");
+ TF_ADD_ENUM_NAME(
+ UnavailableParamBlock,
+ "Unable to get ParamBlock2 for IMetaDataManager/CustAttrib for object.");
+};
+
+enum class PropertyType : int
+{
+ USER_PROP = 1, // "user"
+ CUSTOM_DATA = 2 // "custom"
+};
+
+// The UserDataImportChaserSample class
+// This Import Chaser sample reads customData from the imported prims and adds
+// user defined properties or custom attributes to the corresponding node
+// depending on the arguments. The sample chaser acts with default data or using
+// the arguments that may have been passed to the chaser.
+class UserDataImportChaserSample : public pxr::MaxUsdImportChaser
+{
+public:
+ // Constructor - can be customized for the Import Chaser requirements.
+ UserDataImportChaserSample(
+ Usd_PrimFlagsPredicate& returnPredicate,
+ const MaxUsdReadJobContext& context,
+ const fs::path& filename);
+
+ // Processing that needs to run after the main 3ds Max USD import loop.
+ bool PostImport() override;
+
+private:
+ const MaxUsdReadJobContext context;
+ const fs::path& filename;
+
+ typedef std::map> PropertyMap;
+ PropertyMap dataToImport;
+};
+
+UserDataImportChaserSample::UserDataImportChaserSample(
+ Usd_PrimFlagsPredicate& returnPredicate,
+ const MaxUsdReadJobContext& context,
+ const fs::path& filename)
+ : context(context)
+ , filename(filename)
+{
+ // Default configuration
+ dataToImport
+ = { { PropertyType::USER_PROP, { "myUserProperty" } }, { PropertyType::CUSTOM_DATA, {} } };
+
+ // Parsing the specific import chaser arguments
+ boost::char_separator sep(",");
+
+ for (const auto& args : context.GetArgs().GetAllChaserArgs()) {
+ for (const auto& item : args.second) {
+ if (item.first == "user") {
+ dataToImport[PropertyType::USER_PROP].clear();
+ boost::tokenizer> tokens(item.second, sep);
+ for (const auto& name : tokens) {
+ dataToImport[PropertyType::USER_PROP].insert(name);
+ }
+ } else if (item.first == "custom") {
+ dataToImport[PropertyType::CUSTOM_DATA].clear();
+ boost::tokenizer> tokens(item.second, sep);
+ for (const auto& name : tokens) {
+ dataToImport[PropertyType::CUSTOM_DATA].insert(name);
+ }
+ } else {
+ TF_WARN(
+ "Wrong user data type ('%s') passed as argument to UserPropertyImportChaser",
+ item.first.c_str());
+ }
+ }
+ }
+}
+
+bool UserDataImportChaserSample::PostImport()
+{
+ // Makes a prims to nodes map
+ std::map primsToNodes;
+ {
+ const auto& nodeMap = context.GetReferenceTargetRegistry();
+ for (const std::pair& item : nodeMap) {
+ INode* node = dynamic_cast(item.second);
+ if (node) {
+ primsToNodes.insert({ item.first.GetString(), node });
+ }
+ }
+ }
+
+ // Go through the imported nodes / prims
+ for (const auto& imported : primsToNodes) {
+ pxr::UsdPrim prim = context.GetStage()->GetPrimAtPath(pxr::SdfPath(imported.first));
+ INode* node = imported.second;
+
+ // User properties
+ // Find and add the specified user property to the node as a user defined property.
+ for (const auto& propName : dataToImport[PropertyType::USER_PROP]) {
+ VtValue customData = prim.GetCustomDataByKey(pxr::TfToken(propName));
+ if (!customData.IsEmpty()) {
+ if (customData.GetTypeName() == "bool") {
+ node->SetUserPropBool(
+ MaxUsd::UsdStringToMaxString(propName).data(), customData.Get());
+ } else if (customData.GetTypeName() == "int") {
+ node->SetUserPropInt(
+ MaxUsd::UsdStringToMaxString(propName).data(), customData.Get());
+ } else if (customData.GetTypeName() == "float") {
+ node->SetUserPropFloat(
+ MaxUsd::UsdStringToMaxString(propName).data(), customData.Get());
+ } else if (customData.GetTypeName() == "double") {
+ node->SetUserPropFloat(
+ MaxUsd::UsdStringToMaxString(propName).data(),
+ static_cast(customData.Get()));
+ } else {
+ node->SetUserPropString(
+ MaxUsd::UsdStringToMaxString(propName).data(),
+ MaxUsd::UsdStringToMaxString(customData.Get()));
+ }
+ }
+ }
+
+ // Custom Attributes
+ IMetaDataManager* maxMetaDataManager = IMetaDataManager::GetInstance();
+
+ // Contains the usd metadata names for defining our CustomAttributes and also the value to
+ // set for each parameter
+ std::vector>
+ customAttributesToAddAndUpdate;
+
+ // Finds and adds the custom data property to the node as a custom attribute
+ for (const auto& propName : dataToImport[PropertyType::CUSTOM_DATA]) {
+ VtValue customData = prim.GetCustomDataByKey(pxr::TfToken(propName));
+ if (!customData.IsEmpty()) {
+ caMetaData::ParameterValue val;
+ val.key = MaxUsd::UsdStringToMaxString(propName).data();
+
+ if (customData.GetTypeName() == "bool") {
+ val.boolValue = customData.Get();
+ customAttributesToAddAndUpdate.push_back(
+ std::make_pair(caMetaData::UsdMetaDataTypeCA::CA_BOOL, val));
+ } else if (customData.GetTypeName() == "int") {
+ val.intValue = customData.Get();
+ customAttributesToAddAndUpdate.push_back(
+ std::make_pair(caMetaData::UsdMetaDataTypeCA::CA_INT, val));
+ } else {
+ val.strValue
+ = MaxUsd::UsdStringToMaxString(customData.Get()).data();
+ customAttributesToAddAndUpdate.push_back(
+ std::make_pair(caMetaData::UsdMetaDataTypeCA::CA_STR, val));
+ }
+ }
+ }
+
+ if (!customAttributesToAddAndUpdate.empty()) {
+ IMetaDataManager::MetaDataID usdBuiltInMetaData
+ = caMetaData::getOrDefineCABuiltInMetaData(customAttributesToAddAndUpdate);
+
+ if (usdBuiltInMetaData != EmptyMetaDataID) {
+ CustAttrib* usdCustomAttribute = maxMetaDataManager->AddMetaDataToAnimatable(
+ usdBuiltInMetaData, *(node->GetObjectRef()));
+ if (usdCustomAttribute == nullptr) {
+ TF_ERROR(
+ UnableToAddMetadata,
+ "object '%s'",
+ MaxUsd::MaxStringToUsdString(node->GetObjectRef()->GetObjectName(false))
+ .c_str());
+ return false;
+ }
+
+ IParamBlock2* usdCustomAttributePb = usdCustomAttribute->GetParamBlock(0);
+ if (usdCustomAttributePb == nullptr) {
+ TF_ERROR(
+ UnavailableParamBlock,
+ "object `%s",
+ MaxUsd::MaxStringToUsdString(node->GetObjectRef()->GetObjectName(false))
+ .c_str());
+ return false;
+ }
+
+ for (const auto& prop : customAttributesToAddAndUpdate) {
+ const auto& def = getCAMetaDataDef(prop.first, prop.second.key);
+ auto val = prop.second;
+ const auto stage = context.GetStage();
+ const auto timeConfig = context.GetArgs().GetResolvedTimeConfig(stage);
+ const auto startTime = MaxUsd::GetMaxTimeValueFromUsdTimeCode(
+ stage, timeConfig.GetStartTimeCode());
+ switch (def.usdMetaDataParamDef.m_dataType) {
+ case TYPE_STRING:
+ usdCustomAttributePb->SetValueByName(
+ def.usdMetaDataKey, val.strValue.c_str(), startTime);
+ break;
+ case TYPE_BOOL:
+ usdCustomAttributePb->SetValueByName(
+ def.usdMetaDataKey, val.boolValue, startTime);
+ break;
+ case TYPE_INT:
+ usdCustomAttributePb->SetValueByName(
+ def.usdMetaDataKey, val.intValue, startTime);
+ break;
+ default:
+ // Unhandled type that was not implemented
+ DbgAssert(_T("Unhandled customattribute data type"));
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+// clang-format off
+REGISTER_IMPORT_JOB_CONTEXT_FCT(CustomImportContext, "Custom C++ Import Context", "Custom import plug-in configuration")
+{
+ // The following arguments are based on the variables contained in the
+ // custom data of the prims found in: ./SceneFiles/UserDataChaserSample.usda
+ VtDictionary extraArgs;
+ extraArgs[MaxUsdSceneBuilderOptionsTokens->chaserNames] = VtValue(std::vector { VtValue(std::string("UserDataImport")) } );
+ VtValue chaserArgUserProp(std::vector {
+ VtValue(std::string("UserDataImport")),
+ VtValue(std::string("user")),
+ VtValue(std::string("myUserFloatProperty,myUserProperty")) });
+ VtValue chaserArgCustomProp(std::vector {
+ VtValue(std::string("UserDataImport")),
+ VtValue(std::string("custom")),
+ VtValue(std::string("inGame,strVal")) });
+ extraArgs[MaxUsdSceneBuilderOptionsTokens->chaserArgs] = VtValue(std::vector{ chaserArgUserProp, chaserArgCustomProp });
+
+ return extraArgs;
+}
+
+
+// Macro to register the Import Chaser. Defines a factory method for the chaser name. The 'ctx' will
+// be of type MaxUsdImportChaserRegistry::FactoryContext. The method should return a MaxUsdImportChaser*.
+// There are no guarantees about the lifetime of 'ctx'.
+// It is also very important to set the project option "Remove unreferenced code and data" to NO,
+// not doing so could cause the Macro to be optimized out and the Import Chaser to never be properly registered.
+PXR_MAXUSD_DEFINE_IMPORT_CHASER_FACTORY(UserDataImport, "Import Chaser C++ DEMO", "Custom plug-in configuration", ctx)
+{
+ return new UserDataImportChaserSample(
+ *(new Usd_PrimFlagsPredicate()),
+ ctx.GetContext(),
+ ctx.GetFilename()
+ );
+}
+// clang-format on
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/UserDataImportChaser.filters b/samples/UserDataImportChaserSample/UserDataImportChaser.filters
new file mode 100644
index 0000000..5c53b4a
--- /dev/null
+++ b/samples/UserDataImportChaserSample/UserDataImportChaser.filters
@@ -0,0 +1,51 @@
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Package Files
+
+
+ Package Files
+
+
+
+
+ Source Files\python
+
+
+ Source Files\python
+
+
+
+
+ {736bd8fe-eeb6-4c2b-a0ab-dc1037725910}
+
+
+ {c3509517-bd52-40a7-ab6f-a09227ec306b}
+
+
+ {05cf4688-3af7-4d00-80d5-993a79604ba2}
+
+
+ {a6749ed4-ce14-4a10-9eeb-afdaff80a895}
+
+
+
+
+ Package Files
+
+
+
+
+ Header Files
+
+
+
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/UserDataImportChaser.vcxproj b/samples/UserDataImportChaserSample/UserDataImportChaser.vcxproj
new file mode 100644
index 0000000..5de245a
--- /dev/null
+++ b/samples/UserDataImportChaserSample/UserDataImportChaser.vcxproj
@@ -0,0 +1,85 @@
+
+
+
+
+ Hybrid
+ x64
+
+
+ Release
+ x64
+
+
+
+ UserDataImportChaserPlugin
+ 16.0
+ Win32Proj
+ {CAE8A112-333A-4CB5-81DD-9F9503D004A2}
+ DynamicLibrary
+ Unicode
+ 10.0.19041.0
+
+
+
+
+
+
+
+ <_ProjectFileVersion>10.0.30319.1
+ UserDataImportChaser
+ .dll
+ $(ContentsDir)\Contents\
+
+
+
+ geom.lib;shlwapi.lib;paramblk2.lib;maxscrpt.lib;%(AdditionalDependencies)
+ $(LibDir)\$(TargetName).lib
+
+
+ false
+ ../silence_usd_warnings.h
+
+
+ @echo Do not forget to update "$(OutDir)RegisterPlugin.ms" in order to load the C++ version of the plugin.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ python\%(Filename)%(Extension)
+
+
+ Document
+ PreserveNewest
+ ..\%(Filename)%(Extension)
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/UserDataImportChaserSample/UserDataImportChaser.vcxproj.filters b/samples/UserDataImportChaserSample/UserDataImportChaser.vcxproj.filters
new file mode 100644
index 0000000..17ca853
--- /dev/null
+++ b/samples/UserDataImportChaserSample/UserDataImportChaser.vcxproj.filters
@@ -0,0 +1,55 @@
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ {7d196c1f-86cb-4a3f-8bc2-e2788d383984}
+
+
+ {4fb15ec8-cc4f-4f93-b920-33d3011bb075}
+
+
+ {89532428-a715-4c03-8226-84255c57f907}
+
+
+ {63c81035-1c90-4ae6-aff4-c3fb4989b183}
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Source Files\python
+
+
+ Source Files\python
+
+
+ Source Files\Contents
+
+
+ Source Files\Contents
+
+
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/samples/readme.md b/samples/readme.md
new file mode 100644
index 0000000..e7ccf7c
--- /dev/null
+++ b/samples/readme.md
@@ -0,0 +1,109 @@
+# Samples
+
+The sample projects illustrate how to implement the available APIs. They are constructed to be easily added individually as application plugins to 3ds Max. Remember however that those extensions are not 3ds Max plugins but are plugins to the 3ds Max USD plugin, and are registered in a different way.
+
+The samples are provided as a starting point to help you understand the basic elements to put in place in C++ as well as in Python. They are basic examples of possible extensions a user might want to put in place and are not meant to be full implementation of the feature they are providing.
+
+The Python version of the samples are functional directly from the provided devkit package. You simply need to add the path of the samples root folder or the specific sample you wish to use to the `ADSK_APPLICATION_PLUGINS` environment variable.
+
+The C++ version of the plugins need to be compiled using the same requirements as for 3ds Max plugins. As of today, we are providing all component dependencies along with the devkit package. You will need to edit the `ADSK_APPLICATION_PLUGINS` environment variable to add the `build\samples\bin\x64\\` that got generated at compile time. Also, the sample plugins are configured to be running their Python version by default. Do not forget to edit their `RegisterPlugin.ms` MAXScript post-launch script to enable the C++ section and comment out the Python section of the script.
+
+- **glTFMaterialWriterPlugin** - an example of how to implement a `ShaderWriter` that exports glTFMaterial to UsdPreviewSurface. This is a minimal, non-exhaustive example.
+- **SpherePrimWriterPlugin** - an example of how to implement a `PrimWriter` that exports 3ds Max spheres to USD native spheres.
+- **SpherePrimReaderPlugin** - an example of how to implement a `PrimReader` that imports USD native spheres to 3ds Max spheres.
+- **UserDataExportChaserPlugin** - an example of how to implement an `ExportChaser` to expose user properties and custom attributes to a USD custom data node.
+- **UserDataImportChaserPlugin** - an example of how to implement an `ImportChaser` to import custom data to a 3ds Max user properties and custom attributes.
+
+> The **Qt** user interface code of the *UserDataExportChaser C++ Sample* requires an installation of **Qt** as described in the official 3dsMax SDK documentation. To skip the Qt UI part, removing the Qt code part from the sample is possible as
+well, the rest of the sample will still work regardless (analogous to the UserDataImportChaserSample, that does not have any Qt dependency).
+
+## Requirements
+The requirements for using this 3ds Max USD SDK are the same as the [3ds Max SDK](https://help.autodesk.com/view/MAXDEV/2023/ENU/?guid=sdk_requirements).
+
+The Qt library as described in the official [3ds Max SDK documentation](https://help.autodesk.com/view/MAXDEV/2025/ENU/?guid=sdk_requirements).
+We also recommend to install the _QtVSTools for Visual Studio_ to automatically use the QtMSBuild tools provided by this extension. It may be required to choose the appropriate Qt installation in the project settings - based on your targeted version of 3dsMax.
+
+> The 3ds Max USD component devkit includes pre-builds of the component's dependencies. The devkit is found within an installed 3ds Max USD component.
+
+# Plugin development
+## Creating a plugin project
+
+Plugins need to minimally link against the `maxsdk`, `openusd` and the `maxUsd` (from the 3ds Max USD component) libraries
+**core.lib;maxutil.lib;maxUsd.lib and 3dsmax_\.lib**
+
+> The `openusd` libraries are included in the devkit.
+
+> The requirements for using this SDK are the same as the [3ds Max SDK](https://help.autodesk.com/view/MAXDEV/2023/ENU/?guid=sdk_requirements).
+
+## Registering a plugin
+
+Plugins are registered by providing a path or paths to JSON files that describe the location, structure and contents of the plugin. The standard name for these files is `plugInfo.json`.
+
+See the [USD Registry Reference](https://graphics.pixar.com/usd/dev/api/class_plug_registry.html#details) for more information.
+
+Every provided samples are following those principles.
+
+The Info sub-section in the plugInfo schema for all 3ds Max USD plugins contains those nested properties:
+
+- `MaxUsd` : Mandatory property identifying the 3ds Max Info block.
+ - `PrimWriter` : Optional property stating the current plugin contains PrimWriters.
+ - `PrimReader` : Optional property stating the current plugin contains PrimReader
+ - `providesTranslator` : Required property listing the Usd Prim Types the PrimReader imports; the strings making up the array are the Usd Prim Type name
+ - `ShadingModePlugin` : Optional property stating the current plugin contains a ShadingMode definition.
+ - `ShaderWriter` : Optional property stating the current plugin contains ShaderWriters.
+ - `providesTranslator` : Required property for ShaderWriters, listing the translated 3ds Max material; the strings making up the array are the material non-localized names.
+ - `ShaderReader` : Optional property stating the current plugin contains ShaderReader
+ - `providesTranslator` : Required property listing the imported Usd Shader Ids; the strings making up the array are the token value of Usd Shader Id
+ - `ExportChaser`: Optional property stating the current plugin contains an ExportChaser.
+ - `ImportChaser` : Optional property stating the current plugin contains ImportChaser
+ - `JobContextPlugin` : Optional property stating the current plugin contains a JobContext.
+
+Here is a possible sample `plugInfo.json` file declaring a C++ plugin containing the implementation of:
+
+- a PrimWriter handling the translation of 3ds Max Nodes,
+- a new material target to (material conversion type) or a shader mode exporter (both part of the ShadingModeRegistry),
+- a ShaderWriter handling the translation of the 3ds Max "Physical Material" material type,
+- an ExportChaser fine-tuning the export stage.
+- a JobContext (plug-in configuration) wrapping a function to set options to use at import/export
+
+```json
+{
+ "Plugins":[
+ {
+ "Info":{
+ "MaxUsd":{
+ "PrimWriter": {},
+ "ShadingModePlugin": {},
+ "ShaderWriter":{
+ "providesTranslator":[
+ "Physical Material"
+ ]
+ },
+ "ExportChaser": {}
+ "JobContextPlugin": {}
+ }
+ },
+ "Name":"sampleMaxUsdPlugin",
+ "Type":"library",
+ "LibraryPath":"SampleMaxUSDPlugin.dll"
+ }
+ ]
+}
+```
+
+In case of a Python plugin, the plugin script location must part of the Python path (this can be achieved in a startup script with the Python command `sys.path.insert`).
+
+For the USD PlugRegistry to find the `plugInfo.json` file, the file directory must be registered.
+
+```python
+from pxr import Plug
+Plug.Registry.RegisterPlugins()
+```
+
+Again, this can be achieved in a 3ds Max startup script for the plugin, or you can use the environment variable **PXR_PLUGINPATH_NAME** to define the USD plugin path (the path where the `plugInfo.json` file is located).
+
+Regardless of the type of plugin, C++ or Python, to register your plugin you'll need to add a registration script to your 3ds Max plugin in the "post-start-up scripts parts", these USD plugins need to be loaded only when Max is ready to be interacted with and NOT be loaded by the 3ds Max plugin itself prior to that. You can find examples of such scripts in the SDK sample projects.
+
+When creating a C++ plugin (.dll), it should be a separate project from your actual 3ds Max plugin. USD plugin DLLs should be loaded via the USD plugin API, USD does not expect the DLL to be already loaded when the plugin is loaded - and if it is, that can create issues.
+
+For a C++ plugin it is also very important to set the project option "Remove unreferenced code and data" to NO. Not doing so could cause the Macro to be optimized out and the Writer to never be properly registered.
diff --git a/samples/silence_usd_warnings.h b/samples/silence_usd_warnings.h
new file mode 100644
index 0000000..17b5aaa
--- /dev/null
+++ b/samples/silence_usd_warnings.h
@@ -0,0 +1,44 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+// Getting rid of multiple C++17 compliance warnings part of the Pixar USD header files
+
+#pragma warning(push)
+#pragma warning(disable : 4244 4305)
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#pragma warning(pop)
+
+#pragma warning(push)
+#pragma warning(disable : 4267)
+#include
+#pragma warning(pop)
+
+#pragma warning(push)
+#pragma warning(disable : 4251)
+#include
+#pragma warning(pop)
\ No newline at end of file
diff --git a/src/3dsmax.common.settings.props b/src/3dsmax.common.settings.props
new file mode 100644
index 0000000..1a68712
--- /dev/null
+++ b/src/3dsmax.common.settings.props
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)\..\'))
+
+
+ 1
+
+ 2025
+ usd-component-$(VersionTarget)
+ 3dsmax-usd-$(VersionTarget)
+ $(RepoRootDir)build\bin\$(Platform)\$(Configuration)\
+ ApplicationPlugins\usd-component\Contents\bin\
+ $(RepoRootDir)src\
+ $(RepoRootDir)src\tfiles\
+ $(RepoRootDir)src\$(PluginsRelDir)\
+ $(ApplicationPluginsDir)$(PrivateComponentName)\Contents\
+ $(RepoRootDir)build\lib\$(Platform)\$(Configuration)\$(VersionTarget)\
+ $(RepoRootDir)build\bin\$(Platform)\$(Configuration)\$(PrivateComponentName)-test
+
+ $(RepoRootDir)artifacts\
+ 6.5.3
+ 5.15.1
+ $(Artifacts)$(VersionTarget)\maxsdk
+ $(MaxSDK)\lib\$(Platform)\Release
+
+
+
+
+
+
+
+
+ $(RepoRootDir)build\obj\$(Platform)\$(Configuration)\$(VersionTarget)\$(ProjectName)\
+ $(RepoRootDir)build\lib\$(Platform)\$(Configuration)\$(VersionTarget)\
+ $(IntDir)
+ $(ContentsDir)\bin\
+ $(RepoRootDir)additional_includes\MaxRestrictedSdk\$(VersionTarget)
+
+ $(Artifacts)$(VersionTarget)\$(VersionTarget)_3dsmax-component-materialX
+
+ $(MaxUsdDevKit)\spdlog
+ $(Artifacts)\spdlog
+
+ $(Artifacts)$(VersionTarget)\gtest
+ $(GoogleTestDir)\include
+ $(GoogleTestDir)\lib
+ $(GoogleTestDir)\lib\$(debug_or_release)
+ $(GoogleTestDir)\bin
+ $(GoogleTestDir)\bin\$(debug_or_release)
+
+ $(MaxUsdDevKit)\PySide6\Pyside6
+ $(MaxUsdDevKit)\PySide2\site-packages/PySide2
+ $(Artifacts)$(VersionTarget)\PySide6\PySide6
+ $(Artifacts)$(VersionTarget)\PySide2\site-packages/PySide2
+ $(MaxUsdDevKit)\PySide6\shiboken6
+ $(MaxUsdDevKit)PySide2\site-packages\shiboken2
+ $(Artifacts)$(VersionTarget)\PySide6\shiboken6
+ $(Artifacts)$(VersionTarget)\PySide2\site-packages\shiboken2
+ $(PySideDir)\include
+ $(PySideShibokenDir)_generator\include
+ $(PySideDir)
+ $(PySideShibokenDir)
+ shiboken6.cp311-win_amd64.lib;pyside6.cp311-win_amd64.lib
+ shiboken2.abi3.lib;pyside2.abi3.lib
+ shiboken2.cp39-win_amd64.lib;pyside2.cp39-win_amd64.lib
+ shiboken2.cp37-win_amd64.lib;pyside2.cp37-win_amd64.lib
+
+
+ $(Artifacts)$(VersionTarget)\QtMsBuild
+
+
+
+
+
+
+
+
+ core;gui;widgets;opengl
+ $(Artifacts)$(VersionTarget)\Qt\$(QTVER)
+
+
+ core;gui;widgets
+ $(Artifacts)$(VersionTarget)\Qt\$(QTVER)
+
+
+
+
+
+
+ SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_WCHAR_FILENAMES;FMT_HEADER_ONLY;%(PreprocessorDefinitions)
+ USE_PYSIDE_6;%(PreprocessorDefinitions)
+ $(SpdlogInc);%(AdditionalIncludeDirectories)
+ stdcpp17
+ true
+
+
+
+
+
+
+
diff --git a/src/3dsmax.component.settings.props b/src/3dsmax.component.settings.props
new file mode 100644
index 0000000..a10f847
--- /dev/null
+++ b/src/3dsmax.component.settings.props
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+ $(OutDir)
+ $(MaxBinariesDir)\
+
+
+
+
+
+ $(MaxSdkInc);$(MaxRestrictedSdkInc);%(AdditionalIncludeDirectories)
+ MAX_$(VersionTarget);%(PreprocessorDefinitions)
+
+
+ $(MaxSdkLibRelease);%(AdditionalLibraryDirectories)
+
+
+
diff --git a/src/ApplicationPlugins/usd-component/Contents/Help/en-US/bootstrap.min.css b/src/ApplicationPlugins/usd-component/Contents/Help/en-US/bootstrap.min.css
new file mode 100644
index 0000000..6561b6f
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/Help/en-US/bootstrap.min.css
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v4.0.0 (https://getbootstrap.com)
+ * Copyright 2011-2018 The Bootstrap Authors
+ * Copyright 2011-2018 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014 \00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm,.input-group-lg>.form-control-plaintext.form-control,.input-group-lg>.input-group-append>.form-control-plaintext.btn,.input-group-lg>.input-group-append>.form-control-plaintext.input-group-text,.input-group-lg>.input-group-prepend>.form-control-plaintext.btn,.input-group-lg>.input-group-prepend>.form-control-plaintext.input-group-text,.input-group-sm>.form-control-plaintext.form-control,.input-group-sm>.input-group-append>.form-control-plaintext.btn,.input-group-sm>.input-group-append>.form-control-plaintext.input-group-text,.input-group-sm>.input-group-prepend>.form-control-plaintext.btn,.input-group-sm>.input-group-prepend>.form-control-plaintext.input-group-text{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-sm>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:calc(1.8125rem + 2px)}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-lg>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:calc(2.875rem + 2px)}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(40,167,69,.8);border-radius:.2rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{background-color:#71dd8a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(40,167,69,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label::before,.was-validated .custom-file-input:valid~.custom-file-label::before{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(220,53,69,.8);border-radius:.2rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{background-color:#efa2a9}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(220,53,69,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label::before,.was-validated .custom-file-input:invalid~.custom-file-label::before{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-check{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}.btn:not(:disabled):not(.disabled).active,.btn:not(:disabled):not(.disabled):active{background-image:none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;background-color:transparent}.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link.focus,.btn-link:focus{text-decoration:underline;border-color:transparent;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropup .dropdown-menu{margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file:focus,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::before{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label,.input-group>.custom-file:not(:first-child) .custom-file-label::before{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-webkit-box;display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:active~.custom-control-label::before{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{margin-bottom:0}.custom-control-label::before{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#dee2e6}.custom-control-label::after{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:inset 0 1px 2px rgba(0,0,0,.075),0 0 5px rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-control{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:focus~.custom-file-control::before{border-color:#80bdff}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(calc(2.25rem + 2px) - 1px * 2);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:1px solid #ced4da;border-radius:0 .25rem .25rem 0}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .dropup .dropdown-menu{top:auto;bottom:100%}}.navbar-expand{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .dropup .dropdown-menu{top:auto;bottom:100%}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%}}.breadcrumb{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-webkit-box;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;background-color:#007bff;transition:width .6s ease}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;opacity:.75}.close:not(:disabled):not(.disabled){cursor:pointer}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-dialog-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-content{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;-webkit-clip-path:inset(50%);clip-path:inset(50%);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal;-webkit-clip-path:none;clip-path:none}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-muted{color:#6c757d!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}
+/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/Help/en-US/index.html b/src/ApplicationPlugins/usd-component/Contents/Help/en-US/index.html
new file mode 100644
index 0000000..931a2e8
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/Help/en-US/index.html
@@ -0,0 +1,3020 @@
+
+
+
+
+
+
+
+
+
+ USD for Autodesk 3ds Max
+
+
+
+
+
+
+
+
+
Autodesk 3ds Max
+
+
+
+
USD for Autodesk 3ds Max
+
Support for USD content in Autodesk 3ds Max
+
+
+
+
Overview
+
This plugin adds support for Pixar's Universal Scene Description (USD) in Autodesk 3ds Max.
+
If you like you what you see or have any feedback about these features, please let us
+ know. We look forward to hearing from you to help us shape the future of this initiative.
+
+
+
We may make major breaking changes from one version to the next in order to improve our APIs, or to provide support for new use cases.
+ Please keep that in mind when making purchasing decisions based on the current capabilities.
+
+
+
Uninstalling
+
To uninstall this plugin:
+
+
Open Window's Add or remove program tool.
+
Search for "USD for Autodesk 3ds Max".
+
Select the result and click the "Uninstall" button.
+
+
+
Get in touch
+
We are eager to hear what you think of these capabilities!
+
Was there anything you liked? Did you find a bug? Do you have any ideas or USD content that you would like
+ share with us? Let us know on the Autodesk 3ds Max Beta forums or give your feedback here.
+
The success of this initiative depends on how we can serve your needs. Your feedback is important to us in
+ this partnership.
+
+
Release notes
+
v0.9.0 New
+ What's New:
+
+
+ Bump component version to 0.9 - new public release
+
+
+ Fixes:
+
+
+ Changed MaterialX plugin utility to export using colorspace "sRGB" by default instead of "gamma22".
+
+
+ Fixed an issue where setting Textures for Draw Modes in the Prim Attributes could cause 3ds Max to become unresponsive.
+
+
+ Cleaned up USD Attribute Tooltips.
+
+
+ Fix expected behavior on PrimReader plugin not being loaded when 'providesTranslator' type is an ancestor type.
+
+
+ Fixed spinner for USD Prims to allow negative values.
+
+
+ Fixed options for loading a stage not persisting between multiple stage-loading actions.
+
+
+ Fixed unnecessary memory usage from textures that should not be loaded from deactivated prims in a USD Stage Object.
+
+
+ Update some labels and tooltips around the Draw Mode functions in the USD Stage Object.
+
+
+
+
v0.8.4
+ What's New:
+
+
+ Added a rollup to the property editor to display "Extra Attributes" that do not belong to any USD Schema.
+
+
+ Added new display option on a USD Stage Object to display PointInstances as Box or Cross cards. The option will author all the necessary settings on the prims in the stage to make draw modes display on Pointinstancers into an anonymous sublayer of the session layer.
+
+
+ Added new function to generate a BezierShapeClass object from a USD Curve.
+
+
+ Fixes:
+
+
+ Fixed an issue where the Command Panel Rollouts for the USD Stage could undesirably alter the appearance of the Command Panel.
+
+
+ Fixed issue where undoable commands were generated while editing spinners interactively in the property editor
+
+
+ Fixed an issue where the property editor UI fell out of sync with the data when attempting to edit USD instances.
+
+
+ Fixed an issue with transforming prototypes used by point instancers.
+
+
+ Fixed the behavior of the Plug-in Configuration Rollup to recall the user's last set state.
+
+
+ Fixed an issue where invalid blendshape could crash when importing the stage
+
+
+ Fixed changes to a prim's purpose not being displayed immediately in the viewport.
+
+
+ Fixed crash related to using USD Snaps on an empty stage.
+
+
+ Fix expected behavior on PrimReader plugin not being loaded when 'providesTranslator' type is an ancestor type.
+
+
+ Fixed the USD Explorer's ame-light theme readability for text and selection elements.
+
+
+
+
v0.8.3
+ What's New:
+
+
+ Added support for USDGeomCurves in the USD Importer.
+
+
+ Added ability to select and transform USD Point Instances.
+
+
+ Added support for the USD modify tab to remember rollup state even when multiple prims are selected.
+
+
+ Added prim attributes to the modify tab when selecting a prim, allowing the user to make edits on prim data as they would with standard Max objects.
+
+
+ Updated the USD Exporter to remember the rollup state between sessions.
+
+
+ Fixes:
+
+
+ Fixed command panel not refreshing when undoing some edits to prim properties.
+
+
+ Fixed file inputs missing from the USD Attribute editor.
+
+
+ Fixed critical error causing a crash when importing USD.
+
+
+
+
v0.8.2
+ What's New:
+
+
+ Updated USD export/import options to use a Dictionary.
+
+
+ Added new USD category to Snaps that allows snapping to USD Faces, Vertices and Edges.
+
+
+ Updated the functions in the USD Explorer for changing variants, visibility, etc. to respect the current EditTarget a user may have set via scripting. This also works for transforming prims in the viewport. Previously, the edits in the USD Explorer were always going to the session layer.
+
+
+
v0.8.1
+ What's New:
+
+
+ Updated Max USD Export options to Dictionaries.
+
+
+ Fixes:
+
+
+ Fixed an issue where deleting and adding a new skin modifier to a mesh will log an error message of multiple bind pose.
+
+ Change to Import/Export "Enter" key interaction to avoid accidental dialog confirmation.
+
+
+ Updated viewport selection of prims to default to selecting editable prims when selecting instanceable prims.
+
+
+ Fixes:
+
+
+ Fixed a Crash when exporting Skel animation with an invalid root prim
+
+
+ Fixed an issue with skinned animation data when Offset Baked was disabled during Export.
+
+
+
+
v0.7.7
+ What's New:
+
+
+ Updated cameras created for a USDStageObject to be undeletable by users. The cameras are controlled by the USDStageObject.
+
+
+ Fixes:
+
+
+ Fixed bad labels in USD float controllers.
+
+
+ Fixed an issue where under specific circumstances performing an Undo operation on a USD Stage node generation would crash.
+
+
+ Fixed issue where the wrong value was set on "Reset to Default" for the Stage Object animation option "Frames per Second"
+
+
+ Fixed an issue with USD Stage during Animation playback where performance was lost in Proxy mode when switching between Proxy/Render.
+
+
+ Fixed a Color Space issue impacting 3ds Max 2024 and later where Gamma was not set to 1.0 for USD Material Nodes.
+
+
+ Exposed bump amount value in the UsdPreviewSurface scripted material to allow correcting normal map display issues in the 3dsMax viewport.
+
+
+ 2x performance improvement of USD Explorer Auto-Expand to Selection.
+
+
+
+
v0.7.6
+ What's New:
+
+
+ The default translators (writers/readers) are all plugins to the USD component.
+
+
+ Added new USD Float Controller that can drive float, point2, point3 and point4 values in Max.
+
+
+ Updated to USD 0.24.05 for the beta build.
+
+
+ Exposed the TranslatorMaterial::AssignMaterial helper from the Max USD C++ SDK to Python.
+
+
+ Added support to use Zoom Extents on selected USD Prims.
+
+
+ Fixes:
+
+
+ Fixed a freeze when loading some USD scenes when the PXR_PLUGINPATH_NAME envirornment variable is set to the pluginInfo.json folder.
+
+
+ Fixed an issue where under specific circumstances performing an Undo operation on a USD Stage node generation would crash.
+
+
+ Fixed a missing string for USD Stage prim selection action being populated in the Undo stack as a generic action.
+
+
+ Fixed USD error in MAXScript listener when reactivating a USD Camera.
+
+
+ Fixed performance drop in the viewport when loading a USD Stage with instances containing many geomsubsets.
+
+
+ Fixed performance degradation on selecting prims in a USD stage.
+
+
+
+
v0.7.5
+ What's New:
+
+
+ Added new USD Xform Controller that allows a Max node transform to be controlled by the transform of an xformable prim in a USD Stage.
+
+
+ Added samples to Max USD SDK to demonstrate how to create a custom UI for plugin-configurations in the USD Exporter and Importer.
+
+
+ Updated the USD Stage Object to bring in proxy cameras into 3ds Max that are controlled by underlying USD camera data. This allows a user to render from cameras in a USD Stage as well as look through them.
+
+
+ Fixes:
+
+
+ Fixed display of animated focal length when importing animating USD camera
+
+
+ Fixed USD Payload load/unload state not being properly restored with Undo/Redo.
+
+
+ Fix regression of the kind type combobox in the USD stage object Viewport selection rollup.
+
+
+
+
v0.7.4
+ What's New:
+
+
+ Added new USD Xform Controller that allows a Max node transform to be controlled by the transform of an xformable prim in a USD Stage.
+
+
+ Added support for multi-selection of USD prims in the viewport. Note that deep selection is not currently supported with USD Prims.
+
+
+ Added ability for plugin-developers to share a custom UI for their contexts in the USD Exporter to allow users to set custom plugin settings.
+
+
+ Fixes:
+
+
+ Fixed severe performance degradation when the Undo Stack contains USD prim multi-selection.
+
+
+ Python exposure to USDSceneBuilderOptions Content Source option.
+
+
+ Fixed an issue where Importing animation data was not setting the Start and End frames to match the incoming animation frame range.
+
+
+ Fix issue where user can create prims under deactivated prims even though error message is shown. It is no longer possible to create a prim under a deactivated prim.
+
+
+ Fixed a performance issue of visibility switching of some USD large multi-prim selections.
+
+
+
+
v0.7.3
+ What's New:
+
+
+ Added support for converting Progressive Morphers to Inbetween Blendshshapes in the USD Exporter.
+
+
+ Added support for multi-selection of USD prims in the viewport.
+
+
+ Fixed issue with exporting multimaterials with connected material switchers on instanced object.
+
+
+ Fixes:
+
+
+ Fixed an import error with Meshes with just a morpher in a scene with other skinned meshes with bones bindings
+
+
+ Fixed Python exposure to USDSceneBuilderOptions Content Source option
+
+
+ Fixed a Morpher export issue with USD invalid identifier names of morph target starting with numeric values
+
+
+ Fixed a current frame export issue with multiple morphers in the scene
+
+
+ Fixed getClassName() not returning the correct class name with the USDStageObject class.
+
+
+ Fixed a problem in the Import Chaser C++ samples casting floats as doubles.
+
+
+
+
v0.7.2
+ What's New:
+
+
+ Added support for converting Progressive Morphers to Inbetween Blenshshapes in the USD Exporter.
+
+
+ Added support for toggling the visibility icons on prims in the USD Explorer.
+
+
+ Fixed an issue where undo didn't work for some text fields and drop downs on the stage's command panel.
+
+
+ Fixes:
+
+
+ Fixed an issue where Undo would not display Prim selections in the Viewport.
+
+
+ Fixed issue with possible corruption of the material UI export options when modified from scripting.
+
+
+
+
v0.7.1
+ What's New:
+
+
+ Fixed issue with exporting Material switchers with connected multi-materials on instanced object.
+
+
+ Added support for undo/redo when selecting USD prims.
+
+
+ Added support for selecting multiple USD prims.
+
+
+ Removal of the export/import 'onCompletion' callbacks in favor of the export/import Chaser mechanics.
+
+
+ C++ SDK changes on the container mapping the exported Nodes to Prims.
+
+
+ Fixes:
+
+
+ Fixed issue with bad relative path resolution when exporting MaterialX materials to separate layers.
+
+
+
+
v0.7.0
+ What's New:
+
+
+ Adjusted UI behavior in the USD Exporter to reduce likelihood of unintended changes to values with Spinners when using the scroll wheel.
+
+
+ Fixes:
+
+
+ Fixed an issue where toggling USD Visibility within the USD Explorer could impact the Viewports performance.
+
+
+ Fix to prevent desynchonization between UI export options set through scripting and the interface.
+
+
+ Fixed an issue where changing the Morpher limits would affect the Morpher's animation weight.
+
+
+
+
v0.6.9
+ What's New:
+
+
+ Added support for mixing the material switcher and multi/sub-object materials in the USD Exporter for creating material variants.
+
+
+ Added a column in the USD Explorer to display a prim's authored kind.
+
+
+ Added support to import blendshapes from USD into Morphers.
+
+
+ Fixes:
+
+
+ Fixed an issue where toggling USD Visibility within the USD Explorer could impact the Viewports performance.
+
+
+ USDView no longer causes a lingering 3dsMax Python 3 Interpreter process after closing the window.
+
+ Fix explorer refresh issue with long treeview.
+
+
+ Fix issue with row selection in USD Explorer where horizontal scroll position shifts back and forth.
+
+
+ Fix animated instance selection display.
+
+
+ Fixed selection highlighting issue causing needless errors in the listener, affecting performance.
+
+
+ Stabilized the sub-object mode transform operations.
+
+
+ Prevent materials from disappearing after prim transform operations.
+
+
+ Prevent auto-key feature from interfering with animation import.
+
+
+ Only move prims that belong to the selected USD Stage.
+
+
+
+
v0.6.8
+ What's New:
+
+
+ Added ability to expand/contract the entire hierarchy below a prim with CTRL+LMB.
+
+
+ Improved the visibility of inactive prims in the USD Explorer.
+
+
+ Added a column in the USD Explorer to display a prim's authored kind.
+
+
+ Added ability to transform a USD prim in the viewport.
+
+
+ Brought back the Load Payloads checkbox in the load stage dialog.
+
+
+ Added parent highlighting to selected prims in the USD Explorer.
+
+
+ Updated the labels and tooltips in the USD Preview Surface Material.
+
+
+ Added support to keep the USD Context menus open when holding down the CRTL key. The is helpful when changing variants on a USD Prim and wanting to test different variant selections.
+
+
+ Updated the USD exporter to use the current timeline frame range when initially setting the export range.
+
+
+ Fixes:
+
+
+ Fixed defect in orientation of bones when exporting to USDSkel.
+
+
+
v0.6.7
+ What's New:
+
+
+ Added selection highlighting to prims in a USD Stage.
+
+
+ Added support for multiple material targets when exporting a Material Switcher.
+
+
+ Prim name column now auto-expands to fill available size of USD Explorer.
+
+
+ Added ability to customize what columns are available in the USD Explorer.
+
+
+ Updated the USD exporter to use the current timeline frame range when initially setting the export range.
+
+
+ Fixed Stage Node layout to be consistent with other windows layout.
+
+
+ Fixed an issue where toggling the visibility state of some prims could cause it to stay visually out of sync.
+
+
+
v0.6.6
+ What's New:
+
+
+ Added support for importing animated attributes for USD lights.
+
+
+ Animated camera attributes like FOV are now supported in the USD Importer.
+
+
+ Fixes:
+
+
+ Fixed USD Explorer visibility state to refresh correctly.
+
+
+
v0.6.5
+ What's New:
+
+
+ Improved the USD SDK samples to require less effort to get running.
+
+
+ Updated several classes in the MaxUSD projects that were not namespaced to be namespaced to maxUsd.
+
+
+ Added a new Prim sub-object mode to select prims in a USDStageObject.
+
+
+ New option added to the USD explorer to auto-expand the tree to the current selection.
+
+
+ Prim name column now auto-expands to fill available size of USD Explorer.
+
+
+ The checkbox for loading payloads was removed from the USDStageObject modify tab since payload rules are now working in the USD Explorer.
+
+
+ Added ability to specify specific animation ranges in the USD Importer.
+
+
+ Update for 3ds Max SDK 2025 freeze (M4479)
+
+
+ Fixes:
+
+
+ Fixed importing some USDSkel prims into Max.
+
+
+ Fixed USD Exporter not fitting onto small screen resolutions.
+
+
+ Fixed an undo crash in the USD Explorer after toggling a prim's instanceable setting.
+
+
+ Restricted the material layer setting to be output to the same file as the main USD file when exporting a material layer.
+
+
+ Fixed an issue where USD Skel exports had deformation results that weren't matching the source animation.
+
+
+
v0.6.4
+ What's New:
+
+
+ Updated the USD Exporter to set the prim type of the material scope from Def to Scope.
+
+
+ Updated USD version to 23.11 for 3ds Max 2025.
+
+
+ Fixed loading USD files into a Stage node that were coming up empty but could be loaded into USDView.
+
+
+ USD Importer now has a setting for using/disabling the progress bar via scripting (UIOption.UseProgressBar = true|false).
+
+
+
+
v0.6.3
+ What's New:
+
+
+ Added a new export options to enable or disable the last resort USDPreviewSurface material (options.UseLastResortUSDPreviewSurfaceWriter).
+
+
+ Added further validation of target material file paths to prevent unicode characters.
+
+
+ Added support for the <filename> token in exported material files paths. It is replaced by the root layer file name at export time.
+
+
+ Improved the Prim reader API documentation and sample.
+
+
+ Added support for basic rectangle selection of the USD Stage Object (only support's crossing mode).
+
+
+ Fixes:
+
+
+ Fixed issue with animation import not setting key frames at time 0.
+
+
+ Fixed erroneously filtered typeless prims in the USD Stage prim selection dialog.
+
+
+ Fixed a crash when exporting to a material layer already loaded in memory.
+
+
+ Fixed a USDSkel bone import issue with Y-up source data.
+
+ Added a sample to the USD SDK on using an Import Chaser.
+
+
+ Fixes:
+
+
+ Fixed incorrect transforms when importing instances.
+
+
+ Fixed error that could cause the USD Explorer to not show any USD data when opened.
+
+
+ Fixed the name of the USD Plugin not having a human-readable format in the Windows Control Panel.
+
+
+ Fixed defect causing the USD Tools to not launch from the command line.
+
+
+ Fixed problem in the USD Importer importing materials on prims with multiple geomsubsets.
+
+
+ Fixed incorrect log error in USD Importer when importing some USD files.
+
+
+ Fixed incorrect bind transform when importing USDSKel from some USD files.
+
+
+ Fixed missing translation and rotation animation at "root" level when importing some USD files (such as the Walking Pixar Female).
+
+
+ Fixed duplicate nodes being created when importing USD files with instances containing hierarchies.
+
+
+ Updated the USD Exporter to block the export and warn the user of invalid characters in material names.
+
+
+
+
v0.6.1
+ What's New:
+
+
+ 3ds Max USD Component updated to a newer installer framework. With this new installer, previous versions from 0.5 and older need to be manually uninstalled to be removed from the system.
+
+
+ The USD Exporter can now export the Material Switcher material as variantSets.
+
+
+ Added Context and Chasers to the USD Importer SDK.
+
+
+ Fixes:
+
+
+ Fixed an issue resizing the "Prim name" column in the USD Explorer to not work in odd ways. Note that after this fix, the prim name column will no longer auto-expand to fit available space.
+
+
+ Fixed an issue where importing a USD file with different system units format would bring bad data.
+
+
+ Fixed an issue when specifying .usdz for Material File Generation caused 3ds Max to crash
+
+
+ Fixed an issue where wrong bone animation was being used outside the USD time range when importing a USD file using USDSkel.
+
+
+ Fixed an issue where skinned meshes were not being bound to the correct bones when importing USD.
+
+
+ Fixed an issue where Blendshape content was exported with Y-up settings would not have the expected spatial coordinates.
+
+
+ Fixed an issue where the plugin would throw an error when loaded in localized 3ds Max versions
+
+
+ Fixed USDView not launching properly in 3ds Max 2025.
+
+
+ Fixed an issue where "Copy Prim Path" menu option would show up in unintended locations.
+
+
+
+
v0.6.0
+ What's New:
+
+
+ Updated SDK documentation for 0.6 release to include Import Chasers, ShaderReader and PrimReader.
+
+
+ Fixes:
+
+
+ Fixed issue with animation import not setting key frames at time 0.
+
+
+ Fixed a USDSkel bone import issue with Y-up source data.
+
+
+ Fixed a crash when exporting to a material layer already loaded in memory.
+
+
+ Fix an issue where importing usd data would create duplicate geometry on instances with hierarchies.
+
+
+
+
v0.5.8
+ What's New:
+
+
+ Added support to import USD Skel Animations into 3ds Max.
+
+
+ It's now possible to the edit the Target layer, and default layer of materials in the exporter.
+
+
+ Fixes:
+
+
+ Fixed an issue with bounding boxes in a Stage with Skel animation and unskinned meshes.
+
+
+
+
v0.5.7
+ What's New:
+
+
+ Updated the visibility icon for the USD Explorer to be more legible.
+
+
+ Update several tooltips to better inform the user.
+
+
+ API Change, new feature: Prim readers.
+
+
+ Fixes:
+
+
+ Fixed an issue where the '?' button wouldn't open the documentation page.
+
+
+ Fixed an issue where tool tips where not showing up in the correct place for the USD animation roll up.
+
+
+ Fixed an issue that could cause a visual bug when docking the USD Explorer.
+
+
+
+
v0.5.6
+ What's New:
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M3364)
+
+
+ Added support for the USD Explorer to respect window snapping.
+
+
+ Updated the USD Plugin to use USD 23.08 in Max 2025 builds.
+
+
+ Updated the USD Plugin to use Python 3.11.3 in Max 2025 builds.
+
+
+ Added new right-click menu in the USD Explorer to copy a prim's path to the clipboard.
+
+
+ Added support to import USD UDIM textures to 3ds Max from a UDIM in a USDPreviewSurface shader.
+
+
+ Fixes:
+
+
+ Fixed a crash when undoing the loading root layer into a USD Stage.
+
+
+ Fixed an issue where clearing session layer was not refreshing with invisible and deactivated prims
+
+
+ Fixed an issue where the stage root bounding box would not be correct for scenes exported with UsdSkel
+
+
+ Fixed an issue where the USD Explorer would not update properly with user inputs
+
+
+
v0.5.5
+ What's New:
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M3231)
+
+
+ Added support for the USD Explorer to respect window snapping.
+
+
+ A display menu has been added to hide inactive prims from the USD Explorer.
+
+
+ Clearing a search in the USD Explorer will now restore the previous collapsed or expanded state of the tree view.
+
+
+ The USD Explorer now only auto-expands the first level of prims in the tree view.
+
+
+ Fixes:
+
+
+ Fixed an issue where material bindings were not always binding to the correct prims.
+
+
+ Fixed a crash that could occur when adding prims to instantiated USD content.
+
+
+ Fixed an issue preventing users from adding prims to the root of a USD Stage in the USD Explorer.
+
+
+
v0.5.4
+ What's New:
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M3190)
+
+
+ Users can now undo edits made from the USD Explorer Tree View.
+
+
+ Added support for interface nodes for MaterialX.
+
+
+ USD Explorer now supports the Visibility property.
+
+
+ A new option is available from USD Parameters rollout to open the USD Explorer.
+
+
+ Added a PostExport() Callback for PrimWriters. This is an SDK change.
+
+
+ Removed unnecessary UsdTransform2d from material exports.
+
+
+ Added Icons and Badges to USD Explorer Tree View.
+
+
+ Fixes:
+
+
+ Fixed an issue where it was no longer possible to Copy USD Stages in the Viewport.
+
+
+ UFE python bindings now available in all 3dsMax versions.
+
+
+
v0.5.3
+ What's New:
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M2961)
+
+
+ Added an option to disable the export progress bar for script execution.
+
+
+ USD Explorer context menu enrichment.
+
+
+ USD Exporter supports UDIMs for USD Preview Surface.
+
+
+ Updated the USD Exporter to report an export config error if a non-empty node list is provided, but the content source is not properly set.
+
+
+ Fixes:
+
+
+ Fixed an incorrect error being generated in the logs anytime a Chaser was enabled during export.
+
+
+ Fixed a crash related to instantiated content within a USD Stage.
+
+
+ Fixed an issue where baked animation data using Polys could have corrupted normals when exported to .usd.
+
+
+ Fixed USD Explorer Focus and Docking behavior.
+
+
+
v0.5.2
+ What's New:
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M2821)
+
+
+ Updated the USDStageObject to not allow an empty stage mask value.
+ If and empty string value is given, the value is automatically set to "/".
+
+
+ A tree view of the USD data is now available so that artists can view the prims that make up their stage object.
+
+
+
+ Fixes:
+
+
+ Fixed a crash in the MaterialX Shader when undoing the explode MaterialX function.
+
+
+ Fixed an issue where USD Skel exports had deformation results that weren't matching the source animation.
+
+
+ Fixed an issue where baked animation data using Polys could have corrupted normals when exported to .usd.
+
+
+ Fixed the command line USD Tools not working when using a string that contains multiple consecutive spaces in the commands.
+
+
+
v0.5.1
+ What's New:
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M2608)
+
+
+ Updated the USDStageObject to not allow an empty stage mask value. If and empty string value is given, the value is automatically set to "/".
+
+
+ Fixes:
+
+
+ Updated the MaterialX Importer/Exporter to not pop up with a messagebox on every warning; now prints warnings to the MAXScript listener.
+
+
+ The USD Exporter now sets Map Channel 1 to be named "st" in meshes by default.
+
+
+ Fixed a crash in the MaterialX Material when undoing the explode MaterialX function.
+
+
+ Fixed an issue where USD and MaterialX shared the same name in the Plug-in Manager.
+
+
+ Fixed an issue where the description for the mtlXIOUtil utility was being truncated.
+
+
+ Fixed an issue where USD Skel exports had deformation results that weren't matching the source animation.
+
+
+ Fixed an issue where some MaterialX plug-in files were being generated in unexpected locations.
+
+
+
v0.5.0
+ What's New:
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M2394)
+
+
+ Basic MaterialX support is now available with the USD installer.
+
+
+ A ShaderWriter is available that exports the MaterialX scripted material in the USD file as a Mtlx Reference.
+ A "MaterialX" target is also available in the Export Dialog UI.
+
+
+
v0.4.6
+ What's New:
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M2159)
+
+
+ Fixes:
+
+
+ Fixed an issue where Morphers with assigned non-zero values on first frame were not being evaluated correctly.
+
+
+ Fixed an issue where Morpher data was not being exported correctly.
+
+
+ Fixed an issue where some USD buttons were not being sized correctly for certain screen resolutions.
+
+
+ Fixed some erroneous tooltips being displayed in the USD Export UI.
+
+
+ Fixed loading a USD Stage into 3ds Max when the USD file referenced MaterialX files.
+
+
+
v0.4.5
+ What's New:
+
+
+ Added support to export Morpher modifier channels as Blend Shapes in USD.
+
+
+ Added button in USDStageObject to clear the current session layer. A temporary solution to clearing scripted USD changes in the session layer in preparation for adding a layer editor.
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M1979)
+
+
+
v0.4.4
+ What's New:
+
+
+ Compatibility release for the 3ds Max Midgard SDK. (M1740)
+
+
+
v0.4.3
+ What's New:
+
+
+ Moved Skin option to the Animation rollout in the USD Exporter.
+
+
+ Added a new Python class to export materials to USD and bind to prims to help scripters and tool developers to build custom tools with materials.
+
+
+ Fixes:
+
+
+ Fixed some mismatched materials on USD prims compared to some multimaterial setups.
+
+
+ Fixed USD exporter UI not respecting several options.
+
+
+ Fixed the Display Icon not working properly in the USDStageObject.
+
+
+
+
v0.4.2
+ What's New:
+
+
+ Updated the USD Exporter to export non-time-varying topology changes for data channels that do not change in animated exports. This reduces file sizes and improves performance for animated meshes viewed in a Stage.
+
+
+ Added the ability to supply an explicit python.exe path (via --python-exe) to the USD tools bundled with the USD Plugin. Read RunUsdTool.bat in the bin folder of the plugin for more information.
+
+
+ Improved the performance of generating preview materials in the viewport for a USD Stage Object set to display materials.
+
+
+ Updated the USD Exporter to properly bind materials to USD GeomSubsets when the material IDs on the mesh are greater than the Multimaterial numsubs.
+
+
+ Updated USDView that ships with the 3ds Max USD plugin to display MaterialX.
+
+
+ Updated the USD Exporter to use the new menu system introduced in 3ds Max 2025 branch to create the File > Reference > USD Stage... menu.
+
+
+ Refactored the USD Exporter code to be significantly faster at exporting complex scenes with interdependencies that are animated.
+
+
SDK CHANGES :
+
+ The export changes required major changes to the PrimWriter API. The PrimWriter interface now also better aligns with the ShaderWriter interface.
+
+ All the samples were updated accordingly.
+
+
Notable changes :
+ - Every Node->Prim translation now gets its own PrimWriter instance. Previously, a single instance was created for the entire export.
+ - The CanConvert() method was renamed to CanExport() and was made static. A static_assert makes sure the method exists in derived PrimWriters.
+ - As a consequence, the PrimWriter's methods no longer receive a 3dsMax node as argument, instead, the node can be retrieved using PrimWriter::GetNode().
+ - PrimWriter::Write() is now called multiple times on animated data, for each frame that needs to be exported.
+ - New PrimWriter method GetValidityInterval() : Enables PrimWriter to specify the validity interval of their exported data at a certain time. From this, the exporter figures out at what frames PrimWriter::Write() needs to be called (base implementation returns the object's validity interval).
+ - New header TimeUtils.h
+ - The TimeConfig class has been modified to ease usage, all conversions TimeValue/3dsMax frame/TimeCode are now handled from there.
+ - New class "ExportTime" : Represents a single time sample export configuration. I.e. the 3dsMax time value, and what USD timecode it corresponds to.
+ - MeshConverter::ConvertToUSDMesh() now also receives a specific time frame to convert the mesh at.
+
+
+ Fixes:
+
+
+ Animation Frame Range now correctly accepts negative values.
+
+
+
v0.4.1
+ What's New:
+
+
+ Updated the default material scope in the USD Exporter from "Materials" to "mtl" to align with Maya.
+
+
+ Updated the USD Exporter to be organized into rollouts.
+
+
+ Updated the USD Exporter to export non-renderable geometry objects with the Guide purpose.
+
+
+ Updated USD Exporter to export Group Nodes as the USD "group" kind.
+
+
+ Changed the defaultPrim to "root" in the USD Exporter.
+
+
+ Separated USDZ from the other USD file types in the USD Exporter to make it easier to understand how to export USDZ.
+
+
+ Fixes:
+
+
+ Fixed a defect in the USD Exporter when exporting some scenes with big numbers on their names.
+
+
+
+
+ Fixed incorrect USDStageObject ParamBlock parameter SourceAnimationEndTimeCode naming from SourceAnimationEndFrameTimeCode to SourceAnimationEndTimeCode.
+
+
+
+
v0.4.0
+ What's New:
+
+
+ The USD Exporter has been updated to merge Plugin-Configuration settings and user-settings, allowing USD Preview Surface materials to be written alongside context-driven materials, such as Vray Materials (when an appropriate version of Vray is installed).
+
+
+ Fixes:
+
+
+ Updated the USD Exporter to use the Non-Localized name of Material classes in the shader writer.
+
+
+
+
v0.3.8
+ Fixes:
+
+
+ Fixed some transparent materials not displaying in the viewport.
+
+
+
+
+ Added warnings to the USD Export log when exporting to USDSkel and there are modifiers above the Skin modifier.
+
+
+
+
+ Disable modifiers changing the geometry in a way incompatible with UsdSkel on export.
+
+
+
+
+ Fixed USDStageObject.SelectRootLayerAndPrim() not returning the Payalods option. The Payloads option now returns as a boolean in the third item in the resulting array.
+
+
+
+
+ Removed the term "beta" from the USD plugin.
+
+
+
+
+ Updated the USD Exporter label for USDSkel to "Skin" instead of "Skeletons" to match the conventions of other exporters.
+
+
+
v0.3.7
+ What's New:
+
+
+ Updated the USD version to 22.11 for 3ds Max 2024.
+
+
+
+
+ Added USDSkel support to the USD Exporter. When enabled, skinned meshes will export as USDSkel instead of animated vertex caches.
+
+
+
+
+ Cleaned up some tooltips in the Animation rollout of the USDStageObject.
+
+
+
+
+ Cleaned up API to reduce the amount of build warnings when creating plugins for the 3ds Max USD.
+
+
+ Fixes:
+
+
+ Fixed some transparent materials not displaying in the viewport.
+
+
+
+
+ Updated USD Exporter default primvar name of UV channel 1 from "st" to "st0". This is a temporary fix to deal with a MaxToA issue introduced with the change to USD 22.11.
+
+
+
+
+ Added warnings to the USD Export log when exporting to USDSkel and there are modifiers above the Skin modifier.
+
+
+
v0.3.6
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 SDK O888-64 release; SDK freeze.
+
+
+ Fixes:
+
+
+ Improved support for opacity in the MaxUsdPreviewSurface material.
+
+
+
+
+ Fixed defect causing unexpected evaluation of the stage while manually editing the Stage mask.
+
+
+
v0.3.5
+ What's New:
+
+
+ Update the material target list in USD exporter UI to only list USD Preview Surface, other targets are expected to be enabled part of plugin configurations.
+
+
+ The items in the plugin configuration combobox are now checkable. Multiple elements can be selected or none of them can be selected.
+
+
+ Fixed missing sourceColorSpace, scale and bias settings for Normal maps in the USD Exporter that was casing incorrect normals display in USD. Also, for non-Normal maps, sourceColorSpace will be set to "raw".
+
+
+ Added support for Alembic references in the USD Stage.
+
+
+ Fixes:
+
+
+ Fixed defect where texture paths could be wrong in the USDZ exporter.
+
+
+ Fixed crash when changing a USD Stage's prim's active state via scripting and then changing the display settings of the stage.
+
+
+ Fixed defect where the RenderUSDTimeCode could only be queried at the current frame in the Max timeline.
+
+
+
+
v0.3.4
+
+
+ Fixed crash in USD Exporter when a PrimWriter had changed the USD hierarchy in a way that the exporter did not expect.
+
+
+ The USD Exporter now utilizes the Context to set needed Chaser and Shader settings for plugins. Plugins need to update their implementation to use the new Context feature. This removes the need for the user to turn on multiple options in the exporter for custom plugins.
+
+
+ Support multiple UV channels for offline USD Stage rendering using the fallback method.
+
+
+
v0.3.3
+ What's New:
+
+
+ Updated the USD Exporter to comply with USD standards for not nesting shader nodes.
+
+
+ Added self.GetNodesToPrims() to PrimWriters that will get a map of prims made from nodes in the scene. Helpful for writing associations to prims created from other nodes in the scene.
+
+
+ Added the .version property to the maxUsd Python module to know the USD plugin version.
+
+
+ Added an option in the USD Exporter to not nest Gprims. This option will now be on by default and makes the USD created from 3ds Max conform more to the general USD guidelines.
+
+
+
v0.3.2
+ What's New:
+
+
+ Updated the implementation of Macroscripts in the USD installer to align with how Application Plugins are intended to be packaged.
+
+
+ Chaser registration now supports additional fields: a nice name and description. Improves how chasers are displayed in the USD export UI.
+
+
+
v0.3.1
+ What's New:
+
+
+ Inform user in case of missing requirements for HdStorm
+
+
+ Fixes:
+
+
+ Fixed defects around objects that have non-invertible matrices in the USD Exporter and the USD Stage Object.
+
+
+
+
v0.3.0
+ What's New:
+
+
+ First public beta release of the USD Stage Node to reference USD files directly into 3ds Max.
+
+
+ First public beta release of the Max USD SDK that allows 3rd-party developers to extend the USD Exporter. SDK available in a separate download.
+
+
+ Fixes:
+
+
+ Fixed viewport clipping on USD data with missing extent information.
+
+
+ Fixed python error output to the console on certain USD librairies being initialized.
+
+
+
v0.2.26
+ What's New:
+
+
+ Added a menu item into the File > Reference Menu to load in a USD file as a reference at the scene origin.
+
+
+ Updated the order of the rollouts in the USD Stage object.
+
+
+ Increased the size of the File and Mask buttons in the USD Stage parameters rollout.
+
+
+ Fixes:
+
+
+ Fixed the Animation rollout in a USD Stage Object not matching the width of other rollouts.
+
+
+ Fixed the the "Create Stage from USD File" menu opening while in the USD Stage Object File selection menu and the user presses 'cancel'.
+
+
+ Stage performance improvements.
+
+
+ Bounding box rework.
+
+
+
v0.2.25
+ What's New:
+
+
+ The USD Stage Parameters UI has been cleaned up.
+
+
+ The USD Stage now has a Start and Speed option to set animation offsets and scaling.
+
+
+ USDGeomSubSet, Material and Shader prims are no longer listed when using the USD Stage's Prim Browser.
+
+
+ The Max USD SDK has been updated to expose the MeshConverter to the SDK.
+
+
+ New method `OpenInUsdView` in the `maxUsd` Python module to launch USDView from inside a Python or MAXScript script.
+
+
+ Fixes:
+
+
+ Fixed a crash loading some USD files into the USD Stage node.
+
+
+ Fixed a crash when rendering some USD Stage nodes.
+
+
+
v0.2.24
+ What's New:
+
+
+ Added Python and C++ samples for export chasers in the USD SDK.
+
+
+ Added new Timeline rollout to the USDStageObject. This rollout gives options to offset, limit and scale the animation of the stage.
+
+
+ Added new MAXScript command UsdStageObject.SelectRootLayerAndPrim() that will open a browsing window to select a USD file and a prim path. Returns a string array where element 1 is the path to the USD file and element 2 will return a prim path if one is selected.
+
+
+ Fixes:
+
+
+ Fixed problems picking USD Stage nodes that are instanced.
+
+
+ Fix issue around USDView not opening from exporter when the PATH environment variable contains binaries that are conflicting with the ones from the 3ds Max USD component.
+
+
+ Fixed a performance regression viewing a USD Stage that had pointinstances.
+
+
+ Fixed the Prim Mask list not remembering current selection when opening a USD Stage prim mask dialog and a prim had already been selected.
+
+
+ Fixed a display error in the USD Stage prim browser that would show a blank list.
+
+
+ Fixed an issue that would cause the USD prim browser window to keep growing in size with some actions.
+
+
+ Fixed file references in exported USD to use forward slashes instead of back slashes so that USD files created from Max can load in all USD environments instead of just Windows.
+
+
+
+
v0.2.23
+
+
+ Compatibility release for 3ds Max 2024 Beta O656-63.16 release.
+
+
+ Exposed new UI to configure export chasers from the USD Export dialog.
+
+
+ Normalize export asset reference paths (using forward slashes, for compatibility with mac/unix).
+
+
+ Fixed issue with "Mesh Merge" when display purposes are toggled.
+
+
+ Fixed issue with UsdStageObject material textures not updated as expected when reloading USD layers.
+
+
+ Fixed export to USDZ for 3dsMax scene with USD Stage Objects.
+
+
+ Properly export USD purpose and kind attribute on 3dsMax object with modifiers applied.
+
+
+
v0.2.22
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 Beta O630-63.15 release.
+
+
+ Improvements in material creation speed
+
+
+ Fix minor UI related bugs
+
+
+
v0.2.21
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 Beta O608-63.14 release.
+
+
+ Fix crash on material export for instanced data from usd stage objects
+
+
+ Fix kitchen set instance transforms
+
+
+ Subset consolidation improvement
+
+
+
v0.2.20
+ What's New:
+
+
+ Removed legacy material conversion scripts. Now always using Shader Writers.
+
+
+ Export "chasers" can now be registered for export.
+
+
+ Updated to USD 21.11
+
+
+ Fixed a bug preventing the user to properly set the root layer paths, when creating multiple USD Stage objects.
+
+
+
v0.2.19
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 Beta O561-63.12 release.
+
+
+ There is a new property to the USDStageObject called .PointedPrim which is a string denoting the path to prim currently under the cursor. This property allows tool developers to create scripted tools for working with USD data in the viewport.
+
+
+ Added a new dialog in the command panel for a USD Stage node to browse for a USD file to use as a reference.
+
+
+ Added PrimWriter and ShaderWriter to new USD Extensibility system.
+
+
+
v0.2.18
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 Beta O545-63.11 release.
+
+
+ Fix to 'DisplayGuide' setting does not respect setting during Filo I/O operations.
+
+
+ Fix object picking when changing the system units of the 3dsmax scene / payload render problem.
+
+
+ Make sure to include the mesh's transform when computing the instance transforms.
+
+
+
v0.2.17
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 Beta O522-63.10 release.
+
+
+
v0.2.16
+ What's New:
+
+
+ Improved USD Stage rollup UI, added stage mask functionality and dialog for browsing for a prim.
+
+
+ Added Stage Icon for Stage object in the viewport.
+
+
+ Replace Display Mode checkboxes with DropDown.
+
+
+ Fixed issues relating to Intel dataset:
+
+
+ Fixed badly exported data on corrupt normals.
+
+
+ Fixed crash in the UsdStage object on badly formed USD primvars.
+
+
+
+
+
v0.2.15
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 Beta O479-63.8 release.
+
+
+
v0.2.14
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 Beta O457-63.7 release.
+
+ Fixed a crash when exporting USD Stage objects are USD references.
+
+
+
v0.2.12
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 Beta O408-63.4 release.
+
+
+ The UsdStageObject now properly displays UsdPreviewSurface materials using texture transforms (UsdTransform2d) and texture wrap modes.
+
+
+
v0.2.11
+ What's New:
+
+
+ Compatibility release for 3ds Max 2024 Beta O384-63.3 release.
+
+
+
v0.2.10
+ What's New:
+
+
+ Improved viewport performance with topology changes in USD stage.
+
+
+ Fixed MAXScript not being able to toggle the castShadows parameter of a Photometric Light.
+
+
+
+
v0.2.9
+ What's New:
+
+
+ Added support for wireframe display.
+
+
+ USD Stage now supports computed primvars such as USDSkel.
+
+
+
+
v0.2.8
+ What's New:
+
+
+ Updated 2023 maxsdk dependency version to maxsdk-25.0.0-V951-62.0.
+
+
+
+
v0.2.7
+ What's New:
+
+
+ Add button to explitly reload layers instead of doing it everytime a stage is added.
+
+
+ Hit testing now properly sets up the hit depth, so that the closest object to the camera is prioritized.
+
+
+ Updating materials from python now works as expected. Better reuse material across meshes.
+
+
+ Fixes:
+
+
+ Fixed an issue with USDZ exports where dependency (bitmaps, etc.) paths were not correctly built.
+
+
+
+
v0.2.6
+ What's New:
+
+
+ Added Basic Frustum Culling.
+
+
+ Updated to Python 3.9 and Boost 1.76 in 2023.
+
+
+
v0.2.5
+ What's New:
+
+
+ USD Stage Objects can now be exported as USD references.
+
+
+ Fixes:
+
+
+ Resizing columns in the USD Export dialog's custom UV mapping dialog now limits the amount of resetting possible so that the columns' header text can still be visible.
+
+
+ Fixed an issue when updating primvars via scripting on a USD Stage Object resulting in incorrect mapping.
+
+
+ Fixed an issue with vertex color animation not working as expected on the USD Stage Object.
+
+
+
+
v0.2.4
+ What's New:
+
+
+ Update to USD 21.08 in USD plugins for 3ds Max 2021, 2022, and 2023.
+
+
+ Minimal support for rendering a USD Stage added as default fallback when renderers do not support accessing a USD stage directly.
+
+
+
v0.2.3
+ What's New:
+
+
+ USD Stage Node improvements : normals, UVs and UsdPreviewSurface materials are now properly represented in the viewport.
+
+
+ Improved performance and reliability of hit testing for the USD Stage Node.
+
+
+
v0.2.2
+ What's New:
+
+
+ USD Stage node added to beta build (For 3dsMax 2022, already present in 3dsMax 2023).
+
+
+
v0.2.1
+
+
+ Compatibility release for 3ds Max Beta V685-61.16 release.
+
+
+
v0.2.0
+ Fixes:
+
+
+ Added UI to configure mapped channels export to USD primvars.
+
+
+ Update labels and tooltips in the USD export UI.
+
+
+ Disabled export of transparency maps, as there is no support in USD preview surface.
+
+
+
+
v0.1.8
+ What's New:
+
+
+ New MAXScript function USDImporter.ConvertUsdMesh() to convert a USD Mesh Prim to a TriMesh, using the stage cache.
+
+
+ Photometric Sun Positioner exports as a simple UsdLuxDistantLight (sun orientation + color and intensity).
+
+
+ Greatly reduce the export time of 3dsMax scenes containing many nodes.
+
+
+ Fixes:
+
+
+ Fixed an issue with bad transforms when round-tripping 3dsMax scenes with instances (3dsMax -> Usd -> 3dsMax).
+
+
+
+
v0.1.7
+ What's New:
+
+
+ USD Importer has improved support for lights as Photometric Lights.
+
+
+ Exporting very large triangle mesh data is now as much as 50% faster than previously in some cases.
+
+
+
v0.1.6
+ What's New:
+
+
+ Export Shape objects to linear UsdGeomBasisCurves.
+
+
+
v0.1.5
+ What's New:
+
+
+ Added help context to USD Import and Export Dialogs.
+
+
+ USD Export options added to choose mesh format type to help speed up exports as well as give users control of whether their geometry should enforce polygon geometry or tris. Previously, all exports were done as Polygon which is slower in export time and performance in USD View. Exporting as tris exports faster and offers better viewport performance in USD View.
+
+
+ Photometric lights now export with all mappable properties into USD.
+
+
+ Update to USD 21.05 in USD plugins for 3ds Max 2021, 2022, and 2023.
+
+
+ Fixed broken sample/frame UI control.
+
+
+
v0.1.4
+ What's New:
+
+
+ Compatibility release for 3ds Max Beta V522-61.10 release
+
+
+
v0.1.3
+ What's New:
+
+
+ Python module PyOpenGL is now preinstalled to facilitate the use of the USDView tool (user must run the RunUsdView.bat script to benefit from this preinstall).
+
+
+ Added new option to open exported USD files in USD View after export.
+
+
+ USD exports bone objects to the non-renderable purpose (USDpurpose=guide).
+
+
+ USD Exporter now has an option to skip hidden geometry from export. There is also an option to assign USD Visibility to exported prims based on their visibility in the Max scene.
+
+
+
v0.1.2
+ What's New:
+
+
+ Transform and Vertex animations now supported in USD Exporter.
+
+
+ Added enhanced support for both physical and standard cameras in the importer.
+
+
+
v0.1.1
+ What's New
+
+
The bitmap texture's Tile, Offset and W Angle information now imports and exports correctly.
+
Added enhanced support for both physical and standard cameras export.
+
Added enhanced support for USD camera import. Imports to physical cameras.
+
+
v0.1.0
+ Fixes:
+
+
Bad USD Api call causes significant export slow down.
+
Undefined string metadata crash.
+
Metadata property names are case sensitive.
+
Suspend hold for import/export to avoid restore objects being created.
+
+
v0.0.28
+ What's New:
+
+
+ An export option of RootPrimPath can be used to create a root scope for export.
+
+
+ USDExporter.RootPrimPath = string (a valid USD prim path)
+
+
+ Default is "/" - this is the USD default, the pseudo-root and the previous
+ behavior.
+
+
+ Specified root is defined as a scope unless "/".
+
+
+
+
+ Pivots are conserved in all cases with World Space Modifiers.
+
+
Inverse of node transform is baked into the geometry to bring points back into local space.
+
+
+
+ Fixes:
+
+
Fixed import of normals and uvs for leftHanded geometry.
+
+
v0.0.27
+ What's New:
+
+
+ All USD DLLs are prefixed by 3dsmax_ to prevent colliding with other plugins which may ship
+ with USD (i.e. Omniverse).
+
+
+ Textures can use any map channel for import and export.
+
+
+ Primvar channel mapping is respected on import and export.
+
+
+
+
+ Relative paths are now used when exporting textures.
+
+
+ Fixes
+
Installer ProductCode is unique for each MSI.
+
+
v0.0.26
+ What's New:
+
+
+ Added tooltips to import/export dialogs.
+
+
+ Fixes:
+
+
Prevent Python native types from generating log spam when comparing types.
+
Maxcript enum option validation.
+
Default prim should not use prototypes.
+
Fixes/Improvements for handling of world space modifiers on export.
+
Prims are not instanced if a single instance is exported.
+
Save import UI options.
+
Fixes for log spam with material imports
+
USD filename inside a USDZ file kept the same as the USDZ filename.
+
Instances with children not exporting to usd instances properly.
+
+
v0.0.25
+ What's New:
+
+
Added support for OSLBitmap and UberBitmap.osl bitmaps.
+
+
By default, USDImporter now imports USDTextures as UberBitmap and channels are connected as
+ specified in USD.
+
+
+
+ Fixes:
+
+
USDExporter export frame can now be set to a negative number in export UI.
+
Installers are no longer signed with expired certificate.
+
Export Selected with instance resulting syntax error in USD file.
+
Export Selected could result in name conflicts.
+
Extra crease data properties are no longer created after import and re-export.
+
Material issues:
+
+
Roughness is not properly set on export with physical materials when rougness_inv is true.
+
Cutout or opacity map is not mapped on exported in either physical material or VRay material.
+
Exporter is mapping bump maps to the normal input for UsdPreviewSurface.
+
+
+
v0.0.24
+
+
+
Update to USD 21.02 in USD plugins for 3ds Max 2021, 2022 and 2023.
+
UsdMtlx library is available in USD plugin for 3ds Max 2021, 2022 and 2023 in
+ Contents\plugin\usd\usdMtlx folder.
+
+
+ The #onExportComplete and #onImportComplete callbacks have been updated to
+ include the import/export options and the usd filename:
+
+
+
+ Maxscript:
+
+
func myCallback <int>stageId <struct>conversionInfo <string>usd_filename <IUSDImportOptions or IUSDExportOptions>options
+
Removed an error message dialog when cancelling import.
+
+
+
Fixed an issue where object pivots were not properly imported.
+
+
+
v0.0.23
+ What's New:
+
+
+
Import and Export UI has been updated, exposing missing options.
+
+
+
Material import can be enabled/disabled through UI.
+
Material boolean option added to Import/Export options struct.
+
+
+
Export time option added to UI and ExportOptions struct:
+
TimeMode = #current exports from the current time code
+
TimeMode = #explicit exports from the time frame specified by .TimeFrame.
+
TimeMode only allows #explicit or #current and not other values (like a random integer or #name)
+
If a non-default (non zero) value is set for TimeFrame, and #current is used, a warning is written to
+ the log.
+
+
+
v0.0.22
+ What's New:
+
+
+ InferMainUvPrimvar option no longer exists, it is replaced by
+ .ImportUnmappedPrimvars
+
+
+ If false, only primvars with an explicit mapping are imported.
+
+
+ If true :
+
+
+ Channel 1 (main UV) is filled with a primvar of type (prioritized in this order) :
+
+
+ TexCoord2fArray
+
+
+ TexCoord2dArray
+
+
+ TexCoord2hArray
+
+
+ TexCoord3fArray
+
+
+ TexCoord3dArray
+
+
+ TexCoord3hArray
+
+
+ Float2Array
+
+
+ Double2Array
+
+
+ Half2Array
+
+
+
+
+ Channel 0 (VertexColor) if filled with a primvar of type (in this order):
+
+
+ Color3fArray
+
+
+ Color3dArray
+
+
+ Color3hArray
+
+
+
+
+ All other unmapped (and still unused) primvars of dimension <= 3 are
+ imported to unused channels 2+.
+
+
For example, if channels 2, 4, 6 are already used by explicit mappings, unmapped
+ primvars that are not the main UV or VertexColor are imported to 3,5,7,etc.
+ TexCoord types are imported to lower channels.
+
+
+
+
+
+
+
+
+ Space warp is now supported when exporting to USD.
+
+
+ Expose Log interface
+
+
Added Log command to USDExporter and USDImporter to log
+ errors/warnings/info to USD log for import and export during callbacks.
When set to attribute, normals are exported as attribute instead of primvar. If #none, no normals exported.
+
+
+
+ Fixes:
+
+
Fixed a crash when passing undefined to SetPrimvarChannelMapping
+
Fixed a performance issue when importing large usd scenes in 2021.3 and above.
+
+
Too frequent updates to max's status bar during import was causing huge slowdown in import time
+
+
+
+
v0.0.20
+ What's New:
+
+
+
TimeCode now can be #startTime, #endTime or #explicit.
+
+
+
Default if importing at #startTime for both UI and script - they should now behave the same, drag and drop as well, as long as options are not touched with scripting.
+
+
+
Using #explicit without specifying a value for TimeCodeValue fails with an error.
+
+
+
If #explicit, time value (the old .TimeCode) is specified via .TimeCodeValue
+
+
+
Using #startTime or #endTime with a value specified for TimeCodeValue logs a warning in the log (#warn level).
+
+
+
The actual time code used for import is always logged as an info (#info).
+
+
+
+
+
Users can now import from a USD stage cache by providing the stageCacheId
+
+
+ Maxscript:
+
+ pyUsd = python.import("pxr.Usd")
+ pyUsdUtils = python.import("pxr.UsdUtils")
+ local stage = pyUsd.Stage.Open(@"C:\Kitchen_set.usd")
+ local stageCache = pyUsdUtils.StageCache.Get()
+ stageCache.Insert(stage)
+ local stageId = (stageCache.GetId(stage)).ToLongInt()
+ local importOptions = USDImporter.CreateOptions()
+ USDImporter.ImportFromCache stageId importOptions:importOptions
+
+
USD import no longer changes 3ds Max viewport quality settings.
+
+
+
Shader and Material prim will no longer create point helper node on import.
+
+
+
Fixed a crash when import USD scene on non-English 3ds Max.
+
+
+
v0.0.19
+ What's New:
+
+
All known USD files import with their UV data set intact, such that uv#, uv_#, st# and st_# are handled by default.
+
The main uv channel is inferred if .InferMainUvPrimvar is set to true. It is not otherwise.
+
texCoord2f and texCoord3f primvar types are prioritized when inferring the main UV primvar. Float and double array primvars can also be used as UVs if found.
+
When the main UV channel is inferred and not explicitly mapped, an info message is logged.
+
Visibility is now correctly computed on import.
+
+
v0.0.18
+ Fixes:
+
+
Fix crash with animated topology in USD Stage.
+
Fix opacity threshold when round-tripping materials.
+
Fix multi-material binding on instanced prims.
+
Fix export of xref materials.
+
+
v0.0.17
+ What's New:
+
+
Export texture paths as absolute paths until relative paths can be properly supported.
+
+ Fixes:
+
+
+ Disable point helper cross display on import.
+
+
+
v0.0.16
+ What's New:
+
+
Update to USD 20.11.
+
Use point helpers instead of dummies for groups and empty transforms.
+
+ Fixes:
+
+
Fixes for round-tripping material Ids.
+
Fix crash when exporting usd without specifying extension.
+
Fix naming and visibility issues when importing usd with instances.
+
+
v0.0.15
+ What's New:
+
+
+ USD Exporter will convert VRayMtl to USDPreviewSurface on export.
+
+
Importing USD scenes with instanced prims create instances in Max where possible.
+
Users can import USD files with custom options without affecting the UI options.
+ If exportOption is undefined, default export options are used
+
+
If contentSource is undefined, we are exporting from the root node
+
If contentSource is set to #selected, we are exporting from the selected
+ nodes only like before
+
If contentSource is set to #nodeList, the command needs an extra argument
+ nodeList to be defined which will behave like the selected option, but without changing
+ the scene selection.
+
+
USD exports quads to triangles when vertices are non-coplanar when
+ Preserve Edge Orientation is
+ enabled.
+
+ Fixes:
+
+
+ Material IDs are now correctly exported with their appropriate materials.
+
+
Fix for round-tripping opacityThreshold
+
+
v0.0.14
+ What's New:
+
+
+ Added a new experimental USD Stage node for 3ds Max 2022 and higher. Use this node to
+ reference USD scenes in a 3ds Max scene.
+ Known issues :
+
+
All normals are computed smooth normals.
+
Animated topology may cause crashes.
+
No support for skeletal animations / computed geometry & primvars.
+
No Frustum culling.
+
No consolidation within render nodes (affects viewport performance).
+
No UVS / materials.
+
No Wireframe viewport support
+
No rendering support
+
+
+
+ Added new USDPreviewSurface scripted interface for Physical Material. This provides a
+ 1-to-1 implementation of USDPreviewSurface to simplify interop with USD.
+ USDPreviewSurface is the default material used for importing USD scenes with materials.
+
Known issues:
+
Export from PBRSpecGloss does not set UseSpecularWorkflow to true for
+ UsdPreviewSurface.
+
Not fully compatible with 3ds Max 2018.
+
Imports in 2019 fail to add textures.
+
+
+
+
+ USDZ added as an export option.
+
+
+ Added Maxscript export API to export without affecting global state.
+
+
+ Added USDExporter.ExportFile exportPath exportOptions:exportOptions maxscript
+ command
+ which has an optional argument for export options.
+
+
+ To create a new export options object: exportOptions = USDExporter.CreateOptions()
+
+
+ If not specified, default export options are used. Default options are NOT the same as UI export
+ options.
+
+
+ UI export options are accessible: uiExportOptions = USDExporter.UIOptions
+
+ Note: It is recommended to disable the MacroRecorder during import. When importing
+ many nodes with metadata, USDImporter can output a large amount of MacroRecorder text which affects
+ performance.
+
+
+
+
+ Transform offset is no longer exported to a child prim.
+
+
+
v0.0.10
+ What's New:
+
+
+ Added maxscript callbacks for UsdExporter/UsdImporter :
+ #onImportComplete
+ and #onExportComplete.
+
+ These callbacks are triggered before closing or writing the USD stage to disk, and are passed the
+ Usd
+ stageId.
+
+ This allows a scripter to make additional imports/exports, such as special-case metadata handling,
+ custom
+ classes, etc.
+
+ The Usd stage can be accessed during export and import time by stageId:
+
+
+ If id not specified then all callback ids are unregistered.
+
+
+ If event not specified, callbacks are unregistered for all event types.
+
+
+ USDExporter event enums: #onExportComplete
+
+
+ USDImporter event enums: #onImportComplete
+
+
+
+
+
+
+ GetCallbacks()
+
+
+ Returns a dictionary to access all the registered callbacks.
+
+
+ The key is the callback type (for now only the #onImportComplete or
+ #onExportComplete, and the value is another dictionary of callback IDs
+ (Name type)
+ to callback functions.
+
+ USDImport now adds standard USD metadata fields as custom attributes. These custom attributes export to
+ USD when
+ found on exported nodes:
+
+
+ hidden as usd_hidden
+
+
+ kind as usd_kind
+
+
+ purpose as usd_purpose
+
+
+
+
+ By default, vertex colors (channel 0) are mapped to primvar:vertexColor (a generic primvar)
+ instead of
+ primvar:displayColor (well known usd primvar).
+
+
+ Output the wire color to primvar:displayColor, which matches the intent of that
+ primvar
+ (fast display).
+
+
+ Change the import mappings accordingly to recognize vertexColor on import as well / set
+ wireColor from
+ displayColor. If not a constant primvar (i.e. different colors per vertex), use the first value.
+
+
+ All of this can still be overridden with the current system for configuring mappings, i.e.
+ should only
+ export wireColor to displayColor if no explicit mapping is set for
+ displayColor.
+ Same for import. User can always explicitly import the values to a channel if needed via script.
+
+
+
+
+ Fixes:
+
+
+ Vertices that don't belong to any faces are removed upon import.
+
+
+
v0.0.9
+ What's New:
+
+
+ Mesh materialIds are imported and exported as GeomSubset prims. MultiSub material slot
+ names are used if possible, otherwise the materialId is converted to a string: _1_,
+ _2_, etc.
+
+
+ Meshes now import as EditablePoly instead of PolyMesh
+
+
+ USDImport now adds standard USD metadata fields as custom attributes. These custom attributes export to
+ USD when found on exported nodes:
+
+
hidden as usd_hidden
+
kind as usd_kind
+
purpose as usd_purpose
+
+
+
+ Added Logging support
+
+
+ Logging is disabled by default, and no log file is created.
+
+
+ Log path can be specified (import/export) with
+ USDImporter.LogPath/USDExporter.LogPath - default to Max temp folder
+
+
+ Log level can be specified with
+ USDImporter.LogLevel/USDExporter.LogLevel - default #off
+
+
+ #info -> All is logged.
+
+
+ #warn -> Only warnings and errors are logged.
+
+
+ #error -> Only errors are logged.
+
+
+ #off -> Logging disabled.
+
+
+
+
+ Logging is set to 5 rotating files with a 200mb max size.
+
+
+
+
+ Fixes:
+
+
Objects imported from Y-Up scenes were facing the wrong direction. They are now rotated correctly.
+
UsdImport was creating "standard" materials for each mesh. Import now only sets wireColor - no materials are created.
+
+
v0.0.8
+ What's New:
+
+
Added mappings from USD Primvars to 3ds Max mapped channels on import:
+
+
primvars:displayColor to Channel 0/Vertex color.
+
primvars:st or primvars:map1 to the main UV channel.
+
primvars:mapN to channel N, up to 99.
+
primvars:mapShading to the shading channel (MAP_SHADING, -1)
+
primvars:displayOpacity to the alpha channel (MAP_ALPHA, -2)
+
+ All primvar to channel mappings are configurable via Maxscript. It should be possible to import any USD
+ primvar to any 3ds Max channel - as long as the primvar’s data type is compatible.
+
+
+ Support for upAxis in USD:
+
+
3ds Max only support z-up axis. Therefore if y-up axis is used on import or export, 3ds Max will
+ automatically convert the up-axis.
+
On export, if y-up axis is specified, the conversion from z-up to y-up will be saved inside the
+ root prims transform matrix.
+
On import, if the USD stage is set up with y-up axis, the axis conversion will be automatically
+ done to z-up axis.
+
+
+
+
v0.0.7
+ What's New:
+
+
+ For 3ds Max 2021 and newer, USD python bindings are provided for Python3.
+
+
+ DefaultPrim is now set on export to the first valid prim.
+
+
+ Added default map channels export defaults:
+
+
+ Vertex color exports by default to primvars:displayColor.
+
+
+ Vertex alphas export by default to primvars:displayOpacity.
+
+
+ UVs are exported to primvars:map1 to primvars:map99.
+
+
+ Shading map -1 is exported to primvars:mapShading.
+
+
+
+
+ The following interfaces have been added to USDExporter:
+
+ <void>SetChannelPrimvarMappingDefaults()
+
Reset mapping to default.
+
Default mapping is as follows, with autoExpandType set to true:
+
+
+ Vertex color (0) exports by default to primvars:displayColor as
+ #color3fArray.
+
+
+ UVs (1-99) are exported to primvars:map1 to
+ primvars:map99 as #textCoord2fArray.
+
+
+ Vertex alphas (-2) export by default to primvars:displayOpacity as
+ #floatArray.
+
+
+ Shading map values (-1) are exported to primvars:mapShading as
+ #color3fArray.
+
+ Set the target primvar name and type per channel.
+
When AutoExpandType is true, type will expand depending on input values. For example, if
+ type is #textCoord2fArray and the texture coordinate channel has non-zero
+ values
+ in W, then the type will expand to #textCoord3fArray.
USDImporter.InitialLoadSet : can be #loadAll VS #loadNone
+ (whether to load payloads or not)
+
USDImporter.StageMask : Array of Strings (stage mask paths to use)
+
USDImporter.SetDefaults() -> Resets to these default options :
+
+
TimeCode = undefined
+
InitialLoadSet = #loadAll
+
StageMask = "/" (imports everything)
+
+
Options set via USDImporter.XYZ are not reflected in the UI, nor should they be
+ affected by imports going through the UI.
+
+
+
Export object Transforms:
+
+
USDExporter.BakeObjectOffsetTransform : Option to bake the object offset in the
+ geometry.
+
When not baking the transforms, only use the extra xform when necessary, i.e. the offset is not
+ the identity matrix.
+
Groups/helper object transforms are properly exported. When you filter out meshes for example,
+ you really only filter out the meshes - not its hierarchy.
+
+
+
+ Fixes:
+
+
+ Fix unused vertices crash. Do not export or import invalid faces.
+
+
+
v0.0.4
+ What's New:
+
+
Adds maxscript exposure to the Export plugin - the same options as already in the UI.
+
+
From script, not allowed to mismatch the extension VS the output format.
+
UI / Global options state stay synchronised - only saved in memory so the max session.
+
Changes made from the UI are only applied if the Import goes through (I.e. not if you cancel or
+ close from the export window).
+
+
+
Normals support - Default behavior is exporting normals if specified OR smoothing groups exist.
+
+
v0.0.3
+ What's New:
+
+
Added export of Photometric Lights to USD Lights, with a limited set of attributes. Supported
+ Photometric Light shapes include Point, Line, Rectangle, Disc, Sphere and Cylinder.
+
Added export of Standard Cameras to USD Cameras, with a limited set of attributes.
+
Added option to import USD content from a specific time code via a slider on the Import dialog.
+
+
Added options to the Export dialog, allowing:
+
+
Export of USD content in ASCII (USDA) or binary format (USDC).
+
Selecting the up axis of the exported USD Stage.
+
Export of meshes, lights and/or cameras.
+
+
+
+
v0.0.2
+ What's New:
+
+
Support exporting USD content via the File > Export > Export... menu.
+
+
Added some USD diagnostic information when displaying the USD import dialog. This should help
+ identifying potential issues when importing content – thanks for reporting any issues!
+
Added limited import of USD Light attributes. Supported attributes for SphereLight,
+ RectLight, DiskLight and CylinderLight include color,
+ enableColorTemperature, colorTemperature, intensity,
+ shadow:enable, shadow:color, radius, width,
+ height, length and shaping:ies:file.
+
Added limited import of USD Camera attributes. Supported attributes include projection,
+ clippingRange, focalLength and horizontalAperture
+
+
v0.0.1
+ What's New:
+
+
Initial release! 🎉
+
Support importing USD content via the File > Import > Import... menu.
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdChecker.bat b/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdChecker.bat
new file mode 100644
index 0000000..a77eaf3
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdChecker.bat
@@ -0,0 +1,16 @@
+::
+:: Copyright 2023 Autodesk
+::
+:: Licensed under the Apache License, Version 2.0 (the "License");
+:: you may not use this file except in compliance with the License.
+:: You may obtain a copy of the License at
+::
+:: http://www.apache.org/licenses/LICENSE-2.0
+::
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+::
+CALL "%~dp0RunUsdTool" "%~dp0UsdChecker" %*
diff --git a/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdTool.bat b/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdTool.bat
new file mode 100644
index 0000000..48f023a
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdTool.bat
@@ -0,0 +1,133 @@
+::
+:: Copyright 2023 Autodesk
+::
+:: Licensed under the Apache License, Version 2.0 (the "License");
+:: you may not use this file except in compliance with the License.
+:: You may obtain a copy of the License at
+::
+:: http://www.apache.org/licenses/LICENSE-2.0
+::
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+::
+@ECHO OFF
+REM Utility script to run USD tools from an installed 3dsMax USD plugin
+REM This script tries to locate a usable python.exe and then runs the underlying USD tool
+REM with the UsdToolWrapper.py (python wrapper script will properly set the PATH environment variable)
+
+REM Check passed arguments. If none, display help/usage.
+SET ARG_COUNT=0
+setlocal enabledelayedexpansion
+FOR %%x in (%*) DO (
+ SET "argVec[!ARG_COUNT!]=%%x"
+ SET /A ARG_COUNT+=1
+)
+setlocal disabledelayedexpansion
+
+IF %ARG_COUNT%==0 (
+ REM If no arguments are specified, output help / usage.
+ ECHO "USAGE:"
+ ECHO "RunUsdTool.bat [--python-exe C:path/to/3dsmax/python.exe] "
+ ECHO "Ex : "
+ ECHO "RunUsdTool.bat usdview --python-exe "C:/Program Files/Autodesk/3ds Max 2023/python/python.exe" path_to_usd.usda"
+ ECHO "RunUsdTool.bat usdview path_to_usd.usda"
+ ECHO "RunUsdTool.bat usdcat -o output_path.usd --usdFormat usda path_tp_usdz_file.usdz"
+ EXIT /b
+)
+
+REM Clears the PATH to avoid any conflicts. Only keep what we know we need.
+SET PATH="C:\windows\system32"
+
+SETLOCAL
+
+PUSHD %~dp0
+SET SCRIPT_DIR="%CD%"
+POPD
+
+REM Detect the --python-exe switch, it allows to specify the python executable to be used.
+REM Typically it should be the python exe matching the 3dsMax version for which
+REM the 3dsMax USD plugin was compiled.
+IF "%~2"=="--python-exe" (
+ goto PYTHON_ARG
+)
+
+REM Otherwise, try looking at registry keys to find the python executable.
+REM In this case, the USD tool's args start at %2.
+setlocal enabledelayedexpansion
+FOR /L %%i in (1,1,%ARG_COUNT%) do (
+ SET "ARGS=!ARGS!!argVec[%%i]!"
+)
+setlocal disabledelayedexpansion
+if not x%SCRIPT_DIR:2022=%==x%SCRIPT_DIR% goto MAX2022_REGISTRY
+if not x%SCRIPT_DIR:2023=%==x%SCRIPT_DIR% goto MAX2023_REGISTRY
+if not x%SCRIPT_DIR:2024=%==x%SCRIPT_DIR% goto MAX2024_REGISTRY
+if not x%SCRIPT_DIR:2025=%==x%SCRIPT_DIR% goto MAX2025_REGISTRY
+if not x%SCRIPT_DIR:2026=%==x%SCRIPT_DIR% goto MAX2026_REGISTRY
+REM Out of options...
+goto no_python
+
+:PYTHON_ARG
+SET PYTHON_EXE=%~3
+REM In this case, the USD tool's args start at %4.
+
+set ALL_ARGS=%*
+SET SKIPPED_ARGS=%1 %2 %3
+SETLOCAL enabledelayedexpansion
+set ARGS=!ALL_ARGS:%SKIPPED_ARGS%=!
+SETLOCAL DisableDelayedExpansion
+goto run_python_exe
+
+:MAX2022_REGISTRY
+FOR /F "tokens=2,* skip=2" %%a in ('reg query "HKLM\SOFTWARE\Autodesk\3dsMax\24.0" /V InstallDir') do (
+ SET MAX_DIR=%%b
+)
+SET PYTHON_EXE=%MAX_DIR%python37\python.exe
+goto run_python_exe
+
+:MAX2023_REGISTRY
+FOR /F "tokens=2,* skip=2" %%a in ('reg query "HKLM\SOFTWARE\Autodesk\3dsMax\25.0" /V InstallDir') do (
+ SET MAX_DIR=%%b
+)
+SET PYTHON_EXE=%MAX_DIR%Python\python.exe
+goto run_python_exe
+
+:MAX2024_REGISTRY
+FOR /F "tokens=2,* skip=2" %%a in ('reg query "HKLM\SOFTWARE\Autodesk\3dsMax\26.0" /V InstallDir') do (
+ SET MAX_DIR=%%b
+)
+SET PYTHON_EXE=%MAX_DIR%Python\python.exe
+goto run_python_exe
+
+:MAX2025_REGISTRY
+FOR /F "tokens=2,* skip=2" %%a in ('reg query "HKLM\SOFTWARE\Autodesk\3dsMax\27.0" /V InstallDir') do (
+ SET MAX_DIR=%%b
+)
+SET PYTHON_EXE=%MAX_DIR%Python\python.exe
+goto run_python_exe
+
+:MAX2026_REGISTRY
+FOR /F "tokens=2,* skip=2" %%a in ('reg query "HKLM\SOFTWARE\Autodesk\3dsMax\28.0" /V InstallDir') do (
+ SET MAX_DIR=%%b
+)
+SET PYTHON_EXE=%MAX_DIR%Python\python.exe
+goto run_python_exe
+
+:no_python
+ ECHO "Could not find max python based on registry or argument."
+ goto end
+)
+
+:run_python_exe
+PUSHD %SCRIPT_DIR%
+ECHO running %PYTHON_EXE% with args:%ARGS%
+
+REM "Reset" QTDIR env variable to avoid usdview QT crash occurring in dev environment
+set QTDIR=
+"%PYTHON_EXE%" UsdToolWrapper.py %1 %ARGS%
+
+:end
+ENDLOCAL
+POPD
diff --git a/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdView.bat b/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdView.bat
new file mode 100644
index 0000000..ab48428
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdView.bat
@@ -0,0 +1,16 @@
+::
+:: Copyright 2023 Autodesk
+::
+:: Licensed under the Apache License, Version 2.0 (the "License");
+:: you may not use this file except in compliance with the License.
+:: You may obtain a copy of the License at
+::
+:: http://www.apache.org/licenses/LICENSE-2.0
+::
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+::
+CALL "%~dp0RunUsdTool" "%~dp0UsdView" %*
diff --git a/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdZip.bat b/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdZip.bat
new file mode 100644
index 0000000..002859f
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/bin/RunUsdZip.bat
@@ -0,0 +1,16 @@
+::
+:: Copyright 2023 Autodesk
+::
+:: Licensed under the Apache License, Version 2.0 (the "License");
+:: you may not use this file except in compliance with the License.
+:: You may obtain a copy of the License at
+::
+:: http://www.apache.org/licenses/LICENSE-2.0
+::
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+::
+CALL "%~dp0RunUsdTool" "%~dp0UsdZip" %*
diff --git a/src/ApplicationPlugins/usd-component/Contents/bin/UsdToolWrapper.py b/src/ApplicationPlugins/usd-component/Contents/bin/UsdToolWrapper.py
new file mode 100644
index 0000000..9e7a84d
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/bin/UsdToolWrapper.py
@@ -0,0 +1,161 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+'''
+This file is simply a wrapper to handle setting the windows PATH to locate USD binaries
+and python bindings. It also does some basic verifications such as handling missing PyOpenGL
+dependency.
+Written to work in an installed 3dsMax USD Plugin context or a standalone zip structure.
+'''
+from __future__ import print_function
+import sys, os
+
+scriptPath = os.path.dirname(os.path.realpath(__file__))
+
+def getUsdPythonBindingsPath():
+ usdPythonPath = os.path.join(scriptPath, "python")
+ testPaths = [usdPythonPath, os.path.join(scriptPath, "lib", "python")]
+ for tp in testPaths:
+ if os.path.exists(tp):
+ usdPythonPath = tp
+ break
+ return usdPythonPath
+
+def getUsdBinPath():
+ usdBinPath = scriptPath
+ if not os.path.exists(os.path.join(usdBinPath, "3dsmax_usd.dll")):
+ usdBinPath = os.path.join(scriptPath, "bin")
+ return usdBinPath
+
+def getUsdLibPath():
+ usdLibPath = None
+ if os.path.exists(os.path.join(scriptPath, "lib")):
+ usdLibPath = os.path.join(scriptPath, "lib")
+ elif os.path.exists(scriptPath + r"\..\lib"):
+ usdLibPath = scriptPath + r"\..\lib"
+ return usdLibPath
+
+def getMtlxLibPath():
+ mtlxLibPath = scriptPath + r"\..\libraries"
+ return mtlxLibPath
+
+def addUsdToolPythonBindingsToPythonPath():
+ usdToolPythonPath = os.path.join(scriptPath, "python-usd-tool-packages")
+ if os.path.exists(usdToolPythonPath):
+ if not usdToolPythonPath in sys.path:
+ sys.path.insert(0, usdToolPythonPath)
+
+def addUsdPythonBindingsToPythonPath():
+ usdPythonPath = getUsdPythonBindingsPath()
+ # pre-pending to avoid override issues due to PYTHONPATH being set
+ # only for standalone. if using installed max python3, PYTHONPATH is
+ # not taken into account as the python.exe was overwritten with a
+ # custom version in 2021+
+ if not usdPythonPath in sys.path:
+ sys.path.insert(0, usdPythonPath)
+
+def addUsdBinariesToWindowsPath():
+ usdBinPath = getUsdBinPath()
+ usdLibPath = getUsdLibPath()
+ sysPath = os.environ["PATH"]
+ sysPath = usdBinPath + ";" + sysPath
+ if usdLibPath is not None:
+ sysPath = usdLibPath + ";" + sysPath
+ os.environ["PATH"] = sysPath
+
+def addMtlxLibToPath():
+ usdMtlXLibPath = getMtlxLibPath()
+ from pxr import Usd
+ ver = Usd.GetVersion()
+ mtlxLibEnvVarName = None
+ if ver == (0,21,11): # Only version we support with the older env var name
+ mtlxLibEnvVarName = 'PXR_USDMTLX_STDLIB_SEARCH_PATHS'
+ else:
+ mtlxLibEnvVarName = 'PXR_MTLX_STDLIB_SEARCH_PATHS'
+ # Get the current value of the environment variable, or set it to a default value (empty)
+ env_var_value = os.getenv(mtlxLibEnvVarName, '')
+ env_var_value = usdMtlXLibPath + os.pathsep + env_var_value
+ os.environ[mtlxLibEnvVarName] = env_var_value
+
+def validateUsdViewRequirements():
+ status = True
+ if sys.version_info.major == 3 and sys.version_info.minor == 11:
+ try:
+ import PySide6
+ except ImportError:
+ print("WARN: PySide6 is not installed, USDView will not work. You can install pip and PySide6 with the scripts below:")
+ print('"{}" -m ensurepip --upgrade --user'.format(sys.executable))
+ print('"{}" -m pip install --user PySide6==6.5.3'.format(sys.executable))
+ status = False
+ else:
+ try:
+ import PySide2
+ except ImportError:
+ print("WARN: PySide2 is not installed, USDView will not work. You can install pip and PySide2 with the scripts below:")
+ print('"{}" -m ensurepip --upgrade --user'.format(sys.executable))
+ print('"{}" -m pip install --user PySide2==5.15.1'.format(sys.executable))
+ status = False
+
+ try:
+ import OpenGL
+ except ImportError:
+ print("WARN: PyOpenGL is not installed, USDView will not work. You can install pip and PyOpenGL with the scripts below:")
+ print('"{}" -m ensurepip --upgrade --user'.format(sys.executable))
+ print('"{}" -m pip install --user PyOpenGL==3.1.5'.format(sys.executable))
+ status = False
+
+ return status
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ # NOTE: can build a UI here with pyside2 instead of cli usage
+ print("ERROR: Incorrect arguments set, first argument should be name of usd tool such as `usdcat`")
+ exit()
+
+ # make sure usd python bindings path is in `sys.path`
+ addUsdPythonBindingsToPythonPath()
+ addUsdToolPythonBindingsToPythonPath()
+
+ # make sure usd binaries are searchable in windows PATH
+ addUsdBinariesToWindowsPath()
+
+ # make sure MtlX Libraries are searchable for USD
+ addMtlxLibToPath()
+
+ # check if PyOpenGL is installed (only for py3+ and usdview)
+ if not sys.version_info.major == 2:
+ if not validateUsdViewRequirements() and sys.argv[1] == "usdview":
+ print("ERROR: missing required dependencies for running usdview")
+ exit()
+
+ if sys.version_info.major == 3 and sys.version_info.minor == 9 and sys.version_info.micro >= 7:
+ os.add_dll_directory(os.path.dirname(os.path.dirname(sys.executable)))
+
+ cmd = sys.argv[1]
+ newArgs = sys.argv[1: len(sys.argv)]
+ sys.argv = newArgs
+
+ filename = cmd
+ if not os.path.exists(filename):
+ filename = "./bin/" + cmd
+
+ import runpy
+ try:
+ runpy.run_path(filename, run_name='__main__')
+ except ImportError:
+ print("The 'PATH' environment variable contains a conflicting path with the 3ds Max USD component binaries.\nReversing the 'PATH' order and trying a second time to launch '{0}'.".format(filename))
+ os.environ["PATH"] = ';'.join(reversed(os.getenv('PATH', '').split(os.pathsep)))
+ runpy.run_path(filename, run_name='__main__')
diff --git a/src/ApplicationPlugins/usd-component/Contents/cui/usdMenu.mnx b/src/ApplicationPlugins/usd-component/Contents/cui/usdMenu.mnx
new file mode 100644
index 0000000..0ca29d6
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/cui/usdMenu.mnx
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/en-US/registerMenu.mcr.res b/src/ApplicationPlugins/usd-component/Contents/scripts/en-US/registerMenu.mcr.res
new file mode 100644
index 0000000..0cb03ac
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/en-US/registerMenu.mcr.res
@@ -0,0 +1,4 @@
+USD_STAGE_MENU_ITEM = "USD Stage..."
+USD_STAGE_TOOLTIP = "Create a USD Stage from File"
+USD_EXPLORER_MENU_ITEM = "USD Explorer..."
+USD_EXPLORER_TOOLTIP = "Opens the USD Explorer"
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/en-US/registerMenu.ms.res b/src/ApplicationPlugins/usd-component/Contents/scripts/en-US/registerMenu.ms.res
new file mode 100644
index 0000000..3f5fd45
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/en-US/registerMenu.ms.res
@@ -0,0 +1,5 @@
+USD_STAGE_MENU_ITEM = "USD Stage..."
+FILE_MENU_MAIN = "&File"
+REFERENCE_MENU_ITEM = "&Reference"
+USD_EXPLORER_MENU_ITEM = "USD Explorer..."
+TOOLS_MENU_ITEM = "&Tools"
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/USDMaterialAttributeHolder.ms b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/USDMaterialAttributeHolder.ms
new file mode 100644
index 0000000..2e09873
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/USDMaterialAttributeHolder.ms
@@ -0,0 +1,94 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+global UsdMaterialAttributeHolder = attributes UsdMaterialAttributeHolder
+version:1
+attribID:#(0x37a5e0e8, 0x723f032d)
+(
+ function replaceUnicode val =
+ (
+ local updatedStr = ""
+ for i = 1 to val.count do
+ (
+ local char = val[i]
+ if bit.charAsInt char > 127 then
+ (
+ updatedStr += "_"
+ )
+ else
+ (
+ updatedStr += char
+ )
+ )
+ updatedStr
+ )
+
+ local enableFunc, UsdAttributes, enableLayerPath, validatePrimPath
+ parameters USD rollout:UsdAttributes
+ (
+ pathMethod type:#integer default:1 ui:rdo_buttons
+ primPath type:#string default:"mtl" ui:edt_primPath
+ separateLayer type:#boolean default:false ui:ckb_separateLayer
+ filePath type:#string ui:edt_filePath
+
+ on pathMethod set val do
+ (
+ if enableFunc != undefined then enableFunc()
+ )
+ on separateLayer set val do
+ (
+ if enableLayerPath != undefined then enableLayerPath()
+ )
+ on filePath set val do
+ (
+ if val != undefined then
+ (
+ filePath = replaceUnicode val
+ )
+ )
+ )
+
+ rollout UsdAttributes "USD Export Options" autoLayoutOnResize:true
+ (
+ radiobuttons rdo_buttons "" labels:#("Use defaults defined in the USD Exporter", "Specify New Path") default:1 columns:1
+ editText edt_primPath "Scope" enabled:(pathMethod != 1) tooltip:"The path in the USD hierarchy where this prim will reside."
+ checkbox ckb_separateLayer "Separate USD Layer" enabled:(pathMethod != 1) tooltip:"Enable to have materials export to a separate USD file at the File Path specified. When disabled, used the value defined in the USD Exporter."
+ editText edt_filePath "File Path" enabled:(pathMethod !=1) tooltip:"The path to the file where the material is going to be exported."
+
+ on UsdAttributes open do
+ (
+ enableFunc()
+ )
+
+ on edt_filePath entered val do
+ (
+ edt_filePath.text = replaceUnicode val
+ )
+ )
+
+
+ function enableFunc =
+ (
+ UsdAttributes.edt_primPath.enabled = (pathMethod != 1)
+ UsdAttributes.ckb_separateLayer.enabled = (pathMethod != 1)
+ UsdAttributes.edt_filePath.enabled = ((pathMethod != 1) and separateLayer)
+ )
+
+ function enableLayerPath =
+ (
+ UsdAttributes.edt_filePath.enabled = separateLayer
+ )
+)
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/UsdMaterialAttributeMacro.mcr b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/UsdMaterialAttributeMacro.mcr
new file mode 100644
index 0000000..1b098a7
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/UsdMaterialAttributeMacro.mcr
@@ -0,0 +1,29 @@
+macroScript AddUSDCustomAttrToMaterialSelection category:"USD" buttonText:"Add USD Cust Attr to Selected Materials" tooltip:"Add USD Cust Attr to Selected Materials"
+(
+ usdHolder = USDMaterialAttributeHolder
+ if sme.IsOpen() do
+ (
+ v = sme.GetView sme.activeView
+ selectedNode = v.GetSelectedNodes()
+ for n = 1 to selectedNode.count do
+ (
+ m = selectedNode[n].reference
+ if (superClassOf m) == material then
+ (
+ custAttributes.add m usdHolder
+ )
+ )
+ )
+)
+
+macroScript AddUSDCustomAttrToObjectSelection category:"USD" buttonText:"Add USD Cust Attr to Selected objects" tooltip:"Add USD Cust Attr to Selected objects"
+(
+ usdHolder = USDMaterialAttributeHolder
+ for n = 1 to selection.count do
+ (
+ if selection[n].material != undefined do
+ (
+ custAttributes.add selection[n].material usdHolder
+ )
+ )
+)
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/UsdPreviewSurface.ms b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/UsdPreviewSurface.ms
new file mode 100644
index 0000000..21dadd0
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/UsdPreviewSurface.ms
@@ -0,0 +1,522 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+-- A scripted implementation of a USD Preview Surface
+-- You are free to modify this file to make derivative versions to match your workflow/engine.
+-- Remember to change the ClassID if you do...
+-- https://github.com/ADN-DevTech/3dsMax-OSL-Shaders/blob/master/LICENSE.txt
+
+plugin material MaxUsdPreviewSurface
+name:"USD Preview Surface"
+classID:#(0x6afa4933, 0x4787f1c7)
+category:"Materials\USD"
+extends:PhysicalMaterial replaceUI:true version:2019.1215
+(
+ local params
+ local settings
+
+ local _opacityThresholdFloatOp
+ local _opacityThresholdMapOp
+
+ fn setGamma1 map =
+ (
+ if (classof map == bitmapTex) do
+ (
+ -- Accessing .bitmap of a missing map yeilds an exception
+ try (
+ if (map.bitmap != undefined) do
+ (
+ if (map.bitmap.gamma != 1.0) do
+ (
+ local gamma1bmp = undefined
+ local version = maxversion()
+ if version[1] >= 26000 then ( -- 3ds Max 2024 and up, require color space.
+ gamma1bmp = openBitmap map.bitmap.filename gamma:1.0 colorspace:ColorPipelineMgr.DataColorSpace
+ )
+ else (
+ gamma1bmp = openBitmap map.bitmap.filename gamma:1.0
+ )
+ if (gamma1bmp != undefined) do (
+ USDImporter.SetTexmapParamByName map "bitmap" gamma1bmp
+ )
+ )
+ )
+ )
+ catch() -- do-nothing catch
+ )
+ if (classOf map == OSLMap) do
+ (
+ if (hasProperty map "autoGamma") do (
+ USDImporter.SetTexmapParamByName map "autogamma" false
+ )
+
+ if (hasProperty map "manualGamma") do (
+ USDImporter.SetTexmapParamByName map "manualGamma" 1.0
+ )
+
+ -- Set any parameter ending in "_ColorSpace" to "" (Raw)
+ local props = getPropNames map
+ for prop in props do if ((findString (prop as string) "_ColorSpace") != undefined) do setProperty map (prop as string) ""
+ )
+ if (classOf map == MultiOutputChannelTexmapToTexmap) do
+ (
+ setGamma1(map.sourceMap)
+ )
+ )
+
+ -- Returns the opacity threshold operation OSL node. If it does not exist yet, it is created.
+ -- This custom OSL node implements UsdPreviewSurface's "opacityThreshold" for scalar values.
+ -- It has two imputs, opacity and threshold. If the threshold is non-zero, it will either output
+ -- a final opacity of 0.0 or 1.0, depending on whether the input opacity exceeds the threshold or not.
+ -- If the threshold is zero, the input opacity is returned as is.
+ fn getOpacityThresholdFloatOpNode = (
+ if (_opacityThresholdFloatOp == undefined) do (
+ local code =
+ "shader OpacityThresholdFloat
+ (
+ float Opacity = 0.0 ,
+ float Threshold = 0.0,
+ output float Out = 0.0,
+ )
+ {
+ Out = Threshold > 0.0 ? (Opacity >= Threshold ? 1.0 : 0.0) : Opacity;
+ }
+ "
+ _opacityThresholdFloatOp = OSLmap()
+ USDImporter.SetTexmapParamByName _opacityThresholdFloatOp "OSLCode" code
+ USDImporter.SetTexmapParamByName _opacityThresholdFloatOp "OSLAutoUpdate" True
+
+ USDImporter.SetTexmapParamByName _opacityThresholdFloatOp "Opacity" 1.0
+ USDImporter.SetTexmapParamByName _opacityThresholdFloatOp "Threshold" 0.0
+ )
+ return _opacityThresholdFloatOp
+ )
+
+ -- Similar implemenation as getOpacityThresholdFloatOpNode for map values. The RGB intensity is used to
+ -- compare against the threshold.
+ fn getOpacityThresholdMapOpNode = (
+ if (_opacityThresholdMapOp == undefined) do (
+ local codeOpacityThresholdMap =
+ "shader OpacityThresholdMap
+ (
+ color Opacity = 0.0,
+ float Threshold = 0.0,
+ output color Out = 0.0,
+ )
+ {
+ float intensity = (Opacity[0] + Opacity[1] + Opacity[2]) / 3;
+ Out = Threshold > 0.0 ? (intensity >= Threshold ? 1.0 : 0.0) : Opacity;
+ }
+ "
+ _opacityThresholdMapOp = OSLmap()
+ USDImporter.SetTexmapParamByName _opacityThresholdMapOp "OSLCode" codeOpacityThresholdMap
+ USDImporter.SetTexmapParamByName _opacityThresholdMapOp "OSLAutoUpdate" True
+ )
+ return _opacityThresholdMapOp
+ )
+
+ fn specGlossToPhysMap init:false = (
+ if not (init) and (delegate.roughness_map != undefined) do
+ (
+ -- if not intializing return existing map
+ if (delegate.roughness_map.sourceMap != undefined) do
+ return delegate.roughness_map.sourceMap
+ )
+ local code =
+ "shader SpecGlossToPhysical
+ (
+ color In = 0.02,
+ float Gloss = 1.0,
+ float AO = 1.0,
+ int Invert = 1,
+ output float Reflectivity = 0.0,
+ output color ReflColor = 0.0,
+ output float Roughness = 0.0
+ )
+ {
+ Roughness = Invert?1.0 - Gloss:Gloss;
+
+ Reflectivity = max(max(In[0],In[1]),In[2]);
+ if (Reflectivity > 0.0)
+ ReflColor = In / Reflectivity;
+
+ float fres = pow(1-abs(dot(I, N)),5.0) * (1.0-Roughness);
+ Reflectivity = mix(Reflectivity, 1.0, fres);
+ ReflColor = mix(ReflColor, 1.0, fres) * AO;
+ }"
+
+ local tm = OSLmap()
+ tm.OSLCode = code
+
+ local mul = MultiOutputChannelTexmapToTexmap()
+ USDImporter.SetTexmapParamByName mul "sourceMap" tm
+ USDImporter.SetTexmapParamByName mul "outputChannelIndex" 1
+ USDImporter.SetMaterialParamByName delegate "reflectivity_map" mul
+
+ mul = MultiOutputChannelTexmapToTexmap()
+ USDImporter.SetTexmapParamByName mul "sourceMap" tm
+ USDImporter.SetTexmapParamByName mul "outputChannelIndex" 2
+ USDImporter.SetMaterialParamByName delegate "refl_color_map" mul
+
+ mul = MultiOutputChannelTexmapToTexmap()
+ USDImporter.SetTexmapParamByName mul "sourceMap" tm
+ USDImporter.SetTexmapParamByName mul "outputChannelIndex" 3
+ USDImporter.SetMaterialParamByName delegate "roughness_map" mul
+ return tm
+ )
+
+ parameters settings rollout:settings
+ (
+ ao_affects_diffuse type:#boolean ui:aoDiffuse default:true
+ ao_affects_reflection type:#boolean ui:aoReflect default:true
+ normal_flip_red type:#boolean ui:normFlipR default:false
+ normal_flip_green type:#boolean ui:normFlipG default:false
+ )
+
+
+ parameters main rollout:params
+ (
+ useSpecularWorkflow type:#boolean default:false
+ diffuseColor type:#frgba default:[0.18,0.18,0.18,1.0] ui:cpCol
+ diffuseColor_map type:#TextureMap ui:mapBaseColor localizedname:"Diffuse Color Map"
+ metallic type:#float default:0.0 ui:spnmetallic
+ metallic_map type:#TextureMap ui:mapMetallic localizedname:"Metallic Map"
+ specularColor type:#frgba default:[0.0,0.0,0.0,1.0] ui:cpSpecular
+ specularColor_map type:#TextureMap ui:mapSpecular localizedname:"Specular Map"
+ roughness type:#float default:0.5 ui:spnRoughness
+ roughness_map type:#TextureMap ui:mapRoughness localizedname:"Roughness Map"
+ occlusion type:#float default:1.0 ui:spnOcclusion
+ occlusion_map type:#TextureMap ui:mapAmbientOcclusion localizedname:"Occlusion Map"
+ normal type:#frgba default:[0.0,0.0,1.0,1.0] ui:cpNormal
+ bump_map_amt type:#float default:1.0 ui:spnBumpAmount
+ normal_map type:#TextureMap ui:mapNormal localizedname:"Normal Map"
+ emissiveColor type:#frgba default:black ui:cpEmCol
+ emissiveColor_map type:#TextureMap ui:mapEmitColor localizedName:"Emissive Color Map"
+
+ opacity type:#float ui:spnOpacity default:1.0
+ opacity_map type:#TextureMap ui:mapOpacity localizedname:"Opacity Map"
+ opacityThreshold type:#float ui:spnOpacityThreshold default:0.0
+
+ displacement type:#float default:0.0 ui:spnDispAmount
+ displacement_map type:#TextureMap ui:mapDisplace localizedname:"Displacement Map"
+
+ ior type:#float ui:spnIor default:1.5
+ ior_map type:#TextureMap ui:mapIor localizedname:"IOR Map"
+ clearcoat type:#float ui:spnClearcoat default:0.0
+ clearcoat_map type:#TextureMap ui:mapClearcoat localizedname:"Clearcoat Map"
+ clearcoatRoughness type:#float ui:spnClearcoatRoughness default:0.01
+ clearcoatRoughness_map type:#TextureMap ui:mapClearcoatRoughness localizedname:"Clearcoat Roughness Map"
+
+ on useSpecularWorkflow set val do (
+ if val == true then (
+ local x = specGlossToPhysMap init:True
+ USDImporter.SetTexmapParamByName x "In" specularColor
+ USDImporter.SetTexmapParamByName x "In_map" specularColor_map
+ if (ao_affects_reflection) then
+ USDImporter.SetTexmapParamByName x "AO_map" occlusion_map
+ else
+ USDImporter.SetTexmapParamByName x "AO_map" undefined
+ USDImporter.SetMaterialParamByName delegate "metalness" 0.0
+ USDImporter.SetMaterialParamByName delegate "trans_ior" 50.0
+ USDImporter.SetTexmapParamByName x "Gloss_map" roughness_map
+ USDImporter.SetTexmapParamByName x "Gloss" roughness
+ USDImporter.SetTexmapParamByName x "Invert" False
+ ) else (
+ USDImporter.SetMaterialParamByName delegate "metalness" metallic
+ USDImporter.SetMaterialParamByName delegate "trans_ior" ior
+ USDImporter.SetMaterialParamByName delegate "roughness_map" roughness_map
+ USDImporter.SetMaterialParamByName delegate "refl_color_map" undefined
+ if (ao_affects_reflection) then
+ USDImporter.SetMaterialParamByName delegate "reflectivity_map" occlusion_map
+ else
+ USDImporter.SetMaterialParamByName delegate "reflectivity_map" undefined
+ )
+ )
+ on diffuseColor set val do (USDImporter.SetMaterialParamByName delegate "base_color" val)
+ on diffuseColor_map set val do (USDImporter.SetMaterialParamByName delegate "base_color_map" val)
+ on specularColor set val do (
+ if useSpecularWorkflow do (
+ local x = specGlossToPhysMap()
+ USDImporter.SetTexmapParamByName x "In" val
+ )
+ )
+ on specularColor_map set val do (
+ if useSpecularWorkflow do (
+ local x = specGlossToPhysMap()
+ USDImporter.SetTexmapParamByName x "In_map" val
+ )
+ )
+
+ on metallic set val do USDImporter.SetMaterialParamByName delegate "metalness" val
+ on metallic_map set val do (
+ setGamma1(val)
+ USDImporter.SetMaterialParamByName delegate "metalness_map" val
+ )
+ on roughness set val do (
+ if useSpecularWorkflow then (
+ local x = specGlossToPhysMap()
+ USDImporter.SetTexmapParamByName x "Gloss" val
+ USDImporter.SetTexmapParamByName x "Invert" False
+ ) else (
+ USDImporter.SetMaterialParamByName delegate "roughness" val
+ )
+ )
+
+ on roughness_map set val do (
+ setGamma1(val)
+ if useSpecularWorkflow then (
+ local x = specGlossToPhysMap()
+ USDImporter.SetTexmapParamByName x "Gloss_map" val
+ ) else (
+ USDImporter.SetMaterialParamByName delegate "roughness_Map" val
+ )
+ )
+
+ on occlusion_map set val do (
+ setGamma1(val)
+
+ if (ao_affects_diffuse) then
+ USDImporter.SetMaterialParamByName delegate "base_weight_map" val
+ else
+ USDImporter.SetMaterialParamByName delegate "base_weight_map" undefined
+
+ if useSpecularWorkflow then (
+ local x = specGlossToPhysMap()
+
+ if (ao_affects_reflection) then
+ USDImporter.SetTexmapParamByName x "AO_map" val
+ else
+ USDImporter.SetTexmapParamByName x "AO_map" undefined
+ ) else (
+ if (ao_affects_reflection) then
+ USDImporter.SetMaterialParamByName delegate "reflectivity_map" val
+ else
+ USDImporter.SetMaterialParamByName delegate "reflectivity_map" undefined
+ )
+ )
+ on opacityThreshold set val do (
+ -- Set the threshold value in both the float and map threshold op nodes.
+ local opacityThresholdFloatOp = getOpacityThresholdFloatOpNode()
+ USDImporter.SetTexmapParamByName opacityThresholdFloatOp "Threshold" val
+
+ local opacityThresholdMapOp = getOpacityThresholdMapOpNode()
+ USDImporter.SetTexmapParamByName opacityThresholdMapOp "Threshold" val
+
+ )
+
+ on opacity set val do (
+ local opacityThresholdFloatOp = getOpacityThresholdFloatOpNode()
+ -- If the value is not fully opaque, and if still have nothing connected in the cutout_map,
+ -- connect our custom osl node, which will forward the correct opacity to the delegate, given
+ -- the current opacity threshold.
+ if (val < 1.0) then (
+ if (delegate.cutout_map == undefined) then (
+ USDImporter.SetMaterialParamByName delegate "cutout_map" opacityThresholdFloatOp
+ )
+ )
+ -- Fully opaque, remove the cutout_map entirely if the opacity value is what is being used currently.
+ else (
+ if (delegate.cutout_map == opacityThresholdFloatOp) do (
+ USDImporter.SetMaterialParamByName delegate "cutout_map" undefined
+ )
+ )
+ -- Set the opacity value in the OSL node.
+ USDImporter.SetTexmapParamByName opacityThresholdFloatOp "Opacity" val
+ )
+ on opacity_map set val do (
+ if (val != undefined) do (
+ setGamma1(val)
+ )
+
+
+ -- Connected a new map...
+ if (val != undefined) then (
+ -- Setup the custom OSL node to take into account the opacityThreshold, and set it as
+ -- the opacity map for the delegate.
+ local opacityThresholdMapOp = getOpacityThresholdMapOpNode()
+ opacityThresholdMapOp.Opacity_map = val
+ USDImporter.SetMaterialParamByName delegate "cutout_map" opacityThresholdMapOp
+ -- Disconnected
+ ) else (
+ -- If we currently have a non fully opaque value for the opacity, connect the float opacity threshold
+ -- op node. It's opacity/threshold input will already be correct.
+ if (opacity < 1.0) then (
+ local opacityThresholdFloatOp = getOpacityThresholdFloatOpNode()
+ USDImporter.SetMaterialParamByName delegate "cutout_map" opacityThresholdFloatOp
+ )
+ -- Otherwise, disconnect the opacity map in the delegate.
+ else (
+ USDImporter.SetMaterialParamByName delegate "cutout_map" undefined
+ )
+ )
+ )
+
+ on displacement_map set val do (
+ setGamma1(val)
+ USDImporter.SetMaterialParamByName delegate "displacement_map" val
+ USDImporter.SetMaterialParamByName delegate "displacement_map_amt" 1.0
+ )
+
+ on displacement set val do USDImporter.SetMaterialParamByName delegate "displacement_map_amt" val
+ on bump_map_amt set val do delegate.bump_map_amt = val
+ on normal_map set val do (
+ if (val != undefined) then (
+ setGamma1(val)
+ local bm = Gnormal()
+ USDImporter.SetTexmapParamByName bm "normal_map" val
+ USDImporter.SetTexmapParamByName bm "flipred" normal_flip_red
+ USDImporter.SetTexmapParamByName bm "flipgreen" normal_flip_green
+ USDImporter.SetMaterialParamByName delegate "bump_map" bm
+ )
+ else
+ (
+ USDImporter.SetMaterialParamByName delegate "bump_map" undefined
+ )
+ )
+ on emissiveColor set val do (USDImporter.SetMaterialParamByName delegate "emit_color" val)
+ on emissiveColor_map set val do (USDImporter.SetMaterialParamByName delegate "emit_color_map" val)
+ on ior set val do (
+ if not useSpecularWorkflow do (
+ USDImporter.SetMaterialParamByName delegate "trans_ior" val
+ )
+ )
+ on ior_map set val do (
+ if not useSpecularWorkflow do (
+ USDImporter.SetMaterialParamByName delegate "trans_ior_map" val
+ )
+ )
+ on clearcoat set val do (
+ USDImporter.SetMaterialParamByName delegate "coating" val
+ )
+ on clearcoat_map set val do (
+ USDImporter.SetMaterialParamByName delegate "coat_map" val
+ )
+ on clearcoatRoughness set val do (
+ USDImporter.SetMaterialParamByName delegate "coat_roughness" val
+ )
+ on clearcoatRoughness_map set val do (
+ USDImporter.SetMaterialParamByName delegate "coat_rough_map" val
+ )
+
+ )
+
+ rollout params "USD Preview Surface Parameters"
+ (
+ group "Material Parameters" (
+ label lblWorkflow "Workflow:" across:2
+ radiobuttons rdoWorkflow labels:#("Metallic", "Specular") default:1 align:#left
+
+ colorpicker cpCol "Diffuse Color" color:white across:2 fieldWidth:45 offset:[-45,6] align:#right
+ mapbutton mapBaseColor "" align:#right width:190 offset:[-5,5]
+
+ spinner spnmetallic "Metallic" range:[0.0,1.0,0.0] scale:0.01 across:2 fieldWidth:35 offset:[-45,5] align:#right
+ mapbutton mapMetallic "" align:#right width:190 offset:[-5,2]
+
+ colorpicker cpSpecular "Specular Color" color:white across:2 fieldWidth:45 offset:[-45,3] align:#right
+ mapbutton mapSpecular "" align:#right width:190 offset:[-5,2]
+
+ spinner spnRoughness "Roughness" range:[0.0,1.0,0.0] scale:0.01 across:2 fieldWidth:35 offset:[-45,5] align:#right
+ mapbutton mapRoughness "" align:#right width:190 offset:[-5,2]
+
+ spinner spnOcclusion "Occlusion" range:[0.0,1.0,0.0] scale:0.01 across:2 fieldWidth:35 offset:[-45,5] align:#right
+ mapbutton mapAmbientOcclusion "" align:#right width:190 offset:[-5,2]
+
+ colorpicker cpNormal "Normal" color:blue across:2 fieldWidth:45 offset:[-45,3] align:#right
+ mapbutton mapNormal "" align:#right width:190 offset:[-5,2]
+
+ spinner spnBumpAmount "Amount" range:[-5.0,5.0,1.0] scale:0.01 fieldwidth:35 offset:[-45,5] align:#right
+
+ colorpicker cpEmCol "Emissive Color" color:black across:2 fieldWidth:45 offset:[-45,3] align:#right
+ mapbutton mapEmitColor "" align:#right width:190 offset:[-5,2]
+
+ spinner spnDispAmount "Displacement" range:[0.0,999.0,1.0] scale:0.01 fieldwidth:35 across:2 offset:[-45,5] align:#right
+ mapbutton mapDisplace "" align:#right width:190 offset:[-5,2]
+
+ spinner spnOpacity "Opacity" range:[0.0,1.0,0.0] scale:0.01 fieldwidth:35 across:2 offset:[-45,5] align:#right
+ mapbutton mapOpacity "" align:#right width:190 offset:[-5,2]
+
+ spinner spnOpacityThreshold "Opacity Threshold" range:[0.0,1.0,0.1] scale:0.01 fieldwidth:35 offset:[-45,5] align:#right
+
+ spinner spnIor "IOR" range:[0.0, 999.0, 1.0] scale:0.01 fieldwidth:35 offset:[-45,5] align:#right across:2
+ mapbutton mapIor "" align:#right width:190 offset:[-5,2]
+
+ spinner spnClearcoat "Clearcoat" range:[0.0,999.0,1.0] scale:0.01 fieldwidth:35 across:2 offset:[-45,5] align:#right
+ mapbutton mapClearcoat "" align:#right width:190 offset:[-5,2]
+
+ spinner spnClearcoatRoughness "CC Roughness" range:[0.0,999.0,1.0] scale:0.01 fieldwidth:35 across:2 offset:[-45,5] align:#right
+ mapbutton mapClearcoatRoughness "" align:#right width:190 offset:[-5,2]
+ )
+ on params open do
+ (
+ params.setUIText()
+ )
+
+ fn setUIText =
+ (
+ if useSpecularWorkflow then
+ (
+ params.rdoWorkflow.state = 2
+ params.spnmetallic.enabled = false
+ params.cpSpecular.enabled = true
+ )
+ else
+ (
+ params.rdoWorkflow.state = 1
+ params.spnmetallic.enabled = true
+ params.cpSpecular.enabled = false
+ )
+ )
+
+
+ on rdoWorkflow changed state do
+ (
+ useSpecularWorkflow = (state == 2)
+ params.setUIText()
+ )
+
+ )
+
+ rollout settings "Settings"
+ (
+ checkbox aoDiffuse "AO affects Diffuse" offset:[20,0] across:2
+ checkBox normFlipR "Normal Flip Red" align:#left
+ checkbox aoReflect "AO affects Reflection" offset:[20,0] across:2
+ checkBox normFlipG "Normal Flip Green" align:#left
+ label lblSpc "" height:2
+
+ on aoDiffuse changed state do occlusion_map = occlusion_map
+ on aoReflect changed state do occlusion_map = occlusion_map
+ on normFlipR changed state do normal_map = normal_map
+ on normFlipG changed state do normal_map = normal_map
+ )
+
+ on create do
+ (
+ USDImporter.SetMaterialParamByName delegate "base_color" white
+ USDImporter.SetMaterialParamByName delegate "metalness" 0.0
+ USDImporter.SetMaterialParamByName delegate "roughness" 0.5
+ USDImporter.SetMaterialParamByName delegate "bump_map_amt" 1.0
+ USDImporter.SetMaterialParamByName delegate "roughness_inv" false
+ USDImporter.SetMaterialParamByName delegate "reflectivity" 1.0
+ USDImporter.SetMaterialParamByName delegate "emit_color" black
+ USDImporter.SetMaterialParamByName delegate "emit_color_map_on" true
+ USDImporter.SetMaterialParamByName delegate "trans_ior" 1.5
+ USDImporter.SetMaterialParamByName delegate "refl_color_map" undefined
+ USDImporter.SetMaterialParamByName delegate "displacement_map_amt" 1.0
+
+ custAttributes.add this USDMaterialAttributeHolder
+ )
+)
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/data_files/3dsmax_materials.mat_def b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/data_files/3dsmax_materials.mat_def
new file mode 100644
index 0000000..6de6988
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/data_files/3dsmax_materials.mat_def
@@ -0,0 +1,131 @@
+
+{
+ "3dsmax_physical_material": {
+ "id": "PhysicalMaterial",
+ "max_class_id": [
+ 1030429932,
+ 3735928833
+ ],
+ "domain": "3dsmax",
+ "inputs": {
+ "Base_Color": "color",
+ "base_color_map": "texturemap",
+ "base_color_map_on": "use_map",
+ "roughness": "float",
+ "roughness_map": "texturemap",
+ "roughness_map_on": "use_map",
+ "metalness": "float",
+ "metalness_map": "texturemap",
+ "metalness_map_on": "use_map",
+ "bump_map": "texturemap",
+ "bump_map_on": "use_map",
+ "ao_map": "texturemap",
+ "transparency": "float",
+ "transparency_map": "texturemap",
+ "transparency_map_on": "use_map",
+ "cutout_map":"texturemap",
+ "cutout_map_on":"use_map",
+ "emit_color":"color",
+ "emission_map":"texturemap",
+ "emission_map_on":"use_map",
+ "displacement_map":"texturemap",
+ "displacement_map_on":"displacement_map_on",
+ "displacement_map_amt":"float",
+ "coating":"float",
+ "coat_map":"texturemap",
+ "coat_map_on":"use_map",
+ "coat_roughness":"float",
+ "coat_rough_map":"texturemap",
+ "coat_rough_map_on":"use_map",
+ "trans_ior":"float",
+ "trans_ior_map":"texturemap",
+ "trans_ior_map_on":"use_map"
+ }
+ },
+ "3dsmax_usd_preview_surface":{
+ "id": "MaxUsdPreviewSurface",
+ "domain":"3dsmax",
+ "inputs":{
+ "useSpecularWorkflow": "boolean",
+ "diffuseColor":"color",
+ "diffuseColor_map":"texturemap",
+ "metallic":"float",
+ "metallic_map":"texturemap",
+ "specularColor":"color",
+ "specularColor_map":"texturemap",
+ "roughness":"float",
+ "roughness_map":"texturemap",
+ "occlusion":"float",
+ "occlusion_map":"texturemap",
+ "normal":"normal3f",
+ "normal_map":"normalmap",
+ "emissiveColor":"color",
+ "emissiveColor_map":"texturemap",
+ "opacity":"float",
+ "opacity_map":"texturemap",
+ "opacityThreshold":"float",
+ "displacement":"float",
+ "displacement_map":"texturemap",
+ "ior":"float",
+ "ior_map":"texturemap",
+ "clearcoat":"float",
+ "clearcoat_map":"texturemap",
+ "clearcoatRoughness":"float",
+ "clearcoatRoughness_map":"texturemap"
+ }
+ },
+ "3dsmax_pbr_metal_rough" : {
+ "domain": "3dsmax",
+ "id" : "PBRMetalRough",
+ "inputs": {
+ "ao_affects_diffuse" : "boolean",
+ "ao_affects_reflection" : "boolean",
+ "normal_flip_red" : "boolean",
+ "normal_flip_green" : "boolean",
+ "useGlossiness" : "integer",
+ "basecolor": "color",
+ "base_color_map": "texturemap",
+ "emit_color":"color",
+ "emit_color_map":"texturemap",
+ "displacement_amt":"float",
+ "displacement_map":"texturemap",
+ "opacity_map":"texturemap",
+ "roughness": "float",
+ "roughness_map": "texturemap",
+ "metalness": "float",
+ "metalness_map": "texturemap",
+ "norm_map": "normalmap",
+ "bump_map_amt":"float",
+ "ao_map": "texturemap"
+ }
+ },
+ "3dsmax_pbr_spec_gloss" : {
+ "domain": "3dsmax",
+ "id" : "PBRSpecGloss",
+ "inputs": {
+ "ao_affects_diffuse" : "boolean",
+ "ao_affects_reflection" : "boolean",
+ "normal_flip_red" : "boolean",
+ "normal_flip_green" : "boolean",
+ "useGlossiness" : "integer",
+ "basecolor": "color",
+ "base_color_map": "texturemap",
+ "emit_color":"color",
+ "emit_color_map":"texturemap",
+ "displacement_amt":"float",
+ "displacement_map":"texturemap",
+ "opacity_map":"texturemap",
+ "Specular": "color",
+ "specular_map": "texturemap",
+ "glossiness": "color",
+ "glossiness_map": "texturemap",
+ "norm_map": "normalmap",
+ "bump_map_amt":"float",
+ "ao_map": "texturemap"
+ }
+ },
+ "3dsmax_ai_standard_surface" : {
+ "domain": "3dsmax",
+ "id" : "ai_standard_surface"
+ }
+}
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/data_files/default.material_conversion b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/data_files/default.material_conversion
new file mode 100644
index 0000000..4a482d3
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/data_files/default.material_conversion
@@ -0,0 +1,431 @@
+{
+ "usd_preview_surface_to_max_usd_preview_surface": {
+ "target_material": {
+ "id": "MaxUsdPreviewSurface",
+ "domain": "3dsmax"
+ },
+ "source_material": {
+ "id": "UsdPreviewSurface",
+ "domain": "usd"
+ },
+ "parameter_mapping": {
+ "mappings": {
+ "useSpecularWorkflow": "useSpecularWorkflow",
+ "opacityThreshold": "opacityThreshold",
+ "diffuseColor": "diffuseColor",
+ "diffuseColor_map": "diffuseColor",
+ "roughness": "roughness",
+ "roughness_map": "roughness",
+ "metallic": "metallic",
+ "metallic_map": "metallic",
+ "specularColor": "specularColor",
+ "specularColor_map": "specularColor",
+ "normal": "normal",
+ "normal_map": "normal",
+ "occlusion": "occlusion",
+ "occlusion_map": "occlusion",
+ "emissiveColor": "emissiveColor",
+ "emissiveColor_map": "emissiveColor",
+ "opacity": "opacity",
+ "opacity_map": "opacity",
+ "displacement": "displacement",
+ "displacement_map": "displacement",
+ "ior": "ior",
+ "ior_map": "ior",
+ "clearcoat": "clearcoat",
+ "clearcoat_map": "clearcoat",
+ "clearcoatRoughness": "clearcoatRoughness",
+ "clearcoatRoughness_map": "clearcoatRoughness"
+ }
+ }
+ },
+ "max_usd_preview_surface_to_usd_preview_surface": {
+ "source_material": {
+ "id": "MaxUsdPreviewSurface",
+ "domain": "3dsmax"
+ },
+ "target_material": {
+ "id": "UsdPreviewSurface",
+ "domain": "usd"
+ },
+ "parameter_mapping": {
+ "mappings": {
+ "useSpecularWorkflow": "useSpecularWorkflow",
+ "opacityThreshold": "opacityThreshold",
+ "diffuseColor": {
+ "value_parameter": "diffuseColor",
+ "map_parameter": "diffuseColor_map"
+ },
+ "roughness": {
+ "value_parameter": "roughness",
+ "map_parameter": "roughness_map"
+ },
+ "metallic": {
+ "value_parameter": "metallic",
+ "map_parameter": "metallic_map"
+ },
+ "specularColor": {
+ "value_parameter": "specularColor",
+ "map_parameter": "specularColor_map"
+ },
+ "normal": {
+ "value_parameter": "normal",
+ "map_parameter": "normal_map"
+ },
+ "occlusion": {
+ "value_parameter": "occlusion",
+ "map_parameter": "occlusion_map"
+ },
+ "emissiveColor": {
+ "value_parameter": "emissiveColor",
+ "map_parameter": "emissiveColor_map"
+ },
+ "opacity": {
+ "value_parameter": "opacity",
+ "map_parameter": "opacity_map"
+ },
+ "displacement": {
+ "value_parameter": "displacement",
+ "map_parameter": "displacement_map"
+ },
+ "ior": {
+ "value_parameter": "ior",
+ "map_parameter": "ior_map"
+ },
+ "clearcoat": {
+ "value_parameter": "clearcoat",
+ "map_parameter": "clearcoat_map"
+ },
+ "clearcoatRoughness": {
+ "value_parameter": "clearcoatRoughness",
+ "map_parameter": "clearcoatRoughness_map"
+ }
+ }
+ }
+ },
+ "usd_preview_surface_to_pbr_metal_rough": {
+ "target_material": {
+ "id": "PBRMetalRough",
+ "domain": "3dsmax"
+ },
+ "source_material": {
+ "id": "UsdPreviewSurface",
+ "domain": "usd"
+ },
+ "parameter_mapping": {
+ "mappings": {
+ "basecolor": "diffuseColor",
+ "base_color_map": "diffuseColor",
+ "roughness": "roughness",
+ "roughness_map": "roughness",
+ "metalness": "metallic",
+ "metalness_map": "metallic",
+ "norm_map": "normal",
+ "ao_map": "occlusion",
+ "emit_color": "emissiveColor",
+ "emit_color_map": "emissiveColor",
+ "opacity_map": "opacity"
+ }
+ }
+ },
+ "usd_preview_surface_to_physical_material": {
+ "source_material": {
+ "id": "UsdPreviewSurface",
+ "domain": "usd"
+ },
+ "target_material": {
+ "id": "PhysicalMaterial",
+ "domain": "3dsmax"
+ },
+ "parameter_mapping": {
+ "mappings": {
+ "Base_Color": "diffuseColor",
+ "base_color_map": "diffuseColor",
+ "base_color_map_on": "diffuseColor",
+ "roughness": "roughness",
+ "roughness_map": "roughness",
+ "roughness_map_on": "roughness",
+ "metalness": "metallic",
+ "metalness_map": "metallic",
+ "metalness_map_on": "metallic",
+ "bump_map": "normal",
+ "bump_map_on": "normal",
+ "transparency": {
+ "value_parameter": "opacity",
+ "one_minus": true
+ }
+ }
+ }
+ },
+ "max_physical_material_to_usd_preview_surface": {
+ "source_material": {
+ "id": "PhysicalMaterial",
+ "max_class_id": [
+ 1030429932,
+ 3735928833
+ ],
+ "domain": "3dsmax"
+ },
+ "target_material": {
+ "id": "UsdPreviewSurface",
+ "domain": "usd"
+ },
+ "parameter_mapping": {
+ "mappings": {
+ "useSpecularWorkflow": 0,
+ "diffuseColor": {
+ "value_parameter": "Base_Color",
+ "map_parameter": "base_color_map",
+ "use_map_parameter": "base_color_map_on"
+ },
+ "specularColor": {},
+ "emissiveColor": {
+ "value_parameter": "emit_color",
+ "map_parameter": "emission_map",
+ "use_map_parameter": "emission_map_on",
+ "multiplier_parameter": "emission"
+ },
+ "displacement": {
+ "value_parameter": "displacement_map_amt",
+ "map_parameter": "displacement_map",
+ "use_map_parameter": "displacement_map_on",
+ "map_required": true
+ },
+ "opacity": {
+ "case": {
+ "case_parameter": "transparency_map",
+ "None": {
+ "case": {
+ "case_parameter": "cutout_map",
+ "+": {
+ "map_parameter": "cutout_map",
+ "use_map_parameter": "cutout_map_on",
+ "one_minus": false,
+ "map_required": true
+ }
+ }
+ }
+ },
+ "value_parameter": "Transparency",
+ "map_parameter": "transparency_map",
+ "use_map_parameter": false,
+ "one_minus": true
+ },
+ "opacityThreshold": {
+ "case": {
+ "case_parameter": "transparency_map",
+ "None": {
+ "case": {
+ "case_parameter": "cutout_map",
+ "+": {
+ "value": 0.5
+ }
+ }
+ }
+ },
+ "value": 0.0
+ },
+ "roughness": {
+ "value_parameter": "roughness",
+ "map_parameter": "roughness_map",
+ "use_map_parameter": "roughness_map_on",
+ "case": {
+ "case_parameter": "roughness_inv",
+ "True": {
+ "one_minus": true
+ },
+ "False": {
+ "one_minus": false
+ }
+ }
+ },
+ "metallic": {
+ "value_parameter": "metalness",
+ "map_parameter": "metalness_map",
+ "use_map_parameter": "metalness_map_on"
+ },
+ "clearcoat": {
+ "value_parameter": "coating",
+ "map_parameter": "coat_map",
+ "use_map_parameter": "coat_map_on"
+ },
+ "clearcoatRoughness": {
+ "value_parameter": "coat_roughness",
+ "map_parameter": "coat_rough_map",
+ "use_map_parameter": "coat_rough_map_on"
+ },
+ "occlusion": {},
+ "normal": {
+ "map_parameter": "bump_map",
+ "use_map_parameter": "bump_map_on"
+ },
+ "ior": {
+ "value_parameter": "trans_ior",
+ "map_parameter": "trans_ior_map",
+ "use_map_parameter": "trans_ior_map_on"
+ }
+ }
+ }
+ },
+ "pbr_metal_rough_to_usd_preview_surface": {
+ "source_material": {
+ "id": "PBRMetalRough",
+ "domain": "3dsmax",
+ "max_class_id": [
+ 3490651648,
+ 3195528448
+ ]
+ },
+ "target_material": {
+ "id": "UsdPreviewSurface",
+ "domain": "usd"
+ },
+ "parameter_mapping": {
+ "mappings": {
+ "useSpecularWorkflow": 0,
+ "diffuseColor": {
+ "value_parameter": "basecolor",
+ "map_parameter": "base_color_map"
+ },
+ "emissiveColor": {
+ "value_parameter": "emit_color",
+ "map_parameter": "emit_color_map"
+ },
+ "displacement": {
+ "value_parameter": "displacement_amt",
+ "map_parameter": "displacement_map",
+ "map_required": true
+ },
+ "opacity": {
+ "map_parameter": "opacity_map"
+ },
+ "opacityThreshold": {},
+ "roughness": {
+ "value_parameter": "roughness",
+ "map_parameter": "roughness_map",
+ "case": {
+ "case_parameter": "useGlossiness",
+ "1": {
+ "one_minus": true
+ },
+ "2": {
+ "one_minus": false
+ }
+ }
+ },
+ "metallic": {
+ "value_parameter": "metalness",
+ "map_parameter": "metalness_map"
+ },
+ "occlusion": {
+ "map_parameter": "ao_map"
+ },
+ "normal": {
+ "map_parameter": "norm_map"
+ }
+ }
+ }
+ },
+ "pbr_spec_gloss_to_usd_preview_surface": {
+ "source_material": {
+ "id": "PBRSpecGloss",
+ "domain": "3dsmax"
+ },
+ "target_material": {
+ "id": "UsdPreviewSurface",
+ "domain": "usd"
+ },
+ "parameter_mapping": {
+ "mappings": {
+ "useSpecularWorkflow": 1,
+ "diffuseColor": {
+ "value_parameter": "basecolor",
+ "map_parameter": "base_color_map"
+ },
+ "emissiveColor": {
+ "value_parameter": "emit_color",
+ "map_parameter": "emit_color_map"
+ },
+ "displacement": {
+ "value_parameter": "displacement_amt",
+ "map_parameter": "displacement_map",
+ "map_required": true
+ },
+ "opacity": {
+ "map_parameter": "opacity_map"
+ },
+ "opacityThreshold": {},
+ "roughness": {
+ "value_parameter": "glossiness",
+ "map_parameter": "glossiness_map",
+ "case": {
+ "case_parameter": "useGlossiness",
+ "1": {
+ "one_minus": true
+ },
+ "2": {
+ "one_minus": false
+ }
+ }
+ },
+ "specularColor": {
+ "value_parameter": "specular",
+ "map_parameter": "specular_map"
+ },
+ "occlusion": {
+ "map_parameter": "ao_map"
+ },
+ "normal": {
+ "map_parameter": "norm_map"
+ }
+ }
+ }
+ },
+ "ai_standard_surface_to_usd_preview_surface": {
+ "source_material": {
+ "id": "ai_standard_surface",
+ "domain": "3dsmax"
+ },
+ "target_material": {
+ "id": "UsdPreviewSurface",
+ "domain": "usd"
+ },
+ "parameter_mapping": {
+ "mappings": {
+ "useSpecularWorkflow": 0,
+ "diffuseColor": {
+ "value_parameter": "base_color",
+ "map_parameter": "base_color_shader",
+ "use_map_parameter": "base_color_connected"
+ },
+ "specularColor": {},
+ "emissiveColor": {},
+ "displacement": {},
+ "opacity": {},
+ "opacityThreshold": {},
+ "roughness": {
+ "value_parameter": "specular_roughness",
+ "map_parameter": "specular_roughness_shader",
+ "use_map_parameter": "specular_roughness_connected"
+ },
+ "metallic": {
+ "value_parameter": "metalness",
+ "map_parameter": "metalness_shader",
+ "use_map_parameter": "metalness_connected"
+ },
+ "clearcoat": {},
+ "clearcoatRoughness": {},
+ "occlusion": {},
+ "normal": {
+ "map_parameter": "normal_shader",
+ "use_map_parameter": "normal_connected"
+ },
+ "ior": {
+ "value_parameter": "specular_IOR",
+ "map_parameter": "specular_IOR_shader",
+ "use_map_parameter": "specular_IOR_connected"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/data_files/usd_materials.mat_def b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/data_files/usd_materials.mat_def
new file mode 100644
index 0000000..86aa042
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/data_files/usd_materials.mat_def
@@ -0,0 +1,22 @@
+{
+ "usd_preview_surface": {
+ "id": "UsdPreviewSurface",
+ "domain": "usd",
+ "inputs": {
+ "useSpecularWorkflow": "int",
+ "diffuseColor": "color3f",
+ "specularColor": "color3f",
+ "emissiveColor": "color3f",
+ "displacement": "float",
+ "opacity": "float",
+ "opacityThreshold": "float",
+ "roughness": "float",
+ "metallic": "float",
+ "clearcoat": "float",
+ "clearcoatRoughness": "float",
+ "occlusion": "float",
+ "normal": "normal3f",
+ "ior": "float"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/plugInfo.json b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/plugInfo.json
new file mode 100644
index 0000000..6ce6e55
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/plugInfo.json
@@ -0,0 +1,5 @@
+{
+ "Includes": [
+ "reader/", "writer/"
+ ]
+}
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/reader/plugInfo.json b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/reader/plugInfo.json
new file mode 100644
index 0000000..233b544
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/reader/plugInfo.json
@@ -0,0 +1,18 @@
+{
+ "Plugins":[
+ {
+ "Info":{
+ "MaxUsd":{
+ "ShadingModePlugin": {},
+ "ShaderReader":{
+ "providesTranslator":[
+ "UsdPreviewSurface"
+ ]
+ }
+ }
+ },
+ "Name":"shaderReader",
+ "Type":"python"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/shaderReader.py b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/shaderReader.py
new file mode 100644
index 0000000..d5365cb
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/shaderReader.py
@@ -0,0 +1,92 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# maxUsd - the Python module for the 3ds Max USD component
+import maxUsd
+
+from pymxs import runtime as rt
+from pxr import UsdShade
+
+import traceback
+
+# the Python methods for material translation from UsdPreviewSurface
+import usd_material_reader
+
+_material_import_options = {
+ "texture_target_id": usd_material_reader._uberbitmapfile
+}
+
+class DefaultShaderReader(maxUsd.ShaderReader):
+ """
+ Default Shader Reader class handling the material translation to 3ds Max default materials
+ inherits from the base class ShaderReader from the 3ds Max USD component
+ """
+
+ @classmethod
+ def CanImport(cls, importArgs):
+ """
+ Static class method required to determine if the class is handling the
+ translation context required by the import job. The current class only
+ handles converting materials from 'UsdPreviewSurface' elements.
+ """
+ if importArgs.GetPreferredMaterial() == "none" or \
+ importArgs.GetPreferredMaterial() == "maxUsdPreviewSurface" or \
+ importArgs.GetPreferredMaterial() == "pbrMetalRough" or \
+ importArgs.GetPreferredMaterial() == "physicalMaterial":
+ return maxUsd.ShaderReader.ContextSupport.Fallback
+ return maxUsd.ShaderReader.ContextSupport.Unsupported
+
+ def Read(self):
+ """Main import function that runs when the applicable material gets hit"""
+ try:
+ import_options = self.GetArgs()
+ material_import_options = dict(_material_import_options)
+ preferredMaterial = import_options.GetPreferredMaterial()
+ if preferredMaterial == "none" or preferredMaterial == "maxUsdPreviewSurface":
+ material_import_options['material_target_id'] = "MaxUsdPreviewSurface"
+ elif preferredMaterial == "pbrMetalRough":
+ material_import_options['material_target_id'] = "PBRMetalRough"
+ elif preferredMaterial == "physicalMaterial":
+ material_import_options['material_target_id'] = "PhysicalMaterial"
+ else:
+ material_import_options['material_target_id'] = preferredMaterial
+ material_import_options['usd_import_options'] = import_options
+ shader = UsdShade.Shader(self.GetUsdPrim())
+ mat = usd_material_reader.output_max_material(shader, material_import_options, {})
+ if mat:
+ self.RegisterCreatedMaterial(shader.GetPath(), rt.GetHandleByAnim(mat))
+
+ except Exception as e:
+ # Quite useful to debug errors in a Python callback
+ print('Read() - Error: %s' % str(e))
+ print(traceback.format_exc())
+
+ return True
+
+
+# register the ShaderReader to use for the supported materials
+maxUsd.ShaderReader.Register(DefaultShaderReader, "UsdPreviewSurface")
+
+maxUsd.ShadingModeRegistry.RegisterImportConversion( \
+ "UsdPreviewSurface", \
+ UsdShade.Tokens.universalRenderContext, \
+ "USD Preview Surface", \
+ "Fetches back a UsdPreviewSurface material dumped as UsdShade" )
+
+# preferred 3ds Max specific materials
+# - maxUsdPreviewSurface -> "MaxUsdPreviewSurface"
+# - pbrMetalRough -> "PBRMetalRough"
+# - physicalMaterial -> "PhysicalMaterial"
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/shaderWriter.py b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/shaderWriter.py
new file mode 100644
index 0000000..bae6ada
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/shaderWriter.py
@@ -0,0 +1,83 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# maxUsd - the Python module for the 3ds Max USD component
+import maxUsd
+
+from pymxs import runtime as rt
+from pxr import UsdShade
+
+import traceback
+
+# the Python methods for material translation to UsdPreviewSurface
+import usd_material_writer
+
+class DefaultShaderWriter(maxUsd.ShaderWriter):
+ """
+ Default Shader Writer class handling the material translation for
+ the 3ds Max default materials
+ inherits from the base class ShaderWriter from the 3ds Max USD component
+ """
+
+ # We keep a map of all written usd texture prims, to their associated 3dsMax maps.
+ # We should clear this map between USD exports, but there is no great way to know this
+ # currently. We will clear the map when the stage changes, which necessarily implies
+ # we are in a different export.
+ _stage = None
+ _usd_shade_to_max_map = {}
+
+ @classmethod
+ def CanExport(cls, exportArgs):
+ """
+ Static class method required to determine if the class is handling the
+ translation context required by the export job. The current class only
+ handles converting materials to 'UsdPreviewSurface' elements.
+ """
+ if exportArgs.GetConvertMaterialsTo() == "UsdPreviewSurface":
+ return maxUsd.ShaderWriter.ContextSupport.Supported
+ return maxUsd.ShaderWriter.ContextSupport.Unsupported
+
+ def Write(self):
+ """Main export function that runs when the applicable material gets hit"""
+ try:
+ # If the stage changes, it is a different export, clear _usd_shade_to_max_map.
+ if self.GetUsdStage() != DefaultShaderWriter._stage:
+ DefaultShaderWriter._stage = self.GetUsdStage()
+ DefaultShaderWriter._usd_shade_to_max_map = {}
+
+ # the 'GetMaterial()' method returns the anim handle on the material
+ # need to fetch the anim on the handle to get back the pymxs.Material back
+ material = rt.GetAnimByHandle(self.GetMaterial())
+
+ # create the Shade prim
+ nodeShader = UsdShade.Shader.Define(self.GetUsdStage(), self.GetUsdPath())
+ nodeShader.CreateIdAttr("UsdPreviewSurface")
+ # assign the created Shade prim as the USD prim for the ShaderWriter
+ self.SetUsdPrim(nodeShader.GetPrim())
+
+ usd_material_writer.export_material(material, self.GetUsdStage(), nodeShader, self.GetUsdPath(), self.GetFilename(), self.GetExportArgs(), DefaultShaderWriter._usd_shade_to_max_map, self.IsUSDZFile())
+
+ except Exception as e:
+ # Quite useful to debug errors in a Python callback
+ print('Write() - Error: %s' % str(e))
+ print(traceback.format_exc())
+
+
+# register the ShaderWriter to use for the supported materials
+maxUsd.ShaderWriter.Register(DefaultShaderWriter, rt.PhysicalMaterial.nonLocalizedName) # or use the actual non-localized string "Physical Material"
+maxUsd.ShaderWriter.Register(DefaultShaderWriter, "PBR Material (Metal/Rough)")
+maxUsd.ShaderWriter.Register(DefaultShaderWriter, "PBR Material (Spec/Gloss)")
+maxUsd.ShaderWriter.Register(DefaultShaderWriter, "USD Preview Surface")
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/usd_material_reader.py b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/usd_material_reader.py
new file mode 100644
index 0000000..7ccbd6e
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/usd_material_reader.py
@@ -0,0 +1,461 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Material reader methods
+
+It makes use of `.material_conversion` and `.mat_def` user defined json files stored in the "data_files"
+folder located in this directory, or either the "$userTools" or "$userSettings" 3dsMax folders.
+
+"""
+
+import os
+import json
+import sys
+try:
+ import typing
+except ImportError:
+ # this is just for type hints
+ pass
+
+from collections import defaultdict
+
+import pymxs
+from pxr import Usd, UsdShade, Gf, UsdUtils, UsdGeom, Sdf
+
+import usd_utils
+
+__author__ = r'Autodesk Inc.'
+__copyright__ = r'Copyright 2020, Autodesk Inc.'
+
+RT = pymxs.runtime
+RT.pluginManager.loadClass(RT.USDImporter)
+
+WARN = RT.Name("warn")
+ERROR = RT.Name("error")
+
+maxver = RT.maxversion()
+if maxver[0] >= 26000: # 3ds Max 2024 and up
+ _oslbitmapfile = "OSLBitmap2.osl"
+ _uberbitmapfile = "UberBitmap2.osl"
+else:
+ _oslbitmapfile = "OSLBitmap.osl"
+ _uberbitmapfile = "UberBitmap.osl"
+
+UBERBITMAP_OUTPUTS = {
+ "rgb":1,
+ "r":2,
+ "g":3,
+ "b":4,
+ "a":5
+}
+
+def get_usd_texture_map_channel(usd_texture, material_import_options):
+ # type: (UsdShade.Shader, dict) -> int
+ """Get map channel for a usd texture. Fallback to 1 if any unexpected missing connections."""
+ st_attr_input = usd_texture.GetInput("st")
+ if not st_attr_input:
+ RT.UsdImporter.Log(ERROR, "Missing 'st' input for : {0}".format(usd_texture))
+ return 1
+ primvar_reader_outputs = st_attr_input.GetValueProducingAttributes()
+ if not primvar_reader_outputs:
+ RT.UsdImporter.Log(ERROR, "Unable to determine primvar input for : {0}".format(usd_texture))
+ return 1
+ if len(primvar_reader_outputs) > 1:
+ RT.UsdImporter.Log(WARN, "USDTextureReader has more than one 'st' input, using first input : {0}".format(usd_texture))
+
+ st_src_prim = primvar_reader_outputs[0].GetPrim()
+
+ st_source = UsdShade.Shader(st_src_prim)
+ varname_input = st_source.GetInput("varname")
+ id = st_source.GetIdAttr().Get()
+
+ if id == "UsdTransform2d":
+ # Then there must be a transform2D
+ transform_in_input = st_source.GetInput("in")
+ if not transform_in_input:
+ RT.UsdImporter.Log(ERROR, "Unable to determine primvar input for : {0}".format(usd_texture))
+ return 1
+ transform_2d_outputs = transform_in_input.GetValueProducingAttributes()
+ outputs_count = len(transform_2d_outputs)
+ if not transform_2d_outputs or outputs_count < 1:
+ RT.UsdImporter.Log(ERROR, "Unable to determine primvar input for : {0}".format(usd_texture))
+ return 1
+ st_src_prim = transform_2d_outputs[0].GetPrim()
+ usd_primvar_reader = UsdShade.Shader(st_src_prim)
+ elif id == "UsdPrimvarReader_float2":
+ usd_primvar_reader = st_source
+ else:
+ RT.UsdImporter.Log(ERROR, "Unable to determine primvar input for : {0}".format(usd_texture))
+ return 1
+
+ varname_input = usd_primvar_reader.GetInput("varname")
+ varname_value_attributes = varname_input.GetValueProducingAttributes()
+ if not varname_value_attributes:
+ RT.UsdImporter.Log(ERROR, "Unable to determine primvar input for : {0}".format(usd_texture))
+ return 1
+ if len(varname_value_attributes) > 1:
+ RT.UsdImporter.Log(WARN, "USDPrimvarReader has more than one 'varname' input, using first input : {0}".format(usd_primvar_reader))
+
+ primvar_name = varname_value_attributes[0].Get()
+
+ primvar_channel = material_import_options["usd_import_options"].GetPrimvarChannel(primvar_name)
+ if primvar_channel == -1:
+ RT.UsdImporter.Log(ERROR, "Channel is not mapped for primvar name: {0} for {1}. Using fallback channel 1.".format(primvar_name, usd_texture))
+ return 1
+
+ return primvar_channel
+
+def output_max_texture_osl(usd_texture, max_texture_id, material_import_options):
+ # type: (UsdShade.Shader, str, dict) -> pymxs.runtime.Texture
+ """Return an OSL texture reader from a USD texture node"""
+ if not usd_utils.is_supported_osl(max_texture_id):
+ RT.UsdImporter.Log(ERROR, "Unsupported OSL type specified, using {0} instead: {1}".format(_uberbitmapfile, max_texture_id))
+ max_texture_id = _uberbitmapfile
+
+ texture = RT.OSLMap()
+ max_root = RT.symbolicPaths.getPathValue("$max")
+ osl_path = os.path.join(max_root, 'OSL', max_texture_id)
+ texture.OSLPath = osl_path
+ texture.OSLAutoUpdate = True
+
+ file_attr = usd_texture.GetInput('file')
+ if file_attr:
+ resolved_file_path = file_attr.Get().resolvedPath
+
+ # if file_attr Shader input resolvedPath is an empty string, this may be because it is truly
+ # an empty string at the resolvedPath or it can be due to the UDIM case:
+ # https://forum.aousd.org/t/getting-empty-string-when-trying-to-get-resolvedpath-of-udim-path/406/2
+ if resolved_file_path == '':
+ file_path = file_attr.Get().path
+ is_udim = usd_utils.is_filepath_udim(file_path)
+
+ if is_udim:
+ dir = os.path.dirname(os.path.abspath(file_path))
+ udim_file = os.path.basename(file_path)
+ directory_name = os.fsdecode(dir)
+ udim_filename = os.fsdecode(udim_file)
+ first_udim_filename = usd_utils.find_first_valid_udim_for_filename(directory_name, udim_filename)
+
+ if first_udim_filename is None:
+ RT.UsdImporter.Log(ERROR, "UDIM format specified in imported file but no UDIM valid files found at specified directory : {0}".format(file_path))
+ else:
+ list_of_udims_with_udims_in_their_filename = usd_utils.get_all_valid_udims_from_dir_for_filename(directory_name, udim_filename)
+ udim_list_str = ' '.join(list_of_udims_with_udims_in_their_filename)
+
+ texture.filename = file_path
+ texture.UDIM = 1
+ texture.loadUDIM = os.path.join(directory_name, udim_filename)
+ texture.Filename_UDIMList = udim_list_str
+
+ elif os.path.isfile(file_path):
+ # likely user error where they set empty string for "inputs:file" Shader of UsdUVTexture
+ # or could be more cases where resolution fails for whatever reason
+ texture.filename = file_path
+ else:
+ texture.filename = ''
+ else:
+ texture.filename = resolved_file_path
+
+
+ primvar_channel = get_usd_texture_map_channel(usd_texture, material_import_options)
+
+ st_input = usd_texture.GetInput("st")
+ wrap_s_input = usd_texture.GetInput("wrapS")
+ wrap_t_input = usd_texture.GetInput("wrapT")
+
+ wrap_s_val = None
+ wrap_t_val = None
+ scale_val = None
+ rotation_val = None
+ translation_val = None
+
+ if wrap_s_input:
+ wrap_s_val = wrap_s_input.Get()
+
+ if wrap_t_input:
+ wrap_t_val = wrap_t_input.Get()
+
+ if st_input and st_input.GetConnectedSource() is not None:
+ st_conn_source = st_input.GetConnectedSource()[0]
+
+ scale_input = st_conn_source.GetInput("scale")
+ rotation_input = st_conn_source.GetInput("rotation")
+ translation_input = st_conn_source.GetInput("translation")
+
+ scale_val = scale_input.Get()
+ rotation_val = rotation_input.Get()
+ translation_val = translation_input.Get()
+
+ if max_texture_id.lower() == _uberbitmapfile.lower():
+ texture.UVSet = primvar_channel
+
+ # determine and apply wrap mode
+ if wrap_s_val and wrap_t_val and wrap_s_val != wrap_t_val:
+ RT.UsdImporter.Log(WARN, 'No support for differing wrap modes, defined in wrapS and wrapT as defined in {0}. Setting value to "periodic" for import.'.format(usd_texture.GetPrim().GetName()))
+ texture.WrapMode = "periodic"
+ elif wrap_s_val or wrap_t_val:
+ usd_wrap_mode = wrap_s_val or wrap_t_val
+ if usd_wrap_mode == "black":
+ texture.WrapMode = "black"
+ elif usd_wrap_mode == "clamp":
+ texture.WrapMode = "clamp"
+ elif usd_wrap_mode == "repeat":
+ texture.WrapMode = "periodic"
+ elif usd_wrap_mode == "mirror":
+ texture.WrapMode = "mirror"
+ elif usd_wrap_mode == "useMetadata":
+ RT.UsdImporter.Log(WARN, 'No support for wrap mode "useMetadata" found in {0}. Setting value to "periodic" for import.'.format(usd_texture.GetPrim().GetName()))
+ texture.WrapMode = "periodic"
+ else:
+ # default wrap mode in 3dsmax
+ texture.WrapMode = "periodic"
+
+ if translation_val:
+ # apply translation
+ texture.offset = RT.Point3(-translation_val[0], -translation_val[1], 0)
+
+ if rotation_val and not usd_utils.float_almost_equal(rotation_val, 0):
+ # apply rotation
+ texture.rotate = rotation_val
+ texture.RotAxis = RT.Point3(0, 0, 1) # fix rotation axis to (0, 0, 1)
+ texture.RotCenter = RT.Point3(0, 0, 0) # fix rotation axis to (0, 0, 0) (instead of (0.5, 0.5, 0) uber bitmap default)
+
+ rot_matrix = RT.RotateZMatrix(-rotation_val)
+
+ usd_translation = RT.Point3(texture.offset[0], texture.offset[1], 0)
+ rotated_translation = usd_translation * rot_matrix
+
+ texture.offset = rotated_translation
+
+ if scale_val:
+ # apply scaling
+
+ if scale_val[0] == scale_val[1]:
+ texture.tiling = RT.Point3(1, 1, 1) # fix tiling to (1, 1, 1)
+ texture.scale = texture.tiling[0] / scale_val[0] if scale_val[0] != 0 else sys.float_info.max
+ texture.offset = RT.Point3(
+ texture.offset[0] / scale_val[0] if scale_val[0] != 0 else sys.float_info.max,
+ texture.offset[1] / scale_val[1] if scale_val[1] != 0 else sys.float_info.max,
+ 0)
+ else:
+ if rotation_val != 0:
+ RT.UsdImporter.Log(WARN, "Non uniform texture scaling with an applied rotation may result in incorrect texture mapping for {0}.".format(usd_texture.GetPrim().GetName()))
+
+ texture.scale = 1 # fix scale to 1
+ texture.tiling = RT.Point3(
+ scale_val[0] if scale_val[0] != 0 else sys.float_info.max,
+ scale_val[1] if scale_val[1] != 0 else sys.float_info.max,
+ 0)
+ texture.offset = RT.Point3(
+ texture.offset[0] / scale_val[0] if scale_val[0] != 0 else sys.float_info.max,
+ texture.offset[1] / scale_val[1] if scale_val[1] != 0 else sys.float_info.max,
+ 0)
+
+ elif max_texture_id.lower() == _oslbitmapfile.lower():
+ pos_map = RT.OSLMap()
+ pos_osl_path = os.path.join(max_root, 'OSL', 'GetUVW.osl')
+ pos_map.OSLPath = pos_osl_path
+ pos_map.OSLAutoUpdate = True
+ texture.Pos_map = pos_map
+ pos_map.UVSet = primvar_channel
+
+ return texture
+
+def valid_texture_type(max_texture_type):
+ # type: (str) -> bool
+ """Is this a valid texture type?"""
+ if hasattr(RT, max_texture_type):
+ return True
+ if max_texture_type.lower().endswith(".osl") and hasattr(RT, 'OSLMap'):
+ return True
+ return False
+
+def output_max_classic_bitmap(usd_texture, material_import_options):
+ # type: (UsdShade.Shader, dict) -> pymxs.MXSWrapperBase
+ """Output max bitmap from usd texture"""
+ texture = RT.Bitmaptexture()
+ file_attr = usd_texture.GetInput('file')
+ if file_attr:
+ file_path = file_attr.Get().resolvedPath
+ texture.filename = file_path
+ primvar_channel = get_usd_texture_map_channel(usd_texture, material_import_options)
+ texture.coordinates.mapChannel = primvar_channel
+ return texture
+
+def output_max_texture(usd_texture, material_import_options, usd_shader_to_max_map={}):
+ # type: (UsdShade.Shader, dict, dict) -> pymxs.MXSWrapperBase
+ """Output max texture from usd texture, based on material_import_options"""
+ max_texture_id = material_import_options.get("texture_target_id", "Bitmaptexture")
+ if not valid_texture_type(max_texture_id):
+ RT.UsdImporter.Log(WARN, "Invalid texture type specified: {0}".format(max_texture_id))
+ return
+
+ max_texture = usd_shader_to_max_map.get(usd_texture.GetPath(), None)
+ if not max_texture:
+ if max_texture_id.lower().endswith('.osl'):
+ max_texture = output_max_texture_osl(usd_texture, max_texture_id, material_import_options)
+ else:
+ max_texture = output_max_classic_bitmap(usd_texture, material_import_options)
+
+ map_name = usd_texture.GetPath().name
+ max_texture.name = map_name
+ usd_shader_to_max_map[usd_texture.GetPath()] = max_texture
+ return max_texture
+
+def output_max_material(shader, material_import_options, usd_shader_to_max_map={}):
+ # type: (UsdShade.Shader, dict, dict) -> pymxs.MXSWrapperBase
+ """
+ Create a max material and texture connections from a usd shader.
+ """
+ source_id = usd_utils.get_id_from_mat(shader)
+ if not source_id:
+ RT.UsdImporter.Log(WARN, "Unknown shader, cannot get an id: {0}".format(shader))
+ return
+ target_id = material_import_options.get("material_target_id", "MaxUsdPreviewSurface")
+
+ conversion_recipe = usd_utils.get_conversion_recipe(source_id, "usd", target_id, "3dsmax")
+ if not conversion_recipe:
+ RT.UsdImporter.Log(WARN, "No conversion recipe found for {0}".format(shader))
+ return
+
+ # mapping_from_material interperets the JSON conversion_mapping, and returns
+ # a dictionary with the calculated values for each parameter for the target material
+ conversion = usd_utils.mapping_from_material(shader, conversion_recipe)
+ # Bundle the calculated conversion with the whole recipe for passing on
+ conversion_recipe["conversion"] = conversion
+
+ target_material_id = conversion_recipe["target_material"]["id"]
+ target_mat_domain = conversion_recipe["target_material"]["domain"]
+
+ klass = getattr(RT, target_material_id)
+ mat = klass()
+
+ target_mat_def = usd_utils.get_material_def(target_material_id, target_mat_domain)
+ target_inputs = target_mat_def["inputs"]
+
+ # Iterate through all the conversion mapping items.
+ # The key is expected to be the target parameter of the material,
+ # and the value is the mapped value from the source material.
+ for target_param, mapped_value in conversion.items():
+ if mapped_value == None:
+ # The value is None, continue.
+ continue
+
+ # Get the input type for the target material parameter, defined by material definition.
+ input_type = target_inputs.get(target_param, None)
+ if input_type is None:
+ RT.UsdImporter.Log(ERROR, "Input type not defined in material definition for target_param: {0} - {1}".format(target_param, target_mat_def))
+ continue
+
+ # Handle params that may be mapped to usd attributes
+ mapped_to_attribute = isinstance(mapped_value, Usd.Attribute)
+ if input_type == "use_map":
+ # In max materials usually have a boolean 'use_map' for texture maps.
+ # When this maps to some USD Attribute input, this means there
+ # is a texture and so this must be true.
+ setattr(mat, target_param, True)
+ continue
+ elif mapped_to_attribute and input_type in ["texturemap", "normalmap"]:
+ # The value is a USD Attribute, now we need to get the attribute source.
+ usd_input_prim = mapped_value.GetPrim()
+ if usd_input_prim.IsValid():
+
+ channel_output = UsdShade.Output(mapped_value)
+ if not channel_output:
+ RT.UsdImporter.Log(WARN, "Unsupported UsdShade connection:{0}".format(str(mapped_value)))
+ continue
+
+ # 2 scenarios. Either we are connected directly to a shader output, or we are
+ # connected to a node graph's output.
+ # If we have a value producing attribute (node graphs), we need to figure out the
+ # actual shader output (digs down node the node graph recursively).
+ value_producing_attrs = channel_output.GetValueProducingAttributes()
+ # Shader connection
+ if len(value_producing_attrs) == 0:
+ usd_shader = UsdShade.Shader(usd_input_prim)
+ # Node graph connection
+ else:
+ value_attr = value_producing_attrs[0]
+ shader_prim = value_attr.GetPrim()
+ if not shader_prim.IsA(UsdShade.Shader):
+ RT.UsdImporter.Log(WARN, "Unsupported UsdShade connection:{0}".format(str(value_attr)))
+ continue
+ usd_shader = UsdShade.Shader(shader_prim)
+
+ # Our target material input type is a texturemap (or normalmap)
+ # We will need to output a max bitmap texture reader of some kind.
+
+ max_texture = output_max_texture(usd_shader, material_import_options, usd_shader_to_max_map)
+ if RT.classOf(max_texture) == RT.OSLMap:
+ # with oslmaps we can connect specific texture channels
+ output = UsdShade.Output(mapped_value)
+ channels = None
+ if output:
+ value_producing_attrs = output.GetValueProducingAttributes()
+ # NodeGraph connection
+ if len(value_producing_attrs) > 0:
+ value_attr = value_producing_attrs[0]
+ channels = value_attr.GetBaseName()
+ if not channels:
+ # Shader connection
+ channels = mapped_value.GetBaseName()
+ multi_channel_map = RT.MultiOutputChannelTexmapToTexmap()
+ multi_channel_map.sourceMap = max_texture
+ out_channel_index = UBERBITMAP_OUTPUTS.get(channels, 1)
+ multi_channel_map.outputChannelIndex = out_channel_index
+ multi_channel_map.name = "{0}:{1}".format(max_texture.name, channels)
+ setattr(mat, target_param, multi_channel_map)
+ else:
+ # otherwise - a simple connection for standard bitmaps
+ setattr(mat, target_param, max_texture)
+ else:
+ RT.UsdImporter.Log(WARN, "USD Attribute input prim is not valid:{0}".format(mapped_value))
+ continue
+ elif not mapped_to_attribute and input_type in ["texturemap", "normalmap"]:
+ # this occurs as a side effect of how the mapping conversion system was made:
+ # when we have a property that has an associated map property and the property
+ # value is a non-attribute, the associated map property still gets processed by
+ # function and ends up having no meaning in terms of the import process.
+ continue
+ elif mapped_to_attribute:
+ # Skip any remaining mapped_to_attribute mappings.
+ # This is expected because we map N to 1 : max param to one USD param.
+ # For example, this is the mapping for USDPreviewSurface to PhysicalMaterial.
+ # Keys on the left are the parameters for PhysicalMaterial, right-hand side is USD:
+ # "roughness": "roughness"
+ # "roughness_map": "roughness"
+ # "roughness_map_on": "roughness"
+ # All of these might evaluate to a USDAttribute, or they all might evaluate to a float.
+ continue
+
+ # handle params mapped to values
+ if input_type == "color":
+ if isinstance(mapped_value, Gf.Vec3f):
+ # Convert USD color (Vec3f) to a Max color
+ color = RT.color(mapped_value[0]*255, mapped_value[1]*255, mapped_value[2]*255)
+ setattr(mat, target_param, color)
+ else:
+ # Anything else is unexpected.
+ RT.UsdImporter.Log(WARN, "Expected input for color param:{0}".format(mapped_value))
+ elif input_type in ["float", "int", "boolean"]:
+ # A simple value, can be set directly.
+ setattr(mat, target_param, mapped_value)
+ else:
+ # Anything else is unexpected.
+ RT.UsdImporter.Log(WARN, "Unexpected input type or mapped value - {0}:{1}:{2}".format(input_type, mapped_value, target_param))
+ return mat
+
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/usd_material_writer.py b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/usd_material_writer.py
new file mode 100644
index 0000000..ede5af7
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/usd_material_writer.py
@@ -0,0 +1,686 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Register export callback used for material conversions.
+
+The following script registers a callback function for the `export_complete_callback` function defined
+in the 3dsMax USD component plugin. In this example, we use the export complete callback to define and
+apply material export conversion logic. This is achieved with the use of `.material_conversion` and `.mat_def`
+json files, which are user defined, which can be stored in the "data_files" folder located in this directory, or
+either the "$userTools" or "$userSettings" 3dsMax folders.
+"""
+
+import os
+import sys
+import json
+import pathlib
+
+from collections import defaultdict
+
+import itertools
+try:
+ import typing
+except ImportError:
+ # this is just for type hints
+ pass
+import numbers
+import pymxs
+from pxr import Usd, UsdShade, Sdf, Tf, Gf, UsdUtils
+
+import usd_utils
+import maxUsd
+
+__author__ = r'Autodesk Inc.'
+__copyright__ = r'Copyright 2020, Autodesk Inc.'
+
+RT = pymxs.runtime
+RT.pluginManager.loadClass(RT.USDExporter)
+
+WARN = RT.Name("warn")
+ERROR = RT.Name('error')
+
+sdf_type_map = {
+ "float": Sdf.ValueTypeNames.Float,
+ "int": Sdf.ValueTypeNames.Int,
+ "float3": Sdf.ValueTypeNames.Float3,
+ "normal3f": Sdf.ValueTypeNames.Normal3f,
+ "color3f": Sdf.ValueTypeNames.Color3f
+}
+
+OSL_BITMAP_OUTPUTS = {
+ 1:"rgb",
+ 2:"r",
+ 3:"g",
+ 4:"b",
+ 5:"a"
+}
+
+# default export options
+_material_export_options = {
+ "bake": False,
+ "bake_image_type": "png",
+ # Typically a relative sdf path from the configured root primitive, where to export materials.
+ # Assuming default export options, this would resolve to /Materials".
+ # If the path specified here is absolute, then it is used as is.
+ "materials_root": "Materials",
+ "bake_dir": "baked_textures",
+ "bitmap_dimensions": (512, 512),
+ "st_input_name": "map1",
+ "target_material_id": "UsdPreviewSurface",
+ "relative_texture_paths": True,
+ "usd_filename": None
+}
+
+def bake_bitmap(from_tex, material_export_options, file_path=None):
+ """
+ Bake the bitmap for a max texture. Return baked file path
+ """
+ if file_path:
+ bake_dir = os.path.dirname(file_path)
+ else:
+ export_dir = os.path.dirname(material_export_options["usd_filename"])
+ bake_dir = os.path.join(export_dir, material_export_options["bake_dir"])
+ file_path = os.path.join(bake_dir, from_tex.name)
+
+ if not os.path.exists(bake_dir):
+ os.makedirs(bake_dir, exist_ok=True)
+
+ base_name = os.path.basename(file_path)
+ render_name, ext = os.path.splitext(base_name)
+ render_ext = material_export_options.get("bake_image_type")
+ render_file_path = os.path.join(bake_dir, render_name + '.' + render_ext)
+
+ bitmap_dimensions = material_export_options.get("bitmap_dimensions")
+ bitmap = RT.renderMap(from_tex, filename=render_file_path, size=RT.point2(bitmap_dimensions[0], bitmap_dimensions[1]))
+ RT.save(bitmap)
+ RT.close(bitmap)
+
+ return render_file_path
+
+def set_bitmap_scale_bias_sourcecolorspace(uv_texture_shader, is_normal_map):
+ source_color_space_input = uv_texture_shader.CreateInput("sourceColorSpace", Sdf.ValueTypeNames.Token)
+ # when this is implemented in 2024, we need update the logic to check if the max bitmap
+ # color space is set to "raw" or not, for not this is not implemented yet
+ source_color_space_input.Set("raw")
+ if is_normal_map:
+ scale_input = uv_texture_shader.CreateInput("scale", Sdf.ValueTypeNames.Float4)
+ bias_input = uv_texture_shader.CreateInput("bias", Sdf.ValueTypeNames.Float4)
+ scale_input.Set(Gf.Vec4f(2, 2, 2, 1))
+ bias_input.Set(Gf.Vec4f(-1, -1, -1, 0))
+
+def get_max_map_output_channel(max_map):
+ # type: (pymxs.MXSWrapperBase) -> str
+ """Get the output channel as string for the max texture map.
+ Returns rgb by default if not a MultiOutput map."""
+ output_channel = "rgb" # default
+ if RT.ClassOf(max_map) == RT.MultiOutputChannelTexmapToTexmap:
+ output_channel = OSL_BITMAP_OUTPUTS.get(max_map.outputChannelIndex, "")
+ if not output_channel:
+ source_map = max_map.sourceMap
+ RT.UsdExporter.Log(WARN, "Unsupported output channel index for {0}: {1}. Using 'r' channel instead.".format(source_map, max_map.outputChannelIndex))
+ # OSL bitmap lookups index 6-7 are 'luminance' and 'average' which don't have a direct translation.
+ # Fallback to r as it's probably reasonably close.
+ output_channel = 'r'
+ return output_channel
+
+def output_usd_texture(from_max_tex, stage, parent_path, material_export_options, usd_shade_to_max_map, is_normal_map):
+ # type: (pymxs.MXSWrapperBase, Usd.Stage, Sdf.Path, dict, dict, bool) -> UsdShade.Shader
+ """
+ Outupt a usd texture from a max texture.
+ """
+ file_path = None
+ texture_lookup = None
+ bake_enabled = material_export_options.get("bake", False)
+ texture_graph_name = Tf.MakeValidIdentifier(from_max_tex.name)
+ output_channel = "rgb"
+
+ if bake_enabled:
+ file_path = bake_bitmap(from_max_tex, material_export_options, file_path=file_path)
+ else:
+ # see if we can resolve to a texture lookup from this input
+ texture_lookup = resolve_texture_lookup(from_max_tex)
+ if texture_lookup and hasattr(texture_lookup, 'filename'):
+ file_path = usd_utils.get_file_path_mxs(texture_lookup.filename)
+ texture_graph_name = Tf.MakeValidIdentifier(texture_lookup.name)
+ # When figuring out the used channel (rgb,r,g,b or a), do not use the
+ # resolved texture, as the information is not in that node. It lives
+ # in MultiOutputChannelTexmapToTexmap which will be slotted into the
+ # material.
+ output_channel = get_max_map_output_channel(from_max_tex)
+ elif not texture_lookup and hasattr(from_max_tex, 'filename'):
+ file_path = usd_utils.get_file_path_mxs(from_max_tex.filename)
+ texture_lookup = from_max_tex
+
+ if not file_path:
+ # no valid file path for this texture, skip it.
+ RT.UsdExporter.Log(WARN, "Unsupported texture map: {0}".format(from_max_tex))
+ return None, texture_lookup, None, None
+
+ # Make the file path relative.
+ if material_export_options.get("relative_texture_paths", True) and material_export_options["usd_filename"]:
+ start_path = os.path.dirname(material_export_options["usd_filename"])
+ file_path = usd_utils.safe_relpath(file_path, start_path)
+
+ texture_graph_path = parent_path.AppendChild(texture_graph_name)
+
+ # Keep track of whether we are creating a new texture in this call, or simply adding new outputs/connections.
+ is_new = True
+
+ # This is where we check if the texture_graph is already created or not.
+ # If you connect a texturemap to more than one material input, this code will be run multiple times
+ # on the same texture node.
+ # But it will only export once, because we will find the existing texture_graph in the usd_shade_to_max_map
+ texture_graph = None
+ uv_texture_shader = None
+ if usd_shade_to_max_map.get(texture_graph_path) == texture_lookup:
+ # already exported this texture, but we may need to do more connections
+ texture_graph = UsdShade.NodeGraph.Get(stage, texture_graph_path)
+ uv_texture_shader = UsdShade.Shader.Get(stage, texture_graph_path.AppendChild(texture_graph_name))
+ is_new = False
+ if not texture_graph:
+ # TODO : uniquifying of the paths doesnt work as expected, as this function can be called multiple times
+ # add new outputs. This will not work correctly as the second call will not find the unique name created here.
+ texture_graph_path = uniquify_path(stage, parent_path, texture_graph_name)
+ texture_graph = UsdShade.NodeGraph.Define(stage, texture_graph_path)
+
+ uv_texture_shader = UsdShade.Shader.Define(stage, texture_graph_path.AppendChild(texture_graph_name))
+
+ uv_texture_shader.CreateIdAttr("UsdUVTexture")
+ usd_shade_to_max_map[texture_graph.GetPath()] = texture_lookup
+ # Set to repeat by default (USD sets to "metadata" by default).
+ wrap_s_input = uv_texture_shader.CreateInput("wrapS", Sdf.ValueTypeNames.Token)
+ wrap_t_input = uv_texture_shader.CreateInput("wrapT", Sdf.ValueTypeNames.Token)
+ wrap_s_input.Set("repeat")
+ wrap_t_input.Set("repeat")
+
+ if texture_lookup:
+ if RT.classOf(texture_lookup) == RT.Bitmaptexture:
+ if texture_lookup.coordinates.U_Tile == True:
+ wrap_s_input.Set("repeat")
+ if texture_lookup.coordinates.V_Tile == True:
+ wrap_t_input.Set("repeat")
+ if texture_lookup.coordinates.U_Mirror == True:
+ wrap_s_input.Set("mirror")
+ if texture_lookup.coordinates.V_Mirror == True:
+ wrap_t_input.Set("mirror")
+ if not texture_lookup.coordinates.U_Tile and not texture_lookup.coordinates.U_Mirror:
+ wrap_s_input.Set("black")
+ if not texture_lookup.coordinates.V_Tile and not texture_lookup.coordinates.V_Mirror:
+ wrap_t_input.Set("black")
+
+ if RT.classOf(texture_lookup) == RT.OSLMap:
+ osl_file_name = os.path.basename(texture_lookup.OSLPath).lower()
+ # The two OSL bitmaps we support handle wrapping the same way.
+ # U and V wrap modes are not configured individually.
+ if osl_file_name == "uberbitmap.osl" or osl_file_name == "oslbitmap.osl" or \
+ osl_file_name == "uberbitmap2.osl" or osl_file_name == "oslbitmap2.osl":
+ # The "default" wrap mode means "whatever the default is in the renderer".
+ # We will assume "black" here, as this is how it shows in the Max viewport, and
+ # it seems to be the most common default.
+ if texture_lookup.WrapMode == "black" or texture_lookup.WrapMode == "default":
+ wrap_s_input.Set("black")
+ wrap_t_input.Set("black")
+ elif texture_lookup.WrapMode == "mirror":
+ wrap_s_input.Set("mirror")
+ wrap_t_input.Set("mirror")
+ elif texture_lookup.WrapMode == "periodic":
+ wrap_s_input.Set("repeat")
+ wrap_t_input.Set("repeat")
+ elif texture_lookup.WrapMode == "clamp":
+ wrap_s_input.Set("clamp")
+ wrap_t_input.Set("clamp")
+
+ if usd_utils.has_non_ascii_char(file_path):
+ RT.UsdExporter.Log(WARN, "USD does not support unicode in filepaths. Please remove invalid characters from texture filepaths.")
+ else:
+ windows_path = pathlib.PureWindowsPath(file_path)
+ pathStr = str(windows_path.as_posix())
+ # Normalizing the path will strip the ./ from the relative path - as far as the OS is concerned, that is fine.
+ # However, the PXR ArDefaultResolver, if not seeing a ./ will also look into any defined search paths, having the ./
+ # prefix will make it understand that the relative path is anchored to the layer we are exporting. (see
+ # ArDefaultResolver::SetDefaultSearchPath())
+ if not windows_path.is_absolute():
+ pathStr = "./" + pathStr
+ uv_texture_shader.CreateInput("file", Sdf.ValueTypeNames.Asset).Set(pathStr)
+
+ if texture_lookup:
+ if RT.classOf(texture_lookup) == RT.Bitmaptexture:
+ if RT.isProperty(texture_lookup, "bitmap") and texture_lookup.bitmap and texture_lookup.bitmap.gamma == 1.0:
+ set_bitmap_scale_bias_sourcecolorspace(uv_texture_shader, is_normal_map)
+ elif RT.classOf(texture_lookup) == RT.OSLMap:
+ osl_file_name = os.path.basename(texture_lookup.OSLPath).lower()
+ if osl_file_name == "uberbitmap.osl" or osl_file_name == "oslbitmap.osl":
+ if texture_lookup.AutoGamma == 0 and texture_lookup.ManualGamma == 1.0:
+ set_bitmap_scale_bias_sourcecolorspace(uv_texture_shader, is_normal_map)
+ if osl_file_name == "uberbitmap2.osl" or osl_file_name == "oslbitmap2.osl":
+ if texture_lookup.ManualGamma == 1.0:
+ set_bitmap_scale_bias_sourcecolorspace(uv_texture_shader, is_normal_map)
+
+ # Add the output to the texture shader, and wire it up to the node graph output.
+ if len(output_channel) == 3:
+ uv_texture_shader.CreateOutput(output_channel, Sdf.ValueTypeNames.Float3)
+ graph_output = texture_graph.CreateOutput(output_channel, Sdf.ValueTypeNames.Float3)
+ graph_output.ConnectToSource(uv_texture_shader.ConnectableAPI(), output_channel)
+ elif len(output_channel) == 1:
+ uv_texture_shader.CreateOutput(output_channel, Sdf.ValueTypeNames.Float)
+ graph_output = texture_graph.CreateOutput(output_channel, Sdf.ValueTypeNames.Float)
+ graph_output.ConnectToSource(uv_texture_shader.ConnectableAPI(), output_channel)
+ else:
+ RT.UsdExporter.Log(RT.Name('error'), "Texture sampler connections of {0} channels unsupported.".format(len(output_channel)))
+
+ return texture_graph, texture_lookup, uv_texture_shader, is_new
+
+
+def output_usd_uv_reader(stage, parent_path, primvar_name):
+ """
+ Output usd texture sampler from max texture
+ """
+ uv_reader_name = "PrimvarReader_{0}".format(primvar_name)
+ uv_reader_path = uniquify_path(stage, parent_path, uv_reader_name)
+ uv_reader = UsdShade.Shader.Define(stage, uv_reader_path)
+ uv_reader.CreateIdAttr("UsdPrimvarReader_float2")
+ return uv_reader
+
+def normal_bumps():
+ """Returns a list of Normal Bump types."""
+ normal_bumps = [RT.Normal_Bump]
+ if hasattr(RT, "VRayNormalMap"):
+ normal_bumps.append(RT.VRayNormalMap)
+ return normal_bumps
+
+def resolve_texture_lookup(in_value):
+ # type: (pymxs.MXSWrapperBase) -> pymxs.MXSWrapperBase
+ """Given a material input, resolve to the source bitmap lookup if possible."""
+ in_type = RT.classOf(in_value)
+ if in_type == RT.Bitmaptexture:
+ return in_value
+ elif in_type == RT.MultiOutputChannelTexmapToTexmap:
+ # handle known OSL bitmap lookups.
+ source_map = in_value.sourceMap
+ if source_map and RT.classOf(source_map) == RT.OSLMap:
+ if usd_utils.is_supported_osl(source_map.OSLPath):
+ return source_map
+ # make sure normal bump has a texture input
+ elif in_type in normal_bumps():
+ normal_map = in_value.normal_map
+ if normal_map:
+ return resolve_texture_lookup(normal_map)
+
+def uniquify_path(stage, parent_path, name):
+ # type: (Usd.Stage, Sdf.Path, str) -> Sdf.Path
+ """Generate a unique path"""
+ unique_path = parent_path.AppendChild(Tf.MakeValidIdentifier(name))
+ # due to max allowing duplicate names, the following is to deduplicate the paths in
+ # case there already exists a prim with said path in the stage (otherwise it will be overriden)
+ prim = stage.GetPrimAtPath(unique_path)
+ uniq = 1
+ while prim.IsValid():
+ unique_path = parent_path.AppendChild("{0}_{1}".format(Tf.MakeValidIdentifier(name), uniq))
+ uniq += 1
+ prim = stage.GetPrimAtPath(unique_path)
+ return unique_path
+
+def map_channel_from_texturemap(texturemap):
+ # type: (pymxs.texturemap) -> int
+ """From a texturemap, get the channel map index"""
+ map_channel = None
+ warn_msg = ""
+ if RT.classOf(texturemap) == RT.Bitmaptexture:
+ map_channel = texturemap.coordinates.mapChannel
+ elif RT.classOf(texturemap) == RT.MultiOutputChannelTexmapToTexmap:
+ source_map = texturemap.sourceMap
+ if source_map and RT.classOf(source_map) == RT.OSLMap:
+ if hasattr(source_map, 'UVSet'):
+ # UVSet is the attribute for input map channel of uberbitmap
+ # If doesn't exist, fallback to 1 below
+ map_channel = source_map.UVSet
+ elif hasattr(source_map, 'Pos_map'):
+ if hasattr(source_map.Pos_map, 'UVSet'):
+ map_channel = source_map.Pos_map.UVSet
+ else:
+ warn_msg + "Could not derive UVSet from: {0}\n".format(source_map)
+
+ if not map_channel:
+ warn_msg + "OSL texturemap type does not have UVSet: {0}\n".format(texturemap)
+ else:
+ warn_msg + "Unknown texturemap type for map channel: {0}\n".format(texturemap)
+
+ if not map_channel:
+ # default fallback to channel 1
+ RT.UsdExporter.Log(WARN, warn_msg + "Using fallback map channel 1 for texture: {0}.".format(texturemap))
+ map_channel = 1
+
+ return map_channel
+
+def primvar_mapping_from_texturemap(texturemap, material_export_options):
+ # type: (pymxs.texturemap, dict) -> str
+ """From a texturemap, get the primvar mapping string."""
+ channel_map = map_channel_from_texturemap(texturemap)
+ return material_export_options["usd_export_options"].GetChannelPrimvarName(channel_map)
+
+def check_and_warn_for_non_uniform_scaling_with_rotation(scale_x, scale_y, rotation, texture_name):
+ # type: (float, float, float, str) -> None
+ """Check if the scaling is not uniform. If it is not, and we have a rotation applied, we might get skewed textures in USD."""
+ uniformScaling = usd_utils.float_almost_equal(scale_x, scale_y)
+ if rotation != 0.0 and not uniformScaling:
+ RT.UsdExporter.Log(WARN, "Non uniform texture scaling with an applied rotation may result in incorrect texture mapping for {0}.".format(texture_name))
+
+
+def create_2dtransform_for_bitmap(stage, texture_graph, uv_primvar_name, uv_reader, st_input, scale_val = Gf.Vec2f(1.0, 1.0), rotation_val = 0.0, translation_val = Gf.Vec2f(0.0, 0.0)):
+ """
+ Create UsdTransform2d Shader prim if necessary
+ """
+ if scale_val != (1.0, 1.0) or rotation_val != 0.0 or translation_val != (0.0, 0.0):
+ # Connect a UsdTransform2d node to support texture transforms (offset,translate,rotate).
+ transform_2d_name = "TextureTransform_{0}".format(uv_primvar_name)
+ transform_2d_path = uniquify_path(stage, texture_graph.GetPath(), transform_2d_name)
+ transform_2d_prim = UsdShade.Shader.Define(stage, transform_2d_path)
+
+ transform_2d_prim.CreateIdAttr("UsdTransform2d")
+
+ transform_input = transform_2d_prim.CreateInput("in", Sdf.ValueTypeNames.Float2)
+ transform_input.ConnectToSource(uv_reader.ConnectableAPI(), "result")
+ st_input.ConnectToSource(transform_2d_prim.ConnectableAPI(), "result")
+
+ # Transform inputs...
+ if scale_val != (1.0, 1.0):
+ scale_input = transform_2d_prim.CreateInput("scale", Sdf.ValueTypeNames.Float2)
+ scale_input.Set(scale_val)
+
+ if rotation_val != 0.0:
+ rotation_input = transform_2d_prim.CreateInput("rotation", Sdf.ValueTypeNames.Float)
+ rotation_input.Set(rotation_val)
+
+ if translation_val != (0.0, 0.0):
+ translation_input = transform_2d_prim.CreateInput("translation", Sdf.ValueTypeNames.Float2)
+ translation_input.Set(translation_val)
+
+def write_usd_material(from_mat, conversion_mapping, stage, shader, shader_path, material_export_options, usd_shade_to_max_map):
+ # type: (pymxs.MXSWrapperBase, dict, Usd.Stage, Usd.Shader, Sdf.Path, dict, dict) -> None
+ """
+ Output usd material from a max material
+ """
+ target_mat_def = conversion_mapping["target_material"]
+ target_inputs = target_mat_def["inputs"]
+
+ mat_path = shader_path.GetParentPath()
+ material = UsdShade.Material.Get(stage, mat_path)
+
+ # mapping_from_material interperets the JSON conversion_mapping, and returns
+ # a dictionary with the calculated values for each parameter for the target material
+ mapping = usd_utils.mapping_from_material(from_mat, conversion_mapping)
+ # Bundle the calculated conversion with the whole recipe for passing on
+ conversion_mapping["conversion"] = mapping
+
+ # Iterate over the target material definition items which are both strings: "target_param_name":"target_input_type"
+ for target_param_name, target_input_type in target_inputs.items():
+ # store off the calculated in_value from mapping
+ in_value = mapping.get(target_param_name)
+ if in_value == None:
+ # There is no mapping for this target_param_name
+ continue
+ texture_lookup = False
+ # note: If you try to compare types that are coming from a maxscript object within a Python script,
+ # disregarding the fact the types could now be Python native type, it will generate a MAXScript
+ # exception about an unsupported method being called (the operator '>').
+ # (i.e. a float keeps being a Python float and not a MAXScript Value representing a float
+ # which a call to pymxs.runtime.classof ends up doing)
+ in_type = type(in_value)
+ if in_type == pymxs.MXSWrapperBase:
+ in_type = RT.classOf(in_value)
+ texture_lookup = resolve_texture_lookup(in_value)
+ bake_enabled = material_export_options.get("bake", False)
+
+ texture_src_type = ""
+ if in_type == RT.Color:
+ # The input type is a max color, convert to USD color.
+ value = (in_value.r/255, in_value.g/255, in_value.b/255)
+ elif texture_lookup or (RT.superClassOf(in_value) == RT.textureMap and bake_enabled):
+ # We've found a texture lookup, or we just want to bake this input texturemap.
+ if target_input_type == "normal3f":
+ # If our input type is normal3f, we only want to map normal maps, but not bump maps.
+ texture_src_type = usd_utils.get_src_type(target_param_name, conversion_mapping)
+ if not texture_src_type == "normalmap" and not in_type in normal_bumps():
+ msg = "Texture source must be a normal map for {0}.{1}: {2}".format(from_mat, target_param_name, texture_lookup)
+ RT.UsdExporter.Log(WARN, msg)
+ continue
+
+ uv_primvar_name = primvar_mapping_from_texturemap(in_value, material_export_options)
+ if not uv_primvar_name:
+ RT.UsdExporter.Log(ERROR, "Texture ({0}) using a channel not mapped to a primvar.".format(in_value))
+ continue
+
+ texture_graph, resolved_texture, uv_texture, is_new = output_usd_texture(in_value, stage, mat_path.GetParentPath(), material_export_options, usd_shade_to_max_map, (texture_src_type == "normalmap"))
+
+ if not texture_graph:
+ # output_usd_texture could return None
+ continue
+
+ # As per USD documentation, shader connections should not cross material boundaries.
+ # We want to be able to reuse the texture maps we export accross multiple materials,
+ # so they live outside of the materials. To conform to the connectivity rules, we create
+ # an internal reference from inside the material, to the texture's NodeGraph. The
+ # connection is made to the reference, which in within the material, and therefor we
+ # respect the rules.
+ texture_ref = UsdShade.NodeGraph.Define(stage, mat_path.AppendChild(texture_graph.GetPrim().GetName()))
+ texture_ref.GetPrim().GetReferences().AddInternalReference(texture_graph.GetPath())
+ value = texture_ref
+
+ # If the texture is new (i.e. we were not just adding some outputs), add the UsdTransform2d
+ # and UsdPrimvarReader_float2 shading nodes to the graph.
+ if is_new:
+ uv_reader = output_usd_uv_reader(stage, texture_graph.GetPath(), uv_primvar_name)
+ input_frame_name = "frame:{0}".format(uv_primvar_name)
+
+ input_frame_primvar = texture_graph.CreateInput(input_frame_name, Sdf.ValueTypeNames.Token)
+ input_frame_primvar.Set(uv_primvar_name)
+
+ input_varname = uv_reader.CreateInput("varname", Sdf.ValueTypeNames.String)
+ input_varname.ConnectToSource(input_frame_primvar)
+
+ st_input = uv_texture.CreateInput("st", Sdf.ValueTypeNames.Float2)
+
+ if resolved_texture:
+ st_input.ConnectToSource(uv_reader.ConnectableAPI(), "result")
+ if RT.classOf(texture_lookup) == RT.Bitmaptexture:
+ coords = resolved_texture.coords
+
+ if coords.mappingType != 0:
+ RT.UsdExporter.Log(WARN, "Unsupported texture mapping type. Only \"Texture\" is supported when exporting to USD.".format(texture_lookup.name))
+ else :
+ if coords.mapping != 0 and coords.mapping != 1:
+ RT.UsdExporter.Log(WARN, "Unsupported texture mapping. Only \"Explicit Map Channel\" or \"Vertex Color Channel\" are supported when exporting to USD.".format(texture_lookup.name))
+ else:
+ scale = Gf.Vec2f(coords.UVTransform.scalepart[0], coords.UVTransform.scalepart[1])
+ offset = Gf.Vec2f(coords.UVTransform.translation[0], coords.UVTransform.translation[1])
+ rotationEuler = RT.QuatToEuler(coords.UVTransform.rotationpart)
+
+ # Mirroring works differently in BitmapTexture VS USD.
+ # In USD : flip and repeat texture outside unit square.
+ # In BitmapTexture : UV coordinates are mirrored.
+ # Luckily we can repruce max's behavior by scaling the texture by a 2X factor.
+ # Caveat is if we are only mirroring on one axis, we may introduce non-uniform scaling, which doesnt
+ # play well with texture rotations.
+ if coords.U_Mirror:
+ scale[0] *= 2
+ offset[0] *= 2
+ if coords.V_Mirror:
+ scale[1] *= 2
+ offset[1] *= 2
+
+ # In .UVTransform, negative scalings are implemented via rotations around U,V,W.
+ # In USD, we only want rotation around W, and there is no issue with negative scaling.
+ # Undo the rotation, and reapply the sign to the scale.
+ if coords.U_Tiling < 0.0 and coords.V_Tiling < 0.0:
+ scale[0] = -scale[0]
+ scale[1] = -scale[1]
+ rotationEuler.Z = usd_utils.normalize_angle(rotationEuler.Z - 180)
+ elif coords.U_Tiling < 0.0:
+ scale[0] = -scale[0]
+ rotationEuler.X = usd_utils.normalize_angle(rotationEuler.X - 180)
+ elif coords.V_Tiling < 0.0:
+ scale[1] = -scale[1]
+ rotationEuler.X = usd_utils.normalize_angle(rotationEuler.X + 180)
+ rotationEuler.Z = usd_utils.normalize_angle(rotationEuler.Z + 180)
+
+ if scale != None:
+ check_and_warn_for_non_uniform_scaling_with_rotation(scale[0], scale[1], rotationEuler.Z, texture_lookup.name)
+
+ if not usd_utils.float_almost_equal(rotationEuler.X, 0.0) or not usd_utils.float_almost_equal(rotationEuler.Y, 0.0):
+ RT.UsdExporter.Log(WARN, "Unsupported texture rotation axis found on {0}. Only rotations around the Z axis can be exported to USD.".format(texture_lookup.name))
+
+ create_2dtransform_for_bitmap(stage, texture_graph, uv_primvar_name, uv_reader, st_input, scale, rotationEuler.Z, offset)
+
+ if RT.classOf(texture_lookup) == RT.OSLMap:
+ osl_file_name = os.path.basename(texture_lookup.OSLPath)
+
+ if osl_file_name.lower() == "uberbitmap.osl" or osl_file_name.lower() == "uberbitmap2.osl":
+ # Parameters mapped from the OSL nodes inputs cannot be supported as they can be procedural, node dependant, randomized, etc.
+ # Warn the user in those cases...
+ if texture_lookup.Offset_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL UberBitmap Offset input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.RealHeight_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL UberBitmap RealHeight input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.RealWidth_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL UberBitmap RealWidth input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.RotAxis_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL UberBitmap RotAxis input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.RotCenter_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL UberBitmap RotCenter input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.Rotate_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL UberBitmap Rotate input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.Scale_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL UberBitmap Scale input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.Tiling_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL UberBitmap Tiling input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.WrapMode_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL UberBitmap WrapMode input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+
+ # Calculate total scaling, given RealWorld scale config, scale and tiling.
+ # Inspired from the UberBitmap.osl implementation.
+ worldScale = [1.0,1.0]
+ if (texture_lookup.RealWorld):
+ worldScale = [texture_lookup.RealWidth, texture_lookup.RealHeight, 1.0];
+
+ bitmap_u_scaling = texture_lookup.Scale * worldScale[0]
+ bitmap_u_tiling = texture_lookup.tiling[0]
+ if bitmap_u_scaling != 0.0:
+ usd_scale_u = bitmap_u_tiling / bitmap_u_scaling
+ else:
+ usd_scale_u = sys.float_info.max if bitmap_u_tiling > 0.0 else -sys.float_info.max
+
+ bitmap_v_scaling = texture_lookup.Scale * worldScale[1]
+ bitmap_v_tiling = texture_lookup.tiling[1]
+ if bitmap_v_scaling != 0.0:
+ usd_scale_v = bitmap_v_tiling / bitmap_v_scaling
+ else:
+ usd_scale_v = sys.float_info.max if bitmap_v_tiling > 0.0 else -sys.float_info.max
+
+ # Multiplication order is different in OSL vs USdTransform2D, so need to scale the offset.
+ # The offset is for the texture, whereas we are moving the UVs, so we need to inverse it.
+ usd_offset_u = usd_scale_u * -texture_lookup.offset[0]
+ usd_offset_v = usd_scale_v * -texture_lookup.offset[1]
+
+ # USdTransform2D only supports rotations around W.
+ if texture_lookup.RotAxis[0] != 0.0:
+ RT.UsdExporter.Log(WARN, "Unsupported texture rotation, X axis value {0} found on {1} - value must be equal to 0.0. Texture rotation will be ignored.".format(texture_lookup.RotAxis[0], texture_lookup.name))
+ elif texture_lookup.RotAxis[1] != 0.0:
+ RT.UsdExporter.Log(WARN, "Unsupported texture rotation, Y axis value {0} found on {1} - value must be equal to 0.0. texture rotation will be ignored.".format(texture_lookup.RotAxis[1], texture_lookup.name))
+ elif texture_lookup.RotAxis[2] <= 0.0:
+ RT.UsdExporter.Log(WARN, "Unsupported texture rotation, Z axis value {0} found on {1} - value must be greater than 0.0. texture rotation will be ignored.".format(texture_lookup.RotAxis[1], texture_lookup.name))
+ else:
+ # Deal with the rotation. We need to adjust the translation to account for the rotation center.
+ rot_matrix = RT.RotateZMatrix(texture_lookup.Rotate)
+
+ scaled_center_u = texture_lookup.RotCenter[0] * usd_scale_u
+ scaled_center_v = texture_lookup.RotCenter[1] * usd_scale_v
+
+ rot_center_u = usd_offset_u - scaled_center_u
+ rot_center_v = usd_offset_v - scaled_center_v
+ rotatedCenter = RT.Point3(rot_center_u, rot_center_v, 0) * rot_matrix
+
+ usd_offset_u = rotatedCenter[0] + scaled_center_u
+ usd_offset_v = rotatedCenter[1] + scaled_center_v
+
+ scale_value = Gf.Vec2f(usd_scale_u,usd_scale_v)
+ rotate_value = texture_lookup.Rotate
+ translation_value = Gf.Vec2f(usd_offset_u, usd_offset_v)
+
+ if scale_value != None and texture_lookup.Rotate != None:
+ check_and_warn_for_non_uniform_scaling_with_rotation(scale_value[0], scale_value[1], texture_lookup.Rotate, texture_lookup.name)
+
+ create_2dtransform_for_bitmap(stage, texture_graph, uv_primvar_name, uv_reader, st_input, scale_value, rotate_value, translation_value)
+
+ if osl_file_name.lower() == "oslbitmap.osl" or osl_file_name.lower() == "oslbitmap2.osl":
+ # Parameters mapped from the OSL nodes inputs cannot be supported as they can be procedural, node dependant, randomized, etc.
+ # Warn the user in those cases...
+ if texture_lookup.Pos_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL Bitmap Position input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.Scale_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL Bitmap Scale input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+ if texture_lookup.WrapMode_map != None:
+ RT.UsdExporter.Log(WARN, "The OSL Bitmap WrapMode input is not supported when exporting to USD. Only static texture transforms are supported.".format(texture_lookup.name))
+
+ scale = 1.0
+ if texture_lookup.Scale != 0.0:
+ scale = 1.0 / texture_lookup.Scale
+
+ scale_value = Gf.Vec2f(scale,scale)
+
+ create_2dtransform_for_bitmap(stage, texture_graph, uv_primvar_name, uv_reader, st_input, scale_val = scale_value)
+
+ else:
+ st_input.ConnectToSource(uv_reader.ConnectableAPI(), "result")
+ else:
+ value = in_value
+
+ if in_type == RT.Color and target_input_type == "float":
+ # To go from color to float we take the luminance.
+ value = 0.2126 * value[0] + 0.7152 * value[1] + 0.0722 * value[2]
+
+ if isinstance(value, UsdShade.NodeGraph):
+ channel = get_max_map_output_channel(in_value)
+ shader.CreateInput(target_param_name, sdf_type_map[target_input_type]).ConnectToSource(value.ConnectableAPI(), channel)
+ elif isinstance(value, numbers.Number) or RT.superClassOf(value) == RT.Value:
+ shader.CreateInput(target_param_name, sdf_type_map[target_input_type]).Set(value)
+
+
+
+def export_material(material, stage, shader_prim, usd_path, usd_filename, export_options, usd_shade_to_max_map, isUSDZ = False):
+ material_export_options = dict(_material_export_options)
+ material_export_options["usd_filename"] = usd_filename
+ material_export_options["usd_export_options"] = export_options
+
+ # If exporting to USDZ, force absolute paths. There are scenarios in which usdzip completely fails
+ # to resolve relative paths (it will sometimes erroneously resolves from the destination folder...)
+ # Using absolute paths, we avoid these issues, and it makes no difference, as paths are re-anchored
+ # to the USDZ root anyway after the call to usdzip.
+ if isUSDZ:
+ material_export_options["relative_texture_paths"] = False
+
+ export_target_id = material_export_options.get("target_material_id", "UsdPreviewSurface")
+ source_id = usd_utils.get_id_from_mat(material)
+ if not source_id:
+ RT.UsdExporter.Log(WARN, "Unknown material, cannot get an id: {0}".format(material))
+ return
+ conversion_recipe = usd_utils.get_conversion_recipe(source_id, "3dsmax", export_target_id, "usd")
+
+ if not conversion_recipe:
+ RT.UsdExporter.Log(WARN, "No conversion recipe for material: {0}".format(material))
+ return
+
+ write_usd_material(material, conversion_recipe, stage, shader_prim, usd_path, material_export_options, usd_shade_to_max_map)
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/usd_utils.py b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/usd_utils.py
new file mode 100644
index 0000000..b296f5d
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/usd_utils.py
@@ -0,0 +1,430 @@
+#
+# Copyright 2023 Autodesk
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""
+Defined utility functions used by the `usd_material_import.py` and `usd_material_export.py` scripts
+"""
+
+import pymxs
+import json
+import os, re
+
+from contextlib import contextmanager
+from collections import defaultdict
+
+try:
+ import typing
+except ImportError:
+ # this is just for type hints
+ pass
+
+from pxr import UsdShade
+
+__author__ = r'Autodesk Inc.'
+__copyright__ = r'Copyright 2020, Autodesk Inc.'
+
+@contextmanager
+def cwd(path):
+ oldpwd = os.getcwd()
+ os.chdir(path)
+ try:
+ yield
+ finally:
+ os.chdir(oldpwd)
+
+RT = pymxs.runtime
+RT.pluginManager.loadClass(RT.USDExporter)
+
+BITMAP = RT.Name("Bitmap")
+
+_THIS_DIR = os.path.dirname(__file__)
+DATA_FILES_DIR = os.path.join(_THIS_DIR, "data_files")
+
+max_ver_info = RT.maxversion()
+max_ver_year = max_ver_info[0] # RT.maxversion() returns an array where index 0 holds part of the build number
+
+DATA_EXTENSIONS = [".material_conversion", ".mat_def"]
+SEARCH_PATHS = {
+ "defaults": DATA_FILES_DIR
+}
+
+SEARCH_PATHS["UserPath1"] = RT.symbolicPaths.getPathValue("$userTools")
+SEARCH_PATHS["UserPath2"] = RT.symbolicPaths.getPathValue("$userSettings")
+
+# data global dicts
+_CONFIG_DATA = {} # type: typing.Dict[str, dict]
+
+SUPPORTED_OSL_BITMAPS = ['uberbitmap.osl', 'oslbitmap.osl', 'uberbitmap2.osl', 'oslbitmap2.osl']
+
+def is_supported_osl(osl_name):
+ # type: (str) -> bool
+ base_name = os.path.basename(osl_name).lower()
+ return base_name in SUPPORTED_OSL_BITMAPS
+
+def has_non_ascii_char(str):
+ unicodeMatch = re.search("[^\x00-\x7F]", str)
+ return unicodeMatch != None
+
+def get_file_path_mxs(filename):
+ _, file_path = RT.FileResolutionManager.getFullFilePath(pymxs.byref(filename), BITMAP)
+ return file_path
+
+def safe_relpath(path, start):
+ """Return relpath if same drive, else return path."""
+ path_drive, _ = os.path.splitdrive(os.path.abspath(path))
+ start_drive, _ = os.path.splitdrive(os.path.abspath(start))
+ if path_drive.lower() == start_drive.lower():
+ return os.path.relpath(path, start)
+ return path
+
+def _gather_data_files(search_paths=SEARCH_PATHS, data_extensions=DATA_EXTENSIONS):
+ """
+ Search SEARCH_PATHS for DATA_EXTENSIONS files.
+ Return a nested dictionary of data files:
+ data_files["path_type"]["extension"]
+ """
+ data_files = {}
+ with cwd(_THIS_DIR):
+ for path_type, search_path in search_paths.items():
+ path_dict = defaultdict(list)
+ data_files[path_type] = path_dict
+ for file in os.listdir(search_path):
+ for extension in data_extensions:
+ if file.lower().endswith(extension):
+ path_dict[extension].append(os.path.join(search_path, file))
+ return data_files
+
+def _gather_data(data_files):
+ """
+ Get data from data_files and concatenate into dictionaries.
+ Return a nested dictionary of :
+ conversion_data[extension]
+ """
+ conversion_data = {}
+ for extension in DATA_EXTENSIONS:
+ data = {}
+ conversion_data[extension] = data
+ for path_type in SEARCH_PATHS:
+ for data_file in data_files[path_type][extension]:
+ json_data = {}
+ try:
+ with open(data_file) as f:
+ json_data = json.load(f)
+ except:
+ print("Bad json data file: {data_file}")
+ data.update(json_data)
+ return conversion_data
+
+def get_config_data(update=False):
+ """Lazy get of config data."""
+ global _CONFIG_DATA
+ if not update and _CONFIG_DATA:
+ return dict(_CONFIG_DATA)
+
+ config_files = _gather_data_files()
+ _CONFIG_DATA = _gather_data(config_files)
+ return _CONFIG_DATA
+
+def get_material_def(name, domain):
+ """Get a material definition from config data"""
+ config_data = get_config_data()
+
+ material_def = None
+ material_definitions = config_data[".mat_def"]
+ for mat in material_definitions.values():
+ if mat.get("id") == name and mat.get("domain") == domain:
+ material_def = mat
+ break
+
+ if material_def:
+ return material_def
+ else:
+ raise ValueError((name, domain))
+
+def get_conversion_recipe(source_id, source_domain, target_id, target_domain):
+ """From a material source_id and source_domain and target id, return conversion mapping dict"""
+ config_data = get_config_data()
+
+ conversion_recipe = {}
+
+ # get the full material definitions
+ source_def = None
+ target_def = None
+ parameter_mapping = None
+
+ for mat_def_name, mat_def in config_data[".mat_def"].items():
+ mat_id = mat_def.get("id")
+ if not mat_id:
+ Warning("Material definition missing 'id': {0}".format(mat_def_name))
+ continue
+
+ mat_domain = mat_def.get("domain")
+ if not mat_domain:
+ Warning("Material definition missing 'domain': {0}".format(mat_def_name))
+ continue
+
+ if mat_id == source_id and mat_domain == source_domain:
+ source_def = dict(mat_def)
+ elif mat_id == target_id and mat_domain == target_domain:
+ target_def = dict(mat_def)
+
+ if not source_def and target_def:
+ # couldn't find both source and target material definitions
+ return {}
+
+ for recipe_name, recipe in config_data[".material_conversion"].items():
+ recipe_source_material = recipe.get("source_material")
+ if not recipe_source_material:
+ Warning("Material conversion missing 'source_material': {0}".format(recipe_name))
+ continue
+
+ recipe_source_domain = recipe_source_material.get("domain")
+ if not recipe_source_domain:
+ Warning("Material conversion missing source 'domain': {0}".format(recipe_name))
+ continue
+ if not recipe_source_domain == source_domain:
+ continue
+
+ recipe_target_material = recipe.get("target_material")
+ if not recipe_target_material:
+ Warning("Material conversion missing 'target_material': {0}".format(recipe_name))
+ continue
+
+
+ recipe_target_id = recipe_target_material.get("id")
+ if not recipe_target_id:
+ Warning("Material conversion missing target 'id': {0}".format(recipe_name))
+ continue
+
+ recipe_target_domain = recipe_target_material.get("domain")
+ if not recipe_target_domain:
+ Warning("Material conversion missing target 'domain': {0}".format(recipe_name))
+ continue
+ if not recipe_source_domain == source_domain or not recipe_target_domain == target_domain:
+ continue
+ recipe_source_id = recipe_source_material.get("id")
+ if not recipe_source_id:
+ Warning("Material conversion missing source 'id': {0}".format(recipe_name))
+ continue
+ if recipe_target_id == target_id and recipe_source_id == source_id:
+ recipe_param_mapping = recipe.get("parameter_mapping")
+ if not recipe_param_mapping:
+ Warning("Material conversion missing 'parameter_mapping': {0}".format(recipe_name))
+ continue
+
+ parameter_mapping = dict(recipe_param_mapping)
+ break
+
+ if source_def and target_def and parameter_mapping:
+ conversion_recipe["source_material"] = source_def
+ conversion_recipe["target_material"] = target_def
+ conversion_recipe["parameter_mapping"] = parameter_mapping
+ else:
+ # couldn't find both source and target material definitions
+ return {}
+
+ return conversion_recipe
+
+def get_id_from_mat(mat):
+ """Convenience function to return an id from any given material (USD or MAX)"""
+ mat_id = None
+ if isinstance(mat, str):
+ mat_id = mat
+ elif isinstance(mat, pymxs.MXSWrapperBase):
+ mat_id = str(RT.ClassOf(mat))
+ elif isinstance(mat, UsdShade.Shader):
+ shader_id_attr = mat.GetIdAttr()
+ if shader_id_attr:
+ mat_id = shader_id_attr.Get()
+
+ if not mat_id:
+ raise ValueError(mat)
+
+ return mat_id
+
+def mapping_property_getter(material, param):
+ """Get a material property for either max or usd domains
+ This makes mapping_from_material() domain agnostic"""
+ if isinstance(material, pymxs.MXSWrapperBase):
+ return RT.getProperty(material, param)
+ elif isinstance(material, UsdShade.Shader):
+ _input = material.GetInput(param)
+
+ if not _input:
+ return None
+
+ if _input.HasConnectedSource():
+ source, sourceName, sourceType = _input.GetConnectedSource()
+ attr = source.GetOutput(sourceName).GetAttr()
+ return attr
+ else:
+ return _input.Get()
+ else:
+ raise ValueError((material, param))
+
+def max_dict_to_dict(max_dict):
+ """Util to convert max dict to python dict."""
+ return {key:max_dict[key] for key in max_dict.keys}
+
+def get_src_type(input_param, recipe):
+ # type: (str, dict) -> str
+ """Get the source input type as string for the parameter"""
+ mapping = recipe["parameter_mapping"]["mappings"]
+ map_src = mapping.get(input_param)
+ if type(map_src) is dict:
+ map_src = map_src["map_parameter"]
+ return recipe["source_material"]["inputs"].get(map_src)
+
+def mapping_from_material(material, conversion_recipe):
+ """
+ This function interperets the conversion_recipe dict, and returns
+ a dictionary with the calculated values for each parameter of the target material.
+ """
+ mapping = {}
+ parameter_mapping = conversion_recipe["parameter_mapping"]
+ if not parameter_mapping["mappings"].items():
+ raise Exception("There must be at least 1 mapping defined.")
+
+ for in_param, out_param in parameter_mapping["mappings"].items():
+ in_value_param_name = None
+ one_minus_param = None
+ map_param = None
+ use_map_param = None # indicates that there is a param to enable/disable texture map
+ map_required = None # indicates that a map is required to be meaningful
+ multiplier_param = None
+ direct_value = None
+
+ if type(out_param) is dict:
+ out_param = dict(out_param)
+ case = out_param.get("case", False)
+ while case:
+ # iterate potentially recursive case params
+ case_parameter = case.get("case_parameter")
+ if not case_parameter:
+ Warning("A case data block has no case_parameter, skipping : {0}".format(case))
+ break
+ case_params = None
+ case_val = mapping_property_getter(material, case_parameter)
+ case_params = case.get(str(case_val))
+ if case_val and not case_params:
+ # case_val is something, so check the "+" case which is "at least one value" - or NOT None
+ case_params = case.get("+")
+
+ if case_params and type(case_params) is dict:
+ out_param.update(case_params)
+ case = case_params.get("case", False)
+ else:
+ case = False
+
+ in_value_param_name = out_param.get("value_parameter")
+ map_param = out_param.get("map_parameter")
+ use_map_param = out_param.get("use_map_parameter")
+ one_minus_param = out_param.get("one_minus", False)
+ map_required = out_param.get("map_required", False)
+ multiplier_param = out_param.get("multiplier_parameter")
+ direct_value = out_param.get("value")
+
+ elif type(out_param) is str:
+ # if just a string we assume as straight up value mapping
+ in_value_param_name = out_param
+
+ if map_param:
+ use_map = True
+ if use_map_param:
+ use_map = mapping_property_getter(material, use_map_param)
+ map_value = mapping_property_getter(material, map_param)
+
+ if (use_map_param and use_map and map_value) or (use_map_param == None and map_value):
+ mapping[in_param] = map_value
+ continue
+ if map_required:
+ mapping[in_param] = None
+ continue
+
+ if in_value_param_name:
+ in_param_value = mapping_property_getter(material, in_value_param_name)
+ if in_param_value is None:
+ #print("None value for in_value_param_name="+in_value_param_name)
+ mapping[in_param] = None
+ continue
+ if one_minus_param:
+ in_param_value = 1 - in_param_value
+ if multiplier_param:
+ mult = mapping_property_getter(material, multiplier_param)
+ in_param_value = mult * in_param_value
+ if not map_required:
+ mapping[in_param] = in_param_value
+ continue
+ if direct_value:
+ mapping[in_param] = direct_value
+ continue
+
+ return mapping
+
+def float_almost_equal(val1, val2):
+ return abs(val1-val2) < 1e-06
+
+def normalize_angle(angle):
+ # Reduce the angle
+ angle = angle % 360.0
+ # Make it positive : 0 <= angle < 360
+ angle = (angle + 360.0) % 360.0
+ # Normalize : -180 < angle <= 180
+ if (angle > 180.0):
+ angle -= 360.0
+ return angle
+
+def is_filepath_udim(file_path):
+ return "" in file_path
+
+def find_first_valid_udim_for_filename(directory_name, target_filename):
+ return get_all_valid_udims_from_dir_for_filename(directory_name, target_filename, first_only=True)
+
+def get_all_valid_udims_from_dir_for_filename(directory_name, target_filename, first_only=False):
+ u = 0
+ v = 0
+ udim_val = 1001 # first value based on formula: 1000 + (u + 1) + (v * 10)
+ MAX_UDIM_VAL = 9999
+
+ list_of_valid_udims = []
+
+ dir_list = os.listdir(directory_name)
+ while udim_val <= MAX_UDIM_VAL:
+ udim_val_str = str(udim_val)
+ udim_target_filename = target_filename.replace("", udim_val_str)
+ for file in dir_list:
+ filename = os.fsdecode(file)
+
+ if udim_val_str in filename and filename == udim_target_filename:
+ if first_only:
+ return udim_val_str
+
+ list_of_valid_udims.append(udim_val_str)
+ break
+
+ if first_only == True and len(list_of_valid_udims) == 0:
+ break
+
+ u = u + 1
+ if u + 1 > 10:
+ u = 0
+ v = v + 1
+
+ udim_val = 1000 + (u + 1) + (v * 10)
+
+ if len(list_of_valid_udims) == 0:
+ Warning("No valid udim files found in \"\{0}\".".format(directory_name))
+
+ return list_of_valid_udims
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materials/writer/plugInfo.json b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/writer/plugInfo.json
new file mode 100644
index 0000000..fc1b617
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materials/writer/plugInfo.json
@@ -0,0 +1,21 @@
+{
+ "Plugins":[
+ {
+ "Info":{
+ "MaxUsd":{
+ "ShaderWriter":{
+ "providesTranslator":[
+ "Physical Material",
+ "PBR Material (Metal/Rough)",
+ "PBR Material (Spec/Gloss)",
+ "USD Preview Surface",
+ "Standard Surface"
+ ]
+ }
+ }
+ },
+ "Name":"shaderWriter",
+ "Type":"python"
+ }
+ ]
+}
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/materialXShaderWriter.py b/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/materialXShaderWriter.py
new file mode 100644
index 0000000..70d8c2d
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/materialXShaderWriter.py
@@ -0,0 +1,90 @@
+import maxUsd
+from pymxs import runtime as rt
+from pxr import UsdShade, Sdf
+import usd_utils
+import traceback
+import os
+
+class mtlXShaderWriter(maxUsd.ShaderWriter):
+ def Write(self):
+ try:
+ stage = self.GetUsdStage()
+ material = rt.GetAnimByHandle(self.GetMaterial())
+ isMultiTarget = len(self.GetExportArgs().GetAllMaterialConversions()) > 1
+
+ # Get what we need from the Scripted Material.
+ # The curent material name, so we know which one to connect.
+ curMatName = material.curMatName
+ if curMatName == None or curMatName == "":
+ rt.UsdExporter.Log(rt.Name("warn"), ("No valid MaterialX material found for " + material.name + ", conversion can't proceed."))
+ return
+ # The mtlx file path, so we can add it as a reference.
+ mtlxFilePath = material.MaterialXFile
+ # Make sure we have the absolute path, so we can make it relative to the exported file...
+ if not os.path.isabs(mtlxFilePath):
+ mtlxFilePath = rt.pathConfig.convertPathToAbsolute(mtlxFilePath)
+
+ # Find the prim where we want to add the reference
+ if isMultiTarget:
+ refHolderPrim = UsdShade.NodeGraph.Get(stage, self.GetUsdPath().GetParentPath())
+ else:
+ refHolderPrim = UsdShade.Material.Get(stage, self.GetUsdPath().GetParentPath())
+ ref = refHolderPrim.GetPrim().GetReferences()
+
+ # In case the material is not exported to the main layer, the path on disk can be different.
+ # This need to be taken in consideration before the reference to the MaterialX file.
+ targetLayer = stage.GetEditTarget().GetLayer()
+ outputFile = self.GetFilename()
+ if not targetLayer.anonymous:
+ outputFile = targetLayer.identifier
+
+ # Add the reference to the Mtlx file.
+ if self.IsUSDZFile():
+ rt.UsdExporter.Log(rt.Name("warn"), ("Conversion of MaterialX material : " + material.name + ", is limited for USDZ and will not yield correct results."))
+ refPath = mtlxFilePath
+ else:
+ refPath = usd_utils.safe_relpath(mtlxFilePath, os.path.dirname(outputFile))
+ refPath = refPath.replace('\\','/')
+
+ # Set-up the current working directory as this can cause issues when adding the reference :
+ # The stage currently live's in memory, and doesn't know where it will end up on disk.
+ # So when using a relative path for the reference, it is unable to resolve the path, and so materialX are never added to the stage.
+ # By setting up the current working directory, to the target output file's directory, we help it resolve the reference.
+ directory = os.path.dirname(outputFile)
+ os.makedirs(directory, exist_ok=True)
+ cwd = os.getcwd()
+ os.chdir(directory)
+ ref.AddReference(refPath, primPath="/MaterialX")
+ os.chdir(cwd)
+
+ # Add a pass-through connection between the prim holding the reference and the Material we are using inside of the Mtlx file.
+ matXPath = self.GetUsdPath().GetParentPath().AppendPath("Materials").AppendPath(curMatName)
+ matXPrim = UsdShade.Material.Get(stage, matXPath)
+ surfOutput = matXPrim.GetSurfaceOutput("mtlx")
+ if isMultiTarget:
+ refHolderSurfOut = refHolderPrim.CreateOutput("mtlx:surface", Sdf.ValueTypeNames.Token)
+ else:
+ refHolderSurfOut = refHolderPrim.CreateSurfaceOutput("mtlx")
+ refHolderSurfOut.ConnectToSource(surfOutput)
+
+ # Here we want to handle the case of exporting to multiple targets.
+ # Since this ShaderWriter doesn't really respect the API (no call to SetUsdPrim()),
+ # we need to create the pass-through between the NodeGraph encapsulating the MaterialX material and the material prim.
+ if isMultiTarget:
+ globalMat = UsdShade.Material.Get(stage, refHolderPrim.GetPath().GetParentPath())
+ surfaceOutput = globalMat.CreateSurfaceOutput("mtlx")
+ surfaceOutput.ConnectToSource(refHolderSurfOut)
+
+ except Exception as e:
+ # Quite useful to debug errors in a Python callback
+ print('Write() - Error: %s' % str(e))
+ print(traceback.format_exc())
+
+ @classmethod
+ def CanExport(cls, exportArgs):
+ if exportArgs.GetConvertMaterialsTo() == "MaterialX":
+ return maxUsd.ShaderWriter.ContextSupport.Fallback
+ return maxUsd.ShaderWriter.ContextSupport.Unsupported
+
+# Register the writer.
+maxUsd.ShaderWriter.Register(mtlXShaderWriter, "MaterialX Material")
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/plugInfo.json b/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/plugInfo.json
new file mode 100644
index 0000000..db6758b
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/plugInfo.json
@@ -0,0 +1,17 @@
+{
+ "Plugins":[
+ {
+ "Info": {
+ "MaxUsd": {
+ "ShaderWriter": {
+ "providesTranslator": [
+ "MaterialX Material"
+ ]
+ }
+ }
+ },
+ "Name": "materialXShaderWriter",
+ "Type": "python"
+ }
+ ]
+}
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/register-materialx-target.ms b/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/register-materialx-target.ms
new file mode 100644
index 0000000..6268b8c
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/register-materialx-target.ms
@@ -0,0 +1,13 @@
+(
+ -- Look for the usd plug-in
+ findUSD = false
+ for i = 1 to pluginManager.pluginDllCount do (
+ if (pluginManager.pluginDllName i) == "usdexport.dle" then findUSD = true
+ )
+
+ if findUSD then
+ (
+ pyMaxUsd = python.import("maxUsd")
+ pyMaxUsd.ShadingModeRegistry.RegisterExportConversion "MaterialX" "mtlx" "MaterialX" "Export USD assets referencing MaterialX material targets."
+ )
+)
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/registerPlugin.ms b/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/registerPlugin.ms
new file mode 100644
index 0000000..b15c2ff
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/materialx/registerPlugin.ms
@@ -0,0 +1,29 @@
+(
+ -- Look for the usd plug-in
+ findUSD = false
+ for i = 1 to pluginManager.pluginDllCount do (
+ if (pluginManager.pluginDllName i) == "usdexport.dle" then findUSD = true
+ )
+
+ if findUSD then
+ (
+ pyUsdPlug = python.import("pxr.Plug")
+
+ function addMatXShaderWriterPlugin =
+ (
+ pysys = python.import("sys")
+ pyos = python.import("os")
+
+ scriptPath = getThisScriptFilename()
+ pluginPath = pathConfig.removePathLeaf scriptPath
+ pluginPath = pluginPath + "\\plugInfo.json"
+ -- The plugin script location must part of the Python path
+ pysys.path.insert 0 (pathConfig.removePathLeaf pluginPath)
+
+ plugRegistry = pyUsdPlug.Registry()
+ plugRegistry.RegisterPlugins pluginPath
+ )
+
+ addMatXShaderWriterPlugin()
+ )
+)
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/plugInfo.json b/src/ApplicationPlugins/usd-component/Contents/scripts/plugInfo.json
new file mode 100644
index 0000000..51c12ee
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/plugInfo.json
@@ -0,0 +1,5 @@
+{
+ "Includes": [
+ "materials/"
+ ]
+}
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/register-usd-plugin-paths.ms b/src/ApplicationPlugins/usd-component/Contents/scripts/register-usd-plugin-paths.ms
new file mode 100644
index 0000000..c1e39eb
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/register-usd-plugin-paths.ms
@@ -0,0 +1,43 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+/*
+ * Registers the 3ds Max usd plugin paths to USD Plug Registry
+ * so that max users can use the usd plugins out of the box.
+*/
+(
+
+pyUsdPlug = python.import("pxr.Plug")
+
+function addUsdPluginPaths =
+(
+ scriptPath = getThisScriptFilename()
+ componentPath = pathConfig.removePathLeaf (pathConfig.removePathLeaf scriptPath)
+ plugRegistry = pyUsdPlug.Registry()
+
+ pluginPaths = #(
+ "plugin\\MaxUsd_Translators",
+ "plugin\\BasePxrUsdPreviewSurface",
+ "scripts"
+ )
+ for pluginPath in pluginPaths do (
+ fullPluginPath = pathConfig.appendPath componentPath pluginPath
+ plugRegistry.RegisterPlugins fullPluginPath
+ )
+)
+
+addUsdPluginPaths()
+
+)
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/register-usd-python-runtime.ms b/src/ApplicationPlugins/usd-component/Contents/scripts/register-usd-python-runtime.ms
new file mode 100644
index 0000000..2e9a6c2
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/register-usd-python-runtime.ms
@@ -0,0 +1,89 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+/*
+ * Appends usd python bindings to max's python path
+ * so that max users can use the usd python out of the box.
+*/
+(
+
+function getUsdImporterPluginIndex =
+(
+ for i = 1 to pluginManager.pluginDllCount do (
+ if (pluginManager.pluginDllName i) == "usdimport.dli" then return i
+ )
+ return -1;
+)
+
+function addUsdToPythonPath =
+(
+ pluginIndex = getUsdImporterPluginIndex()
+ pluginPath = pluginManager.pluginDllDirectory pluginIndex
+ -- In versions < 2023, pluginManager.pluginDllDirectory would return the plugin's dll
+ -- path and not the directory, this issue was since fixed.
+ if (getFilenameType pluginPath) != "" do (
+ pluginPath = pathConfig.removePathLeaf pluginPath
+ )
+ usdPythonPath = pathConfig.appendPath pluginPath "python"
+
+ --need to add the pluginPath itself (bin folder) for the python imports to work properly
+ pysys = python.import("sys")
+ pyos = python.import("os")
+
+ if pysys.version_info[1] == 3 and not pysys.version_info[2] < 8 then (
+ pyos.add_dll_directory(pluginPath)
+ ) else (
+ pyos.environ["PATH"] = pluginPath + pyos.pathsep + pyos.environ["PATH"]
+ )
+
+ --need to add usdPluginsPath to PATH for usd plugins dlls to load properly
+ usdPluginsPath = pathConfig.appendPath (pathConfig.removePathLeaf pluginPath) "plugin\\usd"
+ if pysys.version_info[1] == 3 and not pysys.version_info[2] < 8 then (
+ pyos.add_dll_directory(usdPluginsPath)
+ ) else (
+ pyos.environ["PATH"] = usdPluginsPath + pyos.pathsep + pyos.environ["PATH"]
+ )
+
+ if pysys.path.__contains__(usdPythonPath) then (
+ return false
+ ) else (
+ if doesFileExist usdPythonPath then (
+ --favor prepend because we currently do not want users to override the path by mistake when setting PYTHONPATH
+ pysys.path.insert 0 usdPythonPath
+ ) else (
+ logsystem.logEntry ("Could not find usd python path at " + usdPythonPath) broadcast:true warning:true
+ )
+ )
+
+ local mtlxLibPathEnvVar = undefined
+ maxver = maxversion()
+ if maxver[1] >= 26000 then ( -- 3ds Max 2024 and up use USD 22.11+ env var name has changed in later versions.
+ mtlxLibPathEnvVar = "PXR_MTLX_STDLIB_SEARCH_PATHS"
+ ) else (
+ mtlxLibPathEnvVar = "PXR_USDMTLX_STDLIB_SEARCH_PATHS"
+ )
+
+ usdMtlXLibPath = pathConfig.appendPath pluginPath "..\\libraries"
+ if pyos.environ[mtlxLibPathEnvVar] == undefined then (
+ pyos.environ[mtlxLibPathEnvVar] = usdMtlXLibPath
+ ) else (
+ pyos.environ[mtlxLibPathEnvVar] = usdMtlXLibPath + pyos.pathsep + pyos.environ[mtlxLibPathEnvVar]
+ )
+)
+
+addUsdToPythonPath()
+
+)
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/registerMenu.mcr b/src/ApplicationPlugins/usd-component/Contents/scripts/registerMenu.mcr
new file mode 100644
index 0000000..eb45a0a
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/registerMenu.mcr
@@ -0,0 +1,22 @@
+/**********************************************************************
+// Copyright 2022 Autodesk, Inc. All rights reserved.
+**********************************************************************/
+--this script adds the macro action for USD Stage menu item.
+
+macroScript CreateUSDStage category:"USD" buttonText:~USD_STAGE_MENU_ITEM~ tooltip:~USD_STAGE_TOOLTIP~
+(
+ result = USDStageObject.SelectRootLayerAndPrim filterMode:#exclude filteredTypes:#( "Material" , "Shader" , "GeomSubset" ) useUserSettings:true
+ if result != undefined then
+ (
+ stageObject = USDStageObject()
+ stageObject.SetRootLayer result[1] stageMask:result[2] payloadsLoaded:result[3]
+ if (result[4]) do (
+ stageObject.OpenInUsdExplorer()
+ )
+ )
+)
+
+macroScript OpenUsdExplorer category:"USD" buttonText:~USD_EXPLORER_MENU_ITEM~ tooltip:~USD_EXPLORER_MENU_ITEM~
+(
+ USDStageObject.OpenUsdExplorer()
+)
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/registerMenu.ms b/src/ApplicationPlugins/usd-component/Contents/scripts/registerMenu.ms
new file mode 100644
index 0000000..c36f4e5
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/registerMenu.ms
@@ -0,0 +1,148 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+--this script adds the USD Stage menu item to the File->Reference menu.
+
+--Finds the "targetMenuName" menu entry inside the "menuToSearch", returns true if the menu was found.
+fn findMenuEntry menuToSearch targetMenuName &referenceMenuItemIdx = (
+
+ if menuToSearch != undefined then
+ (
+ local menuItemCount = menuToSearch.numItems()
+ for i = 1 to menuItemCount do
+ (
+ local curItem = menuToSearch.getItem i
+ local curItemTitle = curItem.getTitle()
+
+ if ( curItemTitle == targetMenuName ) then
+ (
+ referenceMenuItemIdx = i
+ return true
+ )
+ )
+ )
+
+ return false
+)
+
+fn addUSDStageReferenceMenuItem = (
+
+ local referenceMenuFound = false
+ local referenceMenuItemIdx = -1
+ local referenceMenu = undefined
+ mainFileMenu = menuMan.findMenu ~FILE_MENU_MAIN~
+ referenceMenuFound = findMenuEntry mainFileMenu ~REFERENCE_MENU_ITEM~ &referenceMenuItemIdx
+
+ if referenceMenuFound then
+ (
+ referenceMenu = mainFileMenu.getItem referenceMenuItemIdx
+ referenceMenu = referenceMenu.getSubMenu()
+
+ local USDStageItemIdx = -1
+ local USDStageItemFound = findMenuEntry referenceMenu ~USD_STAGE_MENU_ITEM~ &USDStageItemIdx
+
+ if not USDStageItemFound then
+ (
+ -- create a menu items that calls the macroscript
+ local theMenuItem = menuMan.createActionItem "CreateUSDStage" "USD"
+ theMenuItem.setTitle ~USD_STAGE_MENU_ITEM~
+ theMenuItem.setUseCustomTitle true
+ referenceMenu.addItem theMenuItem -1
+
+ -- redraw the menu bar with the new item
+ menuMan.updateMenuBar()
+ )
+ )
+)
+
+fn removeUSDStageReferenceMenuItem = (
+
+ local referenceMenuFound = false
+ local referenceMenuItemIdx = -1
+ local referenceMenu = undefined
+ mainFileMenu = menuMan.findMenu ~FILE_MENU_MAIN~
+ referenceMenuFound = findMenuEntry mainFileMenu ~REFERENCE_MENU_ITEM~ &referenceMenuItemIdx
+
+ if referenceMenuFound then
+ (
+ referenceMenu = mainFileMenu.getItem referenceMenuItemIdx
+ referenceMenu = referenceMenu.getSubMenu()
+
+ local USDStageItemIdx = -1
+ local USDStageItemFound = findMenuEntry referenceMenu ~USD_STAGE_MENU_ITEM~ &USDStageItemIdx
+
+ if USDStageItemFound then
+ (
+ referenceMenu.removeItemByPosition USDStageItemIdx
+ )
+ )
+)
+
+fn addUSDMenuItem = (
+ toolsMenu = menuMan.findMenu ~TOOLS_MENU_ITEM~
+ if toolsMenu != undefined then
+ (
+ local USDItemIdx = -1
+ local USDItemFound = findMenuEntry toolsMenu "USD" &USDItemIdx
+
+ if not USDItemFound then
+ (
+ usdMenu = menuMan.createMenu "USD"
+
+ -- create a menu items that calls the macroscript
+ local theMenuItem = menuMan.createActionItem "OpenUsdExplorer" "USD"
+ theMenuItem.setTitle ~USD_EXPLORER_MENU_ITEM~
+ theMenuItem.setUseCustomTitle true
+ usdMenu.addItem theMenuItem -1
+ subMenuItem = menuMan.createsubMenuItem "USD" usdMenu
+
+ toolsMenu.addItem (menuMan.createSeparatorItem()) -1
+ toolsMenu.addItem subMenuItem -1
+
+ -- redraw the menu bar with the new item
+ menuMan.updateMenuBar()
+ )
+ )
+)
+
+fn removeUSDMenuItem = (
+ toolsMenu = menuMan.findMenu ~TOOLS_MENU_ITEM~
+ local USDItemIdx = -1
+ local USDItemFound = findMenuEntry toolsMenu "USD" &USDItemIdx
+ if USDItemFound then
+ (
+ toolsMenu.removeItemByPosition USDItemIdx
+ )
+)
+
+function addUSDMenus = (
+ addUSDStageReferenceMenuItem()
+ addUSDMenuItem()
+)
+
+function removeUSDMenus = (
+ removeUSDStageReferenceMenuItem()
+ removeUSDMenuItem()
+)
+
+--add a callback to that whenever a menu file is loaded, such as during theme or workspace loading, we add potentially missing entries
+callbacks.addScript #postLoadingMenus "addUSDMenus()" id:#USDMenuEntryPostLoad
+--remove ourself from the menu, we don't want to be saved
+callbacks.addScript #preSavingMenus "removeUSDMenus()" id:#USDMenuEntryRemoval
+--add ourself again when the saving is done
+callbacks.addScript #postSavingMenus "addUSDMenus()" id:#USDMenuEntryRemoval
+--call the function once to handle first load, because menus are loaded before startup scripts and the callback won't trip
+addUSDMenus()
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/Contents/scripts/usd-python-scripts.ms b/src/ApplicationPlugins/usd-component/Contents/scripts/usd-python-scripts.ms
new file mode 100644
index 0000000..e2a9db0
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/Contents/scripts/usd-python-scripts.ms
@@ -0,0 +1,42 @@
+--
+-- Copyright 2023 Autodesk
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+/*
+ * Appends usd python scripts ptah to max's python path
+*/
+(
+
+function insertScriptsToSysPath =
+(
+ local script_file = getSourceFileName()
+ local script_path = substituteString (getFilenamePath(script_file)) "\\" "/"
+ local pysys = python.import("sys")
+
+ if pysys.path.__contains__(script_path) then (
+ return false
+ ) else (
+ if doesFileExist script_path then (
+ --favor prepend because we currently do not want users to override the path by mistake when setting PYTHONPATH
+ pysys.path.insert 0 script_path
+ pysys.path.insert 0 (script_path+"/materials")
+ ) else (
+ logsystem.logEntry ("Could not find usd python path at " + script_path) broadcast:true warning:true
+ )
+ )
+)
+
+insertScriptsToSysPath()
+)
\ No newline at end of file
diff --git a/src/ApplicationPlugins/usd-component/PackageContents.xml b/src/ApplicationPlugins/usd-component/PackageContents.xml
new file mode 100644
index 0000000..6e194cb
--- /dev/null
+++ b/src/ApplicationPlugins/usd-component/PackageContents.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DependencyPathOverrides.props b/src/DependencyPathOverrides.props
new file mode 100644
index 0000000..29918ed
--- /dev/null
+++ b/src/DependencyPathOverrides.props
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MaxUsd.h b/src/MaxUsd.h
new file mode 100644
index 0000000..63884c0
--- /dev/null
+++ b/src/MaxUsd.h
@@ -0,0 +1,43 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include
+
+// MaxUsd public namespace string will never change.
+#define MAXUSD_NS MaxUsd
+// C preprocessor trickery to expand arguments.
+#define MAXUSD_CONCAT(A, B) MAXUSD_CONCAT_IMPL(A, B)
+#define MAXUSD_CONCAT_IMPL(A, B) A##B
+// Versioned namespace includes the major version number.
+#define MAXUSD_VERSIONED_NS MAXUSD_CONCAT(MAXUSD_NS, MAXUSD_CONCAT(_v,COMPONENT_VERSION_MAJOR))
+
+namespace MAXUSD_VERSIONED_NS {}
+
+// With a using namespace declaration, pull in the versioned namespace into the
+// MaxUsd public namespace, to allow client code to use the plain MaxUsd
+// namespace, e.g. MaxUsd::Class.
+namespace MAXUSD_NS {
+ using namespace MAXUSD_VERSIONED_NS;
+}
+
+// Macro to place the MaxUsd symbols in the versioned namespace, which is how
+// they will appear in the shared library, e.g. MaxUsd_v1::Class.
+#ifdef DOXYGEN
+#define MAXUSD_NS_DEF MAXUSD_NS
+#else
+#define MAXUSD_NS_DEF MAXUSD_VERSIONED_NS
+#endif
\ No newline at end of file
diff --git a/src/MaxUsd/Builders/JobContextRegistry.cpp b/src/MaxUsd/Builders/JobContextRegistry.cpp
new file mode 100644
index 0000000..8239c68
--- /dev/null
+++ b/src/MaxUsd/Builders/JobContextRegistry.cpp
@@ -0,0 +1,244 @@
+//
+// Copyright 2022 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "JobContextRegistry.h"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+// clang-format off
+TF_DEFINE_PRIVATE_TOKENS(
+ _tokens,
+
+ (MaxUsd)
+ (JobContextPlugin)
+);
+// clang-format on
+
+namespace {
+static const TfTokenVector PLUGIN_SCOPE = { _tokens->MaxUsd, _tokens->JobContextPlugin };
+} // namespace
+
+namespace {
+struct _HashInfo
+{
+ size_t operator()(const MaxUsdJobContextRegistry::ContextInfo& info) const noexcept
+ {
+ return info.jobContext.Hash();
+ }
+};
+
+struct _EqualInfo
+{
+ bool operator()(
+ const MaxUsdJobContextRegistry::ContextInfo& lhs,
+ const MaxUsdJobContextRegistry::ContextInfo& rhs) const noexcept
+ {
+ return rhs.jobContext == lhs.jobContext;
+ }
+};
+
+using _JobContextRegistry
+ = std::unordered_set;
+static _JobContextRegistry _jobContextReg;
+} // namespace
+
+void MaxUsdJobContextRegistry::RegisterExportJobContext(
+ const std::string& jobContext,
+ const std::string& niceName,
+ const std::string& description,
+ EnablerFn enablerFct,
+ bool fromPython)
+{
+ TF_DEBUG(PXR_MAXUSD_REGISTRY).Msg("Registering export job context %s.\n", jobContext.c_str());
+ TfToken key(jobContext);
+ ContextInfo newInfo { key, TfToken(niceName), TfToken(description), enablerFct };
+ auto itFound = _jobContextReg.find(newInfo);
+ if (itFound == _jobContextReg.end()) {
+ _jobContextReg.insert(newInfo);
+ MaxUsd_RegistryHelper::AddUnloader(
+ [key]() {
+ ContextInfo toErase { key };
+ _jobContextReg.erase(toErase);
+ },
+ fromPython);
+ } else {
+ if (!itFound->exportEnablerCallback) {
+ if (niceName != itFound->niceName) {
+ TF_CODING_ERROR(
+ "Export enabler has differing nice name: %s != %s",
+ niceName.c_str(),
+ itFound->niceName.GetText());
+ }
+ // Keep the import part, fill in the export part:
+ ContextInfo updatedInfo { key,
+ itFound->niceName,
+ TfToken(description),
+ enablerFct,
+ {},
+ itFound->importDescription,
+ itFound->importEnablerCallback,
+ itFound->importOptionsCallback };
+ _jobContextReg.erase(updatedInfo);
+ _jobContextReg.insert(updatedInfo);
+ } else {
+ TF_CODING_ERROR("Multiple enablers for export job context %s", jobContext.c_str());
+ }
+ }
+}
+
+void MaxUsdJobContextRegistry::SetExportOptionsUI(
+ const std::string& jobContext,
+ OptionsFn optionsFct,
+ bool fromPython)
+{
+ TF_DEBUG(PXR_MAXUSD_REGISTRY)
+ .Msg("Registering export options ui callback for job context %s.\n", jobContext.c_str());
+ TfToken key(jobContext);
+
+ ContextInfo searchInfo { key };
+ auto itFound = _jobContextReg.find(searchInfo);
+ if (itFound != _jobContextReg.end()) {
+ ContextInfo updatedInfo { key,
+ itFound->niceName,
+ itFound->exportDescription,
+ itFound->exportEnablerCallback,
+ optionsFct,
+ itFound->importDescription,
+ itFound->importEnablerCallback,
+ itFound->importOptionsCallback };
+ _jobContextReg.erase(updatedInfo);
+ _jobContextReg.insert(updatedInfo);
+ } else {
+ TF_CODING_ERROR("No export job context found named %s", jobContext.c_str());
+ }
+}
+
+void MaxUsdJobContextRegistry::RegisterImportJobContext(
+ const std::string& jobContext,
+ const std::string& niceName,
+ const std::string& description,
+ EnablerFn enablerFct,
+ bool fromPython)
+{
+ TF_DEBUG(PXR_MAXUSD_REGISTRY).Msg("Registering import job context %s.\n", jobContext.c_str());
+ TfToken key(jobContext);
+ ContextInfo newInfo { key, TfToken(niceName), {}, {}, {}, TfToken(description), enablerFct };
+ auto itFound = _jobContextReg.find(newInfo);
+ if (itFound == _jobContextReg.end()) {
+ _jobContextReg.insert(newInfo);
+ MaxUsd_RegistryHelper::AddUnloader(
+ [key]() {
+ ContextInfo toErase { key };
+ _jobContextReg.erase(toErase);
+ },
+ fromPython);
+ } else {
+ if (!itFound->importEnablerCallback) {
+ if (niceName != itFound->niceName) {
+ TF_CODING_ERROR(
+ "Import enabler has differing nice name: %s != %s",
+ niceName.c_str(),
+ itFound->niceName.GetText());
+ }
+ // Keep the export part, fill in the import part:
+ ContextInfo updatedInfo { key,
+ itFound->niceName,
+ itFound->exportDescription,
+ itFound->exportEnablerCallback,
+ itFound->exportOptionsCallback,
+ TfToken(description),
+ enablerFct };
+ _jobContextReg.erase(updatedInfo);
+ _jobContextReg.insert(updatedInfo);
+ } else {
+ TF_CODING_ERROR("Multiple enablers for import job context %s", jobContext.c_str());
+ }
+ }
+}
+
+void MaxUsdJobContextRegistry::SetImportOptionsUI(
+ const std::string& jobContext,
+ OptionsFn optionsFct,
+ bool fromPython)
+{
+ TF_DEBUG(PXR_MAXUSD_REGISTRY)
+ .Msg("Registering import options ui callback for job context %s.\n", jobContext.c_str());
+ TfToken key(jobContext);
+
+ ContextInfo searchInfo { key };
+ auto itFound = _jobContextReg.find(searchInfo);
+ if (itFound != _jobContextReg.end()) {
+ ContextInfo updatedInfo { key,
+ itFound->niceName,
+ itFound->exportDescription,
+ itFound->exportEnablerCallback,
+ itFound->exportOptionsCallback,
+ itFound->importDescription,
+ itFound->importEnablerCallback,
+ optionsFct };
+ _jobContextReg.erase(updatedInfo);
+ _jobContextReg.insert(updatedInfo);
+ } else {
+ TF_CODING_ERROR("No import job context found named %s", jobContext.c_str());
+ }
+}
+
+TfTokenVector MaxUsdJobContextRegistry::_ListJobContexts()
+{
+ MaxUsd_RegistryHelper::FindAndLoadMaxUsdPlugs(PLUGIN_SCOPE);
+ TfRegistryManager::GetInstance().SubscribeTo();
+ TfTokenVector ret;
+ ret.reserve(_jobContextReg.size());
+ for (const auto& e : _jobContextReg) {
+ ret.push_back(e.jobContext);
+ }
+ return ret;
+}
+
+const MaxUsdJobContextRegistry::ContextInfo&
+MaxUsdJobContextRegistry::_GetJobContextInfo(const TfToken& jobContext)
+{
+ MaxUsd_RegistryHelper::FindAndLoadMaxUsdPlugs(PLUGIN_SCOPE);
+ TfRegistryManager::GetInstance().SubscribeTo();
+ ContextInfo key { jobContext, {}, {}, {}, {}, {} };
+ auto it = _jobContextReg.find(key);
+ static const ContextInfo _emptyInfo;
+ return it != _jobContextReg.end() ? *it : _emptyInfo;
+}
+
+TF_INSTANTIATE_SINGLETON(MaxUsdJobContextRegistry);
+
+MaxUsdJobContextRegistry& MaxUsdJobContextRegistry::GetInstance()
+{
+ return TfSingleton::GetInstance();
+}
+
+MaxUsdJobContextRegistry::MaxUsdJobContextRegistry() { }
+
+MaxUsdJobContextRegistry::~MaxUsdJobContextRegistry() { }
+
+PXR_NAMESPACE_CLOSE_SCOPE
diff --git a/src/MaxUsd/Builders/JobContextRegistry.h b/src/MaxUsd/Builders/JobContextRegistry.h
new file mode 100644
index 0000000..51a648d
--- /dev/null
+++ b/src/MaxUsd/Builders/JobContextRegistry.h
@@ -0,0 +1,179 @@
+//
+// Copyright 2022 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+class QWidget;
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+TF_DECLARE_WEAK_PTRS(MaxUsdJobContextRegistry);
+
+/// We provide macros that are entry points into the job context logic.
+class MaxUsdJobContextRegistry : public TfWeakBase
+{
+public:
+ /// An job context basically wraps a function that tweaks the set of import/export options. This
+ /// job context has a name and UI components, as well as an enabler function that allows
+ /// specifying the options dictionary.
+ ///
+ /// To register an export job context, you need to use the REGISTER_EXPORT_JOB_CONTEXT macro for
+ /// each export job context supported by the library.
+ ///
+ /// In order for the core system to discover the plugin, you need a \c plugInfo.json that
+ /// declares job contexts. \code
+ /// {
+ /// "Plugins": [
+ /// {
+ /// "Info": {
+ /// "MaxUsd": {
+ /// "JobContextPlugin": {
+ /// }
+ /// }
+ /// },
+ /// "Name": "myUsdPlugin",
+ /// "LibraryPath": "../myUsdPlugin.dll",
+ /// "Type": "library"
+ /// }
+ /// ]
+ /// }
+ /// \endcode
+
+ /// Enabler function, returns a dictionary containing all the options for the context.
+ using EnablerFn = std::function;
+ using OptionsFn = std::function;
+
+ /// Get all registered export job contexts:
+ static TfTokenVector ListJobContexts() { return GetInstance()._ListJobContexts(); }
+
+ /// All the information registered for a specific job context.
+ struct ContextInfo
+ {
+ TfToken jobContext;
+ TfToken niceName;
+ TfToken exportDescription;
+ EnablerFn exportEnablerCallback;
+ OptionsFn exportOptionsCallback;
+ TfToken importDescription;
+ EnablerFn importEnablerCallback;
+ OptionsFn importOptionsCallback;
+ };
+
+ /// Gets the conversion information associated with \p jobContext on export and import
+ static const ContextInfo& GetJobContextInfo(const TfToken& jobContext)
+ {
+ return GetInstance()._GetJobContextInfo(jobContext);
+ }
+
+ /// Registers an export job context, with nice name, description and enabler function.
+ ///
+ /// \param jobContext name will be used directly as one of the valid values of the job context option.
+ /// \param niceName is the name displayed in the options dialog.
+ /// \param description is displayed as a tooltip in the options dialog.
+ /// \param enablerFct will be called after option parsing to enable context specific options.
+ MaxUSDAPI void RegisterExportJobContext(
+ const std::string& jobContext,
+ const std::string& niceName,
+ const std::string& description,
+ EnablerFn enablerFct,
+ bool fromPython = false);
+
+ /// Registers an export job context ui option callback.
+ ///
+ /// \param jobContext the name of the registered job context.
+ /// \param optionsFct will be called after the user hits the option button in the USD export UI.
+ MaxUSDAPI void SetExportOptionsUI(
+ const std::string& jobContext,
+ OptionsFn optionsFct,
+ bool fromPython = false);
+
+ /// Registers an import job context, with nice name, description and enabler function.
+ ///
+ /// \param jobContext name will be used directly as one of the valid values of the job context option.
+ /// \param niceName is the name displayed in the options dialog.
+ /// \param description is displayed as a tooltip in the options dialog.
+ /// \param enablerFct will be called after option parsing to enable context specific options.
+ MaxUSDAPI void RegisterImportJobContext(
+ const std::string& jobContext,
+ const std::string& niceName,
+ const std::string& description,
+ EnablerFn enablerFct,
+ bool fromPython = false);
+
+ /// Registers an import job context ui option callback.
+ ///
+ /// \param jobContext the name of the registered job context.
+ /// \param optionsFct will be called after the user hits the option button in the USD import UI.
+ MaxUSDAPI void SetImportOptionsUI(
+ const std::string& jobContext,
+ OptionsFn optionsFct,
+ bool fromPython = false);
+
+ MaxUSDAPI static MaxUsdJobContextRegistry& GetInstance();
+
+private:
+ MaxUSDAPI TfTokenVector _ListJobContexts();
+ MaxUSDAPI const ContextInfo& _GetJobContextInfo(const TfToken&);
+
+ MaxUsdJobContextRegistry();
+ ~MaxUsdJobContextRegistry();
+ friend class TfSingleton;
+};
+
+#define REGISTER_EXPORT_JOB_CONTEXT(name, niceName, description, enablerFct) \
+ TF_REGISTRY_FUNCTION(MaxUsdJobContextRegistry) \
+ { \
+ MaxUsdJobContextRegistry::GetInstance().RegisterExportJobContext( \
+ name, niceName, description, enablerFct); \
+ }
+
+#define REGISTER_EXPORT_JOB_CONTEXT_FCT(name, niceName, description) \
+ static VtDictionary _ExportJobContextEnabler_##name(); \
+ TF_REGISTRY_FUNCTION(MaxUsdJobContextRegistry) \
+ { \
+ MaxUsdJobContextRegistry::GetInstance().RegisterExportJobContext( \
+ #name, niceName, description, &_ExportJobContextEnabler_##name); \
+ } \
+ VtDictionary _ExportJobContextEnabler_##name()
+
+#define REGISTER_IMPORT_JOB_CONTEXT(name, niceName, description, enablerFct) \
+ TF_REGISTRY_FUNCTION(MaxUsdJobContextRegistry) \
+ { \
+ MaxUsdJobContextRegistry::GetInstance().RegisterImportJobContext( \
+ name, niceName, description, enablerFct); \
+ }
+
+#define REGISTER_IMPORT_JOB_CONTEXT_FCT(name, niceName, description) \
+ static VtDictionary _ImportJobContextEnabler_##name(); \
+ TF_REGISTRY_FUNCTION(MaxUsdJobContextRegistry) \
+ { \
+ MaxUsdJobContextRegistry::GetInstance().RegisterImportJobContext( \
+ #name, niceName, description, &_ImportJobContextEnabler_##name); \
+ } \
+ VtDictionary _ImportJobContextEnabler_##name()
+
+PXR_NAMESPACE_CLOSE_SCOPE
diff --git a/src/MaxUsd/Builders/MaxSceneBuilder.cpp b/src/MaxUsd/Builders/MaxSceneBuilder.cpp
new file mode 100644
index 0000000..08611fd
--- /dev/null
+++ b/src/MaxUsd/Builders/MaxSceneBuilder.cpp
@@ -0,0 +1,390 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "MaxSceneBuilder.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include // for IInstanceMgr::GetAutoMtlPropagation()
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+namespace MAXUSD_NS_DEF {
+
+MaxSceneBuilder::MaxSceneBuilder() = default;
+
+bool MaxSceneBuilder::ExcludedPrimNode(UsdPrimRange::iterator& primIt)
+{
+ return primIt->IsA() || primIt->IsA()
+ || primIt->IsA() || primIt->IsA();
+}
+
+void MaxSceneBuilder::DoImportPrimIt(
+ UsdPrimRange::iterator& primIt,
+ MaxUsdReadJobContext& readCtx,
+ PrimReaderMap& primReaderMap)
+{
+ const UsdPrim& prim = *primIt;
+
+ // The iterator will hit each prim twice. IsPostVisit tells us if
+ // this is the pre-visit (Read) step or post-visit (PostReadSubtree)
+ // step.
+ if (primIt.IsPostVisit()) {
+ // This is the PostReadSubtree step, if the PrimReader has specified one.
+ auto primReaderIt = primReaderMap.find(prim.GetPath());
+ if (primReaderIt != primReaderMap.end()) {
+ if (primReaderIt->second->HasPostReadSubtree()) {
+ primReaderIt->second->PostReadSubtree();
+ }
+ }
+ } else {
+ // This is the normal Read step (pre-visit).
+ TfToken typeName = prim.GetTypeName();
+ if (MaxUsdPrimReaderRegistry::ReaderFactoryFn factoryFn
+ = MaxUsdPrimReaderRegistry::FindOrFallback(typeName, readCtx.GetArgs(), prim)) {
+ MaxUsdPrimReaderSharedPtr primReader = factoryFn(prim, readCtx);
+ if (primReader) {
+ primReader->Read();
+ primReaderMap[prim.GetPath()] = primReader;
+ }
+ // has the last PrimReader took care of its children, then prune rest of tree branch
+ if (readCtx.GetPruneChildren()) {
+ primIt.PruneChildren();
+ readCtx.SetPruneChildren(false);
+ }
+ }
+ }
+}
+
+void MaxSceneBuilder::DoImportInstanceIt(
+ UsdPrimRange::iterator& primIt,
+ MaxUsdReadJobContext& readCtx,
+ PrototypeLookupMaps& prototypeLookupMaps,
+ bool insidePrototype)
+{
+ if (primIt.IsPostVisit()) {
+ return;
+ }
+
+ const UsdPrim& prim = *primIt;
+ const UsdPrim prototype = prim.GetPrototype();
+ if (!prototype) {
+ return;
+ }
+
+ // get instance prototype path
+ const SdfPath prototypePath = prototype.GetPath();
+ // was the prototype already imported previously
+ INode* prototypeNode = readCtx.GetMaxNode(prototypePath, false);
+ if (!prototypeNode) {
+ ImportPrototype(prototype, readCtx, prototypeLookupMaps);
+ prototypeNode = readCtx.GetMaxNode(prototypePath, false);
+ if (!prototypeNode) {
+ MaxUsd::Log::Error(
+ "The prototype node ({0}) could not be found and will not be instanciated. Import "
+ "issue "
+ "should be resolved.",
+ prototypePath.GetString().c_str());
+ return;
+ }
+ }
+
+ // clone prototype as instance
+ INodeTab inputTab, sourceTab, outputTab;
+ Point3 offset(0, 0, 0);
+ inputTab.AppendNode(prototypeNode);
+ GetCOREInterface()->CloneNodes(inputTab, offset, true, NODE_INSTANCE, &sourceTab, &outputTab);
+
+ // Rename the node to remove the automatically prepended number by 3dsMax on clone.
+ INode* createdNode = outputTab[0];
+ createdNode->SetName(MaxUsd::UsdStringToMaxString(prim.GetName()).data());
+ prototypeLookupMaps.prototypeReaderMap[prototypePath]->InstanceCreated(prim, createdNode);
+
+ // if instancing from within a prototype, map the cloned nodes to the reader they originate from
+ if (insidePrototype) {
+ prototypeLookupMaps.nodeToPrototypeMap[createdNode] = prim.GetPath();
+ prototypeLookupMaps.prototypeReaderMap[prim.GetPath()]
+ = prototypeLookupMaps
+ .prototypeReaderMap[prototypeLookupMaps.nodeToPrototypeMap[sourceTab[0]]];
+ }
+
+ // Add duplicate node to registry.
+ readCtx.RegisterNewMaxRefTargetHandle(prim.GetPath(), createdNode);
+
+ INode* parentNode = readCtx.GetMaxNode(prim.GetParent().GetPath(), false);
+ if (parentNode) {
+ parentNode->AttachChild(createdNode);
+ }
+
+ // Read xformable attributes from the
+ // UsdPrim on to the transform node.
+ MaxUsdTranslatorXformable::Read(prim, createdNode, readCtx);
+
+ // process the cloned node's children
+ // - rename nodes to prototype name
+ // - add nodes to created nodes list
+ // - call InstanceCreated using the proper prototype reader that was used
+ // - keep track of nodes created and their original prototype
+ bool hideCloned = !prototype.IsHidden() && createdNode->IsHidden();
+ int nbClones = sourceTab.Count();
+ for (int i = nbClones - 1; i > 0;
+ --i) // clones are listed in reverse order traversal - we need depth first
+ {
+ INode* sourceChildNode = sourceTab[i];
+ INode* clonedChildNode = outputTab[i];
+ if (hideCloned) {
+ clonedChildNode->Hide(true);
+ }
+ // for the instance prototype to know from which prototype it comes from
+ auto subInstancePath = prototypeLookupMaps.nodeToPrototypeMap[sourceChildNode];
+ auto instancePrimPath = subInstancePath.ReplacePrefix(prototypePath, primIt->GetPath());
+
+ clonedChildNode->SetName(sourceChildNode->GetName());
+ readCtx.RegisterNewMaxRefTargetHandle(instancePrimPath, clonedChildNode);
+
+ // Read xformable attributes from the
+ // UsdPrim on to the transform node.
+ MaxUsdTranslatorXformable::Read(
+ readCtx.GetStage()->GetPrimAtPath(instancePrimPath), clonedChildNode, readCtx);
+
+ // if instancing from within a prototype, map the cloned nodes to the reader they originate
+ // from
+ if (insidePrototype) {
+ prototypeLookupMaps.nodeToPrototypeMap[clonedChildNode] = instancePrimPath;
+ prototypeLookupMaps.prototypeReaderMap[instancePrimPath]
+ = prototypeLookupMaps
+ .prototypeReaderMap[prototypeLookupMaps.nodeToPrototypeMap[sourceChildNode]];
+ }
+ prototypeLookupMaps
+ .prototypeReaderMap[prototypeLookupMaps.nodeToPrototypeMap[sourceChildNode]]
+ ->InstanceCreated(readCtx.GetStage()->GetPrimAtPath(instancePrimPath), clonedChildNode);
+ }
+}
+
+void MaxSceneBuilder::ImportPrototype(
+ const UsdPrim& prototype,
+ MaxUsdReadJobContext& readCtx,
+ PrototypeLookupMaps& prototypeLookupMaps)
+{
+ PrimReaderMap primReaderMap;
+ const UsdPrimRange range = UsdPrimRange::PreAndPostVisit(prototype);
+ for (auto primIt = range.begin(); primIt != range.end(); ++primIt) {
+ if (ExcludedPrimNode(primIt)) {
+ continue;
+ }
+ const UsdPrim& prim = *primIt;
+ if (prim.IsInstance()) {
+ DoImportInstanceIt(primIt, readCtx, prototypeLookupMaps, true);
+ } else {
+ DoImportPrimIt(primIt, readCtx, primReaderMap);
+ }
+ }
+ // add to the prototypeReaderMap, the readers that were used to load the prototype
+ prototypeLookupMaps.prototypeReaderMap.insert(primReaderMap.begin(), primReaderMap.end());
+ for (const auto& element : primReaderMap) {
+ prototypeLookupMaps.nodeToPrototypeMap[readCtx.GetMaxNode(element.first, false)]
+ = element.first;
+ }
+}
+
+int MaxSceneBuilder::Build(
+ INode* node,
+ const pxr::UsdPrim& prim,
+ const MaxSceneBuilderOptions& buildOptions,
+ const fs::path& filename)
+{
+ pxr::UsdStageRefPtr stage = prim.GetStage();
+
+ // Insert the stage in the global cache for the time of the import. Useful so it can be accessed
+ // from callbacks. Removed from the cache using RAII.
+ const MaxUsd::StageCacheScopeGuard stageCacheGuard { stage };
+
+ const auto timeConfig = buildOptions.GetResolvedTimeConfig(stage);
+ const auto startTime = buildOptions.GetStartTimeCode();
+ const auto endTime = buildOptions.GetEndTimeCode();
+
+ std::string timeCodeLogMessage = "Importing at ";
+ switch (buildOptions.GetTimeMode()) {
+ case MaxSceneBuilderOptions::ImportTimeMode::AllRange: {
+ if (startTime != endTime || startTime != 0) {
+ MaxUsd::Log::Warn("A non-default TimeCode is specified, but will be ignored, as the "
+ "TimeMode property is "
+ "configured as #AllRange.");
+ }
+ timeCodeLogMessage.append(
+ std::string("#AllRange timeCode : ") + std::to_string(timeConfig.GetStartTimeCode())
+ + " " + std::to_string(timeConfig.GetEndTimeCode()));
+ break;
+ }
+ case MaxSceneBuilderOptions::ImportTimeMode::CustomRange: {
+ timeCodeLogMessage.append(
+ std::string("#CustomRange timeCode : ") + std::to_string(timeConfig.GetStartTimeCode())
+ + " " + std::to_string(timeConfig.GetEndTimeCode()));
+ break;
+ }
+ case MaxSceneBuilderOptions::ImportTimeMode::StartTime: {
+ timeCodeLogMessage.append(
+ std::string("#StartTime timeCode : ") + std::to_string(timeConfig.GetStartTimeCode()));
+ break;
+ }
+ case MaxSceneBuilderOptions::ImportTimeMode::EndTime: {
+ timeCodeLogMessage.append(
+ std::string("#EndTime timeCode : ") + std::to_string(timeConfig.GetEndTimeCode()));
+ break;
+ }
+ default: DbgAssert(_T("Unhandled TimeMode mode type. Importing using default values.")); break;
+ }
+ MaxUsd::Log::Info(timeCodeLogMessage);
+
+ MaxUsdReadJobContext context(buildOptions, stage);
+
+ // We want both pre- and post- visit iterations over the prims in this
+ // method. To do so, iterate over all the root prims of the input range,
+ // and create new PrimRanges to iterate over their subtrees.
+ PrimReaderMap primReaderMap;
+ auto predicates = !pxr::UsdPrimIsAbstract && pxr::UsdPrimIsDefined;
+ pxr::UsdPrimRange newPrimRange = pxr::UsdPrimRange::PreAndPostVisit(prim, predicates);
+
+ // Prepare 3ds Max to expose information to the User about the progress of the import:
+ int currentPrimIndex = 0;
+ MaxProgressBar progressBar(
+ GetString(IDS_IMPORT_PROGRESS_MESSAGE),
+ std::distance(newPrimRange.cbegin(), newPrimRange.cend()));
+ progressBar.SetEnabled(buildOptions.GetUseProgressBar());
+ progressBar.Start();
+
+ IInstanceMgr* pInstanceMgr = IInstanceMgr::GetInstanceMgr();
+ const bool autoMtlPropagation = pInstanceMgr && pInstanceMgr->GetAutoMtlPropagation();
+ if (autoMtlPropagation) {
+ // temporarily disable auto material propagation
+ pInstanceMgr->SetAutoMtlPropagation(false);
+ }
+
+ PrototypeLookupMaps prototypeLookupMaps;
+ for (auto primIt = newPrimRange.begin(); primIt != newPrimRange.end(); ++primIt) {
+ // Stop the import in its current state if the User chose to cancel it.
+ //
+ // NOTE: This will result in partially-loaded content, which may require additional handling
+ // to make sure the User understands that this may cause side-effects. All the geometry
+ // content should be removed. However, some non-geometry can still have been imported
+ // (materials, textures, etc.)
+ if (coreInterface->GetCancel()) {
+ bool cancelImport = true;
+
+ // Avoid displaying a blocking dialog if 3ds Max is running in Quiet Mode:
+ if (!coreInterface->GetQuietMode()) {
+ cancelImport = MaxUsd::Ui::AskYesNoQuestion(
+ GetStdWString(IDS_IMPORT_CANCEL_TEXT),
+ GetStdWString(IDS_IMPORT_CANCEL_CAPTION));
+ }
+
+ if (cancelImport) {
+ progressBar.Stop();
+ // delete created nodes
+ {
+ std::vector nodes;
+ context.GetAllCreatedNodes(nodes);
+ INodeTab newNodes;
+ newNodes.Insert(0, static_cast(nodes.size()), nodes.data());
+ GetCOREInterface17()->DeleteNodes(newNodes);
+ }
+ MaxUsd::Log::Info("USD import canceled.");
+ return IMPEXP_CANCEL;
+ }
+ coreInterface->SetCancel(false);
+ }
+
+ if (primIt->IsPseudoRoot() || ExcludedPrimNode(primIt)) {
+ continue;
+ }
+
+ if (primIt->IsInstance()) {
+ DoImportInstanceIt(primIt, context, prototypeLookupMaps);
+ } else {
+ DoImportPrimIt(primIt, context, primReaderMap);
+ }
+
+ // Update the progress bar displayed by 3ds Max to notify the User about the status of the
+ // operation:
+ progressBar.UpdateProgress(currentPrimIndex++);
+ }
+
+ // delete prototype nodes that are now useless
+ std::function deleteNode = [&](INode* node) {
+ while (node->NumChildren() > 0) {
+ deleteNode(node->GetChildNode(0));
+ }
+ context.RemoveNode(node);
+ GetCOREInterface17()->DeleteNode(node, false);
+ };
+ auto prototypes = context.GetStage()->GetPrototypes();
+ for (const auto& prototype : prototypes) {
+ const SdfPath prototypePath = prototype.GetPath();
+ INode* prototypeNode = context.GetMaxNode(prototypePath, false);
+ if (prototypeNode) {
+ deleteNode(prototypeNode);
+ }
+ }
+
+ context.RescaleRegisteredNodes();
+
+ if (autoMtlPropagation) {
+ pInstanceMgr->SetAutoMtlPropagation(true);
+ }
+
+ // Report that we are running chasers...
+ progressBar.UpdateProgress(
+ progressBar.GetTotal(), false, GetString(IDS_IMPORT_CHASERS_PROGRESS_MESSAGE));
+
+ // call chasers
+ // populate the chasers and run post import
+ std::vector> chasers;
+ pxr::MaxUsdImportChaserRegistry::FactoryContext ctx(predicates, context, filename);
+ // for available chasers to load if not done already
+ pxr::MaxUsdImportChaserRegistry::GetAllRegisteredChasers();
+
+ for (const std::string& chaserName : buildOptions.GetChaserNames()) {
+ if (pxr::MaxUsdImportChaserRefPtr fn
+ = pxr::MaxUsdImportChaserRegistry::Create(chaserName, ctx)) {
+ chasers.push_back(std::make_pair(chaserName, fn));
+ } else {
+ MaxUsd::Log::Error("Failed to create chaser: {0}", chaserName.c_str());
+ }
+ }
+
+ for (const auto& chaser : chasers) {
+ if (chaser.second->PostImport()) {
+ MaxUsd::Log::Info("Successfully executed PostImport() for {0}", chaser.first);
+ } else {
+ MaxUsd::Log::Error("Failed executing PostImport() for {0}", chaser.first);
+ }
+ }
+
+ return IMPEXP_SUCCESS;
+}
+
+} // namespace MAXUSD_NS_DEF
\ No newline at end of file
diff --git a/src/MaxUsd/Builders/MaxSceneBuilder.h b/src/MaxUsd/Builders/MaxSceneBuilder.h
new file mode 100644
index 0000000..846346e
--- /dev/null
+++ b/src/MaxUsd/Builders/MaxSceneBuilder.h
@@ -0,0 +1,131 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include "MaxSceneBuilderOptions.h"
+
+#include
+#include
+
+#include
+
+#include
+
+namespace MAXUSD_NS_DEF {
+
+/**
+ * \brief 3ds Max Scene Builder.
+ * \remarks This current implementation is a work-in-progress that will evolve as additional conversion operations
+ * between USD and 3ds Max are supported. Performance of the import process is a design concern, and
+ * while CRTP-type solutions are not (currently) implemented, future work should attempt to
+ * improve/maintain run-time performance while maintaining a high level of flexibility.
+ * \remarks This current implementation moves some of the import logic away from the USDSceneController where it was
+ * previously located. In the process, the import still owns some of the UI/UX import process such
+ * as the handling of 3ds Max's progress bar. Future work should abstract away this behavior, and
+ * expose more control to the caller (e.g. through callbacks, or notifications about the current
+ * state of the import process, etc.).
+ */
+class MaxUSDAPI MaxSceneBuilder
+{
+public:
+ /**
+ * \brief Constructor.
+ */
+ MaxSceneBuilder();
+
+ /**
+ * \brief Destructor.
+ */
+ virtual ~MaxSceneBuilder() = default;
+
+ /**
+ * \brief Start the scene building process.
+ * \param node 3ds Max Node from which to start building the Scene.
+ * \param prim USD Prim from which to start building the Scene.
+ * \param buildOptions Options for the translation of USD content into 3ds Max content.
+ * \param filename The filename of the USD file being used to build to the max scene.
+ * \return IMPEXP_FAIL if failed, IMPEXP_SUCCESS if success and IMPEXP_CANCEL if canceled by user
+ */
+ int Build(
+ INode* node,
+ const pxr::UsdPrim& prim,
+ const MaxSceneBuilderOptions& buildOptions,
+ const fs::path& filename = "");
+
+protected:
+ typedef std::unordered_map
+ PrimReaderMap;
+ typedef std::unordered_map
+ NodeToPrimMap; // inverse lookup of PrimReaderMap
+ // Structure used to correlate the prototype nodes and their clones to the original prototype
+ // prim they originate from
+ struct PrototypeLookupMaps
+ {
+ PrimReaderMap
+ prototypeReaderMap; // mapping the prototype path to the reader used to import it
+ NodeToPrimMap nodeToPrototypeMap; // mapping 3ds Max Node to the original prototype path it
+ // got created from
+ };
+
+ /**
+ * \brief Create the prim's 3ds Max Node.
+ * \param primIt PrimRange iterator on the UsdPrim to import
+ * \param readCtx The read job context being used in the current import job
+ * \param primReaderMap The map between the imported prim path and its reader.
+ */
+ void DoImportPrimIt(
+ pxr::UsdPrimRange::iterator& primIt,
+ pxr::MaxUsdReadJobContext& readCtx,
+ PrimReaderMap& primReaderMap);
+
+ /**
+ * \brief Creates the prim's 3ds Max Node instances upon creating first the associated prototype.
+ * \param primIt PrimRange iterator on the UsdPrim to import
+ * \param readCtx The read job context being used in the current import job
+ * \param protypeLookupMaps Maps to correlate the prototype nodes and their clones
+ * to the original prototype prim they originate from
+ * \param insidePrototype Report if instancing from within a prototype
+ */
+ void DoImportInstanceIt(
+ pxr::UsdPrimRange::iterator& primIt,
+ pxr::MaxUsdReadJobContext& readCtx,
+ PrototypeLookupMaps& prototypeLookupMaps,
+ bool insidePrototype = false);
+
+ /**
+ * \brief Imports the prototype prim (and descendants) and adds it (them) to the read context
+ * \param prototype The prim serving as prototype to import
+ * \param readCtx The read job context being used in the current import job.
+ * \param protypeLookupMaps Maps to correlate the prototype nodes and their clones
+ * to the original prototype prim they originate from.
+ */
+ void ImportPrototype(
+ const pxr::UsdPrim& prototype,
+ pxr::MaxUsdReadJobContext& readCtx,
+ PrototypeLookupMaps& protypeLookupMaps);
+
+ /**
+ * \brief Small helper to exclude some prim types from being handled by PrimReaders
+ * \param primIt PrimRange iterator on the UsdPrim to import
+ * \return True if the prim type is to be excluded, false otherwise
+ */
+ bool ExcludedPrimNode(pxr::UsdPrimRange::iterator& primIt);
+
+ /// Reference to the Core Interface to use to interface with 3ds Max:
+ Interface17* coreInterface { GetCOREInterface17() };
+};
+
+} // namespace MAXUSD_NS_DEF
diff --git a/src/MaxUsd/Builders/MaxSceneBuilderOptions.cpp b/src/MaxUsd/Builders/MaxSceneBuilderOptions.cpp
new file mode 100644
index 0000000..620ea75
--- /dev/null
+++ b/src/MaxUsd/Builders/MaxSceneBuilderOptions.cpp
@@ -0,0 +1,450 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "MaxSceneBuilderOptions.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+using namespace MaxUsd;
+PXR_NAMESPACE_USING_DIRECTIVE
+
+PXR_NAMESPACE_OPEN_SCOPE
+TF_DEFINE_PUBLIC_TOKENS(MaxUsdMaxSceneBuilderOptionsTokens, PXR_MAXUSD_MAX_SCENE_BUILDER_TOKENS);
+TF_DEFINE_PUBLIC_TOKENS(MaxUsdShadingModesTokens, PXR_MAXUSD_SHADING_MODES_TOKENS);
+PXR_NAMESPACE_CLOSE_SCOPE
+
+namespace MAXUSD_NS_DEF {
+
+MaxSceneBuilderOptions::MaxSceneBuilderOptions()
+{
+ // default settings must be applied manually
+ // it implies loading the USD plugin material conversion types
+ // which must take place outside the DLL initialization
+}
+
+MaxSceneBuilderOptions::MaxSceneBuilderOptions(const VtDictionary& dict)
+{
+ VtDictionary tmp = dict;
+ DictUtils::CoerceDictToGuideType(tmp, MaxSceneBuilderOptions::GetDefaultDictionary());
+
+ if (VtDictionaryIsHolding(
+ tmp, MaxUsdMaxSceneBuilderOptionsTokens->shadingModes)) {
+ auto shadingModes
+ = VtDictionaryGet(tmp, MaxUsdMaxSceneBuilderOptionsTokens->shadingModes);
+ for (VtDictionary& shadingMode : shadingModes) {
+ DictUtils::CoerceDictToGuideType(shadingMode, GetShadingModeDefaultDictionary());
+ }
+ tmp.SetValueAtPath(MaxUsdMaxSceneBuilderOptionsTokens->shadingModes, VtValue(shadingModes));
+ }
+
+ options = VtDictionaryOver(tmp, GetDefaultDictionary());
+}
+
+namespace {
+const std::string importUnmappedPrimvarsKey
+ = MaxUsdMaxSceneBuilderOptionsTokens->primvarMappingOptions.GetString() + ":"
+ + MaxUsdPrimvarMappingOptions->importUnmappedPrimvars.GetString();
+} // namespace
+
+/* static */
+const pxr::VtDictionary& MaxSceneBuilderOptions::GetDefaultDictionary()
+{
+ static VtDictionary defaultDict;
+ static std::once_flag once;
+ std::call_once(once, []() {
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->version] = 1;
+ // Base defaults.
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->initialLoadSet]
+ = static_cast(UsdStage::LoadAll);
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->timeMode]
+ = static_cast(ImportTimeMode::AllRange);
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->stageMaskPaths]
+ = std::vector { SdfPath::AbsoluteRootPath() };
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->metaDataIncludes]
+ = std::set { MetaData::Kind, MetaData::Purpose, MetaData::Hidden };
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->preferredMaterial]
+ = MaxUsdPreferredMaterialTokens->none;
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->useProgressBar] = true;
+
+ defaultDict[MaxUsdSceneBuilderOptionsTokens->contextNames] = std::set();
+ defaultDict[MaxUsdSceneBuilderOptionsTokens->jobContextOptions] = VtDictionary();
+ defaultDict[MaxUsdSceneBuilderOptionsTokens->chaserNames] = std::vector();
+ defaultDict[MaxUsdSceneBuilderOptionsTokens->chaserArgs]
+ = std::map();
+
+ defaultDict[MaxUsdSceneBuilderOptionsTokens->logLevel] = static_cast(Log::Level::Off);
+
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->primvarMappingOptions]
+ = PrimvarMappingOptions().GetOptions();
+ defaultDict.SetValueAtPath(importUnmappedPrimvarsKey, VtValue(true));
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->shadingModes] = ShadingModes();
+
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->startTimeCode] = 0.0;
+ defaultDict[MaxUsdMaxSceneBuilderOptionsTokens->endTimeCode] = 0.0;
+ });
+ // Purposefully left out of the call_once in order to always fetch the latest value for
+ // "APP_TEMP_DIR", it might change during the session.
+ defaultDict[MaxUsdSceneBuilderOptionsTokens->logPath]
+ = fs::path(std::wstring(MaxSDKSupport::GetString(GetCOREInterface()->GetDir(APP_TEMP_DIR)))
+ .append(L"\\MaxUsdImport.log"));
+ // Purposefully left out the call_once, as new shading modes might be added during the session.
+ SetDefaultShadingModes(defaultDict);
+
+ return defaultDict;
+}
+
+/* static */
+const pxr::VtDictionary& MaxSceneBuilderOptions::GetShadingModeDefaultDictionary()
+{
+ static VtDictionary defaultDict;
+ static std::once_flag once;
+ std::call_once(once, []() {
+ // Base defaults.
+ defaultDict[MaxUsdShadingModesTokens->mode] = MaxUsdShadingModeTokens->useRegistry;
+ defaultDict[MaxUsdShadingModesTokens->materialConversion]
+ = UsdImagingTokens->UsdPreviewSurface;
+ });
+
+ return defaultDict;
+}
+
+void MaxSceneBuilderOptions::SetOptions(const MaxSceneBuilderOptions& options)
+{
+ this->options = options.options;
+}
+
+void MaxSceneBuilderOptions::SetDefaults() { options = GetDefaultDictionary(); }
+
+void MaxSceneBuilderOptions::SetDefaultShadingModes(VtDictionary& dict)
+{
+ ShadingModes shadingModes;
+
+ // Define the lambda to simplify the emplace_back operation
+ auto addShadingMode = [&shadingModes](const TfToken& mode, const TfToken& materialConversion) {
+ shadingModes.emplace_back(VtDictionary {
+ { MaxUsdShadingModesTokens->mode, VtValue(mode) },
+ { MaxUsdShadingModesTokens->materialConversion, VtValue(materialConversion) } });
+ };
+
+ for (const auto& c : MaxUsdShadingModeRegistry::ListMaterialConversions()) {
+ if (c != UsdImagingTokens->UsdPreviewSurface) {
+ auto const& info = MaxUsdShadingModeRegistry::GetMaterialConversionInfo(c);
+ if (info.hasImporter) {
+ addShadingMode(MaxUsdShadingModeTokens->useRegistry, c);
+ }
+ }
+ }
+ for (const auto& s : MaxUsdShadingModeRegistry::ListImporters()) {
+ if (s != MaxUsdShadingModeTokens->useRegistry) {
+ addShadingMode(s, MaxUsdShadingModeTokens->none);
+ }
+ }
+ addShadingMode(MaxUsdShadingModeTokens->useRegistry, UsdImagingTokens->UsdPreviewSurface);
+
+ dict.SetValueAtPath(MaxUsdMaxSceneBuilderOptionsTokens->shadingModes, VtValue(shadingModes));
+}
+
+void MaxSceneBuilderOptions::SetDefaultShadingModes() { SetDefaultShadingModes(options); }
+
+void MaxSceneBuilderOptions::SetStageInitialLoadSet(UsdStage::InitialLoadSet initialLoadSet)
+{
+ options[MaxUsdMaxSceneBuilderOptionsTokens->initialLoadSet] = static_cast(initialLoadSet);
+}
+
+pxr::UsdStage::InitialLoadSet MaxSceneBuilderOptions::GetStageInitialLoadSet() const
+{
+ return static_cast(
+ VtDictionaryGet(options, MaxUsdMaxSceneBuilderOptionsTokens->initialLoadSet));
+}
+
+void MaxSceneBuilderOptions::SetTimeMode(const ImportTimeMode& timeMode)
+{
+ options[MaxUsdMaxSceneBuilderOptionsTokens->timeMode] = static_cast(timeMode);
+}
+
+MaxSceneBuilderOptions::ImportTimeMode MaxSceneBuilderOptions::GetTimeMode() const
+{
+ return static_cast(
+ VtDictionaryGet(options, MaxUsdMaxSceneBuilderOptionsTokens->timeMode));
+}
+
+void MaxSceneBuilderOptions::SetStartTimeCode(double startTimeCode)
+{
+ options[MaxUsdMaxSceneBuilderOptionsTokens->startTimeCode] = startTimeCode;
+ if (GetEndTimeCode() < startTimeCode) {
+ SetEndTimeCode(startTimeCode);
+ }
+}
+
+void MaxSceneBuilderOptions::SetEndTimeCode(double endTimeCode)
+{
+ options[MaxUsdMaxSceneBuilderOptionsTokens->endTimeCode] = endTimeCode;
+ if (GetStartTimeCode() > endTimeCode) {
+ SetStartTimeCode(endTimeCode);
+ }
+}
+
+double MaxSceneBuilderOptions::GetStartTimeCode() const
+{
+ return VtDictionaryGet(options, MaxUsdMaxSceneBuilderOptionsTokens->startTimeCode);
+}
+
+double MaxSceneBuilderOptions::GetEndTimeCode() const
+{
+ return VtDictionaryGet(options, MaxUsdMaxSceneBuilderOptionsTokens->endTimeCode);
+}
+
+MaxUsd::ImportTimeConfig
+MaxSceneBuilderOptions::GetResolvedTimeConfig(const pxr::UsdStagePtr& stage) const
+{
+ MaxUsd::ImportTimeConfig resultTimeConfig {};
+ if (!stage) {
+ return resultTimeConfig;
+ }
+
+ double startTime = 0;
+ double endTime = 0;
+ switch (GetTimeMode()) {
+ case MaxSceneBuilderOptions::ImportTimeMode::AllRange: {
+ // this has to be set for this case so that the user's don't have to manually do this
+ startTime = stage->GetStartTimeCode();
+ endTime = stage->GetEndTimeCode();
+ break;
+ }
+ case MaxSceneBuilderOptions::ImportTimeMode::CustomRange: {
+ ImportTimeConfig timeConfig(GetStartTimeCode(), GetEndTimeCode());
+ startTime = timeConfig.GetStartTimeCode();
+ endTime = timeConfig.GetEndTimeCode();
+ break;
+ }
+ case MaxSceneBuilderOptions::ImportTimeMode::StartTime: {
+ startTime = stage->GetStartTimeCode();
+ endTime = stage->GetStartTimeCode();
+ break;
+ }
+ case MaxSceneBuilderOptions::ImportTimeMode::EndTime: {
+ startTime = stage->GetEndTimeCode();
+ endTime = stage->GetEndTimeCode();
+ break;
+ }
+ default: DbgAssert(_T("Unhandled TimeMode mode type. Importing using default values.")); break;
+ }
+
+ resultTimeConfig.SetStartTimeCode(startTime);
+ resultTimeConfig.SetEndTimeCode(endTime);
+
+ return resultTimeConfig;
+}
+
+void MaxSceneBuilderOptions::SetStageMaskPaths(const std::vector& paths)
+{
+ options[MaxUsdMaxSceneBuilderOptionsTokens->stageMaskPaths] = paths;
+}
+
+const std::vector& MaxSceneBuilderOptions::GetStageMaskPaths() const
+{
+ return VtDictionaryGet>(
+ options, MaxUsdMaxSceneBuilderOptionsTokens->stageMaskPaths);
+}
+
+void MaxSceneBuilderOptions::SetMetaData(const std::set& filters)
+{
+ options[MaxUsdMaxSceneBuilderOptionsTokens->metaDataIncludes] = filters;
+}
+
+const std::set& MaxSceneBuilderOptions::GetMetaData() const
+{
+ return VtDictionaryGet>(
+ options, MaxUsdMaxSceneBuilderOptionsTokens->metaDataIncludes);
+}
+
+PrimvarMappingOptions MaxSceneBuilderOptions::GetPrimvarMappingOptions() const
+{
+ return PrimvarMappingOptions(VtDictionaryGet(
+ options, MaxUsdMaxSceneBuilderOptionsTokens->primvarMappingOptions));
+}
+
+void MaxSceneBuilderOptions::SetPrimvarMappingOptions(
+ const PrimvarMappingOptions& primvarMappingOptions)
+{
+ options.SetValueAtPath(
+ MaxUsdMaxSceneBuilderOptionsTokens->primvarMappingOptions,
+ VtValue(primvarMappingOptions.GetOptions()));
+}
+
+bool MaxSceneBuilderOptions::GetTranslateMaterials() const
+{
+ const auto& shadingModes = GetShadingModes();
+ // if the ShadingModes is not set to 'none', materials are imported
+ auto modesIter
+ = std::find_if(shadingModes.begin(), shadingModes.end(), [](const VtDictionary& sm) {
+ if (VtDictionaryIsHolding(sm, MaxUsdShadingModesTokens->mode)) {
+ return VtDictionaryGet(sm, MaxUsdShadingModesTokens->mode)
+ == MaxUsdShadingModeTokens->none;
+ }
+ return false;
+ });
+ return modesIter == shadingModes.end();
+}
+
+void MaxSceneBuilderOptions::SetShadingModes(const ShadingModes& modes)
+{
+ auto SetNoneShadingMode = [this]() {
+ options.SetValueAtPath(
+ MaxUsdMaxSceneBuilderOptionsTokens->shadingModes,
+ VtValue(ShadingModes({ VtDictionary {
+ { MaxUsdShadingModesTokens->mode, VtValue(MaxUsdShadingModeTokens->none) },
+ { MaxUsdShadingModesTokens->materialConversion,
+ VtValue(MaxUsdPreferredMaterialTokens->none) } } })));
+ };
+ if (modes.empty()) {
+ SetNoneShadingMode();
+ return;
+ }
+ if (modes.size() > 1) {
+ auto modesIter = std::find_if(modes.begin(), modes.end(), [](const VtDictionary& sm) {
+ if (VtDictionaryIsHolding(sm, MaxUsdShadingModesTokens->mode)) {
+ return VtDictionaryGet(sm, MaxUsdShadingModesTokens->mode)
+ == MaxUsdShadingModeTokens->none;
+ }
+ return false;
+ });
+ if (modesIter != modes.end()) {
+ Log::Error(
+ "Cannot set multiple import ShadingModes when one of those modes is set to 'none'. "
+ "Keeping only 'none' as the import ShadingMode - no material will get imported.");
+ SetNoneShadingMode();
+ return;
+ }
+ }
+ options.SetValueAtPath(MaxUsdMaxSceneBuilderOptionsTokens->shadingModes, VtValue(modes));
+}
+
+const MaxSceneBuilderOptions::ShadingModes& MaxSceneBuilderOptions::GetShadingModes() const
+{
+ return VtDictionaryGet(options, MaxUsdMaxSceneBuilderOptionsTokens->shadingModes);
+}
+
+pxr::TfToken MaxSceneBuilderOptions::GetMaterialConversion() const
+{
+ const auto& shadingModes = GetShadingModes();
+ return shadingModes.empty()
+ ? TfToken()
+ : VtDictionaryGet(
+ shadingModes.front(), MaxUsdShadingModesTokens->materialConversion);
+}
+
+void MaxSceneBuilderOptions::SetPreferredMaterial(const pxr::TfToken& targetMaterial)
+{
+ options[MaxUsdMaxSceneBuilderOptionsTokens->preferredMaterial] = targetMaterial;
+}
+
+pxr::TfToken MaxSceneBuilderOptions::GetPreferredMaterial() const
+{
+ return VtDictionaryGet(
+ options,
+ MaxUsdMaxSceneBuilderOptionsTokens->preferredMaterial,
+ VtDefault = MaxUsdPreferredMaterialTokens->none);
+}
+
+MaxSceneBuilderOptions MaxSceneBuilderOptions::OptionsWithAppliedContexts() const
+{
+ auto thisCopy = *this;
+
+ VtDictionary allContextArgs;
+ if (MergeJobContexts(false, this->GetContextNames(), allContextArgs)) {
+ if (!allContextArgs.empty()) {
+ if (allContextArgs.count(MaxUsdSceneBuilderOptionsTokens->chaserNames) > 0) {
+ auto copyChaserNames = thisCopy.GetChaserNames();
+ if (copyChaserNames.empty()) {
+ copyChaserNames = DictUtils::ExtractVector(
+ allContextArgs, MaxUsdSceneBuilderOptionsTokens->chaserNames);
+ } else // merge args
+ {
+ auto values = DictUtils::ExtractVector(
+ allContextArgs, MaxUsdSceneBuilderOptionsTokens->chaserNames);
+ for (auto element : values) {
+ if (std::find(copyChaserNames.begin(), copyChaserNames.end(), element)
+ == copyChaserNames.end()) {
+ copyChaserNames.push_back(element);
+ }
+ }
+ }
+ thisCopy.SetChaserNames(copyChaserNames);
+ }
+ if (allContextArgs.count(MaxUsdSceneBuilderOptionsTokens->chaserArgs) > 0) {
+ auto copyChaserArgs = thisCopy.GetAllChaserArgs();
+ if (copyChaserArgs.empty()) {
+ copyChaserArgs = ExtractChaserArgs(
+ allContextArgs, MaxUsdSceneBuilderOptionsTokens->chaserArgs);
+ } else // merge args
+ {
+ auto values = ExtractChaserArgs(
+ allContextArgs, MaxUsdSceneBuilderOptionsTokens->chaserArgs);
+ for (auto element : values) {
+ if (copyChaserArgs.find(element.first) == copyChaserArgs.end()) {
+ copyChaserArgs[element.first] = element.second;
+ } else {
+ auto& currentChaserArgs = copyChaserArgs[element.first];
+ for (auto arg : element.second) {
+ if (currentChaserArgs.find(arg.first) == currentChaserArgs.end()) {
+ currentChaserArgs[arg.first] = arg.second;
+ } else {
+ if (currentChaserArgs[arg.first] != arg.second) {
+ TF_WARN(TfStringPrintf(
+ "Multiple argument value for '%s' associated to chaser "
+ "'%s'. Keeping "
+ "the argument value set to '%s' from Context.",
+ arg.first.c_str(),
+ element.first.c_str(),
+ arg.second));
+ // take the argument from the context, and forget the user's
+ currentChaserArgs[arg.first] = arg.second;
+ }
+ }
+ }
+ }
+ }
+ }
+ thisCopy.SetAllChaserArgs(copyChaserArgs);
+ }
+ }
+ } else {
+ MaxUsd::Log::Error("Errors while processing import contexts. Using base import options.");
+ }
+
+ return thisCopy;
+}
+
+bool MaxSceneBuilderOptions::GetUseProgressBar() const
+{
+ return VtDictionaryGet(
+ options, MaxUsdMaxSceneBuilderOptionsTokens->useProgressBar, VtDefault = true);
+}
+
+void MaxSceneBuilderOptions::SetUseProgressBar(bool useProgressBar)
+{
+ options[MaxUsdMaxSceneBuilderOptionsTokens->useProgressBar] = useProgressBar;
+}
+
+} // namespace MAXUSD_NS_DEF
diff --git a/src/MaxUsd/Builders/MaxSceneBuilderOptions.h b/src/MaxUsd/Builders/MaxSceneBuilderOptions.h
new file mode 100644
index 0000000..e0c9fec
--- /dev/null
+++ b/src/MaxUsd/Builders/MaxSceneBuilderOptions.h
@@ -0,0 +1,274 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include
+#pragma warning(push)
+#pragma warning(disable : 4275) // non dll-interface class 'boost::python::api::object' used as base
+ // for dll-interface struct 'boost::python::detail::list_base'
+#include
+#pragma warning(pop)
+
+#include "SceneBuilderOptions.h"
+
+#include
+#include
+#include
+
+PXR_NAMESPACE_OPEN_SCOPE
+// clang-format off
+#define PXR_MAXUSD_MAX_SCENE_BUILDER_TOKENS \
+ /* Dictionary keys */ \
+ (version) \
+ (initialLoadSet) \
+ (timeMode) \
+ (stageMaskPaths) \
+ (metaDataIncludes) \
+ (preferredMaterial) \
+ (useProgressBar) \
+ (primvarMappingOptions) \
+ (shadingModes) \
+ (startTimeCode) \
+ (endTimeCode)
+
+
+#define PXR_MAXUSD_SHADING_MODES_TOKENS \
+ /* Dictionary keys */ \
+ /* importer to use */ \
+ (mode) \
+ /* material to import */ \
+ (materialConversion)
+// clang-format on
+
+TF_DECLARE_PUBLIC_TOKENS(
+ MaxUsdMaxSceneBuilderOptionsTokens,
+ MaxUSDAPI,
+ PXR_MAXUSD_MAX_SCENE_BUILDER_TOKENS);
+TF_DECLARE_PUBLIC_TOKENS(MaxUsdShadingModesTokens, MaxUSDAPI, PXR_MAXUSD_SHADING_MODES_TOKENS);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+namespace MAXUSD_NS_DEF {
+
+/**
+ * \brief 3ds Max Scene Build configuration options.
+ * \remarks In the future, additional properties will be included to support transfer of more refined import controls.
+ * This includes start/end Time Codes for animation, variants, etc.
+ */
+class MaxSceneBuilderOptions : public SceneBuilderOptions
+{
+public:
+ /**
+ * \brief Time mode for import.
+ * AllRange considers the stage's entire range
+ * CustomRange will use the range defined in the import configuration (from StartTimeCode to
+ * EndTimeCode) StartTime will use the stage's start time code EndTime will use the stage's end
+ * time code
+ */
+ enum class MaxUSDAPI ImportTimeMode
+ {
+ AllRange,
+ CustomRange,
+ StartTime,
+ EndTime
+ };
+
+ /**
+ * \brief Constructor.
+ */
+ MaxUSDAPI MaxSceneBuilderOptions();
+
+ /**
+ * \brief This is used internally to initialize the options from a dictionary,
+ * and it is a costly operation due to the validation of the dictionary.
+ * \param dict The dictionary to initialize the options from.
+ */
+ MaxUSDAPI MaxSceneBuilderOptions(const pxr::VtDictionary& dict);
+
+ /**
+ * \brief Copies the values from an existing options object.
+ */
+ MaxUSDAPI void SetOptions(const MaxSceneBuilderOptions& options);
+
+ /**
+ * \brief Resets the importer options to default values.
+ */
+ MaxUSDAPI void SetDefaults();
+
+ /**
+ * \brief Resets the importer Shading Modes option to default values.
+ */
+ MaxUSDAPI void SetDefaultShadingModes();
+
+ /**
+ * \brief Check if materials should be translated
+ * \return "true" if materials should be translated
+ */
+ MaxUSDAPI bool GetTranslateMaterials() const;
+
+ /**
+ * \brief Sets the USD stage's initial load set to use for the import of content into 3ds Max.
+ * \param initialLoadSet USD Stage initial load set to use for the import of content into 3ds Max.
+ */
+ MaxUSDAPI void SetStageInitialLoadSet(pxr::UsdStage::InitialLoadSet initialLoadSet);
+
+ /**
+ * \brief Return the USD Stage initial load set to use for the import of content into 3ds Max.
+ * \return The USD Stage initial load set to use for the import of content into 3ds Max.
+ */
+ MaxUSDAPI pxr::UsdStage::InitialLoadSet GetStageInitialLoadSet() const;
+
+ /**
+ * \brief Sets the TimeMode at which the translation should take place.
+ */
+ MaxUSDAPI void SetTimeMode(const ImportTimeMode& timeMode);
+
+ /**
+ * \brief Set the usd timeCode for the starting time range at which the import should take place.
+ * \param startTimeCode The timeCode value for the initial value for the import time range.
+ */
+ MaxUSDAPI void SetStartTimeCode(double startTimeCode);
+
+ /**
+ * \brief Set the usd timeCode for the end time range at which the import should take place.
+ * \param endTimeCode The timeCode value for the end value for the import time range.
+ */
+ MaxUSDAPI void SetEndTimeCode(double endTimeCode);
+
+ /**
+ * \brief Gets usd timeCode value set for the start of the import time range.
+ * \return The TimeCode value set for start time for the import time range.
+ */
+ MaxUSDAPI double GetStartTimeCode() const;
+
+ /**
+ * \brief Gets usd timeCode value for the end of the import time range.
+ * \return The TimeCode value set for end time for the import time range.
+ */
+ MaxUSDAPI double GetEndTimeCode() const;
+
+ /**
+ * \brief Gets the TimeMode at which the translation should take place.
+ * \return The TimeMode to be used.
+ */
+ MaxUSDAPI ImportTimeMode GetTimeMode() const;
+
+ /**
+ * \brief Resolves the Time Configuration at which the conversion should take place when translating the USD stage
+ * to Max data.
+ * \param stage A reference to the stage to be translated.
+ */
+ MaxUSDAPI virtual MaxUsd::ImportTimeConfig
+ GetResolvedTimeConfig(const pxr::UsdStagePtr& stage) const;
+
+ /**
+ * \brief Sets the stage mask's paths. Only USD prims at or below these paths will be imported.
+ * \param The mask paths.
+ */
+ MaxUSDAPI void SetStageMaskPaths(const std::vector& paths);
+
+ /**
+ * \brief Returns the currently configured stage mask paths. Only USD prims at or below these paths
+ * will be imported.
+ * \return The mask's paths.
+ */
+ MaxUSDAPI const std::vector& GetStageMaskPaths() const;
+
+ /**
+ * \param filters Contains a list of MaxUsd::MetaData::MetaDataType that will be included during import
+ */
+ MaxUSDAPI void SetMetaData(const std::set& filters);
+
+ /**
+ * \return Returns the list of MaxUsd::MetaData::MetaDataType that will be included during import
+ */
+ MaxUSDAPI const std::set& GetMetaData() const;
+
+ /**
+ * \brief Returns the primvar/channel mapping options.
+ * \return The primvar/channel mapping options.
+ */
+ MaxUSDAPI PrimvarMappingOptions GetPrimvarMappingOptions() const;
+
+ /**
+ * \brief Sets the primvar/channel mapping options.
+ * \param primvarMappingOptions The new primvar/channel mapping options.
+ */
+ MaxUSDAPI void SetPrimvarMappingOptions(const PrimvarMappingOptions& primvarMappingOptions);
+
+ using ShadingModes = std::vector;
+ /**
+ * Set the shading modes to use at import
+ * \param modes A vector of VtDictionary, each dictionary is expected to contain two keys, 'materialConversion' and
+ * 'mode'
+ */
+ MaxUSDAPI void SetShadingModes(const ShadingModes& modes);
+
+ /**
+ * Get the shading modes to use at import
+ * \return the array of ShadingMode to use
+ */
+ MaxUSDAPI const ShadingModes& GetShadingModes() const;
+
+ /**
+ * Get the current material conversion
+ */
+ MaxUSDAPI pxr::TfToken GetMaterialConversion() const;
+ /**
+ * \brief Sets the preferred conversion material to use for material import
+ * \param targetMaterial The preferred conversion material to use for material import
+ */
+ MaxUSDAPI void SetPreferredMaterial(const pxr::TfToken& targetMaterial);
+ /**
+ * \brief Gets the preferred conversion material set for material import
+ * \return The preferred conversion material set for material import
+ */
+ MaxUSDAPI pxr::TfToken GetPreferredMaterial() const;
+
+ /**
+ * \brief Returns a copy of the current MaxSceneBuilderOptions with
+ * the JobContext option overrides applied on that copy
+ */
+ MaxUSDAPI MaxSceneBuilderOptions OptionsWithAppliedContexts() const;
+
+ /**
+ * \brief Gets whether to use the progress bar or not.
+ * \return True if the progress bar should be used.
+ */
+ MaxUSDAPI bool GetUseProgressBar() const;
+
+ /**
+ * \brief Sets whether to use the progress bar.
+ */
+ MaxUSDAPI void SetUseProgressBar(bool useProgressBar);
+
+private:
+ /**
+ * \brief Returns the default dictionary for the importer options.
+ * \return The default dictionary for the importer options.
+ */
+ static MaxUSDAPI const pxr::VtDictionary& GetDefaultDictionary();
+
+ /**
+ * \brief Returns the default dictionary for the importer shading modes.
+ * \return The default dictionary for the importer shading modes.
+ */
+ static MaxUSDAPI const pxr::VtDictionary& GetShadingModeDefaultDictionary();
+
+ static MaxUSDAPI void SetDefaultShadingModes(pxr::VtDictionary& dict);
+};
+
+} // namespace MAXUSD_NS_DEF
diff --git a/src/MaxUsd/Builders/SceneBuilderOptions.cpp b/src/MaxUsd/Builders/SceneBuilderOptions.cpp
new file mode 100644
index 0000000..3c4d09a
--- /dev/null
+++ b/src/MaxUsd/Builders/SceneBuilderOptions.cpp
@@ -0,0 +1,293 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "SceneBuilderOptions.h"
+
+#include "JobContextRegistry.h"
+#include "UsdSceneBuilderOptions.h"
+
+#include
+
+#include
+
+PXR_NAMESPACE_OPEN_SCOPE
+TF_DEFINE_PUBLIC_TOKENS(MaxUsdSceneBuilderOptionsTokens, PXR_MAXUSD_SCENE_BUILDER_OPTIONS_TOKENS);
+PXR_NAMESPACE_CLOSE_SCOPE
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+namespace MAXUSD_NS_DEF {
+
+// Merges all the jobContext arguments dictionaries found while exploring the jobContexts into a
+// single one. Also checks for conflicts and errors.
+bool MergeJobContexts(
+ bool isExport,
+ const std::set& contexts,
+ VtDictionary& allContextArgs)
+{
+ // List of all argument dictionaries found while exploring jobContexts
+ std::vector contextArgs;
+ bool canMergeContexts = true;
+ // This first loop gathers all job context argument dictionaries found in the userArgs
+ const TfToken& jcKey = MaxUsdSceneBuilderOptionsTokens->jobContext;
+ for (const std::string& v : contexts) {
+ const TfToken jobContext(v);
+ const MaxUsdJobContextRegistry::ContextInfo& ci
+ = MaxUsdJobContextRegistry::GetJobContextInfo(jobContext);
+ auto enablerCallback = isExport ? ci.exportEnablerCallback : ci.importEnablerCallback;
+ if (enablerCallback) {
+ VtDictionary extraArgs = enablerCallback();
+ // Add the job context name to the args (for reference when merging):
+ VtDictionary::iterator jobContextNamesIt = extraArgs.find(jcKey);
+ if (jobContextNamesIt != extraArgs.end()) {
+ // We already have a vector. Ensure it is of size 1 and contains only the
+ // current context name:
+ const std::vector& currContextNames
+ = VtDictionaryGet>(extraArgs, jcKey);
+ if ((currContextNames.size() == 1 && currContextNames.front() != v)
+ || currContextNames.size() > 1) {
+ TF_RUNTIME_ERROR(TfStringPrintf(
+ "Arguments for job context '%s' can not include extra contexts.",
+ jobContext.GetText()));
+ canMergeContexts = false;
+ }
+ }
+ std::vector jobContextNames;
+ jobContextNames.push_back(VtValue(v));
+ extraArgs[jcKey] = jobContextNames;
+ contextArgs.push_back(extraArgs);
+ } else {
+ MaxUsd::Log::Warn("Ignoring unknown job context '{0}'.", jobContext.GetText());
+ }
+ }
+ // Convenience map holding the jobContext that first introduces an argument to the final
+ // dictionary. Allows printing meaningful error messages.
+ std::map argInitialSource;
+ // Traverse argument dictionaries and look for merge conflicts while building the returned
+ // allContextArgs.
+ for (auto const& dict : contextArgs) {
+ // We made sure the value exists in the above loop, so we can fetch without fear:
+ const std::string& sourceName = VtDictionaryGet>(dict, jcKey)
+ .front()
+ .UncheckedGet();
+ for (auto const& dictTuple : dict) {
+ const std::string& k = dictTuple.first;
+ const VtValue& v = dictTuple.second;
+ auto allContextIt = allContextArgs.find(k);
+ if (allContextIt == allContextArgs.end()) {
+ // First time we see this argument. Store and remember source.
+
+ // special treatment on a deprecated base option ('chaser')
+ if (k == MaxUsdSceneBuilderOptionsTokens->chaser) {
+ TF_WARN(TfStringPrintf(
+ "Deprecated option key '%s' was found. Key shoud be replaced with '%s' unless otherwise required.",
+ MaxUsdSceneBuilderOptionsTokens->chaser,
+ MaxUsdSceneBuilderOptionsTokens->chaserNames));
+ }
+
+ allContextArgs[k] = v;
+ argInitialSource[k] = sourceName;
+ } else {
+ // We have already seen this argument from another jobContext. Look for conflicts:
+ const VtValue& allContextValue = allContextIt->second;
+ if (allContextValue.IsHolding>()) {
+ if (v.IsHolding>()) {
+ // We merge arrays:
+ std::vector mergedValues
+ = allContextValue.UncheckedGet>();
+ for (const VtValue& element : v.UncheckedGet>()) {
+ if (element.IsHolding>()) {
+ // vector> is common for chaserArgs and shadingModes
+ auto findElement = [&element](const VtValue& a) {
+ // make this comparison simple for now
+ return element == a;
+ };
+ if (std::find_if(
+ mergedValues.begin(), mergedValues.end(), findElement)
+ == mergedValues.end()) {
+ mergedValues.push_back(element);
+ }
+ } else {
+ if (std::find(mergedValues.begin(), mergedValues.end(), element)
+ == mergedValues.end()) {
+ mergedValues.push_back(element);
+ }
+ }
+ }
+ allContextArgs[k] = mergedValues;
+ } else {
+ // We have both an array and a scalar under the same argument name.
+ TF_RUNTIME_ERROR(TfStringPrintf(
+ "Context '%s' and context '%s' do not agree on type of argument '%s'.",
+ sourceName.c_str(),
+ argInitialSource[k].c_str(),
+ k.c_str()));
+ canMergeContexts = false;
+ }
+ } else {
+ // Scalar value already exists. Check for value conflicts:
+ if (allContextValue != v) {
+ TF_RUNTIME_ERROR(TfStringPrintf(
+ "Context '%s' and context '%s' do not agree on argument '%s'.",
+ sourceName.c_str(),
+ argInitialSource[k].c_str(),
+ k.c_str()));
+ canMergeContexts = false;
+ }
+ }
+ }
+ }
+ }
+ return canMergeContexts;
+}
+
+std::map
+ExtractChaserArgs(const pxr::VtDictionary& userArgs, const pxr::TfToken& key)
+{
+ const std::vector> chaserArgs
+ = DictUtils::ExtractVector>(userArgs, key);
+
+ std::map result;
+ for (const std::vector& argTriple : chaserArgs) {
+ if (argTriple.size() != 3) {
+ TF_CODING_ERROR("Each chaser arg must be a triple (chaser, arg, value)");
+ return std::map();
+ }
+
+ const std::string& chaser = argTriple[0].Get();
+ const std::string& arg = argTriple[1].Get();
+ const std::string& value = argTriple[2].Get();
+
+ // any conflicts present
+ auto chaserArgs = result.find(chaser);
+ if (chaserArgs == result.end()) {
+ result[chaser][arg] = value;
+ } else {
+ auto chaserArg = chaserArgs->second.find(arg);
+ if (chaserArg == chaserArgs->second.end()) {
+ chaserArgs->second[arg] = value;
+ } else {
+ if (chaserArg->second != value) {
+ // keep the argument value from the first context to use that argument, and
+ // forget the other values
+ TF_WARN(TfStringPrintf(
+ "Multiple argument value for '%s' associated to chaser '%s'. Keeping value "
+ "set to '%s'.",
+ arg.c_str(),
+ chaser.c_str(),
+ chaserArg->second));
+ }
+ }
+ }
+ }
+ return result;
+}
+
+const Log::Options& SceneBuilderOptions::GetLogOptions() const
+{
+ static Log::Options logOptions;
+ logOptions.level = static_cast(
+ VtDictionaryGet(options, MaxUsdSceneBuilderOptionsTokens->logLevel));
+ logOptions.path = VtDictionaryGet(options, MaxUsdSceneBuilderOptionsTokens->logPath);
+ return logOptions;
+}
+
+void SceneBuilderOptions::SetLogOptions(const Log::Options& logOptions)
+{
+ options[MaxUsdSceneBuilderOptionsTokens->logLevel] = static_cast(logOptions.level);
+ options[MaxUsdSceneBuilderOptionsTokens->logPath] = logOptions.path;
+}
+
+void SceneBuilderOptions::SetLogPath(fs::path logPath)
+{
+ options[MaxUsdSceneBuilderOptionsTokens->logPath] = logPath;
+}
+
+const fs::path& SceneBuilderOptions::GetLogPath() const
+{
+ return VtDictionaryGet(options, MaxUsdSceneBuilderOptionsTokens->logPath);
+}
+
+void SceneBuilderOptions::SetLogLevel(Log::Level logLevel)
+{
+ options[MaxUsdSceneBuilderOptionsTokens->logLevel] = static_cast(logLevel);
+}
+
+Log::Level SceneBuilderOptions::GetLogLevel() const
+{
+ return static_cast(
+ VtDictionaryGet(options, MaxUsdSceneBuilderOptionsTokens->logLevel));
+}
+
+const pxr::VtDictionary&
+SceneBuilderOptions::GetJobContextOptions(const pxr::TfToken& jobContext) const
+{
+ auto jobCtxOpts = VtDictionaryGet(
+ options, MaxUsdSceneBuilderOptionsTokens->jobContextOptions);
+ if (VtDictionaryIsHolding(jobCtxOpts, jobContext)) {
+ return VtDictionaryGet(jobCtxOpts, jobContext);
+ }
+
+ static const VtDictionary emptyDict;
+ return emptyDict;
+}
+
+void SceneBuilderOptions::SetJobContextOptions(
+ const pxr::TfToken& jobContext,
+ const pxr::VtDictionary& ctxOptions)
+{
+ if (VtDictionaryIsHolding(
+ options, MaxUsdSceneBuilderOptionsTokens->jobContextOptions)) {
+ std::string key = MaxUsdSceneBuilderOptionsTokens->jobContextOptions.GetString() + ":"
+ + jobContext.GetString();
+ options.SetValueAtPath(key, VtValue(ctxOptions));
+ }
+}
+
+const std::vector& SceneBuilderOptions::GetChaserNames() const
+{
+ return VtDictionaryGet>(
+ options, MaxUsdSceneBuilderOptionsTokens->chaserNames);
+}
+
+void SceneBuilderOptions::SetChaserNames(const std::vector& chasers)
+{
+ options[MaxUsdSceneBuilderOptionsTokens->chaserNames] = chasers;
+}
+
+const std::map&
+SceneBuilderOptions::GetAllChaserArgs() const
+{
+ return VtDictionaryGet>(
+ options, MaxUsdSceneBuilderOptionsTokens->chaserArgs);
+}
+
+void SceneBuilderOptions::SetAllChaserArgs(const std::map& chaserArgs)
+{
+ options[MaxUsdSceneBuilderOptionsTokens->chaserArgs] = chaserArgs;
+}
+
+void SceneBuilderOptions::SetContextNames(const std::set contexts)
+{
+ options[MaxUsdSceneBuilderOptionsTokens->contextNames] = contexts;
+}
+
+const std::set& SceneBuilderOptions::GetContextNames() const
+{
+ return VtDictionaryGet>(
+ options, MaxUsdSceneBuilderOptionsTokens->contextNames);
+}
+
+} // namespace MAXUSD_NS_DEF
\ No newline at end of file
diff --git a/src/MaxUsd/Builders/SceneBuilderOptions.h b/src/MaxUsd/Builders/SceneBuilderOptions.h
new file mode 100644
index 0000000..7f5f4f4
--- /dev/null
+++ b/src/MaxUsd/Builders/SceneBuilderOptions.h
@@ -0,0 +1,166 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include
+#include
+
+#include
+#include
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+// clang-format off
+#define PXR_MAXUSD_SCENE_BUILDER_OPTIONS_TOKENS \
+ /* Dictionary keys */ \
+ (convertMaterialsTo) \
+ (contextNames) \
+ (jobContext) \
+ (jobContextOptions) \
+ (chaserNames) \
+ /* 'chaser' is a deprecated option replaced with 'chaserNames' */ \
+ (chaser) \
+ (chaserArgs) \
+ /* Log Options */ \
+ (logPath) \
+ (logLevel)
+
+// clang-format on
+
+TF_DECLARE_PUBLIC_TOKENS(
+ MaxUsdSceneBuilderOptionsTokens,
+ MaxUSDAPI,
+ PXR_MAXUSD_SCENE_BUILDER_OPTIONS_TOKENS);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+namespace MAXUSD_NS_DEF {
+
+// \brief Merges all the jobContext arguments dictionaries found while exploring the jobContexts into a
+// single one. Also checks for conflicts and errors.
+//
+// \param[in] isExport if we are calling the import or the export jobContext callback.
+// \param[in] contexts jobContexts to merge.
+//
+// \param[out] allContextArgs dictionary of all extra jobContext arguments merged together.
+// \return true if the merge was successful, false if a conflict or an error was detected.
+MaxUSDAPI bool MergeJobContexts(
+ bool isExport,
+ const std::set& contexts,
+ pxr::VtDictionary& allContextArgs);
+
+// The chaser args are stored as vectors of vectors (since this is how you
+// would need to pass them in the Max Python API). Convert this to a
+// map of maps.
+MaxUSDAPI std::map>
+ ExtractChaserArgs(const pxr::VtDictionary& userArgs, const pxr::TfToken& key);
+
+/**
+ * \brief Class for getting and setting builder options.
+ */
+class SceneBuilderOptions : public DictionaryOptionProvider
+{
+public:
+ typedef std::map ChaserArgs;
+
+ MaxUSDAPI virtual ~SceneBuilderOptions() = default;
+
+ /**
+ * \brief Returns the builder's logging options.
+ * \return The logging options.
+ */
+ MaxUSDAPI const Log::Options& GetLogOptions() const;
+
+ /**
+ * \brief Sets the logging options for the builder.
+ * \param logOptions The logging options to set.
+ */
+ MaxUSDAPI void SetLogOptions(const Log::Options& logOptions);
+
+ /**
+ * \brief Sets the log path.
+ * \param logPath The log path.
+ */
+ MaxUSDAPI void SetLogPath(fs::path logPath);
+ /**
+ * \brief Returns the log path.
+ * \return The log's path.
+ */
+ MaxUSDAPI const fs::path& GetLogPath() const;
+ /**
+ * \brief Sets the log level.
+ * \param logLevel The log level.
+ */
+ MaxUSDAPI void SetLogLevel(MaxUsd::Log::Level logLevel);
+ /**
+ * \brief Returns the logging level (info, warn, error, etc.)
+ * \return The log level.
+ */
+ MaxUSDAPI MaxUsd::Log::Level GetLogLevel() const;
+
+ /**
+ * \brief Gets the list of export chasers to be called at USD export
+ * \return All export chasers to be called at USD export
+ */
+ MaxUSDAPI const std::vector& GetChaserNames() const;
+
+ /**
+ * \brief Sets the chaser list to use at export
+ * \param chasers The list of export chaser to call at export
+ */
+ MaxUSDAPI void SetChaserNames(const std::vector& chasers);
+
+ /**
+ * \brief Gets the map of export chasers with their specified arguments
+ * \return The map of export chasers with their specified arguments
+ */
+ MaxUSDAPI const std::map& GetAllChaserArgs() const;
+
+ /**
+ * \brief Sets the export chasers' arguments map
+ * \param chaserArgs a map of arguments
+ */
+ MaxUSDAPI void SetAllChaserArgs(const std::map& chaserArgs);
+
+ /**
+ * \brief Sets the context list to use at export
+ * \param contexts The list of context to apply at export
+ */
+ MaxUSDAPI void SetContextNames(const std::set contexts);
+
+ /**
+ * \brief Gets the list of contexts (plug-in configurations) to be applied on USD export
+ * \return All contexts names to be applied on USD export
+ */
+ MaxUSDAPI const std::set& GetContextNames() const;
+
+ /**
+ * \brief Get the dictionary holding the options for the given job context.
+ * \param jobContext The job context to get the options for.
+ * \return The dictionary holding the options for the given job context.
+ */
+ MaxUSDAPI const pxr::VtDictionary& GetJobContextOptions(const pxr::TfToken& jobContext) const;
+
+ /**
+ * \brief Set the options for the given job context.
+ * \param jobContext The job context to set the options for.
+ * \param ctxOptions The dictionary options to set.
+ */
+ MaxUSDAPI void
+ SetJobContextOptions(const pxr::TfToken& jobContext, const pxr::VtDictionary& options);
+};
+
+} // namespace MAXUSD_NS_DEF
diff --git a/src/MaxUsd/Builders/USDSceneBuilder.cpp b/src/MaxUsd/Builders/USDSceneBuilder.cpp
new file mode 100644
index 0000000..6995e42
--- /dev/null
+++ b/src/MaxUsd/Builders/USDSceneBuilder.cpp
@@ -0,0 +1,1235 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include "USDSceneBuilder.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace MAXUSD_NS_DEF {
+
+USDSceneBuilder::USDSceneBuilder() = default;
+
+pxr::UsdStageRefPtr USDSceneBuilder::Build(
+ const USDSceneBuilderOptions& buildOptions,
+ bool& cancelled,
+ const fs::path& filename,
+ std::map& editedLayers,
+ bool isUSDZ = false)
+{
+ pxr::UsdStageRefPtr stage = pxr::UsdStage::CreateInMemory();
+
+ auto& exportOptions = const_cast(buildOptions);
+
+ // Create the write job context - used for shader and prim writers.
+ // Also resolve the token that can be in the MaterialLayerPath, so that the full path can be
+ // used by the writers through the context.
+ pxr::MaxUsdWriteJobContext writeJobContext { stage, filename.string(), exportOptions, isUSDZ };
+ exportOptions.SetMaterialLayerPath(
+ writeJobContext.ResolveString(buildOptions.GetMaterialLayerPath()));
+
+ if (buildOptions.GetUseSeparateMaterialLayer()) {
+ const auto matFilePath
+ = USDCore::sanitizedFilename(buildOptions.GetMaterialLayerPath(), ".usda");
+ if (matFilePath.empty()) {
+ MaxUsd::Log::Error(
+ "Invalid material layer path: {0}", buildOptions.GetMaterialLayerPath());
+ } else if (matFilePath.extension() == ".usdz") {
+ MaxUsd::Log::Error(
+ "Invalid material layer path: {0}. USDZ is not a valid file format for material "
+ "layers.",
+ buildOptions.GetMaterialLayerPath());
+ } else {
+ auto ext = pxr::SdfFileFormat::FindByExtension(buildOptions.GetMaterialLayerPath());
+ const auto identifier = matFilePath.string();
+
+ // The layer could already be in memory...(previous version loaded in a stage)
+ const auto matLayer = MaxUsd::CreateOrOverwriteLayer(ext, identifier);
+ if (matLayer) {
+ writeJobContext.AddUsedLayerIdentifier(matFilePath.string(), matLayer);
+ } else {
+ MaxUsd::Log::Error("Material Layer for {} failed to be created", identifier);
+ }
+ }
+ }
+
+ // Insert the stage in the global cache for the time of the export. Useful so it can be accessed
+ // from callbacks. Removed from the cache using RAII.
+ const MaxUsd::StageCacheScopeGuard stageCacheGuard { stage };
+
+ pxr::TfToken upAxis;
+ if (buildOptions.GetUpAxis() == USDSceneBuilderOptions::UpAxis::Y) {
+ upAxis = pxr::UsdGeomTokens->y;
+ } else {
+ upAxis = pxr::UsdGeomTokens->z;
+ }
+
+ pxr::UsdGeomSetStageUpAxis(stage, upAxis);
+
+ // Export units setup
+ double stageScale = GetSystemUnitScale(UNITS_METERS);
+ // round float imprecision
+ stageScale = MaxUsd::MathUtils::RoundToSignificantDigit(
+ stageScale, std::numeric_limits::digits10);
+ pxr::UsdGeomSetStageMetersPerUnit(stage, stageScale);
+
+ INode* rootNode = coreInterface->GetRootNode();
+ if (rootNode == nullptr) {
+ return stage;
+ }
+
+ const auto timeConfig = buildOptions.GetResolvedTimeConfig();
+ if (timeConfig.IsAnimated()) {
+ stage->SetStartTimeCode(timeConfig.GetStartFrame());
+ stage->SetEndTimeCode(timeConfig.GetEndFrame());
+ // In 3dsMax, one tick is defined as 1/4800th of a second.
+ const auto maxFramePerSecond = (4800.0 / double(GetTicksPerFrame()));
+ stage->SetTimeCodesPerSecond(maxFramePerSecond);
+ // Typically the FramePerSeconds and TimeCodePerSeconds are equal, although they
+ // don't need necessarily need to be. According to the docs, FramePerSecond
+ // "makes an advisory statement about how the contained data can be most usefully
+ // consumed and presented. It's primarily an indication of the expected playback rate
+ // for the data, but a timeline editing tool might also want to use this to decide
+ // how to scale and label its timeline."
+ stage->SetFramesPerSecond(maxFramePerSecond);
+ }
+
+ // If we are not exporting the whole scene, build the set of the nodes to export, for easy
+ // access later.
+ nodesToExportSet.clear();
+ if (buildOptions.GetContentSource() == USDSceneBuilderOptions::ContentSource::NodeList) {
+ const auto nodesToExport = buildOptions.GetNodesToExport();
+ for (int i = 0; i < nodesToExport.Count(); ++i) {
+ nodesToExportSet.emplace(nodesToExport[i]);
+ }
+ } else if (
+ buildOptions.GetContentSource() == USDSceneBuilderOptions::ContentSource::Selection) {
+ for (int i = 0; i < GetCOREInterface()->GetSelNodeCount(); ++i) {
+ nodesToExportSet.emplace(GetCOREInterface()->GetSelNode(i));
+ }
+ }
+
+ // If we are only exporting a set of nodes, and it is empty, we are done!
+ // In practice, a user would most likely get stopped before reaching this point if trying to
+ // export from an empty selection or an empty list of nodes.
+ if (buildOptions.GetContentSource() != USDSceneBuilderOptions::ContentSource::RootNode
+ && nodesToExportSet.empty()) {
+ return stage;
+ }
+
+ MaxUsd::UniqueNameGenerator primNameGenerator;
+
+ // Keep track of each prim's source node. To be able to notify users if needed.
+ pxr::MaxUsdExportChaserRegistry::FactoryContext::PrimToNodeMap primsToNodes;
+ pxr::TfHashSet primsToMaterialBind;
+
+ static const std::wstring progressBarTitle = GetString(IDS_EXPORT_PROGRESS_TITLE);
+ MaxProgressBar progressBar(progressBarTitle.c_str());
+ progressBar.SetEnabled(buildOptions.GetUseProgressBar());
+ static const std::wstring completionMsg = GetString(IDS_EXPORT_PROGRESS_COMPLETED_MESSAGE);
+ const auto progressScopeGuard = MaxUsd::MakeScopeGuard(
+ [&progressBar]() { progressBar.Start(); },
+ [&progressBar]() { progressBar.Stop(false, completionMsg.c_str()); });
+
+ if (!BuildStageFromMaxNodes(writeJobContext, primsToNodes, primsToMaterialBind, progressBar)) {
+ // export was cancelled
+ cancelled = true;
+ stage.Reset();
+ return stage;
+ }
+
+ // Set the first valid prim as default prim.
+ if (!stage->HasDefaultPrim()) {
+ pxr::UsdPrimSiblingRange allChildren
+ = stage->GetPrimAtPath(pxr::SdfPath::AbsoluteRootPath()).GetAllChildren();
+
+ for (const auto& prim : allChildren) {
+ // Only prims which originate from 3dsMax nodes should be used as default prims.
+ // For example, prototype prims (classes), should not be considered.
+ if (primsToNodes.find(prim.GetPath()) == primsToNodes.end() || !prim.IsValid()) {
+ continue;
+ }
+ stage->SetDefaultPrim(prim);
+ break;
+ }
+ }
+
+ if (buildOptions.GetTranslateMaterials()) {
+ pxr::MaxUsdTranslatorMaterial::ExportMaterials(
+ writeJobContext, primsToMaterialBind, progressBar);
+ }
+
+ // Report that we are running chasers...
+ progressBar.UpdateProgress(
+ progressBar.GetTotal(), false, GetString(IDS_EXPORT_CHASERS_PROGRESS_MESSAGE));
+
+ // call chasers
+ // populate the chasers and run post export
+ std::vector> chasers;
+ pxr::MaxUsdExportChaserRegistry::FactoryContext ctx(
+ stage, primsToNodes, buildOptions, filename);
+ // for available chasers to load if not done already
+ pxr::MaxUsdExportChaserRegistry::GetAllRegisteredChasers();
+ for (const std::string& chaserName : buildOptions.GetChaserNames()) {
+ if (pxr::MaxUsdExportChaserRefPtr fn
+ = pxr::MaxUsdExportChaserRegistry::Create(chaserName, ctx)) {
+ chasers.push_back(std::make_pair(chaserName, fn));
+ } else {
+ MaxUsd::Log::Error("Failed to create chaser: {0}", chaserName.c_str());
+ }
+ }
+
+ for (const auto& chaser : chasers) {
+ if (chaser.second->PostExport()) {
+ MaxUsd::Log::Info("Successfully executed PostExport() for {0}", chaser.first);
+ } else {
+ MaxUsd::Log::Error("Failed executing PostExport() for {0}", chaser.first);
+ }
+ }
+
+ editedLayers = writeJobContext.GetLayerMap();
+
+ return stage;
+}
+
+struct NodeToExportStackItem
+{
+ INode* nodeToExport;
+ std::shared_ptr nameGenerator;
+ pxr::SdfPath parentPrimPath;
+ // Keep track of the highest ancester in the hierarchy which is hidden.
+ // If none, this wil remain null.
+ INode* hiddenAncestor;
+};
+
+int USDSceneBuilder::GetNumberOfNodeToExport(const USDSceneBuilderOptions& buildOptions)
+{
+ switch (buildOptions.GetContentSource()) {
+ case USDSceneBuilderOptions::ContentSource::Selection: return coreInterface->GetSelNodeCount();
+ case USDSceneBuilderOptions::ContentSource::NodeList:
+ return buildOptions.GetNodesToExport().Count();
+ case USDSceneBuilderOptions::ContentSource::RootNode: return MaxUsd::GetSceneObjectCount();
+ default: DbgAssert(_T("Unhandled content source data type"));
+ }
+
+ return 0;
+}
+
+bool USDSceneBuilder::ReuseParentPrimForInstancing(const TranslationContext& context)
+{
+ if (context.parentPrimPath.IsAbsoluteRootPath()) {
+ return false;
+ }
+
+ INode* parentNode = context.node->GetParentNode();
+ if (parentNode == nullptr || parentNode->IsRootNode() || parentNode->NumberOfChildren() > 1) {
+ return false;
+ }
+
+ Object* parentObject = parentNode->EvalWorldState(context.timeConfig.GetStartTime()).obj;
+ if (parentObject->ClassID() != POINTHELPER_TYPE_ID) {
+ return false;
+ }
+
+ // Check for conflicts on the usd-metadata (usd_kind, usd_hidden, etc.). This meta-data is not
+ // animatable, so we can simply look for a conflict at the start frame.
+ const auto childObject = context.node->EvalWorldState(context.timeConfig.GetStartTime()).obj;
+ if (MaxUsd::MetaData::CheckForConflict(
+ childObject, parentObject, context.timeConfig.GetStartTime())) {
+ return false;
+ }
+
+ return true;
+}
+
+bool USDSceneBuilder::BuildStageFromMaxNodes(
+ pxr::MaxUsdWriteJobContext& writeJobContext,
+ pxr::MaxUsdExportChaserRegistry::FactoryContext::PrimToNodeMap& primToNodeMap,
+ pxr::TfHashSet& primsToMaterialBind,
+ MaxProgressBar& progress)
+{
+ std::map exportedNodesToPrims;
+ auto rootNamesGenerator = std::make_shared();
+
+ auto& buildOptions = writeJobContext.GetArgs();
+
+ std::stack nodeToExportStack;
+ auto pushNodeChildrensToExportStack =
+ [&nodeToExportStack, &buildOptions, this, &rootNamesGenerator](
+ INode* node, const pxr::SdfPath& parentPrimPath, INode* parentHiddenAncestor) {
+ // If exporting the selection or from a node list, nodes that don't have their parents
+ // selected end up being exported at the root level. They use the same name generator to
+ // ensure that their generated prim paths are unique.
+ std::shared_ptr nameGenerator;
+ if (!nodesToExportSet.empty()
+ && (node->IsRootNode() || nodesToExportSet.find(node) == nodesToExportSet.end())) {
+ nameGenerator = rootNamesGenerator;
+ } else {
+ nameGenerator = std::make_shared();
+ }
+
+ std::unordered_set names;
+ std::list children;
+ std::list childrenWithNameConflict;
+
+ for (int i = 0; i < node->NumberOfChildren(); ++i) {
+ const auto child = node->GetChildNode(i);
+ const auto name = child->GetName();
+ // No conflict -> add to the front of the list.
+ if (names.find(name) == names.end()) {
+ names.emplace(name);
+ children.emplace_front(child);
+ continue;
+ }
+ // Conflict -> add to the end of the list.
+ childrenWithNameConflict.emplace_front(child);
+ }
+
+ // As we go down the hierarchy, we want to keep track if an ancestor in the
+ // node's hierarchy was hidden so we can warn users in case we cannot export
+ // the visibility correctly, which may be the case as in USD, visibility is
+ // inherited, whereas it is not in 3dsMax. If we are not using USD visibility
+ // to match the max node's hidden state, no need to track this. As no USD prim
+ // will be flagged as invisible by the exporter.
+ INode* hiddenAncestor = nullptr;
+ if (buildOptions.GetUseUSDVisibility()) {
+ if (parentHiddenAncestor) {
+ hiddenAncestor = parentHiddenAncestor;
+ } else {
+ if (node->IsNodeHidden()) {
+ hiddenAncestor = node;
+ }
+ }
+ }
+
+ // If the name was already used by a sibling, we must use another. We do not need to
+ // worry about "stealing" the name of a sibling not traversed yet, as we made sure to
+ // process siblings with conflicting names at the very end by pushing them deeper in the
+ // stack of node to be exported.
+ for (auto it = childrenWithNameConflict.begin(); it != childrenWithNameConflict.end();
+ ++it) {
+ NodeToExportStackItem nodeStackItem {
+ *it, nameGenerator, parentPrimPath, hiddenAncestor
+ };
+ nodeToExportStack.push(nodeStackItem);
+ }
+
+ for (auto it = children.begin(); it != children.end(); ++it) {
+ NodeToExportStackItem nodeStackItem {
+ *it, nameGenerator, parentPrimPath, hiddenAncestor
+ };
+ nodeToExportStack.push(nodeStackItem);
+ }
+ };
+
+ // additional operations to perform on translated prims
+ std::vector primConfigurators;
+ // 3ds Max Usd Custom Attributes to USD Attributes
+ primConfigurators.emplace_back(
+ [this](Object* object) { return true; },
+ [this](const TranslationContext& context, pxr::UsdPrim& translatedPrim) {
+ ConfigureUsdAttributes(context, translatedPrim);
+ ConfigureKind(context.node, translatedPrim);
+ });
+
+ const int numberOfItemToExport = GetNumberOfNodeToExport(buildOptions);
+
+ pxr::SdfPath rootPath(buildOptions.GetRootPrimPath());
+ auto stage = writeJobContext.GetUsdStage();
+
+ // Unless the absolute root path is used ("/"), we want the root prim to be specified as a
+ // Scope.
+ if (!rootPath.IsAbsoluteRootPath()) {
+ MaxUsd::FetchOrCreatePrim(
+ stage, rootPath, pxr::MaxUsdPrimTypeTokens->Xform);
+ // The top level primitive from that path should be our default prim.
+ auto path = rootPath;
+ while (path.GetParentPath() != pxr::SdfPath::AbsoluteRootPath()) {
+ path = path.GetParentPath();
+ }
+ stage->SetDefaultPrim(stage->GetPrimAtPath(path));
+ }
+
+ const auto timeConfig = buildOptions.GetResolvedTimeConfig();
+ if (buildOptions.GetTimeMode() == USDSceneBuilderOptions::TimeMode::CurrentFrame
+ && !MaxUsd::MathUtils::IsAlmostZero(static_cast(timeConfig.GetStartFrame()))) {
+ MaxUsd::Log::Warn("The export TimeMode is configured as #current, the specified StartFrame "
+ "will be ignored.");
+ }
+
+ if (buildOptions.GetTimeMode() == USDSceneBuilderOptions::TimeMode::FrameRange
+ && buildOptions.GetEndFrame() < buildOptions.GetStartFrame()) {
+ MaxUsd::Log::Warn(
+ "A frame range is exported, but the endFrame is smaller than the startFrame, only the "
+ "startFrame will be exported.");
+ }
+
+ // 1) Export preview pass :
+ // Traverse the 3dsMax scene and export the nodes in "preview" mode. This only simulates the
+ // export, figuring out what USD Prims will be created at what paths.
+
+ PrepareExportPass();
+ // Used to collect all the USD prims that will be needed to export the scene.
+ MaxUsd::PrimDefVector scenePrimDefs;
+ scenePrimDefs.reserve(
+ numberOfItemToExport); // May get a bit bigger if nodes need multiple prims.
+
+ // Used to collect all translations that we will need to perform to export everything in the
+ // scene. Basically a Max node, and where it needs to be exported in the USD Hierarchy.
+ std::vector translationItems;
+
+ // Traverse the 3dsMax scene, breadth first.
+ std::unordered_set hiddenAncestorsWithVisibleDescendants;
+ INode* nodeToConvert = coreInterface->GetRootNode();
+ pushNodeChildrensToExportStack(nodeToConvert, rootPath, nullptr);
+ while (!nodeToExportStack.empty()) {
+ const NodeToExportStackItem& itemToExport = nodeToExportStack.top();
+ nodeToConvert = itemToExport.nodeToExport;
+
+ // Warn if visibility cannot be exported correctly (visibility is inherited in USD but not
+ // in 3dsMax). Make sure to only warn once per problematic hierarchy.
+ if (itemToExport.hiddenAncestor && !nodeToConvert->IsNodeHidden()
+ && hiddenAncestorsWithVisibleDescendants.find(itemToExport.hiddenAncestor)
+ == hiddenAncestorsWithVisibleDescendants.end()) {
+ MaxUsd::Log::Warn(
+ L"Node {0} is hidden but has visible descendants. Because in USD visibility is "
+ L"inherited, this "
+ L"may lead to objects visible in 3dsMax being hidden in USD.",
+ itemToExport.hiddenAncestor->GetName());
+ hiddenAncestorsWithVisibleDescendants.insert(itemToExport.hiddenAncestor);
+ }
+
+ pxr::SdfPath nodeRootPrimPath;
+ MaxUsd::PrimDefVectorPtr nodePrimSpecs;
+
+ // Check if the node should be excluded from export. If nodesToExportSet is empty, it means
+ // we want to export the entire scene.
+ bool excludeNode = !nodesToExportSet.empty()
+ && nodesToExportSet.find(nodeToConvert) == nodesToExportSet.end();
+
+ const std::string primName
+ = pxr::TfMakeValidIdentifier(MaxUsd::MaxStringToUsdString(nodeToConvert->GetName()));
+ const std::string uniquePrimName = itemToExport.nameGenerator->GetName(primName);
+ if (primName != uniquePrimName) {
+ MaxUsd::Log::Warn(
+ L"Found node name conflict, exporting node {0} as {1} instead.",
+ nodeToConvert->GetName(),
+ MaxUsd::UsdStringToMaxString(uniquePrimName).data());
+ }
+
+ if (!excludeNode) {
+ // Preview the export of the node using the "preview" mode.
+ // Just figuring out what prims would get created.
+ TranslationContext translationContext {
+ nodeToConvert, stage, itemToExport.parentPrimPath, uniquePrimName, timeConfig,
+ true // Preview mode.
+ };
+
+ {
+ // Disable logging while exporting nodes in preview mode.
+ const auto scopeGuard = MaxUsd::MakeScopeGuard(
+ []() { MaxUsd::Log::Pause(); }, []() { MaxUsd::Log::Resume(); });
+
+ bool doMtlAssign;
+ MaxUsd::AnimExportTask animTask { buildOptions.GetResolvedTimeConfig() };
+ nodePrimSpecs = ProcessNode(
+ translationContext,
+ writeJobContext,
+ primConfigurators,
+ stage,
+ doMtlAssign,
+ animTask);
+ }
+
+ // Save this translation item for the second pass, which will actually export the node's
+ // data.
+ translationContext.preview = false;
+ translationItems.push_back(translationContext);
+
+ if (nodePrimSpecs && !nodePrimSpecs->empty()) {
+ scenePrimDefs.insert(
+ scenePrimDefs.end(), nodePrimSpecs->begin(), nodePrimSpecs->end());
+ // The first prim in the vector is the root prim for the node.
+ nodeRootPrimPath = nodePrimSpecs->front().path;
+ if (!nodeRootPrimPath.IsEmpty()) {
+ primToNodeMap.insert({ nodeRootPrimPath, nodeToConvert });
+ }
+ exportedNodesToPrims.emplace(nodeToConvert, nodeRootPrimPath);
+ }
+ }
+
+ // Remove the node we just exported from the stack
+ nodeToExportStack.pop();
+
+ // Push all the node childrens to the stack to be exported.
+ pushNodeChildrensToExportStack(
+ nodeToConvert,
+ nodeRootPrimPath.IsEmpty() ? rootPath : nodeRootPrimPath,
+ itemToExport.hiddenAncestor);
+ }
+
+ // 2) Prim creation pass :
+ // Create all Prims in a single pass. Using Sdf APIs and batching the creation of all prims
+ // in a single SdfChangeBlock speeds up the export considerably, as all notifications can be
+ // processed at the same time.
+ {
+ pxr::SdfChangeBlock primBatchCreate;
+ for (const auto& primSpec : scenePrimDefs) {
+ if (primSpec.path.IsEmpty()) {
+ continue;
+ }
+ auto prim = pxr::SdfCreatePrimInLayer(stage->GetRootLayer(), primSpec.path);
+ if (primSpec.type == pxr::MaxUsdPrimTypeTokens->Class) {
+ prim->SetSpecifier(pxr::SdfSpecifierClass);
+ prim->SetTypeName(primSpec.type);
+ } else if (primSpec.type == pxr::MaxUsdPrimTypeTokens->Over) {
+ prim->SetSpecifier(pxr::SdfSpecifierOver);
+ // No type name for "over" prims.
+ } else {
+ prim->SetSpecifier(pxr::SdfSpecifierDef);
+ prim->SetTypeName(primSpec.type);
+ }
+ }
+ }
+
+ // 3) Export pass :
+ // Populate the USD prim properties from the nodes' data.
+ // This is where most of the work happens, and where we perform the conversion
+ // of Max content to USD content.
+ writeJobContext.SetNodeToPrimMap(exportedNodesToPrims);
+
+ PrepareExportPass();
+
+ progress.SetTotal(numberOfItemToExport);
+
+ // As we process each node, we accumulate some work that we need to do for each node->prim
+ // translation. Namely, writing the prim attributes, and transforms. Everything else is setup
+ // right away as we process the node. The reason we need to delay the write of attributes and
+ // transforms is we want to evaluate all object and transforms at a time "t" at the same time,
+ // to benefit from 3dsmax's caching of the world state. AnimExportTask accepts work that needs
+ // to be run at a certain time, it then makes sure to batch all the work that needs to evaluate
+ // max data at the same time together.
+ MaxUsd::AnimExportTask timeSamplesExportTask { buildOptions.GetResolvedTimeConfig() };
+
+ const auto prepareExportProgressMsg = GetString(IDS_EXPORT_PREPARING_EXPORT);
+ int progressIndex = 0;
+ for (const auto& translationItem : translationItems) {
+ // Stop the import in its current state if the User chose to cancel it.
+ // NOTE: This will result in partially-loaded content, which may require additional handling
+ // to make sure the User understands that this may cause side-effects.
+ if (coreInterface->GetCancel()) {
+ MaxUsd::Log::Info("USD Export cancelled.");
+ return false;
+ }
+ bool doMtlAssign = false;
+ const auto prims = ProcessNode(
+ translationItem,
+ writeJobContext,
+ primConfigurators,
+ stage,
+ doMtlAssign,
+ timeSamplesExportTask);
+
+ // Report to the caller if the prim associated with this node should be considered for
+ // material assignment.
+ if (prims && !prims->empty()) {
+ // Two cases.
+ // 1) The node is not an instance, perform material on the node if ExportNode() tells us
+ // we should. 2) The node is an instance, ExportNode() will only return doAssignMaterial
+ // = true for the first instances (assuming the translated object should be assigned a
+ // material), as the prim writer is only queried / executed for the first instance.
+ // Subsequent instances just point to the already created class prim, and the prim
+ // writer is not involved. For this reason, in the case of instances, we keep track of
+ // the doAssignMaterial material returned for the first instances.
+
+ // A lambda to find the prototype (class) prim used by a given prim. The passed prim is
+ // assumed to be the top level prim for a node. Depending on the export scenario (object
+ // offset, xform split, etc. the instance prim can be that prim directly, or its first
+ // child.
+ auto findPrototypePrim = [this, stage](const pxr::SdfPath& primPath) {
+ const auto protoIt = instanceToPrototype.find(primPath);
+ if (protoIt != instanceToPrototype.end()) {
+ return protoIt->second;
+ }
+ const auto prim = stage->GetPrimAtPath(primPath);
+ const auto& children = prim.GetChildren();
+ if (!children.empty()) {
+ const auto& path = children.front().GetPath();
+ const auto it = instanceToPrototype.find(path);
+ if (it != instanceToPrototype.end()) {
+ return it->second;
+ }
+ }
+ // No instancing.
+ return pxr::SdfPath {};
+ };
+
+ const auto& nodeRoot = prims->front();
+ auto protoPrim = findPrototypePrim(nodeRoot.path);
+
+ // Handle instancing scenario.
+ if (!protoPrim.IsEmpty()) {
+ // First instance, store info that instances of this prototype should have materials
+ // assigned.
+ if (doMtlAssign) {
+ prototypeMaterialReq.insert(protoPrim);
+ } else {
+ const auto it = prototypeMaterialReq.find(protoPrim);
+ if (it != prototypeMaterialReq.end()) {
+ doMtlAssign = true;
+ }
+ }
+ }
+
+ if (doMtlAssign) {
+ primsToMaterialBind.insert(nodeRoot.path);
+ }
+ }
+
+ progress.UpdateProgress(++progressIndex, true, prepareExportProgressMsg);
+ }
+
+ // Export time sample data.
+ timeSamplesExportTask.Execute(progress);
+
+ // 4) Instancing setup pass :
+ // Set up instancing properties. This triggers stage notifications, which can dramatically
+ // slow down the export, for this reason, we wrap all these in a SdfChangeBlock, so that
+ // the stage can process all the notifications at the same time, quickly.
+ {
+ pxr::SdfChangeBlock instancingSetup;
+ for (const auto& pair : instanceToPrototype) {
+ auto prim = stage->GetPrimAtPath(pair.first);
+ prim.GetInherits().AddInherit(pair.second);
+ prim.SetInstanceable(true);
+ }
+ }
+
+ return true;
+}
+
+MaxUsd::PrimDefVectorPtr USDSceneBuilder::ProcessNode(
+ const TranslationContext& context,
+ const pxr::MaxUsdWriteJobContext& writeJobContext,
+ const std::vector& primConfigurators,
+ const pxr::UsdStageRefPtr& stage,
+ bool& doAssignMaterial,
+ MaxUsd::AnimExportTask& animExportTask)
+{
+ MaxUsd::PrimDefVectorPtr exportedPrims;
+
+ // Translate the node using the first matching operator:
+ bool translationHandled = false;
+
+ // Dummy object evaluation to work around an animation controller issue present in 3dsMax,
+ // some controllers will wrongly report an "instantaneous" interval the first time they are
+ // queried. This dummy evaluation might be wrong, but the next one will be correct. We only
+ // ever need to do this one, so do it here.
+ context.node->EvalWorldState(context.timeConfig.GetStartTime() - 1);
+ const auto object = context.node->EvalWorldState(context.timeConfig.GetStartTime()).obj;
+
+ const auto& buildOptions = writeJobContext.GetArgs();
+
+ // Check if we need to export the node, if it is hidden.
+ if (buildOptions.GetTranslateHidden() || !context.node->IsNodeHidden()) {
+ size_t numRegisteredWriters = 0;
+
+ auto primWriter = pxr::MaxUsdPrimWriterRegistry::FindWriter(
+ writeJobContext, context.node, numRegisteredWriters);
+
+ if (primWriter) {
+ if (numRegisteredWriters > 1) {
+ MaxUsd::Log::Info(
+ L"Multiple registered prim writers can support node {0}, using {1}.",
+ context.node->GetName(),
+ primWriter->GetWriterName().data());
+ }
+
+ const auto targetRootPath = buildOptions.GetRootPrimPath();
+
+ exportedPrims = WriteNodePrims(
+ [this, &primWriter, &doAssignMaterial, &animExportTask, &buildOptions](
+ const TranslationContext& context, bool applyOffsetTransform) {
+ // Ask the writer about the prim's type and name for this node.
+ const auto resolvedPrimName = pxr::TfToken(
+ pxr::TfMakeValidIdentifier(primWriter->GetPrimName(context.primName)));
+ const auto primType = primWriter->GetPrimType();
+
+ MaxUsd::PrimDef targetPrim {
+ context.parentPrimPath.AppendChild(resolvedPrimName), primType
+ };
+
+ // In preview, only interested to know where the prim will be exported, and what
+ // it's type is.
+ if (context.preview) {
+ return targetPrim;
+ }
+
+ // Get the prim that was created for us.
+ auto usdPrim = context.stage->GetPrimAtPath(targetPrim.path);
+ if (!usdPrim.IsValid()) {
+ MaxUsd::Log::Error(
+ L"Unable to write the 3dsMax node \"{0}\" to the prim at {1}. This "
+ L"prim is no "
+ L"longer valid. It may have been pruned by the actions of a prim "
+ L"writer.",
+ context.node->GetName(),
+ MaxUsd::UsdStringToMaxString(targetPrim.path.GetString()).data());
+ return targetPrim;
+ }
+
+ doAssignMaterial = primWriter->RequiresMaterialAssignment()
+ == MaxUsd::MaterialAssignRequirement::Default;
+
+ // Before we hand off the prim to the prim writer, apply the object offset
+ // transform. Do this now because if the prim writer needs to add a transform to
+ // the stack part of the object's translation, in most cases it will need to be
+ // added after the object offset, so we are making their job easier. If needed,
+ // the writer can specify that it wants to handle the object offset transform
+ // itself (for example we use this feature when baking the object offset
+ // transform into the geometry). Avoid adding object offset transforms for
+ // groups, special case.
+ const auto writerHandlesOffset = primWriter->HandlesObjectOffsetTransform();
+ if (!writerHandlesOffset && applyOffsetTransform
+ && !context.node->IsGroupHead()) {
+ // The "root" primitive object are exported to, should always be Xformable.
+ if (!usdPrim.IsA()) {
+ MaxUsd::Log::Error(
+ "The prim created for the node \"{0}\" is not an Xformable, "
+ "unable to apply the object offset.");
+ } else {
+ pxr::UsdGeomXformable xformable { usdPrim };
+ MaxUsd::ApplyObjectOffsetTransform(
+ context.node, xformable, context.timeConfig.GetStartTime());
+ }
+ }
+
+ // If the writer requested to handle the object offset transform, let it know if
+ // it should apply it or not part of this translation (for example, we never
+ // want to apply object offsets to instanced prims directly).
+ const auto requestApplyOffset = applyOffsetTransform && writerHandlesOffset;
+
+ auto node = context.node;
+
+ // Queue the work of writing the node properties to the prim - it will be
+ // batched with other translation operations needing to be done at the same
+ // 3dsMax time values (we figure this out from the validity intervals). This
+ // prevents us re-evaluating the same objects multiple times at the same time
+ // values.
+ animExportTask.AddObjectExportOp(
+ [primWriter](TimeValue time) {
+ return primWriter->GetValidityInterval(time);
+ },
+ [primWriter, node, usdPrim, requestApplyOffset](
+ const MaxUsd::ExportTime time) mutable {
+ if (!primWriter->Write(usdPrim, requestApplyOffset, time)) {
+ MaxUsd::Log::Error(
+ L"Failed to write the prim properties for {0} as time {1}.",
+ node->GetName(),
+ std::to_wstring(time.GetUsdTime().GetValue()));
+ }
+ },
+ [primWriter, node, usdPrim]() mutable {
+ if (!primWriter->PostExport(usdPrim)) {
+ MaxUsd::Log::Error(
+ L"Failed to execute post export for {0}.", node->GetName());
+ }
+ });
+
+ return targetPrim;
+ },
+ context,
+ primWriter->GetObjectPrimSuffix(),
+ primWriter->RequiresXformPrim(),
+ primWriter->RequiresInstancing(),
+ targetRootPath);
+
+ if (!exportedPrims->empty() && !exportedPrims->front().path.IsEmpty()) {
+ translationHandled = true;
+ }
+ }
+ }
+ // If the object type was not handled (either it is not supported or it is excluded
+ // from the export), we might still need to export it as an Xform if it has children
+ // so that any of its exported descendants will have the correct transforms.
+ if (!translationHandled) {
+ if (HasExportableDescendants(context.node, buildOptions)) {
+ MaxUsd::PrimDef primSpec
+ = { context.parentPrimPath.AppendChild(pxr::TfToken(context.primName)),
+ pxr::MaxUsdPrimTypeTokens->Xform };
+
+ exportedPrims = std::make_shared();
+ exportedPrims->push_back(primSpec);
+ if (context.preview) {
+ return exportedPrims;
+ }
+ MaxUsd::FetchOrCreatePrim(
+ stage, primSpec.path, pxr::MaxUsdPrimTypeTokens->Xform);
+ translationHandled = true;
+ MaxUsd::Log::Info(
+ L"Node {0} will be exported to a Xform prim. It is either excluded from export by "
+ L"configuration or unsupported, but it has exported descendants.",
+ context.node->GetName());
+ } else {
+ MaxUsd::Log::Info(
+ L"Node {0} will be skipped. It is either excluded from export by configuration or "
+ L"unsupported.",
+ context.node->GetName());
+ }
+ }
+
+ if (translationHandled) {
+ auto nodeRootPrimPath = exportedPrims->front().path;
+
+ pxr::UsdPrim prim = stage->GetPrimAtPath(nodeRootPrimPath);
+ if (prim.IsValid()) {
+ if (!prim.IsA()) {
+ MaxUsd::Log::Error(
+ L"The root primitive created for node {0} is not Xformable. Unable to apply "
+ L"the node's transform.",
+ context.node->GetName());
+ } else {
+
+ // Compute object transforms:
+ pxr::UsdGeomXformable xFormPrim(prim);
+
+ // Setup the USD visibility, from the Max node's hidden state, if requested.
+ if (context.node->IsNodeHidden() && buildOptions.GetUseUSDVisibility()) {
+ xFormPrim.MakeInvisible(pxr::UsdTimeCode::Default());
+ }
+
+ // Queue the work of writing the node's transform - it will be batched with other
+ // translation operations needing to be done at the same 3dsMax time values (we
+ // figure this out from the validity intervals). This prevents us re-evaluating the
+ // same objects multiple times at the same time values.
+ auto node = context.node;
+ animExportTask.AddTransformExportOp(
+ [&buildOptions, node, xFormPrim](
+ const MaxUsd::ExportTime& time, pxr::UsdGeomXformOp& usdGeomXFormOp) {
+ pxr::GfMatrix4d maxTransformMatrix
+ = MaxUsd::ToUsd(node->GetNodeTM(time.GetMaxTime()));
+ MaxUsd::MathUtils::RoundMatrixValues(
+ maxTransformMatrix, std::numeric_limits::digits10);
+
+ if (buildOptions.GetUpAxis() == USDSceneBuilderOptions::UpAxis::Y) {
+ MaxUsd::MathUtils::ModifyTransformZToYUp(maxTransformMatrix);
+ }
+
+ // Compute the local transform of the prim. The current transform in the
+ // hierarchy (i.e. the world transform of the parent).
+ pxr::GfMatrix4d parentWorldTransform;
+ parentWorldTransform.SetIdentity();
+
+ const auto parentNode = node->GetParentNode();
+ if (parentNode && !parentNode->IsRootNode()) {
+ parentWorldTransform = MaxUsd::GetNodeTransform(
+ parentNode,
+ time.GetMaxTime(),
+ buildOptions.GetUpAxis() == USDSceneBuilderOptions::UpAxis::Y);
+ }
+
+ // The parent transform must be invertible for us to be able to compute the
+ // local transform. A matrix with a non-zero determinant is invertible.
+ if (parentWorldTransform.GetDeterminant() != 0.0) {
+ pxr::GfMatrix4d transformMatrix
+ = maxTransformMatrix * parentWorldTransform.GetInverse();
+ // If exporting a single frame, no need to specify the transform if it
+ // is the identity. When exporting an animation, we need to, as the
+ // transform might change over time. If the frame at the identity was
+ // not exported, the transform at that frame would be interpolated from
+ // other authored frames, which would be wrong.
+ const auto timeConfig = buildOptions.GetResolvedTimeConfig();
+ if (!MaxUsd::MathUtils::IsIdentity(transformMatrix)
+ || timeConfig.IsAnimated()) {
+ bool resetsXformStack = false;
+ const size_t nbOfOps
+ = xFormPrim.GetOrderedXformOps(&resetsXformStack).size();
+ if (!usdGeomXFormOp.IsDefined()) {
+ usdGeomXFormOp = xFormPrim.AddXformOp(
+ pxr::UsdGeomXformOp::TypeTransform,
+ pxr::UsdGeomXformOp::PrecisionDouble,
+ nbOfOps > 0 ? pxr::TfToken("t" + std::to_string(nbOfOps))
+ : pxr::TfToken());
+ }
+ usdGeomXFormOp.Set(transformMatrix, time.GetUsdTime());
+ }
+ } else {
+ MaxUsd::Log::Error(
+ std::wstring(L"The parent prim of ") + node->GetName()
+ + std::wstring(L" has a non-invertible world transform matrix. "
+ L"Unable to compute its local transform at frame ")
+ + std::to_wstring(
+ double(time.GetMaxTime()) / double(GetTicksPerFrame())));
+ }
+ });
+ }
+
+ for (const auto& configuratorStep : primConfigurators) {
+ if (configuratorStep.AppliesToObject(object)) {
+ configuratorStep.Execute(context, prim);
+ break;
+ }
+ }
+ MaxUsd::Log::Info(
+ L"Exported node {0} to {1}.",
+ context.node->GetName(),
+ MaxUsd::UsdStringToMaxString(nodeRootPrimPath.GetString()).data());
+ }
+ }
+ return exportedPrims;
+}
+
+bool USDSceneBuilder::HasExportableDescendants(
+ INode* node,
+ const USDSceneBuilderOptions& buildOptions)
+{
+ // Check if we already have the answer in the cache.
+ const auto it = hasExportableDescendantsMap.find(node);
+ if (it != hasExportableDescendantsMap.end()) {
+ return it->second;
+ }
+
+ bool exportableHierarchy = false;
+ // If we are exporting from a node list, make sure the node should be considered.
+ if (nodesToExportSet.empty() || nodesToExportSet.find(node) != nodesToExportSet.end()) {
+ // Should the node be ignored because it is hidden?
+ if (!node->IsNodeHidden() || buildOptions.GetTranslateHidden()) {
+ // Check if any of the translation operations apply to the node's object. If so, it is
+ // considered exportable.
+ exportableHierarchy = pxr::MaxUsdPrimWriterRegistry::CanBeExported(node, buildOptions);
+ }
+ }
+
+ // If the given node itself is not exportable, we must check its children, we do so via
+ // recursion.
+ if (!exportableHierarchy) {
+ for (int i = 0; i < node->NumChildren(); ++i) {
+ auto childNode = node->GetChildNode(i);
+ // If exporting from the a node list, check if the child should be considered. If not,
+ // we must stop the recursion, as if any descendants are exported, they will be parented
+ // at the root, and so not part of the nodes sub-hierarchy on the USD side. Example :
+ // Node A (Selected)
+ // - Node B
+ // - Node C (Selected)
+ // Will export to :
+ // - Prim Node A
+ // - Prim Node C (same level as Node A in the USD hierarchy)
+ if (!nodesToExportSet.empty()
+ && nodesToExportSet.find(childNode) == nodesToExportSet.end()) {
+ continue;
+ }
+
+ if (HasExportableDescendants(childNode, buildOptions)) {
+ exportableHierarchy = true;
+ break;
+ }
+ }
+ }
+
+ hasExportableDescendantsMap.insert({ node, exportableHierarchy });
+ return exportableHierarchy;
+}
+
+MaxUsd::PrimDefVectorPtr USDSceneBuilder::WriteNodePrims(
+ std::function
+ createObjectPrim,
+ const TranslationContext& context,
+ const std::string& objectPrimSuffix,
+ const MaxUsd::XformSplitRequirement& xformRequirement,
+ const MaxUsd::InstancingRequirement& instancingRequirement,
+ const pxr::SdfPath& rootPrim)
+{
+ bool isInstanceableNode
+ = maxNodeToClassPrimMap.find(context.node) != maxNodeToClassPrimMap.end();
+
+ bool createdClass = false;
+ MaxUsd::PrimDef instanceObjectPrimSpec;
+ MaxUsd::PrimDef classPrimSpec;
+
+ INodeTab instanceNodes;
+
+ // Create the class prim if it is the first time we identify this node as being instanceable.
+ // i.e. The node was not previously in the map and we found other node that can be exported as
+ // instance of each other.
+ if (instancingRequirement == MaxUsd::InstancingRequirement::Default && !isInstanceableNode
+ && MaxUsd::FindInstanceableNodes(context.node, instanceNodes, nodesToExportSet)) {
+ isInstanceableNode = true;
+
+ std::string classPrimName
+ = "_class_" + classPrimBaseNameGenerator.GetName(context.primName);
+ const pxr::SdfPath classPrimPath = rootPrim.AppendChild(pxr::TfToken(classPrimName));
+
+ classPrimSpec = { classPrimPath, pxr::MaxUsdPrimTypeTokens->Class };
+
+ if (!context.preview) {
+ context.stage->CreateClassPrim(classPrimPath);
+ }
+
+ TranslationContext objectContext { context.node, context.stage, classPrimPath,
+ context.primName, context.timeConfig, context.preview };
+
+ instanceObjectPrimSpec = createObjectPrim(objectContext, false);
+
+ // Populate the map for every instance node we found to avoid searching later when we
+ // encounter them.
+ for (int i = 0; i < instanceNodes.Count(); i++) {
+ maxNodeToClassPrimMap.insert(
+ std::pair(instanceNodes[i], classPrimPath));
+ }
+
+ createdClass = true;
+ }
+
+ pxr::SdfPath currentPrimPath = context.parentPrimPath;
+ // This will keep track of all the prims that were created on export to represent the max node.
+ MaxUsd::PrimDefVectorPtr exportedPrimPaths = std::make_shared();
+ bool applyObjectPrimSuffix = false;
+
+ const Matrix3 offsetTransform = MaxUsd::GetMaxObjectOffsetTransform(context.node);
+
+ // Create a prim for the node's object if necessary. In some cases, it is possible to export
+ // a node and its object to a single USD prim. In others we need to keep them separate, most
+ // often because of the inheritance rules, indeed only the Node's transform should be inherited.
+ bool isIdentityOffset = MaxUsd::MathUtils::IsIdentity(offsetTransform);
+
+ // If a WSM is applied and the object is not at the identity transform, we might need to
+ // transform the geometry's points back into local space (with the inverse of the node's
+ // transform), so that with the inherited transforms from the USD hierarchy, the overall
+ // transforms of the points are correct.
+ const bool wsmTransformToLocal
+ = MaxUsd::WsmRequiresTransformToLocalSpace(context.node, context.timeConfig.GetStartTime());
+
+ // 1) Prim writers can specify their needs of having a dedicated xform to encode the node's
+ // transform. If the requirement from the writer is "ForOffsetObjects", two interesting cases :
+ // - If the offset is the identity, no need to create a prim for the offset.
+ // - If there is a WSM, the vertices are already transformed to world space, the offset already
+ // considered.
+ bool objectXformRequiredFromConfig = (xformRequirement == MaxUsd::XformSplitRequirement::Always)
+ || (xformRequirement == MaxUsd::XformSplitRequirement::ForOffsetObjects && !isIdentityOffset
+ && !wsmTransformToLocal);
+
+ // 2) When instancing a USD prim, it cannot have children, as they would be ignored. And we cant
+ // bake the offset into the geometry, as it is reused. therefore if there are any children, or
+ // if the offset is not the identity, we must create a separate prim for the object, so that it
+ // can be instanced.
+ bool objectXFormRequiredFromInstancing
+ = isInstanceableNode && (!isIdentityOffset || context.node->NumChildren() > 0);
+
+ // 3) Need an extra Xform for objects exported as guides. In USD, the purpose is inherited, so
+ // to avoid any children of the object exported as guides to also be set as guide, we use an
+ // extra XForm. Later in this function, some objects (for now, only bones) will be set as
+ // guides, to avoid them being rendered unless explicitly requested. Geometry set as
+ // non-renderable will also be set as guides.
+ bool isGuideObject
+ = MaxUsd::IsBoneObject(context.node->EvalWorldState(context.timeConfig.GetStartTime()).obj)
+ || context.node->Renderable() == 0;
+
+ if (objectXformRequiredFromConfig || objectXFormRequiredFromInstancing || isGuideObject) {
+ pxr::SdfPath xformPrimPath = currentPrimPath.AppendChild(pxr::TfToken(context.primName));
+
+ if (!context.preview) {
+ MaxUsd::FetchOrCreatePrim(
+ context.stage, xformPrimPath, pxr::MaxUsdPrimTypeTokens->Xform);
+ }
+
+ applyObjectPrimSuffix = true;
+ currentPrimPath = xformPrimPath;
+ exportedPrimPaths->push_back({ xformPrimPath, pxr::MaxUsdPrimTypeTokens->Xform });
+ }
+
+ // if it is an instanceable node, find or create the prim that will inherit from the class prim
+ if (isInstanceableNode) {
+ // Reuse the parent prim if no prim was created for object transform and we identify
+ // that the prim should be reused for instancing.
+ pxr::SdfPath instancePrimPath;
+
+ if (exportedPrimPaths->empty() && ReuseParentPrimForInstancing(context)) {
+ instancePrimPath = context.parentPrimPath;
+ } else {
+ instancePrimPath = currentPrimPath.AppendChild(pxr::TfToken(pxr::TfMakeValidIdentifier(
+ applyObjectPrimSuffix ? context.primName + "_" + objectPrimSuffix
+ : context.primName)));
+ }
+
+ exportedPrimPaths->push_back({ instancePrimPath, pxr::MaxUsdPrimTypeTokens->Xform });
+
+ pxr::SdfPath prototypePrimPath = maxNodeToClassPrimMap[context.node];
+
+ // If we created the class while exporting this instance, add it to the created prims
+ // following the order of the hierarchy.
+ if (createdClass) {
+ exportedPrimPaths->push_back(classPrimSpec);
+ exportedPrimPaths->push_back(instanceObjectPrimSpec);
+ }
+
+ if (context.preview) {
+ return exportedPrimPaths;
+ }
+
+ pxr::UsdGeomXformable xformable = MaxUsd::FetchOrCreatePrim(
+ context.stage, instancePrimPath, pxr::MaxUsdPrimTypeTokens->Xform);
+
+ // Postpone setting up the instanceable & inherit required on the prim for instancing to
+ // avoid triggering notifications on each instance. We will do this all at once within an
+ // SdfChangeBlock at the end of the export.
+ auto xformPath = xformable.GetPrim().GetPath();
+ instanceToPrototype.insert({ xformPath, prototypePrimPath });
+
+ // The timeValue passed is used to check for a WSM, in which case we would not need to apply
+ // an offset transform. Whether or not a WSM is applied is not animatable, so we can just
+ // consider it at the startFrame.
+ MaxUsd::ApplyObjectOffsetTransform(
+ context.node, xformable, context.timeConfig.GetStartTime());
+
+ // The root prim created for the node is at .front(). This prim will be where the node
+ // object transform will be applied. This should be the instance prim unless we had unbaked
+ // offset transform to manage. If this is the case, the front prim should be an xform prim
+ // with as children the instance prim which contains the offset transform.
+ return exportedPrimPaths;
+ }
+
+ TranslationContext objectContext {
+ context.node,
+ context.stage,
+ currentPrimPath,
+ pxr::TfMakeValidIdentifier(
+ applyObjectPrimSuffix ? context.primName + "_" + objectPrimSuffix : context.primName),
+ context.timeConfig,
+ context.preview
+ };
+
+ MaxUsd::PrimDef createdPrimPath = createObjectPrim(objectContext, true);
+
+ if (isGuideObject && !context.preview) {
+ const auto imageable
+ = pxr::UsdGeomImageable(context.stage->GetPrimAtPath(createdPrimPath.path));
+ imageable.CreatePurposeAttr().Set(pxr::UsdGeomTokens->guide);
+ }
+ exportedPrimPaths->push_back(createdPrimPath);
+
+ // The root prim created for the node is at .front(). This prim will be where the node object
+ // transform will be applied. This should be the prim created by createObjectPrim unless we had
+ // unbaked offset transform to manage. If this is the case, the front prim should be an xform
+ // prim with as children the prim created by createObjectPrim.
+ return exportedPrimPaths;
+}
+
+void USDSceneBuilder::PrepareExportPass()
+{
+ maxNodeToClassPrimMap.clear();
+ classPrimBaseNameGenerator.Reset();
+}
+
+void USDSceneBuilder::ConfigureUsdAttributes(
+ const TranslationContext& translationContext,
+ pxr::UsdPrim& translatedPrim)
+{
+ const auto object = translationContext.node->GetObjectRef();
+ bool hiddenFound = false, kindFound = false, purposeFound = false;
+ IParamBlock2* usdCustomAttributePb = nullptr;
+ // Get non-animatable metadata, we first look at all the modifiers from top to bottom
+ // until we find at least one of each of the attributes we're looking for.
+ if (object != nullptr && object->SuperClassID() == GEN_DERIVOB_CLASS_ID) {
+ const auto derivedObj = dynamic_cast(object);
+ for (int i = 0; i < derivedObj->NumModifiers(); i++) {
+ usdCustomAttributePb
+ = MaxUsd::MetaData::FindUsdCustomAttributeParamBlock(derivedObj->GetModifier(i));
+ if (usdCustomAttributePb != nullptr) {
+ if (!hiddenFound) {
+ hiddenFound = MaxUsd::SetPrimHiddenFromCA(usdCustomAttributePb, translatedPrim);
+ }
+ if (!kindFound) {
+ kindFound = MaxUsd::SetPrimKindFromCA(usdCustomAttributePb, translatedPrim);
+ }
+ if (!purposeFound) {
+ purposeFound
+ = MaxUsd::SetPrimPurposeFromCA(usdCustomAttributePb, translatedPrim);
+ }
+ }
+ // We found all of them, stop the loop.
+ if (hiddenFound && kindFound && purposeFound) {
+ return;
+ }
+ }
+ }
+
+ // We didn't find all of the attribute, let's look at the base object
+ const auto baseObject = object->FindBaseObject();
+ usdCustomAttributePb = MaxUsd::MetaData::FindUsdCustomAttributeParamBlock(baseObject);
+
+ if (usdCustomAttributePb == nullptr) {
+ // no paramblock with usd custom attributes found, skip.
+ return;
+ }
+
+ if (!hiddenFound) {
+ MaxUsd::SetPrimHiddenFromCA(usdCustomAttributePb, translatedPrim);
+ }
+ if (!kindFound) {
+ MaxUsd::SetPrimKindFromCA(usdCustomAttributePb, translatedPrim);
+ }
+ if (!purposeFound) {
+ MaxUsd::SetPrimPurposeFromCA(usdCustomAttributePb, translatedPrim);
+ }
+}
+
+void USDSceneBuilder::ConfigureKind(INode* node, pxr::UsdPrim& translatedPrim)
+{
+ // Check if kind previously set from custom attributes
+ pxr::TfToken kind;
+ if (!pxr::UsdModelAPI(translatedPrim).GetKind(&kind) && kind.IsEmpty()) {
+ // Set kind to group on export if max group
+ if (node->IsGroupHead()) {
+ pxr::UsdModelAPI(translatedPrim).SetKind(pxr::KindTokens->group);
+ }
+ }
+}
+
+} // namespace MAXUSD_NS_DEF
diff --git a/src/MaxUsd/Builders/USDSceneBuilder.h b/src/MaxUsd/Builders/USDSceneBuilder.h
new file mode 100644
index 0000000..42cfdb7
--- /dev/null
+++ b/src/MaxUsd/Builders/USDSceneBuilder.h
@@ -0,0 +1,328 @@
+//
+// Copyright 2023 Autodesk
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include "USDSceneBuilderOptions.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+namespace MAXUSD_NS_DEF {
+
+/**
+ * \brief USD Scene Builder.
+ * \remarks This current implementation is a work-in-progress that will evolve as additional conversion operations
+ * between USD and 3ds Max are supported. Performance of the import process is a design concern, and
+ * while CRTP-type solutions are not (currently) implemented, future work should attempt to
+ * improve/maintain run-time performance while maintaining a high level of flexibility.
+ * \remarks This current implementation moves some of the import logic away from the USDSceneController where it was
+ * previously located. In the process, the import still owns some of the UI/UX import process such
+ * as the handling of 3ds Max's progress bar. Future work should abstract away this behavior, and
+ * expose more control to the caller (e.g. through callbacks, or notifications about the current
+ * state of the import process, etc.).
+ */
+class USDSceneBuilder
+{
+public:
+ /**
+ * \brief Constructor.
+ */
+ USDSceneBuilder();
+
+ /**
+ * \brief Destructor.
+ */
+ virtual ~USDSceneBuilder() = default;
+
+ /**
+ * \brief Build a USD Stage from the given build options.
+ * \param buildOptions Build configuration options to use during the translation process.
+ * \param cancelled Flag that will be set to true if cancelled by user.
+ * \param filename The filename of the USD file that is being built.
+ * \param editedLayers Identifiers of layers edited during the export. All these layers will be saved to disk at the end of the export process.
+ * \param isUSDZ flag that will be set to true if exported file will end up packaged in a usdz archive.
+ * \return The USD Stage built from the given options.
+ */
+ pxr::UsdStageRefPtr Build(
+ const USDSceneBuilderOptions& buildOptions,
+ bool& cancelled,
+ const fs::path& filename,
+ std::map& editedLayers,
+ bool isUSDZ);
+
+protected:
+ /**
+ * \brief Context for each translation operation to be performed as part of the USD Stage building process.
+ */
+ struct TranslationContext
+ {
+ /// Reference to the 3ds Max Node to translate:
+ INode* node { nullptr };
+
+ /// Reference to the USD Stage into which to perform the translation:
+ const pxr::UsdStageRefPtr& stage;
+ /// Path where to create the new prim(s):
+ const pxr::SdfPath parentPrimPath;
+
+ /// Name to give the new prim
+ const std::string primName;
+
+ /// USD time configuration for the translation operation:
+ const MaxUsd::TimeConfig timeConfig;
+
+ /// In preview mode, we do not actually translate the nodes, but only
+ /// figure out what Prims will be exported, and where in the hierarchy.
+ bool preview = false;
+ };
+
+ /**
+ * \brief Holder for translation operations to execute, based on the success of a given predicate.
+ */
+ template struct TranslationOperation
+ {
+ /**
+ * \brief Definition for the type of the predicate identifying if the translation operation should be
+ * applied to the provided 3ds Max Object.
+ */
+ using selector_t = std::function;
+ /**
+ * \brief Definition for the type of the translation operation to execute if the predicate elected to
+ * operate on the provided 3ds Max Object.
+ */
+ using operation_t = std::function;
+
+ /**
+ * \brief Default constructor.
+ */
+ TranslationOperation() = default;
+
+ /**
+ * \brief Constructor.
+ * \param selectorPredicate Predicate used to elect if the given translation operation should be applied to
+ * the provided 3ds Max object.
+ * \param operation Translation operation to apply to the provided 3ds Max object, if the predicate elected it
+ * for processing.
+ */
+ TranslationOperation(
+ const selector_t& selectorPredicate,
+ const operation_t& operation) noexcept
+ : AppliesToObject { selectorPredicate }
+ , Execute { operation }
+ {
+ // Nothing to do.
+ }
+
+ /**
+ * \brief Predicate to elect if the provided USD Prim should be selected for processing.
+ */
+ selector_t AppliesToObject { [](Object*) { return false; } };
+ /**
+ * \brief Translation operation to apply to the provided USD Prim, if the predicate elected it for
+ * processing.
+ */
+ operation_t Execute { [](InputTypes...) { return nullptr; } };
+ };
+
+ /**
+ * \brief Type definition for translation operations to be performed to convert 3ds Max content into USD content.
+ */
+ using translationOperation_t
+ = TranslationOperation;
+
+ /**
+ * \brief Type definition for additional translation operations performed after 3ds Max object converted into USD Prims
+ */
+ using translationPrimConfigurator_t
+ = TranslationOperation;
+
+protected:
+ /**
+ * \brief Start the scene export process to usd
+ * \param writeJobContext The write job context (options, stage, target file, etc.)
+ * \param primToNode A map of prims to their source nodes to be filled. Used to keep track of what prim was exported
+ * from what node.
+ * \param primsToMaterialBind The method will fill this set with the prims on which we should do material assignment
+ * later in the export process.
+ * \param progress Reference to the progress bar, to report progress with, as this can be a lengthy operation.
+ * \return true if operation completed, false if operation failed or cancelled
+ */
+ bool BuildStageFromMaxNodes(
+ pxr::MaxUsdWriteJobContext& writeJobContext,
+ pxr::MaxUsdExportChaserRegistry::FactoryContext::PrimToNodeMap& primToNode,
+ pxr::TfHashSet& primsToMaterialBind,
+ MaxProgressBar& progress);
+
+ /**
+ * \brief Processes a 3ds Max node for export to the USD Stage, using the provided translation context.
+ * Will setup the prims in the stage and queue time dependent work (writing properties and
+ * transforms) in the passed AnimnExportTask (indeed, this work is delayed to make sure we only
+ * evaluate each 3dsMax object once at a certain time).
+ * \param context The context of the node translation operation.
+ * \param buidOptions Build configuration options to use during the translation process.
+ * \param primCreators The prim creators to be used to convert the node.
+ * \param primConfigurators The object configurators to be used to convert the node.
+ * \param stage USD Stage into which to add the new prim(s).
+ * \param doAssignMaterial True if the prim create for the 3dsMax node should be considered for material assignement,
+ * this is from the implementation of the prim writer. IMPORTANT: In the case of instanced
+ * objects, only the first instance may return doAssignMaterial = true, as subsequent instances
+ * do not need to involve the prim writer at all. It is the responsibility of the caller to
+ * handle this case.
+ * \param animExportTask Time samples export task - where time-dependent work will be queued up so it can later
+ * be batched with other work which need to be run at the same 3dsMax time. Essentially writing
+ * object properties & transforms.
+ * \return A vector of the created USD prims for the 3dsMax node (or that would be created if in preview mode).
+ * The first prim in the vector is the root prim for the node.
+ */
+ MaxUsd::PrimDefVectorPtr ProcessNode(
+ const TranslationContext& context,
+ const pxr::MaxUsdWriteJobContext& jobContext,
+ const std::vector& primConfigurators,
+ const pxr::UsdStageRefPtr& stage,
+ bool& doAssignMaterial,
+ MaxUsd::AnimExportTask& animExportTask);
+
+ /**
+ * \brief Checks if anything in the hierarchy starting at a given node should be exported.
+ * \param node The top-most node of the hierarchy. The function is called recursively on the
+ * node's children, and a cache is maintained to avoid unnecessary traversals.
+ * \param buildOptions The USD scene builder options.
+ * \return True if the hierarchy contains objects which should be exported, false otherwise.
+ */
+ bool HasExportableDescendants(INode* node, const USDSceneBuilderOptions& buildOptions);
+
+ /**
+ * \brief Writes all the prims required to translate a Max node to USD. If the node's object has an offset,
+ * we will need an extra xform prim, so that the object's offset is not propagated to children
+ * nodes. If the object can be exported as an instanceable prim, the function will create the
+ * appropriate class prim and xform prim referencing the class prim.
+ * \param createObjectPrim The translation function to use for the node's object itself. The function take
+ * 2 parameters, the translation context for the node and a boolean that indicate if the
+ * translated prim should include the object transform.
+ * \param context The translation context for the node. If in preview, we are just gathering all the prims that
+ * will be written to, but not actually writing to them - we are previewing what we will write
+ * to - knowing this we can later pre-create in all prims (without their properties) in a single
+ * SdfChange block, to avoid costly notifications being sent out.
+ * \param objectPrimSuffix The suffix to use in the object's prim name if it is created. The full name of that
+ * \param xformRequirement Specifies if an extra Xform needs to be created to encode the node's transform, or if
+ * it can be applied onto the object's prim directly. This is a requirement from the prim
+ * writer, additional requirements may come into play forcing the creation of the Xform, even if
+ * the prim writer itself doesn't need it (for example when using instancing).
+ * \param instancingRequirement Whether or not default instancing should be used (i.e. instanced 3dsMax nodes would
+ * generate instanced prims).
+ * This will be ignored for node that are exported as instances, in which case the prim will
+ * always be created if the offset transform is not identity.
+ * \parm rootPrim The root prim we are exporting to. This may be useful in case we need to create prims outside
+ * the hierarchy, but that should still be under the targeted root prim for export. For example,
+ * prototype prims.
+ * \return A vector of definitions of prims created from the node (or that would be created if in preview mode).
+ * The first prim in the vector is the root prim for the node.
+ */
+ MaxUsd::PrimDefVectorPtr WriteNodePrims(
+ std::function
+ createObjectPrim,
+ const TranslationContext& context,
+ const std::string& objectPrimSuffix,
+ const MaxUsd::XformSplitRequirement& xformRequirement,
+ const MaxUsd::InstancingRequirement& instancingRequirement,
+ const pxr::SdfPath& rootPrim);
+
+ /**
+ * \brief Translate USD round-trip attributes stored as Max custom attributes
+ * \param context The context of the translation operation.
+ * \param translatedPrim the usd prim being translated to
+ */
+ virtual void ConfigureUsdAttributes(
+ const TranslationContext& translationContext,
+ pxr::UsdPrim& translatedPrim);
+
+ /**
+ * \brief Configure the USD Kind for a node's exported prim - only do so if none already authored.
+ * \param node The node we are exporting the prim from
+ * \param translatedPrim The usd prim being translated to
+ */
+ void ConfigureKind(INode* node, pxr::UsdPrim& translatedPrim);
+
+ /**
+ * \brief Return the number of nodes to be exported for the given buildOptions
+ * \param buildOptions Build configuration options to use during the translation process.
+ * \return number of nodes that will be exported
+ */
+ int GetNumberOfNodeToExport(const USDSceneBuilderOptions& buildOptions);
+
+ /**
+ * \brief Return whether or not the parent prim should be used to export an instance.
+ * We want to reuse the parent prim in certain situations to avoid creating extra layers of
+ * prims when round-tripping usd data inside 3ds Max multiple times.
+ * \param context The context of the translation operation.
+ * \return True if the parent prim should be reuse to export the instance node.
+ */
+ bool ReuseParentPrimForInstancing(const TranslationContext& context);
+
+private:
+ /// Class ID of the Mesh type to select for translation operations:
+ const Class_ID MESH_TYPE_ID { TRIOBJ_CLASS_ID, 0 };
+ /// Class ID of the Camera type to select for translation operations:
+ const Class_ID CAMERA_TYPE_ID { SIMPLE_CAM_CLASS_ID, 0 };
+ /// Class ID of the Light type to select for translation operations:
+ const Class_ID PHOTOMETRIC_LIGHT_TYPE_ID { LIGHTSCAPE_LIGHT_CLASS };
+ /// Class ID of the Dummy type to select for translation operations:
+ const Class_ID DUMMY_TYPE_ID { DUMMY_CLASS_ID, 0 };
+ /// Class ID of the PointHelper type to select for translation operations:
+ const Class_ID POINTHELPER_TYPE_ID { POINTHELP_CLASS_ID, 0 };
+
+ /// Reference to the Core Interface to use to interface with 3ds Max:
+ Interface17* coreInterface { GetCOREInterface17() };
+
+ // Map of instanced Node to usd class Prim
+ std::map maxNodeToClassPrimMap;
+ MaxUsd::UniqueNameGenerator classPrimBaseNameGenerator;
+
+ // Explicit set of nodes to be exported. Used when exporting the selection or from a node list.
+ // Remains empty if exporting the whole scene.
+ std::unordered_set nodesToExportSet;
+
+ // Cached maintained by HasExportableDescendants() to avoid extra scene graph
+ // traversals. For each node in the map, the boolean value specifies whether or
+ // not itself, or any of its descendants should be exported.
+ std::map hasExportableDescendantsMap;
+
+ // Instance to prototype prim map. We collect this during export so that we set up instancing
+ // all at once in a single SdfChangeBlock at the end.
+ pxr::TfHashMap