GLB to USDZ conversion

Step by Step Docker python pipeline

Overview

The rapid adoption of 3D technologies has driven the demand for efficient file formats that cater to various platforms and devices. glTF (GL Transmission Format) has emerged as the de facto standard for web and real-time 3D applications due to its compactness and ease of transmission. However, for augmented reality (AR) applications—especially on Apple devices—the USDZ format is indispensable. Introduced with iOS 12 and ARKit 2.0, USDZ enables seamless integration of 3D content into the Apple ecosystem.

Despite its importance, there is no standardized method for converting glTF files into USDZ. This gap poses significant challenges for developers who need to work across different ecosystems. In this guide, I detail my experience creating a Python-based conversion pipeline using a Docker container. This comprehensive article will cover:

  • A detailed introduction to glTF and USDZ formats
  • The challenges inherent in converting between these formats
  • An in-depth explanation of the design and architecture of my solution
  • A step-by-step breakdown of the code and its functionality
  • Best practices for integrating and scaling the solution

By the end of this guide, you will have a solid understanding of the conversion process and be equipped to implement, modify, or extend the pipeline for your own projects.

Understanding the File Formats

glTF: The 3D File Standard

glTF was introduced by the Khronos Group in 2015 as a file format designed to efficiently transmit 3D models and scenes. It was later updated to glTF 2.0 in 2017, which improved support for modern rendering techniques such as Physically-Based Rendering (PBR). Often described as the “JPEG of 3D files,” glTF is widely adopted due to its:

  • Compact file size: Minimizes loading times and bandwidth usage.
  • Efficient structure: Separates geometry, textures, and materials in a way that is easily consumable by modern graphics engines.
  • Broad compatibility: Supported by a wide range of software, engines, and web frameworks.

There are two common variants:

  • .gltf Files: A JSON-based format that references external resources such as textures and binary data.
  • .glb Files: A binary version that bundles all necessary components into a single file for ease of use.

USDZ: The AR-Optimized Format

USDZ is a specialized version of the Universal Scene Description (USD) format, originally developed by Pixar and later adopted by Apple for AR applications. It is a compressed or zipped archive that encapsulates all necessary data—including geometry, textures, animations, and materials—into a single, portable file. Key features include:

  • Native support on iOS and macOS: USDZ files are optimized for ARKit, Quick Look, and web-based AR experiences.
  • Self-contained packaging: Ensures that all assets required for rendering are embedded, reducing dependency issues.
  • Optimized for performance: Designed to balance file size with visual fidelity in AR applications.

Challenges in glTF-to-USDZ Conversion

Converting from glTF to USDZ is not a straightforward process due to several technical hurdles:

  1. Differences in Material Systems: glTF’s reliance on PBR materials contrasts with USDZ’s shading models. This means that material properties must be carefully mapped and, in some cases, adjusted manually.
  2. Handling of Animations and Textures: While glTF supports complex animations and separate texture files, USDZ requires these resources to be embedded. This discrepancy can lead to issues where animations do not transfer correctly or textures are missing.
  3. Lack of Native Conversion Tools: There is no official, cross-platform tool provided by Apple to convert glTF to USDZ. Some utilities exist (such as Google’s usd_from_gltf), but they often require additional toolkits or environments (like Pixar’s USD toolkit) which are not optimized for all use cases.
  4. Cross-Platform Compatibility: Since USDZ is primarily supported on macOS and iOS, developers using Windows or Linux face additional challenges when testing and validating conversions.

To address these challenges, I explored multiple approaches before settling on a Docker-based solution that encapsulates all dependencies and provides a consistent environment across operating systems.

My Approach: Leveraging Docker and Python

Initial Experiments and Lessons Learned

My initial strategy involved using Google’s usd_from_gltf utility to generate an intermediate USD file. I then attempted to package this into a USDZ file using Pixar’s USD toolkit and its usdzip tool. However, this approach proved problematic because:

  • Complex setup: Configuring Pixar’s toolkit was error-prone and introduced functionalities that were not needed.
  • Redundancy: The usd_from_gltf tool itself has the capability to produce USDZ files directly, eliminating the need for a separate packaging step.

The Docker Advantage

After testing several libraries and online conversion tools, I discovered the Docker image plattar/python-xrutils. This image bundles Google’s usd_from_gltf along with other essential libraries, simplifying the conversion process by providing a ready-to-use environment. Docker allows the entire pipeline to run in a containerized setup, which ensures:

  • Consistency across development environments: No matter the host OS, the Docker container will behave the same way.
  • Simplified dependency management: All necessary tools are packaged within the Docker image.
  • Scalability and integration: The solution can easily be integrated into larger workflows or automated systems, such as a Django-based backend.

Detailed Implementation of the Conversion Pipeline

The solution comprises several modules, each responsible for a specific aspect of the conversion process. The key components include:

  1. Command-Line Interface (CLI) and Environment SetupThe conversion script is designed to be run from the command line, accepting parameters to specify input and output directories as well as recursive directory traversal. This flexibility makes it easy to test and integrate the tool.

    Environment Setup

    Using a virtual environment isolates the project’s dependencies:
    python -m venv .
    . ./bin/activate
    pip install docker
    

    CLI Handling with main.py

    The CLI is implemented using Python’s argparse module. Three optional arguments are provided:
    # main.py
    
    import argparse
    
    def main():
        parser = argparse.ArgumentParser(description='Convert glTF files to USDZ.')
        parser.add_argument('--input', type=str, default='input/', help='Input file or directory')
        parser.add_argument('--output', type=str, default='output/', help='Output directory')
        parser.add_argument('--read_dir_recursive', action='store_true', help='Read directories recursively')
        args = parser.parse_args()
    
        # Call the converter function here with the arguments
        convert_gltf_to_usdz(args.input, args.output, args.read_dir_recursive)
    
    if __name__ == '__main__':
        main()
    
    • Input: Path to the glTF file or directory.
    • Output: Directory where USDZ files will be saved.
    • Read Directory Recursively: Flag to enable recursive search through subdirectories.
  2. Input/Output ValidationBefore conversion begins, the paths provided by the user must be validated. The function check_io ensures that the input exists and creates the output directory if necessary.
    # check_io.py
    
    import os
    
    def check_io(arguments):
        input_path = arguments.input
        output_path = arguments.output
        if not os.path.exists(input_path):
            raise ValueError(f"Input path {input_path} does not exist.")
    
        if not os.path.exists(output_path):
            os.makedirs(output_path)
        return True
    
  3. Docker-Based Conversion ModuleThe core of the solution is encapsulated in the Docker utility module, which manages image fetching, volume binding, and command execution within the Docker container.

    Managing Docker Resources with DockerUtil

    The DockerUtil class connects to the local Docker daemon, ensures the required image is available, and sets up the file exchange between the host and the container. It consists of several methods - Below is the full code for the Docker utility:
    # docker_util.py
    
    import docker
    import os
    import glob
    from path_operations import get_absolute_path, new_file_path, change_extension, check_io
    
    class DockerUtil:
        def __init__(self, input, output, recursive):
            self.image_name = 'plattar/python-xrutils'
            self.docker_release = 'latest'
            self.docker_input_path = '/input'
            self.docker_output_path = '/output'
            self.instance = docker.from_env()
            self.input = input
            self.output = output
            self.recursive = recursive
    
        def get_image(self):
            images = self.instance.images.list(self.image_name)
            if len(images) > 0:
                return images[0]
    
            return self.instance.images.pull(self.image_name, tag=self.docker_release)
    
        def bind_volume(self):
            input_path = get_absolute_path(self.input)
            output_path = get_absolute_path(self.output)
            volumes = {
                input_path: {'bind': self.docker_input_path, 'mode': 'rw'},
                output_path: {'bind': self.docker_output_path, 'mode': 'rw'}
            }
            self.container = self.instance.containers.run(
                self.image_name,
                volumes=volumes,
                tty=True,
                detach=True
            )
    
        def call_converter(self, input_file):
            def create_args(input_file):
                input_docker_path = new_file_path(self.docker_input_path, input_file)
                output_docker_path = change_extension(new_file_path(self.docker_output_path, input_file), 'usdz')
                return input_docker_path, output_docker_path
    
            input_docker_path, output_docker_path = create_args(input_file)
            command = f"usd_from_gltf {input_docker_path} {output_docker_path}"
            self.container.exec_run(command)
    
        def stop(self):
            self.container.kill()
            self.container.remove()
    
        def start(self):
            self.get_image()
            self.bind_volume()
            try:
                input_files = list_files(self.input, self.recursive)
                for input_file in input_files:
                    self.call_converter(input_file)
            except Exception as e:
                print(f"An error occurred: {e}")
            finally:
                self.stop()
    
    • Constructor: Initializes key variables such as the image name, Docker paths, and input/output parameters.
    • get_image: Checks if the Docker image is already present and pulls it if necessary.
    • bind_volume: Binds the host directories to the container’s file system to allow file sharing.
    • call_converter: Executes the conversion command (usd_from_gltf) inside the running container.
    • stop: Cleans up the container after conversion.
    • start: Orchestrates the conversion process by calling the above methods sequentially.
  4. File Management and Path OperationsThe set of helper functions in path_operations.py provides consistent file path management, ensuring that host paths are correctly mapped to container paths. These functions also help with operations such as changing file extensions, creating new file paths, and validating input/output directories.
    # path_operations.py
    
    import os
    
    def get_absolute_path(path):
        return os.path.abspath(path)
    
    def new_file_path(new_dir, filename):
        return os.path.join(new_dir, os.path.basename(filename))
    
    def change_extension(filename, new_ext):
        base = os.path.splitext(filename)[0]
        return f"{base}.{new_ext}"
    
    def check_io(arguments):
        input_path = arguments.input
        output_path = arguments.output
        if not os.path.exists(input_path):
            raise ValueError(f"Input path {input_path} does not exist.")
    
        if not os.path.exists(output_path):
            os.makedirs(output_path)
        return True
    
  5. Bringing It All TogetherThe final integration is handled in main.py, which imports the Docker utility class, validates the paths using check_io, and then starts the conversion process.
    # main.py
    
    import argparse
    from docker_util import DockerUtil
    from check_io import check_io
    
    def main():
        parser = argparse.ArgumentParser(description='Convert glTF files to USDZ.')
        parser.add_argument('--input', type=str, default='input/', help='Input file or directory')
        parser.add_argument('--output', type=str, default='output/', help='Output directory')
        parser.add_argument('--read_dir_recursive', action='store_true', help='Read directories recursively')
        args = parser.parse_args()
        check_io(args)
        converter = DockerUtil(args.input, args.output, args.read_dir_recursive)
        converter.start()
    
    if __name__ == '__main__':
        main()
    

In-Depth Analysis and Future Enhancements

Technical Considerations

  • Error Handling: Robust error handling is crucial for production systems. The current implementation catches exceptions during the conversion process and ensures the Docker container is terminated properly. Future work could include more granular error logging and recovery mechanisms.
  • Batch Processing: While the script currently processes a single file or directory based on the provided input, it can be extended to support batch processing. This would allow for the conversion of multiple files in parallel, significantly speeding up workflows in larger projects.
  • Integration with Web Frameworks: Integrating this pipeline with a web framework like Django or Flask opens the door to building an online conversion tool. This would involve exposing the conversion functionality via RESTful APIs, allowing users to upload glTF files and receive USDZ outputs through a web interface.
  • Optimization: Prior to conversion, it may be beneficial to preprocess glTF files with tools like gltf-pipeline to optimize textures and geometry. This step can further reduce file sizes and improve the quality of the resulting USDZ files.

Potential Use Cases

  • AR Content Creation: Designers and developers can use this pipeline to streamline the creation of AR experiences, ensuring that assets designed in glTF can be quickly converted and previewed on Apple devices.
  • Cross-Platform Development: For teams working on cross-platform 3D applications, the containerized approach ensures consistency regardless of the development environment.
  • Automated Workflows: Integration with continuous integration (CI) systems can enable automated testing and deployment of AR assets, reducing manual intervention and potential errors.

Conclusion

This guide has provided an in-depth look at creating a robust, Docker-powered pipeline for converting glTF files to USDZ using Python. By leveraging Docker, the solution overcomes platform-specific challenges and simplifies dependency management, while the modular design allows for easy integration and future enhancements.

Through careful design and implementation, this pipeline offers a scalable solution that can be extended for batch processing, integrated into web services, or optimized further for specific workflows. I hope this guide serves as a valuable resource for anyone looking to bridge the gap between glTF and USDZ in their 3D and AR projects.

Feel free to use, modify, and extend this solution to suit your needs. I welcome any feedback or suggestions for further improvements.