Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 81 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# Material Processor
A tool for Ingestion, Standardization, and Conversion tool for all kinds of Material Networks
for complex multi-input/ multi-output materials in various DCCs.\
Supports USD file format.
Currently, it's in beta with support for Houdini's Arnold, MaterialX, PrincipledShader,
and Redshift as regular nodes and usd prims.
# Materials Processor

A beta tool for ingestion, standardization, and conversion of material networks across DCCs.
It supports Houdini, Blender, and Maya material graph traversal, with USD MaterialX and OpenPBR export paths.

Current package version: `2.0.0-beta`.

<table>
<tr>
Expand All @@ -13,29 +12,96 @@ and Redshift as regular nodes and usd prims.
</tr>
</table>


### Features
- [x] UI supports Drag and drop for dropping mat nodes from the Application.
- [x] Ingests and converts to Most Materials types: PrincipledShader, Arnold, MaterialX, Redshift.
- [x] pip-standard coding practices as much as possible and with proper logging.

- [x] UI supports drag and drop for dropping material nodes from Houdini.
- [x] Houdini ingestion and conversion for Principled Shader, Arnold, MaterialX, Redshift, and OpenPBR paths.
- [x] Blender scene inspection and USD MaterialX/OpenPBR material export from the command line.
- [x] Maya scene inspection and USD MaterialX/OpenPBR material export from the command line.
- [x] Runtime validation for Blender and Maya.

### Installation

- For Python development, use `uv --native-tls sync`.
- On Windows: run the included batch installer `install_houdini_win.bat`.
- Double-click `install_houdini_win.bat` in File Explorer.
- The script copies the project folder to `%USERPROFILE%\Documents\HoudiniTools`.
- The script also copies `Axe_Material_Processor.json` to `%USERPROFILE%\Documents\houdini21.0\packages`.
- To install to a different Houdini version, run `install_houdini_win.bat <HOUDINI_VERSION>`, for example `install_houdini_win.bat 20.5`.
- If you encounter permission or auditing errors, run .bat file as Administrator.
- If you encounter permission or auditing errors, run the batch file as Administrator.
- After installation, restart Houdini to load the tool.

### CLI Quickstart

Show the installed package version:

```powershell
uv --native-tls run materials-processor --version
```

Discover local DCC runtimes:

```powershell
uv --native-tls run materials-processor doctor
```

Run deeper runtime checks:

```powershell
uv --native-tls run materials-processor doctor --validate --material-smoke
```

Inspect a Blender scene without writing USD:

```powershell
uv --native-tls run materials-processor blender inspect "C:\path\to\scene.blend" --report-json "C:\temp\blender_report.json"
```

Export Blender materials to USD MaterialX and OpenPBR:

```powershell
uv --native-tls run materials-processor blender export-usd "C:\path\to\scene.blend" --out-dir "C:\temp\materials"
```

Use Blender texture remapping when a scene points at missing source paths:

```powershell
uv --native-tls run materials-processor blender inspect "C:\path\to\scene.blend" --texture-root "D:\textures" --missing-textures error
```

Inspect a Maya scene without writing USD:

```powershell
uv --native-tls run materials-processor maya inspect "C:\path\to\scene.ma" --report-json "C:\temp\maya_report.json"
```

Export Maya materials to USD MaterialX and OpenPBR:

```powershell
uv --native-tls run materials-processor maya export-usd "C:\path\to\scene.ma" --out-dir "C:\temp\maya_materials"
```

Validate a single runtime directly:

```powershell
uv --native-tls run materials-processor runtime validate --dcc blender --material-smoke
uv --native-tls run materials-processor runtime validate --dcc maya --material-smoke
```

### Current Limitations

- Blender node groups are reported as unsupported nodes; they are not expanded into USD yet.
- Blender texture remapping is available, but Maya texture remapping is not implemented yet.
- Maya CLI export currently traverses shading engines with surface shader connections in a saved `.ma` or `.mb` scene.
- Houdini remains the most mature in-DCC workflow. Blender and Maya CLI support are newer beta paths focused on graph extraction and USD material export.
- USD export currently targets MaterialX and OpenPBR material files, not full asset/shot USD assembly.

### Roadmap

- [x] Add support for Solaris and USD files.
- [x] Finish implementation for Redshift.
- [x] Add command line support for Blender.
- [x] Add command line support for Maya.
- [ ] Add a user-facing cross-DCC Qt launcher around the CLI workflows.
- [ ] Add implementation for Vray and Renderman.
- [ ] Extend support to other apps like Substance Painter, Maya, and blender.



- [ ] Extend support to other apps like Substance Painter.
1 change: 0 additions & 1 deletion VERSION

This file was deleted.

8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[project]
name = "materials-processor"
version = "1.2.4"
description = "Houdini material ingestion, standardization, and conversion tools."
description = "Cross-DCC material ingestion, standardization, and conversion tools."
dynamic = ["version"]
requires-python = ">=3.11"
dependencies = []

[project.scripts]
materials-processor = "materials_processor.cli:main"
materials-processor-blender = "materials_processor.dcc.blender.cli:main"
materials-processor-maya = "materials_processor.dcc.maya.cli:main"

[build-system]
requires = ["hatchling"]
Expand All @@ -23,6 +24,9 @@ dev = [
[tool.hatch.build.targets.wheel]
packages = ["src/materials_processor"]

[tool.hatch.version]
path = "src/materials_processor/_version.py"

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"
Expand Down
2 changes: 2 additions & 0 deletions src/materials_processor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

import logging

from materials_processor._version import __version__

logging.getLogger(__name__).addHandler(logging.NullHandler())
3 changes: 3 additions & 0 deletions src/materials_processor/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Package version metadata."""

__version__ = "2.0.0-beta"
193 changes: 193 additions & 0 deletions src/materials_processor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

import argparse
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

from materials_processor import __version__
from materials_processor.dcc.blender import cli as blender_cli
from materials_processor.dcc.maya import cli as maya_cli


def _default_package_src() -> Path:
Expand All @@ -18,6 +23,32 @@ def _print_json(payload: dict) -> None:
print(json.dumps(payload, indent=2, sort_keys=True))


def _add_doctor_parser(subparsers) -> argparse.ArgumentParser:
doctor_parser = subparsers.add_parser("doctor", help="Discover installed DCC runtimes.")
doctor_parser.add_argument("--validate", action="store_true", help="Run DCC runtime validation when available.")
doctor_parser.add_argument("--timeout", type=int, default=120, help="Validation timeout in seconds.")
doctor_parser.add_argument(
"--package-src",
default=None,
help="Source directory to expose to DCC runtimes. Defaults to this checkout's src directory.",
)
doctor_parser.add_argument("--material-smoke", action="store_true", help="Run DCC material smoke validation.")

blender_group = doctor_parser.add_argument_group("Blender")
blender_group.add_argument("--blender-exe", help="Explicit path to blender.exe.")
blender_group.add_argument("--blender-root", help="Explicit Blender install root.")
blender_group.add_argument("--blender-version", help="Blender version to discover, e.g. 4.5.")

maya_group = doctor_parser.add_argument_group("Maya")
maya_group.add_argument("--maya-root", help="Explicit Maya install root.")
maya_group.add_argument("--maya-version", default="2024", help="Maya version to resolve. Default: 2024.")

houdini_group = doctor_parser.add_argument_group("Houdini")
houdini_group.add_argument("--hython", help="Explicit path to hython.exe.")

return doctor_parser


def _add_runtime_parser(subparsers) -> argparse.ArgumentParser:
runtime_parser = subparsers.add_parser("runtime", help="Runtime discovery and validation commands.")
runtime_subparsers = runtime_parser.add_subparsers(dest="runtime_command", required=True)
Expand Down Expand Up @@ -57,11 +88,22 @@ def _add_blender_parser(subparsers) -> argparse.ArgumentParser:
return blender_parser


def _add_maya_parser(subparsers) -> argparse.ArgumentParser:
maya_parser = subparsers.add_parser("maya", help="Maya scene inspection and export commands.")
maya_subparsers = maya_parser.add_subparsers(dest="maya_command", required=True)
maya_cli.add_maya_export_parser(maya_subparsers)
maya_cli.add_maya_inspect_parser(maya_subparsers)
return maya_parser


def build_parser() -> argparse.ArgumentParser:
"""Build the top-level argument parser."""
parser = argparse.ArgumentParser(prog="materials-processor")
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
subparsers = parser.add_subparsers(dest="command", required=True)
_add_blender_parser(subparsers)
_add_maya_parser(subparsers)
_add_doctor_parser(subparsers)
_add_runtime_parser(subparsers)
return parser

Expand Down Expand Up @@ -124,6 +166,145 @@ def _validate_maya_runtime(args) -> dict:
return result


def _resolve_hython(explicit_hython: str | None = None) -> Path | None:
if explicit_hython:
path = Path(explicit_hython).expanduser().resolve()
return path if path.is_file() else None

env_hython = os.environ.get("MATERIALS_PROCESSOR_HYTHON")
if env_hython:
path = Path(env_hython).expanduser().resolve()
if path.is_file():
return path

path_hython = shutil.which("hython") or shutil.which("hython.exe")
if path_hython:
return Path(path_hython).resolve()

default_hython = Path("C:/Program Files/Side Effects Software/Houdini 21.0.631/bin/hython.exe")
if default_hython.is_file():
return default_hython.resolve()

install_root = Path("C:/Program Files/Side Effects Software")
if install_root.is_dir():
candidates = sorted(install_root.glob("Houdini 21.0*/bin/hython.exe"), reverse=True)
for candidate in candidates:
if candidate.is_file():
return candidate.resolve()

return None


def _validate_houdini_runtime(hython: Path, timeout: int) -> dict:
code = (
"import json\n"
"import hou\n"
"print('MATERIALS_PROCESSOR_HOUDINI_RUNTIME=' + json.dumps({"
"'version': hou.applicationVersionString(), "
"'hfs': hou.getenv('HFS')"
"}, sort_keys=True))\n"
)
completed = subprocess.run(
[str(hython), "-c", code],
check=False,
capture_output=True,
text=True,
timeout=timeout,
)
if completed.returncode != 0:
raise RuntimeError(
"Houdini validation failed with exit code "
f"{completed.returncode}.\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
)
for line in completed.stdout.splitlines():
if line.startswith("MATERIALS_PROCESSOR_HOUDINI_RUNTIME="):
return json.loads(line.split("=", 1)[1])
raise RuntimeError(f"Houdini validation did not report a runtime result.\nstdout:\n{completed.stdout}")


def _doctor_entry(name: str, status: str, **extra) -> dict:
return {"dcc": name, "status": status, **extra}


def _doctor_blender(args) -> dict:
from materials_processor.dcc.blender.runtime import resolve_blender_runtime

try:
runtime = resolve_blender_runtime(
version=args.blender_version,
root=args.blender_root,
blender_exe=args.blender_exe,
)
result = _doctor_entry(
"blender",
"found",
root=str(runtime.root),
blender_exe=str(runtime.blender_exe),
version=runtime.version,
python_version=runtime.python_version,
api_version=runtime.api_version,
)
if args.validate:
validation = _validate_blender_runtime(args)
result.update(validation)
result["status"] = "valid"
return result
except Exception as exc:
return _doctor_entry("blender", "missing" if isinstance(exc, FileNotFoundError) else "error", error=str(exc))


def _doctor_maya(args) -> dict:
from materials_processor.dcc.maya.runtime import resolve_maya_runtime

try:
runtime = resolve_maya_runtime(version=args.maya_version, root=args.maya_root)
result = _doctor_entry(
"maya",
"found",
root=str(runtime.root),
maya_exe=str(runtime.maya_exe),
mayapy_exe=str(runtime.mayapy_exe),
version=runtime.version,
api_version=runtime.api_version,
)
if args.validate:
validation = _validate_maya_runtime(args)
result.update(validation)
result["status"] = "valid"
return result
except Exception as exc:
return _doctor_entry("maya", "missing" if isinstance(exc, FileNotFoundError) else "error", error=str(exc))


def _doctor_houdini(args) -> dict:
try:
hython = _resolve_hython(args.hython)
if hython is None:
return _doctor_entry("houdini", "missing", error="hython was not found.")
result = _doctor_entry("houdini", "found", hython=str(hython))
if args.validate:
result.update(_validate_houdini_runtime(hython, args.timeout))
result["status"] = "valid"
return result
except Exception as exc:
return _doctor_entry("houdini", "error", error=str(exc))


def _doctor(args) -> dict:
return {
"package": {
"name": "materials-processor",
"version": __version__,
"package_src": str(Path(args.package_src).resolve() if args.package_src else _default_package_src()),
},
"runtimes": [
_doctor_blender(args),
_doctor_maya(args),
_doctor_houdini(args),
],
}


def main(argv: list[str] | None = None) -> int:
"""Run the top-level command line interface."""
parser = build_parser()
Expand All @@ -138,6 +319,18 @@ def main(argv: list[str] | None = None) -> int:
_print_json(blender_cli.run_inspect_from_args(args))
return 0

if args.command == "maya":
if args.maya_command == "export-usd":
_print_json(maya_cli.run_export_from_args(args))
return 0
if args.maya_command == "inspect":
_print_json(maya_cli.run_inspect_from_args(args))
return 0

if args.command == "doctor":
_print_json(_doctor(args))
return 0

if args.command == "runtime" and args.runtime_command == "validate":
if args.dcc == "blender":
_print_json(_validate_blender_runtime(args))
Expand Down
Loading
Loading