Preprocessing 3D Assets with Loci

Level up your asset preprocessing workflow using our suite of endpoints.

This guide demonstrates how to create an efficient preprocessing pipeline using Loci's API endpoints.

By following this guide, you'll learn how to:

  • Extract technical metadata from your 3D assets.
  • Generate 2D renders of your asset in the cloud.
  • Optimize your assets by compressing and converting them to the .glb file format.
  • Instantly view your assets in Loci's interactive 3D web viewer.

3D Preprocessing Pipeline: Loci's Endpoints

Before assembling our pipeline, let's go through the endpoints we will be using in this example.

Metadata Extraction

Loci's metadata endpoint (/3d/metadata)provides detailed information about your model's structure, including the number of vertices, edges, polygons, animations and more.

To call the endpoint, we can pass in our asset file. You can find out more about our supported formats through this link. An example written in Python can be found below:

import requests

# Setup
API_URL = "https://loci-api.com/3d/metadata"
API_KEY =  "<<x-api-key>>" # YOUR API KEY HERE
ASSET_FILE = "/path/to/my_asset_file.glb"

headers = {"x-api-key": API_KEY}
with open(ASSET_FILE, "rb") as file:
    files = {"file": file}

    # Call API
    response = requests.post(API_URL, headers=headers, files=files)

Once ran, you will get a response like this:

{
  "filename": "string",  // Name of the 3D model file
  "metadata": {
    "num_vertices": "integer",  // Total number of vertices in the model
    "num_edges": "integer",     // Total number of edges
    "num_polygons": "integer",  // Total number of polygons/faces
    "num_submeshes": "integer", // Number of separate mesh components
    "num_animations": "integer", // Number of animation sequences
    "animation_names": "array",  // List of animation sequence names
    "num_materials": "integer",  // Number of materials used
    "material_names": [         // Array of material identifiers
      "string"
    ],
    "num_texture_images": "integer", // Number of texture files
    "texture_names": "array",        // List of texture filenames
    "mesh_names": [                  // Array of mesh component names
      "string"
    ],
    "is_rigged": "boolean",    // Whether the model has a skeleton/rig
    "dimensions": {            // Model's bounding box dimensions
      "x": "float",           // Width
      "y": "float",           // Height
      "z": "float"            // Depth
    }
  }
}

2: Generating A Render

The 3D Render endpoint (/3d/render) generates an image thumbnail of your asset looks like.

The following code snippet will send an asset to our rendering endpoint:

import requests

# Setup
API_URL = "https://loci-api.com/3d/render"
API_KEY =  "<<x-api-key>>" # YOUR API KEY HERE
ASSET_FILE = "/path/to/my_asset_file.glb"

headers = {"x-api-key": API_KEY}
with open(ASSET_FILE, "rb") as file:
    files = {"file": file}

    # Call API
    response = requests.post(API_URL, headers=headers, files=files)

And in return, you will get a response containing a 384×384 base64 encoded .png image.

{
  "filename": "LittleBookcase.glb",
  "render_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAY...."
}
Example of a render returned by Loci's render endpoint

Example of a render returned by Loci's render endpoint

3: Optimizing Your Asset

Next, we'll optimize the asset for web delivery using our optimization endpoint (/3d/optimize-for-web). This step converts your model to the glTF format and applies Draco compression and texture compression, which can decrease filesizes by up to 90%. As before, we will send our asset to the endpoint like so:

import requests

# Setup
API_URL = "https://loci-api.com/3d/optimize-for-web"
API_KEY =  "<<x-api-key>>" # YOUR API KEY HERE
ASSET_FILE = "/path/to/my_asset_file.glb"

headers = {"x-api-key": API_KEY}
with open(ASSET_FILE, "rb") as file:
    files = {"file": file}

    # Call API
    response = requests.post(API_URL, headers=headers, files=files)

The optimization process does the following:

  • Converts the asset to glTF format (returning a .glb file)
  • Applies Draco compression to geometry
  • Compresses textures
  • Removes background planes

Once ran, your asset will be returned as a ready-to-download link, like so:

{
  "filename": "the_craftsman.glb",
  "url": "https://loci-asset-downloads-prod.s3.amazonaws.com/..."
}

Building a Processing Pipeline & Storing Your Results

Now we know what each of these endpoints does, let's create a practical example where we create a pipeline for processing multiple 3D assets that are stored together in a folder. This example demonstrates how to:

  • Process multiple files from a directory.
  • Handle rate limiting and errors.
  • Store our results for later use or ingestion.
# pip install aiometer
# pip install httpx
import asyncio
from functools import partial
from http import HTTPStatus
from time import time
import aiometer
import httpx
from pathlib import Path
import json
from datetime import datetime

ASSET_DIRECTORY = "path/to/your/3d/assets"  # Directory containing 3D assets
MAX_ASSETS_PER_SECOND = 5
LOCI_API_KEY = "<<x-api-key>>" # Your Loci API key
SUPPORTED_EXTENSIONS = {'.glb', '.gltf', '.obj', '.fbx'} # The extensions we want to select by
BASE_URL = "https://loci-api.com"

async def process_one_asset(file_path: Path, client: httpx.AsyncClient):
    """Process a single 3D asset through all Loci API endpoints."""
    result = {}
    
    try:
        files = {
            "file": (file_path.name, open(file_path, "rb"), "application/octet-stream")
        }
        
        headers = {
            "x-api-key": LOCI_API_KEY,
            "Accept": "application/json"
        }

        # Get metadata
        r_metadata = await client.post(
            f"{BASE_URL}/3d/metadata",
            files=files,
            headers=headers
        )
        if r_metadata.status_code == HTTPStatus.OK:
            result["metadata"] = r_metadata.json()
        
        # Optimize for web
        r_optimize = await client.post(
            f"{BASE_URL}/3d/optimize-for-web",
            files=files,
            headers=headers
        )
        if r_optimize.status_code == HTTPStatus.OK:
            result["optimized"] = r_optimize.json()
            
        # Generate render
        r_render = await client.post(
            f"{BASE_URL}/3d/render",
            files=files,
            headers=headers
        )
        if r_render.status_code == HTTPStatus.OK:
            result["render"] = r_render.json()
            
        # Set success if we got through all endpoints
        if all(k in result for k in ["metadata", "optimized", "render"]):
            result["status"] = "success"
            print(f"Asset {file_path.name} processed successfully")
        else:
            result["status"] = "partial"
            print(f"Asset {file_path.name} partially processed")
            
    except Exception as e:
        print(f"Error processing {file_path.name}: {str(e)}")
        result["status"] = "error"
        result["error"] = str(e)
    
    return result

async def batch_process_assets(directory: str, client: httpx.AsyncClient):
    """Process all supported 3D files in a directory."""
    _start = time()
    
    directory_path = Path(directory)
    if not directory_path.exists():
        raise ValueError(f"Directory not found: {directory}")
        
    files = [
        f for f in directory_path.glob('**/*')
        if f.suffix.lower() in SUPPORTED_EXTENSIONS
    ]
    
    if not files:
        print(f"No supported files found in {directory}")
        return {}
        
    print(f"Found {len(files)} files to process")
    
    jobs = [partial(process_one_asset, file_path, client) for file_path in files]
    results = await aiometer.run_all(jobs, max_per_second=MAX_ASSETS_PER_SECOND)
    
    print(f"Finished {len(files)} assets in {time() - _start:.2f} seconds")
    
    return {str(file_path): result for file_path, result in zip(files, results)}

def save_results(results: dict, output_file: str = "processed_assets.json"):
    """Save processing results with useful metadata."""
    output = {
        "processing_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "total_assets": len(results),
        "successful": sum(1 for v in results.values() if v.get("status") == "success"),
        "assets": results
    }
    
    with open(output_file, "w") as f:
        json.dump(output, f, indent=2)
        
    print(f"Results saved to {output_file}")

if __name__ == "__main__":
    # Create async client with no timeout
    client = httpx.AsyncClient(timeout=None)
    
    # Process assets and save results
    try:
        results = asyncio.run(batch_process_assets(ASSET_DIRECTORY, client=client))
        save_results(results)
    finally:
        # Ensure client is closed
        asyncio.run(client.aclose())

After this has been run, your results will be stored like this:

{
    "processing_date": "2025-01-16 10:30:00",
    "total_assets": 50,
    "successful": 48,
    "assets": {
        "path/to/model1.glb": {
            "status": "success",
            "metadata": { ... },
            "optimized": { ... },
            "render": { ... }
        },
        "path/to/model2.glb": {
            "status": "error",
            "error": "Request timeout"
        }
        // ... more assets
    }
}

This is just a starting point: you can add to and edit any of these endpoints according to your requirements. Loci's suite of endpoints includes AI-powered tagging, captioning, classification and many more.

Loci's outputs can be seamlessly integrated into search services like Elasticsearch (tutorial link) and Algolia (tutorial link).

To view this code as a step-by-step tutorial, see the recipe below:

Viewing Your Assets in Loci's 3D Viewer

After processing, you can view your optimized 3D asset using the Loci viewer component. The Loci viewer provides an intuitive way to interact with 3D models directly in the browser, offering features like rotation, zoom, and pan controls. Integration is as simple as embedding a single custom element on your webpage, like below:

<!-- import the script !-->
<script type="module" src="path/to/loci-viewer.min.js"></script> 
<!-- embed the viewer !-->
<loci-viewer src="your-optimized-model.glb" autoRotate></loci-viewer>

Access to the Loci Viewer library is available on request. Please contact us for more information.