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:
- Select the object
- Go to the Material tab in Properties
- Expand the texture nodes
- 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:
- OBJ/MTL/texture parsing
- Polygon reduction
- Texture optimization
- Draco compression
- GLB export
Your 30MB OBJ folder becomes a 2MB GLB ready for web delivery.
Verifying Your Conversions
After batch conversion, verify the results:
- Use our GLB Inspector to check each file
- Look for missing materials (material count should match original)
- Check polygon counts are reasonable
- 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.