Can't get PerspectiveCamera lookAt to look "down" at a point

Hey all,

I am visualizing a point cloud in threejs. The pointcloud comes form a pcd file that I load with PCDLoader, and was created using python’s Open3D. The points represent star systems in an imaginary galaxy coordinate system (elite:dangerous).
The coordinates of my points are far into the 1000’s positive and negative (they’re lightyears).

I feel in general the official threejs docs are somewhat brief in their explanation on how things work, while the examples contain so much functionality in one example that it is hard to see the threes through the forest, and as a consequence I have a few problems but I’ll limit this question to one of them.

I am trying to load the pointcloud, and then move the camera so that it has the full pointcloud is in view. I do it by setting the camera.postion based on the pointcloud’s bounding box. The x and z ccordinates are in the middle of the bb, while the y position is above the bb. And then I want to turn the camera so the lookAt is the middle of the BB. I expect now to basically have a top-down view of my pointcloud. But for some reason, I am not looking down. I added reference points with the camera position (red), and camera lookat (green) and it turns out they are in the right spot, but the camera is just not looking down at the “lookat” position. The camera starts with nothing in view, and when I zoom out, I see thepointcloud and reference points, but it looks like the “lookat” point was below the camera, while the camera was looking in the negative z direction, instead of the negative y direction. how can I make the camera look in the direction of a specific point?

This is what I see after zooming out (but not rotating/panning):

This is my code:

import * as THREE from 'https://unpkg.com/three/build/three.module.js';
import { OrbitControls, MapControls } from 'https://unpkg.com/three/examples/jsm/controls/OrbitControls.js';
import { TrackballControls  } from 'https://unpkg.com/three/examples/jsm/controls/TrackballControls.js';
import { PLYLoader } from 'https://unpkg.com/three/examples/jsm/loaders/PLYLoader.js';
import { PCDLoader } from 'https://unpkg.com/three/examples/jsm/loaders/PCDLoader.js';

var camera, scene, renderer, controls, loader, geometry, mouse, raycaster;

var black = new THREE.Color(0x000000);
var white = new THREE.Color(0xffffff);
var width, height;
//width = 0.8*window.innerWidth;
//height = 0.8*window.innerHeight;

$( document ).ready(function() {
  init();
  animate();
});


function clear_scene() {
  while(scene.children.length > 0){ 
    scene.remove(scene.children[0]); 
  }
}

function onLayerChange() {
  load_layer(this.value);
}

function onMouseMove( event ) {

	// calculate mouse position in normalized device coordinates
	// (-1 to +1) for both components
  console.log("Moving mouse", event);

	mouse.x = ( event.clientX / width ) * 2 - 1;
	mouse.y = - ( event.clientY / height ) * 2 + 1;

}

function load_layer(name, update_camera=false) {
  console.log('Loading layer ', name);
  clear_scene();
  loader.load(name+'.pcd', function(points) {
    console.log("Loaded pcd file");
    var sprite = new THREE.TextureLoader().load( 'images/circle.png' );
    
    console.log("Material:", points.material);
    
   
    points.material.vertexColors = false;
    points.material.size = 20;
    points.material.map = sprite;
    points.material.transparent = true;
    points.material.alphaTest = 0.8;
    points.material.sizeAttenuation = false;
    points.material.needsUpdate = true;
    console.log("Points:", points);
    scene.add(points);
    
    points.geometry.computeBoundingBox();
    if(update_camera){
      let bb = points.geometry.boundingBox
      console.log("Bounding Box", bb);
      
      // Set the camera half the y size of the bounding box above it, but with x and z in the middle of the bb.
      let camera_pos = new THREE.Vector3((bb.max.x - bb.min.x)/2+bb.min.x, (bb.max.y - bb.min.y)/2+bb.max.y, (bb.max.z - bb.min.z)/2+bb.min.z);
      console.log("Moving camera to", camera_pos);
      
      //Set the lookAt parameter to the middle of the bounding box.
      let camera_lookat = new THREE.Vector3((bb.max.x - bb.min.x)/2+bb.min.x,(bb.max.y - bb.min.y)/2+bb.min.y, (bb.max.z - bb.min.z)/2+bb.min.z);
      //let controls_target = camera_lookAt;
      
      console.log("Looking at", camera_lookat);
      camera.position.set(camera_pos.x, camera_pos.y, camera_pos.z);
      camera.lookAt(camera_lookat.x, camera_lookat.y, camera_lookat.z);
      //controls.target.set(controls_target);
      camera.updateMatrixWorld();
      
      //Add the camera_lookat and camera_pos as points in there for debugging.
      let ref_geom = new THREE.BufferGeometry();
      let ref_colors = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
      let ref_points_array = [camera_pos.x, camera_pos.y, camera_pos.z, camera_lookat.x, camera_lookat.y, camera_lookat.z];
      ref_geom.setAttribute('position', new THREE.Float32BufferAttribute(ref_points_array, 3));
      ref_geom.setAttribute('color', new THREE.Float32BufferAttribute(ref_colors, 3));
      ref_geom.computeBoundingSphere();
      let points_material = new THREE.PointsMaterial({ size: 50, vertexColors: true});
      let ref_points = new THREE.Points(ref_geom, points_material);
      console.log("Reference points:", ref_points);
      scene.add(ref_points);

    }
    
    console.log("Added points");
    //controls.update();
    //render();
  });
  
}

function init() {
  width = $("#map").width();
  height = $("#map").height();
  console.log('size:', width, height);
  
  //raycaster = new THREE.Raycaster();
  mouse = new THREE.Vector2();
  
  renderer = new THREE.WebGLRenderer( { antialias: true } );
  renderer.setSize( width, height );
  $('#map').append( renderer.domElement );
  
  scene = new THREE.Scene();
  //scene.background = new THREE.Color(0xffffff);

  camera = new THREE.PerspectiveCamera( 90, width / height, 1, 100000 );
  //camera.up.set(0,-1,0);
  
  controls = new OrbitControls(camera, renderer.domElement);
  controls.autoRotate = false;
  controls.screenSpacePanning = true;
  controls.enableDamping = true;
  //controls.maxPolarAngle = Math.pi/2; //90 degrees;
  
  
  controls.addEventListener('change', onChangeCamera );
  window.addEventListener( 'mousemove', onMouseMove, false );
  window.addEventListener( 'resize', resize, false );
  render();
  
  $('input[name=layer]').change(onLayerChange);
  loader = new PCDLoader();

  load_layer('all_systems', true)

}

function onChangeCamera() {
  //console.log("Camera position:",camera.position);
  //render();

}

function resize() {
	console.log("resizing");
  width = $("#map").width();
  height = $("#map").height();

	camera.aspect = width / width;
	camera.updateProjectionMatrix();

	renderer.setSize( width, height );
	render();
}


function animate() {
  requestAnimationFrame( animate );
  controls.update();
  render();
}

function render() {
  /*
  raycaster.setFromCamera( mouse, camera );
  console.log("children: ", scene.children);
  let intersects = raycaster.intersectObjects( scene.children );
  if(intersects.length > 0){
    console.log("Intersects: ", intersects);
  }
  
  for ( var i = 0; i < intersects.length; i++ ) {
    console.log("Intersecting point", intersects[i]);
		intersects[ i ].object.material.color.set( 0xff0000 );
    intersects[ i ].object.material.colorNeedsUpdate = true;
	}
  */
  
  renderer.render( scene, camera );
}

Ok, I managed to get it to work. It turns out place that the camera looks at with OrbitControls is controls.target, not camera.lookat. So by setting controls.target.set(controls_target.x, controls_target.y, controls_target.z);, I manage to get the camera to look at that location.