Attaching transformcontrols to imported 3D object

Been able to figure out a bunch of stuff looking through examples and the forum, but I am stuck right now and looking for some help.

I have imported a 3d model of a skull into 3js. The skull has 2 objects that I modeled in blender, the skull and jaw.

What I am trying to do is attach transformcontrols to just the jaw part. I confirmed that the object’s name is jaw through the console.

What ends up happening right now is that the controls looks like its just attached to the scene instead. I can interact and manipulate the controls when it’s in rotate, but nothing happens. When it’s on scale mode, I can interact with it (all the axes highlight when I mouse over them), but I can’t scale the model as if it was locked. If I use transform, the code just breaks. These errors I get:

Uncaught TypeError: this.picker[this.mode] is undefined
Uncaught TypeError: object is undefined

So I have no idea what the controls is even attached to even though I specified the jaw.

Here’s the code I’m using right now and doesn’t give me any errors, but it’s not working as intended:

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
import WebGL from 'three/addons/capabilities/WebGL.js';
import GUI from 'lil-gui';

let skull;


const scene = new THREE.Scene( );
	scene.background = new THREE.Color( 0xdddddd );
	scene.add( new THREE.GridHelper( 1000, 1000, 0x888888, 0x444444 ) );

const camera = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, 1, 1000 );

const hlight = new THREE.AmbientLight ( 0x404040, 10 );
	scene.add( hlight );

const directionalLight = new THREE.DirectionalLight( 0xffffff, 10 );
	directionalLight.position.set( 0, 1, 0 );
	directionalLight.castShadow = true;
	scene.add( directionalLight );

const renderer = new THREE.WebGLRenderer( );
	renderer.setSize( window.innerWidth, window.innerHeight );
	renderer.setAnimationLoop( animate );
	document.body.appendChild( renderer.domElement );

const loader = new GLTFLoader( );
	loader.load( 'skull.glb', function ( gltf ) {
		skull = gltf.scene;
		skull.traverse( function( object ) {

    			if ( object.isMesh ) console.log( object );

		} );
		scene.add( skull );
	}, undefined, function ( error ) {
		console.error( error );

	} );
		

const controls = new OrbitControls( camera, renderer.domElement );
	controls.update( );
	controls.addEventListener( 'change', animate );

const tcontrols = new TransformControls( camera, renderer.domElement );
	tcontrols.setMode( 'rotate' );
	tcontrols.addEventListener( 'change', animate );
	tcontrols.addEventListener( 'dragging-changed', function ( event ) {
		controls.enabled = ! event.value;
	} );

	tcontrols.size = 1;
	tcontrols.space = 'local';

const Jaw = new THREE.Mesh( skull );
	scene.add( Jaw );

	tcontrols.attach( Jaw );
	scene.add ( tcontrols );


camera.position.x = 157;
camera.position.y = -245;
camera.position.z = 127;

const gui = new GUI( );

	const myObject = {
		myBoolean: true,
		myFunction: function( ) { alert( 'hi') },
		myString: 'lil-gui',
		myNumber: 1
	};

	gui.add( myObject, 'myBoolean' );  // Checkbox
	gui.add( myObject, 'myFunction' ); // Button
	gui.add( myObject, 'myString' );   // Text Field
	gui.add( myObject, 'myNumber' );   // Number Field

	// Add sliders to number fields by passing min and max
	gui.add( myObject, 'myNumber', 0, 1 );
	gui.add( myObject, 'myNumber', 0, 100, 2 ); // snap to even numbers

	// Create dropdowns by passing an array or object of named values
	gui.add( myObject, 'myNumber', [ 0, 1, 2 ] );
	gui.add( myObject, 'myNumber', { Label1: 0, Label2: 1, Label3: 2 } );

	// Create color pickers for multiple color formats
	const colorFormats = {
		string: '#ffffff',
		int: 0xffffff,
		object: { r: 1, g: 1, b: 1 },
		array: [ 1, 1, 1 ]
	};

	gui.addColor( colorFormats, 'string' );


function animate( ) {
	renderer.render( scene, camera );
}

I am able to attach the controls to just a regular geometric shape when I use this based off the examples:

	orbit = new OrbitControls( currentCamera, renderer.domElement );
	orbit.update();
	orbit.addEventListener( 'change', render );

	control = new TransformControls( currentCamera, renderer.domElement );
	control.setMode( 'rotate' );
	control.addEventListener( 'change', render );

	control.addEventListener( 'dragging-changed', function ( event ) {

		orbit.enabled = ! event.value;

	} );

	const mesh = new THREE.Mesh( geometry );
	scene.add( mesh );

	control.attach( mesh );
	scene.add( control );

But trying to get the controls on the imported object is confusing me. Maybe it’s the placement of some of the parts in the code?

Any help would be greatly appreciated.

Try using this code:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { TransformControls } from "three/addons/controls/TransformControls.js";
import WebGL from "three/addons/capabilities/WebGL.js";
import GUI from "lil-gui";

let skull;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xdddddd);
scene.add(new THREE.GridHelper(1000, 1000, 0x888888, 0x444444));

const camera = new THREE.OrthographicCamera(
  window.innerWidth / -2,
  window.innerWidth / 2,
  window.innerHeight / 2,
  window.innerHeight / -2,
  1,
  1000
);

const hlight = new THREE.AmbientLight(0x404040, 10);
scene.add(hlight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 10);
directionalLight.position.set(0, 1, 0);
directionalLight.castShadow = true;
scene.add(directionalLight);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animate);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
controls.addEventListener("change", animate);

const tcontrols = new TransformControls(camera, renderer.domElement);
tcontrols.setMode("rotate");
tcontrols.addEventListener("change", animate);
tcontrols.addEventListener("dragging-changed", function (event) {
  controls.enabled = !event.value;
});

tcontrols.size = 1;
tcontrols.space = "local";

const loader = new GLTFLoader();
loader.load(
  "skull.glb",
  function (gltf) {
    skull = gltf.scene;
    skull.traverse(function (object) {
      if (object.isMesh) console.log(object);
    });
    tcontrols.attach(Skull);
    scene.add(tcontrols);
    scene.add(skull);
  },
  undefined,
  function (error) {
    console.error(error);
  }
);

camera.position.x = 157;
camera.position.y = -245;
camera.position.z = 127;

const gui = new GUI();

const myObject = {
  myBoolean: true,
  myFunction: function () {
    alert("hi");
  },
  myString: "lil-gui",
  myNumber: 1,
};

gui.add(myObject, "myBoolean"); // Checkbox
gui.add(myObject, "myFunction"); // Button
gui.add(myObject, "myString"); // Text Field
gui.add(myObject, "myNumber"); // Number Field

// Add sliders to number fields by passing min and max
gui.add(myObject, "myNumber", 0, 1);
gui.add(myObject, "myNumber", 0, 100, 2); // snap to even numbers

// Create dropdowns by passing an array or object of named values
gui.add(myObject, "myNumber", [0, 1, 2]);
gui.add(myObject, "myNumber", { Label1: 0, Label2: 1, Label3: 2 });

// Create color pickers for multiple color formats
const colorFormats = {
  string: "#ffffff",
  int: 0xffffff,
  object: { r: 1, g: 1, b: 1 },
  array: [1, 1, 1],
};

gui.addColor(colorFormats, "string");

function animate() {
  renderer.render(scene, camera);
}

Used that code and the controls are actually on the skull, thanks! Looks like I had to put all that together within the loader function to work, instead of outside/below it.

That got me halfway to where I wanted to be, with the controls specifically on the jaw part.

But I remembered one of the examples with IK stuff had some function to call the parts of a model. So I went off of that and put in this inside with the other loader stuff:

skull.traverse( n => {
			if ( n.name === 'Jaw' ) part.Jaw = n;
		} );
		tcontrols.attach( part.Jaw );
		scene.add ( tcontrols );
		scene.add( skull );

Now it’s on the jaw and only the jaw rotates. Thanks again for the help!

Side note: I accidentally used scene.add( part.Jaw ) and just got only that part to load :sweat_smile:. That might be useful to know later on, maybe. Probably after I start learning the IK stuff.

I wasn’t aware of the Jaw part! The problem here is that GLTFLoader.loader() is an asynchronous function and the model thats loaded inside the loader() can’t be accessed outside. Accessing it outside can be only possible if you assign the the gltf.scene to a global veraible.

I don’t know if you are familar with three.js file formatting pattern but that is very helpful in these cases!

Oh, I didn’t know about that. Now all the let variables I see at the top of all the example codes make more sense. Just read up a bit on async functions, and I see how they’re used and are better. I thought all code just ran in steps, waiting for the previous one to finish. Still very new to 3js (and programming in general) and picking up things as I go. Thanks!

~WRD3578.jpg