How to Load a .MTL File for a .OBJ File?

I never write on forums looking for answers. I always feel I need to gut it out myself. But I am completely stumped on this.

I have spent two whole days looking for solutions to this simple problem and don’t find any examples showing how to do this.

One fellow said he just drops the .MTL file somehow uses a default file. Having worked with the .OBJ format for years I was surprised to hear there was such a thing. I have no idea how set that up.

Another fellow said the way to do it was to pass in a URL in the OBJ to point to it. Well, I set up a Python server that dishes the .MTL file and when I put

localhost:8000/OBJ_Color_Library.mtl

in the Browser address it pops open File Manager on that file, and the Python server shows the file has a GET.

But I put:

mtllib localhost:8000/OBJ_Color_Library.mtl

in the .OBJ file and nothing happens.

I would really rather just pass a blob thru a URL, which works great with the OBJ data content, but the MTL content remains ignored.

There is no clear definitive documentation on how to do this, even though there is this MTLParser out there I have tried.

Here’s another approach I tried:

This fails also in all is variations.

I find the assertion that”

“Babylon.js will know how to load the obj file and its mtl file automatically”

at

to be quite ironic. Babylon may know how to do it, but nobody else does.

All you have to do is place the .mtl file somewhere and point at it from the .obj file.

For eg, from scenes/StanfordBunny.obj of the Playground public repository, line 3:

There’s a StanfordBunny.mtl file in the same location than StanfordBunny.obj and it is being used when loading the file:

It is not obvious it creates the material from the .mtl file because the diffuse color is grey in the .mtl file (Kd 0.80000 0.800000 0.80000), but I have modified the .mtl file in my local installation to set a green diffuse texture (Kd 0.0000 0.800000 0.0000):

Not sure about your use case, but check out this thread, maybe it helps

Keywords: MTLFileLoader and Tools.PreprocessUrl.

Also as I understand, usually when you export OBJ + MTL files, if they are in the same directory MTL will load automatically when you load OBJ. But might depend on the creation/export process.

As you can see from my two playgrounds, I have loaded the OBJ data via a Blob/URL:

    const OBJ_ArrayBuffer = OBJ_String;
    const OBJ_Blob = new Blob([OBJ_ArrayBuffer]);
    const OBJ_Url = URL.createObjectURL(OBJ_Blob);

        BABYLON.SceneLoader.Append(OBJ_Url, undefined, scene, function (scene) {
            scene.createDefaultCameraOrLight(true, true, true);
            scene.activeCamera.alpha += Math.PI;
        }, undefined, undefined, ".obj");

  1. Is there any option for referencing an MTL file in this scenario? As you can see in my playgrounds, I have tried multiple methods.

Is the only way to include an MTL reference thru an actual .OBJ file written to disk somewhere?

2… If so, where are these files to be physically located:

mtllib StanfordBunny.mtl
          o StanfordBunny

Do you just place those in the root of your website?

The path you use in the mtllib command in the .obj file should be relative to the .obj file, so intead of mtllib localhost:8000/OBJ_Color_Library.mtl do mtllib OBJ_Color_Library.mtl if OBJ_Color_Library.obj is at the root of your web server.

If you have a mtl subfolder and put the .mtl file in it, you can do mtllib mtl/OBJ_Color_Library.mtl, again assuming OBJ_Color_Library.obj is at the root.

There’s currently no way to pass the content of the mtl file to the load method. However, looking at the code, it seems you could put a base64 encoded mtl content instead of the name of the file in the mtllib command, because the parameter after mtllib is parsed as a url… For this to work (in case it works, I didn’t test it!), you will have to pass an empty string as the root url in the append / importMesh method.

[…] Actually, I just tested it:

I have not had a chance to test it because I must leave my computers to go out in the wilds for a few days. I will check this out on Sunday when I get back.

Thank you so much for your help!

–Dave

1 Like

Thanks so much, guys. I think I finally have my OBJ and MTL working as desired.

See

However, I have these basic problems with what the Playground is doing in with my OBJ and MTL file in comparison with what my desktop programs do with it. Here’s MS-Office and Windows 3D viewer’s instant responses on being loaded with this data:

  1. The desktop viewers instantly display something. The Playground is initially blank. It’s not until you hit it with the mouse and try twirling things that something shows up.

  2. There’s none of the clearly defined edges of the faces and the sharp shadowing of the desktop viewers.

What do I need to do here to achieve instant display with clearly defined edges?

Also I should mention that it seems like you can rotate the object in the playground to make it invisible. I don’t want that behavior either. The desktop apps don’t do it.

You don’t provide normals, so you should create them yourself (you can call Mesh.createNormals() to let the system create them).

To clearly see the faces, you can then call Mesh.convertToFlatShadedMesh() so that normals are computed only relatively to each face and not to adjacent faces.

Lastly, if you want to see your mesh even from the back, you should disable back face culling no the material:

Evgeni, you are such an incredible help. I would have spent months figuring that out I expect.

I have adjusted the parameters a little to get the camera position where I want it.

But I wonder if you could point me in the right direction to make this material a bit more shiny or glossy.

What I am really trying to accomplish is to make the contours and edges of this object as clearly distinct as possible. Any suggestions? Thanks again for all your help!

You could use the edge renderer, for eg:

Or the screen space curvature post process:

Holy cow! That is fantastic! I love it! It sounds like ScreenSpaceCurvaturePostProcess was invented precisely for this problem! And I’d been thinking about putting colored lines on the edges for application reasons, and here you’ve given me that option right out of Bablylon! What a system! What a phenomenal user support community! You folks are fantastic! Thank you so much!

My method to load .obj, .mtl and image from local hard disk overriding CORS restriction: just ask the user to select all the needed files, then process them accordingly to create proper filecontents in memory and load into babylon scene. (Note: AI helped a lot in this!)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Carica un OBJ in Babylon.js</title>
    <script src="babylon.js"></script>
    <script src="babylonjs.loaders.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput" multiple accept=".obj,.mtl,.png,.jpg.jpeg,.gif,.bmp,.webp" />
    <canvas id="renderCanvas" style="width: 100%; height: 100%;"></canvas>

    <script>
        const canvas = document.getElementById("renderCanvas");
        const engine = new BABYLON.Engine(canvas, true);

        // Crea una scena di base
        const createScene = () => {
            const scene = new BABYLON.Scene(engine);
            scene.useRightHandedSystem = true; // Fix babylon reference frame
            const axes = new BABYLON.AxesViewer(scene, 2); // Add cartesian axes

                // Camera
            const  camera = new BABYLON.ArcRotateCamera("camera1", 0, 0, 0, new BABYLON.Vector3(0, 0, -0), scene);
            camera.setPosition(new BABYLON.Vector3(0, 0, -10));
            camera.attachControl(canvas, true);
            camera.wheelDeltaPercentage = 0.01;
            camera.attachControl(canvas, true);


            const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
            const light2 = new BABYLON.PointLight("light", new BABYLON.Vector3(0, 1, 0), scene);
            scene.addLight(light);
            scene.addLight(light2);

            return scene;
        };

        const scene = createScene();

////////////////////////////

        const readFileAsText = (file) => {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = () => resolve(reader.result);
                reader.onerror = reject;
                reader.readAsText(file);
            });
        };

        // Read a file and convert into base64 format:
        const fileToDataURL = (file) => {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = () => resolve(reader.result);
                reader.onerror = reject;
                reader.readAsDataURL(file);
            });
        };

///////////////////////////////

        // Process loaded files
        const processFiles = async (files) => {
            // Receive a "files" array from fileInput control in HTML page and process them
            // It's mandatory to provide 3 files: .obj, .mtl and an image file
            fileMap = {};
            for (let file of files) {
                fileMap[file.name] = file;
            }

            // Find files .obj, .mtl e .png
            objFile = Object.keys(fileMap).find(name => name.endsWith(".obj"));
            mtlFile = Object.keys(fileMap).find(name => name.endsWith(".mtl"));

            const supportedExtensions = [".png", ".gif", ".bmp", ".jpeg", ".jpg", ".webp"];
            textureFile = Object.keys(fileMap).find(name =>
                supportedExtensions.some(ext => name.toLowerCase().endsWith(ext))
            );

            //textureFile = Object.keys(fileMap).find(name => name.endsWith(".png"));

            if (!objFile || !mtlFile || !textureFile) {
                alert("Please load a file .obj, a file .mtl and a texture (.png).");
                return;
            }

            let objContent = await readFileAsText(fileMap[objFile]);
            objContent = ZdownToZin(objContent);
            let mtlContent = await readFileAsText(fileMap[mtlFile]);
            const textureBase64 = await fileToDataURL(fileMap[textureFile]);

            // Replace image filename in .mtl file (after "map_Kd") by BASE64 coding of the image      <<<<<<------------
            mtlContent = mtlContent.replace(/map_Kd\s+[^\s]+/, `map_Kd ${textureBase64}`);

            const numMeshes = scene.meshes.length; // Pre-load number of existing meshes

            // Load .obj file; may raise error due to "mtllib" directive, but execution keeps going    <<<<<<-----------
            BABYLON.SceneLoader.Append("", "data:" + objContent, scene, (scene) => {}, undefined, undefined, ".obj");

            // Put in babylon scene material taken from modified .mtl file
            var mtlLoader = new BABYLON.MTLFileLoader().parseMTL(scene, mtlContent, "");


            //////// Assign material to object
            finalMat = null;
            materialName = "texture_" + files[0].name.split(".")[0];
            scene.materials.forEach(mat => {
                if (mat.id === materialName) {
                    finalMat = mat;
                }
            });
            scene.meshes[numMeshes].material = finalMat; // Material is assigned to the last-loaded mesh
            scene.meshes[numMeshes].material.backFaceCulling = false; // Make material visible on both faces

        };


////////////////////////////

        // Listener for file loading
        const fileInput = document.getElementById("fileInput");
        fileInput.addEventListener("change", async (event) => {
            const files = event.target.files;
            if (files.length > 0) {
                await processFiles(files);
            }
        });





        // Avvia il rendering
        engine.runRenderLoop(() => {
            scene.render();
        });

        window.addEventListener("resize", () => {
            engine.resize();
        });



        function ZdownToZin(objFileContent) {
            /* Convert from reference system with Z+ downward, Y right and X into screen to
             * reference system with Z+ to inside screen and Y+ upward (babylon).
            */

            const lines = objFileContent.split("\n");

            // Map rows
            const transformedLines = lines.map(line => {
                // Vertex line starts by "v"
                if (line.startsWith("v ")) {
                    const parts = line.split(" ");

                    //////// Swap
                    const x = parts[1];
                    const y = parts[2];
                    const z = parts[3];
                    return `v ${x} ${-z} ${y}`;
                }

                // Return unchanged lines
                return line;
            });

            //Merge evwerything
            return transformedLines.join("\n");
        }

    </script>
</body>
</html>