How to change the color of two objects (or parts of them) gltf&

Hello to all. It is possible to change the color of objects (or parts of objects) loaded in the format gltf. An example here is https://threejs.org/examples/webgl_loader_gltf_variants.html. Description here https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_variants. Who knows how to change the color of two (or more) objects gltf (or parts of them) at the same time? For example, if there are two different sneakers next to each other?

After you’ve loaded a glTF file, it’s a three.js scene graph and the original format isn’t particularly relevant anymore. You can use the THREE.Mesh API to access mesh.material and modify any property of the material. The material is usually a THREE.MeshStandardMaterial.

If your glTF model does not already use KHR_materials_variants, I wouldn’t worry about the details of that particular example. There are many other ways to modify the scene.

Thank you for your reply. However, the gltf objects I load are made up of multiple children. I only change the color on one child of the object and use the KHR_materials_variants. But I need to change the color at the same time on the elements of several similar gltf objects, without creating its own GUI for each object.

three.js code (see Acess GLTF Values - #6 by donmccurdy) can change as many materials on as many meshes as you need. Figuring out which alternative materials go to which meshes would probably need to be customized for your application.

this may be a long shot but if you consider gltfjsx GitHub - pmndrs/gltfjsx: 🎮 Turns GLTFs into JSX components an option then all of this is quite easily possible without loader plugins. this will also re-use the models, so you can have variants without creating more geometries and you still re-use the materials that remain the same across the variants.

small example: Re-using GLTFs - CodeSandbox

You propose to me to search in each object for the element I need with the material I need and then change it with it. This only complicates the problem.
I have 50 objects GLTF in the scene. Each object has 2-4 elements with its own material and color. 45 objects GLTF move in a certain way when you double-click on it. I need to attach individual cube camera to each object. In each object, at the same time, one element must change material (color) when using the GUI. The issue with changing the material for one element of one object has been resolved (link above). But I need to change at the same time the material of the object element in 45 objects using GUI and KHR materials variants.

Many thanks. This is very valuable information. Maybe this will help me. This requires careful study.

Hello. Below is my code. It works. I have two objects GLTF (actually there are more) with the prescribed “KHR_materials_variants”. I add a cube camera to each object and assign a “envMap = cubeRenderTarget” to each object. A GUI panel has been added to each object. The colors of both objects can be switched in one panel. How do I hide the second panel (gui29)? Or redo the code so that there is only one panel?

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>salunMagia1</title>
		<linc rel="stylesheet" href="css/styles.css">		
		</head>			
	    <canvas id="sal6"></canvas>	    
	    <div id="stats"></div>	    
	    <div id="container"></div>	    
		<div id = "cursor" style="cursor:crosshair">	    
	    </div>	    
	    <div id="blocker">
        <div id="instructions">        
        </div>
        </div>
	<body>

		<!--<script type="module" src='bundle1-utf8.js'>-->
	   <script type="module">


import * as THREE from '../build/three.module.js';
import Stats from './jsm/libs/stats.module.js';

			import { OrbitControls } from './jsm/controls/OrbitControls.js';
			import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
  			import { PointerLockControls } from './jsm/controls/PointerLockControls.js';
			import { TrackballControls } from './jsm/controls/TrackballControls.js';
			import { FirstPersonControls } from './jsm/controls/FirstPersonControls.js';
			import { GUI } from './jsm/libs/dat.gui.module.js';
    var scene, camera, mesh;
    var camControls;
    var clock;	
	clock = new THREE.Clock();	    
  	var width = window.innerWidth;
	var height = window.innerHeight;	
	var canvas = document.getElementById('sal6');
	var blocker = document.getElementById('blocker');
	var statsNode = document.getElementById('stats');
	  canvas.setAttribute('width', width);
	  canvas.setAttribute('height', height);
	  
	  		scene = new THREE.Scene();
				
	    var renderer = new THREE.WebGLRenderer({canvas: canvas, antialias:true,alpha:true,transparent:true,premultipliedAlpha:false});
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0xffffff);
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.BasicShadowMap;
        renderer.gammaInput = true;
        renderer.gammaOutput = true;
        
        camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
		camera.position.set(0, 10, 15);
		var maxanisotropy=renderer.capabilities.getMaxAnisotropy();
				const color = 0xFFFFFF;
	const intensity = 1;
	const light = new THREE.DirectionalLight(color, intensity);
	light.position.set(2, 0.35, 0);
	light.target.position.set(0, -0.2, 0);
	scene.add(light,light.target);
	light.target.updateMatrixWorld();
	
					var loaderMatFloo = new THREE.TextureLoader();
		loaderMatFloo.crossOrigin = "use-credentials";
		var textureFloo = loaderMatFloo.load('js/textures/parket.jpg');
		textureFloo.wrapS = THREE.MirroredRepeatWrapping;
		textureFloo.wrapT = THREE.MirroredRepeatWrapping;
		var timesToRepeatHorizontally = 4;
		var timesToRepeatVertically = 4;

		textureFloo.repeat.set(timesToRepeatHorizontally, timesToRepeatVertically);
		textureFloo.anisotropy = maxanisotropy;
		var materialFloo = new THREE.MeshBasicMaterial({map: textureFloo,});
		var meshFloor = new THREE.Mesh(
	    new THREE.PlaneGeometry(20,20,1,1),
	    materialFloo);
		meshFloor.rotation.x -= Math.PI / 2;		
		meshFloor.position.y = 0;
		meshFloor.receiveShadow = true;
		meshFloor.name = "floo";
		scene.add(meshFloor);
		
		camControls = new OrbitControls(camera, renderer.domElement );
	    camControls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
		camControls.dampingFactor = 0.25;
		camControls.screenSpacePanning = false;
		camControls.minDistance = 0.2;
		camControls.maxDistance = 1;
		camControls.maxPolarAngle = Math.PI;
		camControls.minPolarAngle = -Math.PI/4;
		camControls.keyPanSpeed  = 1;
		camControls.rotateSpeed = 0.05;
		
			var gui;
			var gui29;			
			gui = new GUI();
			gui29 = new GUI();
								
				const cubeRenderTarget2 = new THREE.WebGLCubeRenderTarget(512, {
		        format: THREE.RGBFormat,
		        generateMipmaps: true,
		        minFilter: THREE.LinearMipmapLinearFilter,
		        encoding: THREE.sRGBEncoding
			    });
				 const cubeCamera2 = new THREE.CubeCamera(0.1,1000,cubeRenderTarget2
				);
			//------------object1-----------					
	var Door30 = new GLTFLoader();					
	    Door30.load('gltfmodels/door27_r2.gltf', function(ball2){
		ball2.scene;
				var door30Bbox = new THREE.Box3();
				door30Bbox.setFromObject(ball2.scene);
				var bboxDoor30Center = door30Bbox.getCenter().clone();
				bboxDoor30Center.multiplyScalar(-1);				
					ball2.scene.traverse(function(child){
						if (child.isMesh) {
                   			child.material.envMap = cubeRenderTarget2.texture;
                		}
	              			child.castShadow = true;
    						child.receiveShadow = true;							
						if (child instanceof THREE.Mesh){
							child.geometry.translate(bboxDoor30Center.x, bboxDoor30Center.y, bboxDoor30Center.z); ;
	              		}                               
					});
				door30Bbox.setFromObject(ball2.scene);
		//------------------------------------------------				
				scene.add(ball2.scene);
	
				ball2.scene.variant = 'Alum';
							const parser = ball2.parser;
							const variantsExtension = ball2.userData.gltfExtensions[ 'KHR_materials_variants' ];
							const variants = variantsExtension.variants.map( ( variant ) => variant.name );
							const variantsCtrl = gui.add( ball2.scene, 'variant', variants ).name( 'Fasad Colour' );

							selectVariant( scene, parser, variantsExtension, ball2.scene.variant  );

							variantsCtrl.onChange( ( value ) => selectVariant( scene, parser, variantsExtension, value ) );
				ball2.scene.position.set(+0.5, 0.5, -1);
				ball2.scene.rotation.y = Math.PI/2;
				cubeCamera2.position.copy( ball2.scene.position );
				render();
		});	
				
				function selectVariant( scene, parser, extension, variantName ) {

				const variantIndex = extension.variants.findIndex( ( v ) => v.name.includes( variantName ) );

				scene.traverse( async ( objectgui ) => {	
					if ( ! objectgui.isMesh || ! objectgui.userData.gltfExtensions ) return;
					const meshVariantDef = objectgui.userData.gltfExtensions[ 'KHR_materials_variants' ];
					if ( ! meshVariantDef ) return;
					if ( ! objectgui.userData.originalMaterial ) {
						objectgui.userData.originalMaterial = objectgui.material;
					}
					const mapping = meshVariantDef.mappings
						.find( ( mapping ) => mapping.variants.includes( variantIndex ) );
					if ( mapping ) {
						objectgui.material = await parser.getDependency( 'material', mapping.material );
						parser.assignFinalMaterial(objectgui);
					} else {
						objectgui.material = objectgui.userData.originalMaterial;
					}
					 if (objectgui.isMesh) { 	                  
                   objectgui.material.envMap = cubeRenderTarget2.texture;
                	}
					render();
				} );
			}
						
			const cubeRenderTarget3 = new THREE.WebGLCubeRenderTarget(512, {
		        format: THREE.RGBFormat,
		        generateMipmaps: true,
		        minFilter: THREE.LinearMipmapLinearFilter,
		        encoding: THREE.sRGBEncoding
			});
			const cubeCamera3 = new THREE.CubeCamera(0.1,1000,cubeRenderTarget3
			);
                     //------------object2-----------				
				var Door29 = new GLTFLoader();					
				    Door29.load('gltfmodels/door29.gltf', function(ball29){
					ball29.scene;

				var door29Bbox = new THREE.Box3();
					door29Bbox.setFromObject(ball29.scene);
				var bboxDoor29Center = door29Bbox.getCenter().clone();
					bboxDoor29Center.multiplyScalar(-1);				
						ball29.scene.traverse(function(child){
							if (child.isMesh) {
	                   			child.material.envMap = cubeRenderTarget3.texture;
	                		}
		              			child.castShadow = true;
	    						child.receiveShadow = true;							
							if (child instanceof THREE.Mesh){
								child.geometry.translate(bboxDoor29Center.x, bboxDoor29Center.y, bboxDoor29Center.z); ;
		              		}                               
						});
				door29Bbox.setFromObject(ball29.scene);
		//------------------------------------------------				
				scene.add(ball29.scene);	
				ball29.scene.variant = 'Alum';
							const parser = ball29.parser;
							const variantsExtension = ball29.userData.gltfExtensions[ 'KHR_materials_variants' ];
							const variants = variantsExtension.variants.map( ( variant ) => variant.name );
							const variantsCtrl = gui29.add( ball29.scene, 'variant', variants ).name( 'Fasad Colour' );

							selectVariant29( scene, parser, variantsExtension, ball29.scene.variant  );

							variantsCtrl.onChange( ( value ) => selectVariant( scene, parser, variantsExtension, value ) );
							
				ball29.scene.position.set(-0.5, 0.5, -1);
				cubeCamera3.position.copy( ball29.scene.position );
				render();
		});
		
		function selectVariant29( scene, parser, extension, variantName ) {

				const variantIndex = extension.variants.findIndex( ( v ) => v.name.includes( variantName ) );

				scene.traverse( async ( objectgui ) => {
					if ( ! objectgui.isMesh || ! objectgui.userData.gltfExtensions ) return;
					const meshVariantDef = objectgui.userData.gltfExtensions[ 'KHR_materials_variants' ];
					if ( ! meshVariantDef ) return;
					if ( ! objectgui.userData.originalMaterial ) {
						objectgui.userData.originalMaterial = objectgui.material;
					}
					const mapping = meshVariantDef.mappings
						.find( ( mapping ) => mapping.variants.includes( variantIndex ) );
					if ( mapping ) {
						objectgui.material = await parser.getDependency( 'material', mapping.material );
						parser.assignFinalMaterial(objectgui);
					} else {
						objectgui.material = objectgui.userData.originalMaterial;
					}
					 if (objectgui.isMesh) { 	                  
                   objectgui.material.envMap = cubeRenderTarget3.texture;
                	}
					render();
				} );
			}
			
	function animate() {			
		requestAnimationFrame(animate);		
		var delta = clock.getDelta();
  		camControls.update(delta);
  		cubeCamera3.update( renderer, scene );
  		cubeCamera2.update( renderer, scene );		
		render()		
	}
			function render() {
				renderer.render( scene, camera );
			}	   
		animate();		
	   		</script>
</body>
</html>

Hi!
Have a look at this SO answer

This is my question. And the answer doesn’t solve the problem. Note: GLTF, Cube Camera, KHR materials change, many gltf objects, one Gui panel. The problem must be solved in a comprehensive manner, and not in part.

@const Put all the objects, whose color you want to change, in an array, and iterate through it, setting color in .onChange() event handler. Or do I miss something in your description of the problem?

Where do I add array elements? The GUI panel is called from inside the GLTF loader. And from there the selectVariant () function is called in which I add the appropriate objectgui.material.envMap = cubeRenderTarget_.texture. I was trying to assign the guiObj[1] = ball2.scene to the guiObj[] and pass the guiObj to the gui. At the same time I tried to pass the cRT parameter to the selectVariant () function as cubeRenderTarget_.texture.

			var gui;			
			gui = new GUI();
			const cubeRenderTarget2 = new THREE.WebGLCubeRenderTarget(512, {
		        format: THREE.RGBFormat,
		        generateMipmaps: true,
		        minFilter: THREE.LinearMipmapLinearFilter,
		        encoding: THREE.sRGBEncoding
			    });
				 const cubeCamera2 = new THREE.CubeCamera(0.1,1000,cubeRenderTarget2
				);
				
				
			var guiObj = [];	
			var cRT;	
			//------------object1-----------	
			
			
							
	var Door30 = new GLTFLoader();					
	    Door30.load('gltfmodels/door27_r2.gltf', function(ball2){
		ball2.scene;
				var door30Bbox = new THREE.Box3();
				door30Bbox.setFromObject(ball2.scene);
				var bboxDoor30Center = door30Bbox.getCenter().clone();
				bboxDoor30Center.multiplyScalar(-1);				
					ball2.scene.traverse(function(child){
						if (child.isMesh) {
                   			child.material.envMap = cubeRenderTarget2.texture;
                		}
	              			child.castShadow = true;
    						child.receiveShadow = true;							
						if (child instanceof THREE.Mesh){
							child.geometry.translate(bboxDoor30Center.x, bboxDoor30Center.y, bboxDoor30Center.z); ;
	              		}                               
					});
				door30Bbox.setFromObject(ball2.scene);
		//------------------------------------------------				
				scene.add(ball2.scene);
	guiObj[1] = ball2.scene;
	cRT = cubeRenderTarget2.texture;
				ball2.scene.variant = 'Alum';
							const parser = ball2.parser;
							const variantsExtension = ball2.userData.gltfExtensions[ 'KHR_materials_variants' ];
							const variants = variantsExtension.variants.map( ( variant ) => variant.name );
							const variantsCtrl = gui.add( guiObj, 'variant', variants ).name( 'Fasad Colour' );

							selectVariant( scene, parser, variantsExtension, ball2.scene.variant, cRT  );

							variantsCtrl.onChange( ( value ) => selectVariant( scene, parser, variantsExtension, value ) );
				ball2.scene.position.set(+0.5, 0.5, -1);
				ball2.scene.rotation.y = Math.PI/2;
				cubeCamera2.position.copy( ball2.scene.position );
				render();
		});	
				
				function selectVariant( scene, parser, extension, variantName, cRT ) {

				const variantIndex = extension.variants.findIndex( ( v ) => v.name.includes( variantName ) );

				scene.traverse( async ( objectgui ) => {	
					if ( ! objectgui.isMesh || ! objectgui.userData.gltfExtensions ) return;
					const meshVariantDef = objectgui.userData.gltfExtensions[ 'KHR_materials_variants' ];
					if ( ! meshVariantDef ) return;
					if ( ! objectgui.userData.originalMaterial ) {
						objectgui.userData.originalMaterial = objectgui.material;
					}
					const mapping = meshVariantDef.mappings
						.find( ( mapping ) => mapping.variants.includes( variantIndex ) );
					if ( mapping ) {
						objectgui.material = await parser.getDependency( 'material', mapping.material );
						parser.assignFinalMaterial(objectgui);
					} else {
						objectgui.material = objectgui.userData.originalMaterial;
					}
					 if (objectgui.isMesh) { 	                  
                   objectgui.material.envMap = cRT;
                	}
					render();
				} );
			}

But it doesn’t work. The cube camera does not display and the colors do not change.

I still can’t get, what difficulty to have two shoes at once and change their colors, having one GUI control.
I’ve made minor modifications in the official example: https://codepen.io/prisoner849/pen/YzraByP?editors=0010

Sorry, cloning an object in two is not a problem. But I’m trying to explain that I have different gltf objects, with different sizes and locations. Each object, when double-clicking on it, moves into space according to a certain law. For each object, I need to add a individual cube camera so that the object reflects real space according to its position (here is an example of application for three objects https://sbcode.net/threejs/cubecamera/). And while I still need to be able to change the color of several objects of the same type at the same time.

Sorry, I don’t think I’m going to be able to help much based on your description of the problem. If you can share complete code and assets to reproduce the problem (e.g. a github repository, a ZIP, or a running demo) someone may be able to help debug what is going on here.

Changing the color of multiple objects is very simple in principle — loop over your materials and update them all! — but perhaps more complicated in practice, because you need to keep track of exactly which changes are made to which materials. How to do that is generally specific to your use case.

1 Like

Thanks to everyone who responded to the discussion of this issue. Obviously, there is no correct solution for it at the moment. As I wrote above, the colors of all GLTF objects are changed using one GUI panel. I was able to make the GUI panels invisible for other objects by applying the following in the code:

			var gui = new GUI();
			var gui29 = new GUI({ autoPlace: false });

I understand that this is not a completely correct solution, but so far it works.

It may be possible that you are conflating three different problems.

GLTF is not really related to three.js, it’s a format, a standard, that can be interpreted by infinitely many applications and libraries. In your context, it’s just a delivery format. Could be plain text instead, json, obj, fbx, it could even be embedded in your web app/page.

The next area is three.js, which relies on a scene graph and Mesh as people have pointed above. The proper way to interact with a mesh is to get a reference to a mesh and… interact with it.

The third, completely unrelated area is a GUI. There are infinitely many ways and libraries to implement a GUI. I’m guessing that you are using dat.gui, but, again, it could be any other library or plain JS code.

“Correct” is a bit abstract, if it works it works, but you are probably over complicating this and doing it in an awkward way.

I’d say, if you have loaded a GLTF into a three.js Scene forget about GLTF, it does not exist anymore and then go from there.

Oh, the double clicking may be yet another area. Mouse interaction in a 3d view is unrelated to the GUI and somewhat unrelated to three, this is a DOM problem. The DOM detects a double click, and then you do something with three.js upon that event.

2 Likes