0
votes

I have a Three.js scene with points and am trying to figure out the relationship between my points' positions and screen coordinates. I thought I could use the function @WestLangley provided to a previous question but implementing this function has raised some confusion.

In the scene below, I'm storing the x coordinates of the left and right-most points in world.bb.x, and am logging the world coordinates of the cursor each time the mouse moves. However, when I mouse to the left and right-most points, the world coordinates do not match the min or max x-coordinate values in world.bb.x, which is what I expected.

Do others know what I can do to figure out the world coordinates of my cursor at any given time? Any help others can offer is greatly appreciated!

function World() {
  this.scene = this.getScene();
  this.camera = this.getCamera();
  this.renderer = this.getRenderer();
  this.controls = this.getControls();
  this.color = new THREE.Color();
  this.addPoints();
  this.render();
}

World.prototype.getScene = function() {
  var scene = new THREE.Scene();
  scene.background = new THREE.Color(0xefefef);
  return scene;
}

World.prototype.getCamera = function() {
  var renderSize = getRenderSize(),
      aspectRatio = renderSize.w / renderSize.h,
      camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 100000);
  camera.position.set(0, 1, -10);
  return camera;
}

World.prototype.getRenderer = function() {
  var renderSize = getRenderSize(),
      renderer = new THREE.WebGLRenderer({antialias: true});
  renderer.setPixelRatio(window.devicePixelRatio); // retina displays
  renderer.setSize(renderSize.w, renderSize.h); // set w,h
  find('#gl-target').appendChild(renderer.domElement);
  return renderer;
}

World.prototype.getControls = function() {
  var controls = new THREE.TrackballControls(this.camera, this.renderer.domElement);
  controls.zoomSpeed = 0.4;
  controls.panSpeed = 0.4;
  return controls;
}

World.prototype.render = function() {
  requestAnimationFrame(this.render.bind(this));
  this.renderer.render(this.scene, this.camera);
  this.controls.update();
}

World.prototype.getMouseWorldCoords = function(e) {
  var vector = new THREE.Vector3(),
      camera = world.camera,
      x = (e.clientX / window.innerWidth) * 2 - 1,
      y = (e.clientY / window.innerHeight) * 2 + 1;
  vector.set(x, y, 0.5);
  vector.unproject(camera);
  var direction = vector.sub(camera.position).normalize(),
      distance = - camera.position.z / direction.z,
      scaled = direction.multiplyScalar(distance),
      coords = camera.position.clone().add(scaled);
  return {
    x: coords.x, y: coords.y,
  };
}

World.prototype.addPoints = function() {
  // this geometry builds a blueprint and many copies of the blueprint
  var IBG = THREE.InstancedBufferGeometry,
      BA = THREE.BufferAttribute,
      IBA = THREE.InstancedBufferAttribute,
      Vec3 = THREE.Vector3,
      Arr = Float32Array;

  // add data for each observation; n = num observations
  var geometry = new IBG(),
      n = 10000,
      rootN = n**(1/2),
      // find max min for each dim to center camera
      xMax = Number.NEGATIVE_INFINITY,
      xMin = Number.POSITIVE_INFINITY,
      yMax = Number.NEGATIVE_INFINITY,
      yMin = Number.POSITIVE_INFINITY;

  var translations = new Arr(n * 3),
      colors = new Arr(n * 3),
      uidColors = new Arr(n * 3),
      translationIterator = 0,
      colorIterator = 0,
      uidColorIterator = 0;

  var colorMap = this.getColorMap();

  for (var i=0; i<n; i++) {
    var x = Math.sin(i) * 4,
        y = Math.floor(i / (n/20)) * 0.3,
        color = colorMap[ Math.floor(i / (n/20)) ],
        uidColor = this.color.setHex(i + 1);
    if (x > xMax) xMax = x;
    if (x < xMin) xMin = x;
    if (y > yMax) yMax = y;
    if (y < yMin) yMin = y;
    translations[translationIterator++] = x;
    translations[translationIterator++] = y;
    translations[translationIterator++] = 0;
    colors[colorIterator++] = color.r / 255;
    colors[colorIterator++] = color.g / 255;
    colors[colorIterator++] = color.b / 255;
    uidColors[uidColorIterator++] = uidColor.r;
    uidColors[uidColorIterator++] = uidColor.g;
    uidColors[uidColorIterator++] = uidColor.b;
  }

  // store the min and max coords in each dimension
  this.bb = {
    x: {
      min: xMin,
      max: xMax,
    },
    y: {
      min: yMin,
      max: yMax,
    }
  }

  // center the camera
  this.center = {
    x: (xMax + xMin) / 2,
    y: (yMax + yMin) / 2
  }
  this.camera.position.set(this.center.x, this.center.y, -6);
  this.camera.lookAt(this.center.x, this.center.y, 0);
  this.controls.target = new Vec3(this.center.x, this.center.y, 0);

  // add attributes
  geometry.addAttribute('position', new BA( new Arr([0, 0, 0]), 3));
  geometry.addAttribute('translation', new IBA(translations, 3, 1) );
  geometry.addAttribute('color', new IBA(colors, 3, 1) );
  geometry.addAttribute('uidColor', new IBA(uidColors, 3, 1) );

  var material = new THREE.RawShaderMaterial({
    vertexShader: find('#vertex-shader').textContent,
    fragmentShader: find('#fragment-shader').textContent,
  });

  var mesh = new THREE.Points(geometry, material);
  mesh.frustumCulled = false; // prevent the mesh from being clipped on drag
  this.scene.add(mesh);
}

World.prototype.getColorMap = function() {

  function toHex(c) {
    var hex = c.toString(16);
    return hex.length == 1 ? '0' + hex : hex;
  }

  function rgbToHex(r, g, b) {
    return '#' + toHex(r) + toHex(g) + toHex(b);
  }

  function hexToRgb(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
      r: parseInt(result[1], 16),
      g: parseInt(result[2], 16),
      b: parseInt(result[3], 16),
    } : null;
  }

  var hexes = [
    '#fe4445','#ff583b','#ff6a2f','#ff7a20','#ff8800',
    '#ff9512','#ffa31f','#ffaf2a','#ffbb34',
    '#cfc522','#99cc01',
    '#91c14a','#85b66e','#73ac8f','#57a3ac','#0099cb',
    '#14a0d1','#20a7d8','#2aaedf','#33b5e6'
  ]

  var colorMap = {};
  hexes.forEach(function(c, idx) { colorMap[idx] = hexToRgb(c) })
  return colorMap;
}

/**
* Helpers
**/

function getRenderSize() {
  var elem = find('#gl-target');
  return {
    w: elem.clientWidth,
    h: elem.clientHeight,
  }
}

function find(selector) {
  return document.querySelector(selector);
}

/**
* Main
**/

var world = new World();
world.controls.enabled = false;

find('canvas').addEventListener('mousemove', function(e) {
  find('#bar').style.left = e.clientX + 'px';
  var coords = world.getMouseWorldCoords(e);
  console.log(coords, world.bb.x);
})
html, body {
  width: 100%;
  height: 100%;
  background: #000;
}

body {
  margin: 0;
  overflow: hidden;
}

canvas {
  width: 100%;
  height: 100%;
}

.gl-container {
  position: relative;
}

#gl-target {
  width:700px;
  height:400px
}

#bar {
  width: 1px;
  height: 100%;
  display: inline-block;
  position: absolute;
  left: 30px;
  background: red;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/95/three.min.js'></script>
<script src='https://rawgit.com/YaleDHLab/pix-plot/master/assets/js/trackball-controls.js'></script>

<script type='x-shader/x-vertex' id='vertex-shader'>
precision highp float;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

attribute vec3 position;
attribute vec3 translation;

#ifdef PICKING
  attribute vec3 uidColor;
  varying vec3 vUidColor;
#else
  attribute vec3 color;
#endif

varying vec3 vColor;

void main() {
  #ifdef PICKING
    vUidColor = uidColor;
  #else
    vColor = color;
  #endif

  // set point position
  vec3 raw = position + translation;
  vec4 pos = projectionMatrix * modelViewMatrix * vec4(raw, 1.0);
  gl_Position = pos;

  // set point size
  gl_PointSize = 10.0;
}
</script>

<script type='x-shader/x-fragment' id='fragment-shader'>
precision highp float;

#ifdef PICKING
  varying vec3 vUidColor;
#else
  varying vec3 vColor;
#endif

void main() {

  // make point circular
  vec2 coord = gl_PointCoord - vec2(0.5);
  if (length(coord) > 0.5) discard;

  // color the point
  #ifdef PICKING
    gl_FragColor = vec4(vUidColor, 1.0);
  #else
    gl_FragColor = vec4(vColor, 1.0);
  #endif
}
</script>

<div class='gl-container'>
  <div id='bar'></div>
  <div id='gl-target'></div>
</div>
1

1 Answers

3
votes

Aha, instead of dividing the event x and y coordinates by the window width (which only applies to canvases that extend through the full window height and width), I need to divide the event x and y coordinates by the canvas's width and height!

function World() {
  this.scene = this.getScene();
  this.camera = this.getCamera();
  this.renderer = this.getRenderer();
  this.color = new THREE.Color();
  this.addPoints();
  this.render();
}

World.prototype.getScene = function() {
  var scene = new THREE.Scene();
  scene.background = new THREE.Color(0xefefef);
  return scene;
}

World.prototype.getCamera = function() {
  var renderSize = getRenderSize(),
      aspectRatio = renderSize.w / renderSize.h,
      camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 100000);
  camera.position.set(0, 1, -10);
  return camera;
}

World.prototype.getRenderer = function() {
  var renderSize = getRenderSize(),
      renderer = new THREE.WebGLRenderer({antialias: true});
  renderer.setPixelRatio(window.devicePixelRatio); // retina displays
  renderer.setSize(renderSize.w, renderSize.h); // set w,h
  find('#gl-target').appendChild(renderer.domElement);
  return renderer;
}

World.prototype.render = function() {
  requestAnimationFrame(this.render.bind(this));
  this.renderer.render(this.scene, this.camera);
}

World.prototype.getMouseWorldCoords = function(e) {
  var elem = find('#gl-target'),
      vector = new THREE.Vector3(),
      camera = world.camera,
      x = (e.clientX / elem.clientWidth) * 2 - 1,
      y = (e.clientY / elem.clientHeight) * 2 + 1;
  vector.set(x, y, 0.5);
  vector.unproject(camera);
  var direction = vector.sub(camera.position).normalize(),
      distance = - camera.position.z / direction.z,
      scaled = direction.multiplyScalar(distance),
      coords = camera.position.clone().add(scaled);
  return {
    x: coords.x,
    y: coords.y,
  };
}

World.prototype.addPoints = function() {
  // this geometry builds a blueprint and many copies of the blueprint
  var IBG = THREE.InstancedBufferGeometry,
      BA = THREE.BufferAttribute,
      IBA = THREE.InstancedBufferAttribute,
      Vec3 = THREE.Vector3,
      Arr = Float32Array;

  // add data for each observation; n = num observations
  var geometry = new IBG(),
      n = 10000,
      rootN = n**(1/2),
      // find max min for each dim to center camera
      xMax = Number.NEGATIVE_INFINITY,
      xMin = Number.POSITIVE_INFINITY,
      yMax = Number.NEGATIVE_INFINITY,
      yMin = Number.POSITIVE_INFINITY;

  var translations = new Arr(n * 3),
      colors = new Arr(n * 3),
      uidColors = new Arr(n * 3),
      translationIterator = 0,
      colorIterator = 0,
      uidColorIterator = 0;

  var colorMap = this.getColorMap();

  for (var i=0; i<n; i++) {
    var x = Math.sin(i) * 4,
        y = Math.floor(i / (n/20)) * 0.3,
        color = colorMap[ Math.floor(i / (n/20)) ],
        uidColor = this.color.setHex(i + 1);
    if (x > xMax) xMax = x;
    if (x < xMin) xMin = x;
    if (y > yMax) yMax = y;
    if (y < yMin) yMin = y;
    translations[translationIterator++] = x;
    translations[translationIterator++] = y;
    translations[translationIterator++] = 0;
    colors[colorIterator++] = color.r / 255;
    colors[colorIterator++] = color.g / 255;
    colors[colorIterator++] = color.b / 255;
    uidColors[uidColorIterator++] = uidColor.r;
    uidColors[uidColorIterator++] = uidColor.g;
    uidColors[uidColorIterator++] = uidColor.b;
  }

  // store the min and max coords in each dimension
  this.bb = {
    x: {
      min: xMin,
      max: xMax,
    },
    y: {
      min: yMin,
      max: yMax,
    }
  }

  // center the camera
  this.center = {
    x: (xMax + xMin) / 2,
    y: (yMax + yMin) / 2
  }

  this.camera.position.set(this.center.x, this.center.y, -6);
  this.camera.lookAt(this.center.x, this.center.y, 0);
 
  // add attributes
  geometry.addAttribute('position', new BA( new Arr([0, 0, 0]), 3));
  geometry.addAttribute('translation', new IBA(translations, 3, 1) );
  geometry.addAttribute('color', new IBA(colors, 3, 1) );
  geometry.addAttribute('uidColor', new IBA(uidColors, 3, 1) );

  var material = new THREE.RawShaderMaterial({
    vertexShader: find('#vertex-shader').textContent,
    fragmentShader: find('#fragment-shader').textContent,
  });

  var mesh = new THREE.Points(geometry, material);
  mesh.frustumCulled = false; // prevent the mesh from being clipped on drag
  this.scene.add(mesh);
}

World.prototype.getColorMap = function() {

  function toHex(c) {
    var hex = c.toString(16);
    return hex.length == 1 ? '0' + hex : hex;
  }

  function rgbToHex(r, g, b) {
    return '#' + toHex(r) + toHex(g) + toHex(b);
  }

  function hexToRgb(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
      r: parseInt(result[1], 16),
      g: parseInt(result[2], 16),
      b: parseInt(result[3], 16),
    } : null;
  }

  var hexes = [
    '#fe4445','#ff583b','#ff6a2f','#ff7a20','#ff8800',
    '#ff9512','#ffa31f','#ffaf2a','#ffbb34',
    '#cfc522','#99cc01',
    '#91c14a','#85b66e','#73ac8f','#57a3ac','#0099cb',
    '#14a0d1','#20a7d8','#2aaedf','#33b5e6'
  ]

  var colorMap = {};
  hexes.forEach(function(c, idx) { colorMap[idx] = hexToRgb(c) })
  return colorMap;
}

/**
* Helpers
**/

function getRenderSize() {
  var elem = find('#gl-target');
  return {
    w: elem.clientWidth,
    h: elem.clientHeight,
  }
}

function find(selector) {
  return document.querySelector(selector);
}

/**
* Main
**/

var world = new World();

find('canvas').addEventListener('mousemove', function(e) {
  find('#bar').style.left = e.clientX + 'px';
  var coords = world.getMouseWorldCoords(e);
  console.log(coords, world.bb.x);
})
html, body {
  width: 100%;
  height: 100%;
  background: #000;
}

body {
  margin: 0;
  overflow: hidden;
}

canvas {
  width: 100%;
  height: 100%;
}

.gl-container {
  position: relative;
}

#gl-target {
  width:700px;
  height:400px
}

#bar {
  width: 1px;
  height: 100%;
  display: inline-block;
  position: absolute;
  left: 30px;
  background: red;
}
<div class='gl-container'>
  <div id='bar'></div>
  <div id='gl-target'></div>
</div>

<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/95/three.min.js'></script>

<script type='x-shader/x-vertex' id='vertex-shader'>
precision highp float;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

attribute vec3 position;
attribute vec3 translation;

#ifdef PICKING
  attribute vec3 uidColor;
  varying vec3 vUidColor;
#else
  attribute vec3 color;
#endif

varying vec3 vColor;

void main() {
  #ifdef PICKING
    vUidColor = uidColor;
  #else
    vColor = color;
  #endif

  // set point position
  vec3 raw = position + translation;
  vec4 pos = projectionMatrix * modelViewMatrix * vec4(raw, 1.0);
  gl_Position = pos;

  // set point size
  gl_PointSize = 10.0;
}
</script>

<script type='x-shader/x-fragment' id='fragment-shader'>
precision highp float;

#ifdef PICKING
  varying vec3 vUidColor;
#else
  varying vec3 vColor;
#endif

void main() {

  // make point circular
  vec2 coord = gl_PointCoord - vec2(0.5);
  if (length(coord) > 0.5) discard;

  // color the point
  #ifdef PICKING
    gl_FragColor = vec4(vUidColor, 1.0);
  #else
    gl_FragColor = vec4(vColor, 1.0);
  #endif
}
</script>