Skip to main content

How to Convert OBJ to GLB (Single Files & Batch)

You've inherited a folder of 500 OBJ files from a client. Or your 3D asset library is all OBJ from years of legacy workflows. Now you need them as GLB for a web project, and the deadline is next week.

OBJ to GLB conversion sounds simple until you actually try it. Missing textures, broken materials, file path issues - there's a reason "OBJ hell" is a common phrase among 3D developers.

Here's how to handle it properly, whether you're converting one file or a thousand.

Why Convert OBJ to GLB?

OBJ was designed in the 1980s. It's lasted this long because it's simple and universal - almost every 3D tool can read and write OBJ. But for modern web delivery, it has serious limitations:

Feature OBJ GLB
File structure Multiple files (OBJ + MTL + textures) Single binary file
Compression None (text format) Draco mesh compression
PBR materials Limited (no metallic/roughness) Full PBR support
Animations Not supported Fully supported
Web loading Requires multiple requests Single request
AR compatibility Not supported Direct support

For any web, mobile, or AR application, GLB is the standard. OBJ is for archival and interchange, not delivery.

The OBJ File Structure Problem

An OBJ file alone contains only geometry. Everything else lives in separate files:

product/
├── model.obj          # Geometry only
├── model.mtl          # Material definitions
├── diffuse.jpg        # Referenced by MTL
├── normal.png         # Referenced by MTL
└── roughness.png      # Referenced by MTL

The MTL file references textures by path. If those paths are wrong (absolute paths from another machine, or relative paths that don't match your folder structure), the conversion fails silently - you get geometry with no materials.

This is where most OBJ conversions break.

Manual Conversion: Blender

Blender is the most reliable free option for OBJ to GLB conversion.

Step 1: Import the OBJ

File → Import → Wavefront (.obj). Check "Split by Object" and "Split by Group" if you want to preserve object hierarchy.

Step 2: Fix Material Paths

If textures are missing:

  1. Select the object
  2. Go to the Material tab in Properties
  3. Expand the texture nodes
  4. Re-link each texture file manually

For many objects, this is tedious. Blender's "Find Missing Files" (File → External Data → Find Missing Files) can help, but it's not always reliable.

Step 3: Export as GLB

File → Export → glTF 2.0 (.glb). Key settings:

  • Format: glTF Binary (.glb)
  • Include: Check "Selected Objects" if you don't want the whole scene
  • Mesh: Enable Draco compression for smaller files
  • Materials: Ensure "Export Materials" is checked

Time per file: 5-15 minutes depending on complexity and texture issues.

Batch Conversion in Blender (Python)

For multiple files, use Blender's Python scripting:

import bpy
import os
import glob

input_dir = "/path/to/obj/files"
output_dir = "/path/to/glb/output"

# Clear the scene
bpy.ops.wm.read_factory_settings(use_empty=True)

for obj_file in glob.glob(os.path.join(input_dir, "*.obj")):
    # Clear scene
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete()

    # Import OBJ
    bpy.ops.wm.obj_import(filepath=obj_file)

    # Export GLB
    basename = os.path.splitext(os.path.basename(obj_file))[0]
    output_path = os.path.join(output_dir, f"{basename}.glb")

    bpy.ops.export_scene.gltf(
        filepath=output_path,
        export_format='GLB',
        export_draco_mesh_compression_enable=True
    )

    print(f"Converted: {basename}")

Run with: blender --background --python convert_batch.py

This works, but doesn't handle missing textures - you'll still need to verify each output.

Automated Conversion: API

For reliable batch conversion with automatic texture handling:

Single File (with textures in ZIP)

The most reliable approach: bundle OBJ + MTL + textures into a ZIP:

# Create a ZIP with all dependencies
zip -r model.zip model.obj model.mtl *.jpg *.png

# Convert via API
curl -X POST https://webdeliveryengine.com/optimize \
  -H "Authorization: Bearer sk_your_key" \
  -F "[email protected]" \
  -F "mode=decimate" \
  -F "ratio=0.8" \
  --output model.glb

The API extracts the ZIP, resolves texture paths automatically, converts to GLB, and optionally optimizes in one step.

Batch Processing Script

#!/bin/bash

INPUT_DIR="./obj_files"
OUTPUT_DIR="./glb_output"
API_KEY="sk_your_key"

mkdir -p "$OUTPUT_DIR"

# Process each OBJ file
for obj_file in "$INPUT_DIR"/*.obj; do
    basename=$(basename "$obj_file" .obj)
    mtl_file="$INPUT_DIR/${basename}.mtl"
    zip_file="/tmp/${basename}.zip"

    echo "Processing: $basename"

    # Create ZIP with OBJ, MTL, and associated textures
    cd "$INPUT_DIR"

    # Find textures referenced in MTL (if it exists)
    if [ -f "$mtl_file" ]; then
        # Extract texture filenames from MTL
        textures=$(grep -E "map_|bump|disp" "${basename}.mtl" | \
                   awk '{print $2}' | \
                   xargs -I {} basename {})

        # Create ZIP with all files
        zip -j "$zip_file" "${basename}.obj" "${basename}.mtl" $textures 2>/dev/null
    else
        # Just the OBJ if no MTL
        zip -j "$zip_file" "${basename}.obj"
    fi

    cd - > /dev/null

    # Convert via API
    curl -s -X POST https://webdeliveryengine.com/optimize \
        -H "Authorization: Bearer $API_KEY" \
        -F "file=@$zip_file" \
        -F "mode=decimate" \
        -F "ratio=0.8" \
        --output "$OUTPUT_DIR/${basename}.glb"

    # Cleanup
    rm "$zip_file"

    echo "Done: $basename.glb"
done

echo "Batch conversion complete!"

Node.js Batch Script

const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const FormData = require('form-data');

const INPUT_DIR = './obj_files';
const OUTPUT_DIR = './glb_output';
const API_KEY = process.env.MESHOPT_API_KEY;

async function convertObjToGlb(objPath) {
    const basename = path.basename(objPath, '.obj');
    const dir = path.dirname(objPath);

    // Create temp ZIP
    const zipPath = `/tmp/${basename}.zip`;
    const output = fs.createWriteStream(zipPath);
    const archive = archiver('zip');

    archive.pipe(output);

    // Add OBJ file
    archive.file(objPath, { name: `${basename}.obj` });

    // Add MTL if exists
    const mtlPath = path.join(dir, `${basename}.mtl`);
    if (fs.existsSync(mtlPath)) {
        archive.file(mtlPath, { name: `${basename}.mtl` });

        // Parse MTL for texture references
        const mtlContent = fs.readFileSync(mtlPath, 'utf8');
        const textureRegex = /(?:map_|bump|disp)\s+(.+)/g;
        let match;

        while ((match = textureRegex.exec(mtlContent)) !== null) {
            const textureName = path.basename(match[1].trim());
            const texturePath = path.join(dir, textureName);

            if (fs.existsSync(texturePath)) {
                archive.file(texturePath, { name: textureName });
            }
        }
    }

    await archive.finalize();

    // Wait for ZIP to be written
    await new Promise(resolve => output.on('close', resolve));

    // Upload to API
    const form = new FormData();
    form.append('file', fs.createReadStream(zipPath));
    form.append('mode', 'decimate');
    form.append('quality', '80');

    const response = await fetch('https://webdeliveryengine.com/optimize', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${API_KEY}` },
        body: form
    });

    // Save GLB
    const buffer = Buffer.from(await response.arrayBuffer());
    const outputPath = path.join(OUTPUT_DIR, `${basename}.glb`);
    fs.writeFileSync(outputPath, buffer);

    // Cleanup
    fs.unlinkSync(zipPath);

    console.log(`Converted: ${basename}.glb`);
}

// Process all OBJ files
async function main() {
    fs.mkdirSync(OUTPUT_DIR, { recursive: true });

    const objFiles = fs.readdirSync(INPUT_DIR)
        .filter(f => f.endsWith('.obj'))
        .map(f => path.join(INPUT_DIR, f));

    for (const objFile of objFiles) {
        await convertObjToGlb(objFile);
    }

    console.log('Batch conversion complete!');
}

main().catch(console.error);

Common Problems & Solutions

Problem: White/Gray Model (No Textures)

The textures didn't transfer. Check:

  • Is the MTL file present and correctly named?
  • Are texture paths in MTL relative or absolute?
  • Are all referenced texture files present?

Fix: Use the ZIP approach to bundle everything together. The API will resolve paths automatically.

Problem: Model Appears Inside-Out

OBJ files sometimes have inconsistent face winding (normals pointing inward).

Fix: In Blender, select the mesh, go to Edit Mode, select all faces, and use Mesh → Normals → Recalculate Outside.

Problem: Model is Rotated Wrong

OBJ uses Y-up or Z-up depending on the source software. GLB is Y-up.

Fix: Apply rotation in Blender before export, or handle it in your Three.js code:

model.rotation.x = -Math.PI / 2; // Z-up to Y-up

Problem: Scale is Wrong

OBJ has no unit system - a "1" could be 1 meter, 1 inch, or 1 mile.

Fix: Scale in Blender before export, or handle in code. Check the source software's export settings.

Problem: Multiple Objects Merged

Some OBJ exporters combine everything into one mesh.

Fix: Use "Split by Object" and "Split by Group" during OBJ import in Blender. Or accept the merge - fewer meshes means fewer draw calls.

Conversion + Optimization

Since you're already converting, this is the perfect time to optimize:

curl -X POST https://webdeliveryengine.com/optimize \
  -H "Authorization: Bearer sk_your_key" \
  -F "[email protected]" \
  -F "mode=decimate" \
  -F "ratio=0.5" \
  --output model-optimized.glb

One API call handles:

  1. OBJ/MTL/texture parsing
  2. Polygon reduction
  3. Texture optimization
  4. Draco compression
  5. GLB export

Your 30MB OBJ folder becomes a 2MB GLB ready for web delivery.

Verifying Your Conversions

After batch conversion, verify the results:

  1. Use our GLB Inspector to check each file
  2. Look for missing materials (material count should match original)
  3. Check polygon counts are reasonable
  4. Verify textures loaded (texture count > 0 if original had textures)

For large batches, spot-check 10-20% of files visually in a 3D viewer.

Summary

OBJ to GLB conversion is straightforward for single files with co-located textures. It gets complicated with:

  • Broken texture paths
  • Missing MTL files
  • Absolute paths from another machine
  • Hundreds of files to process

The ZIP approach solves most of these problems by bundling everything together and letting the conversion tool resolve paths.

For one-off conversions, Blender works fine. For batch processing legacy catalogs, an automated pipeline saves days of manual work.

Convert & Optimize in One Step

Upload your OBJ files (or ZIPs) and get optimized GLB output. free credits to start.

Start Converting