How to make the camera look at a specific child in THREE.js

I'm using THREE v0.86 with React and I'm to trying to get the world position of a specific child of a parent, so when I click a button I can change its material color and make the camera face to it.

First, I store a reference for each of the children I'm interested, on the onLoad function:

const onLoad = object => {

      object.name = "scene 1";
      object.position.set(0, 5, 0);
      obj3d.add(object);
      object.traverse(
        function(child) {
          if (condition === true) {
            let material = new THREE.MeshBasicMaterial({ color: 0xffff00 });
            child.material = material;
            this[child.name] = child; // <-- Here
          }
        }.bind(this)
      );
    };

Then, I've tried it two ways: first with controls enabled: TrackballControls

moveCamera(selectedChild) {

    this[selectedChild].material.color.setHex(0xff0000);
    const material = new THREE.MeshBasicMaterial({ color: "#FF0000" });
    this[selectedChild].material = material;

    const newPosition = new THREE.Vector3();
    newPosition.setFromMatrixPosition(this[selectedChild].matrixWorld);

// I've tried this:
    this.controls.target = this[selectedChild].getWorldPosition();

// OR this
    this.controls.target = newPosition;

    this.camera.lookAt(this[selectedChild]);
  }

Also, I've disabled the controls and tried this:

moveCamera(selectedChild) {

    this[selectedChild].material.color.setHex(0xff0000);
    const material = new THREE.MeshBasicMaterial({ color: "#FF0000" });
    this[selectedChild].material = material;

    this.camera.lookAt(this[selectedChild].getWorldPosition());
  }

With this in the renderScene() function:

renderScene() {
    this.renderer.autoClear = true;
    this.renderer.setClearColor(0xfff0f0);
    this.renderer.setClearAlpha(0.0);
    this.scene.updateMatrixWorld(); // <- THIS

    if (this.composer) {
      this.composer.render();
    } else {
      this.renderer.render(this.scene, this.camera);
    }

  }

The moveCamera(childName) does change the specific child color but the problem is that it ALWAYS looks in the same direction, so I consoled log the parent object and took a glance at the children and they all have the same matrix, matrixAutoUpdate: true, matrixWorld, matrixWorldNeedsUpdate: false properties values between them and they are all the parent's values as well, so of course the newPosition.setFromMatrixPosition(this[selectedChild].matrixWorld); vector will always be the same. What am I missing? why aren't the different children objects positions relative to the world different?

This is my scene setup

componentDidMount() {
    const { THREE, innerWidth, innerHeight } = window;

    OBJLoader(THREE);
    MTLLoader(THREE);

    this.setScene();
    this.setCamera();
    this.setRenderer();

    this.group = new THREE.Group();
    this.selectedObjects = [];
    this.mouse = new THREE.Vector2();
    this.raycaster = new THREE.Raycaster();

    this.enableControls();

    this.start();
  }

Where:

this.setScene()

setScene() {
    const { THREE } = window;
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0xcccccc);
    scene.fog = new THREE.FogExp2(0xcccccc, 0.002);

    // LIGHTS
    scene.add(new THREE.AmbientLight(0xaaaaaa, 0.2));
    const light = new THREE.DirectionalLight(0xddffdd, 0.6);
    light.position.set(1, 1, 1);
    light.castShadow = true;
    light.shadow.mapSize.width = 1024;
    light.shadow.mapSize.height = 1024;
    const d = 10;
    light.shadow.camera.left = -d;
    light.shadow.camera.right = d;
    light.shadow.camera.top = d;
    light.shadow.camera.bottom = -d;
    light.shadow.camera.far = 1000;
    scene.add(light);

    const hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.5);
    hemiLight.color.setHSL(0.6, 1, 0.6);
    hemiLight.groundColor.setHSL(0.095, 1, 0.75);
    hemiLight.position.set(0, 500, 0);
    scene.add(hemiLight);

    // GROUND
    const groundGeo = new THREE.PlaneBufferGeometry(1000, 1000);
    const groundMat = new THREE.MeshPhongMaterial({
      color: 0xffffff,
      specular: 0x050505
    });
    groundMat.color.setHSL(0.095, 1, 0.75);

    const ground = new THREE.Mesh(groundGeo, groundMat);
    ground.rotation.x = -Math.PI / 2;
    ground.position.y = -0.5;
    scene.add(ground);

    // SKYDOME
    const vertexShader =
      "varying vec3 vWorldPosition; void main() { vec4 worldPosition = modelMatrix * vec4( position, 1.0 ); vWorldPosition = worldPosition.xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); }";
    const fragmentShader =
      "uniform vec3 topColor; uniform vec3 bottomColor; uniform float offset; uniform float exponent; varying vec3 vWorldPosition; void main() { float h = normalize( vWorldPosition + offset ).y; gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( max( h , 0.0), exponent ), 0.0 ) ), 1.0 ); }";
    const uniforms = {
      topColor: { value: new THREE.Color(0x0077ff) },
      bottomColor: { value: new THREE.Color(0xffffff) },
      offset: { value: 0 },
      exponent: { value: 0.6 }
    };
    uniforms.topColor.value.copy(hemiLight.color);

    scene.fog.color.copy(uniforms.bottomColor.value);

    const skyGeo = new THREE.SphereGeometry(300, 32, 15);
    const skyMat = new THREE.ShaderMaterial({
      vertexShader: vertexShader,
      fragmentShader: fragmentShader,
      uniforms: uniforms,
      side: THREE.BackSide
    });

    const sky = new THREE.Mesh(skyGeo, skyMat);
    scene.add(sky);

    this.scene = scene;
  }

this.setCamera

setCamera() {
    const { THREE, innerWidth, innerHeight } = window;
    const camera = new THREE.PerspectiveCamera(
      45,
      innerWidth / innerHeight,
      0.1,
      100
    );
    camera.position.set(-4.1, 7.2, 4.2);
    this.camera = camera;
  }

this.setRenderer

setRenderer() {
    const { THREE, innerWidth, innerHeight } = window;
    const renderer = new THREE.WebGLRenderer({ antialias: false });
    renderer.setSize(innerWidth, innerHeight * this.windowMultiplier);
    this.renderer = renderer;
  }

this.start this.stop this.animate

start() {
    if (!this.frameId) {
      this.frameId = requestAnimationFrame(this.animate);
    }
  }

  stop() {
    cancelAnimationFrame(this.frameId);
  }

  animate() {
    this.renderScene();
    this.frameId = window.requestAnimationFrame(this.animate);
    if (!!this.controls) {
      this.controls.update();
    }
  }

2 answers

  • answered 2018-03-13 21:46 Don McCurdy

    The Object3D .lookAt method expects a position in world space:

    var position = new THREE.Vector3().copy( child.position );
    child.localToWorld( position );
    camera.lookAt( position );
    

    Using TrackballControls or OrbitControls would complicate this, I'm not sure whether lookAt will work properly with either of those involved.

    three.js r89

  • answered 2018-03-14 13:34 LuisEgan

    I got it! All of my code works perfectly, the problem was in the .obj itself, all of the children's pivot points where the same as the parent, they weren't at the object's center (how one would expect):

    Here you can see the highlighted object (a child) and its pivot point (same as the parent, which is wrong)