OBJ Loads, MTL is always shiny and dark when Texture is mapped

Hello, I have got the OBJ loading every time and then I thought I would try loading the MTL and associated texture. However, initially the simple turtorial that I was following would constantly complain about the texture file. Although the texture file works.

Attached is the image of the OBJ loaded in CloudCompare.

Then this is my little app at the moment with loading just the OBJ and not the MTL and Texture. The material is simple

const phong_material = new MeshPhongMaterial({ color: 0xffffff, side: DoubleSide, flatShading: true });

Then this is the result of the OBJ loaded with the MTL and Texture so that it will hopefully look like the CloudCompare Image … but you guessed it doesn’t

This OBJ opens in MeshLab, CloudCompare and @GitHubDragonFly 's Viewer…

Hopefully someone with more experience can see the issues.
Below is the entire fileOBJLoader.js only 127 lines with lots of console outputs.
You can find the App at https://blastingapps.xyz/dist/index.html

import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
import { TextureLoader, MeshPhongMaterial, DoubleSide, Vector3, Box3 } from "three";
import { params } from "../../drawing/createScene.js";
import { updateGuiControllers } from "../../settings/worldOriginSetting.js";

export function handleOBJNoEvent(files, canvas) {
	const objFile = files.find((file) => file.name.endsWith(".obj"));
	const mtlFile = files.find((file) => file.name.endsWith(".mtl"));
	const textureFiles = files.filter((file) => file.type.startsWith("image/"));

	//Exit when no file is found
	if (!objFile) {
		console.error("OBJ file not found");
		return;
	}

	const reader = new FileReader();

	reader.onload = function (event) {
		const contents = event.target.result;
		alert("OBJ file loaded successfully.\nFile name: " + objFile.name);

		const objLoader = new OBJLoader();

		if (mtlFile) {
			const mtlReader = new FileReader();
			mtlReader.onload = function (mtlEvent) {
				const mtlContents = mtlEvent.target.result;
				const mtlLoader = new MTLLoader();
				const materials = mtlLoader.parse(mtlContents);

				console.log("Materials info keys:", Object.keys(materials.materialsInfo));
				console.log("Parsed materials:", materials.materialsInfo);

				const textureLoader = new TextureLoader();
				textureFiles.forEach((textureFile) => {
					const url = URL.createObjectURL(textureFile);
					textureLoader.load(url, (texture) => {
						console.log("Texture loaded:", texture);
						// Apply the texture to the material
						if (materials.materialsInfo["texture"]) {
							console.log("Applying texture ", textureFile.name, " to material 'texture'");

							const material = new MeshPhongMaterial({ color: 0xffffff, side: DoubleSide, map: texture });
							//adjust texture map settings to remove shiny effect
							material.shininess = 0;
							material.specular = 0xffffff;
							material.flatShading = true;

							material.needsUpdate = true;
							console.log("Material after texture application", material);
						}
					});
				});

				materials.preload();
				objLoader.setMaterials(materials);

				const object = objLoader.parse(contents);
				processLoadedObject(object, canvas);
			};
			mtlReader.readAsText(mtlFile);
		} else {
			const object = objLoader.parse(contents);
			processLoadedObject(object, canvas);
		}
	};

	reader.onerror = function (error) {
		console.error("Error reading the OBJ file:", error);
	};

	reader.readAsText(objFile);
}

function processLoadedObject(object, canvas) {
	const boundingBox = new Box3().setFromObject(object);
	const center = boundingBox.getCenter(new Vector3());

	const offsetX = params.worldXCenter !== 0 ? params.worldXCenter : center.x;
	const offsetY = params.worldYCenter !== 0 ? params.worldYCenter : center.y;

	//set the world center
	if (params.worldXCenter === 0 || params.worldYCenter === 0) {
		params.worldXCenter = center.x;
		params.worldYCenter = center.y;
		updateGuiControllers();
	}

	object.position.set(0, 0, 0);
	object.scale.set(1, 1, 1);
	object.name = object.name;

	//backup material for objects without mtl file
	const phong_material = new MeshPhongMaterial({ color: 0xffffff, side: DoubleSide, flatShading: true });

	object.traverse(function (child) {
		if (child.isMesh) {
			const position = child.geometry.attributes.position;
			for (let i = 0; i < position.count; i++) {
				//offset as the objects are in UTM and real world coordinates and float32 precision is not enough - offset to 0,0
				position.setXYZ(i, position.getX(i) - offsetX, position.getY(i) - offsetY, position.getZ(i));
			}

			child.geometry.computeVertexNormals();
			position.needsUpdate = true;

			//Got no materila or texture? - apply default material
			if (!child.material || !child.material.map) {
				child.material = phong_material;
				child.material.needsUpdate = true;
			}

			child.geometry.computeBoundingBox();
			child.geometry.computeBoundingSphere();

			canvas.scene.add(child);
		}
	});

	if (params.debugComments) {
		console.log("Loaded OBJ position:", object.position);
		console.log("Loaded OBJ rotation:", object.rotation);
		console.log("Loaded OBJ scale:", object.scale);
	}
}

A small OBJ to use as a test.
https://blastingapps.xyz/downloads/small.mtl
https://blastingapps.xyz/downloads/small.obj
https://blastingapps.xyz/downloads/231208_PIT_texture.jpg

Thanks in advance

Looks like you’re doing a bunch of extra stuff there…

You might want to try the simpler approach in the OBJLoader+MTLLoader sample here: three.js/examples/webgl_loader_obj_mtl.html at 1845f6cd0525b5c73b9da33c40e198c360af29f1 · mrdoob/three.js · GitHub

new MTLLoader()
	.load( 'small.mtl', function ( materials ) {
		materials.preload();
		new OBJLoader()
			.setMaterials( materials )
			.load( 'small.obj', function ( object ) {
				scene.add( object );
			});
	} );

It also looks like you don’t have any lights in your scene, so any material that interacts with lighting will just show up black. It’s possible this is what is happening… or it’s perhaps not finding your texture, or a combination of both.

I took your files and loaded them in my test app:

I had to scale the model down a bunch and add an ambient light to get it visible.

I also had to specify the path to my “assets” directory for the OBJ/MTL loaders.

Here’s the code i ended up with:



import {MTLLoader} from "three/addons/loaders/MTLLoader.js"
import {OBJLoader} from "three/addons/loaders/OBJLoader.js"
new MTLLoader()
    .setPath("./assets/")
	.load( 'small.mtl', function ( materials ) {
		materials.preload();
		new OBJLoader()
            .setPath("./assets/")
			.setMaterials( materials )
			.load( 'small.obj', function ( object ) {
				scene.add( object );
                object.scale.multiplyScalar(.01)
                let box = new THREE.Box3().setFromObject(object)
                let center = box.getCenter(new THREE.Vector3())
                object.position.sub(center)
			});
	} );

scene.add(new THREE.AmbientLight());

The model itself is a bit jittery when rendered, I think because the units in the file are very large.
You may want to scale the geometry itself down if possible into a more manageably range, if you see the vertices jumping around when rendering.

Edit: I added some code to re-center the mesh around it’s center of mass, and rescale it down to size 1, so that it doesn’t jitter.

new MTLLoader()
    .setPath("./assets/")
	.load( 'small.mtl', function ( materials ) {
		materials.preload();
		new OBJLoader()
            .setPath("./assets/")
			.setMaterials( materials )
			.load( 'small.obj', function ( object ) {
				scene.add( object );
                object.position.set(0,0,0)
                object.scale.set(1,1,1);
                let mesh = object.children[0]; //Grab the mesh (since we know it's child 0
                let box = new THREE.Box3().setFromObject(object)
                let center = box.getCenter(new THREE.Vector3())
                mesh.geometry.translate(-center.x,-center.y,-center.z); //Center the object by subtracting the center point from its vertices
                let size = box.getSize(new THREE.Vector3())
                let maxSz = Math.max(size.x,Math.max(size.y,size.z));
                let rescale = 1/maxSz;
//Compute the scaling factor to scale the model down to size 1 on its max axis
                mesh.geometry.scale(rescale,rescale,rescale);

                mesh.geometry.rotateX(Math.PI*-.5);//Rotate it to Y=up

//Now scale the parent object up so that it's in a nice visual range (100)
                object.scale.multiplyScalar(100.)
			});
	} );

Now it renders nice and jitter free:

(To be clear… the scaling issue is due to your 3d scan being in what looks like to be a 64 bit coordinate system, (global coordinate system?) so the units are very large, and lose accuracy when converted/rendered with 32 bit floats in WebGL, thus the need to rescale/recenter the model around the origin.)

1 Like

@BrentBuffham here is what I would suggest you try first, check your code and maybe change the following line:

console.log("Texture loaded:", texture);

to

console.log('Texture loaded: ', texture);
console.log('materials.materialsInfo: ', materials.materialsInfo);
console.log('materials.materialsInfo["texture"]: ', materials.materialsInfo["texture"]);

This way you can see what exists in materials.materialsInfo and whether materials.materialsInfo["texture"] is undefined (since you appear to be conditioning this by adding quotation marks around "texture").

Then you can try following what @manthrax suggested. His pictures suggest that there is nothing wrong with the example files you posted.

2 Likes

@BrentBuffham if everything logs properly as per my first suggestion, then just as an additional question, where exactly are you applying the first material you created and assigned the texture to the map:

const material = new MeshPhongMaterial({ color: 0xffffff, side: DoubleSide, map: texture });
1 Like

@BrentBuffham here is another code suggestion, which could possibly work and replace your textureFiles.forEach((textureFile) => { function :

const map_strings = [ 'bump', 'map_bump', 'disp', 'map_disp', 'norm', 'map_kn',
	'map_d', 'map_ka', 'map_kd', 'map_ke', 'map_emissive', 'map_ks', 'map_ns'
];

textureFiles.forEach((textureFile) => {
	const url = URL.createObjectURL(textureFile);

	for ( const [ key, value ] of Object.entries( materials.materialsInfo ) ) {
		map_strings.forEach( map_str => {
			if ( value[ map_str ] && value[ map_str ].endsWith( textureFile.name ) ) {
				value[ map_str ] = url;
			}
		});
	}

	URL.revokeObjectURL(textureFile);
});

This way you would check on multiple maps, modify the materialsInfo and the OBJ + MTL loaders might just load it for you without a need to load the texture manually. I did not test this particular code myself.

Just as a side note, after looking at your MTL file I noticed that your material was actually named texture so maybe try avoiding the names that might be used as a variable (which in your case is the output variable of the texture loader, which is produced upon loading the model).

In general, always try using unique and somewhat descriptive names.

Also, MTL files can have multiple materials, with each having a unique name, and each of those materials could possibly have multiple textures assigned to different slots.

There could also be some texture parameters included in the same line, for example:

  map_Kd -s 1 1 1 -o 0 0 0 truck.png

It’s all fine while testing but your app should not really rely on the users providing “clean” entries, so you would try circumventing that in your code.

1 Like

Right well I got it… not exactly how it was suggested but essentially the way @manthrax suggested thankyou all.

I believe I had it sorted about 3 days ago but I was trying to load a very large OBJ and it was not getting the texture.

As you can see below the source OBJ is very large and has an appropriately large JPG. I have tried a few thousand time and I can’t get the texture…

The smaller OBJ is an excerpt from the larger one. Are there specific limits to OBJs

The only other issue is the lack of vivid colour as I have seen other posts about this, it is most likely lighting.

// File: fileOBJLoader.js
// Dependencies: OBJLoader.js, MTLLoader.js, TextureLoader.js, MeshPhongMaterial.js, DoubleSide.js, Vector3.js, Box3.js, createScene.js, worldOriginSetting.js
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
import { TextureLoader, MeshPhongMaterial, DoubleSide, Vector3, Box3 } from "three";
import { params } from "../../drawing/createScene.js";
import { updateGuiControllers } from "../../settings/worldOriginSetting.js";

let materials;
let object;

/**
 * Handles the OBJ file import without triggering any events.
 * @param {FileList} files - The list of files selected by the user.
 * @param {HTMLCanvasElement} canvas - The canvas element to render the object on.
 */
export function handleOBJNoEvent(files, canvas) {
	console.clear();
	const objFile = files.find((file) => file.name.endsWith(".obj"));
	const mtlFile = files.find((file) => file.name.endsWith(".mtl"));
	const textureFile = files.find((file) => file.name.endsWith(".jpg") || file.name.endsWith(".png"));

	// Exit when no file is found
	if (!objFile) {
		console.error("00) OBJ file not found");
		return;
	}
	if (objFile) {
		console.log("00) OBJ file:", objFile.name);
	}
	if (mtlFile) {
		console.log("00) MTL file:", mtlFile.name);
	}
	if (textureFile) {
		console.log("00) Texture file:", textureFile.name);
	}

	if (mtlFile) {
		console.log("01) MTL File:", mtlFile);
		const fileMTL = mtlFile;
		const mtlReader = new FileReader();
		mtlReader.onload = function (e) {
			console.log("02) MTL Event Details:", e.target.result);
			const mtlLoader = new MTLLoader();
			const mtlContents = e.target.result;
			materials = mtlLoader.parse(mtlContents);
			materials.preload();

			if (textureFile) {
				const textureLoader = new TextureLoader();
				const textureReader = new FileReader();
				textureReader.onload = function (event) {
					console.log("03) Texture Event Details:", event.target.result);
					const texture = textureLoader.load(event.target.result);
					applyTextureToMaterials(texture);
					loadOBJFile(objFile, canvas);
				};
				textureReader.readAsDataURL(textureFile);
			} else {
				loadOBJFile(objFile, canvas);
				alert("04) No texture file selected");
			}
		};
		mtlReader.readAsText(fileMTL);
	} else {
		console.error("05) MTL file not found");
		alert("06) No MTL file found");
		loadOBJFile(objFile, canvas);
	}

	console.log("07) MTL:", mtlFile);
	console.log("08) OBJ:", objFile);
}

/**
 * Applies the given texture to the materials.
 * @param {Texture} texture - The texture to apply.
 */
function applyTextureToMaterials(texture) {
	if (materials) {
		for (const material of Object.values(materials.materials)) {
			//Attempt to condition the material to show the texture better
			material.map = texture;
			material.flatShading = true;
			material.side = DoubleSide;
			material.blending = 1;
			material.opacity = 0.5;

			console.log("09) Applying Texture:", material.map);
			material.needsUpdate = true;
		}
	}
}

/**
 * Loads the OBJ file and processes it.
 * @param {File} objFile - The OBJ file to load.
 * @param {HTMLCanvasElement} canvas - The canvas element to render the object on.
 */
function loadOBJFile(objFile, canvas) {
	console.log("10) OBJ File:", objFile);
	const fileOBJ = objFile;
	const objReader = new FileReader();
	objReader.onload = function (e) {
		console.log("11) OBJ Event Details:", e.target.result);
		const objLoader = new OBJLoader();
		if (materials) {
			console.log("12) Materials:", materials);
			objLoader.setMaterials(materials);
		}
		const contents = e.target.result;
		object = objLoader.parse(contents);
		processLoadedObject(object, canvas, materials);
	};
	objReader.readAsText(fileOBJ);
}

/**
 * Processes the loaded object and adds it to the canvas scene.
 * @param {Object3D} object - The loaded object.
 * @param {HTMLCanvasElement} canvas - The canvas element to render the object on.
 * @param {Material} materials - The materials to apply to the object.
 */
function processLoadedObject(object, canvas, materials) {
	const boundingBox = new Box3().setFromObject(object);
	const center = boundingBox.getCenter(new Vector3());
	console.log("13) Bounding Box:", boundingBox);
	const offsetX = params.worldXCenter !== 0 ? params.worldXCenter : center.x;
	const offsetY = params.worldYCenter !== 0 ? params.worldYCenter : center.y;

	// Set the world center
	if (params.worldXCenter === 0 || params.worldYCenter === 0) {
		console.log("14) World Center:", offsetX, offsetY);
		params.worldXCenter = center.x;
		params.worldYCenter = center.y;
		updateGuiControllers();
	}

	console.log("15) Setting the Objects Center:", offsetX, offsetY);
	object.position.set(0, 0, 0);
	object.scale.set(1, 1, 1);
	object.name = object.name;

	// Backup material for objects without MTL file
	const phong_material = new MeshPhongMaterial({ color: 0xffffff, side: DoubleSide, flatShading: true });

	object.traverse(function (child) {
		if (child.isMesh) {
			const position = child.geometry.attributes.position;
			for (let i = 0; i < position.count; i++) {
				// Offset as the objects are in UTM and real world coordinates and float32 precision is not enough - offset to 0,0
				position.setXYZ(i, position.getX(i) - offsetX, position.getY(i) - offsetY, position.getZ(i));
			}

			child.geometry.computeVertexNormals();
			position.needsUpdate = true;

			// Got no material or texture? - apply default material
			if (!child.material || !child.material.map) {
				console.log("16) child.material", child.material);
				child.material = phong_material;
				child.material.needsUpdate = true;
			} else {
				console.log("17) child.material", child.material);
				child.material.flatShading = true;
				child.material.side = DoubleSide;
				child.material.needsUpdate = true;
			}

			child.geometry.computeBoundingBox();
			child.geometry.computeBoundingSphere();

			canvas.scene.add(child);
		}
	});
}

I still have texture issues on very large OBJs that haven’t come from CloudCompare. So these must have an issue. The fact CloudCompare can deal with them means its possible … to get them in.

Anyhow thanks @manthrax and @GitHubDragonFly

EDIT: I needed SRGBColorSpace…
I added this line and the texture looks great.
texture.colorSpace = SRGBColorSpace;

1 Like

Nice! Glad you got it working.